1095 lines
38 KiB
Rust
1095 lines
38 KiB
Rust
//! # Entropyk Components
|
|
//!
|
|
//! This crate provides the core component trait definitions for the Entropyk
|
|
//! thermodynamic simulation library. All thermodynamic components (compressors,
|
|
//! condensers, evaporators, etc.) implement the [`Component`] trait.
|
|
//!
|
|
//! ## Core Concept
|
|
//!
|
|
//! The [`Component`] trait defines the interface between thermodynamic components
|
|
//! and the solver engine. Each component is responsible for:
|
|
//!
|
|
//! - Computing residuals based on current system state
|
|
//! - Providing Jacobian entries for numerical solving
|
|
//! - Reporting the number of equations it contributes
|
|
//!
|
|
//! ## Object Safety
|
|
//!
|
|
//! The [`Component`] trait is designed to be **object-safe**, meaning it supports
|
|
//! dynamic dispatch via `Box<dyn Component>` or `&dyn Component`. This is essential
|
|
//! for the solver to work with collections of heterogeneous components.
|
|
//!
|
|
//! ## Example
|
|
//!
|
|
//! ```rust
|
|
//! use entropyk_components::{Component, ComponentError, StateSlice, ResidualVector, JacobianBuilder, ConnectedPort};
|
|
//!
|
|
//! struct MockComponent {
|
|
//! n_equations: usize,
|
|
//! }
|
|
//!
|
|
//! impl Component for MockComponent {
|
|
//! fn compute_residuals(&self, state: &StateSlice, residuals: &mut ResidualVector) -> Result<(), ComponentError> {
|
|
//! // Component-specific residual computation
|
|
//! Ok(())
|
|
//! }
|
|
//!
|
|
//! fn jacobian_entries(&self, state: &StateSlice, jacobian: &mut JacobianBuilder) -> Result<(), ComponentError> {
|
|
//! // Component-specific Jacobian contributions
|
|
//! Ok(())
|
|
//! }
|
|
//!
|
|
//! fn n_equations(&self) -> usize {
|
|
//! self.n_equations
|
|
//! }
|
|
//!
|
|
//! fn get_ports(&self) -> &[ConnectedPort] {
|
|
//! &[]
|
|
//! }
|
|
//! }
|
|
//!
|
|
//! // Trait object usage
|
|
//! let component: Box<dyn Component> = Box::new(MockComponent { n_equations: 3 });
|
|
//! ```
|
|
|
|
#![warn(missing_docs)]
|
|
#![warn(rust_2018_idioms)]
|
|
|
|
pub mod air_boundary;
|
|
pub mod brine_boundary;
|
|
pub mod bypass_valve;
|
|
pub mod compressor;
|
|
pub mod curves;
|
|
pub mod drum;
|
|
pub mod expansion_valve;
|
|
pub mod external_model;
|
|
pub mod fan;
|
|
pub mod flow_junction;
|
|
pub mod free_cooling_exchanger;
|
|
pub mod heat_exchanger;
|
|
pub mod node;
|
|
pub mod params;
|
|
pub mod pipe;
|
|
pub mod polynomials;
|
|
pub mod port;
|
|
pub mod pump;
|
|
pub mod registry;
|
|
pub mod python_components;
|
|
pub mod refrigerant_boundary;
|
|
pub mod screw_economizer_compressor;
|
|
pub mod state_machine;
|
|
|
|
pub use air_boundary::{AirSink, AirSource};
|
|
pub use brine_boundary::{BrineSink, BrineSource};
|
|
pub use bypass_valve::{BypassValve, BypassValveConfig, ValveCharacteristics};
|
|
pub use compressor::{Ahri540Coefficients, Compressor, CompressorModel, SstSdtCoefficients};
|
|
pub use curves::{
|
|
BoundedCurve, CurveEngine, CurveEval, CurveResult, CurveSet, CurveWarning,
|
|
};
|
|
pub use drum::Drum;
|
|
pub use expansion_valve::{ExpansionValve, PhaseRegion};
|
|
pub use external_model::{
|
|
ExternalModel, ExternalModelConfig, ExternalModelError, ExternalModelMetadata,
|
|
ExternalModelType, MockExternalModel, ThreadSafeExternalModel,
|
|
};
|
|
pub use fan::{Fan, FanCurves};
|
|
pub use free_cooling_exchanger::{
|
|
FreeCoolingConfig, FreeCoolingControlMode, FreeCoolingExchanger, FreeCoolingMode,
|
|
};
|
|
pub use flow_junction::{
|
|
CompressibleMerger, CompressibleSplitter, FlowMerger, FlowSplitter, FluidKind,
|
|
IncompressibleMerger, IncompressibleSplitter,
|
|
};
|
|
pub use heat_exchanger::model::FluidState;
|
|
pub use heat_exchanger::{
|
|
Condenser, CondenserCoil, Economizer, EpsNtuModel, Evaporator, EvaporatorCoil, ExchangerType,
|
|
FloodedCondenser, FloodedEvaporator, FlowConfiguration, HeatExchanger, HeatExchangerBuilder,
|
|
HeatTransferModel, HxSideConditions, LmtdModel, MchxCondenserCoil,
|
|
};
|
|
pub use node::{Node, NodeMeasurements, NodePhase};
|
|
pub use params::ComponentParams;
|
|
pub use registry::{RegistryError, create_component};
|
|
pub use pipe::{friction_factor, roughness, Pipe, PipeGeometry};
|
|
pub use polynomials::{AffinityLaws, PerformanceCurves, Polynomial1D, Polynomial2D};
|
|
pub use port::{
|
|
validate_port_continuity, Connected, ConnectedPort, ConnectionError, Disconnected, FluidId,
|
|
Port,
|
|
};
|
|
pub use pump::{Pump, PumpCurves};
|
|
pub use python_components::{
|
|
PyCompressorReal, PyExpansionValveReal, PyFlowMergerReal, PyFlowSinkReal, PyFlowSourceReal,
|
|
PyFlowSplitterReal, PyHeatExchangerReal, PyPipeReal,
|
|
PyRefrigerantSourceReal, PyRefrigerantSinkReal, PyBrineSourceReal, PyBrineSinkReal,
|
|
PyAirSourceReal, PyAirSinkReal,
|
|
};
|
|
pub use refrigerant_boundary::{RefrigerantSink, RefrigerantSource};
|
|
pub use screw_economizer_compressor::{ScrewEconomizerCompressor, ScrewPerformanceCurves};
|
|
pub use state_machine::{
|
|
CircuitId, OperationalState, StateHistory, StateManageable, StateTransitionError,
|
|
StateTransitionRecord,
|
|
};
|
|
|
|
use entropyk_core::{MassFlow, Power};
|
|
use thiserror::Error;
|
|
|
|
/// Errors that can occur during component operations.
|
|
///
|
|
/// This enum represents all possible error conditions that components
|
|
/// may encounter during computation, providing detailed context for debugging.
|
|
#[derive(Error, Debug, Clone, PartialEq)]
|
|
pub enum ComponentError {
|
|
/// Invalid state vector dimensions.
|
|
///
|
|
/// The state vector provided does not have the expected dimensions
|
|
/// for this component's computation.
|
|
#[error("Invalid state vector dimensions: expected at least {expected}, got {actual}")]
|
|
InvalidStateDimensions {
|
|
/// Expected minimum dimension
|
|
expected: usize,
|
|
/// Actual dimension received
|
|
actual: usize,
|
|
},
|
|
|
|
/// Invalid residual vector dimensions.
|
|
///
|
|
/// The residual vector does not match the expected size for this component.
|
|
#[error("Invalid residual vector dimensions: expected {expected}, got {actual}")]
|
|
InvalidResidualDimensions {
|
|
/// Expected dimension (from n_equations)
|
|
expected: usize,
|
|
/// Actual dimension received
|
|
actual: usize,
|
|
},
|
|
|
|
/// Numerical computation error.
|
|
///
|
|
/// Occurs when a numerical operation fails (e.g., division by zero,
|
|
/// logarithm of non-positive number, NaN or infinite results).
|
|
#[error("Numerical error in component computation: {0}")]
|
|
NumericalError(String),
|
|
|
|
/// Invalid component state.
|
|
///
|
|
/// The component is in an invalid state for computation (e.g.,
|
|
/// disconnected ports, uninitialized parameters).
|
|
#[error("Invalid component state: {0}")]
|
|
InvalidState(String),
|
|
|
|
/// Invalid state transition.
|
|
///
|
|
/// The requested state transition is not allowed for this component.
|
|
#[error("Invalid state transition from {from:?} to {to:?}: {reason}")]
|
|
InvalidStateTransition {
|
|
/// State before attempted transition
|
|
from: OperationalState,
|
|
/// Attempted target state
|
|
to: OperationalState,
|
|
/// Reason for rejection
|
|
reason: String,
|
|
},
|
|
|
|
/// Calculation dynamically failed.
|
|
///
|
|
/// Occurs when an underlying model or backend fails to evaluate
|
|
/// properties at the requested state.
|
|
#[error("Calculation failed: {0}")]
|
|
CalculationFailed(String),
|
|
}
|
|
|
|
/// Represents the state of the entire thermodynamic system.
|
|
///
|
|
/// Re-exported from `entropyk_core` for convenience. Each edge in the system
|
|
/// graph has two state variables: pressure and enthalpy.
|
|
///
|
|
/// See [`entropyk_core::SystemState`] for full documentation.
|
|
pub use entropyk_core::SystemState;
|
|
|
|
/// Type alias for state slice used in component methods.
|
|
///
|
|
/// This allows both `&Vec<f64>` and `&SystemState` to be passed via deref coercion.
|
|
pub type StateSlice = [f64];
|
|
|
|
/// Vector of residual values for equation solving.
|
|
///
|
|
/// Residuals represent the difference between expected and actual values
|
|
/// in the system of equations. The solver aims to drive all residuals to zero.
|
|
pub type ResidualVector = Vec<f64>;
|
|
|
|
/// Builder for constructing Jacobian matrix entries.
|
|
///
|
|
/// The Jacobian matrix contains partial derivatives of residuals with respect
|
|
/// to state variables. This builder accumulates entries in coordinate format
|
|
/// (row, column, value) before assembly into a sparse matrix.
|
|
#[derive(Debug, Default)]
|
|
pub struct JacobianBuilder {
|
|
entries: Vec<(usize, usize, f64)>,
|
|
}
|
|
|
|
impl JacobianBuilder {
|
|
/// Creates a new empty Jacobian builder.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::JacobianBuilder;
|
|
///
|
|
/// let builder = JacobianBuilder::new();
|
|
/// ```
|
|
pub fn new() -> Self {
|
|
Self {
|
|
entries: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Adds a single entry to the Jacobian.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `row` - Row index in the Jacobian matrix (equation index)
|
|
/// * `col` - Column index in the Jacobian matrix (state variable index)
|
|
/// * `value` - Partial derivative value ∂residual/∂state
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::JacobianBuilder;
|
|
///
|
|
/// let mut builder = JacobianBuilder::new();
|
|
/// builder.add_entry(0, 1, 2.5);
|
|
/// ```
|
|
pub fn add_entry(&mut self, row: usize, col: usize, value: f64) {
|
|
self.entries.push((row, col, value));
|
|
}
|
|
|
|
/// Adds multiple entries at once.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `entries` - Iterator of (row, col, value) tuples
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::JacobianBuilder;
|
|
///
|
|
/// let mut builder = JacobianBuilder::new();
|
|
/// builder.add_entries(vec![(0, 0, 1.0), (0, 1, 2.0)]);
|
|
/// ```
|
|
pub fn add_entries(&mut self, entries: impl IntoIterator<Item = (usize, usize, f64)>) {
|
|
self.entries.extend(entries);
|
|
}
|
|
|
|
/// Returns all accumulated entries.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::JacobianBuilder;
|
|
///
|
|
/// let mut builder = JacobianBuilder::new();
|
|
/// builder.add_entry(0, 0, 1.0);
|
|
/// let entries = builder.entries();
|
|
/// assert_eq!(entries.len(), 1);
|
|
/// ```
|
|
pub fn entries(&self) -> &[(usize, usize, f64)] {
|
|
&self.entries
|
|
}
|
|
|
|
/// Clears all entries from the builder.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::JacobianBuilder;
|
|
///
|
|
/// let mut builder = JacobianBuilder::new();
|
|
/// builder.add_entry(0, 0, 1.0);
|
|
/// builder.clear();
|
|
/// assert!(builder.entries().is_empty());
|
|
/// ```
|
|
pub fn clear(&mut self) {
|
|
self.entries.clear();
|
|
}
|
|
|
|
/// Returns the number of accumulated entries.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::JacobianBuilder;
|
|
///
|
|
/// let mut builder = JacobianBuilder::new();
|
|
/// builder.add_entry(0, 0, 1.0);
|
|
/// assert_eq!(builder.len(), 1);
|
|
/// ```
|
|
pub fn len(&self) -> usize {
|
|
self.entries.len()
|
|
}
|
|
|
|
/// Returns true if no entries have been added.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::JacobianBuilder;
|
|
///
|
|
/// let builder = JacobianBuilder::new();
|
|
/// assert!(builder.is_empty());
|
|
/// ```
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
}
|
|
|
|
/// Core trait for all thermodynamic components.
|
|
///
|
|
/// The `Component` trait defines the interface between thermodynamic components
|
|
/// (compressors, heat exchangers, valves, etc.) and the solver engine. All
|
|
/// components in the system must implement this trait.
|
|
///
|
|
/// # Object Safety
|
|
///
|
|
/// This trait is **object-safe**, meaning it can be used with dynamic dispatch:
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::{Component, ComponentError, StateSlice, ResidualVector, JacobianBuilder, ConnectedPort};
|
|
///
|
|
/// struct SimpleComponent;
|
|
/// impl Component for SimpleComponent {
|
|
/// fn compute_residuals(&self, _state: &StateSlice, _residuals: &mut ResidualVector) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
/// fn jacobian_entries(&self, _state: &StateSlice, _jacobian: &mut JacobianBuilder) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
/// fn n_equations(&self) -> usize { 1 }
|
|
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
|
/// }
|
|
///
|
|
/// let component: Box<dyn Component> = Box::new(SimpleComponent);
|
|
/// ```
|
|
///
|
|
/// # Required Methods
|
|
///
|
|
/// Implementors must provide three methods:
|
|
///
|
|
/// - [`compute_residuals`](Self::compute_residuals): Compute residual values
|
|
/// representing the component's contribution to the system of equations.
|
|
///
|
|
/// - [`jacobian_entries`](Self::jacobian_entries): Provide partial derivatives
|
|
/// of residuals with respect to state variables.
|
|
///
|
|
/// - [`n_equations`](Self::n_equations): Report how many equations this
|
|
/// component contributes to the overall system.
|
|
///
|
|
/// # Error Handling
|
|
///
|
|
/// Both computation methods return [`Result`] to allow components to report
|
|
/// errors such as invalid state dimensions, numerical issues, or invalid
|
|
/// component configuration.
|
|
///
|
|
/// # Type Parameters
|
|
///
|
|
/// Currently, this trait uses simple slice types for state and residuals.
|
|
/// Future iterations may introduce generic type parameters for enhanced
|
|
/// type safety and performance.
|
|
pub trait Component {
|
|
/// Computes residual values for this component.
|
|
///
|
|
/// Residuals represent the difference between the component's expected
|
|
/// behavior and its actual state. The solver attempts to drive all
|
|
/// residuals to zero.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `state` - Current state vector of the entire system as a slice
|
|
/// * `residuals` - Mutable slice to store computed residual values
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns `Ok(())` on success, or a [`ComponentError`] if computation fails.
|
|
///
|
|
/// # Implementation Notes
|
|
///
|
|
/// The `residuals` slice has length equal to [`n_equations`](Self::n_equations).
|
|
/// Each index corresponds to a specific equation for this component.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::{Component, ComponentError, StateSlice, ResidualVector, JacobianBuilder, ConnectedPort};
|
|
///
|
|
/// struct MassBalanceComponent;
|
|
/// impl Component for MassBalanceComponent {
|
|
/// fn compute_residuals(&self, state: &StateSlice, residuals: &mut ResidualVector) -> Result<(), ComponentError> {
|
|
/// // Validate dimensions
|
|
/// if state.len() < 2 {
|
|
/// return Err(ComponentError::InvalidStateDimensions { expected: 2, actual: state.len() });
|
|
/// }
|
|
/// // Mass balance: inlet - outlet = 0
|
|
/// residuals[0] = state[0] - state[1];
|
|
/// Ok(())
|
|
/// }
|
|
///
|
|
/// fn jacobian_entries(&self, _state: &StateSlice, _jacobian: &mut JacobianBuilder) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
/// fn n_equations(&self) -> usize { 1 }
|
|
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
|
/// }
|
|
/// ```
|
|
fn compute_residuals(
|
|
&self,
|
|
state: &StateSlice,
|
|
residuals: &mut ResidualVector,
|
|
) -> Result<(), ComponentError>;
|
|
|
|
/// Provides Jacobian matrix entries for this component.
|
|
///
|
|
/// The Jacobian contains partial derivatives of residuals with respect
|
|
/// to state variables: J[i,j] = ∂residual[i]/∂state[j]
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `state` - Current state vector of the entire system as a slice
|
|
/// * `jacobian` - Builder for accumulating Jacobian entries
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns `Ok(())` on success, or a [`ComponentError`] if computation fails.
|
|
///
|
|
/// # Implementation Notes
|
|
///
|
|
/// Use [`JacobianBuilder::add_entry`] to add individual partial derivatives.
|
|
/// Only add non-zero entries to optimize sparse matrix storage.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::{Component, ComponentError, StateSlice, ResidualVector, JacobianBuilder, ConnectedPort};
|
|
///
|
|
/// struct LinearComponent;
|
|
/// impl Component for LinearComponent {
|
|
/// fn compute_residuals(&self, _state: &StateSlice, _residuals: &mut ResidualVector) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
///
|
|
/// fn jacobian_entries(&self, _state: &StateSlice, jacobian: &mut JacobianBuilder) -> Result<(), ComponentError> {
|
|
/// // ∂r₀/∂s₀ = 2.0
|
|
/// jacobian.add_entry(0, 0, 2.0);
|
|
/// // ∂r₀/∂s₁ = -1.0
|
|
/// jacobian.add_entry(0, 1, -1.0);
|
|
/// Ok(())
|
|
/// }
|
|
///
|
|
/// fn n_equations(&self) -> usize { 1 }
|
|
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
|
/// }
|
|
/// ```
|
|
fn jacobian_entries(
|
|
&self,
|
|
state: &StateSlice,
|
|
jacobian: &mut JacobianBuilder,
|
|
) -> Result<(), ComponentError>;
|
|
|
|
/// Returns the number of equations contributed by this component.
|
|
///
|
|
/// This determines the size of the residual vector passed to
|
|
/// [`compute_residuals`](Self::compute_residuals).
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::{Component, ComponentError, StateSlice, ResidualVector, JacobianBuilder, ConnectedPort};
|
|
///
|
|
/// struct ThreeEquationComponent;
|
|
/// impl Component for ThreeEquationComponent {
|
|
/// fn compute_residuals(&self, _state: &StateSlice, _residuals: &mut ResidualVector) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
/// fn jacobian_entries(&self, _state: &StateSlice, _jacobian: &mut JacobianBuilder) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
/// fn n_equations(&self) -> usize { 3 }
|
|
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
|
/// }
|
|
///
|
|
/// let component = ThreeEquationComponent;
|
|
/// assert_eq!(component.n_equations(), 3);
|
|
/// ```
|
|
fn n_equations(&self) -> usize;
|
|
|
|
/// Returns the connected ports of this component.
|
|
///
|
|
/// This method provides access to the component's ports for topology
|
|
/// validation and graph construction. Components without ports should
|
|
/// return an empty slice.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::{Component, ComponentError, StateSlice, ResidualVector, JacobianBuilder, ConnectedPort};
|
|
///
|
|
/// struct PortlessComponent;
|
|
/// impl Component for PortlessComponent {
|
|
/// fn compute_residuals(&self, _state: &StateSlice, _residuals: &mut ResidualVector) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
/// fn jacobian_entries(&self, _state: &StateSlice, _jacobian: &mut JacobianBuilder) -> Result<(), ComponentError> {
|
|
/// Ok(())
|
|
/// }
|
|
/// fn n_equations(&self) -> usize { 0 }
|
|
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
|
/// }
|
|
///
|
|
/// let component = PortlessComponent;
|
|
/// assert!(component.get_ports().is_empty());
|
|
/// ```
|
|
fn get_ports(&self) -> &[ConnectedPort];
|
|
|
|
/// Returns the names of this component's ports in index order.
|
|
///
|
|
/// The default implementation returns an empty vector. Components with
|
|
/// named ports should override this to return human-readable names
|
|
/// (e.g., `["suction", "discharge"]` for a compressor).
|
|
///
|
|
/// Port names are used by [`SystemBuilder::edge_with_ports`] to create
|
|
/// validated connections using string identifiers instead of integer indices.
|
|
fn port_names(&self) -> Vec<String> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Resolves a port name string to a port index for this component.
|
|
///
|
|
/// First checks the explicit [`port_names`](Self::port_names) override. If the
|
|
/// component defines named ports and `name` matches one, returns the matching index.
|
|
///
|
|
/// If no explicit port names are defined, falls back to a convention-based lookup
|
|
/// using standard thermodynamic port naming:
|
|
///
|
|
/// | Name pattern | Index |
|
|
/// |-------------------------------------------|-------|
|
|
/// | `inlet`, `in`, `suction`, `cold_in` | 0 |
|
|
/// | `outlet`, `out`, `discharge`, `cold_out` | 1 |
|
|
/// | `hot_in`, `hot_inlet`, `refrigerant_in` | 2 |
|
|
/// | `hot_out`, `hot_outlet`, `refrigerant_out` | 3 |
|
|
/// | `economizer`, `eco`, `economiser` | 2 |
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns a string describing why the port name could not be resolved.
|
|
fn resolve_port_name(&self, name: &str) -> Result<usize, String> {
|
|
let names = self.port_names();
|
|
if !names.is_empty() {
|
|
for (i, n) in names.iter().enumerate() {
|
|
if n.eq_ignore_ascii_case(name) {
|
|
return Ok(i);
|
|
}
|
|
}
|
|
return Err(format!(
|
|
"Port '{}' not found on component (valid ports: {})",
|
|
name,
|
|
names
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, n)| format!("{i}: {n}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
));
|
|
}
|
|
|
|
let lower = name.to_ascii_lowercase();
|
|
match lower.as_str() {
|
|
"inlet" | "in" | "suction" | "cold_in" => Ok(0),
|
|
"outlet" | "out" | "discharge" | "cold_out" => Ok(1),
|
|
"hot_in" | "hot_inlet" | "refrigerant_in" | "feed_inlet" | "evaporator_return" => {
|
|
Ok(2 % self.n_equations().max(2))
|
|
}
|
|
"hot_out" | "hot_outlet" | "refrigerant_out" | "liquid_outlet" | "vapor_outlet" => {
|
|
Ok(3 % self.n_equations().max(2))
|
|
}
|
|
"economizer" | "eco" | "economiser" | "flash_in" => Ok(2),
|
|
other => Err(format!("Unknown port name '{other}' for component")),
|
|
}
|
|
}
|
|
|
|
/// Injects system-level context into a component during topology finalization.
|
|
///
|
|
/// Called by [`System::finalize()`] after all edge state indices are computed.
|
|
/// The default implementation is a no-op; override this in components that need
|
|
/// to know their position in the global state vector (e.g. `MacroComponent`).
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `state_offset` — The index in the global state vector where this component's
|
|
/// *internal* state block begins. For ordinary leaf components this is never
|
|
/// needed; for `MacroComponent` it replaces the manual `set_global_state_offset`
|
|
/// call.
|
|
/// * `external_edge_state_indices` — A slice of `(p_idx, h_idx)` pairs for every
|
|
/// edge incident to this component's node in the parent graph (incoming and
|
|
/// outgoing), in traversal order. `MacroComponent` uses these to emit
|
|
/// port-coupling residuals.
|
|
fn set_system_context(
|
|
&mut self,
|
|
_state_offset: usize,
|
|
_external_edge_state_indices: &[(usize, usize)],
|
|
) {
|
|
// Default: no-op for all ordinary leaf components.
|
|
}
|
|
|
|
/// Returns the number of internal state variables this component maintains.
|
|
///
|
|
/// The default implementation returns 0, which is correct for all ordinary
|
|
/// leaf components. Hierarchical components (like `MacroComponent`) should
|
|
/// override this to return the size of their internal state block.
|
|
fn internal_state_len(&self) -> usize {
|
|
0
|
|
}
|
|
|
|
/// Returns the mass flow vector associated with the component's ports.
|
|
///
|
|
/// The returned vector matches the order of ports returned by `get_ports()`.
|
|
/// Positive values indicate flow *into* the component, negative values flow *out*.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `state` - The global system state vector as a slice
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Ok(Vec<MassFlow>)` containing the mass flows if calculation is supported
|
|
/// * `Err(ComponentError::NotImplemented)` by default
|
|
fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
|
Err(ComponentError::CalculationFailed(
|
|
"Mass flow calculation not implemented for this component".to_string(),
|
|
))
|
|
}
|
|
|
|
/// Returns the specified enthalpy vector associated with the component's ports.
|
|
///
|
|
/// The returned vector matches the order of ports returned by `get_ports()`.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `state` - The global system state vector as a slice
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `Ok(Vec<Enthalpy>)` containing the enthalpies if calculation is supported
|
|
/// * `Err(ComponentError::NotImplemented)` by default
|
|
fn port_enthalpies(
|
|
&self,
|
|
_state: &StateSlice,
|
|
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
|
|
Err(ComponentError::CalculationFailed(
|
|
"Enthalpy calculation not implemented for this component".to_string(),
|
|
))
|
|
}
|
|
|
|
/// Injects control variable indices for calibration parameters into a component.
|
|
///
|
|
/// Called by the solver (e.g. `System::finalize()`) after matching `BoundedVariable`s
|
|
/// to components, so the component can read calibration factors dynamically from
|
|
/// the system state vector.
|
|
fn set_calib_indices(&mut self, _indices: entropyk_core::CalibIndices) {
|
|
// Default: no-op for components that don't support inverse calibration
|
|
}
|
|
|
|
/// Updates a single calibration factor on this component.
|
|
///
|
|
/// Returns `true` if the factor was recognized and updated. The default
|
|
/// implementation returns `false` (component does not support calibration).
|
|
/// Components that override this should also apply side effects (e.g.
|
|
/// updating internal model parameters).
|
|
fn update_calib_factor(&mut self, _factor: &str, _value: f64) -> bool {
|
|
false
|
|
}
|
|
|
|
/// Injects a fluid backend into this component for thermodynamic property queries.
|
|
///
|
|
/// Called by [`SystemBuilder::build()`] when a default or per-circuit backend is configured.
|
|
/// Components that already have a backend (set via their own builder) should ignore the call
|
|
/// to preserve the pre-assigned backend.
|
|
///
|
|
/// The default implementation is a no-op — components that don't use fluid backends
|
|
/// silently ignore this.
|
|
fn set_fluid_backend_from_builder(&mut self, _backend: std::sync::Arc<dyn entropyk_fluids::FluidBackend>) {
|
|
// Default: no-op for components that don't use fluid backends
|
|
}
|
|
|
|
/// Evaluates the energy interactions of the component with its environment.
|
|
///
|
|
/// Returns a tuple of `(HeatTransfer, WorkTransfer)` in Watts (converted to `Power`).
|
|
/// - `HeatTransfer` > 0 means heat added TO the component from the environment.
|
|
/// - `WorkTransfer` > 0 means work done BY the component on the environment.
|
|
///
|
|
/// The default implementation returns `None`, indicating that the component does
|
|
/// not support or has not implemented energy transfer reporting. Components that
|
|
/// are strictly adiabatic and passive (like Pipes) should return `Some((Power(0.0), Power(0.0)))`.
|
|
fn energy_transfers(&self, _state: &StateSlice) -> Option<(Power, Power)> {
|
|
None
|
|
}
|
|
|
|
/// Generates a string signature of the component's configuration (parameters, fluid, etc.).
|
|
/// Used for simulation traceability (input hashing).
|
|
/// Default implementation is provided, but components should override this to include
|
|
/// their specific parameters (e.g., fluid type, geometry).
|
|
fn signature(&self) -> String {
|
|
"Component".to_string()
|
|
}
|
|
|
|
/// Extracts component parameters for serialization.
|
|
///
|
|
/// Returns a `ComponentParams` struct containing all information needed to
|
|
/// reconstruct this component later (component type, configuration parameters).
|
|
///
|
|
/// The default implementation returns a generic "Component" type with no parameters.
|
|
/// Component implementations should override this to provide their specific parameters.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use entropyk_components::{Component, ComponentParams, ComponentError, StateSlice, ResidualVector, JacobianBuilder, ConnectedPort};
|
|
///
|
|
/// struct MyComponent;
|
|
/// impl Component for MyComponent {
|
|
/// fn compute_residuals(&self, _s: &StateSlice, _r: &mut ResidualVector) -> Result<(), ComponentError> { Ok(()) }
|
|
/// fn jacobian_entries(&self, _s: &StateSlice, _j: &mut JacobianBuilder) -> Result<(), ComponentError> { Ok(()) }
|
|
/// fn n_equations(&self) -> usize { 2 }
|
|
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
|
///
|
|
/// fn to_params(&self) -> ComponentParams {
|
|
/// ComponentParams::new("MyComponent")
|
|
/// .with_param("value1", 42.0)
|
|
/// .with_param("value2", "test")
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
fn to_params(&self) -> ComponentParams {
|
|
ComponentParams::new(self.signature())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Mock component for testing trait object safety
|
|
struct MockComponent {
|
|
n_equations: usize,
|
|
}
|
|
|
|
impl Component for MockComponent {
|
|
fn compute_residuals(
|
|
&self,
|
|
state: &StateSlice,
|
|
residuals: &mut ResidualVector,
|
|
) -> Result<(), ComponentError> {
|
|
// Validate dimensions
|
|
if residuals.len() != self.n_equations {
|
|
return Err(ComponentError::InvalidResidualDimensions {
|
|
expected: self.n_equations,
|
|
actual: residuals.len(),
|
|
});
|
|
}
|
|
|
|
for (i, residual) in residuals.iter_mut().enumerate() {
|
|
*residual = state.get(i).copied().unwrap_or(0.0);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn jacobian_entries(
|
|
&self,
|
|
_state: &StateSlice,
|
|
jacobian: &mut JacobianBuilder,
|
|
) -> Result<(), ComponentError> {
|
|
// Add identity-like entries for testing
|
|
for i in 0..self.n_equations {
|
|
jacobian.add_entry(i, i, 1.0);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn n_equations(&self) -> usize {
|
|
self.n_equations
|
|
}
|
|
|
|
fn get_ports(&self) -> &[super::ConnectedPort] {
|
|
&[]
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_component_trait_object_compiles() {
|
|
// This test verifies that Component trait is object-safe
|
|
let component: Box<dyn Component> = Box::new(MockComponent { n_equations: 3 });
|
|
assert_eq!(component.n_equations(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_component_reference_trait_object() {
|
|
// Test with reference trait object
|
|
let mock = MockComponent { n_equations: 2 };
|
|
let component: &dyn Component = &mock;
|
|
assert_eq!(component.n_equations(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compute_residuals() {
|
|
let component = MockComponent { n_equations: 3 };
|
|
let state = vec![1.0, 2.0, 3.0];
|
|
let mut residuals = vec![0.0; 3];
|
|
|
|
let result = component.compute_residuals(&state, &mut residuals);
|
|
|
|
assert!(result.is_ok());
|
|
assert_eq!(residuals, vec![1.0, 2.0, 3.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compute_residuals_with_wrong_residual_size() {
|
|
let component = MockComponent { n_equations: 3 };
|
|
let state = vec![1.0, 2.0, 3.0];
|
|
let mut residuals = vec![0.0; 2]; // Wrong size
|
|
|
|
let result = component.compute_residuals(&state, &mut residuals);
|
|
|
|
assert!(result.is_err());
|
|
match result {
|
|
Err(ComponentError::InvalidResidualDimensions { expected, actual }) => {
|
|
assert_eq!(expected, 3);
|
|
assert_eq!(actual, 2);
|
|
}
|
|
_ => panic!("Expected InvalidResidualDimensions error"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_compute_residuals_with_empty_state() {
|
|
let component = MockComponent { n_equations: 3 };
|
|
let state: Vec<f64> = vec![];
|
|
let mut residuals = vec![0.0; 3];
|
|
|
|
// Should still work, using unwrap_or(0.0) for missing state values
|
|
let result = component.compute_residuals(&state, &mut residuals);
|
|
|
|
assert!(result.is_ok());
|
|
assert_eq!(residuals, vec![0.0, 0.0, 0.0]); // Defaults to 0.0
|
|
}
|
|
|
|
#[test]
|
|
fn test_compute_residuals_with_zero_equations() {
|
|
let component = MockComponent { n_equations: 0 };
|
|
let state = vec![1.0, 2.0, 3.0];
|
|
let mut residuals: Vec<f64> = vec![];
|
|
|
|
let result = component.compute_residuals(&state, &mut residuals);
|
|
|
|
assert!(result.is_ok());
|
|
assert!(residuals.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_jacobian_entries() {
|
|
let component = MockComponent { n_equations: 2 };
|
|
let state = vec![0.0; 2];
|
|
let mut jacobian = JacobianBuilder::new();
|
|
|
|
let result = component.jacobian_entries(&state, &mut jacobian);
|
|
|
|
assert!(result.is_ok());
|
|
let entries = jacobian.entries();
|
|
assert_eq!(entries.len(), 2);
|
|
assert_eq!(entries[0], (0, 0, 1.0));
|
|
assert_eq!(entries[1], (1, 1, 1.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_jacobian_entries_with_zero_equations() {
|
|
let component = MockComponent { n_equations: 0 };
|
|
let state = vec![1.0, 2.0];
|
|
let mut jacobian = JacobianBuilder::new();
|
|
|
|
let result = component.jacobian_entries(&state, &mut jacobian);
|
|
|
|
assert!(result.is_ok());
|
|
assert!(jacobian.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_jacobian_builder() {
|
|
let mut builder = JacobianBuilder::new();
|
|
assert!(builder.is_empty());
|
|
|
|
builder.add_entry(0, 1, 2.5);
|
|
assert_eq!(builder.len(), 1);
|
|
assert!(!builder.is_empty());
|
|
|
|
let entries = builder.entries();
|
|
assert_eq!(entries[0], (0, 1, 2.5));
|
|
}
|
|
|
|
#[test]
|
|
fn test_jacobian_builder_add_entries() {
|
|
let mut builder = JacobianBuilder::new();
|
|
let entries = vec![(0, 0, 1.0), (0, 1, 2.0), (1, 0, 3.0)];
|
|
|
|
builder.add_entries(entries);
|
|
|
|
assert_eq!(builder.len(), 3);
|
|
assert_eq!(builder.entries()[0], (0, 0, 1.0));
|
|
assert_eq!(builder.entries()[1], (0, 1, 2.0));
|
|
assert_eq!(builder.entries()[2], (1, 0, 3.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_jacobian_builder_clear() {
|
|
let mut builder = JacobianBuilder::new();
|
|
builder.add_entry(0, 0, 1.0);
|
|
builder.add_entry(1, 1, 2.0);
|
|
|
|
assert_eq!(builder.len(), 2);
|
|
|
|
builder.clear();
|
|
|
|
assert!(builder.is_empty());
|
|
assert_eq!(builder.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_n_equations() {
|
|
let component = MockComponent { n_equations: 5 };
|
|
assert_eq!(component.n_equations(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_n_equations_zero() {
|
|
let component = MockComponent { n_equations: 0 };
|
|
assert_eq!(component.n_equations(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_of_components() {
|
|
// Verify we can create a vector of trait objects
|
|
let components: Vec<Box<dyn Component>> = vec![
|
|
Box::new(MockComponent { n_equations: 1 }),
|
|
Box::new(MockComponent { n_equations: 2 }),
|
|
Box::new(MockComponent { n_equations: 3 }),
|
|
];
|
|
|
|
let total_equations: usize = components.iter().map(|c| c.n_equations()).sum();
|
|
assert_eq!(total_equations, 6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_with_zero_equation_component() {
|
|
let components: Vec<Box<dyn Component>> = vec![
|
|
Box::new(MockComponent { n_equations: 0 }),
|
|
Box::new(MockComponent { n_equations: 1 }),
|
|
Box::new(MockComponent { n_equations: 0 }),
|
|
];
|
|
|
|
let total_equations: usize = components.iter().map(|c| c.n_equations()).sum();
|
|
assert_eq!(total_equations, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_component_error_display() {
|
|
let err = ComponentError::InvalidStateDimensions {
|
|
expected: 5,
|
|
actual: 3,
|
|
};
|
|
let msg = format!("{}", err);
|
|
assert!(msg.contains("Invalid state vector dimensions"));
|
|
assert!(msg.contains("expected at least 5"));
|
|
assert!(msg.contains("got 3"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_component_error_numerical() {
|
|
let err = ComponentError::NumericalError("Division by zero".to_string());
|
|
let msg = format!("{}", err);
|
|
assert!(msg.contains("Numerical error"));
|
|
assert!(msg.contains("Division by zero"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_component_error_invalid_state() {
|
|
let err = ComponentError::InvalidState("Port not connected".to_string());
|
|
let msg = format!("{}", err);
|
|
assert!(msg.contains("Invalid component state"));
|
|
assert!(msg.contains("Port not connected"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_clonable() {
|
|
let err = ComponentError::InvalidStateDimensions {
|
|
expected: 2,
|
|
actual: 1,
|
|
};
|
|
let cloned = err.clone();
|
|
assert_eq!(err, cloned);
|
|
}
|
|
|
|
#[test]
|
|
fn test_component_with_ports_integration() {
|
|
use crate::port::{FluidId, Port};
|
|
use entropyk_core::{Enthalpy, Pressure};
|
|
|
|
struct ComponentWithPorts {
|
|
ports: Vec<ConnectedPort>,
|
|
}
|
|
|
|
impl ComponentWithPorts {
|
|
fn new() -> Self {
|
|
let port1 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(1.0),
|
|
Enthalpy::from_joules_per_kg(400000.0),
|
|
);
|
|
let port2 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(1.0),
|
|
Enthalpy::from_joules_per_kg(400000.0),
|
|
);
|
|
let (connected1, connected2) =
|
|
port1.connect(port2).expect("connection should succeed");
|
|
Self {
|
|
ports: vec![connected1, connected2],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Component for ComponentWithPorts {
|
|
fn compute_residuals(
|
|
&self,
|
|
_state: &StateSlice,
|
|
_residuals: &mut ResidualVector,
|
|
) -> Result<(), ComponentError> {
|
|
Ok(())
|
|
}
|
|
fn jacobian_entries(
|
|
&self,
|
|
_state: &StateSlice,
|
|
_jacobian: &mut JacobianBuilder,
|
|
) -> Result<(), ComponentError> {
|
|
Ok(())
|
|
}
|
|
fn n_equations(&self) -> usize {
|
|
0
|
|
}
|
|
fn get_ports(&self) -> &[ConnectedPort] {
|
|
&self.ports
|
|
}
|
|
}
|
|
|
|
let component = ComponentWithPorts::new();
|
|
assert_eq!(component.get_ports().len(), 2);
|
|
assert_eq!(component.get_ports()[0].fluid_id().as_str(), "R134a");
|
|
|
|
let boxed: Box<dyn Component> = Box::new(component);
|
|
assert_eq!(boxed.get_ports().len(), 2);
|
|
}
|
|
}
|