Previously conflated two different kinds of constraint:
- operational deadbands (|T_c - T_c0| <= 5 F) used by the DRC for mode
transitions. Symmetric bands around setpoint. Violating these is an
operator/operational issue, not a safety issue.
- safety limits (T_f <= 1200 C, T_c <= 320 C, n <= 1.15, etc.) are
hard one-sided halfspaces corresponding to physical damage mechanisms
or reactor-trip setpoints. THESE are what a safety barrier/reach must
discharge.
predicates.json now has three groups:
- operational_deadbands (t_avg_above_min, t_avg_in_range, p_above_crit)
- safety_limits (fuel_centerline, t_avg_high_trip, t_avg_low_trip,
n_high_trip, n_low_operation, cold_leg_subcooled)
- mode_invariants (inv1_holds, inv2_holds as conjunctions of safety_limits)
reach_operation.m and barrier_lyapunov.m both now report halfspace-by-
halfspace margins against inv2_holds. Attributable failure analysis:
we can see WHICH limit is tightest.
Reach tube (under +/-15% Q_sg load): passes all 6 safety halfspaces.
Tightest margin is n_high_trip at +0.138 (12% from trip). Temperature
directions have 10-870 K margin.
Lyapunov barrier (same): fails all 6. Worst is n_high_trip with -2365
margin — the ellipsoid says n could deviate by +/-2364, which is
physically meaningless. Anisotropy cost made visible per-direction.
Motivates SOS / polytopic barriers for the thesis chapter.
load_predicates.m now returns .operational_deadbands, .safety_limits,
and .mode_invariants. Existing callers that only used .constants or
.t_avg_in_range still work because those live under the old keys.
Hacker-Split: user caught that the barrier was checking the wrong
invariant; safety limits != operating deadband. Restructured so the
proof target matches the physical claim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
4.4 KiB
Matlab
108 lines
4.4 KiB
Matlab
function pred = load_predicates(plant)
|
|
% LOAD_PREDICATES Read predicates.json and resolve rhs_expr into numbers.
|
|
%
|
|
% Returns:
|
|
% pred.constants - struct with T_c0, T_cold0, T_f0, T_standby
|
|
% pred.operational_deadbands - struct of predicates (each: A_poly, b_poly, meaning)
|
|
% pred.safety_limits - struct of halfspace limits (each: A_poly, b_poly, meaning)
|
|
% pred.mode_invariants - struct mapping mode-invariant name to
|
|
% its conjoined (A_poly, b_poly, components)
|
|
%
|
|
% Each halfspace is of the form { x : A_poly * x <= b_poly }. Conjunctions
|
|
% (polytopes) stack rows into A_poly / b_poly.
|
|
%
|
|
% Usage:
|
|
% plant = pke_params();
|
|
% pred = load_predicates(plant);
|
|
% inv2 = pred.mode_invariants.inv2_holds;
|
|
% is_in = all(inv2.A_poly * x <= inv2.b_poly);
|
|
|
|
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, ...
|
|
'T_fuel_limit_C', J.derived.T_fuel_limit_C, ...
|
|
'T_c_high_trip_C', J.derived.T_c_high_trip_C, ...
|
|
'n_high_trip', J.derived.n_high_trip);
|
|
|
|
% --- operational_deadbands ---
|
|
pred.operational_deadbands = parse_group(J.operational_deadbands, ...
|
|
T_c0, T_f0, T_cold0, T_standby);
|
|
|
|
% --- safety_limits ---
|
|
pred.safety_limits = parse_group(J.safety_limits, ...
|
|
T_c0, T_f0, T_cold0, T_standby);
|
|
|
|
% --- mode_invariants: conjunctions of safety_limits entries ---
|
|
inv_names = fieldnames(J.mode_invariants);
|
|
for k = 1:numel(inv_names)
|
|
name = inv_names{k};
|
|
if startsWith(name, '_') || startsWith(name, 'x_'), continue, end
|
|
entry = J.mode_invariants.(name);
|
|
if ~isstruct(entry) || ~isfield(entry, 'conjunction_of'), continue, end
|
|
components = entry.conjunction_of;
|
|
if ischar(components), components = {components}; end
|
|
A_all = [];
|
|
b_all = [];
|
|
for i = 1:numel(components)
|
|
comp = components{i};
|
|
A_all = [A_all; pred.safety_limits.(comp).A_poly]; %#ok<AGROW>
|
|
b_all = [b_all; pred.safety_limits.(comp).b_poly]; %#ok<AGROW>
|
|
end
|
|
pred.mode_invariants.(name).A_poly = A_all;
|
|
pred.mode_invariants.(name).b_poly = b_all;
|
|
pred.mode_invariants.(name).meaning = entry.meaning;
|
|
pred.mode_invariants.(name).components = components;
|
|
end
|
|
end
|
|
|
|
function group_out = parse_group(group_in, T_c0, T_f0, T_cold0, T_standby)
|
|
names = fieldnames(group_in);
|
|
group_out = struct();
|
|
for k = 1:numel(names)
|
|
name = names{k};
|
|
% MATLAB jsondecode renames "_comment" -> "x_comment" and similar
|
|
if startsWith(name, '_') || startsWith(name, 'x_'), continue, end
|
|
entry = group_in.(name);
|
|
if ~isstruct(entry) || ~isfield(entry, 'halfspaces'), continue, end
|
|
hs_list = entry.halfspaces;
|
|
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) = evalin_context(hs.rhs_expr, T_c0, T_f0, T_cold0, T_standby);
|
|
end
|
|
group_out.(name).A_poly = A_poly;
|
|
group_out.(name).b_poly = b_poly;
|
|
group_out.(name).meaning = entry.meaning;
|
|
end
|
|
end
|
|
|
|
function val = evalin_context(expr, T_c0, T_f0, T_cold0, T_standby) %#ok<INUSD>
|
|
val = eval(expr);
|
|
end
|