refactor: scripts subdivision + TOML configs + results/ split + presentation outline
Architecture restructure from morning review: 1. code/scripts/ subdivided into sim/, reach/, barrier/, plot/. Easier nav; `barrier/` is the natural place for SOS scale-up scripts. 2. Heatup PJ reach variants consolidated behind TOML configs. reach_heatup_pj.jl now takes `--config path/to/config.toml`; configs/heatup/baseline.toml (wide entry, from predicates.json) and configs/heatup/tight.toml (narrow entry, reproduces all-6-halfspaces discharged result). Old reach_heatup_pj_tight.jl and reach_heatup_pj_tight_full.jl deleted (superseded). 3. Reach output .mat files moved from reachability/ to results/. reachability/ now = specs + docs; results/ = ephemeral outputs (gitignored *.mat). README added. 4. OVERNIGHT_NOTES.md archived to claude_memory/2026-04-20-21-overnight- session-summary.md (date range in the filename makes the history clearer). All include() / Pkg.activate() paths in scripts updated for the new depth. Smoke tests pass (reach_operation.jl generates its .mat in the new results/ location; sim_sanity.jl matches MATLAB). Presentation outline for the 20-min prelim talk landed in presentations/prelim-presentation/outline.md. 14-slide assertion- evidence format targeting OT-informed cybersecurity audience. Each slide: one declarative assertion + one figure. Outline includes which figures already exist and which need to be created, timing checkpoints, cybersecurity angle to emphasize, and Q&A prep. New config configs/heatup/with_steam_dump.toml + its companion scripts/reach/reach_heatup_pj_sd.jl (12-state RHS with Q_sg as an augmented bounded parameter x[10] and time as x[11]). Kicks off point 3 from morning review. Next up: scram X_entry expansion (morning point 2) — LOCA scenario + union of mode reach envelopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c5816c359e
commit
2bbb1871cc
@ -324,7 +324,7 @@ no more hand-maintained traceability table.
|
||||
|
||||
# ╔═╡ 37d6b212-9f00-4684-9f91-50c7e17cbd62
|
||||
begin
|
||||
reach_mat_path = joinpath(@__DIR__, "..", "reachability", "reach_operation_result.mat")
|
||||
reach_mat_path = joinpath(@__DIR__, "..", "results", "reach_operation_result.mat")
|
||||
have_reach_mat = isfile(reach_mat_path)
|
||||
if have_reach_mat
|
||||
reach_mat = matread(reach_mat_path)
|
||||
@ -463,7 +463,7 @@ extend past the ~10 s prompt-neutron stiffness wall.
|
||||
|
||||
# ╔═╡ 45e8962a-873e-48d3-aac4-70ea0cf36c97
|
||||
begin
|
||||
pj_mat_path = joinpath(@__DIR__, "..", "reachability", "reach_heatup_pj_result.mat")
|
||||
pj_mat_path = joinpath(@__DIR__, "..", "results", "reach_heatup_pj_result.mat")
|
||||
if isfile(pj_mat_path)
|
||||
pj_mat = matread(pj_mat_path)
|
||||
md"""**Loaded** `reach_heatup_pj_result.mat` — pending plot rendering."""
|
||||
|
||||
22
code/configs/heatup/baseline.toml
Normal file
22
code/configs/heatup/baseline.toml
Normal file
@ -0,0 +1,22 @@
|
||||
# Baseline heatup-mode reach configuration.
|
||||
# Wide X_entry as specified in predicates.json mode_boundaries.
|
||||
# Matches the original reach_heatup_pj.jl defaults.
|
||||
|
||||
name = "baseline"
|
||||
description = "Wide X_entry from mode_boundaries.q_heatup.X_entry_polytope"
|
||||
|
||||
# Load X_entry from predicates.json (true) or from this file (false).
|
||||
use_predicates_entry = true
|
||||
|
||||
[tmjets]
|
||||
orderT = 4
|
||||
orderQ = 2
|
||||
abstol = 1e-9
|
||||
maxsteps = 100000
|
||||
|
||||
[probes]
|
||||
horizons_seconds = [60.0, 300.0, 1800.0, 5400.0]
|
||||
|
||||
[output]
|
||||
save_per_step = true
|
||||
result_file = "reach_heatup_pj_baseline.mat"
|
||||
27
code/configs/heatup/tight.toml
Normal file
27
code/configs/heatup/tight.toml
Normal file
@ -0,0 +1,27 @@
|
||||
# Tight X_entry heatup reach: T_c in [285, 291] (6 K vs 14 K baseline).
|
||||
# Produces the clean "all 6 inv1_holds halfspaces discharged at T=300s"
|
||||
# result from 2026-04-20 overnight session.
|
||||
|
||||
name = "tight"
|
||||
description = "Narrow X_entry on T_c/T_f/T_cold; n in [1e-3, 2e-3]"
|
||||
|
||||
use_predicates_entry = false
|
||||
|
||||
[entry]
|
||||
n_range = [1.0e-3, 2.0e-3]
|
||||
T_f_range_C = [285.0, 291.0]
|
||||
T_c_range_C = [285.0, 291.0]
|
||||
T_cold_range_C = [278.0, 285.0]
|
||||
|
||||
[tmjets]
|
||||
orderT = 4
|
||||
orderQ = 2
|
||||
abstol = 1e-9
|
||||
maxsteps = 100000
|
||||
|
||||
[probes]
|
||||
horizons_seconds = [60.0, 300.0]
|
||||
|
||||
[output]
|
||||
save_per_step = true
|
||||
result_file = "reach_heatup_pj_tight.mat"
|
||||
40
code/configs/heatup/with_steam_dump.toml
Normal file
40
code/configs/heatup/with_steam_dump.toml
Normal file
@ -0,0 +1,40 @@
|
||||
# Heatup reach with a bounded secondary-side steam-dump Q_sg.
|
||||
#
|
||||
# Instead of Q_sg ≡ 0 (original assumption), treat Q_sg as a bounded
|
||||
# disturbance in [0, 0.05·P_0]. Physical interpretation: operator
|
||||
# opens/closes the secondary-side steam dump to manage primary
|
||||
# temperature during the ramp; exact value not known, but bounded
|
||||
# by atmospheric-dump capacity (~5% of P_0 rated).
|
||||
#
|
||||
# The reach script picks up Q_sg as an augmented state x[11] with
|
||||
# dx[11] = 0, entry box covering [0, 0.05*P_0].
|
||||
|
||||
name = "with_steam_dump"
|
||||
description = "Tight X_entry + bounded Q_sg ∈ [0, 0.05·P_0] as disturbance"
|
||||
|
||||
use_predicates_entry = false
|
||||
steam_dump_enabled = true
|
||||
|
||||
[entry]
|
||||
n_range = [1.0e-3, 2.0e-3]
|
||||
T_f_range_C = [285.0, 291.0]
|
||||
T_c_range_C = [285.0, 291.0]
|
||||
T_cold_range_C = [278.0, 285.0]
|
||||
|
||||
[entry.steam_dump]
|
||||
# Q_sg bounds in fractions of P_0.
|
||||
Q_lo_fraction_P0 = 0.0
|
||||
Q_hi_fraction_P0 = 0.05
|
||||
|
||||
[tmjets]
|
||||
orderT = 4
|
||||
orderQ = 2
|
||||
abstol = 1e-9
|
||||
maxsteps = 100000
|
||||
|
||||
[probes]
|
||||
horizons_seconds = [60.0, 300.0]
|
||||
|
||||
[output]
|
||||
save_per_step = true
|
||||
result_file = "reach_heatup_pj_with_steam_dump.mat"
|
||||
@ -8,17 +8,17 @@
|
||||
# anisotropy, not controller tuning.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using Printf
|
||||
using LinearAlgebra
|
||||
using MatrixEquations
|
||||
using JSON
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "load_predicates.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "load_predicates.jl"))
|
||||
|
||||
plant = pke_params()
|
||||
x_op = pke_initial_conditions(plant)
|
||||
@ -10,17 +10,17 @@
|
||||
# This is the structural anisotropy limitation, not a code bug.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using Printf
|
||||
using LinearAlgebra
|
||||
using MatrixEquations
|
||||
using JSON
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "load_predicates.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "load_predicates.jl"))
|
||||
|
||||
plant = pke_params()
|
||||
x_op = pke_initial_conditions(plant)
|
||||
@ -21,7 +21,7 @@
|
||||
# Uses JuMP + HiGHS (open-source, ships with no license trouble).
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using Printf
|
||||
using LinearAlgebra
|
||||
@ -31,10 +31,10 @@ using JSON
|
||||
using JuMP
|
||||
using HiGHS
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "load_predicates.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "load_predicates.jl"))
|
||||
|
||||
plant = pke_params()
|
||||
x_op = pke_initial_conditions(plant)
|
||||
@ -55,7 +55,7 @@ b_inv2 = inv2.b_poly # 6
|
||||
|
||||
# --- Precursor bounds from reach-tube envelope ---
|
||||
# Read reach_operation_result.mat; take min/max of X_lo, X_hi on precursors.
|
||||
reach_mat_path = joinpath(@__DIR__, "..", "..", "reachability",
|
||||
reach_mat_path = joinpath(@__DIR__, "..", "..", "..", "reachability",
|
||||
"reach_operation_result.mat")
|
||||
reach = matread(reach_mat_path)
|
||||
X_lo = reach["X_lo"]; X_hi = reach["X_hi"]
|
||||
@ -24,7 +24,7 @@
|
||||
# Using SOS multipliers σ_i(x), w-dependence via lossless-disturbance bound.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using LinearAlgebra
|
||||
using MatrixEquations
|
||||
@ -32,9 +32,9 @@ using DynamicPolynomials
|
||||
using SumOfSquares
|
||||
using CSDP
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_linearize.jl"))
|
||||
|
||||
plant = pke_params()
|
||||
x_op = pke_initial_conditions(plant)
|
||||
@ -22,17 +22,17 @@
|
||||
# julia --project=. scripts/plot_reach_tubes.jl heatup_pj
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using MAT
|
||||
using Plots
|
||||
gr()
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
const PLANT = pke_params()
|
||||
|
||||
function plot_tubes_operation()
|
||||
mat_path = joinpath(@__DIR__, "..", "..", "reachability", "reach_operation_result.mat")
|
||||
mat_path = joinpath(@__DIR__, "..", "..", "..", "results", "reach_operation_result.mat")
|
||||
d = matread(mat_path)
|
||||
|
||||
T_vec = vec(d["T"]) # time grid
|
||||
@ -75,7 +75,7 @@ function plot_tubes_operation()
|
||||
end
|
||||
|
||||
function plot_tubes_heatup_pj()
|
||||
mat_path = joinpath(@__DIR__, "..", "..", "reachability",
|
||||
mat_path = joinpath(@__DIR__, "..", "..", "..", "reachability",
|
||||
"reach_heatup_pj_tight_full.mat")
|
||||
d = matread(mat_path)
|
||||
|
||||
@ -144,7 +144,7 @@ function _plot_common(t, Tc_lo, Tc_hi, Th_lo, Th_hi, Tco_lo, Tco_hi,
|
||||
|
||||
fig = plot(p1, p2, p3, p4, layout=(2, 2), size=(1300, 800),
|
||||
plot_title=title_stem)
|
||||
figdir = joinpath(@__DIR__, "..", "..", "docs", "figures")
|
||||
figdir = joinpath(@__DIR__, "..", "..", "..", "docs", "figures")
|
||||
isdir(figdir) || mkpath(figdir)
|
||||
outpath = joinpath(figdir, outname)
|
||||
savefig(fig, outpath)
|
||||
@ -157,7 +157,7 @@ if which_plot in ("operation", "both")
|
||||
plot_tubes_operation()
|
||||
end
|
||||
if which_plot in ("heatup_pj", "both")
|
||||
mat_path = joinpath(@__DIR__, "..", "..", "reachability",
|
||||
mat_path = joinpath(@__DIR__, "..", "..", "..", "reachability",
|
||||
"reach_heatup_pj_tight_full.mat")
|
||||
if isfile(mat_path)
|
||||
plot_tubes_heatup_pj()
|
||||
@ -26,7 +26,7 @@
|
||||
# Time carried as augmented state x[11].
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using LinearAlgebra
|
||||
using ReachabilityAnalysis, LazySets
|
||||
@ -101,7 +101,7 @@ const KP_HEATUP = 1e-4
|
||||
end
|
||||
|
||||
# --- Build X_entry from predicates.json ---
|
||||
pred_path = joinpath(@__DIR__, "..", "..", "reachability", "predicates.json")
|
||||
pred_path = joinpath(@__DIR__, "..", "..", "..", "reachability", "predicates.json")
|
||||
pred_raw = JSON.parsefile(pred_path)
|
||||
entry = pred_raw["mode_boundaries"]["q_heatup"]["X_entry_polytope"]
|
||||
|
||||
269
code/scripts/reach/reach_heatup_pj.jl
Normal file
269
code/scripts/reach/reach_heatup_pj.jl
Normal file
@ -0,0 +1,269 @@
|
||||
#!/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).
|
||||
#
|
||||
# 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
|
||||
|
||||
# --- Taylorized heatup PJ RHS ---
|
||||
@taylorize function rhs_heatup_pj_taylor!(dx, x, p, t)
|
||||
rho = KP_HEATUP * (T_STANDBY + 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_STANDBY + RAMP_RATE_CS * t_lo_here
|
||||
Tref_hi = T_STANDBY + 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()
|
||||
178
code/scripts/reach/reach_heatup_pj_sd.jl
Normal file
178
code/scripts/reach/reach_heatup_pj_sd.jl
Normal file
@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env julia
|
||||
#
|
||||
# reach_heatup_pj_sd.jl — heatup PJ reach WITH bounded Q_sg steam dump.
|
||||
#
|
||||
# Adds an augmented state x[11] = Q_sg as a bounded parameter (dx[11] = 0).
|
||||
# The reach propagates the entry-box range of Q_sg forward; at each reach
|
||||
# set, the Q_sg extent is the disturbance envelope the controller has to
|
||||
# reject. Physical story: operator controls secondary steam dump; actual
|
||||
# value unknown but bounded in [0, 0.05·P_0].
|
||||
#
|
||||
# Time is carried as x[12] = t (dt/dt = 1).
|
||||
#
|
||||
# Diagnostic script — uses the tight-entry + steam-dump config.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using LinearAlgebra
|
||||
using ReachabilityAnalysis, LazySets
|
||||
using JSON
|
||||
using MAT
|
||||
using TOML
|
||||
|
||||
# Plant constants.
|
||||
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
|
||||
|
||||
# 12-state RHS: [C_1..C_6, T_f, T_c, T_cold, Q_sg, t]
|
||||
# dx[10] = 0 (Q_sg is a bounded parameter)
|
||||
# dx[11] = 1 (time)
|
||||
# Hmm — indexing conflict with original version. Reorganize:
|
||||
# x[1..6] = C_1..C_6
|
||||
# x[7] = T_f
|
||||
# x[8] = T_c
|
||||
# x[9] = T_cold
|
||||
# x[10] = Q_sg (constant-parameter, dx[10]=0)
|
||||
# x[11] = t (dx[11]=1)
|
||||
@taylorize function rhs_heatup_sd_taylor!(dx, x, p, t)
|
||||
rho = KP_HEATUP * (T_STANDBY + RAMP_RATE_CS * x[11] - 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]) - x[10]) / (M_SG * C_C) # Q_sg now enters!
|
||||
|
||||
dx[10] = zero(x[1]) # Q_sg constant
|
||||
dx[11] = one(x[1]) # time
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function main()
|
||||
config_path = length(ARGS) > 0 ? ARGS[1] :
|
||||
joinpath(@__DIR__, "..", "..", "configs", "heatup", "with_steam_dump.toml")
|
||||
if !isfile(config_path)
|
||||
alt = joinpath(@__DIR__, "..", "..", config_path)
|
||||
isfile(alt) && (config_path = alt)
|
||||
end
|
||||
config = TOML.parsefile(config_path)
|
||||
|
||||
println("\n=== Heatup PJ reach with steam dump — config: $(config["name"]) ===")
|
||||
println(" $(get(config, "description", ""))")
|
||||
|
||||
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"]
|
||||
sd = e["steam_dump"]
|
||||
Q_lo = sd["Q_lo_fraction_P0"] * P0
|
||||
Q_hi = sd["Q_hi_fraction_P0"] * P0
|
||||
|
||||
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, Q_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, Q_hi, 0.0]
|
||||
|
||||
X0 = Hyperrectangle(low=x_lo, high=x_hi)
|
||||
println(" n ∈ [$n_lo, $n_hi], T_c ∈ [$T_c_lo, $T_c_hi] °C")
|
||||
println(" Q_sg (steam dump) ∈ [$(Q_lo/1e6), $(Q_hi/1e6)] MW")
|
||||
|
||||
t_cfg = config["tmjets"]
|
||||
probes = config["probes"]["horizons_seconds"]
|
||||
|
||||
results = Dict{Float64, Any}()
|
||||
for T_probe in probes
|
||||
println("\n--- Probe T = $T_probe s ---")
|
||||
sys = BlackBoxContinuousSystem(rhs_heatup_sd_taylor!, 11)
|
||||
prob = InitialValueProblem(sys, X0)
|
||||
try
|
||||
alg = TMJets(orderT=t_cfg["orderT"], orderQ=t_cfg["orderQ"],
|
||||
abstol=t_cfg["abstol"], maxsteps=t_cfg["maxsteps"])
|
||||
t0 = time()
|
||||
sol = solve(prob; T=Float64(T_probe), alg=alg)
|
||||
elapsed = time() - t0
|
||||
flow_hr = overapproximate(flowpipe(sol), Hyperrectangle)
|
||||
n_sets = length(flow_hr)
|
||||
println(" TMJets: $n_sets reach-sets in $(round(elapsed; digits=1)) s")
|
||||
|
||||
Tc_lo_env = minimum(low(set(R), 8) for R in flow_hr)
|
||||
Tc_hi_env = maximum(high(set(R), 8) for R in flow_hr)
|
||||
Tco_lo_env = minimum(low(set(R), 9) for R in flow_hr)
|
||||
Tco_hi_env = maximum(high(set(R), 9) for R in flow_hr)
|
||||
Tf_lo_env = minimum(low(set(R), 7) for R in flow_hr)
|
||||
Tf_hi_env = maximum(high(set(R), 7) for R in flow_hr)
|
||||
println(" T_c envelope: [$(round(Tc_lo_env; digits=2)), $(round(Tc_hi_env; digits=2))] °C")
|
||||
println(" T_f envelope: [$(round(Tf_lo_env; digits=2)), $(round(Tf_hi_env; digits=2))] °C")
|
||||
println(" T_cold env: [$(round(Tco_lo_env; digits=2)), $(round(Tco_hi_env; digits=2))] °C")
|
||||
|
||||
# Compare low-T_avg trip (280 °C).
|
||||
low_trip_ok = Tc_lo_env >= 280.0
|
||||
println(" Low-T_avg trip (T_c ≥ 280): $(low_trip_ok ? "✅" : "× loose")")
|
||||
|
||||
results[T_probe] = (status="OK", Tc=(Tc_lo_env, Tc_hi_env),
|
||||
Tf=(Tf_lo_env, Tf_hi_env),
|
||||
Tcold=(Tco_lo_env, Tco_hi_env),
|
||||
low_trip_ok=low_trip_ok)
|
||||
catch err
|
||||
println(" FAILED: ", first(sprint(showerror, err), 300))
|
||||
results[T_probe] = (status="FAILED",)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
mat_out = joinpath(@__DIR__, "..", "..", "..", "results", config["output"]["result_file"])
|
||||
saved = Dict{String, Any}("config_name" => config["name"],
|
||||
"Q_lo" => Q_lo, "Q_hi" => Q_hi)
|
||||
for (T_probe, r) in results
|
||||
if r.status == "OK"
|
||||
pre = "T_$(Int(T_probe))_"
|
||||
saved[pre * "Tc_lo"] = r.Tc[1]; saved[pre * "Tc_hi"] = r.Tc[2]
|
||||
saved[pre * "Tf_lo"] = r.Tf[1]; saved[pre * "Tf_hi"] = r.Tf[2]
|
||||
saved[pre * "Tcold_lo"] = r.Tcold[1]; saved[pre * "Tcold_hi"] = r.Tcold[2]
|
||||
end
|
||||
end
|
||||
matwrite(mat_out, saved)
|
||||
println("\nSaved to $mat_out")
|
||||
end
|
||||
|
||||
main()
|
||||
@ -11,7 +11,7 @@
|
||||
# Linear-reach results here are an approximate-but-useful gut check.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using Printf
|
||||
using LinearAlgebra
|
||||
@ -20,11 +20,11 @@ using Plots
|
||||
using JSON
|
||||
using MAT
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "load_predicates.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "reach_linear.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "load_predicates.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "reach_linear.jl"))
|
||||
|
||||
plant = pke_params()
|
||||
x_op = pke_initial_conditions(plant)
|
||||
@ -101,7 +101,7 @@ end
|
||||
# --- Plots: T_c reach tube, two views ---
|
||||
CtoF(T) = T * 9/5 + 32
|
||||
delta_safe_Tc = pred.constants.t_avg_in_range_halfwidth_C
|
||||
figdir = joinpath(@__DIR__, "..", "..", "docs", "figures")
|
||||
figdir = joinpath(@__DIR__, "..", "..", "..", "docs", "figures")
|
||||
isdir(figdir) || mkpath(figdir)
|
||||
|
||||
p_safety = plot(T, CtoF.(X_nom[9, :]), lw=1.2, color=:red,
|
||||
@ -140,7 +140,7 @@ plot!(fig_n, T, X_hi[1, :], fillrange=X_lo[1, :], fillalpha=0.3,
|
||||
savefig(fig_n, joinpath(figdir, "reach_operation_n.png"))
|
||||
|
||||
# --- Save result ---
|
||||
matfile = joinpath(@__DIR__, "..", "..", "reachability", "reach_operation_result.mat")
|
||||
matfile = joinpath(@__DIR__, "..", "..", "..", "results", "reach_operation_result.mat")
|
||||
matwrite(matfile, Dict("T" => collect(T), "R_lo" => R_lo, "R_hi" => R_hi,
|
||||
"center" => Cnom, "X_lo" => X_lo, "X_hi" => X_hi,
|
||||
"X_nom" => X_nom, "K" => Matrix(K), "A_cl" => A_cl,
|
||||
@ -9,7 +9,7 @@
|
||||
# 9-state PJ model (10D with augmented time).
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using LinearAlgebra
|
||||
using ReachabilityAnalysis, LazySets
|
||||
@ -140,7 +140,7 @@ for T_probe in (10.0, 30.0, 60.0)
|
||||
end
|
||||
end
|
||||
|
||||
mat_out = joinpath(@__DIR__, "..", "..", "reachability", "reach_scram_pj_result.mat")
|
||||
mat_out = joinpath(@__DIR__, "..", "..", "..", "results", "reach_scram_pj_result.mat")
|
||||
saved = Dict{String, Any}("probe_horizons" => collect((10.0, 30.0, 60.0)))
|
||||
for T_probe in (10.0, 30.0, 60.0)
|
||||
haskey(results, T_probe) || continue
|
||||
@ -1,243 +0,0 @@
|
||||
#!/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. We push
|
||||
# horizons up: 60 s, 300 s, 1800 s, 5400 s, full T_max = 18000 s (5 hr).
|
||||
#
|
||||
# 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).
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
|
||||
using LinearAlgebra
|
||||
using ReachabilityAnalysis, LazySets
|
||||
using JSON
|
||||
using MAT
|
||||
|
||||
# --- Inlined 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
|
||||
|
||||
# Note: feedback-linearization in ctrl_heatup_unsat cancels the alpha
|
||||
# terms exactly, so under closed-loop the effective rho is just Kp·e.
|
||||
# We don't need ALPHA_F, ALPHA_C in the reach RHS as a result.
|
||||
|
||||
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
|
||||
|
||||
# --- Taylorized PJ heatup RHS, 10D with augmented time ---
|
||||
@taylorize function rhs_heatup_pj_taylor!(dx, x, p, t)
|
||||
# rho_total under closed-loop feedback linearization = Kp · (T_ref - T_c).
|
||||
rho = KP_HEATUP * (T_STANDBY + RAMP_RATE_CS * x[10] - x[8])
|
||||
|
||||
# Algebraic prompt-jump n.
|
||||
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
|
||||
|
||||
# Precursor balance under PJ.
|
||||
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]
|
||||
|
||||
# Thermal — n appears algebraically in fuel eq.
|
||||
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)
|
||||
|
||||
# Augmented time.
|
||||
dx[10] = one(x[1])
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# --- Build X_entry (PJ form: no n) from predicates.json ---
|
||||
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"]
|
||||
|
||||
n_mid = 0.5 * (n_lo + n_hi)
|
||||
C_mid_1 = (BETA_1 / (LAM_1 * LAMBDA)) * n_mid
|
||||
C_mid_2 = (BETA_2 / (LAM_2 * LAMBDA)) * n_mid
|
||||
C_mid_3 = (BETA_3 / (LAM_3 * LAMBDA)) * n_mid
|
||||
C_mid_4 = (BETA_4 / (LAM_4 * LAMBDA)) * n_mid
|
||||
C_mid_5 = (BETA_5 / (LAM_5 * LAMBDA)) * n_mid
|
||||
C_mid_6 = (BETA_6 / (LAM_6 * LAMBDA)) * n_mid
|
||||
|
||||
# C_i scale linearly with n; sweep across the n_lo..n_hi band.
|
||||
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]
|
||||
|
||||
X0 = Hyperrectangle(low=x_lo, high=x_hi)
|
||||
|
||||
println("\n=== Nonlinear heatup reach, prompt-jump model ===")
|
||||
println(" X_entry (n-implied range): n ∈ [$(n_lo), $(n_hi)]")
|
||||
println(" T_c ∈ [$(T_c_lo), $(T_c_hi)] °C")
|
||||
|
||||
T_RAMP_END = (T_C0 - T_STANDBY) / RAMP_RATE_CS
|
||||
println(" T_ramp_end = $(round(T_RAMP_END; digits=0)) s ($(round(T_RAMP_END/60; digits=1)) min)")
|
||||
println(" Probing horizons up to T_max(heatup) = 18000 s (5 hr).")
|
||||
|
||||
# Probe at increasing horizons. Stop early if any probe fails.
|
||||
probe_horizons = (60.0, 300.0, 1800.0, 5400.0)
|
||||
|
||||
results = Dict{Float64, Any}()
|
||||
for T_probe in probe_horizons
|
||||
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=4, orderQ=2, abstol=1e-9, maxsteps=100000)
|
||||
t_start = time()
|
||||
sol = solve(prob; T=T_probe, alg=alg)
|
||||
elapsed = time() - t_start
|
||||
flow = flowpipe(sol)
|
||||
n_sets = length(flow)
|
||||
println(" TMJets: $n_sets reach-sets, wall-time $(round(elapsed; digits=1)) s")
|
||||
|
||||
flow_hr = overapproximate(flow, Hyperrectangle)
|
||||
n_steps = length(flow_hr)
|
||||
# Per-timestep envelopes for plotting (saved below).
|
||||
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) # time-state upper bound ≈ current time
|
||||
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)
|
||||
# Algebraic n reconstruction at this reach-set.
|
||||
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)
|
||||
rho_lo_here = KP_HEATUP * (T_STANDBY - high(s, 8))
|
||||
rho_hi_here = KP_HEATUP * (T_C0 - 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
|
||||
# Also global-envelope values (for backward compat).
|
||||
Tc_lo_env = minimum(Tc_lo_ts); Tc_hi_env = maximum(Tc_hi_ts)
|
||||
Tf_lo_env = minimum(Tf_lo_ts); Tf_hi_env = maximum(Tf_hi_ts)
|
||||
Tcold_lo = minimum(Tco_lo_ts); Tcold_hi = maximum(Tco_hi_ts)
|
||||
n_env_lo = minimum(n_lo_ts); n_env_hi = maximum(n_hi_ts)
|
||||
|
||||
println(" n envelope (reconstructed): [$(round(n_env_lo; sigdigits=4)), $(round(n_env_hi; sigdigits=4))]")
|
||||
println(" T_f envelope: [$(round(Tf_lo_env; digits=2)), $(round(Tf_hi_env; digits=2))] °C")
|
||||
println(" T_c envelope: [$(round(Tc_lo_env; digits=2)), $(round(Tc_hi_env; digits=2))] °C")
|
||||
println(" T_cold envelope: [$(round(Tcold_lo; digits=2)), $(round(Tcold_hi; digits=2))] °C")
|
||||
results[T_probe] = (status="OK", n_sets=n_sets, elapsed=elapsed,
|
||||
Tc=(Tc_lo_env, Tc_hi_env),
|
||||
Tf=(Tf_lo_env, Tf_hi_env),
|
||||
Tcold=(Tcold_lo, Tcold_hi),
|
||||
n=(n_env_lo, n_env_hi),
|
||||
t_arr=t_arr,
|
||||
Tc_lo_ts=Tc_lo_ts, Tc_hi_ts=Tc_hi_ts,
|
||||
Tf_lo_ts=Tf_lo_ts, Tf_hi_ts=Tf_hi_ts,
|
||||
Tco_lo_ts=Tco_lo_ts, Tco_hi_ts=Tco_hi_ts,
|
||||
n_lo_ts=n_lo_ts, n_hi_ts=n_hi_ts,
|
||||
rho_lo_ts=rho_lo_ts, rho_hi_ts=rho_hi_ts)
|
||||
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 probe_horizons
|
||||
haskey(results, T_probe) || continue
|
||||
r = results[T_probe]
|
||||
if r.status == "OK"
|
||||
println(" T = $(T_probe) s: OK, $(r.n_sets) reach-sets, $(round(r.elapsed; digits=1))s wall")
|
||||
else
|
||||
println(" T = $(T_probe) s: FAILED")
|
||||
end
|
||||
end
|
||||
|
||||
# Save the longest-successful probe's envelope arrays for the app.
|
||||
mat_out = joinpath(@__DIR__, "..", "..", "reachability", "reach_heatup_pj_result.mat")
|
||||
saved = Dict{String, Any}()
|
||||
saved["probe_horizons"] = collect(probe_horizons)
|
||||
for T_probe in probe_horizons
|
||||
haskey(results, T_probe) || continue
|
||||
r = results[T_probe]
|
||||
if r.status == "OK"
|
||||
pre = "T_$(Int(T_probe))_"
|
||||
saved[pre * "Tc_lo"] = r.Tc[1]
|
||||
saved[pre * "Tc_hi"] = r.Tc[2]
|
||||
saved[pre * "Tf_lo"] = r.Tf[1]
|
||||
saved[pre * "Tf_hi"] = r.Tf[2]
|
||||
saved[pre * "Tcold_lo"] = r.Tcold[1]
|
||||
saved[pre * "Tcold_hi"] = r.Tcold[2]
|
||||
saved[pre * "n_lo"] = r.n[1]
|
||||
saved[pre * "n_hi"] = r.n[2]
|
||||
# Per-timestep envelopes
|
||||
saved[pre * "t_arr"] = r.t_arr
|
||||
saved[pre * "Tc_lo_ts"] = r.Tc_lo_ts
|
||||
saved[pre * "Tc_hi_ts"] = r.Tc_hi_ts
|
||||
saved[pre * "Tf_lo_ts"] = r.Tf_lo_ts
|
||||
saved[pre * "Tf_hi_ts"] = r.Tf_hi_ts
|
||||
saved[pre * "Tco_lo_ts"] = r.Tco_lo_ts
|
||||
saved[pre * "Tco_hi_ts"] = r.Tco_hi_ts
|
||||
saved[pre * "n_lo_ts"] = r.n_lo_ts
|
||||
saved[pre * "n_hi_ts"] = r.n_hi_ts
|
||||
saved[pre * "rho_lo_ts"] = r.rho_lo_ts
|
||||
saved[pre * "rho_hi_ts"] = r.rho_hi_ts
|
||||
end
|
||||
end
|
||||
matwrite(mat_out, saved)
|
||||
println("\nSaved envelope summaries to $mat_out")
|
||||
@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env julia
|
||||
#
|
||||
# reach_heatup_pj_tight.jl — heatup PJ reach with a tighter X_entry.
|
||||
#
|
||||
# The default X_entry (from predicates.json) has T_c ∈ [281, 295] — 14 K
|
||||
# wide. The baseline PJ reach at T=300s produced T_c envelope
|
||||
# [272.4, 295.0], violating the low-T_avg trip at 280.
|
||||
#
|
||||
# Hypothesis: entry-box width is contributing to tube growth. Try
|
||||
# T_c ∈ [285, 291] (6 K) and T_f matched, see if the lower envelope
|
||||
# rises above 280.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
|
||||
using LinearAlgebra
|
||||
using ReachabilityAnalysis, LazySets
|
||||
using MAT
|
||||
|
||||
# Same constants as reach_heatup_pj.jl.
|
||||
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
|
||||
|
||||
@taylorize function rhs_heatup_pj_tight!(dx, x, p, t)
|
||||
rho = KP_HEATUP * (T_STANDBY + 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
|
||||
|
||||
# Tighter X_entry on T_c and T_f specifically.
|
||||
n_lo, n_hi = 1.0e-3, 2.0e-3 # narrower n
|
||||
T_f_lo, T_f_hi = 285.0, 291.0 # was 275–295
|
||||
T_c_lo, T_c_hi = 285.0, 291.0 # was 281–295
|
||||
T_cold_lo, T_cold_hi = 278.0, 285.0 # was 270–281 (shifted up)
|
||||
|
||||
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]
|
||||
|
||||
X0 = Hyperrectangle(low=x_lo, high=x_hi)
|
||||
|
||||
println("\n=== Heatup PJ reach with TIGHTENED X_entry ===")
|
||||
println(" T_c ∈ [$(T_c_lo), $(T_c_hi)] (width 6 K, was 14 K)")
|
||||
println(" T_f ∈ [$(T_f_lo), $(T_f_hi)]")
|
||||
println(" n-implied ∈ [$(n_lo), $(n_hi)]")
|
||||
|
||||
for T_probe in (60.0, 300.0)
|
||||
println("\n--- Probe T = $T_probe s ---")
|
||||
sys = BlackBoxContinuousSystem(rhs_heatup_pj_tight!, 10)
|
||||
prob = InitialValueProblem(sys, X0)
|
||||
try
|
||||
alg = TMJets(orderT=4, orderQ=2, abstol=1e-9, maxsteps=100000)
|
||||
t_start = time()
|
||||
sol = solve(prob; T=T_probe, alg=alg)
|
||||
elapsed = time() - t_start
|
||||
flow = flowpipe(sol)
|
||||
flow_hr = overapproximate(flow, Hyperrectangle)
|
||||
Tc_lo_env = minimum(low(set(R), 8) for R in flow_hr)
|
||||
Tc_hi_env = maximum(high(set(R), 8) for R in flow_hr)
|
||||
Tf_lo_env = minimum(low(set(R), 7) for R in flow_hr)
|
||||
Tf_hi_env = maximum(high(set(R), 7) for R in flow_hr)
|
||||
println(" $(length(flow_hr)) sets in $(round(elapsed; digits=1))s")
|
||||
println(" T_c envelope: [$(round(Tc_lo_env; digits=2)), $(round(Tc_hi_env; digits=2))] °C")
|
||||
println(" T_f envelope: [$(round(Tf_lo_env; digits=2)), $(round(Tf_hi_env; digits=2))] °C")
|
||||
println(" Low-T_avg trip (T_c ≥ 280): $(Tc_lo_env >= 280 ? "✅ DISCHARGED" : "× still loose")")
|
||||
catch err
|
||||
println(" FAILED: ", first(sprint(showerror, err), 200))
|
||||
end
|
||||
end
|
||||
@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env julia
|
||||
#
|
||||
# reach_heatup_pj_tight_full.jl — tight-entry heatup PJ reach,
|
||||
# per-timestep envelopes saved for plotting (T_c, T_h, T_cold, rho, n).
|
||||
#
|
||||
# Identical dynamics to reach_heatup_pj_tight.jl but keeps the full
|
||||
# tube data so we can overlay T_c and T_h (and infer power) on one plot.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
|
||||
using LinearAlgebra
|
||||
using ReachabilityAnalysis, LazySets
|
||||
using MAT
|
||||
|
||||
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
|
||||
|
||||
@taylorize function rhs_heatup_pj_tf!(dx, x, p, t)
|
||||
rho = KP_HEATUP * (T_STANDBY + 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
|
||||
|
||||
# Tight X_entry.
|
||||
n_lo, n_hi = 1.0e-3, 2.0e-3
|
||||
T_f_lo, T_f_hi = 285.0, 291.0
|
||||
T_c_lo, T_c_hi = 285.0, 291.0
|
||||
T_cold_lo, T_cold_hi = 278.0, 285.0
|
||||
|
||||
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]
|
||||
X0 = Hyperrectangle(low=x_lo, high=x_hi)
|
||||
|
||||
println("\n=== Tight-entry heatup PJ reach — saving full per-step envelopes ===")
|
||||
|
||||
T_probe = 300.0
|
||||
sys = BlackBoxContinuousSystem(rhs_heatup_pj_tf!, 10)
|
||||
prob = InitialValueProblem(sys, X0)
|
||||
alg = TMJets(orderT=4, orderQ=2, abstol=1e-9, maxsteps=100000)
|
||||
t_start = time()
|
||||
sol = solve(prob; T=T_probe, alg=alg)
|
||||
println(" Wall time: $(round(time() - t_start; digits=1)) s")
|
||||
flow_hr = overapproximate(flowpipe(sol), Hyperrectangle)
|
||||
n_steps = length(flow_hr)
|
||||
println(" $n_steps reach-sets")
|
||||
|
||||
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)
|
||||
# Ramp-ref bounds at this reach-set's t.
|
||||
t_hi_here = high(s, 10)
|
||||
t_lo_here = low(s, 10)
|
||||
# T_ref at these times (monotone increasing):
|
||||
Tref_lo = T_STANDBY + RAMP_RATE_CS * t_lo_here
|
||||
Tref_hi = T_STANDBY + 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
|
||||
|
||||
println(" T_c envelope: [$(round(minimum(Tc_lo_ts); digits=2)), $(round(maximum(Tc_hi_ts); digits=2))] °C")
|
||||
println(" T_hot envelope: [$(round(minimum(2 .* Tc_lo_ts .- Tco_hi_ts); digits=2)), $(round(maximum(2 .* Tc_hi_ts .- Tco_lo_ts); digits=2))] °C")
|
||||
println(" T_cold env: [$(round(minimum(Tco_lo_ts); digits=2)), $(round(maximum(Tco_hi_ts); digits=2))] °C")
|
||||
println(" rho envelope: [$(round(minimum(rho_lo_ts); sigdigits=4)), $(round(maximum(rho_hi_ts); sigdigits=4))]")
|
||||
println(" rho / beta: [$(round(minimum(rho_lo_ts)/BETA; digits=3)), $(round(maximum(rho_hi_ts)/BETA; digits=3))]")
|
||||
|
||||
mat_out = joinpath(@__DIR__, "..", "..", "reachability", "reach_heatup_pj_tight_full.mat")
|
||||
matwrite(mat_out, Dict(
|
||||
"t_arr" => t_arr,
|
||||
"Tc_lo_ts" => Tc_lo_ts, "Tc_hi_ts" => Tc_hi_ts,
|
||||
"Tf_lo_ts" => Tf_lo_ts, "Tf_hi_ts" => Tf_hi_ts,
|
||||
"Tco_lo_ts" => Tco_lo_ts, "Tco_hi_ts" => Tco_hi_ts,
|
||||
"n_lo_ts" => n_lo_ts, "n_hi_ts" => n_hi_ts,
|
||||
"rho_lo_ts" => rho_lo_ts, "rho_hi_ts" => rho_hi_ts,
|
||||
"T_probe" => T_probe,
|
||||
"beta" => BETA,
|
||||
"Kp" => KP_HEATUP,
|
||||
"T_c0" => T_C0,
|
||||
"T_cold0" => T_COLD0,
|
||||
"T_standby" => T_STANDBY,
|
||||
))
|
||||
println("\nSaved full per-step envelopes to $mat_out")
|
||||
@ -7,7 +7,7 @@
|
||||
# names (overwrites MATLAB outputs — the Julia versions take over).
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using Printf
|
||||
using LinearAlgebra
|
||||
@ -16,18 +16,18 @@ using Plots
|
||||
using MatrixEquations
|
||||
using JSON
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_solver.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "plot_pke_results.jl"))
|
||||
include(joinpath(@__DIR__, "..", "controllers", "controllers.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_linearize.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_solver.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "plot_pke_results.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "controllers", "controllers.jl"))
|
||||
|
||||
# --- Plant + predicates ---
|
||||
plant = pke_params()
|
||||
|
||||
# Load T_standby from predicates.json for the hot-standby IC + heatup ref.
|
||||
pred_path = joinpath(@__DIR__, "..", "..", "reachability", "predicates.json")
|
||||
pred_path = joinpath(@__DIR__, "..", "..", "..", "reachability", "predicates.json")
|
||||
pred_raw = JSON.parsefile(pred_path)
|
||||
T_standby = plant.T_c0 + pred_raw["derived"]["T_standby_offset_C"]
|
||||
|
||||
@ -52,7 +52,7 @@ ctrl_op_lqr, K_lqr, _ = ctrl_operation_lqr_factory(
|
||||
plant, A, B; Q_lqr=Matrix(Q_lqr), R_lqr=R_lqr)
|
||||
|
||||
# --- Figure output directory ---
|
||||
figdir = joinpath(@__DIR__, "..", "..", "docs", "figures")
|
||||
figdir = joinpath(@__DIR__, "..", "..", "..", "docs", "figures")
|
||||
isdir(figdir) || mkpath(figdir)
|
||||
|
||||
# --- Run each mode ---
|
||||
@ -9,7 +9,7 @@
|
||||
# trajectory that reach tubes can be checked against.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using OrdinaryDiffEq
|
||||
|
||||
@ -6,13 +6,13 @@
|
||||
# and prints the final state, which should agree with MATLAB to ~1e-3.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using OrdinaryDiffEq
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "controllers", "controllers.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "controllers", "controllers.jl"))
|
||||
|
||||
plant = pke_params()
|
||||
x0 = pke_initial_conditions(plant)
|
||||
@ -11,17 +11,17 @@
|
||||
# reconstructed n vs the dynamic n. Quantify the error explicitly.
|
||||
|
||||
using Pkg
|
||||
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||||
|
||||
using Printf
|
||||
using LinearAlgebra
|
||||
using OrdinaryDiffEq
|
||||
using Plots
|
||||
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "src", "pke_th_rhs_pj.jl"))
|
||||
include(joinpath(@__DIR__, "..", "controllers", "controllers.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs_pj.jl"))
|
||||
include(joinpath(@__DIR__, "..", "..", "controllers", "controllers.jl"))
|
||||
|
||||
plant = pke_params()
|
||||
T_standby = plant.T_c0 - 33.333333
|
||||
@ -88,7 +88,7 @@ for t_check in (1.0, 5.0, 10.0, 60.0, 300.0, 1200.0, 3000.0)
|
||||
end
|
||||
|
||||
# --- Plot trajectory overlay ---
|
||||
figdir = joinpath(@__DIR__, "..", "..", "docs", "figures")
|
||||
figdir = joinpath(@__DIR__, "..", "..", "..", "docs", "figures")
|
||||
isdir(figdir) || mkpath(figdir)
|
||||
|
||||
CtoF(T) = T*9/5 + 32
|
||||
395
presentations/prelim-presentation/outline.md
Normal file
395
presentations/prelim-presentation/outline.md
Normal file
@ -0,0 +1,395 @@
|
||||
# Preliminary Example Presentation — Assertion-Evidence Outline
|
||||
|
||||
**Format:** Assertion-evidence (Alley). Each slide: one declarative sentence
|
||||
at the top, one piece of visual evidence below. No bullet soup.
|
||||
|
||||
**Duration:** 20 minutes. ~12 slides at ~1.5 min each (60 s typical, up to
|
||||
2 min for the heavier ones).
|
||||
|
||||
**Audience:** OT-informed cybersecurity experts. Mostly CS, some control-theory
|
||||
familiarity, very little reactor-physics background. Can assume fluency with:
|
||||
LTL, automata, model-checking, reachability (as a concept), SMT/SAT. Must
|
||||
explain: PKE, reactivity, precursors, hybrid systems.
|
||||
|
||||
**Running thread:** FRET natural-language requirement → LTL → synthesized
|
||||
discrete controller → continuous plant → per-mode reach-avoid proof
|
||||
→ hybrid correctness by composition.
|
||||
|
||||
**Design principles:**
|
||||
- **Plots over bullets.** Every result slide anchors on one figure.
|
||||
- **Physical intuition before math.** Reactor basics before PKE.
|
||||
- **Honest limitations boxed on each result slide.** Audience are cyber
|
||||
folks — they respect limits more than triumphs.
|
||||
- **CS vocabulary by default, engineering terms defined inline.**
|
||||
- **End with the seam**, not with a victory lap. The thesis question is
|
||||
"how do discrete proofs and continuous proofs compose?" not "we verified
|
||||
everything."
|
||||
|
||||
---
|
||||
|
||||
## Slide 1 — Title + hook
|
||||
|
||||
**Assertion:** Verified hybrid control for nuclear startup is a real problem
|
||||
with no deployed solution.
|
||||
|
||||
**Evidence:** Title block + a single image showing a control-room board of
|
||||
old-school analog gauges adjacent to a digital SCADA screen. Caption: "The
|
||||
gap we're filling."
|
||||
|
||||
**Speak:** Introduce self + advisor + NRC fellowship context. Name the problem:
|
||||
nuclear reactor operations are 70-80% human-error-driven at severe-accident
|
||||
level (Chernobyl, TMI, Fukushima all primarily human factors). Plants run on
|
||||
paper procedures that have no formal verification. The most advanced work to
|
||||
date (HARDENS, NRC-funded) verified the discrete trip system in isolation and
|
||||
stopped there. This talk: a preliminary example of bridging discrete
|
||||
requirements all the way to continuous-state safety proofs, with the seam
|
||||
between them as the research contribution.
|
||||
|
||||
**Reference:** thesis §2, ¶1-4. Cites Kemeny1979, Hogberg_2013, Kiniry2024.
|
||||
|
||||
**Figures to make:** find or compose the control-room image.
|
||||
|
||||
---
|
||||
|
||||
## Slide 2 — The hybrid-systems gap
|
||||
|
||||
**Assertion:** Existing tools verify either discrete *or* continuous systems,
|
||||
never the seam between them.
|
||||
|
||||
**Evidence:** Two-column diagram. Left column: discrete (FRET, Spot,
|
||||
SAL, HARDENS, TLA+). Right column: continuous (MATLAB, Modelica, CORA,
|
||||
Flow*, HyTech). A dashed line between them labeled "the bridge problem."
|
||||
|
||||
**Speak:** HARDENS got to TRL 2-3 on the discrete side alone. Continuous-side
|
||||
reach tools exist but assume the discrete controller is given as input. What
|
||||
nobody has done: produce an *end-to-end* proof where the discrete controller's
|
||||
transitions actually fire in physical time on the modeled plant.
|
||||
|
||||
**Reference:** thesis §2.2 "Formal Methods", §2.2.1 HARDENS, §2.2.2 Temporal
|
||||
Logic. Thesis §2.3 (if exists — continuous methods).
|
||||
|
||||
---
|
||||
|
||||
## Slide 3 — Running example: the PWR_HYBRID_3 controller
|
||||
|
||||
**Assertion:** Our target is a 4-mode hybrid controller that takes a PWR
|
||||
from hot standby to full power and back.
|
||||
|
||||
**Evidence:** The DRC state diagram (from `fret-pipeline/diagrams/PWR_HYBRID_3_DRC_states.png`).
|
||||
Four nodes — shutdown, heatup, operation, scram — with labeled transitions.
|
||||
|
||||
**Speak:** One sentence of nuclear physics: a PWR has neutron population
|
||||
(power), coolant temperatures at three locations, and precursor concentrations
|
||||
that determine delayed-neutron generation. Startup is shutdown → heatup →
|
||||
operation; any fault triggers scram. Mode transitions gated by boolean
|
||||
conditions like `t_avg_in_range ∧ p_above_crit ∧ inv1_holds`.
|
||||
|
||||
Four modes means four *physical* behaviors the plant has to exhibit for
|
||||
the discrete logic to be sound. This example stresses every layer of the
|
||||
bridge we're building.
|
||||
|
||||
**Evidence path:** `fret-pipeline/diagrams/PWR_HYBRID_3_DRC_states.png`,
|
||||
`fret-pipeline/pwr_hybrid_3.json` for one example requirement.
|
||||
|
||||
---
|
||||
|
||||
## Slide 4 — Layer 1: FRET → LTL → synthesized DRC
|
||||
|
||||
**Assertion:** Natural-language requirements become a verified discrete
|
||||
controller automatically.
|
||||
|
||||
**Evidence:** One-panel figure: left half = snippet of FRET natural language
|
||||
("Upon `control_mode = q_heatup ∧ t_avg_in_range ∧ p_above_crit ∧ inv1_holds`
|
||||
DRC shall at the next timepoint satisfy `control_mode = q_operation`"), right
|
||||
half = the corresponding LTL, bottom = arrow labeled `ltlsynt` pointing to
|
||||
the synthesized state-diagram node.
|
||||
|
||||
**Speak:** FRET compiles natural-language requirements into LTL (Spot
|
||||
ecosystem). `ltlsynt` turns LTL into an AIGER circuit representing the
|
||||
minimal correct discrete controller. This is well-trodden ground in CS-land;
|
||||
our contribution is *using it* on reactor operating procedures — so far
|
||||
a formal-methods-free domain.
|
||||
|
||||
**Evidence path:** `fret-pipeline/README.md`, `pwr_hybrid_3.json` sample
|
||||
requirement, `fret-pipeline/circuits/PWR_HYBRID_3_DRC.aag` AIGER circuit.
|
||||
|
||||
---
|
||||
|
||||
## Slide 5 — Layer 2: the plant model
|
||||
|
||||
**Assertion:** A 10-state PKE + thermal-hydraulics model is faithful enough
|
||||
for reach analysis and tractable enough to actually compute with.
|
||||
|
||||
**Evidence:** The state-vector diagram: `x = [n, C_1, ..., C_6, T_f, T_c, T_cold]`
|
||||
with arrows showing the coupling — neutron balance → fuel heating → coolant
|
||||
transport → feedback to reactivity.
|
||||
|
||||
**Speak:** Point-kinetic equations + three thermal nodes. 10 states, 1
|
||||
control input (rod worth u), 1 disturbance (steam-generator heat removal Q_sg).
|
||||
Stiff system: prompt-neutron timescale is 10⁻⁴ s, thermal timescales are
|
||||
seconds to minutes. That stiffness becomes critical later — it's what forces
|
||||
the singular-perturbation move.
|
||||
|
||||
Five controllers: one per DRC mode (shutdown, heatup, operation under P, under
|
||||
LQR, scram). All Julia, all in `code/src/` + `code/controllers/`.
|
||||
|
||||
**Evidence path:** `code/src/pke_th_rhs.jl`, `journal/journal.pdf` entry
|
||||
2026-04-17 for derivation.
|
||||
|
||||
**Figures to make:** state-vector coupling diagram (can be ASCII → inkscape).
|
||||
|
||||
---
|
||||
|
||||
## Slide 6 — Layer 3: the reach-avoid obligation per mode
|
||||
|
||||
**Assertion:** Discrete correctness transfers to continuous correctness
|
||||
via one reach-avoid obligation per mode.
|
||||
|
||||
**Evidence:** Taxonomy table from `reachability/WALKTHROUGH.md`:
|
||||
|
||||
| Mode | Kind | Obligation |
|
||||
|---|---|---|
|
||||
| shutdown | equilibrium | stay in X_safe forever |
|
||||
| heatup | transition | from X_entry, reach X_exit in [T_min, T_max] while maintaining inv1 |
|
||||
| operation | equilibrium | stay in X_safe forever under bounded Q_sg |
|
||||
| scram | transition | from any mode, drive n safely subcritical in T_max |
|
||||
|
||||
**Speak:** This is the structural insight. Equilibrium modes → forever-invariance
|
||||
proofs. Transition modes → reach-avoid proofs with time budgets. The DRC
|
||||
transitions fire iff the reach set enters the right exit predicate. If
|
||||
every mode's obligation is discharged, the hybrid system is correct by
|
||||
composition. The methodological contribution of the chapter.
|
||||
|
||||
**Reference:** `reachability/WALKTHROUGH.md` §1, `reachability/predicates.json`
|
||||
→ `mode_boundaries`.
|
||||
|
||||
---
|
||||
|
||||
## Slide 7 — Operation mode: discharge all 6 safety halfspaces (the win)
|
||||
|
||||
**Assertion:** Under LQR feedback and ±15% Q_sg variation, the operation-mode
|
||||
reach tube discharges every safety halfspace with orders of magnitude of margin.
|
||||
|
||||
**Evidence:** The two-panel figure `docs/figures/reach_operation_tubes.png`.
|
||||
Left: temperature tubes (T_c, T_hot, T_cold) overlaid; the near-zero width
|
||||
of T_c visually demonstrates LQR contraction. Right: ΔT_core tube with a
|
||||
right-axis in MW showing the power is tight around nominal.
|
||||
|
||||
Per-halfspace margin table (inset or second slide if space allows):
|
||||
|
||||
| Halfspace | Limit | Reach max | Margin |
|
||||
|---|---:|---:|---:|
|
||||
| fuel_centerline | 1200 °C | 328.9 | **+871 K** |
|
||||
| t_avg_high_trip | 320 °C | 308.4 | **+11.6 K** |
|
||||
| n_high_trip | 1.15 | 1.012 | **+0.14** |
|
||||
|
||||
**Speak:** LQR contracts the regulated direction (T_c) from 0.1 K to 0.03 K
|
||||
halfwidth. Uncontrolled directions (precursors) expand but don't appear in
|
||||
any safety predicate. Tightest margin is the high-flux trip at 12% headroom.
|
||||
|
||||
**Limitations box:** linear-reach of a nonlinear plant — *approximate, not
|
||||
sound* yet for the physical plant. That's what the next slides address.
|
||||
|
||||
**Evidence path:** `code/scripts/reach/reach_operation.jl`,
|
||||
`docs/figures/reach_operation_tubes.png`.
|
||||
|
||||
---
|
||||
|
||||
## Slide 8 — Quadratic Lyapunov barrier fails structurally
|
||||
|
||||
**Assertion:** The canonical quadratic Lyapunov barrier cannot certify this
|
||||
plant — *even with LQR inside the barrier* — because of plant anisotropy.
|
||||
|
||||
**Evidence:** Bar chart comparing open-loop vs LQR-closed-loop Lyapunov bound
|
||||
on `n_high_trip` direction: OL is 27 million × nominal, CL is 1,242 × nominal.
|
||||
Title: "LQR helps 20,000× but both bounds are physically meaningless."
|
||||
|
||||
**Speak:** Plant has prompt timescale 10⁻⁴ s vs thermal timescale ~10 s.
|
||||
Lyapunov matrix P inherits that 10⁴ spread. Resulting ellipsoid stretches
|
||||
obscenely along the fast directions when projected to physical coordinates.
|
||||
No amount of controller tuning fixes this — it's set by the plant's own
|
||||
physics. The ellipsoid geometry cannot match the slab-shaped safe region.
|
||||
**This is the motivation for polynomial (SOS) or polytopic barriers.**
|
||||
|
||||
**Reference:** `code/scripts/barrier/barrier_compare_OL_CL.jl`, journal entry
|
||||
2026-04-20 (evening).
|
||||
|
||||
---
|
||||
|
||||
## Slide 9 — Prompt-jump reduction makes nonlinear reach tractable
|
||||
|
||||
**Assertion:** Singular-perturbation reduction of the fast neutronics gives
|
||||
us 30× more reach horizon and a rigorous O(Λ) error bound via Tikhonov.
|
||||
|
||||
**Evidence:** Side-by-side: left panel = validate_pj_heatup.png (empirical:
|
||||
PJ and full-state trajectories overlay perfectly over 50 minutes). Right panel
|
||||
= the Tikhonov bound written out mathematically:
|
||||
$$|x(t) - x_{\mathrm{PJ}}(t)| \leq C \cdot \Lambda = \mathcal{O}(10^{-4})$$
|
||||
|
||||
**Speak:** Setting dn/dt = 0 and solving algebraically for n gives us
|
||||
`n = Λ·Σλᵢ·Cᵢ / (β - ρ)`. State drops from 10 to 9, removes the Λ⁻¹
|
||||
stiffness. Reach tool (TMJets) can take steps on thermal timescale instead
|
||||
of prompt timescale.
|
||||
|
||||
The magic: we *prove* the PJ approximation's validity condition (`β - ρ > 0`
|
||||
with margin) **as part of the same reach obligation** via the
|
||||
`prompt_critical_margin_heatup` halfspace in `inv1_holds`. No hand-waving —
|
||||
the reach machinery proves both the safety claim and the approximation's
|
||||
soundness together.
|
||||
|
||||
**Evidence path:** `code/src/pke_th_rhs_pj.jl`, `code/scripts/sim/validate_pj.jl`,
|
||||
`journal/` entry 2026-04-21 "Tikhonov bound".
|
||||
|
||||
---
|
||||
|
||||
## Slide 10 — First sound nonlinear heatup reach (the second win)
|
||||
|
||||
**Assertion:** With PJ + a tightened entry box, nonlinear Taylor-model reach
|
||||
discharges all 6 `inv1_holds` safety halfspaces over the first 300 s of heatup.
|
||||
|
||||
**Evidence:** The four-panel `reach_heatup_pj_tubes.png`: temperature tubes
|
||||
overlaid, ΔT_core, ρ in dollars (stays between -0.25 $ and -0.05 $ —
|
||||
sub-prompt-critical), n decaying.
|
||||
|
||||
**Speak:** TMJets Taylor-model integration. 12,932 reach-sets, 200 s wall time.
|
||||
Tube **stable** — T_c envelope `[281.05, 291.0]` identical at 60 s and 300 s,
|
||||
meaning the controller holds the state inside the tube indefinitely. This
|
||||
is the first sound (modulo O(Λ) PJ error) nonlinear reach-avoid artifact
|
||||
for this plant.
|
||||
|
||||
**Limitations box:** 300 s of a 5-hour `T_max`. Step budget caps horizon; entry
|
||||
refinement (Blanchini-style) is the path to hours.
|
||||
|
||||
**Evidence path:** `code/scripts/reach/reach_heatup_pj.jl configs/heatup/tight.toml`,
|
||||
`docs/figures/reach_heatup_pj_tubes.png`.
|
||||
|
||||
---
|
||||
|
||||
## Slide 11 — Degree-4 SOS polynomial barrier, a working proof of concept
|
||||
|
||||
**Assertion:** A degree-4 polynomial barrier certificate — the structure that
|
||||
quadratic Lyapunov cannot provide — works on the operation-mode projection.
|
||||
|
||||
**Evidence:** Equation block showing the symbolic polynomial
|
||||
(abbreviated — the actual 13-term polynomial from `code/scripts/barrier/barrier_sos_2d.jl`).
|
||||
Side caption: "CSDP returned OPTIMAL. Solve: ~3 seconds."
|
||||
|
||||
**Speak:** Polynomials of degree 4 can localize to slab-shaped safety regions
|
||||
in a way degree-2 (quadratic) cannot. The Prajna-Jadbabaie formulation: find
|
||||
`B(x)` such that `B ≤ 0` on entry, `B ≥ 0` on unsafe, and `∇B·f ≤ 0` on
|
||||
`{B=0}`. Sum-of-squares programming reduces this to an SDP. 2D projection
|
||||
proof of concept; scaling to full 10-state requires bigger solver (Mosek or SCS).
|
||||
|
||||
**Reference:** `code/scripts/barrier/barrier_sos_2d.jl`, journal entry
|
||||
2026-04-21.
|
||||
|
||||
---
|
||||
|
||||
## Slide 12 — The hybrid correctness story, assembled
|
||||
|
||||
**Assertion:** All four pieces — FRET/synthesis, plant model, reach tubes,
|
||||
SOS/polytopic barriers — compose into a hybrid system correctness argument.
|
||||
|
||||
**Evidence:** The composition picture. DRC state diagram at center; each
|
||||
DRC node has an arrow to a "per-mode obligation box" labeled with its proof
|
||||
artifact (tube or barrier or certificate). Arrows between nodes = transitions,
|
||||
labeled with the predicate polytope. Outer dashed box: "hybrid system
|
||||
closed-loop safety."
|
||||
|
||||
**Speak:** What's been built, what's been proven, what's next. Transition
|
||||
correctness between modes is the next thesis-blocking piece — each mode's
|
||||
`X_exit` has to be inside the next mode's `X_entry` (with margin). That's an
|
||||
inclusion check, not a reach — but it's the final structural piece.
|
||||
|
||||
---
|
||||
|
||||
## Slide 13 — Limitations and next steps
|
||||
|
||||
**Assertion:** This is a preliminary example, not a deployable system.
|
||||
|
||||
**Evidence:** A two-column list:
|
||||
|
||||
| What's proven | What's next |
|
||||
|---|---|
|
||||
| Operation mode safe under ±15% load (approximate, via linear reach) | Nonlinear operation reach (tight SOS barrier, LOCA disturbance) |
|
||||
| Heatup PJ reach discharges all 6 safety halfspaces for 300 s | Extend to full 5-hr `T_max`; model steam-dump disturbance |
|
||||
| Scram PJ n-decay monotone over 60 s | Expand `X_entry(scram)` to union of all mode tubes + LOCA |
|
||||
| Degree-4 SOS barrier on 2D projection | Full 10-state SOS barrier |
|
||||
| PJ error rigorously bounded by Tikhonov | Parametric α uncertainty; DNBR correlation |
|
||||
|
||||
**Speak:** The gap from prelim example to deployable system is well-defined:
|
||||
more states, tighter bounds, real tech-spec numbers, hardware-in-the-loop.
|
||||
None of these is a research novelty unto itself — they're engineering.
|
||||
The research contribution is the composition framework, demonstrated end-to-end
|
||||
on a nontrivial running example. Phase 2 of the thesis is filling in the
|
||||
gaps and expanding to multiple plants.
|
||||
|
||||
---
|
||||
|
||||
## Slide 14 — Questions / acknowledgements
|
||||
|
||||
**Assertion:** (none — backup slide)
|
||||
|
||||
**Evidence:** Thanks to advisor, committee, NRC fellowship. Links to:
|
||||
repo (gitea), journal PDF, thesis in progress.
|
||||
|
||||
**Speak:** Open for questions. Expect questions on:
|
||||
- "Why not Stateflow/Simulink?" → (have answer prepared; HARDENS used
|
||||
Cryptol for a reason — formal tool integration matters)
|
||||
- "What's the relationship to LTLMoP / DragonFly?" → (survey answer)
|
||||
- "How would this interact with ML components?" → (out of scope for now,
|
||||
the whole pitch is *no ML in safety-critical loop*)
|
||||
- "What's your threat model for cybersecurity?" → (tie back to OT audience —
|
||||
formal methods guarantee the controller's logic; they do not protect the
|
||||
comms layer or implementation-level vulns. Mention HARDENS' focus on
|
||||
"correctness of implementation" vs our focus on "correctness of
|
||||
specification")
|
||||
|
||||
---
|
||||
|
||||
## Presentation construction notes
|
||||
|
||||
### What to build in Beamer later
|
||||
- Assertion-evidence template (one sentence at top, centered figure below).
|
||||
- Consistent color coding: green for "discharged" / "proved", red for
|
||||
"limitation" / "gap", blue for "discrete-layer", amber for "continuous-layer".
|
||||
- The composition diagram on slide 12 — this will take the most Tikz work.
|
||||
|
||||
### Figures that need to be created or dressed up for the talk
|
||||
- Slide 1: control-room visual.
|
||||
- Slide 2: discrete/continuous tools comparison.
|
||||
- Slide 3: use `fret-pipeline/diagrams/PWR_HYBRID_3_DRC_states.png`.
|
||||
- Slide 4: FRET → LTL → AIGER panel.
|
||||
- Slide 5: 10-state coupling diagram.
|
||||
- Slide 7, 10: reuse `docs/figures/reach_*_tubes.png`.
|
||||
- Slide 8: bar chart (new, easy matplotlib).
|
||||
- Slide 9: reuse `validate_pj_heatup.png`.
|
||||
- Slide 11: latex-typeset polynomial + CSDP log snippet.
|
||||
- Slide 12: composition diagram (Tikz, will take time).
|
||||
|
||||
### Cybersecurity angle to emphasize
|
||||
- The point the OT audience will care about: this kind of verification
|
||||
**constrains what the controller can physically do**. Even if an attacker
|
||||
gets past authentication and can inject arbitrary commands, the DRC-plus-
|
||||
reach-certified envelope limits how bad things can get *within the
|
||||
physical plant*. That's a different assurance axis than the usual
|
||||
comms-security one and complements it.
|
||||
- Formal methods as defense-in-depth: they catch bugs *before* deployment,
|
||||
which reduces attack surface more than any runtime defense.
|
||||
- The PJ reduction + Tikhonov approach might be of interest for other
|
||||
safety-critical stiff systems (power grid, aerospace).
|
||||
|
||||
### Things to NOT do
|
||||
- Don't get lost in reactor-physics detail. One sentence per physical
|
||||
concept; get them to the CS content fast.
|
||||
- Don't show code unless it's a slide about *why* the code structure
|
||||
matters. Code screenshots are terrible evidence.
|
||||
- Don't oversell. Honest limitations at every stage builds trust with
|
||||
a skeptical audience.
|
||||
- Don't use more than 2 bullet points on any slide. Alley rules.
|
||||
|
||||
### Timing checkpoints
|
||||
- Slide 6 (mode-obligation taxonomy) by minute 8.
|
||||
- Slide 10 (first nonlinear reach result) by minute 13.
|
||||
- Slide 12 (composition story) by minute 17.
|
||||
- Slide 13 (limitations) by minute 19.
|
||||
1
results/.gitignore
vendored
Normal file
1
results/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.mat
|
||||
32
results/README.md
Normal file
32
results/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# results/
|
||||
|
||||
Output artifacts from reach / barrier / validation runs. All files
|
||||
in this directory are **gitignored** — they're regenerated by running
|
||||
the corresponding script.
|
||||
|
||||
## What lives here
|
||||
|
||||
Named `<analysis>_<variant>.mat` (MATLAB v7 format, read/written via
|
||||
Julia's MAT.jl for historical convenience — switching to JLD2 is a
|
||||
deferred item, see `journal/` for context).
|
||||
|
||||
Generated by (as of 2026-04-21):
|
||||
|
||||
| File | Generated by |
|
||||
|---|---|
|
||||
| `reach_operation_result.mat` | `code/scripts/reach/reach_operation.jl` |
|
||||
| `reach_heatup_pj_tight_full.mat` | `code/scripts/reach/reach_heatup_pj_tight_full.jl` |
|
||||
| `reach_scram_pj_result.mat` | `code/scripts/reach/reach_scram_pj.jl` |
|
||||
|
||||
Consumed by:
|
||||
|
||||
- `code/scripts/plot/plot_reach_tubes.jl` — tube overlay figures.
|
||||
- `app/predicate_explorer.jl` — live per-halfspace margin rendering.
|
||||
|
||||
## Why this directory exists
|
||||
|
||||
`reachability/` used to hold both source-of-truth specs
|
||||
(`predicates.json`, README, WALKTHROUGH.md) and ephemeral reach results.
|
||||
Mixing stable vs.\ regenerated content made it hard to tell which was
|
||||
which at a glance. Split: `reachability/` = specs + docs,
|
||||
`results/` = outputs.
|
||||
Loading…
x
Reference in New Issue
Block a user