Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/sweep-accuracy-state.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
8 changes: 7 additions & 1 deletion xrspatial/pathfinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
25 changes: 25 additions & 0 deletions xrspatial/tests/test_pathfinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading