xarray-spatial 0.10.7__tar.gz → 0.10.9__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.7 → xarray_spatial-0.10.9}/.claude/sweep-accuracy-state.csv +41 -41
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/sweep-metadata-state.csv +16 -15
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/sweep-performance-state.csv +51 -51
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/sweep-style-state.csv +2 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/deep-sweep.md +5 -5
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/CHANGELOG.md +34 -0
- {xarray_spatial-0.10.7/xarray_spatial.egg-info → xarray_spatial-0.10.9}/PKG-INFO +1 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9/xarray_spatial.egg-info}/PKG-INFO +1 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xarray_spatial.egg-info/SOURCES.txt +4 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/_version.py +3 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/classify.py +7 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/contour.py +30 -40
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/cost_distance.py +32 -15
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/dasymetric.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/diffusion.py +5 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/emerging_hotspots.py +5 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/fire.py +25 -10
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/flood.py +4 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/focal.py +72 -12
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/__init__.py +5 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_attrs.py +15 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_backends/dask.py +10 -5
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_backends/gpu.py +11 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_errors.py +25 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_geotags.py +40 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_validation.py +27 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_writers/eager.py +15 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_writers/gpu.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/attrs/test_contract.py +43 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/gpu/test_writer.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_mask_and_scale_dtype_parity_3066.py +4 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_nodata.py +110 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py +9 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_scale_zero_3104.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/release_gates/test_features.py +6 -1
- xarray_spatial-0.10.9/xrspatial/geotiff/tests/unit/test_degenerate_pixel_size_3331.py +186 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_safe_xml.py +2 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_signatures.py +40 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_metadata.py +9 -7
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_basic.py +48 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_pack_3064.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_pack_band_subset_3161.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_pack_float_width_3080.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_pack_lazy_nan_guard_3235.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_pack_nodata_kwarg_3168.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_pack_range_guard_3260.py +6 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/glcm.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/basin_d8.py +6 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/fill_d8.py +3 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_accumulation_d8.py +4 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_accumulation_dinf.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_accumulation_mfd.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_direction_d8.py +6 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_direction_dinf.py +5 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_length_d8.py +3 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_path_d8.py +10 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_path_dinf.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_path_mfd.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/hand_d8.py +9 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/hand_dinf.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/hand_mfd.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/sink_d8.py +4 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/snap_pour_point_d8.py +6 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/stream_link_d8.py +6 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/stream_link_dinf.py +5 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/stream_link_mfd.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/stream_order_d8.py +8 -5
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/stream_order_dinf.py +7 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/stream_order_mfd.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/watershed_d8.py +4 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/watershed_dinf.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/watershed_mfd.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/interpolate/_idw.py +5 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/interpolate/_kriging.py +7 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/interpolate/_spline.py +5 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/mahalanobis.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/mcda/combine.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/mcda/sensitivity.py +2 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/mcda/standardize.py +6 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/morphology.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/multispectral.py +46 -24
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/perlin.py +6 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/polygon_clip.py +3 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/proximity.py +13 -2
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/rasterize.py +326 -52
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/__init__.py +40 -14
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_itrf.py +15 -8
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_projections.py +4 -1
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_vertical.py +11 -3
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/resample.py +19 -5
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/sky_view_factor.py +3 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/terrain_metrics.py +14 -8
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_contour.py +132 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_cost_distance.py +259 -0
- xarray_spatial-0.10.9/xrspatial/tests/test_dask_task_names.py +357 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_focal.py +37 -9
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_coverage_2026_06_09.py +52 -53
- xarray_spatial-0.10.9/xrspatial/tests/test_rasterize_lines_all_touched_3102.py +251 -0
- xarray_spatial-0.10.9/xrspatial/tests/test_reproject_cupy_promotion_3281.py +144 -0
- xarray_spatial-0.10.9/xrspatial/tests/test_reproject_parallel_kernels_3141.py +125 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject_streaming_3101.py +22 -11
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/worley.py +6 -4
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/zonal.py +8 -3
- xarray_spatial-0.10.7/xrspatial/tests/test_dask_task_names.py +0 -154
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/backend-parity.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/bench.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/dask-notebook.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/deep-sweep.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/efficiency-audit.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/new-issues.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/ready-to-merge.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/release-major.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/release-minor.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/release-patch.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/review-contributor-pr.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/review-pr.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/rockout.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/sweep-accuracy.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/sweep-api-consistency.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/sweep-metadata.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/sweep-performance.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/sweep-security.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/sweep-style.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/sweep-test-coverage.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/user-guide-notebook.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/commands/validate.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/sweep-api-consistency-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/sweep-security-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.claude/sweep-test-coverage-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/backend-parity.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/bench.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/dask-notebook.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/deep-sweep.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/efficiency-audit.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/new-issues.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/ready-to-merge.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/release-major.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/release-minor.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/release-patch.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/review-contributor-pr.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/review-pr.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/rockout.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/sweep-accuracy.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/sweep-api-consistency.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/sweep-metadata.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/sweep-performance.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/sweep-security.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/sweep-style.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/sweep-test-coverage.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/user-guide-notebook.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/commands/validate.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/sweep-accuracy-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/sweep-api-consistency-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/sweep-metadata-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/sweep-performance-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/sweep-security-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/sweep-style-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.codex/sweep-test-coverage-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/backend-parity.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/bench.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/dask-notebook.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/deep-sweep.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/efficiency-audit.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/new-issues.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/ready-to-merge.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/release-major.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/release-minor.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/release-patch.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/review-contributor-pr.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/review-pr.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/rockout.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/sweep-accuracy.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/sweep-api-consistency.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/sweep-metadata.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/sweep-performance.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/sweep-security.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/sweep-style.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/sweep-test-coverage.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/user-guide-notebook.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursor/rules/validate.mdc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.cursorrules +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.efficiency-audit-baseline.json +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.efficiency-audit-baseline.prev.json +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.gitattributes +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/ISSUE_TEMPLATE/feature-proposal.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/ISSUE_TEMPLATE/new-contributor.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/labeler.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/pull_request_template.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/benchmarks.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/copilot-review.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/docs.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/labeler.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/pypi-publish.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/test-cog-validator.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/test-geotiff-corpus.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/test.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.github/workflows/welcome-contributor.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.gitignore +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/backend-parity.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/bench.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/dask-notebook.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/efficiency-audit.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/new-issues.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/ready-to-merge.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/release-major.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/release-minor.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/release-patch.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/review-contributor-pr.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/review-pr.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/rockout.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/sweep-accuracy.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/sweep-api-consistency.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/sweep-metadata.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/sweep-performance.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/sweep-security.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/sweep-style.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/sweep-test-coverage.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/user-guide-notebook.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/command/validate.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/sweep-accuracy-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/sweep-api-consistency-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/sweep-metadata-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/sweep-performance-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/sweep-security-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/sweep-style-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.kilo/sweep-test-coverage-state.csv +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/.readthedocs.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/AI_POLICY.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/CLAUDE.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/CODE_OF_CONDUCT.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/CONTRIBUTING.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/Citation-styles.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/LICENSE.txt +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/MANIFEST.in +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/README.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/RELEASE.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/codecov.yml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/pyproject.toml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/setup.cfg +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/setup.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xarray_spatial.egg-info/dependency_links.txt +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xarray_spatial.egg-info/entry_points.txt +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xarray_spatial.egg-info/not-zip-safe +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xarray_spatial.egg-info/requires.txt +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xarray_spatial.egg-info/top_level.txt +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/__main__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/accessor.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/analytics.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/aspect.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/balanced_allocation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/bilateral.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/bump.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/convolution.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/corridor.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/curvature.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/dataset_support.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/datasets/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/datasets/sentinel-2/blue_band.nc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/datasets/sentinel-2/green_band.nc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/datasets/sentinel-2/nir_band.nc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/datasets/sentinel-2/red_band.nc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/datasets/sentinel-2/swir1_band.nc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/datasets/sentinel-2/swir2_band.nc +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/diagnostics.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/edge_detection.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/erosion.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/experimental/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/experimental/min_observable_height.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geodesic.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_backends/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_backends/_gpu_helpers.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_backends/vrt.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_cog_http.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_compression.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_coords.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_crs.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_decode.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_dtypes.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_encode.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_gpu_decode.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_header.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_layout.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_nodata.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_overview.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_overview_kernels.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_reader.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_runtime.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_safe_xml.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_sidecar.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_sources.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_vrt.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_vrt_validation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_write_layout.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_writer.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_writers/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/_writers/vrt.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/_geotiff_fixtures.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/_helpers/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/_helpers/markers.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/_helpers/tiff_builders.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/_helpers/tiff_surgery.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/attrs/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/bench_vs_rioxarray.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/conftest.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/README.md +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/_marks.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/_oracle.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/cog_internal_overview_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_deflate_predictor2_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_deflate_predictor3_float32.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_jpeg_uint8_ycbcr.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_lerc_float32.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_lzw_predictor2_int16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/compression_none_uint8.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/crs_citation_only.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/crs_epsg_3857.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/crs_wkt_utm10n.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float32.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float64.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int32.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int8.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint32.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint8.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/extra_tags_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/gdal_metadata_namespaced_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/nodata_int_sentinel_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/nodata_miniswhite_uint8.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/nodata_nan_float32.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/overview_external_ovr_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/overview_external_ovr_uint16.tif.ovr +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/overview_internal_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/planar_separate_uint8_rgb.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/sparse_tiled_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/stripped_be_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/stripped_le_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/tiled_be_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/fixtures/tiled_le_uint16.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/generate.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/manifest.yaml +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_compression.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_corpus_determinism.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_dask_gpu.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_dask_numpy.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_dtype_variants.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_eager_numpy.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_fsspec.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_gpu.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_http.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_layout_endian.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_manifest.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_metadata_tags.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_nodata_sentinels.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_oracle.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_overview_cog.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/golden_corpus/test_vrt.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/gpu/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/gpu/test_codec.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/gpu/test_reader.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/integration/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/integration/test_dask_pipeline.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/integration/test_gpu_pipeline.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/integration/test_http_sources.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/integration/test_sidecar.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/parity/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/parity/test_api_consolidation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/parity/test_backend_matrix.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/parity/test_finalization.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/parity/test_pixel_equality.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/parity/test_reference.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/parity/test_signature_contract.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_basic.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_bbox_2555.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_bbox_vrt_2668.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_compression.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_coords.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_crs.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_degenerate_shapes.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_dtypes.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_endianness.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_georef.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_overview.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_streaming.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_tiling.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/read/test_unpack_noop_doc_3263.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/release_gates/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/release_gates/test_stable_features.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_edge_cases.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_fuzz_hypothesis.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_polish.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_round_trip.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_security.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_shutdown_cleanup_2486.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_stable_only_bbox_ordering_2869.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/test_stable_only_remote_2821.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_codec_roundtrip.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_compression.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_exception_exports_3265.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_geotags.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_header.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_ifd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_input_validation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_metadata.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_photometric.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/unit/test_predictor.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_dtype_conversion.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_missing_sources.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_non_georef_placement_3116.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_parity.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_source_opt_ins_2672.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_validation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/vrt/test_window.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_bigtiff.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_cog.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_crs.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_nodata.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_overview.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_pack_64bit_sentinel_3264.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_streaming.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/geotiff/tests/write/test_vrt_atomic.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/gpu_rtx/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/gpu_rtx/_memory.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/gpu_rtx/cuda_utils.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/gpu_rtx/hillshade.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/gpu_rtx/mesh_utils.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/gpu_rtx/viewshed.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hillshade.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/_boundary_store.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_direction_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_length_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/flow_length_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/conftest.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_basin_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_fill_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_accumulation_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_accumulation_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_accumulation_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_direction_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_direction_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_direction_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_length_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_length_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_length_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_path_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_path_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_flow_path_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_hand_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_hand_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_hand_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_sink_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_snap_pour_point_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_stream_link_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_stream_link_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_stream_link_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_stream_order_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_stream_order_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_stream_order_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_twi_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_validate_cellsize.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_validate_mfd_companion_shape.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_validate_mfd_fractions.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_validate_scalar_params.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_validate_secondary_args.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_watershed_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_watershed_dinf.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/tests/test_watershed_mfd.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/hydro/twi_d8.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/interpolate/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/interpolate/_validation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/kde.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/mcda/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/mcda/constrain.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/mcda/weights.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/normalize.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/pathfinding.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/polygonize.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/preview.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_crs_utils.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_grid.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_interpolate.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_lite_crs.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_merge.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_projections_cuda.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/_transform.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/reproject/grids/us_nga_egm96_15.tif +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/sieve.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/slope.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/surface_distance.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/terrain.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/__init__.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/bench_reproject_vs_rioxarray.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/conftest.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/general_checks.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_accessor.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_analytics.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_aspect.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_balanced_allocation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_bilateral.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_bump.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_classify.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_convolution.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_corridor.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_curvature.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_dask_cupy_gaps.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_dask_laziness.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_dasymetric.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_dataset_support.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_datasets.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_diagnostics.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_diffusion.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_edge_detection.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_emerging_hotspots.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_erosion.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_fire.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_flood.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_fused_overlap.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_geodesic_aspect.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_geodesic_slope.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_glcm.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_glcm_metric_order.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_gpu_rtx_has_rtx.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_gpu_rtx_memory.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_gpu_rtx_mesh.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_hillshade.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_hypsometric_integral.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_interpolation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_kde.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_lite_crs.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_mahalanobis.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_mcda.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_min_observable_height.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_morphology.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_morphology_derived.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_multi_overlap.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_multispectral.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_normalize.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_northness_eastness.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_open_geotiff_coregister.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_open_geotiff_resampling.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_optional_shapely.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_pathfinding.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_perlin.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygon_clip.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_atol_rtol_backend_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_coverage_2026_05_19.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_dask_row_batch_2608.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_issue_2172.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_issue_2583.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_issue_2606.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_issue_2666.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_issue_2677.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_issue_3292.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_issue_3303.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_mask_chunk_mismatch_3299.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_polygonize_mask_dtype_coverage_2026_05_29.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_preview.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_proximity.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_accuracy.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_all_touched_supercover_2169.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_alloc_3107.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_coverage_2026_05_17.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_coverage_2026_05_21.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_coverage_2026_05_29.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_crs_mismatch_3058.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_descending_x_2568.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_dtype_annot_3291.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_fill_dtype_3054.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_geom_crs_3087.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_gpu_alias_3089.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_gpu_callable_warn_3057.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_gpu_race_2167.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_int_precision_3056.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_linearring_3055.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_merge_dedup_3304.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_mixed_type_ordered_merge_3296.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_nan_int_fill_2504.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_nan_propagation_2255.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_nonfinite_burn_3085.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_nonfinite_burn_3088.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_nonfinite_coords_3295.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_partial_dims_2569.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_props_hoist_2506.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_resolution_exact_2573.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_resolution_validation_2576.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_signature_annot_2250.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_signed_step_2566.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rasterize_tile_props_slice_2020.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_rechunk_no_shuffle.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject_cupy_gate_2564.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject_inverse_kernels_3274.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject_itrf_scale_3276.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject_pyproj_warning_3242.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_reproject_sphere_ellipsoid_guard_3275.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_resample.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_resample_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_resample_cupy_agg_fallback_2615.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_resample_input_validation_2574.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_resample_irregular_coords_2663.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_resample_nodata_dask_parity_3073.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_resample_signature_annot_2544.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_sieve.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_sieve_gdal_parity.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_sky_view_factor.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_slope.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_surface_distance.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_sweep_state_csv_merge_2754.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_terrain.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_terrain_metrics.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_utils.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_validation.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_viewshed.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_visibility.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_zonal.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/tests/test_zonal_backend_coverage_2026_05_27.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/utils.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/viewshed.py +0 -0
- {xarray_spatial-0.10.7 → xarray_spatial-0.10.9}/xrspatial/visibility.py +0 -0
|
@@ -1,41 +1,41 @@
|
|
|
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-10,3214,MEDIUM,1;5,"mean() dtype divergence: numpy/dask+numpy cast to float64 (astype(float)) while cupy/dask+cupy forced float32, so output dtype was backend-dependent and float64 rasters lost precision on GPU (offset 1e7: GPU error 0.58 > true spread 0.42, same class as fixed #2831). mean() was left out of the #2769 _promote_float contract that apply/focal_stats follow. Fix #3214: _promote_float in mean(), drop hardcoded cupy.float32 in _mean_cupy/_mean_dask_cupy, excludes cast to working dtype for cross-backend match parity. CUDA available; all 4 backends executed (245 focal tests pass incl new 3214 dtype tests). Cats 2-4 clean: GPU kernels two-pass std/var (#2831 fix verified), NaN checks via v!=v, map_overlap depths == kernel radius, Gi* validated against reference test. LOW (documented, not fixed): mean() excludes mask only the center pixel; excluded sentinel values (e.g. -9999) still contribute to neighboring cells' means on all backends -- docstring says 'left unchanged rather than averaged', backend-consistent."
|
|
16
|
-
geotiff,2026-06-12,3260,HIGH,1;2,"Pass 27 (2026-06-12, deep-sweep): HIGH fixed -- issue #3260. to_geotiff(pack=True) cast the packed values to the integer dtype from attrs['mask_and_scale_dtype'] with no range or finiteness check: finite values outside the dtype range wrapped in the astype (4000.0 with SCALE=0.1 on int16 packs to 40000 and landed on disk as -25536, unpacking to -2553.6) and +/-Inf cast to a platform-defined integer (0 on linux/x86), all silently on all four backends; dask deferred the cast into the write's compute so there was no warning at all. Internally inconsistent: the adjacent NaN-no-sentinel guard exists precisely because 'the astype below would silently wrap'. Fix adds _pack_guard_int_range after the round, before the cast (eager raises at call time, dask via map_blocks from the write's single compute, mirroring #3235); exclusive upper bound iinfo.max+1 stays exact in float64 so int64/uint64 reject exactly-2**63 instead of wrapping. Also fixed en route: eager cupy no-sentinel integer pack crashed with TypeError in bool(out.isnull().any()) (implicit cupy->numpy); now routed through the cupy-safe _pack_guard_no_nan. 15 new tests in tests/write/test_pack_range_guard_3260.py covering all four backends (CUDA available, gpu legs executed), boundary iinfo.min/max round trips, round-back-into-range, uint underflow, and the 2**63 float64 bound. Scope this pass: post-2026-06-09 commits only (pack/unpack #3174/#3175/#3239/#3240/#3241, VRT offsets #3135, GPU streaming writer) since Pass 26 covered the rest 3 days earlier; overview kernels, _coords transform math, _decode predictor/orientation/LERC fill, and _nodata lifecycle re-read with no new findings; GPU streaming writer reviewed (per-band NaN rewrite and tile-row alignment mirror the full-array path). cuda-available. | Pass 26 (2026-06-09, deep-sweep): MEDIUM fixed -- issue #3098. _apply_eager_nodata_mask in _attrs.py compared the integer nodata sentinel AFTER promoting the buffer to float64, so int64/uint64 sentinels above 2**53 (INT64_MAX/UINT64_MAX) swallowed up to 512/1024 nearby valid values into NaN on the numpy-eager and cupy-eager backends, while the dask per-chunk mask (_delayed_read_window), the GPU GDS chunk path (_apply_nodata_mask_gpu), and the VRT path all compare at native integer width and masked only exact hits. Reproduced end-to-end: int64 file with nodata=INT64_MAX and values INT64_MAX-1..-513 gave 4 NaN eager vs 1 NaN dask. Same function was internally inconsistent: the mask_nodata=False pixels_present scan already compared at native width. Fix computes the mask at source dtype width before the float64 promotion (promotion itself stays unconditional per #2990); one site fixes both numpy and cupy eager since GPU routes through the helper via duck typing. 5 regression tests in tests/read/test_nodata.py (int64 exact-hit, eager-vs-dask parity, uint64, near-sentinel-no-hit pixels_present, gpu eager); verified on all 4 backends with CUDA. Also audited this pass with no findings: overview reduce kernels CPU vs GPU (empirical parity run incl. float32 median midpoint analysis: RN(a+b)/2 == RN((a+b)/2) so no divergence), unpack/pack scale-offset paths (#3075/#3065, mask-before-scale ordering consistent eager/dask, dask+gpu reuses CPU dask graph), bbox-to-window floor/ceil (GDAL touched semantics), VRT nearest mapping floor((out+0.5)*src/out) and Int64 nodata native-width round-trip, predictor 2/3 GPU kernels (lossless), writer NaN-to-sentinel gates. cuda-available; GPU paths executed, not just reviewed. | 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
|
-
mcda,2026-06-10,3146,MEDIUM,5,"Cat5 backend failures, all raise loudly (no wrong numbers): owa raised on every dask backend (da.sort does not exist in dask.array; fixed via rechunk+map_blocks np.sort with explicit meta) and on cupy (numpy order-weight array * cupy stack); standardize piecewise raised on cupy (cupy.interp needs cupy bp/vl + C-contiguous input) and dask+cupy (np.asarray on cupy chunk), categorical raised on dask+cupy (same asarray); monte-carlo sensitivity raised on cupy/dask+cupy (.values implicit conversion; now Welford accumulates with matching array module). All fixed + GPU tests added (issue #3146). Cats 1-4 clean: Welford already used, AHP Perron eigenvector + Saaty RI table correct, NaN propagation verified across combine ops, no neighborhood/geodesic code. constrain on cupy raises cupy.astype AttributeError = known cupy 13.6 + xarray xr.where incompat (dependency pin, not mcda). CUDA available; cupy + dask+cupy executed for all probes and tests."
|
|
24
|
-
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).
|
|
25
|
-
multispectral,2026-03-30T14:00:00Z,1094,,,
|
|
26
|
-
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.
|
|
27
|
-
perlin,2026-04-10T12:00:00Z,,,,Improved Perlin noise implementation correct. Fade/gradient functions verified. Backend-consistent. Continuous at cell boundaries.
|
|
28
|
-
polygon_clip,2026-06-10,3186,HIGH,5,"Cat5 backend inconsistency: dask+cupy clip_polygon rasterizes the mask with a uniform chunk size from the raster's first chunk, then feeds raster+mask to da.map_blocks (positional block pairing). Non-uniform raster chunks gave the mask a different block layout -> IndexError/ValueError (or silent mis-stamp). Repro (8,6) rechunk ((3,5),(6,)) on dask+cupy raised ValueError Shapes do not align; dask+numpy was fine via xarray.where rechunk. Fix #3186/PR: rechunk cond to raster.data.chunks[-2:] before map_blocks; added non-uniform regression tests for dask+numpy and dask+cupy. use_cuda->gpu migration in that branch was already landed by #3089/#3122. CUDA available; cupy+dask+cupy verified, 25 tests pass. Cats 1-4 clean: numpy path uses raster.where, cupy path operates on raw arrays, NaN inputs preserved, no neighborhood ops/curvature. Prior fix #1197/#1200 (crop+all_touched) merged and unrelated."
|
|
29
|
-
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."
|
|
30
|
-
proximity,2026-06-09,3108,HIGH,4;5,"Cat5/Cat4: bounded GREAT_CIRCLE dask (numpy+cupy) missed targets across the +/-180 antimeridian seam: _halo_depth sized x-halo as linear parallel-arc sum, but haversine is periodic in lon and chords shorten near poles, so array-space adjacency is no lower bound on spherical distance; numpy/cupy (brute force) found the wrap target (~111 km), dask returned NaN. Fixed in #3108 via chord bound 2R asin(cos(lat_max)|sin(dlon/2)|) + x-axis fold when seam/180-deg chord within max_distance (covers over-pole too). CUDA host: cupy + dask+cupy executed, 417+ tests pass. Cat1-3 clean (float32 output documented; NaN via isfinite consistent; bounds guards correct; tie-break unified in #2881). LOW (not fixed): great_circle_distance uses WGS84 equatorial radius 6378137 as sphere radius (~0.1% vs mean-radius convention) but documented and exposed as param."
|
|
31
|
-
rasterize,2026-06-12,3304,HIGH,3,"Cat3 HIGH: merge='sum'/'count' burned the same geometry into a pixel 2-3x on all 4 backends (cross-backend parity probes never caught it because all backends shared the bug). Two sources: all_touched=True polygons overlap the scanline center-fill with the supercover boundary pass and re-burn shared ring-vertex cells (count up to 3 for ONE polygon; rasterio MergeAlg.add gives 1); lines re-burn the connecting vertex of consecutive Bresenham segments (count 2 per interior vertex; rasterio gives 1 even for self-crossing lines). Fix #3304/PR #3313: for sum/count only, enumerate line + boundary cells host-side, dedup per (geometry,row,col), drop boundary cells the geometry's own scanline covers via a crossing-parity replay of the fill's exact float arithmetic on the same edge table, burn survivors through the point kernels (numpy/cupy/dask+numpy/dask+cupy). Coverage unchanged (==merge='last'); points intentionally not deduped (GDAL adds once per point, dup MultiPoint=2 matches rasterio). CUDA available; cupy + dask+cupy executed. 790 rasterize tests + 48 new pass. Cats 1/2/4/5 clean this pass: GPU atomic min/max NaN masks (#2255) correct, GPU ceil emulation matches np.ceil for negatives, no curvature surface, backends bit-identical on mixed-geometry probes. Pre-existing known-OK: Bresenham vs GDAL line coverage tie-breaks (pinned in coverage_2026_06_09 tests); dead all_touched branch in _extract_edges_vectorized (callers never pass all_touched) noted, not removed."
|
|
32
|
-
reproject,2026-06-12,3274,HIGH,1;4,"3 confirmed bugs, all kernel-vs-PROJ parity: #3274 HIGH LAEA inverse spurious /rq (2.6 km err for 3035) + _authalic_apa inverse series wrong (4.8 m in AEA/CEA inverses; PROJ 3-term = 1.6 mm), CPU+CUDA kernels both; #3275 HIGH _is_wgs84_compatible_ellipsoid passes R-defined spheres (MODIS sinusoidal 18.9 km err) and _aea_params/_cea_params lack the guard entirely (23.8 km on spherical aea/cea); #3276 MEDIUM itrf helmert scale 1e-9 but PROJ +s is ppm (1e-6), ~23 mm err. Verified clean: merc/emerc/UTM/tmerc/LCC/polar stere (incl lat_ts akm1) forward+inverse <=1e-5 m vs pyproj; resampling kernels NaN handling and GDAL renorm match across numpy/cupy/dask (CUDA run, gpu-vs-cpu 1.3e-7); dask footprint chunk-skip bbox is a superset in all probed cases (no holes). LOW (documented only): _source_footprint_in_target probe array typo uses x-midpoint mx as a latitude in last 3 ys entries (bbox superset, correctness unaffected)."
|
|
33
|
-
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."
|
|
34
|
-
sieve,2026-04-13T12:00:00Z,,,,Union-find CCL correct. NaN excluded from labeling. All backends funnel through _sieve_numpy.
|
|
35
|
-
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
|
|
36
|
-
terrain,2026-04-10T12:00:00Z,,,,Perlin/Worley/ridged noise correct. Dask chunk boundaries produce bit-identical results. No precision issues.
|
|
37
|
-
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."
|
|
38
|
-
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.
|
|
39
|
-
visibility,2026-04-13T12:00:00Z,,,,"Bresenham line, LOS kernel, Fresnel zone all correct. All backends converge to numpy."
|
|
40
|
-
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."
|
|
41
|
-
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."
|
|
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-10,3214,MEDIUM,1;5,"mean() dtype divergence: numpy/dask+numpy cast to float64 (astype(float)) while cupy/dask+cupy forced float32, so output dtype was backend-dependent and float64 rasters lost precision on GPU (offset 1e7: GPU error 0.58 > true spread 0.42, same class as fixed #2831). mean() was left out of the #2769 _promote_float contract that apply/focal_stats follow. Fix #3214: _promote_float in mean(), drop hardcoded cupy.float32 in _mean_cupy/_mean_dask_cupy, excludes cast to working dtype for cross-backend match parity. CUDA available; all 4 backends executed (245 focal tests pass incl new 3214 dtype tests). Cats 2-4 clean: GPU kernels two-pass std/var (#2831 fix verified), NaN checks via v!=v, map_overlap depths == kernel radius, Gi* validated against reference test. LOW (documented, not fixed): mean() excludes mask only the center pixel; excluded sentinel values (e.g. -9999) still contribute to neighboring cells' means on all backends -- docstring says 'left unchanged rather than averaged', backend-consistent."
|
|
16
|
+
geotiff,2026-06-14,3331,MEDIUM,2,"Pass 28 (2026-06-14, deep-sweep accuracy): MEDIUM fixed -- issue #3331 / PR #3332. Direct GeoTIFF read accepted a zero or non-finite ModelPixelScale (33550) / ModelTransformation (34264) diagonal: _geotags._extract_transform read sx=scale[0]/sy=scale[1] (and m[0]/m[5]) with no finite-nonzero check, then coords_from_pixel_geometry built arange(N)*pixel_width+origin, so a zero scale collapsed the whole axis onto the origin (constant, non-georeferenced coords) and a NaN/Inf scale produced an all-NaN/all-Inf axis -- a degenerate raster returned with no error on all four backends. Asymmetric: the VRT read path already rejected zero res_x/res_y (VRTUnsupportedError, _vrt_validation.py:202, comment notes the eager read 'would currently surface this as an opaque coord error' -- but it actually surfaced nothing) and the writer rejected zero-step coords (NonUniformCoordsError, _validation.py:1361). Fix adds DegeneratePixelSizeError (GeoTIFFAmbiguousMetadataError subclass, exported + docs table) and a _check_finite_nonzero_pixel_size guard in _extract_transform covering the scale-only, tiepoint+scale, and ModelTransformation paths; the unit-scale tiepoint fallback (literal 1.0) and the allow_rotated no-georef path are untouched. Guard sits in the shared extract path so numpy/dask/gpu/dask+gpu all reject identically -- verified all four locally on a degenerate fixture (CUDA available). NaN ModelTransformation diagonal still rejected: the rotation-tol check short-circuits on NaN (x>NaN is False) and falls through to the new guard. 29 tests in tests/unit/test_degenerate_pixel_size_3331.py (zero/NaN/+-Inf x 3 paths, subclass contract, positive controls, end-to-end open_geotiff). Existing geotiff unit+read suites: 2271 passed, 5 skipped; flake8+isort clean. Scope this pass: numerical core re-audit (overview kernels, coords transform math, decode predictor/nodata, dtypes) via 3 parallel readers -- overview float32-vs-float64 mean accumulator 'divergence' DISMISSED (the ngjit kernel float path and the numpy nanmean float path are mutually exclusive: float mean/min/max/median route to the kernel and return early, _overview.py:340-353); coords single-neighbor pixel-size recovery LOW (standard, negligible, bypassed by transform attr); decode/predictor/nodata clean (#3098 int-sentinel-before-float64-promote pattern not repeated). cuda-available. | Pass 27 (2026-06-12, deep-sweep): HIGH fixed -- issue #3260. to_geotiff(pack=True) cast the packed values to the integer dtype from attrs['mask_and_scale_dtype'] with no range or finiteness check: finite values outside the dtype range wrapped in the astype (4000.0 with SCALE=0.1 on int16 packs to 40000 and landed on disk as -25536, unpacking to -2553.6) and +/-Inf cast to a platform-defined integer (0 on linux/x86), all silently on all four backends; dask deferred the cast into the write's compute so there was no warning at all. Internally inconsistent: the adjacent NaN-no-sentinel guard exists precisely because 'the astype below would silently wrap'. Fix adds _pack_guard_int_range after the round, before the cast (eager raises at call time, dask via map_blocks from the write's single compute, mirroring #3235); exclusive upper bound iinfo.max+1 stays exact in float64 so int64/uint64 reject exactly-2**63 instead of wrapping. Also fixed en route: eager cupy no-sentinel integer pack crashed with TypeError in bool(out.isnull().any()) (implicit cupy->numpy); now routed through the cupy-safe _pack_guard_no_nan. 15 new tests in tests/write/test_pack_range_guard_3260.py covering all four backends (CUDA available, gpu legs executed), boundary iinfo.min/max round trips, round-back-into-range, uint underflow, and the 2**63 float64 bound. Scope this pass: post-2026-06-09 commits only (pack/unpack #3174/#3175/#3239/#3240/#3241, VRT offsets #3135, GPU streaming writer) since Pass 26 covered the rest 3 days earlier; overview kernels, _coords transform math, _decode predictor/orientation/LERC fill, and _nodata lifecycle re-read with no new findings; GPU streaming writer reviewed (per-band NaN rewrite and tile-row alignment mirror the full-array path). cuda-available. | Pass 26 (2026-06-09, deep-sweep): MEDIUM fixed -- issue #3098. _apply_eager_nodata_mask in _attrs.py compared the integer nodata sentinel AFTER promoting the buffer to float64, so int64/uint64 sentinels above 2**53 (INT64_MAX/UINT64_MAX) swallowed up to 512/1024 nearby valid values into NaN on the numpy-eager and cupy-eager backends, while the dask per-chunk mask (_delayed_read_window), the GPU GDS chunk path (_apply_nodata_mask_gpu), and the VRT path all compare at native integer width and masked only exact hits. Reproduced end-to-end: int64 file with nodata=INT64_MAX and values INT64_MAX-1..-513 gave 4 NaN eager vs 1 NaN dask. Same function was internally inconsistent: the mask_nodata=False pixels_present scan already compared at native width. Fix computes the mask at source dtype width before the float64 promotion (promotion itself stays unconditional per #2990); one site fixes both numpy and cupy eager since GPU routes through the helper via duck typing. 5 regression tests in tests/read/test_nodata.py (int64 exact-hit, eager-vs-dask parity, uint64, near-sentinel-no-hit pixels_present, gpu eager); verified on all 4 backends with CUDA. Also audited this pass with no findings: overview reduce kernels CPU vs GPU (empirical parity run incl. float32 median midpoint analysis: RN(a+b)/2 == RN((a+b)/2) so no divergence), unpack/pack scale-offset paths (#3075/#3065, mask-before-scale ordering consistent eager/dask, dask+gpu reuses CPU dask graph), bbox-to-window floor/ceil (GDAL touched semantics), VRT nearest mapping floor((out+0.5)*src/out) and Int64 nodata native-width round-trip, predictor 2/3 GPU kernels (lossless), writer NaN-to-sentinel gates. cuda-available; GPU paths executed, not just reviewed. | 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
|
+
mcda,2026-06-10,3146,MEDIUM,5,"Cat5 backend failures, all raise loudly (no wrong numbers): owa raised on every dask backend (da.sort does not exist in dask.array; fixed via rechunk+map_blocks np.sort with explicit meta) and on cupy (numpy order-weight array * cupy stack); standardize piecewise raised on cupy (cupy.interp needs cupy bp/vl + C-contiguous input) and dask+cupy (np.asarray on cupy chunk), categorical raised on dask+cupy (same asarray); monte-carlo sensitivity raised on cupy/dask+cupy (.values implicit conversion; now Welford accumulates with matching array module). All fixed + GPU tests added (issue #3146). Cats 1-4 clean: Welford already used, AHP Perron eigenvector + Saaty RI table correct, NaN propagation verified across combine ops, no neighborhood/geodesic code. constrain on cupy raises cupy.astype AttributeError = known cupy 13.6 + xarray xr.where incompat (dependency pin, not mcda). CUDA available; cupy + dask+cupy executed for all probes and tests."
|
|
24
|
+
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).
|
|
25
|
+
multispectral,2026-03-30T14:00:00Z,1094,,,
|
|
26
|
+
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.
|
|
27
|
+
perlin,2026-04-10T12:00:00Z,,,,Improved Perlin noise implementation correct. Fade/gradient functions verified. Backend-consistent. Continuous at cell boundaries.
|
|
28
|
+
polygon_clip,2026-06-10,3186,HIGH,5,"Cat5 backend inconsistency: dask+cupy clip_polygon rasterizes the mask with a uniform chunk size from the raster's first chunk, then feeds raster+mask to da.map_blocks (positional block pairing). Non-uniform raster chunks gave the mask a different block layout -> IndexError/ValueError (or silent mis-stamp). Repro (8,6) rechunk ((3,5),(6,)) on dask+cupy raised ValueError Shapes do not align; dask+numpy was fine via xarray.where rechunk. Fix #3186/PR: rechunk cond to raster.data.chunks[-2:] before map_blocks; added non-uniform regression tests for dask+numpy and dask+cupy. use_cuda->gpu migration in that branch was already landed by #3089/#3122. CUDA available; cupy+dask+cupy verified, 25 tests pass. Cats 1-4 clean: numpy path uses raster.where, cupy path operates on raw arrays, NaN inputs preserved, no neighborhood ops/curvature. Prior fix #1197/#1200 (crop+all_touched) merged and unrelated."
|
|
29
|
+
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."
|
|
30
|
+
proximity,2026-06-09,3108,HIGH,4;5,"Cat5/Cat4: bounded GREAT_CIRCLE dask (numpy+cupy) missed targets across the +/-180 antimeridian seam: _halo_depth sized x-halo as linear parallel-arc sum, but haversine is periodic in lon and chords shorten near poles, so array-space adjacency is no lower bound on spherical distance; numpy/cupy (brute force) found the wrap target (~111 km), dask returned NaN. Fixed in #3108 via chord bound 2R asin(cos(lat_max)|sin(dlon/2)|) + x-axis fold when seam/180-deg chord within max_distance (covers over-pole too). CUDA host: cupy + dask+cupy executed, 417+ tests pass. Cat1-3 clean (float32 output documented; NaN via isfinite consistent; bounds guards correct; tie-break unified in #2881). LOW (not fixed): great_circle_distance uses WGS84 equatorial radius 6378137 as sphere radius (~0.1% vs mean-radius convention) but documented and exposed as param."
|
|
31
|
+
rasterize,2026-06-12,3304,HIGH,3,"Cat3 HIGH: merge='sum'/'count' burned the same geometry into a pixel 2-3x on all 4 backends (cross-backend parity probes never caught it because all backends shared the bug). Two sources: all_touched=True polygons overlap the scanline center-fill with the supercover boundary pass and re-burn shared ring-vertex cells (count up to 3 for ONE polygon; rasterio MergeAlg.add gives 1); lines re-burn the connecting vertex of consecutive Bresenham segments (count 2 per interior vertex; rasterio gives 1 even for self-crossing lines). Fix #3304/PR #3313: for sum/count only, enumerate line + boundary cells host-side, dedup per (geometry,row,col), drop boundary cells the geometry's own scanline covers via a crossing-parity replay of the fill's exact float arithmetic on the same edge table, burn survivors through the point kernels (numpy/cupy/dask+numpy/dask+cupy). Coverage unchanged (==merge='last'); points intentionally not deduped (GDAL adds once per point, dup MultiPoint=2 matches rasterio). CUDA available; cupy + dask+cupy executed. 790 rasterize tests + 48 new pass. Cats 1/2/4/5 clean this pass: GPU atomic min/max NaN masks (#2255) correct, GPU ceil emulation matches np.ceil for negatives, no curvature surface, backends bit-identical on mixed-geometry probes. Pre-existing known-OK: Bresenham vs GDAL line coverage tie-breaks (pinned in coverage_2026_06_09 tests); dead all_touched branch in _extract_edges_vectorized (callers never pass all_touched) noted, not removed."
|
|
32
|
+
reproject,2026-06-12,3274,HIGH,1;4,"3 confirmed bugs, all kernel-vs-PROJ parity: #3274 HIGH LAEA inverse spurious /rq (2.6 km err for 3035) + _authalic_apa inverse series wrong (4.8 m in AEA/CEA inverses; PROJ 3-term = 1.6 mm), CPU+CUDA kernels both; #3275 HIGH _is_wgs84_compatible_ellipsoid passes R-defined spheres (MODIS sinusoidal 18.9 km err) and _aea_params/_cea_params lack the guard entirely (23.8 km on spherical aea/cea); #3276 MEDIUM itrf helmert scale 1e-9 but PROJ +s is ppm (1e-6), ~23 mm err. Verified clean: merc/emerc/UTM/tmerc/LCC/polar stere (incl lat_ts akm1) forward+inverse <=1e-5 m vs pyproj; resampling kernels NaN handling and GDAL renorm match across numpy/cupy/dask (CUDA run, gpu-vs-cpu 1.3e-7); dask footprint chunk-skip bbox is a superset in all probed cases (no holes). LOW (documented only): _source_footprint_in_target probe array typo uses x-midpoint mx as a latitude in last 3 ys entries (bbox superset, correctness unaffected)."
|
|
33
|
+
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."
|
|
34
|
+
sieve,2026-04-13T12:00:00Z,,,,Union-find CCL correct. NaN excluded from labeling. All backends funnel through _sieve_numpy.
|
|
35
|
+
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
|
|
36
|
+
terrain,2026-04-10T12:00:00Z,,,,Perlin/Worley/ridged noise correct. Dask chunk boundaries produce bit-identical results. No precision issues.
|
|
37
|
+
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."
|
|
38
|
+
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.
|
|
39
|
+
visibility,2026-04-13T12:00:00Z,,,,"Bresenham line, LOS kernel, Fresnel zone all correct. All backends converge to numpy."
|
|
40
|
+
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."
|
|
41
|
+
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."
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
module,last_inspected,issue,severity_max,categories_found,notes
|
|
2
|
-
aspect,2026-05-29,2682,MEDIUM,4;5,"Audited 2026-05-29 (agent-a3b7c82e34312ffcb worktree, branch deep-sweep-metadata-aspect-2026-05-29). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live for aspect/northness/eastness across planar and geodesic methods. Cat 1 attrs, Cat 2 coords, Cat 3 dims, and .name all preserved correctly on every backend: the 3 public functions re-emit coords=agg.coords, dims=agg.dims, attrs=agg.attrs at the xr.DataArray constructor. NEW MEDIUM finding #2682 (Cat 4 + Cat 5): the planar dask backends (_run_dask_numpy, _run_dask_cupy) called map_overlap with a default-dtype meta (np.array(()) / cupy.array(())), so the lazy DataArray advertised float64 while the chunk functions _cpu / _run_cupy cast to and return float32. numpy and cupy backends already reported float32, and the geodesic dask paths already passed dtype=np.float32, so only the two planar dask paths were inconsistent: a backend-inconsistent metadata bug where agg.dtype differs by backend and silently flips float64->float32 on .compute(). Fix in PR #2741: pass dtype=np.float32 / dtype=cupy.float32 to the planar dask meta. northness/eastness derive from aspect so they inherit the corrected dtype. 5 new tests (test_dask_numpy_advertised_dtype_matches_computed parametrized over 4 boundary modes, plus test_dask_cupy_advertised_dtype_matches_computed) assert lazy dtype == computed dtype == float32. Full aspect suite 69 passed. slope.py and curvature.py share the same default-dtype meta pattern on their planar dask paths (out of scope for this aspect-only sweep; likely same inconsistency). No CRITICAL/HIGH/LOW findings."
|
|
3
|
-
contour,2026-05-29,2700,HIGH,1;5,"Audited 2026-05-29 (agent-ab7fff484a8f57de2 worktree, branch deep-sweep-metadata-contour-2026-05-29). CUDA available; cupy and dask+cupy paths exercised live. contours() returns a list of (level, ndarray) tuples or a GeoDataFrame, not a DataArray, so Cat 2/3 DataArray checks reinterpreted as coordinate-transform + CRS propagation. Coordinate transform (np.interp over input dims, descending y respected) is correct and identical across all 4 backends (tracing is host-side via _contours_numpy). Cat 4 N/A: library convention is NaN-as-nodata; slope/aspect/curvature/focal do not read attrs['nodatavals'] either, so contour not reading it is consistent, not a bug. NEW HIGH finding #2700 (Cat 1/Cat 5): contours(return_type='geopandas') crashed with 'Assigning CRS to a GeoDataFrame without a geometry column is not supported' whenever the input had attrs['crs'] but the result was empty (flat raster, levels outside data range) because _to_geopandas built gpd.GeoDataFrame([], crs=crs) with no geometry column; separately the all-NaN early-return passed crs=None and silently dropped the CRS. Fix (PR #2708): _to_geopandas builds an empty frame with an explicit geometry column so the CRS attaches; all-NaN early-return forwards agg.attrs['crs']. Both empty paths now return a well-formed empty GeoDataFrame carrying the CRS. 4 new tests in TestGeoDataFrame cover populated-CRS, empty-with-CRS, all-NaN-with-CRS, and empty-without-CRS. Full contour suite 28 passed. numpy-return path emits no DataArray attrs by design (list of tuples)."
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
module,last_inspected,issue,severity_max,categories_found,notes
|
|
2
|
+
aspect,2026-05-29,2682,MEDIUM,4;5,"Audited 2026-05-29 (agent-a3b7c82e34312ffcb worktree, branch deep-sweep-metadata-aspect-2026-05-29). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live for aspect/northness/eastness across planar and geodesic methods. Cat 1 attrs, Cat 2 coords, Cat 3 dims, and .name all preserved correctly on every backend: the 3 public functions re-emit coords=agg.coords, dims=agg.dims, attrs=agg.attrs at the xr.DataArray constructor. NEW MEDIUM finding #2682 (Cat 4 + Cat 5): the planar dask backends (_run_dask_numpy, _run_dask_cupy) called map_overlap with a default-dtype meta (np.array(()) / cupy.array(())), so the lazy DataArray advertised float64 while the chunk functions _cpu / _run_cupy cast to and return float32. numpy and cupy backends already reported float32, and the geodesic dask paths already passed dtype=np.float32, so only the two planar dask paths were inconsistent: a backend-inconsistent metadata bug where agg.dtype differs by backend and silently flips float64->float32 on .compute(). Fix in PR #2741: pass dtype=np.float32 / dtype=cupy.float32 to the planar dask meta. northness/eastness derive from aspect so they inherit the corrected dtype. 5 new tests (test_dask_numpy_advertised_dtype_matches_computed parametrized over 4 boundary modes, plus test_dask_cupy_advertised_dtype_matches_computed) assert lazy dtype == computed dtype == float32. Full aspect suite 69 passed. slope.py and curvature.py share the same default-dtype meta pattern on their planar dask paths (out of scope for this aspect-only sweep; likely same inconsistency). No CRITICAL/HIGH/LOW findings."
|
|
3
|
+
contour,2026-05-29,2700,HIGH,1;5,"Audited 2026-05-29 (agent-ab7fff484a8f57de2 worktree, branch deep-sweep-metadata-contour-2026-05-29). CUDA available; cupy and dask+cupy paths exercised live. contours() returns a list of (level, ndarray) tuples or a GeoDataFrame, not a DataArray, so Cat 2/3 DataArray checks reinterpreted as coordinate-transform + CRS propagation. Coordinate transform (np.interp over input dims, descending y respected) is correct and identical across all 4 backends (tracing is host-side via _contours_numpy). Cat 4 N/A: library convention is NaN-as-nodata; slope/aspect/curvature/focal do not read attrs['nodatavals'] either, so contour not reading it is consistent, not a bug. NEW HIGH finding #2700 (Cat 1/Cat 5): contours(return_type='geopandas') crashed with 'Assigning CRS to a GeoDataFrame without a geometry column is not supported' whenever the input had attrs['crs'] but the result was empty (flat raster, levels outside data range) because _to_geopandas built gpd.GeoDataFrame([], crs=crs) with no geometry column; separately the all-NaN early-return passed crs=None and silently dropped the CRS. Fix (PR #2708): _to_geopandas builds an empty frame with an explicit geometry column so the CRS attaches; all-NaN early-return forwards agg.attrs['crs']. Both empty paths now return a well-formed empty GeoDataFrame carrying the CRS. 4 new tests in TestGeoDataFrame cover populated-CRS, empty-with-CRS, all-NaN-with-CRS, and empty-without-CRS. Full contour suite 28 passed. numpy-return path emits no DataArray attrs by design (list of tuples)."
|
|
4
|
+
cost_distance,2026-06-15,3344,MEDIUM,5,"Audited 2026-06-15 (agent-ad0b84e7f7b212360 worktree, branch deep-sweep-metadata-cost_distance-2026-06-15). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live end-to-end with a rich attrs set (res/crs/transform/nodatavals/_FillValue/units). Cat 1 attrs, Cat 2 coords (values + float64 dtype), and Cat 3 dims (y,x) all preserved and identical across the 4 backends -- public cost_distance() wraps with xr.DataArray(coords=raster.coords, dims=raster.dims, attrs=raster.attrs). NEW MEDIUM finding #3344 (Cat 5): the dask+numpy and dask+cupy backends leaked the internal dask graph name (_trim-<hash> from map_overlap, asarray-<hash> from the dask+cupy convert-back path) into result.name while numpy/cupy returned None; .name was a nondeterministic per-run token that breaks .to_dataset() variable keys and any name-keyed pipeline. Same .name-leak class as proximity #2723 and zonal #2611. Fix (PR #3349 on this branch): return result.rename(raster.name) -- a constructor name= kwarg does not override a named dask array, and name=None is treated as infer-from-data, so .rename() is required. supports_dataset path unaffected (keys by var_name, verified live). New parametrized regression test test_result_name_matches_input over 4 backends x {None, named}; full cost_distance suite 63 passed (post-merge with origin/main). LOW (documented, not fixed): output float32 uses NaN as the unreachable sentinel but input nodatavals/_FillValue (e.g. -9999) are carried through verbatim, so a downstream reader masks a value that never appears -- this is the library-wide attrs=raster.attrs convention shared by proximity/slope/aspect/focal, not a cost_distance-specific bug, so fixing it in isolation would diverge this module from every peer. No CRITICAL/HIGH findings."
|
|
5
|
+
focal,2026-06-10,3217,MEDIUM,4;5,"Re-audited 2026-06-10 (agent-ad0d55a894c6abc60 worktree, branch deep-sweep-metadata-focal-2026-06-10). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live for mean, apply, focal_stats, hotspots. Cats 1-3 clean: attrs (res/crs/nodatavals/_FillValue/unit), coords (values, dtype, coord attrs), dims, .name, 3D per-band path, and hotspots unit=% all preserved and identical across the 4 backends. NEW MEDIUM finding #3217 (Cat 4 + Cat 5): (a) mean() hardcoded float32 on the GPU paths (_mean_cupy cupy.asarray(dtype=float32), _mean_dask_cupy astype(float32)) while numpy/dask+numpy returned float64 (mean() casts astype(float) before dispatch), so float64 input silently lost precision on cupy/dask+cupy; dask+cupy also advertised float64 (untyped meta) but computed float32. (b) apply()/focal_stats() dask paths passed untyped meta (np.array(()) / cupy.array(())) to map_overlap, so for float32/int input the lazy DataArray advertised float64 but computed the promoted float32 (#2805 typed the chunk fns but not the meta). Same class as aspect #2682 and proximity #2723. Fix: the mean() GPU dtype half landed on main first via duplicate issue #3214/PR #3221 (_promote_float contract: float dtypes preserved, ints->float32, GPU bit-exact vs CPU in float64); PR #3226 (branch deep-sweep-metadata-focal-2026-06-10-01) types every map_overlap meta with data.dtype and aligns tests to the _promote_float contract; 25 new parametrized regression tests (4 backends x 3 dtypes mean; dask backends x 3 dtypes apply/focal_stats; exact CPU/GPU parity). Full focal suite 258 passed. No other CRITICAL/HIGH/MEDIUM/LOW findings."
|
|
6
|
+
geotiff,2026-06-09,3116,HIGH,2;3,"Re-audited 2026-06-09 (agent-ae89ff94a64e3ee8f worktree, branch deep-sweep-metadata-geotiff-2026-06-09). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live. Focus: surfaces changed since the 2026-05-18 audit (unpack rename + GPU/dask+GPU support #3075, pack=True #3065/#3079, masked int->float promotion #2994, bbox= reads, rioxarray param alignment #2963, no-georef VRT coord synthesis #2824, GeoTransform omission #2971). Live probes: unpack attrs (scale_factor/add_offset/mask_and_scale_dtype/nodata/masked_nodata), masked=True promotion, default masked=False, bbox window+transform shift, multi-band band=N, dims/name/coords (incl. coord dtype) all identical across the 4 backends; nodata_pixels_present absent on dask paths is the documented lazy contract, not a bug. pack->unpack round trips verified on numpy/dask/gpu-write; pack of a cupy-backed read raises via the known cupy+xarray xp.astype incompat (see memory cupy_where_astype_incompat; dependency-pin fix, raises loudly, not a metadata bug). VRT reads (full/masked/window/bbox) and no-georef TIFF reads agree across the 4 backends. NEW HIGH finding #3116 (Cat 2+3): to_geotiff(non_georef_da, out.vrt, tile_size=N) wrote a corrupt index for arrays spanning >1 tile -- write_vrt derives placement from each source GeoTransform and non-georef tiles all carry the identity transform, so rasterX/YSize collapsed to one tile and every DstRect landed at the origin; reads silently returned a single tile (24x32 in -> 16x16 out). Gap left by #2966/#2971 (tests only covered one non-georef source). Fix: _write_vrt_tiled threads per-tile pixel offsets through _build_vrt -> write_vrt via internal dst_offsets kwarg; write_vrt refuses >1 all-non-georef sources without explicit placement and rejects dst_offsets alongside georeferenced sources. 18 new tests in tests/vrt/test_non_georef_placement_3116.py incl. 4-backend round trip, dask-backed and plain-ndarray writes, XML DstRect assertions, georef placement regression, and the write_vrt error contract. Full vrt suite 520 passed; write+round-trip suites 1292 passed."
|
|
7
|
+
interpolate,2026-06-12,3288,MEDIUM,5,kriging K_inv-None fallback was numpy-backed on all backends and misnamed the variance raster; fixed via #3288. All 4 backends verified end-to-end on GPU host. LOW (documented only): template nodatavals/_FillValue copied verbatim while fill_value is the actual output sentinel; tests codify attrs==template.attrs
|
|
8
|
+
mcda,2026-06-10,3147,HIGH,1,"constrain() dropped all attrs (res/crs/nodatavals) whenever exclude non-empty (xr.where takes attrs from scalar fill); fixed via attrs restore, tests for numpy/dask/dask+cupy. All other mcda funcs keep attrs/coords/dims on all 4 backends. Out-of-scope crashes noted for backend-parity: owa broken on cupy (numpy order-weights x cupy) and on dask (da.sort does not exist); sensitivity monte_carlo crashes on cupy/dask+cupy (.values on cupy); xr.where compute on cupy/dask+cupy hits known cupy13.6/xarray2025.12 incompat."
|
|
9
|
+
polygonize,2026-06-12,3293,MEDIUM,1,"Audited 2026-06-12 (agent-a86d90abea41b04cf worktree, branch deep-sweep-metadata-polygonize-2026-06-12). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live for int, float+NaN, and no-georef rasters. polygonize returns vector output (numpy/awkward/geopandas/spatialpandas/geojson), not a DataArray, so Cats 2-4 reinterpreted as transform/CRS/value-dtype propagation. Transform auto-detect (attrs['transform'] -> rio.transform() -> x/y coords, #2536/#2607) and CRS resolution run in public polygonize() before dispatch, so all 4 backends emit identical columns, bounds, and CRS (verified live). Column value dtype follows input dtype on every backend. NEW MEDIUM finding #3293 (Cat 1): _detect_raster_crs ignored the _xrspatial_no_georef marker that _detect_raster_transform honours, so a geotiff-reader crs_only raster (attrs carry both crs and the marker; metadata_to_attrs writes crs independent of has_georef) produced a GeoDataFrame claiming EPSG:#### over pixel-space geometries -- the #2536 metadata-lies-about-the-data mismatch through the marker channel. contour.py imports the same helper and inherits the fix. Fix: early return None in _detect_raster_crs on the marker + docstring note; 2 new tests in TestPolygonizeCRSPropagation. polygonize+contour suites 274 passed; all 9 auxiliary polygonize test files 303 passed. rotated-read path unaffected (reader drops CRS there). No CRITICAL/HIGH/LOW findings."
|
|
10
|
+
proximity,2026-05-29,2723,MEDIUM,4;5,"Audited 2026-05-29 (agent-a61dbadc2452a2003 worktree, branch deep-sweep-metadata-proximity-2026-05-29). CUDA+cupy available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live end-to-end for proximity/allocation/direction, both bounded (finite max_distance) and unbounded. Cat 1 (attrs res/crs/transform/nodatavals/_FillValue), Cat 2 (coords + coord dtype), and Cat 3 (dims) all preserved and identical across the 4 backends -- public funcs wrap with xr.DataArray(coords=raster.coords, dims=raster.dims, attrs=raster.attrs). NEW MEDIUM finding #2723 (Cat 4 + Cat 5): (a) bounded dask+numpy path (_process_dask -> da.map_overlap with meta=np.array(())) declared output dtype float64 while the chunk fn returns float32 and numpy/cupy/dask+cupy + the unbounded KDTree path all declare float32; docstrings show dtype=float32. Fix: meta=np.array((), dtype=np.float32). (b) dask backends leaked an internal dask op name (_trim-<hash>, _kdtree_chunk_fn-<hash>, asarray-<hash>) into result.name while numpy/cupy return None. Fix: assign result.name=None after construction in all 3 public funcs (xarray ignores a name=None kwarg for named dask arrays, so the reset must happen post-construction). Same .name-leak class as zonal #2611. PR #2728 off child branch deep-sweep-metadata-proximity-2026-05-29-01. New parametrized regression test test_output_metadata_consistent_across_backends asserts declared dtype float32 + name None across all 4 backends x 3 funcs x bounded/unbounded; full test_proximity.py suite 93 passed. No other CRITICAL/HIGH/MEDIUM/LOW findings."
|
|
11
|
+
rasterize,2026-06-09,3087,MEDIUM,1,GeoDataFrame .crs dropped on no-like path (Cat 1); fixed via #3087 emitting attrs crs/crs_wkt when output has no CRS. like-path attrs/coords/dims/nodata verified live on all 4 backends (CUDA available); Cats 2-5 clean.
|
|
12
|
+
reproject,2026-06-12,3262,MEDIUM,4,"Re-audited 2026-06-12 (agent-ae420c90e50a23c5c worktree, branch deep-sweep-metadata-reproject-2026-06-12). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live end-to-end for reproject() and merge(). Cat 1 attrs (crs/nodata/res/transform/_FillValue/nodatavals refreshed or carried, crs_wkt dropped), Cat 2 coords (pixel-center verified numerically, scalar time + band coord carry, float64), Cat 3 dims (lat/lon names, band-first (band,y,x) round-trip), Cat 4 int16 sentinel parity, and Cat 5 cross-backend attr parity all identical across the 4 backends for reproject(); vertical_crs=4979/vertical_datum verified on numpy + dask; geoid_height_raster carries input attrs per its documented contract. NEW MEDIUM finding #3262 (Cat 4): merge() hardcoded float64 output on every path (_merge_inmemory, _merge_dask template/meta, empty-chunk fills) while reproject() round-trips integer dtypes on all 5 paths (#2185/#2505/#3093/#3096); undocumented and unpinned by tests, so an int16/uint8 mosaic silently promoted (8x memory for uint8, GeoTIFF round-trip changes file dtype). Fix on this branch: shared-integer-dtype inputs now cast back via the reproject round/clip/cast convention (_cast_merged_dtype), output nodata resolved with _detect_nodata dtype hint (NaN->sentinel swap per #2185, explicit out-of-range raises per #2572), dask template/meta + empty-chunk fills use the output dtype (#3096 trap), docstring documents the rule; mixed/float inputs keep float64. 13 new tests in TestMergeIntegerDtype (eager/dask/dask-empty-chunk/cupy, sentinel defaults, mean rounding, out-of-range raise); full reproject suite 514 passed. LOW (documented, not fixed): reproject() docstring says dask inputs are fully lazy but the dask+cupy VRAM-fitting fast path returns an eager cupy array (codified in tests; doc nit). Prior LOW from 2026-06-09 (geoid_height ndarray return for DataArray input) unchanged."
|
|
13
|
+
resample,2026-05-27,2542,MEDIUM,2;4;5,"Audited 2026-05-27 (agent-a8135a6a246ecb93c worktree, branch deep-sweep-metadata-resample-2026-05-27). Cat 2 MEDIUM + Cat 4 MEDIUM + Cat 5 MEDIUM all rolled into issue #2542. (a) 2D non-identity path dropped scalar non-dim coords like rioxarrays spatial_ref and squeezed time/band selectors; identity path (scale==1.0, agg.copy()) and 3D path (per-band xr.concat) preserved them, so the bug was path-inconsistent (Cat 5). (b) _resolve_nodata reads attrs[nodata] as a fallback sentinel but the output post-processing only refreshed _FillValue and nodatavals, leaving attrs[nodata]=-9999 alongside data that was now NaN. Fix in resample(): refresh attrs[nodata] to NaN whenever the input had it, and carry across zero-dim non-dim coords on the 2D non-identity path. 7 new tests in TestMetadataPropagation cover nodata-attr refresh, spatial_ref/scalar coord carry, identity-vs-downsample coord parity, and the explicit choice to drop spatially-shaped extra coords. 4-backend (numpy/cupy/dask+numpy/dask+cupy) parity verified for spatial_ref carry; nodata-attr refresh verified on numpy/cupy/dask+numpy (dask+cupy non-NaN nodata masking hits a pre-existing xarray xr.where + cupy.astype quirk unrelated to this audit). Full resample test suite (175 passed) clean."
|
|
14
|
+
viewshed,2026-05-29,2743,MEDIUM,4;5,output .name differed across backends (None/viewshed/dask-token) and dtype float32 on GPU vs float64 on CPU; added name= param and forced float64 on all backends; attrs/coords/dims already preserved
|
|
15
|
+
visibility,2026-06-10,3193,HIGH,5,"cupy backend crash in cumulative_viewshed/visibility_frequency (count np vs cupy add) -> no result/metadata emitted; fixed by cupy count branch + cupy tests. numpy/dask preserve coords/dims/attrs incl crs; visibility_frequency keeps attrs through astype/divide. line_of_sight Dataset drops crs/transform (LOW, transect not raster, documented only)."
|
|
16
|
+
zonal,2026-05-29,2611,MEDIUM,5,"Audited 2026-05-29 (agent-ae8d8b65cc3a5c40a worktree, branch deep-sweep-metadata-zonal-2026-05-29). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live. 5 DataArray-returning functions checked end-to-end: apply, regions, hypsometric_integral, trim, crop. attrs (res/crs/transform/nodatavals), dims, and coords preserved correctly on all 4 backends for every function; trim/crop slice coords with no half-pixel drift. stats() and crosstab() return DataFrames by design so Cat 1-3 DataArray checks N/A. NEW MEDIUM finding #2611 (Cat 5): apply() never set output .name, so numpy/cupy returned None while dask+numpy/dask+cupy inherited a non-deterministic internal dask task name (e.g. _chunk_fn-<hash>). regions/hypsometric_integral/trim/crop all set deterministic names; apply was the outlier. Fix in PR #2611/#2622: add name param (default None) and assign result.name after DataArray construction (setting name= at construction does not override the dask graph name). New parametrized test test_apply_name_consistent_across_backends covers default-None and explicit-name on all 4 backends. Full zonal suite 213 passed. No other CRITICAL/HIGH/MEDIUM findings; no LOW findings to document."
|