# 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 1. **A thermodynamic system is a square algebraic problem:** \[ n_{\text{equations}} = n_{\text{unknowns}} \] 2. **Fixing a quantity consumes one degree of freedom.** Somewhere else an unknown must be freed (or an equation dropped). There is no free lunch. 3. **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. 4. **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. 5. **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`): ```text [ ṁ_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 ```text 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) ```text 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 ```text 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) ```rust 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 ```rust 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 ```text 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) - [x] Study + this plan document - [x] `crates/solver/src/dof.rs` — `DofReport`, `SystemDofBalance`, errors - [x] `crates/components/src/dof.rs` — `EquationRole` + `Component::equation_roles()` - [x] `System::dof_report()` / `System::validate_system_dof()` / `total_equation_count()` - [x] Hard fail in `finalize()` on **over-constrained** (`enforce_dof_gate`, default on) - [x] Full square check via `validate_system_dof()` (production path) - [x] Roles on FloodedEvaporator, Condenser, BrineSource/Sink, Anchor - [x] Unit tests: `crates/solver/tests/dof_balance.rs` (balanced / over / under) ### Phase D1 — FloodedEvaporator honesty ✅ core done (2026-07-17) - [x] 4-port live secondary via `set_port_context` (ports 2/3), `flow_paths=[(0,1),(2,3)]` - [x] Secondary energy residual `ṁ_sec(h_out−h_in) + Q = 0` - [x] `coupled_ready` requires live secondary edges in **system mode** - [x] Scalar `with_secondary_stream` documented as **rating-mode only** - [x] Remove mass→pressure index fallback; error if ṁ index missing - [x] Quality residual: return `Err` if quality uncomputable (no fake residual) - [x] `with_quality_control` documents DoF cost - [x] Equation roles complete for flooded - [ ] CLI example: flooded + BrineSrc/Sink secondary loop - [ ] Replace quality target with `FloodedEvaporatorControl` enum ### Phase D2 — Shared DoF diagnostics in CLI + UI ✅ done (2026-07-17) - [x] CLI `run` hard-fails on non-square `validate_system_dof` after finalize - [x] `SimulationResult.dof` summary (n_eq, n_unk, balance, ledger text) - [x] CLI `validate` builds system and reports DoF - [x] Example `chiller_flooded_4port_watercooled.json` - [x] Web UI status bar: live eqs vs unk estimate - [x] Web UI Fix/Free badges on parameters + secondary wiring warnings - [x] `validateConfig` requires 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 - [ ] `MultiCircuitFloodedEvaporator` with 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: 1. Name its `EquationRole`. 2. State which unknown it closes (or which free it requires). 3. Update `n_equations()` and Jacobian. 4. Add a DoF unit test (balance before/after). 5. Never add a residual “to make it converge”. Before adding an unknown (actuator, coupling Q, …): 1. Name its `UnknownKind`. 2. Provide the closing residual (component or system). 3. 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_transfers` sum ≈ 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) 1. This plan document. 2. `solver::dof` module + public API. 3. Hard DoF validation wired to `System`. 4. Flooded evaporator path toward live secondary (D1 start). 5. Tests proving overconstrained quality control is detected. --- **Last updated:** 2026-07-17