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:
@@ -150,22 +150,21 @@ fn bphx_condenser_sdt_calibration_converges_and_solves_z_ua() {
|
||||
"tolerance": 1e-06,
|
||||
"timeout_ms": 60000
|
||||
},
|
||||
"controls": [
|
||||
"embeddings": [
|
||||
{
|
||||
"type": "SaturatedController",
|
||||
"id": "sdt_calib",
|
||||
"measure": {
|
||||
"component": "cond",
|
||||
"output": "saturationTemperature"
|
||||
},
|
||||
"actuator": {
|
||||
"id": "emb_cond_z_ua",
|
||||
"unknown": {
|
||||
"component": "cond",
|
||||
"factor": "z_ua",
|
||||
"initial": 0.3,
|
||||
"start": 0.3,
|
||||
"min": 0.05,
|
||||
"max": 2.0
|
||||
},
|
||||
"target": 315.0
|
||||
"equation": {
|
||||
"component": "cond",
|
||||
"output": "saturationTemperature",
|
||||
"value": 315.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
40
crates/cli/tests/embeddings_reject_control_z.rs
Normal file
40
crates/cli/tests/embeddings_reject_control_z.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
//! Z-factors must use embeddings[], not controls[]/SaturatedController.
|
||||
|
||||
use entropyk_cli::run::{run_simulation, SimulationStatus};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn controls_with_z_ua_are_rejected() {
|
||||
// Minimal config — reject happens before component graph build.
|
||||
let json = r#"
|
||||
{
|
||||
"fluid": "R134a",
|
||||
"fluid_backend": "Test",
|
||||
"circuits": [],
|
||||
"controls": [
|
||||
{
|
||||
"type": "SaturatedController",
|
||||
"id": "bad_calib",
|
||||
"measure": { "component": "sst", "output": "saturationTemperature" },
|
||||
"actuator": { "component": "evap", "factor": "z_ua", "initial": 1, "min": 0.05, "max": 3 },
|
||||
"target": 278.15
|
||||
}
|
||||
],
|
||||
"solver": { "strategy": "newton", "max_iterations": 10, "tolerance": 1e-6 }
|
||||
}
|
||||
"#;
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("bad.json");
|
||||
std::fs::write(&path, json).unwrap();
|
||||
let result = run_simulation(&path, None, false).unwrap();
|
||||
assert!(
|
||||
matches!(result.status, SimulationStatus::Error),
|
||||
"expected Error, got {:?}",
|
||||
result.status
|
||||
);
|
||||
let err = result.error.unwrap_or_default();
|
||||
assert!(
|
||||
err.contains("embeddings") && err.contains("z_ua"),
|
||||
"error should point to embeddings[], got: {err}"
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
//! Calibration redesign (HARD RULE — Probe for ALL measurements) — integration
|
||||
//! test: a `Probe` node measuring SDT on the condenser refrigerant inlet edge
|
||||
//! drives the plain inverse embedding for `z_ua`. The Probe is the measurement
|
||||
//! source (`control.measure.component` names the Probe); the freed z-factor
|
||||
//! lives on the BPHX condenser (`control.actuator.component`). The two are
|
||||
//! linked 1:1 (+1 residual on Probe SDT target, +1 unknown z_ua).
|
||||
//!
|
||||
//! This is the Probe-based variant of `calibration_sdt.rs`. It proves the
|
||||
//! end-to-end path the UI emits after the calibration redesign.
|
||||
//! test: a `Probe` measuring SDT + free `cond/z_ua` via model `embeddings[]`
|
||||
//! (Modelica unknown + equation — not SaturatedController / controls[]).
|
||||
|
||||
use entropyk_cli::run::{run_simulation, SimulationResult, SimulationStatus};
|
||||
use tempfile::tempdir;
|
||||
@@ -139,22 +133,21 @@ fn probe_based_sdt_calibration_converges_and_solves_z_ua() {
|
||||
"tolerance": 1e-06,
|
||||
"timeout_ms": 60000
|
||||
},
|
||||
"controls": [
|
||||
"embeddings": [
|
||||
{
|
||||
"type": "SaturatedController",
|
||||
"id": "probe_sdt_calib",
|
||||
"measure": {
|
||||
"component": "cond_sdt_probe",
|
||||
"output": "saturationTemperature"
|
||||
},
|
||||
"actuator": {
|
||||
"id": "emb_cond_z_ua",
|
||||
"unknown": {
|
||||
"component": "cond",
|
||||
"factor": "z_ua",
|
||||
"initial": 0.3,
|
||||
"start": 0.3,
|
||||
"min": 0.05,
|
||||
"max": 2.0
|
||||
},
|
||||
"target": 315.0
|
||||
"equation": {
|
||||
"component": "cond_sdt_probe",
|
||||
"output": "saturationTemperature",
|
||||
"value": 315.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -185,6 +178,22 @@ fn probe_based_sdt_calibration_converges_and_solves_z_ua() {
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sdt_c = None;
|
||||
if let Some(state) = result.state.as_ref() {
|
||||
for e in state.iter() {
|
||||
if e.target.as_deref() == Some("cond") || e.source.as_deref() == Some("cond_sdt_probe")
|
||||
{
|
||||
if let Some(ts) = e.saturation_temperature_c {
|
||||
sdt_c = Some(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let sdt_k = sdt_c.expect("must read SDT near condenser inlet probe") + 273.15;
|
||||
assert!(
|
||||
(sdt_k - 315.0).abs() < 0.5,
|
||||
"SDT must hit target 315.0 K within 0.5 K, got {sdt_k}"
|
||||
);
|
||||
assert!(
|
||||
solved.value > 0.05 && solved.value < 2.0,
|
||||
"z_ua must solve within bounds, got {}",
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
//! Calibration redesign (HARD RULE — Probe for ALL measurements) — integration
|
||||
//! test: a `Probe` node measuring SDT on the condenser refrigerant inlet edge
|
||||
//! drives the plain inverse embedding for `z_ua`. The Probe is the measurement
|
||||
//! source (`control.measure.component` names the Probe); the freed z-factor
|
||||
//! lives on the BPHX condenser (`control.actuator.component`). The two are
|
||||
//! linked 1:1 (+1 residual on Probe SDT target, +1 unknown z_ua).
|
||||
//! Probe Fixed Tsat on suction line + free `evap/z_ua` (model embedding).
|
||||
//!
|
||||
//! This is the Probe-based variant of `calibration_sdt.rs`. It proves the
|
||||
//! end-to-end path the UI emits after the calibration redesign.
|
||||
//! HVAC placement: SST lives on the suction line (`evap:outlet → probe → comp:inlet`),
|
||||
//! never on the two-phase EXV→evap inlet.
|
||||
|
||||
use entropyk_cli::run::{run_simulation, SimulationResult, SimulationStatus};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn run_config(json: &str) -> SimulationResult {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("probe_sdt_calib.json");
|
||||
let path = dir.path().join("probe_sst_calib.json");
|
||||
std::fs::write(&path, json).unwrap();
|
||||
run_simulation(&path, None, false).unwrap()
|
||||
}
|
||||
@@ -22,8 +17,8 @@ fn run_config(json: &str) -> SimulationResult {
|
||||
fn probe_based_sst_evap_calibration() {
|
||||
let json = r#"
|
||||
{
|
||||
"name": "Probe-based SDT calibration (R134a BPHX chiller)",
|
||||
"description": "Same vapor-compression cycle as calibration_sdt.rs, but the SDT measurement lives on a Probe node spliced into the condenser refrigerant inlet edge. control.measure.component = the Probe name.",
|
||||
"name": "Probe-based SST calibration (R134a BPHX chiller)",
|
||||
"description": "Suction Probe Fixed Tsat drives evap/z_ua via model embedding.",
|
||||
"fluid": "R134a",
|
||||
"fluid_backend": "CoolProp",
|
||||
"circuits": [
|
||||
@@ -60,7 +55,7 @@ fn probe_based_sst_evap_calibration() {
|
||||
{
|
||||
"type": "Probe",
|
||||
"name": "evap_sst_probe",
|
||||
"measure": "SDT",
|
||||
"measure": "SST",
|
||||
"fluid": "R134a"
|
||||
},
|
||||
{
|
||||
@@ -81,7 +76,8 @@ fn probe_based_sst_evap_calibration() {
|
||||
"emergent_pressure": true,
|
||||
"correlation": "Longo2004",
|
||||
"dp_correlation": "SimplifiedChannel",
|
||||
"ua": 2000.0
|
||||
"ua": 2000.0,
|
||||
"z_ua": 1.0
|
||||
},
|
||||
{
|
||||
"type": "BrineSource",
|
||||
@@ -121,12 +117,11 @@ fn probe_based_sst_evap_calibration() {
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "exv:outlet", "to": "evap_sst_probe:inlet" },
|
||||
{ "from": "evap_sst_probe:outlet","to": "cond:inlet" },
|
||||
{ "from": "comp:outlet", "to": "cond:inlet" },
|
||||
{ "from": "cond:outlet", "to": "exv:inlet" },
|
||||
{ "from": "exv:outlet", "to": "evap:inlet" },
|
||||
{ "from": "evap:outlet", "to": "evap_sst_probe:inlet" },
|
||||
{ "from": "evap_sst_probe:outlet", "to": "comp:inlet" },
|
||||
{ "from": "evap_sst_probe:outlet","to": "comp:inlet" },
|
||||
{ "from": "cond_water_in:outlet", "to": "cond:secondary_inlet" },
|
||||
{ "from": "cond:secondary_outlet","to": "cond_water_out:inlet" },
|
||||
{ "from": "evap_water_in:outlet", "to": "evap:secondary_inlet" },
|
||||
@@ -140,22 +135,21 @@ fn probe_based_sst_evap_calibration() {
|
||||
"tolerance": 1e-06,
|
||||
"timeout_ms": 60000
|
||||
},
|
||||
"controls": [
|
||||
"embeddings": [
|
||||
{
|
||||
"type": "SaturatedController",
|
||||
"id": "probe_sdt_calib",
|
||||
"measure": {
|
||||
"component": "evap_sst_probe",
|
||||
"output": "saturationTemperature"
|
||||
},
|
||||
"actuator": {
|
||||
"id": "emb_evap_z_ua",
|
||||
"unknown": {
|
||||
"component": "evap",
|
||||
"factor": "z_ua",
|
||||
"initial": 0.3,
|
||||
"start": 0.3,
|
||||
"min": 0.05,
|
||||
"max": 2.0
|
||||
},
|
||||
"target": 277.55
|
||||
"equation": {
|
||||
"component": "evap_sst_probe",
|
||||
"output": "saturationTemperature",
|
||||
"value": 277.55
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -163,7 +157,7 @@ fn probe_based_sst_evap_calibration() {
|
||||
let result = run_config(json);
|
||||
assert!(
|
||||
matches!(result.status, SimulationStatus::Converged),
|
||||
"Probe-based SDT calibration must converge: {:?} ({:?})",
|
||||
"Probe-based SST calibration must converge: {:?} ({:?})",
|
||||
result.status,
|
||||
result.error
|
||||
);
|
||||
@@ -171,21 +165,40 @@ fn probe_based_sst_evap_calibration() {
|
||||
.solved_variables
|
||||
.iter()
|
||||
.find(|v| v.variable == "z_ua" && v.component.as_deref() == Some("evap"))
|
||||
.expect("solved_variables must contain cond/z_ua");
|
||||
.expect("solved_variables must contain evap/z_ua");
|
||||
eprintln!("DIAG z_ua résolu = {}", solved.value);
|
||||
eprintln!("DIAG cible SST = 277.55 K (41.85°C)");
|
||||
eprintln!("DIAG cible SST = 277.55 K (4.4°C)");
|
||||
|
||||
let mut sst_c = None;
|
||||
if let Some(state) = result.state.as_ref() {
|
||||
for e in state.iter() {
|
||||
if e.target.as_deref() == Some("evap") || e.source.as_deref() == Some("evap") {
|
||||
eprintln!("DIAG edge {}→{} P={}bar T_sat={}°C T={}°C",
|
||||
if e.source.as_deref() == Some("evap")
|
||||
|| e.target.as_deref() == Some("evap_sst_probe")
|
||||
|| e.source.as_deref() == Some("evap_sst_probe")
|
||||
{
|
||||
eprintln!(
|
||||
"DIAG edge {}→{} P={}bar T_sat={}°C T={}°C",
|
||||
e.source.as_deref().unwrap_or("?"),
|
||||
e.target.as_deref().unwrap_or("?"),
|
||||
e.pressure_bar,
|
||||
e.saturation_temperature_c.unwrap_or(f64::NAN),
|
||||
e.temperature_c.unwrap_or(f64::NAN));
|
||||
e.temperature_c.unwrap_or(f64::NAN)
|
||||
);
|
||||
if e.source.as_deref() == Some("evap_sst_probe")
|
||||
|| e.target.as_deref() == Some("evap_sst_probe")
|
||||
{
|
||||
if let Some(ts) = e.saturation_temperature_c {
|
||||
sst_c = Some(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let sst_k = sst_c.expect("must read SST from suction probe edge") + 273.15;
|
||||
assert!(
|
||||
(sst_k - 277.55).abs() < 0.5,
|
||||
"SST must hit target 277.55 K within 0.5 K, got {sst_k}"
|
||||
);
|
||||
assert!(
|
||||
solved.value > 0.05 && solved.value < 2.0,
|
||||
"z_ua must solve within bounds, got {}",
|
||||
|
||||
Reference in New Issue
Block a user