573 lines
22 KiB
Rust
573 lines
22 KiB
Rust
//! Boundary Condition Components — Source & Sink
|
||
//!
|
||
//! This module provides `FlowSource` and `FlowSink` for both incompressible
|
||
//! (water, glycol, brine) and compressible (refrigerant, CO₂) fluid systems.
|
||
//!
|
||
//! ## Design Philosophy (à la Modelica)
|
||
//!
|
||
//! - **`FlowSource`** imposes a fixed thermodynamic state (P, h) on its outlet
|
||
//! edge. It is the entry point of a fluid circuit — it represents an infinite
|
||
//! reservoir at constant conditions (city water supply, district heating header,
|
||
//! refrigerant reservoir, etc.).
|
||
//!
|
||
//! - **`FlowSink`** absorbs flow at a fixed pressure (back-pressure). It is the
|
||
//! termination point of a circuit. Optionally, a fixed outlet enthalpy can also
|
||
//! be imposed (isothermal return, phase separator, etc.).
|
||
//!
|
||
//! ## Equations
|
||
//!
|
||
//! ### FlowSource — 2 equations
|
||
//!
|
||
//! ```text
|
||
//! r_P = P_edge − P_set = 0 (pressure boundary condition)
|
||
//! r_h = h_edge − h_set = 0 (enthalpy / temperature BC)
|
||
//! ```
|
||
//!
|
||
//! ### FlowSink — 1 or 2 equations
|
||
//!
|
||
//! ```text
|
||
//! r_P = P_edge − P_back = 0 (back-pressure boundary condition)
|
||
//! [optional] r_h = h_edge − h_back = 0
|
||
//! ```
|
||
//!
|
||
//! ## Incompressible vs Compressible
|
||
//!
|
||
//! Same physics, different construction-time validation. Use:
|
||
//! - `FlowSource::incompressible` / `FlowSink::incompressible` for water, glycol…
|
||
//! - `FlowSource::compressible` / `FlowSink::compressible` for refrigerant, CO₂…
|
||
//!
|
||
//! ## Example
|
||
//!
|
||
//! ```no_run
|
||
//! use entropyk_components::flow_boundary::{FlowSource, FlowSink};
|
||
//! use entropyk_components::port::{FluidId, Port};
|
||
//! use entropyk_core::{Pressure, Enthalpy};
|
||
//!
|
||
//! let make_port = |p: f64, h: f64| {
|
||
//! let a = Port::new(FluidId::new("Water"), Pressure::from_pascals(p),
|
||
//! Enthalpy::from_joules_per_kg(h));
|
||
//! let b = Port::new(FluidId::new("Water"), Pressure::from_pascals(p),
|
||
//! Enthalpy::from_joules_per_kg(h));
|
||
//! a.connect(b).unwrap().0
|
||
//! };
|
||
//!
|
||
//! // City water supply: 3 bar, 15°C (h ≈ 63 kJ/kg)
|
||
//! let source = FlowSource::incompressible(
|
||
//! "Water", 3.0e5, 63_000.0, make_port(3.0e5, 63_000.0),
|
||
//! ).unwrap();
|
||
//!
|
||
//! // Return header: 1.5 bar back-pressure
|
||
//! let sink = FlowSink::incompressible(
|
||
//! "Water", 1.5e5, None, make_port(1.5e5, 63_000.0),
|
||
//! ).unwrap();
|
||
//! ```
|
||
|
||
use crate::{
|
||
flow_junction::is_incompressible, flow_junction::FluidKind, Component, ComponentError,
|
||
ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
|
||
};
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// FlowSource — Fixed P & h boundary condition
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// A boundary source that imposes fixed pressure and enthalpy on its outlet edge.
|
||
///
|
||
/// Represents an ideal infinite reservoir (city water, refrigerant header, steam
|
||
/// drum, etc.) at constant thermodynamic conditions.
|
||
///
|
||
/// # Equations (always 2)
|
||
///
|
||
/// ```text
|
||
/// r₀ = P_edge − P_set = 0
|
||
/// r₁ = h_edge − h_set = 0
|
||
/// ```
|
||
#[derive(Debug, Clone)]
|
||
pub struct FlowSource {
|
||
/// Fluid kind.
|
||
kind: FluidKind,
|
||
/// Fluid name.
|
||
fluid_id: String,
|
||
/// Set-point pressure [Pa].
|
||
p_set_pa: f64,
|
||
/// Set-point specific enthalpy [J/kg].
|
||
h_set_jkg: f64,
|
||
/// Connected outlet port (links to first edge in the System).
|
||
outlet: ConnectedPort,
|
||
}
|
||
|
||
impl FlowSource {
|
||
// ── Constructors ─────────────────────────────────────────────────────────
|
||
|
||
/// Creates an **incompressible** source (water, glycol, brine…).
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `fluid` — fluid identifier string (e.g. `"Water"`)
|
||
/// * `p_set_pa` — set-point pressure in Pascals
|
||
/// * `h_set_jkg` — set-point specific enthalpy in J/kg
|
||
/// * `outlet` — connected port linked to the first system edge
|
||
pub fn incompressible(
|
||
fluid: impl Into<String>,
|
||
p_set_pa: f64,
|
||
h_set_jkg: f64,
|
||
outlet: ConnectedPort,
|
||
) -> Result<Self, ComponentError> {
|
||
let fluid = fluid.into();
|
||
if !is_incompressible(&fluid) {
|
||
return Err(ComponentError::InvalidState(format!(
|
||
"FlowSource::incompressible: '{}' does not appear incompressible. \
|
||
Use FlowSource::compressible for refrigerants.",
|
||
fluid
|
||
)));
|
||
}
|
||
Self::new_inner(FluidKind::Incompressible, fluid, p_set_pa, h_set_jkg, outlet)
|
||
}
|
||
|
||
/// Creates a **compressible** source (R410A, CO₂, steam…).
|
||
pub fn compressible(
|
||
fluid: impl Into<String>,
|
||
p_set_pa: f64,
|
||
h_set_jkg: f64,
|
||
outlet: ConnectedPort,
|
||
) -> Result<Self, ComponentError> {
|
||
let fluid = fluid.into();
|
||
Self::new_inner(FluidKind::Compressible, fluid, p_set_pa, h_set_jkg, outlet)
|
||
}
|
||
|
||
fn new_inner(
|
||
kind: FluidKind,
|
||
fluid: String,
|
||
p_set_pa: f64,
|
||
h_set_jkg: f64,
|
||
outlet: ConnectedPort,
|
||
) -> Result<Self, ComponentError> {
|
||
if p_set_pa <= 0.0 {
|
||
return Err(ComponentError::InvalidState(
|
||
"FlowSource: set-point pressure must be positive".into(),
|
||
));
|
||
}
|
||
Ok(Self { kind, fluid_id: fluid, p_set_pa, h_set_jkg, outlet })
|
||
}
|
||
|
||
// ── Accessors ────────────────────────────────────────────────────────────
|
||
|
||
/// Fluid kind.
|
||
pub fn fluid_kind(&self) -> FluidKind { self.kind }
|
||
/// Fluid id.
|
||
pub fn fluid_id(&self) -> &str { &self.fluid_id }
|
||
/// Set-point pressure [Pa].
|
||
pub fn p_set_pa(&self) -> f64 { self.p_set_pa }
|
||
/// Set-point enthalpy [J/kg].
|
||
pub fn h_set_jkg(&self) -> f64 { self.h_set_jkg }
|
||
/// Reference to the outlet port.
|
||
pub fn outlet(&self) -> &ConnectedPort { &self.outlet }
|
||
|
||
/// Updates the set-point pressure (useful for parametric studies).
|
||
pub fn set_pressure(&mut self, p_pa: f64) -> Result<(), ComponentError> {
|
||
if p_pa <= 0.0 {
|
||
return Err(ComponentError::InvalidState(
|
||
"FlowSource: pressure must be positive".into(),
|
||
));
|
||
}
|
||
self.p_set_pa = p_pa;
|
||
Ok(())
|
||
}
|
||
|
||
/// Updates the set-point enthalpy.
|
||
pub fn set_enthalpy(&mut self, h_jkg: f64) {
|
||
self.h_set_jkg = h_jkg;
|
||
}
|
||
}
|
||
|
||
impl Component for FlowSource {
|
||
fn n_equations(&self) -> usize { 2 }
|
||
|
||
fn compute_residuals(
|
||
&self,
|
||
_state: &SystemState,
|
||
residuals: &mut ResidualVector,
|
||
) -> Result<(), ComponentError> {
|
||
if residuals.len() < 2 {
|
||
return Err(ComponentError::InvalidResidualDimensions {
|
||
expected: 2,
|
||
actual: residuals.len(),
|
||
});
|
||
}
|
||
// Pressure residual: P_edge − P_set = 0
|
||
residuals[0] = self.outlet.pressure().to_pascals() - self.p_set_pa;
|
||
// Enthalpy residual: h_edge − h_set = 0
|
||
residuals[1] = self.outlet.enthalpy().to_joules_per_kg() - self.h_set_jkg;
|
||
Ok(())
|
||
}
|
||
|
||
fn jacobian_entries(
|
||
&self,
|
||
_state: &SystemState,
|
||
jacobian: &mut JacobianBuilder,
|
||
) -> Result<(), ComponentError> {
|
||
// Both residuals are linear in the edge state: ∂r/∂x = 1
|
||
jacobian.add_entry(0, 0, 1.0);
|
||
jacobian.add_entry(1, 1, 1.0);
|
||
Ok(())
|
||
}
|
||
|
||
fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// FlowSink — Back-pressure boundary condition
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// A boundary sink that imposes a fixed back-pressure (and optionally enthalpy)
|
||
/// on its inlet edge.
|
||
///
|
||
/// Represents an infinite low-pressure reservoir (drain, condenser header,
|
||
/// discharge line, atmospheric vent, etc.).
|
||
///
|
||
/// # Equations (1 or 2)
|
||
///
|
||
/// ```text
|
||
/// r₀ = P_edge − P_back = 0 [always]
|
||
/// r₁ = h_edge − h_back = 0 [only if h_back is set]
|
||
/// ```
|
||
#[derive(Debug, Clone)]
|
||
pub struct FlowSink {
|
||
/// Fluid kind.
|
||
kind: FluidKind,
|
||
/// Fluid name.
|
||
fluid_id: String,
|
||
/// Back-pressure [Pa].
|
||
p_back_pa: f64,
|
||
/// Optional fixed outlet enthalpy [J/kg].
|
||
h_back_jkg: Option<f64>,
|
||
/// Connected inlet port.
|
||
inlet: ConnectedPort,
|
||
}
|
||
|
||
impl FlowSink {
|
||
// ── Constructors ─────────────────────────────────────────────────────────
|
||
|
||
/// Creates an **incompressible** sink (water, glycol…).
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `fluid` — fluid identifier string
|
||
/// * `p_back_pa` — back-pressure in Pascals
|
||
/// * `h_back_jkg` — optional fixed return enthalpy; `None` = free (solver decides)
|
||
/// * `inlet` — connected port
|
||
pub fn incompressible(
|
||
fluid: impl Into<String>,
|
||
p_back_pa: f64,
|
||
h_back_jkg: Option<f64>,
|
||
inlet: ConnectedPort,
|
||
) -> Result<Self, ComponentError> {
|
||
let fluid = fluid.into();
|
||
if !is_incompressible(&fluid) {
|
||
return Err(ComponentError::InvalidState(format!(
|
||
"FlowSink::incompressible: '{}' does not appear incompressible. \
|
||
Use FlowSink::compressible for refrigerants.",
|
||
fluid
|
||
)));
|
||
}
|
||
Self::new_inner(FluidKind::Incompressible, fluid, p_back_pa, h_back_jkg, inlet)
|
||
}
|
||
|
||
/// Creates a **compressible** sink (R410A, CO₂, steam…).
|
||
pub fn compressible(
|
||
fluid: impl Into<String>,
|
||
p_back_pa: f64,
|
||
h_back_jkg: Option<f64>,
|
||
inlet: ConnectedPort,
|
||
) -> Result<Self, ComponentError> {
|
||
let fluid = fluid.into();
|
||
Self::new_inner(FluidKind::Compressible, fluid, p_back_pa, h_back_jkg, inlet)
|
||
}
|
||
|
||
fn new_inner(
|
||
kind: FluidKind,
|
||
fluid: String,
|
||
p_back_pa: f64,
|
||
h_back_jkg: Option<f64>,
|
||
inlet: ConnectedPort,
|
||
) -> Result<Self, ComponentError> {
|
||
if p_back_pa <= 0.0 {
|
||
return Err(ComponentError::InvalidState(
|
||
"FlowSink: back-pressure must be positive".into(),
|
||
));
|
||
}
|
||
Ok(Self { kind, fluid_id: fluid, p_back_pa, h_back_jkg, inlet })
|
||
}
|
||
|
||
// ── Accessors ────────────────────────────────────────────────────────────
|
||
|
||
/// Fluid kind.
|
||
pub fn fluid_kind(&self) -> FluidKind { self.kind }
|
||
/// Fluid id.
|
||
pub fn fluid_id(&self) -> &str { &self.fluid_id }
|
||
/// Back-pressure [Pa].
|
||
pub fn p_back_pa(&self) -> f64 { self.p_back_pa }
|
||
/// Optional back-enthalpy [J/kg].
|
||
pub fn h_back_jkg(&self) -> Option<f64> { self.h_back_jkg }
|
||
/// Reference to the inlet port.
|
||
pub fn inlet(&self) -> &ConnectedPort { &self.inlet }
|
||
|
||
/// Updates the back-pressure.
|
||
pub fn set_pressure(&mut self, p_pa: f64) -> Result<(), ComponentError> {
|
||
if p_pa <= 0.0 {
|
||
return Err(ComponentError::InvalidState(
|
||
"FlowSink: back-pressure must be positive".into(),
|
||
));
|
||
}
|
||
self.p_back_pa = p_pa;
|
||
Ok(())
|
||
}
|
||
|
||
/// Sets a fixed return enthalpy (activates the second equation).
|
||
pub fn set_return_enthalpy(&mut self, h_jkg: f64) {
|
||
self.h_back_jkg = Some(h_jkg);
|
||
}
|
||
|
||
/// Removes the fixed enthalpy constraint (solver determines enthalpy freely).
|
||
pub fn clear_return_enthalpy(&mut self) {
|
||
self.h_back_jkg = None;
|
||
}
|
||
}
|
||
|
||
impl Component for FlowSink {
|
||
fn n_equations(&self) -> usize {
|
||
if self.h_back_jkg.is_some() { 2 } else { 1 }
|
||
}
|
||
|
||
fn compute_residuals(
|
||
&self,
|
||
_state: &SystemState,
|
||
residuals: &mut ResidualVector,
|
||
) -> Result<(), ComponentError> {
|
||
let n = self.n_equations();
|
||
if residuals.len() < n {
|
||
return Err(ComponentError::InvalidResidualDimensions {
|
||
expected: n,
|
||
actual: residuals.len(),
|
||
});
|
||
}
|
||
// Back-pressure residual
|
||
residuals[0] = self.inlet.pressure().to_pascals() - self.p_back_pa;
|
||
// Optional enthalpy residual
|
||
if let Some(h_back) = self.h_back_jkg {
|
||
residuals[1] = self.inlet.enthalpy().to_joules_per_kg() - h_back;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn jacobian_entries(
|
||
&self,
|
||
_state: &SystemState,
|
||
jacobian: &mut JacobianBuilder,
|
||
) -> Result<(), ComponentError> {
|
||
let n = self.n_equations();
|
||
for i in 0..n {
|
||
jacobian.add_entry(i, i, 1.0);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Convenience type aliases (à la Modelica)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// Source for incompressible fluids (water, glycol, brine…).
|
||
pub type IncompressibleSource = FlowSource;
|
||
/// Source for compressible fluids (refrigerant, CO₂, steam…).
|
||
pub type CompressibleSource = FlowSource;
|
||
/// Sink for incompressible fluids.
|
||
pub type IncompressibleSink = FlowSink;
|
||
/// Sink for compressible fluids.
|
||
pub type CompressibleSink = FlowSink;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Tests
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::port::{FluidId, Port};
|
||
use entropyk_core::{Enthalpy, Pressure};
|
||
|
||
fn make_port(fluid: &str, p_pa: f64, h_jkg: f64) -> ConnectedPort {
|
||
let a = Port::new(FluidId::new(fluid), Pressure::from_pascals(p_pa),
|
||
Enthalpy::from_joules_per_kg(h_jkg));
|
||
let b = Port::new(FluidId::new(fluid), Pressure::from_pascals(p_pa),
|
||
Enthalpy::from_joules_per_kg(h_jkg));
|
||
a.connect(b).unwrap().0
|
||
}
|
||
|
||
// ── FlowSource ────────────────────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn test_source_incompressible_water() {
|
||
// City water supply: 3 bar, 15°C (h ≈ 63 kJ/kg)
|
||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||
let s = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||
assert_eq!(s.n_equations(), 2);
|
||
assert_eq!(s.fluid_kind(), FluidKind::Incompressible);
|
||
assert_eq!(s.p_set_pa(), 3.0e5);
|
||
assert_eq!(s.h_set_jkg(), 63_000.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_source_compressible_refrigerant() {
|
||
// R410A high-side: 24 bar, h = 465 kJ/kg (superheated vapour)
|
||
let port = make_port("R410A", 24.0e5, 465_000.0);
|
||
let s = FlowSource::compressible("R410A", 24.0e5, 465_000.0, port).unwrap();
|
||
assert_eq!(s.n_equations(), 2);
|
||
assert_eq!(s.fluid_kind(), FluidKind::Compressible);
|
||
}
|
||
|
||
#[test]
|
||
fn test_source_rejects_refrigerant_as_incompressible() {
|
||
let port = make_port("R410A", 24.0e5, 465_000.0);
|
||
let result = FlowSource::incompressible("R410A", 24.0e5, 465_000.0, port);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_source_rejects_zero_pressure() {
|
||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||
let result = FlowSource::incompressible("Water", 0.0, 63_000.0, port);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_source_residuals_zero_at_set_point() {
|
||
let p = 3.0e5_f64;
|
||
let h = 63_000.0_f64;
|
||
let port = make_port("Water", p, h);
|
||
let s = FlowSource::incompressible("Water", p, h, port).unwrap();
|
||
let state = vec![0.0; 4];
|
||
let mut res = vec![0.0; 2];
|
||
s.compute_residuals(&state, &mut res).unwrap();
|
||
assert!(res[0].abs() < 1.0, "P residual = {}", res[0]);
|
||
assert!(res[1].abs() < 1.0, "h residual = {}", res[1]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_source_residuals_nonzero_on_mismatch() {
|
||
// Port at 2 bar but set-point 3 bar → residual = -1e5
|
||
let port = make_port("Water", 2.0e5, 63_000.0);
|
||
let s = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||
let state = vec![0.0; 4];
|
||
let mut res = vec![0.0; 2];
|
||
s.compute_residuals(&state, &mut res).unwrap();
|
||
assert!((res[0] - (-1.0e5)).abs() < 1.0, "expected -1e5, got {}", res[0]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_source_set_pressure() {
|
||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||
let mut s = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||
s.set_pressure(5.0e5).unwrap();
|
||
assert_eq!(s.p_set_pa(), 5.0e5);
|
||
assert!(s.set_pressure(0.0).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_source_as_trait_object() {
|
||
let port = make_port("R410A", 8.5e5, 260_000.0);
|
||
let src: Box<dyn Component> =
|
||
Box::new(FlowSource::compressible("R410A", 8.5e5, 260_000.0, port).unwrap());
|
||
assert_eq!(src.n_equations(), 2);
|
||
}
|
||
|
||
// ── FlowSink ──────────────────────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn test_sink_incompressible_back_pressure_only() {
|
||
// Return header: 1.5 bar, free enthalpy
|
||
let port = make_port("Water", 1.5e5, 63_000.0);
|
||
let s = FlowSink::incompressible("Water", 1.5e5, None, port).unwrap();
|
||
assert_eq!(s.n_equations(), 1);
|
||
assert_eq!(s.fluid_kind(), FluidKind::Incompressible);
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_with_fixed_return_enthalpy() {
|
||
// Fixed return temperature: 12°C, h ≈ 50.4 kJ/kg
|
||
let port = make_port("Water", 1.5e5, 50_400.0);
|
||
let s = FlowSink::incompressible("Water", 1.5e5, Some(50_400.0), port).unwrap();
|
||
assert_eq!(s.n_equations(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_compressible_refrigerant() {
|
||
// R410A low-side: 8.5 bar
|
||
let port = make_port("R410A", 8.5e5, 260_000.0);
|
||
let s = FlowSink::compressible("R410A", 8.5e5, None, port).unwrap();
|
||
assert_eq!(s.n_equations(), 1);
|
||
assert_eq!(s.fluid_kind(), FluidKind::Compressible);
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_rejects_refrigerant_as_incompressible() {
|
||
let port = make_port("R410A", 8.5e5, 260_000.0);
|
||
let result = FlowSink::incompressible("R410A", 8.5e5, None, port);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_rejects_zero_back_pressure() {
|
||
let port = make_port("Water", 1.5e5, 63_000.0);
|
||
let result = FlowSink::incompressible("Water", 0.0, None, port);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_residual_zero_at_back_pressure() {
|
||
let p = 1.5e5_f64;
|
||
let port = make_port("Water", p, 63_000.0);
|
||
let s = FlowSink::incompressible("Water", p, None, port).unwrap();
|
||
let state = vec![0.0; 4];
|
||
let mut res = vec![0.0; 1];
|
||
s.compute_residuals(&state, &mut res).unwrap();
|
||
assert!(res[0].abs() < 1.0, "P residual = {}", res[0]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_residual_with_enthalpy() {
|
||
let p = 1.5e5_f64;
|
||
let h = 50_400.0_f64;
|
||
let port = make_port("Water", p, h);
|
||
let s = FlowSink::incompressible("Water", p, Some(h), port).unwrap();
|
||
let state = vec![0.0; 4];
|
||
let mut res = vec![0.0; 2];
|
||
s.compute_residuals(&state, &mut res).unwrap();
|
||
assert!(res[0].abs() < 1.0, "P residual = {}", res[0]);
|
||
assert!(res[1].abs() < 1.0, "h residual = {}", res[1]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_dynamic_enthalpy_toggle() {
|
||
let port = make_port("Water", 1.5e5, 63_000.0);
|
||
let mut s = FlowSink::incompressible("Water", 1.5e5, None, port).unwrap();
|
||
assert_eq!(s.n_equations(), 1);
|
||
|
||
s.set_return_enthalpy(50_400.0);
|
||
assert_eq!(s.n_equations(), 2);
|
||
|
||
s.clear_return_enthalpy();
|
||
assert_eq!(s.n_equations(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_sink_as_trait_object() {
|
||
let port = make_port("R410A", 8.5e5, 260_000.0);
|
||
let sink: Box<dyn Component> =
|
||
Box::new(FlowSink::compressible("R410A", 8.5e5, Some(260_000.0), port).unwrap());
|
||
assert_eq!(sink.n_equations(), 2);
|
||
}
|
||
}
|