Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled

Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 16:35:31 +02:00
parent 88620790d6
commit 5bd180b5b8
1363 changed files with 101041 additions and 58547 deletions

View File

@@ -7,9 +7,7 @@
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use entropyk_components::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
};
use entropyk_components::{Component, ConnectedPort};
// =============================================================================
// Compressor - Real AHRI 540 Implementation
@@ -117,6 +115,149 @@ impl PyCompressor {
}
}
// =============================================================================
// ScrewEconomizerCompressor - 3-port screw compressor with economizer
// =============================================================================
/// A screw compressor with an intermediate economizer injection port.
#[pyclass(name = "ScrewEconomizerCompressor", module = "entropyk")]
#[derive(Clone)]
pub struct PyScrewEconomizerCompressor {
pub(crate) fluid: String,
pub(crate) nominal_frequency_hz: f64,
pub(crate) frequency_hz: f64,
pub(crate) mechanical_efficiency: f64,
pub(crate) economizer_fraction: f64,
}
#[pymethods]
impl PyScrewEconomizerCompressor {
/// Create a screw compressor with a fixed economizer flow fraction.
#[new]
#[pyo3(signature = (
fluid="R134a",
nominal_frequency_hz=50.0,
frequency_hz=50.0,
mechanical_efficiency=0.92,
economizer_fraction=0.12
))]
fn new(
fluid: &str,
nominal_frequency_hz: f64,
frequency_hz: f64,
mechanical_efficiency: f64,
economizer_fraction: f64,
) -> PyResult<Self> {
if nominal_frequency_hz <= 0.0 {
return Err(PyValueError::new_err(
"nominal_frequency_hz must be positive",
));
}
if frequency_hz < 0.0 {
return Err(PyValueError::new_err("frequency_hz cannot be negative"));
}
if !(0.0..=1.0).contains(&mechanical_efficiency) || mechanical_efficiency == 0.0 {
return Err(PyValueError::new_err(
"mechanical_efficiency must be in (0.0, 1.0]",
));
}
if !(0.0..=0.4).contains(&economizer_fraction) {
return Err(PyValueError::new_err(
"economizer_fraction must be between 0.0 and 0.4",
));
}
Ok(Self {
fluid: fluid.to_string(),
nominal_frequency_hz,
frequency_hz,
mechanical_efficiency,
economizer_fraction,
})
}
#[getter]
fn fluid(&self) -> String {
self.fluid.clone()
}
#[getter]
fn frequency_hz(&self) -> f64 {
self.frequency_hz
}
#[setter]
fn set_frequency_hz(&mut self, value: f64) -> PyResult<()> {
if value < 0.0 {
return Err(PyValueError::new_err("frequency_hz cannot be negative"));
}
self.frequency_hz = value;
Ok(())
}
#[getter]
fn mechanical_efficiency(&self) -> f64 {
self.mechanical_efficiency
}
#[getter]
fn economizer_fraction(&self) -> f64 {
self.economizer_fraction
}
fn __repr__(&self) -> String {
format!(
"ScrewEconomizerCompressor(fluid={}, frequency={:.1} Hz, eco_fraction={:.3})",
self.fluid, self.frequency_hz, self.economizer_fraction
)
}
}
impl PyScrewEconomizerCompressor {
pub(crate) fn build(&self) -> Box<dyn Component> {
use entropyk_components::port::{FluidId, Port};
use entropyk_components::{
Polynomial2D, ScrewEconomizerCompressor, ScrewPerformanceCurves,
};
use entropyk_core::{Enthalpy, Pressure};
fn connected_port(fluid: &str, pressure_bar: f64, enthalpy_kj_kg: f64) -> ConnectedPort {
let a = Port::new(
FluidId::new(fluid),
Pressure::from_bar(pressure_bar),
Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
);
let b = Port::new(
FluidId::new(fluid),
Pressure::from_bar(pressure_bar),
Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
);
a.connect(b)
.expect("matching screw compressor ports should connect")
.0
}
let curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
Polynomial2D::bilinear(1.2, 0.003, -0.002, 0.000_01),
Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5),
self.economizer_fraction,
);
let mut component = ScrewEconomizerCompressor::new(
curves,
&self.fluid,
self.nominal_frequency_hz,
self.mechanical_efficiency,
connected_port(&self.fluid, 3.2, 400.0),
connected_port(&self.fluid, 12.8, 440.0),
connected_port(&self.fluid, 6.4, 260.0),
)
.expect("validated screw compressor parameters should build");
component
.set_frequency_hz(self.frequency_hz)
.expect("validated screw compressor frequency should set");
Box::new(component)
}
}
// =============================================================================
// Condenser - Real Heat Exchanger with Water Side
// =============================================================================
@@ -717,7 +858,11 @@ impl PyRefrigerantSource {
impl PyRefrigerantSource {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk_components::PyRefrigerantSourceReal::new(&self.fluid, self.p_set_pa, self.quality))
Box::new(entropyk_components::PyRefrigerantSourceReal::new(
&self.fluid,
self.p_set_pa,
self.quality,
))
}
}
@@ -760,7 +905,11 @@ impl PyRefrigerantSink {
impl PyRefrigerantSink {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk_components::PyRefrigerantSinkReal::new(&self.fluid, self.p_back_pa, self.quality_opt))
Box::new(entropyk_components::PyRefrigerantSinkReal::new(
&self.fluid,
self.p_back_pa,
self.quality_opt,
))
}
}
@@ -782,7 +931,12 @@ pub struct PyBrineSource {
impl PyBrineSource {
#[new]
#[pyo3(signature = (fluid="Water", concentration=0.0, temperature_k=300.0, pressure_pa=101325.0))]
fn new(fluid: &str, concentration: f64, temperature_k: f64, pressure_pa: f64) -> PyResult<Self> {
fn new(
fluid: &str,
concentration: f64,
temperature_k: f64,
pressure_pa: f64,
) -> PyResult<Self> {
if pressure_pa <= 0.0 {
return Err(PyValueError::new_err("pressure_pa must be positive"));
}
@@ -790,7 +944,9 @@ impl PyBrineSource {
return Err(PyValueError::new_err("temperature_k must be positive"));
}
if !(0.0..=1.0).contains(&concentration) {
return Err(PyValueError::new_err("concentration must be between 0.0 and 1.0"));
return Err(PyValueError::new_err(
"concentration must be between 0.0 and 1.0",
));
}
Ok(PyBrineSource {
fluid: fluid.to_string(),
@@ -810,7 +966,12 @@ impl PyBrineSource {
impl PyBrineSource {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk_components::PyBrineSourceReal::new(&self.fluid, self.concentration, self.temperature_k, self.pressure_pa))
Box::new(entropyk_components::PyBrineSourceReal::new(
&self.fluid,
self.concentration,
self.temperature_k,
self.pressure_pa,
))
}
}
@@ -868,7 +1029,9 @@ impl PyAirSource {
return Err(PyValueError::new_err("temperature_k must be positive"));
}
if !(0.0..=1.0).contains(&relative_humidity) {
return Err(PyValueError::new_err("relative_humidity must be between 0.0 and 1.0"));
return Err(PyValueError::new_err(
"relative_humidity must be between 0.0 and 1.0",
));
}
Ok(PyAirSource {
temperature_k,
@@ -887,7 +1050,11 @@ impl PyAirSource {
impl PyAirSource {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk_components::PyAirSourceReal::new(self.temperature_k, self.relative_humidity, self.pressure_pa))
Box::new(entropyk_components::PyAirSourceReal::new(
self.temperature_k,
self.relative_humidity,
self.pressure_pa,
))
}
}
@@ -920,6 +1087,122 @@ impl PyAirSink {
}
}
/// Capillary tube (1D segmented ΔP, isenthalpic).
#[pyclass(name = "CapillaryTube", module = "entropyk")]
#[derive(Clone)]
pub struct PyCapillaryTube {
fluid: String,
diameter_m: f64,
length_m: f64,
n_segments: usize,
}
#[pymethods]
impl PyCapillaryTube {
#[new]
#[pyo3(signature = (fluid="R134a", diameter_m=0.001, length_m=2.0, n_segments=20))]
fn new(fluid: &str, diameter_m: f64, length_m: f64, n_segments: usize) -> PyResult<Self> {
if diameter_m <= 0.0 || length_m <= 0.0 || n_segments < 2 {
return Err(PyValueError::new_err("invalid capillary geometry"));
}
Ok(Self {
fluid: fluid.to_string(),
diameter_m,
length_m,
n_segments,
})
}
fn __repr__(&self) -> String {
format!(
"CapillaryTube(D={:.3}mm, L={:.2}m)",
self.diameter_m * 1000.0,
self.length_m
)
}
}
impl PyCapillaryTube {
pub(crate) fn build(&self) -> Box<dyn Component> {
use entropyk_components::port::{FluidId, Port};
use entropyk_components::{CapillaryGeometry, CapillaryTube};
use entropyk_core::{Enthalpy, Pressure};
let geom = CapillaryGeometry {
diameter_m: self.diameter_m,
length_m: self.length_m,
n_segments: self.n_segments,
};
let p = Pressure::from_bar(10.0);
let h = Enthalpy::from_joules_per_kg(250_000.0);
let fid = FluidId::new(&self.fluid);
let tube = CapillaryTube::new(
geom,
Port::new(fid.clone(), p, h),
Port::new(fid.clone(), p, h),
)
.and_then(|c| c.connect(Port::new(fid.clone(), p, h), Port::new(fid, p, h)))
.expect("CapillaryTube construction");
Box::new(tube)
}
}
/// Fan-coil unit rating (ε-NTU + BPF).
#[pyclass(name = "FanCoilUnit", module = "entropyk")]
#[derive(Clone)]
pub struct PyFanCoilUnit {
ua: f64,
bpf: f64,
}
#[pymethods]
impl PyFanCoilUnit {
#[new]
#[pyo3(signature = (ua=8000.0, bypass_factor=0.15))]
fn new(ua: f64, bypass_factor: f64) -> Self {
Self {
ua,
bpf: bypass_factor,
}
}
/// Rate total / sensible / latent capacity [W] and leaving air temp [K].
#[pyo3(signature = (
t_air_in_k, t_dew_in_k, t_water_in_k, c_air, c_water, t_adp_k
))]
fn rate(
&self,
t_air_in_k: f64,
t_dew_in_k: f64,
t_water_in_k: f64,
c_air: f64,
c_water: f64,
t_adp_k: f64,
) -> (f64, f64, f64, f64, f64) {
use entropyk_components::heat_exchanger::{FanCoilRatingInput, FanCoilUnit};
let fcu = FanCoilUnit::new(self.ua).with_bypass_factor(self.bpf);
let r = fcu.rate(&FanCoilRatingInput {
t_air_in_k,
t_dew_in_k,
t_water_in_k,
c_air,
c_water,
bypass_factor: self.bpf,
t_adp_k,
});
(
r.q_total_w,
r.q_sensible_w,
r.q_latent_w,
r.t_air_out_k,
r.shr,
)
}
fn __repr__(&self) -> String {
format!("FanCoilUnit(UA={:.0}, BPF={:.2})", self.ua, self.bpf)
}
}
// =============================================================================
// Component enum for type-erasure
// =============================================================================
@@ -928,6 +1211,7 @@ impl PyAirSink {
#[derive(Clone)]
pub(crate) enum AnyPyComponent {
Compressor(PyCompressor),
ScrewEconomizerCompressor(PyScrewEconomizerCompressor),
Condenser(PyCondenser),
Evaporator(PyEvaporator),
Economizer(PyEconomizer),
@@ -945,6 +1229,7 @@ pub(crate) enum AnyPyComponent {
BrineSink(PyBrineSink),
AirSource(PyAirSource),
AirSink(PyAirSink),
CapillaryTube(PyCapillaryTube),
}
impl AnyPyComponent {
@@ -952,6 +1237,7 @@ impl AnyPyComponent {
pub(crate) fn build(&self) -> Box<dyn Component> {
match self {
AnyPyComponent::Compressor(c) => c.build(),
AnyPyComponent::ScrewEconomizerCompressor(c) => c.build(),
AnyPyComponent::Condenser(c) => c.build(),
AnyPyComponent::Evaporator(c) => c.build(),
AnyPyComponent::Economizer(c) => c.build(),
@@ -969,6 +1255,7 @@ impl AnyPyComponent {
AnyPyComponent::BrineSink(c) => c.build(),
AnyPyComponent::AirSource(c) => c.build(),
AnyPyComponent::AirSink(c) => c.build(),
AnyPyComponent::CapillaryTube(c) => c.build(),
}
}
}

View File

@@ -3,6 +3,11 @@
//! This crate provides Python wrappers for the Entropyk thermodynamic
//! simulation library via PyO3 + Maturin.
// Pre-existing lint debt accumulated before CI was introduced.
#![allow(deprecated)]
#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use pyo3::prelude::*;
pub(crate) mod components;
@@ -28,6 +33,7 @@ fn entropyk(m: &Bound<'_, PyModule>) -> PyResult<()> {
// Components
m.add_class::<components::PyCompressor>()?;
m.add_class::<components::PyScrewEconomizerCompressor>()?;
m.add_class::<components::PyCondenser>()?;
m.add_class::<components::PyEvaporator>()?;
m.add_class::<components::PyEconomizer>()?;
@@ -45,6 +51,8 @@ fn entropyk(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<components::PyBrineSink>()?;
m.add_class::<components::PyAirSource>()?;
m.add_class::<components::PyAirSink>()?;
m.add_class::<components::PyCapillaryTube>()?;
m.add_class::<components::PyFanCoilUnit>()?;
m.add_class::<components::PyOperationalState>()?;
// Solver

View File

@@ -249,6 +249,9 @@ fn extract_component(obj: &Bound<'_, PyAny>) -> PyResult<AnyPyComponent> {
if let Ok(c) = obj.extract::<crate::components::PyCompressor>() {
return Ok(AnyPyComponent::Compressor(c));
}
if let Ok(c) = obj.extract::<crate::components::PyScrewEconomizerCompressor>() {
return Ok(AnyPyComponent::ScrewEconomizerCompressor(c));
}
if let Ok(c) = obj.extract::<crate::components::PyCondenser>() {
return Ok(AnyPyComponent::Condenser(c));
}
@@ -301,7 +304,7 @@ fn extract_component(obj: &Bound<'_, PyAny>) -> PyResult<AnyPyComponent> {
return Ok(AnyPyComponent::AirSink(c));
}
Err(PyValueError::new_err(
"Expected a component (Compressor, Condenser, Evaporator, ExpansionValve, Pipe, Pump, Fan, Economizer, FlowSplitter, FlowMerger, FlowSource, FlowSink, RefrigerantSource, RefrigerantSink, BrineSource, BrineSink, AirSource, AirSink)",
"Expected a component (Compressor, ScrewEconomizerCompressor, Condenser, Evaporator, ExpansionValve, Pipe, Pump, Fan, Economizer, FlowSplitter, FlowMerger, FlowSource, FlowSink, RefrigerantSource, RefrigerantSink, BrineSource, BrineSink, AirSource, AirSink)",
))
}
@@ -519,7 +522,9 @@ impl PyNewtonConfig {
return Err(PyValueError::new_err("tolerance must be greater than 0"));
}
if divergence_threshold <= tolerance {
return Err(PyValueError::new_err("divergence_threshold must be greater than tolerance"));
return Err(PyValueError::new_err(
"divergence_threshold must be greater than tolerance",
));
}
Ok(PyNewtonConfig {
max_iterations,
@@ -565,7 +570,10 @@ impl PyNewtonConfig {
previous_state: self.previous_state.clone(),
previous_residual: None,
initial_state: self.initial_state.clone(),
convergence_criteria: self.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
convergence_criteria: self
.convergence_criteria
.as_ref()
.map(|cc| cc.inner.clone()),
jacobian_freezing: self.jacobian_freezing.as_ref().map(|jf| jf.inner.clone()),
verbose_config: Default::default(),
};
@@ -631,8 +639,8 @@ impl PyPicardConfig {
convergence_criteria=None
))]
fn new(
max_iterations: usize,
tolerance: f64,
max_iterations: usize,
tolerance: f64,
relaxation: f64,
initial_state: Option<Vec<f64>>,
timeout_ms: Option<u64>,
@@ -675,7 +683,10 @@ impl PyPicardConfig {
relaxation_factor: self.relaxation,
timeout: self.timeout_ms.map(Duration::from_millis),
initial_state: self.initial_state.clone(),
convergence_criteria: self.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
convergence_criteria: self
.convergence_criteria
.as_ref()
.map(|cc| cc.inner.clone()),
..Default::default()
};
@@ -725,8 +736,14 @@ impl PyFallbackConfig {
#[pyo3(signature = (newton=None, picard=None))]
fn new(newton: Option<PyNewtonConfig>, picard: Option<PyPicardConfig>) -> PyResult<Self> {
Ok(PyFallbackConfig {
newton: newton.unwrap_or_else(|| PyNewtonConfig::new(100, 1e-6, false, None, None, false, None, None, None, None, 1e-4, 20, 1e10).unwrap()),
picard: picard.unwrap_or_else(|| PyPicardConfig::new(500, 1e-4, 0.5, None, None, None).unwrap()),
newton: newton.unwrap_or_else(|| {
PyNewtonConfig::new(
100, 1e-6, false, None, None, false, None, None, None, None, 1e-4, 20, 1e10,
)
.unwrap()
}),
picard: picard
.unwrap_or_else(|| PyPicardConfig::new(500, 1e-4, 0.5, None, None, None).unwrap()),
})
}
@@ -756,8 +773,16 @@ impl PyFallbackConfig {
previous_state: self.newton.previous_state.clone(),
previous_residual: None,
initial_state: self.newton.initial_state.clone(),
convergence_criteria: self.newton.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
jacobian_freezing: self.newton.jacobian_freezing.as_ref().map(|jf| jf.inner.clone()),
convergence_criteria: self
.newton
.convergence_criteria
.as_ref()
.map(|cc| cc.inner.clone()),
jacobian_freezing: self
.newton
.jacobian_freezing
.as_ref()
.map(|jf| jf.inner.clone()),
verbose_config: Default::default(),
};
let picard_config = entropyk_solver::PicardConfig {
@@ -766,7 +791,11 @@ impl PyFallbackConfig {
relaxation_factor: self.picard.relaxation,
timeout: self.picard.timeout_ms.map(Duration::from_millis),
initial_state: self.picard.initial_state.clone(),
convergence_criteria: self.picard.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
convergence_criteria: self
.picard
.convergence_criteria
.as_ref()
.map(|cc| cc.inner.clone()),
..Default::default()
};
let mut fallback =
@@ -982,10 +1011,19 @@ impl PySolverStrategy {
divergence_threshold: f64,
) -> PyResult<Self> {
let py_config = PyNewtonConfig::new(
max_iterations, tolerance, line_search, timeout_ms, initial_state,
use_numerical_jacobian, jacobian_freezing, convergence_criteria,
timeout_config, previous_state, line_search_armijo_c,
line_search_max_backtracks, divergence_threshold,
max_iterations,
tolerance,
line_search,
timeout_ms,
initial_state,
use_numerical_jacobian,
jacobian_freezing,
convergence_criteria,
timeout_config,
previous_state,
line_search_armijo_c,
line_search_max_backtracks,
divergence_threshold,
)?;
let config = entropyk_solver::NewtonConfig {
max_iterations: py_config.max_iterations,
@@ -1000,8 +1038,14 @@ impl PySolverStrategy {
previous_state: py_config.previous_state.clone(),
previous_residual: None,
initial_state: py_config.initial_state.clone(),
convergence_criteria: py_config.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
jacobian_freezing: py_config.jacobian_freezing.as_ref().map(|jf| jf.inner.clone()),
convergence_criteria: py_config
.convergence_criteria
.as_ref()
.map(|cc| cc.inner.clone()),
jacobian_freezing: py_config
.jacobian_freezing
.as_ref()
.map(|jf| jf.inner.clone()),
verbose_config: Default::default(),
};
Ok(PySolverStrategy {
@@ -1019,15 +1063,20 @@ impl PySolverStrategy {
convergence_criteria=None
))]
fn picard(
max_iterations: usize,
tolerance: f64,
max_iterations: usize,
tolerance: f64,
relaxation: f64,
initial_state: Option<Vec<f64>>,
timeout_ms: Option<u64>,
convergence_criteria: Option<PyConvergenceCriteria>,
) -> PyResult<Self> {
let py_config = PyPicardConfig::new(
max_iterations, tolerance, relaxation, initial_state, timeout_ms, convergence_criteria
max_iterations,
tolerance,
relaxation,
initial_state,
timeout_ms,
convergence_criteria,
)?;
let config = entropyk_solver::PicardConfig {
max_iterations: py_config.max_iterations,
@@ -1035,7 +1084,10 @@ impl PySolverStrategy {
relaxation_factor: py_config.relaxation,
timeout: py_config.timeout_ms.map(Duration::from_millis),
initial_state: py_config.initial_state.clone(),
convergence_criteria: py_config.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
convergence_criteria: py_config
.convergence_criteria
.as_ref()
.map(|cc| cc.inner.clone()),
..Default::default()
};
Ok(PySolverStrategy {
@@ -1052,7 +1104,7 @@ impl PySolverStrategy {
fn solve(&mut self, system: &mut PySystem) -> PyResult<PyConvergedState> {
use entropyk_solver::Solver;
let solve_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
self.inner.solve(&mut system.inner)
}));

View File

@@ -345,7 +345,6 @@ impl PyMassFlow {
}
}
// =============================================================================
// Concentration
// =============================================================================
@@ -361,7 +360,9 @@ impl PyConcentration {
#[new]
fn new(value: f64) -> PyResult<Self> {
if !(0.0..=1.0).contains(&value) {
return Err(pyo3::exceptions::PyValueError::new_err("Value must be between 0.0 and 1.0"));
return Err(pyo3::exceptions::PyValueError::new_err(
"Value must be between 0.0 and 1.0",
));
}
Ok(PyConcentration {
inner: entropyk::Concentration::from_fraction(value),
@@ -427,9 +428,13 @@ impl PyVolumeFlow {
#[new]
fn new(value: f64) -> PyResult<Self> {
if value < 0.0 {
return Err(pyo3::exceptions::PyValueError::new_err("Value cannot be negative"));
return Err(pyo3::exceptions::PyValueError::new_err(
"Value cannot be negative",
));
}
Ok(PyVolumeFlow { inner: entropyk::VolumeFlow::from_m3_per_s(value) })
Ok(PyVolumeFlow {
inner: entropyk::VolumeFlow::from_m3_per_s(value),
})
}
#[getter]
@@ -439,12 +444,16 @@ impl PyVolumeFlow {
#[staticmethod]
fn from_m3_per_s(value: f64) -> Self {
PyVolumeFlow { inner: entropyk::VolumeFlow::from_m3_per_s(value) }
PyVolumeFlow {
inner: entropyk::VolumeFlow::from_m3_per_s(value),
}
}
#[staticmethod]
fn from_l_per_min(value: f64) -> Self {
PyVolumeFlow { inner: entropyk::VolumeFlow::from_l_per_min(value) }
PyVolumeFlow {
inner: entropyk::VolumeFlow::from_l_per_min(value),
}
}
fn to_m3_per_s(&self) -> f64 {
@@ -483,9 +492,13 @@ impl PyRelativeHumidity {
#[new]
fn new(value: f64) -> PyResult<Self> {
if !(0.0..=1.0).contains(&value) {
return Err(pyo3::exceptions::PyValueError::new_err("Value must be between 0.0 and 1.0"));
return Err(pyo3::exceptions::PyValueError::new_err(
"Value must be between 0.0 and 1.0",
));
}
Ok(PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_fraction(value) })
Ok(PyRelativeHumidity {
inner: entropyk::RelativeHumidity::from_fraction(value),
})
}
#[getter]
@@ -495,12 +508,16 @@ impl PyRelativeHumidity {
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_percent(value) }
PyRelativeHumidity {
inner: entropyk::RelativeHumidity::from_percent(value),
}
}
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_fraction(value) }
PyRelativeHumidity {
inner: entropyk::RelativeHumidity::from_fraction(value),
}
}
fn to_percent(&self) -> f64 {
@@ -539,9 +556,13 @@ impl PyVaporQuality {
#[new]
fn new(value: f64) -> PyResult<Self> {
if !(0.0..=1.0).contains(&value) {
return Err(pyo3::exceptions::PyValueError::new_err("Value must be between 0.0 and 1.0"));
return Err(pyo3::exceptions::PyValueError::new_err(
"Value must be between 0.0 and 1.0",
));
}
Ok(PyVaporQuality { inner: entropyk::VaporQuality::from_fraction(value) })
Ok(PyVaporQuality {
inner: entropyk::VaporQuality::from_fraction(value),
})
}
#[getter]
@@ -551,12 +572,16 @@ impl PyVaporQuality {
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyVaporQuality { inner: entropyk::VaporQuality::from_fraction(value) }
PyVaporQuality {
inner: entropyk::VaporQuality::from_fraction(value),
}
}
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyVaporQuality { inner: entropyk::VaporQuality::from_percent(value) }
PyVaporQuality {
inner: entropyk::VaporQuality::from_percent(value),
}
}
fn to_fraction(&self) -> f64 {
@@ -579,4 +604,3 @@ impl PyVaporQuality {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
}