Skip to content

Fix HEALPix spin!=0 inverse/forward inaccuracy from Wigner-d node - #387

Open
slosar wants to merge 1 commit into
astro-informatics:mainfrom
slosar:fix/healpix-spin-recursion-node
Open

slosar wants to merge 1 commit into
astro-informatics:mainfrom
slosar:fix/healpix-spin-recursion-node

Conversation

@slosar

@slosar slosar commented Jun 14, 2026

Copy link
Copy Markdown

Note: this bug was uncovered and fixed by agentic AI tools, but it is a real non-trivial bug whose fix should be merged. We need sasfe_abs for avoit hitting nans for auto-diff. There is a test added that fails in the current main version, but passes here, so it would be good to keep it. Below is the machine generated description:

The on-the-fly Price-McEwen Wigner-d recursion renormalises each step by bigi = 1/|dl_entry| and tracks lbig = log|dl_entry|. When an intermediate recursion value dl_entry is exactly zero (a node), bigi becomes inf and lbig becomes -inf, so dl_iter = inf*0 = NaN and the running log-norm goes to -inf. These NaNs are silently dropped by the nansum that accumulates ftm/flm, discarding that mode's contribution and producing percent-level errors.

Exact-zero nodes are essentially never hit by the generic theta samples of mw/mwss/dh/gl sampling, but HEALPix rings sit at rational cos(theta) values that land exactly on nodes for spin != 0, so spin-2 HEALPix transforms carried few-percent pointwise errors (spin-0 was unaffected and existing HEALPix tests only covered spin 0).

Guard the renormalisation so that where dl_entry == 0 we use bigi = 1 and lbig = 0, leaving lrenorm unchanged and setting the normalised value to 0, which is exactly consistent with the recursion bookkeeping. Applied to all four recursion paths (numpy/jax inverse and forward).

Adds a regression test comparing the recursive HEALPix spin-1/2 inverse against the independent Turok-recursion base transform.

The on-the-fly Price-McEwen Wigner-d recursion renormalises each step by
bigi = 1/|dl_entry| and tracks lbig = log|dl_entry|. When an intermediate
recursion value dl_entry is exactly zero (a node), bigi becomes inf and
lbig becomes -inf, so dl_iter = inf*0 = NaN and the running log-norm goes
to -inf. These NaNs are silently dropped by the nansum that accumulates
ftm/flm, discarding that mode's contribution and producing percent-level
errors.

Exact-zero nodes are essentially never hit by the generic theta samples of
mw/mwss/dh/gl sampling, but HEALPix rings sit at rational cos(theta) values
that land exactly on nodes for spin != 0, so spin-2 HEALPix transforms
carried few-percent pointwise errors (spin-0 was unaffected and existing
HEALPix tests only covered spin 0).

Guard the renormalisation so that where dl_entry == 0 we use bigi = 1 and
lbig = 0, leaving lrenorm unchanged and setting the normalised value to 0,
which is exactly consistent with the recursion bookkeeping. Applied to all
four recursion paths (numpy/jax inverse and forward).

Adds a regression test comparing the recursive HEALPix spin-1/2 inverse
against the independent Turok-recursion base transform.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov

codecov Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.11%. Comparing base (4ef1ee5) to head (cefdf46).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #387      +/-   ##
==========================================
+ Coverage   96.10%   96.11%   +0.01%     
==========================================
  Files          34       34              
  Lines        3539     3551      +12     
==========================================
+ Hits         3401     3413      +12     
  Misses        138      138              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@slosar

slosar commented Jul 23, 2026

Copy link
Copy Markdown
Author

Can we merge this into main at some point, as our project now depends on a hacked s2fft to function properly?

christianhbye added a commit to slosar/croissant that referenced this pull request Aug 10, 2026
Add the regression test owed by the polarization docs: the forward
spin transform is validated against analytically sampled spin-weighted
spherical harmonics (external ground truth, exercising croissant's
compute_alm call), and the inverse against s2fft's independent
Turok-recursion base implementation. Neither is a round trip, so the
partially cancelling forward/inverse errors cannot mask the defect.

Verified two-sided: 8/8 pass at the pinned slosar/s2fft revision;
6/8 fail on stock s2fft 1.4.0 (both test families). Doubles as the
acceptance test for adopting the upstream release once
astro-informatics/s2fft#387 merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
christianhbye added a commit to christianhbye/croissant that referenced this pull request Aug 11, 2026
* feat: add dense spherical harmonic engine

Add a cached native-JAX dense transform path for low-band-limit workloads. Support independent HEALPix lmax selection, finite iterative refinement, packed m>=0 storage, cache management, and automatic differentiation. Document the API and validate equivalence, JIT behavior, gradients, and low-L/high-nside operation.

* feat: add full-Stokes pair response support

* feat: support topocentric polarized skies

* Document the polarization + dense merge in the changelog

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

* docs: fold the polarization plan into docs/polarization.md

Move the durable parts of POLARIZATION_PLAN.md into the normative
convention document: the pair-response design rationale (why Jones
patterns would introduce Wigner-3j coupling and break the diagonal-in-m
time kernel), the public data model and its metadata contract, the
spin-weighted dense cache behavior, the s2fft pin rationale, and the
scalar API compatibility guarantees.

Drop the plan document itself: the phase list, test plan, acceptance
criteria, and co-development scheduling are working notes, not repository
documentation.

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

* ci: pin ruff version to make lint pass

* docs: add note about speed testing for dense vs s2fft

* test: add engine equivalence tests for gradients and truncated lmax

* docs: clarify recommended way to pre-warm dense matrix computation

* fix: remove no-op dtype argument from dense matrix API

The `dtype` argument of `precompute_dense_matrix` never affected the
built matrix: both builders choose complex128/complex64 from JAX's x64
setting, which is what `s2fft.forward` itself produces regardless of the
input map dtype. Its only effects were a duplicate cache entry per
requested dtype and, in the non-HEALPix builder, an under-precise
one_hot basis when float32 was requested under x64.

Resolve the matrix dtypes in a single `_dense_dtypes()` helper and stamp
the resolved complex dtype into the cache key. float32 and float64 maps
now share one cached matrix, while toggling `jax_enable_x64` still
changes the key so a complex64 matrix is never reused under x64.

Forcing the matrix to a requested dtype instead was rejected: it would
make the dense engine's output dtype diverge from s2fft's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop stale dtype argument left by the main merge

Merging main into feat/full-polarization combined main's five-parameter
_dense_matrix_key (dtype removed in 1a25240, since the matrix precision
follows JAX's x64 setting rather than the map dtype) with the
polarization branch's caller, which still passed data.dtype. Every
compute_alm(..., engine="dense") call without a precomputed matrix
therefore raised TypeError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci: drop Python 3.10 from the test matrix

The branch raises requires-python to >=3.11 (the pinned s2fft fork
needs it), so uv refuses to sync a 3.10 environment and the matrix
job fails before tests run.

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

* test: certify the pinned s2fft HEALPix spin-recursion fix

Add the regression test owed by the polarization docs: the forward
spin transform is validated against analytically sampled spin-weighted
spherical harmonics (external ground truth, exercising croissant's
compute_alm call), and the inverse against s2fft's independent
Turok-recursion base implementation. Neither is a round trip, so the
partially cancelling forward/inverse errors cannot mask the defect.

Verified two-sided: 8/8 pass at the pinned slosar/s2fft revision;
6/8 fail on stock s2fft 1.4.0 (both test families). Doubles as the
acceptance test for adopting the upstream release once
astro-informatics/s2fft#387 merges.

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

* chore: let release-please own the version

Restore 5.2.1 (the manifest-tracked version); the 6.0.0 bump will come
from release-please via a breaking-change squash title when the PR
merges.

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

* docs: fix typos and the HEALPix link in math.md

Addresses Copilot review feedback on #124; the typos predate the PR
but the touched file made them visible.

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

* docs: drop premature version-6 references

The version number is release-please's decision at merge time; describe
features in the present tense and reference 5.2.1 concretely as the
last Python 3.10-compatible release.

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

* docs: pin the psi orientation and the IAU circular-polarization label

Define the basis-rotation angle psi (right-handed about the outward
radial) so the spin labels are unambiguous and explicitly tied to the
McEwen & Wiaux convention s2fft implements, and derive the IEEE/IAU
label for the normative Stokes-V fixture: (1, +i) with exp(+i w t) is
right-hand circular, so V = RCP - LCP = (RR - LL)/2.

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

* docs: fix pre-existing typo in the README simulator overview

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

* chore: drop unused multiprocessing scaffolding from the benchmark

freeze_support() only matters for frozen Windows executables that spawn
processes; the benchmark spawns none.

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

* test: certify the dense analysis matrix on complex input

The existing direct-comparison test uses real input, for which a
conjugated analysis matrix would only conjugate the output; complex
input distinguishes the matrix from its conjugate and pins the VJP
row-extraction convention at 1e-12, with a 1j-scaled batch entry as a
second conjugation canary on the apply path.

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

* docs: state the horizon default and beam_rot units for PairStokesBeam

Both are load-bearing behaviors of the polarized beam that the
conventions document did not record.

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

* test: pin the unpolarized reduction to the scalar pipeline

An (I, 0, 0, 0) sky through an I-only pair response now provably
reproduces the scalar convolve visibilities end to end (phases and
multiple frequencies included), rather than the equivalence being
implied by separate sky-side and beam-side alm tests.

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

---------

Co-authored-by: Arnur Nigmetov <anigmetov@lbl.gov>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Christian Hellum Bye <chbye@berkeley.edu>
christianhbye added a commit to lusee-night/luseepy that referenced this pull request Aug 11, 2026
croissant-sim v5.3.0.dev0 (the full-Stokes polarization merge,
christianhbye/croissant@d4736db) is not on PyPI: croissant pins s2fft
to the slosar fork carrying the HEALPix spin-recursion fix
(astro-informatics/s2fft#387), and PyPI rejects packages with
direct-URL dependencies. Until that lands upstream, luseepy pins the
croissant tag directly and, because luseepy imports s2fft itself,
co-pins the identical s2fft revision per croissant's guidance in
docs/polarization.md. Both pins dissolve when s2fft#387 is released
and croissant 5.3.0 reaches PyPI.

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

This branch has not been deployed

No deployments
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