Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/sweep-test-coverage-state.csv
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ corridor,2026-06-22,,HIGH,1;3;4;5,"Deep-sweep 2026-06-22 test-coverage on a CUDA
cost_distance,2026-06-16,3367,MEDIUM,1;2,"Pass (2026-06-16 deep-sweep test-coverage, CUDA host). cost_distance is heavily tested: 1122 test lines for 1354 src lines, all 4 backends parametrized + regression tests for #1191/#880/#1252/#1262/#3340/#3341/#3343/#3344. Found one MEDIUM Cat 1+2 gap: _cost_distance_dask f_min<=0 early return (all-impassable friction, finite max_cost -> da.full NaN preserving chunks) was unreached -- numpy equiv covered by test_source_on_impassable_cell, iterative dask by test_iterative_narrow_corridor, but the bounded map_overlap wrapper shortcut was not. Filed #3367, added test_dask_all_impassable_friction_returns_nan (all-zero friction, dask+numpy chunks(3,3), max_cost=5; asserts all-NaN, dask-backed, npartitions>1). RAN + PASSED; -W error::UserWarning confirms early return taken (no iterative warning). Full file 85 passed on CUDA host. LOW (documented, not fixed): non-square cellsize numeric correctness untested (_make_meta_raster uses res=(2,3) but test_metadata_preserved checks metadata only)."
dasymetric,2026-06-20,3407;3406,HIGH,2;3;4;5,"deep-sweep test-coverage on a CUDA host (CUDA available, GPU tests ran). Module is well covered (813 test loc / 834 src): 4-backend equivalence for disaggregate weighted+binary, conservation, NaN/nodata/negative-weight, limiting_variable + cupy/dask NotImplemented guards, pycnophylactic numpy+cupy+dask-raises, validate_disaggregation all backends, memory guards (#1261). Filed #3407 (test-only) for real gaps and added 4 new test classes (11 passed, 2 xfailed). Cat5 HIGH: metadata (attrs res/crs + coords) never asserted -> TestMetadataPreservation (numpy/dask). Cat3 HIGH: true 1x1 raster untested (only 1x2 strip) -> TestSinglePixel for disaggregate weighted/binary + pycnophylactic (degenerate no-shift smoothing) + dask parity. Cat2 MEDIUM: Inf weight collapses zone total to 0 (silent conservation break) -> TestInfWeight pins current behaviour. Cat4 MEDIUM: 3-class limiting_variable (multi-break + per-class caps) untested despite docstring -> TestLimitingVariableThreeClass. SOURCE BUG found (filed #3406, NOT fixed - test-only sweep): pycnophylactic raises ValueError (np.nanmax on zero-size array) when no pixel is valid for smoothing (all-NaN zones or no zone id in values); disaggregate handles same input gracefully (all-NaN). Pinned with TestPycnophylacticEmptyValid xfail(strict, raises=ValueError) -> flips red when #3406 fixed. LOW (documented, not fixed): non-square cellsize never exercised (all tests use res 0.5/0.5); disaggregate cupy/dask+cupy 1x1 + metadata not separately added (eager numpy gap was the real one, GPU dispatch already covered by TestCrossBackend)."
diffusion,2026-06-20,3422,HIGH,1;2;3;4,"Pass 1 (2026-06-20, deep-sweep test-coverage, CUDA host). diffuse() dispatch table registers all 4 backends but test_diffusion.py only exercised numpy + dask+numpy. Cat 1 HIGH: cupy (_diffuse_cupy/_diffuse_step_gpu) and dask+cupy (_diffuse_dask_cupy/_diffuse_chunk_cupy) registered but never invoked -- no test ran them. Cat 4 HIGH: boundary accepts nan/nearest/reflect/wrap; only nearest+wrap tested, reflect had none. Cat 3 HIGH: 1x1 single-pixel and Nx1/1xN strip rasters never tested. Cat 2 MEDIUM: NaN tested numpy-only; Inf and all-NaN inputs untested. Filed #3422, added 14 tests (PR #3424, test-only, source untouched): cupy/dask+cupy parity vs numpy (incl. spatially-varying alpha + NaN propagation), reflect boundary across all 4 backends, 1x1 + Nx1 + 1xN (numpy + chunked dask strip), all-NaN stays NaN, Inf contamination smoke test. All 14 RAN+PASSED on a CUDA host; the 4 cupy/dask+cupy tests genuinely executed (not skipped); full file 39 passed. All paths verified correct before the tests were added -- coverage gap, not a bug. LOW (documented, not fixed): non-square cellsize (res[0]!=res[1]) never exercised -- diffuse uses res[0] as dx and assumes square cells; empty 0-row/0-col raster untested; asv benchmark absent; 'nan' boundary-mode edge=NaN behaviour not directly asserted on diffuse (covered indirectly via wrap/nearest)."
fire,2026-06-19,,HIGH,1;5,"Cat1 HIGH: dask+cupy dispatch registered but untested for rdnbr/burn_severity_class/fireline_intensity/flame_length/rate_of_spread/kbdi (only dnbr had it). Cat5 MEDIUM: only dnbr asserted attrs/coords/dims preservation. Added 6 test_numpy_equals_dask_cupy + 6 test_output_preserves_metadata; all 66 fire tests pass on a CUDA host. Cat2 LOW (not fixed): Inf inputs untested (pure per-cell math, low risk)."
fire,2026-06-25,,HIGH,2,"Deep-sweep 2026-06-25 test-coverage on a CUDA host. Backend matrix already complete: all 7 public funcs (dnbr/rdnbr/burn_severity_class/fireline_intensity/flame_length/rate_of_spread/kbdi) x 4 backends present and green (Cat 1 no gap). NaN covered (per-func nan_propagation + #3394 dtype parity). Cat 4 covered: rate_of_spread tests all 13 fuel models + invalid 0/14; kbdi annual_precip invalid 0/-100; fireline heat_content default+custom. Cat 5 covered via general_output_checks on every func. Found one gap: Cat 2 +Inf/-Inf inputs were untested on every function. Probed all 4 backends live: behavior is fully consistent and well-defined (no divergence, no bug) -- e.g. dnbr inf-inf->nan, burn_severity_class +inf->7/-inf->1, kbdi prev=inf clamps to 800, rate_of_spread slope=inf->nan. Added test-only regression: per-func numpy Inf contract (locks exact values) + 4-backend Inf parity (28 new tests, all RAN and PASSED on GPU). No source change; the kernels' only finite guard is v!=v so these lock that contract. Cat 3 1x1/strip: per-pixel kernels (no neighborhood window) so no degeneracy risk, and 1x1/1xN already exercised by kbdi/rdnbr/flame tests -> LOW, not added."
flood,2026-06-25,,MEDIUM,1,"Deep-sweep 2026-06-25 test-coverage on a CUDA host. Module is densely tested (1051 test LOC vs 966 source). Backend matrix nearly complete: all 7 public funcs x 4 backends present and green EXCEPT vegetation_roughness mode='ndvi' on dask+cupy -- _veg_roughness_ndvi_dask_cupy was dispatched (flood.py:585) but never invoked by any test (nlcd dask+cupy, ndvi cupy, ndvi dask all tested). Cat 1 MEDIUM: added TestVegRoughnessDaskCuPy::test_ndvi_numpy_equals_dask_cupy mirroring the nlcd case; GPU-validated locally (passed, full file 89 passed). Cat 2 NaN well covered per-func incl #1104 (NaN curve_number) and #1437 (mannings_n DataArray) regressions; Inf inputs untested but low-risk (HAND/rainfall Inf -> NaN), not flagged. Cat 3 1xN strips + 1x1 covered for several funcs. Cat 5 metadata preserved is asserted on every backend test via general_output_checks (verify_attrs defaults True), so inundation/curve_number_runoff lacking a dedicated coords test is NOT a real gap. No source bugs found."
focal,2026-06-10,3220;3219;3225,HIGH,1;2;3;4,"Deep-sweep 2026-06-10 on CUDA host, all 4 backends executed. Filed #3220 (coverage) and added 36 tests in PR branch: Inf inputs for mean/focal_stats (HIGH Cat2 - no Inf test existed anywhere), mean NaN input (HIGH Cat2 - default excludes=[nan] semantics never asserted), 1x1 + 1xN/Nx1 strips (HIGH Cat3), empty 0-row raster numpy-only (MEDIUM Cat3), mean passes=2 == mean(mean) and excludes sentinel -9999 behavioral tests (MEDIUM Cat4), dask+cupy non-default boundary modes for mean/apply/focal_stats (MEDIUM Cat1/4). Bugs surfaced, filed separately (NOT fixed here): #3219 hotspots silently returns all zeros on Inf input (nan global std passes the std==0 guard, all 4 backends); #3225 empty raster works on numpy but crashes cupy (raw CudaAPIError) and dask (map_overlap depth ValueError). hotspots+Inf and non-numpy empty behavior left unpinned until those are fixed. Backend matrix for the 4 public funcs was already solid (all 4 backends + parity); boundary modes covered except dask+cupy. Siblings filed #3214-3217 same day (dtype/docstring/apply-default-func) - no overlap."
geotiff,2026-06-25,3518,MEDIUM,1,"Pass 23 (2026-06-25, deep-sweep test-coverage, CUDA host): delta audit of the ~15 geotiff commits since pass 22 (06-12..06-24): #3483 categorical PAM sidecar, #3375/#3376/#3380 xarray backend engine, #3373/#3374 chunked GPU read-once, #3371/#3372 reject predictor+lossy codec, #3331/#3332 reject zero/non-finite ModelPixelScale, #3327 gate dict gdal_metadata behind rich-tag opt-in, #3323/#3325 masked_nodata dtype-cast, #3277 pack nodata native width. Filed #3518 (tests). Cat 1 MEDIUM: the #3483 categorical PAM sidecar (_pam.py + _write_category_sidecar in _writers/eager.py) is round-trip tested only on the eager numpy write path (xrspatial/tests/test_rasterize_categorical_3482.py); the dask streaming (eager.py:1063) and GPU/nvCOMP (eager.py:880) write branches each have their own sidecar emit call that no test touches. Live probe on this CUDA host: dask + GPU categorical round-trips both emit the sidecar and read back names+RGBA colors today (no source bug, pure coverage gap). Added geotiff/tests/write/test_category_sidecar_backends_3483.py: dask write round-trip + GPU write round-trip (requires_gpu, RAN+passing locally) + names-only (category_colors=None build branch + names-only read path, never hit by the existing colors-always suite). 3 tests pass. Verified NOT gaps this pass: #3327 dict gdal_metadata gate has 43-line test_contract.py coverage; #3331/#3332 zero pixel scale covered by unit/test_degenerate_pixel_size_3331.py; #3371/#3372 predictor+lossy-codec reject has 2 test files; #3375/#3376/#3380 xarray engine covered by test_xarray_backend_3365.py + coregister 3376/3379; #3277 pack native width has 3 test files. LOW (carried, documented not fixed): Inf as the declared nodata sentinel still never tested. || Pass 22 (2026-06-12, deep-sweep test-coverage): delta audit of the ~20 commits since pass 21 (06-09..06-12, mostly pack/unpack fixes + #3241 GPU streaming writer + coregister #3254/#3248). Filed #3266 (tests). Cat 1 MEDIUM: pack=True gained working gpu/dask+gpu support in #3240, but three pack features were tested numpy+dask only: float32 width preservation (#3080, test_pack_float_width_3080.py), nodata kwarg fill (#3168, test_pack_nodata_kwarg_3168.py), band-subset per-band SCALE/OFFSET rewrite (#3161, test_pack_band_subset_3161.py). Live probe on this CUDA host: all six gpu/dask+gpu legs pass today (no source bug, pure coverage gap). Added one gpu/dask-gpu parametrized round-trip test per file (6 tests, requires_gpu, RUN+passing locally) and fixed two stale docstrings claiming unpack/pack is CPU-only (wrong since #3075/#3240). Verified NOT gaps this pass: #3128 int64 sentinel tests cover eager+dask+gpu; #3241 streaming writer landed with byte-identical band-first/band-last/BytesIO/small-buffer tests; #3104 scale-zero rejection has gpu legs; #3169 revived the dead compression-corpus oracle gate. Out of scope: coregister=True lives in accessor.py (excluded module); its multi-band + polar gaps are documented as experimental caveats in docs/source/reference/geotiff.rst (#3248). LOW (carried, documented not fixed): Inf as the declared nodata sentinel never tested. || PREVIOUS: Pass 21 (2026-06-09, deep-sweep test-coverage): filed #3114 (tests) + #3112 (source bug). Cat 1 HIGH: to_geotiff(pack=True) round-trip was tested only on numpy and dask+numpy (write/test_pack_3064.py); #3075 made unpack=True work on gpu and dask+gpu reads, but no test packed a GPU-read array back. Live probe on this CUDA host: BOTH GPU legs crash today -- eager gpu raises AttributeError (cupy has no astype, the known cupy 13.6/xarray 2025.12 where/astype incompat) and dask+gpu raises TypeError (numpy fill value inside cupy.where) -- both from _pack's out.fillna(nodata) in _attrs.py; _writers/gpu.py says the pre-dispatch re-pack is supposed to make every write path work. Source bug filed as #3112; test-only PR adds test_pack_round_trip_gpu (gpu + dask-gpu params, requires_gpu, xfail(strict=True) on #3112 so the fix flips them loudly) and fixes the stale module docstring claiming GPU rejects mask_and_scale. Ran on CUDA host: 13 passed, 2 xfailed. Verified NOT gaps this pass (probed before flagging): empty/zero-band writer guard is covered (test_basic.py 2075/2095 blocks incl. gpu + dask + streaming entry points); degenerate shapes covered on all 4 read backends (read/test_degenerate_shapes.py); overview_resampling all 7 modes parametrized; missing_sources raise/warn + invalid, band_nodata first/invalid, unpack on all 4 backends, masked/parse_coordinates/lock/cache/default_name/name-deprecation all exercised; attrs contract per-backend (attrs/test_contract.py). LOW (documented, not fixed): Inf as the declared nodata sentinel is never tested (only one nan+inf data round-trip in test_edge_cases.py). || PREVIOUS: Pass 20 (2026-06-06, deep-sweep test-coverage): filed #2984 and added test_writer.py degenerate-shape GPU write coverage (Cat 1 backend + Cat 3 geometric edge). Read side already covers 1x1/1xN/Nx1 on all 4 backends (read/test_degenerate_shapes.py) and the dask streaming writer covers them (integration/test_dask_pipeline.py); the GPU write path was the gap (smallest shape in gpu/test_writer.py was 2x2). Added test_write_geotiff_gpu_degenerate_round_trip (1x1/1xN/Nx1 x none/deflate) + test_to_geotiff_dask_gpu_degenerate_round_trip (dask+cupy via gpu=True). 9 new tests RUN+passing on a CUDA host. Verified paths work first (not a source bug); transform supplied explicitly via attrs. Wider tree audit (~92k test LOC vs ~33k source): rioxarray-compat (#2961), bbox NaN/Inf/rotated, 8-backend parity matrix, codec round-trips already covered -- no other real gaps. | Pass (2026-06-05 test-coverage sweep): mature module (~31k src / ~124k test LOC, 9 test dirs). Exhaustive existing coverage -- parity/test_backend_matrix.py runs all 4 backends + VRT + HTTP + fsspec; golden_corpus full-manifest parity; read_rioxarray_compat_2961 covers masked/mask_and_scale/parse_coordinates/default_name on eager+dask. Cat1+Cat3 gap found (MEDIUM): degenerate-shape READS (1x1/1xN/Nx1) were tested only on the eager numpy reader (test_edge_cases.py) and the dask streaming WRITE path (integration/test_dask_pipeline.py); the windowed dask READ (chunks=) and GPU READ (gpu=True) on a single-pixel dimension were never exercised (smallest dask-read source in read/test_tiling is 8x8/2x32, parity fixtures 32x32/64x64). Probed: paths work today, no source bug -- pure coverage gap. Added read/test_degenerate_shapes.py (18 tests): dask read x{chunks 1,3,4} x{1x1,1xN,Nx1} + coord/transform/crs parity + GPU read + dask+gpu read. GPU cells RAN and PASSED on this CUDA host (grid-size-1 launch validated). Fixture supplies explicit attrs['transform'] (writer cannot infer pixel size from a 1-element coord axis). Branch deep-sweep-test-coverage-geotiff-degenerate-read-01. NOTE: pre-existing union-merge CRLF/duplicate-record corruption in this CSV left untouched -- appended one clean record; DictReader last-write-wins picks this one."
Expand Down
109 changes: 109 additions & 0 deletions xrspatial/tests/test_fire.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,3 +894,112 @@ def test_dask_numpy_dtype_matches_numpy(call):
assert dk_r.dtype == np_r.dtype
# and must match what the graph actually computes
assert dk_r.data.compute().dtype == np_r.dtype


# ---------------------------------------------------------------------------
# Inf / -Inf handling (Cat 2 edge-case coverage)
# ---------------------------------------------------------------------------
# None of these kernels guard against non-finite-but-not-NaN inputs: the only
# skip test is ``v != v`` (NaN), so +Inf / -Inf flow through the arithmetic.
# The resulting contract is well defined and identical on all four backends.
# These tests lock that contract so a later change to the finite-value guard
# (e.g. swapping ``v != v`` for ``not isfinite(v)``) cannot change behavior
# silently.
_INF = np.float32(np.inf)


def _dnbr_inf_call(backend):
# pre=+inf,-inf,finite,+inf ; post=finite,finite,+inf,+inf
pre = create_test_raster(
np.array([[_INF, -_INF, 0.5, _INF]], dtype='f4'), backend)
post = create_test_raster(
np.array([[0.1, 0.1, _INF, _INF]], dtype='f4'), backend)
return dnbr(pre, post)


def _rdnbr_inf_call(backend):
d = create_test_raster(np.array([[_INF, -_INF, 0.5]], dtype='f4'), backend)
p = create_test_raster(np.array([[500.0, 500.0, _INF]], dtype='f4'), backend)
return rdnbr(d, p)


def _bsc_inf_call(backend):
v = create_test_raster(np.array([[_INF, -_INF]], dtype='f4'), backend)
return burn_severity_class(v)


def _fli_inf_call(backend):
fuel = create_test_raster(np.array([[_INF, 2.0, _INF]], dtype='f4'), backend)
spread = create_test_raster(
np.array([[0.1, _INF, _INF]], dtype='f4'), backend)
return fireline_intensity(fuel, spread)


def _fl_inf_call(backend):
v = create_test_raster(np.array([[_INF, -_INF]], dtype='f4'), backend)
return flame_length(v)


def _ros_inf_call(backend):
slope = create_test_raster(np.array([[_INF, 10.0, 10.0]], dtype='f4'), backend)
wind = create_test_raster(np.array([[10.0, _INF, 10.0]], dtype='f4'), backend)
moist = create_test_raster(
np.array([[0.06, 0.06, _INF]], dtype='f4'), backend)
return rate_of_spread(slope, wind, moist)


def _kbdi_inf_call(backend):
prev = create_test_raster(np.array([[_INF, 100.0, 100.0]], dtype='f4'), backend)
temp = create_test_raster(np.array([[30.0, _INF, 30.0]], dtype='f4'), backend)
precip = create_test_raster(np.array([[0.0, 0.0, _INF]], dtype='f4'), backend)
return kbdi(prev, temp, precip, annual_precip=1500.0)


_INF_CONTRACTS = [
# (call_builder, expected_numpy_output)
(_dnbr_inf_call, np.array([[np.inf, -np.inf, -np.inf, np.nan]], dtype='f4')),
(_rdnbr_inf_call, np.array([[np.inf, -np.inf, 0.0]], dtype='f4')),
(_bsc_inf_call, np.array([[7, 1]], dtype=np.int8)),
(_fli_inf_call, np.array([[np.inf, np.inf, np.inf]], dtype='f4')),
(_fl_inf_call, np.array([[np.inf, 0.0]], dtype='f4')),
(_ros_inf_call, np.array([[np.nan, np.inf, np.nan]], dtype='f4')),
# kbdi: prev=inf and temp=inf both saturate the 800 clamp; precip=inf
# zeroes the deficit, which then re-accumulates the drought factor to
# ~43.96 at temp=30, annual_precip=1500.
(_kbdi_inf_call, np.array([[800.0, 800.0, 43.95906]], dtype='f4')),
]


@pytest.mark.parametrize("call,expected", _INF_CONTRACTS)
def test_inf_contract_numpy(call, expected):
result = call('numpy')
np.testing.assert_allclose(result.data, expected, rtol=1e-5,
equal_nan=True)


@dask_array_available
@pytest.mark.parametrize("call", [c for c, _ in _INF_CONTRACTS])
def test_inf_numpy_equals_dask(call):
np_r = call('numpy')
dk_r = call('dask+numpy')
np.testing.assert_allclose(dk_r.data.compute(), np_r.data,
rtol=1e-5, equal_nan=True)


@cuda_and_cupy_available
@pytest.mark.parametrize("call", [c for c, _ in _INF_CONTRACTS])
def test_inf_numpy_equals_cupy(call):
np_r = call('numpy')
cp_r = call('cupy')
np.testing.assert_allclose(cp_r.data.get(), np_r.data,
rtol=1e-5, equal_nan=True)


@dask_array_available
@cuda_and_cupy_available
@pytest.mark.parametrize("call", [c for c, _ in _INF_CONTRACTS])
def test_inf_numpy_equals_dask_cupy(call):
np_r = call('numpy')
dc_r = call('dask+cupy')
np.testing.assert_allclose(dc_r.data.compute().get(), np_r.data,
rtol=1e-5, equal_nan=True)
Loading