From 4b682dc6de48e0e379b9415b38c0e255823da0ba Mon Sep 17 00:00:00 2001 From: Melissari1997 Date: Sat, 30 May 2026 16:23:59 +0200 Subject: [PATCH 1/3] test(contour): add regression tests for inf-handling (#2704) --- xrspatial/tests/test_contour.py | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/xrspatial/tests/test_contour.py b/xrspatial/tests/test_contour.py index 30c57b57..71b05887 100644 --- a/xrspatial/tests/test_contour.py +++ b/xrspatial/tests/test_contour.py @@ -132,6 +132,65 @@ def test_partial_nan(self): assert np.all(coords[:, 0] < nan_row_y + 1e-10) +# --------------------------------------------------------------------------- +# Inf handling +# --------------------------------------------------------------------------- + +class TestInfHandling: + + def test_pos_inf_center(self): + """A quad touching +inf must not produce NaN coordinates.""" + data = np.array([ + [0., 0., 0., 0., 0.], + [0., 1., 1., 1., 0.], + [0., 1., np.inf, 1., 0.], + [0., 1., 1., 1., 0.], + [0., 0., 0., 0., 0.], + ], dtype=np.float64) + agg = create_test_raster(data, backend='numpy') + result = contours(agg, levels=[1.5]) + for level, coords in result: + assert np.isfinite(coords).all(), \ + f"Non-finite coordinates found at level {level}" + + def test_neg_inf_center(self): + """A quad touching -inf must not produce NaN coordinates.""" + data = np.array([ + [0., 0., 0., 0., 0.], + [0., 1., 1., 1., 0.], + [0., 1., -np.inf, 1., 0.], + [0., 1., 1., 1., 0.], + [0., 0., 0., 0., 0.], + ], dtype=np.float64) + agg = create_test_raster(data, backend='numpy') + result = contours(agg, levels=[1.5]) + for level, coords in result: + assert np.isfinite(coords).all(), \ + f"Non-finite coordinates found at level {level}" + + def test_mixed_inf(self): + """Multiple infinities of opposite signs must not produce NaN.""" + data = np.array([ + [0., 0., 0., 0., 0.], + [0., np.inf, 1., -np.inf, 0.], + [0., 1., 1., 1., 0.], + [0., -np.inf, 1., np.inf, 0.], + [0., 0., 0., 0., 0.], + ], dtype=np.float64) + agg = create_test_raster(data, backend='numpy') + result = contours(agg, levels=[0.5]) + for level, coords in result: + assert np.isfinite(coords).all(), \ + f"Non-finite coordinates found at level {level}" + + def test_all_inf_quad(self): + """A 2x2 raster with all corners infinite produces no contours.""" + data = np.full((2, 2), np.inf, dtype=np.float64) + agg = create_test_raster(data, backend='numpy') + result = contours(agg, levels=[1.0]) + assert result == [] + + # --------------------------------------------------------------------------- # Edge cases # --------------------------------------------------------------------------- From 12a7f27120cb0b9272f46ddb14fa7f5a143eb09e Mon Sep 17 00:00:00 2001 From: Melissari1997 Date: Sat, 30 May 2026 16:24:03 +0200 Subject: [PATCH 2/3] fix(contour): skip quads with non-finite corners in marching squares (#2704) --- xrspatial/contour.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xrspatial/contour.py b/xrspatial/contour.py index 27f7c8ca..d1e8489a 100644 --- a/xrspatial/contour.py +++ b/xrspatial/contour.py @@ -66,8 +66,9 @@ def _marching_squares_kernel(data, level, seg_rows, seg_cols, seg_count): bl = data[r + 1, c] br = data[r + 1, c + 1] - # Skip quads with any NaN corner. - if tl != tl or tr != tr or bl != bl or br != br: + # Skip quads with any non-finite corner (NaN or +/-inf). + if not (np.isfinite(tl) and np.isfinite(tr) and + np.isfinite(bl) and np.isfinite(br)): continue # Build 4-bit case index. From e11043e8a5e38b1a7a44d17683b19b7e8f6505b0 Mon Sep 17 00:00:00 2001 From: Melissari1997 Date: Sun, 31 May 2026 15:14:47 +0200 Subject: [PATCH 3/3] test(contour): fix duplicate TestInfHandling class after rebase (#2704) After rebasing onto the upstream commit that introduced TestInfHandling with xfail(strict=True) markers, the branch ended up with two classes of the same name in test_contour.py. Python silently shadows the first definition, so pytest only collected the second (upstream) class, which still carried the xfail markers. The now-fixed np.isfinite guard caused those tests to pass, producing XPASS(strict) CI failures on macOS/3.14. - Remove the duplicate TestInfHandling block introduced by the rebase - Drop the xfail(strict=True) decorators from test_inf_corner_no_nan_coords --- xrspatial/tests/test_contour.py | 103 +++++++++----------------------- 1 file changed, 28 insertions(+), 75 deletions(-) diff --git a/xrspatial/tests/test_contour.py b/xrspatial/tests/test_contour.py index 71b05887..18b06787 100644 --- a/xrspatial/tests/test_contour.py +++ b/xrspatial/tests/test_contour.py @@ -132,65 +132,6 @@ def test_partial_nan(self): assert np.all(coords[:, 0] < nan_row_y + 1e-10) -# --------------------------------------------------------------------------- -# Inf handling -# --------------------------------------------------------------------------- - -class TestInfHandling: - - def test_pos_inf_center(self): - """A quad touching +inf must not produce NaN coordinates.""" - data = np.array([ - [0., 0., 0., 0., 0.], - [0., 1., 1., 1., 0.], - [0., 1., np.inf, 1., 0.], - [0., 1., 1., 1., 0.], - [0., 0., 0., 0., 0.], - ], dtype=np.float64) - agg = create_test_raster(data, backend='numpy') - result = contours(agg, levels=[1.5]) - for level, coords in result: - assert np.isfinite(coords).all(), \ - f"Non-finite coordinates found at level {level}" - - def test_neg_inf_center(self): - """A quad touching -inf must not produce NaN coordinates.""" - data = np.array([ - [0., 0., 0., 0., 0.], - [0., 1., 1., 1., 0.], - [0., 1., -np.inf, 1., 0.], - [0., 1., 1., 1., 0.], - [0., 0., 0., 0., 0.], - ], dtype=np.float64) - agg = create_test_raster(data, backend='numpy') - result = contours(agg, levels=[1.5]) - for level, coords in result: - assert np.isfinite(coords).all(), \ - f"Non-finite coordinates found at level {level}" - - def test_mixed_inf(self): - """Multiple infinities of opposite signs must not produce NaN.""" - data = np.array([ - [0., 0., 0., 0., 0.], - [0., np.inf, 1., -np.inf, 0.], - [0., 1., 1., 1., 0.], - [0., -np.inf, 1., np.inf, 0.], - [0., 0., 0., 0., 0.], - ], dtype=np.float64) - agg = create_test_raster(data, backend='numpy') - result = contours(agg, levels=[0.5]) - for level, coords in result: - assert np.isfinite(coords).all(), \ - f"Non-finite coordinates found at level {level}" - - def test_all_inf_quad(self): - """A 2x2 raster with all corners infinite produces no contours.""" - data = np.full((2, 2), np.inf, dtype=np.float64) - agg = create_test_raster(data, backend='numpy') - result = contours(agg, levels=[1.0]) - assert result == [] - - # --------------------------------------------------------------------------- # Edge cases # --------------------------------------------------------------------------- @@ -597,18 +538,11 @@ def test_inf_far_level_no_crossing(self): assert np.isfinite(coords).all(), ( "level 0.5 ring should not include the inf quad") - @pytest.mark.xfail( - reason="contours() emits NaN coordinates near an inf corner; " - "see https://github.com/xarray-contrib/xarray-spatial/" - "issues/2704", - strict=True, - ) def test_inf_corner_no_nan_coords(self): """A finite level near a +inf cell must not leak NaN coordinates. - The NaN-skip guard in the kernel uses ``x != x`` which does not - catch infinity, so the inf quad is interpolated and produces NaN. - Tracked as a source bug in #2704. + Regression for #2704: the NaN-skip guard in the kernel used ``x != x`` + which does not catch infinity; fixed by using ``np.isfinite``. """ data = self._inf_peak(np.inf) agg = create_test_raster(data, backend='numpy') @@ -617,14 +551,11 @@ def test_inf_corner_no_nan_coords(self): assert np.isfinite(coords).all(), ( f"non-finite coordinate in contour at level {level}: {coords}") - @pytest.mark.xfail( - reason="contours() emits NaN coordinates near a -inf corner; " - "see https://github.com/xarray-contrib/xarray-spatial/" - "issues/2704", - strict=True, - ) def test_neg_inf_corner_no_nan_coords(self): - """Same NaN leak for a -inf corner. Tracked in #2704.""" + """A finite level near a -inf cell must not leak NaN coordinates. + + Regression for #2704: same fix as test_inf_corner_no_nan_coords. + """ data = self._inf_peak(-np.inf) agg = create_test_raster(data, backend='numpy') result = contours(agg, levels=[0.5]) @@ -632,6 +563,28 @@ def test_neg_inf_corner_no_nan_coords(self): assert np.isfinite(coords).all(), ( f"non-finite coordinate in contour at level {level}: {coords}") + def test_mixed_inf(self): + """Multiple infinities of opposite signs must not produce NaN.""" + data = np.array([ + [0., 0., 0., 0., 0.], + [0., np.inf, 1., -np.inf, 0.], + [0., 1., 1., 1., 0.], + [0., -np.inf, 1., np.inf, 0.], + [0., 0., 0., 0., 0.], + ], dtype=np.float64) + agg = create_test_raster(data, backend='numpy') + result = contours(agg, levels=[0.5]) + for level, coords in result: + assert np.isfinite(coords).all(), \ + f"Non-finite coordinates found at level {level}" + + def test_all_inf_quad(self): + """A 2x2 raster with all corners infinite produces no contours.""" + data = np.full((2, 2), np.inf, dtype=np.float64) + agg = create_test_raster(data, backend='numpy') + result = contours(agg, levels=[1.0]) + assert result == [] + # --------------------------------------------------------------------------- # CRS propagation to GeoDataFrame output (#2704 audit, Cat 5)