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
7 changes: 5 additions & 2 deletions xrspatial/cost_distance.py
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ def cost_distance(
friction: xr.DataArray,
x: str = "x",
y: str = "y",
target_values: list = [],
target_values: list = None,
max_cost: float = np.inf,
connectivity: int = 8,
) -> xr.DataArray:
Expand All @@ -1229,7 +1229,7 @@ def cost_distance(
Name of the y coordinate.
target_values : list, optional
Specific pixel values in *raster* to treat as sources.
If empty, all non-zero finite pixels are sources.
If not provided, all non-zero finite pixels are sources.
max_cost : float, default=np.inf
Maximum accumulated cost. Pixels whose least-cost path exceeds
this budget are set to NaN. A finite value enables efficient
Expand All @@ -1255,6 +1255,9 @@ def cost_distance(
if connectivity not in (4, 8):
raise ValueError("connectivity must be 4 or 8")

if target_values is None:
target_values = []

cellsize_x, cellsize_y = get_dataarray_resolution(raster)
cellsize_x = abs(float(cellsize_x))
cellsize_y = abs(float(cellsize_y))
Expand Down
42 changes: 42 additions & 0 deletions xrspatial/tests/test_cost_distance.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,48 @@ def test_target_values(backend):
np.testing.assert_allclose(out[0, 1], 2.0, atol=1e-5)


def test_target_values_default_is_none_sentinel():
"""target_values default is the None sentinel, not a shared mutable list.

Regression for issue #3340: a mutable default (``list = []``) shares one
list object across calls. cost_distance uses the ``None`` sentinel like
proximity()/allocation() do.
"""
import inspect

default = inspect.signature(cost_distance).parameters['target_values'].default
assert default is None


def test_target_values_none_matches_empty_list():
"""Omitting target_values, passing None, and passing [] are equivalent.

All three mean "every non-zero finite pixel is a source".
"""
source = np.array([
[7.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
])
friction_data = np.ones((3, 3))
raster = _make_raster(source)
friction = _make_raster(friction_data)

out_omitted = _compute(cost_distance(raster, friction))
out_none = _compute(cost_distance(raster, friction, target_values=None))
out_empty = _compute(cost_distance(raster, friction, target_values=[]))

np.testing.assert_array_equal(
np.nan_to_num(out_omitted), np.nan_to_num(out_none)
)
np.testing.assert_array_equal(
np.nan_to_num(out_omitted), np.nan_to_num(out_empty)
)
# Both non-zero finite pixels are sources (cost 0).
assert out_omitted[0, 0] == 0.0
assert out_omitted[2, 2] == 0.0


# -----------------------------------------------------------------------
# Lazy coordinate arrays for dask input
# -----------------------------------------------------------------------
Expand Down
Loading