diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index da5d6fa8..ef9efdc5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -169,3 +169,7 @@ Tips To run a subset of tests:: $ pytest test/unit/test_show_versions.py::test_get_main_info + +When using NumPy's ``assert_almost_equal`` to compare affine transforms, pass +``tuple(transform)`` for both operands. This compares all nine coefficients +with the requested precision and avoids treating Affine 3 objects as scalars. diff --git a/docs/history.rst b/docs/history.rst index 38747916..e19716bf 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -3,6 +3,7 @@ History Latest ------ +- DEP: Require rasterio>=1.5.1 and affine>=3 for matrix multiplication support (#939) 0.23.0 ------ diff --git a/pyproject.toml b/pyproject.toml index 64758495..10d3623a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,8 @@ classifiers = [ requires-python = ">=3.12" dependencies = [ "packaging", - "rasterio>=1.4.3", + "rasterio>=1.5.1", + "affine>=3", # https://github.com/pydata/xarray/issues/11000 "xarray>=2026.2", "pyproj>=3.3", diff --git a/rioxarray/_spatial_utils.py b/rioxarray/_spatial_utils.py index c651f012..c6d37e91 100644 --- a/rioxarray/_spatial_utils.py +++ b/rioxarray/_spatial_utils.py @@ -5,7 +5,7 @@ import math import warnings from collections.abc import Hashable -from typing import Any, Iterable, Optional, Union +from typing import Any, Iterable, Optional, Union, cast import numpy import rasterio.mask @@ -114,12 +114,13 @@ def affine_to_coords( dict: x and y coordinate arrays. """ - transform = affine * affine.translation(0.5, 0.5) + # Affine's typed overloads do not cover NumPy coordinate arrays. + transform = cast(Any, affine @ affine.translation(0.5, 0.5)) if affine.is_rectilinear and not _affine_has_rotation(affine): - x_coords, _ = transform * (numpy.arange(width), numpy.zeros(width)) - _, y_coords = transform * (numpy.zeros(height), numpy.arange(height)) + x_coords, _ = transform @ (numpy.arange(width), numpy.zeros(width)) + _, y_coords = transform @ (numpy.zeros(height), numpy.arange(height)) else: - x_coords, y_coords = transform * numpy.meshgrid( + x_coords, y_coords = transform @ numpy.meshgrid( numpy.arange(width), numpy.arange(height), ) diff --git a/rioxarray/rioxarray.py b/rioxarray/rioxarray.py index f3509a43..9145d945 100644 --- a/rioxarray/rioxarray.py +++ b/rioxarray/rioxarray.py @@ -439,7 +439,7 @@ def transform(self, recalc: bool = False) -> Affine: src_resolution_x, src_resolution_y = self.resolution(recalc=recalc) except (DimensionMissingCoordinateError, DimensionError): return Affine.identity() if transform is None else transform - return Affine.translation(src_left, src_top) * Affine.scale( + return Affine.translation(src_left, src_top) @ Affine.scale( src_resolution_x, src_resolution_y ) diff --git a/test/integration/test_integration_rioxarray.py b/test/integration/test_integration_rioxarray.py index 245eb5be..3887c0d4 100644 --- a/test/integration/test_integration_rioxarray.py +++ b/test/integration/test_integration_rioxarray.py @@ -323,7 +323,8 @@ def test_pad_box(modis_clip): assert padded_size == original_size else: assert_almost_equal( - xdi.rio._cached_transform(), padded_ds.rio._cached_transform() + tuple(xdi.rio._cached_transform()), + tuple(padded_ds.rio._cached_transform()), ) for padded_size, original_size in zip(padded_ds.shape, xdi.shape): assert padded_size == original_size @@ -353,7 +354,9 @@ def test_clip_box(modis_clip): except KeyError: xdc_values = xdc.values assert_almost_equal(clipped_ds_values, xdc_values) - assert_almost_equal(clipped_ds.rio.transform(), xdc.rio.transform()) + assert_almost_equal( + tuple(clipped_ds.rio.transform()), tuple(xdc.rio.transform()) + ) # make sure it safely writes to netcdf clipped_ds.to_netcdf(modis_clip["output"]) @@ -381,7 +384,9 @@ def test_clip_box__auto_expand(modis_clip): except KeyError: xdc_values = xdc.values assert_almost_equal(clipped_ds_values, xdc_values) - assert_almost_equal(clipped_ds.rio.transform(), xdc.rio.transform()) + assert_almost_equal( + tuple(clipped_ds.rio.transform()), tuple(xdc.rio.transform()) + ) # make sure it safely writes to netcdf clipped_ds.to_netcdf(modis_clip["output"]) @@ -549,7 +554,9 @@ def test_slice_xy(modis_clip): except KeyError: xdc_values = xdc.values assert_almost_equal(clipped_ds_values, xdc_values) - assert_almost_equal(clipped_ds.rio.transform(), xdc.rio.transform()) + assert_almost_equal( + tuple(clipped_ds.rio.transform()), tuple(xdc.rio.transform()) + ) # make sure it safely writes to netcdf clipped_ds.to_netcdf(modis_clip["output"]) @@ -1428,7 +1435,7 @@ def test_make_coords__calc_trans(open_func, modis_reproject): dst_height=heightr, ) - assert_almost_equal(calculated_transform, calculated_transformr) + assert_almost_equal(tuple(calculated_transform), tuple(calculated_transformr)) # check to see if they all match if not isinstance(open_func, partial): assert_almost_equal( @@ -1477,7 +1484,7 @@ def test_make_coords__attr_trans(open_func, modis_reproject): dst_width=widthr, dst_height=heightr, ) - assert_almost_equal(attr_transform, calculated_transformr) + assert_almost_equal(tuple(attr_transform), tuple(calculated_transformr)) # check to see if they all match if not isinstance(open_func, partial): assert_almost_equal( diff --git a/test/unit/test_spatial_utils.py b/test/unit/test_spatial_utils.py new file mode 100644 index 00000000..937770a0 --- /dev/null +++ b/test/unit/test_spatial_utils.py @@ -0,0 +1,12 @@ +import numpy +from affine import Affine + +from rioxarray._spatial_utils import affine_to_coords + + +def test_affine_to_coords_avoids_deprecated_matrix_multiplication() -> None: + transform = Affine(1, 0, 0, 0, -1, 4) + coords = affine_to_coords(transform, width=4, height=4) + + numpy.testing.assert_array_equal(coords["x"], [0.5, 1.5, 2.5, 3.5]) + numpy.testing.assert_array_equal(coords["y"], [3.5, 2.5, 1.5, 0.5])