293 lines
8.3 KiB
Rust
293 lines
8.3 KiB
Rust
//! Evaporator Component
|
||
//!
|
||
//! A heat exchanger configured for refrigerant evaporation.
|
||
//! The refrigerant (cold side) evaporates from two-phase mixture to
|
||
/// superheated vapor, absorbing heat from the hot side.
|
||
use super::eps_ntu::{EpsNtuModel, ExchangerType};
|
||
use super::exchanger::HeatExchanger;
|
||
use entropyk_core::Calib;
|
||
use crate::{
|
||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
|
||
};
|
||
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
|
||
|
||
/// Evaporator heat exchanger.
|
||
///
|
||
/// Uses the ε-NTU method for heat transfer calculation.
|
||
/// The refrigerant evaporates on the cold side, absorbing heat
|
||
/// from the hot side (typically water or air).
|
||
///
|
||
/// # Configuration
|
||
///
|
||
/// - Hot side: Heat source (water, air, etc.)
|
||
/// - Cold side: Refrigerant evaporating (phase change)
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// use entropyk_components::heat_exchanger::Evaporator;
|
||
/// use entropyk_components::Component;
|
||
///
|
||
/// let evaporator = Evaporator::new(8_000.0); // UA = 8 kW/K
|
||
/// assert_eq!(evaporator.n_equations(), 3);
|
||
/// ```
|
||
#[derive(Debug)]
|
||
pub struct Evaporator {
|
||
/// Inner heat exchanger with ε-NTU model
|
||
inner: HeatExchanger<EpsNtuModel>,
|
||
/// Saturation temperature for evaporation (K)
|
||
saturation_temp: f64,
|
||
/// Target superheat (K)
|
||
superheat_target: f64,
|
||
}
|
||
|
||
impl Evaporator {
|
||
/// Creates a new evaporator with the given UA value.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `ua` - Overall heat transfer coefficient × Area (W/K)
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// use entropyk_components::heat_exchanger::Evaporator;
|
||
///
|
||
/// let evaporator = Evaporator::new(8_000.0);
|
||
/// ```
|
||
pub fn new(ua: f64) -> Self {
|
||
let model = EpsNtuModel::new(ua, ExchangerType::CounterFlow);
|
||
Self {
|
||
inner: HeatExchanger::new(model, "Evaporator"),
|
||
saturation_temp: 278.15,
|
||
superheat_target: 5.0,
|
||
}
|
||
}
|
||
|
||
/// Creates an evaporator with specific saturation and superheat.
|
||
pub fn with_superheat(ua: f64, saturation_temp: f64, superheat_target: f64) -> Self {
|
||
let model = EpsNtuModel::new(ua, ExchangerType::CounterFlow);
|
||
Self {
|
||
inner: HeatExchanger::new(model, "Evaporator"),
|
||
saturation_temp,
|
||
superheat_target,
|
||
}
|
||
}
|
||
|
||
/// Returns the name of this evaporator.
|
||
pub fn name(&self) -> &str {
|
||
self.inner.name()
|
||
}
|
||
|
||
/// Returns the UA value (effective: f_ua × UA_nominal).
|
||
pub fn ua(&self) -> f64 {
|
||
self.inner.ua()
|
||
}
|
||
|
||
/// Returns calibration factors (f_ua for evaporator).
|
||
pub fn calib(&self) -> &Calib {
|
||
self.inner.calib()
|
||
}
|
||
|
||
/// Sets calibration factors.
|
||
pub fn set_calib(&mut self, calib: Calib) {
|
||
self.inner.set_calib(calib);
|
||
}
|
||
|
||
/// Returns the saturation temperature.
|
||
pub fn saturation_temp(&self) -> f64 {
|
||
self.saturation_temp
|
||
}
|
||
|
||
/// Returns the superheat target.
|
||
pub fn superheat_target(&self) -> f64 {
|
||
self.superheat_target
|
||
}
|
||
|
||
/// Sets the saturation temperature.
|
||
pub fn set_saturation_temp(&mut self, temp: f64) {
|
||
self.saturation_temp = temp;
|
||
}
|
||
|
||
/// Sets the superheat target.
|
||
pub fn set_superheat_target(&mut self, superheat: f64) {
|
||
self.superheat_target = superheat;
|
||
}
|
||
|
||
/// Validates that the outlet quality is >= 0 (fully evaporated or superheated).
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `outlet_enthalpy` - Outlet specific enthalpy (J/kg)
|
||
/// * `h_liquid` - Saturated liquid enthalpy at evaporating pressure
|
||
/// * `h_vapor` - Saturated vapor enthalpy at evaporating pressure
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// Returns Ok(superheat) if valid, Err otherwise
|
||
pub fn validate_outlet_quality(
|
||
&self,
|
||
outlet_enthalpy: f64,
|
||
h_liquid: f64,
|
||
h_vapor: f64,
|
||
cp_vapor: f64,
|
||
) -> Result<f64, ComponentError> {
|
||
if h_vapor <= h_liquid {
|
||
return Err(ComponentError::NumericalError(
|
||
"Invalid saturation enthalpies".to_string(),
|
||
));
|
||
}
|
||
|
||
let quality = (outlet_enthalpy - h_liquid) / (h_vapor - h_liquid);
|
||
|
||
if quality >= 0.0 - 1e-6 {
|
||
if outlet_enthalpy >= h_vapor {
|
||
let superheat = (outlet_enthalpy - h_vapor) / cp_vapor;
|
||
Ok(superheat)
|
||
} else {
|
||
Ok(0.0)
|
||
}
|
||
} else {
|
||
Err(ComponentError::InvalidState(format!(
|
||
"Evaporator outlet quality {} < 0 (subcooled)",
|
||
quality
|
||
)))
|
||
}
|
||
}
|
||
|
||
/// Calculates the superheat residual for inverse control.
|
||
///
|
||
/// Returns (actual_superheat - target_superheat)
|
||
pub fn superheat_residual(&self, actual_superheat: f64) -> f64 {
|
||
actual_superheat - self.superheat_target
|
||
}
|
||
|
||
/// Computes the full thermodynamic state at the hot inlet.
|
||
pub fn hot_inlet_state(&self) -> Result<entropyk_fluids::ThermoState, ComponentError> {
|
||
self.inner.hot_inlet_state()
|
||
}
|
||
|
||
/// Computes the full thermodynamic state at the cold inlet.
|
||
pub fn cold_inlet_state(&self) -> Result<entropyk_fluids::ThermoState, ComponentError> {
|
||
self.inner.cold_inlet_state()
|
||
}
|
||
}
|
||
|
||
impl Component for Evaporator {
|
||
fn compute_residuals(
|
||
&self,
|
||
state: &SystemState,
|
||
residuals: &mut ResidualVector,
|
||
) -> Result<(), ComponentError> {
|
||
self.inner.compute_residuals(state, residuals)
|
||
}
|
||
|
||
fn jacobian_entries(
|
||
&self,
|
||
state: &SystemState,
|
||
jacobian: &mut JacobianBuilder,
|
||
) -> Result<(), ComponentError> {
|
||
self.inner.jacobian_entries(state, jacobian)
|
||
}
|
||
|
||
fn n_equations(&self) -> usize {
|
||
self.inner.n_equations()
|
||
}
|
||
|
||
fn get_ports(&self) -> &[ConnectedPort] {
|
||
self.inner.get_ports()
|
||
}
|
||
}
|
||
|
||
impl StateManageable for Evaporator {
|
||
fn state(&self) -> OperationalState {
|
||
self.inner.state()
|
||
}
|
||
|
||
fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
|
||
self.inner.set_state(state)
|
||
}
|
||
|
||
fn can_transition_to(&self, target: OperationalState) -> bool {
|
||
self.inner.can_transition_to(target)
|
||
}
|
||
|
||
fn circuit_id(&self) -> &CircuitId {
|
||
self.inner.circuit_id()
|
||
}
|
||
|
||
fn set_circuit_id(&mut self, circuit_id: CircuitId) {
|
||
self.inner.set_circuit_id(circuit_id);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_evaporator_creation() {
|
||
let evaporator = Evaporator::new(8_000.0);
|
||
assert_eq!(evaporator.ua(), 8_000.0);
|
||
assert_eq!(evaporator.n_equations(), 3);
|
||
}
|
||
|
||
#[test]
|
||
fn test_evaporator_with_superheat() {
|
||
let evaporator = Evaporator::with_superheat(8_000.0, 278.15, 10.0);
|
||
assert_eq!(evaporator.saturation_temp(), 278.15);
|
||
assert_eq!(evaporator.superheat_target(), 10.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_validate_outlet_quality_superheated() {
|
||
let evaporator = Evaporator::new(8_000.0);
|
||
|
||
let h_liquid = 200_000.0;
|
||
let h_vapor = 400_000.0;
|
||
let outlet_h = 420_000.0;
|
||
let cp_vapor = 1000.0;
|
||
|
||
let result = evaporator.validate_outlet_quality(outlet_h, h_liquid, h_vapor, cp_vapor);
|
||
assert!(result.is_ok());
|
||
|
||
let superheat = result.unwrap();
|
||
assert!((superheat - 20.0).abs() < 1e-10);
|
||
}
|
||
|
||
#[test]
|
||
fn test_validate_outlet_quality_subcooled() {
|
||
let evaporator = Evaporator::new(8_000.0);
|
||
|
||
let h_liquid = 200_000.0;
|
||
let h_vapor = 400_000.0;
|
||
let outlet_h = 150_000.0;
|
||
let cp_vapor = 1000.0;
|
||
|
||
let result = evaporator.validate_outlet_quality(outlet_h, h_liquid, h_vapor, cp_vapor);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_superheat_residual() {
|
||
let evaporator = Evaporator::with_superheat(8_000.0, 278.15, 5.0);
|
||
|
||
let residual = evaporator.superheat_residual(7.0);
|
||
assert!((residual - 2.0).abs() < 1e-10);
|
||
|
||
let residual = evaporator.superheat_residual(3.0);
|
||
assert!((residual - (-2.0)).abs() < 1e-10);
|
||
}
|
||
|
||
#[test]
|
||
fn test_compute_residuals() {
|
||
let evaporator = Evaporator::new(8_000.0);
|
||
|
||
let state = vec![0.0; 10];
|
||
let mut residuals = vec![0.0; 3];
|
||
|
||
let result = evaporator.compute_residuals(&state, &mut residuals);
|
||
assert!(result.is_ok());
|
||
}
|
||
}
|