Removed mathematical singularity in HeatExchanger models (q_hot - q_cold = 0 was redundant) causing them to incorrectly request 3 equations without internal variables. Fixed ScrewEconomizerCompressor internal_state_len to perfectly align with the solver dimensions.
729 lines
24 KiB
Rust
729 lines
24 KiB
Rust
//! Drum - Recirculation Drum for Flooded Evaporators
|
|
//!
|
|
//! This module provides a recirculation drum component that separates a two-phase
|
|
//! mixture into saturated liquid and saturated vapor. Used in recirculation
|
|
//! evaporator systems to improve heat transfer.
|
|
//!
|
|
//! ## Physical Description
|
|
//!
|
|
//! A recirculation drum receives:
|
|
//! 1. Feed from economizer (typically subcooled or two-phase)
|
|
//! 2. Return from evaporator (enriched two-phase)
|
|
//!
|
|
//! And separates into:
|
|
//! - Saturated liquid (x=0) to the recirculation pump
|
|
//! - Saturated vapor (x=1) to the compressor
|
|
//!
|
|
//! ## Equations (8 total)
|
|
//!
|
|
//! | # | Equation | Description |
|
|
//! |---|----------|-------------|
|
|
//! | 1 | `m_liq + m_vap = m_feed + m_return` | Mass balance |
|
|
//! | 2 | `m_liq * h_liq + m_vap * h_vap = m_total * h_mixed` | Energy balance |
|
|
//! | 3 | `P_liq - P_feed = 0` | Pressure equality (liquid) |
|
|
//! | 4 | `P_vap - P_feed = 0` | Pressure equality (vapor) |
|
|
//! | 5 | `h_liq - h_sat(P, x=0) = 0` | Saturated liquid |
|
|
//! | 6 | `h_vap - h_sat(P, x=1) = 0` | Saturated vapor |
|
|
//! | 7-8 | Fluid continuity | Implicit via FluidId |
|
|
//!
|
|
//! ## Example
|
|
//!
|
|
//! ```ignore
|
|
//! use entropyk_components::Drum;
|
|
//! use entropyk_components::port::{Port, FluidId};
|
|
//! use entropyk_core::{Pressure, Enthalpy};
|
|
//! use entropyk_fluids::CoolPropBackend;
|
|
//! use std::sync::Arc;
|
|
//!
|
|
//! let backend = Arc::new(CoolPropBackend::new());
|
|
//!
|
|
//! let feed_inlet = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(250.0));
|
|
//! let evaporator_return = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(350.0));
|
|
//! let liquid_outlet = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(200.0));
|
|
//! let vapor_outlet = Port::new(FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_kj_per_kg(420.0));
|
|
//!
|
|
//! let drum = Drum::new("R410A", feed_inlet, evaporator_return, liquid_outlet, vapor_outlet, backend)?;
|
|
//! ```
|
|
|
|
use crate::port::{ConnectedPort, FluidId};
|
|
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
|
|
use crate::{Component, ComponentError, JacobianBuilder, ResidualVector, StateSlice};
|
|
use entropyk_core::{Enthalpy, MassFlow, Power, Pressure};
|
|
use entropyk_fluids::{FluidBackend, FluidState, Property, Quality};
|
|
use std::sync::Arc;
|
|
|
|
/// Drum - Recirculation drum for flooded evaporator systems.
|
|
///
|
|
/// Separates a two-phase mixture (2 inlets) into:
|
|
/// - Saturated liquid (x=0) to the recirculation pump
|
|
/// - Saturated vapor (x=1) to the compressor
|
|
///
|
|
/// The drum requires a [`FluidBackend`] to calculate saturation properties.
|
|
pub struct Drum {
|
|
/// Fluid identifier (must be pure or pseudo-pure for saturation calculations)
|
|
fluid_id: String,
|
|
/// Feed inlet (from economizer)
|
|
feed_inlet: ConnectedPort,
|
|
/// Evaporator return (two-phase enriched)
|
|
evaporator_return: ConnectedPort,
|
|
/// Liquid outlet (saturated, x=0) to pump
|
|
liquid_outlet: ConnectedPort,
|
|
/// Vapor outlet (saturated, x=1) to compressor
|
|
vapor_outlet: ConnectedPort,
|
|
/// Fluid backend for saturation calculations
|
|
fluid_backend: Arc<dyn FluidBackend>,
|
|
/// Circuit identifier
|
|
circuit_id: CircuitId,
|
|
/// Operational state
|
|
operational_state: OperationalState,
|
|
}
|
|
|
|
impl std::fmt::Debug for Drum {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("Drum")
|
|
.field("fluid_id", &self.fluid_id)
|
|
.field("circuit_id", &self.circuit_id)
|
|
.field("operational_state", &self.operational_state)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Drum {
|
|
/// Creates a new recirculation drum.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `fluid` - Fluid identifier (e.g., "R410A", "R134a")
|
|
/// * `feed_inlet` - Feed inlet port (from economizer)
|
|
/// * `evaporator_return` - Evaporator return port (two-phase)
|
|
/// * `liquid_outlet` - Liquid outlet port (to pump)
|
|
/// * `vapor_outlet` - Vapor outlet port (to compressor)
|
|
/// * `backend` - Fluid backend for saturation calculations
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if ports have incompatible fluids.
|
|
pub fn new(
|
|
fluid: impl Into<String>,
|
|
feed_inlet: ConnectedPort,
|
|
evaporator_return: ConnectedPort,
|
|
liquid_outlet: ConnectedPort,
|
|
vapor_outlet: ConnectedPort,
|
|
backend: Arc<dyn FluidBackend>,
|
|
) -> Result<Self, ComponentError> {
|
|
let fluid_id = fluid.into();
|
|
|
|
Self::validate_fluids(
|
|
&fluid_id,
|
|
&feed_inlet,
|
|
&evaporator_return,
|
|
&liquid_outlet,
|
|
&vapor_outlet,
|
|
)?;
|
|
|
|
Ok(Self {
|
|
fluid_id,
|
|
feed_inlet,
|
|
evaporator_return,
|
|
liquid_outlet,
|
|
vapor_outlet,
|
|
fluid_backend: backend,
|
|
circuit_id: CircuitId::default(),
|
|
operational_state: OperationalState::default(),
|
|
})
|
|
}
|
|
|
|
fn validate_fluids(
|
|
expected: &str,
|
|
feed: &ConnectedPort,
|
|
ret: &ConnectedPort,
|
|
liq: &ConnectedPort,
|
|
vap: &ConnectedPort,
|
|
) -> Result<(), ComponentError> {
|
|
let expected_fluid = FluidId::new(expected);
|
|
|
|
if feed.fluid_id() != &expected_fluid {
|
|
return Err(ComponentError::InvalidState(format!(
|
|
"Drum feed_inlet fluid mismatch: expected {}, got {}",
|
|
expected,
|
|
feed.fluid_id().as_str()
|
|
)));
|
|
}
|
|
if ret.fluid_id() != &expected_fluid {
|
|
return Err(ComponentError::InvalidState(format!(
|
|
"Drum evaporator_return fluid mismatch: expected {}, got {}",
|
|
expected,
|
|
ret.fluid_id().as_str()
|
|
)));
|
|
}
|
|
if liq.fluid_id() != &expected_fluid {
|
|
return Err(ComponentError::InvalidState(format!(
|
|
"Drum liquid_outlet fluid mismatch: expected {}, got {}",
|
|
expected,
|
|
liq.fluid_id().as_str()
|
|
)));
|
|
}
|
|
if vap.fluid_id() != &expected_fluid {
|
|
return Err(ComponentError::InvalidState(format!(
|
|
"Drum vapor_outlet fluid mismatch: expected {}, got {}",
|
|
expected,
|
|
vap.fluid_id().as_str()
|
|
)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the fluid identifier.
|
|
pub fn fluid_id(&self) -> &str {
|
|
&self.fluid_id
|
|
}
|
|
|
|
/// Returns the feed inlet port.
|
|
pub fn feed_inlet(&self) -> &ConnectedPort {
|
|
&self.feed_inlet
|
|
}
|
|
|
|
/// Returns the evaporator return port.
|
|
pub fn evaporator_return(&self) -> &ConnectedPort {
|
|
&self.evaporator_return
|
|
}
|
|
|
|
/// Returns the liquid outlet port.
|
|
pub fn liquid_outlet(&self) -> &ConnectedPort {
|
|
&self.liquid_outlet
|
|
}
|
|
|
|
/// Returns the vapor outlet port.
|
|
pub fn vapor_outlet(&self) -> &ConnectedPort {
|
|
&self.vapor_outlet
|
|
}
|
|
|
|
/// Returns the recirculation ratio (m_liquid / m_feed).
|
|
///
|
|
/// Requires mass flow information to be available in the state vector.
|
|
/// Returns 0.0 if mass flow cannot be determined (e.g., zero feed flow).
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `state` - State vector containing mass flows at indices 0-3:
|
|
/// - state[0]: m_feed (feed inlet mass flow)
|
|
/// - state[1]: m_return (evaporator return mass flow)
|
|
/// - state[2]: m_liq (liquid outlet mass flow, positive = out)
|
|
/// - state[3]: m_vap (vapor outlet mass flow, positive = out)
|
|
pub fn recirculation_ratio(&self, state: &StateSlice) -> f64 {
|
|
if state.len() < 4 {
|
|
return 0.0;
|
|
}
|
|
|
|
let m_feed = state[0];
|
|
let m_liq = state[2]; // Liquid outlet flow (positive = leaving drum)
|
|
|
|
if m_feed.abs() < 1e-10 {
|
|
0.0
|
|
} else {
|
|
m_liq / m_feed
|
|
}
|
|
}
|
|
|
|
/// Gets saturated liquid enthalpy at a given pressure.
|
|
fn saturated_liquid_enthalpy(&self, pressure_pa: f64) -> Result<f64, ComponentError> {
|
|
let fluid = FluidId::new(&self.fluid_id);
|
|
let state = FluidState::from_px(Pressure::from_pascals(pressure_pa), Quality(0.0));
|
|
|
|
self.fluid_backend
|
|
.property(fluid, Property::Enthalpy, state)
|
|
.map_err(|e| {
|
|
ComponentError::CalculationFailed(format!(
|
|
"Failed to get saturated liquid enthalpy: {}",
|
|
e
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Gets saturated vapor enthalpy at a given pressure.
|
|
fn saturated_vapor_enthalpy(&self, pressure_pa: f64) -> Result<f64, ComponentError> {
|
|
let fluid = FluidId::new(&self.fluid_id);
|
|
let state = FluidState::from_px(Pressure::from_pascals(pressure_pa), Quality(1.0));
|
|
|
|
self.fluid_backend
|
|
.property(fluid, Property::Enthalpy, state)
|
|
.map_err(|e| {
|
|
ComponentError::CalculationFailed(format!(
|
|
"Failed to get saturated vapor enthalpy: {}",
|
|
e
|
|
))
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Clone for Drum {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
fluid_id: self.fluid_id.clone(),
|
|
feed_inlet: self.feed_inlet.clone(),
|
|
evaporator_return: self.evaporator_return.clone(),
|
|
liquid_outlet: self.liquid_outlet.clone(),
|
|
vapor_outlet: self.vapor_outlet.clone(),
|
|
fluid_backend: Arc::clone(&self.fluid_backend),
|
|
circuit_id: self.circuit_id,
|
|
operational_state: self.operational_state,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Component for Drum {
|
|
/// Returns 8 equations:
|
|
/// - 1 mass balance
|
|
/// - 1 energy balance
|
|
/// - 2 pressure equalities
|
|
/// - 2 saturation constraints
|
|
/// - 2 fluid continuity (implicit)
|
|
fn n_equations(&self) -> usize {
|
|
8
|
|
}
|
|
|
|
fn compute_residuals(
|
|
&self,
|
|
state: &StateSlice,
|
|
residuals: &mut ResidualVector,
|
|
) -> Result<(), ComponentError> {
|
|
let n_eqs = self.n_equations();
|
|
if residuals.len() < n_eqs {
|
|
return Err(ComponentError::InvalidResidualDimensions {
|
|
expected: n_eqs,
|
|
actual: residuals.len(),
|
|
});
|
|
}
|
|
|
|
if state.len() < 4 {
|
|
return Err(ComponentError::InvalidStateDimensions {
|
|
expected: 4,
|
|
actual: state.len(),
|
|
});
|
|
}
|
|
|
|
// State variables:
|
|
// state[0]: m_feed (feed inlet mass flow, kg/s)
|
|
// state[1]: m_return (evaporator return mass flow, kg/s)
|
|
// state[2]: m_liq (liquid outlet mass flow, kg/s, positive = leaving drum)
|
|
// state[3]: m_vap (vapor outlet mass flow, kg/s, positive = leaving drum)
|
|
|
|
let m_feed = state[0];
|
|
let m_return = state[1];
|
|
let m_liq = state[2];
|
|
let m_vap = state[3];
|
|
|
|
let p_feed = self.feed_inlet.pressure().to_pascals();
|
|
let h_feed = self.feed_inlet.enthalpy().to_joules_per_kg();
|
|
let h_return = self.evaporator_return.enthalpy().to_joules_per_kg();
|
|
|
|
let p_liq = self.liquid_outlet.pressure().to_pascals();
|
|
let h_liq = self.liquid_outlet.enthalpy().to_joules_per_kg();
|
|
|
|
let p_vap = self.vapor_outlet.pressure().to_pascals();
|
|
let h_vap = self.vapor_outlet.enthalpy().to_joules_per_kg();
|
|
|
|
let h_sat_l = self.saturated_liquid_enthalpy(p_feed)?;
|
|
let h_sat_v = self.saturated_vapor_enthalpy(p_feed)?;
|
|
|
|
let mut idx = 0;
|
|
|
|
// Equation 1: Pressure equality (liquid)
|
|
// P_liq - P_feed = 0
|
|
residuals[idx] = p_liq - p_feed;
|
|
idx += 1;
|
|
|
|
// Equation 2: Pressure equality (vapor)
|
|
// P_vap - P_feed = 0
|
|
residuals[idx] = p_vap - p_feed;
|
|
idx += 1;
|
|
|
|
// Equation 3: Saturated liquid constraint
|
|
// h_liq - h_sat(P, x=0) = 0
|
|
residuals[idx] = h_liq - h_sat_l;
|
|
idx += 1;
|
|
|
|
// Equation 4: Saturated vapor constraint
|
|
// h_vap - h_sat(P, x=1) = 0
|
|
residuals[idx] = h_vap - h_sat_v;
|
|
idx += 1;
|
|
|
|
// Equation 5: Mass balance
|
|
// m_liq + m_vap = m_feed + m_return
|
|
// Residual: (m_liq + m_vap) - (m_feed + m_return) = 0
|
|
residuals[idx] = (m_liq + m_vap) - (m_feed + m_return);
|
|
idx += 1;
|
|
|
|
// Equation 6: Energy balance
|
|
// m_liq * h_liq + m_vap * h_vap = m_feed * h_feed + m_return * h_return
|
|
// Residual: (m_liq * h_liq + m_vap * h_vap) - (m_feed * h_feed + m_return * h_return) = 0
|
|
let energy_out = m_liq * h_liq + m_vap * h_vap;
|
|
let energy_in = m_feed * h_feed + m_return * h_return;
|
|
residuals[idx] = energy_out - energy_in;
|
|
idx += 1;
|
|
|
|
// Equations 7-8: Fluid continuity (implicit, enforced by using same fluid_id)
|
|
// These are satisfied by construction since all ports use the same fluid
|
|
residuals[idx] = 0.0;
|
|
idx += 1;
|
|
residuals[idx] = 0.0;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn jacobian_entries(
|
|
&self,
|
|
_state: &StateSlice,
|
|
jacobian: &mut JacobianBuilder,
|
|
) -> Result<(), ComponentError> {
|
|
let n_eqs = self.n_equations();
|
|
for i in 0..n_eqs {
|
|
jacobian.add_entry(i, i, 1.0);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn get_ports(&self) -> &[ConnectedPort] {
|
|
// Note: This is a temporary implementation that returns an empty slice.
|
|
// To properly return the ports, we would need to store them in a Vec
|
|
// or use a different approach. For now, we document the ports here:
|
|
// - Port 0: feed_inlet (from economizer)
|
|
// - Port 1: evaporator_return (two-phase enriched)
|
|
// - Port 2: liquid_outlet (to pump)
|
|
// - Port 3: vapor_outlet (to compressor)
|
|
&[]
|
|
}
|
|
|
|
fn energy_transfers(&self, _state: &StateSlice) -> Option<(Power, Power)> {
|
|
Some((Power::from_watts(0.0), Power::from_watts(0.0)))
|
|
}
|
|
|
|
fn port_mass_flows(&self, state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
|
if state.len() < 4 {
|
|
return Err(ComponentError::InvalidStateDimensions {
|
|
expected: 4,
|
|
actual: state.len(),
|
|
});
|
|
}
|
|
|
|
Ok(vec![
|
|
MassFlow::from_kg_per_s(state[0]),
|
|
MassFlow::from_kg_per_s(state[1]),
|
|
MassFlow::from_kg_per_s(-state[2]),
|
|
MassFlow::from_kg_per_s(-state[3]),
|
|
])
|
|
}
|
|
|
|
fn port_enthalpies(&self, _state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
|
|
Ok(vec![
|
|
self.feed_inlet.enthalpy(),
|
|
self.evaporator_return.enthalpy(),
|
|
self.liquid_outlet.enthalpy(),
|
|
self.vapor_outlet.enthalpy(),
|
|
])
|
|
}
|
|
|
|
fn signature(&self) -> String {
|
|
format!("Drum({})", self.fluid_id)
|
|
}
|
|
}
|
|
|
|
impl StateManageable for Drum {
|
|
fn state(&self) -> OperationalState {
|
|
self.operational_state
|
|
}
|
|
|
|
fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
|
|
if self.operational_state.can_transition_to(state) {
|
|
self.operational_state = state;
|
|
Ok(())
|
|
} else {
|
|
Err(ComponentError::InvalidStateTransition {
|
|
from: self.operational_state,
|
|
to: state,
|
|
reason: "Transition not allowed".to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn can_transition_to(&self, target: OperationalState) -> bool {
|
|
self.operational_state.can_transition_to(target)
|
|
}
|
|
|
|
fn circuit_id(&self) -> &CircuitId {
|
|
&self.circuit_id
|
|
}
|
|
|
|
fn set_circuit_id(&mut self, circuit_id: CircuitId) {
|
|
self.circuit_id = circuit_id;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::port::Port;
|
|
|
|
fn create_connected_port(fluid: &str, pressure_pa: f64, enthalpy_j_kg: f64) -> ConnectedPort {
|
|
let p1 = Port::new(
|
|
FluidId::new(fluid),
|
|
Pressure::from_pascals(pressure_pa),
|
|
Enthalpy::from_joules_per_kg(enthalpy_j_kg),
|
|
);
|
|
let p2 = Port::new(
|
|
FluidId::new(fluid),
|
|
Pressure::from_pascals(pressure_pa),
|
|
Enthalpy::from_joules_per_kg(enthalpy_j_kg),
|
|
);
|
|
let (c1, _c2) = p1.connect(p2).expect("ports should connect");
|
|
c1
|
|
}
|
|
|
|
fn create_test_drum() -> Drum {
|
|
let backend = Arc::new(entropyk_fluids::TestBackend::new());
|
|
|
|
let feed_inlet = create_connected_port("R410A", 1_000_000.0, 250_000.0);
|
|
let evaporator_return = create_connected_port("R410A", 1_000_000.0, 350_000.0);
|
|
let liquid_outlet = create_connected_port("R410A", 1_000_000.0, 200_000.0);
|
|
let vapor_outlet = create_connected_port("R410A", 1_000_000.0, 400_000.0);
|
|
|
|
Drum::new(
|
|
"R410A",
|
|
feed_inlet,
|
|
evaporator_return,
|
|
liquid_outlet,
|
|
vapor_outlet,
|
|
backend,
|
|
)
|
|
.expect("drum should be created")
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_equations_count() {
|
|
let drum = create_test_drum();
|
|
assert_eq!(drum.n_equations(), 8);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_fluid_id() {
|
|
let drum = create_test_drum();
|
|
assert_eq!(drum.fluid_id(), "R410A");
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_energy_transfers() {
|
|
let drum = create_test_drum();
|
|
let state: Vec<f64> = vec![];
|
|
let (heat, work) = drum.energy_transfers(&state).unwrap();
|
|
assert_eq!(heat.to_watts(), 0.0);
|
|
assert_eq!(work.to_watts(), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_state_manageable() {
|
|
let drum = create_test_drum();
|
|
assert_eq!(drum.state(), OperationalState::On);
|
|
assert!(drum.can_transition_to(OperationalState::Off));
|
|
assert!(drum.can_transition_to(OperationalState::Bypass));
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_compute_residuals() {
|
|
let drum = create_test_drum();
|
|
let state: Vec<f64> = vec![0.1, 0.2, 0.15, 0.05];
|
|
let mut residuals = vec![0.0; 8];
|
|
|
|
let result = drum.compute_residuals(&state, &mut residuals);
|
|
|
|
// TestBackend doesn't support FluidState::from_px for saturation queries,
|
|
// so the computation will fail. This is expected - the Drum component
|
|
// requires a real backend (CoolProp) for saturation properties.
|
|
// We test that the method correctly propagates the error.
|
|
assert!(
|
|
result.is_err(),
|
|
"Expected error from TestBackend (doesn't support from_px)"
|
|
);
|
|
|
|
// Verify error message mentions saturation
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(
|
|
err_msg.contains("saturated") || err_msg.contains("UnsupportedProperty"),
|
|
"Error should mention saturation or unsupported property: {}",
|
|
err_msg
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_jacobian_entries() {
|
|
let drum = create_test_drum();
|
|
let state: Vec<f64> = vec![];
|
|
let mut jacobian = JacobianBuilder::new();
|
|
|
|
let result = drum.jacobian_entries(&state, &mut jacobian);
|
|
assert!(result.is_ok());
|
|
assert_eq!(jacobian.len(), 8);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_invalid_state_dimensions() {
|
|
let drum = create_test_drum();
|
|
let state: Vec<f64> = vec![];
|
|
let mut residuals = vec![0.0; 4];
|
|
|
|
let result = drum.compute_residuals(&state, &mut residuals);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_port_mass_flows() {
|
|
let drum = create_test_drum();
|
|
let state: Vec<f64> = vec![0.1, 0.2, 0.15, 0.05];
|
|
|
|
let flows = drum.port_mass_flows(&state).unwrap();
|
|
assert_eq!(flows.len(), 4);
|
|
assert!((flows[0].to_kg_per_s() - 0.1).abs() < 1e-10);
|
|
assert!((flows[1].to_kg_per_s() - 0.2).abs() < 1e-10);
|
|
assert!((flows[2].to_kg_per_s() - (-0.15)).abs() < 1e-10);
|
|
assert!((flows[3].to_kg_per_s() - (-0.05)).abs() < 1e-10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_port_enthalpies() {
|
|
let drum = create_test_drum();
|
|
let state: Vec<f64> = vec![];
|
|
|
|
let enthalpies = drum.port_enthalpies(&state).unwrap();
|
|
assert_eq!(enthalpies.len(), 4);
|
|
assert!((enthalpies[0].to_joules_per_kg() - 250_000.0).abs() < 1e-10);
|
|
assert!((enthalpies[1].to_joules_per_kg() - 350_000.0).abs() < 1e-10);
|
|
assert!((enthalpies[2].to_joules_per_kg() - 200_000.0).abs() < 1e-10);
|
|
assert!((enthalpies[3].to_joules_per_kg() - 400_000.0).abs() < 1e-10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_signature() {
|
|
let drum = create_test_drum();
|
|
assert_eq!(drum.signature(), "Drum(R410A)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_fluid_mismatch() {
|
|
let backend = Arc::new(entropyk_fluids::TestBackend::new());
|
|
|
|
let feed_inlet = create_connected_port("R410A", 1_000_000.0, 250_000.0);
|
|
let evaporator_return = create_connected_port("R134a", 1_000_000.0, 350_000.0);
|
|
let liquid_outlet = create_connected_port("R410A", 1_000_000.0, 200_000.0);
|
|
let vapor_outlet = create_connected_port("R410A", 1_000_000.0, 400_000.0);
|
|
|
|
let result = Drum::new(
|
|
"R410A",
|
|
feed_inlet,
|
|
evaporator_return,
|
|
liquid_outlet,
|
|
vapor_outlet,
|
|
backend,
|
|
);
|
|
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_state_transition() {
|
|
let mut drum = create_test_drum();
|
|
|
|
assert!(drum.set_state(OperationalState::Off).is_ok());
|
|
assert_eq!(drum.state(), OperationalState::Off);
|
|
|
|
assert!(drum.set_state(OperationalState::Bypass).is_ok());
|
|
assert_eq!(drum.state(), OperationalState::Bypass);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_circuit_id() {
|
|
let mut drum = create_test_drum();
|
|
let new_id = CircuitId::from_number(42);
|
|
drum.set_circuit_id(new_id);
|
|
assert_eq!(drum.circuit_id(), &new_id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_clone() {
|
|
let drum = create_test_drum();
|
|
let cloned = drum.clone();
|
|
assert_eq!(drum.fluid_id(), cloned.fluid_id());
|
|
assert_eq!(drum.n_equations(), cloned.n_equations());
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_debug() {
|
|
let drum = create_test_drum();
|
|
let debug_str = format!("{:?}", drum);
|
|
assert!(debug_str.contains("Drum"));
|
|
assert!(debug_str.contains("R410A"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_recirculation_ratio_basic() {
|
|
let drum = create_test_drum();
|
|
// state[0] = m_feed, state[2] = m_liq
|
|
// ratio = m_liq / m_feed
|
|
let state = vec![0.1, 0.2, 0.25, 0.05]; // m_feed=0.1, m_liq=0.25
|
|
let ratio = drum.recirculation_ratio(&state);
|
|
assert!(
|
|
(ratio - 2.5).abs() < 1e-10,
|
|
"Expected ratio 2.5, got {}",
|
|
ratio
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_recirculation_ratio_zero_feed() {
|
|
let drum = create_test_drum();
|
|
// Zero feed flow should return 0.0 (avoid division by zero)
|
|
let state = vec![0.0, 0.2, 0.15, 0.05];
|
|
let ratio = drum.recirculation_ratio(&state);
|
|
assert_eq!(ratio, 0.0, "Expected ratio 0.0 for zero feed flow");
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_recirculation_ratio_small_feed() {
|
|
let drum = create_test_drum();
|
|
// Very small feed flow should return 0.0 (avoid numerical issues)
|
|
let state = vec![1e-12, 0.2, 0.15, 0.05];
|
|
let ratio = drum.recirculation_ratio(&state);
|
|
assert_eq!(ratio, 0.0, "Expected ratio 0.0 for very small feed flow");
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_recirculation_ratio_empty_state() {
|
|
let drum = create_test_drum();
|
|
// Empty state should return 0.0
|
|
let state: Vec<f64> = vec![];
|
|
let ratio = drum.recirculation_ratio(&state);
|
|
assert_eq!(ratio, 0.0, "Expected ratio 0.0 for empty state");
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_recirculation_ratio_insufficient_state() {
|
|
let drum = create_test_drum();
|
|
// State with less than 4 elements should return 0.0
|
|
let state = vec![0.1, 0.2, 0.15]; // Only 3 elements
|
|
let ratio = drum.recirculation_ratio(&state);
|
|
assert_eq!(ratio, 0.0, "Expected ratio 0.0 for insufficient state");
|
|
}
|
|
|
|
#[test]
|
|
fn test_drum_recirculation_ratio_unity() {
|
|
let drum = create_test_drum();
|
|
// When m_liq = m_feed, ratio should be 1.0
|
|
let state = vec![0.1, 0.1, 0.1, 0.1]; // m_feed=0.1, m_liq=0.1
|
|
let ratio = drum.recirculation_ratio(&state);
|
|
assert!(
|
|
(ratio - 1.0).abs() < 1e-10,
|
|
"Expected ratio 1.0, got {}",
|
|
ratio
|
|
);
|
|
}
|
|
}
|