PWR-HYBRID-3/plant-model/main_mode_sweep.m
Dane Sabo e69fd0a6f4 reachability: pin FRET predicates as numerical halfspaces
predicates.json is the single source of truth for concretizing the
FRET-spec predicates (t_avg_above_min, t_avg_in_range, p_above_crit,
inv1_holds, inv2_holds) as polytopes {x : A x <= b}. Until now these
were abstract booleans in the synthesis spec; reach analysis
re-invented ad-hoc thresholds that weren't tied to the spec. Closes
the Thrust-1-meets-Thrust-3 seam.

T_standby now defined as T_c0 - 60 F = 275 C (from user review).
Replaces the earlier simplification where shutdown IC held all temps
at T_cold0. 275 C is inside the model's +/-50 C trust region around
operating point and above coolant saturation at reduced pressure.

load_predicates.m in MATLAB reads the JSON and resolves rhs_expr
strings (which reference plant-derived constants like T_c0, T_cold0,
T_standby) into numeric bounds. Returns per-predicate (A_poly, b_poly)
plus a constants struct.

main_mode_sweep.m now pulls T_standby from predicates and uses it
for shutdown + heatup ICs. Heatup horizon extended to 90 min to
cover the wider 60 F -> operating range at 28 C/hr tech-spec limit.

reach_operation.m reads delta_safe_Tc from the t_avg_in_range
halfspace instead of hardcoding +/-5 K. Current concretization is
+/-2.78 C (~5 F); LQR reach still shows 28x margin.

inv1_holds and inv2_holds are marked PLACEHOLDER in the JSON —
engineering best guesses, not derived from a specific plant's tech
specs or a DNBR correlation. Revisit before thesis defense.

Hacker-Split: single-source concretization for FRET predicates,
end seam with reach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:09:37 -04:00

139 lines
6.6 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');
addpath(fullfile('..', 'reachability')); % for load_predicates
plant = pke_params();
pred = load_predicates(plant); % T_standby and FRET-predicate concretizations
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 flat at T_standby = T_c0 - 60 F ~ 275 C,
% reactor deep subcritical with only a trace neutron population. T_standby
% comes from reachability/predicates.json (single source of truth). Well
% inside the model's +/-50 C trust region around the operating point, and
% above coolant saturation at reduced operating pressure.
T_standby = pred.constants.T_standby;
n_shut = 1e-6;
C_shut = (plant.beta_i ./ (plant.lambda_i * plant.Lambda)) * n_shut;
x0_shut = [n_shut; C_shut; T_standby; T_standby; T_standby];
% Heatup IC: reactor already taken critical at 0.1% power at hot-standby
% temperature. Mirrors real plant practice: achieve criticality, then
% heat up. Same T_standby as the shutdown IC — heatup begins from where
% shutdown left off.
n_heat = 1e-3;
C_heat = (plant.beta_i ./ (plant.lambda_i * plant.Lambda)) * n_heat;
x0_heat = [n_heat; C_heat; T_standby; T_standby; T_standby];
%% ===== 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 = T_standby; % ~275 C (hot standby, = T_c0 - 60 F)
ref_heatup.T_target = plant.T_c0; % 308.35 C (operating setpoint)
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, 5400]; % ~90 min (33 C span at 28 C/hr = 71 min + settling)
[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);