# 2026-04-16 — controllers scaffold and ctrl_operation rough-out

First continuous controller. Established the controller contract, refactored
the plant to thesis naming, and got a closed-loop sim running end-to-end
with a meaningful comparison against the unforced baseline.

## Naming convention (locked in)

Thesis-aligned. Applied to plant and controllers:

- `t` — time
- `x` — continuous state vector `[n; C1..C6; T_f; T_c; T_cold]`
- `plant` — params struct (was `p`; renamed to avoid collision with `p_i`
  predicates from the thesis)
- `u` — control input: external rod reactivity (scalar, `dk/k`)
- `ref` — reference/setpoint struct (e.g. `ref.T_avg`)
- `Q_sg` — bounded disturbance (SG heat removal, `W`), a function handle
  of time

Captured in `plant-model/CLAUDE.md` under "Naming convention" — any future
file in `plant-model/` or `controllers/` uses these names.

## Controller contract

```matlab
u = ctrl_<mode>(t, x, plant, ref)
```

Pure function. Returns scalar `u`. All four args required even if unused
(so `pke_solver` can swap controllers by function handle without branching).

## Plant refactor

Four files touched. Non-cosmetic changes:

- `pke_th_rhs.m` now takes `u` as an **explicit argument** instead of
  reading `p.rho_ext(t)` from the params struct. This decouples plant from
  controller and matches the thesis's `f(x, u)` form. Old `rho_ext` field
  on the params struct is gone.
- `pke_solver.m` signature changed:
  `[t, X, U] = pke_solver(plant, Q_sg, ctrl_fn, ref, tspan)`.
  Returns `U` (the control trajectory), reconstructed post-hoc by
  re-evaluating `ctrl_fn` at each returned time step.
- `plot_pke_results.m` takes `(t, X, U, plant, Q_sg, title)` — reactivity
  panel now shows `u`, feedback, and total separately rather than relying on
  a `p.rho_ext` handle.
- `pke_initial_conditions.m` trivial rename `p → plant`, output `x0`.

## ctrl_operation

Proportional-only for now. `Kp = 1e-4 /K`, chosen to roughly double the
effective moderator coefficient (`alpha_c ≈ -1e-4 /K`). Not tuned for
precision — this is rough-out. PI upgrade would require augmenting state
with integral error; deferred until we actually need tighter tracking.

**Validation run** (100% → 80% Q_sg step at t=30s, 600s horizon):

| Metric | ctrl_null | ctrl_operation |
|---|---|---|
| Final T_avg drift from setpoint | +1.5 °F | +0.8 °F |
| Final `u` | 0 $ | -0.0068 $ |
| Final power | 800 MW ✓ | 800 MW ✓ |

Sign polarity: T_avg > ref → e < 0 → u < 0 (rods in). Correct for a PWR.
Controller reduces steady-state offset by ~47%. Expected result — gain is
modest; the point is the plumbing works, not that the tuning is final.

## What's deferred (on purpose)

- **Integral action in ctrl_operation.** P-only for now. If we want to hit
  `ref.T_avg` exactly (zero steady-state error), augment state with
  `int_e` and upgrade to PI.
- **Gain config.** Gains hardcoded in the controller file. Fine for one
  mode; once we tune 3–4 modes we should lift to a `ctrl_params.m` or
  similar.
- **Mode dispatcher.** No `ctrl_dispatch.m` yet because there's only one
  real controller. Will introduce when we add the second mode and need to
  switch based on discrete state.
- **ctrl_heatup, ctrl_scram, ctrl_shutdown.** Next. Each is a separate
  design problem with its own verification method (reachability, parametric
  reach, trivial).
- **Invariants and `X_safe` regions.** User explicitly said: rough out
  controllers first, then come back to invariants. Per the thesis, we're
  not trying to verify the whole hybrid system at once — one mode's reach
  set at a time.

## Things worth remembering

- `addpath('controllers')` is required in `main.m` — MATLAB won't find
  `@ctrl_operation` otherwise. Putting controllers in a subfolder is
  ergonomically clean but the path has to be set.
- `U` is reconstructed post-hoc in `pke_solver`. If a future controller
  has hysteresis or switching behavior, this reconstruction may not match
  the ODE's internal trajectory (the solver might have evaluated `ctrl_fn`
  at interior points that don't appear in the returned `t`). For smooth
  controllers (P, PI) this is fine; for event-driven / hybrid controllers
  we'll need to log `u` during the solve instead.
- MATLAB `-batch` mode runs `main.m` cleanly without a display — figures
  are created off-screen but the script completes. Good for CI or sanity
  checks without opening the IDE.
- `T_avg` is literally `T_c` in the state vector (`x(9)`). Don't let the
  two names confuse future debugging — `T_avg` is the nuclear-industry term,
  `T_c` is the code variable, they're the same quantity.

## Paths touched

- Modified: `plant-model/pke_th_rhs.m`, `pke_solver.m`,
  `pke_initial_conditions.m`, `plot_pke_results.m`, `main.m`, `README.md`,
  `CLAUDE.md`
- Created: `plant-model/controllers/ctrl_null.m`,
  `plant-model/controllers/ctrl_operation.m`
- Nothing outside `plant-model/` was touched this session.
