% ---------------------------------------------------------------------------
% 2026-04-20 — Predicates restructure, mode taxonomy, Julia nonlinear reach
% Deep / A-style invention-log entry.
% ---------------------------------------------------------------------------

\session{2026-04-20}{afternoon session, $\sim$3 hr}{Walk through the
2026-04-17 work with a critical eye, surface three structural errors
hiding in it, restructure the predicates JSON to fix them, formalize
the per-mode reach-obligation taxonomy (equilibrium vs.\ transition),
and take the first swing at Julia nonlinear reach.  The Julia reach
framework works but is limited to $\sim$10-second horizons by
prompt-neutron stiffness.  The remedy (singular-perturbation reduction)
is identified and deferred.}

\section{2026-04-20 — Predicates restructure, mode taxonomy, nonlinear reach}
\label{sec:20260420-afternoon}

\subsection*{How this session started}

Walking through the 2026-04-17 work slide-by-slide with a critical
reviewer looking for inconsistencies.  Each section of the linear
reach story had a latent bug the first pass missed; by the end of the
walkthrough three structural errors were exposed and fixed.

\subsection*{Error 1: the barrier was checking the wrong invariant}

The 2026-04-17 barrier code compared the Lyapunov-ellipsoid reach
against a \emph{symmetric} slab $|T_c - T_{c0}| \leq 2.78$~\unit{\celsius}
— the operational deadband \texttt{t\_avg\_in\_range}.  But that
predicate is used by the DRC for \emph{mode transitions}: crossing it
triggers heatup$\to$operation, not damage.  The barrier should be
checking the \emph{safety limits} — one-sided halfspaces reflecting
actual trip setpoints and physical damage thresholds.  These are not
symmetric:
\begin{itemize}
  \item $T_f \leq 1200$~\unit{\celsius} (fuel centerline design limit).
  \item $T_c \leq 320$~\unit{\celsius} (high-$T_{\mathrm{avg}}$ trip).
  \item $T_c \geq 280$~\unit{\celsius} (low-$T_{\mathrm{avg}}$ trip).
  \item $n \leq 1.15$ (high-flux trip).
  \item $n \geq 0.15$ (operation range only).
  \item $T_{\mathrm{cold}} \leq T_{\mathrm{cold0}} + 15$ (subcooling).
\end{itemize}

\begin{decision}
Restructure \texttt{reachability/predicates.json} into three categories:
\begin{description}
  \item[\texttt{operational\_deadbands}] soft bands used by DRC for
        mode switching.  \texttt{t\_avg\_above\_min},
        \texttt{t\_avg\_in\_range}, \texttt{p\_above\_crit}.
  \item[\texttt{safety\_limits}] hard one-sided halfspaces.  Six rows
        as listed above, plus new heatup-rate ones (see Error 3).
  \item[\texttt{mode\_invariants}] \texttt{inv1\_holds},
        \texttt{inv2\_holds}: declared as \emph{conjunctions} of
        named safety limits, so changing a limit propagates.
\end{description}
Safety proofs target \texttt{mode\_invariants}; DRC transitions
target \texttt{operational\_deadbands}.  The distinction is
load-bearing.
\end{decision}

\subsection*{Error 2: the "2364$\times$ bound" should have included LQR, and didn't tell us}

After the restructure, re-running \texttt{barrier\_lyapunov.m} against
\texttt{inv2\_holds} produced:

\begin{lstlisting}[style=terminal]
=== Lyapunov barrier vs inv2_holds halfspaces ===
  [fuel_centerline]  max a'dx = 2244.1   margin = -1372.4   too loose
  [t_avg_high_trip]  max a'dx =   33.3   margin =   -21.6   too loose
  [n_high_trip]      max a'dx = 1242.0   margin = -1242.0   too loose   (!!)
  [cold_leg_subcooled] max a'dx = 150.7  margin =  -135.7   too loose
\end{lstlisting}

The $n_{\mathrm{high\_trip}}$ row was the stopper: the barrier says
$n$ could deviate by $\pm 1242\times$ nominal.  Physically meaningless.
Fair enough to ask: is this because \emph{LQR isn't in the
barrier computation?}  The code uses $A_{\mathrm{cl}} = A - BK$, but
that detail was buried and Dane rightly flagged it.

\begin{decision}
\textbf{Write \texttt{barrier\_compare\_OL\_CL.m}} to explicitly
measure LQR's contribution to the barrier.  Same Qbar, same
disturbance envelope, two runs: open-loop $A$ vs.\ closed-loop
$A_{\mathrm{cl}} = A - BK$.
\end{decision}

Result (\cref{fig:ol-vs-cl}):

\begin{figure}[h]
  \centering
  \begin{tabular}{lrrr}
  \hline
  halfspace & open-loop & LQR closed-loop & ratio CL/OL \\
  \hline
  \texttt{fuel\_centerline}      & 26{,}941{,}862 K & 1{,}137 K & $4.2 \times 10^{-5}$ \\
  \texttt{t\_avg\_high\_trip}    & 788{,}220 K    & 33.2 K    & $4.2 \times 10^{-5}$ \\
  \texttt{n\_high\_trip}         & 27{,}379{,}931 & 1{,}242   & $4.5 \times 10^{-5}$ \\
  \texttt{cold\_leg\_subcooled}  & 1{,}806{,}166 K& 77.8 K    & $4.3 \times 10^{-5}$ \\
  $\gamma$ (ellipsoid level)     & $1.04 \times 10^{13}$ & $1.85 \times 10^{4}$ & $1.78 \times 10^{-9}$ \\
  \hline
  \end{tabular}
  \caption{Open-loop vs.\ LQR closed-loop Lyapunov barrier bounds on
  each \texttt{inv2\_holds} halfspace, same $\bar Q$ and same
  disturbance envelope.  LQR improves every bound by a uniform
  $\sim$20{,}000$\times$, but the bounds remain physically absurd.
  The open-loop version says $n$ could deviate by 27~million; the
  closed-loop says 1{,}242.  Both unusable.  The ceiling is plant
  anisotropy ($\Lambda/\tau_{\mathrm{thermal}} \sim 10^{-4}$ timescale
  ratio), not controller tuning.  Generated by
  \texttt{barrier\_compare\_OL\_CL.m}.}
  \label{fig:ol-vs-cl}
\end{figure}

\begin{derivation}
Why the eigenvalues barely move but $\gamma$ changes 9 orders of
magnitude:
\begin{itemize}
  \item $\max \Re(\mathrm{eig}\,A) = -0.0125$ — slowest thermal mode.
  \item $\max \Re(\mathrm{eig}\,A_{\mathrm{cl}}) = -0.0124$ — virtually
        identical.  LQR cannot speed up the slowest mode past what
        physics allows.
  \item But $\gamma = c_{\mathrm{inv}} \propto (w_{\mathrm{bar}} \sqrt{B_w^\top P B_w} / \mu)^2$,
        and $P$ changes \emph{shape} between OL and CL: LQR makes the
        disturbance penetrate $V$ far less.  $B_w^\top P B_w$ drops
        substantially under LQR, $\mu$ stays similar.  Ratio squared
        $\to$ nine orders of magnitude.
\end{itemize}
\end{derivation}

\begin{limitation}
Quadratic Lyapunov barrier is insufficient for this plant regardless
of LQR tuning.  Root cause: the plant's natural timescale spread
(prompt-neutron $\Lambda = 10^{-4}$~\unit{\second} vs.\ thermal $\sim 10$~\unit{\second})
forces any Lyapunov $P$ to be ill-conditioned by $\sim 10^4$.  The
resulting ellipsoid stretches absurdly in the fast directions when
projected to physical coordinates.  Fix:\ non-quadratic barrier
(polytopic or SOS).  Neither implemented yet.
\end{limitation}

\subsection*{Error 3: the heatup-rate invariant IS expressible as a halfspace}

In the first version of \texttt{predicates.json}, I put a comment on
\texttt{inv1\_holds} claiming a ramp-rate constraint could not be
written as a state halfspace without state augmentation.  That was
wrong.

\begin{derivation}
From \texttt{pke\_th\_rhs.m}, $\dot T_c$ is linear in
$(T_f, T_c, T_{\mathrm{cold}})$:
$$\dot T_c = \frac{hA(T_f - T_c) - 2 W c_c (T_c - T_{\mathrm{cold}})}{M_c c_c}
= a_f T_f + a_c T_c + a_{\mathrm{cold}} T_{\mathrm{cold}}$$
with
\begin{align*}
a_f     &= \frac{hA}{M_c c_c} = \frac{5 \times 10^7}{1.09 \times 10^8} = +0.4587~\unit{\per\second} \\
a_c     &= -\frac{hA + 2 W c_c}{M_c c_c} = -\frac{1.045 \times 10^8}{1.09 \times 10^8} = -0.9587~\unit{\per\second} \\
a_{\mathrm{cold}} &= \frac{2 W c_c}{M_c c_c} = \frac{5.45 \times 10^7}{1.09 \times 10^8} = +0.5000~\unit{\per\second}
\end{align*}
Coefficients sum to zero exactly: at thermal equilibrium
($T_f = T_c = T_{\mathrm{cold}}$), $\dot T_c = 0$.

A ramp-rate constraint $|\dot T_c| \leq r_{\max}$ is therefore two
halfspaces over the 10-state vector, with three nonzero coefficients
each (on indices $8, 9, 10$):
\begin{align*}
+a_f T_f + a_c T_c + a_{\mathrm{cold}} T_{\mathrm{cold}} &\leq r_{\max} \\
-a_f T_f - a_c T_c - a_{\mathrm{cold}} T_{\mathrm{cold}} &\leq r_{\max}
\end{align*}
Clean polyhedron; no augmentation.  The confusion came from conflating
this with rate constraints on \emph{non-linearly-driven} states — e.g.\
$|\dot n|$ involves $(\rho - \beta) n / \Lambda$, which is nonlinear,
and \emph{that} would need augmentation.  For the coolant temperature
rate, the thermal-hydraulic subsystem is linear-in-state and the
halfspace drops out directly.
\end{derivation}

$r_{\max}$ chosen as 50~\unit{\celsius\per\hour} (tech-spec nominal is
28~\unit{\celsius\per\hour}; add budget for transient overshoot).
\texttt{plant-model/test\_heatup\_rate.m} measures the actual peak
rate from the current \texttt{ctrl\_heatup.m} controller:

\begin{lstlisting}[style=terminal]
=== Heatup rate statistics ===
  max dT_c/dt:  0.0135 C/s  =   48.5 C/hr
  min dT_c/dt:  0.0000 C/s  =    0.0 C/hr
  rate limit:   +/- 0.01389 C/s = +/- 50.0 C/hr
  violates upper? false
  violates lower? false
\end{lstlisting}

Right at 50~\unit{\celsius\per\hour} peak during mid-ramp — barely
inside the budget, and \emph{70\% above tech-spec nominal}.  Would
fail a strict 28~\unit{\celsius\per\hour} interpretation.  Documented
as an open controller-tuning item.

\subsection*{The mode-obligation taxonomy}

The key conceptual move of the session.  Walking through the reach
code, Dane observed that the per-mode reach obligations aren't all the
same shape.  They split cleanly in two:

\begin{decision}
\textbf{Two kinds of mode, two kinds of obligation.}

\textbf{Equilibrium modes} (\texttt{q\_operation}, \texttt{q\_shutdown}):
plant is parked at a setpoint.  Obligation is forever-invariance under
bounded disturbance.  External disturbance matters — it's the thing
that could push the state out.  \emph{This is what the 2026-04-17
operation-mode reach actually proves} (up to the linearization
caveat).

\textbf{Transition modes} (\texttt{q\_heatup}, \texttt{q\_scram}):
plant is being driven from one mode's territory to another's.
Obligation is \emph{reach-avoid}: from $X_{\mathrm{entry}}$, reach
$X_{\mathrm{exit}}$ within bounded time $[T_{\min}, T_{\max}]$, while
maintaining $\mathrm{inv}_{\mathrm{mode}}$ along the way.  External
disturbance in these modes is essentially zero
($Q_{\mathrm{sg}} = 0$ during heatup, rods slam on scram).  The only
``disturbance'' is parametric uncertainty in the plant coefficients,
which we defer.
\end{decision}

The point of per-mode reach is \emph{not} generic disturbance
rejection — it's to prove that the DRC's discrete transitions
physically fire in finite time on the real plant.  The reach tube is
the artifact that transfers discrete correctness
(\texttt{ltlsynt}-guaranteed) to physical correctness.

Formal claim per transition mode (heatup example):
\begin{align}
&\text{Given } x(0) \in X_{\mathrm{entry}}(\text{heatup}), \notag \\
&\text{applying } u = \texttt{ctrl\_heatup}(t, x, \mathrm{plant}, \mathrm{ref}), \notag \\
&\text{with } Q_{\mathrm{sg}} = 0, \notag \\
&\text{we show } \exists\, T \in [T_{\min}, T_{\max}] \text{ s.t.} \notag \\
&\quad x(T) \in X_{\mathrm{exit}}(\text{heatup}) \quad \text{[reach]} \notag \\
&\quad x(t) \in \mathrm{inv1\_holds} \quad \forall t \in [0, T] \quad \text{[avoid]}
\end{align}

The entry/exit sets and time budgets need machine-readable
definitions.  Added a \texttt{mode\_boundaries} block to
\texttt{predicates.json}:
\begin{itemize}
  \item Each mode's \texttt{X\_entry\_polytope} as box bounds per state.
  \item Each mode's \texttt{X\_exit\_predicate} as a reference to named
        predicates.
  \item Each mode's \texttt{T\_min}, \texttt{T\_max} (\texttt{null} for
        equilibrium modes).
\end{itemize}

Values are engineering-reasonable placeholders:
\texttt{T\_max(heatup)} = 5~\unit{\hour} (tech-spec 28~\unit{\celsius/\hour}
over 60~\unit{\degreeFahrenheit} span is 1.19~\unit{\hour} nominal, 4$\times$
margin).  \texttt{T\_min(heatup)} = 2~\unit{\hour} 9~\unit{\minute} (any
faster violates the rate invariant).  \texttt{T\_max(scram)} = 60~\unit{\second}
(NRC expects seconds; 60 is generous for idealized rod insertion).

\begin{limitation}
All mode-boundary values are engineering guesses.  Thesis defense
will require real tech-spec numbers pinned to a specific plant.
Flagged in the JSON as \texttt{\_placeholder\_warning}.
\end{limitation}

\subsection*{WALKTHROUGH.md — standalone reach documentation}

With the mode-obligation taxonomy, predicate restructure, and barrier
findings in hand, wrote \texttt{reachability/WALKTHROUGH.md} as a
standalone document.  Captures the taxonomy table verbatim, per-mode
obligation definitions, code walkthrough, per-halfspace results, and
every known limitation.

\apass{WALKTHROUGH.md should probably be folded into a journal-style
chapter at some point, but for now it's the external-facing doc people
read first.  Keep it in sync when structural things change.}

\subsection*{Julia nonlinear reach — first attempt, partial success}

With linear work consolidated, turned to the real soundness question.
The linear reach proves the LQR closed-loop is safe, \emph{if} we
trust the linearization.  To close the gap, we need nonlinear reach.

Framework choice: \texttt{ReachabilityAnalysis.jl} with TMJets (Taylor
model integration).  Already have Julia scaffolding in
\texttt{julia-port/} and know the package.

Target: heatup mode reach, saturation disabled (simplifying
assumption to establish the framework; will add back saturation as
a hybrid sub-mode later).

\subsubsection*{The first three attempts failed}

\begin{deadend}
\textbf{Attempt 1:} naive RHS with \texttt{plant} as a function
argument.  Fails immediately with
\texttt{MethodError: setindex!(::Taylor1\{Float64\}, ::TaylorModel1\{...\}, ...)} —
Taylor model arithmetic needs the RHS in a specific form.  Need
\texttt{@taylorize}.

\textbf{Attempt 2:} add \texttt{@taylorize} to the function.  Same error.
Missing piece: the macro only handles \emph{constants} in arithmetic,
not struct-field accesses.  Need all plant parameters as \texttt{const}
globals.

\textbf{Attempt 3:} inline all constants.  Still fails.  Running out
of ideas; then noticed that \texttt{min()} inside the body (for the
ramp-reference clamp) is non-smooth — Taylor models can't handle
non-smooth operations.  Also: the raw time argument \texttt{t} in the
signature was interacting badly with TMJets' internal time parameter.
\end{deadend}

\begin{decision}
\textbf{Attempt 4 (landed):} three changes together.
\begin{enumerate}
  \item All plant parameters as \texttt{const} at module scope.
  \item \texttt{@taylorize} on the RHS, all arithmetic scalar (loops
        unrolled, no function calls in the hot path).
  \item Time carried as an \emph{augmented state} $x_{11}$ with
        $\dot x_{11} = 1$.  Instead of $T_{\mathrm{ref}}(t)$, the RHS
        references $T_{\mathrm{ref}}(x_{11})$ with no \texttt{min()}
        — valid while the ramp hasn't hit the target clamp, which is
        true for our probe horizons.
\end{enumerate}
\end{decision}

The final RHS body (see
\texttt{julia-port/scripts/reach\_heatup\_nonlinear.jl}):

\begin{lstlisting}[language=Julia,caption={Taylor-model-aware heatup RHS}]
@taylorize function rhs_heatup_taylor!(dx, x, p, t)
    rho = KP_HEATUP * (T_STANDBY + RAMP_RATE_CS * x[11] - x[9])

    dx[1] = (rho - BETA) / LAMBDA * x[1] +
            LAM_1*x[2] + LAM_2*x[3] + LAM_3*x[4] +
            LAM_4*x[5] + LAM_5*x[6] + LAM_6*x[7]
    dx[2] = (BETA_1 / LAMBDA) * x[1] - LAM_1 * x[2]
    dx[3] = (BETA_2 / LAMBDA) * x[1] - LAM_2 * x[3]
    dx[4] = (BETA_3 / LAMBDA) * x[1] - LAM_3 * x[4]
    dx[5] = (BETA_4 / LAMBDA) * x[1] - LAM_4 * x[5]
    dx[6] = (BETA_5 / LAMBDA) * x[1] - LAM_5 * x[6]
    dx[7] = (BETA_6 / LAMBDA) * x[1] - LAM_6 * x[7]

    dx[8]  = (P0 * x[1] - HA * (x[8] - x[9])) / (M_F * C_F)
    dx[9]  = (HA * (x[8] - x[9]) - 2 * W_M * C_C * (x[9] - x[10])) / (M_C * C_C)
    dx[10] = (2 * W_M * C_C * (x[9] - x[10])) / (M_SG * C_C)

    dx[11] = one(x[1])     # d(time)/dt = 1, in the right Taylor type
    return nothing
end
\end{lstlisting}

Probes at $T = 10, 60, 300$~\unit{\second}:

\begin{lstlisting}[style=terminal]
--- Probe T = 10.0 s ---
  TMJets: 10583 reach-sets
  t reached:    10.0 s of 10.0
  n envelope:   [-0.0005178, 0.005015]
  T_f envelope: [274.46, 295.01] C
  T_c envelope: [274.45, 295.00] C
  T_cold env:   [270.00, 287.16] C

--- Probe T = 60.0 s ---
  Warning: Maximum number of integration steps reached; exiting.
  TMJets: 50000 reach-sets
  FAILED: AssertionError: radius must be nonnegative but is [NaN, NaN, ...]

--- Probe T = 300.0 s ---
  (same as 60s)
\end{lstlisting}

\textbf{10 seconds works}; 60 seconds onward exhaust the step budget
and then propagate NaN.  The 10-second envelope is sound — the
$n$-envelope going slightly negative is over-approximation tax
(physically impossible, numerically correct for a box hull).

\begin{derivation}
Step-size analysis.  TMJets' adaptive stepper shrinks $\Delta t$ to
resolve the fastest active mode.  Prompt-neutron timescale is
$\Lambda = 10^{-4}$~\unit{\second}.  For the Taylor remainder
(\texttt{abstol=1e-10}) to be bounded, the stepper needs
$\Delta t \lesssim 10^{-3}$~\unit{\second}.  Over a 10~\unit{\second}
horizon that's $\sim 10{,}000$ steps — consistent with the observed
10{,}583.

Extrapolate: a 5-hour heatup reach would need $\sim 1.8 \times 10^7$
steps at this cadence.  Totally infeasible, and we'd hit numerical
precision floor long before that.
\end{derivation}

\begin{limitation}
\textbf{TMJets on the full 10-state PKE is stuck at $\sim$10-second
horizons.}  Prompt-neutron stiffness ($\Lambda = 10^{-4}$~\unit{\second}
vs.\ thermal $\sim 10$~\unit{\second}, ratio $10^5$) forces tiny
time steps.  Step budget runs out, then numerical underflow
produces NaN in the precursor-decay arithmetic.  Not a framework
bug; not a tuning issue; a fundamental property of reach on the
full PKE.
\end{limitation}

\subsection*{The singular-perturbation remedy (not done yet)}

\begin{decision}
For hours-long reach, the prompt neutron timescale must be eliminated
from the state dynamics.  Standard move in reactor-kinetics reach
literature: set $\dot n \approx 0$ and solve for $n$ algebraically.
This gives the \emph{prompt-jump approximation} (sometimes written
as the \emph{reduced-order PKE}):
$$(\rho - \beta) n / \Lambda + \sum_i \lambda_i C_i = 0
\quad \Longrightarrow \quad
n \approx \frac{\Lambda \sum_i \lambda_i C_i}{\beta - \rho}$$
valid when $\rho < \beta$ (subcritical).  $n$ becomes algebraic in
the remaining state; the dynamic state drops to 9-D, the stiffness
disappears, and TMJets can take steps on the thermal timescale
($\sim 1$~\unit{\second} per step is fine).

Tradeoff: we lose the prompt-supercritical regime, which is a
$\sim 50$~\unit{\micro\second} transient.  For heatup over hours, that
transient is irrelevant to safety.  Soundness is reduced (we no
longer capture prompt-dynamic trajectories), but the reduction
introduces a bounded error that can be characterized separately.
Deferred to a future session.
\end{decision}

\subsection*{Session result}

Artifacts:
\begin{itemize}
  \item \texttt{reachability/predicates.json} restructured:
        three-layer (deadbands, safety limits, mode invariants)
        + \texttt{mode\_boundaries} with per-mode entry/exit/time.
  \item \texttt{reachability/load\_predicates.m} handles the new format
        including multi-coefficient rows (for the rate halfspace).
  \item \texttt{reachability/reach\_operation.m} and
        \texttt{barrier\_lyapunov.m} now report per-halfspace margins
        against \texttt{inv2\_holds}.
  \item \texttt{reachability/barrier\_compare\_OL\_CL.m} — OL vs.\
        CL Lyapunov barrier.
  \item \texttt{reachability/WALKTHROUGH.md} — 550-line standalone
        document.
  \item \texttt{julia-port/scripts/reach\_heatup\_nonlinear.jl} and
        \texttt{sim\_heatup.jl}.  Nonlinear reach framework proven
        to 10~\unit{\second}.
  \item Six commits on \texttt{main}.
\end{itemize}

Key findings:
\begin{itemize}
  \item Quadratic Lyapunov barrier fails structurally on this plant.
        LQR improves it 20{,}000$\times$ but not enough.  Need
        polytopic or SOS barriers.
  \item Heatup rate is a clean state halfspace (three-coefficient row).
        Current controller peaks at 48.5~\unit{\celsius\per\hour} —
        tight against a strict 28-spec interpretation.
  \item Per-mode reach obligations split cleanly into equilibrium
        (forever-invariance under disturbance) vs.\ transition
        (reach-avoid with time budget, essentially zero disturbance).
  \item Nonlinear reach framework functional in Julia at 10-second
        horizons.  Prompt-neutron stiffness is the wall; reduced-order
        PKE is the known remedy.
\end{itemize}

\subsection*{Open at close}

\begin{itemize}
  \item Reduced-order PKE for long-horizon nonlinear reach.
  \item Heatup reach to $T_{\max} = 5$~\unit{\hour}.
  \item Scram reach (same reduction will apply).
  \item Add polytopic or SOS barrier as alternative certificate.
  \item Port MATLAB to Julia end-to-end; remove MATLAB dependency.
  \item Build a FRET-adjacent UI for exploring the predicates
        $\to$ halfspaces correspondence.
  \item Lab journal to document all of the above (this is what got
        done in the 2026-04-20 evening session — see next entry).
\end{itemize}
