Mayo-LDCT agentic campaign — RESET 2026-06-19 — START HERE

🛑 The previous campaign (search-20260614-01, 19 solvers) was DISCARDED and purged on 2026-06-19. It was scored on an invalid validation metric, so the dashboard, leaderboard, and the search trajectory itself were all built on a bad signal. Start a FRESH search from iter-1 under the corrected protocol below.

What was wrong (all three fixed in code now):

  1. Val scored the FIRST val_n (4–30) slices of L277 — top-of-volume boundary slices, near-empty / near-identical — not representative anatomy.
  2. The metric used the 256px Sidky inscribed-circle FOV instead of the detector-geometry measurement FOV (237.54 mm → 321 px for L277), discarding the valid 256→321 px annulus (commit 5ced1ec9).
  3. Figures rendered upside-down (default imshow origin) and repeated the same boundary slice.

Corrected protocol for the NEW search (search-20260619-01)

Run-id (NEVER widen the glob): search-20260619-01 Slug: mayo-ldct-claude-agentic-<solver-dash>-search-20260619-01 Mandate: drive all 19 solvers from iter-1 → iter-20 of genuine six-box autoresearch. “2×hr=0 → STOP” is retired; document why a ceiling holds.

  1. Val metric (default now — do NOT revert): score ALL 214 L277 slices (Wagner val patient), evenly-spaced across the volume if val_n<214, with the detector-geometry FOV mask (321 px). Implemented in evaluate_calibrated (gated on AGENT4CT_DATASET=mayo_ldct_2d) + staged_dataset.py. No test patients in the scored metric (Wagner split — test leakage forbidden).
  2. HARD 20-minute compute budget per iter for train + val-score. Size epochs / n_iter / per-image steps to fit. mayo_agentic_iter.sbatch --time=00:30:00 (20-min budget + headroom). No per-solver exceptions (LPD too — the old 5h --time override is gone). See solver_plan.md.
  3. The val FIGURE is OUTSIDE the budget. Per-iter comparison.png is cheap (renders the already-scored recon). The 6-patient leaderboard montage is a SEPARATE unbudgeted job: mayo_showcase.sbatchmake_test_showcase.py (covers all 19 solvers; L277-central + 5 test centrals, full-512, row-flipped). Test patients appear ONLY in the figure (presentation-only, no leakage). Montage policy (user directive, 2026-06-19): regenerate the valtest montage for EVERY solver that records a new iter — INCLUDING regressed / low-scoring / below-baseline results. ALL montages go on the dashboard. Do NOT defer or skip montages to save QOS throughput (the user accepts the 4-slot contention). The only montages to skip are ones with NO valid recon to render (e.g. a 0.0-SSIM OOM/crash iter) — dispatch those once the solver produces a finite recon.
  4. Figures: display full-512 UNMASKED + row-flipped (Mayo truth is stored z-flipped). make_4panel picks evenly-spaced rows (never first-n).
  5. Subagents: Opus 4.8. Every Agent-tool spawn + Workflow agent() uses model:"opus". Never downgrade.
  6. CT-vision caveat: do NOT draw conclusions from CT pixels (vision is unreliable on CT/sinogram imagery). The metric (val_ssim/headroom on the 214-slice, geometry-FOV-masked L277) is the source of truth.

The 19 solvers (SOLVER key → slug-dash)

10 trainers (get a valtest montage): uswin, itnet, itnet_v2, itnet_v3, dual_domain_supervised, dual_domain_bilateral_supervised, learned_primal_dual, hammernik_2017, hammernik_vn, wu_2015_trainable. 9 inference / per-image: naf, ram_zeroshot(→ram), r2gaussian, tv_iterative, tv_iterative_supervised, dual_domain_n2i (PER-IMAGE), dual_domain_bilateral_n2i (PER-IMAGE), diffusion_recon_dcstep_constrained_mayo_v4(→diff-recon-dcstep-constrained-mayo-v4), diffusion_recon_dcstep_unconstrained_mayo_v4(→diff-recon-dcstep-unconstrained-mayo-v4). N2I are PER-IMAGE (section below — do NOT revert to amortized).

Dispatch recipe (env ITER; AGENT4CT_DATASET=mayo_ldct_2d hard-wired in sbatch)

ssh lme-bastion 'cd /cluster/maier/Agent4CT && sbatch \
  --export=ALL,SOLVER=<key>,SLUG=mayo-ldct-claude-agentic-<dash>-search-20260619-01,ITER=<n>,CFG_JSON=/cluster/maier/Agent4CT/agentic_cfgs/<cfg>.json \
  cluster/slurm/mayo_agentic_iter.sbatch'

Ready-to-run loop-tick — paste into /loop in the NEW session

20m Mayo-LDCT agentic campaign tick (run-id search-20260619-01; NEVER widen the glob). GOAL: drive all 19 solvers from iter-1 to iter-20 under the CORRECTED protocol (val = ALL 214 L277 slices + 321px detector-geometry FOV; HARD 20-min train+score budget; val figure OUTSIDE the budget; figures full-512 unmasked + row-flipped). Each tick: (1) ensure README.md + solver_plan.md Step 2 + docs/runs/mayo_campaign_state.md are read this turn; (2) check the 4-slot QOS queue (ssh lme-bastion 'squeue -u maier') + each solver's max iter + best SSIM from docs/runs/mayo-ldct-claude-agentic-*-search-20260619-01/results.tsv; (3) PUBLISH newly-completed iters — targeted per-slug rsync ONLY the campaign dirs (NEVER blanket rsync docs/runs — it re-bloats the index), python3 scripts/rebuild_runs_index.py, git commit + push only if the run count stays sane; (4) for each TRAINER solver (uswin/itnet/itnet_v2/itnet_v3/dual_domain_supervised/dual_domain_bilateral_supervised/learned_primal_dual/hammernik_2017/hammernik_vn/wu_2015_trainable) with a NEW iter, regen its valtest montage via mayo_showcase.sbatch (SEPARATE job — NOT in the 20-min budget); (5) for EVERY free QOS slot, spawn ONE general-purpose subagent (model: opus — Opus 4.8, never a cheaper tier) to set up the next iter for the most-behind solver not yet at iter-20 and not running — the subagent reads README + solver_plan.md + this file + the solver's results.tsv, does the six-box (NUMBERS not CT pixels), SIZES the config so train+score fits the 20-min budget, and dispatches via mayo_agentic_iter.sbatch (env ITER, AGENT4CT_DATASET=mayo_ldct_2d hard-wired); the hard-wired config is staged_canonical data, v3 mayo_ldct_fitted geometry, per-sample-ps, bg_target="truth", val=ALL 214 L277 slices, metric=321px geometry FOV (DEFAULT — do not pass fov=False); NEVER edit shared ddssl_ldct/ modules; dual_domain_n2i / dual_domain_bilateral_n2i stay PER-IMAGE (warm-start pre-pass + per-scan fit; NEVER revert to amortized); (6) when all 19 solvers reach iter-20, STOP the loop (CronDelete) + final dashboard publish. Report one line: solvers-at-20/19, jobs running, figures published this tick.

NOTE: the per-solver knob insights further down this file are from the DISCARDED search-20260614-01 run (boundary-slice metric). Treat them as loose hints about solver behavior, NOT as validated results — re-establish everything under the corrected metric.

iter-1 findings under the CORRECTED metric (search-20260619-01, live)

These are validated under the corrected 214-slice / 321px-FOV metric. iter-2+ subagents MUST apply them:

DATA-COVERAGE is a LIVE LEVER for the END-TO-END trainers (CONFIRMED iter-5, 2026-06-20)

The supervised/end-to-end trainers (uswin, itnet_v3, dual_domain_supervised, learned_primal_dual) load a FIXED stratified train subset ONCE via load_val_split(split='train', n=train_n) and merely reshuffle that same subset every epoch (torch.randperm(train_n)) — they do NOT use RotatingSubsetDataset. So train_n=200 means the net only ever sees 35 % of the 579-slice train pool (3 ps-groups across the 4 train patients: 154 @0.66192, 98 @0.73979, 327 @0.77873). train_n IS load-bearing, NOT inert. Once the per-iter epochs + capacity levers plateau, the next lever is raise train_n (trade epochs DOWN to hold the HARD 20-min wall; optionally keep grad-step parity train_n × epochs constant to isolate data coverage from extra optimization). CONFIRMED: uswin train_n 200→480 lifted hr 0.3532 → 0.3829 (+0.0297, NEW Mayo champion); itnet_v3 train_n 200→400 lifted hr 0.3123 → 0.3288 (+0.0165). Both reverted to their iter-3 BEST config first (NOT the regressed iter-4 capacity-trade). Apply this lever to the OTHER trainers (hammernik_vn, wu_2015_trainable, dual_domain_bilateral_supervised, itnet, itnet_v2, hammernik_2017) once their epoch/capacity levers plateau — it is the strongest untried axis for the trained leaders. (Does NOT apply to the per-image / inference solvers: ram/tv/naf/r2g/N2I/diffusion have no amortized train set.)

AUTHORIZED CODE FIXES — paper-fidelity audit follow-up (user-directed 2026-06-20)

The NAF / R2Gaussian / diffusion-recon audit (workflow wf_8cdb355c, verified against code + papers) found concrete implementation gaps. The user has AUTHORIZED editing these specific solver files — a deliberate, scoped exception to the “never edit solver files” rule — for their NEXT iteration(s). Safety contract for the subagent doing the edit: (1) CONFIG-GATE every new code path behind a new cfg flag whose DEFAULT reproduces the OLD behavior, so concurrent jobs and old cfgs are unaffected; (2) edit on the laptop, python3 -m py_compile, then scp the file to /cluster/maier/Agent4CT/... (cluster is an rsync’d COPY); (3) do NOT edit while a job of that solver is actively importing it. Per the audit these fixes make the impls FAITHFUL but will NOT clear hr>0 on DENSE Mayo (regime-bounded — LD-FBP is near-complete at 2304 views); their real payoff is the 128-view SPARSE benchmarks. The Mayo iter validates the fix runs + does not regress.


N2I solvers are PER-IMAGE self-supervised (2026-06-18 fix — do NOT revert)

dual_domain_n2i and dual_domain_bilateral_n2i were re-implemented from AMORTIZED (one model trained on the 4 train patients, then forward-applied to val/test — wrong for N2I) to genuine per-image Noise2Inverse (commit 1f4c3394, solver files only — ddssl_ldct/training.py untouched, its training_step/predict were already per-sinogram-pure):

  1. short warm-start pre-pass (amortized N2I on the train split, GT-free) → U-Net init;
  2. per-scan fit — each val/test image is optimized on its OWN measurements (angular-subset split), no clean GT, then scored on the SAME L277 val + 5 test scenes via evaluate_calibrated (scoring/loading contract unchanged). Per-image cfg knobs (the search tunes ONE per iter): warm_start (default True), pretrain_epochs, n_iter (per-scan Adam steps), grad_clip=1.0 (mandatory — the 2304-view FBP adjoint overflows otherwise), val_n, outer_wall_s/per_scene_s. training_scheme = noise2inverse_per_image_warmstart_selfsup. GPU-smoke verified (both solvers, finite val_score + figures). NEVER revert to amortized train-then-forward. The 16 prior amortized-N2I runs (mayo 7 + breast 6 + demo 3) were purged 2026-06-18; supervised dual-domain runs were kept. N2I was restarted per-image on all 3 datasets: mayo under search-20260614-01 (driven by this loop); breast_ct + demo_dl under fresh search-20260618-01 via the generic claude_agentic_one_iter.sbatch (env ITER_N, AGENT4CT_DATASET=breast_ct/phantoms, config env DD_CONFIG_PATH). N2I is structurally FBP-bounded on dense-view breast/demo — expect hr near the LD-FBP baseline there.

Per-image N2I knob insights (confirmed across runs, 2026-06-18/19):

Data provenance — staged_canonical (READ BEFORE touching Mayo data)

The mayo_ldct_2d training loader (ddssl_ldct/staged_dataset.py) reads data/mayo_ldct/staged_canonical/: {split}_truth.h5 (dataset key "truth" + a per-slice "ps" array = ps_eff = 0.700857·native_ps/0.703125) and {split}_sino_{lowdose,fulldose}.h5 in the canonical frame (roll + u-flip + slab, per patient, so a uniform angle_start=0 FBP lands on truth). It is built only by data/stage_mayo_canonical.py from the surviving raw/ (truth) + staged_helix2fan_v3/ (per-patient v3 sinos):

python data/stage_mayo_canonical.py --force --validate --subdir staged_helix2fan_v3

(sbatch wrapper cluster/slurm/restage_canonical_v3.sbatch; --validate FBPs the val split per-sample and prints LD-FBP SSIM, expect ~0.81). Do NOT rebuild it with fetch_mayo_ldct.py (writes key "image", no "ps", shuffled) or stage_mayo_sinos.py (older non-canonical packing) — both silently produce data the loader mis-reads. Geometry is v3 (Powell-fitted mayo_ldct_fitted: sod 595.362 / sdd 1086.803 / det_spacing 1.285044 / ps 0.700857).

FOV / masking (corrected 2026-06-18, audited):

Dispatch protocol (cluster: ssh lme-bastion, cd /cluster/maier/Agent4CT)

The 19 solvers

Resumes (10): uswin, itnet, itnet_v2, itnet_v3, dual_domain_supervised, dual_domain_bilateral_supervised, learned_primal_dual, hammernik_2017, hammernik_vn, wu_2015_trainable. Onboards (9) — ALL WIRED + driving (2026-06-16): ram✓, tv_iterative✓, naf✓, r2gaussian✓, tv_iterative_supervised✓, dual_domain_n2i✓, dual_domain_bilateral_n2i✓, diffusion_recon✓ (con+uncon variants; “ddpm” = the reused DDPM prior, no separate recon solver). ALL 19 solvers now have search-20260614-01 iters.

Onboard wiring recipe

Per-solver knob insights / ceilings (updated 2026-06-16)

Publish trigger

Rsync trap (2026-06-18): NEVER blanket rsync docs/runs cluster→laptop. The cluster holds ~100 historical/scratch run dirs that are deliberately pruned from the dashboard; a blanket rsync re-imports them all and bloats the index 78→197. Publish by rsyncing ONLY the campaign slugs: for s in $(ssh lme-bastion 'cd /cluster/maier/Agent4CT && ls -d docs/runs/mayo-ldct-claude-agentic-*-search-20260614-01'); do rsync -az lme-bastion:/cluster/maier/Agent4CT/$s/ $s/; done then rebuild_runs_index.py — sanity-check the count stays ~78 before commit (git checkout -- docs/runs if it jumped). Breast/demo N2I (search-20260618-01) publish the same way, per-slug.

Publish newly-completed iters EVERY tick (per-slug rsync + rebuild_runs_index.py