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

@@ -1,29 +1,23 @@
use entropyk_core::{Pressure, Temperature, Enthalpy};
use entropyk_fluids::backend::FluidBackend;
use entropyk_fluids::coolprop::CoolPropBackend;
use entropyk_fluids::tabular_backend::TabularBackend;
use entropyk_fluids::incompressible::IncompressibleBackend;
use entropyk_fluids::types::{FluidId, FluidState, Property};
use approx::assert_relative_eq;
#[test]
#[cfg(feature = "coolprop")]
fn test_tabular_vs_coolprop_r134a() {
let coolprop = CoolPropBackend::new();
let mut tabular = TabularBackend::new();
// Load table (making sure path is correct relative to workspace root)
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest_dir).join("../../crates/fluids/data/r134a.json");
tabular.load_table(&path).expect("Failed to load R134a table");
tabular
.load_table(&path)
.expect("Failed to load R134a table");
let fluid = FluidId::new("R134a");
// Use grid points from r134a.json to minimize interpolation error
let points = [
(1.0, 25.0), // 1 bar, 25°C -> in JSON: 4.4
(2.0, 25.0), // 2 bar, 25°C -> in JSON: 8.5
(5.0, 25.0), // 5 bar, 25°C -> in JSON: 20.0
(1.0, 25.0), // 1 bar, 25°C -> in JSON: 4.4
(2.0, 25.0), // 2 bar, 25°C -> in JSON: 8.5
(5.0, 25.0), // 5 bar, 25°C -> in JSON: 20.0
];
for (p_bar, t_c) in points {
@@ -31,15 +25,23 @@ fn test_tabular_vs_coolprop_r134a() {
let t = Temperature::from_celsius(t_c);
let state = FluidState::from_pt(p, t);
let rho_c = coolprop.property(fluid.clone(), Property::Density, state.clone()).unwrap();
let rho_t = tabular.property(fluid.clone(), Property::Density, state.clone()).unwrap();
let rho_c = coolprop
.property(fluid.clone(), Property::Density, state.clone())
.unwrap();
let rho_t = tabular
.property(fluid.clone(), Property::Density, state.clone())
.unwrap();
// 20% tolerance due to very coarse placeholder tabular data
assert_relative_eq!(rho_t, rho_c, max_relative = 0.20);
let h_c = coolprop.property(fluid.clone(), Property::Enthalpy, state.clone()).unwrap();
let h_t = tabular.property(fluid.clone(), Property::Enthalpy, state).unwrap();
let h_c = coolprop
.property(fluid.clone(), Property::Enthalpy, state.clone())
.unwrap();
let h_t = tabular
.property(fluid.clone(), Property::Enthalpy, state)
.unwrap();
assert_relative_eq!(h_t, h_c, max_relative = 0.20);
}
}
@@ -52,26 +54,28 @@ fn test_incompressible_vs_coolprop_water() {
let fluid = FluidId::new("Water");
// Liquid water states
let points = [
(1.0, 20.0),
(5.0, 50.0),
(10.0, 80.0),
];
let points = [(1.0, 20.0), (5.0, 50.0), (10.0, 80.0)];
for (p_bar, t_c) in points {
let p = Pressure::from_bar(p_bar);
let t = Temperature::from_celsius(t_c);
let state = FluidState::from_pt(p, t);
let rho_c = coolprop.property(fluid.clone(), Property::Density, state.clone()).unwrap();
let rho_i = incomp.property(fluid.clone(), Property::Density, state.clone()).unwrap();
let rho_c = coolprop
.property(fluid.clone(), Property::Density, state.clone())
.unwrap();
let rho_i = incomp
.property(fluid.clone(), Property::Density, state.clone())
.unwrap();
// Incompressible models are approximations, check for 0.5% agreement
assert_relative_eq!(rho_i, rho_c, max_relative = 0.005);
let cp_c = coolprop.property(fluid.clone(), Property::Cp, state.clone()).unwrap();
let cp_c = coolprop
.property(fluid.clone(), Property::Cp, state.clone())
.unwrap();
let cp_i = incomp.property(fluid.clone(), Property::Cp, state).unwrap();
assert_relative_eq!(cp_i, cp_c, max_relative = 0.005);
}
}

View File

@@ -1,33 +1,31 @@
use entropyk_core::{Pressure, Temperature};
use entropyk_fluids::backend::FluidBackend;
use entropyk_fluids::coolprop::CoolPropBackend;
use entropyk_fluids::cached_backend::CachedBackend;
use entropyk_fluids::types::{FluidId, FluidState, Property};
use rayon::prelude::*;
use approx::assert_relative_eq;
#[test]
#[cfg(feature = "coolprop")]
fn test_cache_concurrent_access() {
let inner = CoolPropBackend::new();
let cached = CachedBackend::new(inner);
let fluid = FluidId::new("R134a");
// Generate many states
let states: Vec<_> = (0..100).map(|i| {
FluidState::from_pt(
Pressure::from_bar(1.0 + (i as f64) * 0.1),
Temperature::from_celsius(25.0)
)
}).collect();
let states: Vec<_> = (0..100)
.map(|i| {
FluidState::from_pt(
Pressure::from_bar(1.0 + (i as f64) * 0.1),
Temperature::from_celsius(25.0),
)
})
.collect();
// Parallel execution via Rayon
states.par_iter().for_each(|state| {
// First call - populates cache
let rho1 = cached.property(fluid.clone(), Property::Density, state.clone()).unwrap();
let rho1 = cached
.property(fluid.clone(), Property::Density, state.clone())
.unwrap();
// Second call - should hit cache
let rho2 = cached.property(fluid.clone(), Property::Density, state.clone()).unwrap();
let rho2 = cached
.property(fluid.clone(), Property::Density, state.clone())
.unwrap();
assert_eq!(rho1, rho2);
});
}
@@ -38,23 +36,30 @@ fn test_cache_quantization_hit() {
let inner = CoolPropBackend::new();
let cached = CachedBackend::new(inner);
let fluid = FluidId::new("R134a");
let p = Pressure::from_bar(10.0);
let t = Temperature::from_kelvin(300.0);
let state1 = FluidState::from_pt(p, t);
// Result 1
let rho1 = cached.property(fluid.clone(), Property::Density, state1).unwrap();
let rho1 = cached
.property(fluid.clone(), Property::Density, state1)
.unwrap();
// State 2: very small perturbation (within 1e-10 relative)
// Quantization is at 1e-9, so this SHOULD hit the same cache line
let t2 = Temperature::from_kelvin(300.0 + 1e-11);
let state2 = FluidState::from_pt(p, t2);
// If it hits the cache, it returns EXACTLY rho1 (even if physical value changed by 1e-12)
let rho2 = cached.property(fluid.clone(), Property::Density, state2).unwrap();
assert_eq!(rho1, rho2, "Cache quantization fail: small pertubations should return cached value");
let rho2 = cached
.property(fluid.clone(), Property::Density, state2)
.unwrap();
assert_eq!(
rho1, rho2,
"Cache quantization fail: small pertubations should return cached value"
);
}
#[test]
@@ -63,19 +68,23 @@ fn test_cache_quantization_miss() {
let inner = CoolPropBackend::new();
let cached = CachedBackend::new(inner);
let fluid = FluidId::new("R134a");
let p = Pressure::from_bar(10.0);
let t = Temperature::from_kelvin(300.0);
let state1 = FluidState::from_pt(p, t);
let rho1 = cached.property(fluid.clone(), Property::Density, state1).unwrap();
let rho1 = cached
.property(fluid.clone(), Property::Density, state1)
.unwrap();
// Large perturbation (1e-6) - should be a cache miss and calculate new value
let t2 = Temperature::from_kelvin(300.0 + 1e-6);
let state2 = FluidState::from_pt(p, t2);
let rho2 = cached.property(fluid.clone(), Property::Density, state2).unwrap();
let rho2 = cached
.property(fluid.clone(), Property::Density, state2)
.unwrap();
// Value should be slightly different, not identical to cached one
assert_ne!(rho1, rho2);
assert_relative_eq!(rho1, rho2, max_relative = 1e-4);

View File

@@ -1,44 +1,30 @@
use entropyk_core::{Pressure, Temperature};
use entropyk_fluids::backend::FluidBackend;
use entropyk_fluids::coolprop::CoolPropBackend;
use entropyk_fluids::types::{FluidId, FluidState, Property};
use approx::assert_relative_eq;
#[test]
#[cfg(feature = "coolprop")]
fn test_co2_damping_near_critical() {
let inner = CoolPropBackend::new();
let damped = CoolPropBackend::with_damping();
let fluid = FluidId::new("CO2");
// Near critical point of CO2: Tc=304.13K, Pc=7.3773 MPa
let tc = 304.13;
let pc = 7.3773e6;
// Query points approaching the critical point
let temperatures = [
tc * 0.95,
tc * 0.99,
tc * 0.999,
tc,
tc * 1.001,
tc * 1.05
];
let temperatures = [tc * 0.95, tc * 0.99, tc * 0.999, tc, tc * 1.001, tc * 1.05];
for t_k in temperatures {
let state = FluidState::from_pt(
Pressure::from_pascals(pc),
Temperature::from_kelvin(t_k)
);
let state = FluidState::from_pt(Pressure::from_pascals(pc), Temperature::from_kelvin(t_k));
// Raw CoolProp might return very large values or NaN for Cp near critical
let cp_inner = inner.property(fluid.clone(), Property::Cp, state.clone()).unwrap_or(f64::NAN);
let cp_inner = inner
.property(fluid.clone(), Property::Cp, state.clone())
.unwrap_or(f64::NAN);
let cp_damped = damped.property(fluid.clone(), Property::Cp, state).unwrap();
// Damped value must be finite and respect cp_max (default 1e6)
assert!(cp_damped.is_finite());
assert!(cp_damped <= 2e6); // Some margin over default cp_max
if cp_inner.is_finite() && cp_inner < 1e4 {
// Far from critical, they should be identical
assert_relative_eq!(cp_damped, cp_inner, max_relative = 0.01);
@@ -57,13 +43,17 @@ fn test_damping_smoothness() {
// Small step near critical to check for discontinuities
let t1 = tc - 0.001;
let t2 = tc + 0.001;
let state1 = FluidState::from_pt(Pressure::from_pascals(pc), Temperature::from_kelvin(t1));
let state2 = FluidState::from_pt(Pressure::from_pascals(pc), Temperature::from_kelvin(t2));
let cp1 = damped.property(fluid.clone(), Property::Cp, state1).unwrap();
let cp2 = damped.property(fluid.clone(), Property::Cp, state2).unwrap();
let cp1 = damped
.property(fluid.clone(), Property::Cp, state1)
.unwrap();
let cp2 = damped
.property(fluid.clone(), Property::Cp, state2)
.unwrap();
// Sigmoid damping should ensure finite delta
assert!((cp1 - cp2).abs() < 50000.0);
assert!((cp1 - cp2).abs() < 50000.0);
}

View File

@@ -4,7 +4,7 @@
//! damping stability near the critical point, and cache integrity.
pub mod backend_consistency;
pub mod mixture_glide;
pub mod damping_stability;
pub mod cache_integrity;
pub mod damping_stability;
pub mod mixture_glide;
pub mod r134a_cycle;

View File

@@ -1,32 +1,26 @@
use entropyk_core::{Pressure, Temperature, Enthalpy};
use entropyk_fluids::backend::FluidBackend;
use entropyk_fluids::coolprop::CoolPropBackend;
use entropyk_fluids::mixture::Mixture;
use entropyk_fluids::types::{FluidId, FluidState, Property};
use approx::assert_relative_eq;
#[test]
#[cfg(feature = "coolprop")]
fn test_mixture_glide_r454b() {
let backend = CoolPropBackend::new();
// R410A composition (mass fractions)
let mixture = Mixture::from_mass_fractions(&[
("R32", 0.5),
("R125", 0.5),
]).unwrap();
let p = Pressure::from_bar(10.0);
// R410A composition (mass fractions)
let mixture = Mixture::from_mass_fractions(&[("R32", 0.5), ("R125", 0.5)]).unwrap();
let p = Pressure::from_bar(10.0);
let t_bubble = backend.bubble_point(p, &mixture).unwrap();
let t_dew = backend.dew_point(p, &mixture).unwrap();
let glide = backend.temperature_glide(p, &mixture).unwrap();
// R410A is near-azeotropic, glide should be very small (< 0.2K)
assert!(t_dew.to_kelvin() >= t_bubble.to_kelvin() - 0.1);
assert!(t_dew.to_kelvin() >= t_bubble.to_kelvin() - 0.1);
assert!(glide < 0.5);
assert_relative_eq!(glide, t_dew.to_kelvin() - t_bubble.to_kelvin(), epsilon = 1e-6);
assert_relative_eq!(
glide,
t_dew.to_kelvin() - t_bubble.to_kelvin(),
epsilon = 1e-6
);
// Typically glide for R454B is around 1.5K at 10 bar
assert!(glide > 0.5 && glide < 3.0);
}
@@ -35,34 +29,37 @@ fn test_mixture_glide_r454b() {
#[cfg(feature = "coolprop")]
fn test_mixture_ph_state() {
let backend = CoolPropBackend::new();
let mixture = Mixture::from_mass_fractions(&[
("R32", 0.689),
("R1234yf", 0.311),
]).unwrap();
let mixture = Mixture::from_mass_fractions(&[("R32", 0.689), ("R1234yf", 0.311)]).unwrap();
let p = Pressure::from_bar(10.0);
// Middle of two-phase region (Quality ~ 0.5)
let h_bubble = backend.property(
FluidId::new("R454B"),
Property::Enthalpy,
FluidState::from_px_mix(p, 0.0.into(), mixture.clone())
).unwrap();
let h_dew = backend.property(
FluidId::new("R454B"),
Property::Enthalpy,
FluidState::from_px_mix(p, 1.0.into(), mixture.clone())
).unwrap();
let h_bubble = backend
.property(
FluidId::new("R454B"),
Property::Enthalpy,
FluidState::from_px_mix(p, 0.0.into(), mixture.clone()),
)
.unwrap();
let h_dew = backend
.property(
FluidId::new("R454B"),
Property::Enthalpy,
FluidState::from_px_mix(p, 1.0.into(), mixture.clone()),
)
.unwrap();
let h_mid = Enthalpy::from_joules_per_kg((h_bubble + h_dew) / 2.0);
let state = FluidState::from_ph_mix(p, h_mid, mixture.clone());
let t = backend.property(FluidId::new("R454B"), Property::Temperature, state).unwrap();
let t = backend
.property(FluidId::new("R454B"), Property::Temperature, state)
.unwrap();
// Temperature in two-phase should be between bubble and dew point
let t_bubble = backend.bubble_point(p, &mixture).unwrap();
let t_dew = backend.dew_point(p, &mixture).unwrap();
// Use some epsilon for equality
// Use some epsilon for equality
assert!(t >= t_bubble.to_kelvin() - 0.5 && t <= t_dew.to_kelvin() + 0.5);
}

View File

@@ -77,8 +77,14 @@ fn test_r134a_saturation_against_nist() {
);
eprintln!("=== R134a Saturation (CoolProp vs NIST) ===");
eprintln!("T=0°C: P_sat={:.0} Pa, h_f={:.0}, h_g={:.0}", p_sat_0c, hf_0c, hg_0c);
eprintln!("T=40°C: P_sat={:.0} Pa, h_f={:.0}, h_g={:.0}", p_sat_40c, hf_40c, hg_40c);
eprintln!(
"T=0°C: P_sat={:.0} Pa, h_f={:.0}, h_g={:.0}",
p_sat_0c, hf_0c, hg_0c
);
eprintln!(
"T=40°C: P_sat={:.0} Pa, h_f={:.0}, h_g={:.0}",
p_sat_40c, hf_40c, hg_40c
);
}
/// T-CYCLE-01b: Full vapor-compression cycle verification.
@@ -112,7 +118,10 @@ fn test_r134a_simple_cycle() {
// === Saturation pressures ===
let p_evap = unsafe { entropyk_coolprop_sys::props_si_tq("P", t_evap_k, 1.0, fluid) };
let p_cond = unsafe { entropyk_coolprop_sys::props_si_tq("P", t_cond_k, 1.0, fluid) };
assert!(!p_evap.is_nan() && !p_cond.is_nan(), "NaN saturation pressures");
assert!(
!p_evap.is_nan() && !p_cond.is_nan(),
"NaN saturation pressures"
);
// === State 1: Evaporator outlet (superheated) ===
let h1 = unsafe { entropyk_coolprop_sys::props_si_pt("H", p_evap, t_evap_k + tsh, fluid) };
@@ -153,11 +162,21 @@ fn test_r134a_simple_cycle() {
let eta_is = 0.85; // isentropic efficiency
let h2 = h1 + (h2s - h1) / eta_is;
assert!(!h2.is_nan(), "h2 is NaN: h1={}, h2s={}, eta_is={}", h1, h2s, eta_is);
assert!(
!h2.is_nan(),
"h2 is NaN: h1={}, h2s={}, eta_is={}",
h1,
h2s,
eta_is
);
// === Mass flow rate ===
let delta_h_evap = h1 - h4;
assert!(delta_h_evap > 0.0, "delta_h_evap must be positive, got {}", delta_h_evap);
assert!(
delta_h_evap > 0.0,
"delta_h_evap must be positive, got {}",
delta_h_evap
);
let m_dot = q_evap_target / delta_h_evap;
// === Energy ===
@@ -197,7 +216,12 @@ fn test_r134a_simple_cycle() {
);
// Q_cond > Q_evap
assert!(q_cond > q_evap, "Q_cond ({:.0}) must exceed Q_evap ({:.0})", q_cond, q_evap);
assert!(
q_cond > q_evap,
"Q_cond ({:.0}) must exceed Q_evap ({:.0})",
q_cond,
q_evap
);
// First Law: < 3% (2% ideal, but binary search on h2s introduces small error)
assert!(
@@ -217,14 +241,16 @@ fn test_r134a_simple_cycle() {
assert!(
cop_heating > cop_cooling,
"COP_heating ({:.3}) > COP_cooling ({:.3})",
cop_heating, cop_cooling
cop_heating,
cop_cooling
);
// Second Law: COP < Carnot
assert!(
cop_cooling < cop_carnot,
"COP ({:.3}) must be < Carnot ({:.3})",
cop_cooling, cop_carnot
cop_cooling,
cop_carnot
);
// Second Law efficiency: 0.40-0.60
@@ -235,14 +261,24 @@ fn test_r134a_simple_cycle() {
);
// Isenthalpic: h3 = h4
assert!((h4 - h3).abs() < 1e-6, "Isenthalpic violated: h3={:.2}, h4={:.2}", h3, h4);
assert!(
(h4 - h3).abs() < 1e-6,
"Isenthalpic violated: h3={:.2}, h4={:.2}",
h3,
h4
);
// Pressure ratio ~3.5
let pr = p_cond / p_evap;
assert!(pr > 2.5 && pr < 5.0, "PR = {:.2}, expected ~3.5", pr);
eprintln!("=== T-CYCLE-01 Results ===");
eprintln!("P_evap = {:.3} bar, P_cond = {:.3} bar (PR = {:.2})", p_evap / 1e5, p_cond / 1e5, pr);
eprintln!(
"P_evap = {:.3} bar, P_cond = {:.3} bar (PR = {:.2})",
p_evap / 1e5,
p_cond / 1e5,
pr
);
eprintln!("h1 = {:.0} J/kg (evap outlet)", h1);
eprintln!("h2 = {:.0} J/kg (comp discharge)", h2);
eprintln!("h3 = {:.0} J/kg (cond outlet)", h3);
@@ -251,7 +287,10 @@ fn test_r134a_simple_cycle() {
eprintln!("W_comp = {:.0} W", w_comp);
eprintln!("Q_evap = {:.0} W", q_evap);
eprintln!("Q_cond = {:.0} W", q_cond);
eprintln!("COP_cool = {:.3}, COP_heat = {:.3}", cop_cooling, cop_heating);
eprintln!(
"COP_cool = {:.3}, COP_heat = {:.3}",
cop_cooling, cop_heating
);
eprintln!("COP_Carnot = {:.3}, eta_II = {:.3}", cop_carnot, eta_ii);
eprintln!("First Law error = {:.4}%", first_law_error * 100.0);
}