Cover degenerate shape + NaN/Inf reads on GPU and dask geotiff backends#1630
Conversation
Backend coverage gap for 1x1, 1xN, Nx1 rasters and for all-NaN, Inf, and NaN-sentinel inputs on non-eager backends. The eager numpy path covers these in test_edge_cases; the GPU, dask+numpy, and dask+cupy paths went unexercised. Adds 23 tests pinning the contract. Cat 3 HIGH (geometric edges on non-eager backends): - 1x1 reads on dask+numpy, GPU, dask+cupy - 1xN single-row reads on the same three backends - Nx1 single-column reads on the same three backends - 1x1 / 1xN / Nx1 writes through write_geotiff_gpu Cat 2 MEDIUM (NaN / Inf / nodata edge cases): - All-NaN raster reads on GPU + dask+cupy - Inf / -Inf reads on every non-eager backend - NaN sentinel mask on the float dask read path, including a sentinel block that straddles a chunk boundary
There was a problem hiding this comment.
Pull request overview
Adds targeted GeoTIFF backend regression coverage for degenerate raster shapes (1x1 / 1xN / Nx1) and special float values (NaN/Inf) across the non-eager read/write backends (GPU, dask+numpy, dask+cupy), closing previously unexercised edge cases without changing any production code.
Changes:
- Add a new test module exercising degenerate-shape reads across dask+numpy, GPU, and dask+cupy, plus degenerate-shape GPU-writer round-trips.
- Add backend coverage for all-NaN and +/-Inf reads, and for finite nodata sentinels being masked to NaN on the dask read path.
- Update the sweep tracking state CSV to record this audit pass and covered categories.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
xrspatial/geotiff/tests/test_degenerate_shapes_backends_2026_05_11.py |
Adds 23 regression tests covering degenerate shapes and NaN/Inf behavior across non-eager GeoTIFF backends (incl. GPU writer). |
.claude/sweep-test-coverage-state.csv |
Updates audit tracking notes/state for the geotiff module sweep pass. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * 1x1 and 1xN writes through ``write_geotiff_gpu`` (Cat 3 HIGH for | ||
| the GPU writer's degenerate-shape path). |
There was a problem hiding this comment.
Fixed in 8147b35: module docstring bullet now reads "1x1, 1xN, and Nx1 writes through write_geotiff_gpu" to match the actual GPU-writer coverage in the file.
| """``write_geotiff_gpu`` must accept 1-pixel and 1-row inputs. | ||
|
|
||
| The GPU writer's tile-encoding path uses an internal grid sizing | ||
| helper that fell back to host code for shapes smaller than the | ||
| default tile. The fallback exists but had no regression test that | ||
| would catch a future "fast-path only" refactor. |
There was a problem hiding this comment.
Fixed in 8147b35: TestGpuWriterDegenerateShapes docstring now says "must accept 1-pixel, 1-row, and 1-column inputs", and the section header above the class also lists Nx1.
| class TestNanSentinelDaskRead: | ||
| """Float raster with ``nodata=NaN`` sentinel reads consistently | ||
| across backends. | ||
|
|
||
| The integer-sentinel equivalent is pinned by issue #1597. The | ||
| float path has no such per-chunk dtype divergence (the input is | ||
| already float), but the dask graph still has to forward the | ||
| sentinel substitution. A regression in the float branch of | ||
| ``_delayed_read_window`` would silently break this. | ||
| """ | ||
|
|
||
| @pytest.fixture | ||
| def nan_sentinel_path(self, tmp_path): | ||
| arr = np.arange(64, dtype=np.float32).reshape(8, 8) | ||
| arr[2:4, 2:4] = -9999.0 | ||
| arr[6, 0] = -9999.0 | ||
| p = tmp_path / "nan_sentinel_float.tif" | ||
| to_geotiff(arr, str(p), nodata=-9999.0) | ||
| return str(p), arr | ||
|
|
||
| def test_eager_path_baseline(self, nan_sentinel_path): | ||
| """Baseline: eager path replaces the sentinel with NaN.""" | ||
| path, _ = nan_sentinel_path | ||
| result = open_geotiff(path) | ||
| assert np.isnan(result.values[2, 2]) | ||
| assert np.isnan(result.values[6, 0]) | ||
| assert result.values[0, 0] == 0.0 # non-sentinel survives | ||
|
|
||
| def test_dask_numpy_matches_eager(self, nan_sentinel_path): | ||
| """dask compute reproduces the eager mask exactly.""" | ||
| path, _ = nan_sentinel_path | ||
| eager = open_geotiff(path) | ||
| dk = open_geotiff(path, chunks=4).compute() | ||
| np.testing.assert_array_equal(np.isnan(dk.values), np.isnan(eager.values)) | ||
| finite = ~np.isnan(eager.values) | ||
| np.testing.assert_array_equal(dk.values[finite], eager.values[finite]) | ||
|
|
||
| def test_dask_numpy_chunks_smaller_than_sentinel_block(self, nan_sentinel_path): | ||
| """Sentinels split across two chunks still mask correctly. | ||
|
|
||
| The 2x2 sentinel block at rows 2-3 cols 2-3 lands in a single | ||
| chunk for chunks=4 (rows 0-3) but straddles a chunk boundary | ||
| for chunks=2 (rows 2-3 split between chunks 1 and 2). This | ||
| exercises the per-block sentinel comparison. | ||
| """ | ||
| path, _ = nan_sentinel_path |
There was a problem hiding this comment.
Fixed in 8147b35: class docstring now reads "Float raster with a finite nodata sentinel (-9999.0) is masked to NaN consistently across backends on read", matching what the fixture actually writes.
- Module docstring: list Nx1 GPU-writer coverage that was already tested - Class docstring (DegenerateGPUWriter): mention 1-column inputs - NaN-sentinel docstring: clarify it's a finite sentinel masked to NaN Tests unchanged; docstrings only.
Summary
and NaN-sentinel inputs end-to-end. The GPU, dask+numpy, and
dask+cupy backends went unexercised for the same inputs.
test_degenerate_shapes_backends_2026_05_11.pypinning the contract across every non-eager backend.
Cat 3 HIGH (geometric edges):
write_geotiff_gpuCat 2 MEDIUM (NaN / Inf / nodata):
sentinel block straddling a chunk boundary
No source changes; tests only. Test-coverage gap sweep 2026-05-11
pass 5 against the geotiff module.
Test plan
pytest xrspatial/geotiff/tests/test_degenerate_shapes_backends_2026_05_11.py(23 pass on a GPU host)test_edge_cases.py,test_attrs_parity_1548.py,test_dask_int_nodata_chunks_1597.py,test_gpu_nodata_1542.py,test_dask_cupy_combined.py).