Snapshot WIP: Probe calibration path, faer LU backend, and BPHX phase-change duty.
Some checks failed
CI / check (push) Has been cancelled
Some checks failed
CI / check (push) Has been cancelled
Checkpoint incomplete calibration work (cond SDT green, evap SST failing) plus related solver/UI changes so the next pass can fix and extend safely. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -315,7 +315,8 @@ fn process_single_file(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,7 +426,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "test2.json".to_string(),
|
||||
@@ -439,7 +441,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -483,7 +486,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "test2.json".to_string(),
|
||||
@@ -497,7 +501,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -529,7 +534,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "test2.json".to_string(),
|
||||
@@ -543,7 +549,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -571,7 +578,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "test2.json".to_string(),
|
||||
@@ -585,7 +593,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 1000,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -643,7 +652,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 100,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -667,7 +677,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "ok2.json".to_string(),
|
||||
@@ -681,7 +692,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 60,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "fail.json".to_string(),
|
||||
@@ -695,7 +707,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -452,6 +452,9 @@ pub struct SolverConfig {
|
||||
/// Solver strategy: "newton" or "picard".
|
||||
#[serde(default = "default_solver_strategy")]
|
||||
pub strategy: String,
|
||||
/// Dense linear backend: "nalgebra" (default) or "faer" (Story 1.5 opt-in).
|
||||
#[serde(default = "default_linear_backend")]
|
||||
pub linear_backend: String,
|
||||
/// Maximum iterations.
|
||||
#[serde(default = "default_max_iterations")]
|
||||
pub max_iterations: usize,
|
||||
@@ -470,6 +473,10 @@ fn default_solver_strategy() -> String {
|
||||
"newton".to_string()
|
||||
}
|
||||
|
||||
fn default_linear_backend() -> String {
|
||||
"nalgebra".to_string()
|
||||
}
|
||||
|
||||
fn default_max_iterations() -> usize {
|
||||
100
|
||||
}
|
||||
@@ -482,6 +489,7 @@ impl Default for SolverConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
strategy: default_solver_strategy(),
|
||||
linear_backend: default_linear_backend(),
|
||||
max_iterations: default_max_iterations(),
|
||||
tolerance: default_tolerance(),
|
||||
timeout_ms: 0,
|
||||
|
||||
@@ -372,7 +372,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -407,7 +408,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -717,7 +719,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -737,7 +740,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -782,7 +786,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -823,7 +828,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -867,7 +873,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -887,7 +894,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -957,7 +965,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -983,7 +992,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
let find_component = |name: &str| {
|
||||
config
|
||||
@@ -1042,7 +1052,63 @@ fn execute_simulation(
|
||||
|
||||
// Register declared control loops (saturated-PI, co-solved). Must happen
|
||||
// BEFORE finalize() so the actuator is wired to the component's CalibIndices.
|
||||
//
|
||||
// Routing (calibration redesign): controls whose actuator is a z-factor
|
||||
// (z_flow/z_flow_eco/z_dp/z_ua/z_power/z_etav) go through PLAIN inverse
|
||||
// embedding (+1 residual measured−target, +1 unknown z) — not the
|
||||
// SaturatedController (2+2 with integrator, meant for physical actuators).
|
||||
let control_error = |msg: String| SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(msg),
|
||||
failure_diagnostics: None,
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
for control in &config.controls {
|
||||
let factor = control.actuator.factor.trim();
|
||||
// Plain embedding applies to single-point z-factor calibration only.
|
||||
// A control WITH `objectives` is a supervisory override network
|
||||
// (selector semantics) and always keeps the SaturatedController path,
|
||||
// regardless of the actuator factor.
|
||||
if entropyk_core::normalize_factor_name(factor).is_some() && control.objectives.is_empty() {
|
||||
match build_plain_z_embedding(control, factor) {
|
||||
Ok((constraint, bounded_var, actuator_id)) => {
|
||||
if let Err(e) = system.add_constraint(constraint) {
|
||||
return control_error(format!(
|
||||
"Failed to add constraint for control '{}': {:?}",
|
||||
control.id, e
|
||||
));
|
||||
}
|
||||
if let Err(e) = system.add_bounded_variable(bounded_var) {
|
||||
return control_error(format!(
|
||||
"Failed to add actuator for control '{}': {:?}",
|
||||
control.id, e
|
||||
));
|
||||
}
|
||||
if let Err(e) = system.link_constraint_to_control(
|
||||
&entropyk_solver::inverse::ConstraintId::new(control.id.clone()),
|
||||
&actuator_id,
|
||||
) {
|
||||
return control_error(format!(
|
||||
"Failed to link control '{}': {:?}",
|
||||
control.id, e
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(msg) => {
|
||||
return control_error(format!("Invalid control '{}': {}", control.id, msg));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match build_saturated_control(control) {
|
||||
Ok((bounded_var, controller)) => {
|
||||
if let Err(e) = system.add_bounded_variable(bounded_var) {
|
||||
@@ -1061,7 +1127,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
system.add_saturated_controller(controller);
|
||||
@@ -1079,7 +1146,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1253,7 +1321,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
system.add_free_actuator(var_id);
|
||||
@@ -1275,7 +1344,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1296,7 +1366,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1524,10 +1595,35 @@ fn execute_simulation(
|
||||
initialization_diagnostics: None,
|
||||
dof: Some(dof_summary),
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
// Dense linear backend dispatch (Story 1.5): "nalgebra" (default) or
|
||||
// "faer"; env ENTROPYK_LINEAR_BACKEND overrides for the test harness.
|
||||
if !matches!(config.solver.linear_backend.as_str(), "nalgebra" | "faer") {
|
||||
return SimulationResult {
|
||||
input: input_name.to_string(),
|
||||
status: SimulationStatus::Error,
|
||||
convergence: None,
|
||||
iterations: None,
|
||||
state: None,
|
||||
performance: None,
|
||||
error: Some(format!(
|
||||
"Unsupported linear backend '{}'. Use 'nalgebra' or 'faer'.",
|
||||
config.solver.linear_backend
|
||||
)),
|
||||
failure_diagnostics: None,
|
||||
initialization_diagnostics: None,
|
||||
dof: Some(dof_summary),
|
||||
elapsed_ms,
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
}
|
||||
entropyk_solver::linear::set_linear_backend_override(Some(&config.solver.linear_backend));
|
||||
|
||||
// Build + run the configured strategy for a single solve. CLI
|
||||
// `solver.max_iterations` / `tolerance` are now honoured by every stage
|
||||
// (previously the defaults capped Newton at 100 iterations). Reused by the
|
||||
@@ -1799,7 +1895,8 @@ fn execute_simulation(
|
||||
initialization_diagnostics: Some(initialization_diagnostics),
|
||||
dof: Some(dof_summary),
|
||||
elapsed_ms,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2968,9 +3065,8 @@ fn bphx_dp_correlation_from_params(
|
||||
"{component_name}: dp_correlation must be a string (got {value})"
|
||||
)));
|
||||
};
|
||||
BphxDpCorrelation::parse(raw).map_err(|msg| {
|
||||
CliError::Config(format!("{component_name}: {msg}"))
|
||||
})
|
||||
BphxDpCorrelation::parse(raw)
|
||||
.map_err(|msg| CliError::Config(format!("{component_name}: {msg}")))
|
||||
}
|
||||
|
||||
/// Extract calibration Z-factors for BphxEvaporator/BphxCondenser from JSON params.
|
||||
@@ -3134,6 +3230,43 @@ fn parse_component_output(
|
||||
}
|
||||
|
||||
/// Builds a bounded actuator + saturated controller from a control config.
|
||||
/// Plain inverse embedding for z-factor calibration controls (calibration
|
||||
/// redesign, WS-2): one `Constraint` (measured − target) + one `BoundedVariable`
|
||||
/// (the z-factor), linked 1:1. The bounded var id reuses the
|
||||
/// `{component}__{z_factor}` convention so `finalize()` wires it to the
|
||||
/// component's matching `CalibIndices` slot (system.rs).
|
||||
fn build_plain_z_embedding(
|
||||
control: &crate::config::ControlConfig,
|
||||
factor: &str,
|
||||
) -> Result<
|
||||
(
|
||||
entropyk_solver::inverse::Constraint,
|
||||
entropyk_solver::inverse::BoundedVariable,
|
||||
entropyk_solver::inverse::BoundedVariableId,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId, Constraint, ConstraintId};
|
||||
|
||||
let output = parse_component_output(&control.measure.component, &control.measure.output)?;
|
||||
let actuator_id =
|
||||
BoundedVariableId::new(saturated_actuator_id(&control.actuator.component, factor));
|
||||
let bounded_var = BoundedVariable::with_component(
|
||||
actuator_id.clone(),
|
||||
&control.actuator.component,
|
||||
control.actuator.initial,
|
||||
control.actuator.min,
|
||||
control.actuator.max,
|
||||
)
|
||||
.map_err(|e| format!("invalid actuator bounds: {e:?}"))?;
|
||||
let constraint = Constraint::new(
|
||||
ConstraintId::new(control.id.clone()),
|
||||
output,
|
||||
control.target,
|
||||
);
|
||||
Ok((constraint, bounded_var, actuator_id))
|
||||
}
|
||||
|
||||
fn build_saturated_control(
|
||||
control: &crate::config::ControlConfig,
|
||||
) -> Result<
|
||||
@@ -5249,6 +5382,55 @@ fn create_component(
|
||||
Ok(Box::new(load))
|
||||
}
|
||||
|
||||
"Probe" => {
|
||||
use entropyk::{ComponentFluidId, Enthalpy, Port, Probe, ProbeMeasure, Pressure};
|
||||
// Calibration-redesign measurement tap (zero-residual, 2-port splice).
|
||||
// The user drops it onto a wire (UI `insertOnEdge`) and picks a
|
||||
// measure kind; the freed z-factor on a calibrated component
|
||||
// elsewhere is paired with this Probe via a calibration control
|
||||
// whose `measure.component` names the Probe.
|
||||
let measure_str = params
|
||||
.get("measure")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("SH");
|
||||
let measure = ProbeMeasure::parse(measure_str).map_err(CliError::Config)?;
|
||||
|
||||
let fluid = params
|
||||
.get("fluid")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| _primary_fluid.as_str())
|
||||
.to_string();
|
||||
|
||||
// Nominal edge state for the port pair (mirrors Pipe's idiom —
|
||||
// both pairs carry identical nominal values, the live solver state
|
||||
// overrides them via `set_system_context`).
|
||||
let p_bar = params.get("p_bar").and_then(|v| v.as_f64()).unwrap_or(5.0);
|
||||
let h_j = params
|
||||
.get("h_j_kg")
|
||||
.and_then(|v| v.as_f64())
|
||||
.or_else(|| {
|
||||
params
|
||||
.get("h_kj_kg")
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|k| k * 1000.0)
|
||||
})
|
||||
.unwrap_or(250_000.0);
|
||||
|
||||
let mk = || {
|
||||
Port::new(
|
||||
ComponentFluidId::new(&fluid),
|
||||
Pressure::from_bar(p_bar),
|
||||
Enthalpy::from_joules_per_kg(h_j),
|
||||
)
|
||||
};
|
||||
let probe = Probe::new(measure, &fluid, mk(), mk())
|
||||
.with_fluid_backend(backend)
|
||||
.connect(mk(), mk())
|
||||
.map_err(CliError::Component)?;
|
||||
Ok(Box::new(probe))
|
||||
}
|
||||
|
||||
"Anchor" | "RefrigerantNode" => {
|
||||
use entropyk::{Anchor, AnchorConstraint};
|
||||
// BOLT `BoundaryNode.Refrigerant.Node` equivalent: inline spec node
|
||||
@@ -5362,7 +5544,7 @@ fn create_component(
|
||||
}
|
||||
|
||||
_ => Err(CliError::Config(format!(
|
||||
"Unknown component type: '{}'. Supported: IsentropicCompressor, IsenthalpicExpansionValve, ScrewEconomizerCompressor, CentrifugalCompressor, CapillaryTube, MchxCondenserCoil, FloodedEvaporator, BphxEvaporator, BphxCondenser, FreeCoolingExchanger, Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, ReversingValve, Pump, Fan, Pipe, RefrigerantPipe, WaterPipe, AirDuct, Placeholder",
|
||||
"Unknown component type: '{}'. Supported: IsentropicCompressor, IsenthalpicExpansionValve, ScrewEconomizerCompressor, CentrifugalCompressor, CapillaryTube, MchxCondenserCoil, FloodedEvaporator, BphxEvaporator, BphxCondenser, FreeCoolingExchanger, Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, ReversingValve, Pump, Fan, Pipe, RefrigerantPipe, WaterPipe, AirDuct, Probe, Anchor, Placeholder",
|
||||
component_type
|
||||
))),
|
||||
}
|
||||
@@ -5672,7 +5854,8 @@ mod tests {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&result).unwrap();
|
||||
@@ -5839,10 +6022,8 @@ mod tests {
|
||||
.unwrap(),
|
||||
BphxDpCorrelation::SimplifiedChannel
|
||||
);
|
||||
let params = std::collections::HashMap::from([(
|
||||
"DpCorrelation".to_string(),
|
||||
json!("Martin1996"),
|
||||
)]);
|
||||
let params =
|
||||
std::collections::HashMap::from([("DpCorrelation".to_string(), json!("Martin1996"))]);
|
||||
assert_eq!(
|
||||
bphx_dp_correlation_from_params(¶ms, "BphxCondenser").unwrap(),
|
||||
BphxDpCorrelation::Martin1996
|
||||
@@ -5853,10 +6034,8 @@ mod tests {
|
||||
fn test_bphx_dp_correlation_rejects_unknown() {
|
||||
use serde_json::json;
|
||||
|
||||
let params = std::collections::HashMap::from([(
|
||||
"dp_correlation".to_string(),
|
||||
json!("Amalfi2016"),
|
||||
)]);
|
||||
let params =
|
||||
std::collections::HashMap::from([("dp_correlation".to_string(), json!("Amalfi2016"))]);
|
||||
let error = bphx_dp_correlation_from_params(¶ms, "BphxEvaporator").unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("unsupported dp_correlation"),
|
||||
@@ -5868,15 +6047,9 @@ mod tests {
|
||||
fn test_bphx_dp_correlation_rejects_non_string() {
|
||||
use serde_json::json;
|
||||
|
||||
let params = std::collections::HashMap::from([(
|
||||
"dp_correlation".to_string(),
|
||||
json!(42),
|
||||
)]);
|
||||
let params = std::collections::HashMap::from([("dp_correlation".to_string(), json!(42))]);
|
||||
let error = bphx_dp_correlation_from_params(¶ms, "BphxEvaporator").unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("must be a string"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(error.to_string().contains("must be a string"), "{error}");
|
||||
}
|
||||
|
||||
fn comp_from_json(v: serde_json::Value) -> crate::config::ComponentConfig {
|
||||
|
||||
@@ -92,7 +92,8 @@ fn test_simulation_result_statuses() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "fail.json".to_string(),
|
||||
@@ -106,7 +107,8 @@ fn test_simulation_result_statuses() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "timeout.json".to_string(),
|
||||
@@ -120,7 +122,8 @@ fn test_simulation_result_statuses() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 1000,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -163,7 +166,8 @@ fn test_batch_aggregator_csv_output() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 150,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "scenario2.json".to_string(),
|
||||
@@ -183,7 +187,8 @@ fn test_batch_aggregator_csv_output() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 200,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "scenario3.json".to_string(),
|
||||
@@ -197,7 +202,8 @@ fn test_batch_aggregator_csv_output() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -229,7 +235,8 @@ fn test_batch_aggregator_json_summary() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "test2.json".to_string(),
|
||||
@@ -243,7 +250,8 @@ fn test_batch_aggregator_json_summary() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 75,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
SimulationResult {
|
||||
input: "test3.json".to_string(),
|
||||
@@ -257,7 +265,8 @@ fn test_batch_aggregator_json_summary() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 5000,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -322,7 +331,8 @@ fn test_batch_summary_csv_with_convergence() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 300,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
}];
|
||||
|
||||
let summary = BatchSummary {
|
||||
|
||||
190
crates/cli/tests/calibration_sdt.rs
Normal file
190
crates/cli/tests/calibration_sdt.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
//! Calibration redesign (WS-2/WS-3/WS-4) — AC#1: BPHX condenser SDT
|
||||
//! calibration via plain inverse embedding converges and back-solves z_ua
|
||||
//! (the case that diverged with a singular Jacobian before the redesign).
|
||||
|
||||
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("bphx_sdt_calib.json");
|
||||
std::fs::write(&path, json).unwrap();
|
||||
run_simulation(&path, None, false).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bphx_condenser_sdt_calibration_converges_and_solves_z_ua() {
|
||||
let json = r#"
|
||||
{
|
||||
"name": "BPHX Water-Cooled Chiller R134a",
|
||||
"description": "Full vapor-compression cycle with brazed-plate evaporator and condenser (4-port). Both Bphx units use emergent_pressure so outlet closures (superheat / subcooling) square the DoF like Condenser/Evaporator.",
|
||||
"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": "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
|
||||
},
|
||||
{
|
||||
"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": "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
|
||||
},
|
||||
"controls": [
|
||||
{
|
||||
"type": "SaturatedController",
|
||||
"id": "sdt_calib",
|
||||
"measure": {
|
||||
"component": "cond",
|
||||
"output": "saturationTemperature"
|
||||
},
|
||||
"actuator": {
|
||||
"component": "cond",
|
||||
"factor": "z_ua",
|
||||
"initial": 0.3,
|
||||
"min": 0.05,
|
||||
"max": 2.0
|
||||
},
|
||||
"target": 315.0
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let result = run_config(json);
|
||||
assert!(
|
||||
matches!(result.status, SimulationStatus::Converged),
|
||||
"BPHX SDT calibration must converge (was the singular-Jacobian case): {:?} ({:?})",
|
||||
result.status,
|
||||
result.error
|
||||
);
|
||||
let solved = result
|
||||
.solved_variables
|
||||
.iter()
|
||||
.find(|v| v.variable == "z_ua" && v.component.as_deref() == Some("cond"))
|
||||
.expect("solved_variables must contain cond/z_ua");
|
||||
assert!(
|
||||
solved.value > 0.05 && solved.value < 2.0,
|
||||
"z_ua must solve within bounds, got {}",
|
||||
solved.value
|
||||
);
|
||||
}
|
||||
193
crates/cli/tests/probe_calibration.rs
Normal file
193
crates/cli/tests/probe_calibration.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
//! 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.
|
||||
|
||||
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");
|
||||
std::fs::write(&path, json).unwrap();
|
||||
run_simulation(&path, None, false).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_based_sdt_calibration_converges_and_solves_z_ua() {
|
||||
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.",
|
||||
"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": "cond_sdt_probe",
|
||||
"measure": "SDT",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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_sdt_probe:inlet" },
|
||||
{ "from": "cond_sdt_probe:outlet","to": "cond:inlet" },
|
||||
{ "from": "cond:outlet", "to": "exv:inlet" },
|
||||
{ "from": "exv:outlet", "to": "evap:inlet" },
|
||||
{ "from": "evap: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
|
||||
},
|
||||
"controls": [
|
||||
{
|
||||
"type": "SaturatedController",
|
||||
"id": "probe_sdt_calib",
|
||||
"measure": {
|
||||
"component": "cond_sdt_probe",
|
||||
"output": "saturationTemperature"
|
||||
},
|
||||
"actuator": {
|
||||
"component": "cond",
|
||||
"factor": "z_ua",
|
||||
"initial": 0.3,
|
||||
"min": 0.05,
|
||||
"max": 2.0
|
||||
},
|
||||
"target": 315.0
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let result = run_config(json);
|
||||
assert!(
|
||||
matches!(result.status, SimulationStatus::Converged),
|
||||
"Probe-based SDT calibration must converge: {:?} ({:?})",
|
||||
result.status,
|
||||
result.error
|
||||
);
|
||||
let solved = result
|
||||
.solved_variables
|
||||
.iter()
|
||||
.find(|v| v.variable == "z_ua" && v.component.as_deref() == Some("cond"))
|
||||
.expect("solved_variables must contain cond/z_ua");
|
||||
eprintln!("DIAG z_ua résolu = {}", solved.value);
|
||||
eprintln!("DIAG cible SDT = 315.0 K (41.85°C)");
|
||||
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") {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
solved.value > 0.05 && solved.value < 2.0,
|
||||
"z_ua must solve within bounds, got {}",
|
||||
solved.value
|
||||
);
|
||||
}
|
||||
194
crates/cli/tests/probe_sst_evap.rs
Normal file
194
crates/cli/tests/probe_sst_evap.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! 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.
|
||||
|
||||
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");
|
||||
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 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.",
|
||||
"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": "SDT",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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": "exv:outlet", "to": "evap_sst_probe:inlet" },
|
||||
{ "from": "evap_sst_probe: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
|
||||
},
|
||||
"controls": [
|
||||
{
|
||||
"type": "SaturatedController",
|
||||
"id": "probe_sdt_calib",
|
||||
"measure": {
|
||||
"component": "evap_sst_probe",
|
||||
"output": "saturationTemperature"
|
||||
},
|
||||
"actuator": {
|
||||
"component": "evap",
|
||||
"factor": "z_ua",
|
||||
"initial": 0.3,
|
||||
"min": 0.05,
|
||||
"max": 2.0
|
||||
},
|
||||
"target": 277.55
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let result = run_config(json);
|
||||
assert!(
|
||||
matches!(result.status, SimulationStatus::Converged),
|
||||
"Probe-based SDT 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 cond/z_ua");
|
||||
eprintln!("DIAG z_ua résolu = {}", solved.value);
|
||||
eprintln!("DIAG cible SST = 277.55 K (41.85°C)");
|
||||
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",
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
solved.value > 0.05 && solved.value < 2.0,
|
||||
"z_ua must solve within bounds, got {}",
|
||||
solved.value
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,8 @@ fn test_simulation_result_serialization() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 50,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&result).unwrap();
|
||||
@@ -70,7 +71,8 @@ fn test_error_result_serialization() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
@@ -96,7 +98,8 @@ fn test_error_result_serializes_failure_diagnostics() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
@@ -2303,7 +2306,8 @@ fn test_structural_failure_serializes_without_diagnostics() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 0,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
@@ -2348,7 +2352,8 @@ fn test_success_result_does_not_include_failure_diagnostics() {
|
||||
initialization_diagnostics: None,
|
||||
dof: None,
|
||||
elapsed_ms: 120,
|
||||
raw_state_vector: None, solved_variables: Vec::new(),
|
||||
raw_state_vector: None,
|
||||
solved_variables: Vec::new(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user