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 @@ -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
46 changes: 40 additions & 6 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,16 +862,17 @@ 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
snap_start : bool, default=False
Snap the start location to the nearest valid value before
beginning pathfinding.
snap_goal: bool, default=False
snap_goal : bool, default=False
Snap the goal location to the nearest valid value before
beginning pathfinding.
friction : xr.DataArray, optional
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