Update project structure and configurations

This commit is contained in:
2026-05-23 10:19:55 +02:00
parent ab5dc7e568
commit 62efea0646
1832 changed files with 83568 additions and 51829 deletions

View File

@@ -32,23 +32,27 @@ fn main() {
if let Some(coolprop_path) = coolprop_src_path() {
println!("cargo:rerun-if-changed={}", coolprop_path.display());
// Build CoolProp using CMake
let dst = cmake::Config::new(&coolprop_path)
// Build CoolProp using CMake (always Release to match Rust's CRT)
let mut config = cmake::Config::new(&coolprop_path);
config
.define("COOLPROP_SHARED_LIBRARY", "OFF")
.define("COOLPROP_STATIC_LIBRARY", "ON")
.define("COOLPROP_CATCH_TEST", "OFF")
.define("COOLPROP_C_LIBRARY", "ON")
.define("COOLPROP_MY_IFCO3_WRAPPER", "OFF")
.build();
.profile("Release");
let dst = config.build();
println!("cargo:rustc-link-search=native={}/build", dst.display());
println!("cargo:rustc-link-search=native={}/build/Debug", dst.display());
println!("cargo:rustc-link-search=native={}/build/Release", dst.display());
println!("cargo:rustc-link-search=native={}/lib", dst.display());
println!(
"cargo:rustc-link-search=native={}/build",
coolprop_path.display()
); // Fallback
// Link against CoolProp statically
// Link against CoolProp statically (always Release build, no 'd' suffix)
println!("cargo:rustc-link-lib=static=CoolProp");
// On macOS, force load the static library so its symbols are exported in the final cdylib

View File

@@ -130,10 +130,10 @@ pub enum CoolPropInputPair {
// CoolProp C functions
extern "C" {
/// Get a property value using pressure and temperature
/// Get a property value using pressure and temperature
#[cfg_attr(target_os = "macos", link_name = "\x01__Z7PropsSIPKcS0_dS0_dS0_")]
#[cfg_attr(not(target_os = "macos"), link_name = "_Z7PropsSIPKcS0_dS0_dS0_")]
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "_Z7PropsSIPKcS0_dS0_dS0_")]
#[cfg_attr(target_os = "windows", link_name = "?PropsSI@@YANPEBD0N0N0@Z")]
fn PropsSI(
Output: *const c_char,
Name1: *const c_char,
@@ -145,12 +145,14 @@ extern "C" {
/// Get a property value using input pair
#[cfg_attr(target_os = "macos", link_name = "\x01__Z8Props1SIPKcS0_")]
#[cfg_attr(not(target_os = "macos"), link_name = "_Z8Props1SIPKcS0_")]
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "_Z8Props1SIPKcS0_")]
#[cfg_attr(target_os = "windows", link_name = "?Props1SI@@YANPEBD0@Z")]
fn Props1SI(Fluid: *const c_char, Output: *const c_char) -> c_double;
/// Get CoolProp version string
#[cfg_attr(target_os = "macos", link_name = "\x01__Z23get_global_param_stringPKcPci")]
#[cfg_attr(not(target_os = "macos"), link_name = "get_global_param_string")]
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "get_global_param_string")]
#[cfg_attr(target_os = "windows", link_name = "?get_global_param_string@@YAJPEBDPEADH@Z")]
fn get_global_param_string(
Param: *const c_char,
Output: *mut c_char,
@@ -159,7 +161,8 @@ extern "C" {
/// Get fluid info
#[cfg_attr(target_os = "macos", link_name = "\x01__Z22get_fluid_param_stringPKcS0_Pci")]
#[cfg_attr(not(target_os = "macos"), link_name = "get_fluid_param_string")]
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "get_fluid_param_string")]
#[cfg_attr(target_os = "windows", link_name = "?get_fluid_param_string@@YAJPEBD0PEADH@Z")]
fn get_fluid_param_string(
Fluid: *const c_char,
Param: *const c_char,

View File

@@ -3,6 +3,10 @@
//! This module provides a mock backend that returns simplified/idealized
//! property values for testing without requiring external dependencies
//! like CoolProp.
//!
//! For R134a, the backend uses tabulated saturation data from NIST REFPROP
//! (referenced in the thermodynamic test specifications document) to support
//! PressureQuality and PressureEnthalpy state inputs.
use crate::backend::FluidBackend;
use crate::errors::{FluidError, FluidResult};
@@ -12,16 +16,38 @@ use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
use entropyk_core::{Pressure, Temperature};
use std::collections::HashMap;
/// Saturation data point for a refrigerant.
/// Values from NIST REFPROP / thermodynamic-test-specifications.md.
struct SatPoint {
t_celsius: f64,
p_bar: f64,
hf_kjkg: f64,
hg_kjkg: f64,
rho_f: f64,
rho_g: f64,
}
/// Saturation table for a single fluid, sorted by pressure ascending.
struct SatTable {
fluid: String,
points: Vec<SatPoint>,
}
/// Test backend for unit testing.
///
/// This backend provides simplified thermodynamic property calculations
/// suitable for testing without external dependencies. Values are idealized
/// approximations and should NOT be used for real simulations.
///
/// For R134a, saturation data is interpolated from NIST reference tables,
/// enabling PressureQuality (P,x) and PressureEnthalpy (P,h) queries.
pub struct TestBackend {
/// Map of fluid names to critical points
critical_points: HashMap<String, CriticalPoint>,
/// List of available test fluids
available_fluids: Vec<String>,
/// Saturation tables per fluid (R134a, R410A, etc.)
sat_tables: Vec<SatTable>,
}
impl TestBackend {
@@ -90,9 +116,157 @@ impl TestBackend {
"Air".to_string(),
];
// R134a saturation table from NIST REFPROP / thermodynamic-test-specifications.md §2.1
let r134a_sat = SatTable {
fluid: "R134a".to_string(),
points: vec![
SatPoint { t_celsius: -10.0, p_bar: 2.013, hf_kjkg: 186.7, hg_kjkg: 392.7, rho_f: 1295.0, rho_g: 10.2 },
SatPoint { t_celsius: 0.0, p_bar: 2.928, hf_kjkg: 200.0, hg_kjkg: 398.6, rho_f: 1295.0, rho_g: 14.4 },
SatPoint { t_celsius: 7.0, p_bar: 3.748, hf_kjkg: 209.1, hg_kjkg: 402.4, rho_f: 1262.0, rho_g: 18.2 },
SatPoint { t_celsius: 10.0, p_bar: 4.150, hf_kjkg: 213.0, hg_kjkg: 404.0, rho_f: 1251.0, rho_g: 20.2 },
SatPoint { t_celsius: 20.0, p_bar: 5.719, hf_kjkg: 227.5, hg_kjkg: 409.4, rho_f: 1226.0, rho_g: 27.8 },
SatPoint { t_celsius: 25.0, p_bar: 6.658, hf_kjkg: 234.6, hg_kjkg: 412.0, rho_f: 1207.0, rho_g: 32.3 },
SatPoint { t_celsius: 35.0, p_bar: 8.875, hf_kjkg: 249.0, hg_kjkg: 414.4, rho_f: 1168.0, rho_g: 43.1 },
SatPoint { t_celsius: 40.0, p_bar: 10.170, hf_kjkg: 256.4, hg_kjkg: 419.4, rho_f: 1148.0, rho_g: 50.8 },
SatPoint { t_celsius: 45.0, p_bar: 11.597, hf_kjkg: 263.7, hg_kjkg: 420.6, rho_f: 1129.0, rho_g: 58.9 },
SatPoint { t_celsius: 50.0, p_bar: 13.180, hf_kjkg: 271.4, hg_kjkg: 421.2, rho_f: 1102.0, rho_g: 68.2 },
],
};
// R410A saturation table from thermodynamic-test-specifications.md §2.2
// Extended with low-T points for BPHX evaporator at 4 bar (~-20°C)
let r410a_sat = SatTable {
fluid: "R410A".to_string(),
points: vec![
SatPoint { t_celsius: -30.0, p_bar: 2.34, hf_kjkg: 156.0, hg_kjkg: 422.0, rho_f: 1140.0, rho_g: 14.0 },
SatPoint { t_celsius: -20.0, p_bar: 4.01, hf_kjkg: 175.0, hg_kjkg: 427.0, rho_f: 1113.0, rho_g: 23.0 },
SatPoint { t_celsius: -10.0, p_bar: 5.85, hf_kjkg: 178.0, hg_kjkg: 428.0, rho_f: 1100.0, rho_g: 30.0 },
SatPoint { t_celsius: 0.0, p_bar: 7.97, hf_kjkg: 192.0, hg_kjkg: 432.0, rho_f: 1080.0, rho_g: 40.0 },
SatPoint { t_celsius: 10.0, p_bar: 10.82, hf_kjkg: 207.0, hg_kjkg: 436.0, rho_f: 1050.0, rho_g: 50.0 },
SatPoint { t_celsius: 20.0, p_bar: 14.48, hf_kjkg: 225.0, hg_kjkg: 436.0, rho_f: 1020.0, rho_g: 65.0 },
SatPoint { t_celsius: 30.0, p_bar: 18.95, hf_kjkg: 245.0, hg_kjkg: 434.0, rho_f: 985.0, rho_g: 82.0 },
SatPoint { t_celsius: 40.0, p_bar: 24.27, hf_kjkg: 268.0, hg_kjkg: 432.0, rho_f: 950.0, rho_g: 100.0 },
SatPoint { t_celsius: 50.0, p_bar: 30.47, hf_kjkg: 290.0, hg_kjkg: 427.0, rho_f: 900.0, rho_g: 130.0 },
],
};
TestBackend {
critical_points,
available_fluids,
sat_tables: vec![r134a_sat, r410a_sat],
}
}
/// Interpolate saturation properties for any fluid with a table.
/// Returns (T_sat_C, h_f_kJkg, h_g_kJkg, rho_f, rho_g) or None.
fn sat_at_p(&self, fluid: &str, p_pa: f64) -> Option<(f64, f64, f64, f64, f64)> {
let table = self.sat_tables.iter().find(|t| t.fluid == fluid)?;
let p_bar = p_pa / 1e5;
let sat = &table.points;
if p_bar < sat.first()?.p_bar || p_bar > sat.last()?.p_bar {
return None;
}
let idx = sat.iter().position(|s| s.p_bar >= p_bar)?;
if idx == 0 {
let s = &sat[0];
return Some((s.t_celsius, s.hf_kjkg, s.hg_kjkg, s.rho_f, s.rho_g));
}
let lo = &sat[idx - 1];
let hi = &sat[idx];
let t = (p_bar - lo.p_bar) / (hi.p_bar - lo.p_bar);
Some((
lo.t_celsius + t * (hi.t_celsius - lo.t_celsius),
lo.hf_kjkg + t * (hi.hf_kjkg - lo.hf_kjkg),
lo.hg_kjkg + t * (hi.hg_kjkg - lo.hg_kjkg),
lo.rho_f + t * (hi.rho_f - lo.rho_f),
lo.rho_g + t * (hi.rho_g - lo.rho_g),
))
}
/// Property from (P, quality) for any fluid with a saturation table.
fn property_px(&self, fluid: &str, property: Property, p_pa: f64, x: f64) -> FluidResult<f64> {
let (t_sat, hf, hg, rho_f, rho_g) = self
.sat_at_p(fluid, p_pa)
.ok_or(FluidError::InvalidState {
reason: format!("{} pressure {:.2} bar outside TestBackend table range", fluid, p_pa / 1e5),
})?;
let h = hf + x * (hg - hf); // kJ/kg
match property {
Property::Enthalpy => Ok(h * 1000.0), // J/kg
Property::Temperature => Ok(t_sat + 273.15), // K
Property::Density => {
let vf = 1.0 / rho_f;
let vg = 1.0 / rho_g;
let v = vf + x * (vg - vf);
Ok(1.0 / v)
}
Property::Pressure => Ok(p_pa),
Property::Cp => Ok(1500.0),
_ => Err(FluidError::UnsupportedProperty {
property: property.to_string(),
}),
}
}
/// Property from (P, h) for any fluid with a saturation table.
fn property_ph(&self, fluid: &str, property: Property, p_pa: f64, h_jkg: f64) -> FluidResult<f64> {
let (t_sat, hf, hg, rho_f, rho_g) = self
.sat_at_p(fluid, p_pa)
.ok_or(FluidError::InvalidState {
reason: format!("{} pressure {:.2} bar outside TestBackend table range", fluid, p_pa / 1e5),
})?;
let h_kjkg = h_jkg / 1000.0;
let hf_j = hf * 1000.0;
let hg_j = hg * 1000.0;
match property {
Property::Temperature => {
if h_jkg <= hf_j {
// Subcooled liquid: T ≈ T_sat - (hf - h) / cp_liquid
Ok(t_sat + 273.15 - (hf - h_kjkg) / 1.5)
} else if h_jkg >= hg_j {
// Superheated vapor: T ≈ T_sat + (h - hg) / cp_vapor
Ok(t_sat + 273.15 + (h_kjkg - hg) / 1.2)
} else {
// Two-phase: T = T_sat
Ok(t_sat + 273.15)
}
}
Property::Quality => {
if h_jkg <= hf_j {
Ok(0.0) // Subcooled
} else if h_jkg >= hg_j {
Ok(1.0) // Superheated
} else {
Ok((h_kjkg - hf) / (hg - hf))
}
}
Property::Density => {
if h_jkg <= hf_j {
Ok(rho_f)
} else if h_jkg >= hg_j {
// Superheated: ideal gas approx
let t_k = t_sat + 273.15 + (h_kjkg - hg) / 1.2;
Ok(p_pa / (t_k * 100.0)) // rough
} else {
let x = (h_kjkg - hf) / (hg - hf);
let vf = 1.0 / rho_f;
let vg = 1.0 / rho_g;
Ok(1.0 / (vf + x * (vg - vf)))
}
}
Property::Enthalpy => Ok(h_jkg),
Property::Pressure => Ok(p_pa),
Property::Cp => Ok(1500.0),
_ => Err(FluidError::UnsupportedProperty {
property: property.to_string(),
}),
}
}
@@ -182,15 +356,29 @@ impl TestBackend {
fn refrigerant_property(
&self,
_fluid: &str,
fluid: &str,
property: Property,
state: FluidState,
) -> FluidResult<f64> {
// Use tabulated saturation data for P-h and P-x queries
match state {
FluidState::PressureQuality(p, x) => {
return self.property_px(fluid, property, p.to_pascals(), x.0);
}
FluidState::PressureEnthalpy(p, h) => {
return self.property_ph(fluid, property, p.to_pascals(), h.to_joules_per_kg());
}
_ => {} // fall through to P-T handling below
}
let (p, t) = match state {
FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin()),
_ => {
return Err(FluidError::InvalidState {
reason: "TestBackend only supports P-T state for refrigerants".to_string(),
reason: format!(
"TestBackend only supports P-T state for {} (P-x and P-h available for R134a)",
fluid
),
})
}
};
@@ -314,6 +502,8 @@ impl FluidBackend for TestBackend {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Quality;
use entropyk_core::Enthalpy;
#[test]
fn test_backend_available_fluids() {
@@ -436,4 +626,110 @@ mod tests {
);
assert!(state_mix.is_mixture());
}
// ─── R134a saturation table tests (from thermodynamic-test-specifications.md §2.1) ───
/// T-COMP-BACKEND-01: R134a saturation enthalpy from quality
/// At P=2.928 bar (T_sat=0°C), x=0 → h_f=200 kJ/kg, x=1 → h_g=398.6 kJ/kg
#[test]
fn test_r134a_sat_enthalpy_quality_0_at_0c() {
let backend = TestBackend::new();
let state = FluidState::from_px(
Pressure::from_bar(2.928),
Quality(0.0),
);
let h = backend.property(FluidId::new("R134a"), Property::Enthalpy, state).unwrap();
// h_f at 0°C = 200 kJ/kg = 200000 J/kg
assert!(
(h - 200_000.0).abs() < 500.0,
"h_f at 0°C: expected ~200000 J/kg, got {:.0}",
h
);
}
#[test]
fn test_r134a_sat_enthalpy_quality_1_at_0c() {
let backend = TestBackend::new();
let state = FluidState::from_px(
Pressure::from_bar(2.928),
Quality(1.0),
);
let h = backend.property(FluidId::new("R134a"), Property::Enthalpy, state).unwrap();
// h_g at 0°C = 398.6 kJ/kg = 398600 J/kg
assert!(
(h - 398_600.0).abs() < 500.0,
"h_g at 0°C: expected ~398600 J/kg, got {:.0}",
h
);
}
/// T-COMP-BACKEND-02: R134a quality from (P, h) — isenthalpic expansion
/// Saturated liquid at 40°C (h_f=256.4 kJ/kg) expanded to 0°C (P=2.928 bar)
/// x = (h - h_f_evap) / h_fg_evap = (256.4 - 200.0) / (398.6 - 200.0) = 0.284
#[test]
fn test_r134a_quality_from_ph_after_expansion() {
let backend = TestBackend::new();
let state = FluidState::from_ph(
Pressure::from_bar(2.928),
Enthalpy::from_kilojoules_per_kg(256.4),
);
let x = backend.property(FluidId::new("R134a"), Property::Quality, state).unwrap();
assert!(
(x - 0.284).abs() < 0.01,
"quality after isenthalpic expansion: expected ~0.284, got {:.4}",
x
);
}
/// T-COMP-BACKEND-03: R134a T_sat from (P, h) in two-phase region
/// At P=10.17 bar (40°C), two-phase should return T_sat ≈ 40°C = 313.15 K
#[test]
fn test_r134a_tsat_from_ph_twophase() {
let backend = TestBackend::new();
let state = FluidState::from_ph(
Pressure::from_bar(10.17),
Enthalpy::from_kilojoules_per_kg(350.0), // mid two-phase
);
let t = backend.property(FluidId::new("R134a"), Property::Temperature, state).unwrap();
assert!(
(t - 313.15).abs() < 1.0,
"T_sat at 10.17 bar: expected ~313.15 K, got {:.2} K",
t
);
}
/// T-COMP-BACKEND-04: R134a density in two-phase from (P, x)
/// At P=2.928 bar (0°C), x=0.5: should be between rho_f and rho_g
#[test]
fn test_r134a_density_twophase() {
let backend = TestBackend::new();
let state = FluidState::from_px(
Pressure::from_bar(2.928),
Quality(0.5),
);
let rho = backend.property(FluidId::new("R134a"), Property::Density, state).unwrap();
// rho_f=1295, rho_g=14.4 at 0°C. At x=0.5, should be much closer to rho_g
assert!(
rho > 14.4 && rho < 1295.0,
"density at x=0.5: expected between 14.4 and 1295, got {:.1}",
rho
);
}
/// T-COMP-BACKEND-05: R134a interpolated enthalpy between table points
/// At P=5.719 bar (20°C), x=0 → h_f=227.5 kJ/kg
#[test]
fn test_r134a_sat_20c_liquid() {
let backend = TestBackend::new();
let state = FluidState::from_px(
Pressure::from_bar(5.719),
Quality(0.0),
);
let h = backend.property(FluidId::new("R134a"), Property::Enthalpy, state).unwrap();
assert!(
(h - 227_500.0).abs() < 500.0,
"h_f at 20°C: expected ~227500 J/kg, got {:.0}",
h
);
}
}