diff --git a/xrspatial/cost_distance.py b/xrspatial/cost_distance.py index a080b06cd..f9d06febb 100644 --- a/xrspatial/cost_distance.py +++ b/xrspatial/cost_distance.py @@ -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: @@ -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 @@ -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)) diff --git a/xrspatial/tests/test_cost_distance.py b/xrspatial/tests/test_cost_distance.py index 309a515e2..ef6d8a259 100644 --- a/xrspatial/tests/test_cost_distance.py +++ b/xrspatial/tests/test_cost_distance.py @@ -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 # -----------------------------------------------------------------------