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>
8.0 KiB
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:
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
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
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_sgis the disturbance, never an actuator. Cold leg temperature is a dynamic state resulting from SG heat removal — the reachability formulation needsQ_sg ∈ [Q_min, Q_max]as a bounded disturbance, not a control input.uis explicit.pke_th_rhs!takesudirectly 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.
Rodas5is required because the system is stiff:Lambda(~10⁻⁴ s) vs thermal time constants (~10–100 s).- LQR gain is cached in a
Refinsidectrl_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 asconstglobals in each reach script —@taylorizeneeds 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_avgis 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)withQ_sgas the disturbancew.- 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) inscripts/reach_heatup_nonlinear.jl.
Robustness caveats (idealized in current artifacts)
- α_f, α_c are treated as known exactly. The feedback-linearization
in
ctrl_heatupassumes 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'sclamp(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.