PWR-HYBRID-3/app/predicate_explorer.jl
Dane Sabo c5133401e0 Session work scratch: scram X_exit refactor, hot-standby SOS, fat scram tubes, model cheatsheet, journal entry
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
2026-05-02 23:02:50 -04:00

529 lines
19 KiB
Julia
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### 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