Skip to content

proximity: max_distance comparison precision differs between CPU and GPU backends #3389

@brendancol

Description

@brendancol

Describe the bug

In xrspatial/proximity.py, the CPU brute-force distance helper _distance casts its result to np.float32 before returning (around line 261). The brute-force search then compares that float32-rounded distance against max_distance via best_dist <= max_distance. The CUDA kernel _proximity_cuda_kernel and its device helpers compute and compare in float64.

When max_distance lands between the float32-rounded distance and the true float64 distance of a target, the backends disagree: the CPU path treats the target as in range and returns a finite value, while the GPU path treats it as out of range and returns NaN (or the reverse).

This hits every path that runs the brute-force search: GREAT_CIRCLE proximity/allocation/direction, and EUCLIDEAN/MANHATTAN allocation/direction. EUCLIDEAN/MANHATTAN proximity on numpy goes through the cKDTree path, which bounds differently.

Reproducer

A single-target lat/lon raster, GREAT_CIRCLE proximity, with max_distance set between the float32 and float64 distance of one column:

import numpy as np, xarray as xr, cupy as cp
from xrspatial import proximity
from xrspatial.proximity import great_circle_distance

lon = np.linspace(0, 39, 40); lat = np.array([0.0])
data = np.zeros((1, 40)); data[0, 0] = 1.0
j = 20
d64 = great_circle_distance(lon[0], lon[j], lat[0], lat[0])
md = (float(np.float32(d64)) + d64) / 2.0   # between f32 and f64 distance

r_np = proximity(xr.DataArray(data, dims=['lat','lon'], coords={'lon':lon,'lat':lat}),
                 x='lon', y='lat', distance_metric='GREAT_CIRCLE', max_distance=md)
r_cp = proximity(xr.DataArray(cp.asarray(data), dims=['lat','lon'], coords={'lon':lon,'lat':lat}),
                 x='lon', y='lat', distance_metric='GREAT_CIRCLE', max_distance=md)

print(r_np.data[0, j])          # 2226389.75 (finite)
print(r_cp.data.get()[0, j])    # nan

numpy and dask+numpy return the finite distance; cupy and dask+cupy return NaN for the same input.

Expected behavior

All four backends should agree on whether a target is within max_distance. The comparison should use the same precision everywhere.

Severity

Medium. The divergence is silent, with no error raised, but it only surfaces when max_distance is set with sub-ulp precision relative to a target's distance. That is an adversarial input rather than typical usage.

Found by /sweep-accuracy on the proximity module.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:proximityArea: proximitybugSomething isn't workinggpuCuPy / CUDA GPU supportsweep-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