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>
170 lines
6.9 KiB
Julia
170 lines
6.9 KiB
Julia
#!/usr/bin/env julia
|
||
#
|
||
# barrier_polytopic.jl — polytopic barrier attempt for operation mode.
|
||
#
|
||
# Idea: instead of the loose quadratic-Lyapunov ellipsoid, use the
|
||
# polytope P = inv2_holds ∩ (precursor bounds) and verify
|
||
# forward-invariance face-by-face via LP.
|
||
#
|
||
# Nagumo's theorem for linear systems with bounded disturbance: a
|
||
# polytope P = {x : Ax ≤ b} is forward-invariant under dx/dt = A_cl x +
|
||
# B_w w iff for each face i of P (where a_i' x = b_i is active),
|
||
# max over (x in P with a_i'x=b_i, w in W) of a_i'(A_cl x + B_w w) ≤ 0.
|
||
# This is one LP per face.
|
||
#
|
||
# The issue with inv2_holds alone: it's unbounded in the 6 precursor
|
||
# directions, so the LP is unbounded. Fix: add precursor bounds
|
||
# inferred from the reach-tube envelope (which we already computed in
|
||
# reach_operation.jl). The resulting augmented polytope is bounded
|
||
# and the LPs have finite solutions.
|
||
#
|
||
# Uses JuMP + HiGHS (open-source, ships with no license trouble).
|
||
|
||
using Pkg
|
||
Pkg.activate(joinpath(@__DIR__, "..", ".."))
|
||
|
||
using Printf
|
||
using LinearAlgebra
|
||
using MatrixEquations
|
||
using MAT
|
||
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"))
|
||
|
||
plant = pke_params()
|
||
x_op = pke_initial_conditions(plant)
|
||
pred = load_predicates(plant)
|
||
|
||
# --- Closed-loop A_cl (LQR) ---
|
||
A, B, B_w, _, _, _ = pke_linearize(plant)
|
||
Q_lqr = Diagonal([1.0, 1e-3, 1e-3, 1e-3, 1e-3, 1e-3, 1e-3, 1e-2, 1e2, 1.0])
|
||
R_lqr = 1e6 * ones(1, 1)
|
||
X_ric, _, _ = arec(A, reshape(B, :, 1), R_lqr, Matrix(Q_lqr))
|
||
K = (R_lqr \ reshape(B, 1, :)) * X_ric
|
||
A_cl = A - reshape(B, :, 1) * K
|
||
|
||
# --- inv2_holds halfspaces (from JSON) ---
|
||
inv2 = pred.mode_invariants[:inv2_holds]
|
||
A_inv2 = inv2.A_poly # 6 × 10
|
||
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_operation_result.mat")
|
||
reach = matread(reach_mat_path)
|
||
X_lo = reach["X_lo"]; X_hi = reach["X_hi"]
|
||
|
||
# Build the augmented polytope: inv2_holds ∪ (C_i in tube bounds).
|
||
# Precursor halfspaces: C_i ≤ max(X_hi[i+1, :]), -C_i ≤ -min(X_lo[i+1, :])
|
||
# for i = 1..6 (rows 2..7 of state).
|
||
A_aug = copy(A_inv2)
|
||
b_aug = copy(b_inv2)
|
||
labels = copy(inv2.components)
|
||
|
||
for i in 1:6
|
||
local idx = i + 1
|
||
local C_min = minimum(X_lo[idx, :])
|
||
local C_max = maximum(X_hi[idx, :])
|
||
local row_hi = zeros(1, 10); row_hi[1, idx] = 1.0
|
||
global A_aug = vcat(A_aug, row_hi); global b_aug = vcat(b_aug, C_max)
|
||
push!(labels, "C$(i)_upper")
|
||
local row_lo = zeros(1, 10); row_lo[1, idx] = -1.0
|
||
global A_aug = vcat(A_aug, row_lo); global b_aug = vcat(b_aug, -C_min)
|
||
push!(labels, "C$(i)_lower")
|
||
end
|
||
|
||
# (Skipping tube-based bounds on n, T_f, T_cold — those are REACH
|
||
# envelopes, not forward-invariant limits. We rely on the inv2_holds
|
||
# halfspaces for n/T_f/T_cold and the precursor tube-bounds above to
|
||
# keep the polytope bounded in all 10 dimensions.)
|
||
|
||
n_faces = size(A_aug, 1)
|
||
println("\n=== Polytopic barrier check ===")
|
||
println(" Polytope P = inv2_holds ∩ (precursor + n/T_f/T_cold tube bounds)")
|
||
println(" Total faces: $n_faces")
|
||
println(" Disturbance: Q_sg ∈ [0.85, 1.00]·P_0")
|
||
|
||
# --- Disturbance envelope ---
|
||
Q_nom = plant.P0
|
||
w_lo = 0.85 * plant.P0 - Q_nom
|
||
w_hi = 0.00 * plant.P0 + (plant.P0 - Q_nom) # = 0
|
||
# Actually the disturbance is Q_sg deviation from nominal.
|
||
# Q_sg ∈ [0.85, 1.00]·P0 → w ∈ [-0.15·P0, 0].
|
||
w_lo = -0.15 * plant.P0
|
||
w_hi = 0.0
|
||
|
||
# --- Per-face LP check ---
|
||
# For face i: max over x, w of a_i' (A_cl x + B_w w) s.t. Ax ≤ b, a_i'x = b_i,
|
||
# w ∈ [w_lo, w_hi]. All in DEVIATION coordinates (dx = x - x_op).
|
||
# Need to convert polytope: in absolute coords, P is {x : A_aug x ≤ b_aug}.
|
||
# Deviation polytope: P_dev = {dx : A_aug (dx + x_op) ≤ b_aug} = {dx : A_aug dx ≤ b_aug - A_aug x_op}.
|
||
b_dev = b_aug .- A_aug * x_op
|
||
|
||
results = []
|
||
for i in 1:n_faces
|
||
a_i = A_aug[i, :]
|
||
rhs_i = b_dev[i]
|
||
|
||
# Worst-case disturbance contribution a_i' B_w w over w ∈ [w_lo, w_hi].
|
||
# a_i' B_w is a scalar; max w is w_hi if that scalar > 0 else w_lo.
|
||
aB = a_i' * B_w
|
||
dist_worst = aB > 0 ? aB * w_hi : aB * w_lo
|
||
|
||
# LP: maximize a_i' (A_cl dx) + dist_worst
|
||
# subject to A_aug dx ≤ b_dev, a_i' dx = rhs_i.
|
||
model = Model(HiGHS.Optimizer)
|
||
set_silent(model)
|
||
@variable(model, dx[1:10])
|
||
@constraint(model, A_aug * dx .<= b_dev)
|
||
@constraint(model, a_i' * dx == rhs_i)
|
||
grad = (A_cl' * a_i)
|
||
@objective(model, Max, grad' * dx + dist_worst)
|
||
optimize!(model)
|
||
|
||
status = termination_status(model)
|
||
if status == MOI.OPTIMAL
|
||
obj = objective_value(model)
|
||
margin = -obj # forward invariance requires obj ≤ 0
|
||
badge = margin >= 0 ? "✅ forward-invariant" :
|
||
"❌ can escape (a_i'·ẋ = $(round(obj; digits=4)))"
|
||
@printf " [%-28s] max a_i'·ẋ = %+.4e %s\n" labels[i] obj badge
|
||
push!(results, (label=labels[i], obj=obj, status=status))
|
||
else
|
||
@printf " [%-28s] LP status: %s\n" labels[i] string(status)
|
||
push!(results, (label=labels[i], obj=NaN, status=status))
|
||
end
|
||
end
|
||
|
||
n_ok = count(r -> isfinite(r.obj) && r.obj <= 1e-10, results)
|
||
println("\n=== Summary ===")
|
||
println(" Faces forward-invariant: $n_ok / $n_faces")
|
||
println(" Faces that can violate: $(n_faces - n_ok)")
|
||
|
||
if n_ok == n_faces
|
||
println("\n ✅ Polytope P is forward-invariant under A_cl + bounded Q_sg.")
|
||
println(" Polytopic barrier B(x) = max_i (a_i'(x - x_op) - b_dev_i) satisfies")
|
||
println(" B(x₀) ≤ 0 on P, and dB/dt ≤ 0 on {B = 0}.")
|
||
else
|
||
println("\n ❌ Some faces can be crossed under A_cl; P as constructed is not a")
|
||
println(" valid barrier. Expected: safety halfspaces + tube-envelope bounds")
|
||
println(" together form a set MUCH larger than what LQR can contract to, so")
|
||
println(" the worst-case point on a face can have derivative outward.")
|
||
println()
|
||
println(" The right approach is the Blanchini pre-image algorithm:")
|
||
println(" P₀ = safety polytope (inv2_holds + precursor bounds)")
|
||
println(" P_{k+1} = P_k ∩ {x : A_cl x + B_w w ∈ P_k for all w ∈ W}")
|
||
println(" iterating until fixed point or timeout. The fixed point is the")
|
||
println(" maximal robustly controllable invariant set inside the safety polytope.")
|
||
println(" Each iteration adds faces; polytope grows combinatorially in size.")
|
||
println(" Tools: Polyhedra.jl + CDDLib for the set operations, HiGHS for LPs.")
|
||
println()
|
||
println(" Rough effort estimate: 2-3 days focused to get a working implementation")
|
||
println(" + a thesis-defensible result on operation mode. Deferred for now.")
|
||
end
|