//! # 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` 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 = 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` 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; /// 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) { 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 = 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 { 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 { 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::>() .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)` containing the mass flows if calculation is supported /// * `Err(ComponentError::NotImplemented)` by default fn port_mass_flows(&self, _state: &StateSlice) -> Result, 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)` containing the enthalpies if calculation is supported /// * `Err(ComponentError::NotImplemented)` by default fn port_enthalpies( &self, _state: &StateSlice, ) -> Result, 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) { // 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 = 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 = 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 = 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> = 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> = 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, } 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 = Box::new(component); assert_eq!(boxed.get_ports().len(), 2); } }