Description
mean(), apply(), and focal_stats() ignore the boundary parameter on
the single-GPU CuPy backend. The numpy and dask backends honor
boundary='nearest', 'reflect', and 'wrap', but the CuPy paths always
behave as boundary='nan' (edge clamping).
The CuPy dispatch functions in xrspatial/focal.py never receive the
boundary argument:
mean() -> cupy_func=_mean_cupy
apply() -> cupy_func=_apply_cupy
focal_stats() -> cupy_func=_focal_stats_cupy
Each is registered in the ArrayTypeFunctionMapping without a boundary
partial, unlike the matching numpy and dask entries. The GPU kernels clamp
the neighbourhood window to the raster edge, which matches boundary='nan'
semantics no matter what the caller passed.
Reproduce
import numpy as np, xarray as xr, cupy
from xrspatial import mean
data = np.random.default_rng(0).random((6, 6))
np_agg = xr.DataArray(data, dims=['y', 'x'])
cp_agg = xr.DataArray(cupy.asarray(data), dims=['y', 'x'])
for b in ['nan', 'nearest', 'reflect', 'wrap']:
n = mean(np_agg, boundary=b).data
c = mean(cp_agg, boundary=b).data.get()
print(b, np.allclose(n, c, equal_nan=True, rtol=1e-4))
Output:
nan True
nearest False
reflect False
wrap False
The same divergence occurs for apply() and focal_stats().
Expected
For a given boundary value, all four backends (numpy, cupy, dask+numpy,
dask+cupy) should produce matching results on identical input.
Fix
Pad the CuPy input per the boundary mode (reuse _pad_array) before the GPU
kernel and trim the result, mirroring the numpy boundary path in
_mean_numpy_boundary and _apply_numpy_boundary.
Description
mean(),apply(), andfocal_stats()ignore theboundaryparameter onthe single-GPU CuPy backend. The numpy and dask backends honor
boundary='nearest','reflect', and'wrap', but the CuPy paths alwaysbehave as
boundary='nan'(edge clamping).The CuPy dispatch functions in
xrspatial/focal.pynever receive theboundaryargument:mean()->cupy_func=_mean_cupyapply()->cupy_func=_apply_cupyfocal_stats()->cupy_func=_focal_stats_cupyEach is registered in the
ArrayTypeFunctionMappingwithout aboundarypartial, unlike the matching numpy and dask entries. The GPU kernels clamp
the neighbourhood window to the raster edge, which matches
boundary='nan'semantics no matter what the caller passed.
Reproduce
Output:
The same divergence occurs for
apply()andfocal_stats().Expected
For a given
boundaryvalue, all four backends (numpy, cupy, dask+numpy,dask+cupy) should produce matching results on identical input.
Fix
Pad the CuPy input per the boundary mode (reuse
_pad_array) before the GPUkernel and trim the result, mirroring the numpy boundary path in
_mean_numpy_boundaryand_apply_numpy_boundary.