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. diff --git a/xrspatial/tests/test_contour.py b/xrspatial/tests/test_contour.py index 30c57b57..18b06787 100644 --- a/xrspatial/tests/test_contour.py +++ b/xrspatial/tests/test_contour.py @@ -538,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') @@ -558,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]) @@ -573,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)