Describe the bug
apply() and focal_stats() in xrspatial/focal.py document the kernel argument as "2D array where values of 1 indicate the kernel", i.e. a binary membership mask. But the CPU and GPU paths disagree on what a non-binary value (say, 2) means, so the same call can return different numbers depending on the backend.
CPU: _apply_numpy only copies a neighbour into the window when kernel[...] == 1. A cell with weight 2 is neither masked out (0) nor included (1), so it gets dropped. focal_stats on numpy and dask+numpy delegates to apply and inherits this.
GPU: _focal_sum_cuda and _focal_mean_cuda treat every nonzero cell as a weight and accumulate w * v, so a 2 doubles that neighbour's contribution. The min/max/range/variety kernels treat nonzero as plain membership and ignore the weight, which is yet another interpretation of the same kernel.
So focal_stats(..., stats_funcs=['mean', 'sum']) and apply(...) can give different answers on the same non-binary kernel depending which backend runs.
Expected behavior
Same result across all four backends for a given kernel. The documented contract is a binary mask, and weighting is supposed to happen inside the user's func (the apply docstring example does exactly that). So apply and focal_stats should reject non-binary kernels with a clear error instead of silently producing backend-dependent numbers. convolve_2d and hotspots genuinely use weighted kernels and are unaffected.
Additional context
Affected functions: apply, focal_stats. Backends: numpy, cupy, dask+numpy, dask+cupy.
Describe the bug
apply()andfocal_stats()inxrspatial/focal.pydocument thekernelargument as "2D array where values of 1 indicate the kernel", i.e. a binary membership mask. But the CPU and GPU paths disagree on what a non-binary value (say, 2) means, so the same call can return different numbers depending on the backend.CPU:
_apply_numpyonly copies a neighbour into the window whenkernel[...] == 1. A cell with weight 2 is neither masked out (0) nor included (1), so it gets dropped.focal_statson numpy and dask+numpy delegates toapplyand inherits this.GPU:
_focal_sum_cudaand_focal_mean_cudatreat every nonzero cell as a weight and accumulatew * v, so a 2 doubles that neighbour's contribution. The min/max/range/variety kernels treat nonzero as plain membership and ignore the weight, which is yet another interpretation of the same kernel.So
focal_stats(..., stats_funcs=['mean', 'sum'])andapply(...)can give different answers on the same non-binary kernel depending which backend runs.Expected behavior
Same result across all four backends for a given kernel. The documented contract is a binary mask, and weighting is supposed to happen inside the user's
func(theapplydocstring example does exactly that). Soapplyandfocal_statsshould reject non-binary kernels with a clear error instead of silently producing backend-dependent numbers.convolve_2dandhotspotsgenuinely use weighted kernels and are unaffected.Additional context
Affected functions:
apply,focal_stats. Backends: numpy, cupy, dask+numpy, dask+cupy.