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
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"
153 changes: 153 additions & 0 deletions xrspatial/tests/test_pathfinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,159 @@ 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):
# Deliberately NOT _make_raster: that helper stores attrs['res'] with
# the y spacing first, while get_dataarray_resolution reads attrs['res']
# as (cellsize_x, cellsize_y) -- with anisotropic cells that would flip
# cx and cy. No 'res' attr here, so resolution comes from the coords.
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]))


# (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
pytest.param(8, (0, 0), (5, 5), 212.13203435596427, id='open-8conn'),
pytest.param(4, (0, 0), (5, 5), 300.0, id='open-4conn'),
]

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

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

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


@pytest.mark.parametrize("backend", _backends)
@pytest.mark.parametrize("connectivity,start_px,goal_px,expected",
_GOLDEN_OPEN_GRID)
def test_golden_open_grid(backend, 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("start_px,goal_px,expected", _GOLDEN_MAZE)
def test_golden_barrier_maze(backend, 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("start_px,goal_px,expected", _GOLDEN_FRICTION)
def test_golden_friction(backend, 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("connectivity,start_px,goal_px,expected",
_GOLDEN_ANISO)
def test_golden_anisotropic_cellsize(backend, 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