feat(components): add ThermoState generators and Eurovent backend demo

This commit is contained in:
Sepehr
2026-02-20 22:01:38 +01:00
parent 375d288950
commit 4a40fddfe3
271 changed files with 28614 additions and 447 deletions

View 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());
}
}