Description
For proximity, allocation, and direction with distance_metric='EUCLIDEAN', the numpy and dask+numpy backends return NaN at a target pixel when max_distance=0, while cupy and dask+cupy return the correct value (0.0 for proximity, the target value for allocation, 0.0 for direction). The same divergence shows up whenever max_distance is set exactly to an achievable distance.
A target pixel is distance 0 from itself, so with max_distance=0 it should qualify. The brute-force backends (cupy, dask+cupy) get this right. The cKDTree-based numpy/dask+numpy paths drop it to NaN.
Root cause
The numpy/dask KDTree path converts the inclusive max_distance into cKDTree's exclusive distance_upper_bound. In _process_numpy_kdtree it widens the bound by one ulp: upper = np.nextafter(max_distance, np.inf). For Euclidean queries (p=2) cKDTree compares squared distances internally, and nextafter(0.0, inf) is the smallest subnormal (5e-324); squaring it underflows to 0.0, so the exclusive bound collapses back to 0 and the distance-0 target is excluded. Manhattan (p=1) does not square, so it is unaffected.
Separately, _kdtree_query_lowest_index (used by the dask global and tiled KDTree paths, and by allocation/direction on dask) passes the raw max_distance as distance_upper_bound with no widening at all, so it stays exclusive and drops pixels sitting exactly at max_distance.
Reproduce
import numpy as np, xarray as xr
from xrspatial import proximity
data = np.zeros((3, 3)); data[1, 1] = 1.0
r = xr.DataArray(data, dims=['y', 'x'])
r['y'] = np.arange(3)[::-1].astype(float)
r['x'] = np.arange(3).astype(float)
print(proximity(r, max_distance=0.0).values[1, 1]) # numpy -> nan, should be 0.0
cupy returns 0.0 at the target for the same input.
Suggested fix
Route every cKDTree call through one inclusive-to-exclusive bound helper that holds up for both p=1 and p=2, including max_distance=0.
Found by /sweep-accuracy.
Description
For
proximity,allocation, anddirectionwithdistance_metric='EUCLIDEAN', the numpy and dask+numpy backends return NaN at a target pixel whenmax_distance=0, while cupy and dask+cupy return the correct value (0.0 for proximity, the target value for allocation, 0.0 for direction). The same divergence shows up whenevermax_distanceis set exactly to an achievable distance.A target pixel is distance 0 from itself, so with
max_distance=0it should qualify. The brute-force backends (cupy, dask+cupy) get this right. The cKDTree-based numpy/dask+numpy paths drop it to NaN.Root cause
The numpy/dask KDTree path converts the inclusive
max_distanceinto cKDTree's exclusivedistance_upper_bound. In_process_numpy_kdtreeit widens the bound by one ulp:upper = np.nextafter(max_distance, np.inf). For Euclidean queries (p=2) cKDTree compares squared distances internally, andnextafter(0.0, inf)is the smallest subnormal (5e-324); squaring it underflows to 0.0, so the exclusive bound collapses back to 0 and the distance-0 target is excluded. Manhattan (p=1) does not square, so it is unaffected.Separately,
_kdtree_query_lowest_index(used by the dask global and tiled KDTree paths, and by allocation/direction on dask) passes the rawmax_distanceasdistance_upper_boundwith no widening at all, so it stays exclusive and drops pixels sitting exactly atmax_distance.Reproduce
cupy returns 0.0 at the target for the same input.
Suggested fix
Route every cKDTree call through one inclusive-to-exclusive bound helper that holds up for both p=1 and p=2, including
max_distance=0.Found by /sweep-accuracy.