Validate barriers, search_radius, start/goal points, and dims in pathfinding#3664
Conversation
…#3649) String-typed barriers were silently ignored (numba compares float cells to unicode, always False), so a_star_search routed straight through walls the caller asked to block. Negative or float search_radius either returned a silent all-NaN "no path" or crashed deep in slicing code. multi_stop_search with mismatched dim names raised a bare KeyError 'y' where a_star_search raises ValueError, and scalar start/goal points died inside _get_pixel_id. Adds _validate_barriers, _validate_search_radius, _validate_point, and _validate_surface_dims helpers, wires them into both public functions, and improves the dims message to name the actual dims and the x=/y= parameters. Also records the sweep state-CSV row. Claude-Session: https://claude.ai/code/session_0155N4QGamQVxgpAAPbpQNq4
brendancol
left a comment
There was a problem hiding this comment.
PR Review: Validate barriers, search_radius, start/goal points, and dims in pathfinding
Blockers (must fix before merge)
None found. The silent-barrier repro from #3649 (wall of zeros, barriers=['0']) now raises TypeError on all four backends, and the previously-crashing inputs get errors that name the parameter.
Suggestions (should fix, not blocking)
-
multi_stop_searchforwardsbarriersunvalidated and relies on the firsta_star_searchsegment call to reject it (xrspatial/pathfinding.py:1553). The error still fires before any real work, but withoptimize_order=Truethe traceback points into_optimize_waypoint_orderinstead of the user's call. One_validate_barriers(barriers)line next to the other multi_stop checks would keep the failure at the API boundary.
Nits (optional improvements)
- The
a_star_searchdocstring still describessearch_radiusas plainint, optionalandbarriersasarray like object(xrspatial/pathfinding.py:925, 932); neither mentions the new constraints (non-negative integer; 1-D numeric). Same for theRaisessection ofmulti_stop_search(xrspatial/pathfinding.py:1479), which lists only ValueError while the barrier/radius type checks raise TypeError. -
_validate_barriersrejects boolean arrays (barriers=[True]) sincenp.bool_is neither integer nor floating. Almost certainly nobody does this, but it did compare successfully before. Worth a conscious decision rather than an accident. - proximity.py:1346 carries its own copy of the old malformed dims message ("raster.coords should be named as coordinates:"). Out of scope here; noting it so the wording fix can be replicated there in a follow-up.
What looks good
- The fix targets the failure at the dispatch boundary, so numpy, cupy, dask+numpy, and dask+cupy all get identical error behavior, and the PR states the battery was executed on all four.
- Exception types on already-raising paths are preserved; only the two silent-wrong-output cases start raising.
- Tests assert message content, not just exception type, and include positives (numeric barriers still block,
search_radius=0accepted, custom dims still work) so the new validation can't over-reject. - The scalar-barriers hint ("Did you mean barriers=[0]?") is the kind of message that saves a user a trip to the docs.
Checklist
- Algorithm matches reference/paper (no algorithm change; validation only)
- All implemented backends produce consistent results (checks run pre-dispatch)
- NaN handling is correct (untouched)
- Edge cases are covered by tests (scalar/string/3-element/negative/float inputs)
- Dask chunk boundaries handled correctly (untouched)
- No premature materialization or unnecessary copies (barriers copy is a handful of floats)
- Benchmark exists or is not needed (validation-only change; existing pathfinding benchmark unaffected)
- README feature matrix updated (n/a, no new function)
- Docstrings present and accurate (see nit on parameter constraint docs)
…t constraints (#3649) Claude-Session: https://claude.ai/code/session_0155N4QGamQVxgpAAPbpQNq4
brendancol
left a comment
There was a problem hiding this comment.
PR Review follow-up (after 0c8c517)
Disposition of the first-pass findings:
- Suggestion, barriers validated only inside the first segment call: fixed.
multi_stop_searchnow calls_validate_barriersnext to its other boundary checks (xrspatial/pathfinding.py:1496), with a test asserting the TypeError fires from the multi_stop call (test_string_barriers_raise_in_multi_stop). Re-validation ina_star_searchis idempotent on the coerced float64 array. - Nit, docstring constraints: fixed.
barriersandsearch_radiusparameter docs ina_star_searchstate the 1-D-numeric and non-negative-integer requirements, andmulti_stop_search's Raises section now lists the TypeError paths. - Nit, boolean barriers rejected: kept as-is, now with a code comment recording the decision.
[True]as a barrier list is more plausibly a bug than a request for "cells equal to 1". - Nit, proximity.py's copy of the old dims message: not touched. This sweep is scoped to the pathfinding module; the wording fix can ride along whenever proximity gets its pass.
72 tests pass locally including the cupy and dask+cupy lanes; flake8 output is byte-identical to main for both touched files. Nothing further from me.
…ling-pathfinding-2026-07-08 # Conflicts: # xrspatial/pathfinding.py # xrspatial/tests/test_pathfinding.py
brendancol
left a comment
There was a problem hiding this comment.
PR Review: Validate barriers, search_radius, start/goal points, and dims in pathfinding
Re-review after resolving the merge conflict with main. The branch was 4 commits
behind and conflicted with the sibling pathfinding PRs (#3653 API consistency, #3667
optimize_order, #3652/#3656 dask token, #3655/#3663 golden tests). Merged
origin/main at 9f448802; the net PR diff keeps its original scope (94 lines in
pathfinding.py, 123 in the tests, plus one state-CSV row).
Conflict resolution
- barriers docstring (
a_star_search): kept main's "Default is no barriers" note
alongside this branch's "must be a 1-D sequence of numeric values" note, in one
sentence. - barriers handling (
a_star_search, ~line 1043): #3653 changed the default from
[]toNoneand added anif barriers is None: barriers = []guard. Kept that
guard, then called_validate_barriers(barriers), which is how the merge had already
resolved the same pattern inmulti_stop_search. - test_pathfinding.py (~line 1463): both sides only add tests. Kept both this
branch'sTestPathfindingErrorHandlingclass and main's module-level API-consistency
tests. - The state CSV auto-merged to a single pathfinding row.
Integration check against #3653's new signatures
The main risk was validation still attaching to the pre-merge signatures. What I checked:
barriers: Optional[list] = Noneandsearch_radius: Optional[int] = Noneare the
new defaults._validate_search_radius(None)returns early, and_validate_barriers
runs after theNone -> []guard, so both defaults pass.- #3653 added Dataset routing through the
@supports_datasetdecorator on both
functions. That decorator unwraps a Dataset into per-variable DataArrays and calls the
wrapped body on each one, so_validate_surface_dims(which does
surface.dims != (y, x)) never sees a Dataset, where.dimsis a mapping rather than
a tuple. Dataset input is not falsely rejected. - main's new
test_barriers_default_none_matches_empty_listchecks thatbarriers=None,
barriers=[], and the default all agree. It passes under this branch's validation.
Tests
PYTHONPATH=<worktree> python -m pytest xrspatial/tests/test_pathfinding.py: 99 passed
in 3.53s. CUDA + cupy are present here, so the GPU and dask+cupy paths ran locally
rather than being skipped.
What looks good
- Validation sits at the API boundary, before backend dispatch, so all four backends
get the same errors. - Error messages name the offending parameter and value and point at the fix (the
scalar-barriers hint, the list-wrapping suggestion). - Rejecting bool barrier arrays on purpose is a defensible call, and it's commented.
No blockers. The resolution keeps both this branch's validation and the merged sibling
work.
Closes #3649
barriersbefore any kernel runs.barriers=['0']used to silently disable every barrier (numba compares float cells to unicode, always False) and route straight through walls;barriers=0died with a numba TypingError. Numeric values are now coerced to float64.search_radiusvalues that are not None or a non-negative integer. Negative radii used to return a silent all-NaN "no path" on numpy/cupy (a misleading "no path between waypoints" throughmulti_stop_search), or crash with "negative dimensions are not allowed" / "slice indices must be integers" depending on where start and goal sit.multi_stop_search(was a bareKeyError: 'y'from xarray internals) and rewrite the shared message to name the actual dims and thex=/y=parameters.start/goalare 2-element pairs ina_star_search, matching the existing waypoint check, and make that check survive scalar waypoints inmulti_stop_search.Backend coverage: the new checks run before backend dispatch, so behavior is identical on numpy, cupy, dask+numpy, and dask+cupy. Verified by executing the bad-input battery on all four (CUDA host).
Exception types on already-raising paths are unchanged. The two silent cases (string barriers, negative radius) now raise, which only affects calls that returned wrong output.
Test plan:
pytest xrspatial/tests/test_pathfinding.py(71 passed, includes cupy and dask+cupy on this host)TestPathfindingErrorHandlingclass: string/scalar barriers, negative/float/zero radius, scalar start/goal/waypoint, dims mismatch in both functions, custom dims still work, numeric barriers still blockState-CSV row for the error-handling sweep is included.
https://claude.ai/code/session_0155N4QGamQVxgpAAPbpQNq4