chore: remove deprecated flow_boundary and update docs to match new architecture
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
472
crates/fluids/src/dll_backend.rs
Normal file
472
crates/fluids/src/dll_backend.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user