Three caveats surfaced during walkthrough lived only in the conversation transcript before this commit. Now they live where future agents and future-me will actually see them: - reach_operation.m and reachability/README.md state prominently that the current reach tube is an over-approximation of the LINEAR model, not a sound tube for the nonlinear plant. Thesis-blocking for a real safety claim. Upgrade paths documented. - ctrl_heatup.m header and plant-model/CLAUDE.md note that the feedback-linearization u_ff assumes exact alpha_f, alpha_c. Real plants drift (burnup ~20%, boron ~10x, xenon). Robust treatment = parametric reach with alpha as an interval. - ctrl_heatup.m header and plant-model/CLAUDE.md note that sat() is formally a 3-mode piecewise-affine sub-system. Operation-mode LQR is dormant (trivially); heatup will need either a dormancy proof or explicit hybrid modeling. README.md top-level now has a run-commands table for the reach artifacts and a pointer to the soundness status. Hacker-Split: raise caveats from transcript to artifact so the work is actually reviewable by people who weren't in the room. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
178 lines
7.5 KiB
Matlab
178 lines
7.5 KiB
Matlab
%% reach_operation.m — linear reach set for operation mode (LQR closed-loop)
|
|
%
|
|
% *** SOUNDNESS STATUS: APPROXIMATE, NOT SOUND. ***
|
|
%
|
|
% This file computes a reach tube for the *linearized* closed-loop
|
|
% system (A_cl = A - BK around x_op) under bounded Q_sg. The tube
|
|
% itself is a sound over-approximation of the LINEAR model's reach
|
|
% set — it uses conservative box hulls and elementwise-absolute-value
|
|
% matrix propagation. But the LINEAR MODEL is only an approximation
|
|
% of the real nonlinear plant (pke_th_rhs.m), so the result is not a
|
|
% sound reach tube for the actual plant.
|
|
%
|
|
% To upgrade to a sound result, pick one:
|
|
% (a) Nonlinear reach directly (CORA's nonlinearSys, JuliaReach's
|
|
% BlackBoxContinuousSystem). Expensive, honest.
|
|
% (b) Linear reach + Taylor-remainder inflation: compute an upper
|
|
% bound on ||f_nonlinear(x,u) - (A x + B u)|| over the reach
|
|
% set and inflate the linear tube by that bound. Cheaper,
|
|
% requires a Hessian-norm estimate.
|
|
% Tracked as a thesis-blocking todo. For now, the 5-orders-of-margin
|
|
% buffer (|dT_c| ~ 0.03 K vs safety band 5 K) gives us a lot of room
|
|
% to absorb linearization error, but that's not a proof.
|
|
%
|
|
% Compute a reach-tube over-approximation starting from a box around
|
|
% x_op, under LQR feedback, with Q_sg in a specified interval. Check
|
|
% that T_avg stays inside the t_avg_in_range predicate for all t in
|
|
% [0, T_final].
|
|
%
|
|
% This is the *continuous-mode obligation* for q_operation:
|
|
% X_entry := { x : |x - x_op| <= delta_entry }
|
|
% W := [Q_min, Q_max]
|
|
% X_safe := { x : |T_c - T_c0| <= delta_safe }
|
|
% Obligation: ReachTube(X_entry, W, [0, T_final]) subset X_safe.
|
|
%
|
|
% If this passes, we've discharged the Thrust-3 verification for one
|
|
% continuous mode at the level the thesis calls for.
|
|
|
|
clear; clc; close all;
|
|
addpath('../plant-model', '../plant-model/controllers');
|
|
|
|
plant = pke_params();
|
|
x_op = pke_initial_conditions(plant);
|
|
|
|
%% ===== Closed-loop linearization =====
|
|
[A, B, B_w, ~, ~, ~] = pke_linearize(plant, x_op, 0, plant.P0);
|
|
|
|
% LQR gain using the same Q, R as ctrl_operation_lqr.m. Keep these in
|
|
% sync if you retune there.
|
|
Q_lqr = diag([1, 1e-3, 1e-3, 1e-3, 1e-3, 1e-3, 1e-3, 1e-2, 1e2, 1]);
|
|
R_lqr = 1e6;
|
|
try
|
|
K = lqr(A, B, Q_lqr, R_lqr);
|
|
catch
|
|
[~, ~, K] = icare(A, B, Q_lqr, R_lqr);
|
|
end
|
|
|
|
A_cl = A - B*K;
|
|
|
|
fprintf('\n=== Closed-loop spectrum (A - BK) ===\n');
|
|
eigs_cl = eig(A_cl);
|
|
fprintf(' max Re(eig) = %.3e\n', max(real(eigs_cl)));
|
|
fprintf(' min Re(eig) = %.3e\n', min(real(eigs_cl)));
|
|
assert(all(real(eigs_cl) < -1e-8), 'A_cl not Hurwitz — gain tuning issue');
|
|
|
|
%% ===== Define sets =====
|
|
% X_entry: 1% box on n, 0.1% boxes on precursors (their magnitudes are
|
|
% huge due to 1/Lambda), 0.1 K on fuel, 0.1 K on coolant, 0.1 K on cold leg.
|
|
% This represents "we entered operation mode near the steady state".
|
|
delta_entry = [0.01 * x_op(1); % n
|
|
0.001 * abs(x_op(2:7)); % C1..C6
|
|
0.1; % T_f [C]
|
|
0.1; % T_c [C]
|
|
0.1]; % T_cold [C]
|
|
|
|
% Disturbance: Q_sg = [0.85*P0, 1.00*P0] -> this captures up to a 15%
|
|
% down-step load demand (realistic load-follow envelope).
|
|
Q_nom = plant.P0;
|
|
Q_min = 0.85 * plant.P0;
|
|
Q_max = 1.00 * plant.P0;
|
|
dQ_lo = Q_min - Q_nom; % -0.15 * P0
|
|
dQ_hi = Q_max - Q_nom; % 0
|
|
|
|
% X_safe: |T_c - T_c0| <= 5 K (roughly matching the "t_avg_in_range"
|
|
% predicate window — adjust once the FRET predicate threshold is locked).
|
|
delta_safe_Tc = 5.0; % [C]
|
|
|
|
%% ===== Reach set =====
|
|
% Propagate in deviation coordinates: dx = x - x_op.
|
|
tspan = [0, 600];
|
|
dt = 0.5;
|
|
|
|
[T, R_lo, R_hi, C] = reach_linear(A_cl, B_w, zeros(10,1), delta_entry, dQ_lo, dQ_hi, tspan, dt);
|
|
|
|
% Translate back to absolute coordinates for reporting
|
|
Xabs_lo = R_lo + x_op;
|
|
Xabs_hi = R_hi + x_op;
|
|
Cabs = C + x_op;
|
|
|
|
%% ===== Safety check =====
|
|
T_c_lo = Xabs_lo(9, :);
|
|
T_c_hi = Xabs_hi(9, :);
|
|
|
|
violation_mask = (T_c_hi > plant.T_c0 + delta_safe_Tc) | ...
|
|
(T_c_lo < plant.T_c0 - delta_safe_Tc);
|
|
fprintf('\n=== Operation-mode reach-set safety ===\n');
|
|
fprintf(' Horizon = [%g, %g] s\n', tspan(1), tspan(2));
|
|
fprintf(' Entry box T_c [C] = [%.3f, %.3f] (x_op +/- %.1f C)\n', ...
|
|
x_op(9) - delta_entry(9), x_op(9) + delta_entry(9), delta_entry(9));
|
|
fprintf(' Disturbance Q_sg = [%.3f, %.3f] MW\n', Q_min/1e6, Q_max/1e6);
|
|
fprintf(' Safe band on T_c = x_op(T_c0) +/- %.1f C -> [%.3f, %.3f]\n', ...
|
|
delta_safe_Tc, plant.T_c0 - delta_safe_Tc, plant.T_c0 + delta_safe_Tc);
|
|
fprintf(' Reach T_c envelope = [%.3f, %.3f]\n', min(T_c_lo), max(T_c_hi));
|
|
if any(violation_mask)
|
|
t_first = T(find(violation_mask, 1));
|
|
fprintf(' *** SAFETY VIOLATED at t = %.2f s ***\n', t_first);
|
|
else
|
|
fprintf(' OK: reach set stays inside the safe band.\n');
|
|
end
|
|
|
|
%% Per-state reach-set growth diagnostic (final time vs initial)
|
|
state_names = {'n','C1','C2','C3','C4','C5','C6','T_f','T_c','T_cold'};
|
|
fprintf('\n=== Reach-set width at t=0 vs t=T_final ===\n');
|
|
fprintf(' %-7s %-14s %-14s %-8s\n', 'state', 'init halfwidth', 'final halfwidth', 'ratio');
|
|
for i = 1:10
|
|
hi = 0.5 * (R_hi(i, 1) - R_lo(i, 1));
|
|
hf = 0.5 * (R_hi(i, end) - R_lo(i, end));
|
|
fprintf(' %-7s %-14.4e %-14.4e %-8.2f\n', state_names{i}, hi, hf, hf/max(hi,eps));
|
|
end
|
|
|
|
%% ===== Plots =====
|
|
figdir = fullfile('..', 'docs', 'figures');
|
|
if ~exist(figdir, 'dir'), mkdir(figdir); end
|
|
CtoF = @(T) T*9/5 + 32;
|
|
|
|
% Two-panel plot: wide view with safety band, zoom view showing actual tube.
|
|
figure('Position', [100 80 1400 500], 'Name', 'Reach tube: T_c');
|
|
|
|
subplot(1,2,1);
|
|
fill([T; flipud(T)], CtoF([T_c_hi.'; flipud(T_c_lo.')]), [1.0 0.85 0.85], ...
|
|
'EdgeColor', 'none'); hold on;
|
|
plot(T, CtoF(Cabs(9, :)), 'r-', 'LineWidth', 1.2);
|
|
yline(CtoF(plant.T_c0 + delta_safe_Tc), 'k--', 'LineWidth', 1.0);
|
|
yline(CtoF(plant.T_c0 - delta_safe_Tc), 'k--', 'LineWidth', 1.0);
|
|
yline(CtoF(plant.T_c0), 'k:', 'LineWidth', 1.0);
|
|
xlabel('Time [s]'); ylabel('T_{avg} [F]'); grid on;
|
|
title('Safety-band view');
|
|
legend('reach tube', 'nominal', 'safety +/- 5 C', 'Location', 'best');
|
|
|
|
subplot(1,2,2);
|
|
Tc_dev_lo = T_c_lo.' - plant.T_c0; % M x 1, deviation in K
|
|
Tc_dev_hi = T_c_hi.' - plant.T_c0;
|
|
fill([T; flipud(T)], [Tc_dev_hi; flipud(Tc_dev_lo)], [1.0 0.85 0.85], ...
|
|
'EdgeColor', 'none'); hold on;
|
|
plot(T, Cabs(9, :).' - plant.T_c0, 'r-', 'LineWidth', 1.2);
|
|
yline(0, 'k:', 'LineWidth', 1.0);
|
|
xlabel('Time [s]'); ylabel('T_{avg} - T_{c0} [K]'); grid on;
|
|
max_dev = max(abs([Tc_dev_lo; Tc_dev_hi]));
|
|
title(sprintf('Zoomed: max |dT_c| = %.3e K', max_dev));
|
|
|
|
sgtitle(sprintf('Operation-mode reach tube, LQR, Q_{sg} in [%.0f%%, %.0f%%] P_0', ...
|
|
100*Q_min/Q_nom, 100*Q_max/Q_nom));
|
|
exportgraphics(gcf, fullfile(figdir, 'reach_operation_Tc.png'), 'Resolution', 150);
|
|
|
|
figure('Position', [100 80 1100 500], 'Name', 'Reach tube: n');
|
|
fill([T; flipud(T)], [R_hi(1,:).' + x_op(1); flipud(R_lo(1,:).' + x_op(1))], ...
|
|
[0.85 0.85 1.0], 'EdgeColor', 'none'); hold on;
|
|
plot(T, Cabs(1, :), 'b-', 'LineWidth', 1.2);
|
|
xlabel('Time [s]'); ylabel('n'); grid on;
|
|
title('Operation mode reach tube on normalized power');
|
|
legend('reach tube', 'nominal', 'Location', 'best');
|
|
exportgraphics(gcf, fullfile(figdir, 'reach_operation_n.png'), 'Resolution', 150);
|
|
|
|
save(fullfile('.', 'reach_operation_result.mat'), ...
|
|
'T', 'R_lo', 'R_hi', 'C', 'Xabs_lo', 'Xabs_hi', 'Cabs', ...
|
|
'K', 'A_cl', 'x_op', 'delta_entry', 'Q_min', 'Q_max', 'delta_safe_Tc', '-v7');
|
|
|
|
fprintf('\nSaved reach result to ./reach_operation_result.mat\n');
|