ngmix: position-seeded per-object RNG for Pujol noise cancellation (#796)#803
ngmix: position-seeded per-object RNG for Pujol noise cancellation (#796)#803cailmdaley wants to merge 1 commit into
Conversation
) Add an opt-in SEED_FROM_POSITION knob to the ngmix module (default False). When on, the object loop replaces the tile-level RNG with a per-object RandomState seeded from the object's sky position, and rebuilds the prior from that same RNG, so every stochastic step of the fit is a deterministic function of (ra, dec, ccd). Why: image-sim m-bias uses the Pujol estimator — the same scene under different applied shears, with shear response read from the branch difference of the SAME objects. Metacal's fixnoise adds a counter-noise realisation from an RNG seeded per tile, so an object gets different added noise in different branches (detection order differs) and the noise fails to cancel in the difference, inflating sigma_m. Seeding per object from position makes the same object draw the same added noise AND the same fit guesses in every branch, so both cancel and the m-bias error shrinks. Design and box math are Fabian's issue #796. The prior must be rebuilt per object, not just the RNG: the ngmix guesser draws its initial guess via prior.sample(), which consumes the RNG the prior was CONSTRUCTED with (ngmix 2.4.0 guessers.py / joint_prior.py). A per-object RNG alone would leave the guess drawing from the shared tile stream and break cancellation. The fixnoise counter-noise itself rides on the observation's attached noise image (use_noise_image=True), which prepare_ngmix_weights draws from the per-object RNG — so no fresh draw from the bootstrapper RNG is needed. Deviation from #796: Fabian combines the 3-arcsec position boxes as box_x + box_y, which collides along anti-diagonals — boxes (10,20) and (11,19) both give seed 30, so two distinct objects share a noise stream. This replaces the sum with a Cantor pairing (a bijection on the non-negative integers) after a zig-zag fold that maps signed box indices (Dec can be negative) onto non-negative ones, reduced mod 2**32 into the valid RandomState seed range. The box math is kept exactly as his: floor(ra*3600/3)+(ccd+1), floor(dec*3600/3)+(ccd+2), first-epoch coordinates (coord_list[0] convention), so all epochs of one object share the stream. Off by default => production path is byte-identical to develop (verified: metacal g for all five types matches unmodified develop bit-for-bit; the full fast suite passes, 326 tests). New property tests cover the seed's range, sub-box jitter invariance, adjacent-box distinctness, and the resolved anti-diagonal collision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J9qEVRCWEekT5ACNB3rjkq
|
Empirical validation at survey scale: same-tile A/B on the SKiLLS sims (tile 233.293, grid_1). Three measurements on existing data first, then an A/B of this branch:
The A/B (identical chain end-to-end; only
Measured gain: 4.8× (m1) / 6.4× (m2) in σ_m per tile (Var(Δe) down 26–41×; implied cross-branch noise correlation ρ ≈ 0.96–0.98). Matched-pair count even rose (1560→1645) — shared noise makes marginal detections consistent across branches. The chain's own m-bias output for the position-seeded arm — the first end-to-end number with every repair active (unit object weights AND unweighted global response per sp_validation#227, Sizing consequence: percent-level σ_m needs ~4–7 tiles with this PR (vs ~140 without); 15 tiles → σ_m ≈ 0.005–0.006. And a science consequence: the m ≈ +0.25 offset is now ~10σ from zero on a single tile, isotropic (m1≈m2), with clean additive terms — it is not estimator noise. The direct statement of the anomaly: the true sample response measured across paired branches is Δe/2γ ≈ 0.80, while metacal's finite-difference R̄ ≈ 0.64 — metacal under-measures the response by ~20% on these sims. The in-memory twin passes at |m|<5e-3 with the same metacal code and an exact PSF, and its measured PSF-size-error coefficient (m ≈ +1.8·δ_size) would place a +0.25 bias at ≈ +14% PSF size mismatch — so Where the code landed: this branch is now also on — Claude (Fable) on behalf of Cail |
…ling) into im_sims Resolutions: - ngmix.py: Martin's zero-noise edge-stamp guard kept, generalized to np.all(sig_noise > 0) so it stays valid on the per-pixel bkg_rms path, composed with the #803 per-object rng draws (his branch predated #803). - ngmix_runner.py: both imports (os + SqliteDict). - scripts/image_sims_pipeline/*, docs, auxdir tile lists, init_run .sh->.py: Martin's side wholesale — his live operational tooling on candide. The shapepipe-produces/sp_validation-orchestrates consolidation is deferred to the #766 develop-merge review, not imposed on his working branch. - fake_psf.py / vignetmaker.py auto-merges reviewed: robustness/logging only, no PSF content change. - His .felt debug fibers ride along (grid_4 blank tiles, snakemake OMP thread exhaustion) — both operationally important. Verified: tests/module/test_ngmix.py 22/22 in the runtime env (srun). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UvQvyJTEwYcLCwDEgpWMJ6
Summary
Adds an opt-in
SEED_FROM_POSITIONknob (defaultFalse) to the ngmix module. When enabled, the per-object RNG — and the prior it feeds — are seeded from the object's sky position instead of once per tile, so metacal's fixnoise counter-noise and the fit's initial guesses are identical for the same object across Pujol image-simulation shear branches and cancel in the branch difference, shrinking σ(m). For image simulations only; no benefit on real data, so off by default.Design
position_seed(ra, dec, ccd). Box math is Seed fix based on position for Pujol noise cancellation for image simulations #796's exactly (floor(ra*3600/3)+(ccd+1),floor(dec*3600/3)+(ccd+2), first-epoch coordinates); the 3-arcsec boxes make the seed robust to detection-order and sub-box centroid changes across branches.box_x + box_y, which collides along anti-diagonals (e.g. boxes (10,20) and (11,19) both → 30, giving two distinct objects the same noise stream). Replaced with a Cantor pairing (bijection on non-negative integers) after a zig-zag fold for negative Dec, reduced mod 2^32 into the validRandomStaterange. Box math untouched.prior.sample(), which draws from the RNG the prior was constructed with (ngmix 2.4.0guessers.py/priors.py). A per-object RNG alone leaves the guess on the shared tile stream and silently breaks the cancellation. Anyone wiring Seed fix based on position for Pujol noise cancellation for image simulations #796 by hand would hit this without an error.Verification (in-container, dev image)
gbyte-identical for all five types (noshear/1p/1m/2p/2m).Coordination notes
ngmix.py/ngmix_runner.pytoo and should land first; this diff is small and localized, the rebase is ours to do.example/cfis/config_tile_Ng_template.ini; the image-sims configs (example/cfis_image_sims/, on the Image simulations workflow #766 branch) should setSEED_FROM_POSITION = Trueonce both are in — happy to add that there after Image simulations workflow #766 merges.Design and box math: @fabianhervaspeters (#796).
— Claude Fable on behalf of Cail
🤖 Generated with Claude Code
https://claude.ai/code/session_01J9qEVRCWEekT5ACNB3rjkq