Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 4 additions & 3 deletions .claude/sweep-reference-validation-state.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module,last_inspected,issue,severity_max,verdict,tolerance,notes
convolution,2026-07-02,3619,MEDIUM,CONVENTION-DIFF,0.0 exact (float64),scipy 1.16.1; gdal-unavailable astropy-unavailable; cuda True. convolve_2d interior MATCHES scipy.ndimage.correlate exactly (0.0) across nan/nearest/reflect/wrap; cupy parity 0.0. circle_kernel/annulus_kernel match analytic int-truncated disc exactly; calc_cellsize correct. CONVENTION-DIFF: named 'convolution' but computes cross-correlation (kernel not flipped) -> diverges from scipy.ndimage.convolve on asymmetric kernels (built-in kernels are symmetric so common path unaffected). Convention was undocumented -> MEDIUM doc fix + golden test pinning correlation.
geotiff,2026-07-02,,,MATCHES,rtol=1e-6 float64; float32 codecs exact,"rasterio 1.4.4 (GDAL 3.10.3); gdal-cli 3.11.4; tifffile 2026.3.3; richdem-unavailable; osgeo-python-unavailable. Reader/writer round-trip vs rasterio/GDAL: codecs zstd/deflate/lzw/packbits/none + predictor 2&3 dmax=0; CRS EPSG:32611 & 4326 round-trip exact; GeoTransform north-up, non-square px, tiepoint & ModelTransformation all correct; nodata float-NaN->sentinel + int16/uint8/int32/uint16 sentinels exact; BigTIFF magic+data ok; windowed reads match rasterio.Window incl coord alignment; multiband round-trips (band-last is xrspatial's documented convention); COG valid per 'rio cogeo validate'; overview mean matches numpy block-mean to float32 ulp; tifffile tag check: Compression/Predictor/SampleFormat/GeoKeyDir all correct. Existing repo parity suite parity/test_reference.py 31 passed (incl dask+cupy). No divergence; golden_corpus already pins parity."
module,last_inspected,issue,severity_max,verdict,tolerance,notes
convolution,2026-07-02,3619,MEDIUM,CONVENTION-DIFF,0.0 exact (float64),scipy 1.16.1; gdal-unavailable astropy-unavailable; cuda True. convolve_2d interior MATCHES scipy.ndimage.correlate exactly (0.0) across nan/nearest/reflect/wrap; cupy parity 0.0. circle_kernel/annulus_kernel match analytic int-truncated disc exactly; calc_cellsize correct. CONVENTION-DIFF: named 'convolution' but computes cross-correlation (kernel not flipped) -> diverges from scipy.ndimage.convolve on asymmetric kernels (built-in kernels are symmetric so common path unaffected). Convention was undocumented -> MEDIUM doc fix + golden test pinning correlation.
geotiff,2026-07-02,,,MATCHES,rtol=1e-6 float64; float32 codecs exact,"rasterio 1.4.4 (GDAL 3.10.3); gdal-cli 3.11.4; tifffile 2026.3.3; richdem-unavailable; osgeo-python-unavailable. Reader/writer round-trip vs rasterio/GDAL: codecs zstd/deflate/lzw/packbits/none + predictor 2&3 dmax=0; CRS EPSG:32611 & 4326 round-trip exact; GeoTransform north-up, non-square px, tiepoint & ModelTransformation all correct; nodata float-NaN->sentinel + int16/uint8/int32/uint16 sentinels exact; BigTIFF magic+data ok; windowed reads match rasterio.Window incl coord alignment; multiband round-trips (band-last is xrspatial's documented convention); COG valid per 'rio cogeo validate'; overview mean matches numpy block-mean to float32 ulp; tifffile tag check: Compression/Predictor/SampleFormat/GeoKeyDir all correct. Existing repo parity suite parity/test_reference.py 31 passed (incl dask+cupy). No divergence; golden_corpus already pins parity."
pathfinding,2026-07-08,3655,,MATCHES,1e-12 rel (observed <=2e-15),"a_star_search vs scipy 1.16.1 sparse.csgraph.dijkstra + skimage 0.26.0 MCP_Geometric + analytic open-grid truth; 64x64 open/maze/friction/NaN-friction/anisotropic inputs, 4+8 conn; path chains adjacency+increment consistent; backend parity numpy/dask+numpy/cupy/dask+cupy confirmed (CUDA host); no prior golden test -> added golden-value tests (#3655); gdal/richdem/wbt not applicable to pathfinding"
148 changes: 148 additions & 0 deletions xrspatial/tests/test_pathfinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,154 @@ def test_multi_stop_dask_cupy_matches_numpy():
assert np.isfinite(computed_opt.get()).any()


# =====================================================================
# Issue #3655: golden-value reference regression tests
# =====================================================================
#
# Expected costs below were generated on 2026-07-08 and verified to
# machine precision (rel diff <= 2e-15) against two independent
# reference implementations with the same edge model:
# - scipy 1.16.1 scipy.sparse.csgraph.dijkstra on an explicit grid
# graph (edge weight = geometric distance, or
# distance * mean endpoint friction)
# - scikit-image 0.26.0 skimage.graph.MCP_Geometric with
# sampling=(cellsize_y, cellsize_x)
# plus the closed-form value where one exists (open uniform grids).
# The tests hardcode the verified goal costs so this parity cannot
# silently regress; neither reference library is needed at test time.

def _golden_maze_8x8():
# np.random.RandomState(42).rand(8, 8) < 0.3 -> 0.0 (barrier value),
# with (0,0), (7,0), (7,7) forced passable. Written out literally so
# the test does not depend on RandomState reproducibility.
return np.array([
[1, 1, 1, 1, 0, 0, 0, 1],
[1, 1, 0, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 1, 0, 0, 1],
[1, 1, 0, 1, 1, 0, 1, 0],
[0, 1, 1, 1, 1, 0, 1, 1],
[0, 1, 0, 1, 0, 1, 1, 1],
[1, 0, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 1, 0, 1, 1],
], dtype=np.float64)


def _golden_friction_6x6():
# np.round(0.5 + 4.5 * np.random.RandomState(7).rand(6, 6), 2),
# written out literally.
return np.array([
[0.84, 4.01, 2.47, 3.76, 4.90, 2.92],
[2.76, 0.82, 1.71, 2.75, 3.56, 4.12],
[2.21, 0.80, 1.80, 4.59, 1.46, 2.53],
[4.69, 0.61, 3.20, 4.78, 1.54, 2.97],
[4.59, 1.10, 2.86, 3.88, 3.51, 2.60],
[1.42, 2.71, 2.18, 2.65, 2.15, 4.27],
])


def _golden_raster(data, cx=30.0, cy=30.0):
import xarray as xr
h, w = data.shape
r = xr.DataArray(data.astype(np.float64), dims=['y', 'x'])
r['y'] = np.linspace((h - 1) * cy, 0, h) # descending, like GeoTIFFs
r['x'] = np.linspace(0, (w - 1) * cx, w)
return r


def _pixel_coords(raster, py, px):
return (float(raster['y'].values[py]), float(raster['x'].values[px]))


# (case id, connectivity, start px, goal px, expected goal cost)
_GOLDEN_OPEN_GRID = [
# 6x6 open grid, 30 m square cells; closed form:
# 5 diagonals of sqrt(2)*30, or 10 cardinals of 30
('open-8conn', 8, (0, 0), (5, 5), 212.13203435596427),
('open-4conn', 4, (0, 0), (5, 5), 300.0),
]

_GOLDEN_MAZE = [
('maze-corner', (0, 0), (7, 7), 296.98484809834997),
('maze-mid', (7, 0), (4, 4), 174.8528137423857),
('maze-no-path', (7, 0), (0, 7), np.nan),
]

_GOLDEN_FRICTION = [
('friction-diag', (0, 0), (5, 5), 416.6432249718464),
('friction-anti-diag', (5, 0), (0, 5), 452.5139715437149),
]

_GOLDEN_ANISO = [
# cx=30, cy=10: diagonal = sqrt(30^2 + 10^2)
('aniso-8conn', 8, (0, 0), (5, 5), 158.11388300841895),
('aniso-4conn', 4, (0, 3), (5, 0), 140.0),
]


@pytest.mark.parametrize("backend", _backends)
@pytest.mark.parametrize("case,connectivity,start_px,goal_px,expected",
_GOLDEN_OPEN_GRID)
def test_golden_open_grid(backend, case, connectivity, start_px, goal_px,
expected):
agg = _golden_raster(np.ones((6, 6)))
if backend == 'dask+numpy':
agg.data = da.from_array(agg.data, chunks=(3, 3))
start = _pixel_coords(agg, *start_px)
goal = _pixel_coords(agg, *goal_px)
path = a_star_search(agg, start, goal, connectivity=connectivity)
goal_cost = float(np.asarray(path.values)[goal_px[0], goal_px[1]])
np.testing.assert_allclose(goal_cost, expected, rtol=1e-12)


@pytest.mark.parametrize("backend", _backends)
@pytest.mark.parametrize("case,start_px,goal_px,expected", _GOLDEN_MAZE)
def test_golden_barrier_maze(backend, case, start_px, goal_px, expected):
agg = _golden_raster(_golden_maze_8x8())
if backend == 'dask+numpy':
agg.data = da.from_array(agg.data, chunks=(4, 4))
start = _pixel_coords(agg, *start_px)
goal = _pixel_coords(agg, *goal_px)
path = a_star_search(agg, start, goal, barriers=[0])
vals = np.asarray(path.values)
goal_cost = float(vals[goal_px[0], goal_px[1]])
if np.isnan(expected):
# no path: the whole output is NaN
assert not np.isfinite(vals).any()
else:
np.testing.assert_allclose(goal_cost, expected, rtol=1e-12)
assert float(vals[start_px[0], start_px[1]]) == 0.0


@pytest.mark.parametrize("backend", _backends)
@pytest.mark.parametrize("case,start_px,goal_px,expected", _GOLDEN_FRICTION)
def test_golden_friction(backend, case, start_px, goal_px, expected):
agg = _golden_raster(np.ones((6, 6)))
friction = _golden_raster(_golden_friction_6x6())
if backend == 'dask+numpy':
agg.data = da.from_array(agg.data, chunks=(3, 3))
friction.data = da.from_array(friction.data, chunks=(3, 3))
start = _pixel_coords(agg, *start_px)
goal = _pixel_coords(agg, *goal_px)
path = a_star_search(agg, start, goal, friction=friction)
goal_cost = float(np.asarray(path.values)[goal_px[0], goal_px[1]])
np.testing.assert_allclose(goal_cost, expected, rtol=1e-12)


@pytest.mark.parametrize("backend", _backends)
@pytest.mark.parametrize("case,connectivity,start_px,goal_px,expected",
_GOLDEN_ANISO)
def test_golden_anisotropic_cellsize(backend, case, connectivity, start_px,
goal_px, expected):
agg = _golden_raster(np.ones((6, 6)), cx=30.0, cy=10.0)
if backend == 'dask+numpy':
agg.data = da.from_array(agg.data, chunks=(3, 3))
start = _pixel_coords(agg, *start_px)
goal = _pixel_coords(agg, *goal_px)
path = a_star_search(agg, start, goal, connectivity=connectivity)
goal_cost = float(np.asarray(path.values)[goal_px[0], goal_px[1]])
np.testing.assert_allclose(goal_cost, expected, rtol=1e-12)


# =====================================================================
# Issue #1439: input validation
# =====================================================================
Expand Down
Loading