From 7fade01a34a4b6aee9ec90fc6d4bcf13cfdcc9e5 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Mon, 18 May 2026 13:53:33 -0700 Subject: [PATCH 1/2] rasterize: reject NaN bounds from empty geometries (#2065) Three related fixes: 1. _parse_input returns None for inferred bounds when GeoPandas total_bounds is non-finite (empty GeoDataFrame). 2. Inferred-bounds calculation filters out geometries whose bbox is non-finite so a single empty Polygon doesn't poison the extent. 3. Explicit non-finite check on resolved bounds with a clear error message before the existing xmin 0: - final_bounds = (geom_bboxes[:, 0].min(), - geom_bboxes[:, 1].min(), - geom_bboxes[:, 2].max(), - geom_bboxes[:, 3].max()) + finite = np.all(np.isfinite(geom_bboxes), axis=1) + geom_bboxes = geom_bboxes[finite] + if len(geom_bboxes) > 0: + final_bounds = (geom_bboxes[:, 0].min(), + geom_bboxes[:, 1].min(), + geom_bboxes[:, 2].max(), + geom_bboxes[:, 3].max()) if final_bounds is None: raise ValueError( "bounds must be provided when geometries are empty or have " "no spatial extent") xmin, ymin, xmax, ymax = final_bounds + if not all(np.isfinite(v) for v in (xmin, ymin, xmax, ymax)): + raise ValueError( + f"Invalid bounds: all of (xmin, ymin, xmax, ymax) must be " + f"finite, got {(xmin, ymin, xmax, ymax)!r}") if xmin >= xmax or ymin >= ymax: raise ValueError( f"Invalid bounds: xmin ({xmin}) must be < xmax ({xmax}) and " diff --git a/xrspatial/tests/test_rasterize.py b/xrspatial/tests/test_rasterize.py index 20d3a33b8..5f4cd0aad 100644 --- a/xrspatial/tests/test_rasterize.py +++ b/xrspatial/tests/test_rasterize.py @@ -364,6 +364,38 @@ def test_invalid_chunks(self, bad): width=10, height=10, bounds=(0, 0, 1, 1), chunks=bad) + def test_empty_geometry_does_not_poison_inferred_bounds(self): + # Issue #2065: an empty geometry returns nan from .bounds; the + # caller used .min()/.max() unfiltered, so a single empty geom + # poisoned the inferred extent and produced a raster with nan + # x/y coords. Drop the empty and infer from the rest. + empty = Polygon() + result = rasterize( + [(empty, 99), (box(0, 0, 1, 1), 1)], + width=2, height=2) + assert np.all(np.isfinite(result.x.values)) + assert np.all(np.isfinite(result.y.values)) + assert np.all(result.values == 1) + + def test_only_empty_geometry_requires_explicit_bounds(self): + empty = Polygon() + with pytest.raises(ValueError, match="bounds must be provided"): + rasterize([(empty, 1.0)], width=2, height=2) + + def test_empty_geodataframe_requires_explicit_bounds(self): + # Issue #2065: total_bounds on an empty frame is (nan,nan,nan,nan). + # That used to bypass the empty-bounds guard. + import geopandas as gpd + empty_gdf = gpd.GeoDataFrame( + {'value': []}, geometry=gpd.GeoSeries([])) + with pytest.raises(ValueError, match="bounds must be provided"): + rasterize(empty_gdf, width=2, height=2, column='value') + + def test_explicit_nan_bounds_rejected(self): + with pytest.raises(ValueError, match="must be finite"): + rasterize([(box(0, 0, 1, 1), 1.0)], width=2, height=2, + bounds=(0, 0, float('nan'), 1)) + # --------------------------------------------------------------------------- # all_touched mode From e5e60ae6c5af8e560688fdb7203f3e7776a129ea Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Mon, 18 May 2026 14:12:11 -0700 Subject: [PATCH 2/2] rasterize: add MultiPolygon empty-bounds regression test (#2065) Address review nit: extend coverage to empty MultiPolygon. The existing fix in _geometry_bboxes / inferred-bounds filtering catches it (shapely returns the same (nan,nan,nan,nan) for an empty MultiPolygon as for an empty Polygon), but a dedicated test locks that in. --- xrspatial/tests/test_rasterize.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/xrspatial/tests/test_rasterize.py b/xrspatial/tests/test_rasterize.py index 5f4cd0aad..78a993ba7 100644 --- a/xrspatial/tests/test_rasterize.py +++ b/xrspatial/tests/test_rasterize.py @@ -396,6 +396,17 @@ def test_explicit_nan_bounds_rejected(self): rasterize([(box(0, 0, 1, 1), 1.0)], width=2, height=2, bounds=(0, 0, float('nan'), 1)) + def test_empty_multipolygon_filtered_from_inferred_bounds(self): + # Same shape as the empty-Polygon case but exercises the + # MultiPolygon branch in shapely's is_empty / bounds path. + empty_mp = MultiPolygon() + result = rasterize( + [(empty_mp, 99), (box(0, 0, 1, 1), 1)], + width=2, height=2) + assert np.all(np.isfinite(result.x.values)) + assert np.all(np.isfinite(result.y.values)) + assert np.all(result.values == 1) + # --------------------------------------------------------------------------- # all_touched mode