# 2026-04-27 — Scram `X_exit` redefinition: shutdown-margin halfspace ## What changed Replaced the scram-mode `X_exit` predicate from `n <= 1e-4 AND T_f <= T_f0 + 50 C` to a single linear halfspace `shutdown_margin`: alpha_f * T_f + alpha_c * T_c <= -rho_SDM - U_SCRAM + alpha_f*T_f0 + alpha_c*T_c0 ≈ 0.002972 with `rho_SDM = 0.01` (1% Δk/k tech-spec floor). Files touched: - `reachability/predicates.json` — added `shutdown_margin` under `safety_limits`; updated `mode_definitions.q_scram.X_exit_predicate` and `X_safe_predicate`; left a `_X_exit_history` field for forensics. - `code/scripts/reach/reach_scram_pj.jl` — added `RHO_SDM`, `SDM_RHS` constants; reach loop now reports ρ-bounds and the halfspace LHS sup per probe horizon; `.mat` output gets `sdm_lhs_hi`, `rho_max`, `sdm_ok` per horizon plus a global `sdm_rhs`, `rho_sdm`. ## Why 1. **Power threshold was nonlinear in PJ state.** In the prompt-jump reduction, `n = Λ * sum(λ_i*C_i) / (β - ρ)` — ρ depends on `T_f`, `T_c`. So `n ≤ 1e-4` is *not* a halfspace in the reach state. We were reconstructing it post-hoc on every probe, which works for a diagnostic check but doesn't compose with halfspace-based discharge. 2. **`T_f ≤ T_f0 + 50` was infeasible-by-construction at 60 s.** Decay heat (`Q_sg = 3% P0`) plus a fuel time constant `M_f*C_f / hA ≈ 0.3 s` means `T_f` rapidly equilibrates with `T_c`, but the system loses only ~5 °C in 60 s under constant decay — nowhere near the threshold, and never going back up either. The bound was dressing, not work. 3. **Shutdown margin is the actual NRC criterion.** Tech specs phrase scram success in Δk/k, not in flux. And ρ is *linear* in `(T_f, T_c)` post-scram (constant `u = U_SCRAM`), so it's a clean single-row halfspace. ## Result `reach_scram_pj.jl` discharges `shutdown_margin` at all three probe horizons (10, 30, 60 s), with massive margin: | T (s) | reach-sets | wall (s) | ρ at horizon | discharged | |-------|------------|----------|---------------------------|------------| | 10 | 6919 | 98.6 | [-0.0507, -0.0504] | ✓ | | 30 | 9900 | 130.5 | [-0.0506, -0.0503] | ✓ | | 60 | 12340 | 164.2 | [-0.0503, -0.0500] | ✓ | Required: `ρ ≤ -0.01`. Actual: `|ρ| ≈ 5%`. The Doppler/moderator contributions vary by ~3% of `U_SCRAM`, so the margin is dominated by rod worth — exactly what physical intuition predicts. `.mat` output: `results/reach_scram_pj_result.mat`. ## Subtlety I tripped on Hand-derived `SDM_RHS = 0.00402` using rounded `T_F0=320`, `T_C0=300`. Actual values from `pke_params`: `T_C0 = 308.35`, `T_F0 = 328.35` (because `DT_CORE = P0/(W_M*C_C) = 36.7 °C`, not 20). The script computes `SDM_RHS` from constants so the run was correct (0.002972), but the predicate JSON had the stale 0.00402. Fixed by switching the `rhs_expr` to a symbolic form. **Rule for next time:** if a constant in `predicates.json` is derivable from `pke_params`, write it as a symbolic expression, not a baked number — that's what `T_cold0`, `T_c0`, `T_standby` are for. ## What's still open - `X_safe_predicate` still says "fuel_centerline AND cold_leg_subcooled" but the reach script doesn't actually discharge those over the trajectory — only at the probe horizons. Not a problem for the demo (T_f and T_cold are monotone after scram), but the formal obligation is reach-AVOID, not just reach. Worth a follow-up: discharge the invariant halfspaces over the *full flowpipe*, not the endpoint. - `prompt_critical_margin_heatup` is the controller-specific PJ-validity predicate. Scram has its own analogous concern (PJ valid only when `β - ρ > 0` with margin). Trivially satisfied here (ρ ≈ -0.05, far from `β = 0.0065`), but worth a parallel `prompt_critical_margin_scram` predicate for completeness — would document the assumption rather than leave it implicit. ## Graduation candidates - The "rhs_expr should be symbolic when derivable" rule probably belongs in `code/CLAUDE.md` near the predicate-loading section. Hold for now — one occurrence isn't a pattern yet.