focal: honour boundary on the cupy backend (#2730)#2736
Merged
Conversation
mean(), apply(), and focal_stats() ignored the boundary parameter on the single-GPU cupy backend, always behaving as boundary='nan' (edge clamping) while numpy and dask honoured 'nearest'/'reflect'/'wrap'. Pad the cupy input per the boundary mode (reusing _pad_array) and trim the result, mirroring the numpy boundary path. Add cupy-vs-numpy boundary-equivalence tests.
brendancol
commented
May 30, 2026
Contributor
Author
brendancol
left a comment
There was a problem hiding this comment.
PR Review: focal honours boundary on the cupy backend (#2730)
Blockers (must fix before merge)
None.
Suggestions (should fix, not blocking)
None.
Nits (optional improvements)
xrspatial/focal.py:_apply_cupy_boundaryand_focal_stats_cupy_boundaryrepeat ther0/r1/c0/c1trim slicing that already lives in_apply_numpy_boundary. A small shared_trim(result, pad_h, pad_w)helper would drop the duplication. I'd leave it as-is though, since the current form matches the existing numpy code and a one-off helper for three call sites is a wash.
What looks good
- The three cupy wrappers follow the existing
_mean_numpy_boundary/_apply_numpy_boundaryshape, so the boundary semantics line up with numpy without a separate code path to keep in sync. boundary='nan'short-circuits to the old code, so the default path is untouched._focal_stats_cupy_boundaryrebuilds the output DataArray and reattaches the original coords and attrs after trimming. The new coord-preservation test covers that.- Tests check cupy against numpy across all four boundary modes for mean, apply, and focal_stats, run on a real CUDA host.
Checklist
- Algorithm matches the numpy boundary reference (pad then trim)
- All implemented backends produce consistent results (numpy vs cupy verified across modes)
- NaN handling correct ('nan' mode unchanged)
- Edge cases covered (1x1 kernel pads 0, trims to full array via the
if pad else Noneguard) - Dask chunk boundaries unchanged (dask+cupy already forwarded boundary)
- No premature materialization or unnecessary copies
- Benchmark not needed (bug fix, no new function)
- README feature matrix not applicable
- Docstrings accurate (boundary already documented on each public function)
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 #2730
What
The single-GPU cupy backend of
mean(),apply(), andfocal_stats()ignored the
boundaryparameter. It always behaved asboundary='nan'(edge clamping), while numpy and dask honoured
'nearest','reflect',and
'wrap'. The cupy dispatch functions were registered in theArrayTypeFunctionMappingwithout aboundarypartial.Change
_mean_cupy_boundary,_apply_cupy_boundary, and_focal_stats_cupy_boundarywrappers that pad the input per theboundary mode with
_pad_arrayand trim the result, mirroring theexisting numpy boundary path. The
'nan'mode is unchanged.focal_statswrapper rebuilds the output DataArray so theoriginal coords, dims, and attrs are preserved after trimming.
Backend coverage
numpy and dask were already correct. This fixes the cupy path. dask+cupy
already routed boundary through
map_overlap. All four backends now matchfor a given
boundaryvalue.Test plan
mean/apply/focal_statscupy results match numpy acrossnan/nearest/reflect/wrap(new parametrized tests)focal_statspreserves coords/dims/attrs under non-nan boundarytest_focal.pysuite: 129 passed (CUDA host)