Files
Entropyk/crates/components/src/heat_exchanger/evaporator.rs

293 lines
8.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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());
}
}