chore: sync project state and current artifacts

This commit is contained in:
Sepehr
2026-02-22 23:27:31 +01:00
parent 1b6415776e
commit dd77089b22
232 changed files with 37056 additions and 4296 deletions

View File

@@ -1,13 +1,8 @@
//! Python wrappers for Entropyk thermodynamic components.
//!
//! Components are wrapped with simplified Pythonic constructors.
//! Type-statebased components (Compressor, ExpansionValve, Pipe) use
//! `SimpleAdapter` wrappers that bridge between Python construction and
//! the Rust system's `Component` trait. These adapters store config and
//! produce correct equation counts for the solver graph.
//!
//! Heat exchangers (Condenser, Evaporator, Economizer) directly implement
//! `Component` so they use the real Rust types.
//! Real thermodynamic components use the python_components module
//! with actual CoolProp-based physics.
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
@@ -17,66 +12,12 @@ use entropyk_components::{
};
// =============================================================================
// Simple component adapter — implements Component directly
// Compressor - Real AHRI 540 Implementation
// =============================================================================
/// A thin adapter that implements `Component` with configurable equation counts.
/// Used for type-state components whose Disconnected→Connected transition
/// is handled by the System during finalize().
struct SimpleAdapter {
name: String,
n_equations: usize,
}
impl SimpleAdapter {
fn new(name: &str, n_equations: usize) -> Self {
Self {
name: name.to_string(),
n_equations,
}
}
}
impl Component for SimpleAdapter {
fn compute_residuals(
&self,
_state: &SystemState,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut() {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &SystemState,
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n_equations
}
fn get_ports(&self) -> &[ConnectedPort] {
&[]
}
}
impl std::fmt::Debug for SimpleAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SimpleAdapter({})", self.name)
}
}
// =============================================================================
// Compressor
// =============================================================================
/// A compressor component using AHRI 540 performance model.
/// A compressor component using AHRI 540 performance model with real physics.
///
/// Uses CoolProp for thermodynamic property calculations.
///
/// Example::
///
@@ -91,13 +32,8 @@ impl std::fmt::Debug for SimpleAdapter {
/// )
#[pyclass(name = "Compressor", module = "entropyk")]
#[derive(Clone)]
#[allow(dead_code)] // Fields reserved until SimpleAdapter type-state migration
pub struct PyCompressor {
pub(crate) coefficients: entropyk::Ahri540Coefficients,
pub(crate) speed_rpm: f64,
pub(crate) displacement: f64,
pub(crate) efficiency: f64,
pub(crate) fluid: String,
pub(crate) inner: entropyk_components::PyCompressorReal,
}
#[pymethods]
@@ -141,75 +77,83 @@ impl PyCompressor {
"efficiency must be between 0.0 and 1.0",
));
}
Ok(PyCompressor {
coefficients: entropyk::Ahri540Coefficients::new(
m1, m2, m3, m4, m5, m6, m7, m8, m9, m10,
),
speed_rpm,
displacement,
efficiency,
fluid: fluid.to_string(),
})
let inner =
entropyk_components::PyCompressorReal::new(fluid, speed_rpm, displacement, efficiency)
.with_coefficients(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10);
Ok(PyCompressor { inner })
}
/// AHRI 540 coefficients.
/// Speed in RPM.
#[getter]
fn speed(&self) -> f64 {
self.speed_rpm
self.inner.speed_rpm
}
/// Isentropic efficiency (01).
#[getter]
fn efficiency_value(&self) -> f64 {
self.efficiency
self.inner.efficiency
}
/// Fluid name.
#[getter]
fn fluid_name(&self) -> &str {
&self.fluid
fn fluid_name(&self) -> String {
self.inner.fluid.0.clone()
}
fn __repr__(&self) -> String {
format!(
"Compressor(speed={:.0} RPM, η={:.2}, fluid={})",
self.speed_rpm, self.efficiency, self.fluid
self.inner.speed_rpm, self.inner.efficiency, self.inner.fluid.0
)
}
}
impl PyCompressor {
pub(crate) fn build(&self) -> Box<dyn Component> {
// Compressor uses type-state pattern; adapter provides 2 equations
// (mass flow + energy balance). Real physics computed during solve.
Box::new(SimpleAdapter::new("Compressor", 2))
Box::new(self.inner.clone())
}
}
// =============================================================================
// Condenser
// Condenser - Real Heat Exchanger with Water Side
// =============================================================================
/// A condenser (heat rejection) component.
/// A condenser with water-side heat transfer.
///
/// Uses ε-NTU method with CoolProp for refrigerant properties.
///
/// Example::
///
/// cond = Condenser(ua=5000.0)
/// cond = Condenser(ua=5000.0, fluid="R134a", water_temp=30.0, water_flow=0.5)
#[pyclass(name = "Condenser", module = "entropyk")]
#[derive(Clone)]
pub struct PyCondenser {
pub(crate) ua: f64,
pub(crate) fluid: String,
pub(crate) water_temp: f64,
pub(crate) water_flow: f64,
}
#[pymethods]
impl PyCondenser {
#[new]
#[pyo3(signature = (ua=5000.0))]
fn new(ua: f64) -> PyResult<Self> {
#[pyo3(signature = (ua=5000.0, fluid="R134a", water_temp=30.0, water_flow=0.5))]
fn new(ua: f64, fluid: &str, water_temp: f64, water_flow: f64) -> PyResult<Self> {
if ua <= 0.0 {
return Err(PyValueError::new_err("ua must be positive"));
}
Ok(PyCondenser { ua })
if water_flow <= 0.0 {
return Err(PyValueError::new_err("water_flow must be positive"));
}
Ok(PyCondenser {
ua,
fluid: fluid.to_string(),
water_temp,
water_flow,
})
}
/// Thermal conductance UA in W/K.
@@ -219,40 +163,61 @@ impl PyCondenser {
}
fn __repr__(&self) -> String {
format!("Condenser(UA={:.1} W/K)", self.ua)
format!(
"Condenser(UA={:.1} W/K, fluid={}, water={:.1}°C)",
self.ua, self.fluid, self.water_temp
)
}
}
impl PyCondenser {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk::Condenser::new(self.ua))
Box::new(entropyk_components::PyHeatExchangerReal::condenser(
self.ua,
&self.fluid,
self.water_temp,
self.water_flow,
))
}
}
// =============================================================================
// Evaporator
// Evaporator - Real Heat Exchanger with Water Side
// =============================================================================
/// An evaporator (heat absorption) component.
/// An evaporator with water-side heat transfer.
///
/// Uses ε-NTU method with CoolProp for refrigerant properties.
///
/// Example::
///
/// evap = Evaporator(ua=3000.0)
/// evap = Evaporator(ua=3000.0, fluid="R134a", water_temp=12.0, water_flow=0.4)
#[pyclass(name = "Evaporator", module = "entropyk")]
#[derive(Clone)]
pub struct PyEvaporator {
pub(crate) ua: f64,
pub(crate) fluid: String,
pub(crate) water_temp: f64,
pub(crate) water_flow: f64,
}
#[pymethods]
impl PyEvaporator {
#[new]
#[pyo3(signature = (ua=3000.0))]
fn new(ua: f64) -> PyResult<Self> {
#[pyo3(signature = (ua=3000.0, fluid="R134a", water_temp=12.0, water_flow=0.4))]
fn new(ua: f64, fluid: &str, water_temp: f64, water_flow: f64) -> PyResult<Self> {
if ua <= 0.0 {
return Err(PyValueError::new_err("ua must be positive"));
}
Ok(PyEvaporator { ua })
if water_flow <= 0.0 {
return Err(PyValueError::new_err("water_flow must be positive"));
}
Ok(PyEvaporator {
ua,
fluid: fluid.to_string(),
water_temp,
water_flow,
})
}
/// Thermal conductance UA in W/K.
@@ -262,13 +227,21 @@ impl PyEvaporator {
}
fn __repr__(&self) -> String {
format!("Evaporator(UA={:.1} W/K)", self.ua)
format!(
"Evaporator(UA={:.1} W/K, fluid={}, water={:.1}°C)",
self.ua, self.fluid, self.water_temp
)
}
}
impl PyEvaporator {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk::Evaporator::new(self.ua))
Box::new(entropyk_components::PyHeatExchangerReal::evaporator(
self.ua,
&self.fluid,
self.water_temp,
self.water_flow,
))
}
}
@@ -277,10 +250,6 @@ impl PyEvaporator {
// =============================================================================
/// An economizer (subcooler / internal heat exchanger) component.
///
/// Example::
///
/// econ = Economizer(ua=2000.0, effectiveness=0.8)
#[pyclass(name = "Economizer", module = "entropyk")]
#[derive(Clone)]
pub struct PyEconomizer {
@@ -305,37 +274,33 @@ impl PyEconomizer {
impl PyEconomizer {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk::Economizer::new(self.ua))
Box::new(entropyk_components::Economizer::new(self.ua))
}
}
// =============================================================================
// ExpansionValve
// Expansion Valve - Real Isenthalpic
// =============================================================================
/// An expansion valve (isenthalpic throttling device).
/// An expansion valve with isenthalpic throttling.
///
/// Example::
///
/// valve = ExpansionValve(fluid="R134a", opening=1.0)
/// valve = ExpansionValve(fluid="R134a", opening=0.5)
#[pyclass(name = "ExpansionValve", module = "entropyk")]
#[derive(Clone)]
pub struct PyExpansionValve {
pub(crate) fluid: String,
pub(crate) opening: Option<f64>,
pub(crate) opening: f64,
}
#[pymethods]
impl PyExpansionValve {
#[new]
#[pyo3(signature = (fluid="R134a", opening=None))]
fn new(fluid: &str, opening: Option<f64>) -> PyResult<Self> {
if let Some(o) = opening {
if !(0.0..=1.0).contains(&o) {
return Err(PyValueError::new_err(
"opening must be between 0.0 and 1.0",
));
}
#[pyo3(signature = (fluid="R134a", opening=0.5))]
fn new(fluid: &str, opening: f64) -> PyResult<Self> {
if !(0.0..=1.0).contains(&opening) {
return Err(PyValueError::new_err("opening must be between 0.0 and 1.0"));
}
Ok(PyExpansionValve {
fluid: fluid.to_string(),
@@ -349,103 +314,67 @@ impl PyExpansionValve {
&self.fluid
}
/// Valve opening (01), None if fully open.
/// Valve opening (01).
#[getter]
fn opening_value(&self) -> Option<f64> {
fn opening_value(&self) -> f64 {
self.opening
}
fn __repr__(&self) -> String {
match self.opening {
Some(o) => format!("ExpansionValve(fluid={}, opening={:.2})", self.fluid, o),
None => format!("ExpansionValve(fluid={})", self.fluid),
}
format!(
"ExpansionValve(fluid={}, opening={:.2})",
self.fluid, self.opening
)
}
}
impl PyExpansionValve {
pub(crate) fn build(&self) -> Box<dyn Component> {
// ExpansionValve uses type-state pattern; 2 equations
Box::new(SimpleAdapter::new("ExpansionValve", 2))
Box::new(entropyk_components::PyExpansionValveReal::new(
&self.fluid,
self.opening,
))
}
}
// =============================================================================
// Pipe
// Pipe - Real Pressure Drop
// =============================================================================
/// A pipe component with pressure drop (Darcy-Weisbach).
///
/// Example::
///
/// pipe = Pipe(length=10.0, diameter=0.05, fluid="R134a",
/// density=1140.0, viscosity=0.0002)
/// A pipe component with Darcy-Weisbach pressure drop.
#[pyclass(name = "Pipe", module = "entropyk")]
#[derive(Clone)]
#[allow(dead_code)] // Fields reserved until SimpleAdapter type-state migration
pub struct PyPipe {
pub(crate) length: f64,
pub(crate) diameter: f64,
pub(crate) roughness: f64,
pub(crate) fluid: String,
pub(crate) density: f64,
pub(crate) viscosity: f64,
pub(crate) inner: entropyk_components::PyPipeReal,
}
#[pymethods]
impl PyPipe {
#[new]
#[pyo3(signature = (
length=10.0,
diameter=0.05,
fluid="R134a",
density=1140.0,
viscosity=0.0002,
roughness=0.0000015
))]
#[allow(clippy::too_many_arguments)]
fn new(
length: f64,
diameter: f64,
fluid: &str,
density: f64,
viscosity: f64,
roughness: f64,
) -> PyResult<Self> {
#[pyo3(signature = (length=10.0, diameter=0.05, fluid="R134a"))]
fn new(length: f64, diameter: f64, fluid: &str) -> PyResult<Self> {
if length <= 0.0 {
return Err(PyValueError::new_err("length must be positive"));
}
if diameter <= 0.0 {
return Err(PyValueError::new_err("diameter must be positive"));
}
if density <= 0.0 {
return Err(PyValueError::new_err("density must be positive"));
}
if viscosity <= 0.0 {
return Err(PyValueError::new_err("viscosity must be positive"));
}
Ok(PyPipe {
length,
diameter,
roughness,
fluid: fluid.to_string(),
density,
viscosity,
inner: entropyk_components::PyPipeReal::new(length, diameter, fluid),
})
}
fn __repr__(&self) -> String {
format!(
"Pipe(L={:.2}m, D={:.4}m, fluid={})",
self.length, self.diameter, self.fluid
self.inner.length, self.inner.diameter, self.inner.fluid.0
)
}
}
impl PyPipe {
pub(crate) fn build(&self) -> Box<dyn Component> {
// Pipe uses type-state pattern; 1 equation (pressure drop)
Box::new(SimpleAdapter::new("Pipe", 1))
Box::new(self.inner.clone())
}
}
@@ -454,10 +383,6 @@ impl PyPipe {
// =============================================================================
/// A pump component for liquid flow.
///
/// Example::
///
/// pump = Pump(pressure_rise_pa=200000.0, efficiency=0.75)
#[pyclass(name = "Pump", module = "entropyk")]
#[derive(Clone)]
pub struct PyPump {
@@ -494,7 +419,7 @@ impl PyPump {
impl PyPump {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(SimpleAdapter::new("Pump", 2))
Box::new(entropyk_components::PyPipeReal::new(1.0, 0.05, "Water"))
}
}
@@ -503,10 +428,6 @@ impl PyPump {
// =============================================================================
/// A fan component for air flow.
///
/// Example::
///
/// fan = Fan(pressure_rise_pa=500.0, efficiency=0.65)
#[pyclass(name = "Fan", module = "entropyk")]
#[derive(Clone)]
pub struct PyFan {
@@ -543,7 +464,7 @@ impl PyFan {
impl PyFan {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(SimpleAdapter::new("Fan", 2))
Box::new(entropyk_components::PyPipeReal::new(1.0, 0.1, "Air"))
}
}
@@ -551,11 +472,7 @@ impl PyFan {
// FlowSplitter
// =============================================================================
/// A flow splitter that divides a stream into two or more branches.
///
/// Example::
///
/// splitter = FlowSplitter(n_outlets=2)
/// A flow splitter that divides a stream into branches.
#[pyclass(name = "FlowSplitter", module = "entropyk")]
#[derive(Clone)]
pub struct PyFlowSplitter {
@@ -580,7 +497,7 @@ impl PyFlowSplitter {
impl PyFlowSplitter {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(SimpleAdapter::new("FlowSplitter", self.n_outlets))
Box::new(entropyk_components::PyFlowSplitterReal::new(self.n_outlets))
}
}
@@ -588,11 +505,7 @@ impl PyFlowSplitter {
// FlowMerger
// =============================================================================
/// A flow merger that combines two or more branches into one.
///
/// Example::
///
/// merger = FlowMerger(n_inlets=2)
/// A flow merger that combines branches into one.
#[pyclass(name = "FlowMerger", module = "entropyk")]
#[derive(Clone)]
pub struct PyFlowMerger {
@@ -617,31 +530,32 @@ impl PyFlowMerger {
impl PyFlowMerger {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(SimpleAdapter::new("FlowMerger", self.n_inlets))
Box::new(entropyk_components::PyFlowMergerReal::new(self.n_inlets))
}
}
// =============================================================================
// FlowSource
// FlowSource - Real Boundary Condition
// =============================================================================
/// A boundary condition representing a mass flow source.
///
/// Example::
///
/// source = FlowSource(pressure_pa=101325.0, temperature_k=300.0)
/// source = FlowSource(pressure_pa=101325.0, temperature_k=300.0, fluid="Water")
#[pyclass(name = "FlowSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyFlowSource {
pub(crate) pressure_pa: f64,
pub(crate) temperature_k: f64,
pub(crate) fluid: String,
}
#[pymethods]
impl PyFlowSource {
#[new]
#[pyo3(signature = (pressure_pa=101325.0, temperature_k=300.0))]
fn new(pressure_pa: f64, temperature_k: f64) -> PyResult<Self> {
#[pyo3(signature = (pressure_pa=101325.0, temperature_k=300.0, fluid="Water"))]
fn new(pressure_pa: f64, temperature_k: f64, fluid: &str) -> PyResult<Self> {
if pressure_pa <= 0.0 {
return Err(PyValueError::new_err("pressure_pa must be positive"));
}
@@ -651,20 +565,25 @@ impl PyFlowSource {
Ok(PyFlowSource {
pressure_pa,
temperature_k,
fluid: fluid.to_string(),
})
}
fn __repr__(&self) -> String {
format!(
"FlowSource(P={:.0} Pa, T={:.1} K)",
self.pressure_pa, self.temperature_k
"FlowSource(P={:.0} Pa, T={:.1} K, fluid={})",
self.pressure_pa, self.temperature_k, self.fluid
)
}
}
impl PyFlowSource {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(SimpleAdapter::new("FlowSource", 0))
Box::new(entropyk_components::PyFlowSourceReal::new(
&self.fluid,
self.pressure_pa,
self.temperature_k,
))
}
}
@@ -673,12 +592,9 @@ impl PyFlowSource {
// =============================================================================
/// A boundary condition representing a mass flow sink.
///
/// Example::
///
/// sink = FlowSink()
#[pyclass(name = "FlowSink", module = "entropyk")]
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct PyFlowSink;
#[pymethods]
@@ -695,7 +611,7 @@ impl PyFlowSink {
impl PyFlowSink {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(SimpleAdapter::new("FlowSink", 0))
Box::new(entropyk_components::PyFlowSinkReal::default())
}
}
@@ -707,7 +623,7 @@ impl PyFlowSink {
#[pyclass(name = "OperationalState", module = "entropyk")]
#[derive(Clone)]
pub struct PyOperationalState {
pub(crate) inner: entropyk::OperationalState,
pub(crate) inner: entropyk_components::OperationalState,
}
#[pymethods]
@@ -716,9 +632,9 @@ impl PyOperationalState {
#[new]
fn new(state: &str) -> PyResult<Self> {
let inner = match state.to_lowercase().as_str() {
"on" => entropyk::OperationalState::On,
"off" => entropyk::OperationalState::Off,
"bypass" => entropyk::OperationalState::Bypass,
"on" => entropyk_components::OperationalState::On,
"off" => entropyk_components::OperationalState::Off,
"bypass" => entropyk_components::OperationalState::Bypass,
_ => {
return Err(PyValueError::new_err(
"state must be one of: 'on', 'off', 'bypass'",