Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop. Co-authored-by: Cursor <cursoragent@cursor.com>
10 KiB
DoF Balance & Real-Machine Simulation Plan
Date: 2026-07-17
Status: Active implementation plan
Audience: architects + implementers
Related: docs/audits/heat_exchanger_architecture_audit.md, ADR-0001, mission HX
0. Non-negotiable principles
- A thermodynamic system is a square algebraic problem: [ n_{\text{equations}} = n_{\text{unknowns}} ]
- Fixing a quantity consumes one degree of freedom. Somewhere else an unknown must be freed (or an equation dropped). There is no free lunch.
- Parameters are not states. If a value is not in the Newton state vector, it is either (a) a true physical constant/input boundary, or (b) a cheat that must be labeled and forbidden in system mode.
- No bricolage. Silent clamping, fake residuals, scalar secondary streams that pretend to be a water loop, and design-point pressure pins are not “real machine” simulation.
- Hard gate before solve. Imbalance is an error, not a warning.
1. Diagnosis — what the codebase does today
1.1 State vector (unknowns)
Post CM1.4 layout (System::finalize):
[ ṁ_branch_0 … ṁ_branch_{B-1} | P_e0, h_e0, … P_e{|E|-1}, h_e{|E|-1}
| inverse_controls | coupling_Q | saturated_(u,x) pairs | free_actuators ]
- Series edges share one
ṁunknown per branch (topology.rs). - Full length:
full_state_vector_len().
1.2 Equations
n_eq = Σ component.n_equations()
+ n_constraints
+ n_thermal_couplings
+ 2 × n_saturated_controllers
Free actuators contribute unknowns only; the owning component supplies the
closing residual inside n_equations().
1.3 Existing Fix/Free mechanisms (good)
| Mechanism | Fix (equation) | Free (unknown) |
|---|---|---|
BrineSource P,h,(ṁ) |
Dirichlet on edge | — (boundary input) |
BrineSink P_back |
Dirichlet on P | h free (emergent outlet) |
Anchor + constraint |
+1 thermo spec | must free elsewhere |
add_constraint + BoundedVariable |
+1 measure residual | +1 control |
add_free_actuator |
component residual | +1 state slot |
superheat_regulated |
drops SH residual | EXV opening closes it |
| Emergent HX | outlet closure pins P via balance | design-point P removed from compressor/EXV |
1.4 Cheating patterns (forbidden for real-machine system solve)
| Cheat | Where | Why it is wrong |
|---|---|---|
Secondary as scalars T_sec, C_sec without edges |
FloodedEvaporator coupled path |
Water loop has no First-Law residual; h_sec,out never emerges |
target_quality = 0.7 + quality residual |
Flooded default | +1 equation without free actuator / level unknown |
Fake residual -target_quality when quality fails |
Flooded | Hides config error |
unwrap_or(inlet_p_idx) for mass index |
Cond/Evap/Flooded | Jacobian writes energy ∂/∂ṁ into pressure column |
| Design-point P pins (non-emergent) | compressor/EXV legacy | Pressures not free to respond to secondary |
p_in > 10_000 hard model switch |
several HX | Discontinuous Jacobian near seed |
| UI historically “hiding” secondary edges | obsolete HANDOFF claim | Must keep live edges in graph |
Rating rate(p) used as if system solve |
qualification APIs | OK only when labeled rating mode |
1.5 Honest water-cooled chiller budget (reference)
Ref loop: Comp ⇄ Cond ⇄ EXV ⇄ Evap → 1 ṁ + 4×(P,h) = 9
CW loop: BrineSrc → Cond.sec → Sink → 1 ṁ + 2×(P,h) = 5
CHW loop: BrineSrc → Evap.sec → Sink → 1 ṁ + 2×(P,h) = 5
Total unknowns = 19
Eqs (emergent, same-branch):
Comp 2 + Cond (3 thermo + 1 sec energy) 4 + EXV 1
+ Evap (3 + 1 sec) 4 + 2×(BrineSrc 3 + BrineSink 1) = 19
19 = 19. This is the template. Flooded + dual circuit must keep the same discipline.
2. Fix / Free ledger (the core abstraction)
2.1 Rules
USER FIX (non-boundary) ⇒ must FREE something OR DROP a closing residual
BOUNDARY FIX (source) ⇒ normal: physical input of the machine
OUTLET CLOSURE (SH/SC/x) ⇒ either free actuator elsewhere OR mode drop of that residual
Examples:
| User intent | Fix | Free / drop |
|---|---|---|
| Fixed chilled-water supply T | load-side T_set or capacity constraint | free compressor speed or free EXV / free ṁ_ref |
| Fixed superheat 5 K | SH residual OR anchor | free EXV opening (superheat_regulated + orifice) |
| Fixed flooded level | level residual | free EXV / charge related |
| Fixed circuit capacity | capacity constraint | free speed or free slide valve |
| Quality target without actuator | ILLEGAL | refuse at validate |
2.2 Equation roles (semantic)
pub enum EquationRole {
MassConservation { stream: StreamId },
MomentumOrPressureDrop { stream: StreamId },
EnergyBalance { stream: StreamId },
OutletClosure { kind: OutletClosureKind }, // SH | SC | Quality | Level
BoundaryDirichlet { quantity: StateQuantity }, // P | H | M
ActuatorClosure { name: String },
CouplingDuty,
ControlTracking { name: String },
Unspecified, // legacy only; must disappear over time
}
2.3 Unknown kinds
pub enum UnknownKind {
BranchMassFlow { branch_id: usize },
EdgePressure { edge: usize },
EdgeEnthalpy { edge: usize },
InverseControl { id: String },
CouplingHeat { index: usize },
SaturatedActuator { index: usize },
SaturatedIntegrator { index: usize },
FreeActuator { id: String },
}
2.4 System-level report
DofReport {
n_unknowns, n_equations, balance: Balanced | Over(k) | Under(k),
per_component: [(name, n_eq, roles)],
free_actuators, constraints, couplings,
diagnostics: ["FloodedEvaporator quality_control +1 eq without free actuator", ...]
}
Hard fail when balance != Balanced before Newton.
3. Implementation phases (ordered, each leaves workspace green)
Phase D0 — Ledger + hard gate ✅ done (2026-07-17)
- Study + this plan document
crates/solver/src/dof.rs—DofReport,SystemDofBalance, errorscrates/components/src/dof.rs—EquationRole+Component::equation_roles()System::dof_report()/System::validate_system_dof()/total_equation_count()- Hard fail in
finalize()on over-constrained (enforce_dof_gate, default on) - Full square check via
validate_system_dof()(production path) - Roles on FloodedEvaporator, Condenser, BrineSource/Sink, Anchor
- Unit tests:
crates/solver/tests/dof_balance.rs(balanced / over / under)
Phase D1 — FloodedEvaporator honesty ✅ core done (2026-07-17)
- 4-port live secondary via
set_port_context(ports 2/3),flow_paths=[(0,1),(2,3)] - Secondary energy residual
ṁ_sec(h_out−h_in) + Q = 0 coupled_readyrequires live secondary edges in system mode- Scalar
with_secondary_streamdocumented as rating-mode only - Remove mass→pressure index fallback; error if ṁ index missing
- Quality residual: return
Errif quality uncomputable (no fake residual) with_quality_controldocuments DoF cost- Equation roles complete for flooded
- CLI example: flooded + BrineSrc/Sink secondary loop
- Replace quality target with
FloodedEvaporatorControlenum
Phase D2 — Shared DoF diagnostics in CLI + UI ✅ done (2026-07-17)
- CLI
runhard-fails on non-squarevalidate_system_dofafter finalize SimulationResult.dofsummary (n_eq, n_unk, balance, ledger text)- CLI
validatebuilds system and reports DoF - Example
chiller_flooded_4port_watercooled.json - Web UI status bar: live eqs vs unk estimate
- Web UI Fix/Free badges on parameters + secondary wiring warnings
validateConfigrequires live secondary ports for 4-port HX
Phase D3 — Zero-flow safe residuals (linked to HX mission §7)
- Reuse
entropyk_core::smoothing - Activity blend with documented scales; Jacobian parity tests at m=0
Phase D4 — Multi-circuit shared vessel (ADR-0001)
- Solver contract for multi-circuit ports on one component
MultiCircuitFloodedEvaporatorwith N independent ref balances + shared secondary energy- OFF circuit: zero duty, fixed eq count, no singularity
Phase D5 — Geometry / correlations / product fixtures
- As HX mission Phases B–F, always under DoF ledger gate
- No geometry mode ships without equation/unknown inventory table
4. Component checklist (every new residual)
Before adding a residual equation:
- Name its
EquationRole. - State which unknown it closes (or which free it requires).
- Update
n_equations()and Jacobian. - Add a DoF unit test (balance before/after).
- Never add a residual “to make it converge”.
Before adding an unknown (actuator, coupling Q, …):
- Name its
UnknownKind. - Provide the closing residual (component or system).
- Bounds if physical actuator.
5. Acceptance criteria (robust machine simulation)
A configuration is accepted as real-machine system mode only if:
validate_system_dof()passes (hard)- Secondary fluids of every HX are live edges (or explicitly marked rating-only)
- Pressures of interest are emergent (no design-point pin unless documented fixed-P study)
- First Law closes at cycle level (
energy_transferssum ≈ 0 within tolerance) - No fake residual on missing properties
- Jacobian analytic except documented property derivatives
- Staging / zero flow keeps constant equation count
6. Relation to inverse control
Inverse (Eurovent / calibration) already has a partial DoF check. It is incomplete
(misses free actuators, couplings, saturated controllers). The new
validate_system_dof supersedes it as the single square-system gate; inverse
mapping remains the Fix/Free pairing API for user constraints.
7. Immediate deliverables (this implementation slice)
- This plan document.
solver::dofmodule + public API.- Hard DoF validation wired to
System. - Flooded evaporator path toward live secondary (D1 start).
- Tests proving overconstrained quality control is detected.
Last updated: 2026-07-17