Fix code review findings for Story 5-1

- Fixed Critical issue: Wired up _state to the underlying HeatExchanger boundary conditions so the Newton-Raphson solver actually sees numerical gradients.
- Fixed Critical issue: Bubble up FluidBackend errors via ComponentError::CalculationFailed instead of silently swallowing backend evaluation failures.
- Fixed Medium issue: Connected condenser_with_backend into the eurovent.rs system architecture so the demo solves instead of just printing output.
- Fixed Medium issue: Removed heavy FluidId clones inside query loop.
- Fixed Low issue: Added physical validations to HxSideConditions.
This commit is contained in:
Sepehr
2026-02-20 21:25:44 +01:00
parent be70a7a6c7
commit 73ad750f31
9 changed files with 5590 additions and 34 deletions

View File

@@ -22,7 +22,7 @@
//! ## Example
//!
//! ```rust
//! use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder};
//! use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder, ConnectedPort};
//!
//! struct MockComponent {
//! n_equations: usize,
@@ -42,6 +42,10 @@
//! fn n_equations(&self) -> usize {
//! self.n_equations
//! }
//!
//! fn get_ports(&self) -> &[ConnectedPort] {
//! &[]
//! }
//! }
//!
//! // Trait object usage
@@ -51,6 +55,41 @@
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
pub mod compressor;
pub mod expansion_valve;
pub mod external_model;
pub mod fan;
pub mod heat_exchanger;
pub mod pipe;
pub mod polynomials;
pub mod port;
pub mod pump;
pub mod state_machine;
pub use compressor::{Ahri540Coefficients, Compressor, CompressorModel, SstSdtCoefficients};
pub use expansion_valve::{ExpansionValve, PhaseRegion};
pub use external_model::{
ExternalModel, ExternalModelConfig, ExternalModelError, ExternalModelMetadata,
ExternalModelType, MockExternalModel, ThreadSafeExternalModel,
};
pub use fan::{Fan, FanCurves};
pub use heat_exchanger::model::FluidState;
pub use heat_exchanger::{
Condenser, CondenserCoil, Economizer, EpsNtuModel, Evaporator, EvaporatorCoil, ExchangerType,
FlowConfiguration, HeatExchanger, HeatExchangerBuilder, HeatTransferModel, HxSideConditions,
LmtdModel,
};
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 state_machine::{
CircuitId, OperationalState, StateHistory, StateManageable, StateTransitionError,
StateTransitionRecord,
};
use thiserror::Error;
/// Errors that can occur during component operations.
@@ -95,6 +134,26 @@ pub enum ComponentError {
/// 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.
@@ -246,7 +305,7 @@ impl JacobianBuilder {
/// This trait is **object-safe**, meaning it can be used with dynamic dispatch:
///
/// ```
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder};
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder, ConnectedPort};
///
/// struct SimpleComponent;
/// impl Component for SimpleComponent {
@@ -257,6 +316,7 @@ impl JacobianBuilder {
/// Ok(())
/// }
/// fn n_equations(&self) -> usize { 1 }
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
/// }
///
/// let component: Box<dyn Component> = Box::new(SimpleComponent);
@@ -310,7 +370,7 @@ pub trait Component {
/// # Example
///
/// ```
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder};
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder, ConnectedPort};
///
/// struct MassBalanceComponent;
/// impl Component for MassBalanceComponent {
@@ -328,6 +388,7 @@ pub trait Component {
/// Ok(())
/// }
/// fn n_equations(&self) -> usize { 1 }
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
/// }
/// ```
fn compute_residuals(
@@ -358,7 +419,7 @@ pub trait Component {
/// # Example
///
/// ```
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder};
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder, ConnectedPort};
///
/// struct LinearComponent;
/// impl Component for LinearComponent {
@@ -375,6 +436,7 @@ pub trait Component {
/// }
///
/// fn n_equations(&self) -> usize { 1 }
/// fn get_ports(&self) -> &[ConnectedPort] { &[] }
/// }
/// ```
fn jacobian_entries(
@@ -391,7 +453,7 @@ pub trait Component {
/// # Examples
///
/// ```
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder};
/// use entropyk_components::{Component, ComponentError, SystemState, ResidualVector, JacobianBuilder, ConnectedPort};
///
/// struct ThreeEquationComponent;
/// impl Component for ThreeEquationComponent {
@@ -402,12 +464,41 @@ pub trait Component {
/// 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, SystemState, ResidualVector, JacobianBuilder, ConnectedPort};
///
/// struct PortlessComponent;
/// impl Component for PortlessComponent {
/// fn compute_residuals(&self, _state: &SystemState, _residuals: &mut ResidualVector) -> Result<(), ComponentError> {
/// Ok(())
/// }
/// fn jacobian_entries(&self, _state: &SystemState, _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];
}
#[cfg(test)]
@@ -454,6 +545,10 @@ mod tests {
fn n_equations(&self) -> usize {
self.n_equations
}
fn get_ports(&self) -> &[super::ConnectedPort] {
&[]
}
}
#[test]
@@ -667,4 +762,64 @@ mod tests {
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: &SystemState,
_residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
Ok(())
}
fn jacobian_entries(
&self,
_state: &SystemState,
_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);
}
}