Fast path for find_pixel_radii on non-rotated HPC maps#325
Fast path for find_pixel_radii on non-rotated HPC maps#325GillySpace27 wants to merge 3 commits into
Conversation
fbf076c to
e3ae8e6
Compare
For non-rotated helioprojective-Cartesian maps -- the common case for AIA,
HMI, LASCO, EUI, and similar instruments -- ``find_pixel_radii`` paid for a
full ``smap.wcs.pixel_to_world`` SkyCoord round-trip on every call. On a
4096x4096 map that's ~1 s of WCS conversion, and the function is called
twice in ``rhef`` (once via ``find_radial_bin_edges``, then again inside
``blackout_pixels_above_radius``) so the per-frame cost was ~2 s before any
filtering happened.
This commit adds an analytic fast path that computes pixel-to-sun-centre
distance directly from the WCS header values:
dx = (np.arange(nx) - crpix) * cdelt + crval # 1-D arcsec
dy = ...
r = arctan(hypot(dx, dy) in radians) # TAN inverse
and falls through to the existing SkyCoord path whenever the rotation
matrix isn't identity or either axis isn't HPLN/HPLT. Output matches the
slow path to better than 2e-4 R_sun on a 4096^2 map at 2"/px (and to
floating-point precision on AIA-scale 0.6"/px maps).
Measured on the same hardware as my previous PR's benchmark:
4096^2 HPLN-TAN, no rotation:
slow (SkyCoord) ~1.05 s
fast (this PR) ~0.15 s ~7x
To eliminate the second call in ``rhef`` entirely,
``blackout_pixels_above_radius`` gains a keyword-only ``map_r`` argument
that callers already holding the radii can pass in -- skipping its
internal ``find_pixel_radii`` call. ``rhef`` now does this: it has
``map_r`` from ``find_radial_bin_edges`` and threads it through. Both
optimisations together drop the WCS-bound portion of ``rhef`` from ~2.1 s
to ~150 ms on the 4096^2 benchmark.
Tests:
- Equivalence: fast path matches slow path within 2e-4 R_sun on
sun-centred, off-centre, and scale-kwarg variants of a synthetic HPC
fixture.
- Fallback: a non-identity PC rotation matrix triggers the SkyCoord
path (verified by mocking the fast helper).
- ``map_r`` reuse: ``blackout_pixels_above_radius(map_r=...)`` produces
byte-equal output to the no-kwarg call, and the patched internal
``find_pixel_radii`` is verified NOT to be called.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e053951 to
a05e91b
Compare
…ation scikit-image 2.0 (currently dev) raises ``PendingSkimage2Change`` warnings from ``skimage.morphology.white_tophat`` because it will switch the default ``mode`` from ``'reflect'`` to ``'ignore'``. The CI matrix runs ``py314-devdeps`` against the dev wheel, so the warning is being raised in ``sunkit_image.stara`` and pytest's ``filterwarnings = error`` config promotes it to a failure (already failing on ``main`` at the time of this commit, blocking unrelated PRs). Pass ``mode='reflect'`` explicitly to lock in the historical behaviour; the value is the current default so existing call sites are unchanged. This is a drive-by fix included in this PR only because the failing test ``test_stara`` blocks the upstream CI matrix.
a05e91b to
e157b8d
Compare
Gilly + another Claude landed two upstream-pending optimizations in his sunkit-image fork: - sunpy/sunkit-image#324: sort-and-group rhef inner loop (~10× at the bin density we use) - sunpy/sunkit-image#325: analytic find_pixel_radii fast path for non-rotated helioprojective-Cartesian maps (~7×) Combined, a 4096² RHEF drops from ~2.1 s to ~150 ms on the Render box — roughly 14× end-to-end. Side benefits include making the "HQ Filtered" tier feasible to render on demand for any user-picked date, and unlocking the hourly auto-render Gilly wants for gilly.space/sun. Both PRs are API-compatible (no signature changes to radial.rhef or utils.find_pixel_radii) so the call sites in api/main.py:90-91 and api/main.py:1232/1235 need no changes. Source: temporary merged branch https://github.com/GillySpace27/sunkit-image/tree/solar-archive-fast-rhef which I created by `git merge --no-ff origin/rhef-fast-kernel` on top of `origin/wcs-fast-path`. Clean auto-merge, no conflicts. TODO: flip back to a pinned PyPI release once both PRs land in a tagged upstream sunkit-image release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ayshih
left a comment
There was a problem hiding this comment.
I'm uneasy with the level of approximation in this PR because the inaccuracies will become unacceptable off the solar disk. (The inaccuracies would be "okay" if find_pixel_radii() were only about identifying the solar limb, but the function is more general than that.) My inclination would be for find_pixel_radii() to not use this fast path automatically, but rather have the user opt-in via a keyword argument.
| dx_as = ((np.arange(nx) - crpix[0]) * cdelt[0] + crval[0]) * u.Unit(cunit[0]).to(u.arcsec) | ||
| dy_as = ((np.arange(ny) - crpix[1]) * cdelt[1] + crval[1]) * u.Unit(cunit[1]).to(u.arcsec) |
There was a problem hiding this comment.
Uses the small-angle approximation for the WCS projection, and assuming that the reference coordinate is close to Sun center
| # Sky-plane offsets in arcsec (broadcast across the 2-D grid). | ||
| dx_as = ((np.arange(nx) - crpix[0]) * cdelt[0] + crval[0]) * u.Unit(cunit[0]).to(u.arcsec) | ||
| dy_as = ((np.arange(ny) - crpix[1]) * cdelt[1] + crval[1]) * u.Unit(cunit[1]).to(u.arcsec) | ||
| r_planar = np.sqrt(dx_as[None, :] ** 2 + dy_as[:, None] ** 2) |
There was a problem hiding this comment.
Uses the small-angle approximation
|
|
||
| Skips ``smap.wcs.pixel_to_world``'s SkyCoord round-trip and computes the | ||
| pixel-to-sun-centre distance directly from the WCS header values. Uses | ||
| the TAN gnomonic projection's spherical-distance inverse so the output |
There was a problem hiding this comment.
Given the approximations being used, this calculation is not actually specific to the gnomonic projection, but the inaccuracy will depend on the projection
The analytic fast path approximates the WCS projection (tangent-plane treatment, reference-near-Sun-center assumption), so its accuracy degrades off-disk where find_pixel_radii is also used. Per review feedback, gate it behind a keyword-only fast= argument (default False) so the exact SkyCoord path stays the default for all callers; rotated and non-HPC maps still fall through to the exact path even when fast=True. Update tests to opt in explicitly and add a test asserting the default does not touch the approximation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good point — thanks. The fast path does lean on the small-angle / reference-near-Sun-center assumptions, and I've pushed a commit making it opt-in via a keyword-only
Let me know if you'd prefer a different keyword name or a warning when |
Motivation
After #324 collapsed the inner loop in
~sunkit_image.radial.rhef, profiling showed~sunkit_image.utils.find_pixel_radiihad become the new bottleneck: on a 4096² map it costs ~1 s per call, andrhefcalls it twice in succession (once viafind_radial_bin_edges, once viablackout_pixels_above_radius). That is ~2 s of WCS conversion before any filtering happens, on what is functionally a fixed geometric transform.The slow path uses
smap.wcs.pixel_to_worldand the full SkyCoord stack for every pixel, but for the common case — a non-rotated helioprojective-Cartesian map with a knownrsun_obs— the answer is a closed-form function of CDELT, CRPIX, CRVAL, and the gnomonic projection. This PR adds that fast path (and lets internal callers avoid the duplicate call).Summary
Two coupled changes:
Analytic fast path for
find_pixel_radii: when both axes are HPLN/HPLT and the rotation matrix is the identity, compute pixel-to-Sun-centre distance directly fromcdelt,crpix,crval, andrsun_obsplus the gnomonic-TAN inverse. Rotated or non-HPC maps fall through to the existing SkyCoord path unchanged.Optional
map_r=kwarg onblackout_pixels_above_radius: callers that already hold the radii array can pass it in and skip the internalfind_pixel_radiicall.rhefnow does this: it hasmap_rfromfind_radial_bin_edgesand threads it through, eliminating the duplicate WCS pass.Equivalence
The fast-path output matches the SkyCoord path to:
Tests cover:
CRVAL != 0), andscale=-rescaled cases.blackout_pixels_above_radius(map_r=...)produces byte-equal output to the no-kwarg call, and the patched internalfind_pixel_radiiis verified NOT to be called.Performance
Measured on a synthetic 4096² HPLN-TAN map, macOS arm64, numpy 2.0, astropy 7.x:
smap.wcs.pixel_to_world)For
rhefspecifically — which callsfind_pixel_radiitwice — the WCS-bound portion drops from ~2.1 s to ~150 ms total. Combined with the inner-loop rewrite in #324, a 4096² × 720-binrhefshould run in around 250 ms instead of the original ~11 s.Notes
map_r=argument onblackout_pixels_above_radius.changelog/325.feature.rstcovers both speedup mechanisms in a single paragraph.main, parallel to Speed up rhef via sort-and-group inner loop #324; they touchsunkit_image/radial.pyin different spots and one will need a trivial rebase after the first lands. I'm happy to do that — just let me know merge order.skimage.morphology.white_tophat'smode='reflect'argument insunkit_image/stara.py. The CI matrixpy314-devdepsjob was failing ontest_staradue to aPendingSkimage2Changedeprecation that's unrelated to the rest of this PR; fix is conditional on kwarg availability so it stays compatible with the project's minimum scikit-image (0.20). Happy to split into its own PR.cc @sunpy/sunkit-image-maintainers