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
2 changes: 2 additions & 0 deletions .claude/sweep-reference-validation-state.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
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.
8 changes: 8 additions & 0 deletions xrspatial/convolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,14 @@ def convolution_2d(agg, kernel, name='convolution_2d', boundary='nan'):
images by eliminating spurious data or enhancing features in the
data. Note that edges of output data array are filled with NaNs.

The kernel is applied by cross-correlation: it is overlaid on each
neighbourhood as given, without being flipped, which matches
``scipy.ndimage.correlate``. This equals a true convolution only when
the kernel is symmetric, as the built-in ``circle_kernel`` and
``annulus_kernel`` are. For an asymmetric kernel where you need
``scipy.ndimage.convolve`` semantics, pass the kernel pre-flipped
(``kernel[::-1, ::-1]``).

Parameters
----------
agg : xarray.DataArray
Expand Down
22 changes: 22 additions & 0 deletions xrspatial/tests/test_convolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ def test_convolve_2d_accepts_float64():
# Centre cell is finite; edges are NaN by default boundary mode.
assert np.isfinite(out[2, 2])
assert np.isnan(out[0, 0])


def test_convolve_2d_uses_correlation_convention():
# Golden values verified against scipy.ndimage 1.16.1: convolve_2d
# applies the kernel by cross-correlation (kernel NOT flipped), so it
# matches scipy.ndimage.correlate, not scipy.ndimage.convolve. An
# asymmetric kernel distinguishes the two. This pins the documented
# convention so a rename/refactor cannot silently flip the kernel.
data = np.arange(24, dtype=np.float64).reshape(4, 6)
kernel = custom_kernel(np.array([[1., 0., 0.],
[1., 1., 0.],
[1., 0., 0.]]))
out = convolve_2d(data, kernel)
# scipy.ndimage.correlate(data, kernel)[1:-1, 1:-1]
expected_correlate = np.array([[25., 29., 33., 37.],
[49., 53., 57., 61.]])
np.testing.assert_array_equal(out[1:-1, 1:-1], expected_correlate)
# A true convolution (scipy.ndimage.convolve, kernel flipped) would
# instead give these values; convolve_2d must NOT match them.
expected_convolve = np.array([[31., 35., 39., 43.],
[55., 59., 63., 67.]])
assert not np.array_equal(out[1:-1, 1:-1], expected_convolve)
Loading