Smokescreen-fork blinding: per-part-at-birth blind, hash-commitment custody, CAMB↔CCL cross-check (PR 6)#253
Draft
cailmdaley wants to merge 2 commits into
Draft
Smokescreen-fork blinding: per-part-at-birth blind, hash-commitment custody, CAMB↔CCL cross-check (PR 6)#253cailmdaley wants to merge 2 commits into
cailmdaley wants to merge 2 commits into
Conversation
This was referenced Jul 10, 2026
cailmdaley
force-pushed
the
feat/sacc-6-blinding
branch
from
July 10, 2026 22:52
971767b to
11b8895
Compare
cailmdaley
changed the base branch from
feat/sacc-5-firecrown-likelihood
to
feat/sacc-2-sacc-io
July 10, 2026 22:52
cailmdaley
added a commit
that referenced
this pull request
Jul 11, 2026
…ll image from lock (#266) * felt: fork-implementation — #243 green, #253 rework under fix; fork PRs reviewed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KpaRHk3QwN13myduQ3hJyf * felt: fork-implementation — constitution absorbs #241 re-rulings (one-file layout, per-part blinding at birth) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KpaRHk3QwN13myduQ3hJyf * deps: declare cosmo_numba + numba, adopt committed uv.lock, install image from lock Make sp_validation's environment reproducible so an image build can never re-resolve numpy past numba's ceiling — the drift that silently upgraded numpy to 2.5 and broke numba/ngmix. - Declare cosmo-numba (aguinot/cosmo-numba@main; not on PyPI) — the numba B-mode kernels b_modes.py imports. main carries numpy-2 FFT support via its rocket-fft dep and declares its own deps, so numba's numpy window reaches the resolver. Also pin numba directly: the one load-bearing constraint, made visible and resilient to cosmo-numba's dep metadata (which has emptied out between refs). - Declare the other imported-but-undeclared deps the audit found: matplotlib, pandas, pyyaml (core, src/); fitsio (glass extra); a new `workflow` extra for snakemake + mpi4py. cv_runner and unions_wl left undeclared (no resolvable source) with a NOTE. - requires-python and ruff target -> 3.12 (the container's Python; cosmo-numba's floor). - Commit uv.lock (un-ignored) as the SSOT; scope it to Linux via [tool.uv]. numpy resolves to 2.4.6, inside numba 0.66's <2.5 window. - Dockerfile installs from the lock: `uv sync --frozen --inexact` into the base image's /app/.venv (--inexact keeps the ShapePipe stack). Drops the ad-hoc snakemake layer and the cs_util `--upgrade` workaround — the lock pins cs_util 0.2.2 (with cs_util.size), decoupling us from the base image's cs_util. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vtw1qcgTrQ6YuMvxwYzNup * felt: uv-lock-cosmo-numba — @main resolution, install model, cosmo-numba gotcha finding Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vtw1qcgTrQ6YuMvxwYzNup * test: don't load base image's stale pytest-pydocstyle/pycodestyle plugins uv sync installs a newer pytest than the base ShapePipe image's pytest-pydocstyle/pytest-pycodestyle (>=2.4) expect; their pytest_collect_file hooks use the removed `path` arg and crash collection. sp_validation lints with ruff, so disable both via addopts (`-p no:`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vtw1qcgTrQ6YuMvxwYzNup --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
cailmdaley
force-pushed
the
feat/sacc-2-sacc-io
branch
from
July 16, 2026 00:39
55cb97d to
4489dd4
Compare
…abulary
Rebuild the per-part-at-birth blinding stack on top of feat/sacc-2-sacc-io.
sacc_io.py is now PR-2's canonical module verbatim + a single appended
gather(): the terminal assembly delegates its tracer/point/covariance
assembly to PR-2's merge() and adds only the blind-custody call
(assert_consistent_blind) and shared-stamp write.
The fail-closed load gate lives in sacc_io.load() per the PRD ("sacc_io fails
closed on load"); the blinding tooling uses allow_unblinded=True explicitly
where it must read a not-yet-blinded real vector (blind_part). save() now
carries the required type= (inherited from each part's provenance). Grid
vocabulary swept coarse->reporting, fine->integration across blinding.py,
blinding_theory.py, b_modes.py, blind_data_vector.py, and the blinding tests.
_extract_block/_set_values stay index-based (a code comment says why):
Smokescreen's ConcealDataVector aligns theory_fn output to the sub-SACC mean
by row position, so the block must be carved/written by contiguous integer
index, not by PR-2's tag-matching extract()/update_statistic().
Escrow/custody/CAMB<->CCL machinery is preserved exactly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzUt7VbtXwr2SCHUdiQTyt
…nfig Three integration-drift fixes surfaced by the reconciled base: - test_ac6_ac8 / test_ac8_dotted: the end-to-end fixtures now pass type="mock" to sio.save (PR-2 requires the provenance stamp); the parts are mocks, blind_part re-saves inheriting that type. - test_ac13: the CosmoSIS halofit config moved to cosmo_inference/cosmosis_config/templates/ on develop; point the AC13 assertion at the new path (token unchanged: mead2020_feedback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzUt7VbtXwr2SCHUdiQTyt
cailmdaley
force-pushed
the
feat/sacc-6-blinding
branch
from
July 16, 2026 09:55
4b058f7 to
c09b063
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #252.
Blinds the tomographic data vector per part, at birth, through the UNIONS-WL/Smokescreen fork: each blindable intermediate SACC product — reporting ξ±, integration ξ±, pseudo-Cℓ — is shifted by
t(hidden) − t(fiducial)(Muir et al. 2019) the moment the pipeline computes it, with the hidden cosmology drawn inside a fixed amplitude envelope by the fork's own CCL-native, per-key-independent draw. One seed per catalogue version drives every part's draw, so all parts share one hidden cosmology by construction. Only blinded parts persist on disk; derived statistics (COSEBIs, pure-E/B) are computed downstream from the already-blinded integration ξ± by the pipeline's own estimators, so they are born blinded — there is no re-derivation step inside the blinding code. No one can read S8 off the data until the collaboration unblinds; both the blind and the unblind are verifiable without a trusted keyholder.Stacks on #245 (
sacc_io); depends on the fork protocol (ConcealDataVector(fiducial_params, shifts_dict, sacc_data, *, seed, theory_fn), pinned fork SHA588a6b9). No firecrown anywhere in this PR's diff — the fork is the concealment engine, and every theory vector is a direct CCL computation.Reconciliation with #245's canonical
sacc_ioThis branch was rebuilt to stack on
feat/sacc-2-sacc-io(#245) rather than carry its own copy ofsacc_io.src/sp_validation/sacc_io.pyis now #245's canonical module verbatim plus one appendedgather()(the terminal part assembly); everything else in the module is byte-identical to #245. The reconciled surface:sacc_io: SACC read/write layer for the standard data-product layout #245's:grid='reporting'(analysis ξ±) /grid='integration'(COSEBIs/pure-EB input), swept from this branch's formercoarse/fineeverywhere (tags, filenames, docstrings, tests).save(s, path, *, type)stampstype='data'|'mock'in metadata (with a re-stamp guard); the blinding tooling passes each part's own provenance through.sacc_io.load(), per the PRD's letter ("sacc_io fails closed on load"):load(path, *, allow_unblinded=False)raises on atype='data'file lackingconcealed=True. The blinding/unblinding tooling — the two legitimate readers of an unblinded real vector — passesallow_unblinded=Trueexplicitly (blind_part); loads of blinded files carryconcealed=Trueand need no escape hatch. This branch's custody logic (commitment verification, escrow, seed math) stays inblinding.py; only the type/concealed load gate issacc_io's.gather()builds onsacc_io: SACC read/write layer for the standard data-product layout #245'smerge(): the tracer-union / ordered-point / block-diagonal-covariance assembly ismerge, sogatherdelegates to it and adds only the one blind-aware thing merge cannot know — theassert_consistent_blindcustody call and the shared-stamp write.The three-verb surface
blind-init(once per catalogue version) — draws an OS-entropy seed, writes a repo-committablecommitment.json(sha256(seed)+ a canonical config digest binding the envelope and fiducial), and encrypts the seed into a Fernet bundle (smokescreen.encryption). The plaintext seed is never written. These outputs are the blind's fixed state; every other call reads them.blind-part(per intermediate, at birth) — reads the encrypted seed (verifying both hashes against the commitment first), loads the plaintext part viasacc_io.load(..., allow_unblinded=True), conceals it through its matchingtheory_fnbackend under the shared seed, writes the blinded part (type=inherited from the part) with the part's true vector escrowed into a per-part encrypted bundle beside it, and deletes the plaintext part. Each escrow is self-contained: restoring a part needs only that part's bundle plus the seed bundle — corruption of one bundle loses one part, not all. Blinded metadata:concealed=True,blindlabel,blind_commitment,blind_config_digest;seed_smokescreenstripped.unblind(per part, or the assembled file viagrid-tag selection) — verifies bothsha256(seed)and the config digest againstcommitment.jsonbefore subtracting anything, recomputes the part's shift from the seed through the same backend, and subtracts — the seed-subtracted vector is the authority. When the part's escrow bundle is beside it and its storedseed_sha256matches the commitment, the escrow serves two subordinate roles: a tighter equality check (fail closed if it disagrees) and clearing the add-then-subtract ulp residue so the restore is bit-for-bit. An escrow bound to a different seed, or one that disagrees materially, never determines the output.Terminal assembly —
sacc_io.gatherassembles the standalone part SACCs into the one-file{version}.sacc(via #245'smerge: tracer merge, ordered points with all tags including bandpower windows, block-diagonal covariance). Its single blind-aware touch is one call toblinding.assert_consistent_blind, which fails the gather closed unless every blindable part carries the identicalblind_commitment(mixed blinded/plaintext assemblies also refuse), and stamps the shared commitment on the assembled file. ρ/τ diagnostic parts are exempt and pass through untouched.What's in the box
sp_validation/blinding_theory.py— the blinding backend's theory surface.TheoryConfig(frozen dataclass; fail-fast overrides), the home of the blinding/inference fiducial (CosmoSIS v1.4.6.3 IA-fiducial values: S8=0.80, Ωm=0.30, h=0.70, n_s=0.96, Σm_ν=0.06 eV, HMCode2020+feedback with logT_AGN=7.5). Carries two halofit tokens (ccl_halofit_version,camb_halofit_version) naming the one recipe per stack;Neff=3.046/T_CMB=2.7255are emitted explicitly to both stacks. Two independent ξ± paths live here: the CCL-native route (Boltzmann-CAMB P(k) → CCL Limber + FFTLog — the blinding recipe) and an independent pycamb P(k) →Pk2D→ CCL projection with a closed-form σ8-matched A_s rescale. numpy-only at module scope.sp_validation/blinding.py—{sigma8, Omega_c}half-widths at the fiducial (σ8 = S8/√(Ωm/0.3); Ω_c one-to-one). Half-widths are config, not code.theory_fnbackends, one per part: reporting ξ±, integration ξ± (gridtags), and pseudo-Cℓ (W @ Cℓ_EEon the storedBandpowerWindows; ΔBB = ΔEB ≡ 0 — the shift is pure E-mode). Two-tracer cross-pairs; rows aligned to each part's own stored order, never an assumed pairing.theory_fnspans — the fork's length guard enforces the agreement), driveConcealDataVector, write the shifted values back at their recorded indices (row order preserved). This block carve/write-back is deliberately index-based (_extract_block/_set_values), notsacc_io: SACC read/write layer for the standard data-product layout #245's tag-matchingextract()/update_statistic():ConcealDataVectoraligns itstheory_fnoutput to the sub-SACCmeanby row position, so the rows must be addressed by contiguous integer index. Covariance (including the integration part's dense per-pair block) and BB/EB rows are byte-identical to the input.blind_init/blind_part/unblind_partas above, plusassert_consistent_blindfor the gather. A part with no blindable block (ρ/τ diagnostics) refuses a blind call loudly.sp_validation/sacc_io.py— #245's canonical module verbatim +gather(terminal part assembly, one blind-aware call site as above).sp_validation/b_modes.py— gains the values-only estimator seamscosebis_from_xiandpure_eb_from_xi, exposing the samecosmo_numbakernelscalculate_cosebis/calculate_pure_eb_correlationdrive to callers holding ξ± arrays (e.g. a blinded integration-ξ± SACC part) rather than TreeCorr correlations. This is where born-blinded derived statistics come from — the estimators stay in the pipeline's namespace, not the blinding module's.scripts/blind_data_vector.py—blind-init/blind-part/unblind/verifyCLI;blind-initrefuses to overwrite an existing blind's state before drawing anything.Tests (all observed in the shared-venv run on candide)
test_blinding.pycovers the per-part acceptance criteria AC1–AC9;test_camb_ccl_crosscheck.pycovers AC10–14. Heavy precision tests are@pytest.mark.slow; the derived-statistics testsimportorskip("cosmo_numba"). The blinding suite +test_sacc_io.pyon candide (this reconciled branch): 90 passed, 0 failed, 0 skipped (slow theory tests included;test_sacc_io.py+test_blinding.py+test_camb_ccl_crosscheck.py).Observed numbers from the runs:
The AC4/AC9 comparands run the pipeline's own downstream convention — measured reporting ξ± + integration grid + edge-based bounds, through the
b_modesseams — on blinded vs true parts, so they check the born-blinded derivation path against the pipeline convention rather than any blinding-internal seam. AC2 is fork-draw recovery + placement (both sides share the backend; backend correctness is AC3's independently written reference). AC1 (zero-shift identity, per part + downstream derivation), AC5 (covariance/row-order/BB-EB/ρτ untouched), AC6 (custody: init writes only commitment + encrypted bundle; plaintext parts deleted; gather fails closed on mismatched or mixed blinds; unblind fails closed on either tampered hash), AC7 (same seed ⇒ identical shifts), and AC8 (end-to-end init → three blind-parts → gather → per-part bit-for-bit unblind → true derived statistics reproduced) are asserted exactly.Not in this PR
Snakemake wiring — which pipeline rule invokes
blind-part, whereblind-initfires per catalogue version — belongs to the Snakemake-parts row; the per-part surface here is designed for that wiring, but the DAG edges are authored there. Covariance blinding, unblinding-criteria policy, and the fiducial-cosmology group decision are likewise out of scope (config values, not code).Dependencies
The theory stack (
sacc, the UNIONS-WL Smokescreen fork pinned by commit,cosmo_numba,cryptography,pyccl) is already declared as a core dependency on thefeat/sacc-2-sacc-iobase (inherited fromdevelop), so this branch adds no dependency changes and no[blinding]extra — the earlier fork-pin duplication is gone now that the base carries the fork as core. The CI blinding-stack smoke test still imports the fork-onlydraw_param_shiftsand the kernel modules (cosmo_numba.B_modes.cosebis.COSEBIS,schneider2022.get_pure_EB_modes), so an upstream Smokescreen package or a missing kernel cannot ship green.Review-convergence additions (PRD #241 review, #280)
Boltzmann backend is one config knob (#280).
TheoryConfig.transfer_function(defaultboltzmann_camb) threads throughccl_cosmologyinto all three theory backends, so blinding and the CosmoSIS+CAMB inference share one power-spectrum path. Halofit follows the choice — CAMB HMCode tokens only underboltzmann_camb, CCL's own halofit otherwise (documented as a cross-check tool, not a production setting) — and the backend is bound into the blinding config digest (a different P(k) path is a different blind). AC13 pins the shipped default against the inference stack; AC2 is parametrized over a non-default backend (eisenstein_hu) to prove the knob threads consistently.Mock-only plaintext assembly (PRD §4, "Mocks vs data"). Terminal assembly is additionally gated on declared mocks: every unconcealed blindable part must carry metadata
type: mock(the tagsacc_io.savestamps on #245) or the gather fails closed — an unconcealeddatapart, or one missing the tag, can never silently assemble, so skipping the blind cannot expose real data. Mixed concealed/plaintext assemblies fail closed regardless of type.Closes #280.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WzUt7VbtXwr2SCHUdiQTyt