# 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
