From 299c3b11b577db551ed4a262146df061f4f244e6 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:17:36 -0500 Subject: [PATCH 1/4] Freeze the public surface: export the README-only functions, add __all__, and pin the matrix to it (#2930) --- README.md | 53 ++++---- benchmarks/benchmarks/watershed.py | 4 +- docs/source/reference/surface.rst | 7 ++ docs/source/reference/zonal.rst | 1 + xrspatial/__init__.py | 167 ++++++++++++++++++++++++- xrspatial/accessor.py | 4 +- xrspatial/tests/test_public_surface.py | 71 +++++++++++ 7 files changed, 280 insertions(+), 27 deletions(-) create mode 100644 xrspatial/tests/test_public_surface.py diff --git a/README.md b/README.md index 71866ea3f..9a87752b0 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,8 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for per-pixel coord | [rechunk_no_shuffle](xrspatial/utils.py) | Rechunk dask arrays using whole-chunk multiples (no shuffle) | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [fused_overlap](xrspatial/utils.py) | Fuse sequential map_overlap calls into a single pass | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [multi_overlap](xrspatial/utils.py) | Run multi-output kernel in a single overlap pass | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [validate](xrspatial/validate.py) | Check a raster against the xarray-spatial input contract (`.xrs.validate()`) | Custom | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [validate](xrspatial/validate.py) | Check a raster against the xarray-spatial input contract (`.xrs.validate()`) | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [diagnose](xrspatial/diagnostics.py) | Report which xarray-spatial tools a raster is ready for and what blocks the rest | Custom | ๐Ÿ”ผ | | | | ----------- @@ -275,6 +276,7 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for per-pixel coord | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| | [from_template](xrspatial/templates.py) | Empty study-area grid for a named region (CONUS, NYC, ...), a world city (London, Tokyo, ... in its UTM zone), a country code, or a whole-world projection (web_mercator, wgs84/latlon, equal_earth); `preserve='area'/'shape'` picks an EPSG projection by property; `list_templates()` lists every accepted name | Custom | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [list_templates](xrspatial/templates.py) | Names of the curated regions, cities, countries, and projections `from_template` accepts (backend-independent) | Custom | ๐Ÿ”ผ | | | | ----------- @@ -290,7 +292,7 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for per-pixel coord | [Roughness](xrspatial/terrain_metrics.py) | Computes local relief as max minus min elevation in a 3ร—3 window | GDAL gdaldem | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Sky-View Factor](xrspatial/sky_view_factor.py) | Measures the fraction of visible sky hemisphere at each cell | Zakek et al. 2011 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Slope](xrspatial/slope.py) | Computes terrain gradient steepness at each cell in degrees | Horn 1981 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Terrain Generation](xrspatial/terrain.py) | Generates synthetic terrain from fBm or ridged fractal noise with optional domain warping, Worley blending, and hydraulic erosion | Custom (fBm) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Terrain Generation](xrspatial/terrain.py) `generate_terrain` | Generates synthetic terrain from fBm or ridged fractal noise with optional domain warping, Worley blending, and hydraulic erosion | Custom (fBm) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [TPI](xrspatial/terrain_metrics.py) | Computes Topographic Position Index (center minus mean of neighbors) | Weiss 2001 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [TRI](xrspatial/terrain_metrics.py) | Computes Terrain Ruggedness Index (local elevation variation) | Riley et al. 1999 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Landforms](xrspatial/terrain_metrics.py) | Classifies terrain into 10 landform types using the Weiss (2001) TPI scheme | Weiss 2001 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | @@ -299,10 +301,10 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for per-pixel coord | [Visibility Frequency](xrspatial/visibility.py) | Fraction of observers with line-of-sight to each cell | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Line of Sight](xrspatial/visibility.py) | Elevation profile and visibility along a point-to-point transect | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Min Observable Height](xrspatial/experimental/min_observable_height.py) | Finds the minimum observer height needed to see each cell | Custom | ๐Ÿงช | | | | -| [Perlin Noise](xrspatial/perlin.py) | Generates smooth continuous random noise for procedural textures | Perlin 1985 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Worley Noise](xrspatial/worley.py) | Generates cellular (Voronoi) noise returning distance to the nearest feature point | Worley 1996 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Hydraulic Erosion](xrspatial/erosion.py) | Simulates particle-based water erosion to carve valleys and deposit sediment | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Bump Mapping](xrspatial/bump.py) | Adds randomized bump features to simulate natural terrain variation | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Perlin Noise](xrspatial/perlin.py) `perlin` | Generates smooth continuous random noise for procedural textures | Perlin 1985 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Worley Noise](xrspatial/worley.py) `worley` | Generates cellular (Voronoi) noise returning distance to the nearest feature point | Worley 1996 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Hydraulic Erosion](xrspatial/erosion.py) `erode` | Simulates particle-based water erosion to carve valleys and deposit sediment | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Bump Mapping](xrspatial/bump.py) `bump` | Adds randomized bump features to simulate natural terrain variation | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ----------- @@ -336,7 +338,7 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for per-pixel coord | [Travel Time](xrspatial/flood.py) | Estimates overland flow travel time via simplified Manning's equation | Manning 1891 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Vegetation Roughness](xrspatial/flood.py) | Derives Manning's roughness coefficients from NLCD land cover or NDVI | SCS/NRCS | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Vegetation Curve Number](xrspatial/flood.py) | Derives SCS curve numbers from land cover and hydrologic soil group | SCS/NRCS | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Flood Depth (Vegetation)](xrspatial/flood.py) | Manning-based steady-state flow depth incorporating vegetation roughness | Manning 1891 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Flood Depth (Vegetation)](xrspatial/flood.py) `flood_depth_vegetation` | Manning-based steady-state flow depth incorporating vegetation roughness | Manning 1891 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ----------- @@ -389,11 +391,11 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| -| [Apply](xrspatial/focal.py) | Applies a custom function over a sliding neighborhood window | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Apply](xrspatial/focal.py) `focal_apply` | Applies a custom function over a sliding neighborhood window | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Hotspots](xrspatial/focal.py) | Identifies statistically significant spatial clusters using Getis-Ord Gi* | Getis & Ord 1992 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Emerging Hotspots](xrspatial/emerging_hotspots.py) | Classifies time-series hot/cold spot trends using Gi* and Mann-Kendall | Getis & Ord 1992, Mann 1945 | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Mean](xrspatial/focal.py) | Computes the mean value within a sliding neighborhood window | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Focal Statistics](xrspatial/focal.py) | Computes summary statistics over a sliding neighborhood window | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Focal Statistics](xrspatial/focal.py) `focal_stats` | Computes summary statistics over a sliding neighborhood window | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Bilateral](xrspatial/bilateral.py) | Feature-preserving smoothing via bilateral filtering | Tomasi & Manduchi 1998 | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [GLCM Texture](xrspatial/glcm.py) | Computes Haralick GLCM texture features over a sliding window | Haralick et al. 1973 | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Sobel X](xrspatial/edge_detection.py) | Horizontal gradient via Sobel operator (detects vertical edges) | Sobel & Feldman 1968 | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | @@ -414,6 +416,9 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | [Least-Cost Corridor](xrspatial/corridor.py) | Identifies zones of low cumulative cost between two source locations | Standard (Dijkstra) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Direction](xrspatial/proximity.py) | Computes the direction from each cell to the nearest source feature | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Proximity](xrspatial/proximity.py) | Computes the distance from each cell to the nearest source feature | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Euclidean Distance](xrspatial/proximity.py) `euclidean_distance` | Straight-line distance between two points; the scalar metric behind `proximity` (backend-independent) | Standard | ๐Ÿ”ผ | | | | +| [Manhattan Distance](xrspatial/proximity.py) `manhattan_distance` | City-block distance between two points (backend-independent) | Standard | ๐Ÿ”ผ | | | | +| [Great-Circle Distance](xrspatial/proximity.py) `great_circle_distance` | Haversine distance between two lon/lat points in meters (backend-independent) | Haversine | ๐Ÿ”ผ | | | | | [Surface Distance](xrspatial/surface_distance.py) | Computes distance along the 3D terrain surface to the nearest source | Standard (Dijkstra) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Surface Allocation](xrspatial/surface_distance.py) | Assigns each cell to the nearest source by terrain surface distance | Standard (Dijkstra) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Surface Direction](xrspatial/surface_distance.py) | Computes compass direction to the nearest source by terrain surface distance | Standard (Dijkstra) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | @@ -424,13 +429,15 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| -| [Apply](xrspatial/zonal.py) | Applies a custom function to each zone in a classified raster | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Apply](xrspatial/zonal.py) `zonal_apply` | Applies a custom function to each zone in a classified raster | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Clip Polygon](xrspatial/polygon_clip.py) | Clips a raster to an arbitrary polygon with masking | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Crop](xrspatial/zonal.py) | Extracts the bounding rectangle of a specific zone | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Regions](xrspatial/zonal.py) | Identifies connected regions of non-zero cells | Standard (CCL) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Trim](xrspatial/zonal.py) | Removes nodata border rows and columns from a raster | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Zonal Statistics](xrspatial/zonal.py) | Computes summary statistics for a value raster within each zone | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Zonal Cross Tabulate](xrspatial/zonal.py) | Cross-tabulates agreement between two categorical rasters | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Zonal Statistics](xrspatial/zonal.py) `zonal_stats` | Computes summary statistics for a value raster within each zone | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Zonal Cross Tabulate](xrspatial/zonal.py) `zonal_crosstab` | Cross-tabulates agreement between two categorical rasters | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Hypsometric Integral](xrspatial/zonal.py) `hypsometric_integral` | Elevation-relief ratio of each zone (Strahler hypsometric integral) | Strahler 1952 | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Suggest Zonal Canvas](xrspatial/zonal.py) `suggest_zonal_canvas` | Pick a raster extent and cell size for rasterizing zone polygons (backend-independent) | Custom | ๐Ÿ”ผ | | | | ----------- @@ -448,13 +455,13 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| -| [Erode](xrspatial/morphology.py) | Morphological erosion (local minimum over structuring element) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Dilate](xrspatial/morphology.py) | Morphological dilation (local maximum over structuring element) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Opening](xrspatial/morphology.py) | Erosion then dilation (removes small bright features) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Closing](xrspatial/morphology.py) | Dilation then erosion (fills small dark gaps) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Gradient](xrspatial/morphology.py) | Dilation minus erosion (edge detection) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [White Top-hat](xrspatial/morphology.py) | Original minus opening (isolate bright features) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Black Top-hat](xrspatial/morphology.py) | Closing minus original (isolate dark features) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Erode](xrspatial/morphology.py) `morph_erode` | Morphological erosion (local minimum over structuring element) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Dilate](xrspatial/morphology.py) `morph_dilate` | Morphological dilation (local maximum over structuring element) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Opening](xrspatial/morphology.py) `morph_opening` | Erosion then dilation (removes small bright features) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Closing](xrspatial/morphology.py) `morph_closing` | Dilation then erosion (fills small dark gaps) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Gradient](xrspatial/morphology.py) `morph_gradient` | Dilation minus erosion (edge detection) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [White Top-hat](xrspatial/morphology.py) `morph_white_tophat` | Original minus opening (isolate bright features) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Black Top-hat](xrspatial/morphology.py) `morph_black_tophat` | Closing minus original (isolate dark features) | Standard (morphology) | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Sieve](xrspatial/sieve.py) | Remove small connected clumps from classified rasters | GDAL sieve | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ------- @@ -496,15 +503,17 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| -| [Mahalanobis Distance](xrspatial/mahalanobis.py) | Measures statistical distance from a multi-band reference distribution, accounting for band correlations | Mahalanobis 1936 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Mahalanobis Distance](xrspatial/mahalanobis.py) `mahalanobis` | Measures statistical distance from a multi-band reference distribution, accounting for band correlations | Mahalanobis 1936 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ------- ### **Multi-Criteria Decision Analysis (MCDA)** +These functions live in the `xrspatial.mcda` subpackage (`from xrspatial.mcda import wlc`); they are not re-exported at the top level. + | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| -| [Standardize](xrspatial/mcda/standardize.py) | Converts criterion rasters to 0-1 suitability scale (linear, sigmoidal, gaussian, triangular, piecewise, categorical) | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Standardize (MCDA)](xrspatial/mcda/standardize.py) `standardize` | Converts criterion rasters to 0-1 suitability scale (linear, sigmoidal, gaussian, triangular, piecewise, categorical) | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [AHP Weights](xrspatial/mcda/weights.py) | Derives criterion weights from pairwise comparisons using the Saaty eigenvector method with consistency ratio | Saaty 1980 | โœ… | ๐Ÿšซ | ๐Ÿšซ | ๐Ÿšซ | | [Rank Weights](xrspatial/mcda/weights.py) | Derives weights from a rank ordering (ROC, rank sum, reciprocal) | Standard | โœ… | ๐Ÿšซ | ๐Ÿšซ | ๐Ÿšซ | | [WLC](xrspatial/mcda/combine.py) | Weighted Linear Combination (fully compensatory weighted sum) | Malczewski 2006 | โœ… | ๐Ÿ”ผ | ๐Ÿงช | ๐Ÿงช | @@ -522,7 +531,7 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | Name | Description | Source | NumPy xr.DataArray | Dask xr.DataArray | CuPy GPU xr.DataArray | Dask GPU xr.DataArray | |:----------:|:------------|:------:|:----------------------:|:--------------------:|:-------------------:|:------:| -| [A* Pathfinding](xrspatial/pathfinding.py) | Finds the least-cost path between two cells on a cost surface | Hart et al. 1968 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [A* Pathfinding](xrspatial/pathfinding.py) `a_star_search` | Finds the least-cost path between two cells on a cost surface | Hart et al. 1968 | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Multi-Stop Search](xrspatial/pathfinding.py) | Routes through N waypoints in sequence, with optional TSP reordering | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ---------- diff --git a/benchmarks/benchmarks/watershed.py b/benchmarks/benchmarks/watershed.py index 3ee029d4e..a68f3d30d 100644 --- a/benchmarks/benchmarks/watershed.py +++ b/benchmarks/benchmarks/watershed.py @@ -1,4 +1,4 @@ -from xrspatial import flow_direction, watershed, basins +from xrspatial import flow_direction, watershed, basin from .common import get_xr_dataarray @@ -59,6 +59,6 @@ def setup(self, nx, type): ) def time_basins(self, nx, type): - result = basins(self.flow_dir) + result = basin(self.flow_dir) if hasattr(result.data, 'compute'): result.data.compute() diff --git a/docs/source/reference/surface.rst b/docs/source/reference/surface.rst index 04e0098e1..f00aa87f3 100644 --- a/docs/source/reference/surface.rst +++ b/docs/source/reference/surface.rst @@ -109,6 +109,13 @@ Perlin Noise xrspatial.perlin.perlin +Worley Noise +============ +.. autosummary:: + :toctree: _autosummary + + xrspatial.worley.worley + Bump Mapping ============ .. autosummary:: diff --git a/docs/source/reference/zonal.rst b/docs/source/reference/zonal.rst index 13c1200c8..36c79ff9b 100644 --- a/docs/source/reference/zonal.rst +++ b/docs/source/reference/zonal.rst @@ -57,6 +57,7 @@ Zonal Statistics :toctree: _autosummary xrspatial.zonal.get_full_extent + xrspatial.zonal.hypsometric_integral xrspatial.zonal.stats xrspatial.zonal.suggest_zonal_canvas diff --git a/xrspatial/__init__.py b/xrspatial/__init__.py index 8dbacdd21..78d1d46d3 100644 --- a/xrspatial/__init__.py +++ b/xrspatial/__init__.py @@ -1,4 +1,4 @@ -from xrspatial._numpy_floor import _check_numpy_version # noqa: F401 +from xrspatial import _numpy_floor # noqa: F401 โ€” raises on NumPy 1.x before any submodule loads from xrspatial.aspect import aspect # noqa from xrspatial.aspect import eastness # noqa from xrspatial.aspect import northness # noqa @@ -55,6 +55,9 @@ from xrspatial.hydro import flow_direction # noqa: unified wrapper from xrspatial.hydro import flow_length # noqa: unified wrapper from xrspatial.hydro import flow_path # noqa: unified wrapper +from xrspatial.focal import apply as focal_apply # noqa +from xrspatial.focal import focal_stats # noqa +from xrspatial.focal import hotspots # noqa from xrspatial.focal import mean # noqa from xrspatial.glcm import glcm_texture # noqa from xrspatial.morphology import morph_black_tophat # noqa @@ -69,22 +72,28 @@ from xrspatial.mahalanobis import mahalanobis # noqa from xrspatial.multispectral import arvi # noqa from xrspatial.multispectral import bai # noqa +from xrspatial.multispectral import ebbi # noqa from xrspatial.multispectral import evi # noqa +from xrspatial.multispectral import gci # noqa from xrspatial.multispectral import mndwi # noqa from xrspatial.multispectral import msavi2 # noqa from xrspatial.multispectral import nbr # noqa +from xrspatial.multispectral import nbr2 # noqa from xrspatial.multispectral import ndbi # noqa +from xrspatial.multispectral import ndmi # noqa from xrspatial.multispectral import ndsi # noqa from xrspatial.multispectral import ndvi # noqa from xrspatial.multispectral import ndwi # noqa from xrspatial.multispectral import osavi # noqa from xrspatial.multispectral import savi # noqa from xrspatial.multispectral import sipi # noqa +from xrspatial.multispectral import true_color # noqa from xrspatial.pathfinding import a_star_search # noqa from xrspatial.pathfinding import multi_stop_search # noqa from xrspatial.normalize import rescale # noqa from xrspatial.normalize import standardize # noqa from xrspatial.perlin import perlin # noqa +from xrspatial.worley import worley # noqa from xrspatial.preview import preview # noqa from xrspatial.proximity import allocation # noqa from xrspatial.rasterize import rasterize # noqa @@ -121,7 +130,6 @@ from xrspatial.visibility import line_of_sight # noqa from xrspatial.visibility import visibility_frequency # noqa from xrspatial.hydro import basin # noqa: unified wrapper -from xrspatial.hydro import basins # noqa: backward-compat alias from xrspatial.hydro import watershed # noqa: unified wrapper from xrspatial.zonal import apply as zonal_apply # noqa from xrspatial.zonal import crop # noqa @@ -142,6 +150,161 @@ import xrspatial.mcda # noqa: F401 โ€” exposes xrspatial.mcda subpackage import xrspatial.accessor # noqa: F401 โ€” registers .xrs accessors +# The names below, plus the ``xrspatial.mcda`` subpackage and the ``.xrs`` +# accessor, are the 1.x LTS surface (docs/source/user_guide/stability_policy.rst). +# xrspatial/tests/test_public_surface.py keeps this list and the README +# feature matrix in step. +__all__ = [ + 'a_star_search', + 'allocation', + 'arvi', + 'aspect', + 'bai', + 'balanced_allocation', + 'basin', + 'bilateral', + 'binary', + 'box_plot', + 'bump', + 'burn_severity_class', + 'clip_polygon', + 'contours', + 'cost_distance', + 'crop', + 'cumulative_viewshed', + 'curvature', + 'curve_number_runoff', + 'diagnose', + 'diffuse', + 'direction', + 'disaggregate', + 'dnbr', + 'eastness', + 'ebbi', + 'emerging_hotspots', + 'equal_interval', + 'erode', + 'euclidean_distance', + 'evi', + 'fill', + 'fireline_intensity', + 'flame_length', + 'flood_depth', + 'flood_depth_vegetation', + 'flow_accumulation', + 'flow_direction', + 'flow_length', + 'flow_path', + 'focal_apply', + 'focal_stats', + 'from_template', + 'fused_overlap', + 'gci', + 'generate_terrain', + 'glcm_texture', + 'great_circle_distance', + 'hand', + 'head_tail_breaks', + 'hillshade', + 'hotspots', + 'hypsometric_integral', + 'idw', + 'inundation', + 'kbdi', + 'kde', + 'kriging', + 'LANDFORM_CLASSES', + 'landforms', + 'laplacian', + 'least_cost_corridor', + 'line_density', + 'line_of_sight', + 'list_templates', + 'mahalanobis', + 'manhattan_distance', + 'maximum_breaks', + 'mcda', + 'mean', + 'merge', + 'mndwi', + 'morph_black_tophat', + 'morph_closing', + 'morph_dilate', + 'morph_erode', + 'morph_gradient', + 'morph_opening', + 'morph_white_tophat', + 'msavi2', + 'multi_overlap', + 'multi_stop_search', + 'natural_breaks', + 'nbr', + 'nbr2', + 'ndbi', + 'ndmi', + 'ndsi', + 'ndvi', + 'ndwi', + 'northness', + 'open_geotiff', + 'osavi', + 'percentiles', + 'perlin', + 'polygonize', + 'preview', + 'prewitt_x', + 'prewitt_y', + 'proximity', + 'pycnophylactic', + 'quantile', + 'rasterize', + 'rate_of_spread', + 'rdnbr', + 'rechunk_no_shuffle', + 'reclassify', + 'regions', + 'reproject', + 'resample', + 'rescale', + 'roughness', + 'savi', + 'sieve', + 'sink', + 'sipi', + 'sky_view_factor', + 'slope', + 'snap_pour_point', + 'sobel_x', + 'sobel_y', + 'spline', + 'standardize', + 'std_mean', + 'stream_link', + 'stream_order', + 'suggest_zonal_canvas', + 'surface_allocation', + 'surface_direction', + 'surface_distance', + 'to_geotiff', + 'tpi', + 'travel_time', + 'tri', + 'trim', + 'true_color', + 'twi', + 'validate', + 'validate_disaggregation', + 'vegetation_curve_number', + 'vegetation_roughness', + 'viewshed', + 'visibility_frequency', + 'watershed', + 'worley', + 'zonal_apply', + 'zonal_crosstab', + 'zonal_stats', +] + try: from ._version import version as __version__ diff --git a/xrspatial/accessor.py b/xrspatial/accessor.py index 55eae0310..590ba462b 100644 --- a/xrspatial/accessor.py +++ b/xrspatial/accessor.py @@ -2379,7 +2379,9 @@ def _delegated_doc(method_name): """Return the docstring to surface for an accessor method, or None.""" if method_name == 'min_observable_height': from .experimental.min_observable_height import min_observable_height - return min_observable_height.__doc__ + return ('Experimental: not part of the 1.x LTS surface, and its ' + 'signature or output may change in a minor release.\n\n' + + (min_observable_height.__doc__ or '')) import xrspatial source_name = _DOC_SOURCE_OVERRIDES.get(method_name, method_name) func = getattr(xrspatial, source_name, None) diff --git a/xrspatial/tests/test_public_surface.py b/xrspatial/tests/test_public_surface.py new file mode 100644 index 000000000..f8f4590d1 --- /dev/null +++ b/xrspatial/tests/test_public_surface.py @@ -0,0 +1,71 @@ +"""The README feature matrix and ``xrspatial.__all__`` describe the same +surface (issue #2930). A row without an export, or an export without a +row, fails here so the two cannot drift apart. +""" +import re +from pathlib import Path + +import xrspatial +import xrspatial.mcda + +README = Path(xrspatial.__file__).resolve().parents[1] / "README.md" + +# Exported names that are not spatial operations and so have no matrix row. +EXPORTS_WITHOUT_ROW = { + "LANDFORM_CLASSES", # constant backing the Landforms row + "mcda", # subpackage; its functions are checked against xrspatial.mcda +} + +_ROW = re.compile(r"^\|\s*\[([^\]]+)\]\(([^)]+)\)\s*(?:`([^`]+)`)?") + + +def _row_name(label, path, override): + if override: + return override + m = re.search(r"\(([^()]+)\)\s*$", label) + if m: + return m.group(1).lower() + return re.sub(r"[\s/-]+", "_", label.strip()).lower() + + +def _matrix_rows(): + """Yield (section, function name, link path) for each function row.""" + text = README.read_text(encoding="utf-8") + start = text.index("#### Supported Spatial Functions") + section = None + for line in text[start:].splitlines(): + if line.startswith("### "): + section = line.strip("# *") + continue + m = _ROW.match(line) + if m and m.group(2).startswith("xrspatial/"): + yield section, _row_name(*m.groups()), m.group(2) + + +def test_readme_matrix_matches_public_surface(): + top_rows, mcda_rows = {}, {} + for section, name, path in _matrix_rows(): + if path.startswith("xrspatial/experimental/"): + continue # experimental: documented, deliberately not exported + (mcda_rows if "MCDA" in section else top_rows)[name] = path + + exports = set(xrspatial.__all__) - EXPORTS_WITHOUT_ROW + mcda_exports = set(xrspatial.mcda.__all__) + + missing_export = sorted(n for n in top_rows if n not in exports) + missing_row = sorted(exports - set(top_rows)) + mcda_missing_export = sorted(n for n in mcda_rows if n not in mcda_exports) + + assert not missing_export, ( + "README rows with no matching name in xrspatial.__all__ " + f"(add the export or a `name` override on the row): {missing_export}") + assert not missing_row, ( + "xrspatial.__all__ names with no README matrix row " + f"(add a row or list it in EXPORTS_WITHOUT_ROW): {missing_row}") + assert not mcda_missing_export, ( + f"MCDA README rows not in xrspatial.mcda.__all__: {mcda_missing_export}") + + +def test_all_names_resolve(): + for name in xrspatial.__all__: + assert hasattr(xrspatial, name), name From afce0acf4ace793ef9e3c7e1a009fa9dca8bebfc Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:19:52 -0500 Subject: [PATCH 2/4] Address review: match README row text to the docstrings, skip the parity test outside a checkout (#2930) --- README.md | 8 ++++---- xrspatial/tests/test_public_surface.py | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9a87752b0..4ba84f9d3 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for per-pixel coord | [fused_overlap](xrspatial/utils.py) | Fuse sequential map_overlap calls into a single pass | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [multi_overlap](xrspatial/utils.py) | Run multi-output kernel in a single overlap pass | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [validate](xrspatial/validate.py) | Check a raster against the xarray-spatial input contract (`.xrs.validate()`) | Custom | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [diagnose](xrspatial/diagnostics.py) | Report which xarray-spatial tools a raster is ready for and what blocks the rest | Custom | ๐Ÿ”ผ | | | | +| [diagnose](xrspatial/diagnostics.py) | Heuristic checks for common pitfalls before running a tool; `tool=` narrows the checks to one function | Custom | ๐Ÿ”ผ | | | | ----------- @@ -418,7 +418,7 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | [Proximity](xrspatial/proximity.py) | Computes the distance from each cell to the nearest source feature | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Euclidean Distance](xrspatial/proximity.py) `euclidean_distance` | Straight-line distance between two points; the scalar metric behind `proximity` (backend-independent) | Standard | ๐Ÿ”ผ | | | | | [Manhattan Distance](xrspatial/proximity.py) `manhattan_distance` | City-block distance between two points (backend-independent) | Standard | ๐Ÿ”ผ | | | | -| [Great-Circle Distance](xrspatial/proximity.py) `great_circle_distance` | Haversine distance between two lon/lat points in meters (backend-independent) | Haversine | ๐Ÿ”ผ | | | | +| [Great-Circle Distance](xrspatial/proximity.py) `great_circle_distance` | Haversine distance between two lon/lat points, in the unit of `radius` (meters by default) (backend-independent) | Haversine | ๐Ÿ”ผ | | | | | [Surface Distance](xrspatial/surface_distance.py) | Computes distance along the 3D terrain surface to the nearest source | Standard (Dijkstra) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Surface Allocation](xrspatial/surface_distance.py) | Assigns each cell to the nearest source by terrain surface distance | Standard (Dijkstra) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Surface Direction](xrspatial/surface_distance.py) | Computes compass direction to the nearest source by terrain surface distance | Standard (Dijkstra) | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | @@ -436,8 +436,8 @@ For a broader catalog of spectral indices and sensor-specific band combinations, | [Trim](xrspatial/zonal.py) | Removes nodata border rows and columns from a raster | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Zonal Statistics](xrspatial/zonal.py) `zonal_stats` | Computes summary statistics for a value raster within each zone | Standard | โœ… | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | | [Zonal Cross Tabulate](xrspatial/zonal.py) `zonal_crosstab` | Cross-tabulates agreement between two categorical rasters | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Hypsometric Integral](xrspatial/zonal.py) `hypsometric_integral` | Elevation-relief ratio of each zone (Strahler hypsometric integral) | Strahler 1952 | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | -| [Suggest Zonal Canvas](xrspatial/zonal.py) `suggest_zonal_canvas` | Pick a raster extent and cell size for rasterizing zone polygons (backend-independent) | Custom | ๐Ÿ”ผ | | | | +| [Hypsometric Integral](xrspatial/zonal.py) `hypsometric_integral` | Hypsometric integral `(mean - min) / (max - min)` of the elevations in each zone, painted back to a raster | Standard | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | ๐Ÿ”ผ | +| [Suggest Zonal Canvas](xrspatial/zonal.py) `suggest_zonal_canvas` | Canvas `(height, width)` in pixels so the smallest zone polygon covers at least `min_pixels` cells (backend-independent) | Custom | ๐Ÿ”ผ | | | | ----------- diff --git a/xrspatial/tests/test_public_surface.py b/xrspatial/tests/test_public_surface.py index f8f4590d1..2b2505c7a 100644 --- a/xrspatial/tests/test_public_surface.py +++ b/xrspatial/tests/test_public_surface.py @@ -5,6 +5,8 @@ import re from pathlib import Path +import pytest + import xrspatial import xrspatial.mcda @@ -43,6 +45,8 @@ def _matrix_rows(): def test_readme_matrix_matches_public_surface(): + if not README.exists(): + pytest.skip("README.md is only present in a repository checkout") top_rows, mcda_rows = {}, {} for section, name, path in _matrix_rows(): if path.startswith("xrspatial/experimental/"): From 3fed93bbfd1d32664ea6b2ac6bdcc46c768cff2c Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:33:41 -0500 Subject: [PATCH 3/4] Import _check_numpy_version from its module in the floor test (#2930) --- xrspatial/tests/test_numpy_floor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrspatial/tests/test_numpy_floor.py b/xrspatial/tests/test_numpy_floor.py index 98dc9252b..3ca9cacdb 100644 --- a/xrspatial/tests/test_numpy_floor.py +++ b/xrspatial/tests/test_numpy_floor.py @@ -8,7 +8,7 @@ """ import pytest -from xrspatial import _check_numpy_version +from xrspatial._numpy_floor import _check_numpy_version @pytest.mark.parametrize("version", ["1.26.0", "1.26.4", "1.26.0.dev0+abc", "1.99.9"]) From 61ffbcb0031caf0281184bb5e9d81bb3cff0f290 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:47:07 -0500 Subject: [PATCH 4/4] Drop the removed top-level basins alias from the hydro public-API test (#2930) --- xrspatial/hydro/tests/test_routing_public_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrspatial/hydro/tests/test_routing_public_api.py b/xrspatial/hydro/tests/test_routing_public_api.py index de3962ba7..2f4b64e9c 100644 --- a/xrspatial/hydro/tests/test_routing_public_api.py +++ b/xrspatial/hydro/tests/test_routing_public_api.py @@ -17,7 +17,7 @@ WRAPPERS = [ 'flow_direction', 'flow_accumulation', 'flow_length', 'flow_path', 'watershed', 'hand', 'stream_link', 'stream_order', - 'basin', 'basins', 'sink', 'snap_pour_point', 'fill', 'twi', + 'basin', 'sink', 'snap_pour_point', 'fill', 'twi', ] # Representative suffixed implementations across every family.