Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions xrspatial/proximity.py
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,23 @@ def _process(

target_values = np.asarray(target_values)

# Reject non-finite explicit target_values. On numpy a pixel holding inf
# matched target_values=[inf] and ran the search, while dask/cupy mask
# non-finite pixels out and returned all-NaN for the same input. nan can
# never match a pixel anyway (nan == nan is False). Fail fast instead of
# producing backend-dependent output (issue #2850). A non-numeric dtype
# (e.g. strings) can't index a raster either, so it gets the same error
# rather than a downstream TypeError from np.isfinite.
if target_values.size and (
target_values.dtype.kind not in "iuf"
or not np.isfinite(target_values).all()
):
raise ValueError(
"target_values must all be finite numbers, got {0!r}.".format(
target_values.tolist()
)
)

if max_distance is None:
max_distance = np.inf

Expand Down Expand Up @@ -1547,7 +1564,8 @@ def proximity(
target_values: list
Target pixel values to measure the distance from. If this option
is not provided, proximity will be computed from non-zero pixel
values.
values. All entries must be finite; a non-finite value (inf or
nan) raises ValueError.

max_distance: float, default=np.inf
The maximum distance to search. Proximity distances greater than
Expand Down Expand Up @@ -1696,7 +1714,8 @@ def allocation(
target_values : list
Target pixel values to measure the distance from. If this option
is not provided, allocation will be computed from non-zero pixel
values.
values. All entries must be finite; a non-finite value (inf or
nan) raises ValueError.

max_distance: float, default=np.inf
The maximum distance to search. Proximity distances greater than
Expand Down Expand Up @@ -1847,7 +1866,8 @@ def direction(
target_values: list
Target pixel values to measure the distance from. If this
option is not provided, proximity will be computed from
non-zero pixel values.
non-zero pixel values. All entries must be finite; a non-finite
value (inf or nan) raises ValueError.

max_distance: float, default=np.inf
The maximum distance to search. Proximity distances greater than
Expand Down
33 changes: 33 additions & 0 deletions xrspatial/tests/test_proximity.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,39 @@ def test_zero_max_distance_keeps_meaning(test_raster, func):
general_output_checks(test_raster, result)


@pytest.mark.parametrize("backend", ['numpy', 'dask+numpy', 'cupy', 'dask+cupy'])
@pytest.mark.parametrize("func", [proximity, allocation, direction])
@pytest.mark.parametrize("target_values", [[np.inf], [-np.inf], [np.nan], [2, np.inf]])
def test_non_finite_target_values_raises(test_raster, func, target_values):
# A non-finite target_values entry used to produce backend-dependent
# output: numpy matched inf pixels and returned a real grid, while
# dask/cupy masked non-finite pixels out and returned all-NaN for the
# same raster (issue #2850). It must raise on every backend instead.
with pytest.raises(ValueError, match="target_values"):
func(test_raster, x='lon', y='lat', target_values=target_values)


@pytest.mark.parametrize("backend", ['numpy', 'dask+numpy', 'cupy', 'dask+cupy'])
@pytest.mark.parametrize("func", [proximity, allocation, direction])
def test_non_numeric_target_values_raises(test_raster, func):
# A non-numeric target_values can't index a raster; it must raise a clear
# ValueError on every backend rather than a downstream TypeError (#2850).
with pytest.raises(ValueError, match="target_values"):
func(test_raster, x='lon', y='lat', target_values=['a', 'b'])


@pytest.mark.parametrize("backend", ['numpy', 'dask+numpy', 'cupy', 'dask+cupy'])
@pytest.mark.parametrize("func", [proximity, allocation, direction])
def test_finite_target_values_run(test_raster, func):
# The raster carries an inf and a nan pixel; the default (empty
# target_values) path must keep ignoring them on every backend, and
# explicit finite targets must keep working unchanged.
result_default = func(test_raster, x='lon', y='lat')
general_output_checks(test_raster, result_default)
result_explicit = func(test_raster, x='lon', y='lat', target_values=[1, 3])
general_output_checks(test_raster, result_explicit)


def test_proximity_distance_against_qgis(raster, qgis_proximity_distance_target_values):
target_values, qgis_result = qgis_proximity_distance_target_values
input_raster = create_test_raster(raster)
Expand Down
Loading