Skip to content

Commit 1af378c

Browse files
committed
Accept target_values=None in the surface_* trio like the proximity trio (#3712)
surface_distance, surface_allocation and surface_direction declared target_values: list = [] while proximity, allocation, direction and cost_distance declare None and normalise it in the body. Passing None therefore reached np.asarray(None, dtype=float64) and blew up inside the _seed_sources numba kernel with a type-inference error that never mentioned target_values. Switch the three defaults to None, normalise in _compute, and update the docstring. Not a breaking change: None and [] both mean 'every non-zero finite pixel is a source'.
1 parent 069fc13 commit 1af378c

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

xrspatial/surface_distance.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,8 @@ def _chunk_func(source_block, elev_block):
12971297
def _compute(raster, elevation, x, y, target_values, max_distance,
12981298
connectivity, method, mode):
12991299
"""Core dispatcher for surface_distance / allocation / direction."""
1300+
if target_values is None:
1301+
target_values = []
13001302
_validate_raster(raster, func_name='surface_distance', name='raster')
13011303
_validate_raster(elevation, func_name='surface_distance',
13021304
name='elevation')
@@ -1421,7 +1423,7 @@ def surface_distance(
14211423
elevation: xr.DataArray,
14221424
x: str = "x",
14231425
y: str = "y",
1424-
target_values: list = [],
1426+
target_values: list = None,
14251427
max_distance: float = np.inf,
14261428
connectivity: int = 8,
14271429
method: str = 'planar',
@@ -1445,9 +1447,9 @@ def surface_distance(
14451447
Name of the x coordinate.
14461448
y : str, default='y'
14471449
Name of the y coordinate.
1448-
target_values : list, optional
1450+
target_values : list, default=None
14491451
Specific pixel values in *raster* to treat as sources.
1450-
If empty, all non-zero finite pixels are sources.
1452+
If ``None`` or empty, all non-zero finite pixels are sources.
14511453
max_distance : float, default=np.inf
14521454
Maximum surface distance. Pixels beyond this budget are NaN.
14531455
A finite value enables efficient Dask parallelisation.
@@ -1482,7 +1484,7 @@ def surface_allocation(
14821484
elevation: xr.DataArray,
14831485
x: str = "x",
14841486
y: str = "y",
1485-
target_values: list = [],
1487+
target_values: list = None,
14861488
max_distance: float = np.inf,
14871489
connectivity: int = 8,
14881490
method: str = 'planar',
@@ -1524,7 +1526,7 @@ def surface_direction(
15241526
elevation: xr.DataArray,
15251527
x: str = "x",
15261528
y: str = "y",
1527-
target_values: list = [],
1529+
target_values: list = None,
15281530
max_distance: float = np.inf,
15291531
connectivity: int = 8,
15301532
method: str = 'planar',

xrspatial/tests/test_surface_distance.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,42 @@ def test_target_values():
359359
assert sd[0, 1] == pytest.approx(2.0, abs=1e-5)
360360

361361

362+
@pytest.mark.parametrize("backend",
363+
['numpy', 'cupy', 'dask+numpy', 'dask+cupy'])
364+
@pytest.mark.parametrize("func",
365+
[surface_distance, surface_allocation,
366+
surface_direction])
367+
def test_target_values_none_matches_empty_list(backend, func):
368+
"""target_values=None means the same thing as [] on every backend.
369+
370+
The proximity trio and cost_distance already accept None; issue #3712
371+
reported that the surface_* trio raised a numba TypingError instead.
372+
"""
373+
source = np.zeros((6, 6), dtype=np.float64)
374+
source[0, 0] = 1.0
375+
source[5, 5] = 3.0
376+
elev = np.arange(36, dtype=np.float64).reshape(6, 6) * 0.5
377+
378+
raster = _make_raster(source, backend)
379+
elevation = _make_raster(elev, backend)
380+
381+
with_none = _compute(func(raster, elevation, target_values=None,
382+
max_distance=2.0))
383+
with_empty = _compute(func(raster, elevation, target_values=[],
384+
max_distance=2.0))
385+
386+
np.testing.assert_allclose(with_none, with_empty, equal_nan=True)
387+
388+
389+
def test_target_values_default_is_not_mutable():
390+
"""The public signatures must not carry a mutable default."""
391+
import inspect
392+
393+
for func in (surface_distance, surface_allocation, surface_direction):
394+
default = inspect.signature(func).parameters['target_values'].default
395+
assert default is None, f"{func.__name__} has a mutable default"
396+
397+
362398
# ---------------------------------------------------------------------------
363399
# Tests — connectivity
364400
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)