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

@@ -10,19 +10,25 @@
//!
//! | Entropyk field | Legacy `f_*` | BOLT equivalent | Effect |
//! |---|---|---|---|
//! | `z_flow` | `f_m` | `Z_flow_suc` | ṁ_suc,eff = z_flow × ṁ_suc,nominal |
//! | `z_flow` | `f_m` | `Z_flow_suc` | ṁ_suc,eff = z_flow × ṁ_suc,nominal (machine **capacity**) |
//! | `z_flow_eco` | — | `Z_flow_eco` | ṁ_eco,eff = z_flow_eco × ṁ_eco,nominal |
//! | `z_dp` | `f_dp` | `Z_dpc`, `Z_dp_ref` | ΔP_eff = z_dp × ΔP_nominal |
//! | `z_ua` | `f_ua` | `Z_UA`, `Z_Ucd`, `Z_Uev` | UA_eff = z_ua × UA_nominal |
//! | `z_power` | `f_power` | `Z_power` | Ẇ_eff = z_power × Ẇ_nominal |
//! | `z_etav` | `f_etav` | — (η_v correction) | η_v,eff = z_etav × η_v,nominal |
//! | `f_w` | `f_w` | energy retention | \(h_{dis}=h_{suc}+f_w\cdot\dot W/\dot m\) (**DGT**) |
//!
//! `f_w` is the fraction of compressor work **retained** in the refrigerant
//! (not a loss fraction): `f_w = 1` → adiabatic (no shell loss);
//! `f_w = 0.98` → ~2% lost to ambient; `f_w = 0` → all work lost (Δh → 0).
//!
//! ## Recommended calibration order
//!
//! 1. **z_flow** — mass flow (compressor power + ṁ measurements)
//! 2. **z_dp** — pressure drops (inlet/outlet pressures)
//! 3. **z_ua** — heat transfer (superheat, subcooling, capacity)
//! 4. **z_power** — compressor power (if z_flow insufficient)
//! 1. **z_flow** — machine capacity via ṁ (pair with Capacity probe)
//! 2. **f_w** — compressor energy retention / shell loss (pair with discharge T / DGT)
//! 3. **z_dp** — pressure drops (pair with P)
//! 4. **z_ua** — heat transfer (pair with Tsat / Tsh)
//! 5. **z_power** — compressor power map (if needed)
use serde::{Deserialize, Serialize};
@@ -42,11 +48,17 @@ pub const Z_UA: &str = "z_ua";
pub const Z_POWER: &str = "z_power";
/// Canonical name for the volumetric-efficiency Z-factor (`z_etav`).
pub const Z_ETAV: &str = "z_etav";
/// Canonical name for the compressor energy-retention factor (`f_w`).
///
/// Fraction of shaft work delivered to the refrigerant enthalpy rise.
/// `1.0` = adiabatic; `0.0` = all work lost to ambient.
pub const F_W: &str = "f_w";
/// Normalizes a user/config factor string to a canonical Z-factor name.
///
/// Accepts legacy `f_*` names, canonical `z_*` names, and common BOLT spellings
/// (`Z_power`, `Z_flow_suc`, `Z_Ucd`, …).
/// (`Z_power`, `Z_flow_suc`, `Z_Ucd`, …). Also accepts heat-loss aliases
/// (`f_w`, `fw`, `f_q`, `z_fw`).
pub fn normalize_factor_name(factor: &str) -> Option<&'static str> {
match factor.trim().to_ascii_lowercase().replace('_', "").as_str() {
"fm" | "zflow" | "zflowsuc" => Some(Z_FLOW),
@@ -55,6 +67,7 @@ pub fn normalize_factor_name(factor: &str) -> Option<&'static str> {
"fua" | "zua" | "zucd" | "zuev" => Some(Z_UA),
"fpower" | "zpower" => Some(Z_POWER),
"fetav" | "zetav" => Some(Z_ETAV),
"fw" | "fq" | "zfw" | "heatloss" => Some(F_W),
_ => None,
}
}
@@ -74,6 +87,9 @@ pub fn id_ends_with_calib_suffix(id: &str) -> Option<&'static str> {
"z_uev",
"z_dpc",
"z_dp",
"f_w",
"f_q",
"z_fw",
"f_m",
"f_dp",
"f_ua",
@@ -146,6 +162,22 @@ pub struct Calib {
/// z_etav: η_v,eff = z_etav × η_v,nominal (compressor displacement correction)
#[serde(default = "one", rename = "z_etav", alias = "zEtav", alias = "f_etav")]
pub z_etav: f64,
/// Compressor energy-retention factor: fraction of shaft work kept in the
/// refrigerant (\(h_{dis}=h_{suc}+f_w\cdot\dot W/\dot m\)).
///
/// - `1.0` — adiabatic (nothing lost to ambient) — **default**
/// - `0.98` — ~2% shell loss
/// - `0.0` — all work lost (no discharge enthalpy rise)
#[serde(
default = "one",
rename = "f_w",
alias = "fw",
alias = "f_q",
alias = "fQ",
alias = "z_fw",
alias = "heat_loss"
)]
pub f_w: f64,
/// Traceability: identifier or hash of the test data used to derive these factors.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calibration_source: Option<String>,
@@ -160,6 +192,7 @@ impl Default for Calib {
z_ua: 1.0,
z_power: 1.0,
z_etav: 1.0,
f_w: 1.0,
calibration_source: None,
}
}
@@ -183,6 +216,8 @@ pub struct CalibIndices {
pub z_power: Option<usize>,
/// State index for z_etav multiplier
pub z_etav: Option<usize>,
/// State index for compressor energy-retention factor `f_w`
pub f_w: Option<usize>,
/// State index for a *physical actuator* free variable (dimensioned, not a
/// multiplier). Interpreted per component: EXV/orifice opening [0..1], fan
/// speed ratio, screw slide position, injection-valve opening, condenser
@@ -201,18 +236,25 @@ pub struct CalibValidationError {
impl std::fmt::Display for CalibValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let range = if self.factor == F_W {
"[0.0, 1.0]"
} else {
"[0.2, 3.0]"
};
write!(
f,
"calib {} = {} is outside allowed range [0.5, 2.0]",
self.factor, self.value
"calib {} = {} is outside allowed range {}",
self.factor, self.value, range
)
}
}
impl std::error::Error for CalibValidationError {}
const MIN_Z: f64 = 0.5;
const MAX_Z: f64 = 2.0;
const MIN_Z: f64 = 0.2;
const MAX_Z: f64 = 3.0;
const MIN_F_W: f64 = 0.0;
const MAX_F_W: f64 = 1.0;
impl Calib {
/// Updates a single factor by name (accepts canonical `z_*`, legacy `f_*`, BOLT `Z_*`).
@@ -242,14 +284,18 @@ impl Calib {
self.z_etav = value;
true
}
Some(F_W) => {
self.f_w = value;
true
}
_ => false,
}
}
/// Validates that all factors lie in [0.5, 2.0]. Returns `Ok(())` or the first invalid factor.
/// Validates multiplicative Z-factors in [0.2, 3.0] and `f_w` in [0, 1].
pub fn validate(&self) -> Result<(), CalibValidationError> {
let check = |name: &'static str, value: f64| {
if !(MIN_Z..=MAX_Z).contains(&value) {
let check = |name: &'static str, value: f64, min: f64, max: f64| {
if !(min..=max).contains(&value) {
Err(CalibValidationError {
factor: name,
value,
@@ -258,12 +304,13 @@ impl Calib {
Ok(())
}
};
check(Z_FLOW, self.z_flow)?;
check(Z_FLOW_ECO, self.z_flow_eco)?;
check(Z_DP, self.z_dp)?;
check(Z_UA, self.z_ua)?;
check(Z_POWER, self.z_power)?;
check(Z_ETAV, self.z_etav)?;
check(Z_FLOW, self.z_flow, MIN_Z, MAX_Z)?;
check(Z_FLOW_ECO, self.z_flow_eco, MIN_Z, MAX_Z)?;
check(Z_DP, self.z_dp, MIN_Z, MAX_Z)?;
check(Z_UA, self.z_ua, MIN_Z, MAX_Z)?;
check(Z_POWER, self.z_power, MIN_Z, MAX_Z)?;
check(Z_ETAV, self.z_etav, MIN_Z, MAX_Z)?;
check(F_W, self.f_w, MIN_F_W, MAX_F_W)?;
Ok(())
}
}
@@ -281,6 +328,7 @@ mod tests {
assert_eq!(c.z_ua, 1.0);
assert_eq!(c.z_power, 1.0);
assert_eq!(c.z_etav, 1.0);
assert_eq!(c.f_w, 1.0); // retention: 1 = adiabatic
assert!(c.validate().is_ok());
}
@@ -293,23 +341,31 @@ mod tests {
z_ua: 2.0,
z_power: 1.0,
z_etav: 1.0,
f_w: 0.98, // ~2% shell loss
calibration_source: None,
};
assert!(ok.validate().is_ok());
let bad_m = Calib {
z_flow: 0.4,
z_flow: 0.1,
..Default::default()
};
let err = bad_m.validate().unwrap_err();
assert_eq!(err.factor, Z_FLOW);
let bad_high = Calib {
z_ua: 2.1,
z_ua: 3.1,
..Default::default()
};
let err2 = bad_high.validate().unwrap_err();
assert_eq!(err2.factor, Z_UA);
let bad_fw = Calib {
f_w: 1.1,
..Default::default()
};
let err3 = bad_fw.validate().unwrap_err();
assert_eq!(err3.factor, F_W);
}
#[test]
@@ -321,6 +377,7 @@ mod tests {
z_ua: 1.0,
z_power: 1.05,
z_etav: 1.0,
f_w: 0.98,
calibration_source: None,
};
let json = serde_json::to_string(&c).unwrap();
@@ -361,6 +418,8 @@ mod tests {
assert_eq!(normalize_factor_name("Z_flow_eco"), Some(Z_FLOW_ECO));
assert_eq!(normalize_factor_name("Z_power"), Some(Z_POWER));
assert_eq!(normalize_factor_name("Z_Ucd"), Some(Z_UA));
assert_eq!(normalize_factor_name("f_w"), Some(F_W));
assert_eq!(normalize_factor_name("f_q"), Some(F_W));
assert_eq!(normalize_factor_name("unknown"), None);
}

View File

@@ -65,7 +65,7 @@ pub use types::{
// Re-export calibration types
pub use calib::{
id_ends_with_calib_suffix, normalize_factor_name, Calib, CalibIndices, CalibValidationError,
id_ends_with_calib_suffix, normalize_factor_name, Calib, CalibIndices, CalibValidationError, F_W,
Z_DP, Z_ETAV, Z_FLOW, Z_FLOW_ECO, Z_POWER, Z_UA,
};