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:
@@ -91,6 +91,7 @@ impl std::fmt::Debug for SimpleAdapter {
|
||||
/// )
|
||||
#[pyclass(name = "Compressor", module = "entropyk")]
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)] // Fields reserved until SimpleAdapter type-state migration
|
||||
pub struct PyCompressor {
|
||||
pub(crate) coefficients: entropyk::Ahri540Coefficients,
|
||||
pub(crate) speed_rpm: f64,
|
||||
@@ -381,6 +382,7 @@ impl PyExpansionValve {
|
||||
/// density=1140.0, viscosity=0.0002)
|
||||
#[pyclass(name = "Pipe", module = "entropyk")]
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)] // Fields reserved until SimpleAdapter type-state migration
|
||||
pub struct PyPipe {
|
||||
pub(crate) length: f64,
|
||||
pub(crate) diameter: f64,
|
||||
|
||||
@@ -37,6 +37,7 @@ pub fn register_exceptions(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
}
|
||||
|
||||
/// Converts a `ThermoError` into the appropriate Python exception.
|
||||
#[allow(dead_code)]
|
||||
pub fn thermo_error_to_pyerr(err: entropyk::ThermoError) -> PyErr {
|
||||
use entropyk::ThermoError;
|
||||
match &err {
|
||||
@@ -48,6 +49,8 @@ pub fn thermo_error_to_pyerr(err: entropyk::ThermoError) -> PyErr {
|
||||
TimeoutError::new_err(msg)
|
||||
} else if solver_msg.contains("saturation") || solver_msg.contains("Saturation") {
|
||||
ControlSaturationError::new_err(msg)
|
||||
} else if solver_msg.contains("validation") || solver_msg.contains("Validation") {
|
||||
ValidationError::new_err(msg)
|
||||
} else {
|
||||
SolverError::new_err(msg)
|
||||
}
|
||||
@@ -67,6 +70,7 @@ pub fn thermo_error_to_pyerr(err: entropyk::ThermoError) -> PyErr {
|
||||
| ThermoError::Mixture(_)
|
||||
| ThermoError::InvalidInput(_)
|
||||
| ThermoError::NotSupported(_)
|
||||
| ThermoError::NotFinalized => EntropykError::new_err(err.to_string()),
|
||||
| ThermoError::NotFinalized
|
||||
| ThermoError::Validation { .. } => EntropykError::new_err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ fn entropyk(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<solver::PyFallbackConfig>()?;
|
||||
m.add_class::<solver::PyConvergedState>()?;
|
||||
m.add_class::<solver::PyConvergenceStatus>()?;
|
||||
m.add_class::<solver::PyConstraint>()?;
|
||||
m.add_class::<solver::PyBoundedVariable>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Python wrappers for Entropyk solver and system types.
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::exceptions::{PyValueError, PyRuntimeError};
|
||||
use std::time::Duration;
|
||||
@@ -25,7 +23,90 @@ use crate::components::AnyPyComponent;
|
||||
/// system.finalize()
|
||||
#[pyclass(name = "System", module = "entropyk", unsendable)]
|
||||
pub struct PySystem {
|
||||
inner: entropyk_solver::System,
|
||||
pub(crate) inner: entropyk_solver::System,
|
||||
}
|
||||
|
||||
#[pyclass(name = "Constraint")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyConstraint {
|
||||
pub(crate) inner: entropyk_solver::inverse::Constraint,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyConstraint {
|
||||
#[staticmethod]
|
||||
#[pyo3(signature = (id, component_id, target_value, tolerance=1e-6))]
|
||||
fn superheat(id: String, component_id: String, target_value: f64, tolerance: f64) -> Self {
|
||||
use entropyk_solver::inverse::{ComponentOutput, Constraint, ConstraintId};
|
||||
Self {
|
||||
inner: Constraint::with_tolerance(
|
||||
ConstraintId::new(id),
|
||||
ComponentOutput::Superheat { component_id },
|
||||
target_value,
|
||||
tolerance,
|
||||
).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
#[pyo3(signature = (id, component_id, target_value, tolerance=1e-6))]
|
||||
fn subcooling(id: String, component_id: String, target_value: f64, tolerance: f64) -> Self {
|
||||
use entropyk_solver::inverse::{ComponentOutput, Constraint, ConstraintId};
|
||||
Self {
|
||||
inner: Constraint::with_tolerance(
|
||||
ConstraintId::new(id),
|
||||
ComponentOutput::Subcooling { component_id },
|
||||
target_value,
|
||||
tolerance,
|
||||
).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
#[pyo3(signature = (id, component_id, target_value, tolerance=1e-6))]
|
||||
fn capacity(id: String, component_id: String, target_value: f64, tolerance: f64) -> Self {
|
||||
use entropyk_solver::inverse::{ComponentOutput, Constraint, ConstraintId};
|
||||
Self {
|
||||
inner: Constraint::with_tolerance(
|
||||
ConstraintId::new(id),
|
||||
ComponentOutput::Capacity { component_id },
|
||||
target_value,
|
||||
tolerance,
|
||||
).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Constraint(id='{}', target={}, tol={})", self.inner.id(), self.inner.target_value(), self.inner.tolerance())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(name = "BoundedVariable")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyBoundedVariable {
|
||||
pub(crate) inner: entropyk_solver::inverse::BoundedVariable,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBoundedVariable {
|
||||
#[new]
|
||||
#[pyo3(signature = (id, value, min, max, component_id=None))]
|
||||
fn new(id: String, value: f64, min: f64, max: f64, component_id: Option<String>) -> PyResult<Self> {
|
||||
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId};
|
||||
let inner = match component_id {
|
||||
Some(cid) => BoundedVariable::with_component(BoundedVariableId::new(id), cid, value, min, max),
|
||||
None => BoundedVariable::new(BoundedVariableId::new(id), value, min, max),
|
||||
};
|
||||
match inner {
|
||||
Ok(v) => Ok(Self { inner: v }),
|
||||
Err(e) => Err(PyValueError::new_err(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
// use is_saturated if available but simpler:
|
||||
format!("BoundedVariable(id='{}', value={}, bounds=[{}, {}])", self.inner.id(), self.inner.value(), self.inner.min(), self.inner.max())
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
@@ -69,6 +150,34 @@ impl PySystem {
|
||||
Ok(edge.index())
|
||||
}
|
||||
|
||||
/// Register a human-readable name for a component node to be used in Constraints.
|
||||
fn register_component_name(&mut self, name: &str, node_idx: usize) -> PyResult<()> {
|
||||
let node = petgraph::graph::NodeIndex::new(node_idx);
|
||||
self.inner.register_component_name(name, node);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a constraint to the system.
|
||||
fn add_constraint(&mut self, constraint: &PyConstraint) -> PyResult<()> {
|
||||
self.inner.add_constraint(constraint.inner.clone())
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
|
||||
/// Add a bounded variable to the system.
|
||||
fn add_bounded_variable(&mut self, variable: &PyBoundedVariable) -> PyResult<()> {
|
||||
self.inner.add_bounded_variable(variable.inner.clone())
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
|
||||
/// Link a constraint to a control variable for the inverse solver.
|
||||
fn link_constraint_to_control(&mut self, constraint_id: &str, control_id: &str) -> PyResult<()> {
|
||||
use entropyk_solver::inverse::{ConstraintId, BoundedVariableId};
|
||||
self.inner.link_constraint_to_control(
|
||||
&ConstraintId::new(constraint_id),
|
||||
&BoundedVariableId::new(control_id),
|
||||
).map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
|
||||
/// Finalize the system graph: build state index mapping and validate topology.
|
||||
///
|
||||
/// Must be called before ``solve()``.
|
||||
|
||||
Reference in New Issue
Block a user