Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -7,5 +7,6 @@ flood,2026-06-25,,HIGH,1;4;5,"Cat4 HIGH: vegetation_roughness, vegetation_curve_
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
interpolate-idw,2026-06-26,,MEDIUM,1;5,"Cat1 MEDIUM: idw (only public func in _idw.py) had Parameters/Returns/Raises but no Examples section. Cat5 MEDIUM: backend support undocumented (numpy/cupy/dask+numpy/dask+cupy via ArrayTypeFunctionMapping) + k-nearest GPU rejection (NotImplementedError) and NaN/inf input-point dropping undocumented. Doc-only fix on deep-sweep-documentation-interpolate-idw-2026-06-26: added backend-support line, Examples block (executed, output matches), Returns dtype/NaN/fill_value notes. Cat2 clean (all 9 params documented, defaults/types match signature). Cat3 n/a (no prior examples). Cat4 clean (idw listed in reference/interpolation.rst autosummary). CUDA available: ran numpy example; cupy/dask paths covered by test_interpolation.py (92 pass). repo issues disabled so no issue number.",1/1
mahalanobis,2026-06-30,3579,MEDIUM,1;2;5,"Cat1 MEDIUM: mahalanobis (only public func) had Parameters/Returns but no Examples section (peers normalize.rescale/standardize do). Cat5 MEDIUM: NaN propagation (any non-finite band -> NaN pixel) and auto-stats N+1 all-finite requirement undocumented. Cat2 LOW: name param default not noted. Doc-only fix on deep-sweep-documentation-mahalanobis-2026-06-30: added Notes (NaN+backends) and a runnable Examples block (executed, output matches incl. NaN), set name default='mahalanobis'. Returns float64/shape and 4-backend claim verified against ArrayTypeFunctionMapping (accurate, left as-is). Cat3 n/a (no prior examples). Cat4 clean (listed in reference/utilities.rst). issue #3579. CUDA available: ran numpy example; cupy/dask covered by test_mahalanobis.py (24 pass).",1/1
pathfinding,2026-07-08,3651,MEDIUM,1;3;5,"Cat1 MEDIUM: multi_stop_search had Parameters/Returns/Raises but no Examples section (a_star_search has 13 example lines); added runnable example pinning waypoint_order/segment_costs/total_cost. Cat3 MEDIUM: a_star_search Examples used '... sourcecode:: python' (three dots, only occurrence in codebase) so the rendered page shows literal text instead of a code block; fixed to '..'; example itself executes clean. Cat5 MEDIUM: NaN surface cells are always impassable (_is_not_crossable, pinned by input_data_with_nans tests) but neither docstring said so (friction NaN rule was documented); documented in summary + barriers entries of both funcs. LOW fixed in passing: 'y: str' missing numpydoc space. Deferred to #3644 (api-consistency sibling, same day): 'values to bin' surface description + empty connectivity description. Clean: params match both signatures (grouped-entry aware check), both funcs in reference/pathfinding.rst, backend table verified by running all 4 backends (CUDA available; dask path returns equally-optimal alternate path, goal cost identical). Added 3 docstring-validation tests to test_pathfinding.py. Fix on deep-sweep-documentation-pathfinding-2026-07-08, issue #3651.",2/2
perlin,2026-06-23,,MEDIUM,2;5,"name param undocumented (Cat2) + float-dtype requirement/ValueError undocumented, no Raises section (Cat5); fixed in deep-sweep-documentation-perlin-2026-06-23; repo has issues disabled so no issue number; example runs and output matches; 1 public func (perlin) listed in reference/surface.rst",1/1
templates,2026-06-26,3541,MEDIUM,3;5,"Cat3/Cat5 MEDIUM: from_template docstring example 'from_template(""FRA"", preserve=""shape"").attrs[""crs""]' showed 32631/UTM 31N but code returns 32630/UTM 30N (GADM FRA bbox includes overseas territories, centroid lon -2.98 -> UTM 30N; code correct per documented centroid-UTM contract). Doc-only fix on deep-sweep-documentation-templates-2026-06-26; issue #3541, PR pending. Both public funcs (from_template, list_templates) fully documented and in reference/templates.rst (no Cat1/Cat2/Cat4). CUDA available: ran all docstring examples incl. preserve paths; 63/63 test_templates.py pass.",2/2
42 changes: 38 additions & 4 deletions xrspatial/pathfinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,9 @@ def a_star_search(surface: xr.DataArray,
The output is an equal-sized ``xr.DataArray`` with NaN for non-path
pixels and the accumulated cost at each path pixel.

NaN cells in *surface* are always impassable, whether or not they
appear in ``barriers``.

**Backend support**

============= ===========================================================
Expand Down Expand Up @@ -859,10 +862,11 @@ def a_star_search(surface: xr.DataArray,
bounds raises a ``ValueError``.
barriers : array like object, default=[]
List of values inside the surface which are barriers
(cannot cross).
(cannot cross). Cells whose value is NaN are always
impassable, regardless of this list.
x : str, default='x'
Name of the x coordinate in input surface raster.
y: str, default='y'
y : str, default='y'
Name of the y coordinate in input surface raster.
connectivity : int, default=8
snap_start: bool, default=False
Expand Down Expand Up @@ -899,7 +903,7 @@ def a_star_search(surface: xr.DataArray,

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

>>> import numpy as np
>>> import xarray as xr
Expand Down Expand Up @@ -1386,7 +1390,8 @@ def multi_stop_search(surface: xr.DataArray,
Sequence of ``(y, x)`` coordinate pairs to visit. Must contain
at least two points.
barriers : list, default=[]
Surface values that are impassable.
Surface values that are impassable. Cells whose value is NaN
are always impassable, regardless of this list.
x, y : str, default ``'x'`` / ``'y'``
Coordinate dimension names.
connectivity : int, default=8
Expand Down Expand Up @@ -1416,6 +1421,35 @@ def multi_stop_search(surface: xr.DataArray,
If the surface is not 2-D, fewer than two waypoints are given,
waypoints fall outside the surface bounds, or a segment is
unreachable.

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

>>> import numpy as np
>>> import xarray as xr
>>> from xrspatial import multi_stop_search
>>> agg = xr.DataArray(np.array([
... [0, 1, 0, 0],
... [1, 1, 0, 0],
... [0, 1, 2, 2],
... [1, 0, 2, 0],
... [0, 2, 2, 2]
... ], dtype=np.float64), dims=['lat', 'lon'])
>>> height, width = agg.shape
>>> agg['lon'] = np.linspace(0, width - 1, width)
>>> agg['lat'] = np.linspace(height - 1, 0, height)

>>> # visit 3 waypoints; pixels with value 0 are barriers
>>> waypoints = [(3, 0), (1, 2), (0, 1)]
>>> path_agg = multi_stop_search(
... agg, waypoints, barriers=[0], x='lon', y='lat')
>>> path_agg.attrs['waypoint_order']
[(3, 0), (1, 2), (0, 1)]
>>> path_agg.attrs['segment_costs']
[2.8284271247461903, 1.4142135623730951]
>>> path_agg.attrs['total_cost']
4.242640687119286
"""
# --- Input validation ---
_validate_raster(surface, func_name='multi_stop_search',
Expand Down
80 changes: 80 additions & 0 deletions xrspatial/tests/test_pathfinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -1195,3 +1195,83 @@ def test_multi_stop_caps_waypoints(self):
too_many = [(i % 10, (i * 7) % 10) for i in range(_MAX_WAYPOINTS + 1)]
with pytest.raises(ValueError, match=f"at most {_MAX_WAYPOINTS}"):
multi_stop_search(s, too_many)


@pytest.mark.parametrize("func_name", ["a_star_search", "multi_stop_search"])
def test_docstring_params_match_signature(func_name):
# Every parameter documented in the numpy-style "Parameters" section
# must exist in the signature (and vice versa). Same guard as
# test_multispectral.py::test_docstring_params_match_signature, plus
# support for grouped entries like "x, y : str".
import inspect
import re

import xrspatial.pathfinding as pf

func = getattr(pf, func_name)
sig_params = set(inspect.signature(func).parameters)

doc = inspect.getdoc(func) or ""
lines = doc.splitlines()
start = next(i for i, ln in enumerate(lines) if ln.strip() == "Parameters")
documented = set()
for ln in lines[start + 2:]:
if ln.strip() in ("Returns", "Raises", "References", "Notes",
"Examples"):
break
m = re.match(r"^(\w+(?:\s*,\s*\w+)*)\s*:", ln)
if m:
documented.update(re.split(r"\s*,\s*", m.group(1)))

assert documented == sig_params, (
f"{func_name}: documented params {sorted(documented)} != "
f"signature params {sorted(sig_params)}"
)


@pytest.mark.parametrize("func_name", ["a_star_search", "multi_stop_search"])
def test_docstring_structure(func_name):
# Both public functions carry the required numpydoc sections (issue
# #3651: multi_stop_search shipped without an Examples section), use
# the two-dot sourcecode directive (three dots render as literal
# text), and document that NaN cells are impassable.
import inspect

import xrspatial.pathfinding as pf

doc = inspect.getdoc(getattr(pf, func_name))
for section in ("Parameters", "Returns", "Examples"):
assert f"\n{section}\n" in doc, f"{func_name}: missing {section}"
assert "... sourcecode::" not in doc
assert ".. sourcecode:: python" in doc
assert "always impassable" in doc


def test_multi_stop_search_docstring_example():
# Run the multi_stop_search docstring example and pin its documented
# attrs output.
import xarray as xr

from xrspatial import multi_stop_search

agg = xr.DataArray(np.array([
[0, 1, 0, 0],
[1, 1, 0, 0],
[0, 1, 2, 2],
[1, 0, 2, 0],
[0, 2, 2, 2]
], dtype=np.float64), dims=['lat', 'lon'])
height, width = agg.shape
agg['lon'] = np.linspace(0, width - 1, width)
agg['lat'] = np.linspace(height - 1, 0, height)

waypoints = [(3, 0), (1, 2), (0, 1)]
path_agg = multi_stop_search(
agg, waypoints, barriers=[0], x='lon', y='lat')

assert path_agg.attrs['waypoint_order'] == [(3, 0), (1, 2), (0, 1)]
np.testing.assert_allclose(
path_agg.attrs['segment_costs'],
[2.8284271247461903, 1.4142135623730951])
np.testing.assert_allclose(
path_agg.attrs['total_cost'], 4.242640687119286)
Loading