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-documentation-state.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module,last_inspected,issue,severity_max,categories_found,notes,doc_coverage
bump,2026-07-02,#3606,MEDIUM,2;5,"height_func Parameters description gave a per-point f(x,y) contract; actual call is height_func(locations) with a (count,2) array returning a length-count heights array (TypeError on literal use). count typed int with no default; actually Optional, defaults to min(w*h//10, 10_000_000). Docstring plot example executes clean (numpy). Fixed both in PR. LOW-only: no Raises section for MemoryError/ValueError (left documented). CUDA available; cupy/dask+cupy example paths n/a (single .. plot:: is numpy).",1/1
classify,2026-06-25,3506,MEDIUM,1;3,"Cat3: reclassify (numpy/dask/cupy blocks) + equal_interval example outputs were stale/wrong, binary used np.nan in array repr; corrected to actual output (tests confirm code is correct). Cat1: added missing Examples to std_mean, head_tail_breaks, percentiles, maximum_breaks, box_plot. Fixed in deep-sweep-documentation-classify-2026-06-25 (PR for #3506). Cat2 natural_breaks num_sample-None omission already tracked in #3501 (left alone). All 10 public funcs listed in reference/classification.rst (no Cat4 gap). CUDA available: ran numpy examples; cupy/dask reprs reviewed statically.",10/10
convolution,2026-07-02,,MEDIUM,1;3;5,"Deep-sweep 2026-07-02 (deep-sweep-documentation-convolution). Public API per reference/focal.rst 'Focal Statistics': convolution_2d, annulus_kernel, calc_cellsize, circle_kernel, custom_kernel (via xrspatial.focal); convolve_2d listed in metadata public_funcs but not in any reference page. Cat1 MEDIUM: custom_kernel had a one-line stub (added Parameters/Returns/Examples); convolve_2d had NO docstring (added numpydoc). Cat5 MEDIUM: convolution_2d agg backend line omitted Dask+CuPy (code dispatches _convolve_2d_dask_cupy) and had typos 'to processed'/'CuPybacked' -> fixed to name all 4 backends. Cat3 MEDIUM: annulus_kernel example print used comma-array format + stray trailing ')' (real print output has no commas/paren) -> corrected to measured output; convolution_2d dask .compute() example claimed dtype=float32 but np.ones input stays float64 -> removed label; cupy type repr 'cupy.core.core.ndarray' -> 'cupy.ndarray'. Cat3 LOW (fixed in review follow-up): calc_cellsize km example wrote output line as '>>> (1000.0, 1000.0)' (stray prompt on an output line) -> removed the prompt to match the other two output lines in the same example. Cat2/Cat4 clean: params match signatures; all 5 reference-listed funcs present, no stale/dup entries. Verified: numpy+dask+cupy examples executed (CUDA available on host, cupy example ran), values match; test_convolution.py 6 pass; flake8 unchanged (1 pre-existing F401 not_implemented_func baseline).",6/6
edge_detection,2026-07-18,3674,MEDIUM,1;5,"Cat1: all 5 public funcs lacked Examples sections (0 >>> blocks in module); added executed examples. Cat5: gradient docstrings said 'convolving with' kernel but convolve_2d cross-correlates; antisymmetric Sobel/Prewitt kernels flip sign (measured: sobel_x=+8 vs convolve=-8 on left-right ramp); reworded + sign convention + pinned test. Cat5 NaN-propagation gap found here too but fixed by sibling error-handling PR #3678 (Notes sections); scoped out to avoid conflicting edits. Cat2 clean (params/order/defaults match). Cat4 clean (all 5 in reference/focal.rst). Backend claim verified on numpy/cupy/dask+numpy/dask+cupy (CUDA host). LOW unfixed: no module-level docstring. Former LOW int-promotion doc gap resolved by security PR #3686 (wide ints now promote to float64, documented in Returns; merged into this branch).",5/5
fire,2026-06-25,,MEDIUM,1;5,"all 7 public funcs (dnbr, rdnbr, burn_severity_class, fireline_intensity, flame_length, rate_of_spread, kbdi) lacked Examples section (Cat1 MEDIUM) and backend-support note (Cat5 MEDIUM); fixed in deep-sweep-documentation-fire-2026-06-25-01; repo issues disabled so no issue number; examples run and outputs match numpy backend; all 7 listed in reference/fire.rst; no Cat2/Cat3/Cat4 issues",7/7
flood,2026-06-25,,HIGH,1;4;5,"Cat4 HIGH: vegetation_roughness, vegetation_curve_number, flood_depth_vegetation public but absent from reference/flood.rst; Cat1 MEDIUM: no Examples on any of 7 public funcs; Cat5 MEDIUM: backend support undocumented (all 4 backends) + NaN propagation undocumented for curve_number_runoff/travel_time. Fixed in deep-sweep-documentation-flood-2026-06-25: added 3 rst entries, Examples+backend Notes to all 7 funcs (examples executed OK on CUDA host), NaN notes. PR #3502 opened with the fix; gh issue create blocked by auto-mode classifier so no issue number.",7/7
geotiff,2026-07-02,,LOW,5,"Re-audit 2026-07-02 (deep-sweep-documentation-geotiff): prior #3592 PAM-sidecar doc fix confirmed merged (HEAD 8ecf14d9). Public surface = open_geotiff, to_geotiff (both re-exported at top level). Cat1 clean: both have full numpydoc Parameters/Returns/Notes/Examples (to_geotiff also Raises); module __doc__ present. Cat2 clean: programmatic inspect.signature vs Parameters diff = 0 for both (28/28 open_geotiff, 24/24 to_geotiff params; no phantom params, no default drift -- sentinel defaults for deprecated aliases documented as such). Cat3 clean: doctest examples are copy-paste-safe +SKIP blocks, pytest --doctest-modules on __init__.py + eager.py = 2 passed 0 fail; rst examples are non-executed code-block:: python. Cat4 clean: both funcs in reference/geotiff.rst autosummary, no dupes/stale entries; SUPPORTED_FEATURES + all referenced error classes (MixedBandMetadataError, PixelSafetyLimitError, CloudSizeLimitError, VRTStableSourcesOnlyError) import OK. Cat5 clean: Returns backend claim (NumPy/Dask/CuPy/Dask+CuPy) matches gpu/chunks dispatch. LOW (not fixed, no /rockout per MEDIUM+ gate): 8 docstrings incl. both public funcs cite docs/source/reference/geotiff_release_contract.rst but the page exists only as .md; open_geotiff hedges 'once that page lands' (intentional fwd-ref), to_geotiff/_writers/_backends cite it plainly. Literal code-span, not a Sphinx xref, so no build break. CUDA available on host.",2/2
Expand Down
114 changes: 110 additions & 4 deletions xrspatial/edge_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ def _promote_wide_int(data):
def sobel_x(agg, name='sobel_x', boundary='nan'):
"""Compute the horizontal gradient of a raster using the Sobel operator.

Detects vertical edges by convolving with the Sobel-X kernel::
Detects vertical edges by cross-correlating with the Sobel-X kernel::

[[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]

This matches ``scipy.ndimage.correlate``: the response is positive
where values increase toward higher column index.

Parameters
----------
agg : xarray.DataArray
Expand All @@ -71,6 +74,25 @@ def sobel_x(agg, name='sobel_x', boundary='nan'):
integers as float32, 32/64-bit integers as float64 so that
large values keep unit precision (exact up to 2**53 for
64-bit integers).

Examples
--------
.. sourcecode:: python

>>> import numpy as np
>>> import xarray as xr
>>> from xrspatial import sobel_x
>>> data = np.array([
... [0., 1., 2., 3.],
... [0., 1., 2., 3.],
... [0., 1., 2., 3.],
... [0., 1., 2., 3.]])
>>> raster = xr.DataArray(data, dims=['y', 'x'])
>>> sobel_x(raster).data
array([[nan, nan, nan, nan],
[nan, 8., 8., nan],
[nan, 8., 8., nan],
[nan, nan, nan, nan]])
"""
_validate_raster(agg, func_name='sobel_x', name='agg')
out = convolve_2d(_promote_wide_int(agg.data), SOBEL_X, boundary)
Expand All @@ -81,12 +103,15 @@ def sobel_x(agg, name='sobel_x', boundary='nan'):
def sobel_y(agg, name='sobel_y', boundary='nan'):
"""Compute the vertical gradient of a raster using the Sobel operator.

Detects horizontal edges by convolving with the Sobel-Y kernel::
Detects horizontal edges by cross-correlating with the Sobel-Y kernel::

[[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]]

This matches ``scipy.ndimage.correlate``: the response is positive
where values increase toward higher row index.

Parameters
----------
agg : xarray.DataArray
Expand All @@ -104,6 +129,25 @@ def sobel_y(agg, name='sobel_y', boundary='nan'):
integers as float32, 32/64-bit integers as float64 so that
large values keep unit precision (exact up to 2**53 for
64-bit integers).

Examples
--------
.. sourcecode:: python

>>> import numpy as np
>>> import xarray as xr
>>> from xrspatial import sobel_y
>>> data = np.array([
... [0., 0., 0., 0.],
... [1., 1., 1., 1.],
... [2., 2., 2., 2.],
... [3., 3., 3., 3.]])
>>> raster = xr.DataArray(data, dims=['y', 'x'])
>>> sobel_y(raster).data
array([[nan, nan, nan, nan],
[nan, 8., 8., nan],
[nan, 8., 8., nan],
[nan, nan, nan, nan]])
"""
_validate_raster(agg, func_name='sobel_y', name='agg')
out = convolve_2d(_promote_wide_int(agg.data), SOBEL_Y, boundary)
Expand All @@ -120,6 +164,8 @@ def laplacian(agg, name='laplacian', boundary='nan'):
[ 1, -4, 1],
[ 0, 1, 0]]

The kernel is symmetric, so cross-correlation and convolution agree.

Parameters
----------
agg : xarray.DataArray
Expand All @@ -137,6 +183,22 @@ def laplacian(agg, name='laplacian', boundary='nan'):
integers as float32, 32/64-bit integers as float64 so that
large values keep unit precision (exact up to 2**53 for
64-bit integers).

Examples
--------
.. sourcecode:: python

>>> import numpy as np
>>> import xarray as xr
>>> from xrspatial import laplacian
>>> data = np.zeros((4, 4))
>>> data[1, 1] = 1.
>>> raster = xr.DataArray(data, dims=['y', 'x'])
>>> laplacian(raster).data
array([[nan, nan, nan, nan],
[nan, -4., 1., nan],
[nan, 1., 0., nan],
[nan, nan, nan, nan]])
"""
_validate_raster(agg, func_name='laplacian', name='agg')
out = convolve_2d(_promote_wide_int(agg.data), LAPLACIAN_KERNEL, boundary)
Expand All @@ -147,12 +209,15 @@ def laplacian(agg, name='laplacian', boundary='nan'):
def prewitt_x(agg, name='prewitt_x', boundary='nan'):
"""Compute the horizontal gradient of a raster using the Prewitt operator.

Detects vertical edges by convolving with the Prewitt-X kernel::
Detects vertical edges by cross-correlating with the Prewitt-X kernel::

[[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]]

This matches ``scipy.ndimage.correlate``: the response is positive
where values increase toward higher column index.

Parameters
----------
agg : xarray.DataArray
Expand All @@ -170,6 +235,25 @@ def prewitt_x(agg, name='prewitt_x', boundary='nan'):
integers as float32, 32/64-bit integers as float64 so that
large values keep unit precision (exact up to 2**53 for
64-bit integers).

Examples
--------
.. sourcecode:: python

>>> import numpy as np
>>> import xarray as xr
>>> from xrspatial import prewitt_x
>>> data = np.array([
... [0., 1., 2., 3.],
... [0., 1., 2., 3.],
... [0., 1., 2., 3.],
... [0., 1., 2., 3.]])
>>> raster = xr.DataArray(data, dims=['y', 'x'])
>>> prewitt_x(raster).data
array([[nan, nan, nan, nan],
[nan, 6., 6., nan],
[nan, 6., 6., nan],
[nan, nan, nan, nan]])
"""
_validate_raster(agg, func_name='prewitt_x', name='agg')
out = convolve_2d(_promote_wide_int(agg.data), PREWITT_X, boundary)
Expand All @@ -180,12 +264,15 @@ def prewitt_x(agg, name='prewitt_x', boundary='nan'):
def prewitt_y(agg, name='prewitt_y', boundary='nan'):
"""Compute the vertical gradient of a raster using the Prewitt operator.

Detects horizontal edges by convolving with the Prewitt-Y kernel::
Detects horizontal edges by cross-correlating with the Prewitt-Y kernel::

[[-1, -1, -1],
[ 0, 0, 0],
[ 1, 1, 1]]

This matches ``scipy.ndimage.correlate``: the response is positive
where values increase toward higher row index.

Parameters
----------
agg : xarray.DataArray
Expand All @@ -203,6 +290,25 @@ def prewitt_y(agg, name='prewitt_y', boundary='nan'):
integers as float32, 32/64-bit integers as float64 so that
large values keep unit precision (exact up to 2**53 for
64-bit integers).

Examples
--------
.. sourcecode:: python

>>> import numpy as np
>>> import xarray as xr
>>> from xrspatial import prewitt_y
>>> data = np.array([
... [0., 0., 0., 0.],
... [1., 1., 1., 1.],
... [2., 2., 2., 2.],
... [3., 3., 3., 3.]])
>>> raster = xr.DataArray(data, dims=['y', 'x'])
>>> prewitt_y(raster).data
array([[nan, nan, nan, nan],
[nan, 6., 6., nan],
[nan, 6., 6., nan],
[nan, nan, nan, nan]])
"""
_validate_raster(agg, func_name='prewitt_y', name='agg')
out = convolve_2d(_promote_wide_int(agg.data), PREWITT_Y, boundary)
Expand Down
46 changes: 46 additions & 0 deletions xrspatial/tests/test_edge_detection.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import inspect
import re

import numpy as np
import pytest
import xarray as xr
Expand Down Expand Up @@ -241,6 +244,49 @@ def test_boundary_modes(self, func):
assert_boundary_mode_correctness(np_agg, da_agg, func)


# ---------------------------------------------------------------------------
# Docstring contract
# ---------------------------------------------------------------------------

class TestDocstrings:
@pytest.mark.parametrize('func', [sobel_x, sobel_y, laplacian, prewitt_x, prewitt_y])
def test_docstring_params_match_signature(self, func):
# Every parameter documented in the "Parameters" section must exist
# in the signature (and vice versa, in the same order).
sig_params = list(inspect.signature(func).parameters)
doc = inspect.getdoc(func)
lines = doc.splitlines()
start = next(i for i, ln in enumerate(lines) if ln.strip() == 'Parameters')
documented = []
for ln in lines[start + 2:]:
if ln.strip() in ('Returns', 'References', 'Notes', 'Examples'):
break
m = re.match(r'^(\w+)\s*:', ln)
if m:
documented.append(m.group(1))
assert documented == sig_params, (
f'{func.__name__}: documented params {documented} != '
f'signature params {sig_params}'
)

@pytest.mark.parametrize('func', [sobel_x, sobel_y, laplacian, prewitt_x, prewitt_y])
def test_docstring_has_examples_section(self, func):
doc = inspect.getdoc(func)
assert any(ln.strip() == 'Examples' for ln in doc.splitlines()), (
f'{func.__name__} docstring has no Examples section'
)

@pytest.mark.parametrize('func', [sobel_x, sobel_y, prewitt_x, prewitt_y])
def test_docstring_states_cross_correlation(self, func):
# convolve_2d applies kernels by cross-correlation, which for the
# antisymmetric Sobel/Prewitt kernels differs from convolution by
# sign. The docstrings used to say "convolving", which predicts the
# negated result. Pin the wording so it does not come back.
doc = inspect.getdoc(func)
assert 'cross-correlat' in doc
assert 'by convolving' not in doc


# ---------------------------------------------------------------------------
# Dask backend
# ---------------------------------------------------------------------------
Expand Down
Loading