PWR-HYBRID-3/plant-model/main_mode_sweep.m
Dane Sabo d2997c2861 plant-model: add shutdown/heatup/scram controllers and LQR, linearize
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>
2026-04-17 12:52:03 -04:00

138 lines
6.4 KiB
Matlab

%% main_mode_sweep.m — demo each DRC continuous-mode controller
%
% One scenario per mode, from an initial condition plausible for that
% mode. Figures saved to ../docs/figures/mode_sweep_*.png so the runs
% are reviewable without the MATLAB IDE.
%
% Modes, in the order DRC visits them starting from shutdown:
% 1. shutdown — deep subcritical, cold IC, Q_sg = 0
% 2. heatup — same cold IC, ramped T_avg reference
% 3. operation — operating IC, 100% -> 80% Q_sg step
% 4. scram — operating IC, rods slammed in, Q_sg decays to 0
%
% Each run produces plot_pke_results 4-panel plus saves a lightweight
% summary figure.
clear; clc; close all;
addpath('controllers');
plant = pke_params();
figdir = fullfile('..', 'docs', 'figures');
if ~exist(figdir, 'dir'), mkdir(figdir); end
CtoF = @(T) T*9/5 + 32;
%% ===== IC helpers =====
% Operating IC: full-power steady state.
x0_op = pke_initial_conditions(plant);
% Hot-standby shutdown IC: coolant everywhere at T_cold0 (290 C), reactor
% deep subcritical with only a trace neutron population. Note: our
% feedback model is referenced to the hot full-power state, so going
% below ~290 C violates the linear-coefficient assumption. This IC is
% the lowest T we trust the model at.
n_shut = 1e-6;
C_shut = (plant.beta_i ./ (plant.lambda_i * plant.Lambda)) * n_shut;
x0_shut = [n_shut; C_shut; plant.T_cold0; plant.T_cold0; plant.T_cold0];
% Heatup IC: reactor already taken critical at 0.1% power, low-power
% criticality achieved in shutdown mode before the DRC transitions.
% This avoids the unphysical "ramp from n=0" scenario where the P
% controller has to build power from decay heat before temperature can
% move at all. Mirrors real plant practice: achieve criticality, then
% heat up.
n_heat = 1e-3;
C_heat = (plant.beta_i ./ (plant.lambda_i * plant.Lambda)) * n_heat;
x0_heat = [n_heat; C_heat; plant.T_cold0; plant.T_cold0; plant.T_cold0];
%% ===== Mode 1: SHUTDOWN =====
fprintf('\n===== Mode 1: ctrl_shutdown =====\n');
Q_sg_shut = @(t) 0; % no SG demand
tspan_shut = [0, 600];
[t1, X1, U1] = pke_solver(plant, Q_sg_shut, @ctrl_shutdown, [], tspan_shut, x0_shut);
%% ===== Mode 2: HEATUP =====
fprintf('\n===== Mode 2: ctrl_heatup =====\n');
ref_heatup = struct();
ref_heatup.T_start = plant.T_cold0; % 290 C
ref_heatup.T_target = plant.T_c0; % 308.35 C
ref_heatup.ramp_rate = 28 / 3600; % 28 C/hr, tech-spec limit
Q_sg_heat = @(t) 0; % no SG demand during heatup
tspan_heat = [0, 3000]; % ~50 min
[t2, X2, U2] = pke_solver(plant, Q_sg_heat, @ctrl_heatup, ref_heatup, tspan_heat, x0_heat);
%% ===== Mode 3a: OPERATION (plain P) =====
fprintf('\n===== Mode 3a: ctrl_operation (P) =====\n');
Q_sg_op = @(t) plant.P0 * (1.0 - 0.2 * (t >= 30)); % 100% -> 80% step
ref_op = struct('T_avg', plant.T_c0);
tspan_op = [0, 600];
[t3, X3, U3] = pke_solver(plant, Q_sg_op, @ctrl_operation, ref_op, tspan_op, x0_op);
%% ===== Mode 3b: OPERATION (LQR) =====
fprintf('\n===== Mode 3b: ctrl_operation_lqr =====\n');
[t3b, X3b, U3b] = pke_solver(plant, Q_sg_op, @ctrl_operation_lqr, [], tspan_op, x0_op);
%% ===== Mode 4: SCRAM =====
fprintf('\n===== Mode 4: ctrl_scram =====\n');
% After a scram signal, the turbine trips and SG isolation occurs fast.
% Q_sg drops from P0 to a decay-heat-level sink (3% of P0) in ~10 s,
% then holds. Not a true decay-heat model — good enough to show the
% post-scram thermal trajectory without coolant going unphysically cold.
Q_sg_scr = @(t) plant.P0 * max(0.03, 1 - max(0, t - 10) / 10);
tspan_scr = [0, 600];
[t4, X4, U4] = pke_solver(plant, Q_sg_scr, @ctrl_scram, [], tspan_scr, x0_op);
%% ===== Per-mode 4-panel plots =====
plot_pke_results(t1, X1, U1, plant, Q_sg_shut, 'ctrl\_shutdown (cold IC)');
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_1_shutdown.png'), 'Resolution', 150);
plot_pke_results(t2, X2, U2, plant, Q_sg_heat, 'ctrl\_heatup (ramp T\_avg)');
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_2_heatup.png'), 'Resolution', 150);
plot_pke_results(t3, X3, U3, plant, Q_sg_op, 'ctrl\_operation (P on T\_avg)');
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_3_operation.png'), 'Resolution', 150);
plot_pke_results(t3b, X3b, U3b, plant, Q_sg_op, 'ctrl\_operation\_lqr');
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_3b_operation_lqr.png'), 'Resolution', 150);
plot_pke_results(t4, X4, U4, plant, Q_sg_scr, 'ctrl\_scram');
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_4_scram.png'), 'Resolution', 150);
%% ===== Heatup ramp-tracking figure =====
% Overlay the T_ref signal on T_avg so the lag is visible.
T_ref_trace = min(ref_heatup.T_start + ref_heatup.ramp_rate .* t2, ref_heatup.T_target);
figure('Position', [100 80 900 400], 'Name', 'Heatup ramp tracking');
plot(t2/60, CtoF(T_ref_trace), 'k--', 'LineWidth', 1.2); hold on;
plot(t2/60, CtoF(X2(:,9)), 'r-', 'LineWidth', 1.5);
xlabel('Time [min]'); ylabel('T_{avg} [F]');
legend('T_{ref}(t) (28 C/hr ramp)', 'T_{avg} (plant)', 'Location', 'southeast');
title('ctrl\_heatup: reference tracking');
grid on;
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_heatup_tracking.png'), 'Resolution', 150);
%% ===== P vs LQR head-to-head on T_avg =====
figure('Position', [100 80 900 400], 'Name', 'Operation: P vs LQR');
plot(t3, CtoF(X3(:,9)), 'b-', 'LineWidth', 1.5); hold on;
plot(t3b, CtoF(X3b(:,9)), 'r-', 'LineWidth', 1.5);
yline(CtoF(plant.T_c0), 'k--');
xlabel('Time [s]'); ylabel('T_{avg} [F]');
legend('ctrl\_operation (P)', 'ctrl\_operation\_lqr', 'setpoint', 'Location', 'best');
title('Operation mode: P vs LQR under 100% \rightarrow 80% Q_{sg} step');
grid on;
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_op_P_vs_LQR.png'), 'Resolution', 150);
%% ===== Summary figure: normalized power across all modes =====
figure('Position', [100 80 1000 450], 'Name', 'Normalized power, all modes');
subplot(2,2,1); semilogy(t1, max(X1(:,1), 1e-20)); grid on;
title('Shutdown'); xlabel('t [s]'); ylabel('n (log)');
subplot(2,2,2); plot(t2/60, X2(:,1)); grid on;
title('Heatup'); xlabel('t [min]'); ylabel('n');
subplot(2,2,3); plot(t3, X3(:,1)); grid on;
title('Operation'); xlabel('t [s]'); ylabel('n');
subplot(2,2,4); semilogy(t4, max(X4(:,1), 1e-20)); grid on;
title('Scram'); xlabel('t [s]'); ylabel('n (log)');
sgtitle('Normalized reactor power n(t) across DRC modes');
exportgraphics(gcf, fullfile(figdir, 'mode_sweep_power_overview.png'), 'Resolution', 150);
fprintf('\n=== Figures written to %s ===\n', figdir);