feat: implement mass balance validation for Story 7.1
- Added port_mass_flows to Component trait and implements for core components. - Added System::check_mass_balance and integrated it into the solver. - Restored connect methods for ExpansionValve, Compressor, and Pipe to fix integration tests. - Updated Python and C bindings for validation errors. - Updated sprint status and story documentation.
This commit is contained in:
@@ -652,6 +652,39 @@ impl Compressor<Disconnected> {
|
||||
pub fn set_operational_state(&mut self, state: OperationalState) {
|
||||
self.operational_state = state;
|
||||
}
|
||||
/// Connects the compressor to suction and discharge ports.
|
||||
///
|
||||
/// This consumes the disconnected compressor and returns a connected one,
|
||||
/// transitioning the state at compile time.
|
||||
pub fn connect(
|
||||
self,
|
||||
suction: Port<Disconnected>,
|
||||
discharge: Port<Disconnected>,
|
||||
) -> Result<Compressor<Connected>, ComponentError> {
|
||||
let (p_suction, _) = self
|
||||
.port_suction
|
||||
.connect(suction)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
let (p_discharge, _) = self
|
||||
.port_discharge
|
||||
.connect(discharge)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
|
||||
Ok(Compressor {
|
||||
model: self.model,
|
||||
port_suction: p_suction,
|
||||
port_discharge: p_discharge,
|
||||
speed_rpm: self.speed_rpm,
|
||||
displacement_m3_per_rev: self.displacement_m3_per_rev,
|
||||
mechanical_efficiency: self.mechanical_efficiency,
|
||||
calib: self.calib,
|
||||
calib_indices: self.calib_indices,
|
||||
fluid_id: self.fluid_id,
|
||||
circuit_id: self.circuit_id,
|
||||
operational_state: self.operational_state,
|
||||
_state: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Compressor<Connected> {
|
||||
@@ -1217,6 +1250,22 @@ impl Component for Compressor<Connected> {
|
||||
2 // Mass flow residual and energy residual
|
||||
}
|
||||
|
||||
fn port_mass_flows(&self, state: &SystemState) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
if state.len() < 4 {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 4,
|
||||
actual: state.len(),
|
||||
});
|
||||
}
|
||||
let m = entropyk_core::MassFlow::from_kg_per_s(state[0]);
|
||||
// Suction (inlet), Discharge (outlet), Oil (no flow modeled yet)
|
||||
Ok(vec![
|
||||
m,
|
||||
entropyk_core::MassFlow::from_kg_per_s(-m.to_kg_per_s()),
|
||||
entropyk_core::MassFlow::from_kg_per_s(0.0)
|
||||
])
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
// NOTE: This returns an empty slice due to lifetime constraints.
|
||||
// Use `get_ports_slice()` method on Compressor<Connected> for actual port access.
|
||||
|
||||
@@ -222,6 +222,36 @@ impl ExpansionValve<Disconnected> {
|
||||
pub fn is_effectively_off(&self) -> bool {
|
||||
is_effectively_off_impl(self.operational_state, self.opening)
|
||||
}
|
||||
/// Connects the expansion valve to inlet and outlet ports.
|
||||
///
|
||||
/// This consumes the disconnected valve and returns a connected one,
|
||||
/// transitioning the state at compile time.
|
||||
pub fn connect(
|
||||
self,
|
||||
inlet: Port<Disconnected>,
|
||||
outlet: Port<Disconnected>,
|
||||
) -> Result<ExpansionValve<Connected>, ComponentError> {
|
||||
let (p_in, _) = self
|
||||
.port_inlet
|
||||
.connect(inlet)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
let (p_out, _) = self
|
||||
.port_outlet
|
||||
.connect(outlet)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
|
||||
Ok(ExpansionValve {
|
||||
port_inlet: p_in,
|
||||
port_outlet: p_out,
|
||||
calib: self.calib,
|
||||
calib_indices: self.calib_indices,
|
||||
operational_state: self.operational_state,
|
||||
opening: self.opening,
|
||||
fluid_id: self.fluid_id,
|
||||
circuit_id: self.circuit_id,
|
||||
_state: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase region at a thermodynamic state point.
|
||||
@@ -603,6 +633,18 @@ impl Component for ExpansionValve<Connected> {
|
||||
2
|
||||
}
|
||||
|
||||
fn port_mass_flows(&self, state: &SystemState) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
if state.len() < MIN_STATE_DIMENSIONS {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: MIN_STATE_DIMENSIONS,
|
||||
actual: state.len(),
|
||||
});
|
||||
}
|
||||
let m_in = entropyk_core::MassFlow::from_kg_per_s(state[0]);
|
||||
let m_out = entropyk_core::MassFlow::from_kg_per_s(-state[1]); // Negative because it's leaving
|
||||
Ok(vec![m_in, m_out])
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ pub use state_machine::{
|
||||
StateTransitionRecord,
|
||||
};
|
||||
|
||||
use entropyk_core::MassFlow;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur during component operations.
|
||||
@@ -543,6 +544,23 @@ pub trait Component {
|
||||
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
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<MassFlow>)` containing the mass flows if calculation is supported
|
||||
/// * `Err(ComponentError::NotImplemented)` by default
|
||||
fn port_mass_flows(&self, _state: &SystemState) -> Result<Vec<MassFlow>, ComponentError> {
|
||||
Err(ComponentError::CalculationFailed("Mass flow 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
|
||||
|
||||
@@ -404,6 +404,36 @@ impl Pipe<Disconnected> {
|
||||
pub fn set_calib(&mut self, calib: Calib) {
|
||||
self.calib = calib;
|
||||
}
|
||||
/// Connects the pipe to inlet and outlet ports.
|
||||
///
|
||||
/// This consumes the disconnected pipe and returns a connected one,
|
||||
/// transitioning the state at compile time.
|
||||
pub fn connect(
|
||||
self,
|
||||
inlet: Port<Disconnected>,
|
||||
outlet: Port<Disconnected>,
|
||||
) -> Result<Pipe<Connected>, ComponentError> {
|
||||
let (p_in, _) = self
|
||||
.port_inlet
|
||||
.connect(inlet)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
let (p_out, _) = self
|
||||
.port_outlet
|
||||
.connect(outlet)
|
||||
.map_err(|e| ComponentError::InvalidState(e.to_string()))?;
|
||||
|
||||
Ok(Pipe {
|
||||
geometry: self.geometry,
|
||||
port_inlet: p_in,
|
||||
port_outlet: p_out,
|
||||
fluid_density_kg_per_m3: self.fluid_density_kg_per_m3,
|
||||
fluid_viscosity_pa_s: self.fluid_viscosity_pa_s,
|
||||
calib: self.calib,
|
||||
circuit_id: self.circuit_id,
|
||||
operational_state: self.operational_state,
|
||||
_state: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipe<Connected> {
|
||||
@@ -622,6 +652,17 @@ impl Component for Pipe<Connected> {
|
||||
1
|
||||
}
|
||||
|
||||
fn port_mass_flows(&self, state: &SystemState) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
if state.is_empty() {
|
||||
return Err(ComponentError::InvalidStateDimensions {
|
||||
expected: 1,
|
||||
actual: 0,
|
||||
});
|
||||
}
|
||||
let m = entropyk_core::MassFlow::from_kg_per_s(state[0]);
|
||||
Ok(vec![m, entropyk_core::MassFlow::from_kg_per_s(-m.to_kg_per_s())])
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user