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
25 changes: 25 additions & 0 deletions xrspatial/tests/test_viewshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# -------------------------------------------------------------------
Expand Down
20 changes: 15 additions & 5 deletions xrspatial/viewshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand Down
Loading