feat(python): implement python bindings for all components and solvers
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user