Add model embeddings for Z-factor DoF, separate from SaturatedController.
Some checks failed
CI / check (push) Has been cancelled
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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user