chore: sync project state and current artifacts
This commit is contained in:
18
tests/fluids/Cargo.toml
Normal file
18
tests/fluids/Cargo.toml
Normal 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
|
||||
77
tests/fluids/src/backend_consistency.rs
Normal file
77
tests/fluids/src/backend_consistency.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
82
tests/fluids/src/cache_integrity.rs
Normal file
82
tests/fluids/src/cache_integrity.rs
Normal 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);
|
||||
}
|
||||
69
tests/fluids/src/damping_stability.rs
Normal file
69
tests/fluids/src/damping_stability.rs
Normal 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
9
tests/fluids/src/lib.rs
Normal 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;
|
||||
68
tests/fluids/src/mixture_glide.rs
Normal file
68
tests/fluids/src/mixture_glide.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user