Add model embeddings for Z-factor DoF, separate from SaturatedController.
Some checks failed
CI / check (push) Has been cancelled

Fixed/Free Probe calibration now emits embeddings[] (unknown + equation) instead of controls[], keeping SaturatedController for physical regulation only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 22:42:31 +02:00
parent 3808e0f11b
commit 5425685a48
22 changed files with 1189 additions and 485 deletions

View File

@@ -1083,6 +1083,23 @@ impl Compressor<Connected> {
pub fn set_operational_state(&mut self, state: OperationalState) {
self.operational_state = state;
}
/// Live energy-retention factor \(f_w\): fraction of shaft work kept in the
/// refrigerant (`1` = adiabatic, `0` = all work lost to ambient).
///
/// Reads `state[calib_indices.f_w]` when free; otherwise stored calib.
/// Clamped to [0, 1].
fn live_f_w(&self, state: Option<&StateSlice>) -> f64 {
let raw = if let Some(st) = state {
self.calib_indices
.f_w
.map(|idx| st.get(idx).copied().unwrap_or(self.calib.f_w))
.unwrap_or(self.calib.f_w)
} else {
self.calib.f_w
};
raw.clamp(0.0, 1.0)
}
}
impl Component for Compressor<Connected> {
@@ -1194,9 +1211,13 @@ impl Component for Compressor<Connected> {
// ṁ_calc - ṁ_state = 0
residuals[0] = mass_flow_calc - mass_flow_state;
// Residual 1: Energy balance
// Power_calc - ṁ × (h_discharge - h_suction) / η_mech = 0
// Residual 1: Energy balance with retention factor f_w
// (fraction of shaft work kept in the refrigerant):
// ṁ·Δh = Ẇ·f_w / η_mech ⇒ Ẇ·f_w ṁ·Δh/η_mech = 0
// so h_dis ≈ h_suc + f_w·Ẇ/(ṁ·η_mech).
// f_w = 1 → adiabatic (default); f_w = 0 → all work lost to ambient.
let enthalpy_change = h_discharge - h_suction;
let f_w = self.live_f_w(Some(state));
// Prevent division by zero
if self.mechanical_efficiency.abs() < 1e-10 {
@@ -1205,7 +1226,8 @@ impl Component for Compressor<Connected> {
));
}
residuals[1] = power_calc - mass_flow_state * enthalpy_change / self.mechanical_efficiency;
residuals[1] =
power_calc * f_w - mass_flow_state * enthalpy_change / self.mechanical_efficiency;
// r2: ṁ_discharge ṁ_suction = 0 (mass conservation, CM1.3)
// CM1.4: skip when same_branch_m — ṁ_dis == ṁ_suc (same state index),
@@ -1265,7 +1287,8 @@ impl Component for Compressor<Connected> {
)?;
jacobian.add_entry(0, suc_h_idx, dr0_dh_suction);
// Row 1: Energy residual r1 = power_calc × Δh / η_mech
// Row 1: Energy residual r1 = power·f_w ṁ·Δh/η_mech
let f_w = self.live_f_w(Some(state));
// ∂r1/∂ṁ_suction = (h_discharge h_suction) / η_mech
let dr1_dm = -(h_discharge - h_suction) / self.mechanical_efficiency;
jacobian.add_entry(1, suc_m_idx, dr1_dm);
@@ -1280,7 +1303,7 @@ impl Component for Compressor<Connected> {
Temperature::from_kelvin(t),
Temperature::from_kelvin(t_discharge),
None,
))
) * f_w)
},
h_suction,
1.0,
@@ -1296,7 +1319,7 @@ impl Component for Compressor<Connected> {
Temperature::from_kelvin(t_suction),
Temperature::from_kelvin(t),
None,
))
) * f_w)
},
h_discharge,
1.0,
@@ -1326,7 +1349,18 @@ impl Component for Compressor<Connected> {
Temperature::from_kelvin(t_discharge_k),
None,
);
jacobian.add_entry(1, z_power_idx, p_nominal);
// r1 = (z_power·Ẇ_nom)·f_w … ⇒ ∂r1/∂z_power = Ẇ_nom·f_w
jacobian.add_entry(1, z_power_idx, p_nominal * f_w);
}
if let Some(f_w_idx) = self.calib_indices.f_w {
let p_live = self.power_consumption_cooling(
Temperature::from_kelvin(t_suction_k),
Temperature::from_kelvin(t_discharge_k),
Some(state),
);
// r1 = Ẇ·f_w … ⇒ ∂r1/∂f_w = +Ẇ
jacobian.add_entry(1, f_w_idx, p_live);
}
// ∂r0/∂f_etav (AHRI 540 only): ṁ_calc = f_m · f_etav · base with