Skip to content

Guard degenerate-axis resolution in viewshed CPU sweep#2745

Open
brendancol wants to merge 1 commit into
mainfrom
deep-sweep-security-viewshed-2026-05-29-01
Open

Guard degenerate-axis resolution in viewshed CPU sweep#2745
brendancol wants to merge 1 commit into
mainfrom
deep-sweep-security-viewshed-2026-05-29-01

Conversation

@brendancol
Copy link
Copy Markdown
Contributor

Closes #2744

_viewshed_cpu computed grid resolution as range / (dim - 1). For a single-row or single-column raster that is 0 / 0 = NaN, which then flowed through every distance and gradient calculation and silently marked every non-observer cell INVISIBLE on flat terrain (no error raised).

  • Fall back to unit resolution when an axis has length 1, matching the guard already present in _viewshed_windowed and _viewshed_dask.
  • The max_distance path routes a degenerate window back through _viewshed_cpu, so it is covered by the same fix.

Backend coverage: numpy (direct fix). The dask and cupy/RTX entry points already guarded this, so their behavior is unchanged; the windowed path that delegates to the CPU helper is now correct on degenerate inputs.

Test plan:

  • test_viewshed_single_row_or_column (1x5 and 5x1) — observer cell is 180, all cells visible, no NaN
  • test_viewshed_single_row_or_column_max_distance (1x7 and 7x1) — windowed path correct
  • Full test_viewshed.py suite passes (29 passed)

_viewshed_cpu computed ew_res/ns_res as range/(dim-1), which is 0/0 = NaN
for a single-row or single-column raster. The NaN propagated through all
distance/gradient math, silently marking every non-observer cell INVISIBLE
on flat terrain. Fall back to unit resolution for a degenerate axis,
matching _viewshed_windowed and _viewshed_dask. The max_distance window
path routes a degenerate window back through _viewshed_cpu, so it is fixed
too. Adds regression tests for 1xN and Nx1 rasters on both the full and
max_distance paths.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label May 30, 2026
Copy link
Copy Markdown
Contributor Author

@brendancol brendancol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Guard degenerate-axis resolution in viewshed CPU sweep

Blockers

None.

Suggestions

None.

Nits

None.

What looks good

  • The fix matches the guard already used in _viewshed_windowed (viewshed.py:2078-2079) and _viewshed_dask (viewshed.py:2180-2181), so the three CPU-resolution computations now agree.
  • Falling back to 1.0 for a length-1 axis is the right call: a single row has no defined y-resolution, and unit resolution keeps the distance/gradient geometry well-defined.
  • Tests cover both orientations (1xN and Nx1) and both code paths (full sweep and max_distance windowing, which delegates back to _viewshed_cpu). The assertions check the real failure mode: observer cell at 180, all other cells visible, and no NaN in the output.
  • I checked the 1x1 case by hand (both axes degenerate): it returns [180.] without error, so the two guards compose correctly.

Checklist

  • Algorithm matches reference/convention (mirrors sibling helpers)
  • NaN handling is correct (the fix removes the NaN source)
  • Edge cases covered by tests (1xN, Nx1; 1x1 verified manually)
  • No premature materialization or unnecessary copies
  • Benchmark not needed (bug fix, no perf change)
  • README feature matrix not applicable (no API change)
  • Docstrings unchanged and still accurate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

viewshed() returns wrong results for single-row or single-column rasters (divide-by-zero in resolution)

1 participant