PWR-HYBRID-3/code/scripts/barrier/barrier_sos_2d.jl
Dane Sabo 2bbb1871cc 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>
2026-04-21 20:24:48 -04:00

162 lines
6.1 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
#
# 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