//! 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, /// 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 { 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 { self.inner.hot_inlet_state() } /// Computes the full thermodynamic state at the cold inlet. pub fn cold_inlet_state(&self) -> Result { 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()); } }