Skip to content
Merged
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
85 changes: 85 additions & 0 deletions xrspatial/tests/test_viewshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,91 @@ def test_viewshed_nan_input_across_backends(backend):
assert vals[1, 1] == INVISIBLE


@pytest.mark.parametrize("backend", ["numpy", "dask+numpy"])
def test_viewshed_all_nan_input(backend):
"""An all-NaN raster is the degenerate boundary of the algorithm.

Every cell is NODATA, so no sweep events are generated. The observer
cell is still marked 180 (the viewpoint is always visible to itself)
and every other cell stays at its INVISIBLE fill value. Pins the
current behaviour on the CPU-backed backends; the cupy/RTX path is a
separate implementation and is not covered here.
"""
arr = np.full((5, 5), np.nan)
xs = np.arange(5.0)
ys = np.arange(5.0)

if backend == "numpy":
data = arr
else:
data = da.from_array(arr, chunks=(3, 2))
raster = xa.DataArray(data, coords=dict(x=xs, y=ys), dims=["y", "x"])

v = viewshed(raster, x=2.0, y=2.0, observer_elev=5)
vals = v.values

assert vals[2, 2] == 180.0
# Only the observer cell is non-INVISIBLE; everything else is NODATA.
others = np.delete(vals.ravel(), 2 * 5 + 2)
assert (others == INVISIBLE).all()


@pytest.mark.parametrize("backend", ["numpy", "dask+numpy"])
def test_viewshed_inf_terrain_cell(backend):
"""An infinite terrain elevation value must not crash or poison output.

Tests pass inf only as observer_elev / target_elev / max_distance
parameters, never as an actual elevation cell. An infinitely tall cell
comes back visible (180) and does not leak NaN into the rest of the
grid. Covers the CPU-backed backends (numpy, dask Tier B).
"""
arr = np.full((5, 5), 2.0)
arr[1, 1] = np.inf
xs = np.arange(5.0)
ys = np.arange(5.0)

if backend == "numpy":
data = arr
else:
data = da.from_array(arr, chunks=(3, 2))
raster = xa.DataArray(data, coords=dict(x=xs, y=ys), dims=["y", "x"])

v = viewshed(raster, x=2.0, y=2.0, observer_elev=5)
vals = v.values

assert vals[2, 2] == 180.0
# An infinitely tall cell is visible, not INVISIBLE.
assert vals[1, 1] > INVISIBLE
# The inf value must not poison any cell with NaN.
assert not np.isnan(vals).any()


@pytest.mark.parametrize("backend", ["numpy", "dask+numpy"])
def test_viewshed_single_pixel(backend):
"""A 1x1 single-pixel raster returns just the observer cell at 180.

Both axes are degenerate, so the resolution guard (#2744) falls back to
unit spacing on each axis. The lone cell is the viewpoint and reads 180
without dividing by zero. Single-pixel rasters are a classic spot for
kernel-boundary bugs, so pin the result on numpy and dask Tier B.
"""
arr = np.zeros((1, 1))
xs = np.array([0.0])
ys = np.array([0.0])

if backend == "numpy":
data = arr
else:
data = da.from_array(arr, chunks=(1, 1))
raster = xa.DataArray(data, coords=dict(x=xs, y=ys), dims=["y", "x"])

v = viewshed(raster, x=0.0, y=0.0, observer_elev=5)

assert v.shape == (1, 1)
assert v.values[0, 0] == 180.0
assert not np.isnan(v.values).any()


@pytest.mark.skipif(not has_rtx(), reason="rtxpy not available")
@pytest.mark.parametrize("fine_axis", ["x", "y"])
def test_viewshed_gpu_anisotropic_matches_cpu(fine_axis):
Expand Down
Loading