feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
249
crates/components/src/heat_exchanger/condenser.rs
Normal file
249
crates/components/src/heat_exchanger/condenser.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
//! Condenser Component
|
||||
//!
|
||||
//! A heat exchanger configured for refrigerant condensation.
|
||||
//! The refrigerant (hot side) condenses from superheated vapor to
|
||||
//! subcooled liquid, releasing heat to the cold side.
|
||||
|
||||
use super::exchanger::HeatExchanger;
|
||||
use super::lmtd::{FlowConfiguration, LmtdModel};
|
||||
use entropyk_core::Calib;
|
||||
use crate::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
|
||||
};
|
||||
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
|
||||
|
||||
/// Condenser heat exchanger.
|
||||
///
|
||||
/// Uses the LMTD method for heat transfer calculation.
|
||||
/// The refrigerant condenses on the hot side, releasing heat
|
||||
/// to the cold side (typically water or air).
|
||||
///
|
||||
/// # Configuration
|
||||
///
|
||||
/// - Hot side: Refrigerant condensing (phase change)
|
||||
/// - Cold side: Heat sink (water, air, etc.)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_components::heat_exchanger::Condenser;
|
||||
/// use entropyk_components::Component;
|
||||
///
|
||||
/// let condenser = Condenser::new(10_000.0); // UA = 10 kW/K
|
||||
/// assert_eq!(condenser.n_equations(), 3);
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Condenser {
|
||||
/// Inner heat exchanger with LMTD model
|
||||
inner: HeatExchanger<LmtdModel>,
|
||||
/// Saturation temperature for condensation (K)
|
||||
saturation_temp: f64,
|
||||
}
|
||||
|
||||
impl Condenser {
|
||||
/// Creates a new condenser with the given UA value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ua` - Overall heat transfer coefficient × Area (W/K)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_components::heat_exchanger::Condenser;
|
||||
///
|
||||
/// let condenser = Condenser::new(15_000.0);
|
||||
/// ```
|
||||
pub fn new(ua: f64) -> Self {
|
||||
let model = LmtdModel::new(ua, FlowConfiguration::CounterFlow);
|
||||
Self {
|
||||
inner: HeatExchanger::new(model, "Condenser"),
|
||||
saturation_temp: 323.15,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a condenser with a specific saturation temperature.
|
||||
pub fn with_saturation_temp(ua: f64, saturation_temp: f64) -> Self {
|
||||
let model = LmtdModel::new(ua, FlowConfiguration::CounterFlow);
|
||||
Self {
|
||||
inner: HeatExchanger::new(model, "Condenser"),
|
||||
saturation_temp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the name of this condenser.
|
||||
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 condenser).
|
||||
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
|
||||
}
|
||||
|
||||
/// Sets the saturation temperature.
|
||||
pub fn set_saturation_temp(&mut self, temp: f64) {
|
||||
self.saturation_temp = temp;
|
||||
}
|
||||
|
||||
/// Validates that the outlet quality is <= 1 (fully condensed or subcooled).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `outlet_enthalpy` - Outlet specific enthalpy (J/kg)
|
||||
/// * `h_liquid` - Saturated liquid enthalpy at condensing pressure
|
||||
/// * `h_vapor` - Saturated vapor enthalpy at condensing pressure
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns Ok(true) if fully condensed, Err otherwise
|
||||
pub fn validate_outlet_quality(
|
||||
&self,
|
||||
outlet_enthalpy: f64,
|
||||
h_liquid: f64,
|
||||
h_vapor: f64,
|
||||
) -> Result<bool, 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 <= 1.0 + 1e-6 {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(ComponentError::InvalidState(format!(
|
||||
"Condenser outlet quality {} > 1 (superheated)",
|
||||
quality
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 Condenser {
|
||||
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 Condenser {
|
||||
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_condenser_creation() {
|
||||
let condenser = Condenser::new(10_000.0);
|
||||
assert_eq!(condenser.ua(), 10_000.0);
|
||||
assert_eq!(condenser.n_equations(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condenser_with_saturation_temp() {
|
||||
let condenser = Condenser::with_saturation_temp(10_000.0, 323.15);
|
||||
assert_eq!(condenser.saturation_temp(), 323.15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_outlet_quality_fully_condensed() {
|
||||
let condenser = Condenser::new(10_000.0);
|
||||
|
||||
let h_liquid = 200_000.0;
|
||||
let h_vapor = 400_000.0;
|
||||
let outlet_h = 180_000.0;
|
||||
|
||||
let result = condenser.validate_outlet_quality(outlet_h, h_liquid, h_vapor);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_outlet_quality_superheated() {
|
||||
let condenser = Condenser::new(10_000.0);
|
||||
|
||||
let h_liquid = 200_000.0;
|
||||
let h_vapor = 400_000.0;
|
||||
let outlet_h = 450_000.0;
|
||||
|
||||
let result = condenser.validate_outlet_quality(outlet_h, h_liquid, h_vapor);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_residuals() {
|
||||
let condenser = Condenser::new(10_000.0);
|
||||
|
||||
let state = vec![0.0; 10];
|
||||
let mut residuals = vec![0.0; 3];
|
||||
|
||||
let result = condenser.compute_residuals(&state, &mut residuals);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user