PWR-HYBRID-3/code/CLAUDE.md
Dane Sabo fbbaebff9f julia migration: port MATLAB to Julia, delete MATLAB, rename julia-port -> code
Full toolchain port. Numerical equivalence verified against MATLAB:
- main_mode_sweep.jl: every mode's final state matches MATLAB to 3-4 dp
- reach_operation.jl: per-halfspace margins match MATLAB exactly
- barrier_lyapunov.jl: per-halfspace bounds match (best Qbar from sweep
  yields max|dT_c| = 33.228 K either side)
- barrier_compare_OL_CL.jl: OL gamma 1.038e13, CL gamma 1.848e4
  matching the MATLAB result; LQR helps by ~20,000x on every halfspace.

Phase summary:
  Phase 1: pke_solver.jl, plot_pke_results.jl (Plots.jl), main_mode_sweep.jl
  Phase 2: reach_linear.jl, reach_operation.jl, barrier_lyapunov.jl,
           barrier_compare_OL_CL.jl, load_predicates.jl
  Phase 3 (this commit): delete plant-model/ entirely, delete reach
           code from reachability/ keeping predicates.json + docs,
           git mv julia-port/ -> code/, update root README + CLAUDE,
           write code/CLAUDE.md and code/README.md, update reach
           README + WALKTHROUGH file paths, journal preamble note
           that pre-port entries reference MATLAB paths.

Why now: prompt-neutron stiffness in nonlinear reach made it clear we
need TMJets, which is Julia. Already had the Julia plant model
working and matching MATLAB. Two languages = two sources of truth =
two places to drift. One language, one truth.

Manifest.toml gitignored. .mat results gitignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:44:59 -04:00

186 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
Guidance for Claude Code (and any AI agent) working in this subdirectory.
See also: the parent repo's `CLAUDE.md` (living-documentation rule —
update this file when new knowledge is created), `claude_memory/` for
session notes, and `../journal/` for the long-form invention log.
## What this is
The full plant-model + reachability toolchain for the PWR_HYBRID_3
preliminary example. All Julia — the earlier MATLAB implementation
was ported end-to-end on 2026-04-20 and deleted. A 10-state point
kinetic equation (PKE) model of a PWR coupled with lumped-parameter
thermal-hydraulics, plus mode-specific continuous controllers, linear
reach-set machinery, Lyapunov-barrier attempt, and scaffolding for
nonlinear reach via TMJets.
The model is being developed for hybrid-systems reachability analysis.
The end goal is nonlinear reach (sound, not linearized) over per-mode
reach-avoid obligations, which will require a reduced-order PKE to
eliminate prompt-neutron stiffness.
## Naming convention (thesis-aligned)
Continuous state and control-theory notation matches the thesis:
| Symbol | Meaning |
|---|---|
| `t` | time [s] |
| `x` | continuous state vector, `[n; C1..C6; T_f; T_c; T_cold]` |
| `plant` | parameter NamedTuple from `pke_params()` |
| `u` | control input: external rod reactivity [dk/k] |
| `Q_sg` | bounded disturbance: SG heat removal [W], as a callable `Q_sg(t)` |
| `ref` | reference/setpoint NamedTuple (e.g. `ref.T_avg`) |
`T_avg = T_c` (algebraic alias); `T_hot = 2*T_c - T_cold` (linear
coolant profile assumption).
## Running
From the project root:
```bash
cd code
julia --project=. -e 'using Pkg; Pkg.instantiate()' # first time only
julia --project=. scripts/main_mode_sweep.jl # runs all 5 DRC modes
julia --project=. scripts/reach_operation.jl # operation-mode linear reach
julia --project=. scripts/barrier_lyapunov.jl # Lyapunov-ellipsoid barrier
julia --project=. scripts/barrier_compare_OL_CL.jl # OL vs CL barrier bounds
julia --project=. scripts/reach_heatup_nonlinear.jl # nonlinear heatup reach (10s max, stiffness-limited)
julia --project=. scripts/sim_sanity.jl # operation-mode nominal sim
julia --project=. scripts/sim_heatup.jl # heatup nominal sim
```
Figures save to `../docs/figures/`. Reach results save to
`../reachability/*.mat` (gitignored).
## Architecture
```
code/
Project.toml Julia package manifest
README.md usage overview
src/
pke_params.jl plant parameters and derived steady state
pke_th_rhs.jl dynamics f(t, x, plant, Q_sg, u)
pke_linearize.jl numerical (A, B, B_w) Jacobians
pke_solver.jl closed-loop OrdinaryDiffEq driver
plot_pke_results.jl 4-panel results plot (Plots.jl)
reach_linear.jl hand-rolled box reach propagator
load_predicates.jl reads reachability/predicates.json
controllers/
controllers.jl ctrl_null, shutdown, heatup, operation,
operation_lqr factory, scram
ctrl_heatup_unsat.jl saturation-disabled variant for reach
scripts/
main_mode_sweep.jl runs every mode, saves all figures
sim_sanity.jl operation-mode Rodas5 sim (MATLAB-parity check)
sim_heatup.jl heatup Rodas5 sim
reach_operation.jl linear reach tube + inv2_holds check
barrier_lyapunov.jl Lyapunov barrier sweep
barrier_compare_OL_CL.jl OL vs CL barrier comparison
reach_heatup_nonlinear.jl TMJets nonlinear reach (10s horizon cap)
```
## Controller contract
```julia
u = ctrl_mode(t, x, plant, ref) # all five args required
```
Pure function. Returns scalar `u` (external rod reactivity in `dk/k`).
All four signature args are required even if unused — lets the solver
swap controllers by function reference without caring which one it is.
## Solver contract
```julia
t, X, U = pke_solver(plant, Q_sg, ctrl_fn, ref, tspan; x0=nothing, verbose=true)
```
Returns `(t::Vector, X::Matrix, U::Vector)` where `X[k, :]` is the
state at time `t[k]` and columns of X match the state vector ordering.
The underlying integrator is `Rodas5` from OrdinaryDiffEq (stiff-aware,
matches the MATLAB `ode15s` behavior we validated against). Optional
`x0` argument for starting from a non-equilibrium IC (heatup, shutdown).
## Key design decisions
- **`Q_sg` is the disturbance, never an actuator.** Cold leg
temperature is a dynamic state resulting from SG heat removal — the
reachability formulation needs `Q_sg ∈ [Q_min, Q_max]` as a bounded
disturbance, not a control input.
- **`u` is explicit.** `pke_th_rhs!` takes `u` directly instead of
reading it from the plant struct.
- **Controller gains live in the controller file.** Rough-out phase —
once we tune more than one mode we'll lift to a config struct.
- **`Rodas5` is required** because the system is stiff:
`Lambda` (~10⁻⁴ s) vs thermal time constants (~10100 s).
- **LQR gain is cached** in a `Ref` inside `ctrl_operation_lqr_factory`.
Reconstruct the factory (not just call it) to retune.
- **Plant parameters live in `pke_params()` as a NamedTuple.** For
reach analysis (TMJets) we duplicate them as `const` globals in
each reach script — `@taylorize` needs compile-time constants, not
struct-field accesses. Keep these in sync.
## Conventions
- One function per file where natural; group related functions in
one file when they're small (the controllers all live in one file).
- Internal model uses SI (W, kg, °C); printed/plotted temperatures
in °F.
- Reference setpoints in SI (so `ref.T_avg` is in °C).
- Controllers are pure functions — no persistent state. If a mode
needs integral action, augment `x`, don't hide state in globals.
(Exception: the LQR factory caches K in a closure, but that's
immutable after construction.)
## Model validity range
The feedback coefficients `alpha_f`, `alpha_c` are *linear* slopes
taken about the full-power operating point `(T_f0, T_c0)`. They become
unphysical when extrapolated far from that reference. True cold
shutdown (~50 °C everywhere) violates the linear-coefficient
assumption.
**Trust range:** roughly ±50 °C around the operating point.
"Shutdown" in this demo means hot standby (~275 °C per
`predicates.json` `T_standby`), not cold shutdown. Extending to
cold-shutdown semantics requires piecewise-linear or table-lookup
temperature coefficients — separate modeling project.
## For reachability work
- `pke_th_rhs!(dx, x, t, plant, Q_sg, u)` is the `ẋ = f(x, u)` with
`Q_sg` as the disturbance `w`.
- Use `pke_linearize(plant; x_star, u_star, Q_star)` for `(A, B, B_w)`
at any operating point.
- Safe regions come from `../reachability/predicates.json` — three
layers: `operational_deadbands` (DRC transitions),
`safety_limits` (hard halfspaces), `mode_invariants` (conjunctions
used by per-mode reach).
- Do NOT try to verify the whole hybrid system at once. Pick one
mode, compute its reach set, discharge the per-mode obligation.
- Linear reach in `scripts/reach_operation.jl`; nonlinear (TMJets)
in `scripts/reach_heatup_nonlinear.jl`.
## Robustness caveats (idealized in current artifacts)
- **α_f, α_c are treated as known exactly.** The feedback-linearization
in `ctrl_heatup` assumes the controller's α matches the plant's;
real reactors have α drift (burnup, boron, xenon). Robust treatment
= α as bounded parametric uncertainty.
- **Saturation is a hybrid sub-mode.** `ctrl_heatup`'s `clamp(u, ...)`
is formally piecewise affine. Current reach treats it as dormant
(true for operation/LQR, near-true for the demo heatup trajectory).
A rigorous heatup reach has to model saturation regions explicitly.
- **Linear-model reach is not sound for the nonlinear plant.** The
linear-reach artifacts produce a sound over-approximation of the
LINEAR model's reach set, not of the plant's. To upgrade:
nonlinear reach directly (TMJets, once the reduced-order PKE
eliminates prompt-neutron stiffness), or Taylor-remainder inflation
on linear tubes.