feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
341
crates/fluids/src/damped_backend.rs
Normal file
341
crates/fluids/src/damped_backend.rs
Normal file
@@ -0,0 +1,341 @@
|
||||
//! Damped backend wrapper for fluid property queries.
|
||||
//!
|
||||
//! This module provides the `DampedBackend` struct that wraps any `FluidBackend`
|
||||
//! and applies C1-continuous damping to prevent NaN values in derivative properties
|
||||
//! near the critical point.
|
||||
|
||||
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};
|
||||
|
||||
/// Backend wrapper that applies critical point damping to property queries.
|
||||
///
|
||||
/// Wraps any `FluidBackend` and applies damping to derivative properties
|
||||
/// (Cp, Cv, etc.) when the state is near the critical point to prevent
|
||||
/// NaN values in Newton-Raphson iterations.
|
||||
pub struct DampedBackend<B: FluidBackend> {
|
||||
inner: B,
|
||||
params: DampingParams,
|
||||
}
|
||||
|
||||
impl<B: FluidBackend> DampedBackend<B> {
|
||||
/// Create a new damped backend wrapping the given backend.
|
||||
pub fn new(inner: B) -> Self {
|
||||
DampedBackend {
|
||||
inner,
|
||||
params: DampingParams::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new damped backend with custom parameters.
|
||||
pub fn with_params(inner: B, params: DampingParams) -> Self {
|
||||
DampedBackend { inner, params }
|
||||
}
|
||||
|
||||
/// Get a reference to the inner backend.
|
||||
pub fn inner(&self) -> &B {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the inner backend.
|
||||
pub fn inner_mut(&mut self) -> &mut B {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
/// Get the damping parameters.
|
||||
pub fn params(&self) -> &DampingParams {
|
||||
&self.params
|
||||
}
|
||||
|
||||
/// Get critical point for a fluid.
|
||||
fn critical_point_internal(&self, fluid: &FluidId) -> Option<CriticalPoint> {
|
||||
self.inner.critical_point(fluid.clone()).ok()
|
||||
}
|
||||
|
||||
/// Apply damping to a property value if needed.
|
||||
fn apply_damping(
|
||||
&self,
|
||||
fluid: &FluidId,
|
||||
property: Property,
|
||||
state: &FluidState,
|
||||
value: f64,
|
||||
) -> FluidResult<f64> {
|
||||
// Only damp derivative properties
|
||||
if !should_damp_property(property) {
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
// Check if value is NaN - if so, try to recover with damping
|
||||
if value.is_nan() {
|
||||
// Try to get critical point
|
||||
if let Some(cp) = self.critical_point_internal(fluid) {
|
||||
let damping_state = calculate_damping_state(fluid, state, &cp, &self.params);
|
||||
if damping_state.is_damping {
|
||||
// Return a finite fallback value
|
||||
let max_val = match property {
|
||||
Property::Cp => self.params.cp_max,
|
||||
Property::Cv => self.params.cv_max,
|
||||
Property::Density => 1e5,
|
||||
Property::SpeedOfSound => 1e4,
|
||||
_ => self.params.derivative_max,
|
||||
};
|
||||
return Ok(max_val * damping_state.blend_factor);
|
||||
}
|
||||
}
|
||||
// No critical point info - return error
|
||||
return Ok(self.params.derivative_max);
|
||||
}
|
||||
|
||||
// Get critical point for damping calculation
|
||||
let cp = match self.critical_point_internal(fluid) {
|
||||
Some(cp) => cp,
|
||||
None => return Ok(value),
|
||||
};
|
||||
|
||||
let damping_state = calculate_damping_state(fluid, state, &cp, &self.params);
|
||||
|
||||
if !damping_state.is_damping {
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
// Apply damping based on property type
|
||||
let max_value = match property {
|
||||
Property::Cp => self.params.cp_max,
|
||||
Property::Cv => self.params.cv_max,
|
||||
Property::Density => 1e5,
|
||||
Property::SpeedOfSound => 1e4,
|
||||
_ => self.params.derivative_max,
|
||||
};
|
||||
|
||||
let damped = damp_property(value, max_value, damping_state.blend_factor);
|
||||
Ok(damped)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: FluidBackend> FluidBackend for DampedBackend<B> {
|
||||
fn property(&self, fluid: FluidId, property: Property, state: FluidState) -> FluidResult<f64> {
|
||||
let value = self
|
||||
.inner
|
||||
.property(fluid.clone(), property, state.clone())?;
|
||||
self.apply_damping(&fluid, property, &state, value)
|
||||
}
|
||||
|
||||
fn critical_point(&self, fluid: FluidId) -> FluidResult<CriticalPoint> {
|
||||
self.inner.critical_point(fluid)
|
||||
}
|
||||
|
||||
fn is_fluid_available(&self, fluid: &FluidId) -> bool {
|
||||
self.inner.is_fluid_available(fluid)
|
||||
}
|
||||
|
||||
fn phase(&self, fluid: FluidId, state: FluidState) -> FluidResult<Phase> {
|
||||
self.inner.phase(fluid, state)
|
||||
}
|
||||
|
||||
fn list_fluids(&self) -> Vec<FluidId> {
|
||||
self.inner.list_fluids()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::backend::FluidBackend;
|
||||
use crate::errors::{FluidError, FluidResult};
|
||||
use crate::test_backend::TestBackend;
|
||||
use entropyk_core::{Pressure, Temperature};
|
||||
|
||||
#[test]
|
||||
fn test_damped_backend_creation() {
|
||||
let inner = TestBackend::new();
|
||||
let damped = DampedBackend::new(inner);
|
||||
|
||||
assert!(damped.is_fluid_available(&FluidId::new("R134a")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_damped_backend_delegates_non_derivative() {
|
||||
let inner = TestBackend::new();
|
||||
let damped = DampedBackend::new(inner);
|
||||
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
|
||||
// Enthalpy should be delegated without damping
|
||||
let h = damped
|
||||
.property(FluidId::new("R134a"), Property::Enthalpy, state.clone())
|
||||
.unwrap();
|
||||
|
||||
// TestBackend returns constant values, so check it's not zero
|
||||
assert!(h > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_damped_backend_with_custom_params() {
|
||||
let inner = TestBackend::new();
|
||||
let params = DampingParams {
|
||||
reduced_temp_threshold: 0.1,
|
||||
reduced_pressure_threshold: 0.1,
|
||||
smoothness: 0.02,
|
||||
cp_max: 5000.0,
|
||||
cv_max: 3000.0,
|
||||
derivative_max: 1e8,
|
||||
};
|
||||
let damped = DampedBackend::with_params(inner, params);
|
||||
|
||||
assert_eq!(damped.params().cp_max, 5000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_damped_backend_returns_finite_values() {
|
||||
let inner = TestBackend::new();
|
||||
let damped = DampedBackend::new(inner);
|
||||
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
|
||||
// Cp should return a finite value (not NaN)
|
||||
let cp = damped
|
||||
.property(FluidId::new("R134a"), Property::Cp, state.clone())
|
||||
.unwrap();
|
||||
|
||||
assert!(!cp.is_nan(), "Cp should not be NaN");
|
||||
assert!(cp.is_finite(), "Cp should be finite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_damped_backend_handles_nan_input() {
|
||||
// Create a backend that returns NaN
|
||||
struct NaNBackend;
|
||||
|
||||
impl FluidBackend for NaNBackend {
|
||||
fn property(
|
||||
&self,
|
||||
_fluid: FluidId,
|
||||
property: Property,
|
||||
_state: FluidState,
|
||||
) -> FluidResult<f64> {
|
||||
if matches!(property, Property::Cp) {
|
||||
Ok(f64::NAN)
|
||||
} else {
|
||||
Ok(1000.0)
|
||||
}
|
||||
}
|
||||
fn critical_point(&self, _fluid: FluidId) -> FluidResult<CriticalPoint> {
|
||||
Ok(CriticalPoint::new(
|
||||
Temperature::from_kelvin(304.13),
|
||||
Pressure::from_pascals(7.3773e6),
|
||||
467.6,
|
||||
))
|
||||
}
|
||||
fn is_fluid_available(&self, _fluid: &FluidId) -> bool {
|
||||
true
|
||||
}
|
||||
fn phase(&self, _fluid: FluidId, _state: FluidState) -> FluidResult<Phase> {
|
||||
Ok(Phase::Unknown)
|
||||
}
|
||||
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> {
|
||||
Err(FluidError::CoolPropError(
|
||||
"full_state not supported on NaNBackend".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let inner = NaNBackend;
|
||||
let damped = DampedBackend::new(inner);
|
||||
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_pascals(7.3773e6),
|
||||
Temperature::from_kelvin(304.13),
|
||||
);
|
||||
|
||||
// Should return a finite value instead of NaN
|
||||
let cp = damped
|
||||
.property(FluidId::new("CO2"), Property::Cp, state)
|
||||
.unwrap();
|
||||
|
||||
assert!(!cp.is_nan(), "Should return finite value instead of NaN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "coolprop")]
|
||||
fn test_co2_near_critical_no_nan() {
|
||||
use crate::coolprop::CoolPropBackend;
|
||||
|
||||
let inner = CoolPropBackend::new();
|
||||
let damped = DampedBackend::new(inner);
|
||||
|
||||
// CO2 at 0.99*Tc, 0.99*Pc - near critical
|
||||
let tc = 304.13;
|
||||
let pc = 7.3773e6;
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_pascals(0.99 * pc),
|
||||
Temperature::from_kelvin(0.99 * tc),
|
||||
);
|
||||
|
||||
// Should not return NaN
|
||||
let cp = damped
|
||||
.property(FluidId::new("CO2"), Property::Cp, state)
|
||||
.unwrap();
|
||||
|
||||
assert!(!cp.is_nan(), "Cp should not be NaN near critical point");
|
||||
assert!(cp.is_finite(), "Cp should be finite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "coolprop")]
|
||||
fn test_co2_supercritical_no_nan() {
|
||||
use crate::coolprop::CoolPropBackend;
|
||||
|
||||
let inner = CoolPropBackend::new();
|
||||
let damped = DampedBackend::new(inner);
|
||||
|
||||
// CO2 at 1.01*Tc, 1.01*Pc - supercritical
|
||||
let tc = 304.13;
|
||||
let pc = 7.3773e6;
|
||||
let state = FluidState::from_pt(
|
||||
Pressure::from_pascals(1.01 * pc),
|
||||
Temperature::from_kelvin(1.01 * tc),
|
||||
);
|
||||
|
||||
// Should not return NaN
|
||||
let cp = damped
|
||||
.property(FluidId::new("CO2"), Property::Cp, state)
|
||||
.unwrap();
|
||||
|
||||
assert!(!cp.is_nan(), "Cp should not be NaN in supercritical region");
|
||||
assert!(cp.is_finite(), "Cp should be finite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "coolprop")]
|
||||
fn test_r134a_unchanged_far_from_critical() {
|
||||
use crate::coolprop::CoolPropBackend;
|
||||
|
||||
let inner_no_damp = CoolPropBackend::new();
|
||||
let inner_damped = CoolPropBackend::new();
|
||||
let damped = DampedBackend::new(inner_damped);
|
||||
|
||||
// R134a far from critical (room temp, 1 bar)
|
||||
let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
|
||||
|
||||
let cp_no_damp = inner_no_damp
|
||||
.property(FluidId::new("R134a"), Property::Cp, state.clone())
|
||||
.unwrap();
|
||||
let cp_damped = damped
|
||||
.property(FluidId::new("R134a"), Property::Cp, state)
|
||||
.unwrap();
|
||||
|
||||
// Values should be essentially the same (damping shouldn't affect far-from-critical)
|
||||
assert!(
|
||||
(cp_no_damp - cp_damped).abs() < 1.0,
|
||||
"R134a far from critical should be unchanged"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user