Files
Entropyk/crates/components/src/heat_exchanger/condenser_coil.rs

282 lines
8.2 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.
//! Condenser Coil Component
//!
//! An air-side (finned) heat exchanger for refrigerant condensation.
//! The refrigerant (hot side) condenses, releasing heat to air (cold side).
//! Used in split systems and air-source heat pumps.
//!
//! ## Port Convention
//!
//! - **Hot side (refrigerant)**: Condensing
//! - **Cold side (air)**: Heat sink — connect to Fan outlet/inlet
//!
//! ## Integration with Fan
//!
//! Connect Fan outlet → CondenserCoil air inlet, CondenserCoil air outlet → Fan inlet.
//! Use `FluidId::new("Air")` for air ports.
use super::condenser::Condenser;
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
use crate::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
/// Condenser coil (air-side finned heat exchanger).
///
/// Explicit component for air-source condensers. Uses LMTD method.
/// Refrigerant condenses on hot side, air on cold side.
///
/// # Example
///
/// ```
/// use entropyk_components::heat_exchanger::CondenserCoil;
/// use entropyk_components::Component;
///
/// let coil = CondenserCoil::new(10_000.0); // UA = 10 kW/K
/// assert_eq!(coil.ua(), 10_000.0);
/// assert_eq!(coil.n_equations(), 2);
/// ```
#[derive(Debug)]
pub struct CondenserCoil {
inner: Condenser,
air_validated: std::sync::atomic::AtomicBool,
}
impl CondenserCoil {
/// Creates a new condenser coil with the given UA value.
///
/// # Arguments
///
/// * `ua` - Overall heat transfer coefficient × Area (W/K)
pub fn new(ua: f64) -> Self {
Self {
inner: Condenser::new(ua),
air_validated: std::sync::atomic::AtomicBool::new(false),
}
}
/// Creates a condenser coil with a specific saturation temperature.
pub fn with_saturation_temp(ua: f64, saturation_temp: f64) -> Self {
Self {
inner: Condenser::with_saturation_temp(ua, saturation_temp),
air_validated: std::sync::atomic::AtomicBool::new(false),
}
}
/// Returns the name of this component.
pub fn name(&self) -> &str {
"CondenserCoil"
}
/// Returns the UA value.
pub fn ua(&self) -> f64 {
self.inner.ua()
}
/// Returns the saturation temperature.
pub fn saturation_temp(&self) -> f64 {
self.inner.saturation_temp()
}
/// Sets the saturation temperature.
pub fn set_saturation_temp(&mut self, temp: f64) {
self.inner.set_saturation_temp(temp);
}
}
impl Component for CondenserCoil {
fn compute_residuals(
&self,
state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
if !self
.air_validated
.load(std::sync::atomic::Ordering::Relaxed)
{
if let Some(fluid_id) = self.inner.cold_fluid_id() {
if fluid_id.0.as_str() != "Air" {
return Err(ComponentError::InvalidState(format!(
"CondenserCoil requires Air on the cold side, found {}",
fluid_id.0.as_str()
)));
}
self.air_validated
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
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_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 {
self.inner.signature()
}
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 CondenserCoil {
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_coil_creation() {
let coil = CondenserCoil::new(10_000.0);
assert_eq!(coil.ua(), 10_000.0);
assert_eq!(coil.name(), "CondenserCoil");
}
#[test]
fn test_condenser_coil_n_equations() {
let coil = CondenserCoil::new(10_000.0);
assert_eq!(coil.n_equations(), 2);
}
#[test]
fn test_condenser_coil_with_saturation_temp() {
let coil = CondenserCoil::with_saturation_temp(10_000.0, 323.15);
assert_eq!(coil.saturation_temp(), 323.15);
}
#[test]
fn test_condenser_coil_compute_residuals() {
let coil = CondenserCoil::new(10_000.0);
let state = vec![0.0; 10];
let mut residuals = vec![0.0; 3];
let result = coil.compute_residuals(&state, &mut residuals);
assert!(result.is_ok());
assert!(
residuals.iter().all(|r| r.is_finite()),
"residuals must be finite"
);
}
#[test]
fn test_condenser_coil_rejects_non_air() {
use crate::heat_exchanger::HxSideConditions;
use entropyk_core::{MassFlow, Pressure, Temperature};
let mut coil = CondenserCoil::new(10_000.0);
coil.inner.set_cold_conditions(
HxSideConditions::new(
Temperature::from_celsius(20.0),
Pressure::from_bar(1.0),
MassFlow::from_kg_per_s(1.0),
"Water",
)
.expect("Valid cold conditions"),
);
let state = vec![0.0; 10];
let mut residuals = vec![0.0; 3];
let result = coil.compute_residuals(&state, &mut residuals);
assert!(result.is_err());
if let Err(ComponentError::InvalidState(msg)) = result {
assert!(msg.contains("requires Air"));
} else {
panic!("Expected InvalidState error");
}
}
#[test]
fn test_condenser_coil_jacobian_entries() {
let coil = CondenserCoil::new(10_000.0);
let state = vec![0.0; 10];
let mut jacobian = crate::JacobianBuilder::new();
let result = coil.jacobian_entries(&state, &mut jacobian);
assert!(result.is_ok());
// HeatExchanger base returns empty jacobian until framework implements it
assert!(
jacobian.is_empty(),
"delegation works; empty jacobian expected until HeatExchanger implements entries"
);
}
#[test]
fn test_condenser_coil_set_saturation_temp() {
let mut coil = CondenserCoil::new(10_000.0);
coil.set_saturation_temp(320.0);
assert!((coil.saturation_temp() - 320.0).abs() < 1e-10);
}
#[test]
fn test_condenser_coil_state_manageable() {
use crate::state_machine::{OperationalState, StateManageable};
let mut coil = CondenserCoil::new(10_000.0);
assert_eq!(coil.state(), OperationalState::On);
assert!(coil.can_transition_to(OperationalState::Off));
assert!(coil.set_state(OperationalState::Off).is_ok());
assert_eq!(coil.state(), OperationalState::Off);
}
}