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>
This commit is contained in:
302
crates/components/src/heat_exchanger/air_cooled_condenser.rs
Normal file
302
crates/components/src/heat_exchanger/air_cooled_condenser.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
//! 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 : 10–15 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 : 10–15 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(_))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user