diff --git a/.claude/sweep-documentation-state.csv b/.claude/sweep-documentation-state.csv index c6c51a2bd..54ca71480 100644 --- a/.claude/sweep-documentation-state.csv +++ b/.claude/sweep-documentation-state.csv @@ -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 diff --git a/xrspatial/pathfinding.py b/xrspatial/pathfinding.py index 3c1a4199c..dee14632e 100644 --- a/xrspatial/pathfinding.py +++ b/xrspatial/pathfinding.py @@ -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** ============= =========================================================== @@ -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 @@ -899,7 +903,7 @@ def a_star_search(surface: xr.DataArray, Examples -------- - ... sourcecode:: python + .. sourcecode:: python >>> import numpy as np >>> import xarray as xr @@ -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 @@ -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', diff --git a/xrspatial/tests/test_pathfinding.py b/xrspatial/tests/test_pathfinding.py index 0db74e791..0d8fc686e 100644 --- a/xrspatial/tests/test_pathfinding.py +++ b/xrspatial/tests/test_pathfinding.py @@ -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)