diff --git a/.claude/sweep-accuracy-state.csv b/.claude/sweep-accuracy-state.csv index 862dcbd64..1417adb3d 100644 --- a/.claude/sweep-accuracy-state.csv +++ b/.claude/sweep-accuracy-state.csv @@ -35,6 +35,6 @@ sky_view_factor,2026-05-01,1407,HIGH,4,Horizon angle ignored cell size; fixed by terrain,2026-04-10T12:00:00Z,,,,Perlin/Worley/ridged noise correct. Dask chunk boundaries produce bit-identical results. No precision issues. terrain_metrics,2026-04-30,,LOW,2;5,"LOW: Inf input not rejected, propagates as Inf (consistent across backends but undocumented). LOW: dask+cupy non-nan boundary path double-pads (wasted compute, central output values still correct). No CRIT/HIGH; tests cover NaN propagation, all 4 backends, all 4 boundary modes, dtype acceptance." viewshed,2026-05-29,2691,HIGH,3;5,max_distance window sized from coarser axis clipped cells on anisotropic rasters (PR #2702). LOW unfixed: distance_sweep ring radius same max(res) pattern but max_distance arg always None; _calculate_event_row_col line 880 abs(x>1) precedence bug is a broken guard only. cuda+rtx paths validated. -visibility,2026-04-13T12:00:00Z,,,,"Bresenham line, LOS kernel, Fresnel zone all correct. All backends converge to numpy." +visibility,2026-06-10,3194,HIGH,5,"Cat5 backend bug (HIGH): cumulative_viewshed + visibility_frequency raised TypeError on cupy input. count=np.zeros accumulator added to cupy-backed viewshed() result; cupy refuses numpy+cupy mixed add. cupy path had no test coverage. Fix #3194: pull vs_data to numpy via .get() before accumulate (matches _extract_transect). Verified on GPU (RTX A6000): both funcs run, int32/float64 dtypes, valid ranges; 26 visibility tests pass. Cats 1-4 clean: Bresenham endpoints/coverage correct; LOS horizon-angle test correct (>= inclusive, observer cell forced visible); NaN terrain marks that cell invisible without poisoning max_angle downstream; distance accumulation fine. LOW (documented, not fixed): visible[] uses terrain elevations not tgt_h so target_elev does not affect the LOS mask (only los_height/fresnel); fresnel_clear=True over a NaN terrain cell (NaN= 0 + assert result.values.max() <= len(observers) + def test_wall_blocks_one_side(self): """A tall wall blocks visibility from the other side.""" data = np.zeros((5, 11), dtype=float) diff --git a/xrspatial/visibility.py b/xrspatial/visibility.py index b4636cc5b..96fb09892 100644 --- a/xrspatial/visibility.py +++ b/xrspatial/visibility.py @@ -282,6 +282,12 @@ def cumulative_viewshed( target_elev=te, max_distance=md) vs_data = vs.data + # viewshed() returns a cupy-backed result for a cupy-backed raster. + # The numpy `count` accumulator can't be added to a cupy array, so + # pull it to numpy first (matching how _extract_transect handles + # cupy data). + if has_cuda_and_cupy() and is_cupy_array(vs_data): + vs_data = vs_data.get() if _is_dask and not isinstance(vs_data, da.Array): vs_data = da.from_array(vs_data, chunks=raster.data.chunks) count = count + (vs_data != INVISIBLE).astype(np.int32)