Skip to content

Cover degenerate shape + NaN/Inf reads on GPU and dask geotiff backends#1630

Merged
brendancol merged 2 commits into
mainfrom
deep-sweep-test-coverage-geotiff-2026-05-11-33728
May 11, 2026
Merged

Cover degenerate shape + NaN/Inf reads on GPU and dask geotiff backends#1630
brendancol merged 2 commits into
mainfrom
deep-sweep-test-coverage-geotiff-2026-05-11-33728

Conversation

@brendancol
Copy link
Copy Markdown
Contributor

Summary

  • The geotiff eager numpy path covers 1x1, 1xN, Nx1, all-NaN, Inf,
    and NaN-sentinel inputs end-to-end. The GPU, dask+numpy, and
    dask+cupy backends went unexercised for the same inputs.
  • Adds 23 tests in test_degenerate_shapes_backends_2026_05_11.py
    pinning the contract across every non-eager backend.

Cat 3 HIGH (geometric edges):

  • 1x1 single-pixel 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):

  • All-NaN 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 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)
  • Related geotiff tests still pass (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).

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
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label May 11, 2026
@brendancol brendancol requested a review from Copilot May 11, 2026 21:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +15 to +16
* 1x1 and 1xN writes through ``write_geotiff_gpu`` (Cat 3 HIGH for
the GPU writer's degenerate-shape path).
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +217 to +222
"""``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.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +352 to +397
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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@brendancol brendancol merged commit caab40b into main May 11, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants