Skip to content

Merge develop into the tomography branch - #394

Merged
cailmdaley merged 91 commits into
feature/sp_validation-extend-to-tomographyfrom
merge/develop-into-tomo
Oct 5, 2026
Merged

cailmdaley merged 91 commits into
feature/sp_validation-extend-to-tomographyfrom
merge/develop-into-tomo

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

This merges develop into the tomography branch, so that the later merge of the tomography branch into develop (step 4 of #375) becomes a review rather than a rebuild.
The result keeps the tomography branch's API and takes develop's I/O.
Every per-version result stays keyed by bin pair, with non-tomographic results as the ("all","all") pair.
Catalogues are read through develop's readers, and the ("all","all") pair is written through develop's SACC writers.

Closes #374. Part of #375.

How to review

GitHub's diff against the tomography branch is about 30k lines, and almost all of it is develop's own PRs, each reviewed when it merged. Two smaller surfaces matter:

  • What this PR adds to either side (about 5.7k lines): the conflict resolutions, git show --remerge-diff 0d5f2ad1 (2.1k lines) and git show --remerge-diff 8631f608 (0.7k), and the 30 commits after the first merge (git log -p --no-merges 0d5f2ad1..).
  • What develop takes on when the tomography branch merges into it: git diff develop...merge/develop-into-tomo. That is 60 files, +9.0k/−2.2k, of which 1k is n(z) tables. It is the tomographic science, and the part where review moves the science most.

Structure

The first commit, 0d5f2ad, is a true merge of develop (8db2aa5) and only resolves conflicts.
Each later change is its own commit:

Commits What
7781c87 … b9a9e9b Fixes for breakages that the merge left without conflict markers: the workflow scripts on the new API, pol_factor: -1, the B-mode summary and SACC n(z) on the bin-pair shapes, the tests, and this branch's own callers of reshaped helpers
8631f60 Second merge of this branch, after #307 (tomographic leakage). uv.lock re-resolved: shear-psf-leakage develop f1a2c071 (includes CosmoStat/shear_psf_leakage#48), glass 2026.2
45c51e5 Port of #366: pw² only when cell_method == "map", in both the SACC bandpower window and the iNKA fiducial
e6406e4 Port of #367: seeded jackknife patch centres, computed once per version and shared by every bin pair and by both catalogues of a cross pair
f7f2abb, 551f5c9, 465ae28, e3bd484 Review fixes: jackknife ρ/τ draws read under the name the library writes; the catalogue pseudo-Cℓ wrapper defaults to ("all","all"); the object-wise leakage reader accepts #48's row selection; Hartlap debiasing of simulation ρ/τ covariances restored
88fa858, d6356c9 ρ/τ paths in the workflow rules built from one helper; covariance rule robust to NFS cleanup
454e802, a769e10 τ plot: no cov_type=None in the covariance file name; error bars from the configured τ covariance
03bd3ad … 114486c The audit's merge checklist (#374 comment): its regression tests brought in as passing tests, and the fixes they needed
a6e0765, 5c25e3e, 5d3b5d2, 7b34411 From a review of the merge surface: each ρ/τ jackknife draw (cov_estimate_method='jk') gets its own seeded layout instead of all repeating seed 0; the (all,all) SACC readback provides BE (= EB); tomographic COSEBIs refuse a single covariance file for every bin pair

Choices worth a look

  • Catalogue-path covariance without pw². The workflow measures pseudo-Cℓ from catalogues (NmtFieldCatalog), which have no pixels. This branch already dropped pw² from the iNKA fiducial, and develop applied it. pw² now applies only on the map path.
  • Noise below the first band. The analytic iNKA noise below ℓ_min continues at the lowest band's value, as develop had it, because white shape noise is flat (3367b2b, agreed with Sacha in review).
  • The iNKA fiducial stays on CAMB. CAMB differs from develop's CCL by about −4% at ℓ = 1000, mostly through the non-linear model. cs_util's CCL→CAMB conversion dropped m_nu; fixed in Preserve CCL neutrino masses and species in CAMB predictions cs_util#97.
  • File names. The non-tomographic ρ/τ products and the ξ± dump carry _tomo_bin_all, as basename() builds them. Existing products under the old names are recomputed on the next run.
  • No tomographic SACC writers. Tomographic pairs keep this branch's FITS outputs; multi-tracer SACC is Tomographic SACC writers: one tracer and n(z) per bin #376.

Verification

  • The audit's merge checklist (Merge develop into the tomography branch #374, tests from assay/decision-record, epic Scientific audit of sp_validation: decision record, regression tests, defects #377) passes: 19 tests pass, and 2 are strict xfail for per-bin τ centering, whose fix belongs upstream (Centre galaxy ellipticities within the selected tomographic bin shear_psf_leakage#52). Two checklist items needed code changes:

    • the iNKA covariance merge filled its lower cross-polarisation blocks from the wrong HDU (0250493; the merged 36×36 fixture is symmetric and positive definite);
    • plot_cosebis ignored compute_tomography (a71e5fa).

    The other items were already right on the merged branch, and their tests now hold them there.

  • Full container test suite, slow tests included: 516 pass and 1 fails. The failure is a missing configured data path, and it also fails on a769e10. The workflow DAG tests pass, and CI builds the image and runs the unit tests in it.

  • Tomographic ρ/τ rerun on the GLASS mock (the one catalogue with bins): each bin's galaxy mask selects exactly its TOM_BIN_ID rows, and the τ theory covariance is computed per bin from the masked catalogue.

Parity with develop

Both branches ran the full papers/cosmo_val suite on SP_v1.4.6.3 and its _leak_corr variant in the same image: develop 8db2aa5, and this branch d6356c9. To compare, I recomputed and diffed every product.

Identical to float precision:

  • pseudo-Cℓ EE/EB/BB (≤1e-14 σ);
  • ξ± on the npatch=1 grid (≤1e-10);
  • COSEBIs (≤3e-9);
  • pure-E/B means and covariance;
  • the additive bias;
  • the object-wise leakage fit.

ρ/τ differ by up to 0.8σ. This comes from #295's star mask, which drops the 947 of 6,554,897 stars with nonzero HSM flags. Switching the mask off reproduces develop to 1e-12. The flagged stars look like failed fits (e ≈ 0, T ≈ 0.07 against a median of 0.18), so the mask stays. Downstream, the SP_v1.4.6.3 fit moves α 0.029→0.033 (σ 0.011) and η 0.93→1.52 (σ 1.36).

rho/tau shift

Deliberate changes:

  • Seeded patches (Seed the jackknife k-means and drop the patch-centre file #367). The ξ± jackknife covariance diagonal changes by 0.72–1.35×. The npatch=100 means also move, by up to 0.84σ: TreeCorr's tree approximation depends on the patch layout. With develop's patch centres this branch reproduces develop exactly, so the shift comes from the layout, not from this merge. A fix that makes the published means independent of the layout is proposed separately.
    xi npatch=100 shift
  • iNKA covariance (pw² on the catalogue path, ℓ↔index alignment, CAMB fiducial). Each step switched on separately, against the earlier e2e reference (the curves multiply to the total). The leftover difference, up to 7% in EE near ℓ≈70, means the reference also differs elsewhere; its provenance is unknown. Develop has no covariance of its own to compare (Fix one-multipole shift in the pseudo-Cℓ Gaussian covariance theory #372).
    covariance decomposition
  • α from τ₀₊/ρ₀₊ (Update scale-dependent and objectwise leakage computations to tomography #307). For SP_v1.4.6.3, 0.020→0.021. For leak_corr, 0.004→0.009. Develop's ξ^gp/ξ^pp uses the PSF model at galaxy positions, against which the leakage correction was fitted. τ/ρ use the stars. My reading is that this explains the leak_corr gap; I haven't tested it.
    alpha

Tomographic ρ/τ (GLASS mock): τ₀₊ per bin, with and without the bin mask.
tau0+ per bin

Develop's own suite fails in three places, independent of this merge: the pseudo-Cℓ covariance rule (the dict bug of closed #372), COSEBIs and pure-E/B refusing the unblinded ξ± part the same workflow writes, and the τ plot (CosmoStat/shear_psf_leakage#46).

— Claude Opus 5.5 on behalf of Cail

🤖 Generated with Claude Code

cailmdaley and others added 30 commits August 24, 2026 23:18
* chore: add Renovate config (ported from shapepipe)

* chore: gate python bumps behind dashboard approval
The metacal estimator and calibrate_comprehensive_cat.py carried a
workaround that overwrote the metacal no-shear reconvolution-PSF size
with the 1P value — a real fix for an older ShapePipe catalogue whose
no-shear PSF was wrong, now a no-op under the current stack: ngmix builds
one magnitude-dilated reconv PSF and reuses it across all metacal types,
so NOSHEAR and 1P are bit-identical per object.

- calibration.py: drop the overwrite; the no-shear branch simply uses the
  no-shear reconv-PSF column it already reads.
- calibrate_comprehensive_cat.py: drop the override and the now-redundant
  NGMIX_T_PSF_RECONV_NOSHEAR_orig column.
- test_calibration.py: update the estimator-stdout note (no hack line now).

No output changes under the current stack. test_calibration.py: 8 passed.


Claude-Session: https://claude.ai/code/session_01Xthu9uZC17TsaeXh899fhc

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The provisional SHA pin predated the fork's merged protocol+packaging
PRs (and its commit is now orphaned). The library is changing quickly,
so pyproject tracks the fork's main and uv.lock carries the exact
commit; updates land via uv lock --upgrade-package smokescreen. The
DRAW_SCHEME custody assertion fails closed if an install's draw
semantics ever drift from the committed blind.


Claude-Session: https://claude.ai/code/session_01G9MahwJEQ1t9EuvUXijmy3

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
check.txt/format.txt/report.md are written by lint.yml during each run;
the autofix step's 'git add -A' kept committing them back. Gitignore them
(plus the residual-pass variants) and drop the tracked copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b34pAS3bXRxN5hVdyq5Sw
* additive bias calculation for paper updated to v1.4.6.3

* added config

* added fill_photoz script

* fill photoz bands fixes

* adding mag errors to fill_photoz

* added Z_ML to fill_photoz

* added more flags to fill_photoz

* added 0p7 and 1p0 aperture magnitudes to PhotoPipe + SP output

* Fixed fill photoz script with new MP_NAME type

* ruff autofix (format + safe lint fixes)

Pushed by the lint gate.

* removed leftover git marker

* fill_photoz_bands: spot-check FITS/HDF5 row order before filling a tile

The fill pairs FITS row k with the k-th HDF5 row of the tile (sorted-index
order) and only ever verified the row *count*, so a PhotoPipe tile ordered
differently from the comprehensive catalogue would be filled with silently
mismatched photo-z.

After the size check, compare RA/Dec for a small sample of rows -- up to five
at each end plus evenly spaced interior rows, --n_check_rows (default 10),
--check_tol_arcsec (default 0.5). Only the sampled HDF5 rows are read (one
fancy-index into the already-sorted index array), so the cost is a handful of
point reads per tile rather than a full per-row match. A failing tile is
warned about, counted as a row-order mismatch in the end-of-run summary,
counted towards the consecutive-failure abort, and skipped without being
added to done_tiles -- exactly as a size mismatch is, so a resume retries it.
--n_check_rows 0 disables the check.

HDF5 columns are RA/Dec (cat_config.yaml ra_col/dec_col for SP_v1.4.x). The
FITS names are resolved at runtime from a candidate list; ALPHA_J2000 /
DELTA_J2000 is first, verified against a real DR6 tile
(/n17data/UNIONS/WL/photometry/UNIONS_DR6/UNIONS.001.227_SP_ugriz_photoz_ext.cat).
If no candidate pair matches, the check disables itself with a warning rather
than skipping tiles.

Test: synthetic 3-tile HDF5 + FITS pair, tiles interleaved so the
non-contiguous write path runs too; the two aligned tiles fill, the tile with
reversed FITS rows is skipped and left empty, and --n_check_rows 0 reproduces
the old unchecked behaviour.

* fill_photoz_bands: row-order check ignores invalid positions, fails if none

A sampled row with a non-finite or sentinel (|Dec| > 90) coordinate on
either side carries no information about the FITS/HDF5 pairing.  Before,
such a row made `sep <= tol` False (a false row-order failure), and an
all-NaN sample raised a RuntimeWarning from np.nanmax.  Now those rows are
excluded from the comparison, n_checked reports only the rows actually
compared, and a tile whose sample has no comparable row is treated as
unverifiable and skipped (distinct warning), so it is retried on resume
rather than written blind.

Adds a direct unit test covering partial/all-invalid samples, a reversed
remainder, and a single-row tile (one-element h5py fancy index).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: martinkilbinger <martinkilbinger@cea.fr>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cail Daley <cail.daley@cea.fr>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…vivor, guard against recurrence (#320)

* cat_config: drop duplicate SP_v1.4.6 / SP_v1.3.6 blocks, repair the survivor

`cosmo_val/cat_config.yaml` carried two top-level `SP_v1.4.6:` keys and two
`SP_v1.3.6:` keys. PyYAML keeps the last, so the second block of each pair
was what every consumer saw — and both were stale. They arrived in merge
c22f075, which resolved a conflict by keeping both sides (and renamed the
dead `SSP_v1.4.6_msel` key to `SP_v1.4.6` in the process).

Delete the shadowing blocks and repair the surviving `SP_v1.4.6` to the
post-aa774b65d convention:

  * `cov_th` stays at A = 2894.03 deg^2 / n_e = 5.0935 (the nside-4096
    footprint mask every other live entry uses), not the 2405 / 6.128 pair
    the shadowing block held.
  * `shear.redshift_distr` -> `shear.redshift_path`: the code only ever reads
    `redshift_path`, so the v1.4.6 n(z) was under a key nothing reads while
    the winning block pointed at the v1.0-era `dndz_SP_A.txt`.
  * `shear.mask` (read nowhere) -> entry-level `mask:` pointing at
    `mask_map_footprint_nside_4096.fits`, matching SP_v1.4.5 / SP_v1.4.6.3
    and the mask `A` was measured from.
  * `shear.R: 1.0` — the delivered e1/e2 columns already have the response
    applied; the shadowing block's `R: 0.92` double-counted it and inflated
    xi_pm by 1/R^2 = 18% against a covariance that never sees R.

`SP_v1.3.6`'s surviving block already carries the nside-4096 footprint mask;
its cov_th still holds the old 2405 / 6.128 pair and needs regenerating
separately.

* tests: reject duplicate keys in cat_config.yaml

`yaml.safe_load` silently keeps the last of a repeated key, so a merge that
resolves a conflict by keeping both sides leaves the shadowed block invisible
to every consumer *and* to every test. That is how two `SP_v1.4.6` and two
`SP_v1.3.6` entries survived on develop.

Add a `yaml.SafeLoader` subclass whose `construct_mapping` raises on a
repeated key, and assert `cosmo_val/cat_config.yaml` parses clean with it.
Fails on the pre-fix config with `duplicate key 'SP_v1.3.6' at line 522
(first seen at line 148)`.

* cat_config: recompute SP_v1.3.6 cov_th (A, n_e, sigma_e) for the nside-4096 footprint

aa774b6 repointed SP_v1.3.6's mask to the 2894 deg² footprint but left its
cov_th holding v1.4.6's old numbers. Recomputed from v1.3.6's own catalogue
with sp_validation.survey; the same code path reproduces SP_v1.4.6.3's
committed A / n_e / sigma_e bit-for-bit. n_psf left as it was.
* Fixes from the snakemake inference run report (#300)

Rules: drop the /automnt prefix from the hardcoded paths in xi_highres
(twopoint.smk) and covariance_glass_mock (covariance.smk). /automnt/nXXdataN
does not exist on the node that owns that disk, so a job landing there fails
immediately, before any log is written. Every canonical path in common.py
already uses the plain /nXXdataN form.

Docs: workflow/README.md gains a note on the /automnt trap and one on host
~/.local shadowing the container's pinned Snakemake; cosmo_inference/README.md
recommends CosmoSIS --mpi over the fragile upstream --smp process pool
(cosmosis#170) and cosmosis >= 3.16.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS3u2QuVSK1Q2CVaxcPNxU

* workflow: profile-driven apptainer containerization (set A)

candide profile now owns the container: software-deployment-method:
apptainer + apptainer-args carries the bind mounts (matching the app
bash-function binds in the top-level UNIONS CLAUDE.md), replacing the
old rationale for leaving containerization to each rule. Rewrites the
profile's doc comment to describe the new model and its two documented
exceptions (xi_highres MPI, covariance_cosmocov host toolchain).

Adds a container_smoke rule (workflow/rules/container_smoke.smk +
scripts/container_smoke.py) as a cheap end-to-end check of the
profile-driven container path (editable sp_validation import, numpy
+ OMP_NUM_THREADS, git provenance) via `script:`, wired unconditionally
into workflow/Snakefile.

Reconciles image_sims/Snakefile's container: None comment: it now
documents that the two-image (SIF/SIF_PIPELINE) chain is a per-rule
container: choice in image_sims.smk, still wrapped by the profile's
apptainer deployment -- not a rule-owned apptainer exec call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS3u2QuVSK1Q2CVaxcPNxU

* workflow: profile-driven apptainer containerization (set B: image_sims)

Strip every rule's explicit apptainer exec wrapper (_EXEC_PREFIX/EXEC/
EXEC_PIPELINE) from image_sims.smk. Each compute rule now carries a
plain per-rule container: SIF / container: SIF_PIPELINE directive;
Snakemake wraps the shell: command via the profile's
software-deployment-method: apptainer + apptainer-args (set A).

PYTHONPATH/PSF_DICT/OMP_NUM_THREADS injection and the SLURM_* env
strip move from apptainer --env/-u flags to plain shell VAR=value /
env -u syntax at the front of each shell: string (_ENV_PREFIX) --
identical effect, no apptainer-specific mechanism, works the same
whether or not the command is container-wrapped.

im_mbias split into im_mbias_config (run:, host-side git/provenance
introspection + yaml write -- Snakemake never containerizes run:
regardless of container:, so this must stay a driver-side step) and
im_mbias (shell:, container: SIF, runs the actual m-bias compute).
This was the one rule whose apptainer call lived inside a run: block's
trailing shell() -- splitting it out is what makes container: apply
to it at all.

binds: dropped from image_sims config/schema -- it's now the profile's
apptainer-args (one bind list for the whole workflow), not a
per-run-config value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS3u2QuVSK1Q2CVaxcPNxU

* workflow: profile-driven apptainer containerization (set C: remaining rules + docs)

Completes the pivot for the rules outside image_sims: xi_highres and
covariance_cosmocov keep their container: None + inline apptainer exec /
host-toolchain call (multi-node MPI and a host-compiled binary respectively,
both genuinely incompatible with Snakemake's own container wrapping), now
documented in their rule docstrings as deliberate exceptions rather than
leftovers. No other rule outside image_sims called apptainer directly.

While touching these files, retired the stale /pure_eb/ absolute paths
left from the old repo layout: added workflow.common.WORKFLOW_SCRIPTS
(Path(__file__)-based, correct under both standalone and module-composed
runs) for the handful of shell: rules that call a workflow script directly,
and reused the existing COSMO_INFERENCE constant elsewhere. Removed the
run_cosmo_val rule in twopoint.smk, dead since cosmo_val.smk decomposed it
into per-diagnostic rules (its own docstring says so) and still pointing at
a stale path plus a nonsensical host .local PYTHONPATH injection. Flagged
(not fixed) covariance_process: it calls cosmo_inference/scripts/
cosmocov_process.py, deleted in the #236 cleanup and never restored, so the
rule fails on the default covariance target -- pre-existing, unrelated to
this pivot.

Rewrote workflow/README.md and cosmo_inference/README.md to the new model:
snakemake is a thin host-side tool pinned via `uv tool install snakemake
snakemake-executor-plugin-slurm`, run directly on the host, never from
inside an apptainer shell; the candide profile's software-deployment-method
puts each job in the container instead. Added a short pointer from the
top-level README's dev-shell instructions to workflow/README.md so the two
don't get conflated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS3u2QuVSK1Q2CVaxcPNxU

* workflow: fix script: directive under the profile-driven container

container_smoke failed for real (SLURM jobs 839818/839819) with
ModuleNotFoundError: snakemake.iocontainers -- the container image had its
own snakemake==9.16.3 pip-installed directly (leftover from the old
apptainer-shell-then-snakemake-inside pattern this pivot retires), shadowing
the host-mounted 9.23.1 orchestrator that script: bind-mounts in and
sys.path.extends (appended, not prepended). Removed it and its
snakemake-executor-plugin-slurm/-slurm-jobstep/-interface-* family from the
image (verified Required-by: none outside the family itself).

Separately, apptainer-args never actually isolated host tooling: the image's
own /.singularity.d/env/50-bashrc.sh unconditionally sourced the host
~/.bashrc for every apptainer action, not just an interactive `apptainer
shell` -- so a host dotfile (asdf init) ran on every exec too, pushing host
PATH entries (~/.local/bin) ahead of the image's own /usr/local/bin. A bare
`python` in any shell:/script: rule was silently running the host's
interpreter, invisibly, surviving --cleanenv. Gated the bashrc sourcing on
APPTAINER_COMMAND=shell (set by apptainer itself before these scripts run).

Fixing both surfaced a third, previously-masked bug: every script: rule
(19 files) imports `from snakemake.script import snakemake`, which is
IDE-hint-only in this snakemake version -- snakemake.script exposes no such
runtime attribute (only the Snakemake class), and the preamble that actually
gets pickled in already provides `snakemake` as a plain global before the
rest of the file executes. Removed the broken import repo-wide; the object
resolves via normal global lookup exactly as before, including inside the
functions/branches a few scripts defer it into.

Verified end to end: container_smoke now completes for real through SLURM
(jobid 839822, python 3.12.12, sp_validation editable install resolved,
correct HEAD commit read from inside the job) with no apptainer exec left in
any rule. Dry-run coverage for every rule that owns an edited script
(masks_only, cv_weights, and friends) shows clean DAGs.

The container-image invariants (no in-image snakemake, exec/run must not
source host dotfiles) aren't reproducible from this repo -- the sandbox at
/n17data/cdaley/containers/containers has no tracked build recipe -- so
they're now documented in workflow/README.md for the next rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS3u2QuVSK1Q2CVaxcPNxU

* workflow: remove last live apptainer-exec shell call + stale sweep-script docs

presentation_pte_cosebis was the one rule left with an inline apptainer exec
in its shell: block (container: None override); every sibling presentation_*
rule already relies on the module-level container default. Drop the override
and the raw call so it's wrapped like the rest.

Also update the three sweep-script docstrings (run_xi_sweep,
run_cosebis_ptes_sweep, run_cl_sweep) whose example invocations still showed
the retired apptainer-exec-then-python pattern, to match the plain `python
script.py ...` convention already used by their sibling CLI scripts.

* profile: add --cleanenv to apptainer-args

Per-node smoke tests showed default env passthrough makes container
python resolution nondeterministic (host ~/.local shadowing — the #302
mechanism). --cleanenv makes every job's environment container-defined.
Verified: container_smoke green through the profile with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS3u2QuVSK1Q2CVaxcPNxU

* tests: adapt pure-E/B and glass-mock tests to the tomographic API

calculate_pure_eb now returns one results dict per tomographic bin pair
(#297); the pure-E/B integration test still indexed the flat mode keys.
Unwrap the non-tomographic "tomo_bin_all_tomo_bin_all" entry and correct
the docstring that still advertised the flat return.

glass_mock's map path imports cosmology.compat.camb, which is absent in
the image, so the xfail's raises=AttributeError no longer matched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QndBZicN3QvyZG4XDDPmGs

* docs: sweep comment bloat across the pivot diff

Remove the 'snakemake is injected' comment repeated in 19 script: files
(one note in workflow/README.md instead), trim the candide profile header
to the operational lessons, and cut re-narrations of the container model
in Snakefile/common.py/README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* image_sims: one image, drop dead SLURM env strip, im_mbias_config as script

Collapse sif/sif_pipeline to the single sp_validation image (it ships
shapepipe; must be rebuilt from uv.lock — the current 2026-07-04 image
predates the lock and its numpy 2.5 breaks numba/ngmix). Remove the
env -u SLURM_* prefix: shapepipe#744 gates mpi4py on OMPI/PMI vars, and
--cleanenv strips the host env anyway (verified in-container). Convert
im_mbias_config from a run: block to script:, drop the redundant
os.makedirs, and trim pivot re-narration from comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* covariance: restore cosmocov_process.py as a containerized script: rule

Deleted in the #236 cleanup with no replacement; restored from history to
workflow/scripts/ (the rule is its only caller) and converted the rule
from shell: to script:. Fixes on the way: bare exit() on a non-PD matrix
returned 0 (Snakemake saw success) — now sys.exit(1); eigvalsh for the
symmetric matrix; Agg backend; plot dpi 2000 -> 300. Verified round-trip
on synthetic input in the container. Drop the NOTE and stale comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* tests: container_smoke becomes a real pytest, out of the main workflow

The rule asserted nothing and put a non-scientific artifact in every
paper's results/. Now: tests/data/container_smoke/{Snakefile,script}
driven by test_container_smoke.py (@slow, skipped off-cluster), which
submits one tiny SLURM job through the committed candide profile and
asserts APPTAINER_CONTAINER is set (the job really ran in the image),
the editable install resolved, seed-42 eigh values match, and git works
inside the container. OMP_NUM_THREADS is recorded, not asserted — unset
is the profile's designed state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* workflow: locate shell-invoked scripts via workflow.source_path

Replace the hand-rolled WORKFLOW_SCRIPTS constant with Snakemake's
first-class mechanism, which the docs specifically prescribe because
manual path construction breaks under module composition. The script
goes in input: (not params:, which would cause spurious reruns), so it
also becomes an honest dependency.

Fixes a live bug on the way: papers/bmodes unblinding_ceremony called
'python workflow/scripts/unblinding_ceremony.py', which from the paper
workdir resolves into the paper's own scripts/ dir -- stale since
091bba8 and only detectable at run time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* workflow: use script: for single-process rules, source_path only for MPI

script: resolves relative to the .smk defining it, module composition
included, so it defeats the paper-workdir trap without making scripts
into input files -- and matches what the other 20+ rules already do.
Only xi_highres keeps source_path, where script: is structurally
impossible (snakemake would wrap the whole mpiexec line in one
container).

unblinding_ceremony was already written for script: -- its
_config_from_snakemake was dead code because the rule invoked it via
shell:, so it silently ran _config_from_cli, which re-derives paths
from constants that no longer exist (a cosmo_val dir deleted from the
referenced checkout, and a _PROJECT_ROOT off by one since the script
moved to papers/bmodes/scripts/). The rule declared 6 inputs and 9
params while passing 2 on the command line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* papers/bmodes: delete the unblinding ceremony

Never used, and silently broken: the rule invoked the script via shell:,
so the script's snakemake branch was dead code and its CLI branch
re-derived paths from constants that no longer exist. Nothing imported
it and no rule consumed its output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* containers: run the CI-published image from one shared path

CI builds ghcr.io/cosmostat/sp_validation on every push from uv.lock;
the hand-built SIFs it replaces were stale in ways that only failed at
run time (no shapepipe.modules in one, numpy 2.5 breaking numba in the
other). Every call site -- the Snakefiles, image_sims, the MPI rule's
own apptainer exec, the paper shell drivers, interactive use -- now
names one file, refreshed deliberately (see workflow/README.md).
Overridable with --config container=<path or docker:// URI>.

im_mbias_config read the OCI revision by opening the configured sif
path; it now reads APPTAINER_CONTAINER, since the image a job actually
ran in may be overridden and current.sif is a moving target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* deps: ship CosmoSIS in the image, drop the vestigial cosmology pin

CosmoSIS was an undeclared, user-supplied dependency of the inference
step -- which is why #303 was hand-patched in someone's ~/.local. It
pip-installs into the image against the base gfortran/GSL/cfitsio in
~2 min, so declare it. MPIFC must be set at build time or the sampler
Makefiles silently skip the MPI targets and --mpi fails at load; chains
must run under MPI because the upstream --smp pool is still broken at
3.25.2.

cosmology 2022.10.9 was vestigial: the cosmology.compat.camb adapter
comes from cosmology-compat-camb via glass[examples]. Relocking drops
it and nothing else. UV_PYTHON pins uv to the image's own interpreter,
since $HOME is bind-mounted and uv would otherwise pick a host CPython
carrying none of the stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* containers: pull the CI image by tag, park the MPI rule

Snakemake pulls docker://ghcr.io/cosmostat/sp_validation:develop into a
shared apptainer prefix on first use and never again, so no digest or
path is written down. One constant, CONTAINER_URI in workflow/common.py,
is the single source of truth; host-side callers that need a concrete
file (the paper shell drivers, interactive use) derive it via
workflow/scripts/container_path.py.

xi_highres is parked as a comment block: it has never been runnable --
its shell is a bare 'python run_2pcf_highres.py' while the script
requires --cat-config and --out -- and parking it leaves
covariance_cosmocov as the workflow's only container exception. The MPI
reasoning is preserved in the block for whoever revives it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* tests: drop the glass map-path xfail, its condition is met

The xfail asked for a compatible glass+cosmology pair verified in a
fresh image. glass 2026.2 with cosmology-compat-camb is that pair: the
map path runs end to end (11 shells, 66 spectra, monotonic kappa
accumulation), so the marker now only hides regressions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* docker: install liblapack-dev, pin MPIFC absolutely

cosmosis's bundled MultiNest links -llapack and the base image ships
only the runtime liblapack.so.3 with no dev symlink, so the build died
at 'cannot find -llapack'. It passed on candide only because that
sandbox had liblapack-dev installed at some point.

MPIFC takes the absolute path: /opt/ompi/bin is not always on PATH, and
a miss silently drops the MPI sampler libraries while the install still
reports success.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* docker: point UV_PYTHON at the venv, not the base interpreter

uv pip honours UV_PYTHON over VIRTUAL_ENV, so naming the system
interpreter sent the editable install of sp_validation there instead of
/app/.venv -- the image built fine and then failed its own import smoke
test. The venv's own python satisfies the original intent (uv can't
wander onto a host CPython from the bind-mounted $HOME) while keeping
uv pip pointed at the venv.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hCncxwiiBZm9ixGgdzCNA

* workflow: run the launched checkout's sp_validation by default

Snakemake's `script:` directive already executes the checkout's script files,
while `import sp_validation` resolved to the image's baked copy -- the two
halves of one commit, split. `common.inject_checkout_pythonpath()` prepends the
checkout's src/ to APPTAINERENV_PYTHONPATH (preserving any user-set value), so
the image supplies the frozen dependency stack and the launched tree supplies
sp_validation. This is what the image-sims chain has always done for both repos
(`_ENV_PREFIX` in rules/image_sims.smk); the main workflow now matches it.

Opt out with `--config checkout_pythonpath=false` to reproduce from the image
alone. The flag is parsed tolerantly because `--config k=false` can arrive as
the string "false".

Also drops the hardcoded candide OpenMPI path from workflow/Snakefile: a machine
path does not belong in generic workflow code, and it moves to the candide
profile in a following commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* profiles: add a machine-independent default, make candide the machine layer

workflow/profiles/default carries the container model and nothing else, so the
workflow runs off candide with `--profile workflow/profiles/default -j N`.
Snakemake cannot compose profiles (one --profile, no `inherits:`), so the small
machine-independent set -- software-deployment-method, rerun-triggers,
latency-wait -- is duplicated verbatim in both files, marked GENERIC and
cross-referenced. That is the least-magic arrangement available.

apptainer-args and apptainer-prefix stay out of the shared block: every machine
has its own disks and image cache. candide's apptainer-args now carries
`--env LD_LIBRARY_PATH=/softs/openmpi/...`, previously an os.environ line in
workflow/Snakefile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* presentation: containerize the two rules that ran bare python

The Moriond talk-figure rules called `python` with no container, so they ran
against whatever interpreter the driver happened to have. Let them inherit the
module-level `container:` like every other rule.

The two ImageMagick `convert` rules keep `container: None`: `convert` is a host
tool, absent from the image. Same for covariance_cosmocov, whose docstring says
so directly rather than pointing at a list elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* image_sims: default sif to null and fall back to the workflow's image

`image_sims: {sif: ...}` was a required structural key, so every run config
repeated the image path — a second place for it to drift from what the rest of
the workflow runs. Default it to null and resolve it through the same code path
as every other entry point; a run config still overrides it to name its own
image or a branch tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* containers: give every user their own image, driven by spv-container

The workflow ran out of one shared image directory on candide
(/n17data/cdaley/containers/snakemake-sif) with a hand-maintained current.sif
symlink pointing at whatever Snakemake last autopulled. That only worked for one
person: refreshing the image or repointing the symlink needed write access to
another user's directory, and a refresh moved the ground under everyone at once.

Everyone now runs their own image file at one canonical per-user path,
~/.cache/sp_validation/sp_validation.sif (SPV_CONTAINER overrides it), owned by
a small CLI:

    spv-container pull            # fetch the tag there, atomically
    spv-container status          # revision label vs. this checkout's HEAD
    spv-container exec <cmd...>   # one-off run inside it, candide binds applied

sp_validation.container is stdlib-only on purpose: it runs on the host, outside
the container, so it must import without the scientific stack — and it works
straight from a checkout (`python3 src/sp_validation/container.py status`) with
nothing installed. It also holds CONTAINER_URI, which workflow/common.py loads
from this checkout by file path, so the CLI and the workflow can never name
different images.

`container:` now resolves to that local .sif when it exists and to the registry
tag otherwise (Snakemake accepts either, and autopulls the tag into
.snakemake/singularity). `--config container=...` still overrides both. The
candide profile drops apptainer-prefix accordingly, and common.configure() warns
— once, never fatally — when the local image predates the checkout.

Also drops workflow/scripts/container_path.py, which existed to locate the
shared cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: tell the container story once, around the per-user image

Rewrites the container sections of workflow/README.md, CLAUDE.md,
CONTRIBUTING.md and README.md for the per-user model: one image per person at
~/.cache/sp_validation/sp_validation.sif, `spv-container` to fill and inspect it,
and how `container:` resolves to it. The shared-prefix machinery is gone --
current.sif bootstrap, the atomic-mv refresh recipe, the group-writable TODO.

Trims the commentary while there. The profile pair says "change one, change the
other" once instead of shouting it in three places; off-candide gets a paragraph
rather than parallel billing, since candide is where everyone runs; and the
enumeration of container exceptions is dropped in favour of the docstring on
each rule that opts out.

Also repoints the two paper Snakefiles, which resolve `container:` themselves,
at common.resolve_container -- and replaces the obsolete `apptainer build
--sandbox` recipe in README.md and installation.rst with `apptainer pull`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* containers: add an opt-in writable sandbox, and one resolution order

The pristine SIF is read-only, which is what you want almost always -- but it
lost the one real advantage of the old hand-built sandbox workflow: `pip install`
mid-analysis, when you need a package the image does not carry yet and a CI
rebuild is too slow a loop to think in.

`spv-container sandbox` unpacks the image into a writable directory at
~/.cache/sp_validation/sandbox/, and `spv-container exec --writable` runs against
it so installs persist. Opt-in: nothing builds one for you.

Resolution order is now one thing, shared by the CLI, the run_*.sh drivers and
the workflow's `container:` -- sandbox if it exists, else SIF if it exists, else
the registry tag. Snakemake execs a sandbox directory as happily as a .sif, so a
package installed into the sandbox is there for workflow jobs too, with no
further wiring. `resolve_image()` in sp_validation.container is the single
implementation; common.resolve_container defers to it.

The build stages into a sibling directory and swaps it in, as `pull` does, for a
sharper reason than pull has: a half-written .sif fails loudly, but a
half-unpacked sandbox is still a *directory*, so resolution would elect it and
every job would silently run a broken tree. Building before removing also means a
`--force` rebuild that fails -- a typo in --source, a network blip -- leaves the
sandbox you already had intact, instead of deleting a working environment on the
way to not replacing it.

The cost of a sandbox is that what runs is no longer fully described by a
revision label, so the divergence is made visible rather than left silent:
`status` names which layer is live and says the revision only describes what the
sandbox was built from (falling back to the SIF's label, marked as inferred, when
the sandbox carries none), and the workflow prints one line at launch when a
sandbox is in play. `spv-container pull && spv-container sandbox --force` resets.

Verified on candide (apptainer 1.5.3): unprivileged `build --sandbox` works
through user namespaces with no fakeroot and no subuid mapping; `exec --writable`
persists writes while plain `exec` gets a read-only filesystem; `inspect
--labels` still reports the source image's OCI labels from a sandbox directory;
`--fix-perms` at build time is what keeps the tree removable afterwards (without
it apptainer leaves directories that defeat `rm -rf`, which would strand
`--force`); and a failed `--force` rebuild leaves the existing sandbox and its
contents untouched, with no staging directory left behind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* image: build the cosmosis-standard-library fork into the container

The cosmo_inference .ini templates pointed COSMOSIS_DIR at two different
people's home directories (/home/guerrini/... and a scratch path of Lisa's), so
running the inference pipeline meant either being one of them or editing the
templates by hand. CosmoSIS itself already ships in the image via the `workflow`
extra; only the Standard Library — the tree of module files the pipelines name —
was missing.

Clone and build it at /opt/cosmosis-standard-library, pinned to Sacha Guerrini's
fork at b26fa7ff. That fork is 4 commits ahead of upstream and 373 behind;
the four are what the UNIONS pipelines need (tau statistics, sample_S8, two
z-dependent linear-alignment modules). Carrying them onto current upstream is
future work, noted in cosmo_inference/README.md.

The templates now read COSMOSIS_DIR from %(CSL_DIR)s, which the image sets —
CosmoSIS reads environment variables into an ini's [DEFAULT] section, which is
how the existing %(SCRATCH)s references already work. Off-image, export CSL_DIR
and the same templates work unchanged.

The build follows CSL's documented procedure for a pip-installed cosmosis
(`source cosmosis-configure && make`), but targets `shear/` rather than the
top-level `make`: the top level also descends into likelihood/, building the
Planck, WMAP and ACT likelihoods, which no UNIONS pipeline uses. Of the modules
our templates do name, all are pure Python except two under shear/ — `limber`,
which project_2d.py links, and cl_to_xi_nicaea's nicaea_interface.so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: prune duplicated and historical comments

The container model, the checkout-PYTHONPATH default, the profile GENERIC
mirroring and the CSL_DIR resolution were each explained in three to six
places. Give every concept one home -- workflow/README.md for the user-facing
story, the docstring of the thing itself for mechanism -- and leave pointers
elsewhere. Drop comments narrating what the code used to do; git holds that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* simplify: collapse duplication added by this branch

The container model landed the same few lines in several places; fold each
into one home.

* the four run_*.sh sweep drivers resolved this user's image (and repeated
  the bind list) inline -- now one sourced papers/bmodes/scripts/container_env.sh,
  resolving exactly as sp_validation/container.py does (sandbox first, then
  SPV_CONTAINER/XDG_CACHE_HOME, binds from SPV_APPTAINER_BINDS)
* container.py: one _require_apptainer() instead of three copies of the PATH
  guard, and compare_revision's merge-base calls go through _git
* common.py: resolve_container takes the override value, so image_sims.smk
  no longer wraps IMSIM["sif"] in a synthetic config dict; drop the
  CONTAINER_URI / local_sif / local_sandbox re-exports, which have no callers
* cosmocov_process.py: only Snakemake runs it, so drop the argv entry point
  and its main() indirection, matching im_mbias_config.py
* xip_xim.py: one catalog() builder for the tomographic and non-tomographic
  paths instead of two near-identical treecorr.Catalog blocks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* simplify: give the sweep drivers one shared preamble; fold a duplicated README section

The four papers/bmodes sweep drivers each repeated the worktree path, the
derived script/source dirs, and a full `apptainer exec ... /usr/local/bin/python`
invocation (six sites). container_env.sh now owns all of it and exposes
`spv_python` / `sweep_versions`; argv is byte-identical.

workflow/README.md explained `--config container=` twice, once for a local .sif
and once for a branch tag. One subsection now covers both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* image: actually build CSL — cosmosis-configure exits 0 under set -u without running make

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C856B4eJ3LwXrj9SCiEuMc

* image: drop set -u in the CSL layer — the configure exports append to unset paths

The test -f artifact guard is what keeps a no-op build loud; -u had become
the thing breaking the build instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C856B4eJ3LwXrj9SCiEuMc

* image: point limber's Makefile at Debian's GSL (GSL_INC/GSL_LIB)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C856B4eJ3LwXrj9SCiEuMc

* rebase onto develop: shed tomography-branch remnants

The container/workflow work is orthogonal to the tomography branch it was
accidentally based on. Restore develop's pure_eb docstring, test_cosmo_val
call shape, and glass_mock xfail; keep develop's glass==2025.1 pinned set
(cosmology 2022.10.9 is load-bearing there, not vestigial) and relock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143zcEsfSWr13AfSroMteEC

* docs: make spv-container the install story

README leads with the four-line install (clone, symlink onto PATH, pull,
exec-check); container.py gets a shebang + exec bit so the symlink is a real
CLI with no packaging. installation.rst carries the depth (subcommands,
per-user model, sandbox, raw apptainer/docker); CONTRIBUTING and
workflow/README point at the same symlink step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143zcEsfSWr13AfSroMteEC

* CSL runtime coverage: import-test template modules; patch fork for scipy>=1.15; add fast-pt

The image build only compiled CSL; pure-Python modules were never loaded
until a pipeline ran. Two runtime breaks shipped in a green image:
scipy>=1.15 removed scipy.special.lpn (legendre.py, reached by every
real-space likelihood via spec_tools -- #316), and project_2d.py imports
fastpt, which the image never carried.

- test_csl_modules.py imports every .py module the ini templates
  reference, in-image (skips without CSL_DIR/cosmosis)
- Dockerfile applies upstream a8a941d5 (lpn fix) as a patch until the
  fork absorbs it (#316)
- fast-pt>=3.2,<4 joins the workflow extra (4.0 restructured; CSL
  expects 3.x)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmGA86b7YyQn54JFj78ssM

* ruff autofix (format + safe lint fixes)

Pushed by the lint gate.

* Repoint CSL at the UNIONS-WL org fork; trim fast-pt annotation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmGA86b7YyQn54JFj78ssM

* ruff autofix (format + safe lint fixes)

Pushed by the lint gate.

* lint: noqa E402 on the in-section container-smoke import

* Drop CSL scipy workaround after fork merge

Pin the container to the current UNIONS-WL fork main commit, remove the now-obsolete lpn patch, and update the inference documentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01XTNCvVQNLXZVTiDR1PZaVG

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Concurrent doc-deploy runs on back-to-back pushes to develop both hit
peaceiris/actions-gh-pages at once, and the loser fails with
"cannot lock ref 'refs/heads/gh-pages'" (seen in run 34250154443).
Scope the job to a gh-pages-<ref> concurrency group; cancel-in-progress
is fine since develop only ever has one active deploy target and the
newest push should win over a stale one still building.


Claude-Session: https://claude.ai/code/session_01Wk8SZkCRuKQ5xu5g38zHpx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Resolve reporting bins from their edges rather than bin means. This keeps the 12--83 arcmin fiducial window at bins 9--15 and preserves the intended degrees of freedom.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The texlive-latex-base/-recommended/-fonts-recommended + cm-super set added
in #338 costs ~1.3 GB and still lacks type1cm.sty, so matplotlib's usetex
path stays broken. Replace it with TinyTeX (~270 MB measured), pinned to
TeX Live 2025 on both halves: bundle v2026.02, and tlmgr pointed at that
year's frozen tlnet-final historic mirror. Packages are an explicit tlmgr
list covering matplotlib's usetex preamble.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Container: TinyTeX pinned to TeX Live 2025, replacing apt texlive
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Pin peaceiris/actions-gh-pages action to 84c30a8
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Deletes code with no live caller:
- workflow/scripts/run_2pcf_highres.py and the parked, unrunnable
  xi_highres block in workflow/rules/twopoint.smk that was its only
  reference; the mpi4py comment in pyproject.toml no longer names it.
- papers/bmodes/scripts/run_cov_sweep.sh, which calls
  workflow/scripts/run_cosmocov_chain.sh, absent from develop.

Fixes docs that point at paths or tools that do not exist:
- CLAUDE.md, CONTRIBUTING.md: single-test example uses test_cosmo_val.py.
- CLAUDE.md: cosmo_inference runs through Snakemake (inference_fiducial),
  not a pipeline.sh driver.
- papers/{bmodes,cosmo_val}/Snakefile: read cat_config through code/
  rather than the deprecated pure_eb symlink.
- ecut_spec.md, update_survey_stats.py: paths under papers/bmodes/.
- README badge and installation docs: Python 3.12 floor, matching
  requires-python.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeMhD6yCyrgBz5oz87bCtR
calculate_pure_eb_correlation returns a self-describing results dict
(theta, bin edges, xi+/- on both grids, n_eff) in place of the gg/gg_int
objects, so calculate_eb_statistics, the plot helpers and
save_pure_eb_results work from values alone. New seams: bins_from_edges,
log_bin_edges, pure_eb_from_xi, pure_eb_covariance_mc and
cosebis_scan_from_xi. The pure-E/B .npz stores n_eff (the realisation
count behind the covariance) instead of npatch.

Callers in cosmo_val, the bmodes calculate_pure_eb_ptes script, its
claims rule and PTE sweep driver are adapted. No numbers change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeMhD6yCyrgBz5oz87bCtR
cs_util's get_theo_xi returns {pair: (xi+, xi-)}, so concatenating its
return value raised TypeError and the semi-analytic pure-E/B covariance
(pure_eb_covariance_mc, and the paper's precompute_pure_eb_chunk.py) could
not run. One n(z) is one tracer pair: unpack that single entry, which also
fails loudly if a tomographic n(z) is passed. The new test stubs the
kernel and checks the draws centre on the binned theory mean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeMhD6yCyrgBz5oz87bCtR
Remove dead scripts and fix stale doc paths
cailmdaley and others added 13 commits October 5, 2026 04:24
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
…riance

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
…files

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
cailmdaley and others added 3 commits October 5, 2026 05:55
Seed each draw with its index and use k-means++ so the initialization samples distinct layouts. Rebuild the shared-layout catalogues because TreeCorr caches patch catalogues independently of reassigned centres.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
BE equals EB for the all/all auto-spectrum, so return a separate copy for the supported BE diagnostic. Tomographic cross-pair FITS readback retains its independent BE.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
One supplied xi covariance describes one pair, not every auto/cross pair. Fail before measuring tomography rather than silently assigning the same uncertainties to different samples.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
@LisaGoh

LisaGoh commented Oct 5, 2026

Copy link
Copy Markdown
Member

Thank you for this! The table in #375 makes things very clear now. I will fix #306 and #256 once everything upstream is merged.

@sachaguer sachaguer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I do not remember why I traded low ell noise bias equal to l_min for zeroes. I think there is no good reason. It does not have much impact I believe but we should go back to the constant noise bias

cailmdaley and others added 2 commits October 5, 2026 14:47
White shape noise is flat in ell; zeroing it below the first band
understated the Gaussian covariance there. Sacha agreed in the #394
review that there was no reason for the zeros.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0133mw5vATUB7QfqbhYac7Fz
The fixture passed one cov_path for every bin pair, which COSEBIs now
rejects for tomography. Four jackknife patches give each pair its own
covariance; the assertion on per-pair results is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0133mw5vATUB7QfqbhYac7Fz
@cailmdaley
cailmdaley merged commit 2ff1b50 into feature/sp_validation-extend-to-tomography Oct 5, 2026
2 checks passed
@cailmdaley
cailmdaley deleted the merge/develop-into-tomo branch October 5, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants