Skip to content

Commit e82ce72

Browse files
authored
Fix nbr() docstring parameter name to match signature (swir2_agg) (#3435)
1 parent 667cdc8 commit e82ce72

3 files changed

Lines changed: 37 additions & 1 deletion

File tree

.claude/sweep-api-consistency-state.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ hydro-d8,2026-05-29,2709,HIGH,1;5,"Sweep 2026-05-29 (deep-sweep-api-consistency-
55
interpolate,2026-06-12,3285,MEDIUM,2,"Sweep 2026-06-12 (deep-sweep-api-consistency-interpolate-2026-06-12). Scope: idw/_idw.py, kriging/_kriging.py, spline/_spline.py, shared _validation.py. 1 MEDIUM Cat 2 finding filed as #3285, fixed on branch -01 off this one: kriging(return_variance=True) singular-matrix fallback (_kriging.py:499) returns prediction, prediction.copy() so the variance DataArray keeps the prediction's name instead of f'{name}_variance' (normal path :523 names it correctly); reproduced by monkeypatching _build_kriging_matrix to None; anything keying on .name (xr.merge, Dataset build) silently collapses the pair. One-line fix + regression test on the singular path. Clean elsewhere: Cat 1 in-module exact (idw/kriging/spline share x, y, z, template positionals and name= default '<func>'; template matches kde's template=); docstring/signature parity exact on all 3 publics (every param documented, Returns sections match incl. kriging's tuple); Cat 4 no default drift (power=2.0, k=None, fill_value=nan, variogram_model='spherical', nlags=15, smoothing=0.0, all single-owner params); Cat 5 no orphan API (all 3 re-exported in xrspatial/__init__.py and autosummaried in docs/source/reference/interpolation.rst; tests touch private helpers only via module paths). Cross-cutting, notes only per template: fill_value (idw) vs fill (rasterize) for the uncovered-pixel value is library-wide drift (idw matches numpy's fill_value convention, left alone); public functions are untyped module-wide (consistent internally, drifts from typed kde/rasterize/proximity siblings -- annotation pass would span the whole module, LOW, not filed); kde's keyword-only style is the library minority so interpolate's positional style matches the rasterize/proximity majority. GPU k-nearest rejection (NotImplementedError) is deliberate and documented in the k param docstring. cuda-validated: CUDA_AVAILABLE=True on this host; idw/kriging/spline smoke-tested with full kwargs on numpy AND cupy DataArrays (variance name parity confirmed on both), dask+numpy and dask+cupy graph construction verified without compute."
66
mcda,2026-06-10,3148,HIGH,1;2;3;5,"Sweep 2026-06-10 (deep-sweep-api-consistency-mcda-2026-06-10). Fixed in this branch (#3148): (HIGH Cat 1) owa() named its criterion-weight dict criterion_weights while wlc/wpm/sensitivity use weights (same semantics, same _validate_weights); renamed to weights with keyword-only criterion_weights deprecation shim (DeprecationWarning; both names -> TypeError; positional callers untouched). (MEDIUM Cat 2) boolean_overlay annotated criteria as dict-only while every sibling combiner takes xr.Dataset; Dataset already worked via the Mapping interface -- now annotated/documented as xr.Dataset | dict. (MEDIUM Cat 3) ahp_weights docstring Raises claimed ValueError on incomplete comparisons but code warns (UserWarning) and defaults missing pairs to 1 -- docstring now documents Warns behaviour. (MEDIUM Cat 5) ConsistencyResult returned by public ahp_weights but absent from xrspatial/mcda __all__ and docs/source/reference/mcda.rst -- exported and documented. Documented, NOT fixed here: (MEDIUM Cat 2, deferred to parallel sweep-metadata sibling to avoid duplicate PR) constrain() drops attrs via xr.where while the other nine public functions preserve them. (LOW Cat 2) ahp_weights returns (weights, ConsistencyResult) tuple vs rank_weights bare dict -- intentional, documented in both docstrings, no fix. (LOW Cat 4) name=None inherit-input-name (standardize/constrain) vs literal-name defaults (combiners) -- defensible split, document only. Pre-existing backend bugs surfaced by the mandated cupy smoke (accuracy/test-coverage lane, recorded in #3148 body): owa fails on cupy (numpy order-weights array mixed into cupy multiply, combine.py ~336-340) and on ANY dask backend at graph construction (da.sort does not exist, combine.py:356, despite the owa MemoryError message recommending dask); sensitivity(method=monte_carlo) fails on cupy (template.values implicit-conversion guard). constrain on cupy blocked by the known library-wide cupy 13.6 + xarray xr.where astype incompat (dependency-pin issue), not mcda-specific. cuda-validated: CUDA_AVAILABLE=True; all 10 public functions smoke-tested on cupy DataArrays; owa weights=/criterion_weights= shim verified on numpy AND cupy entry points (cupy execution stops at the pre-existing mixed-array bug, signature acceptance confirmed)."
77
morphology,2026-06-20,3399,MEDIUM,5,"Sweep 2026-06-20 (deep-sweep-api-consistency-morphology-2026-06-20). 1 MEDIUM Cat 5 finding filed as #3399, fixed in this branch (PR #3409). Cat 5 accessor-parity gap: all 7 public morphology functions are exported in xrspatial/__init__.py, documented, and tested, but the .xrs accessor exposed only morph_erode/dilate/opening/closing on both XrsSpatialDataArrayAccessor and XrsSpatialDatasetAccessor; morph_gradient/white_tophat/black_tophat were missing (da.xrs.morph_gradient -> AttributeError while da.xrs.morph_erode works). Root cause: base 4 ops landed #949 + accessors #1042; derived 3 ops landed later #1026 and the accessor was never updated. Fix adds the 3 forwarding methods to both accessor classes (mechanical, matches existing pattern) plus accessor tests (none existed for morph before) and guards all 7 method names in the expected-methods checks. Clean elsewhere: Cat 1 no in-module naming drift -- all 7 publics share the exact signature (agg, kernel=None, boundary='nan', name='<op>'), verified programmatically; matches kernel-op siblings convolution_2d/focal.apply/hotspots which also use agg/kernel/boundary/name. Cat 2 no return drift (all 7 return xr.DataArray with coords/dims/attrs preserved via _dispatch / @supports_dataset). Cat 3 docstring/signature parity exact on all 7 (every param documented, Returns sections all DataArray). Cat 4 no default drift (kernel=None->3x3 ones, boundary='nan' uniform across all 7). Cross-cutting, notes only per template: convolution_2d orders name before boundary while morphology orders boundary before name (both keyword-defaulted, cosmetic); focal.apply/hotspots accept a keyword-only raster= alias for agg but that alias is focal-only (not a library convention) so morphology lacking it is not drift; library-wide first-arg agg vs raster drift spans 20+ modules, out of per-module scope. cuda-validated: CUDA_AVAILABLE=True on this host; all 7 publics smoke-tested with identical kwargs on numpy AND cupy DataArrays (shape parity, no signature drift between numpy/cupy entry points). PR reviewed (COMMENTED), no findings; branch merged with origin/main (clean), left BLOCKED on REVIEW_REQUIRED for user merge."
8+
multispectral,2026-06-20,3433,MEDIUM,3,"Sweep 2026-06-20 (deep-sweep-api-consistency-multispectral). 18 public funcs, all single 2D DataArray returns except true_color (3D composite, inherently different). (#3433 MEDIUM Cat 3) nbr() docstring documented swir_agg but signature param is swir2_agg; copying the name from docs raises TypeError. Docs-only rename to swir2_agg + guard test test_docstring_params_match_signature over the 17 index funcs. No deprecation needed. LOW, documented not fixed: (Cat 5) gci/nbr2/ndmi/true_color/ebbi are NOT re-exported in xrspatial/__init__.py while 13 module siblings are; not an orphan API since the canonical documented path is xrspatial.multispectral.X (all 18 in multispectral.rst) and tests import from the submodule -- convenience-surface gap only. (Cat 3 LOW) only savi types name: str; true_color lacks band type hints and uses r/g/b (conventional for an RGB composite). Cross-module note (not filed per template): multispectral has no -> xr.DataArray return annotations while sibling fire.py annotates all 7 -- library-wide convention drift. No Cat 1 in-module (all band inputs are <band>_agg), no Cat 2 (return shapes consistent), no Cat 4 (no mutable defaults; soil_factor=1.0 consistent evi/savi). CUDA available: numpy+cupy smoke-tested, signatures parity-clean, full suite 171 passed."
89
polygonize,2026-06-12,3306;3307,MEDIUM,1;3,"Re-sweep 2026-06-12 (deep-sweep-api-consistency-polygonize-2026-06-12); prior pass 2026-05-19 (#2148). 2 MEDIUM findings filed and fixed on branches -01/-02 off this one. (#3306, MEDIUM Cat 3, branch -01) column_name docstring says 'Only used if return_type is geopandas or spatialpandas' but _to_geojson also consumes it as the per-feature property key (verified: properties={'myval': 1}); docs-only fix + test pinning geojson property naming. (#3307, MEDIUM sibling-behavior drift, branch -02) return_type is the only polygonize parameter validated AFTER the computation: invalid value runs the full backend (spy-verified 1 invocation before raise) while sibling contours() validates up front and lists allowed values; fix hoists the check into the top validation block with an allowed-values message (existing test matches on prefix, unaffected). Re-confirmed prior dispositions, still documented-only per cross-module rule: (HIGH Cat 1 cross-module) connectivity (polygonize, matches GDAL/rasterio/skimage) vs neighborhood (sieve.py, zonal.regions) for the identical 4|8 rook/queen concept -- rename shim belongs in sieve/zonal, out of polygonize scope; (LOW Cat 1 cross-cutting) raster (polygonize/sieve/clip_polygon) vs agg (contours/terrain family) first-arg drift, library-wide, not filed per-module. No new Cat 2 (return_type dispatch shapes match docstring Returns section exactly); no Cat 4 (atol/rtol mirror numpy.isclose, connectivity=4 == sieve neighborhood=4); Cat 5 LOW documented-only: module has no __all__ and the non-underscore internals generated_jit + Turn leak via import-star; polygonize re-exported in __init__.py and accessor, no orphan API. Docstring/signature parity otherwise exact (all 10 params documented, all annotated). Open polygonize issues #3292/#3293 checked -- no overlap with these findings. cuda-validated: CUDA_AVAILABLE=True on this host; polygonize smoke-tested with identical full kwargs on numpy, cupy (int + float atol/rtol=0), and dask+cupy; no backend signature drift."
910
proximity,2026-06-09,3090;3091,HIGH,2;3,"Sweep 2026-06-09 (deep-sweep-api-consistency-proximity-2026-06-09). 1 HIGH Cat 2 finding (#3090): dask+numpy (and unbounded dask+cupy, which converts to it) KDTree path violates the documented lowest-flat-index tie-break in allocation()/direction() whenever the raster has >1 chunk column. _collect_region_targets concatenates targets chunk-major (iy outer, ix inner) so the tree's target order is not global row-major; _kdtree_query_lowest_index then ties to the wrong target. Existing tie-break tests put both targets in the same raster row where chunk order coincides with row-major, so they pass. Repro: 5x5, targets 2@(1,3) and 3@(2,2), chunks (5,3), pixel (2,3) tied at d=1 -> numpy gives 2, dask gives 3. Bounded map_overlap paths are fine (local row-major order is offset-invariant). 1 MEDIUM Cat 3 finding (#3091): all 3 public docstrings claim numpy + dask+numpy support only while cupy/dask+cupy backends exist, are dispatched, and are tested (the tie-break paragraphs in the same docstrings name all 4 backends); direction() opens with a stray copy-pasted slope line ('downward slope direction') plus a doubled 'the the'; allocation example output reads as float64 but the function returns float32; stale '# convert to have same type as of input @raster' comment. Within-module Cat 1/4/5 clean: proximity/allocation/direction share an identical signature (raster, x='x', y='y', target_values=None, max_distance=np.inf, distance_metric='EUCLIDEAN'); consistent with surface_distance siblings (raster/x/y/target_values/max_distance); all 6 public symbols (incl. euclidean/manhattan/great_circle_distance) re-exported in __init__.py, no orphan API. Cross-cutting, documented not filed: sibling distance modules (surface_distance, cost_distance, balanced_allocation) use mutable default target_values: list = [] while proximity uses the None sentinel - the mutable-default fix belongs to those modules; proximity's target_values: list = None hint would be more precise as Optional[list] (LOW, matches library style). cuda-validated: CUDA_AVAILABLE=True on this host; proximity/allocation/direction smoke-tested with identical kwargs on numpy, cupy, dask+numpy, dask+cupy (proximity parity passed; allocation/direction parity failure is finding #3090)."
1011
rasterize,2026-06-09,3089,HIGH,1,"Sweep 2026-06-09 (deep-sweep-api-consistency-rasterize-2026-06-09). 1 HIGH Cat 1 fixed in this branch (#3089): rasterize(use_cuda=) vs open_geotiff(gpu=) named the identical GPU-backend opt-in differently; these are the only two public entry points with an explicit GPU boolean (no input array to dispatch on; both pair it with chunks= for dask) and both names were live in the public API at once. Fix renames the positional param to gpu (same slot, positional callers unaffected) and appends use_cuda=None as a deprecated alias: DeprecationWarning on use, TypeError when combined with gpu=True. Docstring, GPU merge warning text, CuPy ImportError text, and polygon_clip.py's internal dask+cupy caller updated (guarded so a legacy use_cuda in rasterize_kw does not collide with the new default); all rasterize test call sites migrated to gpu=; regression tests in test_rasterize_gpu_alias_3089.py pin slot position, warning, TypeError, backend parity, and the warning-free clip_polygon path. Re-inspection after the 2026-05-21 pass (#2250); prior cross-module notes (clip_polygon nodata vs fill, name default drift, polygonize column_name vs column) still documented-only. Docstring/signature parity verified programmatically (17/17 params, order matches). New params since last pass (check_crs, max_pixels) consistent with geotiff naming (max_pixels matches geotiff's). No Cat 2/4/5 findings. LOW noted, not fixed (other module's docs): docs/source/user_guide/focal.ipynb claims convolve_2d takes use_cuda, which it does not. cuda-validated: CUDA_AVAILABLE=True; numpy/cupy/dask+numpy/dask+cupy smoke-tested with identical kwargs, values equal."

xrspatial/multispectral.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ def nbr(nir_agg: xr.DataArray,
575575
----------
576576
nir_agg : xr.DataArray
577577
2D array of near-infrared band.
578-
swir_agg : xr.DataArray
578+
swir2_agg : xr.DataArray
579579
2D array of shortwave infrared band.
580580
(Landsat 4-7: Band 6)
581581
(Landsat 8: Band 7)

xrspatial/tests/test_multispectral.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,3 +1201,38 @@ def test_osavi_approaches_ndvi_for_large_dn(nir_data, red_data, qgis_ndvi):
12011201
osavi_vals[mask] = np.nan
12021202
np.testing.assert_allclose(
12031203
osavi_vals, ndvi_vals, equal_nan=True, rtol=1e-3)
1204+
1205+
1206+
@pytest.mark.parametrize(
1207+
"func",
1208+
[arvi, bai, ebbi, evi, gci, mndwi, msavi2, nbr, nbr2, ndbi,
1209+
ndmi, ndsi, ndvi, ndwi, osavi, savi, sipi],
1210+
)
1211+
def test_docstring_params_match_signature(func):
1212+
# Every parameter documented in the numpy-style "Parameters" section
1213+
# must exist in the signature (and vice versa). Guards against
1214+
# docstring/signature drift such as nbr documenting `swir_agg`
1215+
# while the signature accepts `swir2_agg`.
1216+
import inspect
1217+
import re
1218+
1219+
sig_params = set(inspect.signature(func).parameters)
1220+
1221+
doc = inspect.getdoc(func) or ""
1222+
lines = doc.splitlines()
1223+
start = next(i for i, ln in enumerate(lines) if ln.strip() == "Parameters")
1224+
# Skip the "----------" underline row.
1225+
documented = []
1226+
for ln in lines[start + 2:]:
1227+
if ln.strip() in ("Returns", "References", "Notes", "Examples"):
1228+
break
1229+
# Parameter entries are flush-left "name : type" lines.
1230+
m = re.match(r"^(\w+)\s*:", ln)
1231+
if m:
1232+
documented.append(m.group(1))
1233+
documented = set(documented)
1234+
1235+
assert documented == sig_params, (
1236+
f"{func.__name__}: documented params {sorted(documented)} != "
1237+
f"signature params {sorted(sig_params)}"
1238+
)

0 commit comments

Comments
 (0)