chore: remove deprecated flow_boundary and update docs to match new architecture

This commit is contained in:
Sepehr
2026-03-01 20:00:09 +01:00
parent 20700afce8
commit d88914a44f
105 changed files with 11222 additions and 2994 deletions

View File

@@ -14,10 +14,12 @@ serde.workspace = true
serde_json = "1.0"
lru = "0.12"
entropyk-coolprop-sys = { path = "coolprop-sys", optional = true }
libloading = { version = "0.8", optional = true }
[features]
default = []
coolprop = ["entropyk-coolprop-sys"]
dll = ["libloading"]
[dev-dependencies]
approx = "0.5"

View File

@@ -5,13 +5,13 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use entropyk_core::{Pressure, Temperature};
use entropyk_fluids::{CachedBackend, FluidBackend, FluidId, Property, TestBackend, ThermoState};
use entropyk_fluids::{CachedBackend, FluidBackend, FluidId, FluidState, Property, TestBackend};
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 = FluidState::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| {
@@ -30,7 +30,7 @@ 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 = FluidState::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| {

View File

@@ -1,6 +1,7 @@
//! Build script for coolprop-sys.
//!
//! This compiles the CoolProp C++ library statically.
//! Supports macOS, Linux, and Windows.
use std::env;
use std::path::PathBuf;
@@ -9,10 +10,12 @@ 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").canonicalize().unwrap_or(PathBuf::from("../../../vendor/coolprop")),
PathBuf::from("../../vendor/coolprop")
.canonicalize()
.unwrap_or(PathBuf::from("../../../vendor/coolprop")),
// External directory
PathBuf::from("external/coolprop"),
// System paths
// System paths (Unix)
PathBuf::from("/usr/local/src/CoolProp"),
PathBuf::from("/opt/CoolProp"),
];
@@ -23,7 +26,7 @@ fn coolprop_src_path() -> Option<PathBuf> {
}
fn main() {
let static_linking = env::var("CARGO_FEATURE_STATIC").is_ok() || true; // Force static linking for python wheels
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
// Check if CoolProp source is available
if let Some(coolprop_path) = coolprop_src_path() {
@@ -40,41 +43,67 @@ fn main() {
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
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());
if target_os == "macos" {
println!(
"cargo:rustc-link-arg=-Wl,-force_load,{}/build/libCoolProp.a",
dst.display()
);
}
} else {
println!(
"cargo:warning=CoolProp source not found in vendor/.
For full static build, run:
git clone https://github.com/CoolProp/CoolProp.git vendor/coolprop"
"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");
if target_os == "windows" {
// On Windows, try to find CoolProp as a system library
println!("cargo:rustc-link-lib=CoolProp");
} else {
println!("cargo:rustc-link-lib=dylib=CoolProp");
println!("cargo:rustc-link-lib=static=CoolProp");
}
}
// 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++");
match target_os.as_str() {
"macos" => {
println!("cargo:rustc-link-lib=dylib=c++");
}
"linux" | "freebsd" | "openbsd" | "netbsd" => {
println!("cargo:rustc-link-lib=dylib=stdc++");
}
"windows" => {
// MSVC links the C++ runtime automatically; nothing to do.
// For MinGW, stdc++ is needed but MinGW is less common.
}
_ => {
// Best guess for unknown Unix-like targets
println!("cargo:rustc-link-lib=dylib=stdc++");
}
}
println!("cargo:rustc-link-lib=dylib=m");
// Link libm (only on Unix; on Windows it's part of the CRT)
if target_os != "windows" {
println!("cargo:rustc-link-lib=dylib=m");
}
// Force export symbols for Python extension (macOS only)
if target_os == "macos" {
println!("cargo:rustc-link-arg=-Wl,-all_load");
}
// Linux equivalent (only for shared library builds, e.g., Python wheels)
// Note: --whole-archive must bracket the static lib; the linker handles this
// automatically for Rust cdylib targets, so we don't need it here.
// 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");
}

View File

@@ -149,7 +149,7 @@ extern "C" {
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_stringNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE")]
#[cfg_attr(target_os = "macos", link_name = "\x01__Z23get_global_param_stringPKcPci")]
#[cfg_attr(not(target_os = "macos"), link_name = "get_global_param_string")]
fn get_global_param_string(
Param: *const c_char,
@@ -158,7 +158,7 @@ extern "C" {
) -> c_int;
/// Get fluid info
#[cfg_attr(target_os = "macos", link_name = "\x01__Z22get_fluid_param_stringNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES5_")]
#[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")]
fn get_fluid_param_string(
Fluid: *const c_char,

View File

@@ -0,0 +1,472 @@
//! Runtime-loaded shared library backend for fluid properties.
//!
//! This module provides a `DllBackend` that loads a CoolProp-compatible shared
//! library (`.dll`, `.so`, `.dylib`) at **runtime** via `libloading`.
//!
//! Unlike `CoolPropBackend` (which requires compile-time C++ linking), this
//! backend has **zero native build dependencies** — the user just needs to
//! place the pre-built shared library in a known location.
//!
//! # Supported Libraries
//!
//! Any shared library that exports the standard CoolProp C API:
//! - `PropsSI(Output, Name1, Value1, Name2, Value2, FluidName) -> f64`
//! - `Props1SI(FluidName, Output) -> f64`
//!
//! This includes:
//! - CoolProp shared library (`libCoolProp.so`, `CoolProp.dll`, `libCoolProp.dylib`)
//! - REFPROP via CoolProp wrapper DLL
//! - Any custom wrapper exposing the same C ABI
//!
//! # Example
//!
//! ```rust,no_run
//! use entropyk_fluids::DllBackend;
//! use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property};
//! use entropyk_core::{Pressure, Temperature};
//!
//! // Load from explicit path
//! let backend = DllBackend::load("/usr/local/lib/libCoolProp.so").unwrap();
//!
//! // Or search system paths
//! let backend = DllBackend::load_system_default().unwrap();
//!
//! let density = backend.property(
//! FluidId::new("R134a"),
//! Property::Density,
//! FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0)),
//! ).unwrap();
//! ```
use std::ffi::CString;
use std::path::Path;
use libloading::{Library, Symbol};
use crate::backend::FluidBackend;
use crate::errors::{FluidError, FluidResult};
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property, ThermoState};
/// Type alias for the CoolProp `PropsSI` C function signature.
///
/// ```c
/// double PropsSI(const char* Output, const char* Name1, double Value1,
/// const char* Name2, double Value2, const char* FluidName);
/// ```
type PropsSiFn = unsafe extern "C" fn(
*const std::os::raw::c_char, // Output
*const std::os::raw::c_char, // Name1
f64, // Value1
*const std::os::raw::c_char, // Name2
f64, // Value2
*const std::os::raw::c_char, // FluidName
) -> f64;
/// Type alias for the CoolProp `Props1SI` C function signature.
///
/// ```c
/// double Props1SI(const char* FluidName, const char* Output);
/// ```
type Props1SiFn = unsafe extern "C" fn(
*const std::os::raw::c_char, // FluidName
*const std::os::raw::c_char, // Output
) -> f64;
/// A fluid property backend that loads a CoolProp-compatible shared library at runtime.
///
/// This avoids compile-time C++ dependencies entirely. The user provides the
/// path to a pre-built `.dll`/`.so`/`.dylib` and this backend loads the
/// `PropsSI` and `Props1SI` symbols dynamically.
pub struct DllBackend {
/// The loaded shared library handle. Kept alive for the lifetime of the backend.
_lib: Library,
/// Function pointer to `PropsSI`.
props_si: PropsSiFn,
/// Function pointer to `Props1SI`.
props1_si: Props1SiFn,
}
// SAFETY: The loaded library functions are thread-safe (CoolProp is reentrant
// for property queries). The Library handle must remain alive.
unsafe impl Send for DllBackend {}
unsafe impl Sync for DllBackend {}
impl DllBackend {
/// Load a CoolProp-compatible shared library from the given path.
///
/// The library must export `PropsSI` and `Props1SI` with the standard
/// CoolProp C ABI.
///
/// # Arguments
///
/// * `path` - Path to the shared library file
///
/// # Errors
///
/// Returns `FluidError::DllLoadError` if the library cannot be opened
/// or the required symbols are not found.
pub fn load<P: AsRef<Path>>(path: P) -> FluidResult<Self> {
let path = path.as_ref();
// SAFETY: Loading a shared library is inherently unsafe — the library
// must be a valid CoolProp-compatible binary for the current platform.
let lib = unsafe { Library::new(path) }.map_err(|e| FluidError::CoolPropError(
format!("Failed to load shared library '{}': {}", path.display(), e),
))?;
// Load PropsSI symbol
let props_si: PropsSiFn = unsafe {
let sym: Symbol<PropsSiFn> = lib.get(b"PropsSI\0").map_err(|e| {
FluidError::CoolPropError(format!(
"Symbol 'PropsSI' not found in '{}': {}. \
Make sure this is a CoolProp shared library built with C exports.",
path.display(),
e
))
})?;
*sym
};
// Load Props1SI symbol
let props1_si: Props1SiFn = unsafe {
let sym: Symbol<Props1SiFn> = lib.get(b"Props1SI\0").map_err(|e| {
FluidError::CoolPropError(format!(
"Symbol 'Props1SI' not found in '{}': {}",
path.display(),
e
))
})?;
*sym
};
Ok(Self {
_lib: lib,
props_si,
props1_si,
})
}
/// Search common system paths for a CoolProp shared library and load it.
///
/// Search order:
/// 1. `COOLPROP_LIB` environment variable (explicit override)
/// 2. Current directory
/// 3. System library paths (`/usr/local/lib`, etc.)
///
/// # Errors
///
/// Returns `FluidError::CoolPropError` if no CoolProp library is found.
pub fn load_system_default() -> FluidResult<Self> {
// 1. Check environment variable
if let Ok(path) = std::env::var("COOLPROP_LIB") {
if Path::new(&path).exists() {
return Self::load(&path);
}
}
// 2. Try common library names (OS-specific)
let lib_names = if cfg!(target_os = "windows") {
vec!["CoolProp.dll", "libCoolProp.dll"]
} else if cfg!(target_os = "macos") {
vec!["libCoolProp.dylib"]
} else {
vec!["libCoolProp.so"]
};
// Common search directories
let search_dirs: Vec<&str> = if cfg!(target_os = "windows") {
vec![".", "C:\\CoolProp", "C:\\Program Files\\CoolProp"]
} else {
vec![
".",
"/usr/local/lib",
"/usr/lib",
"/opt/coolprop/lib",
"/usr/local/lib/coolprop",
]
};
for dir in &search_dirs {
for name in &lib_names {
let path = Path::new(dir).join(name);
if path.exists() {
return Self::load(&path);
}
}
}
Err(FluidError::CoolPropError(
"CoolProp shared library not found. \
Set COOLPROP_LIB environment variable to the library path, \
or place it in a standard system library directory. \
Download from: https://github.com/CoolProp/CoolProp/releases"
.to_string(),
))
}
// ========================================================================
// Internal helpers that call the loaded function pointers
// ========================================================================
/// Call PropsSI(Output, Name1, Value1, Name2, Value2, Fluid).
fn call_props_si(
&self,
output: &str,
name1: &str,
value1: f64,
name2: &str,
value2: f64,
fluid: &str,
) -> FluidResult<f64> {
let c_output = CString::new(output).unwrap();
let c_name1 = CString::new(name1).unwrap();
let c_name2 = CString::new(name2).unwrap();
let c_fluid = CString::new(fluid).unwrap();
let result = unsafe {
(self.props_si)(
c_output.as_ptr(),
c_name1.as_ptr(),
value1,
c_name2.as_ptr(),
value2,
c_fluid.as_ptr(),
)
};
if result.is_nan() || result.is_infinite() {
return Err(FluidError::InvalidState {
reason: format!(
"DllBackend: PropsSI returned invalid value for {}({}, {}={}, {}={}, {})",
output, fluid, name1, value1, name2, value2, fluid
),
});
}
Ok(result)
}
/// Call Props1SI(Fluid, Output) for single-parameter queries (e.g., Tcrit).
fn call_props1_si(&self, fluid: &str, output: &str) -> FluidResult<f64> {
let c_fluid = CString::new(fluid).unwrap();
let c_output = CString::new(output).unwrap();
let result = unsafe { (self.props1_si)(c_fluid.as_ptr(), c_output.as_ptr()) };
if result.is_nan() || result.is_infinite() {
return Err(FluidError::InvalidState {
reason: format!(
"DllBackend: Props1SI returned invalid value for {}({})",
output, fluid
),
});
}
Ok(result)
}
/// Convert a `Property` enum to a CoolProp output code string.
fn property_code(property: Property) -> &'static str {
match property {
Property::Density => "D",
Property::Enthalpy => "H",
Property::Entropy => "S",
Property::InternalEnergy => "U",
Property::Cp => "C",
Property::Cv => "O",
Property::SpeedOfSound => "A",
Property::Viscosity => "V",
Property::ThermalConductivity => "L",
Property::SurfaceTension => "I",
Property::Quality => "Q",
Property::Temperature => "T",
Property::Pressure => "P",
}
}
}
impl FluidBackend for DllBackend {
fn property(
&self,
fluid: FluidId,
property: Property,
state: FluidState,
) -> FluidResult<f64> {
let prop_code = Self::property_code(property);
let fluid_name = &fluid.0;
match state {
FluidState::PressureTemperature(p, t) => {
self.call_props_si(prop_code, "P", p.to_pascals(), "T", t.to_kelvin(), fluid_name)
}
FluidState::PressureEnthalpy(p, h) => self.call_props_si(
prop_code,
"P",
p.to_pascals(),
"H",
h.to_joules_per_kg(),
fluid_name,
),
FluidState::PressureQuality(p, q) => {
self.call_props_si(prop_code, "P", p.to_pascals(), "Q", q.value(), fluid_name)
}
FluidState::PressureEntropy(_p, _s) => Err(FluidError::UnsupportedProperty {
property: "P-S state not directly supported".to_string(),
}),
// Mixture states: build CoolProp mixture string
FluidState::PressureTemperatureMixture(p, t, ref mix) => {
let cp_string = mix.to_coolprop_string();
self.call_props_si(prop_code, "P", p.to_pascals(), "T", t.to_kelvin(), &cp_string)
}
FluidState::PressureEnthalpyMixture(p, h, ref mix) => {
let cp_string = mix.to_coolprop_string();
self.call_props_si(
prop_code,
"P",
p.to_pascals(),
"H",
h.to_joules_per_kg(),
&cp_string,
)
}
FluidState::PressureQualityMixture(p, q, ref mix) => {
let cp_string = mix.to_coolprop_string();
self.call_props_si(prop_code, "P", p.to_pascals(), "Q", q.value(), &cp_string)
}
}
}
fn critical_point(&self, fluid: FluidId) -> FluidResult<CriticalPoint> {
let name = &fluid.0;
let tc = self.call_props1_si(name, "Tcrit")?;
let pc = self.call_props1_si(name, "pcrit")?;
let dc = self.call_props1_si(name, "rhocrit")?;
Ok(CriticalPoint::new(
entropyk_core::Temperature::from_kelvin(tc),
entropyk_core::Pressure::from_pascals(pc),
dc,
))
}
fn is_fluid_available(&self, fluid: &FluidId) -> bool {
self.call_props1_si(&fluid.0, "Tcrit").is_ok()
}
fn phase(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
let quality = self.property(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 list_fluids(&self) -> Vec<FluidId> {
// Common refrigerants — we check availability dynamically
let candidates = [
"R134a", "R410A", "R32", "R1234yf", "R1234ze(E)", "R454B", "R513A", "R290", "R744",
"R717", "Water", "Air", "CO2", "Ammonia", "Propane", "R404A", "R407C", "R22",
];
candidates
.iter()
.copied()
.filter(|name| self.is_fluid_available(&FluidId::new(*name)))
.map(|name| FluidId::new(name))
.collect()
}
fn full_state(
&self,
fluid: FluidId,
p: entropyk_core::Pressure,
h: entropyk_core::Enthalpy,
) -> FluidResult<ThermoState> {
let name = &fluid.0;
let p_pa = p.to_pascals();
let h_j_kg = h.to_joules_per_kg();
let t_k = self.call_props_si("T", "P", p_pa, "H", h_j_kg, name)?;
let s = self.call_props_si("S", "P", p_pa, "H", h_j_kg, name)?;
let d = self.call_props_si("D", "P", p_pa, "H", h_j_kg, name)?;
let q = self
.call_props_si("Q", "P", p_pa, "H", h_j_kg, name)
.unwrap_or(f64::NAN);
let phase = self.phase(
fluid.clone(),
FluidState::from_ph(p, h),
)?;
let quality = if (0.0..=1.0).contains(&q) {
Some(crate::types::Quality::new(q))
} else {
None
};
// Saturation temperatures (may fail for supercritical states)
let t_bubble = self.call_props_si("T", "P", p_pa, "Q", 0.0, name).ok();
let t_dew = self.call_props_si("T", "P", p_pa, "Q", 1.0, name).ok();
let subcooling = t_bubble.and_then(|tb| {
if t_k < tb {
Some(crate::types::TemperatureDelta::new(tb - t_k))
} else {
None
}
});
let superheat = t_dew.and_then(|td| {
if t_k > td {
Some(crate::types::TemperatureDelta::new(t_k - td))
} else {
None
}
});
Ok(ThermoState {
fluid,
pressure: p,
temperature: entropyk_core::Temperature::from_kelvin(t_k),
enthalpy: h,
entropy: crate::types::Entropy::from_joules_per_kg_kelvin(s),
density: d,
phase,
quality,
superheat,
subcooling,
t_bubble: t_bubble.map(entropyk_core::Temperature::from_kelvin),
t_dew: t_dew.map(entropyk_core::Temperature::from_kelvin),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_nonexistent_library() {
let result = DllBackend::load("/nonexistent/path/libCoolProp.so");
assert!(result.is_err());
}
#[test]
fn test_load_system_default_graceful_error() {
// In CI/test environments, CoolProp DLL is typically not installed.
// This should return a clean error, not panic.
let result = DllBackend::load_system_default();
// We don't assert is_err() because the user might have it installed;
// we just verify it doesn't panic.
let _ = result;
}
}

View File

@@ -48,6 +48,8 @@ pub mod cached_backend;
pub mod coolprop;
pub mod damped_backend;
pub mod damping;
#[cfg(feature = "dll")]
pub mod dll_backend;
pub mod errors;
pub mod incompressible;
pub mod mixture;
@@ -60,6 +62,8 @@ pub use backend::FluidBackend;
pub use cached_backend::CachedBackend;
pub use coolprop::CoolPropBackend;
pub use damped_backend::DampedBackend;
#[cfg(feature = "dll")]
pub use dll_backend::DllBackend;
pub use damping::{DampingParams, DampingState};
pub use errors::{FluidError, FluidResult};
pub use incompressible::{IncompFluid, IncompressibleBackend, ValidRange};

View File

@@ -230,7 +230,7 @@ mod tests {
use crate::backend::FluidBackend;
use crate::coolprop::CoolPropBackend;
use crate::tabular_backend::TabularBackend;
use crate::types::{FluidId, Property, ThermoState};
use crate::types::{FluidId, FluidState, Property};
use approx::assert_relative_eq;
use entropyk_core::{Pressure, Temperature};
@@ -248,12 +248,12 @@ mod tests {
let fluid = FluidId::new("R134a");
// Spot check: grid point (200 kPa, 290 K)
let state = ThermoState::from_pt(
let state = FluidState::from_pt(
Pressure::from_pascals(200_000.0),
Temperature::from_kelvin(290.0),
);
let rho_t = tabular
.property(fluid.clone(), Property::Density, state)
.property(fluid.clone(), Property::Density, state.clone())
.unwrap();
let rho_c = coolprop
.property(fluid.clone(), Property::Density, state)
@@ -261,9 +261,9 @@ mod tests {
assert_relative_eq!(rho_t, rho_c, epsilon = 0.0001 * rho_c.max(1.0));
// Spot check: interpolated point (1 bar, 25°C)
let state2 = ThermoState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
let state2 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
let h_t = tabular
.property(fluid.clone(), Property::Enthalpy, state2)
.property(fluid.clone(), Property::Enthalpy, state2.clone())
.unwrap();
let h_c = coolprop
.property(fluid.clone(), Property::Enthalpy, state2)