From 83476ddf89032760305b9a7ea2aef80403bc8328 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Wed, 8 Jul 2026 17:22:33 -0400 Subject: [PATCH 1/2] sweep-accuracy: pathfinding re-audit 2026-07-08 (#3646 HIGH, #3647 MEDIUM) Claude-Session: https://claude.ai/code/session_0155N4QGamQVxgpAAPbpQNq4 --- .claude/sweep-accuracy-state.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/sweep-accuracy-state.csv b/.claude/sweep-accuracy-state.csv index 2cee22dfc..5f6f509f2 100644 --- a/.claude/sweep-accuracy-state.csv +++ b/.claude/sweep-accuracy-state.csv @@ -24,7 +24,7 @@ mcda,2026-06-10,3146,MEDIUM,5,"Cat5 backend failures, all raise loudly (no wrong morphology,2026-04-30,"1397,1399",HIGH,2;5,HIGH fixed in #1397/PR #1398: morph_erode/dilate seeded centre cell into running min/max even when kernel[centre]==0 (all 4 backends). HIGH fixed in #1399/PR #1400: dask backends raised on 1xN/Nx1 kernels because empty-slice writeback (0:-0). multispectral,2026-03-30T14:00:00Z,1094,,, normalize,2026-05-01,,,,rescale and standardize across all 4 backends. NaN/inf filtered via isfinite mask before min/max/mean/std. Constant input handled (range=0 -> new_min; std=0 -> 0.0). Output dtype float64 consistently. Backend parity covered by test_matches_numpy. No accuracy issues found. -pathfinding,2026-07-03,3629;3630;3631,HIGH,1;3;5,"Cat1/3 HIGH+MEDIUM #3629: _get_pixel_id used int(abs(point-coord0)/cellsize) so out-of-bounds points on the coords[0] side folded to mirrored interior pixels (silent wrong path instead of ValueError) and truncation gave a half-cell floor bias plus fp flip at exact centers (0.3 on 0.1-res grid -> pixel 2); fixed with signed step + round-to-nearest-center. Cat5 HIGH+MEDIUM #3630: multi_stop_search crashed on dask+cupy (np.asarray(seg.values) hits cupy implicit-conversion TypeError) and returned numpy-backed output for cupy input while a_star_search preserves array type; note test_multi_stop_cupy_matches_numpy had a tautological conversion expression masking it (test-coverage sweep: no dask+cupy multi_stop test existed). Cat2/5 MEDIUM #3631: _hpa_star_search returned a partial finite cost trail when refinement failed (89 finite px on a wall-split 200x200 grid with unreachable goal) vs the all-NaN no-path contract elsewhere. Cat6 clean: a_star cost == scipy csgraph dijkstra on 8-connected 20x25 grid with anisotropic cells + random NaN barriers, friction and no-friction, delta 0.0; A->B==B->A symmetric; dask matches. Cat4: planar coordinate-unit distances (no haversine) is the library-wide convention, noted not flagged. CUDA available: cupy + dask+cupy paths executed (cupy is CPU-fallback by design, dask sparse A* verified vs numpy). HPA* suboptimality is inherent/documented, not flagged." +pathfinding,2026-07-08,3646;3647,HIGH,2,"Re-audit after 2026-07-03 sweep fixes (#3635/#3637/#3638) merged. Cat2 HIGH #3646: multi_stop_search(optimize_order=True) silently drops waypoints; _held_karp returns [start,end] when all tours are inf (best_last=-1), and _optimize_waypoint_order reads segment cost at the UNSNAPPED goal pixel so snap=True makes reachable waypoints inf-distance; repro: wall grid raises without optimize_order but returns finite 2-waypoint route with it; snap case drops a reachable waypoint. Cat2 MEDIUM #3647: _hpa_star_search routes the coarse grid with the caller's barriers but coarse cells are block MEANS, so a mean equal to a barrier value (e.g. -1/+1 data, barriers=[0]) falsely blocks passable regions -> all-NaN no-path; reproduced helper-level and end-to-end via auto-radius HPA* (600x600, mocked RAM); fix routes coarse grid with empty barriers (NaN coarse cells already encode impassable blocks). Cat6 clean: a_star cost == scipy csgraph dijkstra (8-conn 20x25, anisotropic cells, NaN barriers, friction and no-friction, delta 0.0). Cat1/3/4 clean; Cat5: all 57 pathfinding tests pass incl. cupy and dask+cupy (CUDA available, executed)." perlin,2026-04-10T12:00:00Z,,,,Improved Perlin noise implementation correct. Fade/gradient functions verified. Backend-consistent. Continuous at cell boundaries. polygon_clip,2026-06-10,3186,HIGH,5,"Cat5 backend inconsistency: dask+cupy clip_polygon rasterizes the mask with a uniform chunk size from the raster's first chunk, then feeds raster+mask to da.map_blocks (positional block pairing). Non-uniform raster chunks gave the mask a different block layout -> IndexError/ValueError (or silent mis-stamp). Repro (8,6) rechunk ((3,5),(6,)) on dask+cupy raised ValueError Shapes do not align; dask+numpy was fine via xarray.where rechunk. Fix #3186/PR: rechunk cond to raster.data.chunks[-2:] before map_blocks; added non-uniform regression tests for dask+numpy and dask+cupy. use_cuda->gpu migration in that branch was already landed by #3089/#3122. CUDA available; cupy+dask+cupy verified, 25 tests pass. Cats 1-4 clean: numpy path uses raster.where, cupy path operates on raw arrays, NaN inputs preserved, no neighborhood ops/curvature. Prior fix #1197/#1200 (crop+all_touched) merged and unrelated." polygonize,2026-05-29,2606,HIGH,5,"Cat 5 HIGH: dask connectivity=8 cross-chunk merge filled diagonal notch where same-value regions meet only at a corner across a chunk boundary; total area exceeded raster. Hole ring was dropped because containment tested hole[0] (on exterior at pinch). Fixed via _ring_interior_point in PR for #2606. numpy, dask+numpy, dask+cupy area parity now holds; 4-conn was already correct. cupy + dask+cupy paths validated on GPU host. Other cats clean: NaN masked on numpy/cupy float paths (tested), _is_close handles +/-inf via exact-equality short-circuit, atol/rtol/simplify_tolerance reject NaN/inf, integer GPU CCL matches numpy." From 5493e7a3140142761e0f16005452ddecee660aa9 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Wed, 8 Jul 2026 17:25:35 -0400 Subject: [PATCH 2/2] Route the HPA* coarse grid without value barriers (#3647) Coarse cell values are block means of the non-barrier fine cells, so testing them against the exact barrier values can falsely block a passable block whose values average to a barrier value (e.g. -1/+1 data with barriers=[0] gives every block mean 0.0), turning a fully passable grid into all-NaN "no path". _coarsen_surface already encodes fully-barrier blocks as NaN, so the coarse route now runs with an empty barrier list. Claude-Session: https://claude.ai/code/session_0155N4QGamQVxgpAAPbpQNq4 --- xrspatial/pathfinding.py | 8 +++++++- xrspatial/tests/test_pathfinding.py | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/xrspatial/pathfinding.py b/xrspatial/pathfinding.py index 3c1a4199c..f8eebe036 100644 --- a/xrspatial/pathfinding.py +++ b/xrspatial/pathfinding.py @@ -472,10 +472,16 @@ def _hpa_star_search(surface_data, friction_data, start_py, start_px, c_dy, c_dx, c_dd = _neighborhood_structure(coarse_cx, coarse_cy, 8) # --- Route on coarse grid --- + # Coarse cell values are block means of the non-barrier fine cells, + # so testing them against the exact barrier values is meaningless: a + # passable block whose values average to a barrier value would be + # falsely blocked. _coarsen_surface already encodes fully-barrier + # blocks as NaN, so route the coarse grid with no value barriers. + no_barriers = np.empty(0, dtype=barriers.dtype) coarse_path = np.full((ch, cw), np.nan, dtype=np.float64) _a_star_search(coarse_surface, coarse_path, cs_py, cs_px, cg_py, cg_px, - barriers, c_dy, c_dx, c_dd, + no_barriers, c_dy, c_dx, c_dd, coarse_friction, f_min, use_friction, coarse_cx, coarse_cy) diff --git a/xrspatial/tests/test_pathfinding.py b/xrspatial/tests/test_pathfinding.py index 0db74e791..3b1642a17 100644 --- a/xrspatial/tests/test_pathfinding.py +++ b/xrspatial/tests/test_pathfinding.py @@ -799,6 +799,31 @@ def test_hpa_star_unreachable_goal_all_nan(): assert not np.isfinite(path_img).any() +def test_hpa_star_barrier_valued_block_means(): + """Coarse routing must not re-apply value barriers to block means (#3647). + + Alternating -1/+1 columns make every coarse block mean exactly 0.0. + With barriers=[0] no fine cell is a barrier, but the coarse grid used + to be entirely blocked, returning all-NaN on a fully passable grid. + """ + H = W = 200 + data = np.full((H, W), 1.0) + data[:, ::2] = -1.0 + barriers = np.array([0.0]) + assert not np.any(data == 0.0) # every fine cell is passable + + dy, dx, dd = _neighborhood_structure(1.0, 1.0, 8) + + path_img = _hpa_star_search( + data, None, 0, 0, H - 1, W - 1, + barriers, dy, dx, dd, + 1.0, False, 1.0, 1.0, H, W) + + assert np.isfinite(path_img[0, 0]) + assert np.isfinite(path_img[H - 1, W - 1]) + assert path_img[H - 1, W - 1] > 0.0 + + def test_auto_radius_selection(): """When mocked low memory, auto-radius kicks in and finds path.""" data = np.ones((20, 20))