Files
Entropyk/crates/components/src/heat_exchanger/air_cooled_condenser.rs
sepehr 3358b74342 Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.
Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 22:46:46 +02:00

303 lines
8.8 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.
//! Air-Cooled Condenser Component
//!
//! A condenseur à condensation par air où l'ingénieur fournit directement :
//! - `oat_k` : Outdoor Air Temperature [K] (OAT = température de l'air extérieur)
//! - `approach_k` : Approach temperature [K], différence de température entre la
//! condensation et l'air extérieur. Valeur typique : 1015 K.
//! Défaut : 12 K (représentatif d'un chiller air-cooled industriel).
//!
//! La température de condensation est calculée automatiquement :
//! ```text
//! T_cond = OAT + approach_k
//! ```
//!
//! ## Équations (2)
//!
//! - r0 = P_out P_sat(T_cond) [drive outlet pressure to condensing saturation]
//! - r1 = H_out H_sat_liq(T_cond) [drive outlet enthalpy to saturated-liquid]
//!
//! ## Jacobian (analytique)
//!
//! - ∂r0/∂P_out = 1
//! - ∂r1/∂H_out = 1
//!
//! ## Exemple JSON
//!
//! ```json
//! {
//! "type": "AirCooledCondenser",
//! "name": "cond",
//! "oat_k": 308.15,
//! "approach_k": 12.0
//! }
//! ```
//!
//! Soit : OAT = 35°C → T_cond = 47°C → P_cond_sat(R290) ≈ 16.8 bar
use super::condenser::Condenser;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::Calib;
use entropyk_fluids::FluidBackend;
use std::sync::Arc;
/// Air-cooled condenser : T_cond = OAT + approach.
///
/// L'ingénieur saisit directement la température extérieure (OAT) et
/// l'approche de condensation (approach_k). Aucune conversion manuelle
/// en `t_sat_k` n'est nécessaire.
///
/// # Example
///
/// ```
/// use entropyk_components::heat_exchanger::AirCooledCondenser;
/// use entropyk_components::Component;
///
/// // OAT = 35°C, approach = 12 K → T_cond = 47°C = 320.15 K
/// let cond = AirCooledCondenser::new(308.15, 12.0);
/// assert_eq!(cond.n_equations(), 3); // 2 thermo + 1 mass-flow (CM1.3)
/// assert!((cond.t_cond_k() - 320.15).abs() < 1e-9);
/// ```
#[derive(Debug)]
pub struct AirCooledCondenser {
inner: Condenser,
oat_k: f64,
approach_k: f64,
}
impl AirCooledCondenser {
/// Crée un condenseur à air.
///
/// # Arguments
///
/// * `oat_k` — Outdoor Air Temperature \[K\]
/// * `approach_k` — Approach temperature = T_cond OAT \[K\]. Valeur typique : 1015 K.
pub fn new(oat_k: f64, approach_k: f64) -> Self {
let t_cond_k = oat_k + approach_k;
Self {
inner: Condenser::with_saturation_temp(0.0, t_cond_k),
oat_k,
approach_k,
}
}
/// Crée un condenseur à air avec UA connu.
///
/// Le UA n'est pas utilisé dans les équations du solver (approach-based),
/// mais il est disponible pour les calculs de performance post-traitement.
pub fn with_ua(oat_k: f64, approach_k: f64, ua: f64) -> Self {
let t_cond_k = oat_k + approach_k;
Self {
inner: Condenser::with_saturation_temp(ua, t_cond_k),
oat_k,
approach_k,
}
}
/// Attache un identifiant de fluide réfrigérant.
pub fn with_refrigerant(mut self, id: &str) -> Self {
self.inner = self.inner.with_refrigerant(id);
self
}
/// Attache un backend de propriétés thermodynamiques.
pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
self.inner = self.inner.with_fluid_backend(backend);
self
}
/// Retourne la température de condensation calculée [K].
pub fn t_cond_k(&self) -> f64 {
self.oat_k + self.approach_k
}
/// Retourne la température extérieure (OAT) [K].
pub fn oat_k(&self) -> f64 {
self.oat_k
}
/// Retourne l'approche de condensation [K].
pub fn approach_k(&self) -> f64 {
self.approach_k
}
/// Retourne le UA [W/K].
pub fn ua(&self) -> f64 {
self.inner.ua()
}
/// Retourne les facteurs de calibration.
pub fn calib(&self) -> &Calib {
self.inner.calib()
}
/// Met à jour les facteurs de calibration.
pub fn set_calib(&mut self, calib: Calib) {
self.inner.set_calib(calib);
}
/// Met à jour OAT en runtime (ex. simulation paramétrique).
pub fn set_oat_k(&mut self, oat_k: f64) {
self.oat_k = oat_k;
self.inner.set_saturation_temp(self.oat_k + self.approach_k);
}
/// Met à jour l'approche en runtime.
pub fn set_approach_k(&mut self, approach_k: f64) {
self.approach_k = approach_k;
self.inner.set_saturation_temp(self.oat_k + self.approach_k);
}
}
impl Component for AirCooledCondenser {
fn set_system_context(
&mut self,
state_offset: usize,
external_edge_state_indices: &[(usize, usize, usize)],
) {
self.inner
.set_system_context(state_offset, external_edge_state_indices);
}
fn compute_residuals(
&self,
state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
self.inner.compute_residuals(state, residuals)
}
fn jacobian_entries(
&self,
state: &StateSlice,
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()
}
fn set_fluid_backend_from_builder(&mut self, backend: Arc<dyn FluidBackend>) {
self.inner.set_fluid_backend_from_builder(backend);
}
fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
self.inner.set_calib_indices(indices);
}
fn port_mass_flows(
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
self.inner.port_mass_flows(state)
}
fn port_enthalpies(
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
self.inner.port_enthalpies(state)
}
fn energy_transfers(
&self,
state: &StateSlice,
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
self.inner.energy_transfers(state)
}
fn signature(&self) -> String {
format!(
"AirCooledCondenser(oat={:.1}K, approach={:.1}K, t_cond={:.1}K)",
self.oat_k,
self.approach_k,
self.t_cond_k()
)
}
fn to_params(&self) -> crate::ComponentParams {
self.inner.to_params()
}
fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
self.inner.update_calib_factor(factor, value)
}
}
impl StateManageable for AirCooledCondenser {
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_new_t_cond_computed() {
// OAT = 35°C = 308.15 K, approach = 12 K → T_cond = 47°C = 320.15 K
let cond = AirCooledCondenser::new(308.15, 12.0);
assert!((cond.t_cond_k() - 320.15).abs() < 1e-9);
assert_eq!(cond.oat_k(), 308.15);
assert_eq!(cond.approach_k(), 12.0);
// CM1.3: 2 thermo + 1 mass-flow = 3 (delegates to inner Condenser)
assert_eq!(cond.n_equations(), 3);
}
#[test]
fn test_with_ua() {
let cond = AirCooledCondenser::with_ua(308.15, 12.0, 6000.0);
assert_eq!(cond.ua(), 6000.0);
assert!((cond.t_cond_k() - 320.15).abs() < 1e-9);
}
#[test]
fn test_set_oat_updates_t_cond() {
let mut cond = AirCooledCondenser::new(308.15, 12.0);
// Change OAT to 40°C = 313.15 K → T_cond = 52°C = 325.15 K
cond.set_oat_k(313.15);
assert!((cond.t_cond_k() - 325.15).abs() < 1e-9);
}
#[test]
fn test_set_approach_updates_t_cond() {
let mut cond = AirCooledCondenser::new(308.15, 12.0);
cond.set_approach_k(10.0);
assert!((cond.t_cond_k() - 318.15).abs() < 1e-9);
}
#[test]
fn test_no_backend_does_not_fabricate_residuals() {
let cond = AirCooledCondenser::new(308.15, 12.0);
let state = vec![0.0_f64; 10];
let mut residuals = vec![99.0_f64; 3];
let result = cond.compute_residuals(&state, &mut residuals);
assert!(matches!(result, Err(ComponentError::InvalidState(_))));
}
}