From babf66fad80fd0c53c392df1a7ba77a3c9d49bba Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Thu, 2 Jul 2026 00:33:45 -0400 Subject: [PATCH 1/3] Reject 0D/1D DataArray in to_geotiff with the clear rank ValueError A 0D/1D DataArray reached coords_to_transform (which reads dims[-2]) before the ndim check and raised an opaque IndexError from _coords.py, while a numpy array of the same rank and a 4D DataArray already got the clear 'Expected 2D or 3D array, got ND' ValueError. Hoist the check ahead of backend dispatch and georef resolution so the eager TIFF, .vrt, and GPU write paths all reject the same shapes identically. --- xrspatial/geotiff/_writers/eager.py | 13 +++++++++++++ xrspatial/geotiff/tests/test_edge_cases.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/xrspatial/geotiff/_writers/eager.py b/xrspatial/geotiff/_writers/eager.py index 0476eef8d..2c66ba3cb 100644 --- a/xrspatial/geotiff/_writers/eager.py +++ b/xrspatial/geotiff/_writers/eager.py @@ -839,6 +839,19 @@ def _write_sidecars(): _is_vrt_path = ( isinstance(path, str) and path.lower().endswith('.vrt')) + # Reject arrays that are not 2D or 3D before any backend dispatch or + # georef resolution. ``.ndim`` is defined on DataArray, numpy, cupy, + # and dask inputs. Without this, a 0D/1D *DataArray* reaches + # ``coords_to_transform`` (which indexes ``dims[-2]``) and dies with an + # opaque ``IndexError: tuple index out of range`` pointing at + # ``_coords.py`` internals, while a 0D/1D numpy array or a 4D + # DataArray already gets the clear message below. Running the check + # here up front makes every backend (eager, VRT, GPU) reject the same + # bad shapes identically. + _ndim = getattr(data, 'ndim', None) + if _ndim is not None and _ndim not in (2, 3): + raise ValueError(f"Expected 2D or 3D array, got {_ndim}D") + # Resolve GPU dispatch up front so the JPEG opt-in warning fires # exactly once. ``_write_geotiff_gpu`` emits its own warning on the # GPU path; emitting here as well would double-warn callers of diff --git a/xrspatial/geotiff/tests/test_edge_cases.py b/xrspatial/geotiff/tests/test_edge_cases.py index 624a0282d..ac81855e2 100644 --- a/xrspatial/geotiff/tests/test_edge_cases.py +++ b/xrspatial/geotiff/tests/test_edge_cases.py @@ -40,6 +40,26 @@ def test_0d_scalar(self, tmp_path): with pytest.raises(ValueError, match="Expected 2D"): to_geotiff(arr, str(tmp_path / 'bad.tif')) + def test_1d_dataarray(self, tmp_path): + # A 1D DataArray used to reach ``coords_to_transform`` (which + # indexes ``dims[-2]``) and raise an opaque ``IndexError: tuple + # index out of range`` instead of the clear message a 1D numpy + # array or a 4D DataArray already gets. + da = xr.DataArray(np.zeros(10, dtype=np.float32), dims=('x',)) + with pytest.raises(ValueError, match="Expected 2D or 3D array, got 1D"): + to_geotiff(da, str(tmp_path / 'bad.tif')) + + def test_0d_dataarray(self, tmp_path): + da = xr.DataArray(np.float32(42.0)) + with pytest.raises(ValueError, match="Expected 2D or 3D array, got 0D"): + to_geotiff(da, str(tmp_path / 'bad.tif')) + + def test_1d_dataarray_vrt(self, tmp_path): + # The ``.vrt`` write path shares the same pre-dispatch guard. + da = xr.DataArray(np.zeros(10, dtype=np.float32), dims=('x',)) + with pytest.raises(ValueError, match="Expected 2D or 3D array, got 1D"): + to_geotiff(da, str(tmp_path / 'bad.vrt')) + def test_unsupported_compression(self, tmp_path): arr = np.zeros((4, 4), dtype=np.float32) # ``to_geotiff`` validates ``compression`` up-front. The From 5e36fef172dd1caa50e5c6bd756578828cd220b1 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Thu, 2 Jul 2026 00:35:04 -0400 Subject: [PATCH 2/3] Add error-handling sweep state row for geotiff (2026-07-02) --- .claude/sweep-error-handling-state.csv | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .claude/sweep-error-handling-state.csv diff --git a/.claude/sweep-error-handling-state.csv b/.claude/sweep-error-handling-state.csv new file mode 100644 index 000000000..e10b715b9 --- /dev/null +++ b/.claude/sweep-error-handling-state.csv @@ -0,0 +1,2 @@ +module,last_inspected,issue,severity_max,categories_found,notes +geotiff,2026-07-02,,MEDIUM,2;4,"to_geotiff 0D/1D DataArray raised opaque IndexError from _coords.py coords_to_transform (dims[-2]) instead of clean 'Expected 2D or 3D' ValueError; numpy path + 4D DataArray already clean. Fixed: early ndim guard before dispatch (eager/vrt/gpu), +3 tests. Commit babf66fa. Read-side param validation + typed-error hierarchy + allow_rotated/allow_invalid_nodata VRT+chunked opt-in threading all verified clean (CUDA available, GPU paths run). PR/issue blocked by auto-mode external-write classifier; humanized draft in scratchpad geotiff_error_handling_pr_draft.md" From 59634559f29d47a20045245a5613659e36fb94db Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Thu, 2 Jul 2026 00:35:47 -0400 Subject: [PATCH 3/3] Record PR #3604 in error-handling sweep state row for geotiff --- .claude/sweep-error-handling-state.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/sweep-error-handling-state.csv b/.claude/sweep-error-handling-state.csv index e10b715b9..a5ad4dfdd 100644 --- a/.claude/sweep-error-handling-state.csv +++ b/.claude/sweep-error-handling-state.csv @@ -1,2 +1,2 @@ module,last_inspected,issue,severity_max,categories_found,notes -geotiff,2026-07-02,,MEDIUM,2;4,"to_geotiff 0D/1D DataArray raised opaque IndexError from _coords.py coords_to_transform (dims[-2]) instead of clean 'Expected 2D or 3D' ValueError; numpy path + 4D DataArray already clean. Fixed: early ndim guard before dispatch (eager/vrt/gpu), +3 tests. Commit babf66fa. Read-side param validation + typed-error hierarchy + allow_rotated/allow_invalid_nodata VRT+chunked opt-in threading all verified clean (CUDA available, GPU paths run). PR/issue blocked by auto-mode external-write classifier; humanized draft in scratchpad geotiff_error_handling_pr_draft.md" +geotiff,2026-07-02,3604,MEDIUM,2;4,"to_geotiff 0D/1D DataArray raised opaque IndexError from _coords.py coords_to_transform (dims[-2]) instead of clean 'Expected 2D or 3D' ValueError; numpy path + 4D DataArray already clean. Fixed via early ndim guard before dispatch (eager/vrt/gpu) + 3 tests; PR #3604. Read-side param validation + typed-error hierarchy + allow_rotated/allow_invalid_nodata VRT+chunked opt-in threading verified clean (CUDA available, GPU paths run). gh issue create blocked by auto-mode; PR opened. Cat 2+4."