Skip to content

EUCLIDEAN proximity/allocation/direction: KDTree backends return NaN at exact max_distance (incl. max_distance=0) #3442

Description

@brendancol

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:proximityArea: proximitybackend-coverageAdding missing dask/cupy/dask+cupy backend supportbugSomething isn't workingdaskDask backend / chunked arrayssweep-accuracyFound by /sweep-accuracy

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions