Fill out the DRC mode set with ctrl_shutdown (u = -5*beta), ctrl_scram (u = -8*beta), and ctrl_heatup (feedback-linearizing P on ramped T_avg reference, saturated u, no integrator). Add ctrl_operation_lqr as a full-state-feedback counterpart to ctrl_operation — K cached, closed-loop essentially perfect under the 100%->80% Q_sg step where plain P has ~5F overshoot. Add pke_linearize for numerical (A, B, B_w) Jacobians at any operating point; test_linearize confirms ~4e-4 rel err vs nonlinear sim for a 5% Q_sg step. Extend pke_solver with an optional x0 argument so each mode can start from a plausible IC. main_mode_sweep.m exercises all five modes back-to-back and saves the 4-panel plots. CLAUDE.md updated with model-validity-range note (trust region is ~+/-50C around operating point; true cold shutdown is out of scope for the linear feedback coefficients). Hacker-Split: build out control layer end-to-end for reachability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
3.8 KiB
Matlab
107 lines
3.8 KiB
Matlab
%% test_linearize.m — sanity-check the Jacobians against the nonlinear sim
|
|
%
|
|
% Simulate two parallel trajectories under a small Q_sg step (5% down):
|
|
% 1. Full nonlinear plant with u = 0
|
|
% 2. Linear dx/dt = A*dx + B_w*dw propagated by ode45
|
|
% Compare deviations from x_op. For a small-enough disturbance, the
|
|
% trajectories should overlay to within a few percent over a few minutes.
|
|
|
|
clear; clc; close all;
|
|
addpath('controllers');
|
|
|
|
plant = pke_params();
|
|
x_op = pke_initial_conditions(plant);
|
|
|
|
[A, B, B_w, xs, us, Qs] = pke_linearize(plant, x_op, 0, plant.P0);
|
|
|
|
%% ===== Eigenvalue/conditioning check =====
|
|
eigs_A = eig(A);
|
|
fprintf('\n=== Linearization at operating point ===\n');
|
|
fprintf(' ||A|| = %.3e\n', norm(A));
|
|
fprintf(' ||B|| = %.3e\n', norm(B));
|
|
fprintf(' ||B_w|| = %.3e\n', norm(B_w));
|
|
fprintf(' stable? = %s (max Re(eig) = %.3e)\n', ...
|
|
iif(all(real(eigs_A) < 1e-8), 'YES', 'NO'), max(real(eigs_A)));
|
|
fprintf(' fastest pole ~ %.3e /s\n', max(abs(real(eigs_A))));
|
|
fprintf(' slowest pole ~ %.3e /s\n', min(abs(real(eigs_A(real(eigs_A) < -1e-10)))));
|
|
|
|
%% ===== Linear vs nonlinear trajectory under small Q_sg step =====
|
|
% 5% down step at t = 30s.
|
|
dQ = -0.05 * plant.P0;
|
|
Q_sg = @(t) plant.P0 + dQ * (t >= 30);
|
|
|
|
tspan = [0, 300];
|
|
|
|
% Nonlinear
|
|
opts = odeset('RelTol', 1e-8, 'AbsTol', 1e-10);
|
|
rhs_nl = @(t, x) pke_th_rhs(t, x, plant, Q_sg, 0);
|
|
[t_nl, X_nl] = ode15s(rhs_nl, tspan, x_op, opts);
|
|
|
|
% Linear (deviation)
|
|
dQ_of_t = @(t) dQ * (t >= 30);
|
|
rhs_lin = @(t, dx) A*dx + B_w*dQ_of_t(t);
|
|
[t_lin, DX_lin] = ode15s(rhs_lin, tspan, zeros(size(x_op)), opts);
|
|
|
|
% Reconstruct absolute linear state for plotting
|
|
X_lin = DX_lin + x_op.';
|
|
|
|
%% ===== Plot comparison — focus on thermal states =====
|
|
CtoF = @(T) T*9/5 + 32;
|
|
figdir = fullfile('..', 'docs', 'figures');
|
|
if ~exist(figdir, 'dir'), mkdir(figdir); end
|
|
|
|
figure('Position', [100 80 1100 700], 'Name', 'Linear vs nonlinear sanity');
|
|
|
|
subplot(2,2,1);
|
|
plot(t_nl, X_nl(:,1), 'b-', 'LineWidth', 1.5); hold on;
|
|
plot(t_lin, X_lin(:,1), 'r--','LineWidth', 1.5);
|
|
xlabel('t [s]'); ylabel('n'); grid on;
|
|
title('Normalized power'); legend('nonlinear', 'linear', 'Location', 'best');
|
|
|
|
subplot(2,2,2);
|
|
plot(t_nl, CtoF(X_nl(:,8)), 'b-', 'LineWidth', 1.5); hold on;
|
|
plot(t_lin, CtoF(X_lin(:,8)), 'r--','LineWidth', 1.5);
|
|
xlabel('t [s]'); ylabel('T_f [F]'); grid on;
|
|
title('Fuel temperature');
|
|
|
|
subplot(2,2,3);
|
|
plot(t_nl, CtoF(X_nl(:,9)), 'b-', 'LineWidth', 1.5); hold on;
|
|
plot(t_lin, CtoF(X_lin(:,9)), 'r--','LineWidth', 1.5);
|
|
xlabel('t [s]'); ylabel('T_c [F]'); grid on;
|
|
title('Avg coolant');
|
|
|
|
subplot(2,2,4);
|
|
plot(t_nl, CtoF(X_nl(:,10)), 'b-', 'LineWidth', 1.5); hold on;
|
|
plot(t_lin, CtoF(X_lin(:,10)),'r--','LineWidth', 1.5);
|
|
xlabel('t [s]'); ylabel('T_{cold} [F]'); grid on;
|
|
title('Cold-leg coolant');
|
|
|
|
sgtitle('Linear-model sanity check: 5% Q_{sg} down-step, u = 0');
|
|
exportgraphics(gcf, fullfile(figdir, 'linearize_sanity.png'), 'Resolution', 150);
|
|
|
|
%% ===== Quantitative error at a few times =====
|
|
fprintf('\n=== Max |linear - nonlinear| at sampled times ===\n');
|
|
fprintf(' (normalized by operating-point magnitude where nonzero)\n');
|
|
for ts = [60, 120, 300]
|
|
xi_nl = interp1(t_nl, X_nl, ts);
|
|
xi_lin = interp1(t_lin, X_lin, ts);
|
|
rel_err = abs(xi_nl - xi_lin) ./ max(abs(x_op.'), 1e-6);
|
|
fprintf(' t = %3d s: max rel err = %.2e (%s)\n', ts, max(rel_err), ...
|
|
state_name(find(rel_err == max(rel_err), 1)));
|
|
end
|
|
|
|
%% ===== Save linearization =====
|
|
save(fullfile('..', 'reachability', 'linearization_at_op.mat'), ...
|
|
'A', 'B', 'B_w', 'xs', 'us', 'Qs', '-v7');
|
|
fprintf('\nSaved A/B/B_w to ../reachability/linearization_at_op.mat\n');
|
|
|
|
%% ===== helpers =====
|
|
function s = state_name(k)
|
|
names = {'n','C1','C2','C3','C4','C5','C6','T_f','T_c','T_cold'};
|
|
s = names{k};
|
|
end
|
|
|
|
function y = iif(cond, a, b)
|
|
if cond, y = a; else, y = b; end
|
|
end
|