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>
138 lines
6.4 KiB
Matlab
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);
|