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
285 lines
11 KiB
Julia
285 lines
11 KiB
Julia
#!/usr/bin/env julia
|
||
#
|
||
# reach_heatup_pj.jl — nonlinear reach on heatup, prompt-jump model.
|
||
#
|
||
# Reduced from 10-state to 9-state (n is algebraic). Removes the Λ⁻¹
|
||
# stiffness that capped the full-state reach at ~10 s.
|
||
#
|
||
# State (10D with augmented time):
|
||
# x[1..6] = C_1..C_6 (delayed-neutron precursors)
|
||
# x[7] = T_f
|
||
# x[8] = T_c
|
||
# x[9] = T_cold
|
||
# x[10] = t (augmented time, dt/dt = 1)
|
||
#
|
||
# n is algebraic: n = Λ·Σ λ_i C_i / (β - ρ), ρ = K_p·(T_ref - T_c).
|
||
#
|
||
# Controller reference: T_ref(t) = T_REF_START_C + RAMP_RATE_CS · t,
|
||
# linear (no clamp at T_c0 — the reach probes when it enters X_exit;
|
||
# clamping would introduce a non-smooth piecewise dynamics @taylorize
|
||
# can't handle). T_REF_START_C must be at-or-below the X_entry T_c
|
||
# lower bound to keep the controller heating, not cooling.
|
||
#
|
||
# Configuration-driven: pass a TOML config path as the first CLI arg,
|
||
# or omit for the baseline config.
|
||
#
|
||
# julia --project=. scripts/reach/reach_heatup_pj.jl # baseline
|
||
# julia --project=. scripts/reach/reach_heatup_pj.jl configs/heatup/tight.toml
|
||
#
|
||
# Configs live in code/configs/heatup/*.toml. See baseline.toml for
|
||
# the full schema.
|
||
|
||
using Pkg
|
||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||
|
||
using LinearAlgebra
|
||
using ReachabilityAnalysis, LazySets
|
||
using JSON
|
||
using MAT
|
||
using TOML
|
||
|
||
# --- Plant constants (must match pke_params) ---
|
||
const LAMBDA = 1e-4
|
||
const BETA_1, BETA_2, BETA_3, BETA_4, BETA_5, BETA_6 =
|
||
0.000215, 0.001424, 0.001274, 0.002568, 0.000748, 0.000273
|
||
const BETA = BETA_1 + BETA_2 + BETA_3 + BETA_4 + BETA_5 + BETA_6
|
||
const LAM_1, LAM_2, LAM_3, LAM_4, LAM_5, LAM_6 =
|
||
0.0124, 0.0305, 0.111, 0.301, 1.14, 3.01
|
||
|
||
const P0 = 1e9
|
||
const M_F, C_F, M_C, C_C, HA, W_M, M_SG =
|
||
50000.0, 300.0, 20000.0, 5450.0, 5e7, 5000.0, 30000.0
|
||
|
||
const T_COLD0 = 290.0
|
||
const DT_CORE = P0 / (W_M * C_C)
|
||
const T_HOT0 = T_COLD0 + DT_CORE
|
||
const T_C0 = (T_HOT0 + T_COLD0) / 2
|
||
const T_F0 = T_C0 + P0 / HA
|
||
|
||
const T_STANDBY = T_C0 - 33.333333
|
||
const RAMP_RATE_CS = 28.0 / 3600
|
||
const KP_HEATUP = 1e-4
|
||
|
||
# Controller reference starting temperature. Must be at-or-below the
|
||
# X_entry T_c lower bound so the controller's first move is HEATING, not
|
||
# cooling. Originally was T_STANDBY = 275, but the heatup X_entry
|
||
# polytope has T_c >= 281 (post-t_avg_above_min), so the FL controller
|
||
# was commanding cooling for the first ~60 s before the ramp caught up.
|
||
# 285 matches `configs/heatup/tight.toml` T_c_lo. If the entry box
|
||
# changes, update here too.
|
||
const T_REF_START_C = 285.0
|
||
|
||
# --- Taylorized heatup PJ RHS ---
|
||
@taylorize function rhs_heatup_pj_taylor!(dx, x, p, t)
|
||
rho = KP_HEATUP * (T_REF_START_C + RAMP_RATE_CS * x[10] - x[8])
|
||
sum_lam_C = LAM_1*x[1] + LAM_2*x[2] + LAM_3*x[3] +
|
||
LAM_4*x[4] + LAM_5*x[5] + LAM_6*x[6]
|
||
denom = BETA - rho
|
||
n = LAMBDA * sum_lam_C / denom
|
||
inv_factor = sum_lam_C / denom
|
||
|
||
dx[1] = BETA_1 * inv_factor - LAM_1 * x[1]
|
||
dx[2] = BETA_2 * inv_factor - LAM_2 * x[2]
|
||
dx[3] = BETA_3 * inv_factor - LAM_3 * x[3]
|
||
dx[4] = BETA_4 * inv_factor - LAM_4 * x[4]
|
||
dx[5] = BETA_5 * inv_factor - LAM_5 * x[5]
|
||
dx[6] = BETA_6 * inv_factor - LAM_6 * x[6]
|
||
|
||
dx[7] = (P0 * n - HA * (x[7] - x[8])) / (M_F * C_F)
|
||
dx[8] = (HA * (x[7] - x[8]) - 2 * W_M * C_C * (x[8] - x[9])) / (M_C * C_C)
|
||
dx[9] = (2 * W_M * C_C * (x[8] - x[9])) / (M_SG * C_C)
|
||
|
||
dx[10] = one(x[1])
|
||
return nothing
|
||
end
|
||
|
||
# --- Config loader ---
|
||
function load_config(config_path)
|
||
if isfile(config_path)
|
||
return TOML.parsefile(config_path)
|
||
else
|
||
error("Config file not found: $config_path")
|
||
end
|
||
end
|
||
|
||
function build_entry_box(config)
|
||
if get(config, "use_predicates_entry", false)
|
||
pred_path = joinpath(@__DIR__, "..", "..", "..", "reachability", "predicates.json")
|
||
pred_raw = JSON.parsefile(pred_path)
|
||
entry = pred_raw["mode_boundaries"]["q_heatup"]["X_entry_polytope"]
|
||
n_lo, n_hi = entry["n_range"]
|
||
T_f_lo, T_f_hi = entry["T_f_range_C"]
|
||
T_c_lo, T_c_hi = entry["T_c_range_C"]
|
||
T_cold_lo, T_cold_hi = entry["T_cold_range_C"]
|
||
else
|
||
e = config["entry"]
|
||
n_lo, n_hi = e["n_range"]
|
||
T_f_lo, T_f_hi = e["T_f_range_C"]
|
||
T_c_lo, T_c_hi = e["T_c_range_C"]
|
||
T_cold_lo, T_cold_hi = e["T_cold_range_C"]
|
||
end
|
||
|
||
n_mid = 0.5 * (n_lo + n_hi)
|
||
C_mid = [BETA_1/(LAM_1*LAMBDA), BETA_2/(LAM_2*LAMBDA),
|
||
BETA_3/(LAM_3*LAMBDA), BETA_4/(LAM_4*LAMBDA),
|
||
BETA_5/(LAM_5*LAMBDA), BETA_6/(LAM_6*LAMBDA)] .* n_mid
|
||
|
||
x_lo = [C_mid[1]*(n_lo/n_mid), C_mid[2]*(n_lo/n_mid),
|
||
C_mid[3]*(n_lo/n_mid), C_mid[4]*(n_lo/n_mid),
|
||
C_mid[5]*(n_lo/n_mid), C_mid[6]*(n_lo/n_mid),
|
||
T_f_lo, T_c_lo, T_cold_lo, 0.0]
|
||
x_hi = [C_mid[1]*(n_hi/n_mid), C_mid[2]*(n_hi/n_mid),
|
||
C_mid[3]*(n_hi/n_mid), C_mid[4]*(n_hi/n_mid),
|
||
C_mid[5]*(n_hi/n_mid), C_mid[6]*(n_hi/n_mid),
|
||
T_f_hi, T_c_hi, T_cold_hi, 0.0]
|
||
|
||
return Hyperrectangle(low=x_lo, high=x_hi),
|
||
(n_lo=n_lo, n_hi=n_hi,
|
||
T_f_lo=T_f_lo, T_f_hi=T_f_hi,
|
||
T_c_lo=T_c_lo, T_c_hi=T_c_hi,
|
||
T_cold_lo=T_cold_lo, T_cold_hi=T_cold_hi)
|
||
end
|
||
|
||
# --- Per-step envelope extraction ---
|
||
function extract_envelopes(flow_hr)
|
||
n_steps = length(flow_hr)
|
||
t_arr = zeros(n_steps)
|
||
Tc_lo_ts = zeros(n_steps); Tc_hi_ts = zeros(n_steps)
|
||
Tf_lo_ts = zeros(n_steps); Tf_hi_ts = zeros(n_steps)
|
||
Tco_lo_ts = zeros(n_steps); Tco_hi_ts = zeros(n_steps)
|
||
n_lo_ts = zeros(n_steps); n_hi_ts = zeros(n_steps)
|
||
rho_lo_ts = zeros(n_steps); rho_hi_ts = zeros(n_steps)
|
||
for (k, R) in enumerate(flow_hr)
|
||
s = set(R)
|
||
t_arr[k] = high(s, 10)
|
||
Tc_lo_ts[k] = low(s, 8); Tc_hi_ts[k] = high(s, 8)
|
||
Tf_lo_ts[k] = low(s, 7); Tf_hi_ts[k] = high(s, 7)
|
||
Tco_lo_ts[k] = low(s, 9); Tco_hi_ts[k] = high(s, 9)
|
||
sumLC_lo = LAM_1*low(s,1) + LAM_2*low(s,2) + LAM_3*low(s,3) +
|
||
LAM_4*low(s,4) + LAM_5*low(s,5) + LAM_6*low(s,6)
|
||
sumLC_hi = LAM_1*high(s,1) + LAM_2*high(s,2) + LAM_3*high(s,3) +
|
||
LAM_4*high(s,4) + LAM_5*high(s,5) + LAM_6*high(s,6)
|
||
t_hi_here = high(s, 10)
|
||
t_lo_here = low(s, 10)
|
||
Tref_lo = T_REF_START_C + RAMP_RATE_CS * t_lo_here
|
||
Tref_hi = T_REF_START_C + RAMP_RATE_CS * t_hi_here
|
||
rho_lo_here = KP_HEATUP * (Tref_lo - high(s, 8))
|
||
rho_hi_here = KP_HEATUP * (Tref_hi - low(s, 8))
|
||
rho_lo_ts[k] = rho_lo_here
|
||
rho_hi_ts[k] = rho_hi_here
|
||
denom_lo = BETA - rho_hi_here
|
||
denom_hi = BETA - rho_lo_here
|
||
if denom_lo > 0
|
||
n_lo_ts[k] = LAMBDA * sumLC_lo / denom_hi
|
||
n_hi_ts[k] = LAMBDA * sumLC_hi / denom_lo
|
||
end
|
||
end
|
||
return (; t_arr,
|
||
Tc_lo_ts, Tc_hi_ts, Tf_lo_ts, Tf_hi_ts,
|
||
Tco_lo_ts, Tco_hi_ts, n_lo_ts, n_hi_ts,
|
||
rho_lo_ts, rho_hi_ts)
|
||
end
|
||
|
||
# --- Main ---
|
||
function main()
|
||
default_config = joinpath(@__DIR__, "..", "..", "configs", "heatup", "baseline.toml")
|
||
config_path = length(ARGS) > 0 ? ARGS[1] : default_config
|
||
# Allow a config path relative to repo root or code/.
|
||
if !isfile(config_path)
|
||
alt = joinpath(@__DIR__, "..", "..", config_path)
|
||
isfile(alt) && (config_path = alt)
|
||
end
|
||
config = load_config(config_path)
|
||
|
||
println("\n=== Heatup PJ reach — config: $(config["name"]) ===")
|
||
println(" $(get(config, "description", ""))")
|
||
|
||
X0, entry_info = build_entry_box(config)
|
||
println(" X_entry: n ∈ [$(entry_info.n_lo), $(entry_info.n_hi)], " *
|
||
"T_c ∈ [$(entry_info.T_c_lo), $(entry_info.T_c_hi)] °C")
|
||
|
||
tmjets_cfg = config["tmjets"]
|
||
probes = config["probes"]["horizons_seconds"]
|
||
|
||
results = Dict{Float64, Any}()
|
||
|
||
for T_probe in probes
|
||
println("\n--- Probe T = $T_probe s ($(round(T_probe/60; digits=1)) min) ---")
|
||
sys = BlackBoxContinuousSystem(rhs_heatup_pj_taylor!, 10)
|
||
prob = InitialValueProblem(sys, X0)
|
||
try
|
||
alg = TMJets(
|
||
orderT = tmjets_cfg["orderT"],
|
||
orderQ = tmjets_cfg["orderQ"],
|
||
abstol = tmjets_cfg["abstol"],
|
||
maxsteps = tmjets_cfg["maxsteps"],
|
||
)
|
||
t_start = time()
|
||
sol = solve(prob; T=Float64(T_probe), alg=alg)
|
||
elapsed = time() - t_start
|
||
flow = flowpipe(sol)
|
||
n_sets = length(flow)
|
||
println(" TMJets: $n_sets reach-sets, wall $(round(elapsed; digits=1)) s")
|
||
|
||
flow_hr = overapproximate(flow, Hyperrectangle)
|
||
env = extract_envelopes(flow_hr)
|
||
|
||
println(" n envelope: [$(round(minimum(env.n_lo_ts); sigdigits=4)), $(round(maximum(env.n_hi_ts); sigdigits=4))]")
|
||
println(" T_c envelope: [$(round(minimum(env.Tc_lo_ts); digits=2)), $(round(maximum(env.Tc_hi_ts); digits=2))] °C")
|
||
println(" T_f envelope: [$(round(minimum(env.Tf_lo_ts); digits=2)), $(round(maximum(env.Tf_hi_ts); digits=2))] °C")
|
||
println(" T_cold env: [$(round(minimum(env.Tco_lo_ts); digits=2)), $(round(maximum(env.Tco_hi_ts); digits=2))] °C")
|
||
println(" rho env: [$(round(minimum(env.rho_lo_ts); sigdigits=4)), $(round(maximum(env.rho_hi_ts); sigdigits=4))]")
|
||
|
||
results[T_probe] = (status="OK", n_sets=n_sets, elapsed=elapsed, env=env)
|
||
catch err
|
||
msg = sprint(showerror, err)
|
||
println(" FAILED: ", first(msg, 300))
|
||
results[T_probe] = (status="FAILED", err=first(msg, 300))
|
||
break
|
||
end
|
||
end
|
||
|
||
println("\n=== Summary ===")
|
||
for T_probe in probes
|
||
haskey(results, T_probe) || continue
|
||
r = results[T_probe]
|
||
if r.status == "OK"
|
||
println(" T = $(T_probe) s: OK, $(r.n_sets) sets, $(round(r.elapsed; digits=1))s wall")
|
||
else
|
||
println(" T = $(T_probe) s: FAILED")
|
||
end
|
||
end
|
||
|
||
# --- Save ---
|
||
if get(config, "output", Dict()) |> (o -> get(o, "save_per_step", false))
|
||
result_file = config["output"]["result_file"]
|
||
mat_out = joinpath(@__DIR__, "..", "..", "..", "results", result_file)
|
||
saved = Dict{String, Any}(
|
||
"config_name" => config["name"],
|
||
"probe_horizons" => collect(probes),
|
||
"beta" => BETA,
|
||
"Kp" => KP_HEATUP,
|
||
"T_c0" => T_C0,
|
||
"T_cold0" => T_COLD0,
|
||
"T_standby" => T_STANDBY,
|
||
)
|
||
for T_probe in probes
|
||
haskey(results, T_probe) || continue
|
||
r = results[T_probe]
|
||
r.status == "OK" || continue
|
||
pre = "T_$(Int(T_probe))_"
|
||
env = r.env
|
||
saved[pre * "t_arr"] = env.t_arr
|
||
saved[pre * "Tc_lo_ts"] = env.Tc_lo_ts; saved[pre * "Tc_hi_ts"] = env.Tc_hi_ts
|
||
saved[pre * "Tf_lo_ts"] = env.Tf_lo_ts; saved[pre * "Tf_hi_ts"] = env.Tf_hi_ts
|
||
saved[pre * "Tco_lo_ts"] = env.Tco_lo_ts; saved[pre * "Tco_hi_ts"] = env.Tco_hi_ts
|
||
saved[pre * "n_lo_ts"] = env.n_lo_ts; saved[pre * "n_hi_ts"] = env.n_hi_ts
|
||
saved[pre * "rho_lo_ts"] = env.rho_lo_ts; saved[pre * "rho_hi_ts"] = env.rho_hi_ts
|
||
end
|
||
matwrite(mat_out, saved)
|
||
println("\nSaved per-step envelopes to $mat_out")
|
||
end
|
||
end
|
||
|
||
main()
|