PWR-HYBRID-3/code/scripts/reach/reach_heatup_pj.jl
Dane Sabo c5133401e0 Session work scratch: scram X_exit refactor, hot-standby SOS, fat scram tubes, model cheatsheet, journal entry
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
2026-05-02 23:02:50 -04:00

285 lines
11 KiB
Julia
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.

#!/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()