sacc_io: SACC read/write layer for the standard data-product layout#245
Conversation
1d4826c to
bd15a0b
Compare
…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
bd15a0b to
c67ff4d
Compare
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
…-forward) 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
55cb97d to
4489dd4
Compare
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
|
@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 |
|
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
3806f6b to
3abb566
Compare
|
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 θ 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 Multiple covariances: yes — a SACC file holds exactly one covariance, so we'll simply produce multiple merged files: a fiducial — 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
left a comment
There was a problem hiding this comment.
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.
|
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:
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 |
|
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. |
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>
|
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. |
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>
Closes #244. Row 2 of the PRD (#241 §8): the
sacc_iomodule — 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.pyand 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
galaxy_shear_xi_plus/minus(standard)source_i,source_j)theta(arcmin, TreeCorrmeanr),theta_nom,npairs,weight,gridgalaxy_shear_cl_ee/bb/eb(standard)source_i,source_j)ell+ sharedBandpowerWindowgalaxy_shear_cosebi_ee/bb(standard)source_i,source_j)n,theta_min,theta_maxgalaxy_shear_xiPure{E,B,Amb}_{plus,minus}(custom)source_i,source_j)thetapsf_rho{k}_xi_{plus,minus}(custom)psf_stars,psf_stars)thetagalaxyPsf_tau{k}_xi_{plus,minus}(custom)source_i,psf_stars)thetasource_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 thegridtag is the sole disambiguator; the grid names follow the convention already used throughoutpure_eb.py/cosebis.py/b_modes.py.One file per catalogue version
{version}.sacccarries 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'scosmosis_fitting.pyassembly 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 TreeCorrvarxip/varximdiagonal as the explicit degraded fallback.Two things this file is not:
{version}.saccfor 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
add_pseudo_clrequires 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 returnNonefor an absent optional component; explicit selections still fail loud.assemble_covariancehands 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);mergepreserves the blocks.ValueErrorotherwise) 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 viaSacc.indices. Analysis points insert before integration points so covariance blocks tile the mean contiguously.sacc.Sacc.indicesreturns 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, andupdate_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.get_bandpower_windows()through.psf_starsMisc 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=...)requirestype='data'|'mock'(no default) and stamps it into metadata;load(path)fails closed — atype='data'file without theconcealed=TrueSmokescreen stamp raises, so an unblinded real data vector can never be loaded silently.allow_unblinded=Trueis the loudly-documented escape hatch reserved for the blinding/unblinding tooling itself. Mocks load freely, blinded or not.Merge
merge(saccs)thinly wrapssacc.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 (atype='data'input cannot merge with atype='mock'one). It also guards θ consistency by tag semantics: all θ-carrying writers (ξ±, ρ, τ, pure-E/B) taggrid="reporting"by default, and same-length θ arrays sharing agridtag value must be bitwise identical — a near-miss grid (the historical ρ/τ-vs-ξ CosmoSIS failure) raises a loudValueError; grids under different tags (e.g.integration) differ by design and are unconstrained. The same rule covers the harmonic side: pseudo-Cℓ points taggrid="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_covariancefailure modes,extractsub-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_valmixin migration (PR 4), SACC→2pt-FITS converter and OneCovariance glue (PR 3).— Claude (Fable) on behalf of Cail.