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>
208 lines
6.3 KiB
Rust
208 lines
6.3 KiB
Rust
//! Probe Fixed Tsat on suction line + free `evap/z_ua` (model embedding).
|
|
//!
|
|
//! 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_sst_calib.json");
|
|
std::fs::write(&path, json).unwrap();
|
|
run_simulation(&path, None, false).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn probe_based_sst_evap_calibration() {
|
|
let json = r#"
|
|
{
|
|
"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": [
|
|
{
|
|
"id": 0,
|
|
"name": "Refrigerant + water loops",
|
|
"components": [
|
|
{
|
|
"type": "IsentropicCompressor",
|
|
"name": "comp",
|
|
"isentropic_efficiency": 0.7,
|
|
"t_cond_k": 318.15,
|
|
"t_evap_k": 278.15,
|
|
"superheat_k": 5.0,
|
|
"emergent_pressure": true,
|
|
"displacement_m3": 6.5e-05,
|
|
"speed_hz": 50.0,
|
|
"volumetric_efficiency": 0.92
|
|
},
|
|
{
|
|
"type": "BphxCondenser",
|
|
"name": "cond",
|
|
"refrigerant": "R134a",
|
|
"secondary_fluid": "Water",
|
|
"n_plates": 40,
|
|
"plate_length_m": 0.4,
|
|
"plate_width_m": 0.12,
|
|
"target_subcooling_k": 5.0,
|
|
"emergent_pressure": true,
|
|
"correlation": "Longo2004",
|
|
"dp_correlation": "SimplifiedChannel",
|
|
"ua": 2500.0
|
|
},
|
|
{
|
|
"type": "Probe",
|
|
"name": "evap_sst_probe",
|
|
"measure": "SST",
|
|
"fluid": "R134a"
|
|
},
|
|
{
|
|
"type": "IsenthalpicExpansionValve",
|
|
"name": "exv",
|
|
"t_evap_k": 278.15,
|
|
"emergent_pressure": true
|
|
},
|
|
{
|
|
"type": "BphxEvaporator",
|
|
"name": "evap",
|
|
"refrigerant": "R134a",
|
|
"secondary_fluid": "Water",
|
|
"n_plates": 40,
|
|
"plate_length_m": 0.4,
|
|
"plate_width_m": 0.12,
|
|
"target_superheat_k": 5.0,
|
|
"emergent_pressure": true,
|
|
"correlation": "Longo2004",
|
|
"dp_correlation": "SimplifiedChannel",
|
|
"ua": 2000.0,
|
|
"z_ua": 1.0
|
|
},
|
|
{
|
|
"type": "BrineSource",
|
|
"name": "cond_water_in",
|
|
"fluid": "Water",
|
|
"p_set_bar": 2.0,
|
|
"t_set_c": 30.0,
|
|
"m_flow_kg_s": 0.4,
|
|
"fix_pressure": false,
|
|
"fix_temperature": true,
|
|
"fix_mass_flow": true
|
|
},
|
|
{
|
|
"type": "BrineSink",
|
|
"name": "cond_water_out",
|
|
"fluid": "Water",
|
|
"p_back_bar": 2.0,
|
|
"fix_pressure": true
|
|
},
|
|
{
|
|
"type": "BrineSource",
|
|
"name": "evap_water_in",
|
|
"fluid": "Water",
|
|
"p_set_bar": 3.0,
|
|
"t_set_c": 12.0,
|
|
"m_flow_kg_s": 0.5,
|
|
"fix_pressure": false,
|
|
"fix_temperature": true,
|
|
"fix_mass_flow": true
|
|
},
|
|
{
|
|
"type": "BrineSink",
|
|
"name": "evap_water_out",
|
|
"fluid": "Water",
|
|
"p_back_bar": 3.0,
|
|
"fix_pressure": true
|
|
}
|
|
],
|
|
"edges": [
|
|
{ "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": "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" },
|
|
{ "from": "evap:secondary_outlet","to": "evap_water_out:inlet" }
|
|
]
|
|
}
|
|
],
|
|
"solver": {
|
|
"strategy": "fallback",
|
|
"max_iterations": 300,
|
|
"tolerance": 1e-06,
|
|
"timeout_ms": 60000
|
|
},
|
|
"embeddings": [
|
|
{
|
|
"id": "emb_evap_z_ua",
|
|
"unknown": {
|
|
"component": "evap",
|
|
"factor": "z_ua",
|
|
"start": 0.3,
|
|
"min": 0.05,
|
|
"max": 2.0
|
|
},
|
|
"equation": {
|
|
"component": "evap_sst_probe",
|
|
"output": "saturationTemperature",
|
|
"value": 277.55
|
|
}
|
|
}
|
|
]
|
|
}
|
|
"#;
|
|
let result = run_config(json);
|
|
assert!(
|
|
matches!(result.status, SimulationStatus::Converged),
|
|
"Probe-based SST calibration must converge: {:?} ({:?})",
|
|
result.status,
|
|
result.error
|
|
);
|
|
let solved = result
|
|
.solved_variables
|
|
.iter()
|
|
.find(|v| v.variable == "z_ua" && v.component.as_deref() == Some("evap"))
|
|
.expect("solved_variables must contain evap/z_ua");
|
|
eprintln!("DIAG z_ua résolu = {}", solved.value);
|
|
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.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)
|
|
);
|
|
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 {}",
|
|
solved.value
|
|
);
|
|
}
|