Skip to content

Commit 4e3abcd

Browse files
authored
rasterize: validate resolution and chunks (#2066) (#2070)
* 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 * 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.
1 parent e531b1b commit 4e3abcd

2 files changed

Lines changed: 41 additions & 6 deletions

File tree

xrspatial/rasterize.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,12 @@ def _normalize_chunks(chunks, height, width):
15171517
else:
15181518
rchunk, cchunk = chunks
15191519

1520+
# Both axes must have positive chunk sizes. A zero would loop forever
1521+
# below; a negative would diverge.
1522+
if rchunk <= 0 or cchunk <= 0:
1523+
raise ValueError(
1524+
f"chunks must be positive, got ({rchunk}, {cchunk})")
1525+
15201526
row_chunks = []
15211527
remaining = height
15221528
while remaining > 0:
@@ -2104,9 +2110,9 @@ def rasterize(
21042110
name : str, default 'rasterize'
21052111
Name for the output DataArray.
21062112
resolution : float or (x_res, y_res), optional
2107-
Pixel size. When given with ``bounds``, computes ``width`` and
2108-
``height`` automatically. A single float uses the same
2109-
resolution for both axes.
2113+
Pixel size. Must be finite and ``> 0``. When given with
2114+
``bounds``, computes ``width`` and ``height`` automatically.
2115+
A single float uses the same resolution for both axes.
21102116
like : xr.DataArray, optional
21112117
Template raster. Width, height, bounds, and dtype are copied
21122118
from this array (any can still be overridden explicitly).
@@ -2135,9 +2141,9 @@ def rasterize(
21352141
21362142
chunks : int or (int, int), optional
21372143
If given, use the dask backend and split the output raster into
2138-
tiles of this size ``(row_chunk, col_chunk)``. A single int
2139-
uses the same chunk size for both axes. Combined with
2140-
``use_cuda`` to select dask+numpy vs dask+cupy.
2144+
tiles of this size ``(row_chunk, col_chunk)``. Both axes must be
2145+
``> 0``. A single int uses the same chunk size for both axes.
2146+
Combined with ``use_cuda`` to select dask+numpy vs dask+cupy.
21412147
max_pixels : int, default 1_000_000_000
21422148
Safety cap on the resolved output size (``width * height``). The
21432149
function raises ``ValueError`` before any host or device
@@ -2237,6 +2243,13 @@ def rasterize(
22372243
x_res = y_res = float(resolution)
22382244
else:
22392245
x_res, y_res = float(resolution[0]), float(resolution[1])
2246+
# Reject non-finite or non-positive resolution before dimension math.
2247+
# Without this, inf/-1 quietly produce a 1x1 raster, 0 raises an
2248+
# opaque ZeroDivisionError, and nan raises an int-conversion error.
2249+
for r in (x_res, y_res):
2250+
if not np.isfinite(r) or r <= 0:
2251+
raise ValueError(
2252+
f"resolution must be finite and > 0, got {resolution!r}")
22402253
final_width = max(int(np.ceil((xmax - xmin) / x_res)), 1)
22412254
final_height = max(int(np.ceil((ymax - ymin) / y_res)), 1)
22422255
elif like_width is not None:

xrspatial/tests/test_rasterize.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,28 @@ def test_max_pixels_one_over_rejected(self):
342342
width=101, height=100, bounds=(0, 0, 1, 1),
343343
max_pixels=10_000)
344344

345+
@pytest.mark.parametrize("bad", [0, -1, -0.5, float('inf'),
346+
-float('inf'), float('nan')])
347+
def test_invalid_resolution_scalar(self, bad):
348+
with pytest.raises(ValueError, match="resolution must be finite"):
349+
rasterize([(box(0, 0, 1, 1), 1.0)],
350+
resolution=bad, bounds=(0, 0, 1, 1))
351+
352+
@pytest.mark.parametrize("bad", [(0, 1), (1, 0), (-1, 1), (1, float('nan')),
353+
(float('inf'), 1)])
354+
def test_invalid_resolution_tuple(self, bad):
355+
with pytest.raises(ValueError, match="resolution must be finite"):
356+
rasterize([(box(0, 0, 1, 1), 1.0)],
357+
resolution=bad, bounds=(0, 0, 1, 1))
358+
359+
@pytest.mark.parametrize("bad", [0, -1, (0, 1), (1, -1)])
360+
def test_invalid_chunks(self, bad):
361+
# chunks=0 used to hang (issue #2066); negative diverged.
362+
with pytest.raises(ValueError, match="chunks must be positive"):
363+
rasterize([(box(0, 0, 1, 1), 1.0)],
364+
width=10, height=10, bounds=(0, 0, 1, 1),
365+
chunks=bad)
366+
345367

346368
# ---------------------------------------------------------------------------
347369
# all_touched mode

0 commit comments

Comments
 (0)