Skip to content

sacc_io: SACC read/write layer for the standard data-product layout#245

Merged
cailmdaley merged 14 commits into
developfrom
feat/sacc-2-sacc-io
Jul 21, 2026
Merged

sacc_io: SACC read/write layer for the standard data-product layout#245
cailmdaley merged 14 commits into
developfrom
feat/sacc-2-sacc-io

Conversation

@cailmdaley

@cailmdaley cailmdaley commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #244. Row 2 of the PRD (#241 §8): the sacc_io module — every data product written to and read from SACC, with round-trip tests. This is the layout contract PRs 3 (converter), 4 (migration), and 6 (blinding) build on. The PR touches exactly two files: sacc_io.py and its test file (most of the 1500 lines are tests).

SACC in five sentences

A SACC file is one object holding tracers (here: one n(z) per tomographic bin, named source_0, source_1, …), a flat ordered list of data points, and optionally a covariance. Each data point is a data type string (galaxy_shear_xi_plus, …), a tuple of tracer names, and one value — plus a free-form dict of point tags: per-point metadata whose keys are strings and whose values are whatever FITS can store (floats, ints, strings). Tags do double duty: they carry the point's coordinates (theta, ell, n) and bookkeeping (npairs, weight), and they are the query keys readers select points by (e.g. grid='integration'). The covariance has no linkage to the points other than order: row/column i of the matrix is the i-th inserted point, full stop — which is why insertion order is load-bearing everywhere below. Everything, order included, survives FITS save/load bitwise.

What's in the file

Product SACC data type Tracers Point tags
ξ± (reporting + integration grids) galaxy_shear_xi_plus/minus (standard) (source_i, source_j) theta (arcmin, TreeCorr meanr), theta_nom, npairs, weight, grid
pseudo-Cℓ EE/BB/EB galaxy_shear_cl_ee/bb/eb (standard) (source_i, source_j) ell + shared BandpowerWindow
COSEBIs galaxy_shear_cosebi_ee/bb (standard) (source_i, source_j) n, theta_min, theta_max
pure E/B galaxy_shear_xiPure{E,B,Amb}_{plus,minus} (custom) (source_i, source_j) theta
ρ₀…ρ₅ psf_rho{k}_xi_{plus,minus} (custom) (psf_stars, psf_stars) theta
τ₀,τ₂,τ₅ galaxyPsf_tau{k}_xi_{plus,minus} (custom) (source_i, psf_stars) theta
n(z) NZ tracers source_0, source_1, …

Tomography-native throughout: every writer/reader takes a bin pair (i, j); the current single-bin analysis is the (0, 0) case. ξ± lives on two θ grids — the reporting grid (the coarse analysis binning that goes to inference) and the integration grid (the dense input the COSEBIs / pure-E/B integrals consume). Both share data type and tracer pair, so the grid tag is the sole disambiguator; the grid names follow the convention already used throughout pure_eb.py / cosebis.py / b_modes.py.

One file per catalogue version

{version}.sacc carries everything in the table: the analysis vector and the integration-grid ξ±. The covariance is assembled block-diagonally (zero cross-blocks — the same independence assumption today's cosmosis_fitting.py assembly makes): the per-statistic analysis blocks first, then a dense per-pair integration-ξ block — the analytic covariance at the integration binning when it exists (it is the input to the pure-E/B MC error propagation and the COSEBIs covariance transform), with the TreeCorr varxip/varxim diagonal as the explicit degraded fallback.

Two things this file is not:

  • Not where blinding happens. Per PRD §4, each blindable part (reporting ξ±, integration ξ±, pseudo-Cℓ) is blinded at birth, the moment it is computed (PR 6) — nobody waits on the terminal file for a blinded product they care about. This file is a gather of already-blinded parts (PR 4); it never sees a plaintext real data vector and is never itself a blinding step.
  • Not necessarily the archival product, in full. The integration-grid block is an intermediate — consumed by the COSEBIs / pure-E/B derivation before this file is even assembled — and it dominates the size: at the production binning (1000 θ bins, 0.5–300′) the covariance blocks alone are ~32 MB per bin pair, by far the largest thing in a tomographic file. Open question for review: keep it in {version}.sacc for provenance, or drop it from the terminal gather and let it persist only as its per-part file? The writers support both; it's a PR-4 assembly choice, not a format question.

Beyond the PRD's row-2 list, pure-E/B writers are included: §4 derives pure-E/B from blinded ξ± and PR 6's acceptance test measures them, so they need a home in the layout.

Design notes

  • Optional components are genuinely optional. A file carries whichever components of a statistic were actually computed: add_pseudo_cl requires EE (the bandpower window's reference series) but BB/EB are optional; COSEBIs Bₙ and the pure-E/B B/ambiguous pairs likewise (both-or-neither per pair). Composite readers return None for an absent optional component; explicit selections still fail loud.
  • Covariances are stored block-diagonally: assemble_covariance hands sacc a list of blocks → BlockDiagonalCovariance, one FITS table per block, Σ block² on disk instead of dense N² (~tens of MB for a 5-bin analysis vector instead of hundreds); merge preserves the blocks.
  • Insertion order is the covariance order (see the primer) — alignment holds by construction: writers enforce strictly ascending θ/ℓ grids (loud ValueError otherwise) and readers return insertion order, so reader output, covariance sub-blocks, and bandpower-window columns can never silently diverge. Bin pairs normalize to i≤j. Insertion is pair-major ([pair₀ ξ+; pair₀ ξ−; pair₁ ξ+; …]); type-major consumers (DES 2pt-FITS) permute explicitly via Sacc.indices. Analysis points insert before integration points so covariance blocks tile the mean contiguously.
  • Unmatched selections fail loud. sacc.Sacc.indices returns an empty array (warning only) when a selection matches nothing — a typo'd tag or a float scale cut that isn't bitwise-identical to the stored one would silently produce empty data vectors. Every reader, extract(), and covariance-block selector funnels through a shared guard that raises instead.
  • assemble_covariance(s, blocks) places per-statistic covariance blocks with zero cross-blocks and fails loud on any gap, overlap, or size mismatch.
  • extract() (copy + keep_selection) pulls a subset with its correctly-aligned covariance sub-block, and update_statistic(s, sub) is the value-only merge-back: each sub point matches exactly one target point by (data_type, tracers, tags) and only its value is replaced — insertion order and covariance untouched. Together they are PR 6's extract → conceal → merge-back primitive (applied per part, at birth) and the selection layer PR 3's converter builds on.
  • The pseudo-Cℓ writer takes the bandpower window matrix as explicit input: NaMaster computes it today but nothing saves it; PR 4 wires get_bandpower_windows() through.
  • The psf_stars Misc tracer sits beside the NZ tracers because a Sacc has one flat tracer namespace — bookkeeping, not physics; it exists solely so ρ/τ points have a referent.

Blinding hygiene (PRD §4, "Mocks vs data")

save(s, path, type=...) requires type='data'|'mock' (no default) and stamps it into metadata; load(path) fails closed — a type='data' file without the concealed=True Smokescreen stamp raises, so an unblinded real data vector can never be loaded silently. allow_unblinded=True is the loudly-documented escape hatch reserved for the blinding/unblinding tooling itself. Mocks load freely, blinded or not.

Merge

merge(saccs) thinly wraps sacc.concatenate_data_sets: per-statistic files combine in insertion order, shared tracers are stored once, covariances combine block-diagonally with the library's all-or-none rule (anything richer is #247), and metadata is unioned with loud conflicts (a type='data' input cannot merge with a type='mock' one). It also guards θ consistency by tag semantics: all θ-carrying writers (ξ±, ρ, τ, pure-E/B) tag grid="reporting" by default, and same-length θ arrays sharing a grid tag value must be bitwise identical — a near-miss grid (the historical ρ/τ-vs-ξ CosmoSIS failure) raises a loud ValueError; grids under different tags (e.g. integration) differ by design and are unconstrained. The same rule covers the harmonic side: pseudo-Cℓ points tag grid="reporting" too, effective-ℓ arrays sharing a tag value must be bitwise identical, and series on the same ℓ grid must carry identical bandpower windows — the windows, not the effective ℓ, are what theory integrates against.

Testing

68 tests, all synthetic and fast: per-writer round-trips (values, tags, windows, n(z) bitwise), covariance block alignment incl. zero cross-blocks, assemble_covariance failure modes, extract sub-covariance alignment, a 2-bin/3-pair tomographic case, reader/writer mirroring on a mixed file, the end-to-end one-file layout, the varxi-diagonal fallback, the full data/mock × concealed/unconcealed load matrix plus the escape hatch, and merge/update_statistic behaviour (point order, shared tracers, block-diagonal covariance, metadata conflicts, values-only substitution).

Two adversarial review passes have run (8 finder angles, every finding independently verified against the sacc 2.4 source). Pass 1 caught reader-side sorting (the silent covariance-desync class this module is built to prevent); pass 2 caught the silent-empty-selection family now closed by the loud guard, plus the merge/update edge cases above — all fixed, with regression tests.

Not in scope here: cosmo_val mixin migration (PR 4), SACC→2pt-FITS converter and OneCovariance glue (PR 3).

— Claude (Fable) on behalf of Cail.

cailmdaley added a commit that referenced this pull request Jul 11, 2026
…compat

The finding-6/7 commit made add_pure_eb's `bounds` a required keyword-only
argument, which broke three test_sacc_io.py call sites (plain pure-E/B
writes for storage/roundtrip and the non-ascending-theta guard) with a
TypeError — caught by the newly-enabled full CI suite, not the blinding-only
local runs.

add_pure_eb is PR-2's writer surface (feat/sacc-2-sacc-io / #245 owns it).
Make `bounds` optional (default None): a plain write omits the tmin/tmax tags,
exactly the pre-existing behavior. The tags are stamped only when the caller
supplies bounds — which the blinding path does, because blinding re-derivation
needs the pipeline's edge-based integration range and TreeCorr's edges are not
reconstructible from meanr. A file written without bounds simply cannot be
blinded: blinding.rederive_eb_from_fine_xi fails loud on the missing tags
rather than reconstructing a pseudo-edge that would not match the pipeline (the
finding-4/7 contract). No pseudo-edge default is reintroduced.

PR-2 surface note: this adds an optional `bounds` kwarg to add_pure_eb; #245's
owner should be aware the writer now carries the pure-E/B integration-bound
contract for blinding, opt-in and backward-compatible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KpaRHk3QwN13myduQ3hJyf
Base automatically changed from feat/sacc-1-deps to develop July 15, 2026 23:46
cailmdaley and others added 7 commits July 16, 2026 02:17
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>
The two-file split (analysis + {version}_xi_fine.sacc) was premised on a
10000-bin fine grid; the production operating point (Paper II B-modes) is
1000 bins, where a dense per-pair fine covariance block is ~32 MB and the
CosmoCov integration-binning covariance — which feeds pure-E/B and COSEBIs
error propagation — has a natural home as a BlockDiagonal block alongside
the analysis blocks. Layout test replaced with the one-file end-to-end
case (dense fine block, extract() sub-covariance alignment, zero
cross-blocks) plus a varxi-diagonal fallback test.

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

PRD #241 §4 (Mocks vs data): save() requires type='data'|'mock' and stamps
it into metadata; load() raises on type='data' files lacking the
concealed=True blinding stamp, so skipping the blind can never silently
expose real data. Mocks load freely. allow_unblinded=True is the loud
escape hatch reserved for the blinding/unblinding tooling.

merge() wraps sacc.concatenate_data_sets thinly: per-statistic files
combine in order, shared tracers stored once, covariance block-diagonal
(library-enforced all-or-none), metadata union with loud conflicts.
update_statistic() is the value-only merge-back for the
extract -> conceal -> merge blinding flow: matches each sub point by
(data_type, tracers, tags) and overwrites the value, leaving order and
covariance untouched.

Tests: all four data/mock x concealed/unconcealed quadrants, the escape
hatch, stamp validation, merge (points, covariance, metadata conflict,
mixed-covariance failure) and update_statistic (values-only, unique-match).

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

- grid tag values: coarse -> "reporting", fine -> "integration" (descriptive,
  not relative); tag kept because both grids share data type and tracer pair,
  so the tag is the sole disambiguator. Swept module docstrings and tests.
- Module docstring now spells out why insertion order is load-bearing:
  covariance row/column i refers to the i-th inserted data point.
- new_sacc: comment that psf_stars rides in the same tracer list as the NZ
  tracers only because a Sacc has one flat tracer namespace (bookkeeping,
  not physics).
- source_name/_pair helpers kept (4 and 8 call sites) with one-line
  justifications of the conventions they centralize.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWF72ofwJh6ekgnCt9Xx6C
- Factor the theta-tagged insertion loop into _add_theta_series (add_rho,
  add_tau, add_pure_eb); add_pure_eb zips PURE_TYPES.values() against its
  signature order, and PURE_KEYS is derived from PURE_TYPES instead of
  restating it.
- Factor the (theta, plus, minus) read pattern into _get_pm (get_xi,
  get_rho, get_tau).
- add_xi hoists the optional-tag None-filtering out of the point loop;
  extract and merge lose their throwaway mutable dicts; get_cosebis
  builds its scale-cut tags in one expression.
- Tests: shared _add_xi default-ξ builder and _xi_block/_cl_block/
  _cosebi_block canonical index-block helpers replace ~60 lines of
  copy-pasted setup; test_readers_on_mixed_file builds on
  _multi_statistic_sacc.

Behaviour unchanged; 41/41 tests green in the container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWF72ofwJh6ekgnCt9Xx6C
@cailmdaley
cailmdaley marked this pull request as ready for review July 16, 2026 00:39
@cailmdaley
cailmdaley force-pushed the feat/sacc-2-sacc-io branch from 55cb97d to 4489dd4 Compare July 16, 2026 00:39
Adversarial review (pass 2) findings:

- Sacc.indices returns an EMPTY array (warning only) on an unmatched
  selection; every reader, extract(), and covariance-block selector now
  funnels through a shared _indices guard that raises instead — a typo'd
  tag or non-bitwise-identical float scale cut can no longer propagate
  empty arrays downstream (assemble_covariance previously died with an
  opaque IndexError on the same path).
- get_cosebis(scale_cut=None) on a file carrying several scale cuts
  silently concatenated them; now raises and asks for an explicit cut.
- update_statistic let two sub points claim the same target point
  (last-write-wins); now raises.
- merge: the library keeps the FIRST input's tracer on a name clash with
  no equality check; shared tracers are now verified identical (z, nz)
  across inputs before concatenation.

7 regression tests; 48 total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzUt7VbtXwr2SCHUdiQTyt
@cailmdaley
cailmdaley requested review from LisaGoh and sachaguer July 16, 2026 09:29
@cailmdaley

Copy link
Copy Markdown
Collaborator Author

@sachaguer @LisaGoh this should be ready for review! the description should convey the high-level design, let me know if you have thoughts on that or the implementation details. i'm sure we'll make some changes as we run into issues integrating these functions into the codebase in the subsequent PRs, but it'll be good to start from a well thought out foundation

@LisaGoh

LisaGoh commented Jul 16, 2026

Copy link
Copy Markdown
Member

Thank you for this! Just so I understand correctly, this script is meant to be called as part of the Snakemake workflow where each statistic is calculated in cosmo_val, then transformed into separate sacc files, blinded, then once all the components (statistics, covariances, nz) are transformed and calculated, this script is called again to merge them into a single sacc file to rule them all?

Also, should we impose a catch to check that all the theta values are the same? (unless it's already done and I missed it) Because I remember the last time the theta values were not exactly the same for the rho/tau stats which led to CosmoSIS complaining, and we had to manually flush the values.

Finally, can we have the option to save different covariances (eg. one calculated with OneCov, NaMaster etc.) for validation purposes?

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
@cailmdaley
cailmdaley force-pushed the feat/sacc-2-sacc-io branch from 3806f6b to 3abb566 Compare July 18, 2026 09:50
@cailmdaley

Copy link
Copy Markdown
Collaborator Author

Thanks Lisa — good questions, all three.

Workflow: exactly right, with one nuance: this PR is pure library — no scripts and no integration with existing functionality yet. These functions get wired into anything that writes data vectors or n(z)s in the follow-up PRs; the flow you describe (per-statistic parts in cosmo_val → blind → final gather into one file) is precisely the design of the Snakemake migration PR (#247), which calls merge() for that last step.

θ consistency: you didn't miss it — there was no such check, and sacc itself enforces nothing (θ is an opaque tag to the library). Given the ρ/τ history we've now added a guard to merge(): points carrying the same grid tag are declared to live on the same binning, so their θ arrays must be exactly identical — anything less raises a loud ValueError naming the offending statistics, so mismatches get harmonized upstream instead of surfacing as a CosmoSIS complaint. All θ-carrying writers (ξ±, ρ, τ, pure-E/B) tag grid="reporting" by default, so they all sit in one consistency group; grids under different tags (e.g. the integration binning) differ by design and aren't constrained.

Multiple covariances: yes — a SACC file holds exactly one covariance, so we'll simply produce multiple merged files: a fiducial {version}.sacc plus any number of tagged variants. Since the final file is a cheap gather over per-statistic parts, a variant covariance reuses the whole data vector. How variants are declared and named is part of #247.

— Claude (Fable) on behalf of Cail

…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

@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.

The overall spirit seems good to me. What I don't have enough understanding yet, is whether we really want our final version sacc file to contain everything. I am tempted to say no and that we would tipycally want to remove:

  • CL_EB and CL_BB part of the power spectra
  • The finely binned 2PCF
  • Rho/Tau-statistics (you never know maybe leakage will vanish in the next iteration)
  • Their related covariance.

For some of them the existing code seems quite modular to remove these tracers. The only doubt I have is for the null test part of the angular power spectra that do not have a route where it is not saved if it is not given as input or if a certain boolean is True. Might be worth thinking about this at that stage.

@cailmdaley

Copy link
Copy Markdown
Collaborator Author

thanks @sachaguer. what's the argument for not including ~everything in the final sacc file? my thinking was that including everything gives confidence that the data products passed to inference are the same ones that passed all null tests & validation. for example:

(you never know maybe leakage will vanish in the next iteration)

yes! we should be absolutely sure we're using the leakage-free iteration for inference

it should be cheap to do this, since it's just a merge of existing products, and we can still allow the creation of inference FITS from sacc files that don't have all of these components.

i suppose the counter-argument is that if the leakage vanishes, that means something changed upstream and snakemake will also know to recompute the inference data vectors. but still i like the idea of having a single source of truth. i'm happy to let go of this if there's a drawback i haven't considered though

@sachaguer

Copy link
Copy Markdown
Contributor

I think the argument is what seems to me an unnecessary headache and use of storage. Typically, for the Cl, we have the cross-covariance for all polarisations across all tomographic bins, so it's huge, and we probably don't need to save this in the input for the inference.

I agree with the idea of a single source of truth, but if this is a merged file, that is already guaranteed by the upstream files used to generate it. As such, our snakemake workflow should be sufficient to determine, e.g., how a Cl_EB file was produced and whether it matches the merged file (which is essentially your final argument).

Essentially, I am not at ease using several times more storage than needed if we are in any case able to trace the source files (with a header for example). That being said, I have not computed how much storage it would cost to store Cl_eb/bb, or not. I just think, for this PR, it would be nice to have the option to include it in the sacc file as an additional degree of freedom if needed later.

cailmdaley and others added 2 commits July 21, 2026 13:41
assemble_covariance now passes sacc.add_covariance a list of per-block
matrices instead of a dense zero-filled N×N array. sacc.BaseCovariance.make
turns a list into a BlockDiagonalCovariance (one FITS table per block, Σ
block² on disk), so cross-blocks are zero and implicit rather than
materialized. Validation (contiguous/ascending indices, no gap/overlap,
square blocks matching their index span) is unchanged.

Every existing consumer already read through the polymorphic .dense
property, so only the type assertions needed updating (FullCovariance ->
BlockDiagonalCovariance). Added a test that merging two files that already
carry a BlockDiagonalCovariance (assembled via assemble_covariance) stays
block-diagonal through merge and a save/load round-trip. Updated the module
docstring's storage-cost discussion accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pure_eb.py's calculate_pure_eb/plot_pure_eb defaulted nbins_int=100;
cosebis.py's calculate_cosebis already defaulted to 1000, and every
production config (papers/bmodes, papers/cosmo_val fiducial/pure_eb
blocks) already overrides to 1000. This aligns the function-signature
default with what every caller actually uses; config plumbing is
untouched, so any explicit override still wins.

The papers/cosmo_val/config/config.yaml cosebis.nbins_int (currently
2000, production numerics) is deliberately left unchanged — see report.

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

Copy link
Copy Markdown
Collaborator Author

Sacha and I just chatted in person; we concluded it would be overkill to include the integration grids and their covariances in the final sacc. However if we take advantage of the block covariance classes in sacc, storing all the reporting-grid data products and covariances in a single file would only be 10s of MBs which should be fine. Nevertheless the storage should be flexible.

This is largely moot since this PR is just I/O, so after adding the block covariance functionality and making the save functions a little more flexible (e.g. not requiring EB data vectors) this is ready to be merged.

cailmdaley and others added 2 commits July 21, 2026 14:09
Writers no longer force components an analysis may not have computed:
add_pseudo_cl's BB/EB, add_cosebis's Bn, and add_pure_eb's B/ambiguous
blocks now default to None and are simply not written when omitted
(add_pure_eb still requires each +/- pair together). Structural
arguments (grids, tracer/bin identifiers, the value for a component
you ARE adding) remain required with no default.

Composite readers (get_pseudo_cl, get_cosebis, get_pure_eb) return
None / omit the key for an absent optional component instead of
raising, via a new _mean_optional helper; a selection naming a missing
component explicitly (s.indices, _mean, extract) still fails loud, per
the existing empty-selection guard. merge() and assemble_covariance
work unchanged on files with only a subset of components.

Documents the optionality contract in the module docstring, and adds
partial-file round-trip, merge, and explicit-selection-fails-loud
tests.

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

cosebis.nbins_int was 2000; every other integration-grid entry in this
config (pure_eb, the fiducial block) is already 1000, and cosebis.py's
own function defaults are 1000. Unify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cailmdaley
cailmdaley merged commit 9b0a4cc into develop Jul 21, 2026
3 checks passed
@cailmdaley
cailmdaley deleted the feat/sacc-2-sacc-io branch July 21, 2026 12:24
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.

sacc_io: SACC read/write layer for the standard data-product layout

3 participants