Multi-session work bundle on a draft branch. Splits into a clean
sequence of commits later; pushed here so it isn't lost on a reboot.
Reach work
- code/scripts/reach/reach_scram_pj.jl: shutdown_margin halfspace
X_exit (replaces "n <= 1e-4 AND T_f bound" framing); per-step
envelope extraction added.
- code/scripts/reach/reach_scram_pj_fat.jl: per-step envelope
extraction added; shutdown_margin discharge logic mirrored from the
tight scram script. 3 probes (10/30/60s) all discharge from the
fat union polytope.
- code/scripts/reach/reach_scram_full_fat.jl (NEW): full nonlinear
PKE scram reach with fat entry. Hits the stiffness wall at
~1.5 s plant time as expected; saves NaN-tolerant per-step
envelopes. Demonstrates concretely why PJ is the right tool for
the longer-horizon proof.
- code/scripts/reach/reach_heatup_pj.jl: T_REF_START_C constant
(entry-conditioned ramp) replaces T_STANDBY-init that was making
the FL controller command cooling at t=0. Per-step extraction
already in place.
- code/configs/heatup/tight.toml: bumped maxsteps; probe horizon
parameterized.
Hot-standby SOS barrier
- code/scripts/barrier/barrier_sos_2d_shutdown.jl (NEW): mirrors the
operation SOS machinery on the hot-standby thermal projection.
Includes the eps-slack pattern (so feasibility doesn't silently
collapse to B == 0).
- code/scripts/barrier/barrier_sos_2d.jl: refactored to use the same
helper.
- code/src/sos_barrier.jl (NEW): solve_sos_barrier_2d helper module
factoring out the SOS construction; eps-slack with eps_cap=1.0 to
avoid unbounded primal.
Library
- code/src/pke_states.jl (NEW): single source of truth for canonical
initial-condition vectors per DRC mode (op, shutdown, heatup) keyed
off plant + predicates.
- code/scripts/sim/{main_mode_sweep,validate_pj}.jl, code/CLAUDE.md:
migrated to pke_states.
Predicates + invariants
- reachability/predicates.json: new shutdown_margin predicate (1%
dk/k tech-spec floor, expressed as alpha_f*T_f + alpha_c*T_c
halfspace). Used as scram X_exit.
Plot script
- code/scripts/plot/plot_reach_tubes.jl: plot_tubes_scram_pj() with
variant=:fat|:tight knob; plot_tubes_scram_full() for full-PKE
3-panel (T_c, T_f, rho); plot_tubes_heatup_pj() reads results/
not reachability/.
Journal + memory
- journal/entries/2026-04-27-shutdown-sos-and-scram-X_exit.tex (NEW):
long-form entry on the SOS hot-standby barrier and the scram X_exit
refactor.
- journal/journal.tex: input chain updated.
- claude_memory/ — three new session notes:
* 2026-04-27-scram-X_exit-shutdown-margin.md
* 2026-04-28-DICE-2026-conference-intel.md (people, sessions,
strategic notes for the May 12 talk)
* 2026-04-28-path1-sos-pj-sketch.md (sketch of nonlinear-SOS via
polynomial multiply-through; saved for an overnight session)
Docs
- docs/model_cheatsheet.md (NEW): one-page reference of state vector,
dynamics, constants, modes, predicates, sanity numbers — the talk
prep cheatsheet Dane asked for.
- docs/figures/reach_*_tubes.png: regenerated with the new mat data.
- presentations/prelim-presentation/outline.md: revised arc per the
April-28 review pass (cuts: Lyapunov-fails standalone slide,
operation-tube standalone slide, SOS standalone; adds: scopes-of-
control framing, scram on the headline result slide).
- app/predicate_explorer.jl: minor.
Hacker-Split: end-of-session scratch bundle
529 lines
19 KiB
Julia
529 lines
19 KiB
Julia
### A Pluto.jl notebook ###
|
||
# v0.20.24
|
||
|
||
using Markdown
|
||
using InteractiveUtils
|
||
|
||
# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
|
||
macro bind(def, element)
|
||
#! format: off
|
||
return quote
|
||
local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
|
||
local el = $(esc(element))
|
||
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
|
||
el
|
||
end
|
||
#! format: on
|
||
end
|
||
|
||
# ╔═╡ 9d14e486-1faa-45a9-b235-6367a039f9da
|
||
begin
|
||
using Pkg
|
||
Pkg.activate(@__DIR__)
|
||
using JSON
|
||
using PlutoUI
|
||
using Plots
|
||
using MAT
|
||
gr()
|
||
end
|
||
|
||
# ╔═╡ 08e80248-89fe-4639-b55c-f10d96de6c31
|
||
md"""
|
||
# 🦎 Predicate Explorer
|
||
|
||
> *Hybrid-systems extension to FRET — the bridge between boolean predicates
|
||
> and continuous-state halfspaces.*
|
||
|
||
This notebook is the visual companion to `reachability/predicates.json`.
|
||
Each FRET-spec predicate (a boolean condition over named variables) maps to a
|
||
**numerical halfspace** over the 10-state continuous vector
|
||
``x = [n,\, C_1, \ldots, C_6,\, T_f,\, T_c,\, T_{\mathrm{cold}}]``.
|
||
|
||
Use it to:
|
||
- Inspect what each predicate *physically means*.
|
||
- See how mode invariants compose from named safety limits.
|
||
- Trace which reach artifact has tried to discharge each obligation.
|
||
- Sketch edits in a UI that mirrors the JSON structure (read-only v1 — edits
|
||
do not persist).
|
||
|
||
The data behind everything below is `../reachability/predicates.json`.
|
||
Edit the JSON, restart this notebook, results re-render.
|
||
"""
|
||
|
||
# ╔═╡ d7cadb58-e7c4-4701-8fcb-380f1e948d42
|
||
md"""
|
||
## 1 · Source of truth — load `predicates.json`
|
||
"""
|
||
|
||
# ╔═╡ c995a73a-c07e-415b-abe8-57abaf9f4b46
|
||
begin
|
||
pred_path = joinpath(@__DIR__, "..", "reachability", "predicates.json")
|
||
pred_raw = JSON.parsefile(pred_path)
|
||
pred_keys_str = join(sort(collect(keys(pred_raw))), ", ")
|
||
md"Loaded `$(relpath(pred_path))`. Top-level keys:
|
||
$(pred_keys_str)."
|
||
end
|
||
|
||
# ╔═╡ 48f4bec1-ac48-4318-ba6e-92dbf80a7b2d
|
||
md"""
|
||
## 2 · Plant-derived constants
|
||
|
||
These are computed once from `pke_params()` (the plant model in `../code/`)
|
||
and used as the basis for every halfspace.
|
||
|
||
| Symbol | Meaning | Value |
|
||
|---|---|---|
|
||
| `T_c0` | Operating-point average coolant T | $(round(308.349; digits=2)) °C |
|
||
| `T_f0` | Operating-point fuel T | $(round(328.349; digits=2)) °C |
|
||
| `T_cold0` | Operating-point cold-leg T | 290.00 °C |
|
||
| `T_standby` | Hot-standby T | $(round(308.349 + pred_raw["derived"]["T_standby_offset_C"]; digits=2)) °C |
|
||
|
||
Hot standby is defined as `T_c0 - 60 °F = T_c0 - 33.33 °C`. Source-of-truth
|
||
constants live under the `derived` key in the JSON.
|
||
"""
|
||
|
||
# ╔═╡ 7ec2cd9d-5c5c-47d7-aa3c-189091b97204
|
||
md"""
|
||
## 3 · Operational deadbands
|
||
|
||
Soft bands used by the **DRC** to switch modes. These are *not* safety
|
||
properties — violating them triggers a mode change or operator alert,
|
||
not damage.
|
||
|
||
| Predicate | Meaning |
|
||
|---|---|
|
||
$(join(["| `$name` | $(get(entry, "meaning", "")) |" for (name, entry) in pred_raw["operational_deadbands"] if !startswith(name, "_")], "\n"))
|
||
"""
|
||
|
||
# ╔═╡ e99c4c2b-7d29-49a4-a474-047f204e331f
|
||
md"""
|
||
### Concretization of `t_avg_in_range`
|
||
|
||
> *"Average coolant in tight operating band — used for heatup → operation transition."*
|
||
|
||
In FRET this is the boolean predicate `t_avg_in_range`. In the continuous
|
||
state space it concretizes to the box
|
||
|
||
```
|
||
T_c0 - 2.778 ≤ T_c ≤ T_c0 + 2.778 (°C)
|
||
```
|
||
|
||
Halfwidth = 2.778 °C ≈ 5 °F (typical PWR T_avg deadband). The two halfspace
|
||
rows live as `state_index = 9` (which is `T_c`) with coefficients ±1 and the
|
||
appropriate offset.
|
||
"""
|
||
|
||
# ╔═╡ 408432c8-2cc6-4505-8623-9f5614a72c4d
|
||
md"""
|
||
## 4 · Safety limits — the hard constraints
|
||
|
||
These are **one-sided halfspaces** corresponding to physical damage mechanisms
|
||
or reactor-trip setpoints. Asymmetric: the plant is not equally vulnerable on
|
||
both sides of the setpoint. These are what reach analyses target.
|
||
"""
|
||
|
||
# ╔═╡ 0ef6cd25-7f9c-4d64-afdb-ffe8af1ac967
|
||
begin
|
||
safety_rows = []
|
||
for (name, entry) in pred_raw["safety_limits"]
|
||
startswith(name, "_") && continue
|
||
meaning = get(entry, "meaning", "")
|
||
concret = get(entry, "concretization", "")
|
||
push!(safety_rows, (name=name, meaning=meaning, concretization=concret))
|
||
end
|
||
md_table = "| Limit | What it protects | Concretization |\n|---|---|---|\n"
|
||
for r in safety_rows
|
||
md_table *= "| `$(r.name)` | $(r.meaning) | `$(r.concretization)` |\n"
|
||
end
|
||
Markdown.parse(md_table)
|
||
end
|
||
|
||
# ╔═╡ 68b1c7bf-c498-41a9-a435-1332c8154035
|
||
md"""
|
||
## 5 · Mode invariants — conjunctions of safety limits
|
||
|
||
`inv1_holds` (heatup) and `inv2_holds` (operation) are safety envelopes,
|
||
declared as a conjunction of named entries from the safety-limits group.
|
||
Changing a limit propagates automatically.
|
||
"""
|
||
|
||
# ╔═╡ 06be0c87-2a28-4ab3-9fa1-ccde3a9b70d9
|
||
begin
|
||
function render_invariant(name)
|
||
haskey(pred_raw["mode_invariants"], name) || return md"_(not present)_"
|
||
entry = pred_raw["mode_invariants"][name]
|
||
components = entry["conjunction_of"]
|
||
components = isa(components, String) ? [components] : components
|
||
comp_md = join(["- `$c`" for c in components], "\n")
|
||
Markdown.parse(
|
||
"**`$name`**: $(get(entry, "meaning", ""))\n\nConjunction of:\n\n$comp_md\n"
|
||
)
|
||
end
|
||
render_invariant("inv1_holds")
|
||
end
|
||
|
||
# ╔═╡ 8a24ff1e-0afb-4707-89d7-fa50e3b74dbd
|
||
render_invariant("inv2_holds")
|
||
|
||
# ╔═╡ e67f3259-ce97-4bf5-9e63-8f3aef7c6c56
|
||
md"""
|
||
## 6 · Mode boundaries — entry / safe / exit / time
|
||
|
||
For each DRC mode, the reach-analysis triple: where does state start, where
|
||
must it stay, where must it land, by when. **Equilibrium modes** have no
|
||
T_max (forever-invariance is the obligation). **Transition modes** have
|
||
both T_min and T_max (reach-avoid is the obligation).
|
||
"""
|
||
|
||
# ╔═╡ a17586e3-b8f5-4608-950b-69ae45edd6b8
|
||
begin
|
||
mb = pred_raw["mode_boundaries"]
|
||
function render_mode(name)
|
||
haskey(mb, name) || return md"_(not present)_"
|
||
m = mb[name]
|
||
kind = get(m, "kind", "?")
|
||
oblig = get(m, "obligation", "?")
|
||
Tmax = get(m, "T_max_seconds", nothing)
|
||
Tmin = get(m, "T_min_seconds", nothing)
|
||
time_md = if Tmax === nothing
|
||
"_(equilibrium mode — no time bound)_"
|
||
else
|
||
tmax_str = "$(round(Tmax / 60; digits=1)) min ($(Tmax) s)"
|
||
tmin_str = Tmin === nothing ? "_unconstrained_" : "$(round(Tmin / 60; digits=1)) min ($(Tmin) s)"
|
||
"T_min: $(tmin_str) · T_max: $(tmax_str)"
|
||
end
|
||
x_entry = if haskey(m, "X_entry_polytope") && isa(m["X_entry_polytope"], AbstractDict)
|
||
ranges = []
|
||
for (k, v) in m["X_entry_polytope"]
|
||
isa(v, Vector) && length(v) == 2 || continue
|
||
push!(ranges, " - `$k`: [$(v[1]), $(v[2])]")
|
||
end
|
||
join(ranges, "\n")
|
||
else
|
||
"_(non-polytope or referenced — see JSON)_"
|
||
end
|
||
x_safe = get(m, "X_safe_predicate", "?")
|
||
x_exit = get(m, "X_exit_predicate", "?")
|
||
Markdown.parse("""
|
||
**`$name`** ($(kind))
|
||
|
||
$oblig
|
||
|
||
**X_entry:**
|
||
$x_entry
|
||
|
||
**X_safe:** `$x_safe`
|
||
|
||
**X_exit:** `$x_exit`
|
||
|
||
**Time:** $time_md
|
||
""")
|
||
end
|
||
render_mode("q_shutdown")
|
||
end
|
||
|
||
# ╔═╡ dc8758ad-904c-4917-9fba-9a3e42e05e34
|
||
render_mode("q_heatup")
|
||
|
||
# ╔═╡ 60bd9d1d-9a04-48e3-a3c9-a84b8bc91009
|
||
render_mode("q_operation")
|
||
|
||
# ╔═╡ 561c3d22-4d11-4511-90b7-63b77829454b
|
||
render_mode("q_scram")
|
||
|
||
# ╔═╡ f50515e2-b205-45ed-acc9-9790697345f8
|
||
md"""
|
||
## 7 · Visualization — `t_avg_in_range` × `n_high_trip` projection
|
||
|
||
A 2D slice through state space showing the operational deadband on `T_c`
|
||
(blue band) inside the high-flux trip on `n` (red ceiling). The intersection
|
||
is the comfortable operating polytope — where reach tubes should live.
|
||
"""
|
||
|
||
# ╔═╡ 015fa1f4-9813-4ede-ad42-8b4f2340bbb8
|
||
begin
|
||
T_c0 = 308.349
|
||
Tc_lo = T_c0 - 2.778
|
||
Tc_hi = T_c0 + 2.778
|
||
n_high = 1.15
|
||
n_low = 0.15
|
||
p = plot(xlabel="T_avg [°C]", ylabel="n",
|
||
xlim=(280, 322), ylim=(-0.1, 1.4),
|
||
title="Operating polytope (intersection of t_avg_in_range × n bounds)",
|
||
size=(700, 450), legend=:topleft)
|
||
# Operating box
|
||
plot!(p, [Tc_lo, Tc_hi, Tc_hi, Tc_lo, Tc_lo],
|
||
[n_low, n_low, n_high, n_high, n_low],
|
||
linewidth=2, color=:green, label="t_avg_in_range × n bounds")
|
||
# Trip lines
|
||
hline!(p, [n_high], ls=:dash, color=:red, label="n_high_trip = 1.15")
|
||
hline!(p, [n_low], ls=:dash, color=:red, label="n_low_operation = 0.15")
|
||
vline!(p, [Tc_lo, Tc_hi], ls=:dot, color=:blue, label="t_avg_in_range ± 2.78 °C")
|
||
scatter!(p, [T_c0], [1.0], color=:black, markersize=8, label="x_op (operating point)")
|
||
end
|
||
|
||
# ╔═╡ 210a3da2-6a5d-48b2-a48e-d87132d5a32f
|
||
md"""
|
||
## 8 · Edit panel (UX preview — does not persist)
|
||
|
||
This panel shows what live editing would look like. **Sliders below do not
|
||
write back to `predicates.json`** in v1. Use it to feel out the workflow;
|
||
file an issue if a key knob is missing.
|
||
"""
|
||
|
||
# ╔═╡ 3770e247-c2bd-41a1-9470-0f145f73a894
|
||
@bind ui_t_avg_halfwidth Slider(0.5:0.25:8.0, default=2.78, show_value=true)
|
||
|
||
# ╔═╡ 8c67770b-58f8-4868-a3e2-53620565ab5d
|
||
@bind ui_n_high_trip Slider(1.05:0.01:1.30, default=1.15, show_value=true)
|
||
|
||
# ╔═╡ f88f6809-ad3e-4696-bb12-ad8c30d66a9a
|
||
@bind ui_t_standby_offset_F Slider(-90.0:5.0:-30.0, default=-60.0, show_value=true)
|
||
|
||
# ╔═╡ b7fdf92d-948b-41f2-8516-dd8fc20c2efe
|
||
md"""
|
||
**Sketched changes (preview only):**
|
||
|
||
- `t_avg_in_range_halfwidth`: $(ui_t_avg_halfwidth) °C (current JSON: 2.778)
|
||
- `n_high_trip`: $(ui_n_high_trip) (current JSON: 1.15)
|
||
- `T_standby` offset from T_c0: $(ui_t_standby_offset_F) °F (current JSON: -60.0)
|
||
|
||
To apply, edit `../reachability/predicates.json` directly. (Write access
|
||
will land in app v2.)
|
||
"""
|
||
|
||
# ╔═╡ 0560c66a-c12f-4f15-bc49-2c9c8164abd1
|
||
md"""
|
||
## 9 · Reach traceability
|
||
|
||
Which reach artifact has covered which mode invariant? Manual map for now —
|
||
later versions will pull this from the latest reach-result `.mat` files.
|
||
"""
|
||
|
||
# ╔═╡ 1f6abf31-3618-46a5-9f57-4cb3c8022f89
|
||
md"""
|
||
| Mode | Invariant | Reach status | Artifact |
|
||
|---|---|---|---|
|
||
| `q_operation` | `inv2_holds` | ✅ all 6 halfspaces pass (linear, approximate) | `code/scripts/reach_operation.jl` |
|
||
| `q_operation` | `inv2_holds` | ❌ Lyapunov barrier fails all 6 (anisotropy) | `code/scripts/barrier_lyapunov.jl` |
|
||
| `q_heatup` | `inv1_holds` | ⚠️ TMJets nonlinear works to 10s only (stiffness) | `code/scripts/reach_heatup_nonlinear.jl` |
|
||
| `q_heatup` | `inv1_holds` | ❌ no linear reach (time-varying ref needs LTV) | — |
|
||
| `q_scram` | (TBD) | ❌ not started | — |
|
||
| `q_shutdown` | (TBD) | ❌ not started (trivial — constant u) | — |
|
||
"""
|
||
|
||
# ╔═╡ a74106fb-d501-428a-a02e-41f55b49b3da
|
||
md"""
|
||
## 9b · Live reach result ingestion
|
||
|
||
If `code/scripts/reach_operation.jl` has been run, `reach_operation_result.mat`
|
||
exists in `../reachability/`. Load it and show per-halfspace margins live —
|
||
no more hand-maintained traceability table.
|
||
"""
|
||
|
||
# ╔═╡ 37d6b212-9f00-4684-9f91-50c7e17cbd62
|
||
begin
|
||
reach_mat_path = joinpath(@__DIR__, "..", "results", "reach_operation_result.mat")
|
||
have_reach_mat = isfile(reach_mat_path)
|
||
if have_reach_mat
|
||
reach_mat = matread(reach_mat_path)
|
||
md"""**Loaded** `reach_operation_result.mat` — keys: $(join(sort(collect(keys(reach_mat))), ", "))."""
|
||
else
|
||
md"""_(no `reach_operation_result.mat` found — run `cd code && julia --project=. scripts/reach_operation.jl`)_"""
|
||
end
|
||
end
|
||
|
||
# ╔═╡ df53126a-1efa-4cc7-87f4-be4ff0808513
|
||
begin
|
||
inv2_haspath = haskey(pred_raw["mode_invariants"], "inv2_holds")
|
||
if have_reach_mat && inv2_haspath
|
||
# Reconstruct per-halfspace margins from the loaded reach tube.
|
||
T_op_const = 308.349
|
||
T_f0_const = 328.349
|
||
Tcold0_const = 290.0
|
||
Tstandby_local = T_op_const + pred_raw["derived"]["T_standby_offset_C"]
|
||
eval_local(expr) = let T_c0=T_op_const, T_f0=T_f0_const, T_cold0=Tcold0_const, T_standby=Tstandby_local
|
||
eval(Meta.parse(expr))
|
||
end
|
||
|
||
# Build A_inv, b_inv from JSON for inv2_holds (same logic as load_predicates).
|
||
inv2_entry = pred_raw["mode_invariants"]["inv2_holds"]
|
||
components = inv2_entry["conjunction_of"]
|
||
A_inv = zeros(0, 10)
|
||
b_inv = zeros(0)
|
||
labels = String[]
|
||
for comp in components
|
||
entry = pred_raw["safety_limits"][comp]
|
||
for hs in entry["halfspaces"]
|
||
row = zeros(1, 10)
|
||
if haskey(hs, "row")
|
||
for pair in hs["row"]
|
||
row[1, Int(pair[1])] = Float64(pair[2])
|
||
end
|
||
else
|
||
row[1, Int(hs["state_index"])] = Float64(hs["coeff"])
|
||
end
|
||
A_inv = vcat(A_inv, row)
|
||
b = eval_local(hs["rhs_expr"])
|
||
b_inv = vcat(b_inv, b)
|
||
push!(labels, comp)
|
||
end
|
||
end
|
||
|
||
X_lo_mat = reach_mat["X_lo"]
|
||
X_hi_mat = reach_mat["X_hi"]
|
||
|
||
rows = String[]
|
||
push!(rows, "| Safety halfspace | Limit | Reach max | Margin | |")
|
||
push!(rows, "|---|---:|---:|---:|---|")
|
||
for k in 1:size(A_inv, 1)
|
||
a = A_inv[k, :]
|
||
env = (a .> 0) .* X_hi_mat .+ (a .< 0) .* X_lo_mat
|
||
zero_mask = (a .== 0)
|
||
env[zero_mask, :] .= X_hi_mat[zero_mask, :]
|
||
max_ax = maximum(a' * env)
|
||
margin = b_inv[k] - max_ax
|
||
badge = margin >= 0 ? "✅" : "❌"
|
||
push!(rows, "| `$(labels[k])` | $(round(b_inv[k]; digits=3)) | $(round(max_ax; digits=3)) | $(round(margin; digits=3)) | $badge |")
|
||
end
|
||
Markdown.parse(join(rows, "\n"))
|
||
else
|
||
md"_(reach result or inv2_holds unavailable; cannot render live margins)_"
|
||
end
|
||
end
|
||
|
||
# ╔═╡ 9bd8d609-ffa4-46a7-8f0f-d093d87e45e9
|
||
md"""
|
||
## 9c · 2D projection chooser
|
||
|
||
Pick any two state coordinates; render the operating polytope inside the
|
||
safety limits, with the reach tube envelope overlaid (if loaded).
|
||
"""
|
||
|
||
# ╔═╡ e4d56600-7a46-46a1-a7c9-f65a5db95f66
|
||
@bind proj_x Select(["n", "T_f", "T_c", "T_cold"], default="T_c")
|
||
|
||
# ╔═╡ 4fdf70de-3f58-4a38-917b-62b3689ce534
|
||
@bind proj_y Select(["n", "T_f", "T_c", "T_cold"], default="n")
|
||
|
||
# ╔═╡ 37251c95-dd46-402f-9579-d4ede2c1e5ff
|
||
begin
|
||
state_idx = Dict("n"=>1, "T_f"=>8, "T_c"=>9, "T_cold"=>10)
|
||
state_units = Dict("n"=>"", "T_f"=>" [°C]", "T_c"=>" [°C]", "T_cold"=>" [°C]")
|
||
|
||
px_idx = state_idx[proj_x]
|
||
py_idx = state_idx[proj_y]
|
||
|
||
# Determine reasonable axis ranges from operating point + 5% margins.
|
||
x_op_demo = [1.0,
|
||
173.4, 467.0, 114.8, 85.3, 6.56, 0.91,
|
||
328.35, 308.35, 290.0]
|
||
px_op = x_op_demo[px_idx]
|
||
py_op = x_op_demo[py_idx]
|
||
px_span = max(0.2 * abs(px_op), 5.0)
|
||
py_span = max(0.2 * abs(py_op), 5.0)
|
||
|
||
p_proj = plot(xlim=(px_op - px_span, px_op + px_span),
|
||
ylim=(py_op - py_span, py_op + py_span),
|
||
xlabel=proj_x * state_units[proj_x],
|
||
ylabel=proj_y * state_units[proj_y],
|
||
title="Projection: $proj_y vs $proj_x",
|
||
size=(700, 450), legend=:topleft)
|
||
|
||
# Overlay reach tube envelope as a rectangle.
|
||
if have_reach_mat
|
||
X_lo_local = reach_mat["X_lo"]
|
||
X_hi_local = reach_mat["X_hi"]
|
||
rx_lo = minimum(X_lo_local[px_idx, :])
|
||
rx_hi = maximum(X_hi_local[px_idx, :])
|
||
ry_lo = minimum(X_lo_local[py_idx, :])
|
||
ry_hi = maximum(X_hi_local[py_idx, :])
|
||
plot!(p_proj, [rx_lo, rx_hi, rx_hi, rx_lo, rx_lo],
|
||
[ry_lo, ry_lo, ry_hi, ry_hi, ry_lo],
|
||
lw=2, color=:red, label="reach tube envelope")
|
||
end
|
||
|
||
# Operating point.
|
||
scatter!(p_proj, [px_op], [py_op], color=:black, markersize=8,
|
||
label="x_op")
|
||
p_proj
|
||
end
|
||
|
||
# ╔═╡ 4d8459c1-a937-4b53-9eca-975261d6a2cd
|
||
md"""
|
||
## 9d · Prompt-jump heatup reach (if available)
|
||
|
||
If `code/scripts/reach_heatup_pj.jl` has been run, the latest result
|
||
shows up here as a T_c reach envelope vs time.
|
||
|
||
This is the singular-perturbation reduction that lets nonlinear reach
|
||
extend past the ~10 s prompt-neutron stiffness wall.
|
||
"""
|
||
|
||
# ╔═╡ 45e8962a-873e-48d3-aac4-70ea0cf36c97
|
||
begin
|
||
pj_mat_path = joinpath(@__DIR__, "..", "results", "reach_heatup_pj_result.mat")
|
||
if isfile(pj_mat_path)
|
||
pj_mat = matread(pj_mat_path)
|
||
md"""**Loaded** `reach_heatup_pj_result.mat` — pending plot rendering."""
|
||
else
|
||
md"""_(no `reach_heatup_pj_result.mat` yet — run `cd code && julia --project=. scripts/reach_heatup_pj.jl`)_"""
|
||
end
|
||
end
|
||
|
||
# ╔═╡ de922a70-b653-4b2c-bb13-ccc14b14ac91
|
||
md"""
|
||
## 10 · Notes for the next pass
|
||
|
||
- v2 should add **write-back** to `predicates.json` (slider → JSON).
|
||
- v3: **derive** halfspace concretizations directly from the FRET spec
|
||
+ a structured vocabulary of physical bounds. The dream FRET-extension
|
||
feature.
|
||
- Hooks into the latest reach-result MAT files, parsed and shown
|
||
per-halfspace.
|
||
- 2D projection chooser (currently fixed to T_c × n).
|
||
- Mode-transition graph rendered live from `mode_boundaries`.
|
||
|
||
---
|
||
|
||
*"Looks ordinary on the surface but is something else underneath."* 🦎
|
||
"""
|
||
|
||
# ╔═╡ Cell order:
|
||
# ╠═9d14e486-1faa-45a9-b235-6367a039f9da
|
||
# ╟─08e80248-89fe-4639-b55c-f10d96de6c31
|
||
# ╟─d7cadb58-e7c4-4701-8fcb-380f1e948d42
|
||
# ╠═c995a73a-c07e-415b-abe8-57abaf9f4b46
|
||
# ╟─48f4bec1-ac48-4318-ba6e-92dbf80a7b2d
|
||
# ╟─7ec2cd9d-5c5c-47d7-aa3c-189091b97204
|
||
# ╟─e99c4c2b-7d29-49a4-a474-047f204e331f
|
||
# ╟─408432c8-2cc6-4505-8623-9f5614a72c4d
|
||
# ╠═0ef6cd25-7f9c-4d64-afdb-ffe8af1ac967
|
||
# ╟─68b1c7bf-c498-41a9-a435-1332c8154035
|
||
# ╠═06be0c87-2a28-4ab3-9fa1-ccde3a9b70d9
|
||
# ╠═8a24ff1e-0afb-4707-89d7-fa50e3b74dbd
|
||
# ╟─e67f3259-ce97-4bf5-9e63-8f3aef7c6c56
|
||
# ╠═a17586e3-b8f5-4608-950b-69ae45edd6b8
|
||
# ╠═dc8758ad-904c-4917-9fba-9a3e42e05e34
|
||
# ╠═60bd9d1d-9a04-48e3-a3c9-a84b8bc91009
|
||
# ╠═561c3d22-4d11-4511-90b7-63b77829454b
|
||
# ╟─f50515e2-b205-45ed-acc9-9790697345f8
|
||
# ╠═015fa1f4-9813-4ede-ad42-8b4f2340bbb8
|
||
# ╟─210a3da2-6a5d-48b2-a48e-d87132d5a32f
|
||
# ╟─3770e247-c2bd-41a1-9470-0f145f73a894
|
||
# ╟─8c67770b-58f8-4868-a3e2-53620565ab5d
|
||
# ╟─f88f6809-ad3e-4696-bb12-ad8c30d66a9a
|
||
# ╟─b7fdf92d-948b-41f2-8516-dd8fc20c2efe
|
||
# ╟─0560c66a-c12f-4f15-bc49-2c9c8164abd1
|
||
# ╟─1f6abf31-3618-46a5-9f57-4cb3c8022f89
|
||
# ╟─a74106fb-d501-428a-a02e-41f55b49b3da
|
||
# ╠═37d6b212-9f00-4684-9f91-50c7e17cbd62
|
||
# ╠═df53126a-1efa-4cc7-87f4-be4ff0808513
|
||
# ╟─9bd8d609-ffa4-46a7-8f0f-d093d87e45e9
|
||
# ╟─e4d56600-7a46-46a1-a7c9-f65a5db95f66
|
||
# ╟─4fdf70de-3f58-4a38-917b-62b3689ce534
|
||
# ╠═37251c95-dd46-402f-9579-d4ede2c1e5ff
|
||
# ╟─4d8459c1-a937-4b53-9eca-975261d6a2cd
|
||
# ╠═45e8962a-873e-48d3-aac4-70ea0cf36c97
|
||
# ╟─de922a70-b653-4b2c-bb13-ccc14b14ac91
|