Multi-session work bundle on a draft branch. Splits into a clean
sequence of commits later; pushed here so it isn't lost on a reboot.
Reach work
- code/scripts/reach/reach_scram_pj.jl: shutdown_margin halfspace
X_exit (replaces "n <= 1e-4 AND T_f bound" framing); per-step
envelope extraction added.
- code/scripts/reach/reach_scram_pj_fat.jl: per-step envelope
extraction added; shutdown_margin discharge logic mirrored from the
tight scram script. 3 probes (10/30/60s) all discharge from the
fat union polytope.
- code/scripts/reach/reach_scram_full_fat.jl (NEW): full nonlinear
PKE scram reach with fat entry. Hits the stiffness wall at
~1.5 s plant time as expected; saves NaN-tolerant per-step
envelopes. Demonstrates concretely why PJ is the right tool for
the longer-horizon proof.
- code/scripts/reach/reach_heatup_pj.jl: T_REF_START_C constant
(entry-conditioned ramp) replaces T_STANDBY-init that was making
the FL controller command cooling at t=0. Per-step extraction
already in place.
- code/configs/heatup/tight.toml: bumped maxsteps; probe horizon
parameterized.
Hot-standby SOS barrier
- code/scripts/barrier/barrier_sos_2d_shutdown.jl (NEW): mirrors the
operation SOS machinery on the hot-standby thermal projection.
Includes the eps-slack pattern (so feasibility doesn't silently
collapse to B == 0).
- code/scripts/barrier/barrier_sos_2d.jl: refactored to use the same
helper.
- code/src/sos_barrier.jl (NEW): solve_sos_barrier_2d helper module
factoring out the SOS construction; eps-slack with eps_cap=1.0 to
avoid unbounded primal.
Library
- code/src/pke_states.jl (NEW): single source of truth for canonical
initial-condition vectors per DRC mode (op, shutdown, heatup) keyed
off plant + predicates.
- code/scripts/sim/{main_mode_sweep,validate_pj}.jl, code/CLAUDE.md:
migrated to pke_states.
Predicates + invariants
- reachability/predicates.json: new shutdown_margin predicate (1%
dk/k tech-spec floor, expressed as alpha_f*T_f + alpha_c*T_c
halfspace). Used as scram X_exit.
Plot script
- code/scripts/plot/plot_reach_tubes.jl: plot_tubes_scram_pj() with
variant=:fat|:tight knob; plot_tubes_scram_full() for full-PKE
3-panel (T_c, T_f, rho); plot_tubes_heatup_pj() reads results/
not reachability/.
Journal + memory
- journal/entries/2026-04-27-shutdown-sos-and-scram-X_exit.tex (NEW):
long-form entry on the SOS hot-standby barrier and the scram X_exit
refactor.
- journal/journal.tex: input chain updated.
- claude_memory/ — three new session notes:
* 2026-04-27-scram-X_exit-shutdown-margin.md
* 2026-04-28-DICE-2026-conference-intel.md (people, sessions,
strategic notes for the May 12 talk)
* 2026-04-28-path1-sos-pj-sketch.md (sketch of nonlinear-SOS via
polynomial multiply-through; saved for an overnight session)
Docs
- docs/model_cheatsheet.md (NEW): one-page reference of state vector,
dynamics, constants, modes, predicates, sanity numbers — the talk
prep cheatsheet Dane asked for.
- docs/figures/reach_*_tubes.png: regenerated with the new mat data.
- presentations/prelim-presentation/outline.md: revised arc per the
April-28 review pass (cuts: Lyapunov-fails standalone slide,
operation-tube standalone slide, SOS standalone; adds: scopes-of-
control framing, scram on the headline result slide).
- app/predicate_explorer.jl: minor.
Hacker-Split: end-of-session scratch bundle
318 lines
14 KiB
Julia
318 lines
14 KiB
Julia
#!/usr/bin/env julia
|
||
#
|
||
# plot_reach_tubes.jl — multi-panel reach-tube visualization.
|
||
#
|
||
# Produces a four-panel figure from a reach-result .mat file:
|
||
# (1) Temperature tube overlay: T_c, T_hot, T_cold envelopes together.
|
||
# The gap between T_hot and T_cold is a direct proxy for core
|
||
# thermal power (P = W·c_c·ΔT).
|
||
# (2) ΔT_core = T_hot - T_cold envelope (the power proxy, explicit).
|
||
# (3) Reactivity ρ envelope, normalized by β (in dollars).
|
||
# (4) Normalized power n envelope.
|
||
#
|
||
# Two input formats supported:
|
||
# operation: reach_operation_result.mat (linear reach, has R_lo/R_hi
|
||
# matrices, time vector T, nominal X_nom).
|
||
# heatup_pj: reach_heatup_pj_tight_full.mat (per-timestep envelopes
|
||
# Tc_lo_ts/Tc_hi_ts/... already extracted; rho from PJ
|
||
# controller).
|
||
#
|
||
# Usage:
|
||
# julia --project=. scripts/plot_reach_tubes.jl operation
|
||
# julia --project=. scripts/plot_reach_tubes.jl heatup_pj
|
||
|
||
using Pkg
|
||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||
|
||
using MAT
|
||
using Plots
|
||
gr()
|
||
|
||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||
const PLANT = pke_params()
|
||
|
||
function plot_tubes_operation()
|
||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results", "reach_operation_result.mat")
|
||
d = matread(mat_path)
|
||
|
||
T_vec = vec(d["T"]) # time grid
|
||
X_lo = d["X_lo"] # 10 × M lower bounds
|
||
X_hi = d["X_hi"] # 10 × M upper bounds
|
||
X_nom = d["X_nom"] # 10 × M nominal
|
||
|
||
# States at indices: n=1, T_f=8, T_c=9, T_cold=10.
|
||
n_lo = X_lo[1, :]; n_hi = X_hi[1, :]
|
||
Tf_lo = X_lo[8, :]; Tf_hi = X_hi[8, :]
|
||
Tc_lo = X_lo[9, :]; Tc_hi = X_hi[9, :]
|
||
Tco_lo = X_lo[10, :]; Tco_hi = X_hi[10, :]
|
||
|
||
# T_hot = 2*T_c - T_cold; envelope via worst-case signed combination.
|
||
Th_lo = 2 .* Tc_lo .- Tco_hi
|
||
Th_hi = 2 .* Tc_hi .- Tco_lo
|
||
|
||
# ΔT_core = T_hot - T_cold = 2*(T_c - T_cold).
|
||
dT_lo = 2 .* (Tc_lo .- Tco_hi)
|
||
dT_hi = 2 .* (Tc_hi .- Tco_lo)
|
||
|
||
# ρ under LQR: ρ_total = u + α_f·dT_f + α_c·dT_c where u = -K(x-x_op).
|
||
# For a quick envelope, compute ρ at the nominal trajectory and
|
||
# inflate by bounds of the feedback contributions from the tube.
|
||
# Simpler: use total reactivity = u + α_f*(T_f-T_f0) + α_c*(T_c-T_c0).
|
||
# u under LQR is small; we approximate ρ envelope by α feedback
|
||
# alone (the u contribution is ≤ few cents).
|
||
rho_nom = PLANT.alpha_f .* (X_nom[8, :] .- PLANT.T_f0) .+
|
||
PLANT.alpha_c .* (X_nom[9, :] .- PLANT.T_c0)
|
||
# Envelope via worst-case T_f, T_c.
|
||
rho_lo = PLANT.alpha_f .* (Tf_hi .- PLANT.T_f0) .+
|
||
PLANT.alpha_c .* (Tc_hi .- PLANT.T_c0) # both α < 0, max T → min ρ
|
||
rho_hi = PLANT.alpha_f .* (Tf_lo .- PLANT.T_f0) .+
|
||
PLANT.alpha_c .* (Tc_lo .- PLANT.T_c0)
|
||
|
||
title_stem = "Operation-mode LQR reach tubes"
|
||
_plot_common(T_vec, Tc_lo, Tc_hi, Th_lo, Th_hi, Tco_lo, Tco_hi,
|
||
dT_lo, dT_hi, rho_lo, rho_hi, n_lo, n_hi, title_stem,
|
||
"reach_operation_tubes.png")
|
||
end
|
||
|
||
function plot_tubes_heatup_pj(probe_seconds::Int=8000)
|
||
# Reads the per-probe save format from reach_heatup_pj.jl with the
|
||
# latest tight.toml config. Format: keys prefixed with "T_{probe}_"
|
||
# for each requested probe horizon (e.g. T_8000_Tc_lo_ts).
|
||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results",
|
||
"reach_heatup_pj_tight.mat")
|
||
d = matread(mat_path)
|
||
|
||
pre = "T_$(probe_seconds)_"
|
||
if !haskey(d, pre * "t_arr")
|
||
# Fall back to legacy bare-key format (older mat files).
|
||
pre = ""
|
||
haskey(d, "t_arr") || error("Neither '$pre' nor bare keys found in $mat_path. Available probes: " *
|
||
join([replace(k, r"_t_arr$" => "") for k in keys(d) if endswith(k, "_t_arr")], ", "))
|
||
end
|
||
|
||
t_arr = vec(d[pre * "t_arr"])
|
||
Tc_lo = vec(d[pre * "Tc_lo_ts"]); Tc_hi = vec(d[pre * "Tc_hi_ts"])
|
||
Tf_lo = vec(d[pre * "Tf_lo_ts"]); Tf_hi = vec(d[pre * "Tf_hi_ts"])
|
||
Tco_lo = vec(d[pre * "Tco_lo_ts"]); Tco_hi = vec(d[pre * "Tco_hi_ts"])
|
||
n_lo = vec(d[pre * "n_lo_ts"]); n_hi = vec(d[pre * "n_hi_ts"])
|
||
rho_lo = vec(d[pre * "rho_lo_ts"]); rho_hi = vec(d[pre * "rho_hi_ts"])
|
||
|
||
Th_lo = 2 .* Tc_lo .- Tco_hi
|
||
Th_hi = 2 .* Tc_hi .- Tco_lo
|
||
dT_lo = 2 .* (Tc_lo .- Tco_hi)
|
||
dT_hi = 2 .* (Tc_hi .- Tco_lo)
|
||
|
||
title_stem = "Heatup PJ (tight entry, T=$probe_seconds s) reach tubes"
|
||
_plot_common(t_arr, Tc_lo, Tc_hi, Th_lo, Th_hi, Tco_lo, Tco_hi,
|
||
dT_lo, dT_hi, rho_lo, rho_hi, n_lo, n_hi, title_stem,
|
||
"reach_heatup_pj_tubes.png")
|
||
end
|
||
|
||
function _plot_common(t, Tc_lo, Tc_hi, Th_lo, Th_hi, Tco_lo, Tco_hi,
|
||
dT_lo, dT_hi, rho_lo, rho_hi, n_lo, n_hi,
|
||
title_stem, outname)
|
||
CtoF(T) = T*9/5 + 32
|
||
|
||
# Panel 1: T_c / T_hot / T_cold overlaid.
|
||
p1 = plot(xlabel="Time [s]", ylabel="T [°C]",
|
||
title="T tubes", legend=:right)
|
||
plot!(p1, t, Tc_hi, fillrange=Tc_lo, fillalpha=0.30, color=:red,
|
||
linealpha=0, label="T_c tube")
|
||
plot!(p1, t, Th_hi, fillrange=Th_lo, fillalpha=0.25, color=:orange,
|
||
linealpha=0, label="T_hot tube")
|
||
plot!(p1, t, Tco_hi, fillrange=Tco_lo, fillalpha=0.25, color=:blue,
|
||
linealpha=0, label="T_cold tube")
|
||
|
||
# Panel 2: ΔT_core = T_hot - T_cold (power proxy at constant flow).
|
||
P_lo_MW = PLANT.W * PLANT.c_c .* dT_lo ./ 1e6
|
||
P_hi_MW = PLANT.W * PLANT.c_c .* dT_hi ./ 1e6
|
||
p2 = plot(xlabel="Time [s]", ylabel="ΔT_core = T_hot - T_cold [K]",
|
||
title="Core ΔT envelope (power proxy)", legend=:right)
|
||
plot!(p2, t, dT_hi, fillrange=dT_lo, fillalpha=0.30, color=:purple,
|
||
linealpha=0, label="ΔT_core [K]")
|
||
p2b = twinx(p2)
|
||
plot!(p2b, t, P_hi_MW, fillrange=P_lo_MW, fillalpha=0.0, color=:gray,
|
||
linealpha=0.5, linestyle=:dot, label="P=W·c_c·ΔT [MW]",
|
||
ylabel="Primary thermal power [MWth]")
|
||
|
||
# Panel 3: ρ envelope in dollars.
|
||
rho_lo_d = rho_lo ./ PLANT.beta
|
||
rho_hi_d = rho_hi ./ PLANT.beta
|
||
p3 = plot(xlabel="Time [s]", ylabel="ρ [\$]",
|
||
title="Reactivity envelope (1 \$ = β = prompt-critical)",
|
||
legend=:right)
|
||
plot!(p3, t, rho_hi_d, fillrange=rho_lo_d, fillalpha=0.3,
|
||
color=:darkgreen, linealpha=0, label="ρ / β")
|
||
hline!(p3, [1.0, -1.0], ls=:dash, color=:red,
|
||
label="prompt ±1 \$")
|
||
hline!(p3, [0.0], ls=:dot, color=:black, label="critical")
|
||
|
||
# Panel 4: n envelope (log scale if needed).
|
||
p4 = plot(xlabel="Time [s]", ylabel="n (normalized power)",
|
||
title="n envelope", legend=:right)
|
||
plot!(p4, t, n_hi, fillrange=n_lo, fillalpha=0.3, color=:black,
|
||
linealpha=0, label="n tube")
|
||
|
||
fig = plot(p1, p2, p3, p4, layout=(2, 2), size=(1300, 800),
|
||
plot_title=title_stem)
|
||
figdir = joinpath(@__DIR__, "..", "..", "..", "docs", "figures")
|
||
isdir(figdir) || mkpath(figdir)
|
||
outpath = joinpath(figdir, outname)
|
||
savefig(fig, outpath)
|
||
println("Saved $outpath")
|
||
end
|
||
|
||
function plot_tubes_scram_full(probe_seconds::Int=60)
|
||
# 3-panel scram tube plot using FULL-PKE reach (no prompt-jump
|
||
# algebraic substitution). T_c on top, T_f in the middle, ρ on the
|
||
# bottom. Per Dane's 2026-04-30 evening request: "I'd love tubes for
|
||
# t_c t_f and rho."
|
||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results",
|
||
"reach_scram_full_fat.mat")
|
||
d = matread(mat_path)
|
||
pre = "T_$(probe_seconds)_"
|
||
haskey(d, pre * "t_arr") || error("No per-step data for probe $probe_seconds in $mat_path")
|
||
|
||
t_arr = vec(d[pre * "t_arr"])
|
||
Tc_lo = vec(d[pre * "Tc_lo_ts"]); Tc_hi = vec(d[pre * "Tc_hi_ts"])
|
||
Tf_lo = vec(d[pre * "Tf_lo_ts"]); Tf_hi = vec(d[pre * "Tf_hi_ts"])
|
||
rho_lo = vec(d[pre * "rho_lo_ts"]); rho_hi = vec(d[pre * "rho_hi_ts"])
|
||
|
||
# Convert ρ to dollars.
|
||
rho_lo_d = rho_lo ./ PLANT.beta
|
||
rho_hi_d = rho_hi ./ PLANT.beta
|
||
rho_sdm_dollars = 0.01 / PLANT.beta
|
||
|
||
# Panel 1: T_c envelope
|
||
p1 = plot(xlabel="Time after rod insertion [s]", ylabel="T_c [°C]",
|
||
title="Average coolant temperature envelope",
|
||
legend=:right, dpi=180)
|
||
plot!(p1, t_arr, Tc_hi, fillrange=Tc_lo, fillalpha=0.35,
|
||
color=:red, linealpha=0, label="T_c tube")
|
||
|
||
# Panel 2: T_f envelope
|
||
p2 = plot(xlabel="Time after rod insertion [s]", ylabel="T_f [°C]",
|
||
title="Fuel temperature envelope",
|
||
legend=:right, dpi=180)
|
||
plot!(p2, t_arr, Tf_hi, fillrange=Tf_lo, fillalpha=0.35,
|
||
color=:orange, linealpha=0, label="T_f tube")
|
||
|
||
# Panel 3: ρ envelope in dollars
|
||
p3 = plot(xlabel="Time after rod insertion [s]",
|
||
ylabel="ρ [\$] (1 \$ = β = prompt-critical)",
|
||
title="Reactivity envelope",
|
||
legend=:right, dpi=180)
|
||
plot!(p3, t_arr, rho_hi_d, fillrange=rho_lo_d, fillalpha=0.35,
|
||
color=:darkgreen, linealpha=0, label="ρ tube")
|
||
hline!(p3, [0.0], ls=:dot, color=:black, label="critical")
|
||
hline!(p3, [-rho_sdm_dollars], ls=:dash, color=:red, lw=2,
|
||
label="ρ_SDM = -0.01 (\$ = $(round(-rho_sdm_dollars; digits=2)))")
|
||
|
||
fig = plot(p1, p2, p3, layout=(3, 1), size=(900, 900),
|
||
plot_title="Full-PKE scram reach tubes (T=$probe_seconds s, fat entry)")
|
||
figdir = joinpath(@__DIR__, "..", "..", "..", "docs", "figures")
|
||
isdir(figdir) || mkpath(figdir)
|
||
outpath = joinpath(figdir, "reach_scram_full_tubes.png")
|
||
savefig(fig, outpath)
|
||
println("Saved $outpath")
|
||
end
|
||
|
||
function plot_tubes_scram_pj(probe_seconds::Int=60; variant::Symbol=:fat)
|
||
# 2-panel scram tube plot: rho(t) on top, n(t) below.
|
||
# variant=:tight reads reach_scram_pj_result.mat (small box around
|
||
# the operating point). variant=:fat reads reach_scram_pj_fat.mat
|
||
# (union over shutdown + heatup-tight + operation + LOCA envelopes).
|
||
mat_filename = variant == :fat ? "reach_scram_pj_fat.mat" :
|
||
"reach_scram_pj_result.mat"
|
||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results", mat_filename)
|
||
d = matread(mat_path)
|
||
pre = "T_$(probe_seconds)_"
|
||
haskey(d, pre * "t_arr") || error("No per-step data for probe $probe_seconds in $mat_path")
|
||
|
||
t_arr = vec(d[pre * "t_arr"])
|
||
rho_lo = vec(d[pre * "rho_lo_ts"])
|
||
rho_hi = vec(d[pre * "rho_hi_ts"])
|
||
n_lo = vec(d[pre * "n_lo_ts"])
|
||
n_hi = vec(d[pre * "n_hi_ts"])
|
||
|
||
# Convert rho to dollars (rho / beta).
|
||
rho_lo_d = rho_lo ./ PLANT.beta
|
||
rho_hi_d = rho_hi ./ PLANT.beta
|
||
|
||
# Panel 1: rho envelope in dollars.
|
||
p1 = plot(xlabel="Time after rod insertion [s]",
|
||
ylabel="ρ [\$] (1 \$ = β = prompt-critical)",
|
||
title="Reactivity envelope",
|
||
legend=:right, dpi=180)
|
||
plot!(p1, t_arr, rho_hi_d, fillrange=rho_lo_d, fillalpha=0.35,
|
||
color=:darkgreen, linealpha=0, label="ρ tube")
|
||
hline!(p1, [0.0], ls=:dot, color=:black, label="critical")
|
||
# Shutdown margin threshold rho_SDM = 0.01 dk/k → in dollars: 0.01/beta
|
||
rho_sdm_dollars = 0.01 / PLANT.beta
|
||
hline!(p1, [-rho_sdm_dollars], ls=:dash, color=:red, lw=2,
|
||
label="ρ_SDM = -0.01 (\$ = $(round(-rho_sdm_dollars; digits=2)))")
|
||
|
||
# Panel 2: n envelope (log scale).
|
||
# Filter NaN / non-positive (PJ reconstruction can produce slack-negatives).
|
||
n_lo_pos = max.(n_lo, 1e-9)
|
||
n_hi_pos = max.(n_hi, 1e-9)
|
||
p2 = plot(xlabel="Time after rod insertion [s]",
|
||
ylabel="n (normalized power)",
|
||
title="Power envelope (log scale)",
|
||
legend=:right, yaxis=:log, dpi=180)
|
||
plot!(p2, t_arr, n_hi_pos, fillrange=n_lo_pos, fillalpha=0.35,
|
||
color=:black, linealpha=0, label="n tube")
|
||
hline!(p2, [1e-4], ls=:dash, color=:orange, lw=2, label="n = 10⁻⁴ (p_above_crit floor)")
|
||
|
||
title_suffix = variant == :fat ? "fat entry, T=$probe_seconds s" :
|
||
"T=$probe_seconds s"
|
||
fig = plot(p1, p2, layout=(2, 1), size=(900, 700),
|
||
plot_title="Scram reach tubes (PJ-reduced PKE, $title_suffix)")
|
||
figdir = joinpath(@__DIR__, "..", "..", "..", "docs", "figures")
|
||
isdir(figdir) || mkpath(figdir)
|
||
out_filename = variant == :fat ? "reach_scram_pj_fat_tubes.png" :
|
||
"reach_scram_pj_tubes.png"
|
||
outpath = joinpath(figdir, out_filename)
|
||
savefig(fig, outpath)
|
||
println("Saved $outpath")
|
||
end
|
||
|
||
# CLI dispatch.
|
||
which_plot = length(ARGS) > 0 ? ARGS[1] : "both"
|
||
if which_plot in ("operation", "both")
|
||
plot_tubes_operation()
|
||
end
|
||
if which_plot in ("scram_full", "both")
|
||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results",
|
||
"reach_scram_full_fat.mat")
|
||
probe_s = length(ARGS) >= 2 ? parse(Int, ARGS[2]) : 60
|
||
if isfile(mat_path)
|
||
plot_tubes_scram_full(probe_s)
|
||
else
|
||
println("Skipping scram_full plot — $mat_path not found.")
|
||
end
|
||
end
|
||
if which_plot in ("scram_pj", "both")
|
||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results",
|
||
"reach_scram_pj_result.mat")
|
||
probe_s = length(ARGS) >= 2 ? parse(Int, ARGS[2]) : 60
|
||
if isfile(mat_path)
|
||
plot_tubes_scram_pj(probe_s)
|
||
else
|
||
println("Skipping scram_pj plot — $mat_path not found.")
|
||
println("(Run scripts/reach/reach_scram_pj.jl first.)")
|
||
end
|
||
end
|
||
if which_plot in ("heatup_pj", "both")
|
||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results",
|
||
"reach_heatup_pj_tight.mat")
|
||
# Optional second CLI arg: probe horizon in seconds (default 8000).
|
||
probe = length(ARGS) >= 2 ? parse(Int, ARGS[2]) : 8000
|
||
if isfile(mat_path)
|
||
plot_tubes_heatup_pj(probe)
|
||
else
|
||
println("Skipping heatup_pj plot — $mat_path not found.")
|
||
println("(Run scripts/reach/reach_heatup_pj.jl configs/heatup/tight.toml first.)")
|
||
end
|
||
end
|