#!/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.")