342 lines
11 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|