#!/usr/bin/env julia # # reach_operation.jl — Julia port of reachability/reach_operation.m. # # Linear reach tube for operation-mode LQR closed-loop. Per-halfspace # margin check against inv2_holds (from predicates.json, single source # of truth). Figures saved to docs/figures/. # # Soundness caveat: this reach tube is a sound over-approximation of the # LINEARIZED closed-loop system's reach set, not of the nonlinear plant. # Linear-reach results here are an approximate-but-useful gut check. using Pkg Pkg.activate(joinpath(@__DIR__, "..")) using Printf using LinearAlgebra using MatrixEquations using Plots using JSON using MAT 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")) include(joinpath(@__DIR__, "..", "src", "reach_linear.jl")) plant = pke_params() x_op = pke_initial_conditions(plant) pred = load_predicates(plant) # --- Closed-loop linearization + LQR gain --- 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 eigs_cl = eigvals(A_cl) println("\n=== Closed-loop spectrum (A - BK) ===") @printf " max Re(eig) = %.3e\n" maximum(real.(eigs_cl)) @printf " min Re(eig) = %.3e\n" minimum(real.(eigs_cl)) @assert all(real.(eigs_cl) .< -1e-8) "A_cl not Hurwitz" # --- Entry box --- delta_entry = [0.01 * x_op[1]; 0.001 .* abs.(x_op[2:7]); 0.1; 0.1; 0.1] # --- Disturbance envelope --- Q_nom = plant.P0 Q_min = 0.85 * plant.P0 Q_max = 1.00 * plant.P0 dQ_lo = Q_min - Q_nom dQ_hi = Q_max - Q_nom # --- Reach in deviation coordinates --- tspan = (0.0, 600.0) dt = 0.5 T, R_lo, R_hi, Cnom = reach_linear(A_cl, B_w, zeros(10), delta_entry, dQ_lo, dQ_hi, tspan, dt) # Translate back to absolute coordinates. X_lo = R_lo .+ x_op X_hi = R_hi .+ x_op X_nom = Cnom .+ x_op # --- Safety check: inv2_holds halfspace-by-halfspace --- inv2 = pred.mode_invariants[:inv2_holds] A_inv = inv2.A_poly b_inv = inv2.b_poly comps = inv2.components println("\n=== Operation-mode reach vs inv2_holds safety limits ===") for k in 1:size(A_inv, 1) a = A_inv[k, :] # Envelope max of a'*x: take Xhi where a>0, Xlo where a<0. env_for_max = (a .> 0) .* X_hi .+ (a .< 0) .* X_lo # Small correction: zero coeffs have no preference (both values equal). zero_mask = (a .== 0) env_for_max[zero_mask, :] .= X_hi[zero_mask, :] max_ax = maximum(a' * env_for_max) margin = b_inv[k] - max_ax status = margin < 0 ? "*** VIOLATED ***" : "OK" @printf " [%-22s] a'x <= %8.3f | max a'x = %8.3f | margin = %+8.3f %s\n" comps[k] b_inv[k] max_ax margin status end # --- Per-state reach-set width diagnostic --- state_names = ["n","C1","C2","C3","C4","C5","C6","T_f","T_c","T_cold"] println("\n=== Reach-set width at t=0 vs t=T_final ===") @printf " %-7s %-14s %-14s %-8s\n" "state" "init halfwidth" "final halfwidth" "ratio" for i in 1:10 hi = 0.5 * (R_hi[i, 1] - R_lo[i, 1]) hf = 0.5 * (R_hi[i, end] - R_lo[i, end]) @printf " %-7s %.4e %.4e %.2f\n" state_names[i] hi hf hf/max(hi, eps()) end # --- Plots: T_c reach tube, two views --- CtoF(T) = T * 9/5 + 32 delta_safe_Tc = pred.constants.t_avg_in_range_halfwidth_C figdir = joinpath(@__DIR__, "..", "..", "docs", "figures") isdir(figdir) || mkpath(figdir) p_safety = plot(T, CtoF.(X_nom[9, :]), lw=1.2, color=:red, label="nominal", xlabel="Time [s]", ylabel="T_avg [F]", title="Safety-band view") plot!(p_safety, T, CtoF.(X_hi[9, :]), fillrange=CtoF.(X_lo[9, :]), fillalpha=0.3, color=:red, linealpha=0.0, label="reach tube") hline!(p_safety, [CtoF(plant.T_c0 + delta_safe_Tc), CtoF(plant.T_c0 - delta_safe_Tc)], ls=:dash, color=:black, label="safety +/- $(round(delta_safe_Tc; digits=2)) C") dev_lo = X_lo[9, :] .- plant.T_c0 dev_hi = X_hi[9, :] .- plant.T_c0 max_dev = maximum(abs, [dev_lo; dev_hi]) p_zoom = plot(T, X_nom[9, :] .- plant.T_c0, lw=1.2, color=:red, label="nominal", xlabel="Time [s]", ylabel="T_avg - T_c0 [K]", title=@sprintf("Zoomed: max |dT_c| = %.3e K", max_dev)) plot!(p_zoom, T, dev_hi, fillrange=dev_lo, fillalpha=0.3, color=:red, linealpha=0.0, label="reach tube") hline!(p_zoom, [0.0], ls=:dot, color=:black, label=nothing) fig = plot(p_safety, p_zoom, layout=(1, 2), size=(1400, 500), plot_title=@sprintf("Operation-mode reach tube, LQR, Q_sg in [%.0f%%, %.0f%%] P0", 100*Q_min/Q_nom, 100*Q_max/Q_nom)) savefig(fig, joinpath(figdir, "reach_operation_Tc.png")) # n tube fig_n = plot(T, X_nom[1, :], lw=1.2, color=:blue, label="nominal", xlabel="Time [s]", ylabel="n", title="Operation mode reach tube on normalized power") plot!(fig_n, T, X_hi[1, :], fillrange=X_lo[1, :], fillalpha=0.3, color=:blue, linealpha=0.0, label="reach tube") savefig(fig_n, joinpath(figdir, "reach_operation_n.png")) # --- Save result --- matfile = joinpath(@__DIR__, "..", "..", "reachability", "reach_operation_result.mat") matwrite(matfile, Dict("T" => collect(T), "R_lo" => R_lo, "R_hi" => R_hi, "center" => Cnom, "X_lo" => X_lo, "X_hi" => X_hi, "X_nom" => X_nom, "K" => Matrix(K), "A_cl" => A_cl, "delta_entry" => delta_entry, "Q_min" => Q_min, "Q_max" => Q_max, "delta_safe_Tc" => delta_safe_Tc)) println("\nSaved reach result to $matfile")