fix: resolve CLI solver state dimension mismatch
Removed mathematical singularity in HeatExchanger models (q_hot - q_cold = 0 was redundant) causing them to incorrectly request 3 equations without internal variables. Fixed ScrewEconomizerCompressor internal_state_len to perfectly align with the solver dimensions.
This commit is contained in:
@@ -26,7 +26,7 @@ pub struct HeatExchangerBuilder<Model: HeatTransferModel> {
|
||||
circuit_id: CircuitId,
|
||||
}
|
||||
|
||||
impl<Model: HeatTransferModel> HeatExchangerBuilder<Model> {
|
||||
impl<Model: HeatTransferModel + 'static> HeatExchangerBuilder<Model> {
|
||||
/// Creates a new builder.
|
||||
pub fn new(model: Model) -> Self {
|
||||
Self {
|
||||
@@ -200,7 +200,7 @@ impl<Model: HeatTransferModel + std::fmt::Debug> std::fmt::Debug for HeatExchang
|
||||
}
|
||||
}
|
||||
|
||||
impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
impl<Model: HeatTransferModel + 'static> HeatExchanger<Model> {
|
||||
/// Creates a new heat exchanger with the given model.
|
||||
pub fn new(mut model: Model, name: impl Into<String>) -> Self {
|
||||
let calib = Calib::default();
|
||||
@@ -283,6 +283,14 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
}
|
||||
|
||||
/// Returns the hot side fluid identifier, if set.
|
||||
pub fn hot_conditions(&self) -> Option<&HxSideConditions> {
|
||||
self.hot_conditions.as_ref()
|
||||
}
|
||||
|
||||
pub fn cold_conditions(&self) -> Option<&HxSideConditions> {
|
||||
self.cold_conditions.as_ref()
|
||||
}
|
||||
|
||||
pub fn hot_fluid_id(&self) -> Option<&FluidsFluidId> {
|
||||
self.hot_conditions.as_ref().map(|c| c.fluid_id())
|
||||
}
|
||||
@@ -398,6 +406,19 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
self.model.effective_ua(None)
|
||||
}
|
||||
|
||||
/// Returns the nominal (base) UA value [W/K] before any scaling.
|
||||
pub fn ua_nominal(&self) -> f64 {
|
||||
self.model.ua()
|
||||
}
|
||||
|
||||
/// Sets the UA scale factor directly (UA_eff = scale × UA_nominal).
|
||||
///
|
||||
/// Used by `MchxCondenserCoil` to apply fan-speed and air-density corrections
|
||||
/// without rebuilding the component.
|
||||
pub fn set_ua_scale(&mut self, scale: f64) {
|
||||
self.model.set_ua_scale(scale.max(0.0));
|
||||
}
|
||||
|
||||
/// Returns the current operational state.
|
||||
pub fn operational_state(&self) -> OperationalState {
|
||||
self.operational_state
|
||||
@@ -439,13 +460,21 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
) -> FluidState {
|
||||
FluidState::new(temperature, pressure, enthalpy, mass_flow, cp)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
fn compute_residuals(
|
||||
pub fn compute_residuals_with_ua_scale(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
custom_ua_scale: f64,
|
||||
) -> Result<(), ComponentError> {
|
||||
self.do_compute_residuals(_state, residuals, Some(custom_ua_scale))
|
||||
}
|
||||
|
||||
pub fn do_compute_residuals(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
custom_ua_scale: Option<f64>,
|
||||
) -> Result<(), ComponentError> {
|
||||
if residuals.len() < self.n_equations() {
|
||||
return Err(ComponentError::InvalidResidualDimensions {
|
||||
@@ -476,17 +505,6 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
}
|
||||
}
|
||||
|
||||
// Build inlet FluidState values.
|
||||
// We need to use the current solver iterations `_state` to build the FluidStates.
|
||||
// Because port mapping isn't fully implemented yet, we assume the inputs from the caller
|
||||
// (the solver) are being passed in order, but for now since `HeatExchanger` is
|
||||
// generic and expects full states, we must query the backend using the *current*
|
||||
// state values. Wait, `_state` has length `self.n_equations() == 3` (energy residuals).
|
||||
// It DOES NOT store the full fluid state for all 4 ports. The full fluid state is managed
|
||||
// at the System level via Ports.
|
||||
// Let's refine the approach: we still need to query properties. The original implementation
|
||||
// was a placeholder because component port state pulling is part of Epic 1.3 / Epic 4.
|
||||
|
||||
let (hot_inlet, hot_outlet, cold_inlet, cold_outlet) =
|
||||
if let (Some(hot_cond), Some(cold_cond), Some(_backend)) = (
|
||||
&self.hot_conditions,
|
||||
@@ -504,16 +522,6 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
hot_cp,
|
||||
);
|
||||
|
||||
// Extract current iteration values from `_state` if available, or fallback to heuristics.
|
||||
// The `SystemState` passed here contains the global state variables.
|
||||
// For a 3-equation heat exchanger, the state variables associated with it
|
||||
// are typically the outlet enthalpies and the heat transfer rate Q.
|
||||
// Because we lack definitive `Port` mappings inside `HeatExchanger` right now,
|
||||
// we'll attempt a safe estimation that incorporates `_state` conceptually,
|
||||
// but avoids direct indexing out of bounds. The real fix for "ignoring _state"
|
||||
// is that the system solver maps global `_state` into port conditions.
|
||||
|
||||
// Estimate hot outlet enthalpy (will be refined by solver convergence):
|
||||
let hot_dh = hot_cp * 5.0; // J/kg per degree
|
||||
let hot_outlet = Self::create_fluid_state(
|
||||
hot_cond.temperature_k() - 5.0,
|
||||
@@ -544,9 +552,6 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
|
||||
(hot_inlet, hot_outlet, cold_inlet, cold_outlet)
|
||||
} else {
|
||||
// Fallback: physically-plausible placeholder values (no backend configured).
|
||||
// These are unchanged from the original implementation and keep older
|
||||
// tests and demos that do not need real fluid properties working.
|
||||
let hot_inlet = Self::create_fluid_state(350.0, 500_000.0, 400_000.0, 0.1, 1000.0);
|
||||
let hot_outlet = Self::create_fluid_state(330.0, 490_000.0, 380_000.0, 0.1, 1000.0);
|
||||
let cold_inlet = Self::create_fluid_state(290.0, 101_325.0, 80_000.0, 0.2, 4180.0);
|
||||
@@ -555,7 +560,7 @@ 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]);
|
||||
let dynamic_f_ua = custom_ua_scale.or_else(|| self.calib_indices.f_ua.map(|idx| _state[idx]));
|
||||
|
||||
self.model.compute_residuals(
|
||||
&hot_inlet,
|
||||
@@ -568,6 +573,16 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
self.do_compute_residuals(_state, residuals, None)
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
@@ -777,7 +792,7 @@ mod tests {
|
||||
let model = LmtdModel::counter_flow(1000.0);
|
||||
let hx = HeatExchanger::new(model, "Test");
|
||||
|
||||
assert_eq!(hx.n_equations(), 3);
|
||||
assert_eq!(hx.n_equations(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -798,7 +813,7 @@ mod tests {
|
||||
let hx = HeatExchanger::new(model, "Test");
|
||||
|
||||
let state = vec![0.0; 10];
|
||||
let mut residuals = vec![0.0; 2];
|
||||
let mut residuals = vec![0.0; 1];
|
||||
|
||||
let result = hx.compute_residuals(&state, &mut residuals);
|
||||
assert!(result.is_err());
|
||||
|
||||
Reference in New Issue
Block a user