Skip to content

Fix viewshed crash on NaN cells and mark them INVISIBLE (#2857)#2876

Merged
brendancol merged 1 commit into
mainfrom
issue-2857
Jun 3, 2026
Merged

Fix viewshed crash on NaN cells and mark them INVISIBLE (#2857)#2876
brendancol merged 1 commit into
mainfrom
issue-2857

Conversation

@brendancol

Copy link
Copy Markdown
Contributor

Closes #2857

What changed

  • _init_event_list skips NaN cells so they no longer emit ENTERING/CENTER/EXITING events. This removes the ValueError: node not found that hit when a NaN sat on the observer row to the right of the observer (its EXITING event tried to delete a node that was never inserted).
  • _init_event_list now returns the event count, and _viewshed_cpu trims the unused trailing rows of the pre-allocated event list before sorting. Without the trim, the leftover all-zero rows sort as CENTER events at cell (0, 0) and would mark it visible.
  • NaN cells keep their INVISIBLE (-1) fill value. That is the sentinel downstream consumers already test for (visibility.py and experimental/min_observable_height.py both treat != INVISIBLE as visible), so a NaN cell is never counted as visible.

Backends

The fix lives in the CPU sweep, which backs the numpy backend and the dask Tier B path. The cupy/RTX viewshed is a separate implementation and is out of scope here.

Test plan

  • Converted the xfail NaN test into passing tests.
  • NaN at every position around a 5x5 grid, including the observer row to the right.
  • Guard that skipped NaN cells don't leak visibility to cell (0, 0).
  • numpy and dask+numpy backends.
  • test_viewshed.py, test_visibility.py, test_min_observable_height.py all green.

No new public API, so the user-guide notebook and README feature-matrix steps are skipped. No docs change needed; the viewshed reference entry and signature are unchanged.

NaN cells used to generate ENTERING/CENTER/EXITING sweep events. A NaN on
the observer row to the right was never inserted into the status structure,
so its EXITING event made _delete_from_tree raise ValueError: node not found.

_init_event_list now skips NaN cells entirely and returns the event count so
the caller trims unused trailing rows (which would otherwise sort as CENTER
events at cell (0, 0) and spuriously mark it visible). NaN cells keep their
INVISIBLE fill value, which downstream != INVISIBLE checks treat as not
visible.

Convert the xfail regression at test_viewshed.py into passing tests covering
all NaN positions, the observer-row-right case, the (0, 0) leak guard, and
the numpy + dask backends.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label Jun 2, 2026

@brendancol brendancol left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

PR Review: Fix viewshed crash on NaN cells and mark them INVISIBLE (#2857)

Blockers (must fix before merge)

None.

Suggestions (should fix, not blocking)

None.

Nits (optional improvements)

  • The new if np.isnan(inrast[1][j]): continue reads inrast[1][j], the same value the line above stores into e[E_ELEV_1] (viewshed.py:1165). Reusing e[E_ELEV_1] for the guard would drop the second index, but reading inrast[1][j] directly next to the comment is clearer. Wash. Leave as-is.

What looks good

  • Root cause is right. NaN cells emitted EXITING events with no matching insert (the pre-insert loop at viewshed.py:1388 guards on not np.isnan), so _delete_from_tree raised. Skipping event creation for NaN cells removes the asymmetry.
  • The trailing-row trim (event_list = event_list[:count_event]) is load-bearing, not cosmetic. Without it the leftover all-zero rows sort as CENTER events at (0, 0) and would mark that cell visible. test_viewshed_nan_does_not_leak_visibility_to_origin covers that path.
  • Sentinel choice is correct. NaN cells stay at the INVISIBLE fill value, which is what visibility.py:287 and experimental/min_observable_height.py:171 test against. A NaN output would have read as visible; INVISIBLE does not.
  • Verified two more edge cases locally: a NaN observer cell and an all-NaN raster both return cleanly (observer 180, rest INVISIBLE) with no crash.
  • The dask Tier B path runs through _viewshed_cpu, so it picks up the fix. The dask+numpy test confirms it.
  • The Tier C out-of-core distance sweep already skips NaN in _sweep_ring (elev != elev), so it was never affected and the fix correctly leaves it alone.

Checklist

  • Algorithm matches reference (GRASS r.viewshed port; NaN cells are NODATA and excluded)
  • Implemented backends consistent (numpy + dask Tier B share the CPU sweep; cupy/RTX is a separate path, out of scope)
  • NaN handling correct
  • Edge cases covered by tests
  • Dask chunk boundaries handled (Tier B computes to one in-memory array)
  • No premature materialization or unnecessary copies
  • Benchmark not needed (bug fix, no perf change)
  • README feature matrix N/A (no new function)
  • Docstrings accurate (no API change)

@brendancol brendancol merged commit b24bf5e into main Jun 3, 2026
8 of 12 checks passed
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 crashes on NaN cell in observer row (ValueError: node not found)

1 participant