PWR-HYBRID-3/reachability/load_predicates.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

74 lines
2.8 KiB
Matlab

function pred = load_predicates(plant)
% LOAD_PREDICATES Read predicates.json and resolve rhs_expr into numbers.
%
% Each halfspace entry in the JSON stores rhs_expr as a string because
% several of the bounds are defined relative to plant-derived constants
% (T_c0, T_cold0, T_standby). We evaluate those expressions here in a
% controlled workspace that exposes exactly those names plus the
% derived offsets from the JSON.
%
% Returns a struct `pred` with one field per predicate, each a struct
% holding an n_hs x 10 matrix A_poly and an n_hs x 1 vector b_poly such
% that the predicate is { x : A_poly * x <= b_poly }.
%
% Also returns:
% pred.constants - a struct with T_c0, T_cold0, T_f0, T_standby, etc.
%
% Usage:
% plant = pke_params();
% pred = load_predicates(plant);
% A = pred.t_avg_in_range.A_poly; % 2 x 10
% b = pred.t_avg_in_range.b_poly; % 2 x 1
% is_in = all(A * x <= b); % predicate check on state x
here = fileparts(mfilename('fullpath'));
raw = fileread(fullfile(here, 'predicates.json'));
J = jsondecode(raw);
% --- Constants used in rhs_expr evaluations ---
T_c0 = plant.T_c0; %#ok<NASGU>
T_f0 = plant.T_f0; %#ok<NASGU>
T_cold0 = plant.T_cold0; %#ok<NASGU>
T_standby_offset_C = J.derived.T_standby_offset_C;
T_standby = T_c0 + T_standby_offset_C; %#ok<NASGU>
pred.constants = struct( ...
'T_c0', plant.T_c0, ...
'T_f0', plant.T_f0, ...
'T_cold0', plant.T_cold0, ...
'T_standby', T_standby, ...
'T_standby_offset_C', T_standby_offset_C, ...
'T_standby_offset_F', J.derived.T_standby_offset_F, ...
't_avg_in_range_halfwidth_C', J.derived.t_avg_in_range_halfwidth_C, ...
'p_above_crit_threshold_n', J.derived.p_above_crit_threshold_n);
% --- Loop over predicates, build A/b matrices ---
names = fieldnames(J.predicates);
for k = 1:numel(names)
name = names{k};
entry = J.predicates.(name);
hs_list = entry.halfspaces;
% jsondecode returns a struct array if entries are uniform, else cell.
if iscell(hs_list)
n_hs = numel(hs_list);
get_hs = @(i) hs_list{i};
else
n_hs = numel(hs_list);
get_hs = @(i) hs_list(i);
end
A_poly = zeros(n_hs, 10);
b_poly = zeros(n_hs, 1);
for i = 1:n_hs
hs = get_hs(i);
A_poly(i, hs.state_index) = hs.coeff;
b_poly(i) = eval(hs.rhs_expr); %#ok<EVLDF>
end
pred.(name).A_poly = A_poly;
pred.(name).b_poly = b_poly;
pred.(name).meaning = entry.meaning;
end
end