Update project structure and configurations

This commit is contained in:
2026-05-23 10:19:55 +02:00
parent ab5dc7e568
commit 62efea0646
1832 changed files with 83568 additions and 51829 deletions

View File

@@ -12,7 +12,7 @@ name = "entropyk"
crate-type = ["cdylib"]
[features]
default = ["coolprop"]
default = []
coolprop = ["entropyk-fluids/coolprop"]
[dependencies]

View File

@@ -555,7 +555,17 @@ pub struct PyFlowSource {
impl PyFlowSource {
#[new]
#[pyo3(signature = (pressure_pa=101325.0, temperature_k=300.0, fluid="Water"))]
fn new(pressure_pa: f64, temperature_k: f64, fluid: &str) -> PyResult<Self> {
fn new(py: Python<'_>, pressure_pa: f64, temperature_k: f64, fluid: &str) -> PyResult<Self> {
let warnings = py.import_bound("warnings")?;
warnings.call_method1(
"warn",
(
"FlowSource is deprecated. Use RefrigerantSource, BrineSource, or AirSource instead.",
py.get_type_bound::<pyo3::exceptions::PyDeprecationWarning>(),
2,
),
)?;
if pressure_pa <= 0.0 {
return Err(PyValueError::new_err("pressure_pa must be positive"));
}
@@ -600,8 +610,17 @@ pub struct PyFlowSink;
#[pymethods]
impl PyFlowSink {
#[new]
fn new() -> Self {
PyFlowSink
fn new(py: Python<'_>) -> PyResult<Self> {
let warnings = py.import_bound("warnings")?;
warnings.call_method1(
"warn",
(
"FlowSink is deprecated. Use RefrigerantSink, BrineSink, or AirSink instead.",
py.get_type_bound::<pyo3::exceptions::PyDeprecationWarning>(),
2,
),
)?;
Ok(PyFlowSink)
}
fn __repr__(&self) -> String {
@@ -657,6 +676,250 @@ impl PyOperationalState {
}
}
// =============================================================================
// Refrigerant Boundary Conditions
// =============================================================================
/// A boundary condition representing a refrigerant mass flow source.
#[pyclass(name = "RefrigerantSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyRefrigerantSource {
pub(crate) fluid: String,
pub(crate) p_set_pa: f64,
pub(crate) quality: f64,
}
#[pymethods]
impl PyRefrigerantSource {
#[new]
#[pyo3(signature = (fluid="R410A", pressure_pa=101325.0, quality=1.0))]
fn new(fluid: &str, pressure_pa: f64, quality: f64) -> PyResult<Self> {
if pressure_pa <= 0.0 {
return Err(PyValueError::new_err("pressure_pa must be positive"));
}
if !(0.0..=1.0).contains(&quality) {
return Err(PyValueError::new_err("quality must be between 0.0 and 1.0"));
}
Ok(PyRefrigerantSource {
fluid: fluid.to_string(),
p_set_pa: pressure_pa,
quality,
})
}
fn __repr__(&self) -> String {
format!(
"RefrigerantSource(fluid={}, P={:.0} Pa, q={:.2})",
self.fluid, self.p_set_pa, self.quality
)
}
}
impl PyRefrigerantSource {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk_components::PyRefrigerantSourceReal::new(&self.fluid, self.p_set_pa, self.quality))
}
}
/// A boundary condition representing a refrigerant mass flow sink.
#[pyclass(name = "RefrigerantSink", module = "entropyk")]
#[derive(Clone)]
pub struct PyRefrigerantSink {
pub(crate) fluid: String,
pub(crate) p_back_pa: f64,
pub(crate) quality_opt: Option<f64>,
}
#[pymethods]
impl PyRefrigerantSink {
#[new]
#[pyo3(signature = (fluid="R410A", p_back_pa=101325.0, quality=None))]
fn new(fluid: &str, p_back_pa: f64, quality: Option<f64>) -> PyResult<Self> {
if p_back_pa <= 0.0 {
return Err(PyValueError::new_err("p_back_pa must be positive"));
}
if let Some(q) = quality {
if !(0.0..=1.0).contains(&q) {
return Err(PyValueError::new_err("quality must be between 0.0 and 1.0"));
}
}
Ok(PyRefrigerantSink {
fluid: fluid.to_string(),
p_back_pa,
quality_opt: quality,
})
}
fn __repr__(&self) -> String {
format!(
"RefrigerantSink(fluid={}, P_back={:.0} Pa, q={:?})",
self.fluid, self.p_back_pa, self.quality_opt
)
}
}
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))
}
}
// =============================================================================
// Brine Boundary Conditions
// =============================================================================
/// A boundary condition representing a brine mass flow source.
#[pyclass(name = "BrineSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyBrineSource {
pub(crate) fluid: String,
pub(crate) concentration: f64,
pub(crate) temperature_k: f64,
pub(crate) pressure_pa: f64,
}
#[pymethods]
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> {
if pressure_pa <= 0.0 {
return Err(PyValueError::new_err("pressure_pa must be positive"));
}
if temperature_k <= 0.0 {
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"));
}
Ok(PyBrineSource {
fluid: fluid.to_string(),
concentration,
temperature_k,
pressure_pa,
})
}
fn __repr__(&self) -> String {
format!(
"BrineSource(fluid={}, c={:.2}, T={:.1} K, P={:.0} Pa)",
self.fluid, self.concentration, self.temperature_k, self.pressure_pa
)
}
}
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))
}
}
/// A boundary condition representing a brine mass flow sink.
#[pyclass(name = "BrineSink", module = "entropyk")]
#[derive(Clone)]
pub struct PyBrineSink {
pub(crate) p_back_pa: f64,
}
#[pymethods]
impl PyBrineSink {
#[new]
#[pyo3(signature = (p_back_pa=101325.0))]
fn new(p_back_pa: f64) -> PyResult<Self> {
if p_back_pa <= 0.0 {
return Err(PyValueError::new_err("p_back_pa must be positive"));
}
Ok(PyBrineSink { p_back_pa })
}
fn __repr__(&self) -> String {
format!("BrineSink(P_back={:.0} Pa)", self.p_back_pa)
}
}
impl PyBrineSink {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk_components::PyBrineSinkReal::new(self.p_back_pa))
}
}
// =============================================================================
// Air Boundary Conditions
// =============================================================================
/// A boundary condition representing an air mass flow source.
#[pyclass(name = "AirSource", module = "entropyk")]
#[derive(Clone)]
pub struct PyAirSource {
pub(crate) temperature_k: f64,
pub(crate) relative_humidity: f64,
pub(crate) pressure_pa: f64,
}
#[pymethods]
impl PyAirSource {
#[new]
#[pyo3(signature = (temperature_k=300.0, relative_humidity=0.5, pressure_pa=101325.0))]
fn new(temperature_k: f64, relative_humidity: f64, pressure_pa: f64) -> PyResult<Self> {
if pressure_pa <= 0.0 {
return Err(PyValueError::new_err("pressure_pa must be positive"));
}
if temperature_k <= 0.0 {
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"));
}
Ok(PyAirSource {
temperature_k,
relative_humidity,
pressure_pa,
})
}
fn __repr__(&self) -> String {
format!(
"AirSource(T={:.1} K, RH={:.2}, P={:.0} Pa)",
self.temperature_k, self.relative_humidity, self.pressure_pa
)
}
}
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))
}
}
/// A boundary condition representing an air mass flow sink.
#[pyclass(name = "AirSink", module = "entropyk")]
#[derive(Clone)]
pub struct PyAirSink {
pub(crate) p_back_pa: f64,
}
#[pymethods]
impl PyAirSink {
#[new]
#[pyo3(signature = (p_back_pa=101325.0))]
fn new(p_back_pa: f64) -> PyResult<Self> {
if p_back_pa <= 0.0 {
return Err(PyValueError::new_err("p_back_pa must be positive"));
}
Ok(PyAirSink { p_back_pa })
}
fn __repr__(&self) -> String {
format!("AirSink(P_back={:.0} Pa)", self.p_back_pa)
}
}
impl PyAirSink {
pub(crate) fn build(&self) -> Box<dyn Component> {
Box::new(entropyk_components::PyAirSinkReal::new(self.p_back_pa))
}
}
// =============================================================================
// Component enum for type-erasure
// =============================================================================
@@ -676,6 +939,12 @@ pub(crate) enum AnyPyComponent {
FlowMerger(PyFlowMerger),
FlowSource(PyFlowSource),
FlowSink(PyFlowSink),
RefrigerantSource(PyRefrigerantSource),
RefrigerantSink(PyRefrigerantSink),
BrineSource(PyBrineSource),
BrineSink(PyBrineSink),
AirSource(PyAirSource),
AirSink(PyAirSink),
}
impl AnyPyComponent {
@@ -694,6 +963,12 @@ impl AnyPyComponent {
AnyPyComponent::FlowMerger(c) => c.build(),
AnyPyComponent::FlowSource(c) => c.build(),
AnyPyComponent::FlowSink(c) => c.build(),
AnyPyComponent::RefrigerantSource(c) => c.build(),
AnyPyComponent::RefrigerantSink(c) => c.build(),
AnyPyComponent::BrineSource(c) => c.build(),
AnyPyComponent::BrineSink(c) => c.build(),
AnyPyComponent::AirSource(c) => c.build(),
AnyPyComponent::AirSink(c) => c.build(),
}
}
}

View File

@@ -21,6 +21,10 @@ fn entropyk(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<types::PyTemperature>()?;
m.add_class::<types::PyEnthalpy>()?;
m.add_class::<types::PyMassFlow>()?;
m.add_class::<types::PyConcentration>()?;
m.add_class::<types::PyVolumeFlow>()?;
m.add_class::<types::PyRelativeHumidity>()?;
m.add_class::<types::PyVaporQuality>()?;
// Components
m.add_class::<components::PyCompressor>()?;
@@ -35,6 +39,12 @@ fn entropyk(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<components::PyFlowMerger>()?;
m.add_class::<components::PyFlowSource>()?;
m.add_class::<components::PyFlowSink>()?;
m.add_class::<components::PyRefrigerantSource>()?;
m.add_class::<components::PyRefrigerantSink>()?;
m.add_class::<components::PyBrineSource>()?;
m.add_class::<components::PyBrineSink>()?;
m.add_class::<components::PyAirSource>()?;
m.add_class::<components::PyAirSink>()?;
m.add_class::<components::PyOperationalState>()?;
// Solver

View File

@@ -282,8 +282,26 @@ fn extract_component(obj: &Bound<'_, PyAny>) -> PyResult<AnyPyComponent> {
if let Ok(c) = obj.extract::<crate::components::PyFlowSink>() {
return Ok(AnyPyComponent::FlowSink(c));
}
if let Ok(c) = obj.extract::<crate::components::PyRefrigerantSource>() {
return Ok(AnyPyComponent::RefrigerantSource(c));
}
if let Ok(c) = obj.extract::<crate::components::PyRefrigerantSink>() {
return Ok(AnyPyComponent::RefrigerantSink(c));
}
if let Ok(c) = obj.extract::<crate::components::PyBrineSource>() {
return Ok(AnyPyComponent::BrineSource(c));
}
if let Ok(c) = obj.extract::<crate::components::PyBrineSink>() {
return Ok(AnyPyComponent::BrineSink(c));
}
if let Ok(c) = obj.extract::<crate::components::PyAirSource>() {
return Ok(AnyPyComponent::AirSource(c));
}
if let Ok(c) = obj.extract::<crate::components::PyAirSink>() {
return Ok(AnyPyComponent::AirSink(c));
}
Err(PyValueError::new_err(
"Expected a component (Compressor, Condenser, Evaporator, ExpansionValve, Pipe, Pump, Fan, Economizer, FlowSplitter, FlowMerger, FlowSource, FlowSink)",
"Expected a component (Compressor, Condenser, Evaporator, ExpansionValve, Pipe, Pump, Fan, Economizer, FlowSplitter, FlowMerger, FlowSource, FlowSink, RefrigerantSource, RefrigerantSink, BrineSource, BrineSink, AirSource, AirSink)",
))
}

View File

@@ -344,3 +344,239 @@ impl PyMassFlow {
}
}
}
// =============================================================================
// Concentration
// =============================================================================
#[pyclass(name = "Concentration", module = "entropyk")]
#[derive(Clone)]
pub struct PyConcentration {
pub(crate) inner: entropyk::Concentration,
}
#[pymethods]
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"));
}
Ok(PyConcentration {
inner: entropyk::Concentration::from_fraction(value),
})
}
#[getter]
fn value(&self) -> f64 {
self.inner.to_fraction()
}
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyConcentration {
inner: entropyk::Concentration::from_percent(value),
}
}
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyConcentration {
inner: entropyk::Concentration::from_fraction(value),
}
}
fn to_percent(&self) -> f64 {
self.inner.to_percent()
}
fn to_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn to_mass_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn __repr__(&self) -> String {
format!("Concentration({:.2}%)", self.inner.to_percent())
}
fn __float__(&self) -> f64 {
self.inner.to_fraction()
}
fn __eq__(&self, other: &PyConcentration) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
}
// =============================================================================
// VolumeFlow
// =============================================================================
#[pyclass(name = "VolumeFlow", module = "entropyk")]
#[derive(Clone)]
pub struct PyVolumeFlow {
pub(crate) inner: entropyk::VolumeFlow,
}
#[pymethods]
impl PyVolumeFlow {
#[new]
fn new(value: f64) -> PyResult<Self> {
if value < 0.0 {
return Err(pyo3::exceptions::PyValueError::new_err("Value cannot be negative"));
}
Ok(PyVolumeFlow { inner: entropyk::VolumeFlow::from_m3_per_s(value) })
}
#[getter]
fn value(&self) -> f64 {
self.inner.to_m3_per_s()
}
#[staticmethod]
fn from_m3_per_s(value: f64) -> Self {
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) }
}
fn to_m3_per_s(&self) -> f64 {
self.inner.to_m3_per_s()
}
fn to_l_per_min(&self) -> f64 {
self.inner.to_l_per_min()
}
fn __repr__(&self) -> String {
format!("VolumeFlow({:.6} m³/s)", self.inner.to_m3_per_s())
}
fn __float__(&self) -> f64 {
self.inner.to_m3_per_s()
}
fn __eq__(&self, other: &PyVolumeFlow) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-15
}
}
// =============================================================================
// RelativeHumidity
// =============================================================================
#[pyclass(name = "RelativeHumidity", module = "entropyk")]
#[derive(Clone)]
pub struct PyRelativeHumidity {
pub(crate) inner: entropyk::RelativeHumidity,
}
#[pymethods]
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"));
}
Ok(PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_fraction(value) })
}
#[getter]
fn value(&self) -> f64 {
self.inner.to_fraction()
}
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_percent(value) }
}
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_fraction(value) }
}
fn to_percent(&self) -> f64 {
self.inner.to_percent()
}
fn to_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn __repr__(&self) -> String {
format!("RelativeHumidity({:.2}%)", self.inner.to_percent())
}
fn __float__(&self) -> f64 {
self.inner.to_fraction()
}
fn __eq__(&self, other: &PyRelativeHumidity) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
}
// =============================================================================
// VaporQuality
// =============================================================================
#[pyclass(name = "VaporQuality", module = "entropyk")]
#[derive(Clone)]
pub struct PyVaporQuality {
pub(crate) inner: entropyk::VaporQuality,
}
#[pymethods]
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"));
}
Ok(PyVaporQuality { inner: entropyk::VaporQuality::from_fraction(value) })
}
#[getter]
fn value(&self) -> f64 {
self.inner.to_fraction()
}
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyVaporQuality { inner: entropyk::VaporQuality::from_fraction(value) }
}
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyVaporQuality { inner: entropyk::VaporQuality::from_percent(value) }
}
fn to_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn to_percent(&self) -> f64 {
self.inner.to_percent()
}
fn __repr__(&self) -> String {
format!("VaporQuality({:.2}%)", self.inner.to_percent())
}
fn __float__(&self) -> f64 {
self.inner.to_fraction()
}
fn __eq__(&self, other: &PyVaporQuality) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
}

View File

@@ -0,0 +1,180 @@
// =============================================================================
// Concentration
// =============================================================================
#[pyclass(name = "Concentration", module = "entropyk")]
#[derive(Clone)]
pub struct PyConcentration {
pub(crate) inner: entropyk::Concentration,
}
#[pymethods]
impl PyConcentration {
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyConcentration {
inner: entropyk::Concentration::from_percent(value),
}
}
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyConcentration {
inner: entropyk::Concentration::from_fraction(value),
}
}
fn to_percent(&self) -> f64 {
self.inner.to_percent()
}
fn to_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn to_mass_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn __repr__(&self) -> String {
format!("Concentration({:.2}%)", self.inner.to_percent())
}
fn __float__(&self) -> f64 {
self.inner.to_fraction()
}
fn __eq__(&self, other: &PyConcentration) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
}
// =============================================================================
// VolumeFlow
// =============================================================================
#[pyclass(name = "VolumeFlow", module = "entropyk")]
#[derive(Clone)]
pub struct PyVolumeFlow {
pub(crate) inner: entropyk::VolumeFlow,
}
#[pymethods]
impl PyVolumeFlow {
#[staticmethod]
fn from_m3_per_s(value: f64) -> Self {
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) }
}
fn to_m3_per_s(&self) -> f64 {
self.inner.to_m3_per_s()
}
fn to_l_per_min(&self) -> f64 {
self.inner.to_l_per_min()
}
fn __repr__(&self) -> String {
format!("VolumeFlow({:.6} m³/s)", self.inner.to_m3_per_s())
}
fn __float__(&self) -> f64 {
self.inner.to_m3_per_s()
}
fn __eq__(&self, other: &PyVolumeFlow) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-15
}
}
// =============================================================================
// RelativeHumidity
// =============================================================================
#[pyclass(name = "RelativeHumidity", module = "entropyk")]
#[derive(Clone)]
pub struct PyRelativeHumidity {
pub(crate) inner: entropyk::RelativeHumidity,
}
#[pymethods]
impl PyRelativeHumidity {
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_percent(value) }
}
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_fraction(value) }
}
fn to_percent(&self) -> f64 {
self.inner.to_percent()
}
fn to_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn __repr__(&self) -> String {
format!("RelativeHumidity({:.2}%)", self.inner.to_percent())
}
fn __float__(&self) -> f64 {
self.inner.to_fraction()
}
fn __eq__(&self, other: &PyRelativeHumidity) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
}
// =============================================================================
// VaporQuality
// =============================================================================
#[pyclass(name = "VaporQuality", module = "entropyk")]
#[derive(Clone)]
pub struct PyVaporQuality {
pub(crate) inner: entropyk::VaporQuality,
}
#[pymethods]
impl PyVaporQuality {
#[staticmethod]
fn from_fraction(value: f64) -> Self {
PyVaporQuality { inner: entropyk::VaporQuality::from_fraction(value) }
}
#[staticmethod]
fn from_percent(value: f64) -> Self {
PyVaporQuality { inner: entropyk::VaporQuality::from_percent(value) }
}
fn to_fraction(&self) -> f64 {
self.inner.to_fraction()
}
fn to_percent(&self) -> f64 {
self.inner.to_percent()
}
fn __repr__(&self) -> String {
format!("VaporQuality({:.2}%)", self.inner.to_percent())
}
fn __float__(&self) -> f64 {
self.inner.to_fraction()
}
fn __eq__(&self, other: &PyVaporQuality) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
}

View File

@@ -0,0 +1,347 @@
//! Python wrappers for Entropyk core physical types.
//!
//! Each wrapper holds the inner Rust NewType and exposes Pythonic constructors
//! with keyword arguments (e.g., `Pressure(bar=1.0)`) plus unit conversion methods.
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
// =============================================================================
// Pressure
// =============================================================================
/// Pressure in Pascals (Pa).
///
/// Construct with one of: ``pa``, ``bar``, ``kpa``, ``psi``.
///
/// Example::
///
/// p = Pressure(bar=1.0)
/// print(p.to_bar()) # 1.0
/// print(float(p)) # 100000.0
#[pyclass(name = "Pressure", module = "entropyk")]
#[derive(Clone)]
pub struct PyPressure {
pub(crate) inner: entropyk::Pressure,
}
#[pymethods]
impl PyPressure {
/// Create a Pressure. Specify exactly one of: ``pa``, ``bar``, ``kpa``, ``psi``.
#[new]
#[pyo3(signature = (pa=None, bar=None, kpa=None, psi=None))]
fn new(
pa: Option<f64>,
bar: Option<f64>,
kpa: Option<f64>,
psi: Option<f64>,
) -> PyResult<Self> {
let value = match (pa, bar, kpa, psi) {
(Some(v), None, None, None) => v,
(None, Some(v), None, None) => v * 100_000.0,
(None, None, Some(v), None) => v * 1_000.0,
(None, None, None, Some(v)) => v * 6894.75729,
_ => {
return Err(PyValueError::new_err(
"Specify exactly one of: pa, bar, kpa, psi",
))
}
};
Ok(PyPressure {
inner: entropyk::Pressure(value),
})
}
/// Value in Pascals.
fn to_pascals(&self) -> f64 {
self.inner.to_pascals()
}
/// Value in bar.
fn to_bar(&self) -> f64 {
self.inner.to_bar()
}
/// Value in kPa.
fn to_kpa(&self) -> f64 {
self.inner.0 / 1_000.0
}
/// Value in PSI.
fn to_psi(&self) -> f64 {
self.inner.to_psi()
}
fn __repr__(&self) -> String {
format!(
"Pressure({:.2} Pa = {:.4} bar)",
self.inner.0,
self.inner.0 / 100_000.0
)
}
fn __str__(&self) -> String {
format!("{:.2} Pa", self.inner.0)
}
fn __float__(&self) -> f64 {
self.inner.0
}
fn __eq__(&self, other: &PyPressure) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
fn __add__(&self, other: &PyPressure) -> PyPressure {
PyPressure {
inner: self.inner + other.inner,
}
}
fn __sub__(&self, other: &PyPressure) -> PyPressure {
PyPressure {
inner: self.inner - other.inner,
}
}
}
// =============================================================================
// Temperature
// =============================================================================
/// Temperature in Kelvin (K).
///
/// Construct with one of: ``kelvin``, ``celsius``, ``fahrenheit``.
///
/// Example::
///
/// t = Temperature(celsius=25.0)
/// print(t.to_kelvin()) # 298.15
/// print(t.to_celsius()) # 25.0
#[pyclass(name = "Temperature", module = "entropyk")]
#[derive(Clone)]
pub struct PyTemperature {
pub(crate) inner: entropyk::Temperature,
}
#[pymethods]
impl PyTemperature {
/// Create a Temperature. Specify exactly one of: ``kelvin``, ``celsius``, ``fahrenheit``.
#[new]
#[pyo3(signature = (kelvin=None, celsius=None, fahrenheit=None))]
fn new(kelvin: Option<f64>, celsius: Option<f64>, fahrenheit: Option<f64>) -> PyResult<Self> {
let inner = match (kelvin, celsius, fahrenheit) {
(Some(v), None, None) => entropyk::Temperature::from_kelvin(v),
(None, Some(v), None) => entropyk::Temperature::from_celsius(v),
(None, None, Some(v)) => entropyk::Temperature::from_fahrenheit(v),
_ => {
return Err(PyValueError::new_err(
"Specify exactly one of: kelvin, celsius, fahrenheit",
))
}
};
Ok(PyTemperature { inner })
}
/// Value in Kelvin.
fn to_kelvin(&self) -> f64 {
self.inner.to_kelvin()
}
/// Value in Celsius.
fn to_celsius(&self) -> f64 {
self.inner.to_celsius()
}
/// Value in Fahrenheit.
fn to_fahrenheit(&self) -> f64 {
self.inner.to_fahrenheit()
}
fn __repr__(&self) -> String {
format!(
"Temperature({:.2} K = {:.2} °C)",
self.inner.0,
self.inner.0 - 273.15
)
}
fn __str__(&self) -> String {
format!("{:.2} K", self.inner.0)
}
fn __float__(&self) -> f64 {
self.inner.0
}
fn __eq__(&self, other: &PyTemperature) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
fn __add__(&self, other: &PyTemperature) -> PyTemperature {
PyTemperature {
inner: entropyk::Temperature(self.inner.0 + other.inner.0),
}
}
fn __sub__(&self, other: &PyTemperature) -> PyTemperature {
PyTemperature {
inner: entropyk::Temperature(self.inner.0 - other.inner.0),
}
}
}
// =============================================================================
// Enthalpy
// =============================================================================
/// Specific enthalpy in J/kg.
///
/// Construct with one of: ``j_per_kg``, ``kj_per_kg``.
///
/// Example::
///
/// h = Enthalpy(kj_per_kg=250.0)
/// print(h.to_kj_per_kg()) # 250.0
#[pyclass(name = "Enthalpy", module = "entropyk")]
#[derive(Clone)]
pub struct PyEnthalpy {
pub(crate) inner: entropyk::Enthalpy,
}
#[pymethods]
impl PyEnthalpy {
/// Create an Enthalpy. Specify exactly one of: ``j_per_kg``, ``kj_per_kg``.
#[new]
#[pyo3(signature = (j_per_kg=None, kj_per_kg=None))]
fn new(j_per_kg: Option<f64>, kj_per_kg: Option<f64>) -> PyResult<Self> {
let inner = match (j_per_kg, kj_per_kg) {
(Some(v), None) => entropyk::Enthalpy::from_joules_per_kg(v),
(None, Some(v)) => entropyk::Enthalpy::from_kilojoules_per_kg(v),
_ => {
return Err(PyValueError::new_err(
"Specify exactly one of: j_per_kg, kj_per_kg",
))
}
};
Ok(PyEnthalpy { inner })
}
/// Value in J/kg.
fn to_j_per_kg(&self) -> f64 {
self.inner.to_joules_per_kg()
}
/// Value in kJ/kg.
fn to_kj_per_kg(&self) -> f64 {
self.inner.to_kilojoules_per_kg()
}
fn __repr__(&self) -> String {
format!(
"Enthalpy({:.2} J/kg = {:.2} kJ/kg)",
self.inner.0,
self.inner.0 / 1_000.0
)
}
fn __str__(&self) -> String {
format!("{:.2} J/kg", self.inner.0)
}
fn __float__(&self) -> f64 {
self.inner.0
}
fn __eq__(&self, other: &PyEnthalpy) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-10
}
fn __add__(&self, other: &PyEnthalpy) -> PyEnthalpy {
PyEnthalpy {
inner: entropyk::Enthalpy(self.inner.0 + other.inner.0),
}
}
fn __sub__(&self, other: &PyEnthalpy) -> PyEnthalpy {
PyEnthalpy {
inner: entropyk::Enthalpy(self.inner.0 - other.inner.0),
}
}
}
// =============================================================================
// MassFlow
// =============================================================================
/// Mass flow rate in kg/s.
///
/// Construct with one of: ``kg_per_s``, ``g_per_s``.
///
/// Example::
///
/// m = MassFlow(kg_per_s=0.5)
/// print(m.to_g_per_s()) # 500.0
#[pyclass(name = "MassFlow", module = "entropyk")]
#[derive(Clone)]
pub struct PyMassFlow {
pub(crate) inner: entropyk::MassFlow,
}
#[pymethods]
impl PyMassFlow {
/// Create a MassFlow. Specify exactly one of: ``kg_per_s``, ``g_per_s``.
#[new]
#[pyo3(signature = (kg_per_s=None, g_per_s=None))]
fn new(kg_per_s: Option<f64>, g_per_s: Option<f64>) -> PyResult<Self> {
let inner = match (kg_per_s, g_per_s) {
(Some(v), None) => entropyk::MassFlow::from_kg_per_s(v),
(None, Some(v)) => entropyk::MassFlow::from_grams_per_s(v),
_ => {
return Err(PyValueError::new_err(
"Specify exactly one of: kg_per_s, g_per_s",
))
}
};
Ok(PyMassFlow { inner })
}
/// Value in kg/s.
fn to_kg_per_s(&self) -> f64 {
self.inner.to_kg_per_s()
}
/// Value in g/s.
fn to_g_per_s(&self) -> f64 {
self.inner.to_grams_per_s()
}
fn __repr__(&self) -> String {
format!("MassFlow({:.6} kg/s)", self.inner.0)
}
fn __str__(&self) -> String {
format!("{:.6} kg/s", self.inner.0)
}
fn __float__(&self) -> f64 {
self.inner.0
}
fn __eq__(&self, other: &PyMassFlow) -> bool {
(self.inner.0 - other.inner.0).abs() < 1e-15
}
fn __add__(&self, other: &PyMassFlow) -> PyMassFlow {
PyMassFlow {
inner: entropyk::MassFlow(self.inner.0 + other.inner.0),
}
}
fn __sub__(&self, other: &PyMassFlow) -> PyMassFlow {
PyMassFlow {
inner: entropyk::MassFlow(self.inner.0 - other.inner.0),
}
}
}

View File

@@ -0,0 +1,93 @@
import pytest
import warnings
from entropyk import (
Concentration,
VolumeFlow,
RelativeHumidity,
VaporQuality,
RefrigerantSource,
RefrigerantSink,
BrineSource,
BrineSink,
AirSource,
AirSink,
FlowSource,
FlowSink,
System,
)
def test_physical_types_instantiation():
"""Test instantiation and constraints of new physical types."""
c = Concentration(0.3)
assert c.value == 0.3
with pytest.raises(ValueError):
Concentration(1.5)
vf = VolumeFlow(0.1)
assert vf.value == 0.1
with pytest.raises(ValueError):
VolumeFlow(-0.1)
rh = RelativeHumidity(0.5)
assert rh.value == 0.5
with pytest.raises(ValueError):
RelativeHumidity(-0.1)
vq = VaporQuality(0.8)
assert vq.value == 0.8
with pytest.raises(ValueError):
VaporQuality(1.1)
def test_refrigerant_boundary():
"""Test RefrigerantSource and RefrigerantSink instantiation."""
source = RefrigerantSource(fluid="R134a", pressure_pa=200000.0, quality=0.5)
sink = RefrigerantSink(fluid="R134a", p_back_pa=100000.0, quality=1.0)
assert "R134a" in repr(source)
assert "0.50" in repr(source)
assert "R134a" in repr(sink)
sys = System()
sys.add_component(source)
sys.add_component(sink)
def test_brine_boundary():
"""Test BrineSource and BrineSink instantiation."""
source = BrineSource(fluid="EthyleneGlycol", concentration=0.3, temperature_k=280.0, pressure_pa=200000.0)
sink = BrineSink(p_back_pa=150000.0)
assert "EthyleneGlycol" in repr(source)
assert "0.30" in repr(source)
assert "150000" in repr(sink)
sys = System()
sys.add_component(source)
sys.add_component(sink)
def test_air_boundary():
"""Test AirSource and AirSink instantiation."""
source = AirSource(temperature_k=293.15, relative_humidity=0.5, pressure_pa=101325.0)
sink = AirSink(p_back_pa=101325.0)
assert "293.1" in repr(source)
assert "0.50" in repr(source)
sys = System()
sys.add_component(source)
sys.add_component(sink)
def test_deprecated_flow_boundary():
"""Test that FlowSource and FlowSink raise deprecation warnings."""
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
FlowSource(pressure_pa=100000.0, temperature_k=300.0, fluid="Water")
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated" in str(w[-1].message).lower()
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
FlowSink()
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated" in str(w[-1].message).lower()