354 lines
10 KiB
Rust
354 lines
10 KiB
Rust
//! Effectiveness-NTU (ε-NTU) Model
|
||
//!
|
||
//! Implements the ε-NTU method for heat exchanger calculations.
|
||
//!
|
||
//! ## Theory
|
||
//!
|
||
//! The heat transfer rate is calculated as:
|
||
//!
|
||
//! $$\dot{Q} = \varepsilon \cdot \dot{Q}_{max} = \varepsilon \cdot C_{min} \cdot (T_{hot,in} - T_{cold,in})$$
|
||
//!
|
||
//! Where:
|
||
//! - $\varepsilon$: Effectiveness (0 to 1)
|
||
//! - $C_{min} = \min(\dot{m}_{hot} \cdot c_{p,hot}, \dot{m}_{cold} \cdot c_{p,cold})$: Minimum heat capacity rate
|
||
//! - $NTU = UA / C_{min}$: Number of Transfer Units
|
||
//! - $C_r = C_{min} / C_{max}$: Heat capacity ratio
|
||
//!
|
||
//! ## Zero-flow regularization (Story 3.5)
|
||
//!
|
||
//! When $C_{min} < 10^{-10}$ (e.g. zero mass flow on one side), heat transfer is set to zero
|
||
//! and divisions by $C_{min}$ or $C_r$ are avoided to prevent NaN/Inf.
|
||
//!
|
||
//! Note: This module uses `1e-10` kW/K for capacity rate regularization, which is appropriate
|
||
//! for the kW/K scale. For mass flow regularization at the kg/s scale, see
|
||
//! [`MIN_MASS_FLOW_REGULARIZATION_KG_S`](entropyk_core::MIN_MASS_FLOW_REGULARIZATION_KG_S).
|
||
//!
|
||
//! For counter-flow:
|
||
//! $$\varepsilon = \frac{1 - \exp(-NTU \cdot (1 - C_r))}{1 - C_r \cdot \exp(-NTU \cdot (1 - C_r))}$$
|
||
|
||
use super::model::{FluidState, HeatTransferModel};
|
||
use crate::ResidualVector;
|
||
use entropyk_core::Power;
|
||
|
||
/// Heat exchanger type for ε-NTU calculations.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||
pub enum ExchangerType {
|
||
/// Counter-flow (most efficient)
|
||
#[default]
|
||
CounterFlow,
|
||
/// Parallel-flow (co-current)
|
||
ParallelFlow,
|
||
/// Cross-flow, both fluids unmixed
|
||
CrossFlowUnmixed,
|
||
/// Cross-flow, one fluid mixed (C_max mixed)
|
||
CrossFlowMixedMax,
|
||
/// Cross-flow, one fluid mixed (C_min mixed)
|
||
CrossFlowMixedMin,
|
||
/// Shell-and-tube with specified number of shell passes
|
||
ShellAndTube {
|
||
/// Number of shell passes
|
||
passes: usize,
|
||
},
|
||
}
|
||
|
||
/// ε-NTU (Effectiveness-NTU) heat transfer model.
|
||
///
|
||
/// Uses the effectiveness-NTU method for heat exchanger rating.
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// use entropyk_components::heat_exchanger::{EpsNtuModel, ExchangerType, HeatTransferModel};
|
||
///
|
||
/// let model = EpsNtuModel::new(5000.0, ExchangerType::CounterFlow);
|
||
/// assert_eq!(model.ua(), 5000.0);
|
||
/// ```
|
||
#[derive(Debug, Clone)]
|
||
pub struct EpsNtuModel {
|
||
/// Overall heat transfer coefficient × Area (W/K), nominal
|
||
ua: f64,
|
||
/// UA calibration scale: UA_eff = ua_scale × ua (default 1.0)
|
||
ua_scale: f64,
|
||
/// Heat exchanger type
|
||
exchanger_type: ExchangerType,
|
||
}
|
||
|
||
impl EpsNtuModel {
|
||
/// Creates a new ε-NTU model.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `ua` - Overall heat transfer coefficient × Area (W/K). Must be non-negative.
|
||
/// * `exchanger_type` - Type of heat exchanger
|
||
///
|
||
/// # Panics
|
||
///
|
||
/// Panics if `ua` is negative or NaN.
|
||
pub fn new(ua: f64, exchanger_type: ExchangerType) -> Self {
|
||
assert!(
|
||
ua.is_finite() && ua >= 0.0,
|
||
"UA must be non-negative and finite, got {}",
|
||
ua
|
||
);
|
||
Self {
|
||
ua,
|
||
ua_scale: 1.0,
|
||
exchanger_type,
|
||
}
|
||
}
|
||
|
||
/// Creates a counter-flow ε-NTU model.
|
||
pub fn counter_flow(ua: f64) -> Self {
|
||
Self::new(ua, ExchangerType::CounterFlow)
|
||
}
|
||
|
||
/// Creates a parallel-flow ε-NTU model.
|
||
pub fn parallel_flow(ua: f64) -> Self {
|
||
Self::new(ua, ExchangerType::ParallelFlow)
|
||
}
|
||
|
||
/// Creates a cross-flow (unmixed) ε-NTU model.
|
||
pub fn cross_flow_unmixed(ua: f64) -> Self {
|
||
Self::new(ua, ExchangerType::CrossFlowUnmixed)
|
||
}
|
||
|
||
/// Calculates the effectiveness ε.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `ntu` - Number of Transfer Units (UA / C_min)
|
||
/// * `c_r` - Heat capacity ratio (C_min / C_max)
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// The effectiveness ε (0 to 1)
|
||
pub fn effectiveness(&self, ntu: f64, c_r: f64) -> f64 {
|
||
if ntu <= 0.0 {
|
||
return 0.0;
|
||
}
|
||
|
||
match self.exchanger_type {
|
||
ExchangerType::CounterFlow => {
|
||
if c_r < 1e-10 {
|
||
1.0 - (-ntu).exp()
|
||
} else {
|
||
let exp_term = (-ntu * (1.0 - c_r)).exp();
|
||
(1.0 - exp_term) / (1.0 - c_r * exp_term)
|
||
}
|
||
}
|
||
ExchangerType::ParallelFlow => {
|
||
if c_r < 1e-10 {
|
||
1.0 - (-ntu).exp()
|
||
} else {
|
||
(1.0 - (-ntu * (1.0 + c_r)).exp()) / (1.0 + c_r)
|
||
}
|
||
}
|
||
ExchangerType::CrossFlowUnmixed => {
|
||
if c_r < 1e-10 {
|
||
1.0 - (-ntu).exp()
|
||
} else {
|
||
1.0 - (-c_r * (1.0 - (-ntu / c_r).exp())).exp()
|
||
}
|
||
}
|
||
ExchangerType::CrossFlowMixedMax => {
|
||
if c_r < 1e-10 {
|
||
1.0 - (-ntu).exp()
|
||
} else {
|
||
let ntu_c_r = ntu / c_r;
|
||
(1.0 - (-ntu_c_r).exp()) / c_r * (1.0 - (-c_r * ntu).exp())
|
||
}
|
||
}
|
||
ExchangerType::CrossFlowMixedMin => {
|
||
if c_r < 1e-10 {
|
||
1.0 - (-ntu).exp()
|
||
} else {
|
||
(1.0 / c_r) * (1.0 - (-c_r * (1.0 - (-ntu).exp())).exp())
|
||
}
|
||
}
|
||
ExchangerType::ShellAndTube { passes: _ } => {
|
||
if c_r < 1e-10 {
|
||
1.0 - (-ntu).exp()
|
||
} else {
|
||
(1.0 - (-ntu * (1.0 + c_r * c_r).sqrt()).exp()) / (1.0 + c_r)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Calculates the maximum possible heat transfer rate.
|
||
///
|
||
/// Q̇_max = C_min × (T_hot,in - T_cold,in)
|
||
pub fn q_max(&self, c_min: f64, t_hot_in: f64, t_cold_in: f64) -> f64 {
|
||
c_min * (t_hot_in - t_cold_in).max(0.0)
|
||
}
|
||
}
|
||
|
||
impl HeatTransferModel for EpsNtuModel {
|
||
fn compute_heat_transfer(
|
||
&self,
|
||
hot_inlet: &FluidState,
|
||
_hot_outlet: &FluidState,
|
||
cold_inlet: &FluidState,
|
||
_cold_outlet: &FluidState,
|
||
dynamic_ua_scale: Option<f64>,
|
||
) -> Power {
|
||
let c_hot = hot_inlet.heat_capacity_rate();
|
||
let c_cold = cold_inlet.heat_capacity_rate();
|
||
|
||
let (c_min, c_max) = if c_hot < c_cold {
|
||
(c_hot, c_cold)
|
||
} else {
|
||
(c_cold, c_hot)
|
||
};
|
||
|
||
if c_min < 1e-10 {
|
||
return Power::from_watts(0.0);
|
||
}
|
||
|
||
let c_r = c_min / c_max;
|
||
let ntu = self.effective_ua(dynamic_ua_scale) / c_min;
|
||
|
||
let effectiveness = self.effectiveness(ntu, c_r);
|
||
|
||
let q_max = self.q_max(c_min, hot_inlet.temperature, cold_inlet.temperature);
|
||
|
||
Power::from_watts(effectiveness * q_max)
|
||
}
|
||
|
||
fn compute_residuals(
|
||
&self,
|
||
hot_inlet: &FluidState,
|
||
hot_outlet: &FluidState,
|
||
cold_inlet: &FluidState,
|
||
cold_outlet: &FluidState,
|
||
residuals: &mut ResidualVector,
|
||
dynamic_ua_scale: Option<f64>,
|
||
) {
|
||
let q = self
|
||
.compute_heat_transfer(
|
||
hot_inlet,
|
||
hot_outlet,
|
||
cold_inlet,
|
||
cold_outlet,
|
||
dynamic_ua_scale,
|
||
)
|
||
.to_watts();
|
||
|
||
let q_hot =
|
||
hot_inlet.mass_flow * hot_inlet.cp * (hot_inlet.temperature - hot_outlet.temperature);
|
||
let q_cold = cold_inlet.mass_flow
|
||
* cold_inlet.cp
|
||
* (cold_outlet.temperature - cold_inlet.temperature);
|
||
|
||
residuals[0] = q_hot - q;
|
||
residuals[1] = q_cold - q;
|
||
residuals[2] = q_hot - q_cold;
|
||
}
|
||
|
||
fn n_equations(&self) -> usize {
|
||
3
|
||
}
|
||
|
||
fn ua(&self) -> f64 {
|
||
self.ua
|
||
}
|
||
|
||
fn ua_scale(&self) -> f64 {
|
||
self.ua_scale
|
||
}
|
||
|
||
fn set_ua_scale(&mut self, s: f64) {
|
||
self.ua_scale = s;
|
||
}
|
||
|
||
fn effective_ua(&self, dynamic_ua_scale: Option<f64>) -> f64 {
|
||
self.ua * dynamic_ua_scale.unwrap_or(self.ua_scale)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_eps_ntu_model_creation() {
|
||
let model = EpsNtuModel::new(5000.0, ExchangerType::CounterFlow);
|
||
assert_eq!(model.ua(), 5000.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_effectiveness_counter_flow() {
|
||
let model = EpsNtuModel::counter_flow(5000.0);
|
||
|
||
let eps = model.effectiveness(5.0, 0.5);
|
||
assert!(eps > 0.0 && eps < 1.0);
|
||
|
||
let eps_cr_zero = model.effectiveness(5.0, 0.0);
|
||
assert!((eps_cr_zero - (1.0 - (-5.0_f64).exp())).abs() < 1e-10);
|
||
}
|
||
|
||
#[test]
|
||
fn test_effectiveness_parallel_flow() {
|
||
let model = EpsNtuModel::parallel_flow(5000.0);
|
||
|
||
let eps = model.effectiveness(5.0, 0.5);
|
||
assert!(eps > 0.0 && eps < 1.0);
|
||
assert!(eps < model.effectiveness(5.0, 0.5) + 0.1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_effectiveness_zero_ntu() {
|
||
let model = EpsNtuModel::counter_flow(5000.0);
|
||
let eps = model.effectiveness(0.0, 0.5);
|
||
assert_eq!(eps, 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_compute_heat_transfer() {
|
||
let model = EpsNtuModel::counter_flow(5000.0);
|
||
|
||
let hot_inlet = FluidState::new(80.0 + 273.15, 101_325.0, 400_000.0, 0.1, 1000.0);
|
||
let hot_outlet = FluidState::new(60.0 + 273.15, 101_325.0, 380_000.0, 0.1, 1000.0);
|
||
let cold_inlet = FluidState::new(20.0 + 273.15, 101_325.0, 80_000.0, 0.2, 4180.0);
|
||
let cold_outlet = FluidState::new(30.0 + 273.15, 101_325.0, 120_000.0, 0.2, 4180.0);
|
||
|
||
let q =
|
||
model.compute_heat_transfer(&hot_inlet, &hot_outlet, &cold_inlet, &cold_outlet, None);
|
||
|
||
assert!(q.to_watts() > 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_n_equations() {
|
||
let model = EpsNtuModel::counter_flow(1000.0);
|
||
assert_eq!(model.n_equations(), 3);
|
||
}
|
||
|
||
#[test]
|
||
fn test_q_max() {
|
||
let model = EpsNtuModel::counter_flow(5000.0);
|
||
|
||
let c_min = 1000.0;
|
||
let t_hot_in = 350.0;
|
||
let t_cold_in = 300.0;
|
||
|
||
let q_max = model.q_max(c_min, t_hot_in, t_cold_in);
|
||
assert_eq!(q_max, 50_000.0);
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic(expected = "UA must be non-negative")]
|
||
fn test_negative_ua_panics() {
|
||
let _model = EpsNtuModel::new(-1000.0, ExchangerType::CounterFlow);
|
||
}
|
||
|
||
#[test]
|
||
fn test_effectiveness_cross_flow_unmixed_cr_zero() {
|
||
let model = EpsNtuModel::cross_flow_unmixed(5000.0);
|
||
|
||
let eps = model.effectiveness(5.0, 0.0);
|
||
let expected = 1.0 - (-5.0_f64).exp();
|
||
assert!((eps - expected).abs() < 1e-10);
|
||
}
|
||
}
|