chore: sync project state and current artifacts

This commit is contained in:
Sepehr
2026-02-22 23:27:31 +01:00
parent 1b6415776e
commit dd77089b22
232 changed files with 37056 additions and 4296 deletions

18
tests/fluids/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "fluids-integration-tests"
version = "0.1.0"
authors = ["Sepehr <sepehr@entropyk.com>"]
edition = "2021"
publish = false
[dependencies]
entropyk-core = { path = "../../crates/core" }
entropyk-fluids = { path = "../../crates/fluids" }
approx = "0.5"
rayon = "1.8"
[features]
coolprop = ["entropyk-fluids/coolprop"]
[dev-dependencies]
# No separate dev-deps needed as this is a test-only crate

View File

@@ -0,0 +1,77 @@
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");
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
];
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_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();
assert_relative_eq!(h_t, h_c, max_relative = 0.20);
}
}
#[test]
#[cfg(feature = "coolprop")]
fn test_incompressible_vs_coolprop_water() {
let coolprop = CoolPropBackend::new();
let incomp = IncompressibleBackend::new();
let fluid = FluidId::new("Water");
// Liquid water states
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();
// 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_i = incomp.property(fluid.clone(), Property::Cp, state).unwrap();
assert_relative_eq!(cp_i, cp_c, max_relative = 0.005);
}
}

View File

@@ -0,0 +1,82 @@
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();
// 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();
// Second call - should hit cache
let rho2 = cached.property(fluid.clone(), Property::Density, state.clone()).unwrap();
assert_eq!(rho1, rho2);
});
}
#[test]
#[cfg(feature = "coolprop")]
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();
// 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");
}
#[test]
#[cfg(feature = "coolprop")]
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();
// 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();
// 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

@@ -0,0 +1,69 @@
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
];
for t_k in temperatures {
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_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);
}
}
}
#[test]
#[cfg(feature = "coolprop")]
fn test_damping_smoothness() {
let damped = CoolPropBackend::with_damping();
let fluid = FluidId::new("CO2");
let tc = 304.13;
let pc = 7.3773e6;
// 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();
// Sigmoid damping should ensure finite delta
assert!((cp1 - cp2).abs() < 50000.0);
}

9
tests/fluids/src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
//! Integration tests for the fluids backend.
//!
//! These tests verify cross-backend consistency, mixture handling,
//! 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;

View File

@@ -0,0 +1,68 @@
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);
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!(glide < 0.5);
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);
}
#[test]
#[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 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_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();
// 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
assert!(t >= t_bubble.to_kelvin() - 0.5 && t <= t_dew.to_kelvin() + 0.5);
}