diff --git a/xrspatial/tests/test_viewshed.py b/xrspatial/tests/test_viewshed.py index 357a7dd1a..a914d4baa 100644 --- a/xrspatial/tests/test_viewshed.py +++ b/xrspatial/tests/test_viewshed.py @@ -580,6 +580,31 @@ def test_viewshed_custom_name(backend): assert result.name == "my_vs" +@pytest.mark.parametrize("bad", [-1.0, -0.5, float("nan"), + float("inf"), float("-inf")]) +def test_viewshed_invalid_max_distance_raises(bad): + """Negative or non-finite max_distance raises a clear ValueError (#2855). + + Validation lives at the public entry point, before backend dispatch, + so a single numpy raster covers every backend. Previously these + values fell through to confusing internal errors (e.g. "zero-size + array to reduction operation minimum" or "cannot convert float NaN + to integer"). + """ + raster = _make_raster("numpy") + with pytest.raises(ValueError, match="max_distance must be a finite"): + viewshed(raster, x=3, y=2, observer_elev=1, max_distance=bad) + + +@pytest.mark.parametrize("backend", ["numpy", "dask+numpy"]) +@pytest.mark.parametrize("good", [0.0, 3.0]) +def test_viewshed_valid_max_distance_still_works(backend, good): + """Finite max_distance >= 0 passes validation and returns a result.""" + raster = _make_raster(backend) + result = viewshed(raster, x=3, y=2, observer_elev=1, max_distance=good) + assert result.shape == raster.shape + + # ------------------------------------------------------------------- # dask+cupy backend tests # ------------------------------------------------------------------- diff --git a/xrspatial/viewshed.py b/xrspatial/viewshed.py index 1fbb4b294..e49939e07 100644 --- a/xrspatial/viewshed.py +++ b/xrspatial/viewshed.py @@ -1656,10 +1656,12 @@ def viewshed(raster: xarray.DataArray, when it is being analyzed for visibility. max_distance : float, optional Maximum analysis distance from the observer in surface units. - Cells beyond this distance are marked INVISIBLE without being - evaluated. When set and the raster is dask-backed, only the - chunks within the distance window are loaded — this is the most - efficient way to run viewshed on very large dask rasters. + Must be a finite number >= 0; a negative or non-finite value + raises ``ValueError``. Cells beyond this distance are marked + INVISIBLE without being evaluated. When set and the raster is + dask-backed, only the chunks within the distance window are + loaded — this is the most efficient way to run viewshed on very + large dask rasters. name : str, default='viewshed' Name of the output DataArray. Set on every backend so the result name does not depend on which backend ran. @@ -1735,8 +1737,16 @@ def viewshed(raster: xarray.DataArray, """ _validate_raster(raster, func_name='viewshed', name='raster') - # --- max_distance: extract spatial window for any backend --- + # --- max_distance: validate, then extract spatial window for any backend --- if max_distance is not None: + try: + is_bad = not np.isfinite(max_distance) or max_distance < 0 + except (TypeError, ValueError): + is_bad = True + if is_bad: + raise ValueError( + "max_distance must be a finite number >= 0, " + f"got {max_distance!r}") return _viewshed_windowed(raster, x, y, observer_elev, target_elev, max_distance, name)