Full toolchain port. Numerical equivalence verified against MATLAB:
- main_mode_sweep.jl: every mode's final state matches MATLAB to 3-4 dp
- reach_operation.jl: per-halfspace margins match MATLAB exactly
- barrier_lyapunov.jl: per-halfspace bounds match (best Qbar from sweep
yields max|dT_c| = 33.228 K either side)
- barrier_compare_OL_CL.jl: OL gamma 1.038e13, CL gamma 1.848e4
matching the MATLAB result; LQR helps by ~20,000x on every halfspace.
Phase summary:
Phase 1: pke_solver.jl, plot_pke_results.jl (Plots.jl), main_mode_sweep.jl
Phase 2: reach_linear.jl, reach_operation.jl, barrier_lyapunov.jl,
barrier_compare_OL_CL.jl, load_predicates.jl
Phase 3 (this commit): delete plant-model/ entirely, delete reach
code from reachability/ keeping predicates.json + docs,
git mv julia-port/ -> code/, update root README + CLAUDE,
write code/CLAUDE.md and code/README.md, update reach
README + WALKTHROUGH file paths, journal preamble note
that pre-port entries reference MATLAB paths.
Why now: prompt-neutron stiffness in nonlinear reach made it clear we
need TMJets, which is Julia. Already had the Julia plant model
working and matching MATLAB. Two languages = two sources of truth =
two places to drift. One language, one truth.
Manifest.toml gitignored. .mat results gitignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
3.1 KiB
Julia
83 lines
3.1 KiB
Julia
#!/usr/bin/env julia
|
|
#
|
|
# barrier_compare_OL_CL.jl — head-to-head Lyapunov barrier bounds with
|
|
# and without LQR feedback. Julia port of reachability/barrier_compare_OL_CL.m.
|
|
#
|
|
# Expected: LQR improves every bound by ~20,000x, but bounds stay
|
|
# physically meaningless. Point is to show the ceiling is plant
|
|
# anisotropy, not controller tuning.
|
|
|
|
using Pkg
|
|
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"))
|
|
|
|
plant = pke_params()
|
|
x_op = pke_initial_conditions(plant)
|
|
pred = load_predicates(plant)
|
|
|
|
A, B, B_w, _, _, _ = pke_linearize(plant)
|
|
|
|
# LQR gain (same weights as ctrl_operation_lqr).
|
|
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
|
|
|
|
# Shared Qbar — best from the earlier sweep.
|
|
Qbar = Diagonal([1.0, 1e-4, 1e-4, 1e-4, 1e-4, 1e-4, 1e-4, 1.0, 1e2, 1.0])
|
|
w_bar = 0.15 * plant.P0
|
|
|
|
delta_entry = [0.01 * x_op[1];
|
|
0.001 .* abs.(x_op[2:7]);
|
|
0.1; 0.1; 0.1]
|
|
|
|
inv2 = pred.mode_invariants[:inv2_holds]
|
|
A_inv = inv2.A_poly
|
|
b_inv = inv2.b_poly
|
|
comps = inv2.components
|
|
b_dev = b_inv .- A_inv * x_op
|
|
|
|
function run_case(Acase)
|
|
P = lyapc(Matrix(Acase)', Matrix(Qbar))
|
|
Ph = Hermitian(sqrt(Hermitian(P)))
|
|
Phi = inv(Ph)
|
|
mu = minimum(eigvals(Symmetric(Phi * Matrix(Qbar) * Phi)))
|
|
g_bound = sqrt(B_w' * P * B_w)
|
|
c_inv = (2 * w_bar * g_bound / mu)^2
|
|
c_entry = delta_entry' * abs.(P) * delta_entry
|
|
gamma = max(c_inv, c_entry)
|
|
Pinv = inv(P)
|
|
maxima = [sqrt(gamma * (A_inv[k, :]' * Pinv * A_inv[k, :])) for k in 1:size(A_inv, 1)]
|
|
return gamma, maxima, eigvals(Acase)
|
|
end
|
|
|
|
gamma_OL, max_OL, eig_OL = run_case(A)
|
|
gamma_CL, max_CL, eig_CL = run_case(A_cl)
|
|
|
|
println("\n=== Open-loop vs LQR closed-loop Lyapunov barrier ===")
|
|
@printf " max Re(eig) OL = %.3e CL = %.3e\n" maximum(real.(eig_OL)) maximum(real.(eig_CL))
|
|
@printf " min Re(eig) OL = %.3e CL = %.3e\n" minimum(real.(eig_OL)) minimum(real.(eig_CL))
|
|
@printf " gamma OL = %.3e CL = %.3e (ratio CL/OL = %.3g)\n" gamma_OL gamma_CL gamma_CL/gamma_OL
|
|
|
|
println("\n Per-halfspace max |a' dx| on gamma-ellipsoid:")
|
|
@printf " %-22s %-10s %-14s %-14s %-14s %-10s\n" "halfspace" "headroom" "open-loop" "closed-loop" "CL - OL" "ratio"
|
|
for k in 1:size(A_inv, 1)
|
|
ratio = max_CL[k] / max_OL[k]
|
|
@printf " %-22s %10.3f %14.3f %14.3f %+14.3f %10.3gx\n" comps[k] b_dev[k] max_OL[k] max_CL[k] (max_CL[k] - max_OL[k]) ratio
|
|
end
|
|
|
|
println("\n Interpretation:")
|
|
println(" - CL < OL on a row => LQR tightens that halfspace.")
|
|
println(" - CL ~= OL => LQR does not help that direction at all.")
|
|
println(" - CL > OL => LQR made that direction WORSE for the barrier.")
|