Files
Entropyk/crates/components/src/expansion_valve.rs

1730 lines
55 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.
//! Expansion Valve Component Implementation
//!
//! This module provides an expansion valve component that models isenthalpic
//! expansion in refrigeration systems. The expansion valve reduces pressure
//! while maintaining constant enthalpy (throttling process).
//!
//! ## Thermodynamic Model
//!
//! The expansion valve is modeled as an isenthalpic device:
//! ```text
//! h_out = h_in (enthalpy conservation - isenthalpic)
//! ṁ_out = ṁ_in (mass flow continuity)
//! P_out < P_in (pressure drop - throttling)
//! W = 0 (no work done)
//! Q = 0 (adiabatic)
//! ```
//!
//! ## Operational States
//!
//! - **On**: Normal expansion with isenthalpic process
//! - **Off**: Zero mass flow through the valve
//! - **Bypass**: Acts as adiabatic pipe (P_in = P_out, h_in = h_out)
//!
//! ## Example
//!
//! ```rust
//! use entropyk_components::expansion_valve::ExpansionValve;
//! use entropyk_components::port::{FluidId, Port};
//! use entropyk_core::{Pressure, Enthalpy};
//!
//! // Create disconnected ports
//! let inlet = Port::new(
//! FluidId::new("R134a"),
//! Pressure::from_bar(10.0),
//! Enthalpy::from_joules_per_kg(250000.0)
//! );
//! let outlet = Port::new(
//! FluidId::new("R134a"),
//! Pressure::from_bar(10.0),
//! Enthalpy::from_joules_per_kg(250000.0)
//! );
//!
//! // Create expansion valve
//! let valve = ExpansionValve::new(inlet, outlet, None).unwrap();
//! ```
use crate::port::{Connected, Disconnected, FluidId, Port};
use crate::{
CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
ResidualVector, StateSlice,
};
use entropyk_core::Calib;
use std::marker::PhantomData;
const OPENING_THRESHOLD: f64 = 0.01;
const ENTHALPY_TOLERANCE_J_KG: f64 = 100.0;
const MIN_STATE_DIMENSIONS: usize = 2;
fn is_effectively_off_impl(operational_state: OperationalState, opening: Option<f64>) -> bool {
operational_state == OperationalState::Off || opening.is_some_and(|o| o < OPENING_THRESHOLD)
}
/// Expansion valve component for modeling isenthalpic expansion.
///
/// The expansion valve is a throttling device that reduces pressure while
/// maintaining constant enthalpy (isenthalpic process). It implements the
/// [`Component`] trait for integration with the solver.
///
/// # Type Parameters
///
/// * `State` - Either `Disconnected` or `Connected`, tracking connection state
///
/// # Example
///
/// ```rust
/// use entropyk_components::expansion_valve::ExpansionValve;
/// use entropyk_components::port::{FluidId, Port};
/// use entropyk_core::{Pressure, Enthalpy};
///
/// // Create disconnected ports
/// let inlet = Port::new(
/// FluidId::new("R134a"),
/// Pressure::from_bar(10.0),
/// Enthalpy::from_joules_per_kg(250000.0)
/// );
/// let outlet = Port::new(
/// FluidId::new("R134a"),
/// Pressure::from_bar(10.0),
/// Enthalpy::from_joules_per_kg(250000.0)
/// );
///
/// // Create expansion valve with optional opening parameter
/// let valve = ExpansionValve::new(inlet, outlet, Some(1.0)).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct ExpansionValve<State> {
port_inlet: Port<State>,
port_outlet: Port<State>,
/// Calibration: ṁ_eff = f_m × ṁ_nominal (mass flow scaling)
calib: Calib,
/// Calibration indices to extract factors dynamically from SystemState
pub calib_indices: entropyk_core::CalibIndices,
operational_state: OperationalState,
opening: Option<f64>,
fluid_id: FluidId,
circuit_id: CircuitId,
_state: PhantomData<State>,
}
impl ExpansionValve<Disconnected> {
/// Creates a new disconnected expansion valve.
///
/// # Arguments
///
/// * `port_inlet` - Inlet port (high pressure, subcooled liquid)
/// * `port_outlet` - Outlet port (low pressure, two-phase)
/// * `opening` - Optional opening parameter (0.0 = closed, 1.0 = fully open)
///
/// # Errors
///
/// Returns an error if:
/// - Opening is outside [0.0, 1.0] range
/// - Opening is NaN or infinite
/// - Ports have different fluid types
pub fn new(
port_inlet: Port<Disconnected>,
port_outlet: Port<Disconnected>,
opening: Option<f64>,
) -> Result<Self, ComponentError> {
if let Some(o) = opening {
if !(0.0..=1.0).contains(&o) {
return Err(ComponentError::InvalidState(format!(
"Opening must be between 0.0 and 1.0, got {}",
o
)));
}
if o.is_nan() || o.is_infinite() {
return Err(ComponentError::InvalidState(
"Opening must be a finite number".to_string(),
));
}
}
if port_inlet.fluid_id() != port_outlet.fluid_id() {
return Err(ComponentError::InvalidState(
"Inlet and outlet ports must have the same fluid type".to_string(),
));
}
let fluid_id = port_inlet.fluid_id().clone();
Ok(Self {
port_inlet,
port_outlet,
calib: Calib::default(),
calib_indices: entropyk_core::CalibIndices::default(),
operational_state: OperationalState::default(),
opening,
fluid_id,
circuit_id: CircuitId::default(),
_state: PhantomData,
})
}
/// Returns the fluid identifier.
pub fn fluid_id(&self) -> &FluidId {
&self.fluid_id
}
/// Returns the inlet port.
pub fn port_inlet(&self) -> &Port<Disconnected> {
&self.port_inlet
}
/// Returns the outlet port.
pub fn port_outlet(&self) -> &Port<Disconnected> {
&self.port_outlet
}
/// Returns the optional opening parameter (0.0 to 1.0).
pub fn opening(&self) -> Option<f64> {
self.opening
}
/// Returns the current operational state.
pub fn operational_state(&self) -> OperationalState {
self.operational_state
}
/// Sets the operational state.
pub fn set_operational_state(&mut self, state: OperationalState) {
self.operational_state = state;
}
/// Returns the circuit identifier.
pub fn circuit_id(&self) -> &CircuitId {
&self.circuit_id
}
/// Sets the circuit identifier.
pub fn set_circuit_id(&mut self, circuit_id: CircuitId) {
self.circuit_id = circuit_id;
}
/// Returns calibration factors (f_m for mass flow scaling).
pub fn calib(&self) -> &Calib {
&self.calib
}
/// Sets calibration factors.
pub fn set_calib(&mut self, calib: Calib) {
self.calib = calib;
}
/// Returns true if the valve is effectively off.
///
/// The valve is effectively off when:
/// - Operational state is Off, or
/// - Opening is below threshold (< 1%)
pub fn is_effectively_off(&self) -> bool {
is_effectively_off_impl(self.operational_state, self.opening)
}
/// Connects the expansion valve to inlet and outlet ports.
///
/// This consumes the disconnected valve and returns a connected one,
/// transitioning the state at compile time.
pub fn connect(
self,
inlet: Port<Disconnected>,
outlet: Port<Disconnected>,
) -> Result<ExpansionValve<Connected>, ComponentError> {
let (p_in, _) = self
.port_inlet
.connect(inlet)
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
let (p_out, _) = self
.port_outlet
.connect(outlet)
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
Ok(ExpansionValve {
port_inlet: p_in,
port_outlet: p_out,
calib: self.calib,
calib_indices: self.calib_indices,
operational_state: self.operational_state,
opening: self.opening,
fluid_id: self.fluid_id,
circuit_id: self.circuit_id,
_state: PhantomData,
})
}
}
/// Phase region at a thermodynamic state point.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PhaseRegion {
/// Subcooled liquid (below saturation line)
Subcooled,
/// Two-phase mixture (between saturated liquid and vapor)
TwoPhase,
/// Superheated vapor (above saturation line)
Superheated,
}
impl PhaseRegion {
/// Returns true if the region is two-phase.
pub fn is_two_phase(self) -> bool {
self == PhaseRegion::TwoPhase
}
}
impl ExpansionValve<Connected> {
/// Returns the inlet port.
pub fn port_inlet(&self) -> &Port<Connected> {
&self.port_inlet
}
/// Returns the outlet port.
pub fn port_outlet(&self) -> &Port<Connected> {
&self.port_outlet
}
/// Computes the full thermodynamic state at the inlet port.
pub fn inlet_state(
&self,
backend: &impl entropyk_fluids::FluidBackend,
) -> Result<entropyk_fluids::ThermoState, ComponentError> {
backend
.full_state(
entropyk_fluids::FluidId::new(self.port_inlet.fluid_id().as_str()),
self.port_inlet.pressure(),
self.port_inlet.enthalpy(),
)
.map_err(|e| {
ComponentError::CalculationFailed(format!("Failed to compute inlet state: {}", e))
})
}
/// Computes the full thermodynamic state at the outlet port.
pub fn outlet_state(
&self,
backend: &impl entropyk_fluids::FluidBackend,
) -> Result<entropyk_fluids::ThermoState, ComponentError> {
backend
.full_state(
entropyk_fluids::FluidId::new(self.port_outlet.fluid_id().as_str()),
self.port_outlet.pressure(),
self.port_outlet.enthalpy(),
)
.map_err(|e| {
ComponentError::CalculationFailed(format!("Failed to compute outlet state: {}", e))
})
}
/// Returns the optional opening parameter (0.0 to 1.0).
pub fn opening(&self) -> Option<f64> {
self.opening
}
/// Returns the current operational state.
pub fn operational_state(&self) -> OperationalState {
self.operational_state
}
/// Sets the operational state.
pub fn set_operational_state(&mut self, state: OperationalState) {
self.operational_state = state;
}
/// Returns the circuit identifier.
pub fn circuit_id(&self) -> &CircuitId {
&self.circuit_id
}
/// Sets the circuit identifier.
pub fn set_circuit_id(&mut self, circuit_id: CircuitId) {
self.circuit_id = circuit_id;
}
/// Returns the fluid identifier.
pub fn fluid_id(&self) -> &FluidId {
&self.fluid_id
}
/// Returns calibration factors (f_m for mass flow scaling).
pub fn calib(&self) -> &Calib {
&self.calib
}
/// Sets calibration factors.
pub fn set_calib(&mut self, calib: Calib) {
self.calib = calib;
}
/// Returns true if the valve is effectively off.
///
/// The valve is effectively off when:
/// - Operational state is Off, or
/// - Opening is below threshold (< 1%)
pub fn is_effectively_off(&self) -> bool {
is_effectively_off_impl(self.operational_state, self.opening)
}
/// Sets the valve opening parameter.
///
/// # Arguments
///
/// * `opening` - New opening value (0.0 = closed, 1.0 = fully open), or None
///
/// # Errors
///
/// Returns an error if opening is outside [0.0, 1.0] range or is NaN/infinite.
pub fn set_opening(&mut self, opening: Option<f64>) -> Result<(), ComponentError> {
if let Some(o) = opening {
if !(0.0..=1.0).contains(&o) {
return Err(ComponentError::InvalidState(format!(
"Opening must be between 0.0 and 1.0, got {}",
o
)));
}
if o.is_nan() || o.is_infinite() {
return Err(ComponentError::InvalidState(
"Opening must be a finite number".to_string(),
));
}
}
self.opening = opening;
Ok(())
}
/// Returns both ports as an array for solver topology.
pub fn get_ports_slice(&self) -> [&Port<Connected>; 2] {
[&self.port_inlet, &self.port_outlet]
}
/// Validates that the process is isenthalpic (h_in = h_out).
///
/// # Returns
///
/// Returns `Ok(true)` if inlet and outlet enthalpies are equal within tolerance.
pub fn validate_isenthalpic(&self) -> Result<bool, ComponentError> {
let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
if h_in.is_nan() || h_out.is_nan() {
return Err(ComponentError::NumericalError(
"Enthalpy contains NaN value".to_string(),
));
}
Ok((h_in - h_out).abs() < ENTHALPY_TOLERANCE_J_KG)
}
/// Validates that outlet pressure is lower than inlet pressure.
///
/// # Returns
///
/// Returns `Ok(true)` if P_out < P_in, indicating a pressure drop.
pub fn validate_pressure_drop(&self) -> Result<bool, ComponentError> {
let p_in = self.port_inlet.pressure().to_pascals();
let p_out = self.port_outlet.pressure().to_pascals();
if p_in <= 0.0 {
return Err(ComponentError::NumericalError(
"Inlet pressure must be positive".to_string(),
));
}
if p_out <= 0.0 {
return Err(ComponentError::NumericalError(
"Outlet pressure must be positive".to_string(),
));
}
Ok(p_out < p_in)
}
/// Returns the pressure ratio (P_out / P_in).
///
/// A value less than 1.0 indicates a pressure drop through the valve.
pub fn pressure_ratio(&self) -> f64 {
let p_in = self.port_inlet.pressure().to_pascals();
let p_out = self.port_outlet.pressure().to_pascals();
if p_in > 0.0 {
p_out / p_in
} else {
0.0
}
}
/// Detects the phase region of the outlet port based on pressure and enthalpy.
///
/// This method determines if the outlet is in subcooled, two-phase, or superheated
/// region by comparing against saturation enthalpy values at the outlet pressure.
///
/// # Arguments
///
/// * `h_f` - Saturated liquid enthalpy at outlet pressure (J/kg)
/// * `h_g` - Saturated vapor enthalpy at outlet pressure (J/kg)
///
/// # Returns
///
/// The phase region at the outlet.
pub fn detect_phase_region(&self, h_f: f64, h_g: f64) -> PhaseRegion {
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
if h_out < h_f {
PhaseRegion::Subcooled
} else if h_out > h_g {
PhaseRegion::Superheated
} else {
PhaseRegion::TwoPhase
}
}
/// Calculates the vapor quality at the outlet if in two-phase region.
///
/// Quality is defined as: x = (h - h_f) / (h_g - h_f)
/// - x = 0: Saturated liquid
/// - x = 1: Saturated vapor
/// - 0 < x < 1: Two-phase mixture
///
/// # Arguments
///
/// * `h_f` - Saturated liquid enthalpy at outlet pressure (J/kg)
/// * `h_g` - Saturated vapor enthalpy at outlet pressure (J/kg)
///
/// # Returns
///
/// Returns `Ok(quality)` if outlet is in two-phase region,
/// or `Err(ComponentError)` if quality calculation is not applicable.
pub fn outlet_quality(&self, h_f: f64, h_g: f64) -> Result<f64, ComponentError> {
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
let h_range = h_g - h_f;
if h_range <= 0.0 {
return Err(ComponentError::NumericalError(
"Invalid saturation enthalpy range (h_g must be greater than h_f)".to_string(),
));
}
if h_out < h_f || h_out > h_g {
return Err(ComponentError::InvalidState(format!(
"Outlet is not in two-phase region: h_out={} J/kg, h_f={} J/kg, h_g={} J/kg",
h_out, h_f, h_g
)));
}
Ok((h_out - h_f) / h_range)
}
/// Validates that phase change occurs from inlet to outlet.
///
/// For isenthalpic expansion, the outlet should typically be in two-phase
/// if the inlet was subcooled liquid.
///
/// # Arguments
///
/// * `h_f_out` - Saturated liquid enthalpy at outlet pressure (J/kg)
/// * `h_g_out` - Saturated vapor enthalpy at outlet pressure (J/kg)
///
/// # Returns
///
/// Returns `Ok(true)` if phase change occurs from inlet to outlet.
/// This is detected when the outlet is in two-phase region.
pub fn validate_phase_change(
&self,
h_f_out: f64,
h_g_out: f64,
) -> Result<bool, ComponentError> {
let _h_in = self.port_inlet.enthalpy().to_joules_per_kg();
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
if h_out >= h_f_out && h_out <= h_g_out {
return Ok(true);
}
Ok(false)
}
}
impl Component for ExpansionValve<Connected> {
fn compute_residuals(
&self,
state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
if residuals.len() != self.n_equations() {
return Err(ComponentError::InvalidResidualDimensions {
expected: self.n_equations(),
actual: residuals.len(),
});
}
if self.is_effectively_off() {
if state.is_empty() {
return Err(ComponentError::InvalidStateDimensions {
expected: MIN_STATE_DIMENSIONS,
actual: 0,
});
}
residuals[0] = state[0];
residuals[1] = 0.0;
return Ok(());
}
match self.operational_state {
OperationalState::Bypass => {
let p_in = self.port_inlet.pressure().to_pascals();
let p_out = self.port_outlet.pressure().to_pascals();
let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
residuals[0] = p_out - p_in;
residuals[1] = h_out - h_in;
return Ok(());
}
OperationalState::On | OperationalState::Off => {}
}
if state.len() < MIN_STATE_DIMENSIONS {
return Err(ComponentError::InvalidStateDimensions {
expected: MIN_STATE_DIMENSIONS,
actual: state.len(),
});
}
let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
residuals[0] = h_out - h_in;
// Mass flow: ṁ_out = f_m × ṁ_in (calibration factor on inlet flow)
let mass_flow_in = state[0];
let mass_flow_out = state[1];
let f_m = self
.calib_indices
.f_m
.map(|idx| state[idx])
.unwrap_or(self.calib.f_m);
residuals[1] = mass_flow_out - f_m * mass_flow_in;
Ok(())
}
fn jacobian_entries(
&self,
_state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
if self.is_effectively_off() {
jacobian.add_entry(0, 0, 1.0);
jacobian.add_entry(1, 0, 0.0);
return Ok(());
}
match self.operational_state {
OperationalState::Bypass => {
jacobian.add_entry(0, 0, 1.0);
jacobian.add_entry(0, 1, -1.0);
jacobian.add_entry(1, 0, 1.0);
jacobian.add_entry(1, 1, -1.0);
return Ok(());
}
OperationalState::On | OperationalState::Off => {}
}
let f_m = self
.calib_indices
.f_m
.map(|idx| _state[idx])
.unwrap_or(self.calib.f_m);
jacobian.add_entry(0, 0, 0.0);
jacobian.add_entry(0, 1, 0.0);
jacobian.add_entry(1, 0, -f_m);
jacobian.add_entry(1, 1, 1.0);
if let Some(idx) = self.calib_indices.f_m {
// d(R2)/d(f_m) = -mass_flow_in
// We need mass_flow_in here, which is _state[0]
let mass_flow_in = _state[0];
jacobian.add_entry(1, idx, -mass_flow_in);
}
Ok(())
}
fn n_equations(&self) -> usize {
2
}
fn port_mass_flows(
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
if state.len() < MIN_STATE_DIMENSIONS {
return Err(ComponentError::InvalidStateDimensions {
expected: MIN_STATE_DIMENSIONS,
actual: state.len(),
});
}
let m_in = entropyk_core::MassFlow::from_kg_per_s(state[0]);
let m_out = entropyk_core::MassFlow::from_kg_per_s(-state[1]); // Negative because it's leaving
Ok(vec![m_in, m_out])
}
/// Returns the enthalpies at the inlet and outlet ports.
///
/// For an expansion valve (isenthalpic device), the inlet and outlet
/// enthalpies should be equal: h_in ≈ h_out.
///
/// # Returns
///
/// A vector containing `[h_inlet, h_outlet]` in order.
fn port_enthalpies(
&self,
_state: &StateSlice,
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
Ok(vec![
self.port_inlet.enthalpy(),
self.port_outlet.enthalpy(),
])
}
/// Returns the energy transfers for the expansion valve.
///
/// An expansion valve is an isenthalpic throttling device:
/// - **Heat (Q)**: 0 W (adiabatic - no heat exchange with environment)
/// - **Work (W)**: 0 W (no moving parts - no mechanical work)
///
/// # Returns
///
/// `Some((Q=0, W=0))` always, since expansion valves are passive devices.
fn energy_transfers(
&self,
_state: &StateSlice,
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
match self.operational_state {
OperationalState::Off | OperationalState::Bypass | OperationalState::On => Some((
entropyk_core::Power::from_watts(0.0),
entropyk_core::Power::from_watts(0.0),
)),
}
}
fn get_ports(&self) -> &[ConnectedPort] {
&[]
}
fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
self.calib_indices = indices;
}
fn signature(&self) -> String {
format!(
"ExpansionValve(fluid={}, circuit={})",
self.fluid_id.as_str(),
self.circuit_id.0
)
}
fn to_params(&self) -> crate::ComponentParams {
crate::ComponentParams::new("ExpansionValve")
.with_param("fluid", self.fluid_id.as_str())
.with_param("circuitId", self.circuit_id.0)
.with_param("opening", self.opening)
.with_param("calib", serde_json::to_value(&self.calib).unwrap_or(serde_json::Value::Null))
}
fn update_calib_factor(&mut self, factor: &str, value: f64) -> bool {
let mut c = self.calib().clone();
if c.set_factor(factor, value) {
self.set_calib(c);
true
} else {
false
}
}
}
use crate::state_machine::StateManageable;
impl StateManageable for ExpansionValve<Connected> {
fn state(&self) -> OperationalState {
self.operational_state
}
fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
if self.operational_state.can_transition_to(state) {
let from = self.operational_state;
self.operational_state = state;
self.on_state_change(from, 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 approx::assert_relative_eq;
use entropyk_core::{Enthalpy, Pressure};
fn create_test_valve() -> ExpansionValve<Connected> {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
}
}
fn create_disconnected_valve() -> ExpansionValve<Disconnected> {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
ExpansionValve::new(inlet, outlet, Some(1.0)).unwrap()
}
#[test]
fn test_valve_creation() {
let valve = create_disconnected_valve();
assert_eq!(valve.fluid_id().as_str(), "R134a");
assert_eq!(valve.opening(), Some(1.0));
assert_eq!(valve.operational_state(), OperationalState::On);
}
#[test]
fn test_valve_creation_without_opening() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let valve = ExpansionValve::new(inlet, outlet, None).unwrap();
assert_eq!(valve.opening(), None);
}
#[test]
fn test_valve_creation_invalid_opening_high() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let result = ExpansionValve::new(inlet, outlet, Some(1.5));
assert!(result.is_err());
}
#[test]
fn test_valve_creation_invalid_opening_low() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let result = ExpansionValve::new(inlet, outlet, Some(-0.1));
assert!(result.is_err());
}
#[test]
fn test_valve_creation_nan_opening() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let result = ExpansionValve::new(inlet, outlet, Some(f64::NAN));
assert!(result.is_err());
}
#[test]
fn test_valve_creation_incompatible_fluids() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R410A"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let result = ExpansionValve::new(inlet, outlet, Some(1.0));
assert!(result.is_err());
}
#[test]
fn test_isenthalpic_expansion() {
let valve = create_test_valve();
assert_relative_eq!(
valve.port_inlet().enthalpy().to_joules_per_kg(),
valve.port_outlet().enthalpy().to_joules_per_kg(),
epsilon = 1e-10
);
}
#[test]
fn test_validate_isenthalpic() {
let valve = create_test_valve();
let result = valve.validate_isenthalpic();
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_pressure_drop() {
let valve = create_test_valve();
let p_in = valve.port_inlet().pressure().to_bar();
let p_out = valve.port_outlet().pressure().to_bar();
assert!(p_out < p_in, "Outlet pressure should be less than inlet");
}
#[test]
fn test_validate_pressure_drop() {
let valve = create_test_valve();
let result = valve.validate_pressure_drop();
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_pressure_ratio() {
let valve = create_test_valve();
let ratio = valve.pressure_ratio();
assert_relative_eq!(ratio, 0.35, epsilon = 1e-10);
}
#[test]
fn test_off_mode() {
let mut valve = create_test_valve();
valve.set_operational_state(OperationalState::Off);
let state = vec![0.05, 0.05];
let mut residuals = vec![0.0; 2];
valve.compute_residuals(&state, &mut residuals).unwrap();
assert_eq!(valve.operational_state(), OperationalState::Off);
assert!(valve.is_effectively_off());
}
#[test]
fn test_bypass_mode() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::Bypass,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let state = vec![0.05, 0.05];
let mut residuals = vec![0.0; 2];
valve.compute_residuals(&state, &mut residuals).unwrap();
assert_eq!(valve.operational_state(), OperationalState::Bypass);
assert_relative_eq!(residuals[0], 0.0, epsilon = 1e-10);
assert_relative_eq!(residuals[1], 0.0, epsilon = 1e-10);
}
#[test]
fn test_opening_threshold_off() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(0.005),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
assert!(valve.is_effectively_off());
}
#[test]
fn test_opening_threshold_on() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(0.5),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
assert!(!valve.is_effectively_off());
}
#[test]
fn test_component_n_equations() {
let valve = create_test_valve();
assert_eq!(valve.n_equations(), 2);
}
#[test]
fn test_component_compute_residuals() {
let valve = create_test_valve();
let state = vec![0.05, 0.05];
let mut residuals = vec![0.0; 2];
let result = valve.compute_residuals(&state, &mut residuals);
assert!(result.is_ok());
assert_relative_eq!(residuals[0], 0.0, epsilon = 1e-10);
assert_relative_eq!(residuals[1], 0.0, epsilon = 1e-10);
}
#[test]
fn test_component_compute_residuals_wrong_size() {
let valve = create_test_valve();
let state = vec![0.05, 0.05];
let mut residuals = vec![0.0; 3];
let result = valve.compute_residuals(&state, &mut residuals);
assert!(result.is_err());
}
#[test]
fn test_component_jacobian_entries() {
let valve = create_test_valve();
let state = vec![0.05, 0.05];
let mut jacobian = JacobianBuilder::new();
let result = valve.jacobian_entries(&state, &mut jacobian);
assert!(result.is_ok());
assert!(!jacobian.is_empty());
}
#[test]
fn test_circuit_id() {
let mut valve = create_disconnected_valve();
valve.set_circuit_id(CircuitId::from_number(5));
assert_eq!(valve.circuit_id().as_number(), 5);
}
#[test]
fn test_get_ports_slice() {
let valve = create_test_valve();
let ports = valve.get_ports_slice();
assert_eq!(ports.len(), 2);
assert_eq!(ports[0].fluid_id().as_str(), "R134a");
assert_eq!(ports[1].fluid_id().as_str(), "R134a");
}
#[test]
fn test_clone() {
let valve = create_test_valve();
let cloned = valve.clone();
assert_eq!(valve.opening(), cloned.opening());
assert_eq!(valve.operational_state(), cloned.operational_state());
}
#[test]
fn test_mass_flow_continuity_residual() {
let valve = create_test_valve();
let state = vec![0.05, 0.06];
let mut residuals = vec![0.0; 2];
valve.compute_residuals(&state, &mut residuals).unwrap();
assert_relative_eq!(residuals[1], 0.01, epsilon = 1e-10);
}
#[test]
fn test_set_opening_valid() {
let mut valve = create_test_valve();
assert!(valve.set_opening(Some(0.5)).is_ok());
assert_eq!(valve.opening(), Some(0.5));
}
#[test]
fn test_set_opening_invalid_high() {
let mut valve = create_test_valve();
assert!(valve.set_opening(Some(1.5)).is_err());
}
#[test]
fn test_set_opening_invalid_low() {
let mut valve = create_test_valve();
assert!(valve.set_opening(Some(-0.1)).is_err());
}
#[test]
fn test_set_opening_nan() {
let mut valve = create_test_valve();
assert!(valve.set_opening(Some(f64::NAN)).is_err());
}
#[test]
fn test_set_opening_none() {
let mut valve = create_test_valve();
assert!(valve.set_opening(None).is_ok());
assert_eq!(valve.opening(), None);
}
#[test]
fn test_on_mode_empty_state_error() {
let valve = create_test_valve();
let state: Vec<f64> = vec![];
let mut residuals = vec![0.0; 2];
let result = valve.compute_residuals(&state, &mut residuals);
assert!(result.is_err());
}
#[test]
fn test_off_mode_empty_state_error() {
let mut valve = create_test_valve();
valve.set_operational_state(OperationalState::Off);
let state: Vec<f64> = vec![];
let mut residuals = vec![0.0; 2];
let result = valve.compute_residuals(&state, &mut residuals);
assert!(result.is_err());
}
#[test]
fn test_pressure_ratio_zero_inlet() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_pascals(0.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_pascals(0.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_pascals(0.0));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
assert_relative_eq!(valve.pressure_ratio(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_validate_isenthalpic_with_tolerance() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250050.0),
);
let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let result = valve.validate_isenthalpic();
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_bypass_mode_jacobian() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::Bypass,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let state = vec![0.05, 0.05];
let mut jacobian = JacobianBuilder::new();
valve.jacobian_entries(&state, &mut jacobian).unwrap();
let entries = jacobian.entries();
assert!(entries.len() >= 4);
let has_nonzero = entries.iter().any(|(_, _, v)| *v != 0.0);
assert!(has_nonzero, "Bypass jacobian should have non-zero entries");
}
#[test]
fn test_state_manageable_state() {
let valve = create_test_valve();
assert_eq!(valve.state(), OperationalState::On);
}
#[test]
fn test_state_manageable_set_state_on_to_off() {
let mut valve = create_test_valve();
let result = valve.set_state(OperationalState::Off);
assert!(result.is_ok());
assert_eq!(valve.state(), OperationalState::Off);
}
#[test]
fn test_state_manageable_set_state_on_to_bypass() {
let mut valve = create_test_valve();
let result = valve.set_state(OperationalState::Bypass);
assert!(result.is_ok());
assert_eq!(valve.state(), OperationalState::Bypass);
}
#[test]
fn test_state_manageable_can_transition_to() {
let valve = create_test_valve();
assert!(valve.can_transition_to(OperationalState::Off));
assert!(valve.can_transition_to(OperationalState::Bypass));
assert!(valve.can_transition_to(OperationalState::On));
}
#[test]
fn test_state_manageable_circuit_id() {
let valve = create_test_valve();
assert_eq!(*valve.circuit_id(), CircuitId::ZERO);
}
#[test]
fn test_state_manageable_set_circuit_id() {
let mut valve = create_test_valve();
valve.set_circuit_id(CircuitId::from_number(2));
assert_eq!(valve.circuit_id().as_number(), 2);
}
#[test]
fn test_state_transition_cycle() {
let mut valve = create_test_valve();
// On -> Off
valve.set_state(OperationalState::Off).unwrap();
assert_eq!(valve.state(), OperationalState::Off);
// Off -> Bypass
valve.set_state(OperationalState::Bypass).unwrap();
assert_eq!(valve.state(), OperationalState::Bypass);
// Bypass -> On
valve.set_state(OperationalState::On).unwrap();
assert_eq!(valve.state(), OperationalState::On);
}
#[test]
fn test_detect_phase_region_subcooled() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(200000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(200000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
outlet_conn.set_enthalpy(Enthalpy::from_joules_per_kg(180000.0));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let h_f = 200000.0;
let h_g = 400000.0;
let region = valve.detect_phase_region(h_f, h_g);
assert_eq!(region, PhaseRegion::Subcooled);
}
#[test]
fn test_detect_phase_region_two_phase() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let h_f = 200000.0;
let h_g = 400000.0;
let region = valve.detect_phase_region(h_f, h_g);
assert_eq!(region, PhaseRegion::TwoPhase);
assert!(region.is_two_phase());
}
#[test]
fn test_detect_phase_region_superheated() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let h_f = 200000.0;
let h_g = 400000.0;
let region = valve.detect_phase_region(h_f, h_g);
assert_eq!(region, PhaseRegion::Superheated);
}
#[test]
fn test_outlet_quality_valid() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let h_f = 200000.0;
let h_g = 400000.0;
let quality = valve.outlet_quality(h_f, h_g).unwrap();
assert_relative_eq!(quality, 0.25, epsilon = 1e-10);
}
#[test]
fn test_outlet_quality_saturated_liquid() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(200000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(200000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let h_f = 200000.0;
let h_g = 400000.0;
let quality = valve.outlet_quality(h_f, h_g).unwrap();
assert_relative_eq!(quality, 0.0, epsilon = 1e-10);
}
#[test]
fn test_outlet_quality_invalid_not_two_phase() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(450000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let h_f = 200000.0;
let h_g = 400000.0;
let result = valve.outlet_quality(h_f, h_g);
assert!(result.is_err());
}
#[test]
fn test_validate_phase_change_detected() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let h_f_out = 180000.0;
let h_g_out = 380000.0;
let result = valve.validate_phase_change(h_f_out, h_g_out).unwrap();
assert!(result);
}
#[test]
fn test_phase_region_enum() {
assert!(PhaseRegion::Subcooled.is_two_phase() == false);
assert!(PhaseRegion::TwoPhase.is_two_phase() == true);
assert!(PhaseRegion::Superheated.is_two_phase() == false);
}
#[test]
fn test_energy_transfers_zero() {
let valve = create_test_valve();
let state = vec![0.05, 0.05];
let (heat, work) = valve.energy_transfers(&state).unwrap();
assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_energy_transfers_off_mode() {
let mut valve = create_test_valve();
valve.set_operational_state(OperationalState::Off);
let state = vec![0.05, 0.05];
let (heat, work) = valve.energy_transfers(&state).unwrap();
assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_energy_transfers_bypass_mode() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250000.0),
);
let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::Bypass,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let state = vec![0.05, 0.05];
let (heat, work) = valve.energy_transfers(&state).unwrap();
assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_port_enthalpies_returns_two_values() {
let valve = create_test_valve();
let state = vec![0.05, 0.05];
let enthalpies = valve.port_enthalpies(&state).unwrap();
assert_eq!(enthalpies.len(), 2);
}
#[test]
fn test_port_enthalpies_isenthalpic() {
let valve = create_test_valve();
let state = vec![0.05, 0.05];
let enthalpies = valve.port_enthalpies(&state).unwrap();
assert_relative_eq!(
enthalpies[0].to_joules_per_kg(),
enthalpies[1].to_joules_per_kg(),
epsilon = 1e-10
);
}
#[test]
fn test_port_enthalpies_inlet_value() {
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(300000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(300000.0),
);
let (inlet_conn, mut outlet_conn) = inlet.connect(outlet).unwrap();
outlet_conn.set_pressure(Pressure::from_bar(3.5));
let valve = ExpansionValve {
calib_indices: entropyk_core::CalibIndices::default(),
port_inlet: inlet_conn,
port_outlet: outlet_conn,
calib: Calib::default(),
operational_state: OperationalState::On,
opening: Some(1.0),
fluid_id: FluidId::new("R134a"),
circuit_id: CircuitId::default(),
_state: PhantomData,
};
let state = vec![0.05, 0.05];
let enthalpies = valve.port_enthalpies(&state).unwrap();
assert_relative_eq!(enthalpies[0].to_joules_per_kg(), 300000.0, epsilon = 1e-10);
assert_relative_eq!(enthalpies[1].to_joules_per_kg(), 300000.0, epsilon = 1e-10);
}
#[test]
fn test_expansion_valve_energy_balance() {
let valve = create_test_valve();
let state = vec![0.05, 0.05];
let energy = valve.energy_transfers(&state);
let mass_flows = valve.port_mass_flows(&state);
let enthalpies = valve.port_enthalpies(&state);
assert!(energy.is_some());
assert!(mass_flows.is_ok());
assert!(enthalpies.is_ok());
let (heat, work) = energy.unwrap();
let m_flows = mass_flows.unwrap();
let h_flows = enthalpies.unwrap();
assert_eq!(m_flows.len(), h_flows.len());
assert_relative_eq!(heat.to_watts(), 0.0, epsilon = 1e-10);
assert_relative_eq!(work.to_watts(), 0.0, epsilon = 1e-10);
}
}