feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
204
crates/components/src/heat_exchanger/model.rs
Normal file
204
crates/components/src/heat_exchanger/model.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
//! Heat Transfer Model Trait
|
||||
//!
|
||||
//! Defines the Strategy Pattern interface for heat transfer calculations.
|
||||
//! This trait is object-safe for dynamic dispatch.
|
||||
|
||||
use crate::ResidualVector;
|
||||
use entropyk_core::{Enthalpy, MassFlow, Power, Pressure, Temperature};
|
||||
|
||||
/// Fluid state for heat transfer calculations.
|
||||
///
|
||||
/// Represents the thermodynamic state at a port (inlet or outlet).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FluidState {
|
||||
/// Temperature in Kelvin
|
||||
pub temperature: f64,
|
||||
/// Pressure in Pascals
|
||||
pub pressure: f64,
|
||||
/// Specific enthalpy in J/kg
|
||||
pub enthalpy: f64,
|
||||
/// Mass flow rate in kg/s
|
||||
pub mass_flow: f64,
|
||||
/// Specific heat capacity at constant pressure in J/(kg·K)
|
||||
pub cp: f64,
|
||||
}
|
||||
|
||||
impl Default for FluidState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
temperature: 300.0,
|
||||
pressure: 101_325.0,
|
||||
enthalpy: 0.0,
|
||||
mass_flow: 0.1,
|
||||
cp: 1000.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FluidState {
|
||||
/// Creates a new fluid state.
|
||||
pub fn new(temperature: f64, pressure: f64, enthalpy: f64, mass_flow: f64, cp: f64) -> Self {
|
||||
Self {
|
||||
temperature,
|
||||
pressure,
|
||||
enthalpy,
|
||||
mass_flow,
|
||||
cp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a fluid state with default properties for a given temperature.
|
||||
pub fn from_temperature(temperature: f64) -> Self {
|
||||
Self {
|
||||
temperature,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the heat capacity rate C = ṁ × Cp in W/K.
|
||||
pub fn heat_capacity_rate(&self) -> f64 {
|
||||
self.mass_flow * self.cp
|
||||
}
|
||||
|
||||
/// Creates a FluidState from strongly-typed physical quantities.
|
||||
pub fn from_types(
|
||||
temperature: Temperature,
|
||||
pressure: Pressure,
|
||||
enthalpy: Enthalpy,
|
||||
mass_flow: MassFlow,
|
||||
cp: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
temperature: temperature.to_kelvin(),
|
||||
pressure: pressure.to_pascals(),
|
||||
enthalpy: enthalpy.to_joules_per_kg(),
|
||||
mass_flow: mass_flow.to_kg_per_s(),
|
||||
cp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns temperature as a strongly-typed Temperature.
|
||||
pub fn temperature(&self) -> Temperature {
|
||||
Temperature::from_kelvin(self.temperature)
|
||||
}
|
||||
|
||||
/// Returns pressure as a strongly-typed Pressure.
|
||||
pub fn pressure(&self) -> Pressure {
|
||||
Pressure::from_pascals(self.pressure)
|
||||
}
|
||||
|
||||
/// Returns enthalpy as a strongly-typed Enthalpy.
|
||||
pub fn enthalpy(&self) -> Enthalpy {
|
||||
Enthalpy::from_joules_per_kg(self.enthalpy)
|
||||
}
|
||||
|
||||
/// Returns mass flow as a strongly-typed MassFlow.
|
||||
pub fn mass_flow(&self) -> MassFlow {
|
||||
MassFlow::from_kg_per_s(self.mass_flow)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for heat transfer calculation models.
|
||||
///
|
||||
/// This trait uses the Strategy Pattern to allow different heat transfer
|
||||
/// calculation methods (LMTD, ε-NTU, etc.) to be used interchangeably.
|
||||
///
|
||||
/// # Object Safety
|
||||
///
|
||||
/// This trait is object-safe and can be used with dynamic dispatch:
|
||||
///
|
||||
/// ```
|
||||
/// # use entropyk_components::heat_exchanger::model::{HeatTransferModel, FluidState};
|
||||
/// # use entropyk_components::ResidualVector;
|
||||
/// # use entropyk_core::Power;
|
||||
/// struct SimpleModel { ua: f64 }
|
||||
/// impl HeatTransferModel for SimpleModel {
|
||||
/// fn compute_heat_transfer(&self, _: &FluidState, _: &FluidState, _: &FluidState, _: &FluidState) -> Power {
|
||||
/// Power::from_watts(0.0)
|
||||
/// }
|
||||
/// fn compute_residuals(&self, _: &FluidState, _: &FluidState, _: &FluidState, _: &FluidState, _: &mut ResidualVector) {}
|
||||
/// fn n_equations(&self) -> usize { 3 }
|
||||
/// fn ua(&self) -> f64 { self.ua }
|
||||
/// }
|
||||
/// let model: Box<dyn HeatTransferModel> = Box::new(SimpleModel { ua: 1000.0 });
|
||||
/// ```
|
||||
pub trait HeatTransferModel: Send + Sync {
|
||||
/// Computes the heat transfer rate Q̇ (Watts).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `hot_inlet` - Hot side inlet state
|
||||
/// * `hot_outlet` - Hot side outlet state
|
||||
/// * `cold_inlet` - Cold side inlet state
|
||||
/// * `cold_outlet` - Cold side outlet state
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The heat transfer rate in Watts (positive = heat flows from hot to cold)
|
||||
fn compute_heat_transfer(
|
||||
&self,
|
||||
hot_inlet: &FluidState,
|
||||
hot_outlet: &FluidState,
|
||||
cold_inlet: &FluidState,
|
||||
cold_outlet: &FluidState,
|
||||
) -> Power;
|
||||
|
||||
/// Computes residuals for the solver.
|
||||
///
|
||||
/// The residuals represent the error in the heat transfer equations
|
||||
/// that the solver will attempt to drive to zero.
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
hot_inlet: &FluidState,
|
||||
hot_outlet: &FluidState,
|
||||
cold_inlet: &FluidState,
|
||||
cold_outlet: &FluidState,
|
||||
residuals: &mut ResidualVector,
|
||||
);
|
||||
|
||||
/// Returns the number of equations this model contributes.
|
||||
fn n_equations(&self) -> usize;
|
||||
|
||||
/// Returns the nominal UA value (overall heat transfer coefficient × area) in W/K.
|
||||
fn ua(&self) -> f64;
|
||||
|
||||
/// Returns the UA calibration scale (default 1.0). UA_eff = ua_scale × ua_nominal.
|
||||
fn ua_scale(&self) -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Sets the UA calibration scale (e.g. from Calib.f_ua).
|
||||
fn set_ua_scale(&mut self, _s: f64) {}
|
||||
|
||||
/// Returns the effective UA used in heat transfer: ua_scale × ua_nominal.
|
||||
fn effective_ua(&self) -> f64 {
|
||||
self.ua() * self.ua_scale()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_fluid_state_default() {
|
||||
let state = FluidState::default();
|
||||
assert_eq!(state.temperature, 300.0);
|
||||
assert_eq!(state.pressure, 101_325.0);
|
||||
assert_eq!(state.cp, 1000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fluid_state_heat_capacity_rate() {
|
||||
let state = FluidState::new(300.0, 101_325.0, 0.0, 0.5, 2000.0);
|
||||
let c = state.heat_capacity_rate();
|
||||
assert!((c - 1000.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fluid_state_from_temperature() {
|
||||
let state = FluidState::from_temperature(350.0);
|
||||
assert_eq!(state.temperature, 350.0);
|
||||
assert_eq!(state.pressure, 101_325.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user