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

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

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

View File

@@ -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)]

View File

@@ -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(),
}
}
}

View File

@@ -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;

View File

@@ -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());
}