Skip to content

Make planar aspect cupy backend match numpy in [0, 360) (#2827)#2833

Merged
brendancol merged 4 commits into
mainfrom
deep-sweep-accuracy-aspect-2827
Jun 2, 2026
Merged

Make planar aspect cupy backend match numpy in [0, 360) (#2827)#2833
brendancol merged 4 commits into
mainfrom
deep-sweep-accuracy-aspect-2827

Conversation

@brendancol

Copy link
Copy Markdown
Contributor

Closes #2827

What

The planar aspect() cupy device function snapped any aspect above 359.999 down to 0, but the numpy/numba kernel did not. The numpy compass conversion produces values in [0, 360) and never reaches 360, so the clamp lived only on the GPU side and made cupy and dask+cupy disagree with numpy by about 360 on a near-degenerate gradient (tiny east gradient, strong north gradient).

Removing the clamp surfaced a second, related divergence: the GPU used a coarse radian-to-degree constant (57.29578) while numpy uses 180/pi. On an exact-90 tie that pushed the intermediate value into a different compass branch and produced 360 where numpy produced 0 (seen on uint32/uint64 random rasters).

Changes

  • Drop the _aspect > 359.999 -> 0 clamp from the cupy _gpu kernel.
  • Align the GPU radian-to-degree constant with the numpy kernel (180/pi) so the compass branch selects the same way on both backends.
  • Wrap any exact-360 GPU result back to [0, 360) to match the numpy output range.
  • Add cross-backend tests for the near-360 band.

Backend coverage

numpy, dask+numpy, cupy, dask+cupy. The geodesic path is untouched; it already canonicalizes consistently on CPU and GPU.

Test plan

  • New test_near_360_* tests pass on numpy, dask+numpy, cupy, dask+cupy
  • Full test_aspect.py and test_geodesic_aspect.py suites pass (235 passed)
  • Broad scan: numpy-vs-cupy max abs difference is 0 across 360 random rasters
  • Verified the numpy kernel never emits exactly 360, so the GPU wrap is a pure canonicalization

The cupy device function snapped any aspect above 359.999 to 0 while the
numpy/numba kernel left it as-is. Since the numpy compass conversion never
reaches 360, the clamp only existed on the GPU side and made cupy and
dask+cupy disagree with numpy by ~360 on a near-degenerate gradient.

Remove the band clamp, align the GPU radian-to-degree constant with the
numpy kernel's 180/pi so the compass branch selects the same way on both
backends, and wrap any exact-360 result back to [0, 360) to match the
numpy output range.

Add cross-backend tests for the near-360 band covering numpy, dask+numpy,
cupy, and dask+cupy.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label Jun 2, 2026
@brendancol brendancol merged commit 0e36a25 into main Jun 2, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Planar aspect() cupy backend snaps near-360 aspects to 0, diverging from numpy

1 participant