Update project structure and configurations
This commit is contained in:
@@ -12,7 +12,7 @@ name = "entropyk"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["coolprop"]
|
||||
default = []
|
||||
coolprop = ["entropyk-fluids/coolprop"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)",
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
180
bindings/python/src/types_appended.rs
Normal file
180
bindings/python/src/types_appended.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
347
bindings/python/src/types_clean.rs
Normal file
347
bindings/python/src/types_clean.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
bindings/python/tests/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
bindings/python/tests/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
93
bindings/python/tests/test_boundary.py
Normal file
93
bindings/python/tests/test_boundary.py
Normal 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()
|
||||
@@ -22,8 +22,9 @@ js-sys = "0.3"
|
||||
console_error_panic_hook = "0.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
petgraph = "0.6"
|
||||
tracing = "0.1"
|
||||
tracing-wasm = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
@@ -7,7 +7,6 @@ WebAssembly bindings for the [Entropyk](https://github.com/entropyk/entropyk) th
|
||||
- **Browser-native execution**: Run thermodynamic simulations directly in the browser
|
||||
- **TabularBackend**: Pre-computed fluid tables for fast property lookups (100x faster than direct EOS calls)
|
||||
- **Zero server dependency**: No backend required - runs entirely client-side
|
||||
- **Type-safe**: Full TypeScript definitions included
|
||||
- **JSON serialization**: All results are JSON-serializable for easy integration
|
||||
|
||||
## Installation
|
||||
@@ -19,30 +18,56 @@ npm install @entropyk/wasm
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
import init, {
|
||||
WasmSystem,
|
||||
import init, {
|
||||
WasmSystem,
|
||||
WasmCompressor,
|
||||
WasmCondenser,
|
||||
WasmEvaporator,
|
||||
WasmExpansionValve,
|
||||
WasmFallbackConfig
|
||||
WasmFallbackConfig
|
||||
} from '@entropyk/wasm';
|
||||
|
||||
// Initialize the WASM module
|
||||
await init();
|
||||
|
||||
// Create components
|
||||
const compressor = new WasmCompressor("R134a");
|
||||
const condenser = new WasmCondenser("R134a", 1000.0);
|
||||
const evaporator = new WasmEvaporator("R134a", 800.0);
|
||||
const valve = new WasmExpansionValve("R134a");
|
||||
// WasmCompressor(fluid, speed_rpm, displacement_m3_per_rev, efficiency, m1..m10)
|
||||
const compressor = new WasmCompressor(
|
||||
"R134a", // fluid
|
||||
2900.0, // speed_rpm
|
||||
0.0001, // displacement_m3_per_rev
|
||||
0.85, // mechanical_efficiency
|
||||
1.0, 0.0, 0.0, 0.0, 0.0, // AHRI 540 coefficients m1-m5
|
||||
0.0, 0.0, 0.0, 0.0, 0.0 // AHRI 540 coefficients m6-m10
|
||||
);
|
||||
|
||||
// Create system
|
||||
// WasmCondenser(ua) — thermal conductance in W/K
|
||||
const condenser = new WasmCondenser(1000.0);
|
||||
|
||||
// WasmEvaporator(ua) — thermal conductance in W/K
|
||||
const evaporator = new WasmEvaporator(800.0);
|
||||
|
||||
// WasmExpansionValve(fluid, capacity_kW) — pass 0 for no capacity limit
|
||||
const valve = new WasmExpansionValve("R134a", 0.0);
|
||||
|
||||
// Create system and add components
|
||||
const system = new WasmSystem();
|
||||
const n0 = system.add_component(compressor.into_component());
|
||||
const n1 = system.add_component(condenser.into_component());
|
||||
const n2 = system.add_component(valve.into_component());
|
||||
const n3 = system.add_component(evaporator.into_component());
|
||||
|
||||
// Connect components in a cycle
|
||||
system.add_edge(n0, n1)?; // compressor → condenser
|
||||
system.add_edge(n1, n2)?; // condenser → valve
|
||||
system.add_edge(n2, n3)?; // valve → evaporator
|
||||
system.add_edge(n3, n0)?; // evaporator → compressor
|
||||
|
||||
// Finalize topology
|
||||
system.finalize()?;
|
||||
|
||||
// Configure solver
|
||||
const config = new WasmFallbackConfig();
|
||||
config.timeout_ms(1000);
|
||||
|
||||
// Solve
|
||||
const result = system.solve(config);
|
||||
@@ -52,7 +77,17 @@ console.log(result.toJson());
|
||||
// "converged": true,
|
||||
// "iterations": 12,
|
||||
// "final_residual": 1e-8,
|
||||
// "solve_time_ms": 45
|
||||
// "status": "Converged"
|
||||
// }
|
||||
|
||||
// Get thermodynamic state at a node
|
||||
const state = system.get_node_result(0);
|
||||
console.log(state.toJson());
|
||||
// {
|
||||
// "pressure": { "pascals": 1000000.0 },
|
||||
// "temperature": { "kelvin": 310.5 },
|
||||
// "enthalpy": { "joules_per_kg": 420000.0 },
|
||||
// "mass_flow": { "kg_per_s": 0.0 }
|
||||
// }
|
||||
```
|
||||
|
||||
@@ -60,26 +95,56 @@ console.log(result.toJson());
|
||||
|
||||
### Core Types
|
||||
|
||||
- `WasmPressure` - Pressure in Pascals or bar
|
||||
- `WasmTemperature` - Temperature in Kelvin or Celsius
|
||||
- `WasmEnthalpy` - Enthalpy in J/kg or kJ/kg
|
||||
- `WasmMassFlow` - Mass flow rate in kg/s
|
||||
- `WasmPressure` — Pressure in Pascals (constructor validates non-negative)
|
||||
- `new(pascals)` — from Pascals
|
||||
- `from_bar(bar)` — from bar
|
||||
- `WasmTemperature` — Temperature in Kelvin (constructor validates non-negative)
|
||||
- `new(kelvin)` — from Kelvin
|
||||
- `from_celsius(celsius)` — from Celsius
|
||||
- `WasmEnthalpy` — Enthalpy in J/kg (constructor rejects NaN)
|
||||
- `new(joules_per_kg)` — from J/kg
|
||||
- `from_kj_per_kg(kj_per_kg)` — from kJ/kg
|
||||
- `WasmMassFlow` — Mass flow in kg/s (constructor rejects NaN)
|
||||
- `new(kg_per_s)` — from kg/s
|
||||
|
||||
All types have a `toJson()` method returning a JSON string.
|
||||
|
||||
### Components
|
||||
|
||||
- `WasmCompressor` - AHRI 540 compressor model
|
||||
- `WasmCondenser` - Heat rejection heat exchanger
|
||||
- `WasmEvaporator` - Heat absorption heat exchanger
|
||||
- `WasmExpansionValve` - Isenthalpic expansion device
|
||||
- `WasmEconomizer` - Internal heat exchanger
|
||||
- `WasmCompressor` — AHRI 540 compressor model
|
||||
- `new(fluid, speed_rpm, displacement, efficiency, m1..m10)` — all 10 AHRI coefficients required
|
||||
- `WasmCondenser` — Heat rejection heat exchanger
|
||||
- `new(ua)` — thermal conductance in W/K (must be positive)
|
||||
- `WasmEvaporator` — Heat absorption heat exchanger
|
||||
- `new(ua)` — thermal conductance in W/K (must be positive)
|
||||
- `WasmExpansionValve` — Isenthalpic expansion device
|
||||
- `new(fluid, capacity_kW)` — pass 0 for no capacity limit
|
||||
- `WasmPipe` — Transport pipe
|
||||
- `new(fluid, length, diameter, density, viscosity)` — all parameters required
|
||||
|
||||
Each component has an `into_component()` method that converts it to a generic `WasmComponent` for adding to the system.
|
||||
|
||||
### Solver
|
||||
|
||||
- `WasmSystem` - Thermodynamic system container
|
||||
- `WasmNewtonConfig` - Newton-Raphson solver configuration
|
||||
- `WasmPicardConfig` - Sequential substitution solver configuration
|
||||
- `WasmFallbackConfig` - Intelligent fallback solver configuration
|
||||
- `WasmConvergedState` - Solver result
|
||||
- `WasmSystem` — Thermodynamic system container
|
||||
- `add_component(component)` → node index
|
||||
- `add_edge(from_idx, to_idx)` — connect two component nodes
|
||||
- `finalize()` — finalize topology before solving
|
||||
- `solve(config)` — solve with fallback strategy
|
||||
- `solve_newton(config)` — solve with Newton-Raphson only
|
||||
- `get_node_result(idx)` — get thermodynamic state at a node after solving
|
||||
- `WasmNewtonConfig` — Newton-Raphson solver configuration
|
||||
- `set_max_iterations(n)`, `set_tolerance(tol)`
|
||||
- `WasmPicardConfig` — Sequential substitution solver configuration
|
||||
- `set_max_iterations(n)`, `set_relaxation_factor(omega)` — omega clamped to (0, 1]
|
||||
- `WasmFallbackConfig` — Intelligent fallback solver configuration
|
||||
- `set_fallback_enabled(bool)`, `set_return_to_newton_threshold(f64)`, `set_max_fallback_switches(usize)`
|
||||
- `WasmConvergedState` — Solver result with `converged`, `iterations`, `final_residual`, `status`
|
||||
|
||||
### Fluid Management
|
||||
|
||||
- `list_available_fluids()` — returns array of available fluid names
|
||||
- `load_fluid_table(json_string)` — load a custom fluid table from JSON
|
||||
|
||||
## Build Requirements
|
||||
|
||||
@@ -95,13 +160,22 @@ git clone https://github.com/entropyk/entropyk.git
|
||||
cd entropyk/bindings/wasm
|
||||
|
||||
# Build for browsers
|
||||
npm run build
|
||||
wasm-pack build --target web
|
||||
|
||||
# Build for Node.js
|
||||
npm run build:node
|
||||
wasm-pack build --target nodejs
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
# Run Rust WASM tests
|
||||
wasm-pack test --node
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
TypeScript definitions are auto-generated by `wasm-pack build` in the `pkg/` directory.
|
||||
After building, import the generated `.d.ts` file:
|
||||
|
||||
```typescript
|
||||
import init, { WasmSystem } from './pkg/entropyk_wasm';
|
||||
```
|
||||
|
||||
## Performance
|
||||
@@ -115,17 +189,8 @@ npm test
|
||||
## Limitations
|
||||
|
||||
- **CoolProp unavailable**: The WASM build uses TabularBackend with pre-computed tables. CoolProp C++ cannot compile to WebAssembly.
|
||||
- **Limited fluid library**: By default, only R134a is embedded. Additional fluids can be loaded from JSON tables.
|
||||
|
||||
## Loading Custom Fluid Tables
|
||||
|
||||
```javascript
|
||||
import { load_fluid_table } from '@entropyk/wasm';
|
||||
|
||||
// Load a custom fluid table (generated from the entropyk CLI)
|
||||
const r410aTable = await fetch('/path/to/r410a.json').then(r => r.text());
|
||||
await load_fluid_table(r410aTable);
|
||||
```
|
||||
- **Limited fluid library**: By default, only R134a is embedded. Additional fluids can be loaded from JSON tables generated by the entropyk CLI.
|
||||
- **Custom fluid tables**: `load_fluid_table` validates the table but registration in the global backend requires additional infrastructure (planned).
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
|
||||
@@ -37,97 +37,122 @@
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.loading {
|
||||
color: #666;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
.loading { color: #666; }
|
||||
.error { color: #dc3545; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Entropyk WebAssembly Demo</h1>
|
||||
<p>Thermodynamic cycle simulation running entirely in your browser!</p>
|
||||
|
||||
|
||||
<div class="card">
|
||||
<h2>System Information</h2>
|
||||
<div id="info" class="loading">Loading WASM module...</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card">
|
||||
<h2>Run Simulation</h2>
|
||||
<button id="runBtn" disabled>Run Simple Cycle</button>
|
||||
<div id="result" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<script type="module">
|
||||
import init, {
|
||||
version,
|
||||
list_available_fluids,
|
||||
WasmSystem,
|
||||
WasmPressure,
|
||||
WasmTemperature,
|
||||
WasmFallbackConfig
|
||||
import init, {
|
||||
version,
|
||||
list_available_fluids,
|
||||
WasmSystem,
|
||||
WasmCompressor,
|
||||
WasmCondenser,
|
||||
WasmEvaporator,
|
||||
WasmExpansionValve,
|
||||
WasmFallbackConfig
|
||||
} from './pkg/entropyk_wasm.js';
|
||||
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
||||
async function setup() {
|
||||
try {
|
||||
await init();
|
||||
initialized = true;
|
||||
|
||||
|
||||
const infoDiv = document.getElementById('info');
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
|
||||
|
||||
infoDiv.innerHTML = `
|
||||
<p><strong>Version:</strong> ${version()}</p>
|
||||
<p><strong>Available Fluids:</strong> ${list_available_fluids().join(', ')}</p>
|
||||
<p style="color: green;">✓ WASM module loaded successfully</p>
|
||||
<p style="color: green;">WASM module loaded successfully</p>
|
||||
`;
|
||||
|
||||
|
||||
runBtn.disabled = false;
|
||||
runBtn.onclick = runSimulation;
|
||||
|
||||
|
||||
} catch (err) {
|
||||
document.getElementById('info').innerHTML = `
|
||||
<p class="error">Failed to load WASM: ${err.message}</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function runSimulation() {
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = '<p class="loading">Running simulation...</p>';
|
||||
|
||||
|
||||
try {
|
||||
const startTime = performance.now();
|
||||
|
||||
// Create system
|
||||
|
||||
const system = new WasmSystem();
|
||||
|
||||
// Configure solver
|
||||
const config = new WasmFallbackConfig();
|
||||
config.timeout_ms(1000);
|
||||
|
||||
|
||||
// Create components
|
||||
const compressor = new WasmCompressor(
|
||||
"R134a", 2900.0, 0.0001, 0.85,
|
||||
0.85, 2.5, 500.0, 1500.0, -2.5,
|
||||
1.8, 600.0, 1600.0, -3.0, 2.0
|
||||
).into_component();
|
||||
const condenser = new WasmCondenser(5000.0).into_component();
|
||||
const evaporator = new WasmEvaporator(3000.0).into_component();
|
||||
const valve = new WasmExpansionValve("R134a", 0.0).into_component();
|
||||
|
||||
// Add to system
|
||||
const c = system.add_component(compressor);
|
||||
const cd = system.add_component(condenser);
|
||||
const e = system.add_component(evaporator);
|
||||
const v = system.add_component(valve);
|
||||
|
||||
// Connect cycle
|
||||
system.add_edge(c, cd);
|
||||
system.add_edge(cd, v);
|
||||
system.add_edge(v, e);
|
||||
system.add_edge(e, c);
|
||||
system.finalize();
|
||||
|
||||
// Solve
|
||||
const config = new WasmFallbackConfig();
|
||||
const state = system.solve(config);
|
||||
const endTime = performance.now();
|
||||
|
||||
|
||||
let nodeInfo = '';
|
||||
if (state.converged) {
|
||||
const nodeState = system.get_node_result(0);
|
||||
nodeInfo = `<h3>Node 0 State:</h3><pre>${nodeState.toJson()}</pre>`;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<p><strong>Converged:</strong> ${state.converged ? '✓' : '✗'}</p>
|
||||
<p><strong>Converged:</strong> ${state.converged ? 'Yes' : 'No'}</p>
|
||||
<p><strong>Status:</strong> ${state.status}</p>
|
||||
<p><strong>Iterations:</strong> ${state.iterations}</p>
|
||||
<p><strong>Final Residual:</strong> ${state.final_residual.toExponential(2)}</p>
|
||||
<p><strong>Solve Time:</strong> ${(endTime - startTime).toFixed(2)} ms</p>
|
||||
<h3>JSON Output:</h3>
|
||||
<h3>Result JSON:</h3>
|
||||
<pre>${state.toJson()}</pre>
|
||||
${nodeInfo}
|
||||
`;
|
||||
|
||||
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = `<p class="error">Error: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setup();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -4,19 +4,36 @@
|
||||
//! CoolProp C++ cannot compile to WASM, so we must use tabular interpolation.
|
||||
|
||||
use entropyk_fluids::{FluidBackend, TabularBackend};
|
||||
use std::sync::OnceLock;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Embedded R134a fluid table data.
|
||||
const R134A_TABLE: &str = include_str!("../../../crates/fluids/data/r134a.json");
|
||||
|
||||
/// Create the default backend for WASM with embedded fluid tables.
|
||||
// TODO: Generate and embed additional fluid tables (R410A, R32, etc.)
|
||||
// using the TabularBackend table generation tool. Only R134a is currently available.
|
||||
|
||||
/// Global backend instance, lazily initialized.
|
||||
static GLOBAL_BACKEND: OnceLock<TabularBackend> = OnceLock::new();
|
||||
|
||||
/// Get or create the global backend with embedded fluid tables.
|
||||
pub fn global_backend() -> &'static TabularBackend {
|
||||
GLOBAL_BACKEND.get_or_init(|| {
|
||||
let mut backend = TabularBackend::new();
|
||||
backend
|
||||
.load_table_from_str(R134A_TABLE)
|
||||
.expect("Embedded R134a table must be valid");
|
||||
tracing::info!("WASM backend initialized with R134a fluid table");
|
||||
backend
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a default backend (for backwards compatibility and tests).
|
||||
pub fn create_default_backend() -> TabularBackend {
|
||||
let mut backend = TabularBackend::new();
|
||||
|
||||
backend
|
||||
.load_table_from_str(R134A_TABLE)
|
||||
.expect("Embedded R134a table must be valid");
|
||||
|
||||
backend
|
||||
}
|
||||
|
||||
@@ -26,21 +43,31 @@ pub fn create_empty_backend() -> TabularBackend {
|
||||
}
|
||||
|
||||
/// Load a fluid table from a JSON string (exposed to JS).
|
||||
///
|
||||
/// The table is loaded into the global backend so it is available
|
||||
/// for subsequent solve calls. Returns an error if the JSON is invalid
|
||||
/// or the table cannot be parsed.
|
||||
#[wasm_bindgen]
|
||||
pub fn load_fluid_table(json: String) -> Result<(), String> {
|
||||
let mut backend = create_empty_backend();
|
||||
backend
|
||||
// We cannot mutate the OnceLock, so we load into a new backend
|
||||
// and register it via a separate mechanism.
|
||||
// For now, validate the table can be parsed.
|
||||
let mut test_backend = TabularBackend::new();
|
||||
test_backend
|
||||
.load_table_from_str(&json)
|
||||
.map_err(|e| format!("Failed to load fluid table: {}", e))?;
|
||||
|
||||
tracing::warn!(
|
||||
"Fluid table validated but not yet registered in global backend. \
|
||||
Custom fluid loading requires additional infrastructure."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get list of available fluids in the default backend.
|
||||
#[wasm_bindgen]
|
||||
pub fn list_available_fluids() -> Vec<String> {
|
||||
let backend = create_default_backend();
|
||||
backend.list_fluids().into_iter().map(|f| f.0).collect()
|
||||
global_backend().list_fluids().into_iter().map(|f| f.0).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Provides JavaScript-friendly wrappers for thermodynamic components.
|
||||
|
||||
use entropyk_components::port::{Connected, FluidId, Port};
|
||||
use entropyk_components::port::{FluidId, Port};
|
||||
use entropyk_components::Component;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -10,30 +10,39 @@ use wasm_bindgen::prelude::*;
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmComponent {
|
||||
pub(crate) inner: Box<dyn Component>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmComponent {
|
||||
/// Get component name.
|
||||
/// Get component type name.
|
||||
pub fn name(&self) -> String {
|
||||
// This is a simplification; the real Component trait doesn't have name()
|
||||
// but the System stores it. For now, we'll just return a placeholder or
|
||||
// store it in the wrapper if needed.
|
||||
"Component".to_string()
|
||||
self.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM wrapper for Compressor.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmCompressor {
|
||||
pub(crate) inner: entropyk_components::Compressor<Connected>,
|
||||
pub(crate) inner: entropyk_components::Compressor<entropyk_components::port::Connected>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmCompressor {
|
||||
/// Create a new Compressor component.
|
||||
/// Create a new Compressor with AHRI 540 performance model.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fluid` - Fluid identifier (e.g. "R134a", "R410A")
|
||||
/// * `speed_rpm` - Rotational speed in RPM
|
||||
/// * `displacement_m3_per_rev` - Displacement volume in m³/rev
|
||||
/// * `efficiency` - Mechanical efficiency (0.0 to 1.0)
|
||||
/// * `m1`..`m10` - AHRI 540 performance coefficients
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
fluid: String,
|
||||
speed_rpm: f64,
|
||||
displacement_m3_per_rev: f64,
|
||||
efficiency: f64,
|
||||
m1: f64,
|
||||
m2: f64,
|
||||
m3: f64,
|
||||
@@ -44,31 +53,49 @@ impl WasmCompressor {
|
||||
m8: f64,
|
||||
m9: f64,
|
||||
m10: f64,
|
||||
) -> WasmCompressor {
|
||||
) -> Result<WasmCompressor, JsValue> {
|
||||
if speed_rpm <= 0.0 {
|
||||
return Err(js_sys::Error::new("speed_rpm must be positive").into());
|
||||
}
|
||||
if displacement_m3_per_rev <= 0.0 {
|
||||
return Err(js_sys::Error::new("displacement_m3_per_rev must be positive").into());
|
||||
}
|
||||
if !(0.0..=1.0).contains(&efficiency) {
|
||||
return Err(js_sys::Error::new("efficiency must be between 0.0 and 1.0").into());
|
||||
}
|
||||
|
||||
let coeffs =
|
||||
entropyk_components::Ahri540Coefficients::new(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10);
|
||||
let fluid_id = FluidId::new("R410A");
|
||||
let p = entropyk_core::Pressure::from_bar(10.0);
|
||||
let fluid_id = FluidId::new(&fluid);
|
||||
let p = entropyk_core::Pressure::from_bar(5.0);
|
||||
let h = entropyk_core::Enthalpy::from_joules_per_kg(400000.0);
|
||||
let suction = Port::new(fluid_id.clone(), p, h);
|
||||
let discharge = Port::new(fluid_id, p, h);
|
||||
let discharge = Port::new(fluid_id.clone(), p, h);
|
||||
|
||||
let comp =
|
||||
entropyk_components::Compressor::new(coeffs, suction, discharge, 2900.0, 0.0001, 0.85)
|
||||
.unwrap();
|
||||
let comp = entropyk_components::Compressor::new(
|
||||
coeffs,
|
||||
suction,
|
||||
discharge,
|
||||
speed_rpm,
|
||||
displacement_m3_per_rev,
|
||||
efficiency,
|
||||
)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Compressor creation failed: {}", e)))?;
|
||||
|
||||
// Connect to dummy ports to get Connected state
|
||||
let suction_p = Port::new(FluidId::new("R410A"), p, h);
|
||||
let discharge_p = Port::new(FluidId::new("R410A"), p, h);
|
||||
let connected = comp.connect(suction_p, discharge_p).unwrap();
|
||||
let suction_p = Port::new(fluid_id.clone(), p, h);
|
||||
let discharge_p = Port::new(fluid_id, p, h);
|
||||
let connected = comp
|
||||
.connect(suction_p, discharge_p)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Compressor connect failed: {}", e)))?;
|
||||
|
||||
WasmCompressor { inner: connected }
|
||||
Ok(WasmCompressor { inner: connected })
|
||||
}
|
||||
|
||||
/// Convert to a generic WasmComponent.
|
||||
pub fn into_component(self) -> WasmComponent {
|
||||
WasmComponent {
|
||||
inner: Box::new(self.inner),
|
||||
name: "Compressor".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,18 +108,27 @@ pub struct WasmCondenser {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmCondenser {
|
||||
/// Create a new condenser with thermal conductance UA.
|
||||
/// Create a new condenser with thermal conductance UA (W/K).
|
||||
///
|
||||
/// Rejects NaN, negative, zero, and infinite values.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(ua: f64) -> WasmCondenser {
|
||||
WasmCondenser {
|
||||
inner: entropyk_components::Condenser::new(ua),
|
||||
pub fn new(ua: f64) -> Result<WasmCondenser, JsValue> {
|
||||
if ua.is_nan() || ua.is_infinite() {
|
||||
return Err(js_sys::Error::new("UA must be a finite number").into());
|
||||
}
|
||||
if ua <= 0.0 {
|
||||
return Err(js_sys::Error::new("UA must be positive").into());
|
||||
}
|
||||
Ok(WasmCondenser {
|
||||
inner: entropyk_components::Condenser::new(ua),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert to a generic WasmComponent.
|
||||
pub fn into_component(self) -> WasmComponent {
|
||||
WasmComponent {
|
||||
inner: Box::new(self.inner),
|
||||
name: "Condenser".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,18 +141,27 @@ pub struct WasmEvaporator {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmEvaporator {
|
||||
/// Create a new evaporator with thermal conductance UA.
|
||||
/// Create a new evaporator with thermal conductance UA (W/K).
|
||||
///
|
||||
/// Rejects NaN, negative, zero, and infinite values.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(ua: f64) -> WasmEvaporator {
|
||||
WasmEvaporator {
|
||||
inner: entropyk_components::Evaporator::new(ua),
|
||||
pub fn new(ua: f64) -> Result<WasmEvaporator, JsValue> {
|
||||
if ua.is_nan() || ua.is_infinite() {
|
||||
return Err(js_sys::Error::new("UA must be a finite number").into());
|
||||
}
|
||||
if ua <= 0.0 {
|
||||
return Err(js_sys::Error::new("UA must be positive").into());
|
||||
}
|
||||
Ok(WasmEvaporator {
|
||||
inner: entropyk_components::Evaporator::new(ua),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert to a generic WasmComponent.
|
||||
pub fn into_component(self) -> WasmComponent {
|
||||
WasmComponent {
|
||||
inner: Box::new(self.inner),
|
||||
name: "Evaporator".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,33 +169,42 @@ impl WasmEvaporator {
|
||||
/// WASM wrapper for ExpansionValve.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmExpansionValve {
|
||||
pub(crate) inner: entropyk_components::ExpansionValve<Connected>,
|
||||
pub(crate) inner: entropyk_components::ExpansionValve<entropyk_components::port::Connected>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmExpansionValve {
|
||||
/// Create a new expansion valve.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fluid` - Fluid identifier (e.g. "R134a", "R410A")
|
||||
/// * `capacity` - Optional valve capacity (kW). Pass 0.0 for no capacity limit.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmExpansionValve {
|
||||
let fluid_id = FluidId::new("R410A");
|
||||
pub fn new(fluid: String, capacity: f64) -> Result<WasmExpansionValve, JsValue> {
|
||||
let fluid_id = FluidId::new(&fluid);
|
||||
let p = entropyk_core::Pressure::from_bar(10.0);
|
||||
let h = entropyk_core::Enthalpy::from_joules_per_kg(400000.0);
|
||||
let inlet = Port::new(fluid_id.clone(), p, h);
|
||||
let outlet = Port::new(fluid_id, p, h);
|
||||
let outlet = Port::new(fluid_id.clone(), p, h);
|
||||
|
||||
let valve = entropyk_components::ExpansionValve::new(inlet, outlet, Some(1.0)).unwrap();
|
||||
let cap = if capacity > 0.0 { Some(capacity) } else { None };
|
||||
let valve = entropyk_components::ExpansionValve::new(inlet, outlet, cap)
|
||||
.map_err(|e| js_sys::Error::new(&format!("ExpansionValve creation failed: {}", e)))?;
|
||||
|
||||
let inlet_p = Port::new(FluidId::new("R410A"), p, h);
|
||||
let outlet_p = Port::new(FluidId::new("R410A"), p, h);
|
||||
let connected = valve.connect(inlet_p, outlet_p).unwrap();
|
||||
let inlet_p = Port::new(fluid_id.clone(), p, h);
|
||||
let outlet_p = Port::new(fluid_id, p, h);
|
||||
let connected = valve
|
||||
.connect(inlet_p, outlet_p)
|
||||
.map_err(|e| js_sys::Error::new(&format!("ExpansionValve connect failed: {}", e)))?;
|
||||
|
||||
WasmExpansionValve { inner: connected }
|
||||
Ok(WasmExpansionValve { inner: connected })
|
||||
}
|
||||
|
||||
/// Convert to a generic WasmComponent.
|
||||
pub fn into_component(self) -> WasmComponent {
|
||||
WasmComponent {
|
||||
inner: Box::new(self.inner),
|
||||
name: "ExpansionValve".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,38 +212,60 @@ impl WasmExpansionValve {
|
||||
/// WASM wrapper for Pipe.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmPipe {
|
||||
pub(crate) inner: entropyk_components::Pipe<Connected>,
|
||||
pub(crate) inner: entropyk_components::Pipe<entropyk_components::port::Connected>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmPipe {
|
||||
/// Create a new pipe.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fluid` - Fluid identifier (e.g. "Water")
|
||||
/// * `length` - Pipe length in meters (must be positive)
|
||||
/// * `diameter` - Pipe inner diameter in meters (must be positive)
|
||||
/// * `density` - Fluid density in kg/m³ (default: 1000.0 for water)
|
||||
/// * `viscosity` - Fluid dynamic viscosity in Pa·s (default: 0.001 for water)
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(length: f64, diameter: f64) -> WasmPipe {
|
||||
let geometry = entropyk_components::PipeGeometry::smooth(length, diameter).unwrap();
|
||||
let fluid_id = FluidId::new("Water");
|
||||
pub fn new(
|
||||
fluid: String,
|
||||
length: f64,
|
||||
diameter: f64,
|
||||
density: f64,
|
||||
viscosity: f64,
|
||||
) -> Result<WasmPipe, JsValue> {
|
||||
if length.is_nan() || length <= 0.0 {
|
||||
return Err(js_sys::Error::new("Pipe length must be a positive number").into());
|
||||
}
|
||||
if diameter.is_nan() || diameter <= 0.0 {
|
||||
return Err(js_sys::Error::new("Pipe diameter must be a positive number").into());
|
||||
}
|
||||
|
||||
let geometry = entropyk_components::PipeGeometry::smooth(length, diameter)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Invalid pipe geometry: {}", e)))?;
|
||||
|
||||
let fluid_id = FluidId::new(&fluid);
|
||||
let p = entropyk_core::Pressure::from_bar(1.0);
|
||||
let h = entropyk_core::Enthalpy::from_joules_per_kg(100000.0);
|
||||
let inlet = Port::new(fluid_id.clone(), p, h);
|
||||
let outlet = Port::new(fluid_id, p, h);
|
||||
let outlet = Port::new(fluid_id.clone(), p, h);
|
||||
|
||||
let pipe = entropyk_components::Pipe::new(
|
||||
geometry, inlet, outlet, 1000.0, // Default density
|
||||
0.001, // Default viscosity
|
||||
)
|
||||
.unwrap();
|
||||
let pipe = entropyk_components::Pipe::new(geometry, inlet, outlet, density, viscosity)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Pipe creation failed: {}", e)))?;
|
||||
|
||||
let inlet_p = Port::new(FluidId::new("Water"), p, h);
|
||||
let outlet_p = Port::new(FluidId::new("Water"), p, h);
|
||||
let connected = pipe.connect(inlet_p, outlet_p).unwrap();
|
||||
let inlet_p = Port::new(fluid_id.clone(), p, h);
|
||||
let outlet_p = Port::new(fluid_id, p, h);
|
||||
let connected = pipe
|
||||
.connect(inlet_p, outlet_p)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Pipe connect failed: {}", e)))?;
|
||||
|
||||
WasmPipe { inner: connected }
|
||||
Ok(WasmPipe { inner: connected })
|
||||
}
|
||||
|
||||
/// Convert to a generic WasmComponent.
|
||||
pub fn into_component(self) -> WasmComponent {
|
||||
WasmComponent {
|
||||
inner: Box::new(self.inner),
|
||||
name: "Pipe".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
//! Error handling for WASM bindings.
|
||||
//!
|
||||
//! Maps errors to JavaScript exceptions with human-readable messages.
|
||||
//! Maps internal error types to JavaScript exceptions with human-readable messages.
|
||||
|
||||
use entropyk::ThermoError;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
/// Convert a Result to a Result with JsValue error.
|
||||
pub fn result_to_js<T, E: std::fmt::Display>(result: Result<T, E>) -> Result<T, JsValue> {
|
||||
result.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
}
|
||||
|
||||
/// Map ThermoError to a human-readable JsValue.
|
||||
pub fn thermo_error_to_js(e: ThermoError) -> JsValue {
|
||||
let msg = match &e {
|
||||
ThermoError::Component(err) => format!("Component error: {}", err),
|
||||
ThermoError::Solver(err) => format!("Solver error: {}", err),
|
||||
ThermoError::Fluid(err) => format!("Fluid error: {}", err),
|
||||
ThermoError::Topology(err) => format!("Topology error: {}", err),
|
||||
ThermoError::AddEdge(err) => format!("Edge error: {}", err),
|
||||
ThermoError::Connection(err) => format!("Connection error: {}", err),
|
||||
ThermoError::Constraint(err) => format!("Constraint error: {}", err),
|
||||
_ => format!("{}", e),
|
||||
};
|
||||
js_sys::Error::new(&msg).into()
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
//!
|
||||
//! Provides JavaScript-friendly wrappers for the solver and system.
|
||||
|
||||
use crate::backend::global_backend;
|
||||
use crate::components::WasmComponent;
|
||||
use crate::types::{WasmConvergedState, WasmThermoState};
|
||||
use entropyk_components::port::{FluidId, Port};
|
||||
use entropyk_components::Component;
|
||||
use entropyk_fluids::FluidBackend;
|
||||
use entropyk_solver::{
|
||||
ConvergedState, FallbackSolver, NewtonConfig, PicardConfig, Solver, SolverStrategy, System,
|
||||
ConvergenceStatus, ConvergedState, FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig,
|
||||
Solver, SolverStrategy, System,
|
||||
};
|
||||
use petgraph::graph::NodeIndex;
|
||||
use std::cell::RefCell;
|
||||
@@ -70,9 +71,17 @@ impl WasmPicardConfig {
|
||||
self.inner.max_iterations = max;
|
||||
}
|
||||
|
||||
/// Set relaxation factor.
|
||||
/// Set relaxation factor. Must be in (0, 1]. Values outside this range are clamped.
|
||||
pub fn set_relaxation_factor(&mut self, omega: f64) {
|
||||
self.inner.relaxation_factor = omega;
|
||||
if omega.is_nan() || omega <= 0.0 {
|
||||
tracing::warn!("Invalid relaxation factor {}, clamping to 0.1", omega);
|
||||
self.inner.relaxation_factor = 0.1;
|
||||
} else if omega > 1.0 {
|
||||
tracing::warn!("Relaxation factor {} > 1.0, clamping to 1.0", omega);
|
||||
self.inner.relaxation_factor = 1.0;
|
||||
} else {
|
||||
self.inner.relaxation_factor = omega;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +95,7 @@ impl Default for WasmPicardConfig {
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WasmFallbackConfig {
|
||||
newton_config: NewtonConfig,
|
||||
picard_config: PicardConfig,
|
||||
inner: FallbackConfig,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -96,15 +104,23 @@ impl WasmFallbackConfig {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
WasmFallbackConfig {
|
||||
newton_config: NewtonConfig::default(),
|
||||
picard_config: PicardConfig::default(),
|
||||
inner: FallbackConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set timeout (placeholder for compatibility).
|
||||
pub fn timeout_ms(&mut self, _ms: u64) {
|
||||
// FallbackConfig currently doesn't have a direct timeout field in Rust
|
||||
// but it's used in the README example. We'll add this setter for API compatibility.
|
||||
/// Enable or disable automatic fallback to Picard on divergence.
|
||||
pub fn set_fallback_enabled(&mut self, enabled: bool) {
|
||||
self.inner.fallback_enabled = enabled;
|
||||
}
|
||||
|
||||
/// Set residual threshold for returning to Newton from Picard.
|
||||
pub fn set_return_to_newton_threshold(&mut self, threshold: f64) {
|
||||
self.inner.return_to_newton_threshold = threshold;
|
||||
}
|
||||
|
||||
/// Set maximum number of solver switches before staying on current solver.
|
||||
pub fn set_max_fallback_switches(&mut self, max: usize) {
|
||||
self.inner.max_fallback_switches = max;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +149,7 @@ impl WasmSystem {
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a component to the system.
|
||||
/// Add a component to the system. Returns the node index.
|
||||
pub fn add_component(&mut self, component: WasmComponent) -> usize {
|
||||
self.inner
|
||||
.borrow_mut()
|
||||
@@ -142,7 +158,28 @@ impl WasmSystem {
|
||||
}
|
||||
|
||||
/// Add an edge between components.
|
||||
///
|
||||
/// Returns an error if node indices are out of bounds.
|
||||
pub fn add_edge(&mut self, from_idx: usize, to_idx: usize) -> Result<(), JsValue> {
|
||||
let system = self.inner.borrow();
|
||||
let node_count = system.node_count();
|
||||
drop(system);
|
||||
|
||||
if from_idx >= node_count {
|
||||
return Err(js_sys::Error::new(&format!(
|
||||
"from_idx {} is out of bounds (system has {} nodes)",
|
||||
from_idx, node_count
|
||||
))
|
||||
.into());
|
||||
}
|
||||
if to_idx >= node_count {
|
||||
return Err(js_sys::Error::new(&format!(
|
||||
"to_idx {} is out of bounds (system has {} nodes)",
|
||||
to_idx, node_count
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
self.inner
|
||||
.borrow_mut()
|
||||
.add_edge(
|
||||
@@ -162,8 +199,8 @@ impl WasmSystem {
|
||||
}
|
||||
|
||||
/// Solve the system with fallback strategy.
|
||||
pub fn solve(&mut self, _config: WasmFallbackConfig) -> Result<WasmConvergedState, JsValue> {
|
||||
let mut solver = FallbackSolver::default_solver();
|
||||
pub fn solve(&mut self, config: WasmFallbackConfig) -> Result<WasmConvergedState, JsValue> {
|
||||
let mut solver = FallbackSolver::new(config.inner);
|
||||
let state = solver
|
||||
.solve(&mut *self.inner.borrow_mut())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()))?;
|
||||
@@ -197,6 +234,10 @@ impl WasmSystem {
|
||||
}
|
||||
|
||||
/// Get thermodynamic state for a specific node (after solve).
|
||||
///
|
||||
/// Uses the global TabularBackend to compute full thermodynamic properties
|
||||
/// (temperature, entropy, density, phase, quality, etc.) from the solver's
|
||||
/// pressure and enthalpy state.
|
||||
pub fn get_node_result(&self, node_idx: usize) -> Result<WasmThermoState, JsValue> {
|
||||
let system = self.inner.borrow();
|
||||
let state_ref = self.last_state.borrow();
|
||||
@@ -204,39 +245,45 @@ impl WasmSystem {
|
||||
js_sys::Error::new("System must be solved before calling get_node_result")
|
||||
})?;
|
||||
|
||||
// Use traverse_for_jacobian to find the component and its edge indices
|
||||
for (idx, component, edges) in system.traverse_for_jacobian() {
|
||||
if idx.index() == node_idx {
|
||||
if let Some((_edge_idx, p_idx, h_idx)) = edges.first() {
|
||||
if *p_idx >= state.len() || *h_idx >= state.len() {
|
||||
return Err(js_sys::Error::new(&format!(
|
||||
"State index out of bounds: p_idx={}, h_idx={}, state_len={}",
|
||||
p_idx, h_idx, state.len()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let p = state[*p_idx];
|
||||
let h = state[*h_idx];
|
||||
|
||||
// Simple heuristic to get the fluid: look at ports
|
||||
let ports = component.get_ports();
|
||||
let fluid_id = if !ports.is_empty() {
|
||||
entropyk_fluids::FluidId::new(ports[0].fluid_id().as_str())
|
||||
} else {
|
||||
entropyk_fluids::FluidId::new("R410A") // Fallback
|
||||
return Err(js_sys::Error::new(
|
||||
"Component has no ports — cannot determine fluid",
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
// In a real implementation, we would use the system's backend to resolve T and properties.
|
||||
// For now, we return a thermo state with P and h, which is what the user mostly needs.
|
||||
// The WasmThermoState::from implementation we fixed will handle the conversion.
|
||||
let thermo = entropyk_fluids::ThermoState {
|
||||
fluid: fluid_id,
|
||||
pressure: entropyk_core::Pressure::from_pascals(p),
|
||||
temperature: entropyk_core::Temperature::from_kelvin(300.0), // Placeholder
|
||||
enthalpy: entropyk_core::Enthalpy::from_joules_per_kg(h),
|
||||
entropy: entropyk_fluids::Entropy::from_joules_per_kg_kelvin(0.0),
|
||||
density: 1.0,
|
||||
phase: entropyk_fluids::Phase::Unknown,
|
||||
quality: None,
|
||||
superheat: None,
|
||||
subcooling: None,
|
||||
t_bubble: None,
|
||||
t_dew: None,
|
||||
};
|
||||
return Ok(thermo.into());
|
||||
let backend = global_backend();
|
||||
let thermo_state = backend
|
||||
.full_state(
|
||||
fluid_id,
|
||||
entropyk_core::Pressure::from_pascals(p),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(h),
|
||||
)
|
||||
.map_err(|e| {
|
||||
js_sys::Error::new(&format!(
|
||||
"Failed to compute thermodynamic state: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
return Ok(thermo_state.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,18 +301,18 @@ impl WasmSystem {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WasmSystem {
|
||||
fn default() -> Self {
|
||||
Self::new().expect("Failed to create default system")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ConvergedState> for WasmConvergedState {
|
||||
fn from(state: &ConvergedState) -> Self {
|
||||
let status_str = match state.status {
|
||||
ConvergenceStatus::Converged => "Converged",
|
||||
ConvergenceStatus::TimedOutWithBestState => "TimedOut",
|
||||
ConvergenceStatus::ControlSaturation => "ControlSaturation",
|
||||
};
|
||||
WasmConvergedState {
|
||||
converged: state.is_converged(),
|
||||
iterations: state.iterations,
|
||||
final_residual: state.final_residual,
|
||||
status: status_str.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,29 @@ pub struct WasmPressure {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmPressure {
|
||||
/// Create pressure from Pascals.
|
||||
/// Create pressure from Pascals. Rejects NaN and negative values.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(pascals: f64) -> Self {
|
||||
WasmPressure { pascals }
|
||||
pub fn new(pascals: f64) -> Result<WasmPressure, JsValue> {
|
||||
if pascals.is_nan() {
|
||||
return Err(js_sys::Error::new("Pressure cannot be NaN").into());
|
||||
}
|
||||
if pascals < 0.0 {
|
||||
return Err(js_sys::Error::new("Pressure cannot be negative").into());
|
||||
}
|
||||
Ok(WasmPressure { pascals })
|
||||
}
|
||||
|
||||
/// Create pressure from bar.
|
||||
pub fn from_bar(bar: f64) -> Self {
|
||||
WasmPressure {
|
||||
pascals: bar * 100_000.0,
|
||||
/// Create pressure from bar. Rejects NaN and negative values.
|
||||
pub fn from_bar(bar: f64) -> Result<Self, JsValue> {
|
||||
if bar.is_nan() {
|
||||
return Err(js_sys::Error::new("Pressure cannot be NaN").into());
|
||||
}
|
||||
if bar < 0.0 {
|
||||
return Err(js_sys::Error::new("Pressure cannot be negative").into());
|
||||
}
|
||||
Ok(WasmPressure {
|
||||
pascals: bar * 100_000.0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get pressure in Pascals.
|
||||
@@ -41,10 +53,7 @@ impl WasmPressure {
|
||||
|
||||
/// Convert to JSON string.
|
||||
pub fn toJson(&self) -> Result<String, JsValue> {
|
||||
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
|
||||
self.serialize(&serializer)
|
||||
.map(|v| v.as_string().unwrap_or_default())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
serde_json::to_string(self).map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,17 +80,21 @@ pub struct WasmTemperature {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmTemperature {
|
||||
/// Create temperature from Kelvin.
|
||||
/// Create temperature from Kelvin. Rejects NaN and negative values.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(kelvin: f64) -> Self {
|
||||
WasmTemperature { kelvin }
|
||||
pub fn new(kelvin: f64) -> Result<Self, JsValue> {
|
||||
if kelvin.is_nan() {
|
||||
return Err(js_sys::Error::new("Temperature cannot be NaN").into());
|
||||
}
|
||||
if kelvin < 0.0 {
|
||||
return Err(js_sys::Error::new("Temperature cannot be negative (Kelvin)").into());
|
||||
}
|
||||
Ok(WasmTemperature { kelvin })
|
||||
}
|
||||
|
||||
/// Create temperature from Celsius.
|
||||
pub fn from_celsius(celsius: f64) -> Self {
|
||||
WasmTemperature {
|
||||
kelvin: celsius + 273.15,
|
||||
}
|
||||
pub fn from_celsius(celsius: f64) -> Result<Self, JsValue> {
|
||||
Self::new(celsius + 273.15)
|
||||
}
|
||||
|
||||
/// Get temperature in Kelvin.
|
||||
@@ -96,10 +109,7 @@ impl WasmTemperature {
|
||||
|
||||
/// Convert to JSON string.
|
||||
pub fn toJson(&self) -> Result<String, JsValue> {
|
||||
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
|
||||
self.serialize(&serializer)
|
||||
.map(|v| v.as_string().unwrap_or_default())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
serde_json::to_string(self).map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,17 +136,18 @@ pub struct WasmEnthalpy {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmEnthalpy {
|
||||
/// Create enthalpy from J/kg.
|
||||
/// Create enthalpy from J/kg. Rejects NaN.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(joules_per_kg: f64) -> Self {
|
||||
WasmEnthalpy { joules_per_kg }
|
||||
pub fn new(joules_per_kg: f64) -> Result<Self, JsValue> {
|
||||
if joules_per_kg.is_nan() {
|
||||
return Err(js_sys::Error::new("Enthalpy cannot be NaN").into());
|
||||
}
|
||||
Ok(WasmEnthalpy { joules_per_kg })
|
||||
}
|
||||
|
||||
/// Create enthalpy from kJ/kg.
|
||||
pub fn from_kj_per_kg(kj_per_kg: f64) -> Self {
|
||||
WasmEnthalpy {
|
||||
joules_per_kg: kj_per_kg * 1000.0,
|
||||
}
|
||||
pub fn from_kj_per_kg(kj_per_kg: f64) -> Result<Self, JsValue> {
|
||||
Self::new(kj_per_kg * 1000.0)
|
||||
}
|
||||
|
||||
/// Get enthalpy in J/kg.
|
||||
@@ -151,10 +162,7 @@ impl WasmEnthalpy {
|
||||
|
||||
/// Convert to JSON string.
|
||||
pub fn toJson(&self) -> Result<String, JsValue> {
|
||||
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
|
||||
self.serialize(&serializer)
|
||||
.map(|v| v.as_string().unwrap_or_default())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
serde_json::to_string(self).map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,10 +189,13 @@ pub struct WasmMassFlow {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmMassFlow {
|
||||
/// Create mass flow from kg/s.
|
||||
/// Create mass flow from kg/s. Rejects NaN.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(kg_per_s: f64) -> Self {
|
||||
WasmMassFlow { kg_per_s }
|
||||
pub fn new(kg_per_s: f64) -> Result<Self, JsValue> {
|
||||
if kg_per_s.is_nan() {
|
||||
return Err(js_sys::Error::new("Mass flow cannot be NaN").into());
|
||||
}
|
||||
Ok(WasmMassFlow { kg_per_s })
|
||||
}
|
||||
|
||||
/// Get mass flow in kg/s.
|
||||
@@ -194,10 +205,7 @@ impl WasmMassFlow {
|
||||
|
||||
/// Convert to JSON string.
|
||||
pub fn toJson(&self) -> Result<String, JsValue> {
|
||||
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
|
||||
self.serialize(&serializer)
|
||||
.map(|v| v.as_string().unwrap_or_default())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
serde_json::to_string(self).map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,11 +225,13 @@ impl From<WasmMassFlow> for MassFlow {
|
||||
|
||||
/// WASM wrapper for thermodynamic state (result).
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct WasmThermoState {
|
||||
pub pressure: WasmPressure,
|
||||
pub temperature: WasmTemperature,
|
||||
pub enthalpy: WasmEnthalpy,
|
||||
/// Mass flow is not carried by ThermoState upstream; only populated
|
||||
/// when available from the solver context.
|
||||
pub mass_flow: WasmMassFlow,
|
||||
}
|
||||
|
||||
@@ -229,47 +239,62 @@ pub struct WasmThermoState {
|
||||
impl WasmThermoState {
|
||||
/// Convert to JSON string.
|
||||
pub fn toJson(&self) -> Result<String, JsValue> {
|
||||
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
|
||||
self.serialize(&serializer)
|
||||
.map(|v| v.as_string().unwrap_or_default())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
serde_json::to_string(self).map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<entropyk_fluids::ThermoState> for WasmThermoState {
|
||||
fn from(s: entropyk_fluids::ThermoState) -> Self {
|
||||
WasmThermoState {
|
||||
pressure: WasmPressure::new(s.pressure.to_pascals()),
|
||||
temperature: WasmTemperature::new(s.temperature.to_kelvin()),
|
||||
enthalpy: WasmEnthalpy::new(s.enthalpy.to_joules_per_kg()),
|
||||
mass_flow: WasmMassFlow::new(0.0),
|
||||
pressure: WasmPressure::from(s.pressure),
|
||||
temperature: WasmTemperature::from(s.temperature),
|
||||
enthalpy: WasmEnthalpy::from(s.enthalpy),
|
||||
mass_flow: WasmMassFlow { kg_per_s: 0.0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM wrapper for converged state (solver result).
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WasmConvergedState {
|
||||
/// Convergence status
|
||||
/// Whether the solver converged
|
||||
pub converged: bool,
|
||||
/// Number of iterations
|
||||
pub iterations: usize,
|
||||
/// Final residual
|
||||
/// Final residual norm
|
||||
pub final_residual: f64,
|
||||
pub(crate) status: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmConvergedState {
|
||||
/// Get solver status: "Converged", "TimedOut", "ControlSaturation", or "Unknown".
|
||||
pub fn status(&self) -> String {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
/// Convert to JSON string.
|
||||
pub fn toJson(&self) -> Result<String, JsValue> {
|
||||
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
|
||||
self.serialize(&serializer)
|
||||
.map(|v| v.as_string().unwrap_or_default())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
serde_json::to_string(&SerializableConvergedState {
|
||||
converged: self.converged,
|
||||
iterations: self.iterations,
|
||||
final_residual: self.final_residual,
|
||||
status: &self.status,
|
||||
})
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializable representation of WasmConvergedState.
|
||||
#[derive(Serialize)]
|
||||
struct SerializableConvergedState<'a> {
|
||||
converged: bool,
|
||||
iterations: usize,
|
||||
final_residual: f64,
|
||||
status: &'a str,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -277,28 +302,43 @@ mod tests {
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_pressure_creation() {
|
||||
let p = WasmPressure::from_bar(1.0);
|
||||
let p = WasmPressure::from_bar(1.0).unwrap();
|
||||
assert!((p.pascals() - 100000.0).abs() < 1e-6);
|
||||
assert!((p.bar() - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_pressure_rejects_negative() {
|
||||
assert!(WasmPressure::new(-1.0).is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_pressure_rejects_nan() {
|
||||
assert!(WasmPressure::new(f64::NAN).is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_temperature_creation() {
|
||||
let t = WasmTemperature::from_celsius(25.0);
|
||||
let t = WasmTemperature::from_celsius(25.0).unwrap();
|
||||
assert!((t.kelvin() - 298.15).abs() < 1e-6);
|
||||
assert!((t.celsius() - 25.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_temperature_rejects_negative() {
|
||||
assert!(WasmTemperature::new(-1.0).is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_enthalpy_creation() {
|
||||
let h = WasmEnthalpy::from_kj_per_kg(400.0);
|
||||
let h = WasmEnthalpy::from_kj_per_kg(400.0).unwrap();
|
||||
assert!((h.joules_per_kg() - 400000.0).abs() < 1e-6);
|
||||
assert!((h.kj_per_kg() - 400.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_massflow_creation() {
|
||||
let m = WasmMassFlow::new(0.1);
|
||||
let m = WasmMassFlow::new(0.1).unwrap();
|
||||
assert!((m.kg_per_s() - 0.1).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,19 @@ async function main() {
|
||||
const system = new WasmSystem();
|
||||
console.log('System created');
|
||||
|
||||
// Add components
|
||||
// coeffs: m1..m10
|
||||
// Create components with correct API signatures
|
||||
const compressor = new WasmCompressor(
|
||||
0.85, 2.5, 500.0, 1500.0, -2.5,
|
||||
1.8, 600.0, 1600.0, -3.0, 2.0
|
||||
"R134a", // fluid
|
||||
2900.0, // speed_rpm
|
||||
0.0001, // displacement_m3_per_rev
|
||||
0.85, // efficiency
|
||||
0.85, 2.5, 500.0, 1500.0, -2.5, // AHRI m1-m5
|
||||
1.8, 600.0, 1600.0, -3.0, 2.0 // AHRI m6-m10
|
||||
).into_component();
|
||||
|
||||
const condenser = new WasmCondenser(5000.0).into_component();
|
||||
const evaporator = new WasmEvaporator(3000.0).into_component();
|
||||
const valve = new WasmExpansionValve().into_component();
|
||||
const valve = new WasmExpansionValve("R134a", 0.0).into_component();
|
||||
|
||||
const cIdx = system.add_component(compressor);
|
||||
const condIdx = system.add_component(condenser);
|
||||
@@ -69,16 +73,22 @@ async function main() {
|
||||
|
||||
if (result.converged) {
|
||||
console.log('Convergence achieved in', result.iterations, 'iterations');
|
||||
console.log('Status:', result.status);
|
||||
|
||||
// Extract result for a node
|
||||
const state = system.get_node_result(0);
|
||||
console.log('Node 0 state:', state.toJson());
|
||||
} else {
|
||||
console.error('System failed to converge');
|
||||
// This is expected if the simple setup without boundary conditions is unstable,
|
||||
// but it verifies the API pipeline.
|
||||
console.error('System did not converge, status:', result.status);
|
||||
}
|
||||
|
||||
// Test input validation
|
||||
console.log('\n--- Input validation tests ---');
|
||||
try { new WasmPressure(-1.0); console.error('FAIL: negative pressure accepted'); } catch(e) { console.log('OK: negative pressure rejected'); }
|
||||
try { new WasmTemperature(-1.0); console.error('FAIL: negative temperature accepted'); } catch(e) { console.log('OK: negative temperature rejected'); }
|
||||
try { new WasmCondenser(-100.0); console.error('FAIL: negative UA accepted'); } catch(e) { console.log('OK: negative UA rejected'); }
|
||||
try { new WasmCondenser(0.0); console.error('FAIL: zero UA accepted'); } catch(e) { console.log('OK: zero UA rejected'); }
|
||||
|
||||
console.log('\nWASM Integration Test PASSED (API verification complete)');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user