A report archived by the project, published as it was written. All reports · Markdown source

Subagent report: stage 4f — a helicity (pseudoscalar) mode in cubic geometry (2026-09-04)

Provenance: Claude Opus 5 (1M) implementation/numerics subagent, dispatched by the Fable main loop against docs/next-stage-plan-2026-09-04.md §5 item 4f, 2026-09-04. Compute: GB10 (spark-709c), CPU only, no GPU, no ARC (no ARC acknowledgement is owed). Artefacts: an additive change to icemix/panel.py (the new even-parity helicity family, off by default), experiments/06_helicity_cubic/{run_helicity_cubic.py,make_summary.py}, experiments/06_helicity_cubic/results/{results.json,summary.md,run.log,progress.json}, tests/test_stage4f.py. Every number below is traceable to results.json, which make_summary.py renders into summary.md without arithmetic of its own. Nothing was committed; the main loop commits.

The run completed at 11:45:40 (3,413.5 s wall, peak RSS 8.21 GiB, exit status 0). The draft of this report was written while L = 16 was still running, at the main loop’s instruction; the tables were then rebuilt from the finished results.json. §8 keeps the operating material, because how the run was driven is part of the record.


0. Headline

VERDICT: PASS. All seven sizes measured; reading (ii), the FAIL-UP hard stop, was not triggered at any size.

R_h(L) = tau_exp(helicity, q_min) / tau_exp(P_perp, q_min)
L 2 3 4 6 8 12 16
R_h 0.6813 0.5417 0.5131 0.5048 0.5026 0.5010 0.4988
± 0.0051 0.0023 0.0018 0.0017 0.0019 0.0015 0.0025
|P⊥|²/P⊥ 0.4608 0.4687 0.4847 0.4928 0.4958 0.4992 0.4972

R_h falls monotonically to 1/2 — from above through L = 12 and crossing to 0.4988 ± 0.0025 at L = 16, i.e. sitting on 1/2 within half an error bar — exactly as the Gaussian two-photon argument of the plan’s §4 predicts, and in lockstep with the independent |P⊥|²/P⊥ control, which approaches 1/2 from below. The two meet the Wick value from opposite sides by L = 8 and stay there.

There is no slow helicity mode in cubic geometry. The thin-cell pathology of (1,2,2) — where the pseudoscalar is 1.61× slower than the polarisation and is the slowest mode of the whole chain — does not survive the addition of a third thick direction. The odd polarisation P_transverse_h1 remains the headline family at every cubic size, and the note’s “τ is a lower bound on 1/Δ” statements stand unrevised.

The registered readings:

statement outcome
(i) R_h ∈ [0.40, 0.60] at every L ≥ 6 PASS — 0.5048, 0.5026, 0.5010, 0.4988 at L = 6, 8, 12, 16; none out of band
(ii) R_h > 1 at any L ≥ 4 → FAIL-UP, hard stop not triggered — max over L ≥ 4 is 0.5131
(iii) R_h ∈ (0.60, 1] at L ≥ 6 → FAIL-DOWN-SLOW not triggered
(iv) \|P⊥\|²/P⊥ ∈ [0.45, 0.55] at L ≥ 8 PASS — 0.4958, 0.4992, 0.4972 at L = 8, 12, 16
(v) the estimator reproduces R_h = 1.61 at (1,2,2) PASS — see §2; the estimator sees the mode

One thing the run establishes beyond the registered readings: at (1,2,2) the production panel with the new member now makes the helicity the headline family (helicity_total_h1, even, R_h = 1.8986), where the stage-3 panel could not see the slowest mode at all. The parity blind spot that stage 4d §8.6 identified is closed, at no Markov cost, and the fix is now demonstrated to be harmless in cubic geometry rather than merely plausible.


1. What was implemented

The panel change (additive, icemix/panel.py)

PanelSpec.helicity (default False) adds four even-parity families computed from the polarisation components measure_panel already records:

family components what it is
helicity_h1 3 (one per q axis) h_k(q_min) = Im(P_i(q_min)* P_j(q_min)), (i,j,k) cyclic
helicity_total_h1 1 h_x + h_y + h_z at q_min
helicity_h2 3 the same at 2 q_min
helicity_total_h2 1 the sum at 2 q_min

h_k = Re P_i · Im P_j − Im P_i · Re P_j — three multiplies and a subtraction on the float64 group-count values inside the sampling loop, one per axis per harmonic per sample. Both P_i and P_j are transverse to q by construction (asserted in build_panel). Sign and normalisation match experiments/05_thin_even/run_thin_even.py exactly; the run script’s helicity_from_polarisation is the single shared definition and tests/test_stage4f.py asserts the panel’s grouped-count route agrees with it to 1e-9 relative on four independent lanes of a cubic L = 2 state.

icemix.panel.helicity_ratio(panel) computes R_h per family and picks the best resolved q_min member (max over R_h, which is the conservative choice for the FAIL-UP reading).

The change does not touch the existing panel

Verified three ways, all in tests/test_stage4f.py:

  1. Against the pre-change module. git show HEAD:icemix/panel.py was materialised as a temporary module and run on a short deterministic panel at (1,2,2) (1 block, 300 samples, seed 777). Every family’s tau_exp, tau_int and rho_head from the current module is bit-identical to it; those numbers are frozen in the test as GOLDEN_PRE_CHANGE. The same check was run at cubic (2,2,2) with 400 samples during development and also came back bit-identical.
  2. helicity on vs off. With helicity=True the seven stage-3 families return the same tau_exp, tau_int, their errors and their whole rho_head, digit for digit, and the sampling interval and burn-in are unchanged — the observable is a function of numbers already in hand and consumes no RNG.
  3. Against stage 3 itself. The cubic ladder was run with the very tau_guess_sweeps values stage 3 used (read out of experiments/03_campaign/results/results.json), the same seeds (1000 + 7L), block counts, sample counts, per_tau, burn_taus and fast-pass lengths. tau_exp(P_transverse_h1) reproduces the stage-3 table to 1e-6 relative or better at every size — and that residual is the rounding of the six-significant-figure stage-3 constants I compared against, not a difference in the chain (see §5).

2. Calibration at (1,2,2) — reading (v)

Sector: 221,628 W = 0 states in 205 components, giant 221,424, comp(R) = giant, closed under global arrow reversal — reproducing stage 1 and stage 4d exactly.

Exact (30 leading eigenpairs; rho(t) by 2,500 sparse mat-vecs on 15 columns)

quantity this run stage 4d
1/Δ (attempts) 389.95 389.95
tau_exp(helicity_yz) (attempts) 389.45 389.45
tau_exp(P⊥(q_min)) (attempts) 242.07 242.07
R_h exact (asymptotic) 1.6088 1.6088

Reproduced to the last quoted digit from an independent code path (this run’s own enumeration, flip graph, spectrum and observable construction).

Stochastic — the campaign estimator, from the reference state R

512 replicas in 8 blocks, 20,000 samples at 39 attempts (2,000 relaxation times per replica), burn-in 19,497 attempts, giant-start-1, finite-series bias correction applied — i.e. the taurun path the campaign uses.

observable MC tau_exp (att) exact, same estimator (att) rel. error
helicity_yz 378.08 ± 1.92 377.55 +0.14 %
P⊥(q_min) 211.02 ± 0.25 211.60 −0.27 %
R_h 1.7917 ± 0.0094 1.7843 +0.41 %

Reading (v) passes, and the caveat matters. The registered value 1.61 is the asymptotic ratio −1/log λ of the slowest eigenvalue each observable couples to. The stochastic estimator does not measure that: it fits the log-slope of ρ(t) over the window ρ ∈ [0.02, 0.6], which is amplitude-weighted and therefore lands lower for both observables and higher for their ratio. Applying the identical fit to the exact ρ(t), subsampled at the same 39-attempt interval, gives 1.7843 — and the Monte Carlo returns 1.7917 ± 0.0094, agreeing to 0.4 %. So the estimator is faithful; the 1.61 → 1.78 shift is the estimator’s definition, not a defect, and it is measured rather than asserted.

The substantive half of the reading is unambiguous either way: the stochastic estimator sees the helicity mode at (1,2,2), returning R_h ≈ 1.79 ≫ 1 — the helicity relaxing far slower than the polarisation — where every stage-3 panel member is exactly orthogonal to that mode by symmetry. Had the implementation been wrong, this is the number that would have come back at or below 1.

Run through the production panel instead (q on the two thick axes, 512 replicas, 6,000 samples), (1,2,2) now returns headline family helicity_total_h1 (even) with R_h = 1.8986. The panel’s headline is no longer blind to the chain’s slowest mode at this cell.


3. R-parity of the helicity members, verified numerically

Registered as a check rather than assumed from the algebra.

Verdict: helicity even everywhere. This is what makes the family a genuine complement to P⊥ rather than a re-measurement of it.


4. Pre-flight

Timed as briefed, on L = 2 and L = 4, before committing to the rest.

L stage-3 wall (uncontended) measured here ratio
2 15.0 s 54 s 3.6×
4 28.4 s 100 s 3.5×

Mean slowdown ×3.57, projecting 2,330 s (38.8 min) for the remaining sizes and 1,048 s for L = 16, well inside the brief’s “~30 min more” allowance for including 16 — so 16 was kept. The slowdown is machine contention, not the new observable: the helicity is two multiplies and a subtraction per axis per harmonic per sample, on numbers the sampling step has already produced, and it consumes no Markov time at all. What it does cost is analysis: four more families to autocorrelate and jackknife. Timed back to back at L = 2 under the same contention during development, helicity=False took 70.2 s and helicity=True 81.9 s — a 17 % overhead, all of it post-processing, with the two runs’ seven shared families bit-identical. Contention sources were the stage-4a/4b sweep (another session’s job, started 10:43) and, for part of the window, this task’s own test suite. L = 12 in fact took 988 s against its 772 s projection and L = 16 is running slower still (load average reached 22 on a 20-core machine); see §8.

L = 16 in the end took 1,525 s against its 1,048 s projection: the contention cleared part-way through it, so it ran at roughly the projected rate for the first third and much faster afterwards.

Memory: free -g before launch showed 89 GiB available of 121 GiB (RSS-invisible vLLM resident present), load average 3.00. Measured peak RSS of the completed run: 8.21 GiB (meta.peak_rss_bytes = 8,817,414,144; VmHWM agreed), reached in the L = 12/16 panel analysis. Total wall 3,413.5 s (56.9 min); load average at exit 2.32. OMP_NUM_THREADS=12 and --threads 12, per the brief’s cap.

Per-size walls (s): L = 2 → 54, 3 → 99, 4 → 100, 6 → 140, 8 → 229, 12 → 988, 16 → 1,525.


5. The cubic ladder

5.1 The stage-3 reproduction gate

L interval (att) burn-in (att) replicas samples tau_exp(P⊥) here (sw) stage 3 (sw) rel. diff
2 17 8,294 1024 6,000 1.80309 1.80309 +1.8e-06
3 175 87,630 1024 6,000 3.47223 3.47223 −8.1e-07
4 632 316,049 1024 6,000 5.80442 5.80442 +6.3e-07
6 4,387 2,193,321 1024 6,000 12.4284 12.4284 +1.0e-07
8 17,924 8,961,926 1024 6,000 21.8130 21.813 −1.3e-06
12 135,114 67,557,053 1024 6,000 48.4238 48.4238 +8.3e-07
16 561,166 224,466,388 512 6,000 85.9292 85.9292 −8.8e-09

Every residual is at the level of the six-significant-figure stage-3 constants I compared against; the chain is the same chain. Ice-rule violations 0 and winding violations 0 at every size, as in stage 3.

5.2 tau_exp by family (sweeps)

L P_transverse_h1 P_transverse_h2 abs_P_transverse_squared helicity_h1 helicity_total_h1 helicity_h2 helicity_total_h2 headline
2 1.8031 0.98588 0.83084 1.2284 1.1994 0.54950 0.56878 P_transverse_h1 / odd
3 3.4722 1.3284 1.6274 1.8809 1.8306 0.66819 0.68077 P_transverse_h1 / odd
4 5.8044 1.8865 2.8136 2.9782 2.9595 0.93097 0.92914 P_transverse_h1 / odd
6 12.428 3.5269 6.1251 6.2736 6.2174 1.7599 1.7591 P_transverse_h1 / odd
8 21.813 5.8415 10.815 10.963 10.909 2.9172 2.9151 P_transverse_h1 / odd
12 48.424 12.527 24.174 24.260 24.145 6.2671 6.2575 P_transverse_h1 / odd
16 85.929 21.868 42.722 42.860 42.766 10.981 10.966 P_transverse_h1 / odd

Jackknife errors on the three columns the readings use (sweeps):

L 2 3 4 6 8 12 16
P_transverse_h1 ±0.0041 ±0.0053 ±0.0095 ±0.0246 ±0.0421 ±0.0944 ±0.212
helicity_h1 ±0.0087 ±0.0073 ±0.0091 ±0.0176 ±0.0358 ±0.0568 ±0.183
abs_P_transverse_squared ±0.0051 ±0.0044 ±0.0166 ±0.0295 ±0.064 ±0.115 ±0.245

tau_int (sweeps), same rows and columns:

L P_transverse_h1 P_transverse_h2 abs_P_transverse_squared helicity_h1 helicity_total_h1 helicity_h2 helicity_total_h2
2 1.6919 0.88230 0.76863 1.1036 1.0735 0.48461 0.50815
3 3.3664 1.2041 1.5648 1.7721 1.7181 0.63640 0.64862
4 5.6980 1.7701 2.7411 2.8924 2.8705 0.90386 0.90154
6 12.357 3.4470 6.0712 6.2096 6.1600 1.7607 1.7624
8 21.725 5.8129 10.827 10.922 10.871 2.9791 2.9770
12 48.347 12.606 24.198 24.227 24.117 6.5018 6.5100
16 85.779 22.062 42.800 42.887 42.953 11.459 11.467

tau_exp and tau_int agree to a few parts in a thousand for every helicity member from L = 6 up — the mode is a clean single exponential, not a fitted tail on a mixture. Per-family jackknife errors over MSC blocks are in results.json and rendered by make_summary.py (§8); the errors on the ratio itself are in §5.4 and are the ones the readings use.

5.3 The resolved gate — no family fitted to noise

The stage-3 gate is carried unchanged and is doing work:

L families fitted families bounded (unresolved, tau_exp = NaN, upper bound reported instead)
2 all 11 —
3, 4 9 P_longitudinal_h1, P_longitudinal_h2
6 8 P_longitudinal_h1, P_longitudinal_h2, F_flippability_h1
8, 12, 16 7 P_longitudinal_h1, P_longitudinal_h2, F_flippability_h1, n_flippable_total

All four helicity families are resolved at every size, with fit r² between 0.9891 (L = 2, helicity_h2) and 1.0000 (L ≥ 6, every member). No helicity number in this report is a fit to noise. The unresolved families are exactly the ones stage 3 also could not resolve at the slow-tuned interval, for the same reasons (P_longitudinal is near-zero by the divergence-free constraint; F(q) and the flippable count relax on a fast local scale) — the fast-sampling second pass turns them into numbers where it can and an explicit upper bound where it cannot.

5.4 R_h(L), per member

L best q_min member helicity_h1 helicity_total_h1 helicity_h2 helicity_total_h2 |P⊥|²/P⊥
2 0.6813 ± 0.0051 (helicity_h1) 0.6813 0.6652 0.3048 0.3155 0.4608
3 0.5417 ± 0.0023 (helicity_h1) 0.5417 0.5272 0.1924 0.1961 0.4687
4 0.5131 ± 0.0018 (helicity_h1) 0.5131 0.5099 0.1604 0.1601 0.4847
6 0.5048 ± 0.0017 (helicity_h1) 0.5048 0.5003 0.1416 0.1415 0.4928
8 0.5026 ± 0.0019 (helicity_h1) 0.5026 0.5001 0.1337 0.1336 0.4958
12 0.5010 ± 0.0015 (helicity_h1) 0.5010 0.4986 0.1294 0.1292 0.4992
16 0.4988 ± 0.0025 (helicity_h1) 0.4988 0.4977 0.1278 0.1276 0.4972

(The _h2 columns are the ratio of the 2 q_min helicity to the q_min polarisation, so their limit is 1/(2·4) = 0.125 under τ ∼ q^-2, and they approach it from above with the same lattice corrections the stage-3 harmonic ratio shows — a third, free consistency check that the family is measuring what it claims.)

The per-axis member and the summed member agree to within 1 %, as they must in a cubic cell where the three axes are equivalent; taking the max of the two is what helicity_ratio reports and what the readings use.

5.5 What this says physically

Two independent even bilinears in P — the scalar |P⊥(q_min)|² and the pseudoscalar h_k(q_min) — both converge to exactly half the polarisation’s relaxation time, from opposite sides, by L = 8. That is the Wick/two-photon prediction of the plan’s §4 met by a channel the plan did not have, and it is stronger evidence than the |P⊥|² check alone: |P⊥|² at τ/2 is partly a Gaussian identity, whereas the helicity is a different representation of the cubic point group (it changes sign under every improper operation) and had no obligation to land on the same number.

The (1,2,2) anomaly is therefore confirmed to be what stage 4d §6 suspected: a property of cells with a two-dimensional cross-section and only one cell of thickness, not of the Coulomb phase. In cubic geometry the long-wavelength helicity is an ordinary two-photon composite and is faster than the polarisation, so the panel’s headline — and every “lower bound on 1/Δ” statement in the note that rests on it — is unaffected.


6. Feed-card ledger

# what pid launched threads feed card status
1 golden capture, pre- and post-change panel at (1,2,2) and (2,2,2) — 10:35–10:45 1–2 none (sub-minute / ~2 min foreground probes) n/a
2 end-to-end smoke of the run script (--sizes 2 --no-feed --out smoke.json) 1542653 10:41:35 6 none, --no-feed deliberately completed, 127 s; smoke.json deleted
3 tests/test_stage4f.py, then the full suite — 10:44, 10:49 2–4 none (test runs) completed
4 the production run 1563535 10:48:47 12 ~/.local/state/exptop/feeds/spark-709c/ice-mixing-stage4f-1563535 completed 11:45:40, 3,413.5 s, exit 0 — the card carries the run’s own done stamp
5 full test suite, re-run against the finished results.json — 11:47 6 none (test run) completed

One feed card exists for this task and it is the production run’s. No zombie cards were created: the only deliberate stop was of nothing (no run was killed), the smoke probe was given --no-feed rather than a card it would have to clean up, and sub-minute probes opened no feed. If the production run has to be stopped, stop it with tmux send-keys -t stage4f C-c or kill -TERM 1563535 — never tmux kill-session, which delivers SIGHUP and leaves the card reading running against a dead pid (stage 4d §8.5).


7. Defects and deviations recorded

  1. pyrun.sh is unusable in this repository, as in stage 4d: there is no .venv/. Everything ran under ~/.pyenv/versions/3.12.13/bin/python3 (numpy 1.26.4, scipy 1.17.1), as the brief allowed.
  2. exptop is not importable from that interpreter. The run appends ~/repo/project/exptop/src to sys.path and uses the canonical exptop.progress.ProgressReporter — the stage-4d arrangement, copied verbatim. grep -rl "class ProgressReporter" --include='*.py' . | grep -v .venv over this repository returns nothing: no vendored reporter existed and none was created. results.json records which reporter it got (meta.canonical_progress_feed = "canonical"). The repo-local heartbeat runs alongside it with per-writer temp filenames and non-fatal I/O.
  3. A near-zero column has no parity, and the first version of the R-parity check said so wrongly. P_k(q_k) (longitudinal) is identically zero by the divergence-free constraint, so max|v[perm]+v| / max|v| is a ratio of two float-noise quantities and came out ~1.3 instead of ~0. Caught by the test suite before the production run; fixed by scaling each column against the largest member of its family and classifying a numerically-zero column null. No result depended on it, but it would have made all_polarisation_odd read false for a reason that is not physics.
  4. tau_exp fit windows make the (1,2,2) ratio 1.78, not 1.61. Recorded here rather than buried: the registered reading’s 1.61 is an asymptotic eigenvalue ratio and the estimator measures a windowed log-slope. The report quotes both, and the pass criterion compares the Monte Carlo against the exact answer under the identical estimator (1.7843), which is the only like-for-like comparison. Anyone re-reading the plan’s reading (v) should read it as “the estimator reproduces the exact ratio it is capable of measuring”, which it does to 0.4 %.
  5. The machine was contended throughout. The stage-4a/4b sweep from another session (started 10:43) and this task’s own test suite ran alongside; walls are 3.5–4.5× the stage-3 figures and the L = 16 projection of 1,048 s is running long. This affects timings only — the chain, the seeds and every τ are contention-independent, which is why the stage-3 reproduction is exact.
  6. Serialisation was honoured. No Markov-chain measurement was launched until tmux has-session -t stage4abc failed (10:47:57) and pgrep -af run_stage4abc showed no worker. The 4a/4b relaunch under the new session name stage4ab appeared afterwards and was not waited on (it is a single process; the main loop was told).
  7. The incremental-results.json patch post-dates the run it was written for. The main loop asked mid-run for results.json to be flushed after every size so a partial run stays readable. The patch (write after every phase and every size, with meta.complete, and make_summary.py made standalone on a partial file) landed after the production process had started, so that process still wrote results.json once, at the end. run.log carried every per-size line as it landed and was what the draft of this report was built from; the finished tables were then rebuilt from results.json. Any future run gets the incremental behaviour, and make_summary.py will mark such a file PARTIAL RUN.
  8. Two key-name mismatches, caught by the tests and by the first make_summary run. panel.measure_panel records invariants.winding_all_zero, not winding_violations (my assumption from the stage-3 report’s prose). Both make_summary.py and tests/test_stage4f.py were fixed to read the field that exists. No result depended on it — the underlying check is max_abs_winding = 0.0 at every size — but it is the kind of thing that would have silently dropped an invariant assertion had the key been optional rather than raising.

8. Operating record and how to re-derive everything

The run was launched detached, per the conventions, and survived this session’s teardown of its watcher shells — which is exactly the point of the container.

tmux session stage4f (single window)
pid 1563535
command OMP_NUM_THREADS=12 ~/.pyenv/versions/3.12.13/bin/python3 experiments/06_helicity_cubic/run_helicity_cubic.py --threads 12
cwd ~/repo/project/ice-mixing
launched / finished 10:48:47 / 11:45:40 — 3,413.5 s, exit 0
log experiments/06_helicity_cubic/results/run.log
local heartbeat experiments/06_helicity_cubic/results/progress.json
exptop feed card ~/.local/state/exptop/feeds/spark-709c/ice-mixing-stage4f-1563535 — completed
verified at launch pid alive and both feeds advancing within 8 s of start; re-verified at 11:26, 11:29, 11:30 and 11:43

Re-derive the summary and the verdict from results.json:

cd ~/repo/project/ice-mixing
~/.pyenv/versions/3.12.13/bin/python3 experiments/06_helicity_cubic/make_summary.py
grep -n "VERDICT" experiments/06_helicity_cubic/results/summary.md

make_summary.py is standalone and does no arithmetic of its own beyond selecting recorded fields: it re-evaluates the readings against the frozen bands from whatever sizes results.json contains, and marks the file PARTIAL RUN if the run did not finish.

Re-run the whole measurement (≈ 57 min on a contended machine, ≈ 20 min free; the calibration alone is ~4 min):

\
tmux new-session -d -s stage4f \
  "OMP_NUM_THREADS=12 ~/.pyenv/versions/3.12.13/bin/python3 \
   experiments/06_helicity_cubic/run_helicity_cubic.py --threads 12 \
   > experiments/06_helicity_cubic/results/run.log 2>&1"

--sizes, --cal-samples, --cal-blocks, --t-max-rho, --k-spectrum, --out and --no-feed are available; --no-feed is the right flag for a sub-minute probe, so that probes never leave a card behind.

If a rerun must be stopped: tmux send-keys -t stage4f C-c, or kill -TERM <pid>. Not tmux kill-session — that delivers SIGHUP, which the canonical ProgressReporter does not handle, leaving the card reading running against a dead pid (stage 4d §8.5). If that happens, stamp the card failed by hand.

Incremental results. After this run was launched the script was patched to flush results.json after every phase and every size (with meta.complete), so a future partial run is readable and make_summary.py renders it. The run reported here predates the patch and wrote results.json once, at the end; run.log carried every per-size line as it landed and was the source for the draft of this report.


9. What was not run, and why

item why not
L = 5, 7, 10, 14, 20, 24 not registered for 4f; the ladder 2, 3, 4, 6, 8, 12, 16 is the brief’s, and the readings are about R_h(L)’s limit, not a fit
the exact (1,2,3) helicity spectrum stage 4d costed it at 31–243 h and did not run it; nothing in 4f needs it
a dispersion map τ(q) on the helicity the plan files this under §6 polish; the 2 q_min helicity member already gives the one free point
a cubic exact-diagonalisation control out of reach (cubic L = 2 has ~2 × 10¹¹ W = 0 states); this is why the calibration is at (1,2,2)
re-running stage 3’s z fit with the helicity family in the panel the headline family is unchanged at every size, so τ(L) and z are unchanged by construction; the main loop can confirm from results.json if it wants the statement in the note

10. Test count

tests/test_stage4f.py adds 17 tests: 10 that exercise the panel change from definitions (the default spec is untouched; the label; the family set; bit-identity against the pre-change module and against helicity=False; component counts and parities; the hel_rows index map is cyclic and transverse; the grouped route equals the direct Im(P_i* P_j); R-parity on the enumerated (1,2,2) giant; the (1,2,2) slow-mode overlaps including the exactly-orthogonal staggered combination and tau_exp = 389.45; the helicity_ratio helper) and 7 that check the registered readings against results.json (the exact calibration reproduces stage 4d; the stochastic estimator sees the mode; R-parity; the stage-3 τ table and zero ice-rule/winding violations; every size carries a helicity family; the readings are internally consistent; the control ratio).

238 tests before stage 4f  ->  255 collected, 255 passed, 0 skipped

Reproduce with

cd ~/repo/project/ice-mixing
OMP_NUM_THREADS=6 ~/.pyenv/versions/3.12.13/bin/python3 -m pytest -q