Earlier placeholder claimed ramp-rate limits weren't expressible as
state halfspaces without augmentation. That was wrong: dT_c/dt is
linear in (T_f, T_c, T_cold) directly from pke_th_rhs (no neutronics
coupling), so |dT_c/dt| <= r_max is two clean halfspaces over x.
Coefficients from pke_params:
a_f = hA / (M_c*c_c) = +0.4587 /s
a_c = -(hA + 2*W*c_c)/(M_c*c_c) = -0.9587 /s
a_cold = 2*W*c_c / (M_c*c_c) = +0.5000 /s
Sum = 0 exact (equilibrium when all T's equal).
Limit chosen: +/- 50 C/hr (tech-spec 28 C/hr + transient overshoot
budget). Verified on actual heatup sim: max dT_c/dt = 48.5 C/hr, min
= 0 C/hr. Passes our placeholder but tight — a strict 28 C/hr tech-
spec invariant would be violated by current ctrl_heatup tuning
(overshoot factor ~1.7x during mid-ramp).
Generalized load_predicates.m to accept multi-coefficient halfspace
rows via "row": [[state_idx, coeff], ...] format, in addition to the
existing single-coefficient {state_index, coeff} form. Backward
compatible.
inv1_holds now conjoins fuel_centerline, cold_leg_subcooled, and the
two rate halfspaces. DNBR still not modeled (would need an
augmented predicate with a correlation-based safety margin).
Hacker-Split: Dane asked about heatup rate invariant; realizing
my earlier 'needs state augmentation' claim was wrong and the rate
constraint is already linear. Fix it, verify against actual sim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
4.9 KiB
Matlab
119 lines
4.9 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);
|
|
if isfield(hs, 'row')
|
|
% Multi-coefficient halfspace: hs.row is a k x 2 matrix
|
|
% where each row is [state_index, coeff] (jsondecode from
|
|
% JSON array-of-arrays).
|
|
coeffs = hs.row;
|
|
for r = 1:size(coeffs, 1)
|
|
A_poly(i, coeffs(r, 1)) = coeffs(r, 2);
|
|
end
|
|
else
|
|
% Single-coefficient halfspace: hs.state_index + hs.coeff.
|
|
A_poly(i, hs.state_index) = hs.coeff;
|
|
end
|
|
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
|