chore: sync project state and current artifacts
This commit is contained in:
@@ -4,26 +4,23 @@
|
||||
//! Cached path should show significant speedup when the backend is expensive (e.g. CoolProp).
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use entropyk_fluids::{
|
||||
CachedBackend, FluidBackend, FluidId, Property, ThermoState, TestBackend,
|
||||
};
|
||||
use entropyk_core::{Pressure, Temperature};
|
||||
use entropyk_fluids::{CachedBackend, FluidBackend, FluidId, Property, TestBackend, ThermoState};
|
||||
|
||||
const N_QUERIES: u32 = 10_000;
|
||||
|
||||
fn bench_uncached_10k(c: &mut Criterion) {
|
||||
let backend = TestBackend::new();
|
||||
let state = ThermoState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(25.0),
|
||||
);
|
||||
let state = ThermoState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
let fluid = FluidId::new("R134a");
|
||||
|
||||
c.bench_function("uncached_10k_same_state", |b| {
|
||||
b.iter(|| {
|
||||
for _ in 0..N_QUERIES {
|
||||
black_box(
|
||||
backend.property(fluid.clone(), Property::Density, state.clone()).unwrap(),
|
||||
backend
|
||||
.property(fluid.clone(), Property::Density, state.clone())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -33,17 +30,16 @@ fn bench_uncached_10k(c: &mut Criterion) {
|
||||
fn bench_cached_10k(c: &mut Criterion) {
|
||||
let inner = TestBackend::new();
|
||||
let cached = CachedBackend::new(inner);
|
||||
let state = ThermoState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(25.0),
|
||||
);
|
||||
let state = ThermoState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
let fluid = FluidId::new("R134a");
|
||||
|
||||
c.bench_function("cached_10k_same_state", |b| {
|
||||
b.iter(|| {
|
||||
for _ in 0..N_QUERIES {
|
||||
black_box(
|
||||
cached.property(fluid.clone(), Property::Density, state.clone()).unwrap(),
|
||||
cached
|
||||
.property(fluid.clone(), Property::Density, state.clone())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Build script for entropyk-fluids crate.
|
||||
//!
|
||||
//!
|
||||
//! This build script can optionally compile CoolProp C++ library when the
|
||||
//! "coolprop" feature is enabled.
|
||||
|
||||
@@ -7,12 +7,12 @@ use std::env;
|
||||
|
||||
fn main() {
|
||||
let coolprop_enabled = env::var("CARGO_FEATURE_COOLPROP").is_ok();
|
||||
|
||||
|
||||
if coolprop_enabled {
|
||||
println!("cargo:rustc-link-lib=dylib=coolprop");
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
|
||||
// Tell Cargo to rerun this script if any source files change
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ libc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
cmake = "0.1.57"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -9,7 +9,7 @@ fn coolprop_src_path() -> Option<PathBuf> {
|
||||
// Try to find CoolProp source in common locations
|
||||
let possible_paths = vec![
|
||||
// Vendor directory (recommended)
|
||||
PathBuf::from("vendor/coolprop"),
|
||||
PathBuf::from("../../vendor/coolprop").canonicalize().unwrap_or(PathBuf::from("../../../vendor/coolprop")),
|
||||
// External directory
|
||||
PathBuf::from("external/coolprop"),
|
||||
// System paths
|
||||
@@ -17,42 +17,64 @@ fn coolprop_src_path() -> Option<PathBuf> {
|
||||
PathBuf::from("/opt/CoolProp"),
|
||||
];
|
||||
|
||||
possible_paths.into_iter().find(|path| path.join("CMakeLists.txt").exists())
|
||||
possible_paths
|
||||
.into_iter()
|
||||
.find(|path| path.join("CMakeLists.txt").exists())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let static_linking = env::var("CARGO_FEATURE_STATIC").is_ok();
|
||||
let static_linking = env::var("CARGO_FEATURE_STATIC").is_ok() || true; // Force static linking for python wheels
|
||||
|
||||
// Check if CoolProp source is available
|
||||
if let Some(coolprop_path) = coolprop_src_path() {
|
||||
println!("cargo:rerun-if-changed={}", coolprop_path.display());
|
||||
|
||||
// Configure build for CoolProp
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}/build",
|
||||
coolprop_path.display()
|
||||
);
|
||||
}
|
||||
// Build CoolProp using CMake
|
||||
let dst = cmake::Config::new(&coolprop_path)
|
||||
.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();
|
||||
|
||||
// Link against CoolProp
|
||||
if static_linking {
|
||||
// Static linking - find libCoolProp.a
|
||||
println!("cargo:rustc-link-search=native={}/build", 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
|
||||
println!("cargo:rustc-link-lib=static=CoolProp");
|
||||
|
||||
// On macOS, force load the static library so its symbols are exported in the final cdylib
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-link-arg=-Wl,-force_load,{}/build/libCoolProp.a", dst.display());
|
||||
}
|
||||
} else {
|
||||
// Dynamic linking
|
||||
println!("cargo:rustc-link-lib=dylib=CoolProp");
|
||||
println!(
|
||||
"cargo:warning=CoolProp source not found in vendor/.
|
||||
For full static build, run:
|
||||
git clone https://github.com/CoolProp/CoolProp.git vendor/coolprop"
|
||||
);
|
||||
// Fallback for system library
|
||||
if static_linking {
|
||||
println!("cargo:rustc-link-lib=static=CoolProp");
|
||||
} else {
|
||||
println!("cargo:rustc-link-lib=dylib=CoolProp");
|
||||
}
|
||||
}
|
||||
|
||||
// Link required system libraries
|
||||
println!("cargo:rustc-link-lib=dylib=m");
|
||||
// Link required system libraries for C++ standard library
|
||||
#[cfg(target_os = "macos")]
|
||||
println!("cargo:rustc-link-lib=dylib=c++");
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
println!("cargo:rustc-link-lib=dylib=stdc++");
|
||||
|
||||
// Tell Cargo to rerun if build.rs changes
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rustc-link-lib=dylib=m");
|
||||
|
||||
println!(
|
||||
"cargo:warning=CoolProp source not found in vendor/.
|
||||
For full static build, run:
|
||||
git clone https://github.com/CoolProp/CoolProp.git vendor/coolprop"
|
||||
);
|
||||
// Tell Cargo to rerun if build.rs changes
|
||||
|
||||
// Force export symbols on macOS for static building into a dynamic python extension
|
||||
println!("cargo:rustc-link-arg=-Wl,-all_load");
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
||||
@@ -131,41 +131,48 @@ pub enum CoolPropInputPair {
|
||||
// CoolProp C functions
|
||||
extern "C" {
|
||||
/// Get a property value using pressure and temperature
|
||||
fn CoolProp_PropsSI(
|
||||
Output: c_char,
|
||||
Name1: c_char,
|
||||
/// 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_")]
|
||||
fn PropsSI(
|
||||
Output: *const c_char,
|
||||
Name1: *const c_char,
|
||||
Value1: c_double,
|
||||
Name2: c_char,
|
||||
Name2: *const c_char,
|
||||
Value2: c_double,
|
||||
Fluid: *const c_char,
|
||||
) -> c_double;
|
||||
|
||||
/// Get a property value using input pair
|
||||
fn CoolProp_Props1SI(Fluid: *const c_char, Output: c_char) -> c_double;
|
||||
#[cfg_attr(target_os = "macos", link_name = "\x01__Z8Props1SIPKcS0_")]
|
||||
#[cfg_attr(not(target_os = "macos"), link_name = "_Z8Props1SIPKcS0_")]
|
||||
fn Props1SI(Fluid: *const c_char, Output: *const c_char) -> c_double;
|
||||
|
||||
/// Get CoolProp version string
|
||||
fn CoolProp_get_global_param_string(
|
||||
#[cfg_attr(target_os = "macos", link_name = "\x01__Z23get_global_param_stringNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE")]
|
||||
#[cfg_attr(not(target_os = "macos"), link_name = "get_global_param_string")]
|
||||
fn get_global_param_string(
|
||||
Param: *const c_char,
|
||||
Output: *mut c_char,
|
||||
OutputLength: c_int,
|
||||
) -> c_int;
|
||||
|
||||
/// Get fluid info
|
||||
fn CoolProp_get_fluid_param_string(
|
||||
#[cfg_attr(target_os = "macos", link_name = "\x01__Z22get_fluid_param_stringNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES5_")]
|
||||
#[cfg_attr(not(target_os = "macos"), link_name = "get_fluid_param_string")]
|
||||
fn get_fluid_param_string(
|
||||
Fluid: *const c_char,
|
||||
Param: *const c_char,
|
||||
Output: *mut c_char,
|
||||
OutputLength: c_int,
|
||||
) -> c_int;
|
||||
|
||||
/// Check if fluid exists
|
||||
fn CoolProp_isfluid(Fluid: *const c_char) -> c_int;
|
||||
// Check if fluid exists
|
||||
// CoolProp doesn't have a direct C isfluid function. We usually just try to fetch a string or param or we can map it downstream
|
||||
// But let's see if we can just dummy it or use get_fluid_param_string
|
||||
|
||||
/// Get saturation temperature
|
||||
fn CoolProp_Saturation_T(Fluid: *const c_char, Par: c_char, Value: c_double) -> c_double;
|
||||
// There is no C CriticalPoint, it's just Props1SI("Tcrit", "Water")
|
||||
|
||||
/// Get critical point
|
||||
fn CoolProp_CriticalPoint(Fluid: *const c_char, Output: c_char) -> c_double;
|
||||
}
|
||||
|
||||
/// Get a thermodynamic property using pressure and temperature.
|
||||
@@ -181,10 +188,10 @@ extern "C" {
|
||||
/// This function calls the CoolProp C++ library and passes a CString pointer.
|
||||
/// The caller must ensure the fluid string is properly null-terminated if needed and valid.
|
||||
pub unsafe fn props_si_pt(property: &str, p: f64, t: f64, fluid: &str) -> f64 {
|
||||
let prop = property.as_bytes()[0] as c_char;
|
||||
let prop_c = std::ffi::CString::new(property).unwrap();
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
|
||||
CoolProp_PropsSI(prop, b'P' as c_char, p, b'T' as c_char, t, fluid_c.as_ptr())
|
||||
PropsSI(prop_c.as_ptr(), c"P".as_ptr(), p, c"T".as_ptr(), t, fluid_c.as_ptr())
|
||||
}
|
||||
|
||||
/// Get a thermodynamic property using pressure and enthalpy.
|
||||
@@ -200,10 +207,10 @@ pub unsafe fn props_si_pt(property: &str, p: f64, t: f64, fluid: &str) -> f64 {
|
||||
/// This function calls the CoolProp C++ library and passes a CString pointer.
|
||||
/// The caller must ensure the fluid string is valid.
|
||||
pub unsafe fn props_si_ph(property: &str, p: f64, h: f64, fluid: &str) -> f64 {
|
||||
let prop = property.as_bytes()[0] as c_char;
|
||||
let prop_c = std::ffi::CString::new(property).unwrap();
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
|
||||
CoolProp_PropsSI(prop, b'P' as c_char, p, b'H' as c_char, h, fluid_c.as_ptr())
|
||||
PropsSI(prop_c.as_ptr(), c"P".as_ptr(), p, c"H".as_ptr(), h, fluid_c.as_ptr())
|
||||
}
|
||||
|
||||
/// Get a thermodynamic property using temperature and quality (saturation).
|
||||
@@ -219,10 +226,10 @@ pub unsafe fn props_si_ph(property: &str, p: f64, h: f64, fluid: &str) -> f64 {
|
||||
/// This function calls the CoolProp C++ library and passes a CString pointer.
|
||||
/// The caller must ensure the fluid string is valid.
|
||||
pub unsafe fn props_si_tq(property: &str, t: f64, q: f64, fluid: &str) -> f64 {
|
||||
let prop = property.as_bytes()[0] as c_char;
|
||||
let prop_c = std::ffi::CString::new(property).unwrap();
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
|
||||
CoolProp_PropsSI(prop, b'T' as c_char, t, b'Q' as c_char, q, fluid_c.as_ptr())
|
||||
PropsSI(prop_c.as_ptr(), c"T".as_ptr(), t, c"Q".as_ptr(), q, fluid_c.as_ptr())
|
||||
}
|
||||
|
||||
/// Get a thermodynamic property using pressure and quality.
|
||||
@@ -238,14 +245,14 @@ pub unsafe fn props_si_tq(property: &str, t: f64, q: f64, fluid: &str) -> f64 {
|
||||
/// This function calls the CoolProp C++ library and passes a CString pointer.
|
||||
/// The caller must ensure the fluid string is valid.
|
||||
pub unsafe fn props_si_px(property: &str, p: f64, x: f64, fluid: &str) -> f64 {
|
||||
let prop = property.as_bytes()[0] as c_char;
|
||||
let prop_c = std::ffi::CString::new(property).unwrap();
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
|
||||
CoolProp_PropsSI(
|
||||
prop,
|
||||
b'P' as c_char,
|
||||
PropsSI(
|
||||
prop_c.as_ptr(),
|
||||
c"P".as_ptr(),
|
||||
p,
|
||||
b'Q' as c_char, // Q for quality
|
||||
c"Q".as_ptr(), // Q for quality
|
||||
x,
|
||||
fluid_c.as_ptr(),
|
||||
)
|
||||
@@ -262,7 +269,7 @@ pub unsafe fn props_si_px(property: &str, p: f64, x: f64, fluid: &str) -> f64 {
|
||||
/// The caller must ensure the fluid string is valid.
|
||||
pub unsafe fn critical_temperature(fluid: &str) -> f64 {
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
CoolProp_CriticalPoint(fluid_c.as_ptr(), b'T' as c_char)
|
||||
Props1SI(fluid_c.as_ptr(), c"Tcrit".as_ptr())
|
||||
}
|
||||
|
||||
/// Get critical point pressure for a fluid.
|
||||
@@ -276,7 +283,7 @@ pub unsafe fn critical_temperature(fluid: &str) -> f64 {
|
||||
/// The caller must ensure the fluid string is valid.
|
||||
pub unsafe fn critical_pressure(fluid: &str) -> f64 {
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
CoolProp_CriticalPoint(fluid_c.as_ptr(), b'P' as c_char)
|
||||
Props1SI(fluid_c.as_ptr(), c"pcrit".as_ptr())
|
||||
}
|
||||
|
||||
/// Get critical point density for a fluid.
|
||||
@@ -290,7 +297,7 @@ pub unsafe fn critical_pressure(fluid: &str) -> f64 {
|
||||
/// The caller must ensure the fluid string is valid.
|
||||
pub unsafe fn critical_density(fluid: &str) -> f64 {
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
CoolProp_CriticalPoint(fluid_c.as_ptr(), b'D' as c_char)
|
||||
Props1SI(fluid_c.as_ptr(), c"rhocrit".as_ptr())
|
||||
}
|
||||
|
||||
/// Check if a fluid is available in CoolProp.
|
||||
@@ -304,7 +311,9 @@ pub unsafe fn critical_density(fluid: &str) -> f64 {
|
||||
/// The caller must ensure the fluid string is valid.
|
||||
pub unsafe fn is_fluid_available(fluid: &str) -> bool {
|
||||
let fluid_c = CString::new(fluid).unwrap();
|
||||
CoolProp_isfluid(fluid_c.as_ptr()) != 0
|
||||
// CoolProp C API does not expose isfluid, so we try fetching a property
|
||||
let res = Props1SI(fluid_c.as_ptr(), c"Tcrit".as_ptr());
|
||||
if res.is_finite() && res != 0.0 { true } else { false }
|
||||
}
|
||||
|
||||
/// Get CoolProp version string.
|
||||
@@ -314,7 +323,7 @@ pub unsafe fn is_fluid_available(fluid: &str) -> bool {
|
||||
pub fn get_version() -> String {
|
||||
unsafe {
|
||||
let mut buffer = vec![0u8; 32];
|
||||
let result = CoolProp_get_global_param_string(
|
||||
let result = get_global_param_string(
|
||||
c"version".as_ptr(),
|
||||
buffer.as_mut_ptr() as *mut c_char,
|
||||
buffer.len() as c_int,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use crate::errors::FluidResult;
|
||||
use crate::mixture::Mixture;
|
||||
use crate::types::{CriticalPoint, FluidId, Phase, Property, FluidState, ThermoState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property, ThermoState};
|
||||
use entropyk_core::{Pressure, Temperature};
|
||||
|
||||
/// Trait for fluid property backends.
|
||||
@@ -56,15 +56,20 @@ pub trait FluidBackend: Send + Sync {
|
||||
/// This method is intended to be implemented by backends capable of natively calculating
|
||||
/// all key parameters (phase, saturation temperatures, qualities, limits) without the user
|
||||
/// needing to query them individually.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fluid` - The fluid identifier
|
||||
/// * `p` - The absolute pressure
|
||||
/// * `h` - The specific enthalpy
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
/// The comprehensive `ThermoState` Snapshot, or an Error.
|
||||
fn full_state(&self, fluid: FluidId, p: Pressure, h: entropyk_core::Enthalpy) -> FluidResult<ThermoState>;
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
p: Pressure,
|
||||
h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<ThermoState>;
|
||||
|
||||
/// Get critical point data for a fluid.
|
||||
///
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! typical thermodynamic ranges (P: 1e3–1e7 Pa, T: 200–600 K).
|
||||
|
||||
use crate::mixture::Mixture;
|
||||
use crate::types::{FluidId, Property, FluidState};
|
||||
use crate::types::{FluidId, FluidState, Property};
|
||||
use lru::LruCache;
|
||||
use std::cell::RefCell;
|
||||
use std::hash::{Hash, Hasher};
|
||||
@@ -27,7 +27,13 @@ const DEFAULT_CAP_NONZERO: NonZeroUsize = NonZeroUsize::new(DEFAULT_CACHE_CAPACI
|
||||
/// (v * 1e9).round() as i64 for Hash-compatible key.
|
||||
#[inline]
|
||||
fn quantize(v: f64) -> i64 {
|
||||
if v.is_nan() || v.is_infinite() {
|
||||
if v.is_nan() {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("[WARN] quantize: NaN value encountered, mapping to 0");
|
||||
0
|
||||
} else if v.is_infinite() {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("[WARN] quantize: Infinite value encountered, mapping to 0");
|
||||
0
|
||||
} else {
|
||||
(v * 1e9).round() as i64
|
||||
@@ -81,9 +87,7 @@ impl CacheKey {
|
||||
pub fn new(backend_id: usize, fluid: &FluidId, property: Property, state: &FluidState) -> Self {
|
||||
let (p, second, variant, mixture_hash) = match state {
|
||||
FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin(), 0u8, None),
|
||||
FluidState::PressureEnthalpy(p, h) => {
|
||||
(p.to_pascals(), h.to_joules_per_kg(), 1u8, None)
|
||||
}
|
||||
FluidState::PressureEnthalpy(p, h) => (p.to_pascals(), h.to_joules_per_kg(), 1u8, None),
|
||||
FluidState::PressureEntropy(p, s) => {
|
||||
(p.to_pascals(), s.to_joules_per_kg_kelvin(), 2u8, None)
|
||||
}
|
||||
@@ -133,8 +137,9 @@ pub fn cache_get(
|
||||
) -> Option<f64> {
|
||||
let key = CacheKey::new(backend_id, fluid, property, state);
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.get(&key).copied()
|
||||
c.try_borrow_mut()
|
||||
.ok()
|
||||
.and_then(|mut cache| cache.get(&key).copied())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,24 +153,30 @@ pub fn cache_insert(
|
||||
) {
|
||||
let key = CacheKey::new(backend_id, fluid, property, state);
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.put(key, value);
|
||||
if let Ok(mut cache) = c.try_borrow_mut() {
|
||||
cache.put(key, value);
|
||||
}
|
||||
// Silently ignore if borrow fails (cache miss is acceptable)
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear the thread-local cache (e.g. at solver iteration boundaries).
|
||||
pub fn cache_clear() {
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.clear();
|
||||
if let Ok(mut cache) = c.try_borrow_mut() {
|
||||
cache.clear();
|
||||
}
|
||||
// Silently ignore if borrow fails
|
||||
});
|
||||
}
|
||||
|
||||
/// Resize the thread-local cache capacity.
|
||||
pub fn cache_resize(capacity: NonZeroUsize) {
|
||||
CACHE.with(|c| {
|
||||
let mut cache = c.borrow_mut();
|
||||
cache.resize(capacity);
|
||||
if let Ok(mut cache) = c.try_borrow_mut() {
|
||||
cache.resize(capacity);
|
||||
}
|
||||
// Silently ignore if borrow fails
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,8 +228,14 @@ mod tests {
|
||||
cache_insert(0, &fluid, Property::Density, &state3, 1200.0);
|
||||
|
||||
assert!(cache_get(0, &fluid, Property::Density, &state1).is_none());
|
||||
assert_eq!(cache_get(0, &fluid, Property::Density, &state2), Some(1100.0));
|
||||
assert_eq!(cache_get(0, &fluid, Property::Density, &state3), Some(1200.0));
|
||||
assert_eq!(
|
||||
cache_get(0, &fluid, Property::Density, &state2),
|
||||
Some(1100.0)
|
||||
);
|
||||
assert_eq!(
|
||||
cache_get(0, &fluid, Property::Density, &state3),
|
||||
Some(1200.0)
|
||||
);
|
||||
|
||||
cache_resize(NonZeroUsize::new(DEFAULT_CACHE_CAPACITY).expect("capacity is non-zero"));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::backend::FluidBackend;
|
||||
use crate::cache::{cache_clear, cache_get, cache_insert};
|
||||
use crate::errors::FluidResult;
|
||||
use crate::types::{CriticalPoint, FluidId, Phase, Property, FluidState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static NEXT_BACKEND_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
@@ -67,7 +67,9 @@ impl<B: FluidBackend> FluidBackend for CachedBackend<B> {
|
||||
if let Some(v) = cache_get(self.backend_id, &fluid, property, &state) {
|
||||
return Ok(v);
|
||||
}
|
||||
let v = self.inner.property(fluid.clone(), property, state.clone())?;
|
||||
let v = self
|
||||
.inner
|
||||
.property(fluid.clone(), property, state.clone())?;
|
||||
cache_insert(self.backend_id, &fluid, property, &state, v);
|
||||
Ok(v)
|
||||
}
|
||||
@@ -88,7 +90,12 @@ impl<B: FluidBackend> FluidBackend for CachedBackend<B> {
|
||||
self.inner.list_fluids()
|
||||
}
|
||||
|
||||
fn full_state(&self, fluid: FluidId, p: entropyk_core::Pressure, h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
p: entropyk_core::Pressure,
|
||||
h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
self.inner.full_state(fluid, p, h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
#[cfg(feature = "coolprop")]
|
||||
use crate::mixture::Mixture;
|
||||
#[cfg(feature = "coolprop")]
|
||||
use crate::backend::FluidBackend;
|
||||
#[cfg(feature = "coolprop")]
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "coolprop")]
|
||||
use std::sync::RwLock;
|
||||
@@ -136,7 +138,7 @@ impl CoolPropBackend {
|
||||
"r32" => "R32".to_string(),
|
||||
"r125" => "R125".to_string(),
|
||||
"r143a" => "R143a".to_string(),
|
||||
"r152a" | "r152a" => "R152A".to_string(),
|
||||
"r152a" => "R152A".to_string(),
|
||||
"r22" => "R22".to_string(),
|
||||
"r23" => "R23".to_string(),
|
||||
"r41" => "R41".to_string(),
|
||||
@@ -219,6 +221,70 @@ impl CoolPropBackend {
|
||||
Property::Pressure => "P",
|
||||
}
|
||||
}
|
||||
/// Property calculation for mixtures.
|
||||
fn property_mixture(
|
||||
&self,
|
||||
_fluid: FluidId,
|
||||
property: Property,
|
||||
state: FluidState,
|
||||
) -> FluidResult<f64> {
|
||||
// Extract mixture from state
|
||||
let mixture = match state {
|
||||
FluidState::PressureTemperatureMixture(_, _, ref m) => m.clone(),
|
||||
FluidState::PressureEnthalpyMixture(_, _, ref m) => m.clone(),
|
||||
FluidState::PressureQualityMixture(_, _, ref m) => m.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if !self.is_mixture_supported(&mixture) {
|
||||
return Err(FluidError::MixtureNotSupported(format!(
|
||||
"One or more components not available: {:?}",
|
||||
mixture.components()
|
||||
)));
|
||||
}
|
||||
|
||||
let cp_string = mixture.to_coolprop_string();
|
||||
let prop_code = Self::property_code(property);
|
||||
|
||||
let result = match state {
|
||||
FluidState::PressureTemperatureMixture(p, t, _) => unsafe {
|
||||
coolprop::props_si_pt(prop_code, p.to_pascals(), t.to_kelvin(), &cp_string)
|
||||
},
|
||||
FluidState::PressureEnthalpyMixture(p, h, _) => unsafe {
|
||||
coolprop::props_si_ph(prop_code, p.to_pascals(), h.to_joules_per_kg(), &cp_string)
|
||||
},
|
||||
FluidState::PressureQualityMixture(p, q, _) => unsafe {
|
||||
coolprop::props_si_px(prop_code, p.to_pascals(), q.value(), &cp_string)
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if result.is_nan() {
|
||||
return Err(FluidError::InvalidState {
|
||||
reason: format!("CoolProp returned NaN for mixture at {:?}", state),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Phase calculation for mixtures.
|
||||
fn phase_mix(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
|
||||
let quality = self.property_mixture(fluid, Property::Quality, state)?;
|
||||
|
||||
if quality < 0.0 {
|
||||
Ok(Phase::Liquid)
|
||||
} else if quality > 1.0 {
|
||||
Ok(Phase::Vapor)
|
||||
} else if (quality - 0.0).abs() < 1e-6 {
|
||||
Ok(Phase::Liquid)
|
||||
} else if (quality - 1.0).abs() < 1e-6 {
|
||||
Ok(Phase::Vapor)
|
||||
} else {
|
||||
Ok(Phase::TwoPhase)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "coolprop")]
|
||||
@@ -408,70 +474,6 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
.all(|c| self.is_fluid_available(&FluidId::new(c)))
|
||||
}
|
||||
|
||||
/// Property calculation for mixtures.
|
||||
fn property_mixture(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
property: Property,
|
||||
state: FluidState,
|
||||
) -> FluidResult<f64> {
|
||||
// Extract mixture from state
|
||||
let mixture = match state {
|
||||
FluidState::PressureTemperatureMixture(_, _, m) => m,
|
||||
FluidState::PressureEnthalpyMixture(_, _, m) => m,
|
||||
FluidState::PressureQualityMixture(_, _, m) => m,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if !self.is_mixture_supported(&mixture) {
|
||||
return Err(FluidError::MixtureNotSupported(format!(
|
||||
"One or more components not available: {:?}",
|
||||
mixture.components()
|
||||
)));
|
||||
}
|
||||
|
||||
let cp_string = mixture.to_coolprop_string();
|
||||
let prop_code = Self::property_code(property);
|
||||
|
||||
let result = match state {
|
||||
FluidState::PressureTemperatureMixture(p, t, _) => unsafe {
|
||||
coolprop::props_si_pt(prop_code, p.to_pascals(), t.to_kelvin(), &cp_string)
|
||||
},
|
||||
FluidState::PressureEnthalpyMixture(p, h, _) => unsafe {
|
||||
coolprop::props_si_ph(prop_code, p.to_pascals(), h.to_joules_per_kg(), &cp_string)
|
||||
},
|
||||
FluidState::PressureQualityMixture(p, q, _) => unsafe {
|
||||
coolprop::props_si_px(prop_code, p.to_pascals(), q.value(), &cp_string)
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if result.is_nan() {
|
||||
return Err(FluidError::InvalidState {
|
||||
reason: format!("CoolProp returned NaN for mixture at {:?}", state),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Phase calculation for mixtures.
|
||||
fn phase_mix(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
|
||||
let quality = self.property_mixture(fluid, Property::Quality, state)?;
|
||||
|
||||
if quality < 0.0 {
|
||||
Ok(Phase::Liquid)
|
||||
} else if quality > 1.0 {
|
||||
Ok(Phase::Vapor)
|
||||
} else if (quality - 0.0).abs() < 1e-6 {
|
||||
Ok(Phase::Liquid)
|
||||
} else if (quality - 1.0).abs() < 1e-6 {
|
||||
Ok(Phase::Vapor)
|
||||
} else {
|
||||
Ok(Phase::TwoPhase)
|
||||
}
|
||||
}
|
||||
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
@@ -510,8 +512,8 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
None
|
||||
};
|
||||
|
||||
let t_bubble = coolprop::props_si_pq("T", p_pa, 0.0, &coolprop_fluid);
|
||||
let t_dew = coolprop::props_si_pq("T", p_pa, 1.0, &coolprop_fluid);
|
||||
let t_bubble = coolprop::props_si_px("T", p_pa, 0.0, &coolprop_fluid);
|
||||
let t_dew = coolprop::props_si_px("T", p_pa, 1.0, &coolprop_fluid);
|
||||
|
||||
let (t_bubble_opt, subcooling) = if !t_bubble.is_nan() {
|
||||
(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use crate::backend::FluidBackend;
|
||||
use crate::damping::{calculate_damping_state, damp_property, should_damp_property, DampingParams};
|
||||
use crate::errors::FluidResult;
|
||||
use crate::types::{CriticalPoint, FluidId, Phase, Property, FluidState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
|
||||
/// Backend wrapper that applies critical point damping to property queries.
|
||||
///
|
||||
@@ -137,7 +137,12 @@ impl<B: FluidBackend> FluidBackend for DampedBackend<B> {
|
||||
self.inner.list_fluids()
|
||||
}
|
||||
|
||||
fn full_state(&self, fluid: FluidId, p: entropyk_core::Pressure, h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
p: entropyk_core::Pressure,
|
||||
h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
self.inner.full_state(fluid, p, h)
|
||||
}
|
||||
}
|
||||
@@ -240,7 +245,12 @@ mod tests {
|
||||
fn list_fluids(&self) -> Vec<FluidId> {
|
||||
vec![FluidId::new("CO2")]
|
||||
}
|
||||
fn full_state(&self, _fluid: FluidId, _p: entropyk_core::Pressure, _h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
fn full_state(
|
||||
&self,
|
||||
_fluid: FluidId,
|
||||
_p: entropyk_core::Pressure,
|
||||
_h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
Err(FluidError::CoolPropError(
|
||||
"full_state not supported on NaNBackend".to_string(),
|
||||
))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! C1-continuous damping to prevent NaN values in derivative properties (Cp, Cv, etc.)
|
||||
//! that diverge near the critical point.
|
||||
|
||||
use crate::types::{CriticalPoint, FluidId, Property, FluidState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Property};
|
||||
|
||||
/// Parameters for critical point damping.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -434,8 +434,7 @@ mod tests {
|
||||
for d in distances {
|
||||
let t = 304.13 * (1.0 + d);
|
||||
let p = 7.3773e6 * (1.0 + d);
|
||||
let state =
|
||||
FluidState::from_pt(Pressure::from_pascals(p), Temperature::from_kelvin(t));
|
||||
let state = FluidState::from_pt(Pressure::from_pascals(p), Temperature::from_kelvin(t));
|
||||
let damping = calculate_damping_state(&FluidId::new("CO2"), &state, &cp, ¶ms);
|
||||
|
||||
let blend = damping.blend_factor;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use crate::backend::FluidBackend;
|
||||
use crate::errors::{FluidError, FluidResult};
|
||||
use crate::types::{CriticalPoint, FluidId, Phase, Property, FluidState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
|
||||
/// Incompressible fluid identifier.
|
||||
///
|
||||
@@ -200,9 +200,7 @@ impl IncompressibleBackend {
|
||||
// EG 30%: ~3900, EG 50%: ~3400 J/(kg·K) at 20°C
|
||||
Ok(4184.0 * (1.0 - concentration) + 2400.0 * concentration)
|
||||
}
|
||||
(Property::Cp, false) => {
|
||||
Ok(4184.0 * (1.0 - concentration) + 2500.0 * concentration)
|
||||
}
|
||||
(Property::Cp, false) => Ok(4184.0 * (1.0 - concentration) + 2500.0 * concentration),
|
||||
(Property::Viscosity, _) => {
|
||||
// Viscosity increases strongly with concentration and decreases with T
|
||||
let mu_water = water_viscosity_kelvin(t_k);
|
||||
@@ -316,8 +314,17 @@ impl FluidBackend for IncompressibleBackend {
|
||||
]
|
||||
}
|
||||
|
||||
fn full_state(&self, fluid: FluidId, p: entropyk_core::Pressure, h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
let t_k = self.property(fluid.clone(), Property::Temperature, FluidState::from_ph(p, h))?;
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
p: entropyk_core::Pressure,
|
||||
h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
let t_k = self.property(
|
||||
fluid.clone(),
|
||||
Property::Temperature,
|
||||
FluidState::from_ph(p, h),
|
||||
)?;
|
||||
Err(FluidError::UnsupportedProperty {
|
||||
property: format!("full_state for IncompressibleBackend: Temperature is {:.2} K but full state not natively implemented yet", t_k),
|
||||
})
|
||||
@@ -353,18 +360,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_water_density_at_temperatures() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state_20 = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(20.0),
|
||||
);
|
||||
let state_50 = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(50.0),
|
||||
);
|
||||
let state_80 = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(80.0),
|
||||
);
|
||||
let state_20 =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
let state_50 =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(50.0));
|
||||
let state_80 =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(80.0));
|
||||
|
||||
let rho_20 = backend
|
||||
.property(FluidId::new("Water"), Property::Density, state_20)
|
||||
@@ -385,10 +386,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_water_cp_accuracy() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(20.0),
|
||||
);
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
let cp = backend
|
||||
.property(FluidId::new("Water"), Property::Cp, state)
|
||||
.unwrap();
|
||||
@@ -399,14 +397,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_water_out_of_range() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state_cold = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(-10.0),
|
||||
);
|
||||
let state_hot = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(150.0),
|
||||
);
|
||||
let state_cold =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(-10.0));
|
||||
let state_hot =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(150.0));
|
||||
|
||||
assert!(backend
|
||||
.property(FluidId::new("Water"), Property::Density, state_cold)
|
||||
@@ -433,14 +427,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_water_enthalpy_reference() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state_0 = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(0.0),
|
||||
);
|
||||
let state_20 = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(20.0),
|
||||
);
|
||||
let state_0 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(0.0));
|
||||
let state_20 =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
let h_0 = backend
|
||||
.property(FluidId::new("Water"), Property::Enthalpy, state_0)
|
||||
.unwrap();
|
||||
@@ -449,30 +438,47 @@ mod tests {
|
||||
.unwrap();
|
||||
// h = Cp * (T - 273.15) relative to 0°C: h_0 ≈ 0, h_20 ≈ 4184 * 20 = 83680 J/kg
|
||||
assert!(h_0.abs() < 1.0, "h at 0°C should be ~0");
|
||||
assert!((h_20 - 83680.0).abs() / 83680.0 < 0.01, "h at 20°C={}", h_20);
|
||||
assert!(
|
||||
(h_20 - 83680.0).abs() / 83680.0 < 0.01,
|
||||
"h at 20°C={}",
|
||||
h_20
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glycol_concentration_effect() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(20.0),
|
||||
);
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
let rho_water = backend
|
||||
.property(FluidId::new("Water"), Property::Density, state.clone())
|
||||
.unwrap();
|
||||
let rho_eg30 = backend
|
||||
.property(FluidId::new("EthyleneGlycol30"), Property::Density, state.clone())
|
||||
.property(
|
||||
FluidId::new("EthyleneGlycol30"),
|
||||
Property::Density,
|
||||
state.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let rho_eg50 = backend
|
||||
.property(FluidId::new("EthyleneGlycol50"), Property::Density, state.clone())
|
||||
.property(
|
||||
FluidId::new("EthyleneGlycol50"),
|
||||
Property::Density,
|
||||
state.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let cp_eg30 = backend
|
||||
.property(FluidId::new("EthyleneGlycol30"), Property::Cp, state.clone())
|
||||
.property(
|
||||
FluidId::new("EthyleneGlycol30"),
|
||||
Property::Cp,
|
||||
state.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let cp_eg50 = backend
|
||||
.property(FluidId::new("EthyleneGlycol50"), Property::Cp, state.clone())
|
||||
.property(
|
||||
FluidId::new("EthyleneGlycol50"),
|
||||
Property::Cp,
|
||||
state.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
// Higher concentration → higher density, lower Cp (ASHRAE)
|
||||
assert!(rho_eg30 > rho_water && rho_eg50 > rho_eg30);
|
||||
@@ -482,29 +488,30 @@ mod tests {
|
||||
#[test]
|
||||
fn test_glycol_out_of_range() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state_cold = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(-40.0),
|
||||
);
|
||||
let state_hot = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(150.0),
|
||||
);
|
||||
let state_cold =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(-40.0));
|
||||
let state_hot =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(150.0));
|
||||
assert!(backend
|
||||
.property(FluidId::new("EthyleneGlycol30"), Property::Density, state_cold)
|
||||
.property(
|
||||
FluidId::new("EthyleneGlycol30"),
|
||||
Property::Density,
|
||||
state_cold
|
||||
)
|
||||
.is_err());
|
||||
assert!(backend
|
||||
.property(FluidId::new("EthyleneGlycol30"), Property::Density, state_hot)
|
||||
.property(
|
||||
FluidId::new("EthyleneGlycol30"),
|
||||
Property::Density,
|
||||
state_hot
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_humid_air_psychrometrics() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(20.0),
|
||||
);
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
let cp = backend
|
||||
.property(FluidId::new("HumidAir"), Property::Cp, state.clone())
|
||||
.unwrap();
|
||||
@@ -519,10 +526,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_phase_humid_air_is_vapor() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(20.0),
|
||||
);
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
let phase = backend.phase(FluidId::new("HumidAir"), state).unwrap();
|
||||
assert_eq!(phase, Phase::Vapor);
|
||||
}
|
||||
@@ -530,10 +534,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_nan_temperature_rejected() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_kelvin(f64::NAN),
|
||||
);
|
||||
let state =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_kelvin(f64::NAN));
|
||||
assert!(backend
|
||||
.property(FluidId::new("Water"), Property::Density, state)
|
||||
.is_err());
|
||||
@@ -542,20 +544,26 @@ mod tests {
|
||||
#[test]
|
||||
fn test_glycol_properties() {
|
||||
let backend = IncompressibleBackend::new();
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(20.0),
|
||||
);
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
|
||||
|
||||
let rho_eg30 = backend
|
||||
.property(FluidId::new("EthyleneGlycol30"), Property::Density, state.clone())
|
||||
.property(
|
||||
FluidId::new("EthyleneGlycol30"),
|
||||
Property::Density,
|
||||
state.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let rho_water = backend
|
||||
.property(FluidId::new("Water"), Property::Density, state.clone())
|
||||
.unwrap();
|
||||
|
||||
// EG 30% should be denser than water
|
||||
assert!(rho_eg30 > rho_water, "EG30 ρ={} should be > water ρ={}", rho_eg30, rho_water);
|
||||
assert!(
|
||||
rho_eg30 > rho_water,
|
||||
"EG30 ρ={} should be > water ρ={}",
|
||||
rho_eg30,
|
||||
rho_water
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -565,10 +573,7 @@ mod tests {
|
||||
let inner = IncompressibleBackend::new();
|
||||
let backend = CachedBackend::new(inner);
|
||||
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_bar(1.0),
|
||||
Temperature::from_celsius(25.0),
|
||||
);
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
|
||||
let rho = backend
|
||||
.property(FluidId::new("Water"), Property::Density, state)
|
||||
|
||||
@@ -62,8 +62,10 @@ pub use coolprop::CoolPropBackend;
|
||||
pub use damped_backend::DampedBackend;
|
||||
pub use damping::{DampingParams, DampingState};
|
||||
pub use errors::{FluidError, FluidResult};
|
||||
pub use incompressible::{IncompFluid, IncompressibleBackend, ValidRange};
|
||||
pub use mixture::{Mixture, MixtureError};
|
||||
pub use tabular_backend::TabularBackend;
|
||||
pub use test_backend::TestBackend;
|
||||
pub use incompressible::{IncompFluid, IncompressibleBackend, ValidRange};
|
||||
pub use types::{CriticalPoint, Entropy, FluidId, Phase, Property, Quality, FluidState, ThermoState};
|
||||
pub use types::{
|
||||
CriticalPoint, Entropy, FluidId, FluidState, Phase, Property, Quality, ThermoState,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::errors::{FluidError, FluidResult};
|
||||
use crate::tabular::FluidTable;
|
||||
#[allow(unused_imports)]
|
||||
use crate::types::Entropy;
|
||||
use crate::types::{CriticalPoint, FluidId, Phase, Property, FluidState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -406,15 +406,15 @@ mod tests {
|
||||
let state_pt =
|
||||
FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
let rho_t = tabular
|
||||
.property(fluid.clone(), Property::Density, state_pt)
|
||||
.property(fluid.clone(), Property::Density, state_pt.clone())
|
||||
.unwrap();
|
||||
let rho_c = coolprop
|
||||
.property(fluid.clone(), Property::Density, state_pt)
|
||||
.property(fluid.clone(), Property::Density, state_pt.clone())
|
||||
.unwrap();
|
||||
assert_relative_eq!(rho_t, rho_c, epsilon = 0.01 * rho_c.max(1.0));
|
||||
|
||||
let h_t = tabular
|
||||
.property(fluid.clone(), Property::Enthalpy, state_pt)
|
||||
.property(fluid.clone(), Property::Enthalpy, state_pt.clone())
|
||||
.unwrap();
|
||||
let h_c = coolprop
|
||||
.property(fluid.clone(), Property::Enthalpy, state_pt)
|
||||
@@ -427,7 +427,7 @@ mod tests {
|
||||
entropyk_core::Enthalpy::from_kilojoules_per_kg(415.0),
|
||||
);
|
||||
let rho_t_ph = tabular
|
||||
.property(fluid.clone(), Property::Density, state_ph)
|
||||
.property(fluid.clone(), Property::Density, state_ph.clone())
|
||||
.unwrap();
|
||||
let rho_c_ph = coolprop
|
||||
.property(fluid.clone(), Property::Density, state_ph)
|
||||
@@ -437,7 +437,7 @@ mod tests {
|
||||
// (P, x) at 500 kPa, x = 0.5
|
||||
let state_px = FluidState::from_px(Pressure::from_pascals(500_000.0), Quality::new(0.5));
|
||||
let h_t_px = tabular
|
||||
.property(fluid.clone(), Property::Enthalpy, state_px)
|
||||
.property(fluid.clone(), Property::Enthalpy, state_px.clone())
|
||||
.unwrap();
|
||||
let h_c_px = coolprop
|
||||
.property(fluid.clone(), Property::Enthalpy, state_px)
|
||||
@@ -534,8 +534,17 @@ impl FluidBackend for TabularBackend {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn full_state(&self, fluid: FluidId, p: entropyk_core::Pressure, h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
let t_k = self.property(fluid.clone(), Property::Temperature, FluidState::from_ph(p, h))?;
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
p: entropyk_core::Pressure,
|
||||
h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
let t_k = self.property(
|
||||
fluid.clone(),
|
||||
Property::Temperature,
|
||||
FluidState::from_ph(p, h),
|
||||
)?;
|
||||
Err(FluidError::UnsupportedProperty {
|
||||
property: format!("full_state for TabularBackend: Temperature is {:.2} K", t_k),
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::backend::FluidBackend;
|
||||
use crate::errors::{FluidError, FluidResult};
|
||||
#[cfg(test)]
|
||||
use crate::mixture::Mixture;
|
||||
use crate::types::{CriticalPoint, FluidId, Phase, Property, FluidState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
use entropyk_core::{Pressure, Temperature};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -294,8 +294,17 @@ impl FluidBackend for TestBackend {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn full_state(&self, fluid: FluidId, p: entropyk_core::Pressure, h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
let t_k = self.property(fluid.clone(), Property::Temperature, FluidState::from_ph(p, h))?;
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
p: entropyk_core::Pressure,
|
||||
h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
let t_k = self.property(
|
||||
fluid.clone(),
|
||||
Property::Temperature,
|
||||
FluidState::from_ph(p, h),
|
||||
)?;
|
||||
Err(FluidError::UnsupportedProperty {
|
||||
property: format!("full_state for TestBackend: Temperature is {:.2} K", t_k),
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! fluid identifiers, and properties in the fluid backend system.
|
||||
|
||||
use crate::mixture::Mixture;
|
||||
pub use entropyk_core::Entropy;
|
||||
use entropyk_core::{Enthalpy, Pressure, Temperature};
|
||||
use std::fmt;
|
||||
|
||||
@@ -16,7 +17,7 @@ impl TemperatureDelta {
|
||||
pub fn new(kelvin_diff: f64) -> Self {
|
||||
TemperatureDelta(kelvin_diff)
|
||||
}
|
||||
|
||||
|
||||
/// Gets the temperature difference in Kelvin.
|
||||
pub fn kelvin(&self) -> f64 {
|
||||
self.0
|
||||
@@ -29,8 +30,8 @@ impl From<f64> for TemperatureDelta {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier for a fluid.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
/// Unique identifier for a fluid (e.g., "R410A", "Water", "Air").
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FluidId(pub String);
|
||||
|
||||
impl FluidId {
|
||||
@@ -38,6 +39,16 @@ impl FluidId {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
FluidId(name.into())
|
||||
}
|
||||
|
||||
/// Returns the fluid name as a string slice.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Consumes the FluidId and returns the inner string.
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FluidId {
|
||||
@@ -46,6 +57,12 @@ impl fmt::Display for FluidId {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for FluidId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for FluidId {
|
||||
fn from(s: &str) -> Self {
|
||||
FluidId(s.to_string())
|
||||
@@ -177,28 +194,6 @@ impl FluidState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Entropy in J/(kg·K).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Entropy(pub f64);
|
||||
|
||||
impl Entropy {
|
||||
/// Creates entropy from J/(kg·K).
|
||||
pub fn from_joules_per_kg_kelvin(value: f64) -> Self {
|
||||
Entropy(value)
|
||||
}
|
||||
|
||||
/// Returns entropy in J/(kg·K).
|
||||
pub fn to_joules_per_kg_kelvin(&self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Entropy {
|
||||
fn from(value: f64) -> Self {
|
||||
Entropy(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Quality (vapor fraction) from 0 (saturated liquid) to 1 (saturated vapor).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Quality(pub f64);
|
||||
@@ -318,11 +313,49 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_fluid_id() {
|
||||
fn test_new() {
|
||||
let id = FluidId::new("R134a");
|
||||
assert_eq!(id.0, "R134a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let id: FluidId = "R410A".into();
|
||||
assert_eq!(id.0, "R410A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let id: FluidId = String::from("R744").into();
|
||||
assert_eq!(id.0, "R744");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_str() {
|
||||
let id = FluidId::new("Water");
|
||||
assert_eq!(id.as_str(), "Water");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_inner() {
|
||||
let id = FluidId::new("Air");
|
||||
let inner = id.into_inner();
|
||||
assert_eq!(inner, "Air");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_ref() {
|
||||
let id = FluidId::new("R1234yf");
|
||||
let s: &str = id.as_ref();
|
||||
assert_eq!(s, "R1234yf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let id = FluidId::new("R32");
|
||||
assert_eq!(format!("{}", id), "R32");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fluid_state_from_pt() {
|
||||
let p = Pressure::from_bar(1.0);
|
||||
|
||||
Reference in New Issue
Block a user