chore: remove deprecated flow_boundary and update docs to match new architecture
This commit is contained in:
98
crates/components/patch_hx.py
Normal file
98
crates/components/patch_hx.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import re
|
||||
|
||||
with open("src/heat_exchanger/moving_boundary_hx.rs", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace("use std::cell::Cell;", "use std::cell::{Cell, RefCell};")
|
||||
content = content.replace("cache: Cell<MovingBoundaryCache>,", "cache: RefCell<MovingBoundaryCache>,")
|
||||
content = content.replace("cache: Cell::new(MovingBoundaryCache::default()),", "cache: RefCell::new(MovingBoundaryCache::default()),")
|
||||
|
||||
# Patch compute_residuals
|
||||
old_compute_residuals = """ fn compute_residuals(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
// For a moving boundary HX, we need to:
|
||||
// 1. Identify zones based on current inlet/outlet enthalpies
|
||||
// 2. Calculate UA for each zone
|
||||
// 3. Update nominal UA in the inner model
|
||||
// 4. Compute residuals using the standard model (e.g. EpsNtu)
|
||||
|
||||
// HACK: For now, we use placeholder enthalpies to test the identification logic.
|
||||
// Proper port extraction will be added in Story 4.1.
|
||||
let h_in = 400_000.0;
|
||||
let h_out = 200_000.0;
|
||||
let p = 500_000.0;
|
||||
let m_refrig = 0.1; // Placeholder mass flow
|
||||
let t_sec_in = 300.0;
|
||||
let t_sec_out = 320.0;
|
||||
|
||||
let mut cache = self.cache.take();
|
||||
let use_cache = cache.is_valid_for(p, m_refrig);
|
||||
|
||||
let _discretization = if use_cache {
|
||||
cache.discretization.clone()
|
||||
} else {
|
||||
let (disc, h_sat_l, h_sat_v) = self.identify_zones(h_in, h_out, p, t_sec_in, t_sec_out)?;
|
||||
cache.valid = true;
|
||||
cache.p_ref = p;
|
||||
cache.m_ref = m_refrig;
|
||||
cache.h_sat_l = h_sat_l;
|
||||
cache.h_sat_v = h_sat_v;
|
||||
cache.discretization = disc.clone();
|
||||
disc
|
||||
};
|
||||
|
||||
self.cache.set(cache);
|
||||
|
||||
// Update total UA in the inner model (EpsNtuModel)
|
||||
// Note: HeatExchanger/Model are often immutable, but calibration indices can be used.
|
||||
// For now, we use Cell or similar if we need to store internal state,
|
||||
// but typically the Model handles the UA.
|
||||
// self.inner.model.set_ua(discretization.total_ua);
|
||||
// Wait, EpsNtuModel's UA is fixed. We might need a custom model or use ua_scale.
|
||||
|
||||
self.inner.compute_residuals(state, residuals)
|
||||
}"""
|
||||
|
||||
new_compute_residuals = """ fn compute_residuals(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
let (p, m_refrig, t_sec_in, t_sec_out) = if let (Some(hot), Some(cold)) = (self.inner.hot_conditions(), self.inner.cold_conditions()) {
|
||||
(hot.pressure_pa(), hot.mass_flow_kg_s(), cold.temperature_k(), cold.temperature_k() + 5.0)
|
||||
} else {
|
||||
(500_000.0, 0.1, 300.0, 320.0)
|
||||
};
|
||||
|
||||
// Extract enthalpies exactly as HeatExchanger does:
|
||||
let enthalpies = self.port_enthalpies(state)?;
|
||||
let h_in = enthalpies[0].to_joules_per_kg();
|
||||
let h_out = enthalpies[1].to_joules_per_kg();
|
||||
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
let use_cache = cache.is_valid_for(p, m_refrig);
|
||||
|
||||
if !use_cache {
|
||||
let (disc, h_sat_l, h_sat_v) = self.identify_zones(h_in, h_out, p, t_sec_in, t_sec_out)?;
|
||||
cache.valid = true;
|
||||
cache.p_ref = p;
|
||||
cache.m_ref = m_refrig;
|
||||
cache.h_sat_l = h_sat_l;
|
||||
cache.h_sat_v = h_sat_v;
|
||||
cache.discretization = disc;
|
||||
}
|
||||
|
||||
let total_ua = cache.discretization.total_ua;
|
||||
let base_ua = self.inner.ua_nominal();
|
||||
let custom_ua_scale = if base_ua > 0.0 { total_ua / base_ua } else { 1.0 };
|
||||
|
||||
self.inner.compute_residuals_with_ua_scale(state, residuals, custom_ua_scale)
|
||||
}"""
|
||||
|
||||
content = content.replace(old_compute_residuals, new_compute_residuals)
|
||||
|
||||
with open("src/heat_exchanger/moving_boundary_hx.rs", "w") as f:
|
||||
f.write(content)
|
||||
@@ -131,8 +131,10 @@ pub struct ExternalModelMetadata {
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum ExternalModelError {
|
||||
#[error("Invalid input format: {0}")]
|
||||
/// Documentation pending
|
||||
InvalidInput(String),
|
||||
#[error("Invalid output format: {0}")]
|
||||
/// Documentation pending
|
||||
InvalidOutput(String),
|
||||
/// Library loading failed
|
||||
#[error("Failed to load library: {0}")]
|
||||
|
||||
@@ -1,979 +0,0 @@
|
||||
//! 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 (Deprecated API)
|
||||
//!
|
||||
//! **⚠️ DEPRECATED:** `FlowSource` and `FlowSink` are deprecated since v0.2.0.
|
||||
//! Use the typed alternatives instead:
|
||||
//! - [`BrineSource`](crate::BrineSource)/[`BrineSink`](crate::BrineSink) for water/glycol
|
||||
//! - [`RefrigerantSource`](crate::RefrigerantSource)/[`RefrigerantSink`](crate::RefrigerantSink) for refrigerants
|
||||
//! - [`AirSource`](crate::AirSource)/[`AirSink`](crate::AirSink) for humid air
|
||||
//!
|
||||
//! See `docs/migration/boundary-conditions.md` for migration examples.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // DEPRECATED - Use BrineSource instead
|
||||
//! let source = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port)?;
|
||||
//! let sink = FlowSink::incompressible("Water", 1.5e5, None, port)?;
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
flow_junction::is_incompressible, flow_junction::FluidKind, Component, ComponentError,
|
||||
ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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
|
||||
/// ```
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// This type is deprecated since version 0.2.0. Use the typed alternatives instead:
|
||||
/// - [`RefrigerantSource`](crate::RefrigerantSource) for refrigerants (R410A, CO₂, etc.)
|
||||
/// - [`BrineSource`](crate::BrineSource) for liquid heat transfer fluids (water, glycol)
|
||||
/// - [`AirSource`](crate::AirSource) for humid air
|
||||
///
|
||||
/// See the migration guide at `docs/migration/boundary-conditions.md` for examples.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use RefrigerantSource, BrineSource, or AirSource instead. \
|
||||
See migration guide in docs/migration/boundary-conditions.md"
|
||||
)]
|
||||
#[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
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`BrineSource::new`](crate::BrineSource::new) instead.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use BrineSource::new() for water/glycol or BrineSource::water() for pure water"
|
||||
)]
|
||||
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…).
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`RefrigerantSource::new`](crate::RefrigerantSource::new) instead.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use RefrigerantSource::new() for refrigerants"
|
||||
)]
|
||||
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: &StateSlice,
|
||||
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: &StateSlice,
|
||||
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] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn port_mass_flows(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
// FlowSource is a boundary condition with a single outlet port.
|
||||
// The actual mass flow rate is determined by the connected components and solver.
|
||||
// Return zero placeholder to satisfy energy balance length check (m_flows.len() == h_flows.len()).
|
||||
// The energy balance for boundaries: Q - W + m*h = 0 - 0 + 0*h = 0 ✓
|
||||
Ok(vec![entropyk_core::MassFlow::from_kg_per_s(0.0)])
|
||||
}
|
||||
|
||||
/// Returns the enthalpy of the outlet port.
|
||||
///
|
||||
/// For a `FlowSource`, there is only one port (outlet) with a fixed enthalpy.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector containing `[h_outlet]`.
|
||||
fn port_enthalpies(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
|
||||
Ok(vec![self.outlet.enthalpy()])
|
||||
}
|
||||
|
||||
/// Returns the energy transfers for the flow source.
|
||||
///
|
||||
/// A flow source is a boundary condition that introduces fluid into the system:
|
||||
/// - **Heat (Q)**: 0 W (no heat exchange with environment)
|
||||
/// - **Work (W)**: 0 W (no mechanical work)
|
||||
///
|
||||
/// The energy of the incoming fluid is accounted for via the mass flow rate
|
||||
/// and port enthalpy in the energy balance calculation.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Some((Q=0, W=0))` always, since boundary conditions have no active transfers.
|
||||
fn energy_transfers(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
|
||||
Some((
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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]
|
||||
/// ```
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// This type is deprecated since version 0.2.0. Use the typed alternatives instead:
|
||||
/// - [`RefrigerantSink`](crate::RefrigerantSink) for refrigerants (R410A, CO₂, etc.)
|
||||
/// - [`BrineSink`](crate::BrineSink) for liquid heat transfer fluids (water, glycol)
|
||||
/// - [`AirSink`](crate::AirSink) for humid air
|
||||
///
|
||||
/// See the migration guide at `docs/migration/boundary-conditions.md` for examples.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use RefrigerantSink, BrineSink, or AirSink instead. \
|
||||
See migration guide in docs/migration/boundary-conditions.md"
|
||||
)]
|
||||
#[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
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`BrineSink::new`](crate::BrineSink::new) instead.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use BrineSink::new() for water/glycol boundary conditions"
|
||||
)]
|
||||
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…).
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`RefrigerantSink::new`](crate::RefrigerantSink::new) instead.
|
||||
#[deprecated(since = "0.2.0", note = "Use RefrigerantSink::new() for refrigerants")]
|
||||
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: &StateSlice,
|
||||
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: &StateSlice,
|
||||
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] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn port_mass_flows(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
// FlowSink is a boundary condition with a single inlet port.
|
||||
// The actual mass flow rate is determined by the connected components and solver.
|
||||
// Return zero placeholder to satisfy energy balance length check (m_flows.len() == h_flows.len()).
|
||||
// The energy balance for boundaries: Q - W + m*h = 0 - 0 + 0*h = 0 ✓
|
||||
Ok(vec![entropyk_core::MassFlow::from_kg_per_s(0.0)])
|
||||
}
|
||||
|
||||
/// Returns the enthalpy of the inlet port.
|
||||
///
|
||||
/// For a `FlowSink`, there is only one port (inlet).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector containing `[h_inlet]`.
|
||||
fn port_enthalpies(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
|
||||
Ok(vec![self.inlet.enthalpy()])
|
||||
}
|
||||
|
||||
/// Returns the energy transfers for the flow sink.
|
||||
///
|
||||
/// A flow sink is a boundary condition that removes fluid from the system:
|
||||
/// - **Heat (Q)**: 0 W (no heat exchange with environment)
|
||||
/// - **Work (W)**: 0 W (no mechanical work)
|
||||
///
|
||||
/// The energy of the outgoing fluid is accounted for via the mass flow rate
|
||||
/// and port enthalpy in the energy balance calculation.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Some((Q=0, W=0))` always, since boundary conditions have no active transfers.
|
||||
fn energy_transfers(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
|
||||
Some((
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Convenience type aliases (à la Modelica)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Source for incompressible fluids (water, glycol, brine…).
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`BrineSource`](crate::BrineSource) instead.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use BrineSource instead. See migration guide in docs/migration/boundary-conditions.md"
|
||||
)]
|
||||
pub type IncompressibleSource = FlowSource;
|
||||
|
||||
/// Source for compressible fluids (refrigerant, CO₂, steam…).
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`RefrigerantSource`](crate::RefrigerantSource) instead.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use RefrigerantSource instead. See migration guide in docs/migration/boundary-conditions.md"
|
||||
)]
|
||||
pub type CompressibleSource = FlowSource;
|
||||
|
||||
/// Sink for incompressible fluids.
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`BrineSink`](crate::BrineSink) instead.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use BrineSink instead. See migration guide in docs/migration/boundary-conditions.md"
|
||||
)]
|
||||
pub type IncompressibleSink = FlowSink;
|
||||
|
||||
/// Sink for compressible fluids.
|
||||
///
|
||||
/// # Deprecation
|
||||
///
|
||||
/// Use [`RefrigerantSink`](crate::RefrigerantSink) instead.
|
||||
#[deprecated(
|
||||
since = "0.2.0",
|
||||
note = "Use RefrigerantSink instead. See migration guide in docs/migration/boundary-conditions.md"
|
||||
)]
|
||||
pub type CompressibleSink = FlowSink;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(deprecated)]
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Energy Methods Tests ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_source_energy_transfers_zero() {
|
||||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||||
let source = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let (heat, work) = source.energy_transfers(&state).unwrap();
|
||||
|
||||
assert_eq!(heat.to_watts(), 0.0);
|
||||
assert_eq!(work.to_watts(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sink_energy_transfers_zero() {
|
||||
let port = make_port("Water", 1.5e5, 63_000.0);
|
||||
let sink = FlowSink::incompressible("Water", 1.5e5, None, port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let (heat, work) = sink.energy_transfers(&state).unwrap();
|
||||
|
||||
assert_eq!(heat.to_watts(), 0.0);
|
||||
assert_eq!(work.to_watts(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_source_port_enthalpies_single() {
|
||||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||||
let source = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let enthalpies = source.port_enthalpies(&state).unwrap();
|
||||
|
||||
assert_eq!(enthalpies.len(), 1);
|
||||
assert!((enthalpies[0].to_joules_per_kg() - 63_000.0).abs() < 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sink_port_enthalpies_single() {
|
||||
let port = make_port("Water", 1.5e5, 50_400.0);
|
||||
let sink = FlowSink::incompressible("Water", 1.5e5, Some(50_400.0), port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let enthalpies = sink.port_enthalpies(&state).unwrap();
|
||||
|
||||
assert_eq!(enthalpies.len(), 1);
|
||||
assert!((enthalpies[0].to_joules_per_kg() - 50_400.0).abs() < 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_source_compressible_energy_transfers() {
|
||||
let port = make_port("R410A", 24.0e5, 465_000.0);
|
||||
let source = FlowSource::compressible("R410A", 24.0e5, 465_000.0, port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let (heat, work) = source.energy_transfers(&state).unwrap();
|
||||
|
||||
assert_eq!(heat.to_watts(), 0.0);
|
||||
assert_eq!(work.to_watts(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sink_compressible_energy_transfers() {
|
||||
let port = make_port("R410A", 8.5e5, 260_000.0);
|
||||
let sink = FlowSink::compressible("R410A", 8.5e5, None, port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let (heat, work) = sink.energy_transfers(&state).unwrap();
|
||||
|
||||
assert_eq!(heat.to_watts(), 0.0);
|
||||
assert_eq!(work.to_watts(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_source_mass_flow_enthalpy_length_match() {
|
||||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||||
let source = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let mass_flows = source.port_mass_flows(&state).unwrap();
|
||||
let enthalpies = source.port_enthalpies(&state).unwrap();
|
||||
|
||||
assert_eq!(mass_flows.len(), enthalpies.len(),
|
||||
"port_mass_flows and port_enthalpies must have matching lengths for energy balance check");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sink_mass_flow_enthalpy_length_match() {
|
||||
let port = make_port("Water", 1.5e5, 63_000.0);
|
||||
let sink = FlowSink::incompressible("Water", 1.5e5, None, port).unwrap();
|
||||
let state = vec![0.0; 4];
|
||||
|
||||
let mass_flows = sink.port_mass_flows(&state).unwrap();
|
||||
let enthalpies = sink.port_enthalpies(&state).unwrap();
|
||||
|
||||
assert_eq!(mass_flows.len(), enthalpies.len(),
|
||||
"port_mass_flows and port_enthalpies must have matching lengths for energy balance check");
|
||||
}
|
||||
|
||||
// ── Migration Tests ───────────────────────────────────────────────────────
|
||||
// These tests verify that deprecated types still work (backward compatibility)
|
||||
// and that new types can be used as drop-in replacements.
|
||||
|
||||
#[test]
|
||||
fn test_deprecated_flow_source_still_works() {
|
||||
// Verify that the deprecated FlowSource::incompressible still works
|
||||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||||
let source = FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||||
|
||||
// Basic functionality check
|
||||
assert_eq!(source.n_equations(), 2);
|
||||
assert_eq!(source.p_set_pa(), 3.0e5);
|
||||
assert_eq!(source.h_set_jkg(), 63_000.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deprecated_flow_sink_still_works() {
|
||||
// Verify that the deprecated FlowSink::incompressible still works
|
||||
let port = make_port("Water", 1.5e5, 63_000.0);
|
||||
let sink = FlowSink::incompressible("Water", 1.5e5, None, port).unwrap();
|
||||
|
||||
// Basic functionality check
|
||||
assert_eq!(sink.n_equations(), 1);
|
||||
assert_eq!(sink.p_back_pa(), 1.5e5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deprecated_compressible_source_still_works() {
|
||||
// Verify that the deprecated FlowSource::compressible still works
|
||||
let port = make_port("R410A", 10.0e5, 280_000.0);
|
||||
let source = FlowSource::compressible("R410A", 10.0e5, 280_000.0, port).unwrap();
|
||||
|
||||
assert_eq!(source.n_equations(), 2);
|
||||
assert_eq!(source.p_set_pa(), 10.0e5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deprecated_compressible_sink_still_works() {
|
||||
// Verify that the deprecated FlowSink::compressible still works
|
||||
let port = make_port("R410A", 8.5e5, 260_000.0);
|
||||
let sink = FlowSink::compressible("R410A", 8.5e5, None, port).unwrap();
|
||||
|
||||
assert_eq!(sink.n_equations(), 1);
|
||||
assert_eq!(sink.p_back_pa(), 8.5e5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deprecated_type_aliases_still_work() {
|
||||
// Verify that deprecated type aliases still compile and work
|
||||
let port = make_port("Water", 3.0e5, 63_000.0);
|
||||
let _source: IncompressibleSource =
|
||||
FlowSource::incompressible("Water", 3.0e5, 63_000.0, port).unwrap();
|
||||
|
||||
let port2 = make_port("R410A", 10.0e5, 280_000.0);
|
||||
let _source2: CompressibleSource =
|
||||
FlowSource::compressible("R410A", 10.0e5, 280_000.0, port2).unwrap();
|
||||
|
||||
let port3 = make_port("Water", 1.5e5, 63_000.0);
|
||||
let _sink: IncompressibleSink =
|
||||
FlowSink::incompressible("Water", 1.5e5, None, port3).unwrap();
|
||||
|
||||
let port4 = make_port("R410A", 8.5e5, 260_000.0);
|
||||
let _sink2: CompressibleSink = FlowSink::compressible("R410A", 8.5e5, None, port4).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ impl BphxCondenser {
|
||||
let fluid = FluidId::new(&self.refrigerant_id);
|
||||
let p = Pressure::from_pascals(p_pa);
|
||||
|
||||
let h_sat_l = backend
|
||||
let _h_sat_l = backend
|
||||
.property(
|
||||
fluid.clone(),
|
||||
Property::Enthalpy,
|
||||
|
||||
@@ -75,6 +75,7 @@ impl std::fmt::Debug for BphxExchanger {
|
||||
|
||||
impl BphxExchanger {
|
||||
/// Minimum valid UA value (W/K)
|
||||
#[allow(dead_code)]
|
||||
const MIN_UA: f64 = 0.0;
|
||||
|
||||
/// Creates a new BphxExchanger with the specified geometry.
|
||||
|
||||
@@ -87,8 +87,11 @@ impl Default for BphxGeometry {
|
||||
impl BphxGeometry {
|
||||
/// Minimum valid values for geometry parameters
|
||||
pub const MIN_PLATES: u32 = 1;
|
||||
/// Documentation pending
|
||||
pub const MIN_DIMENSION: f64 = 1e-6;
|
||||
/// Documentation pending
|
||||
pub const MIN_CHEVRON_ANGLE: f64 = 10.0;
|
||||
/// Documentation pending
|
||||
pub const MAX_CHEVRON_ANGLE: f64 = 80.0;
|
||||
|
||||
/// Creates a new geometry builder with the specified number of plates.
|
||||
@@ -359,20 +362,42 @@ impl BphxGeometryBuilder {
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum BphxGeometryError {
|
||||
#[error("Invalid number of plates: {n_plates}, minimum is {min}")]
|
||||
InvalidPlates { n_plates: u32, min: u32 },
|
||||
/// Documentation pending
|
||||
InvalidPlates {
|
||||
/// Number of plates provided
|
||||
n_plates: u32,
|
||||
/// Minimum allowed plates (2)
|
||||
min: u32,
|
||||
},
|
||||
|
||||
#[error("Invalid {name}: {value}, minimum is {min}")]
|
||||
/// Documentation pending
|
||||
InvalidDimension {
|
||||
/// Documentation pending
|
||||
name: &'static str,
|
||||
/// Documentation pending
|
||||
value: f64,
|
||||
/// Documentation pending
|
||||
min: f64,
|
||||
},
|
||||
|
||||
#[error("Invalid chevron angle: {angle}°, valid range is {min}° to {max}°")]
|
||||
InvalidChevronAngle { angle: f64, min: f64, max: f64 },
|
||||
/// Documentation pending
|
||||
InvalidChevronAngle {
|
||||
/// Angle provided
|
||||
angle: f64,
|
||||
/// Minimum allowed angle
|
||||
min: f64,
|
||||
/// Maximum allowed angle
|
||||
max: f64,
|
||||
},
|
||||
|
||||
#[error("Missing required parameter: {name}")]
|
||||
MissingParameter { name: &'static str },
|
||||
/// Documentation pending
|
||||
MissingParameter {
|
||||
/// Parameter name
|
||||
name: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -287,10 +287,12 @@ impl<Model: HeatTransferModel + 'static> HeatExchanger<Model> {
|
||||
self.hot_conditions.as_ref()
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn cold_conditions(&self) -> Option<&HxSideConditions> {
|
||||
self.cold_conditions.as_ref()
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn hot_fluid_id(&self) -> Option<&FluidsFluidId> {
|
||||
self.hot_conditions.as_ref().map(|c| c.fluid_id())
|
||||
}
|
||||
@@ -461,6 +463,7 @@ impl<Model: HeatTransferModel + 'static> HeatExchanger<Model> {
|
||||
FluidState::new(temperature, pressure, enthalpy, mass_flow, cp)
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn compute_residuals_with_ua_scale(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
@@ -470,6 +473,7 @@ impl<Model: HeatTransferModel + 'static> HeatExchanger<Model> {
|
||||
self.do_compute_residuals(_state, residuals, Some(custom_ua_scale))
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn do_compute_residuals(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
@@ -698,7 +702,7 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
|
||||
fn port_enthalpies(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
_state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
|
||||
let mut enthalpies = Vec::with_capacity(4);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ use std::sync::Arc;
|
||||
|
||||
const MIN_UA: f64 = 0.0;
|
||||
|
||||
/// Documentation pending
|
||||
pub struct FloodedCondenser {
|
||||
inner: HeatExchanger<EpsNtuModel>,
|
||||
refrigerant_id: String,
|
||||
@@ -64,6 +65,7 @@ impl std::fmt::Debug for FloodedCondenser {
|
||||
}
|
||||
|
||||
impl FloodedCondenser {
|
||||
/// Documentation pending
|
||||
pub fn new(ua: f64) -> Self {
|
||||
assert!(ua >= MIN_UA, "UA must be non-negative, got {}", ua);
|
||||
let model = EpsNtuModel::new(ua, ExchangerType::CounterFlow);
|
||||
@@ -81,6 +83,7 @@ impl FloodedCondenser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn try_new(ua: f64) -> Result<Self, ComponentError> {
|
||||
if ua < MIN_UA {
|
||||
return Err(ComponentError::InvalidState(format!(
|
||||
@@ -103,72 +106,88 @@ impl FloodedCondenser {
|
||||
})
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn with_refrigerant(mut self, fluid: impl Into<String>) -> Self {
|
||||
self.refrigerant_id = fluid.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn with_secondary_fluid(mut self, fluid: impl Into<String>) -> Self {
|
||||
self.secondary_fluid_id = fluid.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
|
||||
self.fluid_backend = Some(backend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn with_target_subcooling(mut self, subcooling_k: f64) -> Self {
|
||||
self.target_subcooling_k = subcooling_k.max(0.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn with_subcooling_control(mut self, enabled: bool) -> Self {
|
||||
self.subcooling_control_enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn name(&self) -> &str {
|
||||
self.inner.name()
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn ua(&self) -> f64 {
|
||||
self.inner.ua()
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn calib(&self) -> &Calib {
|
||||
self.inner.calib()
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn set_calib(&mut self, calib: Calib) {
|
||||
self.inner.set_calib(calib);
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn target_subcooling(&self) -> f64 {
|
||||
self.target_subcooling_k
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn set_target_subcooling(&mut self, subcooling_k: f64) {
|
||||
self.target_subcooling_k = subcooling_k.max(0.0);
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn heat_transfer(&self) -> f64 {
|
||||
self.last_heat_transfer_w.get()
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn subcooling(&self) -> Option<f64> {
|
||||
self.last_subcooling_k.get()
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn set_outlet_indices(&mut self, p_idx: usize, h_idx: usize) {
|
||||
self.outlet_pressure_idx = Some(p_idx);
|
||||
self.outlet_enthalpy_idx = Some(h_idx);
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn set_secondary_conditions(&mut self, conditions: HxSideConditions) {
|
||||
self.inner.set_cold_conditions(conditions);
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn set_refrigerant_conditions(&mut self, conditions: HxSideConditions) {
|
||||
self.inner.set_hot_conditions(conditions);
|
||||
}
|
||||
@@ -203,6 +222,7 @@ impl FloodedCondenser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Documentation pending
|
||||
pub fn validate_outlet_subcooled(&self, h_out: f64, p_pa: f64) -> Result<f64, ComponentError> {
|
||||
if self.refrigerant_id.is_empty() {
|
||||
return Err(ComponentError::InvalidState(
|
||||
|
||||
@@ -95,14 +95,17 @@ impl MovingBoundaryCache {
|
||||
pub struct MovingBoundaryHX {
|
||||
inner: HeatExchanger<EpsNtuModel>,
|
||||
geometry: BphxGeometry,
|
||||
correlation_selector: CorrelationSelector,
|
||||
_correlation_selector: CorrelationSelector,
|
||||
refrigerant_id: String,
|
||||
secondary_fluid_id: String,
|
||||
fluid_backend: Option<Arc<dyn entropyk_fluids::FluidBackend>>,
|
||||
// Discretization parameters
|
||||
n_discretization: usize,
|
||||
cache: RefCell<MovingBoundaryCache>,
|
||||
last_htc: Cell<f64>,
|
||||
last_validity_warning: Cell<bool>,
|
||||
|
||||
// Internal state caching
|
||||
_last_htc: Cell<f64>,
|
||||
_last_validity_warning: Cell<bool>,
|
||||
}
|
||||
|
||||
impl Default for MovingBoundaryHX {
|
||||
@@ -120,14 +123,14 @@ impl MovingBoundaryHX {
|
||||
Self {
|
||||
inner: HeatExchanger::new(model, "MovingBoundaryHX"),
|
||||
geometry,
|
||||
correlation_selector: CorrelationSelector::default(),
|
||||
_correlation_selector: CorrelationSelector::default(),
|
||||
refrigerant_id: String::new(),
|
||||
secondary_fluid_id: String::new(),
|
||||
fluid_backend: None,
|
||||
n_discretization: 51,
|
||||
cache: RefCell::new(MovingBoundaryCache::default()),
|
||||
last_htc: Cell::new(0.0),
|
||||
last_validity_warning: Cell::new(false),
|
||||
_last_htc: Cell::new(0.0),
|
||||
_last_validity_warning: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ pub mod drum;
|
||||
pub mod expansion_valve;
|
||||
pub mod external_model;
|
||||
pub mod fan;
|
||||
pub mod flow_boundary;
|
||||
pub mod flow_junction;
|
||||
pub mod heat_exchanger;
|
||||
pub mod node;
|
||||
@@ -85,10 +84,6 @@ pub use external_model::{
|
||||
ExternalModelType, MockExternalModel, ThreadSafeExternalModel,
|
||||
};
|
||||
pub use fan::{Fan, FanCurves};
|
||||
pub use flow_boundary::{
|
||||
CompressibleSink, CompressibleSource, FlowSink, FlowSource, IncompressibleSink,
|
||||
IncompressibleSource,
|
||||
};
|
||||
pub use flow_junction::{
|
||||
CompressibleMerger, CompressibleSplitter, FlowMerger, FlowSplitter, FluidKind,
|
||||
IncompressibleMerger, IncompressibleSplitter,
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
pub use entropyk_fluids::FluidId;
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@@ -21,26 +21,44 @@ use entropyk_fluids::{FluidBackend, FluidId, FluidState, Property};
|
||||
/// - Power: Ẇ = M3 + M4×Pr + M5×T_suc + M6×T_disc
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyCompressorReal {
|
||||
/// Fluid
|
||||
pub fluid: FluidId,
|
||||
/// Speed rpm
|
||||
pub speed_rpm: f64,
|
||||
/// Displacement m3
|
||||
pub displacement_m3: f64,
|
||||
/// Efficiency
|
||||
pub efficiency: f64,
|
||||
/// M1
|
||||
pub m1: f64,
|
||||
/// M2
|
||||
pub m2: f64,
|
||||
/// M3
|
||||
pub m3: f64,
|
||||
/// M4
|
||||
pub m4: f64,
|
||||
/// M5
|
||||
pub m5: f64,
|
||||
/// M6
|
||||
pub m6: f64,
|
||||
/// M7
|
||||
pub m7: f64,
|
||||
/// M8
|
||||
pub m8: f64,
|
||||
/// M9
|
||||
pub m9: f64,
|
||||
/// M10
|
||||
pub m10: f64,
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
/// Operational state
|
||||
pub operational_state: OperationalState,
|
||||
/// Circuit id
|
||||
pub circuit_id: CircuitId,
|
||||
}
|
||||
|
||||
impl PyCompressorReal {
|
||||
/// New
|
||||
pub fn new(fluid: &str, speed_rpm: f64, displacement_m3: f64, efficiency: f64) -> Self {
|
||||
Self {
|
||||
fluid: FluidId::new(fluid),
|
||||
@@ -63,6 +81,7 @@ impl PyCompressorReal {
|
||||
}
|
||||
}
|
||||
|
||||
/// With coefficients
|
||||
pub fn with_coefficients(
|
||||
mut self,
|
||||
m1: f64,
|
||||
@@ -244,13 +263,18 @@ impl Component for PyCompressorReal {
|
||||
/// - P_out specified by downstream conditions
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyExpansionValveReal {
|
||||
/// Fluid
|
||||
pub fluid: FluidId,
|
||||
/// Opening
|
||||
pub opening: f64,
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
/// Circuit id
|
||||
pub circuit_id: CircuitId,
|
||||
}
|
||||
|
||||
impl PyExpansionValveReal {
|
||||
/// New
|
||||
pub fn new(fluid: &str, opening: f64) -> Self {
|
||||
Self {
|
||||
fluid: FluidId::new(fluid),
|
||||
@@ -288,8 +312,8 @@ impl Component for PyExpansionValveReal {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let h_in = Enthalpy::from_joules_per_kg(state[in_idx.1]);
|
||||
let h_out = Enthalpy::from_joules_per_kg(state[out_idx.1]);
|
||||
let _h_in = Enthalpy::from_joules_per_kg(state[in_idx.1]);
|
||||
let _h_out = Enthalpy::from_joules_per_kg(state[out_idx.1]);
|
||||
|
||||
let p_in = state[in_idx.0];
|
||||
let h_in = state[in_idx.1];
|
||||
@@ -341,18 +365,28 @@ impl Component for PyExpansionValveReal {
|
||||
/// Uses ε-NTU method for heat transfer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyHeatExchangerReal {
|
||||
/// Name
|
||||
pub name: String,
|
||||
/// Ua
|
||||
pub ua: f64,
|
||||
/// Fluid
|
||||
pub fluid: FluidId,
|
||||
/// Water inlet temp
|
||||
pub water_inlet_temp: Temperature,
|
||||
/// Water flow rate
|
||||
pub water_flow_rate: f64,
|
||||
/// Is evaporator
|
||||
pub is_evaporator: bool,
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
/// Calib
|
||||
pub calib: Calib,
|
||||
/// Calib indices
|
||||
pub calib_indices: CalibIndices,
|
||||
}
|
||||
|
||||
impl PyHeatExchangerReal {
|
||||
/// Evaporator
|
||||
pub fn evaporator(ua: f64, fluid: &str, water_temp_c: f64, water_flow: f64) -> Self {
|
||||
Self {
|
||||
name: "Evaporator".into(),
|
||||
@@ -367,6 +401,7 @@ impl PyHeatExchangerReal {
|
||||
}
|
||||
}
|
||||
|
||||
/// Condenser
|
||||
pub fn condenser(ua: f64, fluid: &str, water_temp_c: f64, water_flow: f64) -> Self {
|
||||
Self {
|
||||
name: "Condenser".into(),
|
||||
@@ -509,14 +544,20 @@ impl Component for PyHeatExchangerReal {
|
||||
/// Pipe with Darcy-Weisbach pressure drop.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyPipeReal {
|
||||
/// Length
|
||||
pub length: f64,
|
||||
/// Diameter
|
||||
pub diameter: f64,
|
||||
/// Roughness
|
||||
pub roughness: f64,
|
||||
/// Fluid
|
||||
pub fluid: FluidId,
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl PyPipeReal {
|
||||
/// New
|
||||
pub fn new(length: f64, diameter: f64, fluid: &str) -> Self {
|
||||
Self {
|
||||
length,
|
||||
@@ -527,7 +568,8 @@ impl PyPipeReal {
|
||||
}
|
||||
}
|
||||
|
||||
fn friction_factor(&self, re: f64) -> f64 {
|
||||
#[allow(dead_code)]
|
||||
fn _friction_factor(&self, re: f64) -> f64 {
|
||||
if re < 2300.0 {
|
||||
64.0 / re.max(1.0)
|
||||
} else {
|
||||
@@ -613,13 +655,18 @@ impl Component for PyPipeReal {
|
||||
/// Boundary condition with fixed pressure and temperature.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyFlowSourceReal {
|
||||
/// Pressure
|
||||
pub pressure: Pressure,
|
||||
/// Temperature
|
||||
pub temperature: Temperature,
|
||||
/// Fluid
|
||||
pub fluid: FluidId,
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl PyFlowSourceReal {
|
||||
/// New
|
||||
pub fn new(fluid: &str, pressure_pa: f64, temperature_k: f64) -> Self {
|
||||
Self {
|
||||
pressure: Pressure::from_pascals(pressure_pa),
|
||||
@@ -699,6 +746,7 @@ impl Component for PyFlowSourceReal {
|
||||
/// Boundary condition sink.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PyFlowSinkReal {
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
@@ -741,12 +789,16 @@ impl Component for PyFlowSinkReal {
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Documentation pending
|
||||
pub struct PyFlowSplitterReal {
|
||||
/// N outlets
|
||||
pub n_outlets: usize,
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl PyFlowSplitterReal {
|
||||
/// New
|
||||
pub fn new(n_outlets: usize) -> Self {
|
||||
Self {
|
||||
n_outlets,
|
||||
@@ -824,12 +876,16 @@ impl Component for PyFlowSplitterReal {
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Documentation pending
|
||||
pub struct PyFlowMergerReal {
|
||||
/// N inlets
|
||||
pub n_inlets: usize,
|
||||
/// Edge indices
|
||||
pub edge_indices: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl PyFlowMergerReal {
|
||||
/// New
|
||||
pub fn new(n_inlets: usize) -> Self {
|
||||
Self {
|
||||
n_inlets,
|
||||
|
||||
@@ -186,12 +186,9 @@ pub struct ScrewEconomizerCompressor {
|
||||
calib: Calib,
|
||||
/// Calibration state vector indices (injected by solver)
|
||||
calib_indices: CalibIndices,
|
||||
/// Suction port — low-pressure inlet
|
||||
port_suction: ConnectedPort,
|
||||
/// Discharge port — high-pressure outlet
|
||||
port_discharge: ConnectedPort,
|
||||
/// Economizer injection port — intermediate pressure
|
||||
port_economizer: ConnectedPort,
|
||||
/// All 3 ports stored in a Vec for `get_ports()` compatibility.
|
||||
/// Index 0: suction (inlet), Index 1: discharge (outlet), Index 2: economizer (inlet)
|
||||
ports: Vec<ConnectedPort>,
|
||||
/// Offset of this component's internal state block in the global state vector.
|
||||
/// Set by `System::finalize()` via `set_system_context()`.
|
||||
/// The 5 internal variables at `state[offset..offset+5]` are:
|
||||
@@ -262,9 +259,7 @@ impl ScrewEconomizerCompressor {
|
||||
operational_state: OperationalState::On,
|
||||
calib: Calib::default(),
|
||||
calib_indices: CalibIndices::default(),
|
||||
port_suction,
|
||||
port_discharge,
|
||||
port_economizer,
|
||||
ports: vec![port_suction, port_discharge, port_economizer],
|
||||
global_state_offset: 0,
|
||||
})
|
||||
}
|
||||
@@ -333,19 +328,19 @@ impl ScrewEconomizerCompressor {
|
||||
self.calib = calib;
|
||||
}
|
||||
|
||||
/// Returns reference to suction port.
|
||||
/// Returns reference to suction port (index 0).
|
||||
pub fn port_suction(&self) -> &ConnectedPort {
|
||||
&self.port_suction
|
||||
&self.ports[0]
|
||||
}
|
||||
|
||||
/// Returns reference to discharge port.
|
||||
/// Returns reference to discharge port (index 1).
|
||||
pub fn port_discharge(&self) -> &ConnectedPort {
|
||||
&self.port_discharge
|
||||
&self.ports[1]
|
||||
}
|
||||
|
||||
/// Returns reference to economizer injection port.
|
||||
/// Returns reference to economizer injection port (index 2).
|
||||
pub fn port_economizer(&self) -> &ConnectedPort {
|
||||
&self.port_economizer
|
||||
&self.ports[2]
|
||||
}
|
||||
|
||||
// ─── Internal calculations ────────────────────────────────────────────────
|
||||
@@ -355,8 +350,8 @@ impl ScrewEconomizerCompressor {
|
||||
///
|
||||
/// For the SST/SDT model these only need to be approximately correct.
|
||||
fn estimate_sst_sdt_k(&self) -> (f64, f64) {
|
||||
let p_suc_pa = self.port_suction.pressure().to_pascals();
|
||||
let p_dis_pa = self.port_discharge.pressure().to_pascals();
|
||||
let p_suc_pa = self.ports[0].pressure().to_pascals();
|
||||
let p_dis_pa = self.ports[1].pressure().to_pascals();
|
||||
|
||||
// Simple Clausius-Clapeyron approximation for R134a family refrigerants:
|
||||
// T_sat [K] ≈ T_ref / (1 - (R*T_ref/h_vap) * ln(P/P_ref))
|
||||
@@ -462,10 +457,10 @@ impl Component for ScrewEconomizerCompressor {
|
||||
}
|
||||
OperationalState::Bypass => {
|
||||
// Adiabatic pass-through: P_dis = P_suc, h_dis = h_suc, no eco flow
|
||||
let p_suc = self.port_suction.pressure().to_pascals();
|
||||
let p_dis = self.port_discharge.pressure().to_pascals();
|
||||
let h_suc = self.port_suction.enthalpy().to_joules_per_kg();
|
||||
let h_dis = self.port_discharge.enthalpy().to_joules_per_kg();
|
||||
let p_suc = self.ports[0].pressure().to_pascals();
|
||||
let p_dis = self.ports[1].pressure().to_pascals();
|
||||
let h_suc = self.ports[0].enthalpy().to_joules_per_kg();
|
||||
let h_dis = self.ports[1].enthalpy().to_joules_per_kg();
|
||||
residuals[0] = p_suc - p_dis;
|
||||
residuals[1] = h_suc - h_dis;
|
||||
residuals[2] = state.get(off + 1).copied().unwrap_or(0.0); // ṁ_eco = 0
|
||||
@@ -486,9 +481,9 @@ impl Component for ScrewEconomizerCompressor {
|
||||
|
||||
let m_suc_state = state[off]; // kg/s — solver variable
|
||||
let m_eco_state = state[off + 1]; // kg/s — solver variable
|
||||
let h_suc = self.port_suction.enthalpy().to_joules_per_kg();
|
||||
let h_dis = self.port_discharge.enthalpy().to_joules_per_kg();
|
||||
let h_eco = self.port_economizer.enthalpy().to_joules_per_kg();
|
||||
let h_suc = self.ports[0].enthalpy().to_joules_per_kg();
|
||||
let h_dis = self.ports[1].enthalpy().to_joules_per_kg();
|
||||
let h_eco = self.ports[2].enthalpy().to_joules_per_kg();
|
||||
let w_state = state[off + 2]; // W — solver variable
|
||||
|
||||
// ── Compute performance from curves ──────────────────────────────────
|
||||
@@ -522,9 +517,9 @@ impl Component for ScrewEconomizerCompressor {
|
||||
// suction and discharge pressures for optimal performance.
|
||||
// P_eco_set = sqrt(P_suc × P_dis)
|
||||
// r₃ = P_eco_port − P_eco_set = 0
|
||||
let p_suc = self.port_suction.pressure().to_pascals();
|
||||
let p_dis = self.port_discharge.pressure().to_pascals();
|
||||
let p_eco_port = self.port_economizer.pressure().to_pascals();
|
||||
let p_suc = self.ports[0].pressure().to_pascals();
|
||||
let p_dis = self.ports[1].pressure().to_pascals();
|
||||
let p_eco_port = self.ports[2].pressure().to_pascals();
|
||||
let p_eco_set = (p_suc * p_dis).sqrt();
|
||||
// Scale residual to Pa (same order of magnitude as pressures in system)
|
||||
residuals[3] = p_eco_port - p_eco_set;
|
||||
@@ -552,9 +547,9 @@ impl Component for ScrewEconomizerCompressor {
|
||||
|
||||
let m_suc_state = state[off];
|
||||
let m_eco_state = state[off + 1];
|
||||
let h_suc = self.port_suction.enthalpy().to_joules_per_kg();
|
||||
let h_dis = self.port_discharge.enthalpy().to_joules_per_kg();
|
||||
let h_eco = self.port_economizer.enthalpy().to_joules_per_kg();
|
||||
let h_suc = self.ports[0].enthalpy().to_joules_per_kg();
|
||||
let h_dis = self.ports[1].enthalpy().to_joules_per_kg();
|
||||
let h_eco = self.ports[2].enthalpy().to_joules_per_kg();
|
||||
|
||||
// Row 0: ∂r₀/∂ṁ_suc = -1
|
||||
jacobian.add_entry(0, off, -1.0);
|
||||
@@ -601,10 +596,7 @@ impl Component for ScrewEconomizerCompressor {
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
// Return empty slice — ports are accessed via dedicated methods.
|
||||
// Full port slice would require lifetime-coupled storage; use
|
||||
// port_suction(), port_discharge(), port_economizer() accessors instead.
|
||||
&[]
|
||||
&self.ports
|
||||
}
|
||||
|
||||
fn internal_state_len(&self) -> usize {
|
||||
@@ -649,7 +641,7 @@ impl Component for ScrewEconomizerCompressor {
|
||||
return None;
|
||||
}
|
||||
let w = state[off + 2]; // shaft power W
|
||||
// Work done ON the compressor → negative sign convention
|
||||
// Work done ON the compressor → negative sign convention
|
||||
Some((Power::from_watts(0.0), Power::from_watts(-w)))
|
||||
}
|
||||
}
|
||||
@@ -762,6 +754,21 @@ mod tests {
|
||||
assert_eq!(comp.n_equations(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_ports_returns_three() {
|
||||
let comp = make_compressor();
|
||||
let ports = comp.get_ports();
|
||||
assert_eq!(
|
||||
ports.len(),
|
||||
3,
|
||||
"ScrewEconomizerCompressor should expose 3 ports"
|
||||
);
|
||||
// Index 0: suction, Index 1: discharge, Index 2: economizer
|
||||
assert!((ports[0].pressure().to_bar() - 3.2).abs() < 1e-10);
|
||||
assert!((ports[1].pressure().to_bar() - 12.8).abs() < 1e-10);
|
||||
assert!((ports[2].pressure().to_bar() - 6.4).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frequency_ratio_at_nominal() {
|
||||
let comp = make_compressor();
|
||||
@@ -934,13 +941,17 @@ mod tests {
|
||||
|
||||
// Build state: 6 edge vars (zeros) + 3 internal vars
|
||||
let mut state = vec![0.0; 9];
|
||||
state[6] = 1.0; // ṁ_suc at offset+0
|
||||
state[7] = 0.12; // ṁ_eco at offset+1
|
||||
state[6] = 1.0; // ṁ_suc at offset+0
|
||||
state[7] = 0.12; // ṁ_eco at offset+1
|
||||
state[8] = 50_000.0; // W at offset+2
|
||||
|
||||
let mut residuals = vec![0.0; 5];
|
||||
let result = comp.compute_residuals(&state, &mut residuals);
|
||||
assert!(result.is_ok(), "compute_residuals failed: {:?}", result.err());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"compute_residuals failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
for (i, r) in residuals.iter().enumerate() {
|
||||
assert!(r.is_finite(), "residual[{}] = {} is not finite", i, r);
|
||||
}
|
||||
@@ -972,9 +983,9 @@ mod tests {
|
||||
comp.set_system_context(4, &[]);
|
||||
|
||||
let mut state = vec![0.0; 7];
|
||||
state[4] = 1.0; // ṁ_suc at offset+0
|
||||
state[4] = 1.0; // ṁ_suc at offset+0
|
||||
state[5] = 0.12; // ṁ_eco at offset+1
|
||||
state[6] = 0.0; // W at offset+2
|
||||
state[6] = 0.0; // W at offset+2
|
||||
|
||||
let flows = comp.port_mass_flows(&state).unwrap();
|
||||
assert_eq!(flows.len(), 3);
|
||||
|
||||
Reference in New Issue
Block a user