Skip to content
Open
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
1 change: 1 addition & 0 deletions .claude/sweep-reference-validation-state.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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.
edge_detection,2026-07-18,3677,,MATCHES,"1e-6 rel (observed ~1e-13 interior, bit-exact vs ndi.correlate)",scipy 1.16.1; sobel_x/sobel_y/prewitt_x/prewitt_y/laplacian interior match ndi.sobel/prewitt/laplace and ndi.correlate with same stencils; boundary nan/nearest/reflect/wrap == scipy modes nearest/reflect/wrap incl. edges; tilted plane analytic exact (sobel=8g prewitt=6g laplacian=0); coords/cellsize independent as documented; cupy+dask+numpy+dask+cupy bit-identical to numpy (CUDA run); golden-value test added via #3677
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"
77 changes: 77 additions & 0 deletions xrspatial/tests/test_edge_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,83 @@ def test_prewitt_y_on_ramp(self, numpy_agg):
np.testing.assert_allclose(interior, 0, atol=1e-6)


# ---------------------------------------------------------------------------
# Golden values pinned against scipy.ndimage
# ---------------------------------------------------------------------------

class TestGoldenScipyParity:
"""Pin parity with scipy.ndimage using hardcoded reference outputs.

Expected arrays were generated with scipy.ndimage 1.16.1:
``ndi.sobel(data, axis=1)``, ``ndi.sobel(data, axis=0)``,
``ndi.prewitt(data, axis=1)``, ``ndi.prewitt(data, axis=0)`` and
``ndi.laplace(data)``, all with scipy's default ``mode='reflect'``,
which matches xrspatial's ``boundary='reflect'``. The values are
integer-exact for this input, so comparisons use equality.
"""

DATA = np.array([[3, 1, 4, 1, 5, 9],
[2, 6, 5, 3, 5, 8],
[9, 7, 9, 3, 2, 3],
[8, 4, 6, 2, 6, 4],
[3, 3, 8, 3, 2, 7]], dtype=np.float64)

EXPECTED = {
'sobel_x': np.array([[-2, 6, -3, 3, 29, 15],
[4, 7, -10, -6, 18, 11],
[-4, 1, -13, -14, 7, 3],
[-10, 1, -8, -13, 8, 2],
[-4, 13, -2, -18, 14, 13]], dtype=np.float64),
'sobel_y': np.array([[2, 10, 9, 5, 1, -3],
[24, 23, 18, 6, -10, -21],
[16, 3, -1, 0, -3, -11],
[-22, -15, -6, -1, 4, 12],
[-16, -5, 4, 0, -4, 5]], dtype=np.float64),
'prewitt_x': np.array([[0, 5, -3, 2, 21, 11],
[0, 4, -7, -6, 13, 8],
[-2, 1, -9, -7, 7, 2],
[-6, 3, -6, -13, 6, 4],
[-4, 8, -2, -12, 10, 8]], dtype=np.float64),
'prewitt_y': np.array([[3, 5, 8, 3, 1, -2],
[18, 17, 13, 4, -7, -15],
[10, 5, -2, 1, -4, -7],
[-16, -11, -5, -1, 4, 8],
[-11, -4, 2, -1, 0, 2]], dtype=np.float64),
'laplacian': np.array([[-3, 10, -5, 9, 0, -5],
[12, -9, 2, 2, -2, -7],
[-10, 0, -15, 4, 9, 5],
[-8, 8, -1, 10, -14, 4],
[5, 6, -12, 3, 10, -8]], dtype=np.float64),
}

@pytest.mark.parametrize('func,key', [
(sobel_x, 'sobel_x'),
(sobel_y, 'sobel_y'),
(prewitt_x, 'prewitt_x'),
(prewitt_y, 'prewitt_y'),
(laplacian, 'laplacian'),
])
def test_golden_reflect_full_array(self, func, key):
agg = create_test_raster(self.DATA, backend='numpy')
result = func(agg, boundary='reflect')
np.testing.assert_array_equal(result.data, self.EXPECTED[key])

@pytest.mark.parametrize('func,key', [
(sobel_x, 'sobel_x'),
(sobel_y, 'sobel_y'),
(prewitt_x, 'prewitt_x'),
(prewitt_y, 'prewitt_y'),
(laplacian, 'laplacian'),
])
def test_golden_nan_interior(self, func, key):
# default boundary='nan': interior pixels are unaffected by the
# boundary mode, so they must equal the reference interior
agg = create_test_raster(self.DATA, backend='numpy')
result = func(agg)
np.testing.assert_array_equal(
result.data[1:-1, 1:-1], self.EXPECTED[key][1:-1, 1:-1])


# ---------------------------------------------------------------------------
# Output metadata / backend checks
# ---------------------------------------------------------------------------
Expand Down
Loading