@@ -155,3 +155,93 @@ def test_xy_coords_in_output(self):
155155 # last point should match target
156156 assert abs (result ['x' ].values [- 1 ] - 9.0 ) < 1e-10
157157 assert abs (result ['y' ].values [- 1 ] - 2.0 ) < 1e-10
158+
159+
160+ import dask .array as da
161+ from xrspatial .visibility import cumulative_viewshed
162+
163+
164+ class TestCumulativeViewshed :
165+ def test_flat_terrain_all_visible (self ):
166+ """On flat terrain with elevated observers, every cell is visible."""
167+ data = np .zeros ((10 , 10 ), dtype = float )
168+ raster = _make_raster (data )
169+ observers = [
170+ {'x' : 2.0 , 'y' : 2.0 , 'observer_elev' : 10 },
171+ {'x' : 7.0 , 'y' : 7.0 , 'observer_elev' : 10 },
172+ ]
173+ result = cumulative_viewshed (raster , observers )
174+ assert result .dtype == np .int32
175+ # every cell should be seen by both observers
176+ assert (result .values == 2 ).all ()
177+
178+ def test_single_observer_matches_viewshed (self ):
179+ """Single-observer cumulative should match binary viewshed."""
180+ from xrspatial import viewshed
181+ from xrspatial .viewshed import INVISIBLE
182+ data = np .random .RandomState (42 ).rand (15 , 15 ).astype (float ) * 100
183+ raster = _make_raster (data )
184+ obs = {'x' : 7.0 , 'y' : 7.0 , 'observer_elev' : 50 }
185+ result = cumulative_viewshed (raster , [obs ])
186+ vs = viewshed (raster , x = 7.0 , y = 7.0 , observer_elev = 50 )
187+ expected = (vs .values != INVISIBLE ).astype (np .int32 )
188+ np .testing .assert_array_equal (result .values , expected )
189+
190+ def test_wall_blocks_one_side (self ):
191+ """A tall wall blocks visibility from the other side."""
192+ data = np .zeros ((5 , 11 ), dtype = float )
193+ data [:, 5 ] = 1000 # tall wall across all rows
194+ raster = _make_raster (data )
195+ obs_left = {'x' : 0.0 , 'y' : 2.0 , 'observer_elev' : 1 }
196+ obs_right = {'x' : 10.0 , 'y' : 2.0 , 'observer_elev' : 1 }
197+ result = cumulative_viewshed (raster , [obs_left , obs_right ])
198+ # the wall cell itself is visible to both
199+ assert result .values [2 , 5 ] == 2
200+ # cells far from wall visible to at least one observer
201+ assert result .values [2 , 0 ] >= 1
202+ assert result .values [2 , 10 ] >= 1
203+
204+ def test_per_observer_max_distance (self ):
205+ """Per-observer max_distance limits the analysis radius."""
206+ data = np .zeros ((20 , 20 ), dtype = float )
207+ raster = _make_raster (data )
208+ obs = {'x' : 10.0 , 'y' : 10.0 , 'observer_elev' : 10 , 'max_distance' : 3 }
209+ result = cumulative_viewshed (raster , [obs ])
210+ # corners should be 0 (beyond max_distance)
211+ assert result .values [0 , 0 ] == 0
212+ assert result .values [19 , 19 ] == 0
213+ # center should be 1
214+ assert result .values [10 , 10 ] == 1
215+
216+ def test_empty_observers_raises (self ):
217+ data = np .zeros ((5 , 5 ), dtype = float )
218+ raster = _make_raster (data )
219+ with pytest .raises (ValueError ):
220+ cumulative_viewshed (raster , [])
221+
222+ def test_dask_matches_numpy (self ):
223+ """Dask backend should produce the same result as numpy."""
224+ data = np .random .RandomState (99 ).rand (15 , 15 ).astype (float ) * 50
225+ raster_np = _make_raster (data )
226+ raster_dask = raster_np .copy ()
227+ raster_dask .data = da .from_array (data , chunks = (8 , 8 ))
228+ observers = [
229+ {'x' : 3.0 , 'y' : 3.0 , 'observer_elev' : 30 },
230+ {'x' : 12.0 , 'y' : 12.0 , 'observer_elev' : 30 },
231+ ]
232+ result_np = cumulative_viewshed (raster_np , observers )
233+ result_dask = cumulative_viewshed (raster_dask , observers )
234+ np .testing .assert_array_equal (result_np .values , result_dask .values )
235+
236+ def test_preserves_coords_and_dims (self ):
237+ data = np .zeros ((5 , 5 ), dtype = float )
238+ raster = _make_raster (data )
239+ raster .attrs ['crs' ] = 'EPSG:4326'
240+ observers = [{'x' : 2.0 , 'y' : 2.0 , 'observer_elev' : 10 }]
241+ result = cumulative_viewshed (raster , observers )
242+ assert result .dims == raster .dims
243+ np .testing .assert_array_equal (result .coords ['x' ].values ,
244+ raster .coords ['x' ].values )
245+ np .testing .assert_array_equal (result .coords ['y' ].values ,
246+ raster .coords ['y' ].values )
247+ assert result .attrs .get ('crs' ) == 'EPSG:4326'
0 commit comments