From 5fd27cc020bf689388673b3b7eaa2b09cca8e6d0 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Mon, 18 May 2026 13:51:13 -0700 Subject: [PATCH 1/2] rasterize: validate resolution and chunks (#2066) resolution must be finite and > 0; reject before dimension math so inf/-1 no longer silently produce a 1x1 raster and 0/nan no longer fall through to opaque downstream errors. chunks must be positive; chunks=0 used to hang _normalize_chunks (the loop condition decremented by zero), negatives diverged. Closes #2066 --- xrspatial/rasterize.py | 13 +++++++++++++ xrspatial/tests/test_rasterize.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/xrspatial/rasterize.py b/xrspatial/rasterize.py index 9abdbbc61..5793fd076 100644 --- a/xrspatial/rasterize.py +++ b/xrspatial/rasterize.py @@ -1517,6 +1517,12 @@ def _normalize_chunks(chunks, height, width): else: rchunk, cchunk = chunks + # Both axes must have positive chunk sizes. A zero would loop forever + # below; a negative would diverge. + if rchunk <= 0 or cchunk <= 0: + raise ValueError( + f"chunks must be positive, got ({rchunk}, {cchunk})") + row_chunks = [] remaining = height while remaining > 0: @@ -2232,6 +2238,13 @@ def rasterize( x_res = y_res = float(resolution) else: x_res, y_res = float(resolution[0]), float(resolution[1]) + # Reject non-finite or non-positive resolution before dimension math. + # Without this, inf/-1 quietly produce a 1x1 raster, 0 raises an + # opaque ZeroDivisionError, and nan raises an int-conversion error. + for r in (x_res, y_res): + if not np.isfinite(r) or r <= 0: + raise ValueError( + f"resolution must be finite and > 0, got {resolution!r}") final_width = max(int(np.ceil((xmax - xmin) / x_res)), 1) final_height = max(int(np.ceil((ymax - ymin) / y_res)), 1) elif like_width is not None: diff --git a/xrspatial/tests/test_rasterize.py b/xrspatial/tests/test_rasterize.py index df89328a8..125e1961a 100644 --- a/xrspatial/tests/test_rasterize.py +++ b/xrspatial/tests/test_rasterize.py @@ -342,6 +342,28 @@ def test_max_pixels_one_over_rejected(self): width=101, height=100, bounds=(0, 0, 1, 1), max_pixels=10_000) + @pytest.mark.parametrize("bad", [0, -1, -0.5, float('inf'), + -float('inf'), float('nan')]) + def test_invalid_resolution_scalar(self, bad): + with pytest.raises(ValueError, match="resolution must be finite"): + rasterize([(box(0, 0, 1, 1), 1.0)], + resolution=bad, bounds=(0, 0, 1, 1)) + + @pytest.mark.parametrize("bad", [(0, 1), (1, 0), (-1, 1), (1, float('nan')), + (float('inf'), 1)]) + def test_invalid_resolution_tuple(self, bad): + with pytest.raises(ValueError, match="resolution must be finite"): + rasterize([(box(0, 0, 1, 1), 1.0)], + resolution=bad, bounds=(0, 0, 1, 1)) + + @pytest.mark.parametrize("bad", [0, -1, (0, 1), (1, -1)]) + def test_invalid_chunks(self, bad): + # chunks=0 used to hang (issue #2066); negative diverged. + with pytest.raises(ValueError, match="chunks must be positive"): + rasterize([(box(0, 0, 1, 1), 1.0)], + width=10, height=10, bounds=(0, 0, 1, 1), + chunks=bad) + # --------------------------------------------------------------------------- # all_touched mode From c4f876c8666da065f80993704849c17a326b547e Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Mon, 18 May 2026 14:13:26 -0700 Subject: [PATCH 2/2] rasterize: document resolution and chunks constraints (#2066) Docstring follow-up for the validation added in 5fd27cc: note that resolution must be finite and > 0, and chunks must be > 0. --- xrspatial/rasterize.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/xrspatial/rasterize.py b/xrspatial/rasterize.py index 5793fd076..33626d2b2 100644 --- a/xrspatial/rasterize.py +++ b/xrspatial/rasterize.py @@ -2105,9 +2105,9 @@ def rasterize( name : str, default 'rasterize' Name for the output DataArray. resolution : float or (x_res, y_res), optional - Pixel size. When given with ``bounds``, computes ``width`` and - ``height`` automatically. A single float uses the same - resolution for both axes. + Pixel size. Must be finite and ``> 0``. When given with + ``bounds``, computes ``width`` and ``height`` automatically. + A single float uses the same resolution for both axes. like : xr.DataArray, optional Template raster. Width, height, bounds, and dtype are copied from this array (any can still be overridden explicitly). @@ -2136,9 +2136,9 @@ def rasterize( chunks : int or (int, int), optional If given, use the dask backend and split the output raster into - tiles of this size ``(row_chunk, col_chunk)``. A single int - uses the same chunk size for both axes. Combined with - ``use_cuda`` to select dask+numpy vs dask+cupy. + tiles of this size ``(row_chunk, col_chunk)``. Both axes must be + ``> 0``. A single int uses the same chunk size for both axes. + Combined with ``use_cuda`` to select dask+numpy vs dask+cupy. max_pixels : int, default 1_000_000_000 Safety cap on the resolved output size (``width * height``). The function raises ``ValueError`` before any host or device