Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions xrspatial/geotiff/_coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,23 @@ def require_transform_for_georeferenced(
ydim = da.dims[-2]
xdim = da.dims[-1]
if xdim in da.coords and ydim in da.coords:
x = da.coords[xdim].values
y = da.coords[ydim].values
# Integer coord dtype is the read-side no-georef sentinel
# (#1710, #1753, #1949). ``coords_to_transform`` returns ``None``
# for these so int-coord arrays round-trip cleanly without a
# synthetic unit transform; mirror that here so the writer does
# not fail-close on a legitimate no-georef write.
if x.dtype.kind in ('i', 'u') or y.dtype.kind in ('i', 'u'):
return
raise ValueError(
f"Cannot infer GeoTIFF transform from a "
f"{tuple(da.sizes.values())} array with spatial coords on "
f"both axes: both axes are degenerate (1x1), so neither "
f"coord array carries a pixel size step. 1xN and Nx1 inputs "
f"recover the pixel size from the non-degenerate axis, but "
f"a 1x1 cannot. Supply the affine transform explicitly via "
f"``attrs['transform']`` (rasterio 6-tuple "
f"``(px, 0, ox, 0, py, oy)``) or drop the coords if a "
f"non-georeferenced TIFF is desired."
f"both axes: neither coord array could yield a pixel size "
f"(1x1 inputs, or coords spaced non-uniformly). Supply the "
f"affine transform explicitly via ``attrs['transform']`` "
f"(rasterio 6-tuple ``(px, 0, ox, 0, py, oy)``) or drop the "
f"coords if a non-georeferenced TIFF is desired."
)


Expand Down
77 changes: 77 additions & 0 deletions xrspatial/geotiff/tests/test_int_coords_round_trip_hotfix_1962.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Regression test for the int-coord write hotfix.

PR #1953 added ``require_transform_for_georeferenced`` which raises when
both spatial dims are present in ``da.coords`` and no transform was
resolved. PR #1954 made ``coords_to_transform`` return ``None`` for
integer-dtype x/y coords as a no-georef sentinel (#1949). Their
interaction broke every writer call against an int-coord DataArray:
the resolver returned ``None``, then the guard raised. This test
locks in the int-dtype exemption that mirrors ``coords_to_transform``.
"""
import os
import tempfile

import numpy as np
import xarray as xr

from xrspatial.geotiff import open_geotiff, to_geotiff


def _tmp_path(name):
return os.path.join(tempfile.gettempdir(), name)


class TestIntCoordRoundTripHotfix1962:
def test_int_coords_2d_round_trip(self):
pixels = np.arange(20, dtype=np.float32).reshape(4, 5)
da = xr.DataArray(
pixels,
dims=['y', 'x'],
coords={
'y': np.arange(4, dtype=np.int64),
'x': np.arange(5, dtype=np.int64),
},
attrs={'long_name': 'int_coord_2d'},
)
path = _tmp_path('hotfix_1962_int_2d.tif')
try:
to_geotiff(da, path)
da2 = open_geotiff(path)
np.testing.assert_array_equal(da2.values, pixels)
# Read-side should re-emit the int-coord no-georef
# placeholders, confirming the no-georef contract held.
assert da2.coords['x'].dtype.kind in ('i', 'u')
assert da2.coords['y'].dtype.kind in ('i', 'u')
finally:
if os.path.exists(path):
os.remove(path)

def test_int_coords_3d_band_y_x_round_trip(self):
pixels = np.arange(2 * 4 * 5, dtype=np.float32).reshape(2, 4, 5)
da = xr.DataArray(
pixels,
dims=['band', 'y', 'x'],
coords={
'band': np.arange(2, dtype=np.int64),
'y': np.arange(4, dtype=np.int64),
'x': np.arange(5, dtype=np.int64),
},
attrs={'long_name': 'int_coord_3d'},
)
path = _tmp_path('hotfix_1962_int_3d.tif')
try:
to_geotiff(da, path)
da2 = open_geotiff(path)
# open_geotiff may emit (band, y, x) or (y, x, band) layout;
# compare on a band-by-band basis instead of guessing axis order.
arr = da2.values
assert arr.shape in ((2, 4, 5), (4, 5, 2))
if arr.shape == (2, 4, 5):
np.testing.assert_array_equal(arr, pixels)
else:
np.testing.assert_array_equal(
np.moveaxis(arr, -1, 0), pixels
)
finally:
if os.path.exists(path):
os.remove(path)
Loading