feat(python): implement python bindings for all components and solvers

This commit is contained in:
Sepehr
2026-02-21 20:34:56 +01:00
parent 8ef8cd2eba
commit 4440132b0a
310 changed files with 11577 additions and 397 deletions

View File

@@ -157,6 +157,8 @@ pub struct HeatExchanger<Model: HeatTransferModel> {
name: String,
/// Calibration: f_dp for refrigerant-side ΔP when modeled, f_ua for UA scaling
calib: Calib,
/// Indices for dynamically extracting calibration factors from the system state
calib_indices: entropyk_core::CalibIndices,
operational_state: OperationalState,
circuit_id: CircuitId,
/// Optional fluid property backend for real thermodynamic calculations (Story 5.1).
@@ -190,6 +192,7 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
model,
name: name.into(),
calib,
calib_indices: entropyk_core::CalibIndices::default(),
operational_state: OperationalState::default(),
circuit_id: CircuitId::default(),
fluid_backend: None,
@@ -262,6 +265,16 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
self.fluid_backend.is_some()
}
/// Returns the hot side fluid identifier, if set.
pub fn hot_fluid_id(&self) -> Option<&FluidsFluidId> {
self.hot_conditions.as_ref().map(|c| c.fluid_id())
}
/// Returns the cold side fluid identifier, if set.
pub fn cold_fluid_id(&self) -> Option<&FluidsFluidId> {
self.cold_conditions.as_ref().map(|c| c.fluid_id())
}
/// Computes the full thermodynamic state at the hot inlet.
pub fn hot_inlet_state(&self) -> Result<ThermoState, ComponentError> {
let backend = self.fluid_backend.as_ref().ok_or_else(|| ComponentError::CalculationFailed("No FluidBackend configured".to_string()))?;
@@ -327,7 +340,7 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
/// Returns the effective UA value (f_ua × UA_nominal).
pub fn ua(&self) -> f64 {
self.model.effective_ua()
self.model.effective_ua(None)
}
/// Returns the current operational state.
@@ -487,12 +500,15 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
(hot_inlet, hot_outlet, cold_inlet, cold_outlet)
};
let dynamic_f_ua = self.calib_indices.f_ua.map(|idx| _state[idx]);
self.model.compute_residuals(
&hot_inlet,
&hot_outlet,
&cold_inlet,
&cold_outlet,
residuals,
dynamic_f_ua,
);
Ok(())
@@ -503,6 +519,67 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
_state: &SystemState,
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
// ∂r/∂f_ua = -∂Q/∂f_ua (Story 5.5)
if let Some(f_ua_idx) = self.calib_indices.f_ua {
// Need to compute Q_nominal (with UA_scale = 1.0)
// This requires repeating the residual calculation logic with dynamic_ua_scale = None
// For now, we'll use a finite difference approximation or a simplified nominal calculation.
// Re-use logic from compute_residuals but only for Q
if let (Some(hot_cond), Some(cold_cond), Some(_backend)) = (
&self.hot_conditions,
&self.cold_conditions,
&self.fluid_backend,
) {
let hot_cp = self.query_cp(hot_cond)?;
let hot_h_in = self.query_enthalpy(hot_cond)?;
let hot_inlet = Self::create_fluid_state(
hot_cond.temperature_k(),
hot_cond.pressure_pa(),
hot_h_in,
hot_cond.mass_flow_kg_s(),
hot_cp,
);
let hot_dh = hot_cp * 5.0;
let hot_outlet = Self::create_fluid_state(
hot_cond.temperature_k() - 5.0,
hot_cond.pressure_pa() * 0.998,
hot_h_in - hot_dh,
hot_cond.mass_flow_kg_s(),
hot_cp,
);
let cold_cp = self.query_cp(cold_cond)?;
let cold_h_in = self.query_enthalpy(cold_cond)?;
let cold_inlet = Self::create_fluid_state(
cold_cond.temperature_k(),
cold_cond.pressure_pa(),
cold_h_in,
cold_cond.mass_flow_kg_s(),
cold_cp,
);
let cold_dh = cold_cp * 5.0;
let cold_outlet = Self::create_fluid_state(
cold_cond.temperature_k() + 5.0,
cold_cond.pressure_pa() * 0.998,
cold_h_in + cold_dh,
cold_cond.mass_flow_kg_s(),
cold_cp,
);
let q_nominal = self.model.compute_heat_transfer(
&hot_inlet, &hot_outlet, &cold_inlet, &cold_outlet, None
).to_watts();
// r0 = Q_hot - Q -> ∂r0/∂f_ua = -Q_nominal
// r1 = Q_cold - Q -> ∂r1/∂f_ua = -Q_nominal
// r2 = Q_hot - Q_cold -> ∂r2/∂f_ua = 0
_jacobian.add_entry(0, f_ua_idx, -q_nominal);
_jacobian.add_entry(1, f_ua_idx, -q_nominal);
_jacobian.add_entry(2, f_ua_idx, 0.0);
}
}
Ok(())
}
@@ -510,6 +587,10 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
self.model.n_equations()
}
fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
self.calib_indices = indices;
}
fn get_ports(&self) -> &[ConnectedPort] {
// TODO: Return actual ports when port storage is implemented.
// Port storage pending integration with Port<Connected> system from Story 1.3.