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>
162 lines
6.1 KiB
Julia
162 lines
6.1 KiB
Julia
#!/usr/bin/env julia
|
||
#
|
||
# barrier_sos_2d.jl — SOS polynomial barrier on a 2-state projection.
|
||
#
|
||
# Proof of concept that SumOfSquares.jl + CSDP can fit a polynomial
|
||
# barrier certificate on a reduced version of the operation-mode
|
||
# closed-loop. If this works, scaling to full 10-state is a matter
|
||
# of increasing degree and throughput.
|
||
#
|
||
# Reduced dynamics: project the LQR closed-loop onto (dT_c, dn), the
|
||
# primary safety direction and the dominant unregulated direction.
|
||
# A_red, B_w_red are the 2x2 / 2x1 submatrices corresponding to these
|
||
# components (ignoring cross-coupling into the 8 other states, which is
|
||
# a modeling simplification but keeps the SOS tractable).
|
||
#
|
||
# Safety: |dT_c| ≤ 5 K AND |dn| ≤ 0.15 (i.e. 0.85 ≤ n ≤ 1.15).
|
||
# Entry: |dT_c| ≤ 0.1 AND |dn| ≤ 0.01.
|
||
# Disturbance: Q_sg deviation |dw| ≤ 0.15·P0.
|
||
#
|
||
# Barrier specification (Prajna-Jadbabaie):
|
||
# B(x) ≤ 0 on X_entry
|
||
# B(x) ≥ 0 on X_unsafe (= complement of safety)
|
||
# ∂B/∂x · f(x) ≤ 0 on {B(x) = 0} (for all w in W)
|
||
# Using SOS multipliers σ_i(x), w-dependence via lossless-disturbance bound.
|
||
|
||
using Pkg
|
||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||
|
||
using LinearAlgebra
|
||
using MatrixEquations
|
||
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"))
|
||
|
||
plant = pke_params()
|
||
x_op = pke_initial_conditions(plant)
|
||
|
||
# Full linearization.
|
||
A_full, B_full, B_w_full, _, _, _ = pke_linearize(plant)
|
||
|
||
# Reduced 2x2: rows/cols (1, 9) — n and T_c.
|
||
reduce_idx = [1, 9]
|
||
A_red = A_full[reduce_idx, reduce_idx]
|
||
B_red = B_full[reduce_idx]
|
||
B_w_red = B_w_full[reduce_idx]
|
||
|
||
# LQR on the reduced system. Light weighting on n, heavy on T_c.
|
||
Q_lqr = Diagonal([1.0, 1e2])
|
||
R_lqr = 1e6 * ones(1, 1)
|
||
X_ric, _, _ = arec(A_red, reshape(B_red, :, 1), R_lqr, Matrix(Q_lqr))
|
||
K_red = (R_lqr \ reshape(B_red, 1, :)) * X_ric
|
||
A_cl_red = A_red - reshape(B_red, :, 1) * K_red
|
||
|
||
println("\n=== SOS barrier attempt — 2-state (n, T_c) projection ===")
|
||
println(" A_cl_red =")
|
||
show(stdout, "text/plain", A_cl_red)
|
||
println()
|
||
println(" B_w_red = $B_w_red")
|
||
println(" eigenvalues: ", round.(eigvals(A_cl_red); sigdigits=4))
|
||
println()
|
||
|
||
# --- SOS formulation ---
|
||
# dx = [dn; dTc] = [x[1]; x[2]] in polynomial variables.
|
||
@polyvar x1 x2
|
||
|
||
# Dynamics with worst-case constant w:
|
||
w_bar = 0.15 * plant.P0
|
||
# Split disturbance into its mid + extreme, handle as bounded constant.
|
||
# For the Lie derivative check we use the WORST-CASE w that maximizes
|
||
# the outward velocity. Since B_w_red is a known 2-vector and ∂B/∂x
|
||
# is polynomial in x, the max-over-w is achieved at w ∈ {-w_bar, +w_bar}.
|
||
# Defer that max — check both worst cases separately.
|
||
|
||
f_nom = A_cl_red * [x1; x2] # 2-vector of polynomials in x1, x2
|
||
|
||
# Safety set as intersection of halfspaces g_i ≥ 0:
|
||
# g1 = 5 - x2 (dT_c ≤ 5)
|
||
# g2 = x2 + 5 (dT_c ≥ -5)
|
||
# g3 = 0.15 - x1 (dn ≤ 0.15)
|
||
# g4 = x1 + 0.15 (dn ≥ -0.15)
|
||
# Unsafe set = complement; for SOS we use the Putinar formulation where
|
||
# B ≥ 0 on unsafe. With multiple unsafe regions (each =complement of
|
||
# one safety halfspace) we'd need one constraint per unsafe region.
|
||
# Simpler: pick one unsafe halfspace to focus on — say n >= 1.15
|
||
# (high-flux trip). g_u1 = x1 - 0.15.
|
||
|
||
# Entry set:
|
||
# g_e1 = 0.1 - x2; g_e2 = x2 + 0.1; g_e3 = 0.01 - x1; g_e4 = x1 + 0.01.
|
||
|
||
g_s1 = 5.0 - x2
|
||
g_s2 = x2 + 5.0
|
||
g_s3 = 0.15 - x1
|
||
g_s4 = x1 + 0.15
|
||
g_u_high = x1 - 0.15 # unsafe when n > 1.15 (dn > 0.15)
|
||
g_u_low = -0.15 - x1 # unsafe when n < 0.85 (dn < -0.15)
|
||
g_e1 = 0.1 - x2
|
||
g_e2 = x2 + 0.1
|
||
g_e3 = 0.01 - x1
|
||
g_e4 = x1 + 0.01
|
||
|
||
# --- Build the SOS program ---
|
||
solver = optimizer_with_attributes(CSDP.Optimizer, "printlevel" => 0)
|
||
model = SOSModel(solver)
|
||
|
||
# Barrier polynomial, degree 4.
|
||
monos_B = monomials([x1, x2], 0:4)
|
||
@variable(model, B_poly, Poly(monos_B))
|
||
|
||
# SOS multipliers for each set constraint, degree 2.
|
||
monos_σ = monomials([x1, x2], 0:2)
|
||
|
||
# (1) B ≤ 0 on X_entry: -B - Σᵢ σ_eᵢ · g_eᵢ is SOS.
|
||
@variable(model, σ_e1, SOSPoly(monos_σ))
|
||
@variable(model, σ_e2, SOSPoly(monos_σ))
|
||
@variable(model, σ_e3, SOSPoly(monos_σ))
|
||
@variable(model, σ_e4, SOSPoly(monos_σ))
|
||
@constraint(model, -B_poly - σ_e1*g_e1 - σ_e2*g_e2 - σ_e3*g_e3 - σ_e4*g_e4 in SOSCone())
|
||
|
||
# (2) B ≥ 0 on X_unsafe (using the "high" unsafe region). Include safety
|
||
# constraints so we stay inside the relevant half:
|
||
# B - σ_u_high · g_u_high - σ_u_s2 · g_s2 - σ_u_s3 · (-1) is SOS (dummy)
|
||
# Actually: unsafe-high = {x1 ≥ 0.15} alone (unconstrained in x2).
|
||
# Simplest form:
|
||
@variable(model, σ_u, SOSPoly(monos_σ))
|
||
@constraint(model, B_poly - σ_u * g_u_high in SOSCone())
|
||
|
||
# (3) Lie derivative: ∇B · f ≤ 0 EVERYWHERE (not just on B=0 boundary).
|
||
# Stronger than needed, but keeps the SDP convex. The bilinear
|
||
# Putinar form -(∇B·f) - σ_b·B ≥ SOS requires iterative BMI methods;
|
||
# we skip that for this first attempt and use the stronger "global
|
||
# decrease" condition. If the Hurwitz system admits a quadratic B
|
||
# this should still be solvable.
|
||
dB_dx = [differentiate(B_poly, x1), differentiate(B_poly, x2)]
|
||
# B_w_red is [0, 0] in this projection (Q_sg doesn't directly couple
|
||
# into n or T_c in the linearization), so the disturbance term drops
|
||
# out and the Lie-derivative condition simplifies.
|
||
f_tot = A_cl_red * [x1; x2]
|
||
lie = dB_dx[1] * f_tot[1] + dB_dx[2] * f_tot[2]
|
||
@constraint(model, -lie in SOSCone())
|
||
|
||
# Feasibility problem — no objective needed. Any B that satisfies the
|
||
# three SOS constraints is a valid barrier.
|
||
|
||
println(" Solving SOS program (CSDP)…")
|
||
optimize!(model)
|
||
status = termination_status(model)
|
||
println(" Status: $status")
|
||
if status == MOI.OPTIMAL
|
||
println(" ✅ SOS barrier found.")
|
||
println(" B(x) = ", round(value(B_poly); digits=4))
|
||
elseif status == MOI.INFEASIBLE
|
||
println(" ❌ SOS program infeasible — no degree-4 polynomial B exists")
|
||
println(" with the given sets and dynamics. Try higher degree,")
|
||
println(" larger X_unsafe margin, or different formulation.")
|
||
else
|
||
println(" ⚠ Solver stopped with: $status")
|
||
end
|