xarray-spatial 0.10.2__tar.gz → 0.10.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xarray_spatial-0.10.4/.claude/sweep-accuracy-state.csv +39 -0
- xarray_spatial-0.10.4/.claude/sweep-performance-state.csv +49 -0
- xarray_spatial-0.10.4/.claude/sweep-security-state.csv +49 -0
- xarray_spatial-0.10.4/.claude/sweep-style-state.csv +14 -0
- xarray_spatial-0.10.4/.claude/sweep-test-coverage-state.csv +91 -0
- xarray_spatial-0.10.4/.codex/commands/backend-parity.md +159 -0
- xarray_spatial-0.10.4/.codex/commands/bench.md +127 -0
- xarray_spatial-0.10.4/.codex/commands/dask-notebook.md +148 -0
- xarray_spatial-0.10.4/.codex/commands/deep-sweep.md +438 -0
- xarray_spatial-0.10.4/.codex/commands/efficiency-audit.md +274 -0
- xarray_spatial-0.10.4/.codex/commands/new-issues.md +113 -0
- xarray_spatial-0.10.4/.codex/commands/ready-to-merge.md +153 -0
- xarray_spatial-0.10.4/.codex/commands/release-major.md +109 -0
- xarray_spatial-0.10.4/.codex/commands/release-minor.md +109 -0
- xarray_spatial-0.10.4/.codex/commands/release-patch.md +140 -0
- xarray_spatial-0.10.4/.codex/commands/review-contributor-pr.md +332 -0
- xarray_spatial-0.10.4/.codex/commands/review-pr.md +249 -0
- xarray_spatial-0.10.4/.codex/commands/rockout.md +380 -0
- xarray_spatial-0.10.4/.codex/commands/sweep-accuracy.md +335 -0
- xarray_spatial-0.10.4/.codex/commands/sweep-api-consistency.md +291 -0
- xarray_spatial-0.10.4/.codex/commands/sweep-metadata.md +334 -0
- xarray_spatial-0.10.4/.codex/commands/sweep-performance.md +366 -0
- xarray_spatial-0.10.4/.codex/commands/sweep-security.md +334 -0
- xarray_spatial-0.10.4/.codex/commands/sweep-style.md +316 -0
- xarray_spatial-0.10.4/.codex/commands/sweep-test-coverage.md +293 -0
- xarray_spatial-0.10.4/.codex/commands/user-guide-notebook.md +203 -0
- xarray_spatial-0.10.4/.codex/commands/validate.md +216 -0
- xarray_spatial-0.10.4/.codex/sweep-api-consistency-state.csv +10 -0
- xarray_spatial-0.10.4/.codex/sweep-metadata-state.csv +12 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.gitattributes +1 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.gitignore +2 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/CHANGELOG.md +94 -0
- {xarray_spatial-0.10.2/xarray_spatial.egg-info → xarray_spatial-0.10.4}/PKG-INFO +1 -1
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/setup.cfg +1 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4/xarray_spatial.egg-info}/PKG-INFO +1 -1
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xarray_spatial.egg-info/SOURCES.txt +35 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/_version.py +3 -3
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/aspect.py +97 -47
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/contour.py +97 -19
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/convolution.py +6 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/focal.py +370 -154
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geodesic.py +29 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/__init__.py +51 -10
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_backends/dask.py +22 -6
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_backends/gpu.py +22 -4
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_backends/vrt.py +31 -10
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_errors.py +22 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_validation.py +103 -2
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_writers/eager.py +11 -3
- xarray_spatial-0.10.4/xrspatial/geotiff/tests/_geotiff_fixtures.py +118 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/release_gates/test_features.py +5 -0
- xarray_spatial-0.10.4/xrspatial/geotiff/tests/test_stable_only_bbox_ordering_2869.py +196 -0
- xarray_spatial-0.10.4/xrspatial/geotiff/tests/test_stable_only_remote_2821.py +183 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_geotags.py +2 -82
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_input_validation.py +80 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_metadata.py +4 -85
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/test_parity.py +55 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/gpu_rtx/__init__.py +1 -1
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/gpu_rtx/hillshade.py +20 -11
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/gpu_rtx/mesh_utils.py +68 -17
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/gpu_rtx/viewshed.py +22 -15
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_accumulation_mfd.py +59 -26
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_length_mfd.py +83 -31
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_path_mfd.py +33 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/hand_mfd.py +12 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/stream_link_mfd.py +8 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/stream_order_mfd.py +8 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_accumulation_mfd.py +93 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_length_mfd.py +82 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_path_mfd.py +38 -0
- xarray_spatial-0.10.4/xrspatial/hydro/tests/test_validate_mfd_companion_shape.py +131 -0
- xarray_spatial-0.10.4/xrspatial/hydro/tests/test_validate_mfd_fractions.py +188 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_watershed_mfd.py +70 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/watershed_mfd.py +41 -26
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/interpolate/_kriging.py +26 -11
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/interpolate/_spline.py +29 -8
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/polygonize.py +102 -13
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/proximity.py +369 -49
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/slope.py +64 -17
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/general_checks.py +8 -6
- xarray_spatial-0.10.4/xrspatial/tests/test_aspect.py +594 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_contour.py +365 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_convolution.py +13 -1
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_dask_laziness.py +34 -2
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_focal.py +604 -22
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_geodesic_aspect.py +62 -1
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_geodesic_slope.py +202 -2
- xarray_spatial-0.10.4/xrspatial/tests/test_gpu_rtx_has_rtx.py +50 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_gpu_rtx_mesh.py +96 -4
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_interpolation.py +359 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_northness_eastness.py +129 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize.py +169 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_mask_dtype_coverage_2026_05_29.py +13 -15
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_proximity.py +625 -0
- xarray_spatial-0.10.4/xrspatial/tests/test_slope.py +627 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_utils.py +93 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_viewshed.py +266 -16
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/utils.py +199 -6
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/viewshed.py +85 -23
- xarray_spatial-0.10.2/xrspatial/tests/test_aspect.py +0 -251
- xarray_spatial-0.10.2/xrspatial/tests/test_slope.py +0 -276
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/backend-parity.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/bench.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/dask-notebook.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/deep-sweep.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/efficiency-audit.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/new-issues.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/ready-to-merge.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/release-major.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/release-minor.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/release-patch.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/review-contributor-pr.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/review-pr.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/rockout.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/sweep-accuracy.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/sweep-api-consistency.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/sweep-metadata.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/sweep-performance.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/sweep-security.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/sweep-style.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/sweep-test-coverage.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/user-guide-notebook.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/commands/validate.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/sweep-api-consistency-state.csv +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.claude/sweep-metadata-state.csv +0 -0
- {xarray_spatial-0.10.2/.claude → xarray_spatial-0.10.4/.codex}/sweep-accuracy-state.csv +0 -0
- {xarray_spatial-0.10.2/.claude → xarray_spatial-0.10.4/.codex}/sweep-performance-state.csv +0 -0
- {xarray_spatial-0.10.2/.claude → xarray_spatial-0.10.4/.codex}/sweep-security-state.csv +0 -0
- {xarray_spatial-0.10.2/.claude → xarray_spatial-0.10.4/.codex}/sweep-style-state.csv +0 -0
- {xarray_spatial-0.10.2/.claude → xarray_spatial-0.10.4/.codex}/sweep-test-coverage-state.csv +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.efficiency-audit-baseline.json +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.efficiency-audit-baseline.prev.json +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/ISSUE_TEMPLATE/feature-proposal.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/ISSUE_TEMPLATE/new-contributor.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/labeler.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/pull_request_template.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/benchmarks.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/copilot-review.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/labeler.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/pypi-publish.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/test-cog-validator.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/test-geotiff-corpus.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/test.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.github/workflows/welcome-contributor.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/.readthedocs.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/AI_POLICY.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/CLAUDE.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/CODE_OF_CONDUCT.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/CONTRIBUTING.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/Citation-styles.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/LICENSE.txt +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/MANIFEST.in +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/README.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/RELEASE.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/codecov.yml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/pyproject.toml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/setup.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xarray_spatial.egg-info/dependency_links.txt +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xarray_spatial.egg-info/entry_points.txt +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xarray_spatial.egg-info/not-zip-safe +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xarray_spatial.egg-info/requires.txt +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xarray_spatial.egg-info/top_level.txt +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/__main__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/accessor.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/analytics.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/balanced_allocation.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/bilateral.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/bump.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/classify.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/corridor.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/cost_distance.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/curvature.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/dasymetric.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/dataset_support.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/datasets/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/datasets/sentinel-2/blue_band.nc +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/datasets/sentinel-2/green_band.nc +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/datasets/sentinel-2/nir_band.nc +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/datasets/sentinel-2/red_band.nc +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/datasets/sentinel-2/swir1_band.nc +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/datasets/sentinel-2/swir2_band.nc +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/diagnostics.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/diffusion.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/edge_detection.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/emerging_hotspots.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/erosion.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/experimental/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/experimental/min_observable_height.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/fire.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/flood.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_attrs.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_backends/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_backends/_gpu_helpers.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_cog_http.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_compression.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_coords.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_crs.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_decode.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_dtypes.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_encode.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_geotags.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_gpu_decode.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_header.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_layout.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_nodata.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_overview.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_overview_kernels.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_reader.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_runtime.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_safe_xml.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_sidecar.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_sources.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_vrt.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_vrt_validation.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_write_layout.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_writer.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_writers/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_writers/gpu.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/_writers/vrt.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/_helpers/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/_helpers/markers.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/_helpers/tiff_builders.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/_helpers/tiff_surgery.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/attrs/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/attrs/test_contract.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/bench_vs_rioxarray.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/conftest.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/README.md +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/_marks.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/_oracle.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/cog_internal_overview_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_deflate_predictor2_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_deflate_predictor3_float32.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_jpeg_uint8_ycbcr.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_lerc_float32.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_lzw_predictor2_int16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_none_uint8.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/crs_citation_only.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/crs_epsg_3857.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/crs_wkt_utm10n.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float32.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float64.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int32.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int8.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint32.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint8.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/extra_tags_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/gdal_metadata_namespaced_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/nodata_int_sentinel_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/nodata_miniswhite_uint8.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/nodata_nan_float32.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/overview_external_ovr_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/overview_external_ovr_uint16.tif.ovr +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/overview_internal_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/planar_separate_uint8_rgb.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/sparse_tiled_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/stripped_be_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/stripped_le_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/tiled_be_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/fixtures/tiled_le_uint16.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/generate.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/manifest.yaml +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_compression.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_corpus_determinism.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_dask_gpu.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_dask_numpy.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_dtype_variants.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_eager_numpy.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_fsspec.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_gpu.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_http.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_layout_endian.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_manifest.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_metadata_tags.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_nodata_sentinels.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_oracle.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_overview_cog.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/golden_corpus/test_vrt.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/gpu/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/gpu/test_codec.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/gpu/test_reader.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/gpu/test_writer.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/integration/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/integration/test_dask_pipeline.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/integration/test_gpu_pipeline.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/integration/test_http_sources.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/integration/test_sidecar.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/parity/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/parity/test_backend_matrix.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/parity/test_finalization.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/parity/test_pixel_equality.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/parity/test_reference.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/parity/test_signature_contract.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_basic.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_bbox_2555.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_bbox_vrt_2668.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_compression.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_coords.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_crs.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_dtypes.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_endianness.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_georef.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_nodata.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_overview.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_streaming.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/read/test_tiling.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/release_gates/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/release_gates/test_stable_features.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/test_edge_cases.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/test_fuzz_hypothesis.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/test_polish.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/test_round_trip.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/test_security.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/test_shutdown_cleanup_2486.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_codec_roundtrip.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_compression.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_header.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_ifd.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_photometric.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_predictor.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_safe_xml.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/unit/test_signatures.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/test_dtype_conversion.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/test_metadata.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/test_missing_sources.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/test_source_opt_ins_2672.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/test_validation.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/vrt/test_window.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_basic.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_bigtiff.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_cog.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_crs.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_nodata.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_overview.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_streaming.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/geotiff/tests/write/test_vrt_atomic.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/glcm.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/gpu_rtx/_memory.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/gpu_rtx/cuda_utils.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hillshade.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/_boundary_store.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/basin_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/fill_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_accumulation_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_accumulation_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_direction_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_direction_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_direction_mfd.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_length_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_length_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_path_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/flow_path_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/hand_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/hand_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/sink_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/snap_pour_point_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/stream_link_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/stream_link_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/stream_order_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/stream_order_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/conftest.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_basin_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_fill_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_accumulation_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_accumulation_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_direction_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_direction_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_direction_mfd.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_length_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_length_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_path_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_flow_path_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_hand_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_hand_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_hand_mfd.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_sink_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_snap_pour_point_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_stream_link_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_stream_link_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_stream_link_mfd.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_stream_order_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_stream_order_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_stream_order_mfd.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_twi_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_validate_cellsize.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_validate_scalar_params.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_validate_secondary_args.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_watershed_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/tests/test_watershed_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/twi_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/watershed_d8.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/hydro/watershed_dinf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/interpolate/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/interpolate/_idw.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/interpolate/_validation.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/kde.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/mahalanobis.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/mcda/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/mcda/combine.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/mcda/constrain.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/mcda/sensitivity.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/mcda/standardize.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/mcda/weights.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/morphology.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/multispectral.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/normalize.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/pathfinding.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/perlin.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/polygon_clip.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/preview.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/rasterize.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_crs_utils.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_datum_grids.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_grid.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_interpolate.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_itrf.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_lite_crs.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_merge.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_projections.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_projections_cuda.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_transform.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/_vertical.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/at_bev_AT_GIS_GRID.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/au_icsm_A66_National_13_09_01.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/be_ign_bd72lb72_etrs89lb08.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/ch_swisstopo_CHENyx06_ETRS.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/de_adv_BETA2007.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/es_ign_SPED2ETV2.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/nl_nsgi_rdcorr2018.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/pt_dgt_D73_ETRS89_geo.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/uk_os_OSTN15_NTv2_OSGBtoETRS.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/us_nga_egm96_15.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/us_noaa_alaska.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/us_noaa_conus.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/us_noaa_hawaii.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/us_noaa_nadcon5_nad27_nad83_1986_conus.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/reproject/grids/us_noaa_prvi.tif +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/resample.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/sieve.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/sky_view_factor.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/surface_distance.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/terrain.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/terrain_metrics.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/__init__.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/bench_reproject_vs_rioxarray.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/conftest.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_accessor.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_analytics.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_balanced_allocation.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_bilateral.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_bump.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_classify.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_corridor.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_cost_distance.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_curvature.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_dask_cupy_gaps.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_dasymetric.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_dataset_support.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_datasets.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_diagnostics.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_diffusion.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_edge_detection.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_emerging_hotspots.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_erosion.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_fire.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_flood.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_fused_overlap.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_glcm.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_glcm_metric_order.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_gpu_rtx_memory.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_hillshade.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_hypsometric_integral.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_kde.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_lite_crs.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_mahalanobis.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_mcda.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_min_observable_height.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_morphology.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_morphology_derived.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_multi_overlap.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_multispectral.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_normalize.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_optional_shapely.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_pathfinding.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_perlin.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygon_clip.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_atol_rtol_backend_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_coverage_2026_05_19.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_dask_row_batch_2608.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_issue_2172.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_issue_2583.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_issue_2606.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_polygonize_issue_2666.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_preview.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_accuracy.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_all_touched_supercover_2169.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_coverage_2026_05_17.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_coverage_2026_05_21.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_coverage_2026_05_29.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_descending_x_2568.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_gpu_race_2167.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_nan_int_fill_2504.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_nan_propagation_2255.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_partial_dims_2569.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_props_hoist_2506.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_resolution_exact_2573.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_resolution_validation_2576.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_signature_annot_2250.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_signed_step_2566.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rasterize_tile_props_slice_2020.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_rechunk_no_shuffle.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_reproject.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_reproject_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_reproject_cupy_gate_2564.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_resample.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_resample_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_resample_cupy_agg_fallback_2615.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_resample_input_validation_2574.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_resample_irregular_coords_2663.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_resample_signature_annot_2544.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_sieve.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_sieve_gdal_parity.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_sky_view_factor.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_surface_distance.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_terrain.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_terrain_metrics.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_validation.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_visibility.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_zonal.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/tests/test_zonal_backend_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/visibility.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/worley.py +0 -0
- {xarray_spatial-0.10.2 → xarray_spatial-0.10.4}/xrspatial/zonal.py +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module,last_inspected,issue,severity_max,categories_found,notes
|
|
2
|
+
aspect,2026-06-02,2827,MEDIUM,5,"Cat5 backend divergence: planar cupy _gpu snapped aspect>359.999 to 0 (no such clamp in numpy _cpu, whose range is [0,360) and never reaches 360), so cupy/dask+cupy disagreed with numpy by ~360 on near-degenerate gradients (gx~0+, gy>0). Removing the clamp exposed a 2nd divergence: GPU used coarse 57.29578 vs numpy 180/pi, flipping the >90 compass branch and yielding exact 360 vs 0 on uint32/uint64 random data. Fix #2827/PR #2833: GPU reuses RADIAN and wraps >=360 back to [0,360). Cats 1-4 clean; geodesic path canonicalizes consistently on CPU+GPU and was left untouched. CUDA available; cupy+dask+cupy verified (235 tests pass, numpy-vs-cupy max abs diff 0 over 360 rasters). Dedup: prior aspect fixes #2780 (cellsize)/#2774 (dask mem guard)/#2781 (oracle) all merged and unrelated. Note: PR review COMMENT could not be posted to GitHub (auto-mode permission denial); findings recorded in PR run instead."
|
|
3
|
+
balanced_allocation,2026-04-14T12:00:00Z,1203,,,float32 allocation array caused source ID mismatch for non-integer IDs. Fix in PR #1205.
|
|
4
|
+
bilateral,2026-05-01,,,,"No CRIT/HIGH/MEDIUM. Sigma underflow validated via sqrt(tiny) bound; oversize sigma clamped. float64 throughout numpy/cupy. NaN center returns NaN; NaN neighbors skipped (denom not incremented). w_sum>0 guard avoids div-by-zero. map_overlap depth==kernel radius. CUDA bounds correct. Inf input could yield 0*inf=NaN in v_sum but unvalidated input is general xrspatial pattern, not bilateral-specific."
|
|
5
|
+
contour,2026-05-01,,,,"Marching squares correct: NaN check uses self-inequality, loop bounds (ny-1,nx-1) cover all quads, dask overlap depth=1 matches 2x2 stencil, float64 cast consistent across backends, saddle disambiguation via center value. No CRIT/HIGH issues; minor LOW (Inf inputs not specifically rejected) not flagged."
|
|
6
|
+
corridor,2026-05-01,,LOW,1,"LOW: corridor inherits float32 from cost_distance; for very large accumulated costs, normalized = corridor - corridor_min loses precision near min (intrinsic to upstream dtype, not corridor itself). NaN handling correct (skipna min, np.isfinite check before normalize). All 4 backends route through pure xarray arithmetic; threshold uses dask/cupy/numpy where with try/except dispatch. No CRIT/HIGH issues."
|
|
7
|
+
cost_distance,2026-04-13T12:00:00Z,1191,,,CuPy Bellman-Ford max_iterations = h+w instead of h*w. Fix in PR #1192.
|
|
8
|
+
curvature,2026-03-30T15:00:00Z,,,,Formula matches ArcGIS reference. Backends consistent. No issues found.
|
|
9
|
+
dasymetric,2026-04-14T12:00:00Z,,,,Mass conservation correct. Weighted/binary/limiting_variable all verified. Pycnophylactic Tobler algorithm correct.
|
|
10
|
+
diffusion,2026-05-01,,LOW,1;2;5,"LOW: no Kahan summation across long iterations (drift over 100k steps, standard for explicit Euler); lap=n+s+w+e-4*val has catastrophic cancellation for nearly-uniform large values; res=0 in attrs causes div-by-zero (no guard); dask+cupy boundary='nan' relies on dask accepting cp.nan as fill. CPU/GPU NaN handling consistent (np.isnan vs val!=val). depth=1 matches stencil radius. Memory guards, CFL check, step cap all in place. No CRIT/HIGH."
|
|
11
|
+
edge_detection,2026-05-01,,,,Thin wrappers around convolve_2d with fixed Sobel/Prewitt/Laplacian kernels; no issues found
|
|
12
|
+
emerging_hotspots,2026-04-30,,MEDIUM,2;3,MEDIUM: threshold_90 uses int() (truncation) instead of ceil() so n_times=11 requires only 9/11 (81.8%) instead of 90%. MEDIUM: NaN time steps produce gi_bin=0 which classifier counts as 'non-significant' rather than missing; threshold_90 uses full n_times not valid count. LOW: 'global_std == 0' check does not catch NaN std for fully/mostly NaN inputs.
|
|
13
|
+
fire,2026-04-30,,,,All ops per-pixel (no accumulation/stencil/projected distance). NaN handled via x!=x; CUDA bounds use strict <; rdnbr and ros divisions guarded; CPU/GPU/dask paths algorithmically identical. No accuracy issues found.
|
|
14
|
+
flood,2026-04-30,,MEDIUM,2;5,"MEDIUM (not fixed): dask backend preserves float32 input dtype while numpy promotes to float64 in flood_depth and curve_number_runoff; DataArray inputs for curve_number, mannings_n bypass scalar > 0 (and CN <= 100) range validation, silently producing NaN/garbage."
|
|
15
|
+
focal,2026-06-02,2831,MEDIUM,1;5,"GPU focal_stats std/var used one-pass E[x^2]-E[x]^2 variance in float32; catastrophic cancellation collapsed std/var toward 0 on large-offset rasters (~1e6-1e7), diverging from float64 two-pass numpy/dask. Fixed cupy + dask+cupy via two-pass kernel (issue #2831, PR pending). hotspots() Gi* rewrite (#2803), dask laziness (#2802), float-dtype (#2805), GPU variety (#2800), kernel/stats_funcs validation (#2799/#2798), cupy boundary (#2736) all verified consistent across 4 backends. Cat1+Cat5."
|
|
16
|
+
geotiff,2026-05-15,1975,HIGH,1;2;5,"Pass 25 (2026-05-15): HIGH fixed -- issue #1975. _block_reduce_2d's cubic branch in xrspatial/geotiff/_writer.py gated the sentinel-to-NaN mask on arr2d.dtype.kind=='f', so to_geotiff(cog=True, overview_resampling='cubic', nodata=<finite>) on an integer raster fell through to an unmasked zoom(arr2d, 0.5, order=3). The bicubic spline blended the sentinel (e.g. -9999) into neighbouring valid cells; cast back to the source integer dtype, the boundary pixels surfaced as silent garbage. Reproduction (1024x1024 int16 + 256x256 nodata corner + nodata=-9999): lvl1 boundary [128, 124:132] showed [1082, 1082, 1085, 1134, 5, 93, 100, 100] instead of [-9999/NaN, ..., 100, 100, 100, 100]; max poisoned value 1134 (11x the actual data value of 100) and min -11104 (below the sentinel -9999). Same root cause as #1623 (float cubic + nodata) but for the integer dtype branch. Both CPU and GPU writers affected because _block_reduce_2d_gpu's cubic path falls back to _block_reduce_2d on CPU. Fix mirrors the float branch: promote the cropped block to float64, mask sentinel to NaN via the integer-range guard (mirrors _int_nodata_in_range), run scipy.ndimage.zoom(prefilter=False), rewrite NaN back to the sentinel, then np.round(...).astype(source_int_dtype) so the integer cast is well-defined. 12 regression tests in test_cog_cubic_int_overview_nodata_1975.py: helper-level cubic per int dtype (int16, uint16, int32), no-nodata regression, out-of-range sentinel no-op, fractional sentinel no-op, all-sentinel block fallback, float cubic regression guard, end-to-end 1024x1024 round-trip, non-constant int regression, cubic-vs-mean sentinel-mask parity, and GPU/CPU byte parity. All 3186 non-stale geotiff tests still pass (2 pre-existing failures unrelated: test_predictor2_big_endian_gpu references the hidden read_to_array symbol, and test_size_param_validation_gpu_vrt_1776 asserts pre-#1767 tile_size=4 behaviour). Categories: Cat 1 (precision loss from cubic spline blending sentinel into valid cells) + Cat 2 (NaN-equivalent corruption: the read-side int-to-NaN mask only catches exact sentinel hits, so the poisoned values survive as legitimate measurements) + Cat 5 (backend parity: CPU and GPU writers shared the same wrong cubic path). | Pass 23 (2026-05-14): HIGH fixed -- issue #1847. extract_geo_info parsed GDAL_NODATA via float() unconditionally, which loses 1 ULP on uint64 max (2**64-1) and int64 max (2**63-1). The downstream integer-mask gate info.min <= int(nodata) <= info.max then rejects the cast because float-rounded sentinel is one above the dtype max; the sentinel pixel survives as a literal valid integer instead of NaN. Same float-only parse in _reader._resolve_masked_fill (LERC fill) and _reader._sparse_fill_value (SPARSE_OK fill). VRT _vrt._parse_band_nodata had already fixed this for the XML parse path (PR #1833) but TIFF source-of-truth was never updated, so write_vrt([uint64.tif]) stringified the float-parsed nodata as '1.8446744073709552e+19' into XML where the VRT reader then rejected it for being out of range. Fix: lift the int-first parse into shared helper _parse_nodata_str in _geotags.py and reuse across the three TIFF-side sites. The helper tries int(text) first to preserve full precision, falls back to float(text) for NaN/Inf/scientific/fractional. Downstream gates already handle int values transparently because np.isfinite(int) works and int(int) is a no-op. 25 regression tests in test_nodata_int64_precision_1847.py: unit-level _parse_nodata_str matrix (int vs float branches, edge cases), eager open_geotiff (uint64 max / int64 max / int64 min / uint16 / int32 / float regression guards), read_geotiff_dask (uint64 max, int64 max), write_vrt + read_vrt round-trip with XML literal assertion, and a GPU parity test. All 2434 non-stale geotiff tests still pass (1 pre-existing test_size_param_validation_gpu_vrt_1776 failure unrelated -- test asserts pre-#1767 tile_size=4 behaviour). Categories: Cat 2 (NaN propagation: sentinel pixel survived as literal valid number on all 4 backends) + Cat 5 (backend inconsistency: VRT XML parse path handled 64-bit sentinels via _parse_band_nodata but TIFF parse path did not, even though write_vrt fed the latter into the former). Audited but did not file: LOW silent kwarg drop -- to_geotiff(da, 'out.vrt', photometric='miniswhite') drops the photometric arg at _write_vrt_tiled call (per-tile files written as MinIsBlack). Data round-trips correctly because no inversion happens on either side; only the tile photometric tag disagrees with the user's request. Niche path + no data corruption + metadata-only drift = LOW, not filed. | Pass 22 (2026-05-13): HIGH fixed -- issue #1809. MinIsWhite (photometric=0) inversion ran before the sentinel-to-NaN nodata mask on all four backends (eager numpy in open_geotiff, dask chunk reader, eager GPU in read_geotiff_gpu, GPU stripped fallback). Because the inversion rewrites the original sentinel value (e.g. uint8 nodata=0 becomes 255, float32 nodata=-9999 becomes 9999), the post-inversion mask matched the wrong pixels: cells whose stored value happened to equal iinfo.max - sentinel were flagged NaN while real sentinel cells survived as inverted values. PR #1804 (a5d78e4) had refactored the helper but kept the original ordering. Fix: introduce _miniswhite_inverted_nodata in _reader.py and stash the inverted sentinel on geo_info._mask_nodata; route every backend mask through that field, keeping geo_info.nodata + attrs[nodata] at the original value for write-side round-trip. Dask path also re-inverts the closure nodata at graph-build time, picking up _ifd_photometric / _ifd_samples_per_pixel stashed in _read_geo_info. 9 regression tests in test_miniswhite_nodata_1809.py cover uint8 nodata=0, uint16 nodata=65535, float32 nodata=-9999 across numpy, dask, and GPU backends plus no-collision and no-nodata controls. All 2424 non-stale geotiff tests pass (4 pre-existing failures unrelated to this fix). Categories: Cat 2 (NaN propagation: real data became NaN while sentinel survived as inverted value) + Cat 5 (backend inconsistency: all four backends share the identical wrong result, so they agreed on the wrong answer rather than diverged). | Pass 21 (2026-05-13): MEDIUM fixed -- issue #1774. open_geotiff / read_geotiff_dask / _apply_nodata_mask_gpu crashed with ValueError: cannot convert float NaN to integer when reading an integer TIFF whose GDAL_NODATA tag was the string ""nan"" / ""inf"" / ""-inf"". Three sites in xrspatial/geotiff/__init__.py called int(nodata) on the integer-dtype branch without first checking np.isfinite. _geotags.py:extract_geo_info parses the GDAL_NODATA tag through float(nodata_str) so a ""nan"" tag surfaces as Python NaN; the integer mask code then explodes. Sibling helpers _resolve_masked_fill and _sparse_fill_value in _reader.py already gate on not math.isnan(v) and not math.isinf(v) (the unfinished pass of #1581). Fix: gate each int(nodata) cast on np.isfinite(nodata). A non-finite sentinel on an integer file cannot match any pixel, so the mask is a no-op and the file dtype is preserved; attrs['nodata'] still carries the raw NaN/Inf sentinel so a write round-trip keeps the original GDAL_NODATA tag. The read_geotiff_dask effective_dtype branch already used try/except and was safe in practice, but tightened with the same isfinite gate for readability. 15 regression tests in test_nodata_nan_int_1774.py covering eager numpy (3 NaN variants + 6 Inf variants), in-range finite still masks regression guard, dask (NaN + Inf), and GPU (NaN + Inf + finite). All pass; 2023 existing geotiff tests still pass (7 pre-existing test_predictor2_big_endian_gpu failures unrelated: they reference xrspatial.geotiff.read_to_array which was hidden from the public namespace in #1708, 3 pre-existing matplotlib palette failures in test_features.py unrelated). Categories: Cat 2 (NaN propagation: NaN nodata produced a crash instead of being treated as missing) + Cat 5 (backend inconsistency: _resolve_masked_fill / _sparse_fill_value already guarded; the three __init__.py sites did not). | Pass 20 (2026-05-12): HIGH fixed -- PR #1691 (no issue created; agent harness blocked gh issue create). Integer COG overview pyramid mixed sentinel into reduced pixels. _block_reduce_2d (_writer.py:258-264) and _block_reduce_2d_gpu (_gpu_decode.py:3027-3028) promoted integer blocks to float64 but never masked the sentinel to NaN before nanmean / nanmin / nanmax / nanmedian. The reduction averaged the sentinel into surrounding valid cells (e.g. (-9999 + 100 + 100 + 100)/4 = -2425 cast back to int16), producing overview pixels that the read-side int-to-NaN mask in open_geotiff couldn't recover because they didn't equal the sentinel. Silent garbage at every zoom above level 0 for to_geotiff(int_data, cog=True, nodata=N). Methods affected: mean, min, max, median; nearest/mode safe (no averaging). Fix: gate the sentinel-to-NaN mask on representability in the source integer dtype (mirrors _int_nodata_in_range in _reader.py) so uint16+GDAL_NODATA=""-9999"" stays a no-op; rewrite all-sentinel-block NaN back to sentinel before the integer dtype cast so the cast is well-defined (the caller's post-overview loop in write() only runs for floats). GPU mirror gets the same path with cupy.where + cupy.isnan for byte parity with CPU. 38 regression tests in test_cog_int_overview_nodata_2026_05_12.py: _block_reduce_2d per-dtype/per-method matrix (uint8/uint16/int16/int32 x mean/min/max/median), all-sentinel-block, no-nodata regression, out-of-range sentinel no-op, end-to-end uint16 + int16 round-trip, 3-band integer COG, GPU per-dtype/per-method matrix, CPU/GPU byte-match parity. All 1606 existing geotiff tests still pass. Categories: Cat 1 (precision/representation loss in nan-aware reduction) + Cat 2 (silent NaN-equivalent corruption from sentinel poisoning) + Cat 5 (backend parity between float and integer code paths within the same writer). Deferred LOW: HTTP COG path (_read_cog_http at _reader.py:1638) skips the band-range validation that local/dask/GPU added in #1673; band=-1 silently selects the last channel on HTTP while local raises IndexError. Cat 5, MEDIUM-leaning but separate concern from the overview fix; one-finding-per-PR per project policy. | Pass 19 (2026-05-12): MEDIUM fixed -- issue #1655. read_vrt silently dropped <NODATA>0</NODATA> on a SimpleSource because of src.nodata or nodata at _vrt.py:370. Python treats 0.0 as falsy, so the per-source sentinel fell through to the band-level <NoDataValue> (or None when missing) and pixels equal to 0.0 in the source file survived as valid data. The in-code comment acknowledged the quirk as backward compat, but the resulting behaviour silently biased every NaN-aware aggregation on VRT mosaics whose sources used 0 as a sentinel (a common convention for unsigned remote-sensing imagery). Fix: src_nodata = src.nodata if src.nodata is not None else nodata. Five regression tests in test_vrt_source_nodata_zero_1655.py covering source NODATA=0, integer XML literal, non-zero unchanged, band-level NoDataValue=0 still honoured, and source-overrides-band precedence. All 100 vrt-related geotiff tests still pass; 3 pre-existing test_features.py matplotlib palette failures unrelated. Categories: Cat 2 (NaN propagation) + Cat 5 (backend inconsistency: read_geotiff masks 0 correctly when GDAL_NODATA tag is set; only VRT path was broken). | Pass 18 (2026-05-11): MEDIUM fixed -- issue #1642. PR #1641 (issue #1640) inherited level-0 georef on overview reads but kept the level-0 origin_x/origin_y unchanged. That is correct for PixelIsArea (origin = upper-left corner of pixel (0,0)) but wrong for PixelIsPoint (origin = center of pixel (0,0), GeoKey 1025 = 2). For a 1024x1024 PixelIsPoint COG with 10 m pixels and origin (0, 0), open_geotiff(overview_level=1) returned x[:3]=[0,20,40] instead of [5,25,45] (level-1 pixel 0 covers level-0 pixels 0-1 whose centers are 0 and 10, centroid 5); same for y. Downstream sel/interp/reproject silently snaps to the wrong pixel for any DEM-style PixelIsPoint COG (USGS, OpenTopography, Copernicus DEM). Categories: Cat 3 (off-by-one / boundary handling) + Cat 5 (raster_type-dependent backend convention). Fix: in extract_geo_info_with_overview_inheritance (_geotags.py), pick the effective raster_type first (overview-declared if non-default, otherwise inherited from parent), then when it is PixelIsPoint apply origin_shift = (scale - 1) * 0.5 * pixel_size_lvl0 along each axis before building the new GeoTransform. PixelIsArea path is byte-equivalent. 13 regression tests in test_overview_pixel_is_point_1642.py: centroid identity across all 4 backends, transform tuple across all 4 backends, uniform grid step, unit-level helper tests for both raster_types via stubbed extract_geo_info, own-geokeys-not-clobbered path on PixelIsPoint, and a PixelIsArea regression check. All 1397 existing non-network geotiff tests still pass (3 pre-existing matplotlib palette failures unrelated). Deferred LOW: non-power-of-two overview dimensions cause scale = base_w/ov_w to diverge from the true 2^level reduction (writer drops the right/bottom strip via h2=(h//2)*2; for h=1023 a level-1 overview has 511 rows so scale=2.0019 not 2.0). Fix would need to either (a) emit explicit geo tags on overview IFDs from the writer or (b) pass the level number into the inheritance helper; neither is a one-line change and the resulting coord error is sub-pixel of level 0. | Pass 17 (2026-05-11): MEDIUM fixed -- issue #1634. open_geotiff eager path windowed read produced confusing CoordinateValidationError when window extended past source extent. read_to_array clamped the window internally and returned a smaller array, but the eager code path used unclamped window indices for y/x coord generation (xrspatial/geotiff/__init__.py lines 562-572), so the coord array length differed from the data and xarray refused to construct the DataArray. Same bug affected the windowed transform shift in _populate_attrs_from_geo_info. The dask path (read_geotiff_dask) already validated up front since #1561, raising a clear ValueError with the format 'window=... is outside the source extent (HxW) or has non-positive size.' so the two backends diverged on the contract. Fix: validate the window up front in open_geotiff's eager branch via _read_geo_info (metadata-only read, no extra pixel cost) using the exact same condition the dask path uses, raising the same ValueError message format. Reproduction: 10x10 raster + window=(5,5,15,15) on eager raised CoordinateValidationError('conflicting sizes ... length 5 ... length 10'); now raises ValueError('window=(5, 5, 15, 15) is outside the source extent (10x10) or has non-positive size.'). Categories: Cat 3 (off-by-one / boundary handling) + Cat 5 (backend inconsistency). 12 regression tests in test_window_out_of_bounds_1634.py: negative start, past-right-edge, past-bottom-edge, past-both-edges, zero-size, inverted window, full-extent ok, interior subset, edge-aligned, eager-vs-dask parity, message-format parity, issue reproducer. All 1286 existing non-network geotiff tests still pass. | Pass 16 (2026-05-11): HIGH fixed -- issue #1623. to_geotiff(cog=True, overview_resampling='cubic', nodata=<finite>) on a float raster with NaN regions produced overview pixels with severe ringing artefacts near nodata borders. Same class of bug as #1613 but for the cubic branch: writer rewrites NaN to the sentinel upstream, then _block_reduce_2d(method=cubic) handed the sentinel-poisoned array straight to scipy.ndimage.zoom(order=3). The cubic spline blended the sentinel (e.g. -9999) into neighbouring cells, producing values like 1133.44, -10290.08 where the data was a constant 100. Repro on 16x16 float32 with a 4x4 NaN corner showed 18 polluted pixels in the 8x8 overview. Fix: when nodata is supplied on a float dtype and the sentinel is found, mask sentinel to NaN, run cubic with prefilter=False so a single NaN cannot poison the entire row/column (default B-spline prefilter is global), then rewrite any NaN in the result back to the sentinel. prefilter=False only fires when a sentinel is present so the non-nodata cubic semantics are unchanged. GPU side: _block_reduce_2d_gpu previously raised on method='cubic'; added a CPU fallback (same pattern as 'mode') so GPU writer produces byte-equivalent overviews. GPU_OVERVIEW_METHODS now includes 'cubic'. 12 regression tests in test_cog_cubic_overview_nodata_1623.py (helper no-ringing, poisoning repro, no-nodata unchanged, end-to-end round-trip, GPU fallback, CPU/GPU byte-match, +/-inf nodata mask, NaN-sentinel no-op, GPU_OVERVIEW_METHODS contract). All 1256 existing geotiff tests still pass (3 pre-existing matplotlib failures unrelated). | Pass 15 (2026-05-11): HIGH fixed -- issue #1613. to_geotiff(cog=True, nodata=<finite>) on a float raster with NaN produced a corrupted overview pyramid. The NaN-to-sentinel rewrite in __init__.py:1202 (CPU) and :2852 (GPU write_geotiff_gpu) ran BEFORE _make_overview / make_overview_gpu, so the nan-aware aggregations (np.nanmean/min/max/median, cupy.nanmean/min/max/median) saw the sentinel as a real number and biased every overview pixel. Reproduction with -9999 sentinel produced [[-4998.75,-4997.75],..] where np.nanmean gives [[1.5,3.5],..]. Both CPU and GPU paths affected; backend results matched each other but were both wrong (CAT 2 NaN propagation + CAT 5 documents the parity). Fix: _block_reduce_2d / _block_reduce_2d_gpu accept a nodata kwarg that masks the sentinel back to NaN for float dtypes before the reduction; the writer's overview loop passes nodata in, then rewrites all-sentinel reductions (which surface as NaN from the reducer) back to the sentinel for the on-disk pyramid. 11 regression tests in test_cog_overview_nodata_1613.py (CPU mean / partial-block / min/max/median / no-nodata passthrough / helper kwarg / all-sentinel block / GPU mean / GPU helper / CPU-GPU agreement). All 235 nodata/overview/cog tests still pass. | Pass 14 (2026-05-11): HIGH fixed -- issue #1611. read_vrt(band=None) on a multi-band integer VRT with per-band <NoDataValue> tags only masks band 0's sentinel. __init__.py lines 2795-2809 in read_vrt apply vrt.bands[0].nodata to the full ndim==3 array; bands 1+ keep their integer sentinels as literal finite values (e.g. 65000 surfaces as 65000.0 after the dtype=float64 cast, not NaN). Float-VRT path masks per-band correctly in _vrt._read_data lines 296-297 + 347-351. PR #1602 fixed the single-band band=N case for issue #1598; the band=None multi-band case is the same class of bug. Repro: 2-band uint16 VRT with NoDataValue 65535 / 65000 returns r.values[1,1,1] == 65000.0 instead of NaN; r.values[1,1,0] is NaN (band 0 sentinel masked). Fix scope: in read_vrt, when band is None, iterate over vrt.bands and mask each arr[..., i] slice against its own <NoDataValue> (gated by the same _int_nodata_in_range guard PR #1583 introduced). Severity HIGH (Cat 2 NaN propagation + Cat 5 backend inconsistency: identical input semantics produce different masking outcomes based on dtype, with finite garbage values where NaN expected). Fix in PR #1612: walks vrt.bands when band is None and ndim==3, masks each arr[..., i] slice against its own <NoDataValue> via the refactored _sentinel_for_dtype helper (reuses PR #1583's range guard so out-of-range/non-finite/fractional sentinels are a no-op). attrs['nodata'] still carries band 0's sentinel for band=None reads (documented contract). 7 regression tests in test_vrt_multiband_int_nodata_1611.py: uint16 per-band, int32 negative, mixed presence, dtype preservation when no sentinel hit, out-of-range gating, band=N non-regression, attrs contract. 135 existing vrt/nodata geotiff tests still pass. | Pass 13 (2026-05-11): HIGH fixed -- issue #1599. write_geotiff_gpu (and to_geotiff gpu=True) emitted raw NaN bytes for missing pixels even when nodata=<finite> was supplied, while the CPU writer substituted NaN with the sentinel before encoding. xrspatial-only round-trips were unaffected (the reader masks both NaN and the sentinel), but external readers (rasterio/GDAL/QGIS) that mask only on the GDAL_NODATA tag saw NaN pixels as valid data -- rasterio reported 100% valid pixels on a 25-NaN file vs CPU's 25-invalid report. Root cause: __init__.py lines 2579-2587 jumped from shape/dtype resolution straight to compression, missing the equivalent of the CPU writer's NaN-to-sentinel rewrite at to_geotiff line ~1156. Fix: cupy.isnan + masked write on a defensive copy of arr, gated on np_dtype.kind=='f' and not np.isnan(float(nodata)). Caller's CuPy buffer preserved (copy before mutate). 7 regression tests in test_gpu_writer_nan_sentinel_1599.py: substitution lands as sentinel, CPU/GPU byte-equivalent, caller buffer not mutated, no-NaN no-op, NaN sentinel skips substitution, rasterio sees identical invalid count on CPU/GPU, multiband 3D path. All other GPU writer tests still pass (50 passed across band-first, attrs, nodata, dask+cupy, writer, nodata aliases). | Pass 12 (2026-05-11): HIGH fixed -- issue #1581. Reading a uint TIFF with a negative GDAL_NODATA sentinel (e.g. uint16 + -9999) raised OverflowError on every backend because the nodata-mask code did arr.dtype.type(int(nodata)) with no range check. Three identical cast sites in __init__.py (numpy eager, _apply_nodata_mask_gpu, _delayed_read_window) plus _resolve_masked_fill and _sparse_fill_value in _reader.py. Fix: _int_nodata_in_range helper gates the cast; out-of-range sentinels are a no-op for value matching (the file can never contain that value), file dtype is preserved, attrs['nodata'] still surfaces the original sentinel so write round-trips keep the GDAL_NODATA tag intact. Matches rasterio behavior. 8 regression tests in test_nodata_out_of_range_1581.py cover the helper, both eager and dask read paths, in-range sentinel non-regression, and GPU helper (cupy-gated). | Pass 11 (2026-05-10): CLEAN. Audited the one additional commit since pass 10 -- #1559 (PR 1548, Centralise GeoTIFF attrs population across all read backends). Refactor extracts _populate_attrs_from_geo_info helper and routes eager numpy, dask, GPU stripped, GPU tiled read paths through it; before the fix dask only emitted crs/transform/raster_type/nodata while numpy emitted the full attrs set including x/y_resolution, resolution_unit, image_description, extra_samples, GDAL metadata, and the CRS-description fields. No data-path arithmetic touched; only attrs dict population. Windowed origin math (origin_x + c0*pixel_width, origin_y + r0*pixel_height) verified to produce -98.0 / 48.75 origin for window=(10,20,50,70) on a (0.1,-0.125) pixel-size raster, with PixelIsArea half-pixel offset preserved on coord lookups (-97.95, 48.6875). Cross-backend attrs parity re-verified: numpy/dask/cupy all emit identical key set on deflate+predictor3+nodata round-trip (crs, crs_wkt, nodata, transform, x_resolution, y_resolution). Data bit-parity re-verified across numpy/dask/cupy on same payload (np.array_equal with equal_nan=True). test_attrs_parity_1548.py (5 tests), test_reader.py/test_writer.py/test_dask_cupy_combined.py (25 tests), GPU orientation/predictor2-BE/LERC-mask/nodata/byteswap suites (65 tests) all green. No accuracy or backend-divergence findings. | Pass 10 (2026-05-10): CLEAN. Audited 5 recent commits: #1558 drop-defensive-copies (frombuffer path still .copy()s before in-place predictor decode at _reader.py:778), #1556 fp-predictor ngjit (writer pre-ravels so 1-D slice arg is correct, float32/64 LE+BE bit-exact), #1552 batched D2H (OOM guard fires before cupy.concatenate, host_buf offsets correct), #1551 parallel-decode gate (>= vs > sends 256x256 default to parallel path, no value diff confirmed via partial-tile parity), #1549 nvjpeg constants (gray + RGB GPU JPEG decode pixel-identical to Pillow CPU, max diff = 0). Cross-backend parity re-verified clean: numpy/dask+numpy/cupy/dask+cupy equal .data/.dtype/.coords/nodata/NaN-mask on deflate+predictor3+nodata; orientations 1-8 numpy==GPU; partial edge tiles 100x150, 257x383, 512x257 numpy==GPU==dask; predictor2 LE/BE round-trip uint8/int16/uint16/int32/uint32 pass; predictor3 LE/BE float32/64 pass. Deferred LOW (pre-existing, not opened): float16 (bps=16, SampleFormat=3) absent from tiff_dtype_to_numpy map - writer never emits, asymmetric but unreachable. | Pass 9 (2026-05-09): TWO HIGH fixed -- (a) PR #1539 closes #1537: TIFF Orientation tag 2/3/4 (mirror flips) on georeferenced files left y/x coords computed from the un-flipped transform, so xarray label lookups returned the wrong pixel even though _apply_orientation flipped the buffer. PR #1521 only updated the transform for the 5-8 axis-swap branch. Fix updates origin and pixel-scale signs along whichever axes were flipped, for both PixelIsArea (origin shifts by N*step) and PixelIsPoint (shifts by (N-1)*step). 10 new tests in test_orientation.py. (b) PR #1546 closes #1540: read_geotiff_gpu ignored Orientation tag completely; CPU correctly applied 2-8 (PR #1521) but GPU returned the raw stored buffer. Cross-backend disagreement on every non-default orientation. Fix adds _apply_orientation_gpu (cupy slicing mirror of the CPU helper) and _apply_orientation_geo_info, threads them into the tiled GPU pipeline, reuses CPU-fallback geo_info for the stripped path to avoid double-applying. 28 new tests in test_orientation_gpu.py (every orientation, single-band tiled, single-band stripped, 3-band tiled, mirror-flip sel-fidelity, default no-tag passthrough). Re-confirmed clean: HTTP coalesce_ranges with overlapping ranges and zero-length ranges, parallel streaming write thread-safety (each tile gets independent buffer via copy or padded zeros), planar=2 + chunky GPU LERC mask propagation matches CPU, IFD chain cap MAX_IFDS=256, max_z_error round-trip on tiled write, _resolve_masked_fill float vs integer dtype semantics. Deferred LOW: per-sample LERC mask (3D mask (h,w,samples)) collapsed to per-pixel ""any sample invalid"" on GPU while CPU honours per-sample; LERC implementations rarely emit 3D masks (verified: lerc.encode with 2D mask on 3-band returns 2D mask). Documented planar=2 + LERC + GPU silently drops mask (rare in practice, source comment acknowledges). | Pass 8 (2026-05-07): HIGH fixed in fix-jpeg-tiff-disable -- to_geotiff(compression='jpeg') wrote files that no external reader can decode. The writer tags compression=7 (new-style JPEG) but emits a self-contained JFIF stream per tile/strip and never writes the JPEGTables tag (347) that the TIFF spec requires for that codec. libtiff/GDAL/rasterio all reject the file with TIFFReadEncodedStrip() failed; our reader round-trips because Pillow decodes the standalone JFIF, hiding the break. Pass-4 notes flagged the read side of the same JPEGTables gap and deferred it; pass-8 covers the write side. Fix: reject compression='jpeg' at the to_geotiff entry with a clear ValueError pointing at deflate/zstd/lzw. The internal _writer.write is untouched so the existing self-decoding tests still cover the codec; re-enabling the public path needs a JPEGTables-aware encoder. PR diffs reviewed but not merged: #1512 (BytesIO source) and #1513 (LERC max_z_error) -- both look correct; #1512 file-like read path goes through read_all() once so the per-call BytesIOSource lock is theoretical, and #1513 forwards max_z_error through every overview/tile/strip/streaming path including _write_vrt_tiled and _compress_block. No regressions found in either open PR. Other surfaces audited clean: predictor=3 with float16 (writer auto-promotes to float32 on both eager and streaming paths, value-exact round-trip); planar=2 multi-tile read uses band_idx*tiles_per_band offset so no cross-contamination between planes; _header.py multi-byte tag parsing uses bo (byte_order) consistently; Pillow YCbCr-vs-tagged-RGB photometric mismatch becomes moot once JPEG is disabled. Deferred (LOW/MEDIUM, not filed): JPEG2000 writer accepts arbitrary dtype with no validation (rare codec, narrow risk); float16 dtype not in tiff_dtype_to_numpy decode map (writer never emits it - asymmetric but unreachable); Orientation tag (274) still ignored on read (pass-4 deferral). | Pass 7 (2026-05-07): HIGH fixed in fix-mmap-cache-refcount-after-replace -- _MmapCache.release() looked up the cache entry by realpath, so a holder that acquired the OLD mmap before an os.replace and released it AFTER another caller had acquired the post-replace entry would decrement the new holder's refcount. Subsequent eviction (cache full, or another acquire) closed the still-in-use mmap, breaking reads with 'mmap closed or invalid'. Real exposure: any concurrent reader/writer pattern where to_geotiff replaces a file that another reader had just opened via open_geotiff with chunks= or via _FileSource. PR #1506 added stale-replacement detection but did not fix the refcount confusion across the pop. Fix: acquire returns an opaque entry token; release takes the token and decrements that exact entry, regardless of cache state. Orphaned (popped) entries close their fh+mmap when their own refcount hits zero. _FileSource updated to pass the token. Regression test test_release_after_path_replacement_does_not_clobber_new_holder added. All 665 geotiff tests pass; GPU path verified. | Pass 6 (2026-05-07) PR #1507: BE pred2 numba TypingError. | Pass 5 (2026-05-06) PR #1506: mmap cache stale after file replace. | Pass 4 (2026-05-06) PR #1501: sparse COG tiles. | Pass 3 (2026-05-06) PR #1500: predictor=3 byte order. | Pass 2 (2026-05-05) PR #1498: predictor=2 sample-wise. | Pass 1 (2026-04-23) PR #1247. Re-confirmed clean over passes 2-7: items 2 (writer always emits LE TIFFs - hardcoded b'II'), 3 (RowsPerStrip default = height when missing), 4 (StripByteCounts missing raises clear ValueError), 5 (TileWidth without TileLength caught by 'tw <= 0 or th <= 0' check at _reader.py:688), 9 (read determinism on compressed+tiled+multiband), 11 (predictor=2 with awkward sample stride round-trips), 18 (compression_level=99 raises ValueError 'out of range for deflate (valid: 1-9)'), 21 (concurrent writes serialize correctly via mkstemp+os.replace), 24 (uint16 dtype preserved on numpy backend, dask honors chunks param), 26 (chunks rounds correctly with remainder chunk for non-tile-aligned). Deferred: item 8 (BytesIO/file-like sources are not supported, source.lower() error) - documented as 'str' parameter, not a bug; item 19 (LERC max_z_error not user-exposed by to_geotiff) - missing feature, not a bug."
|
|
17
|
+
glcm,2026-05-01,1408,HIGH,2,"angle=None averaged NaN as 0, masking no-valid-pairs as zero texture; fixed via nanmean-style averaging"
|
|
18
|
+
hillshade,2026-04-10T12:00:00Z,,,,"Horn's method correct. All backends consistent. NaN propagation correct. float32 adequate for [0,1] output."
|
|
19
|
+
hydro,2026-04-30,,LOW,1,Only LOW: twi log(0)=-inf if fa=0 (out-of-contract); MFD weighted sum no Kahan (negligible). No CRIT/HIGH issues.
|
|
20
|
+
interpolate-kriging,2026-06-04,2915,MEDIUM,1,"Cat1 nugget-on-diagonal bug (MEDIUM): _build_kriging_matrix set K[:n,:n]=vario_func(D) where D has 0 diagonal, so vario_func(0)=nugget c0 landed on the matrix diagonal; semivariogram gamma(0)=0 by definition (nugget is the h->0+ limit). Forced exact interpolation of noisy data and biased kriging variance downward. Only bites when fitted nugget>0; existing trend-dominated test data fits ~0 nugget so tests passed. Fix #2915/PR #2922: np.fill_diagonal(G,0.0) in shared host code (all 4 backends consume same K_inv). Cats 2-5 clean: validate_points drops NaN/Inf rows; range floor 1e-12 prevents div blowup; dask map_blocks slices grid coords with correct half-open extents and returns matching block shape (kriging is global, no overlap needed); planar Euclidean distance is expected for kriging (Cat4 n/a); numpy/cupy/dask share one algorithm and parity tests pass rtol=1e-10. CUDA available; all 16 kriging tests pass incl cupy + dask+cupy. Singular-matrix path adds 1e-10*eye Tikhonov term (separate from nugget, unaffected, correct)."
|
|
21
|
+
kde,2026-04-13T12:00:00Z,1198,,,kde/line_density return zeros for descending-y templates. Fix in PR #1199.
|
|
22
|
+
mahalanobis,2026-05-01,,LOW,1,"LOW: np.linalg.inv (no pinv fallback) returns garbage for near-singular cov without raising. LOW: two-pass mean/cov instead of Welford could lose precision for inputs with very large mean/small variance. No CRIT/HIGH; all four backends use float64 throughout, NaN handled via isfinite, dist_sq clamped non-negative, singular case raises ValueError."
|
|
23
|
+
morphology,2026-04-30,"1397,1399",HIGH,2;5,HIGH fixed in #1397/PR #1398: morph_erode/dilate seeded centre cell into running min/max even when kernel[centre]==0 (all 4 backends). HIGH fixed in #1399/PR #1400: dask backends raised on 1xN/Nx1 kernels because empty-slice writeback (0:-0).
|
|
24
|
+
multispectral,2026-03-30T14:00:00Z,1094,,,
|
|
25
|
+
normalize,2026-05-01,,,,rescale and standardize across all 4 backends. NaN/inf filtered via isfinite mask before min/max/mean/std. Constant input handled (range=0 -> new_min; std=0 -> 0.0). Output dtype float64 consistently. Backend parity covered by test_matches_numpy. No accuracy issues found.
|
|
26
|
+
perlin,2026-04-10T12:00:00Z,,,,Improved Perlin noise implementation correct. Fade/gradient functions verified. Backend-consistent. Continuous at cell boundaries.
|
|
27
|
+
polygon_clip,2026-04-13T12:00:00Z,1197,,,crop=True + all_touched=True drops boundary pixels. Fix in PR #1200.
|
|
28
|
+
polygonize,2026-05-29,2606,HIGH,5,"Cat 5 HIGH: dask connectivity=8 cross-chunk merge filled diagonal notch where same-value regions meet only at a corner across a chunk boundary; total area exceeded raster. Hole ring was dropped because containment tested hole[0] (on exterior at pinch). Fixed via _ring_interior_point in PR for #2606. numpy, dask+numpy, dask+cupy area parity now holds; 4-conn was already correct. cupy + dask+cupy paths validated on GPU host. Other cats clean: NaN masked on numpy/cupy float paths (tested), _is_close handles +/-inf via exact-equality short-circuit, atol/rtol/simplify_tolerance reject NaN/inf, integer GPU CCL matches numpy."
|
|
29
|
+
proximity,2026-05-29,2721,MEDIUM,4;5,Bounded GREAT_CIRCLE on dask (both numpy+cupy) raised ValueError: map_overlap pad depth = max_distance/cellsize mixed metre distance with degree cellsize. numpy/cupy backends fine. Fixed by measuring per-pixel pitch with active metric (PR #2722). Cat1 float32 output is documented design choice; NaN/Inf masking via np.isfinite consistent; numpy GDAL-sweep matches exact nearest and cupy brute-force on tested grids.
|
|
30
|
+
reproject,2026-05-29,2620,HIGH,5,"Cat5 backend inconsistency: cupy _resample_cupy (cupyx map_coordinates) diverged from numpy/native on pyproj-fallback CRS pairs (projected->projected, e.g. EPSG:32633->3857). Edge-band cval=0.0 bleed (all modes, ~534/pixel) + cubic B-spline vs Catmull-Rom (~0.45 interior). Fixed PR for #2620: route eager+dask cupy through _resample_cupy_native. Other files clean: _merge numpy/cupy structurally identical; _datum_grids/_vertical/_itrf use -0.5 pixel-center interp and self-inequality NaN checks; WGS84/GRS80 constants correct; curvature correction n/a (no geodesic gradient here). LOW (not fixed): _transform._bilinear_interp_2ch docstring claims parallel but isn't."
|
|
31
|
+
resample,2026-05-29,2610,HIGH,3;5,"dask interp (nearest/bilinear) overlap depth=1 too small on downsample; block-centered source coord landed past chunk, map_coordinates clamped to edge -> wrong seam rows. Fixed PR #2627 via per-axis _downsample_radius. cupy+dask+cupy verified."
|
|
32
|
+
sieve,2026-04-13T12:00:00Z,,,,Union-find CCL correct. NaN excluded from labeling. All backends funnel through _sieve_numpy.
|
|
33
|
+
sky_view_factor,2026-05-01,1407,HIGH,4,Horizon angle ignored cell size; fixed by passing cellsize_x/cellsize_y into CPU+GPU kernels and using ground distance
|
|
34
|
+
terrain,2026-04-10T12:00:00Z,,,,Perlin/Worley/ridged noise correct. Dask chunk boundaries produce bit-identical results. No precision issues.
|
|
35
|
+
terrain_metrics,2026-04-30,,LOW,2;5,"LOW: Inf input not rejected, propagates as Inf (consistent across backends but undocumented). LOW: dask+cupy non-nan boundary path double-pads (wasted compute, central output values still correct). No CRIT/HIGH; tests cover NaN propagation, all 4 backends, all 4 boundary modes, dtype acceptance."
|
|
36
|
+
viewshed,2026-05-29,2691,HIGH,3;5,max_distance window sized from coarser axis clipped cells on anisotropic rasters (PR #2702). LOW unfixed: distance_sweep ring radius same max(res) pattern but max_distance arg always None; _calculate_event_row_col line 880 abs(x>1) precedence bug is a broken guard only. cuda+rtx paths validated.
|
|
37
|
+
visibility,2026-04-13T12:00:00Z,,,,"Bresenham line, LOS kernel, Fresnel zone all correct. All backends converge to numpy."
|
|
38
|
+
worley,2026-05-01,,MEDIUM,2;5,"MEDIUM: numpy backend uses np.empty_like(data) so integer input dtype produces integer output (distances truncated to 0); cupy/dask paths always produce float32. LOW: freq=inf produces 100000 sentinel (sqrt of initial min_dist=1e10), no validation of freq/seed for non-finite values."
|
|
39
|
+
zonal,2026-05-27,2528,MEDIUM,5,"Pass 2 (2026-05-27): MEDIUM fixed -- issue #2528. zonal_stats() on dask-backed inputs silently dropped 'majority' from the requested stats list. The mutable default stats_funcs included 'majority' (added in commit 7c8d5759), but the dask path filtered it out at xrspatial/zonal.py:459 (computed_stats = [s for s in stats_funcs.keys() if s in stats_dict]) because 'majority' is not in _DASK_BLOCK_STATS. Symptom: stats(zones=dask, values=dask) returned 7 columns instead of the 8 the docstring promises; stats(..., stats_funcs=['mean','majority']) returned only ['zone','mean'] with no error or warning. Both dask+numpy and dask+cupy were affected (dask+cupy delegates to dask+numpy). Fix: replaced the mutable list literal default with stats_funcs=None and resolved the default per backend inside the function -- numpy/cupy get the full 8-stat list, dask gets the 7-stat subset (no majority). Explicit majority on dask now raises ValueError with a clear supported-stats message instead of silently filtering. 4 regression tests in test_zonal.py: explicit majority raises on dask, bare default omits majority on dask, bare default keeps majority on numpy, default list is not mutated across calls (covers the historical mutable-default pitfall). All 129 test_zonal.py tests pass (125 pre-existing + 4 new); test_dasymetric.py 61 tests still pass (dasymetric uses zonal.stats internally). Categories: Cat 5 (backend inconsistency: numpy/cupy honoured majority; dask paths silently dropped it). | Pass 1 (2026-03-30T12:00:00Z): historical entry #1090."
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module,last_inspected,oom_verdict,bottleneck,high_count,issue,notes
|
|
2
|
+
aspect,2026-05-29,SAFE,compute-bound,1,2688,"dask+cupy geodesic densified full lat/lon on one GPU at graph build (OOM at scale); fixed via per-block map_blocks cupy conversion. planar/numpy/dask SAFE; geodesic GPU kernel ~184 regs, mitigated by 16x16 blocks."
|
|
3
|
+
balanced_allocation,2026-04-16T12:00:00Z,WILL OOM,memory-bound,8,1114,"Re-audit 2026-04-16 after PR 1203 float32 fix. 8 HIGH found (friction.compute L339, argmin.compute in iter loop L182, double all_nan recompute L206, stacked cost_surfaces allocation). Covered by existing documented limitation on #1114. Not refiled."
|
|
4
|
+
bilateral,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
5
|
+
bump,2026-04-16T12:00:00Z,SAFE,compute-bound,0,1206,Re-audit 2026-04-16: fix verified SAFE. No HIGH findings. MEDIUM: CuPy backend runs CPU kernel then transfers to GPU (documented limitation).
|
|
6
|
+
classify,2026-04-16T18:00:00Z,SAFE,compute-bound,0,fixed-in-tree,"Fixed-in-tree 2026-04-16: _run_dask_head_tail_breaks now persists data_clean once and fuses mean+head_count per iter (912ms -> 339ms, 0.37x IMPROVED); added _run_dask_box_plot that samples via _generate_sample_indices instead of boolean fancy indexing on dask array; _run_dask_cupy_box_plot likewise. 85 existing classify tests pass."
|
|
7
|
+
contour,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
8
|
+
convolution,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
9
|
+
corridor,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
10
|
+
cost_distance,2026-04-16T12:00:00Z,WILL OOM,memory-bound,4,1118,"Re-audit 2026-04-16 after PR 1192 Bellman-Ford fix. 4 HIGH re-surface in iterative tile_cache path (L645 full-dataset materialization, L1015 da.from_delayed wrapping computed tiles). Finite max_cost path remains SAFE. Unbounded path is fundamentally O(dataset) driver memory — covered by #1118."
|
|
11
|
+
curvature,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
12
|
+
dasymetric,2026-03-31T18:00:00Z,SAFE,memory-bound,0,1126,Memory guard added to validate_disaggregation. Core disaggregate uses map_blocks.
|
|
13
|
+
diffusion,2026-03-31T18:00:00Z,WILL OOM,memory-bound,2,1116,Scalar diffusivity now passed as float to chunks. DataArray diffusivity passed as dask array via map_overlap.
|
|
14
|
+
edge_detection,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
15
|
+
emerging_hotspots,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
16
|
+
erosion,2026-03-31T18:00:00Z,WILL OOM,memory-bound,2,1120,Memory guard added. Algorithm inherently global.
|
|
17
|
+
fire,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
18
|
+
flood,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
19
|
+
focal,2026-05-29,SAFE,compute-bound,1,2734,"HIGH: _hotspots_dask_cupy chunk fn round-tripped each chunk host<->GPU (cupy.asnumpy classify cupy.asarray); fixed PR 2739 to reuse _run_gpu_hotspots on device. LOW (not fixed): _apply_numpy/_hotspots_cupy use zeros_like where empty would suffice. CUDA kernels regs<=62, no register-pressure issue."
|
|
20
|
+
geodesic,2026-03-31T18:00:00Z,N/A,compute-bound,0,,
|
|
21
|
+
geotiff,2026-05-20,SAFE,IO-bound,0,2212,"Pass 13 (2026-05-20): 1 MEDIUM found and fixed. _nvjpeg_batch_encode (_gpu_decode.py:~L1560) and _nvjpeg2k_batch_encode (~L2958) called cupy.cuda.Device().synchronize() inside the per-tile encode loops, a whole-device fence that blocked every CUDA stream and serialised concurrent work (e.g. predictor encodes on other streams). The decode-side counterpart _try_nvjpeg_batch_decode already used cupy.cuda.Stream.null.synchronize() at L1442; the encoder side was inconsistent. Filed #2212 and fixed both encoders to use Stream.null.synchronize(), scoping the per-tile sync to the default stream the encode/retrieve calls were issued on. nvJPEG / nvJPEG2000 encoders maintain a single shared state per encoder so encodes within a batch are inherently serial; the fix removes the device-wide blocker without changing the API ordering contract. 5 new tests in test_nvjpeg_encode_stream_sync_2212.py (AST checks that neither encoder contains Device().synchronize() inside a for-loop, that both call Stream.null.synchronize() in the loop, and that the decoder reference pattern stays pinned). All 5 new tests + 19 existing related encode/decode tests pass. nvjpeg/nvjpeg2k shared libs not present on this host so end-to-end encode verification is gated; add cuda-unavailable-libs note to re-validate on a host with the RAPIDS conda env. SAFE/IO-bound verdict holds; no change in dask graph cost. Dask probe: 2560x2560 deflate-tiled file via read_geotiff_dask(chunks=256) yields 400 tasks for 100 chunks (4 tasks/chunk), well under the 50K cap. LOW deferred (no fix in this PR): _build_ifd called twice per IFD level in _assemble_standard_layout (_writer.py:1531+1543), _assemble_cog_layout (1582+1625), and the COG overview path (2519+2546+2740) -- the first call's bytes are discarded; only the overflow byte length is used to compute pixel_data_offset. Cost is bounded by IFD count (typically 1-5 overview levels) so absolute impact is minor. Pre-existing pattern. | Pass 12 (2026-05-18): 1 MEDIUM found and fixed. _try_nvjpeg2k_batch_decode at _gpu_decode.py:~L2725-2778 allocated per-tile per-component cupy.empty buffers (N*S round-trips through the cupy memory pool) and called cupy.cuda.Device().synchronize() once per tile, forcing default-stream serialisation that defeats nvJPEG2000's internal pipelining. Filed #2107 and fixed: pre-allocate a single d_comp_pool sized n_tiles*samples*tile_height*pitch under a _check_gpu_memory guard, derive per-tile/per-component views as slab offsets, and replace the per-tile sync with a single batch-end sync. Same pattern as #1659 (_try_nvcomp_from_device_bufs), #1688 (_try_kvikio_read_tiles), #1712 (_nvcomp_batch_compress). 7 new tests in test_nvjpeg2k_single_alloc_2107.py: AST-level structural assertions confirm no cupy.empty inside the for-loop and no Device().synchronize() inside the loop, plus pool/per_tile_comp_bytes presence and _check_gpu_memory guard checks; lib-absent short-circuit; unsupported-dtype cleanup contract; cupy-only pool slab-non-overlap test (gpu-marked). libnvjpeg2k.so not present on this host so the end-to-end nvJPEG2000 decode is gated -- note added to re-validate on a host with the RAPIDS conda env. All 30 jpeg2000/compression tests + 7 new tests pass. SAFE/IO-bound verdict holds (no change in dask graph cost). Dask probe: 4096x4096 deflate-tiled file via read_geotiff_dask(chunks=512) yields 256 tasks for 64 chunks (4 tasks/chunk), well under the 50K cap. | Pass 11 (2026-05-18): 1 MEDIUM found and fixed. _read_strips (_reader.py:~L1972) and _fetch_decode_cog_http_strips (_reader.py:~L2670) decoded strips sequentially in a Python for-loop while the tile counterparts (_read_tiles L2146, _fetch_decode_cog_http_tiles L2898) gated parallel decode on _PARALLEL_DECODE_PIXEL_THRESHOLD via ThreadPoolExecutor. Filed #2100 and fixed: both strip paths now collect jobs, parallel-decode when n_strips > 1 and strip_pixels >= 64K, then place sequentially. Measured (uint16, 4-core): 4096x4096 deflate 130ms->34ms (3.82x), 8192x8192 deflate 531ms->146ms (3.63x), 8192x8192 zstd 211ms->85ms (2.48x), uncompressed 25ms->22ms (1.14x). 5 new tests in test_parallel_strip_decode_2100.py (parallel/serial parity, pool-engaged on multi-strip, serial-path for single-strip, windowed cross-strip read, HTTP COG strip parity). 3998 tests pass; 8 pre-existing failures predating this change (predictor2 BE + size_param_validation_gpu_vrt reference now-private read_to_array attr). SAFE/IO-bound verdict holds. | Pass 10 (2026-05-15): 1 new MEDIUM found and fixed; 2 LOW noted. MEDIUM (_reader.py:2737): _fetch_decode_cog_http_tiles decoded tiles sequentially in a Python for-loop after the concurrent fetch landed (issue #1480). Local _read_tiles parallelises decode whenever tile_pixels >= 64K via ThreadPoolExecutor (_reader.py:2017); the HTTP path was structurally similar but never picked up the same gate, so wide windowed reads of multi-tile COGs left deflate/zstd decode single-threaded. Mirrored the local-path threshold + pool. 5 new tests in test_cog_http_parallel_decode_2026_05_15.py (parallel + serial round-trip correctness, pool-instantiation branch selection above the threshold, single-tile path skips the pool, structural _decode_strip_or_tile call count == n_tiles). All 262 COG/HTTP tests pass; 3162 of 3164 selected geotiff tests pass overall (2 pre-existing failures predating Pass 9 per prior notes -- test_predictor2_big_endian_gpu_1517 references the now-private read_to_array attr, and the test_size_param_validation_gpu_vrt_1776 tile_size=4 validator failure). LOW deferred (no fix in this PR): (1) _block_reduce_2d_gpu (_gpu_decode.py:3142/3163/3189) does bool(mask.any().item()) per overview level when nodata is set, paying one device sync per level; the alternative (unconditional cupy.putmask) always pays the work cost and the short-circuit is correct under the current API. (2) _nvcomp_batch_compress adler32 staging (_gpu_decode.py:2543-2546) issues n_tiles slice-assign kernels into a fresh contig buffer despite all callers passing slices of a single underlying d_tile_buf; an API refactor to accept the source buffer directly would skip the rebuild. SAFE/IO-bound verdict holds. Dask probe: 2560x2560 chunks=256 yields 400 tasks (4 per chunk), well under the 50000 cap. GPU probe: 1024x1024 float32 zstd read returns CuPy-backed in 236 ms with no host round-trip. | Rockout 2026-05-15: LOW filed #1934 -- _apply_nodata_mask_gpu used cupy.where (allocating); switched to cupy.putmask on the already-owned buffer (float path) and on the post-astype float64 buffer (int path). Saves one chunk-sized device allocation per call. 7 new tests in test_apply_nodata_mask_gpu_inplace_1934.py; 52 related nodata tests pass. | Pass 8 (2026-05-12): 1 new MEDIUM found and fixed. _assemble_standard_layout/_assemble_cog_layout returned bytes(bytearray), doubling peak memory transiently during eager writes. Filed #1756, fixed by returning the bytearray directly. Measured: 95 MB uint8 raster peak drops 202 MB -> 107 MB. _write_bytes / parse_header already accepted the buffer protocol so the change is transparent to callers. 6 new tests in test_assemble_layout_no_bytes_copy_1756.py. 2123 existing geotiff tests pass; the 10 unrelated failures (test_no_georef_windowed_coords_1710, test_predictor2_big_endian_gpu_1517) reference the now-private read_to_array attribute (commit 8adb749, issue #1708) and predate this change. SAFE/IO-bound verdict holds. | Pass 7 (2026-05-12): re-audit identified 4 MEDIUM findings, all real, all backed by microbenches. (1) unpack_bits sub-byte loops for bps=2/4/12 in _compression.py:836-878 were 100-200x slower than vectorised numpy (filed #1713, fixed in this branch: bps=4 2M pixels drops from 165ms to 3ms = 55x; bps=2/12 similar). (2) _write_vrt_tiled at __init__.py:1708 uses scheduler='synchronous' on independent tile writes; measured 33% slowdown on 256-tile zstd write vs threads scheduler (filed #1714, no fix yet). (3) _nvcomp_batch_compress at _gpu_decode.py:2522-2526 still does per-tile cupy.get().tobytes() despite #1552 / #1659 fixing the same pattern elsewhere; measured 45% reduction with concat+single get on n=1024 (filed #1712, no fix yet). (4) _nvcomp_batch_compress at _gpu_decode.py:2457 uses per-tile cupy.empty allocations; 1024 tiles 16KB drops from 4.7ms to 1.0ms with single contiguous + views (bundled into #1712). Cat 6 OOM verdict: SAFE/IO-bound holds -- read_geotiff_dask caps task count at _MAX_DASK_CHUNKS=50_000 and per-chunk memory is bounded by chunk size. _inflate_tiles_kernel resource usage on Ampere: 67 regs/thread, 2896B local/thread, 8192B shared/block (LZW kernel: 29 regs, 24576B shared) -- register pressure under control; high local memory in inflate is unavoidable (LZ77 state) but only thread 0 in each block uses it. | Pass 4 (2026-05-10): re-audit after #1559 (centralise attrs across all read backends). New _populate_attrs_from_geo_info helper at __init__.py:301 runs once per read, not per-chunk -- no perf impact. Probe: 2560x2560 deflate-tiled file opened via read_geotiff_dask yields 400 tasks (4 tasks/chunk for 100 chunks), well under 1M cap. read_geotiff_gpu(1024x1024) returns cupy.ndarray end-to-end with no host round-trip (226ms incl. write+decode). No new HIGH/MEDIUM findings. SAFE/IO-bound holds. | Pass 3 (2026-05-10): SAFE/IO-bound. Audited 4 perf commits: #1558 (in-place NaN writes on uniquely-owned buffers correct), #1556 (fp-predictor ngjit ~297us/tile for 256x256 float32), #1552 (single cupy.concatenate + one .get() for batched D2H at _gpu_decode.py:870-913), #1551 (parallel decode threshold >=65536px engages 256x256 default at _reader.py:1121). Bench: 8192x8192 f32 deflate+pred2 256-tile write 782ms; 4096x4096 f32 deflate read 83ms with parallel decode. Deferred LOW (none filed, all <10% MEDIUM threshold): _writer.py:459/1109 redundant .copy() before predictor encode (~1% per tile), _compression.py:280 lzw_decompress dst[:n].copy() (~2% per LZW tile decode), _writer.py:1419 seg_np.copy() before in-place NaN substitution (negligible, conditional path), _CloudSource.read_range opens fresh fsspec handle per range (pre-existing, predates audit scope). nvCOMP per-tile D2H batching break-even confirmed (variable sizes need staging buffer, no win). | Pass 3 (2026-05-10): audited f157746,39322c3,f23ec8f,1aac3b7. All 5 commits correct. Redundant .copy() in _writer.py:459,1109 and _compression.py:280 (1-2% overhead, LOW). _CloudSource.read_range() per-call open is pre-existing arch issue. No HIGH/MEDIUM regressions. SAFE. | re-audit 2026-05-02: 6 commits since 2026-04-16 (predictor=3 CPU encode/decode, GPU predictor stride fix, validate_tile_layout, BigTIFF LONG8 offsets, AREA_OR_POINT VRT, per-tile alloc guard). 1M dask chunk cap intact at __init__.py:948; adler32 batch transfer intact at _gpu_decode.py:1825. New code is metadata validation and dispatcher logic with no extra materialization or per-tile sync points. No HIGH/MEDIUM regressions. | Pass 5 (2026-05-12): re-audit identified MEDIUM in _gpu_decode.py:1577 _try_nvcomp_from_device_bufs: per-tile cupy.empty + trailing cupy.concatenate doubled peak VRAM and added serial concat. Filed #1659 and fixed to single-buffer + pointer offsets (matches LZW/deflate/host-buffer patterns at L1847/L1878/L1114). Microbench (alloc+concat overhead only, not full nvCOMP latency): n=256 tile_bytes=65536 drops 3.66ms->0.69ms, n=256 tile_bytes=262144 drops 8.18ms->0.13ms. Tests: 5 new tests in test_nvcomp_from_device_bufs_single_alloc_1659.py (codec short-circuit, no-lib short-circuit, memory-guard contract, real ZSTD round-trip via nvCOMP, structural single-buffer check). 1458 existing geotiff tests pass, 3 unrelated matplotlib/py3.14 failures pre-existing. SAFE/IO-bound verdict holds. | Pass 6 (2026-05-12): re-audit on top of #1659. New HIGH in _try_kvikio_read_tiles at _gpu_decode.py:941: per-tile cupy.empty() + blocking IOFuture.get() inside loop serialised GDS reads to ~1 outstanding pread, missed parallelism the kvikio worker pool was designed for, paid per-tile cupy.empty setup (matches #1659 anti-pattern in nvCOMP path), and lacked _check_gpu_memory guard. Filed #1688 and fixed to single contiguous buffer + batched submit + guard. Microbench with 8-worker pool simulation: 256 tiles@1ms latency drops 256ms->38.7ms (~6.6x); single-thread simulation 256ms->28.5ms (9x). Tests: 9 new tests in test_kvikio_batched_pread_1688.py (kvikio-absent path, single-buffer pointer arithmetic, submit-before-get ordering, memory guard, partial-read fallback, round-trip data, zero-size/all-sparse tiles). All 1577 geotiff tests pass except pre-existing matplotlib/py3.14 failures."
|
|
22
|
+
glcm,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,"Downgraded to MEDIUM. da.stack without rechunk is scheduling overhead, not OOM risk."
|
|
23
|
+
hillshade,2026-04-16T12:00:00Z,SAFE,compute-bound,0,,"Re-audit after Horn's method rewrite (PR 1175): clean stencil, map_overlap depth=(1,1), no materialization. Zero findings."
|
|
24
|
+
hydro,2026-05-01,RISKY,memory-bound,0,1416,"Fixed-in-tree 2026-05-01: hand_mfd._hand_mfd_dask now assembles via da.map_blocks instead of eager da.block of pre-computed tiles (matches hand_dinf pattern). Remaining MEDIUM: sink_d8 CCL fully materializes labels (inherently global), flow_accumulation_mfd frac_bdry held in driver dict instead of memmap-backed BoundaryStore. D8 iterative paths (flow_accum/fill/watershed/basin/stream_*) use serial-tile sweep with memmap-backed boundary store -- per-tile RAM bounded but driver iterates O(diameter) times. flow_direction_*, flow_path/snap_pour_point/twi/hand_d8/hand_dinf are SAFE."
|
|
25
|
+
interpolate_spline,2026-06-04,SAFE,compute-bound,0,,"scope=spline-only. Audited _spline.py + _validation.py only (not _idw/_kriging). 1 MEDIUM (Cat3 GPU transfer): _spline_dask_cupy/_spline_cupy re-uploaded invariant x_pts/y_pts/weights host->device once per chunk. Fixed in PR #2929: added _tps_evaluate_gpu taking on-device point/weight arrays + only per-chunk grid slices; dask+cupy uploads invariants once at graph build (verified 48->3 on 16 chunks, scales with chunk count). numpy/cupy/dask+cupy parity ~1e-14. Added cupy+dask+cupy parity tests and an upload-count regression test (red without fix: 48!=3). _tps_cuda_kernel 30 regs/thread, 6 scalar locals -- no register pressure. CPU/dask+numpy eval @ngjit, row-major, no materialization. Dask graph probe 2560x2560/256 chunks = 200 tasks (2/chunk), no fan-in. Memory guard _check_spline_memory bounds N^2 solve. No issue filed -- gh issue create denied by auto-mode classifier; finding surfaced directly by sweep. GitHub issue field left empty."
|
|
26
|
+
interpolate-kriging,2026-06-04,SAFE,graph-bound,0,2923,"MEDIUM: memory guard used full-grid k0 term on dask templates -> spurious MemoryError (issue #2923, fixed). LOW: _experimental_variogram nlags python loop vectorizable via bincount (~1.2x, pair-array materialization dominates) - doc only. Dask graph clean (2 tasks/chunk); cupy returns device arrays; no .values/.compute/.data.get materialization."
|
|
27
|
+
kde,2026-04-14T12:00:00Z,SAFE,compute-bound,0,,Graph construction serialized per-tile. _filter_points_to_tile scans all points per tile. No HIGH findings.
|
|
28
|
+
mahalanobis,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,False positive. Numpy path materializes by design. Dask path uses lazy reductions + map_blocks.
|
|
29
|
+
morphology,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
30
|
+
multispectral,2026-05-02,SAFE,compute-bound,0,,"Re-audit 2026-05-02 after PRs 1292 (true_color memory guard) and 1301 (validate_arrays in true_color). Verified SAFE. No HIGH. MEDIUM: da.stack in _true_color_dask/_true_color_dask_cupy at L1702/L1731 creates (1,1,1,1) chunks along band axis (4 bands so impact is minor, scheduling overhead not OOM). LOW: np.zeros((h,w,4)) at L1681 then full overwrite -- np.empty would suffice. All 17 indices use plain map_blocks with no halo; 8192x8192 ndvi graph is 80 tasks, evi/arvi/ebbi 112 tasks."
|
|
31
|
+
normalize,2026-03-31T18:00:00Z,SAFE,compute-bound,0,1124,Boolean indexing replaced with lazy nanmin/nanmax/nanmean/nanstd.
|
|
32
|
+
pathfinding,2026-04-15T12:00:00Z,SAFE,compute-bound,0,false-positive,Downgraded. CuPy .get() is required -- A* has no GPU kernel. Per-pixel .compute() is only 2 calls for start/goal validation. seg.values in multi_stop_search collects already-computed results for stitching.
|
|
33
|
+
perlin,2026-03-31T18:00:00Z,WILL OOM,memory-bound,0,,
|
|
34
|
+
polygon_clip,2026-04-16T12:00:00Z,SAFE,compute-bound,0,1207,Re-audit 2026-04-16: fix verified SAFE. Mask stays lazy via rasterize chunks kwarg; per-chunk peak bounded.
|
|
35
|
+
polygonize,2026-05-29,RISKY,compute-bound,0,2608,"Pass 2 (2026-05-29): re-audit. 0 HIGH. 1 MEDIUM fixed (#2608): _polygonize_dask called dask.compute() once per chunk in a nested Python loop, serializing one chunk per scheduler round-trip. Fixed to batch one dask.compute() per chunk row. Output byte-identical (verified conn=4 and conn=8). Measured 2.79x faster on a 4-worker LocalCluster (1024x1024/64 chunks); threaded-scheduler win is marginal (~1.03x warm) since @ngjit kernels release the GIL. 8 new tests in test_polygonize_dask_row_batch_2608.py; 299 polygonize tests pass. Cat1 clean (no .values/.compute-in-loop wrapping dask; np.asarray at L1064/L2278 only wrap CPU input / user transform). Cat3: no @cuda.jit kernels; _polygonize_cupy GPU->CPU transfer is documented (boundary tracing is sequential, cannot run on GPU); cupy int path runs end-to-end ~2.2s/512x512, dominated by CPU _scan. Cat4 LOW (not fixed): _calculate_regions_cupy allocates bin_mask=(data==v) per unique value (O(n_unique) passes); verified low impact, _scan dominates. Cat5 clean. Cat6: RISKY unchanged -- driver accumulates O(total polygons) interior polys; per-row batch keeps peak bounded to one row. bottleneck=compute-bound (_scan). | Re-audit 2026-04-16 after PR 1190 NaN fix + 1176 simplification."
|
|
36
|
+
proximity,2026-03-31T18:00:00Z,WILL OOM,memory-bound,3,1111,Memory guard added to line-sweep path. KDTree path (EUCLIDEAN/MANHATTAN + scipy) already had guards. GREAT_CIRCLE unbounded path already guarded.
|
|
37
|
+
rasterize,2026-05-27,SAFE,graph-bound,0,2506,"Pass 3 (2026-05-27): re-audit identified 1 MEDIUM Cat-3 GPU-transfer finding. _run_cupy (L2065/L2083) and _rasterize_tile_cupy (L2541/L2555) called cupy.asarray(poly_props/poly_global) twice when all_touched=True -- once for the scanline poly_launch tuple and once for the supercover boundary_launch tuple. The two tuples reference the same per-tile props tables. Filed #2506 and fixed by hoisting the upload above the scanline/boundary conditional so both launches share the same device buffer. Microbench: 1000 polys/4 cols 0.051->0.024 ms/iter (2.1x); 10000 polys/8 cols 0.218->0.092 ms/iter (2.4x, saves 720 KB/tile of redundant H2D transfer). 12 new tests in test_rasterize_props_hoist_2506.py (4 AST-structural single-asarray-call assertions + 5 cupy all_touched parity merges + 3 dask+cupy smoke tests). All 470 rasterize tests pass. Dask graph probe: 25600x25600 chunks=1024 yields 2500 tasks for 625 tiles (4 tasks/chunk), unchanged. Noted pre-existing dask+cupy all_touched parity gap on boundary segments crossing tile borders (not addressed by this PR). SAFE/graph-bound verdict holds. | Pass 2 (2026-05-17): re-audit identified MEDIUM Cat-2/Cat-3 graph-bound waste in _run_dask_numpy/_run_dask_cupy -- full line_props/point_props embedded in every delayed tile task (polygon path already filtered via poly_props[pmask]). Filed #2020 and fixed: added _slice_props_for_tile helper to remap geom_idx and slice props per tile (mirrors polygon path). Measured 5000 points x 8 cols / 100 tiles graph shrank from ~30 MB to <0.3 MB (37x); localized lines from ~32 MB to ~1.1 MB. 9 new tests in test_rasterize_tile_props_slice_2020.py (helper unit tests + graph-payload bound + numpy/dask output parity for lines/points/sum-merge). All 184 existing rasterize tests pass; dask+cupy parity verified. Dask graph probe: 2560x2560 chunks=256 yields 400 tasks (4 tasks/chunk constant); 25600x25600 chunks=1024 yields 2500 tasks. cupy 512x512 returns cupy.ndarray with no host round-trip. CUDA _scanline_fill_gpu: 39 regs/thread, 24576 B local_mem/thread (matches static cuda.local.array allocations 2048*8 + 2048*4 bytes). SAFE/graph-bound verdict holds; previous 2026-04-15 false-positive on polygon filtering still valid. | Original (2026-04-15): Tile-by-tile graph construction with per-tile geometry filtering is the correct pattern. Pre-filtering ensures each delayed task gets only its relevant subset."
|
|
38
|
+
reproject,2026-05-10,SAFE,compute-bound,1,1571,"Pass 5 (2026-05-10): 1 HIGH filed and fixed in tree -- issue #1571 + fix _merge_block_adapter same-CRS dask path. _place_same_crs in the dask adapter previously called src_data.compute() on the full source per output chunk (68x amplification measured on 256x256x2 source split into 32x32 output chunks, 8.9M pixels materialized vs 131K total source). Fix: added _place_same_crs_lazy at __init__.py:1716 that slices the source window first then computes only that slice. Verified post-fix: 1.00x ratio, 131K pixels materialized for 131K source. New regression test test_merge_dask_same_crs_bounded_materialization codifies the bound. Other audits clean: CUDA resample kernels use 16x16 blocks (cubic=46 regs, bilinear=36, nearest=22 -- well under the 64K-per-block limit, 0 local mem). _reproject_chunk_numpy/cupy already slice source first before .compute(). Dask graph at 25600x25600 src with 1024 chunks yields 4752 tasks (no per-chunk source dependency). _apply_vertical_shift uses in-place += that may not work on dask arrays -- correctness concern, not perf, defer to accuracy sweep."
|
|
39
|
+
resample,2026-04-15T12:00:00Z,SAFE,compute-bound,0,false-positive,Downgraded. GPU-CPU-GPU round-trip only in aggregate path for non-integer scale factors. Interpolation (nearest/bilinear/cubic) stays on GPU. No GPU kernel exists for irregular per-pixel binning.
|
|
40
|
+
sieve,2026-04-14T12:00:00Z,WILL OOM,memory-bound,0,false-positive,False positive. Memory guards already in place on both dask paths. CCL is inherently global — documented limitation. CuPy CPU fallback is deliberate and documented.
|
|
41
|
+
sky_view_factor,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
42
|
+
slope,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
43
|
+
surface_distance,2026-03-31T18:00:00Z,SAFE,memory-bound,0,1128,Memory guard added to dd_grid allocation.
|
|
44
|
+
terrain,2026-03-31T18:00:00Z,RISKY,compute-bound,0,,
|
|
45
|
+
terrain_metrics,2026-03-31T18:00:00Z,SAFE,memory-bound,0,,
|
|
46
|
+
viewshed,2026-04-05T12:00:00Z,SAFE,memory-bound,0,fixed-in-tree,Tier B memory estimate tightened from 280 to 368 bytes/pixel (accounts for lexsort double-alloc + computed raster). astype copy=False avoids needless float64 copy.
|
|
47
|
+
visibility,2026-04-16T12:00:00Z,SAFE,memory-bound,0,fixed-in-tree,"Re-audit after Numba-ize (PR 1177) confirms SAFE. @ngjit kernels clean, type-stable. MEDIUM: K-observer graph growth in cumulative_viewshed (recommend periodic persist)."
|
|
48
|
+
worley,2026-03-31T18:00:00Z,SAFE,compute-bound,0,,
|
|
49
|
+
zonal,2026-05-27,SAFE,compute-bound,0,2526,"Pass 2 (2026-05-27): re-audit identified 3 MEDIUM findings. (1) zonal_apply 3D dask path: da.stack(layers, axis=2) left output chunks at size 1 along axis 2 -- filed #2526 and fixed by rechunking back to values_data.chunks[2] in _apply_dask_numpy (zonal.py:1691) and _apply_dask_cupy (zonal.py:1731). Confirmed via graph probe: 256x256 raster chunks=(64,64) 3 bands previously yielded chunks[2]=(1,1,1); now (3,). 1 new test (test_apply_dask_3d_axis2_rechunked_2526). 126 existing zonal tests pass. (2) _stats_cupy (zonal.py:588-608): per-zone x per-stat Python loop with cupy.float_(result) forces O(n_zones * n_stats) GPU<->CPU sync points; not fixed in this pass (CUDA-native rewrite needed, larger refactor). (3) _parallel_variance @delayed reduce iterates over all blocks in driver memory; for very large block counts the single-task merge becomes scheduler-bound but is not OOM since per-block arrays are O(n_zones). Not fixed (algorithmic refactor needed). Dask graph probe: stats(7 stats) on 2560x2560 chunks=256 -> 4449 tasks (44/chunk); stats(mean only) -> 823 tasks (8/chunk); crosstab -> 304 (3/chunk); hypsometric_integral -> 300 (3/chunk). All under 50K cap. SAFE/compute-bound verdict holds. | Fixed-in-tree 2026-04-16: rewrote hypsometric_integral dask path. Eliminated double-compute (_unique_finite_zones removed, each block discovers own zones). Replaced np.stack (O(n_blocks * n_zones) scheduler memory) with streaming dict-merge (O(n_zones)). 29 existing tests pass."
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module,last_inspected,issue,severity_max,categories_found,followup_issues,notes
|
|
2
|
+
aspect,2026-04-23,,,,,"Clean. aspect() calls _validate_raster at line 400 and _validate_boundary at line 406. northness()/eastness() delegate to aspect() so inherit validation. Cat 1: allocations match input shape. Cat 3: CPU and GPU kernels propagate NaN correctly through arctan2. Cat 4: _run_gpu (planar, aspect.py:144-147) uses combined bounds+stencil guard. _run_gpu_geodesic_aspect (geodesic.py:395) has explicit bounds check. No shared memory. Cat 5: no file I/O. Cat 6: all backends cast dtype explicitly; tests cover int32/int64/uint32/uint64/float32/float64."
|
|
3
|
+
balanced_allocation,2026-04-23,,,,,"Clean. Cat 1: memory guard at lines 311-326 uses _available_memory_bytes() and raises MemoryError when total_estimate (array_bytes * (n_sources + 3)) exceeds 0.8 * avail BEFORE computing any cost surface. Trivial n_sources==0/1 paths only allocate arrays matching input size. Cat 2: np.prod(raster.shape) returns int64, no overflow. Cat 3: divisions by target_weight (lines 373, 380) are guarded by total==0 break (364) and target_weight>0 check (379); fric_weight strips NaN via np.where(np.isfinite & >0). Cat 4: no CUDA kernels. Cat 5: no file I/O. Cat 6: _validate_raster called on both raster and friction (lines 275-277)."
|
|
4
|
+
bilateral,2026-04-23,1236,HIGH,1,,"HIGH (fixed #1236): bilateral() validated sigma_spatial only as > 0, with no upper bound. The derived kernel radius = ceil(2*sigma_spatial) drove the _pad_array allocation (H+2r, W+2r) when boundary != 'nan' and the dask map_overlap depth on every backend. sigma_spatial=1e9 on a 100x100 raster -> radius=2e9 -> ~128 EB padded float64 allocation. sigma_spatial=1e5 -> ~320 GB. Fixed by clamping radius to max(rows, cols) in bilateral() before dispatch; inner numba/CUDA loops were already clamped to rows/cols so the output is unchanged for realistic inputs. No other HIGH findings: GPU kernel has bounds guard (if 0 <= x < cols and 0 <= y < rows), _validate_raster is called on agg, agg.data.astype(float) is applied before dispatch, NaN propagation is explicit (center NaN -> NaN out, neighbor NaN skipped), division by w_sum is guarded (w_sum > 0.0). MEDIUM (unfixed, Cat 3): sigma_spatial underflow (e.g. 1e-200) makes inv_2_ss = inf and can propagate NaN through exp() at the center pixel, but not safety-critical."
|
|
5
|
+
bump,2026-04-22,1231,HIGH,1,,"HIGH (fixed #1231): _finish_bump allocated np.zeros((height, width)) with no memory guard. The existing count guard (added in #1206) only protected the locs/heights arrays, so bump(width=1_000_000, height=1_000_000) passed the guard (count capped at 10M ~ 160 MB) and then tried to allocate an 8 TB float64 raster. Fixed by extending the memory budget check to include raster_bytes = w * h * 8 when the backend will materialize the full array; dask paths build per-chunk and are excluded. No other HIGH findings: _bump_dask_numpy/_bump_dask_cupy build output lazily via da.from_delayed, no CUDA kernels (cupy path wraps the numba CPU kernel), no file I/O, no int32 overflow in realistic scenarios. MEDIUM (unfixed, Cat 6): bump() does not call _validate_raster on agg (dtype is not checked; shape unpacking catches wrong-ndim, but a non-numeric DataArray would fail confusingly downstream)."
|
|
6
|
+
classify,2026-04-24,,,,1244;1246,"Re-audited 2026-04-24 after PRs #1245 (22b325e, equal_interval degenerate input) and #1248 (3963f15, natural_breaks Jenks matrix cap) landed on HEAD. Cat 1: output allocations (_cpu_binary line 57, _cpu_bin line 179, _run_cupy_binary line 99, _run_cupy_bin line 271) all match input shape which is bounded by caller. Jenks matrices in _run_numpy_jenks_matrices are now guarded via _available_memory_bytes() at classify.py:686-697. Head/tail, maximum_breaks, box_plot, percentiles all allocate bounded by input. Cat 2: no flat index math (kernels iterate (y,x) directly); numba loop variables default to int64. Cat 3: _cpu_bin guards NaN via np.isfinite(val) before binary search (line 189); _run_cupy_bin strips inf to nan before the CUDA kernel (line 267) and NaN comparisons fall through to val_bin=-1 which writes np.nan; binary search bounds (bins[mid-1] when mid=0) are safe because nbins>=2 plus val>bins[0] guarantees first iteration takes the start=mid+1 branch. Cat 4: _run_gpu_binary (line 92) and _run_gpu_bin (line 261) both have i/j bounds guards; no shared memory. Cat 5: only /proc/meminfo read (hardcoded, line 41), no user-path I/O. Cat 6: all 10 public functions (binary, reclassify, quantile, natural_breaks, equal_interval, std_mean, head_tail_breaks, percentiles, maximum_breaks, box_plot) call _validate_raster with numeric=True. LOW (not flagged): _generate_sample_indices / _compute_natural_break_bins use np.uint32 for linspace idx (wraps at num_data > 2^32 ~ 4.3B pixels) but a 32+ GB input would already trip the Jenks memory guard. LOW (not flagged): reclassify does not validate bins/new_values dtype; object-dtype input would fail confusingly inside numba but is a self-inflicted caller error."
|
|
7
|
+
contour,2026-04-23,1240,HIGH,1,,"HIGH (fixed #1240): _contours_numpy allocated two (max_segs_per_level, 2) float64 buffers per level with no memory check, where max_segs_per_level = 2*(ny-1)*(nx-1). A 20000x20000 raster peaked at ~12.8 GB per level before touching _stitch_segments' endpoint dict. Fixed by adding _available_memory_bytes() guard (32 bytes/segment) that raises MemoryError before np.empty when estimate > 0.5 * available. CuPy path transfers to CPU and inherits the guard; dask paths process each chunk independently and are not affected. MEDIUM (unfixed, Cat 6): contours() does not call _validate_raster -- only ndim and shape are checked, dtype is not validated (object/string dtypes would fail later with a confusing error). No CUDA kernels. No file I/O. NaN handling via self-comparison (line 50) and division-by-zero guarded in _emit_seg interpolation."
|
|
8
|
+
convolution,2026-04-23,1241,HIGH,1,,"HIGH (fixed #1241): circle_kernel() and annulus_kernel() in xrspatial/convolution.py accepted a user-supplied radius with no upper bound. The kernel is built via _ellipse_kernel(half_w, half_h) where half_w = int(radius_meters/cellsize_x), so memory grew quadratically with the radius. cellsize=1, radius=100000 -> 200001x200001 float64 ~ 320 GB. annulus_kernel calls circle_kernel twice so the same hole applied. Fixed by adding _check_kernel_memory() (local _available_memory_bytes() helper like bump.py/viewshed.py) and calling it in circle_kernel before _ellipse_kernel. Budget = 32 bytes/cell to cover the output plus linspace/ellipse-mask temporaries; raises MemoryError when required > 0.5*available. No other HIGH findings: _convolve_2d_cuda has bounds guard (lines 371-373) and inner-index check (lines 384-385), no shared memory/syncthreads needed. All four backends call _promote_float on input dtype so integer inputs cast to float32 cleanly; _convolve_2d_numpy propagates NaN through multiply+accumulate. No file I/O. MEDIUM (unfixed, Cat 6): convolve_2d() does not call _validate_raster on input; non-numeric DataArray would fail inside numba/cupy with a confusing error. MEDIUM (unfixed, Cat 1): custom_kernel() does not cap kernel shape, so a caller can still pass a huge np.ones((N,N)) directly -- but that is a self-inflicted allocation outside the library, and _convolve_2d_numpy would still try to padded-allocate around it via _pad_array."
|
|
9
|
+
corridor,2026-04-24,,,,,"Clean. Cat 1: corridor = cd_a + cd_b allocates a same-shape array, but cost_distance already applies its own memory guards before materializing cd_a and cd_b, so no new unbounded allocation is introduced here. Pairwise mode creates N*(N-1)/2 corridor surfaces from a user-supplied sources list, but each is bounded by cost_distance's guard and N is under caller control. Cat 2: no int32 index math. Cat 3: cost_distance returns NaN for unreachable pixels (not Inf); NaN propagates correctly through cd_a + cd_b and through the - corridor_min subtraction, so reach-one-unreachable pixels stay NaN. The all-unreachable case (corridor_min is NaN) is handled explicitly via np.isfinite(corridor_min) check returning all-NaN. No divisions in corridor.py. Cat 4: no CUDA kernels. Cat 5: no file I/O. Cat 6: _validate_raster is called on source_a, source_b, each entry in sources, and friction (when precomputed=False). cost_distance itself enforces raster.shape == friction.shape. Precomputed path treats source_a/source_b as cost-distance surfaces and still runs _validate_raster on them. Minor UX (not a security issue): relative threshold uses threshold * corridor_min, which collapses to 0 when sources overlap (corridor_min=0)."
|
|
10
|
+
cost_distance,2026-04-25,1262,HIGH,1,,"HIGH (fixed #1262): the recent #1252/#1253 patch only guarded the numpy path (_cost_distance_numpy -> _check_memory). _cost_distance_cupy on the GPU backend ran the same allocation pattern (cp.full((H,W), inf, float64) + source_mask + passable + cp.where intermediate + float32 out) with no guard. A 100000x100000 cupy raster requested ~80 GB on the device, which the cupy allocator surfaces as an opaque internal error rather than a clean MemoryError pointing at max_cost= or dask. Fixed by adding _available_gpu_memory_bytes() (uses cupy.cuda.runtime.memGetInfo, returns 0 when unavailable) and _check_gpu_memory(h, w) that raises MemoryError before the first cp.full when 24 bytes/pixel exceeds 50% of free GPU RAM. Wired into _cost_distance_cupy at line 407, which also covers the dask+cupy map_overlap path because that path calls _cost_distance_cupy per chunk. The dask+cupy unbounded fallback already converts to dask+numpy and inherits the existing _check_memory guard. MEDIUM (unfixed, Cat 1): _cost_distance_dask map_overlap chunk_func path calls _cost_distance_kernel directly without going through _cost_distance_numpy's _check_memory, so a single very large dask chunk could still OOM -- bounded by user-controlled chunk size, lower priority. MEDIUM (unfixed, Cat 6): _validate_raster(numeric=True) accepts integer-dtype rasters; the kernel's np.isfinite() check on int data is always True so int-encoded sentinel values would not be treated as impassable, but this is caller-controlled. No CUDA bounds issues: _cost_distance_relax_kernel has iy/ix>=H/W guard at line 314-315 and neighbor bounds check at line 327. No file I/O beyond the hardcoded /proc/meminfo read. No int32 overflow risk: max_heap is allocated with explicit dtype=int64."
|
|
11
|
+
curvature,2026-04-25,,,,,"Clean. Small (271 LOC) module computing 3x3 second-derivative stencil. Cat 1: only single output buffer matching input shape (np.empty at line 37, cupy.empty at line 101) -- bounded by caller, per audit guidance not a finding. Cat 2: _cpu numba kernel uses range(1, rows-1)/range(1, cols-1) with simple (y, x) indices; no flat indexing or queue arrays; numba range loops produce int64. Cat 3: division by cellsize*cellsize on line 44 -- cellsize comes from get_dataarray_resolution() (raster property, not user-direct); cellsize=0 is unrealistic and would produce inf consistently across backends. NaN inputs propagate correctly through float arithmetic. Cat 4: _run_gpu (line 79-86) has full bounds guard via 'i + di <= out.shape[0] - 1 and j + dj <= out.shape[1] - 1' which guarantees i < shape[0] and j < shape[1] before the out[i, j] write; no shared memory; out is pre-filled with NaN at line 102 so threads outside the guard correctly leave NaN. Cat 5: no file I/O. Cat 6: curvature() calls _validate_raster at line 253; all four backend paths explicitly cast to float32 (lines 51, 62, 97, 112) so dtype is normalized before any computation; tests cover int32/int64/uint32/uint64/float32/float64 across numpy/cupy/dask+numpy/dask+cupy."
|
|
12
|
+
dasymetric,2026-04-25,1261,HIGH,1;6,,"HIGH (fixed #1261): pycnophylactic() and disaggregate(method='limiting_variable') allocated full-shape working arrays without checking available memory first. _pycnophylactic_numpy additionally stored one full-shape bool mask per zone in zone_masks, so peak memory grew with N_zones * H * W (1000 zones on a 10000x10000 raster ~ 100 GB just for masks on top of ~3.4 GB of iteration buffers). Fixed by adding _available_memory_bytes() helper and two budget functions (_check_disaggregate_memory, _check_pycnophylactic_memory) that raise MemoryError before the first allocation when projected working memory exceeds 50% of available RAM. The disaggregate guard runs only for in-RAM backends (numpy, cupy); dask paths process per-chunk and are skipped. The pycnophylactic guard scales with len(values_dict) so an exploding zone count is rejected even on a small raster. MEDIUM (unfixed, Cat 6): disaggregate() and pycnophylactic() do not call _validate_raster on zones/weight; they only check isinstance(xr.DataArray), ndim, and shape. Object-dtype or other non-numeric input would fail with confusing TypeError from inside numpy.asarray rather than a clean ValueError. Deferred to a separate PR per the security-sweep one-fix-per-PR policy."
|
|
13
|
+
diffusion,2026-04-27,1267,HIGH,1;3,1281,"HIGH (fixed #1267): diffuse() had no memory guard on its core allocations and steps was unbounded. (1) The public API allocated np.full(agg.shape) for scalar diffusivity even when the dispatched backend was dask, forcing a full numpy alpha raster up front -- a 100kx100k input would OOM on an 80 GB allocation before any backend dispatch. (2) _diffuse_step_numpy and _diffuse_cupy allocated per-step buffers with no memory check. (3) steps was validated only with min_val=1, so steps=10**12 was accepted and would loop forever. Fixed by adding _check_memory/_check_gpu_memory helpers (cost_distance pattern, ~32 B/pixel budget for u + out + alpha + padded copy at 50% of available RAM/VRAM), deferring the np.full alpha allocation until after the guard runs in eager paths, teaching _diffuse_dask_cupy to handle scalar alpha lazily via cp.full per chunk (mirroring _diffuse_dask_numpy), and capping steps at _MAX_STEPS = 100_000 in _validate_scalar. GPU kernel _diffuse_step_gpu has bounds guard (if i < rows and j < cols), no shared memory, _validate_raster called on agg and on diffusivity DataArray, NaN check uses val != val correctly, no file I/O, no int32 indexing. Follow-up HIGH (fixed #1281): user-supplied dt was validated only as > 0, but explicit forward-Euler is unconditionally unstable above 0.25 * dx**2 / max(alpha); the dt=None branch already used this exact bound, so the fix hoists it into cfl_max and raises ValueError when the user-supplied dt exceeds it. Single check in the public entrypoint covers all four backends."
|
|
14
|
+
edge_detection,2026-04-25,1271,MEDIUM,6,,"MEDIUM (fixed #1271): the five public functions sobel_x, sobel_y, laplacian, prewitt_x, prewitt_y did not call _validate_raster on agg. Non-DataArray inputs raised AttributeError from agg.data and wrong-ndim DataArrays failed inside numba/cupy with confusing errors instead of clean TypeError/ValueError. Numerical correctness was unaffected because convolve_2d._promote_float casts integer dtypes to float32 before the kernel runs. Fixed by adding _validate_raster(agg, func_name=..., name='agg') at the top of each function. No CRITICAL/HIGH findings: convolve_2d enforces 3x3 odd kernels and 2D agg.data, allocations match input shape, no CUDA kernels owned by this module, no file I/O."
|
|
15
|
+
emerging_hotspots,2026-04-25,1274,HIGH,1,,"HIGH (fixed #1274): emerging_hotspots() public API only validated ndim and shape[0] >= 2. The numpy and cupy backends each materialised three full (T, H, W) cubes (a float32 input copy, gi_zscore float32, gi_bin int8) plus H*W temporaries with no memory check; a (100, 20000, 20000) input projected to ~480 GB. Fixed by adding _available_memory_bytes()/_check_memory(n_times, ny, nx) (12 bytes per cube cell budget) and calling it from the public API for non-dask inputs. Dask paths skip the guard because their map_blocks/map_overlap chunk functions do not materialise the full cube. MEDIUM (unfixed, Cat 6): public API does not call _validate_raster() so non-numeric dtypes fail later with a confusing error rather than a clean TypeError. No GPU kernels in this module (uses convolve_2d). No file I/O. Cat 3 statistical paths are robust: _mann_kendall_statistic_numpy guards var_s <= 0 before sqrt, both numpy and cupy backends raise ZeroDivisionError on global_std == 0, and _mk_pvalue handles z==0 explicitly."
|
|
16
|
+
erosion,2026-04-25,1275,HIGH,1;3;6,,"HIGH (fixed #1275): erode() accepted three user-controlled parameters with no upper bound. (1) iterations sized rng.random((iterations, 2)) on the host (16 B/particle) and was copied to the GPU via cupy.asarray, so iterations=10**12 attempted ~16 TB on each side. (2) params['radius'] drove _build_brush which iterates (2r+1)**2 cells and stores three arrays of the same length, so radius=10**6 allocated ~12 TB of brush data. (3) params['max_lifetime'] is the inner per-particle JIT loop in both _erode_cpu and _erode_gpu_kernel, so max_lifetime=10**12 with the default iterations=50000 ran 5e16 step iterations. The existing _check_erosion_memory helper only fired on dask paths and ignored the random_pos and brush working sets. Fixed by capping all three parameters at the public erode() entry via _validate_scalar(max_val=...) (_MAX_ITERATIONS=1e8, _MAX_RADIUS=1024, _MAX_LIFETIME=1e5), rewriting _check_erosion_memory to include the random_pos buffer and brush bytes in its budget, and wiring the guard into _erode_numpy and _erode_cupy so every backend benefits (the dask paths inherit it via their _erode_numpy/_erode_cupy calls). Mirrors diffuse #1268 pattern. Deferred follow-ups (separate PRs): Cat 3 HIGH NaN input is not guarded in _erode_cpu / _erode_gpu_kernel -- a NaN cell propagates through bilinear interpolation into dir_x/dir_y, NaN bounds checks fall through, and particles can deposit NaN into arbitrary cells via cuda.atomic.add. Cat 6 MEDIUM erode() does not call _validate_raster() on agg -- non-numeric or wrong-ndim input fails inside numba/cupy with a confusing error. No Cat 2 (no int32 flat-index math), no Cat 4 (GPU kernel has bounds guard at line 184 plus per-step bounds checks before every read/write, brush writes are explicitly bounds-checked, no shared memory), no Cat 5 (no file I/O)."
|
|
17
|
+
fire,2026-04-25,,,,,"Clean. Despite the module's size hint, fire.py is purely per-cell raster ops -- not cellular-automaton or front-tracking. Seven public APIs: dnbr, rdnbr, burn_severity_class, fireline_intensity, flame_length, rate_of_spread, kbdi. No iteration, no queues, no multi-channel state, no random numbers, no file paths. Cat 1: every output allocation matches input shape (single buffer, bounded by caller). Anderson-13 fuel table is a fixed 13x8 constant. _rothermel_fuel_constants returns 12 scalars before dispatch (no per-pixel state). Cat 2: no flat-index math, all indexing is 2-D (y, x); no height*width multiplication. Cat 3: rdnbr guards denom < 1e-10; burn_severity_class is threshold-only; flame_length guards v <= 0.0 before fractional power; rate_of_spread guards M_x>0/beta>0/denom>0 and clamps eta_M, U_mmin, R; kbdi clamps Q to [0, 800] and net_P to >= 0. Adversarial wind=inf or T=inf would push exp/power to inf in rate_of_spread/kbdi but inputs are user-controlled rasters, fire model is research-quality (LOW only). Cat 4: all 7 CUDA kernels (_dnbr_gpu L157, _rdnbr_gpu L246, _bsc_gpu L362, _fli_gpu L455, _fl_gpu L552, _ros_gpu L681, _kbdi_gpu L870) have 'y < out.shape[0] and x < out.shape[1]' bounds guard; every kernel is point-wise (no neighbour stencil) so the simple guard is sufficient; no shared memory, no syncthreads needed. Cat 5: no file I/O. Cat 6: every public function calls _validate_raster on each input raster (dnbr/rdnbr/fireline_intensity/rate_of_spread/kbdi pass 2-3 rasters each, all validated), validate_arrays enforces equal shape, _validate_scalar gates heat_content/fuel_model (1-13)/annual_precip, and every input is .astype('f4') before reaching any kernel so dtype is normalized."
|
|
18
|
+
flood,2026-05-03,1437,MEDIUM,3,,Re-audit 2026-05-03. MEDIUM Cat 3 fixed in PR #1438 (travel_time and flood_depth_vegetation now validate mannings_n DataArray values are finite and strictly positive via _validate_mannings_n_dataarray helper). No remaining unfixed findings. Other categories clean: every allocation is same-shape as input; no flat index math; NaN propagation explicit in every backend; tan_slope clamped by _TAN_MIN; no CUDA kernels; no file I/O; every public API calls _validate_raster on DataArray inputs.
|
|
19
|
+
focal,2026-04-27,1284,HIGH,1,,"HIGH (fixed PR #1286): apply(), focal_stats(), and hotspots() accepted unbounded user-supplied kernels via custom_kernel(), which only checks shape parity. The kernel-size guard from #1241 (_check_kernel_memory) only ran inside circle_kernel/annulus_kernel, so a (50001, 50001) custom kernel on a 10x10 raster allocated ~10 GB on the kernel itself plus a much larger padded raster before any work -- same shape as the bilateral DoS in #1236. Fixed by adding _check_kernel_vs_raster_memory in focal.py and wiring it into apply(), focal_stats(), and hotspots() after custom_kernel() validation. All 134 focal tests + 19 bilateral tests pass. No other findings: 10 CUDA kernels all have proper bounds + stencil guards; _validate_raster called on every public entry point; hotspots already raises ZeroDivisionError on constant-value rasters; _focal_variety_cuda uses a fixed-size local buffer (silent truncation but bounded); _focal_std_cuda/_focal_var_cuda clamp the catastrophic-cancellation case via if var < 0.0: var = 0.0; no file I/O."
|
|
20
|
+
geodesic,2026-04-27,1283,HIGH,1,,"HIGH (fixed PR #1285): slope(method='geodesic') and aspect(method='geodesic') stack a (3, H, W) float64 array (data, lat, lon) before dispatch with no memory check. A large lat/lon-tagged raster passed to either function would OOM. Fixed by adding _check_geodesic_memory(rows, cols) in xrspatial/geodesic.py (mirrors morphology._check_kernel_memory): budgets 56 bytes/cell (24 stacked float64 + 4 float32 output + 24 padded copy + slack) and raises MemoryError when > 50% of available RAM; called from slope.py and aspect.py inside the geodesic branch before dispatch. No other findings: 6 CUDA kernels all have bounds guards (e.g. _run_gpu_geodesic_aspect at geodesic.py:395), custom 16x16 thread blocks avoid register spill, no shared memory, _validate_raster runs upstream in slope/aspect, all backends cast to float32, slope_mag < 1e-7 flat threshold prevents arctan2 NaN propagation, curvature correction uses hardcoded WGS84 R."
|
|
21
|
+
geotiff,2026-05-19,2121,HIGH,1,,"Re-audit pass 19 2026-05-19 (deep-sweep p1). HIGH Cat 1 found in _sidecar.py load_sidecar: HTTP and fsspec sidecar downloads bypassed max_cloud_bytes set on the base file, so a hostile server could OOM the reader via a multi-GB .tif.ovr beside a tiny base TIFF (issue #2121). Fixed in deep-sweep-security-geotiff-2026-05-19-01 (PR #2123) by threading max_cloud_bytes through load_sidecar and applying it on both transports (HTTP via _HTTPSource.read_all max_bytes streaming cap, fsspec via fs.size() pre-check raising CloudSizeLimitError). Test: tests/test_sidecar_max_cloud_bytes_2121.py. All other categories verified clean against new commits 68574fe (.tif.ovr sidecar), 6b88cea (allow_rotated rotated MTT), f2e191d (multi-ModelTiepoint GCP rejection), 1e9c432 (GPU per-tile byte cap). Carries forward: JPEG bomb cap (#1792), HTTP read_all byte budget (#2057), VRT XML cap, DOCTYPE rejection, path containment, SSRF, validate_tile_layout, dimension caps, IFD entry caps, MAX_IFDS, MAX_PIXEL_ARRAY_COUNT, GPU bounds guards, atomic writes, realpath canonicalization, dtype validation."
|
|
22
|
+
glcm,2026-04-24,1257,HIGH,1,,"HIGH (fixed #1257): glcm_texture() validated window_size only as >= 3 and distance only as >= 1, with no upper bound on either. _glcm_numba_kernel iterates range(r-half, r+half+1) for every pixel, so window_size=1_000_001 on a 10x10 raster ran ~10^14 loop iterations with all neighbors failing the interior bounds check (CPU DoS). On the dask backends depth = window_size // 2 + distance drove map_overlap padding, so a huge window also caused oversize per-chunk allocations (memory DoS). Fixed by adding max_val caps in the public entrypoint: window_size <= max(3, min(rows, cols)) and distance <= max(1, window_size // 2). One cap covers every backend because cupy and dask+cupy call through to the CPU kernel after cupy.asnumpy. No other HIGH findings: levels is already capped at 256 so the per-pixel np.zeros((levels, levels)) matrix in the kernel is bounded to 512 KB. No CUDA kernels. No file I/O. Quantization clips to [0, levels-1] before the kernel and NaN maps to -1 which the kernel filters with i_val >= 0. Entropy log(p) and correlation p / (std_i * std_j) are both guarded. All four backends use _validate_raster and cast to float64 before quantizing. MEDIUM (unfixed, Cat 1): the per-pixel np.zeros((levels, levels)) allocation inside the hot loop is a perf issue (levels=256 -> 512 KB alloc+free per pixel) but not a security issue because levels is bounded. Could be hoisted out of the loop or replaced with an in-place clear, but that is an efficiency concern, not security."
|
|
23
|
+
gpu_rtx,2026-04-29,1308,HIGH,1,,"HIGH (fixed #1308 / PR #1310): hillshade_rtx (gpu_rtx/hillshade.py:184) and viewshed_gpu (gpu_rtx/viewshed.py:269) allocated cupy device buffers sized by raster shape with no memory check. create_triangulation (mesh_utils.py:23-24) adds verts (12 B/px) + triangles (24 B/px) = 36 B/px; hillshade_rtx adds d_rays(32) + d_hits(16) + d_aux(12) + d_output(4) = 64 B/px (100 B/px total); viewshed_gpu adds d_rays(32) + d_hits(16) + d_visgrid(4) + d_vsrays(32) = 84 B/px (120 B/px total). A 30000x30000 raster asked for 90-108 GB of VRAM before cupy surfaced an opaque allocator error. Fixed by adding gpu_rtx/_memory.py with _available_gpu_memory_bytes() and _check_gpu_memory(func_name, h, w) helpers (cost_distance #1262 / sky_view_factor #1299 pattern, 120 B/px budget covers worst case, raises MemoryError when required > 50% of free VRAM, skips silently when memGetInfo() unavailable). Wired into both entry points after the cupy.ndarray type check and before create_triangulation. 9 new tests in test_gpu_rtx_memory.py (5 helper-unit + 4 end-to-end gated on has_rtx). All 81 existing hillshade/viewshed tests still pass. Cat 4 clean: all CUDA kernels (hillshade.py:25/62/106, viewshed.py:32/74/116, mesh_utils.py:50) have bounds guards; no shared memory, no syncthreads needed. MEDIUM not fixed (Cat 6): hillshade_rtx and viewshed_gpu do not call _validate_raster directly but parent hillshade() (hillshade.py:252) and viewshed() (viewshed.py:1707) already validate, so input validation runs before the gpu_rtx entry point - defense-in-depth, not exploitable. MEDIUM not fixed (Cat 2): mesh_utils.py:64-68 cast mesh_map_index to int32 in the triangle index buffer; overflows at H*W > 2.1B vertices (~46341x46341+) but the new memory guard rejects rasters that large first - documentation/clarity item rather than exploitable. MEDIUM not fixed (Cat 3): mesh_utils.py:19 scale = maxDim / maxH divides by zero on an all-zero raster, propagating inf/NaN into mesh vertex z-coords; separate follow-up. LOW not fixed (Cat 5): mesh_utils.write() opens user-supplied path without canonicalization but its only call site (mesh_utils.py:38-39) sits behind if False: in create_triangulation, not reachable in production."
|
|
24
|
+
hillshade,2026-04-27,,,,,"Clean. Cat 1: only allocation is the output np.empty(data.shape) at line 32 (cupy at line 165) and a _pad_array with hardcoded depth=1 (line 62) -- bounded by caller, no user-controlled amplifier. Azimuth/altitude are scalars and don't drive size. Cat 2: numba kernel uses range(1, rows-1) with simple (y, x) indexing; numba range loops promote to int64. Cat 3: math.sqrt(1.0 + xx_plus_yy) is always >= 1.0 (no neg sqrt, no div-by-zero); NaN elevation propagates correctly through dz_dx/dz_dy -> shaded -> output (the shaded < 0.0 / shaded > 1.0 clamps don't fire on NaN). Azimuth validated to [0, 360], altitude to [0, 90]. Cat 4: _gpu_calc_numba (line 107) guards both grid bounds and 3x3 stencil reads via i > 0 and i < shape[0]-1 and j > 0 and j < shape[1]-1; no shared memory. Cat 5: no file I/O. Cat 6: hillshade() calls _validate_raster (line 252) and _validate_scalar for both azimuth (253) and angle_altitude (254); all four backend paths cast to float32; tests parametrize int32/int64/float32/float64."
|
|
25
|
+
hydro,2026-05-03,1423;1425;1427;1429,HIGH,1;3;6,,"Re-audit 2026-05-03. ALL HIGH and MEDIUM findings fixed across 4 PRs. HIGH (Cat 1) fixed in PR #1424: flow_direction_mfd numpy/cupy memory guard ports _check_memory / _check_gpu_memory from flow_accumulation_mfd. MEDIUM Cat 6 fixed in PR #1426: secondary DataArray args validated across watershed_*/snap_pour_point_d8/flow_path_*/stream_link_*/stream_order_*. MEDIUM Cat 3 scalars fixed in PR #1428: flow_direction_mfd p (finite>0), snap_pour_point_d8 search_radius (positive int), hand_*/threshold (finite), fill_d8 z_limit (non-negative finite or None). MEDIUM Cat 3 cellsize fixed in PR #1430: twi_d8/flow_direction_d8/_dinf/_mfd/flow_length_d8/_dinf/_mfd validate cellsize finite-and-non-zero before division. No remaining findings."
|
|
26
|
+
interpolate-kriging,2026-06-04,2917,MEDIUM,3;6,,"Audited _kriging.py (515 LOC) + _validation.py + __init__.py + tests. Cat 1 (alloc): _check_kriging_memory() guards variogram pair arrays, (N+1)x(N+1) matrix, and (grid_pixels,N+1) k0 against 0.8*host RAM; well-tested. LOW gap: cupy path allocates k0 on GPU but guard reads host /proc/meminfo not GPU mem, so large cupy templates can hit cupy OutOfMemoryError (loud, not silent) -- not fixed. Cat 2 (int overflow): memory math uses Python ints (bigint), triu_indices int64; no int32 overflow. Cat 3 (NaN/Inf): singular matrix caught, regularised, then returns None -> all-NaN raster (explicit). variogram divisors bounded a>=1e-12. Cat 4: no CUDA kernels. Cat 5: no file I/O. Cat 6: validate_points coerces float64+drops NaN; _validate_raster on template. FOUND (MEDIUM, fixed): single-point input (n=1 or all-but-one NaN) crashed with opaque numpy 'zero-size array to reduction' ValueError in _experimental_variogram (dists.max() before max_dist guard). Fixed via issue #2917 / PR #2924. CUDA_AVAILABLE=true; cupy/dask+cupy parity tests pass."
|
|
27
|
+
kde,2026-04-27,1287,HIGH,1,,"HIGH (fixed #1287): kde() and line_density() accepted user-controlled width/height with no upper bound. The eager numpy and cupy backends allocated np.zeros((height, width), dtype=float64) (or cupy.zeros) up front (kde.py: _run_kde_numpy line 308, _run_kde_cupy line 314, line_density inline at line 706). width=1_000_000, height=1_000_000 requested ~8 TB of float64 (or VRAM on the GPU path) before any check ran. Fixed by adding local _available_memory_bytes() helper (mirrors convolution/morphology/bump pattern) and _check_grid_memory(rows, cols) that raises MemoryError when rows*cols*8 exceeds 50% of available RAM. Wired into kde() (skipped for dask paths since _run_kde_dask_numpy/_run_kde_dask_cupy build per-tile via da.from_delayed and are bounded by chunk size) and line_density() (single numpy backend, always guarded). Error message names width/height so the caller knows which knob to turn. No other HIGH findings: Cat 2 (no int32 flat-index math, numba range loops are int64), Cat 3 (bandwidth <= 0 rejected, Silverman fallback returns 1.0 when sigma==0, NaN coords clamp to empty range via min/max), Cat 4 (_kde_cuda has 'if r >= rows or c >= cols: return' bounds guard at line 254, no shared memory, each thread writes own pixel), Cat 5 (no file I/O), Cat 6 (template only used for shape/coords, output dtype forced to float64). MEDIUM (unfixed, Cat 6): _validate_template only checks DataArray + ndim; does not call _validate_raster, but template dtype does not affect compute correctness here."
|
|
28
|
+
mahalanobis,2026-04-27,1288,HIGH,1,,"HIGH (fixed #1288): mahalanobis() had no memory guard. Both _compute_stats_numpy/_compute_stats_cupy and _mahalanobis_pixel_numpy/_mahalanobis_pixel_cupy materialise float64 buffers of shape (n_bands, H*W) -- the np.stack at line 45/80, the reshape+transpose at line 184 (which forces a contiguous BLAS copy), the centered diff, and the diff @ inv_cov result are all live at peak. A 100kx100k 5-band raster projected to ~400 GB of host memory just for the stack. Fixed by adding _available_memory_bytes()/_available_gpu_memory_bytes() (mirroring cost_distance.py:261-292) plus _check_memory/_check_gpu_memory at 32 bytes/cell/band budget, and wiring them into the public mahalanobis() entry point before any np.stack runs. Eager paths (numpy, cupy) are guarded; dask paths skip the check because chunks are bounded by user-supplied chunksize. MEDIUM (unfixed, Cat 6): mahalanobis() does not call _validate_raster on each band -- validate_arrays only enforces matching shape and array-type, so boolean / non-numeric DataArrays silently coerce. Deferred to a separate PR per the security-sweep one-fix-per-PR policy. No other HIGH findings: Cat 2 (no int32 indexing, numpy default int64), Cat 3 (singular covariance raises a clean ValueError, dist_sq is clamped to 0 before sqrt to absorb numerical noise, NaN mask propagates correctly), Cat 4 (no CUDA kernels), Cat 5 (no file I/O beyond /proc/meminfo)."
|
|
29
|
+
mcda,2026-04-29,1311,HIGH,3,,Cat 3: NaN/Inf weights silently pass _validate_weights (combine.py:35-39) and owa order_weights check (combine.py:154-158) because abs(NaN-1.0) > 0.01 is False; produces all-NaN raster. Same shape of bug in ahp_weights (weights.py:94) where val<=0 lets NaN slip past. Fixed in #1311 with explicit np.isfinite checks. MEDIUM Cat 1 noted: sensitivity._monte_carlo eagerly computes full dask Dataset; combine.owa stacks all criteria via xr.concat without size guard. MEDIUM Cat 3 noted: sensitivity n_samples=0 divides by zero; wpm permits zero-base/negative-weight without bounds check. No CUDA kernels (Cat 4 N/A); no file I/O (Cat 5 N/A); no int32 index math (Cat 2 N/A).
|
|
30
|
+
morphology,2026-04-24,1256,HIGH,1,,"HIGH (fixed #1256): morph_erode/morph_dilate/morph_opening/morph_closing/morph_gradient/morph_white_tophat/morph_black_tophat accepted a user-supplied kernel with only shape/dtype/odd-size validation. Kernel dimensions drove np.pad/cp.pad on every backend and map_overlap depth on dask paths; a 99999x99999 kernel on a 1000x1000 raster would try to allocate ~80 GB of padded float64 memory with no warning. Fixed by adding local _available_memory_bytes() helper and _check_kernel_memory(rows, cols, ky, kx) that raises MemoryError before allocation when padded size exceeds 50% of available RAM; wired into _dispatch() so every public API entry point is guarded across all four backends. Mirrors bilateral #1236, convolution #1241, bump #1231. No other HIGH findings: Cat 2 (loop indices are Python ints, numba promotes to int64), Cat 3 (NaN propagation explicit via v!=v in both numpy and CUDA paths, tests verify), Cat 4 (GPU kernels _erode_gpu/_dilate_gpu have if i<rows and j<cols bounds guards, no shared memory), Cat 5 (no file I/O), Cat 6 (_validate_raster called in _dispatch, all backends cast to float64 before kernel)."
|
|
31
|
+
multispectral,2026-04-27,1291,HIGH,1,1293,"HIGH (fixed PR #1292): true_color() stacked three same-shape bands into an (H, W, 4) RGBA float64 cube on numpy/cupy backends with no memory check; a 100k x 100k true-color call would request ~320 GB before any error. Fixed by adding _available_memory_bytes / _available_gpu_memory_bytes helpers and _check_true_color_memory / _check_true_color_gpu_memory budget checks (24 bytes/pixel, 50% of available RAM/VRAM threshold) wired into _true_color_numpy and _true_color_cupy; mirrors the dasymetric/cost_distance/diffusion pattern. Dask paths skipped because they build the cube lazily. 151/151 tests pass including 4 new memory-guard tests. Other findings clean: 10 CUDA kernels all have bounds guards (per-pixel index math, no stencil); every per-index public function (NDVI/EVI/SAVI/ARVI/GCI/NDMI/NBR/NBR2) calls _validate_raster on each band and validate_arrays for shape match; division denominators in normalized-difference indices are guarded by NaN propagation; no int32 overflow paths; no file I/O. MEDIUM follow-up #1293 (Cat 6): true_color() does not call validate_arrays(r, g, b) to enforce equal band shapes -- separate PR per the one-fix-per-security-PR policy."
|
|
32
|
+
normalize,2026-04-27,,,,,"Clean. Both rescale and standardize handle the constant-raster failure mode explicitly in every backend: rescale guards data_range == 0, standardize guards std == 0. Empty-finite-mask case handled. NaN/Inf passthrough is explicit via np.isfinite. Tests cover constant rasters, all-NaN, single cell, inf passthrough, and cross-backend parity. Cat 1: only output-shape np.empty plus a finite-only copy in numpy/cupy paths (~3x input size at peak) -- standard pattern, no user-controlled amplifier. Cat 2: no flat-index math, no height*width arithmetic. Cat 3: division by zero and divide-by-NaN both guarded; integer-dtype path verified working (range scaling correct, in contrast to the perlin failure mode #1232). Cat 4 N/A: no CUDA kernels. Cat 5 N/A: no file I/O. Cat 6: _validate_raster called on inputs (lines 164, 303); _validate_scalar on numeric params; output uniformly np.float64."
|
|
33
|
+
pathfinding,2026-05-03,1439,MEDIUM,1;6,,"Re-audit 2026-05-03. MEDIUM Cat 1 + Cat 6 fixed in PR #1440: a_star_search and multi_stop_search now call _validate_raster(surface) and _validate_raster(friction); multi_stop_search caps len(waypoints) at _MAX_WAYPOINTS=1000 to prevent the O(N^3) optimize_order DoS. No remaining unfixed findings. Other categories clean: _check_memory(h,w) already guards numpy/cupy allocations; auto-radius and HPA* fall back; dask uses sparse dict/set; no CUDA kernels; no file I/O."
|
|
34
|
+
perlin,2026-04-22,1232,HIGH,6,,"HIGH (fixed #1232): perlin() accepted integer-dtyped DataArrays via _validate_raster, but all four backends write float noise into the input buffer in place, then normalize by ptp. With integer storage the float values cast to 0, ptp=0, and the div-by-zero produced NaN/Inf that cast back to INT_MIN on every pixel. Fixed by adding an np.issubdtype(agg.dtype, np.floating) check in perlin() that raises ValueError. MEDIUM (unfixed follow-up): _perlin_numpy/_perlin_cupy/_perlin_dask_numpy/_perlin_dask_cupy all divide by ptp/(max-min) with no zero guard, so degenerate inputs like freq=(0,0) still emit NaN through the normalization step. GPU kernels have bounds guards, shared memory is fixed-size 512 int32 (not user-influenced), cuda.syncthreads() is present after the cooperative load. No file I/O."
|
|
35
|
+
polygon_clip,2026-04-27,,,,,"Clean. Module is a raster mask-and-clip wrapper -- not a Sutherland-Hodgman polygon-vs-polygon clipper. It resolves a shapely geometry into polygon pairs, optionally crops to bbox, delegates mask construction to xrspatial.rasterize (which has its own memory guards), and applies via xarray.where. No manual line-segment intersection, no recursive clip amplification, no float division on user vertices. Cat 1: list(geometry) materializes the user iterable but the dominant memory cost is the rasterize-built mask which is already bounded by guarded raster size. Cat 2: no integer math. Cat 3: NaN bounds from degenerate geometry are caught by the does-not-overlap ValueError (line 93 _crop_to_bbox); shapely raises GEOSException on malformed input. Cat 4 N/A: no CUDA kernels. Cat 5: dynamic geopandas/shapely.ops imports are import-name strings, not user paths. Cat 6: _validate_raster called with default numeric=True; integer raster + np.nan nodata silently coerces but is a UX nit, not a security issue. Vertex amplification attack surface lives in shapely, not here."
|
|
36
|
+
polygonize,2026-05-03,1441,MEDIUM,1;6,,"Re-audit 2026-05-03. MEDIUM Cat 6 fixed in PR #1442: polygonize() now calls _validate_raster on raster (numeric, ndim=2) and on mask (numeric=False). MEDIUM Cat 1 not actionable: _calculate_regions working set is inherent to the union-find algorithm with no caller-controlled amplifier; runtime guard at line 328 already catches uint32-max region count. Other categories clean."
|
|
37
|
+
proximity,2026-04-22,,,,,"Clean. Public APIs (proximity/allocation/direction) all call _validate_raster. GPU kernel _proximity_cuda_kernel has bounds guard at lines 359-360. Dask KDTree path has explicit memory guards (lines 897-903 result array, 1297-1312 unbounded distance fallback, 681-682 cache budget). Index math uses np.int64 for pan_near_x/pan_near_y, target_counts, y_offsets/x_offsets -- no int32 overflow risk. Target detection filters NaN via np.isfinite (lines 533, 657). _calc_direction guards x1==x2 & y1==y2 before arctan2. No file I/O. LOW (not flagged): line 1235 pad_y/pad_x omit abs() while line 437 uses it -- minor inconsistency, not exploitable."
|
|
38
|
+
rasterize,2026-04-21,1223,HIGH,1;2,,HIGH: unbounded out/written allocation in _run_numpy/_run_cupy driven by user-supplied width/height/resolution (no cap). MEDIUM (unfixed): _build_row_csr_numba total=row_ptr[height] is int32 and can wrap for very tall rasters with many long edges.
|
|
39
|
+
reproject,2026-05-17,2026,MEDIUM,4;6,,Re-audit 2026-05-17. One MEDIUM: geoid_height and itrf_transform did not validate lon/lat shape parity; numba @njit(parallel=True) kernel reads OOB and silently returns wrong values. Fix in PR deep-sweep-security-reproject-2026-05-17-01: shape check before ravel in _vertical.geoid_height and _itrf.itrf_transform; h broadcastability check in itrf_transform. Cat 4 OOB read + Cat 6 missing input validation. LOW (documented only): geoid_height_raster does not validate raster coords are finite; +/-inf coords would infinite-loop the longitude wrap in _interp_geoid_point. urlretrieve in _datum_grids and _vertical uses hardcoded filenames from GRID_REGISTRY / _GEOID_MODELS so no path injection. No HIGH/CRITICAL.
|
|
40
|
+
resample,2026-04-28,1295,HIGH,1,,"HIGH (fixed #1295): resample() did not bound output dimensions derived from user-supplied scale_factor / target_resolution. _output_shape returns max(1, round(in_h * scale_y)), max(1, round(in_w * scale_x)) and was passed straight through to the eager numpy / cupy backends, where _run_numpy and _run_cupy / the _AGG_FUNCS numba kernels and _nan_aware_interp_np allocated np.empty / cupy.empty / map_coordinates buffers of that size with no memory check. scale_factor=1e9 on a 4x4 raster requested ~190 EB; target_resolution=1e-9 on a meter-scale raster did the same. Fixed by adding _available_memory_bytes() / _available_gpu_memory_bytes() helpers and _check_resample_memory(out_h, out_w) / _check_resample_gpu_memory(out_h, out_w) guards (12 B/cell budget covering float64 working buffer + float32 output + map_coordinates temporary), wired into resample() before backend dispatch. Eager numpy and cupy paths run the guard; dask paths skip it because per-chunk allocations are bounded by chunk size. Mirrors the kde / line_density (#1287), focal (#1284), geodesic (#1283), cost_distance (#1262), and diffuse (#1267) patterns. No other findings: _validate_raster called at line 698, scale_y > 0 / scale_x > 0 enforced, AGGREGATE_METHODS rejects scale > 1.0, identity fast path bypasses dispatch entirely, all numba kernels guard count > 0 before division, no CUDA kernels (cupy paths use cupy ufuncs + cupyx.scipy.ndimage), no file I/O, all backends cast to float64 before computation and float32 on output."
|
|
41
|
+
sieve,2026-04-28,1296,HIGH,1,,"HIGH (fixed #1296): sieve() on numpy and cupy backends had no memory guard. _label_connected allocates parent (int32, 4B/px), rank (int32, 4B/px, reused as root_to_id), region_map_flat (int32, 4B/px), plus a float64 result copy (8B/px) ~ 20 B/pixel of working memory before any check. The dask paths (_sieve_dask line 343 and _sieve_dask_cupy line 366) already raised MemoryError via _available_memory_bytes() at 28 B/pixel budget, but the public sieve() API at line 489 dispatched np.ndarray inputs straight into _sieve_numpy with no guard, and _sieve_cupy at line 308 transferred to host via data.get() then called _sieve_numpy, inheriting the gap. A 50000x50000 numpy raster requested ~50 GB silently. Fixed by extracting _check_memory(rows, cols) and _check_gpu_memory(rows, cols) helpers (mirrors cost_distance #1262 / mahalanobis #1288 / multispectral #1291 / kde #1287 pattern) at 28 B/pixel host budget plus 16 B/pixel GPU round-trip budget at 50% of available memory threshold. _check_memory wired into _sieve_numpy at the top before the float64 copy. _check_gpu_memory wired into _sieve_cupy before data.get(); it also calls _check_memory so the host budget still applies. Consolidated _available_memory_bytes definition (was duplicated). All 47 tests pass including 2 new memory-guard tests for the numpy backend (_sieve_numpy direct call + public sieve() API). No other findings: Cat 2 int32 indexing in _label_connected docstring acknowledges <2.1B pixel limit; the new memory guard rejects rasters that large before the int32 issue can trigger so this is a documentation/clarity follow-up rather than an exploitable bug. Cat 3 NaN handled via valid mask; Cat 4 no CUDA kernels; Cat 5 only /proc/meminfo read; Cat 6 _validate_raster called at line 478."
|
|
42
|
+
sky_view_factor,2026-04-28,1299,HIGH,1,,"Unbounded numpy/cupy allocation; fixed via _check_memory and _check_gpu_memory guards (16 B/pixel, 50% threshold). Dask paths skip the guard."
|
|
43
|
+
slope,2026-04-28,,,,,"Clean. slope() validates input via _validate_raster (line 383) and _validate_boundary (line 389). Cat 1: planar _cpu/_run_cupy allocate output matching input shape; geodesic paths build (3,H,W) float64 stacked array but are gated by _check_geodesic_memory(rows, cols) at line 410 (already fixed under geodesic audit, PR #1285). Cat 2: no int32 flat-index math; all loops 2D with range(). Cat 3: NaN propagates through arctan in planar kernels; geodesic delegates to _local_frame_project_and_fit which has explicit NaN guards and degenerate det check. Cat 4: _run_gpu (line 146) uses combined bounds+stencil guard 'i-di>=0 and i+di<H and j-dj>=0 and j+dj<W'; geodesic GPU kernels imported from geodesic.py and audited there; _geodesic_cuda_dims uses 16x16 blocks to avoid register spill. Cat 5: no file I/O. Cat 6: all backends cast explicitly to float32 (planar) or float64 (geodesic); lat/lon cast to float64 in _extract_latlon_coords."
|
|
44
|
+
surface_distance,2026-04-28,1303,HIGH,1,,Fixed in PR #1305: added _check_memory and _check_gpu_memory guards to _surface_distance_numpy (line ~233) and _surface_distance_cupy (line ~448) before O(H*W) heap+output allocations. Dask paths inherit via per-chunk numpy call. Other categories clean.
|
|
45
|
+
terrain,2026-05-03,1443,MEDIUM,1;3,,"Re-audit 2026-05-03. MEDIUM Cat 1 + Cat 3 fixed in PR #1444: _terrain_numpy and _terrain_cupy now call _check_memory / _check_gpu_memory (24 B/pixel scratch budget, 50% threshold); generate_terrain rejects non-finite or non-positive lacunarity / persistence. Dask path worley_norm_range pre-pass dask.persist remains documented but not exploitable (caller-controlled). No remaining findings."
|
|
46
|
+
viewshed,2026-04-22,1229,HIGH,1,,"HIGH (fixed #1229): _viewshed_cpu allocated ~500 bytes/pixel of working memory (event_list 3*H*W*7*8 bytes + status_values/status_struct/idle + visibility_grid + lexsort temporary) with no guard. A 20000x20000 raster tried to allocate ~200 GB. Fixed by adding peak-memory guard mirroring the _viewshed_dask pattern (_available_memory_bytes() check, raises MemoryError with max_distance= hint). No other HIGH findings: dask path already guarded, _validate_raster is called, distance-sweep uses dtype=float64, _calc_dist_n_grad guards zero distance."
|
|
47
|
+
visibility,2026-04-28,,,,,"Clean. line_of_sight (line 190) and cumulative_viewshed (line 259) call _validate_raster; visibility_frequency delegates. Cat 1: cumulative_viewshed allocates int32 accumulator (4 B/px) but delegates per-observer to viewshed() which has 500 B/px memory guard at viewshed.py:1523-1531; viewshed will fail first on oversize rasters. _bresenham_line (line 35) and _los_kernel (lines 112-143) bounded by transect length (<=W+H+1). Cat 2: int64 throughout, no int32 overflow path. Cat 3: divisions in _los_kernel guarded (D==0 in _fresnel_radius_1 line 87, distance[i]==0 continue line 133, total_dist>0 check line 123); NaN elevation at observer cell would taint los_height but is a correctness not DoS concern. Cat 4: no CUDA kernels. Cat 5: no file I/O. Cat 6: elevations cast to float64 in _extract_transect line 79."
|
|
48
|
+
worley,2026-04-28,,,,,"Clean. worley() calls _validate_raster at line 234 (Cat 6 OK). Cat 1: output allocation matches input agg.shape (np.empty_like at line 80, cupy.empty at line 174); not a width/height generator like bump, so unbounded alloc N/A. Cat 2: cell_x/cell_y use & 255 mask before perm-table indexing, no overflow risk; tid/block_size math bounded by hardware limits. Cat 3: no division by data-derived values; out.shape guards prevent zero-div in coordinate computation; no NaN read from input (pure noise generator). Cat 4 (PRIMARY): both @cuda.jit kernels (_worley_gpu line 99, _worley_gpu_xy line 135) have correct bounds guard 'if i < out.shape[0] and j < out.shape[1]'. cuda.shared.array(512, nb.int32) uses HARDCODED constant 512 (matches 256*2 perm table size), NOT derived from caller input — safe. cuda.syncthreads() called at line 110/147 between strided shared-mem write and reads. Each thread writes distinct sp[k] indices via 'range(tid, 512, block_size)', no race. All threads (incl. out-of-bounds) participate in the load loop before the bounds check, so syncthreads divergence is avoided. Cat 5: no file I/O. Minor: freq/seed not range-validated, _worley_numpy uses np.empty_like(data) which preserves int dtype if input is int (truncation). Functional, not security."
|
|
49
|
+
zonal,2026-05-27,2523,HIGH,1;2;6,,"Re-audit 2026-05-27. HIGH Cat 1 (fixed #2523): _stats_numpy xarray.DataArray return path allocated np.full((n_stats, H*W), float64) with no memory guard; n_stats user-controlled via stats_funcs dict. Fixed by adding _check_stats_dataarray_memory helper that calls _available_memory_bytes() and raises MemoryError when n_stats*H*W*8 > 0.5*avail. Carry-over MEDIUMs still present (no new commits to zonal.py since 2026-04-22): _strides uses np.int32 stride indices (wraps at H*W > ~2.1B elements); hypsometric_integral() skips _validate_raster on zones/values (only validate_arrays for shape parity); _regions_numpy/_regions_cupy have no memory guard but allocations match input shape (bounded by caller). HIGH #1227 remains fixed. No CUDA bounds issues: _apply CUDA kernel has (y < zones.shape[0] and x < zones.shape[1]) guard. No file I/O beyond hardcoded /proc/meminfo read."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module,last_inspected,issue,severity_max,categories_found,notes
|
|
2
|
+
aspect,2026-05-29,2683,MEDIUM,1,E402+E305 line 38: from xrspatial.geodesic import block sat below _geodesic_cuda_dims; moved up with top-of-file imports. E501 lines 219/263: wrapped two _run_gpu_geodesic_aspect kernel-launch calls (101/109 chars). Cat 4 isort reviewed but NOT applied: slope.py/curvature.py use one-import-per-line for xrspatial.utils so raw isort would make aspect inconsistent. Cat 2/3/5 grep clean. PR #2740. 82 aspect+geodesic tests pass.
|
|
3
|
+
contour,2026-05-29,2698,HIGH,3,"F821 line 557: contours() return annotation ""gpd.GeoDataFrame"" referenced gpd not bound at module scope (only imported inside _to_geopandas). Fixed via TYPE_CHECKING-guarded import geopandas as gpd, matching polygonize.py. No runtime change; geopandas stays optional. isort clean. Cat 1/2/4/5 clean. 24 contour tests pass. PR open."
|
|
4
|
+
focal,2026-05-29,2731,HIGH,3;4;5,"F401 not_implemented_func (import line 36, unused, not re-exported). isort: stdlib reorder (import math before from-imports), dropped stray blank lines in import groups, alphabetised+rewrapped convolution/utils from-imports, moved dataset_support import into order. Cat 5: mutable default excludes=[np.nan] in mean() (line 238) -> None sentinel, resolved to [np.nan] in body; never mutated so behaviour preserved; regression test test_mean_default_excludes_does_not_leak added. Cat 1/2 clean. 115 focal tests pass. PR pending."
|
|
5
|
+
geotiff,2026-05-27,2481,HIGH,1;3;4,"Bundled 387 flake8 + ~30 isort fixes since #2285/#2430. F401 x9, F811 x6, F841 x3. E501 x250 (mostly wrapped, 3 file-scope imports keep noqa: E402+E501). E252 x62, blank-line cluster, E128/E127 indents. importorskip imports use # noqa: E402. Cat 5 grep clean."
|
|
6
|
+
hydro-d8,2026-05-29,2705,HIGH,1;3;4,"flake8+isort over the 13 D8 files only (dinf/mfd out of scope). Cat 3 HIGH: F401 x2 (flow_length_d8 function-local _compute_accum_seeds never called; snap_pour_point_d8 module-level cuda_args unused) - both confirmed dead, no re-export. Cat 1: E127/E128 continuation-indent x90 (mostly multi-line def signatures); E302/E303 blank-line cluster in watershed_d8; E501 x4 (flow_path_d8 + snap_pour_point_d8, wrapped ternaries). Cat 4: isort import-block reordering on all 13 files. No Cat 2 (W-codes), no Cat 5 (grep clean: no bare except, mutable defaults, ==None/==True, or shadowed builtins). flake8+isort clean after fix; 385 D8 tests pass. flow_direction_d8 needed manual blank-line placement to satisfy both isort and E302."
|
|
7
|
+
interpolate-kriging,2026-06-04,2916,MEDIUM,1;4,"flake8 E128 x2: continuation-line under-indent at the _chunk_var kriging-predict calls in _kriging_dask_numpy (L234) and _kriging_dask_cupy (L324); re-indented to visual-indent column. Cat 4 isort: 5-line from xrspatial.utils (...) block collapses to one 88-char line under line_length=100. Cat 2/3/5 grep clean (no W-codes, F-codes, bare except, mutable defaults, ==None/True, or shadowed builtins). flake8+isort clean after fix; 14 kriging tests pass. PR open."
|
|
8
|
+
polygonize,2026-05-27,2534,HIGH,1;3;4,"F401 line 58 (is_cupy_array unused, not re-exported). E127 lines 83/88 (overload continuation indent in generated_jit). isort: 5-line .utils import block collapses to one line at 100-char limit. Cat 2 clean. Cat 5 grep clean."
|
|
9
|
+
proximity,2026-05-29,2725,HIGH,1;3;4;5,"F841 line 1274 original_chunks dead local in unbounded dask+cupy branch (refactor leftover). Cat 5 mutable default target_values: list = [] in proximity/allocation/direction -> None sentinel, normalized to [] in body (never mutated, behaviour preserved). E128 line 291 np.where continuation under-indent in _vectorized_calc_direction. isort: re-sorted xrspatial import block + blank line after inline import cupy as cp. flake8+isort clean after fix; 69 proximity tests pass + new parametrized regression test. Pre-existing E127 (test_proximity.py 726/752) + test-file isort drift left untouched (out of module scope)."
|
|
10
|
+
rasterize,2026-05-27,2503,HIGH,1;3,F401 line 15 + F811 line 1193 (paired: local import warnings shadowed unused module-level import); E306 line 1775 (nested @cuda.jit). isort clean. Cat 5 grep clean. Fix in PR #2507.
|
|
11
|
+
resample,2026-05-27,2543,MEDIUM,4,isort drift only: 4 multi-line parenthesised imports collapsed to single/one-per-line under line_length=100 (top-of-file scipy.ndimage + xrspatial.utils; local cupyx imports in _nan_aware_interp_cupy and _interp_block_cupy); two blank-line nits after import math in _run_dask_numpy/_run_dask_cupy. flake8 clean. Cat 5 grep clean. 169 resample tests pass.
|
|
12
|
+
slope,2026-05-29,2685,HIGH,1;3;4,"F401 line 26 (VALID_BOUNDARY_MODES unused, not re-exported). E402+E305 line 48 (geodesic import block sat after _geodesic_cuda_dims; moved up to top-of-file imports). E501 line 260 (cupy kernel launch, 108 chars) wrapped. isort: consolidated/regrouped xrspatial imports (dataset_support, geodesic, utils). Cat 2 clean. Cat 5 grep clean. 41 slope + 21 geodesic_slope tests pass."
|
|
13
|
+
viewshed,2026-05-29,2690,HIGH,1;4;5,"flake8 E127 x2 (L2013-2014 _viewshed_distance_sweep sig); isort .utils import reflow; shadowed builtin id->node_id (L1409,1474). Fixed via /rockout PR. No behavioural change."
|
|
14
|
+
zonal,2026-05-27,2522,HIGH,1;3;4,"F401 not_implemented_func (line 42, only present on import line). E501 line 455 (dd.concat one-liner, 117 chars) wrapped across 3 lines. isort: consolidated xrspatial.utils block (merged has_dask_array, dropped not_implemented_func, alphabetised, trimmed extra blank line). Cat 5 grep clean. 125 zonal tests pass."
|