Files
Entropyk/crates/fluids/src/damped_backend.rs

342 lines
11 KiB
Rust

//! 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"
);
}
}