Skip to content

cosmo_val + Snakemake migration to the SACC writers (PRD #241 row 4)#251

Draft
cailmdaley wants to merge 19 commits into
feat/sacc-2-sacc-iofrom
feat/sacc-4-cosmo-val-sacc
Draft

cosmo_val + Snakemake migration to the SACC writers (PRD #241 row 4)#251
cailmdaley wants to merge 19 commits into
feat/sacc-2-sacc-iofrom
feat/sacc-4-cosmo-val-sacc

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #247. Row 4 of the PRD #241 §7 table: cosmo_val data products are born as SACC. Stacked on #245 (sacc_io, base branch feat/sacc-2-sacc-io).

What this does

  • Per-statistic SACC part writers (cosmo_val/sacc_writers.py + mixin *_to_sacc_part methods): coarse/fine ξ± (TreeCorr → xi_to_sacc), pseudo-Cℓ EE/BB/EB with the NaMaster BandpowerWindow (new plumbing — nothing saved windows before), COSEBIs at the fiducial scale cut, pure-E/B, ρ/τ (τ theory covariance from the CovTauTh write-side).
  • Snakemake wiring: every producing rule declares its SACC part; a new assemble_sacc rule loads the parts in canonical order, injects the externally-sourced covariance blocks (NaMaster FITS for pseudo-Cℓ; CosmoCov --xi-cov seam for ξ±, ready for the Converters: SACC→2pt-FITS (byte-compared) + SACC↔OneCovariance glue #249 converter round-trip), validates the expected-parts set, and writes the terminal {version}.sacc analysis file. rule xi_highres writes the terminal {version}_xi_fine.sacc (DiagonalCovariance from varxip/varxim) for COSEBIs/pure-E/B integration.
  • Tests: 18 new — part-writer paths, assemble integration (canonical order, block alignment, fail-loud on missing cov/parts, typo'd-input guard), pseudo-Cl end-to-end reconciled to read the SACC part (golden EE/BB/EB held bitwise), a second dry-run guard that actually resolves the cosmo_val DAG, and a CLI-seam smoke test. Fast suite: 163 passed.

Flags for reviewers

  • Adversarially reviewed before opening (5-lens fan-out, every finding independently verified). Confirmed findings, all fixed here: a pseudo-Cl cross-rule file collision (two rules shared an undeclared native basename via a write-then-rename; worst case stamped a wrong-blind n(z) into the terminal file — fixed born-at-declared-name, 60b74c6); the dry-run guard not covering the new rules (6a5df5b); silent part-drop on a misnamed rule input (149a50c); plus a save_fits TypeError in run_xi_sweep.py (3561b36).
  • Placeholder covariance is explicit-opt-in (e0519c8): with no real ξ-covariance wired, assembly fails loudly by default. cosmo_val.allow_placeholder_cov: true exists for dry-runs/tests only — the placeholder is flagged and never a science covariance.
  • blind=A in the analysis file: the pseudo-Cℓ part feeding {version}.sacc is the tagged legacy inference product (blind=A_powspace_nbins=32). The measured spectra are blind-independent (catalogue shear, no n(z) dependence) and the iNKA covariance is analytic, so the tag is provenance-naming, not wrong data; blind-label reconciliation is PR 6's scope (Smokescreen).
  • Part names carry rule wildcards (Snakemake one-wildcard-set constraint); only the two terminal files ({version}.sacc, {version}_xi_fine.sacc) have contract-stable names.
  • History note: dd474b3's "UNVERIFIED WIP" message is historical — it preserved an interrupted session's state; the verification it says is missing is what the subsequent commits did (wildcard fix, assemble rule, tests, review fixes).
  • inference.smk (dormant) still names pre-SACC inputs; migration to consuming {version}.sacc is the native-sacc PR (row 7).
  • Worth a glance on the next real cluster run: xi_highres now resolves its script path from the running checkout (9afaeaa) — replaces a hardcoded path through the deprecated pure_eb/ compat symlink.

— Claude (Fable) on behalf of Cail.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HPfbe2XVTGzbnPn4g7BdN6

cailmdaley and others added 19 commits July 10, 2026 03:19
Add sacc_writers.py: pure per-statistic *_to_sacc functions (xi coarse/fine,
pseudo-Cl, COSEBIs, pure-E/B, rho/tau) that turn computed arrays into
single-statistic SACC "parts", plus assemble_analysis_sacc, which rebuilds the
single {version}.sacc analysis file with a FullCovariance assembled block-
diagonally in canonical order (per the SACC layout contract — not
concatenate_data_sets, whose BlockDiagonalCovariance the contract rules out).

Add bandpower_window_from_workspace to pseudo_cl.py: the NEW plumbing that
threads NaMaster's get_bandpower_windows() into sacc_io.add_pseudo_cl (the
EE/BB/EB diagonal window is verified identical).

Tests: 12 fast tests, each writer round-tripped and checked against the
contract; a real small-nside NaMaster window round-trip; analysis assembly
proven to cover every point with aligned blocks and zero cross-blocks, incl.
the reload-from-disk DAG path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The theoretical tau covariance is not (6·nbin)² over [τ+; τ−]. Read from the
write-side (shear_psf_leakage.rho_tau_cov.CovTauTh.build_cov): it is a
(3·nbin)² k-major matrix over {τ0, τ2, τ5} with plus/minus folded into one
component per index — exactly the flavor today's CosmoSIS chain consumes via
covdat_to_fits. rho_tau_to_sacc now scatters that plus-only block into the
τ-plus rows/columns of the 6·nbin τ block (per-k [+;−] insertion order),
leaves τ-minus a vartau diagonal, and keeps plus↔minus cross zero. The kwarg
is renamed tau_cov -> tau_cov_th to name the flavor. tau_cov_th=None keeps a
compact DiagonalCovariance placeholder.

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

calculate_pseudo_cl_map/_catalog already call pseudo_cl_to_sacc_part (from the
prior commit); this defines it — building the EE/BB/EB part via
pseudo_cl_to_sacc(nz, meta, ell_eff, cl_all, wsp) and saving it as the native
pseudo_cl_{ver}.sacc product. Drops the legacy save_pseudo_cl FITS writer (no
external callers) and the unused _NMT_* constants.

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

Thin *_to_sacc_part methods that turn each already-computed diagnostic into its
single-statistic SACC part via the born-as-SACC writers:

- cosebis.py: cosebis_to_sacc_part picks the fiducial scale cut's {En,Bn,cov}
  (find_conservative_scale_cut_key, else the widest cut — mirroring plot_cosebis)
  and writes it; the multi-cut .npz sidecar (the PTE scan) is untouched.
- pure_eb.py: pure_eb_to_sacc_part writes the six PURE_KEYS blocks at
  gg.meanr with the results['cov'] block.
- psf_systematics.py: calculate_rho_tau_stats now writes a rho_tau_{base}.sacc
  part per version from the handler tables, passing cov_tau_{base}_th.npy as
  tau_cov_th when present (the tau-plus CovTauTh inference block), else a
  loud diagonal-placeholder fallback.

nz/metadata come from the shared core helpers sacc_nz / sacc_metadata.

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

- run_2pcf.py: after calculate_2pcf, write {ver}_xi_coarse.sacc via
  xi_to_sacc(grid='coarse', theta_nom=rnom, npairs, weight) — no covariance
  (added at assembly). Drops the save_fits threading (mixin no longer writes
  DES-style xi FITS).
- run_2pcf_highres.py: on rank 0, write the terminal {version}_xi_fine.sacc via
  xi_to_sacc(grid='fine', variances=[varxip; varxim]) as a DiagonalCovariance.
  Deletes write_xi_fits (DES-FITS); keeps the .txt. sacc_io import works on the
  bare-host MPI path (no healpy), n(z) read from shear.redshift_path.
- generate_pseudo_cl.py: native product is now pseudo_cl_{ver}.sacc (the mixin
  writes it); report reads it back via sacc_io.get_pseudo_cl.
- assemble_sacc.py (new, dual-mode): loads the per-statistic parts in canonical
  order and calls assemble_analysis_sacc. Injects the xi-coarse block from the
  CosmoCov .txt (already [xi+;xi-]-ordered) and the pseudo-Cl block from the
  NaMaster iNKA cov FITS as block-diagonal [EE_EE;BB_BB;EB_EB] (cross-spectrum
  blocks dropped, TODO flagged). --allow-placeholder attaches a documented
  diagonal for cov-less parts so DAG dry-runs / tests produce a valid
  FullCovariance; production requires the real cov inputs (fail-fast otherwise).

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

The migrated pseudo-Cl SACC part calls get_redshift(), which reads
cc[version]['shear']['redshift_path']; the synthetic fixture wrote the
dndz file but never pointed that key at it (KeyError at core.py:422).
Verified: the KeyError layer is resolved; the end-to-end test now fails
one layer deeper (test asserts the legacy ELL/EE/BB/EB FITS columns
while the migrated writer emits the SACC part - the open wiring seam).

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

Working-tree state of the wiring subagent when it died on the session
limit (its final turn also went unreviewed by the safety classifier).
Wires cosmo_val.smk/twopoint.smk rules and cv_* scripts to the SACC
parts + assemble_sacc rule. NOT dry-run-tested (no snakemake in the
shared venv), NOT reviewed; test_calculate_pseudo_cl_catalog_end_to_end
still fails at the writer/consumer seam (test reads legacy ELL columns).
Next session: verify this diff against the DAG design in the PR-4 plan,
reconcile the pseudo-Cl end-to-end test with the born-as-SACC output,
add writer-path tests, run the suite, then de-WIP.

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

calculate_pseudo_cl_catalog is born-as-SACC (pseudo_cl_to_sacc_part writes
EE/BB/EB + a shared bandpower window), so the end-to-end test can no longer
read legacy ELL/EE/EB/BB FITS columns. Round-trip through sacc_io.get_pseudo_cl
instead and assert the bandpower window rides the part per the layout contract.
The pinned golden spectra are unchanged (identical computation, SACC
serialization); they held bitwise through the round-trip.

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

The born-as-SACC parts existed but nothing assembled them. Wire the DAG:
per-statistic parts (xi_coarse, pseudo_cl, cosebis, pure_eb, rho_tau) →
assemble_sacc rule → terminal {version}.sacc.

- rule xi: give the coarse ξ± .sacc output the same reporting-binning wildcards
  as the .txt (Snakemake requires one wildcard set per rule; the bare
  {version}_xi_coarse.sacc name left the binning wildcards unbound and broke DAG
  resolution). run_2pcf.py writes the part to the declared output path.
- rule rho_tau_stats: declare the rho_tau .sacc part (already written by
  calculate_rho_tau_stats) as a real output; run_rho_tau.py verifies it.
- new rule assemble_sacc + assemble_sacc_all: load the five parts in canonical
  order via assemble_sacc.py, injecting a documented diagonal placeholder for
  the cov-less ξ/pseudo-Cℓ blocks (real CosmoCov/NaMaster covariance plugs into
  the same --xi-cov/--pseudo-cl-cov seam later — PR-3 converter territory).
- cosmo_val_all: request the terminal {version}.sacc per version.
- cv_pseudo_cl.py docstring: born-as-SACC, no longer FITS.

Dry-runs clean for assemble_sacc_all, cosmo_val_all, bmode_summary.json, and
the fine-grid xi_fine target.

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

The pure assembler is covered in test_sacc_writers; this pins the DAG-facing
script that loads per-statistic .sacc part *files* and rebuilds {version}.sacc:
canonical block order across all five statistics, the diagonal placeholder for
the cov-less ξ± part, real CosmoCov ξ covariance .txt injection, the fail-loud
path when a required covariance is absent, and the pseudo_cl config toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPfbe2XVTGzbnPn4g7BdN6
The MPI xi_highres rule shells out to run_2pcf_highres.py by absolute path
through the deprecated pure_eb/ compat symlink (it can't use Snakemake's
script: directive — mpiexec wraps the python call). Anchor the path on
common.py's own location (WORKFLOW_SCRIPTS), which resolves to the generic
workflow/scripts of whatever checkout parses the DAG, regardless of which paper
composes it — workflow.basedir is unreliable here (under module composition it
reflects the composing paper, verified: it resolved to papers/cosmo_val/scripts).

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

The analysis {version}.sacc must be byte-comparable against today's
cosmosis_fitting.py assembly (the SACC layout contract's stated requirement for
PR-3's converter). cosmosis consumes the tagged, blinded pseudo-Cl product
(blind=A, powspace, nbins=32 per config harmonic.fiducial), not the untagged
cv_pseudo_cl diagnostic — so assemble_sacc now inputs the tagged part and
injects its real NaMaster covariance from the matching pseudo_cl_cov FITS
(COVAR_EE_EE/BB_BB/EB_EB → block-diagonal). The untagged cv_pseudo_cl part stays
the cv_summarize_bmodes B-mode diagnostic, unchanged.

The ξ± coarse block keeps a documented diagonal placeholder: its real CosmoCov
covariance is blind/gaussian/mask-keyed in the inference tree, and wiring it
couples cosmo_val to the whole inference covariance DAG — that sourcing is PR-3
converter territory, ready at the --xi-cov seam.

test_assemble_sacc: add the pseudo-Cl COVAR-FITS injection path (the live
default) alongside the existing ξ .txt and placeholder paths.

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

test_bmodes_workflow_dry_runs invoked a bare python3.12 (resolves off PATH to
intel-python without snakemake) and inherited the login shell's slurm
SNAKEMAKE_PROFILE (forces an executor plugin the test env need not have). Use
sys.executable (the interpreter pytest/snakemake live in) and drop the profile
for the dry-run. The guard now passes and exercises the migrated rule graph
(rule xi's coarse-sacc output, the WORKFLOW_SCRIPTS path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPfbe2XVTGzbnPn4g7BdN6
…red to PR 7

The SACC migration removed the xi_plus/xi_minus FITS and switched pseudo_cl to
.sacc, so inference_prep's DAG no longer resolves. It is not reachable from the
cosmo_val suite (cosmo_val_all never requests it), so the cosmo_val DAG stays
clean. Per the PR-4 scope this subsystem is left dormant — a comment block at
the rule head names the stale inputs and states PR 7 (native-SACC inference
consumption) rewires it to consume the assembled {version}.sacc directly,
retiring cosmosis_fitting.py's per-product FITS assembly. Comment-only; no
behavior change.

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

Both pseudo-Cl rules call calculate_pseudo_cl, which hardcoded the untagged
native pseudo_cl_{ver}.sacc; generate_pseudo_cl.py then renamed it to the tagged
name. In one cosmo_val_all DAG both fire: if the untagged cv_pseudo_cl (blind=
None) ran first, the tagged rule's (blind=A) skip-if-exists silently adopted the
blind=None file and the rename deleted cv_pseudo_cl's declared output — rebuild
loops, and wrong-blind n(z) stamped into the terminal {version}.sacc's pseudo-Cl
part.

Thread out_path through calculate_pseudo_cl so each part is born directly at its
final declared name (tagged for the producer, untagged for the diagnostic). No
shared native basename, no rename; skip-if-exists keys on the declared path, so
the two rules' paths are provably disjoint and the blind that computes each file
matches its name. Multi-version + out_path now raises. generate_pseudo_cl.py and
its CLI updated to pass the declared/native out_path directly.

Regression tests: out_path is honoured (native name untouched) and the
multi-version guard fires.

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

{version}.sacc is the terminal science file; shipping it with a var=1.0 diagonal
as the LEADING (ξ±) covariance block — ~20 orders off the real variance — is a
silent catastrophic χ²/PTE for any consumer. Drop the unconditional
placeholder_var=1.0. Default: no real ξ-cov wired → assemble_sacc.py's existing
ValueError fires. The placeholder is now gated behind an explicit opt-in
(cosmo_val.allow_placeholder_cov: true), for dry-run / test configs only. The
pseudo-Cℓ block stays real (pseudo_cl_cov input); the integration tests pass
placeholder_var explicitly and are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPfbe2XVTGzbnPn4g7BdN6
test_bmodes_workflow_dry_runs uses papers/bmodes, whose config has no cosmo_val
block, so cosmo_val.smk (the born-as-SACC + assemble rules) is never included —
the guard couldn't catch a break in them. Add test_cosmo_val_workflow_assemble_
dry_runs targeting assemble_sacc_all in papers/cosmo_val (the only paper that
includes cosmo_val.smk): asserts the DAG resolves and each assemble_sacc job
pulls the tagged, blinded pseudo-Cl part + its NaMaster cov (not the untagged
diagnostic) plus all five per-statistic parts. Shared _dry_run helper factored
out of the bmodes guard.

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

run_xi_sweep.py called run_2pcf(..., save_fits=True) — a kwarg the SACC
migration removed, so every invocation TypeErrors. The sweep consumes only the
.txt dump; drop the kwarg. run_2pcf is born-as-SACC, so give its coarse part a
grid-qualified sacc_out (the default {ver}_xi_coarse.sacc carries no binning, so
the reporting + integration grids would collide per version). Stale "+ ξ+/ξ-
FITS" docstring dropped.

test_cli_seams: bind the sweep's exact run_2pcf call against the live signature
(and assert save_fits no longer binds) so this CLI seam can't silently rot —
the compute is cluster-only and never exercised by the fast suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPfbe2XVTGzbnPn4g7BdN6
_from_snakemake built part_paths with a hasattr filter, so a typo'd input
keyword (cosebi for cosebis) silently omitted that statistic from the terminal
{version}.sacc — the exact silent-truncation class this series has been bitten
by. assemble_sacc now takes an `expected` list (the statistics the caller wired,
from its config toggles) and raises if any is missing from part_paths or names a
non-CANONICAL statistic. The rule derives expected from cv_assemble_inputs so it
tracks the include_pseudo_cl toggle. CLI path is already typo-safe (argparse
rejects unknown flags), so it passes expected=None.

Tests: typo'd input key raises; typo in the expected list itself raises.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPfbe2XVTGzbnPn4g7BdN6
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.

1 participant