Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled
Some checks failed
CI / check (push) Has been cancelled
Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -107,6 +107,7 @@ impl From<&EntropykPicardConfig> for entropyk_solver::PicardConfig {
|
||||
|
||||
/// Configuration for the fallback solver (Newton → Picard).
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
pub struct EntropykFallbackConfig {
|
||||
/// Newton solver configuration.
|
||||
pub newton: EntropykNewtonConfig,
|
||||
@@ -114,15 +115,6 @@ pub struct EntropykFallbackConfig {
|
||||
pub picard: EntropykPicardConfig,
|
||||
}
|
||||
|
||||
impl Default for EntropykFallbackConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
newton: EntropykNewtonConfig::default(),
|
||||
picard: EntropykPicardConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque handle to a solver result.
|
||||
///
|
||||
/// Create via `entropyk_solve_*()` functions.
|
||||
|
||||
@@ -28,12 +28,11 @@ pub fn load_model() -> Result<(String, String), String> {
|
||||
|
||||
let model_path = env::var("ENTROPYK_FMU_MODEL")
|
||||
.map_err(|_| "no embedded model and ENTROPYK_FMU_MODEL env var not set".to_string())?;
|
||||
let io_path = env::var("ENTROPYK_FMU_IO")
|
||||
.map_err(|_| "ENTROPYK_FMU_IO env var not set".to_string())?;
|
||||
let io_path =
|
||||
env::var("ENTROPYK_FMU_IO").map_err(|_| "ENTROPYK_FMU_IO env var not set".to_string())?;
|
||||
|
||||
let model = std::fs::read_to_string(&model_path)
|
||||
.map_err(|e| format!("read {model_path}: {e}"))?;
|
||||
let io = std::fs::read_to_string(&io_path)
|
||||
.map_err(|e| format!("read {io_path}: {e}"))?;
|
||||
let model =
|
||||
std::fs::read_to_string(&model_path).map_err(|e| format!("read {model_path}: {e}"))?;
|
||||
let io = std::fs::read_to_string(&io_path).map_err(|e| format!("read {io_path}: {e}"))?;
|
||||
Ok((model, io))
|
||||
}
|
||||
|
||||
@@ -350,4 +350,3 @@ pub unsafe extern "C" fn fmi2GetLastError() -> fmi2String {
|
||||
None => std::ptr::null(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
#![allow(clippy::const_is_empty)]
|
||||
|
||||
pub mod embedded;
|
||||
pub mod fmi2;
|
||||
|
||||
@@ -58,8 +58,8 @@ impl FmuInstance {
|
||||
/// Parse the model JSON and the IO-map JSON. Both are bundled inside the
|
||||
/// `.fmu` (model under `resources/`, IO-map under `resources/fmu_io.json`).
|
||||
pub fn new(config_json: &str, io_json: &str) -> Result<Self, String> {
|
||||
let config = ScenarioConfig::from_json(config_json)
|
||||
.map_err(|e| format!("model JSON: {e}"))?;
|
||||
let config =
|
||||
ScenarioConfig::from_json(config_json).map_err(|e| format!("model JSON: {e}"))?;
|
||||
let io: FmuIoSpec =
|
||||
serde_json::from_str(io_json).map_err(|e| format!("IO-map JSON: {e}"))?;
|
||||
|
||||
@@ -201,10 +201,12 @@ fn extract_output(result: &SimulationResult, out: &IoOutput) -> f64 {
|
||||
"q_cooling_kw" => perf.and_then(|p| p.q_cooling_kw).unwrap_or(f64::NAN),
|
||||
"q_heating_kw" => perf.and_then(|p| p.q_heating_kw).unwrap_or(f64::NAN),
|
||||
"compressor_power_kw" => perf.and_then(|p| p.compressor_power_kw).unwrap_or(f64::NAN),
|
||||
"pressure_bar" => find_edge(result, out.edge).map(|e| e.pressure_bar).unwrap_or(f64::NAN),
|
||||
"enthalpy_kj_kg" => {
|
||||
find_edge(result, out.edge).map(|e| e.enthalpy_kj_kg).unwrap_or(f64::NAN)
|
||||
}
|
||||
"pressure_bar" => find_edge(result, out.edge)
|
||||
.map(|e| e.pressure_bar)
|
||||
.unwrap_or(f64::NAN),
|
||||
"enthalpy_kj_kg" => find_edge(result, out.edge)
|
||||
.map(|e| e.enthalpy_kj_kg)
|
||||
.unwrap_or(f64::NAN),
|
||||
"mass_flow_kg_s" => find_edge(result, out.edge)
|
||||
.and_then(|e| e.mass_flow_kg_s)
|
||||
.unwrap_or(f64::NAN),
|
||||
@@ -215,7 +217,10 @@ fn extract_output(result: &SimulationResult, out: &IoOutput) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_edge<'a>(result: &'a SimulationResult, edge: Option<usize>) -> Option<&'a entropyk_cli::run::StateEntry> {
|
||||
fn find_edge(
|
||||
result: &SimulationResult,
|
||||
edge: Option<usize>,
|
||||
) -> Option<&entropyk_cli::run::StateEntry> {
|
||||
let edge = edge?;
|
||||
result.state.as_ref()?.iter().find(|e| e.edge == edge)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
|
||||
};
|
||||
use entropyk_components::{Component, ConnectedPort};
|
||||
|
||||
// =============================================================================
|
||||
// Compressor - Real AHRI 540 Implementation
|
||||
@@ -117,6 +115,149 @@ impl PyCompressor {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ScrewEconomizerCompressor - 3-port screw compressor with economizer
|
||||
// =============================================================================
|
||||
|
||||
/// A screw compressor with an intermediate economizer injection port.
|
||||
#[pyclass(name = "ScrewEconomizerCompressor", module = "entropyk")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyScrewEconomizerCompressor {
|
||||
pub(crate) fluid: String,
|
||||
pub(crate) nominal_frequency_hz: f64,
|
||||
pub(crate) frequency_hz: f64,
|
||||
pub(crate) mechanical_efficiency: f64,
|
||||
pub(crate) economizer_fraction: f64,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyScrewEconomizerCompressor {
|
||||
/// Create a screw compressor with a fixed economizer flow fraction.
|
||||
#[new]
|
||||
#[pyo3(signature = (
|
||||
fluid="R134a",
|
||||
nominal_frequency_hz=50.0,
|
||||
frequency_hz=50.0,
|
||||
mechanical_efficiency=0.92,
|
||||
economizer_fraction=0.12
|
||||
))]
|
||||
fn new(
|
||||
fluid: &str,
|
||||
nominal_frequency_hz: f64,
|
||||
frequency_hz: f64,
|
||||
mechanical_efficiency: f64,
|
||||
economizer_fraction: f64,
|
||||
) -> PyResult<Self> {
|
||||
if nominal_frequency_hz <= 0.0 {
|
||||
return Err(PyValueError::new_err(
|
||||
"nominal_frequency_hz must be positive",
|
||||
));
|
||||
}
|
||||
if frequency_hz < 0.0 {
|
||||
return Err(PyValueError::new_err("frequency_hz cannot be negative"));
|
||||
}
|
||||
if !(0.0..=1.0).contains(&mechanical_efficiency) || mechanical_efficiency == 0.0 {
|
||||
return Err(PyValueError::new_err(
|
||||
"mechanical_efficiency must be in (0.0, 1.0]",
|
||||
));
|
||||
}
|
||||
if !(0.0..=0.4).contains(&economizer_fraction) {
|
||||
return Err(PyValueError::new_err(
|
||||
"economizer_fraction must be between 0.0 and 0.4",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
fluid: fluid.to_string(),
|
||||
nominal_frequency_hz,
|
||||
frequency_hz,
|
||||
mechanical_efficiency,
|
||||
economizer_fraction,
|
||||
})
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn fluid(&self) -> String {
|
||||
self.fluid.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn frequency_hz(&self) -> f64 {
|
||||
self.frequency_hz
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_frequency_hz(&mut self, value: f64) -> PyResult<()> {
|
||||
if value < 0.0 {
|
||||
return Err(PyValueError::new_err("frequency_hz cannot be negative"));
|
||||
}
|
||||
self.frequency_hz = value;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn mechanical_efficiency(&self) -> f64 {
|
||||
self.mechanical_efficiency
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn economizer_fraction(&self) -> f64 {
|
||||
self.economizer_fraction
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"ScrewEconomizerCompressor(fluid={}, frequency={:.1} Hz, eco_fraction={:.3})",
|
||||
self.fluid, self.frequency_hz, self.economizer_fraction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PyScrewEconomizerCompressor {
|
||||
pub(crate) fn build(&self) -> Box<dyn Component> {
|
||||
use entropyk_components::port::{FluidId, Port};
|
||||
use entropyk_components::{
|
||||
Polynomial2D, ScrewEconomizerCompressor, ScrewPerformanceCurves,
|
||||
};
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
|
||||
fn connected_port(fluid: &str, pressure_bar: f64, enthalpy_kj_kg: f64) -> ConnectedPort {
|
||||
let a = Port::new(
|
||||
FluidId::new(fluid),
|
||||
Pressure::from_bar(pressure_bar),
|
||||
Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
|
||||
);
|
||||
let b = Port::new(
|
||||
FluidId::new(fluid),
|
||||
Pressure::from_bar(pressure_bar),
|
||||
Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
|
||||
);
|
||||
a.connect(b)
|
||||
.expect("matching screw compressor ports should connect")
|
||||
.0
|
||||
}
|
||||
|
||||
let curves = ScrewPerformanceCurves::with_fixed_eco_fraction(
|
||||
Polynomial2D::bilinear(1.2, 0.003, -0.002, 0.000_01),
|
||||
Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5),
|
||||
self.economizer_fraction,
|
||||
);
|
||||
let mut component = ScrewEconomizerCompressor::new(
|
||||
curves,
|
||||
&self.fluid,
|
||||
self.nominal_frequency_hz,
|
||||
self.mechanical_efficiency,
|
||||
connected_port(&self.fluid, 3.2, 400.0),
|
||||
connected_port(&self.fluid, 12.8, 440.0),
|
||||
connected_port(&self.fluid, 6.4, 260.0),
|
||||
)
|
||||
.expect("validated screw compressor parameters should build");
|
||||
component
|
||||
.set_frequency_hz(self.frequency_hz)
|
||||
.expect("validated screw compressor frequency should set");
|
||||
Box::new(component)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Condenser - Real Heat Exchanger with Water Side
|
||||
// =============================================================================
|
||||
@@ -717,7 +858,11 @@ impl PyRefrigerantSource {
|
||||
|
||||
impl PyRefrigerantSource {
|
||||
pub(crate) fn build(&self) -> Box<dyn Component> {
|
||||
Box::new(entropyk_components::PyRefrigerantSourceReal::new(&self.fluid, self.p_set_pa, self.quality))
|
||||
Box::new(entropyk_components::PyRefrigerantSourceReal::new(
|
||||
&self.fluid,
|
||||
self.p_set_pa,
|
||||
self.quality,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,7 +905,11 @@ impl PyRefrigerantSink {
|
||||
|
||||
impl PyRefrigerantSink {
|
||||
pub(crate) fn build(&self) -> Box<dyn Component> {
|
||||
Box::new(entropyk_components::PyRefrigerantSinkReal::new(&self.fluid, self.p_back_pa, self.quality_opt))
|
||||
Box::new(entropyk_components::PyRefrigerantSinkReal::new(
|
||||
&self.fluid,
|
||||
self.p_back_pa,
|
||||
self.quality_opt,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,7 +931,12 @@ pub struct PyBrineSource {
|
||||
impl PyBrineSource {
|
||||
#[new]
|
||||
#[pyo3(signature = (fluid="Water", concentration=0.0, temperature_k=300.0, pressure_pa=101325.0))]
|
||||
fn new(fluid: &str, concentration: f64, temperature_k: f64, pressure_pa: f64) -> PyResult<Self> {
|
||||
fn new(
|
||||
fluid: &str,
|
||||
concentration: f64,
|
||||
temperature_k: f64,
|
||||
pressure_pa: f64,
|
||||
) -> PyResult<Self> {
|
||||
if pressure_pa <= 0.0 {
|
||||
return Err(PyValueError::new_err("pressure_pa must be positive"));
|
||||
}
|
||||
@@ -790,7 +944,9 @@ impl PyBrineSource {
|
||||
return Err(PyValueError::new_err("temperature_k must be positive"));
|
||||
}
|
||||
if !(0.0..=1.0).contains(&concentration) {
|
||||
return Err(PyValueError::new_err("concentration must be between 0.0 and 1.0"));
|
||||
return Err(PyValueError::new_err(
|
||||
"concentration must be between 0.0 and 1.0",
|
||||
));
|
||||
}
|
||||
Ok(PyBrineSource {
|
||||
fluid: fluid.to_string(),
|
||||
@@ -810,7 +966,12 @@ impl PyBrineSource {
|
||||
|
||||
impl PyBrineSource {
|
||||
pub(crate) fn build(&self) -> Box<dyn Component> {
|
||||
Box::new(entropyk_components::PyBrineSourceReal::new(&self.fluid, self.concentration, self.temperature_k, self.pressure_pa))
|
||||
Box::new(entropyk_components::PyBrineSourceReal::new(
|
||||
&self.fluid,
|
||||
self.concentration,
|
||||
self.temperature_k,
|
||||
self.pressure_pa,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,7 +1029,9 @@ impl PyAirSource {
|
||||
return Err(PyValueError::new_err("temperature_k must be positive"));
|
||||
}
|
||||
if !(0.0..=1.0).contains(&relative_humidity) {
|
||||
return Err(PyValueError::new_err("relative_humidity must be between 0.0 and 1.0"));
|
||||
return Err(PyValueError::new_err(
|
||||
"relative_humidity must be between 0.0 and 1.0",
|
||||
));
|
||||
}
|
||||
Ok(PyAirSource {
|
||||
temperature_k,
|
||||
@@ -887,7 +1050,11 @@ impl PyAirSource {
|
||||
|
||||
impl PyAirSource {
|
||||
pub(crate) fn build(&self) -> Box<dyn Component> {
|
||||
Box::new(entropyk_components::PyAirSourceReal::new(self.temperature_k, self.relative_humidity, self.pressure_pa))
|
||||
Box::new(entropyk_components::PyAirSourceReal::new(
|
||||
self.temperature_k,
|
||||
self.relative_humidity,
|
||||
self.pressure_pa,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,6 +1087,122 @@ impl PyAirSink {
|
||||
}
|
||||
}
|
||||
|
||||
/// Capillary tube (1D segmented ΔP, isenthalpic).
|
||||
#[pyclass(name = "CapillaryTube", module = "entropyk")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyCapillaryTube {
|
||||
fluid: String,
|
||||
diameter_m: f64,
|
||||
length_m: f64,
|
||||
n_segments: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCapillaryTube {
|
||||
#[new]
|
||||
#[pyo3(signature = (fluid="R134a", diameter_m=0.001, length_m=2.0, n_segments=20))]
|
||||
fn new(fluid: &str, diameter_m: f64, length_m: f64, n_segments: usize) -> PyResult<Self> {
|
||||
if diameter_m <= 0.0 || length_m <= 0.0 || n_segments < 2 {
|
||||
return Err(PyValueError::new_err("invalid capillary geometry"));
|
||||
}
|
||||
Ok(Self {
|
||||
fluid: fluid.to_string(),
|
||||
diameter_m,
|
||||
length_m,
|
||||
n_segments,
|
||||
})
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"CapillaryTube(D={:.3}mm, L={:.2}m)",
|
||||
self.diameter_m * 1000.0,
|
||||
self.length_m
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PyCapillaryTube {
|
||||
pub(crate) fn build(&self) -> Box<dyn Component> {
|
||||
use entropyk_components::port::{FluidId, Port};
|
||||
use entropyk_components::{CapillaryGeometry, CapillaryTube};
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
let geom = CapillaryGeometry {
|
||||
diameter_m: self.diameter_m,
|
||||
length_m: self.length_m,
|
||||
n_segments: self.n_segments,
|
||||
};
|
||||
let p = Pressure::from_bar(10.0);
|
||||
let h = Enthalpy::from_joules_per_kg(250_000.0);
|
||||
let fid = FluidId::new(&self.fluid);
|
||||
let tube = CapillaryTube::new(
|
||||
geom,
|
||||
Port::new(fid.clone(), p, h),
|
||||
Port::new(fid.clone(), p, h),
|
||||
)
|
||||
.and_then(|c| c.connect(Port::new(fid.clone(), p, h), Port::new(fid, p, h)))
|
||||
.expect("CapillaryTube construction");
|
||||
Box::new(tube)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fan-coil unit rating (ε-NTU + BPF).
|
||||
#[pyclass(name = "FanCoilUnit", module = "entropyk")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyFanCoilUnit {
|
||||
ua: f64,
|
||||
bpf: f64,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFanCoilUnit {
|
||||
#[new]
|
||||
#[pyo3(signature = (ua=8000.0, bypass_factor=0.15))]
|
||||
fn new(ua: f64, bypass_factor: f64) -> Self {
|
||||
Self {
|
||||
ua,
|
||||
bpf: bypass_factor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Rate total / sensible / latent capacity [W] and leaving air temp [K].
|
||||
#[pyo3(signature = (
|
||||
t_air_in_k, t_dew_in_k, t_water_in_k, c_air, c_water, t_adp_k
|
||||
))]
|
||||
fn rate(
|
||||
&self,
|
||||
t_air_in_k: f64,
|
||||
t_dew_in_k: f64,
|
||||
t_water_in_k: f64,
|
||||
c_air: f64,
|
||||
c_water: f64,
|
||||
t_adp_k: f64,
|
||||
) -> (f64, f64, f64, f64, f64) {
|
||||
use entropyk_components::heat_exchanger::{FanCoilRatingInput, FanCoilUnit};
|
||||
let fcu = FanCoilUnit::new(self.ua).with_bypass_factor(self.bpf);
|
||||
let r = fcu.rate(&FanCoilRatingInput {
|
||||
t_air_in_k,
|
||||
t_dew_in_k,
|
||||
t_water_in_k,
|
||||
c_air,
|
||||
c_water,
|
||||
bypass_factor: self.bpf,
|
||||
t_adp_k,
|
||||
});
|
||||
(
|
||||
r.q_total_w,
|
||||
r.q_sensible_w,
|
||||
r.q_latent_w,
|
||||
r.t_air_out_k,
|
||||
r.shr,
|
||||
)
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("FanCoilUnit(UA={:.0}, BPF={:.2})", self.ua, self.bpf)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Component enum for type-erasure
|
||||
// =============================================================================
|
||||
@@ -928,6 +1211,7 @@ impl PyAirSink {
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum AnyPyComponent {
|
||||
Compressor(PyCompressor),
|
||||
ScrewEconomizerCompressor(PyScrewEconomizerCompressor),
|
||||
Condenser(PyCondenser),
|
||||
Evaporator(PyEvaporator),
|
||||
Economizer(PyEconomizer),
|
||||
@@ -945,6 +1229,7 @@ pub(crate) enum AnyPyComponent {
|
||||
BrineSink(PyBrineSink),
|
||||
AirSource(PyAirSource),
|
||||
AirSink(PyAirSink),
|
||||
CapillaryTube(PyCapillaryTube),
|
||||
}
|
||||
|
||||
impl AnyPyComponent {
|
||||
@@ -952,6 +1237,7 @@ impl AnyPyComponent {
|
||||
pub(crate) fn build(&self) -> Box<dyn Component> {
|
||||
match self {
|
||||
AnyPyComponent::Compressor(c) => c.build(),
|
||||
AnyPyComponent::ScrewEconomizerCompressor(c) => c.build(),
|
||||
AnyPyComponent::Condenser(c) => c.build(),
|
||||
AnyPyComponent::Evaporator(c) => c.build(),
|
||||
AnyPyComponent::Economizer(c) => c.build(),
|
||||
@@ -969,6 +1255,7 @@ impl AnyPyComponent {
|
||||
AnyPyComponent::BrineSink(c) => c.build(),
|
||||
AnyPyComponent::AirSource(c) => c.build(),
|
||||
AnyPyComponent::AirSink(c) => c.build(),
|
||||
AnyPyComponent::CapillaryTube(c) => c.build(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
//! This crate provides Python wrappers for the Entropyk thermodynamic
|
||||
//! simulation library via PyO3 + Maturin.
|
||||
|
||||
// Pre-existing lint debt accumulated before CI was introduced.
|
||||
#![allow(deprecated)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub(crate) mod components;
|
||||
@@ -28,6 +33,7 @@ fn entropyk(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
|
||||
// Components
|
||||
m.add_class::<components::PyCompressor>()?;
|
||||
m.add_class::<components::PyScrewEconomizerCompressor>()?;
|
||||
m.add_class::<components::PyCondenser>()?;
|
||||
m.add_class::<components::PyEvaporator>()?;
|
||||
m.add_class::<components::PyEconomizer>()?;
|
||||
@@ -45,6 +51,8 @@ fn entropyk(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<components::PyBrineSink>()?;
|
||||
m.add_class::<components::PyAirSource>()?;
|
||||
m.add_class::<components::PyAirSink>()?;
|
||||
m.add_class::<components::PyCapillaryTube>()?;
|
||||
m.add_class::<components::PyFanCoilUnit>()?;
|
||||
m.add_class::<components::PyOperationalState>()?;
|
||||
|
||||
// Solver
|
||||
|
||||
@@ -249,6 +249,9 @@ fn extract_component(obj: &Bound<'_, PyAny>) -> PyResult<AnyPyComponent> {
|
||||
if let Ok(c) = obj.extract::<crate::components::PyCompressor>() {
|
||||
return Ok(AnyPyComponent::Compressor(c));
|
||||
}
|
||||
if let Ok(c) = obj.extract::<crate::components::PyScrewEconomizerCompressor>() {
|
||||
return Ok(AnyPyComponent::ScrewEconomizerCompressor(c));
|
||||
}
|
||||
if let Ok(c) = obj.extract::<crate::components::PyCondenser>() {
|
||||
return Ok(AnyPyComponent::Condenser(c));
|
||||
}
|
||||
@@ -301,7 +304,7 @@ fn extract_component(obj: &Bound<'_, PyAny>) -> PyResult<AnyPyComponent> {
|
||||
return Ok(AnyPyComponent::AirSink(c));
|
||||
}
|
||||
Err(PyValueError::new_err(
|
||||
"Expected a component (Compressor, Condenser, Evaporator, ExpansionValve, Pipe, Pump, Fan, Economizer, FlowSplitter, FlowMerger, FlowSource, FlowSink, RefrigerantSource, RefrigerantSink, BrineSource, BrineSink, AirSource, AirSink)",
|
||||
"Expected a component (Compressor, ScrewEconomizerCompressor, Condenser, Evaporator, ExpansionValve, Pipe, Pump, Fan, Economizer, FlowSplitter, FlowMerger, FlowSource, FlowSink, RefrigerantSource, RefrigerantSink, BrineSource, BrineSink, AirSource, AirSink)",
|
||||
))
|
||||
}
|
||||
|
||||
@@ -519,7 +522,9 @@ impl PyNewtonConfig {
|
||||
return Err(PyValueError::new_err("tolerance must be greater than 0"));
|
||||
}
|
||||
if divergence_threshold <= tolerance {
|
||||
return Err(PyValueError::new_err("divergence_threshold must be greater than tolerance"));
|
||||
return Err(PyValueError::new_err(
|
||||
"divergence_threshold must be greater than tolerance",
|
||||
));
|
||||
}
|
||||
Ok(PyNewtonConfig {
|
||||
max_iterations,
|
||||
@@ -565,7 +570,10 @@ impl PyNewtonConfig {
|
||||
previous_state: self.previous_state.clone(),
|
||||
previous_residual: None,
|
||||
initial_state: self.initial_state.clone(),
|
||||
convergence_criteria: self.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
|
||||
convergence_criteria: self
|
||||
.convergence_criteria
|
||||
.as_ref()
|
||||
.map(|cc| cc.inner.clone()),
|
||||
jacobian_freezing: self.jacobian_freezing.as_ref().map(|jf| jf.inner.clone()),
|
||||
verbose_config: Default::default(),
|
||||
};
|
||||
@@ -631,8 +639,8 @@ impl PyPicardConfig {
|
||||
convergence_criteria=None
|
||||
))]
|
||||
fn new(
|
||||
max_iterations: usize,
|
||||
tolerance: f64,
|
||||
max_iterations: usize,
|
||||
tolerance: f64,
|
||||
relaxation: f64,
|
||||
initial_state: Option<Vec<f64>>,
|
||||
timeout_ms: Option<u64>,
|
||||
@@ -675,7 +683,10 @@ impl PyPicardConfig {
|
||||
relaxation_factor: self.relaxation,
|
||||
timeout: self.timeout_ms.map(Duration::from_millis),
|
||||
initial_state: self.initial_state.clone(),
|
||||
convergence_criteria: self.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
|
||||
convergence_criteria: self
|
||||
.convergence_criteria
|
||||
.as_ref()
|
||||
.map(|cc| cc.inner.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -725,8 +736,14 @@ impl PyFallbackConfig {
|
||||
#[pyo3(signature = (newton=None, picard=None))]
|
||||
fn new(newton: Option<PyNewtonConfig>, picard: Option<PyPicardConfig>) -> PyResult<Self> {
|
||||
Ok(PyFallbackConfig {
|
||||
newton: newton.unwrap_or_else(|| PyNewtonConfig::new(100, 1e-6, false, None, None, false, None, None, None, None, 1e-4, 20, 1e10).unwrap()),
|
||||
picard: picard.unwrap_or_else(|| PyPicardConfig::new(500, 1e-4, 0.5, None, None, None).unwrap()),
|
||||
newton: newton.unwrap_or_else(|| {
|
||||
PyNewtonConfig::new(
|
||||
100, 1e-6, false, None, None, false, None, None, None, None, 1e-4, 20, 1e10,
|
||||
)
|
||||
.unwrap()
|
||||
}),
|
||||
picard: picard
|
||||
.unwrap_or_else(|| PyPicardConfig::new(500, 1e-4, 0.5, None, None, None).unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -756,8 +773,16 @@ impl PyFallbackConfig {
|
||||
previous_state: self.newton.previous_state.clone(),
|
||||
previous_residual: None,
|
||||
initial_state: self.newton.initial_state.clone(),
|
||||
convergence_criteria: self.newton.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
|
||||
jacobian_freezing: self.newton.jacobian_freezing.as_ref().map(|jf| jf.inner.clone()),
|
||||
convergence_criteria: self
|
||||
.newton
|
||||
.convergence_criteria
|
||||
.as_ref()
|
||||
.map(|cc| cc.inner.clone()),
|
||||
jacobian_freezing: self
|
||||
.newton
|
||||
.jacobian_freezing
|
||||
.as_ref()
|
||||
.map(|jf| jf.inner.clone()),
|
||||
verbose_config: Default::default(),
|
||||
};
|
||||
let picard_config = entropyk_solver::PicardConfig {
|
||||
@@ -766,7 +791,11 @@ impl PyFallbackConfig {
|
||||
relaxation_factor: self.picard.relaxation,
|
||||
timeout: self.picard.timeout_ms.map(Duration::from_millis),
|
||||
initial_state: self.picard.initial_state.clone(),
|
||||
convergence_criteria: self.picard.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
|
||||
convergence_criteria: self
|
||||
.picard
|
||||
.convergence_criteria
|
||||
.as_ref()
|
||||
.map(|cc| cc.inner.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut fallback =
|
||||
@@ -982,10 +1011,19 @@ impl PySolverStrategy {
|
||||
divergence_threshold: f64,
|
||||
) -> PyResult<Self> {
|
||||
let py_config = PyNewtonConfig::new(
|
||||
max_iterations, tolerance, line_search, timeout_ms, initial_state,
|
||||
use_numerical_jacobian, jacobian_freezing, convergence_criteria,
|
||||
timeout_config, previous_state, line_search_armijo_c,
|
||||
line_search_max_backtracks, divergence_threshold,
|
||||
max_iterations,
|
||||
tolerance,
|
||||
line_search,
|
||||
timeout_ms,
|
||||
initial_state,
|
||||
use_numerical_jacobian,
|
||||
jacobian_freezing,
|
||||
convergence_criteria,
|
||||
timeout_config,
|
||||
previous_state,
|
||||
line_search_armijo_c,
|
||||
line_search_max_backtracks,
|
||||
divergence_threshold,
|
||||
)?;
|
||||
let config = entropyk_solver::NewtonConfig {
|
||||
max_iterations: py_config.max_iterations,
|
||||
@@ -1000,8 +1038,14 @@ impl PySolverStrategy {
|
||||
previous_state: py_config.previous_state.clone(),
|
||||
previous_residual: None,
|
||||
initial_state: py_config.initial_state.clone(),
|
||||
convergence_criteria: py_config.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
|
||||
jacobian_freezing: py_config.jacobian_freezing.as_ref().map(|jf| jf.inner.clone()),
|
||||
convergence_criteria: py_config
|
||||
.convergence_criteria
|
||||
.as_ref()
|
||||
.map(|cc| cc.inner.clone()),
|
||||
jacobian_freezing: py_config
|
||||
.jacobian_freezing
|
||||
.as_ref()
|
||||
.map(|jf| jf.inner.clone()),
|
||||
verbose_config: Default::default(),
|
||||
};
|
||||
Ok(PySolverStrategy {
|
||||
@@ -1019,15 +1063,20 @@ impl PySolverStrategy {
|
||||
convergence_criteria=None
|
||||
))]
|
||||
fn picard(
|
||||
max_iterations: usize,
|
||||
tolerance: f64,
|
||||
max_iterations: usize,
|
||||
tolerance: f64,
|
||||
relaxation: f64,
|
||||
initial_state: Option<Vec<f64>>,
|
||||
timeout_ms: Option<u64>,
|
||||
convergence_criteria: Option<PyConvergenceCriteria>,
|
||||
) -> PyResult<Self> {
|
||||
let py_config = PyPicardConfig::new(
|
||||
max_iterations, tolerance, relaxation, initial_state, timeout_ms, convergence_criteria
|
||||
max_iterations,
|
||||
tolerance,
|
||||
relaxation,
|
||||
initial_state,
|
||||
timeout_ms,
|
||||
convergence_criteria,
|
||||
)?;
|
||||
let config = entropyk_solver::PicardConfig {
|
||||
max_iterations: py_config.max_iterations,
|
||||
@@ -1035,7 +1084,10 @@ impl PySolverStrategy {
|
||||
relaxation_factor: py_config.relaxation,
|
||||
timeout: py_config.timeout_ms.map(Duration::from_millis),
|
||||
initial_state: py_config.initial_state.clone(),
|
||||
convergence_criteria: py_config.convergence_criteria.as_ref().map(|cc| cc.inner.clone()),
|
||||
convergence_criteria: py_config
|
||||
.convergence_criteria
|
||||
.as_ref()
|
||||
.map(|cc| cc.inner.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
Ok(PySolverStrategy {
|
||||
@@ -1052,7 +1104,7 @@ impl PySolverStrategy {
|
||||
|
||||
fn solve(&mut self, system: &mut PySystem) -> PyResult<PyConvergedState> {
|
||||
use entropyk_solver::Solver;
|
||||
|
||||
|
||||
let solve_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
|
||||
self.inner.solve(&mut system.inner)
|
||||
}));
|
||||
|
||||
@@ -345,7 +345,6 @@ impl PyMassFlow {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Concentration
|
||||
// =============================================================================
|
||||
@@ -361,7 +360,9 @@ impl PyConcentration {
|
||||
#[new]
|
||||
fn new(value: f64) -> PyResult<Self> {
|
||||
if !(0.0..=1.0).contains(&value) {
|
||||
return Err(pyo3::exceptions::PyValueError::new_err("Value must be between 0.0 and 1.0"));
|
||||
return Err(pyo3::exceptions::PyValueError::new_err(
|
||||
"Value must be between 0.0 and 1.0",
|
||||
));
|
||||
}
|
||||
Ok(PyConcentration {
|
||||
inner: entropyk::Concentration::from_fraction(value),
|
||||
@@ -427,9 +428,13 @@ impl PyVolumeFlow {
|
||||
#[new]
|
||||
fn new(value: f64) -> PyResult<Self> {
|
||||
if value < 0.0 {
|
||||
return Err(pyo3::exceptions::PyValueError::new_err("Value cannot be negative"));
|
||||
return Err(pyo3::exceptions::PyValueError::new_err(
|
||||
"Value cannot be negative",
|
||||
));
|
||||
}
|
||||
Ok(PyVolumeFlow { inner: entropyk::VolumeFlow::from_m3_per_s(value) })
|
||||
Ok(PyVolumeFlow {
|
||||
inner: entropyk::VolumeFlow::from_m3_per_s(value),
|
||||
})
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -439,12 +444,16 @@ impl PyVolumeFlow {
|
||||
|
||||
#[staticmethod]
|
||||
fn from_m3_per_s(value: f64) -> Self {
|
||||
PyVolumeFlow { inner: entropyk::VolumeFlow::from_m3_per_s(value) }
|
||||
PyVolumeFlow {
|
||||
inner: entropyk::VolumeFlow::from_m3_per_s(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn from_l_per_min(value: f64) -> Self {
|
||||
PyVolumeFlow { inner: entropyk::VolumeFlow::from_l_per_min(value) }
|
||||
PyVolumeFlow {
|
||||
inner: entropyk::VolumeFlow::from_l_per_min(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_m3_per_s(&self) -> f64 {
|
||||
@@ -483,9 +492,13 @@ impl PyRelativeHumidity {
|
||||
#[new]
|
||||
fn new(value: f64) -> PyResult<Self> {
|
||||
if !(0.0..=1.0).contains(&value) {
|
||||
return Err(pyo3::exceptions::PyValueError::new_err("Value must be between 0.0 and 1.0"));
|
||||
return Err(pyo3::exceptions::PyValueError::new_err(
|
||||
"Value must be between 0.0 and 1.0",
|
||||
));
|
||||
}
|
||||
Ok(PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_fraction(value) })
|
||||
Ok(PyRelativeHumidity {
|
||||
inner: entropyk::RelativeHumidity::from_fraction(value),
|
||||
})
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -495,12 +508,16 @@ impl PyRelativeHumidity {
|
||||
|
||||
#[staticmethod]
|
||||
fn from_percent(value: f64) -> Self {
|
||||
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_percent(value) }
|
||||
PyRelativeHumidity {
|
||||
inner: entropyk::RelativeHumidity::from_percent(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn from_fraction(value: f64) -> Self {
|
||||
PyRelativeHumidity { inner: entropyk::RelativeHumidity::from_fraction(value) }
|
||||
PyRelativeHumidity {
|
||||
inner: entropyk::RelativeHumidity::from_fraction(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_percent(&self) -> f64 {
|
||||
@@ -539,9 +556,13 @@ impl PyVaporQuality {
|
||||
#[new]
|
||||
fn new(value: f64) -> PyResult<Self> {
|
||||
if !(0.0..=1.0).contains(&value) {
|
||||
return Err(pyo3::exceptions::PyValueError::new_err("Value must be between 0.0 and 1.0"));
|
||||
return Err(pyo3::exceptions::PyValueError::new_err(
|
||||
"Value must be between 0.0 and 1.0",
|
||||
));
|
||||
}
|
||||
Ok(PyVaporQuality { inner: entropyk::VaporQuality::from_fraction(value) })
|
||||
Ok(PyVaporQuality {
|
||||
inner: entropyk::VaporQuality::from_fraction(value),
|
||||
})
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -551,12 +572,16 @@ impl PyVaporQuality {
|
||||
|
||||
#[staticmethod]
|
||||
fn from_fraction(value: f64) -> Self {
|
||||
PyVaporQuality { inner: entropyk::VaporQuality::from_fraction(value) }
|
||||
PyVaporQuality {
|
||||
inner: entropyk::VaporQuality::from_fraction(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn from_percent(value: f64) -> Self {
|
||||
PyVaporQuality { inner: entropyk::VaporQuality::from_percent(value) }
|
||||
PyVaporQuality {
|
||||
inner: entropyk::VaporQuality::from_percent(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_fraction(&self) -> f64 {
|
||||
@@ -579,4 +604,3 @@ impl PyVaporQuality {
|
||||
(self.inner.0 - other.inner.0).abs() < 1e-10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,11 @@ pub fn load_fluid_table(json: String) -> Result<(), String> {
|
||||
/// Get list of available fluids in the default backend.
|
||||
#[wasm_bindgen]
|
||||
pub fn list_available_fluids() -> Vec<String> {
|
||||
global_backend().list_fluids().into_iter().map(|f| f.0).collect()
|
||||
global_backend()
|
||||
.list_fluids()
|
||||
.into_iter()
|
||||
.map(|f| f.0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -100,6 +100,96 @@ impl WasmCompressor {
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM wrapper for a screw compressor with economizer injection.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmScrewEconomizerCompressor {
|
||||
pub(crate) inner: entropyk_components::ScrewEconomizerCompressor,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmScrewEconomizerCompressor {
|
||||
/// Create a new screw economizer compressor with default bilinear curves.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
fluid: String,
|
||||
nominal_frequency_hz: f64,
|
||||
frequency_hz: f64,
|
||||
mechanical_efficiency: f64,
|
||||
economizer_fraction: f64,
|
||||
) -> Result<WasmScrewEconomizerCompressor, JsValue> {
|
||||
if nominal_frequency_hz <= 0.0 {
|
||||
return Err(js_sys::Error::new("nominal_frequency_hz must be positive").into());
|
||||
}
|
||||
if frequency_hz < 0.0 {
|
||||
return Err(js_sys::Error::new("frequency_hz cannot be negative").into());
|
||||
}
|
||||
if !(0.0..=1.0).contains(&mechanical_efficiency) || mechanical_efficiency == 0.0 {
|
||||
return Err(js_sys::Error::new("mechanical_efficiency must be in (0.0, 1.0]").into());
|
||||
}
|
||||
if !(0.0..=0.4).contains(&economizer_fraction) {
|
||||
return Err(
|
||||
js_sys::Error::new("economizer_fraction must be between 0.0 and 0.4").into(),
|
||||
);
|
||||
}
|
||||
|
||||
fn connected_port(
|
||||
fluid: &str,
|
||||
pressure_bar: f64,
|
||||
enthalpy_kj_kg: f64,
|
||||
) -> Result<entropyk_components::ConnectedPort, JsValue> {
|
||||
let fluid_id = FluidId::new(fluid);
|
||||
let port_a = Port::new(
|
||||
fluid_id.clone(),
|
||||
entropyk_core::Pressure::from_bar(pressure_bar),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
|
||||
);
|
||||
let port_b = Port::new(
|
||||
fluid_id,
|
||||
entropyk_core::Pressure::from_bar(pressure_bar),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(enthalpy_kj_kg * 1000.0),
|
||||
);
|
||||
port_a
|
||||
.connect(port_b)
|
||||
.map(|(connected, _)| connected)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Port connection failed: {}", e)).into())
|
||||
}
|
||||
|
||||
let curves = entropyk_components::ScrewPerformanceCurves::with_fixed_eco_fraction(
|
||||
entropyk_components::Polynomial2D::bilinear(1.2, 0.003, -0.002, 0.000_01),
|
||||
entropyk_components::Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5),
|
||||
economizer_fraction,
|
||||
);
|
||||
let mut inner = entropyk_components::ScrewEconomizerCompressor::new(
|
||||
curves,
|
||||
&fluid,
|
||||
nominal_frequency_hz,
|
||||
mechanical_efficiency,
|
||||
connected_port(&fluid, 3.2, 400.0)?,
|
||||
connected_port(&fluid, 12.8, 440.0)?,
|
||||
connected_port(&fluid, 6.4, 260.0)?,
|
||||
)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Screw compressor creation failed: {}", e)))?;
|
||||
inner
|
||||
.set_frequency_hz(frequency_hz)
|
||||
.map_err(|e| js_sys::Error::new(&format!("Frequency update failed: {}", e)))?;
|
||||
|
||||
Ok(WasmScrewEconomizerCompressor { inner })
|
||||
}
|
||||
|
||||
/// Current VFD frequency in Hz.
|
||||
pub fn frequency_hz(&self) -> f64 {
|
||||
self.inner.frequency_hz()
|
||||
}
|
||||
|
||||
/// Convert to a generic WasmComponent.
|
||||
pub fn into_component(self) -> WasmComponent {
|
||||
WasmComponent {
|
||||
inner: Box::new(self.inner),
|
||||
name: "ScrewEconomizerCompressor".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM wrapper for Condenser.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmCondenser {
|
||||
@@ -269,3 +359,48 @@ impl WasmPipe {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM wrapper for CapillaryTube.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmCapillaryTube {
|
||||
pub(crate) inner: entropyk_components::CapillaryTube<entropyk_components::port::Connected>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmCapillaryTube {
|
||||
/// Create a capillary tube (diameter_m, length_m).
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
fluid: String,
|
||||
diameter_m: f64,
|
||||
length_m: f64,
|
||||
) -> Result<WasmCapillaryTube, JsValue> {
|
||||
use entropyk_components::{CapillaryGeometry, CapillaryTube};
|
||||
let fluid_id = FluidId::new(&fluid);
|
||||
let p = entropyk_core::Pressure::from_bar(10.0);
|
||||
let h = entropyk_core::Enthalpy::from_joules_per_kg(250000.0);
|
||||
let geom = CapillaryGeometry {
|
||||
diameter_m,
|
||||
length_m,
|
||||
n_segments: 20,
|
||||
};
|
||||
let tube = CapillaryTube::new(
|
||||
geom,
|
||||
Port::new(fluid_id.clone(), p, h),
|
||||
Port::new(fluid_id.clone(), p, h),
|
||||
)
|
||||
.map_err(|e| js_sys::Error::new(&format!("{e}")))?;
|
||||
let connected = tube
|
||||
.connect(Port::new(fluid_id.clone(), p, h), Port::new(fluid_id, p, h))
|
||||
.map_err(|e| js_sys::Error::new(&format!("{e}")))?;
|
||||
Ok(WasmCapillaryTube { inner: connected })
|
||||
}
|
||||
|
||||
/// Convert to a generic WasmComponent.
|
||||
pub fn into_component(self) -> WasmComponent {
|
||||
WasmComponent {
|
||||
inner: Box::new(self.inner),
|
||||
name: "CapillaryTube".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
//! This crate provides WebAssembly wrappers for the Entropyk thermodynamic
|
||||
//! simulation library via wasm-bindgen.
|
||||
|
||||
// Pre-existing lint debt: wasm-bindgen exposes camelCase JS names.
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub mod backend;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::components::WasmComponent;
|
||||
use crate::types::{WasmConvergedState, WasmThermoState};
|
||||
use entropyk_fluids::FluidBackend;
|
||||
use entropyk_solver::{
|
||||
ConvergenceStatus, ConvergedState, FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig,
|
||||
ConvergedState, ConvergenceStatus, FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig,
|
||||
Solver, SolverStrategy, System,
|
||||
};
|
||||
use petgraph::graph::NodeIndex;
|
||||
@@ -202,7 +202,7 @@ impl WasmSystem {
|
||||
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())
|
||||
.solve(&mut self.inner.borrow_mut())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()))?;
|
||||
|
||||
*self.last_state.borrow_mut() = Some(state.state.clone());
|
||||
@@ -216,7 +216,7 @@ impl WasmSystem {
|
||||
) -> Result<WasmConvergedState, JsValue> {
|
||||
let mut solver = SolverStrategy::NewtonRaphson(config.inner);
|
||||
let state = solver
|
||||
.solve(&mut *self.inner.borrow_mut())
|
||||
.solve(&mut self.inner.borrow_mut())
|
||||
.map_err(|e| js_sys::Error::new(&e.to_string()))?;
|
||||
|
||||
*self.last_state.borrow_mut() = Some(state.state.clone());
|
||||
@@ -251,7 +251,9 @@ impl WasmSystem {
|
||||
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()
|
||||
p_idx,
|
||||
h_idx,
|
||||
state.len()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user