Snapshot WIP: Probe calibration path, faer LU backend, and BPHX phase-change duty.
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:
2026-07-19 21:44:01 +02:00
parent 2f1f7ecb80
commit 3808e0f11b
39 changed files with 3648 additions and 265 deletions

View File

@@ -115,10 +115,11 @@ pub use entropyk_components::{
IncompressibleSplitter, IsenthalpicExpansionValve, IsentropicCompressor, JacobianBuilder,
LmtdModel, MchxCondenserCoil, MockExternalModel, Node, NodeMeasurements, NodePhase,
OperationalState, PerformanceCurves, PhaseRegion, Pipe, PipeGeometry, Polynomial1D,
Polynomial2D, Pump, PumpCurves, RefrigerantSink, RefrigerantSource, RegistryError,
ResidualVector, ScrewEconomizerCompressor, ScrewPerformanceCurves, ShellAndTubeHx,
SstSdtCoefficients, StateHistory, StateManageable, StateTransitionError, SystemState,
ThermalLoad, ThreadSafeExternalModel, UaMode, ValveCharacteristics, ValveFlowModel,
Polynomial2D, Probe, ProbeMeasure, Pump, PumpCurves, RefrigerantSink, RefrigerantSource,
RegistryError, ResidualVector, ScrewEconomizerCompressor, ScrewPerformanceCurves,
ShellAndTubeHx, SstSdtCoefficients, StateHistory, StateManageable, StateTransitionError,
SystemState, ThermalLoad, ThreadSafeExternalModel, UaMode, ValveCharacteristics,
ValveFlowModel,
};
pub use entropyk_components::{ReversingMode, ReversingValve};
@@ -147,11 +148,11 @@ pub use entropyk_solver::{
AddEdgeError, AntoineCoefficients, CircuitConvergence, CircuitId as SolverCircuitId,
ComponentOutput, Constraint, ConstraintError, ConstraintId, ConvergedState,
ConvergenceCriteria, ConvergenceReason, ConvergenceReport, ConvergenceStatus, CyclePerformance,
DomainViolation, FallbackConfig, FallbackSolver, FlowEdge, HomotopyConfig, InitializerConfig,
InitializerError, JacobianFreezingConfig, JacobianMatrix, LinearSolver, MacroComponent,
MacroComponentSnapshot, NalgebraLuSolver, NewtonConfig, PicardConfig, PortMapping,
SmartInitializer, SolveOutcome, Solver, SolverError, SolverStrategy, System, ThermalCoupling,
TimeoutConfig, TopologyError,
DomainViolation, FaerLuSolver, FallbackConfig, FallbackSolver, FlowEdge, HomotopyConfig,
InitializerConfig, InitializerError, JacobianFreezingConfig, JacobianMatrix, LinearSolver,
MacroComponent, MacroComponentSnapshot, NalgebraLuSolver, NewtonConfig, PicardConfig,
PortMapping, SmartInitializer, SolveOutcome, Solver, SolverError, SolverStrategy, System,
ThermalCoupling, TimeoutConfig, TopologyError,
};
// =============================================================================

View File

@@ -526,6 +526,42 @@ pub fn extract_solved_variables(system: &System, state: &[f64]) -> Vec<SolvedVar
}
}
// Calibration factors (z_ua, z_dp, z_flow, z_flow_eco, z_power, z_etav)
// promoted to free unknowns via control loops. Each `Some(idx)` slot means
// the solver computed a value for that factor at `state[idx]`. The generic
// `actuator` slot (physical opening / fan speed) is already covered by the
// bounded-variable path above, so we only surface the six named Z-factors.
for (name, indices) in system.calib_indices_by_name() {
for (factor, slot) in [
("z_flow", indices.z_flow),
("z_flow_eco", indices.z_flow_eco),
("z_dp", indices.z_dp),
("z_ua", indices.z_ua),
("z_power", indices.z_power),
("z_etav", indices.z_etav),
] {
if let Some(idx) = slot {
if idx < state.len() {
let id = format!("{}__{}", name, factor);
// Skip if already emitted by the bounded-variable path.
if out.iter().any(|v| v.id == id) {
continue;
}
out.push(SolvedVariable {
id,
component: Some(name.clone()),
variable: factor.to_string(),
value: state[idx],
// Calibration factors are bounded to [0.5, 2.0]
// (see entropyk_core::CalibValidationError).
min: 0.5,
max: 2.0,
});
}
}
}
}
if skipped > 0 {
tracing::debug!(
skipped,
@@ -547,9 +583,7 @@ pub fn extract_solved_variables(system: &System, state: &[f64]) -> Vec<SolvedVar
/// user-facing `"opening"` label.
fn derive_solved_variable_label(id: &str, component_id: Option<&str>) -> String {
let stripped = match component_id {
Some(c) => id
.strip_prefix(&format!("{}__", c))
.unwrap_or(id),
Some(c) => id.strip_prefix(&format!("{}__", c)).unwrap_or(id),
None => id.rfind("__").map(|i| &id[i + 2..]).unwrap_or(id),
};
match stripped {
@@ -781,6 +815,9 @@ mod tests {
// Component unknown → fall back to last "__"-separated segment.
assert_eq!(derive_solved_variable_label("glob__z_dp", None), "z_dp");
// No separator and no component → id itself.
assert_eq!(derive_solved_variable_label("global_var", None), "global_var");
assert_eq!(
derive_solved_variable_label("global_var", None),
"global_var"
);
}
}