Skip to content

PR 7: adopt CosmoSIS native SACC likelihood (sacc_like), validated against the converter path#255

Draft
cailmdaley wants to merge 43 commits into
feat/sacc-4-cosmo-val-saccfrom
feat/sacc-7-sacc-like
Draft

PR 7: adopt CosmoSIS native SACC likelihood (sacc_like), validated against the converter path#255
cailmdaley wants to merge 43 commits into
feat/sacc-4-cosmo-val-saccfrom
feat/sacc-7-sacc-like

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #254. Row 7 of PRD #241 — the final PR of the SACC/Smokescreen series: the inference pipeline consumes the assembled {version}.sacc natively through CosmoSIS's likelihood/sacc/sacc_like.py, with the PR-3 converter path (SACC → 2pt-FITS → 2pt_like) retained as the cross-validating twin. Each path validates the other, per PRD §2.

The "prototype-grade" verdict, grounded

The PRD flagged sacc_like's ξ± path as prototype-grade. A source read of CSL @ 4fd2f1c found the concrete mechanisms:

  1. No angular-unit conversion. sacc_likelihoods/twopoint.py builds the theory spline on block[section, "theta"] (radians, CosmoSIS convention) and evaluates it at raw SACC theta tags (arcmin, SACC/firecrown convention). 2pt_like explicitly converts everything to radians (2pt_like.py L179–183); sacc_like never does — the spline is evaluated ~3437× outside its grid, SpectrumInterp returns 0 there, and χ² silently collapses to dᵀC⁻¹d. Its only production use (HSC Y3) was the ℓ-space Cℓ path, which is unit-free — hence never caught.
  2. Theory↔data ordering assumed, never enforced. The data vector is get_mean() (insertion order); theory is a type-major loop. A comment claims to_canonical_order was called on load — it never is. Single-pair files align by construction; tomographic pair-major files would silently misalign.
  3. (Latent, not touched here: sacc_like.py L106 references undefined t1, t2 in the keep_tracers removal path.)

Adoption = subclass shim, not fork

src/sp_validation/sacc_like_unions.py subclasses upstream SaccClLikelihood (factory-loaded from csl_dir), containing exactly two fixes: theory extraction runs against a radian-θ copy of the loaded SACC (self.sacc_data stays arcmin, so scale cuts keep arcmin ini ergonomics and upstream's save_theory/save_realization never write radian tags), and a loud ordering guard that reconstructs the upstream theory-loop order and requires it to match insertion order. Everything else — scale-cut grammar, Sellentin/Hartlap, windows for Cℓ — rides upstream unmodified. n(z) loads natively via CSL's number_density/load_nz_sacc. like_name = 2pt_like keeps every block key identical across engines, so chain post-processing is engine-agnostic.

Validation (all observed, in-process, no sampler/CAMB needed)

Both likelihood modules run standalone against identical synthetic theory DataBlocks:

  • Machine-zero equality shim-on-SACC vs 2pt_like-on-converter-FITS: synthetic (realistic covariance) χ² = 1.18829133107 both, Δχ² = 0, max|Δtheory| = 0; real data (SP_v1.4.6_leak_corr, ξ-only, N=40) χ² = 420310.753739 both, Δχ² = 0.
  • Scale cuts equivalent: same arcmin ranges → same post-cut N, identical χ².
  • Tripwire: raw upstream sacc_like on the same arcmin SACC is ~631× off — this test failing one day means upstream fixed units and the shim should be retired.
  • Ordering guard raises loudly on a pair-major tomographic file; θ-conversion scoped to real-category types only (COSEBIs n, spectrum ell untouched); save_theory output verified to carry arcmin tags and theory values, with re-execution χ²-stable.

Workflow revival

inference_prep honors PR 4's dormant-note contract: input = the assembled {version}.sacc (+ the two ini templates, DAG-tracked), outputs = converter 2pt-FITS + generated 2pt_like ini + generated sacc_like ini (workflow/scripts/generate_inference_config.py, dual-mode like assemble_sacc.py). cosmosis_fitting.py is retired from the real-data path; the glass-mock rules keep it, and the PSF/xi_sys pipeline variant stays on 2pt_like (both flagged in comments — follow-up work, not silent scope creep). snakemake -n inference_fiducial resolves the full chain: parts → assemble_saccinference_prepinference_fiducial.

Environment

New inert [cosmosis] extra (cosmosis>=3.25; pip-installs cleanly on py3.12). Tests gate on importorskip("cosmosis") + a CSL_DIR env var pointing at a CSL checkout — they skip in CI (image carries neither) and run on candide. CI runs the rest of the suite as usual.

Adversarial review

Fresh-context multi-lens review (5 lenses, 3 refuters per finding) before this draft opened; two MEDIUM findings confirmed and fixed: (1) save_theory/save_realization wrote radian θ tags into saved SACCs (in-place mutation leak — now a swap-in copy, regression-tested); (2) the ini templates were rule params rather than inputs, so template edits didn't propagate to generated configs (now DAG edges, asserted by the dry-run guard).

Stack note

Base = feat/sacc-4-cosmo-val-sacc (PR #251), with feat/sacc-3-2pt-converter (PR #249) merged in — this PR needs both PR 4's workflow surface and PR 3's converter, so its diff shows #249's files until the stack collapses onto develop. Net-new in THIS PR: sacc_like_unions.py, test_sacc_like.py, test_generate_inference_config.py, cosmosis_pipeline_A_ia_sacc.ini, generate_inference_config.py; changed: inference.smk, config.yaml, pyproject.toml, test_bmodes_workflow_dry_run.py.

Recommended follow-ups (team call): file the unit gap + the L106 keep_tracers bug upstream at joezuntz/cosmosis-standard-library; migrate the PSF/xi_sys variant and glass-mock prep; tomographic support in both the converter and the shim's guard-relaxation.

— Claude (Fable) on behalf of Cail.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EnV8NPWhkxS2SxGVgGSJyt

cailmdaley and others added 30 commits July 10, 2026 01:49
PRD #241 row 1. sacc>=2.4 joins core dependencies (sp_validation.sacc_io
will be core library code). firecrown v1.15.1 + smokescreen 1.5.6 + an
exact pyccl pin form the new [blinding] extra: the blind must be exactly
recomputable from the seed at unblinding time, so the theory stack is
pinned as a set.

firecrown is not on PyPI and hard-depends on numcosmo-py (conda-forge
only) plus the cosmosis/cobaya sampler connectors we never import — the
new uv-overrides.txt drops those three from resolution; the Dockerfile
and README carry the --overrides invocation. Plain installs without the
blinding extra are unaffected.

requires-python moves 3.11 → 3.12: Smokescreen and firecrown both set a
3.12 floor, and the container base (shapepipe:develop) already runs
python:3.12-slim-bookworm — this aligns pyproject with the actual
runtime. ruff target-version follows.

Validated: uv resolution of '.[test,glass,blinding]' with overrides
(235 packages, py3.12) and the full firecrown-core + smokescreen import
chain in a fresh venv on candide. The CI image build on this branch is
the container-side proof.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bGhVMAuhTy1gF6DdUc1dQ
Follow-up hardening after empirical validation of the blinding stack on
candide. Two facts surfaced that the resolve-only check couldn't see:

- firecrown 1.15.1 subclasses npt.NDArray (DataVector); numpy 2.5 made
  that a non-subclassable typing alias, so firecrown breaks at import on
  the numpy>=2.0 resolution (2.5.1). The [blinding] extra now carries
  numpy>=2.2,<2.5 (2.4.3 verified against the full compiled stack + the
  fast suite), and the Dockerfile requests the bound explicitly to dodge
  uv #8410 non-movement of an already-installed numpy.

- pip-installed firecrown hits NumCosmo (conda-forge-only) at import
  time through two paths unrelated to cosmic shear: eager re-export of
  LSST predefined n(z) bins in generators/__init__ (defeating upstream's
  own lazy __getattr__), and the cluster likelihoods -> lsstdesc-crow ->
  Ncm.IntegralND C-subclass at module load. scripts/patch_firecrown.py
  makes the bin re-export lazy, the cluster imports optional, and ships
  a loud numcosmo_py shim (raises on any real use). Exact-string surgery
  against pinned v1.15.1, idempotent, fails loudly on a version bump,
  ends with an import check. Verified: fresh application on a pristine
  install, idempotent re-run, and the full toy Smokescreen blind
  end-to-end after patching.

CI now also smoke-tests the blinding imports in the built image - the
fast suite never imports firecrown, so a broken blinding stack would
otherwise ship green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bGhVMAuhTy1gF6DdUc1dQ
Add sp_validation.sacc_io: the writer/reader layer for the two-file SACC
layout that becomes the package's standard data-product format.

  {version}.sacc       analysis vector — NZ tracers, coarse xi+/-, pseudo-Cl
                       (EE/BB/EB) with a shared BandpowerWindow, COSEBIs,
                       pure E/B, rho/tau PSF diagnostics; one FullCovariance
                       assembled block-diagonally (zero cross-blocks).
  {version}_xi_fine    COSEBIs/pure-EB integration input — same NZ tracers,
                       fine-grid xi+/-, DiagonalCovariance from TreeCorr
                       varxip/varxim.

Covariance order is point-insertion order (SACC preserves it bitwise
through FITS). Writers insert in the canonical order — xi+ then xi-, Cl
(ee, bb, eb), COSEBIs (all En then all Bn), pure E/B in _EB_KEYS order
(xip_E, xim_E, xip_B, xim_B, xip_amb, xim_amb, matching
b_modes.calculate_eb_statistics), rho, then tau — but readers never assume
global order: every getter resolves indices through s.indices(dtype,
tracers, **tags). assemble_covariance validates that blocks are contiguous,
ascending and tile the data vector exactly, failing loud otherwise.

Custom data types (pure E/B, rho, tau) all parse under
sacc.parse_data_type_name. Tag filters are plain kwargs; the tags={...}
form silently selects nothing and is never used.

Test suite (test_sacc_io.py, all synthetic and fast): per-writer
round-trips (arrays/tags/windows/NZ bitwise), covariance block alignment
and zero cross-blocks, assemble_covariance failure modes, DiagonalCovariance
round-trip, extract() sub-covariance alignment, a tomographic multi-pair
case, reader/writer mirroring on a mixed file, and the end-to-end two-file
layout. 20 passed.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Fresh-eyes review caught a correctness bug: readers re-sorted selections by
theta/ell/n, but covariance blocks and bandpower windows stay in insertion
order. On a non-ascending grid the reader output silently desynchronised
from its covariance, and get_pseudo_cl returned sorted cl arrays against
unsorted window columns — internally inconsistent within one return tuple.

Fix by construction, not by sort:
  - Writers validate their grids. add_xi/add_pure_eb/add_rho/add_tau require
    strictly ascending theta; add_pseudo_cl requires strictly ascending
    ell_eff (add_cosebis is inherently safe — it enumerates the mode index).
    Out-of-order grids raise a loud ValueError naming the argument.
  - Readers drop the sort entirely and return in s.indices (insertion) order,
    so every getter is covariance- and window-aligned for ANY file, and
    ascending for canonical files. The _sorted_* helpers are replaced by
    plain insertion-order accessors (_mean/_tag).

Also:
  - _pair normalises (i, j) -> sorted, so get_xi(s, (1, 0)) addresses the same
    symmetric shear-shear pair as (0, 1) instead of a silent empty read.
  - Module docstring documents the tomographic ξ covariance ordering:
    insertion is pair-major ([pair0 xip; pair0 xim; pair1 xip; …]), supplied
    to assemble_covariance as one contiguous block matching add_xi call order;
    type-major converters (DES 2pt-FITS) permute explicitly via s.indices.
  - extract() docstring states tracers takes SACC names, not integer bins.

New tests (26 total, was 20): writers reject non-ascending theta/ell;
(1, 0) == (0, 1) round-trip; a 3-pair tomographic ξ covariance assembled as
one contiguous pair-major block with per-pair sub-blocks recovered via
extract(); get_pseudo_cl window column j <-> ell_eff[j] via window_ind tags.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
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>
…sis_fitting

Add sp_validation.twopoint_convert.sacc_to_twopoint_fits, converting an analysis
SACC into the CosmoSIS 2pt-FITS that 2pt_like (and Sacha's rho/tau
2pt_like_xi_sys fork) reads. Output reproduces today's cosmo_inference
cosmosis_fitting.py assembly HDU-for-HDU and byte-for-byte: NZDATA, XI_PLUS/MINUS,
CELL_EE, the blocked COVMAT (STRT_i offsets) and separate COVMAT_CELL, and the
TAU_{0,2}_PLUS + verbatim RHO_STATS tables.

The data vector and covariance are permuted from SACC's pair-major order to the
2pt-FITS type-major layout via s.indices. The tau covariance is laid in as one
contiguous [tau_0+; tau_2+] block, preserving the tau_0<->tau_2 cross-correlation
that covdat_to_fits carries. Rho/tau HDUs need the rho/tau sidecar FITS: the
RHO_STATS varrho_* variance columns are not stored in the analysis SACC, so the
converter copies them verbatim (never fabricates them); xi, Cl, n(z) and the
covariance are fully reconstructed from SACC alone.

Tests:
- test_twopoint_convert.py: byte-equal vs current cosmosis_fitting.py builders on
  deterministic synthetic inputs for all three product shapes (plain xi; xi+Cl;
  xi+rho/tau), plus the tau_0<->tau_2 cross-correlation and perturbation teeth.
- test_twopoint_convert_realdata.py (skipif on candide paths): build a SACC from a
  real product's own contents, convert, byte-compare vs the current writer.
  Observed byte-equal for SP_v1.4.6_leak_corr and a glass_mock sibling; the stale
  on-disk files differ only by an extra CELL_BB HDU (older script version) and
  ~1e-17 float noise on bin edges, documented and guarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add sp_validation.one_covariance_io, the two small pieces of glue between the
SACC layout and OneCovariance (which knows nothing of SACC and only exchanges
files):

- write_nz / nz_table / nz_config_stanza: the source_i NZ tracers of an analysis
  SACC -> OneCovariance's combined whitespace n(z) file (column 0 = shared z
  grid, one n(z) column per tomographic bin, no bin edges) plus the [redshift]
  config stanza pointing at it. Config keys verified against upstream
  OneCovariance config.ini (zlens_directory/zlens_file/value_loc_in_lensbin);
  the UNIONS template (cosmo_val/pseudo_cl.py._modify_onecov_config) uses the
  z_directory alias, exposed via dir_key. Fails fast on missing bins or
  disagreeing z grids.
- covariance_blocks: OneCovariance's flat covariance_list_*.dat output ->
  dense square block(s) paired with SACC selectors for
  sacc_io.assemble_covariance, reusing statistics.cov_from_one_covariance for the
  per-block reshape (col 10 gaussian / col 9 gauss+non-gaussian). Single-block
  and multi-block (tomography-ready) forms.

Tests (test_one_covariance_io.py, 9 passing): reshape to a hand-built matrix +
gaussian/gauss+ng column selection + perturbation teeth; blocks feed
assemble_covariance and round-trip s.covariance.dense; n(z) write/read round-trip,
UNIONS dir_key, header/no-header, and fail-fast on mismatched grids / missing bin
/ bad value_loc.

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>
…ather

Adversarial-review hardening (one HIGH, one MEDIUM, one coverage gap):

- HIGH: n_bins>1 silently truncated — n_bins drove only the NZDATA column
  count while data/covariance were read from bin (0, 0), so a 2-bin SACC
  emitted a plausible-looking FITS carrying 1/3 of the data. The converter
  now fails fast unless n_bins == 1 and the ξ tracer pairs are exactly
  {(source_0, source_0)}; tomographic emission lands with the tomographic
  round.
- MEDIUM: a ξ-less SACC wrote an empty XI_PLUS and a (0, 0) COVMAT
  silently; now a loud ValueError.
- Coverage: every prior covariance test exercised the identity permutation
  (single-pair SACC order is already type-major). A (row, col)-encoded
  covariance test now pins the exact np.ix_ gather of COVMAT (block-diag
  ξ + joint non-adjacent [τ0+; τ2+]) and COVMAT_CELL — any transposition,
  offset, or swapped block fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Ko89uZF84ez6HEDcTWbJe
…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
Adopt CosmoSIS's native SACC likelihood for the ξ± inference path through a thin
sp_validation-owned subclass that fixes two upstream defects:

  1. arcmin→rad theta-tag conversion — upstream sacc_like builds its theory
     spline on block[section,"theta"] (radians) but evaluates it at raw SACC
     theta tags (arcmin), silently returning theory≈0 and collapsing χ². 2pt_like
     converts to radians for exactly this reason; sacc_like never does.
  2. an ordering guard — the data vector (get_mean, insertion order) and the
     theory loop (get_data_types × tracer_combinations × points) agree only for
     type-major files; a pair-major tomographic file would silently misalign.
     The guard reconstructs the theory-loop order and requires it == arange.

Factory pattern (setup imports the upstream class from csl_dir and subclasses at
call time) so importing the module never needs cosmosis; not imported by
__init__. build_data calls super() first (scale cuts run in arcmin, matching
2pt_like's angle_range grammar) then converts + guards.

Tests (cosmosis + CSL_DIR gated, skip cleanly otherwise) prove in-process
equality against 2pt_like on the PR-3 converter FITS: machine-zero equality on
synthetic (χ²=1.18829133107, Δχ²=0) and real data (χ²=420310.753739, Δχ²=0, N=40),
the unit-gap tripwire (raw sacc_like 631× off), scale-cut equivalence, identical
perturbation response, the ordering guard, and real-type-scoped theta conversion.

pyproject: cosmosis>=3.25 extra (inert in CI, which installs [test,glass,blinding]).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnV8NPWhkxS2SxGVgGSJyt
Add the CosmoSIS pipeline ini template for the native SACC path
(cosmosis_pipeline_A_ia_sacc.ini): copies the A_ia template, swapping
load_nz_fits→load_nz_sacc (native n(z) from the SACC's source_i NZ tracers) and
2pt_like→sacc_like (the shim, via SP_VALIDATION_MODULES, csl_dir=COSMOSIS_DIR,
like_name=2pt_like for block-key parity). Same numeric scale cuts as A_ia, in the
sacc_like angle_range grammar (full data-type names + source_0 pairs).

generate_inference_config.py fills a template's [DEFAULT] section with concrete
paths (SCRATCH, FITS_FILE|SACC_FILE, COSMOSIS_DIR, SP_VALIDATION_MODULES resolved
from sp_validation.__file__). Dual-mode (snakemake object OR argparse CLI) like
assemble_sacc.py; plain text processing (the pipeline.sh sed idiom) that REPLACES
an existing DEFAULT key in place and prepends only new ones — no configparser
round-trip (which would strip comments + %(...)s interpolation) and no duplicate
DEFAULT key (which CosmoSIS's strict parser rejects).

Tests (no cosmosis needed): both templates fill + interpolate cleanly, the
referenced module file exists, no %(...)s placeholder survives, missing-DEFAULT
raises.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnV8NPWhkxS2SxGVgGSJyt
Revive the real-data inference_prep from its dormant, pre-SACC state. It now
consumes the assembled analysis {version}.sacc (cosmo_val.smk's assemble_sacc,
bound lazily through cv_analysis_sacc) and emits the A_ia (IA-only, ξ±) file-prep
products:
  (a) the converter 2pt-FITS (sacc_to_twopoint_fits, pure ξ — no rho/tau
      sidecars, A_ia scope) + a generated 2pt_like ini (the validating/legacy
      path), and
  (b) a generated sacc_like ini pointing at the SACC (the native path, validated
      bit-for-bit against (a)).
The cosmosis_fitting.py real-data assembly is retired from this rule; the
glass-mock rules keep it (their SACC migration is out of scope, noted inline).
inference_fiducial extends to all three prep outputs.

CSL_DIR is read lazily (inference.smk is parsed by every paper workflow, but only
cosmo_val carries inference.csl_dir) — a missing key still fails loudly, just at
DAG time rather than at parse time for an unrelated (bmodes) workflow.

config.yaml: add inference.csl_dir. The workflow dry-run guard gains a test
covering the revived inference_prep DAG (SACC in, converter FITS + both engine
inis out, no cosmosis_fitting.py real-data assembly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnV8NPWhkxS2SxGVgGSJyt
cailmdaley and others added 2 commits July 10, 2026 17:07
…rcmin tags

Review finding (MEDIUM): the shim converted theta tags arcmin→rad in place on
self.sacc_data. Upstream do_likelihood's save_theory / save_realization paths
copy self.sacc_data and overwrite only point values, so they were writing radian
theta tags into the saved SACC — any arcmin-assuming consumer (sacc_io.get_xi, or
re-ingesting the file as data_file, which double-converts to ~8.5e-8) is then
silently off by 3437×.

Keep self.sacc_data in arcmin always. build_data now builds a separate
self._sacc_data_rad copy with the real-category theta tags converted, and
extract_theory_points swaps it in around super().extract_theory_points(block) in
a try/finally — so the theory spline sees radians while everything after
(save_theory / save_realization) sees the untouched arcmin original. The ordering
guard runs on the arcmin object (ordering is unit-independent). Theory equality is
unchanged (the swap produces identical results): synthetic χ²=1.18829133107,
real-data χ²=420310.753739, both Δχ²=0 vs 2pt_like.

New test: run with save_theory set, reload, assert the saved theta tags equal the
input file's arcmin tags exactly (and are NOT the radian conversion) and the saved
values equal the theory vector; assert a second execute() yields the identical χ²
(guards against a double-conversion regression). The theta-scoping test now checks
the rad copy carries radians AND the original stays arcmin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnV8NPWhkxS2SxGVgGSJyt
Review finding (MEDIUM): the two pipeline ini templates were referenced as params,
so there was no DAG edge — editing a template never regenerated the per-version
config files. Bind both as rule inputs (referenced via input.* in the run block).

The templates are source files, so anchor them on the running checkout
(INFERENCE_TEMPLATE_DIR, off the workflow dir's parent) rather than the
env-overridable COSMO_INFERENCE output root; the generated per-version configs
still land in COSMO_INFERENCE (INFERENCE_CONFIG_OUT). In a normal run the two
roots coincide; the split is what lets a template edit in this checkout drive the
DAG. The dry-run guard now asserts both templates appear as inference_prep inputs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnV8NPWhkxS2SxGVgGSJyt
cailmdaley and others added 2 commits July 10, 2026 17:24
…cosmosis

CI (test_imports.py::test_package_module_imports[sp_validation.sacc_like_unions])
failed: the CI image has the science stack but no cosmosis, and the shim imported
`from cosmosis.datablock import SectionOptions, option_section` at module level.
cosmosis is an optional dependency, so a bare `import sp_validation.sacc_like_unions`
must succeed without it (test_imports bare-imports every top-level module).

Move the cosmosis import into setup() (call time). Nothing at top level touches
cosmosis now — only os/sys/numpy (all in the image). The factory's subclass of
the upstream SaccClLikelihood already deferred (setup imports it from csl_dir),
so setup() is the single cosmosis entry point.

Verified: in the container (numpy present, cosmosis genuinely absent — the CI
condition), `import sp_validation.sacc_like_unions` succeeds with setup/execute/
cleanup/ARCMIN_TO_RAD all present, and sacc_like_unions is absent from the
import-sweep failures. Full function intact: test_sacc_like.py 8 passed under the
venv (cosmosis present).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnV8NPWhkxS2SxGVgGSJyt
Adds twopoint_convert (SACC → 2pt-FITS, byte-compatible with
cosmosis_fitting) and one_covariance_io (SACC ↔ OneCovariance glue),
with their test suites. Rebuilt on feat/sacc-2-sacc-io so the diff is
converter-only; adapted to that branch's canonical sacc_io — ξ reads use
grid='reporting' (was 'coarse'). Optional-block probing (pseudo-Cℓ, τ)
stays on raw s.indices behind get_data_types/use_rho_tau guards, so the
new fail-loud sacc_io readers don't trip on absent blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzUt7VbtXwr2SCHUdiQTyt
@cailmdaley
cailmdaley force-pushed the feat/sacc-4-cosmo-val-sacc branch from 149a50c to 72e680f Compare July 16, 2026 09:50
cailmdaley and others added 9 commits July 18, 2026 11:46
Consistency follows tagging semantics: the grid tag declares which
binning a set of points lives on, so all same-length theta arrays under
one tag value must be bitwise identical (sacc never validates angles,
and grids diverging at floating-point level choke CosmoSIS downstream).
Different lengths within a tag group pass (scale-cut subsets); grids
under different tag values are unconstrained (reporting vs integration
differ by design).

The rho/tau/pure-EB writers now tag their points grid="reporting" by
default (overridable via grid=), so they join xi's consistency group
and no untagged group appears in our own files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN9VazXKHUHQg16kiG7Ufk
…seudo-Cl grid="reporting"

The theta consistency guard generalizes to both angular domains: theta
and ell points are grouped separately by grid tag value, and within a
tag value all same-length grids must be bitwise identical. Two ell
series sharing a grid must also carry equal bandpower windows (window
ells and weight matrix); series without windows skip that check.

add_pseudo_cl now stamps grid="reporting" on every point by default
(overridable), joining the merge guard's consistency groups; since
sacc's add_ell_cl accepts no extra tags, its per-point insertion
(ell + shared window + window_ind) is inlined.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN9VazXKHUHQg16kiG7Ufk
One module whose job is converting between the SACC product and
external analysis-tool file formats (CosmoSIS 2pt-FITS, OneCovariance).
Public API unchanged; sacc_io untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01766vawzi2XqrgoyHmeHEY9
Cail's ruling: a single sacc_io.py carries the format contract and the
external-tool converters (CosmoSIS 2pt-FITS, OneCovariance). Public API
unchanged; the sacc_io import name every downstream branch uses is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01766vawzi2XqrgoyHmeHEY9
…feat/sacc-7-sacc-like

# Conflicts:
#	Dockerfile
#	pyproject.toml
#	src/sp_validation/sacc_io.py
#	src/sp_validation/tests/test_sacc_io.py
#	workflow/rules/inference.smk

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01766vawzi2XqrgoyHmeHEY9
The merged layer order ran the [blinding] editable install before COPY .,
so uv-overrides.txt (and the source tree) did not exist in the build context.
The blinding install is itself 'uv pip install -e .[blinding]', so the old
final --no-deps editable layer is redundant and is dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01766vawzi2XqrgoyHmeHEY9
The feat/sacc-3 merge brought the guard-era sacc_io whose save() now
requires a keyword-only `type={'data','mock'}` provenance tag (PRD #241
§4) and whose load() refuses unblinded `type='data'`. Application code and
tests from the pr4/pr7 lineage still called the old `save(s, path)` and
referenced a template file the #236 cleanup had relocated, leaving 21 CI
failures outside the merge-conflict set. This adapts the callers to the
reviewed contract (sacc_io is unchanged).

save() type= threading:
- CosmologyValidation gains a `sacc_type` ctor kwarg (default "data"),
  stamped by every part-writer: cosebis/pure_eb/psf_systematics/pseudo_cl
  now `save(..., type=self.sacc_type)`.
- assemble_sacc inherits provenance from its parts: `type=metadata["type"]`.
- run_2pcf uses `cv.sacc_type`; run_2pcf_highres stamps "data" (real-catalogue
  fine covariance).
- Synthetic-data tests adopt the reviewed idiom `type="mock"` (loads freely):
  test_sacc_writers _roundtrip + reload helper, test_assemble_sacc part writer,
  and the test_pseudo_cl fixture (`sacc_type="mock"`).

Relocated FITS engine template:
- The #236 folder cleanup moved cosmosis_pipeline_A_ia.ini into
  cosmosis_config/templates/ (still used by the legacy cosmosis_fitting.py
  path). The new pr7 Snakemake regime — inference.smk's INFERENCE_TEMPLATE_DIR,
  generate_inference_config, and its tests — resolves templates from
  cosmosis_config/ directly, where pr7 placed A_ia_sacc.ini but never copied
  its FITS sibling. Restore A_ia.ini alongside the sacc template.

Fast suite: 254 passed, 1 skipped. Two remaining failures
(test_calculate_pure_eb pinned-value drift; test_configured_paths_exist_on_candide)
are pre-existing and environmental — they fail identically with these changes
stashed, and neither is in the API-drift set.

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