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
@@ -1,5 +1,5 @@
module,last_inspected,issue,severity_max,categories_found,notes
aspect,2026-06-02,2742;2829,HIGH,3;4,"#2742: degenerate shapes (1x1/Nx1/1xN) + geodesic boundary modes; tests added all 4 backends, GPU-validated. #2829: northness/eastness method='geodesic' branch was untested (planar only); added correctness (diagonal surface where planar!=geodesic) + 4-backend parity, GPU-validated. all-NaN planar/geodesic returns all-NaN (correct). Inf input -> silent -1/flat on spike cell: possible source bug, out of scope for test-only sweep, not filed. Dedup: rectangular-cell oracle #2781 + cell-size #2780 already merged, not duplicated."
aspect,2026-06-21,3439,MEDIUM,2,"#3439 (Cat 2): Inf/-Inf elevation input and all-NaN raster were untested. Added Inf-input finite-neighbors + 4-backend parity, and all-NaN -> all-NaN shape-preserved for aspect/northness/eastness on all 4 backends; GPU-validated (CUDA available). Both behaviors defined and consistent across backends -> coverage gap, not a bug (supersedes prior 'Inf possible source bug, not filed' note). Prior: #2742 degenerate shapes + geodesic boundary modes; #2829 northness/eastness geodesic branch -- all still covered."
classify,2026-06-20,,MEDIUM,3;4,"Deep-sweep 2026-06-20 test-coverage on a CUDA host. Backend matrix was already complete: all 10 public classifiers (binary/reclassify/quantile/natural_breaks/equal_interval/std_mean/head_tail_breaks/percentiles/maximum_breaks/box_plot) x 4 backends present and green (Cat 1 no gap). Cat 2 (NaN/Inf) covered: input_data() fixture embeds -inf/nan/+inf at corners on every numpy/dask/cupy test, plus all-nan/all-inf/all-same dedicated tests. Cat 5 covered via general_output_checks(verify_attrs=True) asserting attrs/dims/coords on every backend test. Found two MEDIUM gaps: Cat 3 (no 1x1 single-pixel, no Nx1/1xN strip tests for any classifier) and Cat 4 (the _validate_scalar k<min_val guards were never exercised: quantile/natural_breaks/maximum_breaks k>=2, equal_interval k>=1). Probed live: single-pixel and strip shapes all work correctly (no source bug -- lone finite pixel -> class 0; binary match -> 1; reclassify -> bin's new_value), so these are untested-but-passing paths. Added test-only (numpy, since the gaps are in shared CPU-side validation + bin-edge logic and the 4-backend dispatch is already fully covered): test_classify_single_pixel, test_binary_single_pixel, test_reclassify_single_pixel, test_classify_nx1_strip, test_classify_1xn_strip, and 4 k-below-min ValueError tests. All RAN and PASSED on the CUDA host; full file 98 passed. classify.py untouched. LOW (documented, not fixed): empty raster (0 rows) raises on numpy equal_interval (zero-size reduction) and differs across backends -- degenerate input, not pinned. Non-square cellsize not exercised (classifiers ignore cellsize, so out of scope)."
contour,2026-06-08,2704;2710;3044,MEDIUM,1;2,"Pass 2 (2026-06-08, deep-sweep test-coverage): re-swept on a CUDA host. Verified issue #2704 is CLOSED (fixed 2026-06-01 via #2749, kernel now uses np.isfinite at contour.py:73-74); the two prior xfail(strict) #2704 pins were already flipped to plain passing assertions in TestInfHandling (test_inf_corner_no_nan_coords / test_neg_inf_corner_no_nan_coords) -- no stale xfail remained. Found one MEDIUM Cat 1+2 gap: no cross-backend parity test fed NaN input -- TestBackendEquivalence uses elevation_raster_no_nans and test_partial_nan is numpy-only, so numpy-interior NaN-skip vs dask NaN-halo (da.overlap) parity at chunk edges was unpinned. Filed #3044, added TestNaNBackendParity (3 tests: dask, cupy, dask+cupy each assert _segments_by_level equality vs numpy on a partial-NaN ramp with a NaN edge row + interior NaN cell inside a non-edge chunk). All 3 RAN and PASSED on a CUDA host; full file 89 passed, 0 skipped. Probed and verified now-resolved: the prior-pass LOW items are no longer real gaps -- the levels=None all-NaN early-return IS asserted (result==[]) on all 4 backends (numpy L148/dask L163/cupy L178/dask+cupy L194) plus geopandas (test_geopandas_all_nan_keeps_crs). LOW (documented, NOT fixed): non-square cellsize (res[0]!=res[1]) still never exercised -- all tests use create_test_raster res (0.5,0.5); probed live that anisotropic coords transform correctly (y scaled 2.0, x scaled 0.5 -> crossing x=1.25, y spans 0..8), works, so it is a LOW coverage gap not a bug. Cat 3 1x1/Nx1/1xN remain rejected by the >=2x2 guard (tested). Test-only PR for #3044; contour.py untouched. | Pass 1 (2026-05-29): added TestInfHandling, TestCRSPropagation, TestNonDefaultDims to test_contour.py (5 passed + 2 strict-xfail on a CUDA host; full file 29 passed, 2 xfailed). All four backends (numpy / cupy / dask+numpy / dask+cupy) were already exercised with cross-backend segment-equality assertions (TestBackendEquivalence), and ran green locally on the CUDA host -- Cat 1 well covered, no new backend tests needed. Cat 2 HIGH (Inf): the marching-squares NaN-skip guard at contour.py:67 uses x!=x which does not catch infinity, so a finite level near a +/-inf corner leaks NaN coordinates into the output. Filed source bug #2704 and added two xfail(strict=True) tests pinning it (+inf and -inf) plus test_inf_far_level_no_crossing covering the safe path where the inf quad classifies as all-above (idx 15) and is skipped before any interpolation. Cat 5 MEDIUM: no test asserted gdf.crs propagation from agg.attrs['crs'] (contour.py:660) -- added test_geopandas_crs_from_attrs (to_epsg()==5070) + test_geopandas_no_crs_attr. Cat 5 MEDIUM: the index-to-coordinate transform (contour.py:644-654) reads agg.dims[0]/[1] coords but no test used non-y/x dims -- added test_lat_lon_dims_coordinate_transform + test_lat_lon_matches_yx_equivalent. PR #2710 (test-only, source untouched). LOW (documented, not fixed): non-square cellsize (cellsize_x != cellsize_y) never exercised -- all tests use res (0.5,0.5); levels=None early-return on all-NaN/all-equal works (probed) but only the explicit-levels all-NaN path is asserted. Cat 3 1x1/Nx1/1xN are rejected by the >=2x2 validation guard and that rejection is already tested (test_too_small, test_minimum_raster)."
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)."
Expand Down
95 changes: 95 additions & 0 deletions xrspatial/tests/test_aspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,101 @@ def test_name_consistent_dask_numpy(name):
_assert_name_derived('dask+numpy', name)


# ---- Inf and all-NaN elevation input (issue #3439) ----
#
# The suite exercises NaN inputs (qgis fixture, derived-function propagation)
# but never an Inf cell or an entirely-NaN raster. Both run without error
# today and agree across the four backends; these pin that contract so a
# backend can't silently diverge on Inf or all-NaN input.

def _inf_raster():
data = np.ones((6, 7), dtype=np.float64) * 100.0
data[2, 3] = np.inf
data[3, 1] = -np.inf
return data


def test_inf_input_numpy_finite_neighbors():
"""An Inf cell does not crash; the planar interior is finite or -1/NaN,
never Inf."""
agg = create_test_raster(_inf_raster(), backend='numpy')
out = _to_numpy(aspect(agg))
assert not np.any(np.isinf(out))


@dask_array_available
def test_inf_input_numpy_equals_dask():
data = _inf_raster()
numpy_agg = create_test_raster(data, backend='numpy')
dask_agg = create_test_raster(data, backend='dask+numpy', chunks=(3, 4))
np_out = _to_numpy(aspect(numpy_agg))
da_out = _to_numpy(aspect(dask_agg))
np.testing.assert_allclose(np_out, da_out, equal_nan=True, rtol=1e-5)


@cuda_and_cupy_available
def test_inf_input_numpy_equals_cupy():
data = _inf_raster()
numpy_agg = create_test_raster(data, backend='numpy')
cupy_agg = create_test_raster(data, backend='cupy')
np_out = _to_numpy(aspect(numpy_agg))
cu_out = _to_numpy(aspect(cupy_agg))
np.testing.assert_allclose(np_out, cu_out, equal_nan=True, rtol=1e-5)


@dask_array_available
@cuda_and_cupy_available
def test_inf_input_numpy_equals_dask_cupy():
data = _inf_raster()
numpy_agg = create_test_raster(data, backend='numpy')
dask_cupy_agg = create_test_raster(data, backend='dask+cupy', chunks=(3, 4))
np_out = _to_numpy(aspect(numpy_agg))
dc_out = _to_numpy(aspect(dask_cupy_agg))
np.testing.assert_allclose(np_out, dc_out, equal_nan=True, rtol=1e-5)


@pytest.mark.parametrize("func", [aspect, northness, eastness])
def test_all_nan_input_numpy(func):
"""An entirely-NaN raster comes back all-NaN with the shape preserved."""
data = np.full((6, 7), np.nan, dtype=np.float64)
agg = create_test_raster(data, backend='numpy')
result = func(agg)
general_output_checks(agg, result)
assert result.shape == (6, 7)
assert np.all(np.isnan(result.data))


@dask_array_available
@pytest.mark.parametrize("func", [aspect, northness, eastness])
def test_all_nan_input_dask_numpy(func):
data = np.full((6, 7), np.nan, dtype=np.float64)
agg = create_test_raster(data, backend='dask+numpy', chunks=(3, 4))
result = func(agg)
assert result.shape == (6, 7)
assert np.all(np.isnan(_to_numpy(result)))


@cuda_and_cupy_available
@pytest.mark.parametrize("func", [aspect, northness, eastness])
def test_all_nan_input_cupy(func):
data = np.full((6, 7), np.nan, dtype=np.float64)
agg = create_test_raster(data, backend='cupy')
result = func(agg)
assert result.shape == (6, 7)
assert np.all(np.isnan(_to_numpy(result)))


@dask_array_available
@cuda_and_cupy_available
@pytest.mark.parametrize("func", [aspect, northness, eastness])
def test_all_nan_input_dask_cupy(func):
data = np.full((6, 7), np.nan, dtype=np.float64)
agg = create_test_raster(data, backend='dask+cupy', chunks=(3, 4))
result = func(agg)
assert result.shape == (6, 7)
assert np.all(np.isnan(_to_numpy(result)))


@cuda_and_cupy_available
@pytest.mark.parametrize("name", [None, 'aspect'])
def test_name_consistent_cupy(name):
Expand Down
Loading