From 78b868f84fd2f2139058fba65baa1ee48d56750b Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 2 Oct 2024 23:15:50 +0100 Subject: [PATCH 1/3] patch: fix rasterio 1.4.x regression --- src/geovista/bridge.py | 14 +++++++- tests/bridge/test_from_tiff.py | 65 ++++++++++++++++++++++------------ 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/geovista/bridge.py b/src/geovista/bridge.py index eafc184b..f95f56b0 100644 --- a/src/geovista/bridge.py +++ b/src/geovista/bridge.py @@ -867,7 +867,19 @@ def from_tiff( cols, rows = np.meshgrid( np.arange(src.width), np.arange(src.height), indexing="xy" ) - xs, ys = rio.transform.xy(src.transform, rows, cols) + # rasterio 1.4.0 (regression) expects 1-D arrays, fixed in 1.4.1 + # see https://github.com/rasterio/rasterio/issues/3191 + xs, ys = rio.transform.xy(src.transform, rows.flatten(), cols.flatten()) + + # ensure we have arrays, rather than a list of arrays + xs, ys = np.asanyarray(xs), np.asanyarray(ys) + + # ensure shape is maintained (rasterio 1.4.1 regression) + if xs.shape != src.shape: + xs = xs.reshape(src.shape) + + if ys.shape != src.shape: + ys = ys.reshape(src.shape) # create the geotiff mesh mesh = cls.from_2d( diff --git a/tests/bridge/test_from_tiff.py b/tests/bridge/test_from_tiff.py index d331dd73..ea82d9fc 100644 --- a/tests/bridge/test_from_tiff.py +++ b/tests/bridge/test_from_tiff.py @@ -44,7 +44,7 @@ def test_rgb_band_fail(mocker): _ = Transform.from_tiff(fname, rgb=True) -@pytest.mark.parametrize("rgb", [True]) +@pytest.mark.parametrize("rgb", [True, False]) @pytest.mark.parametrize("band", [1, 2, 3]) @pytest.mark.parametrize("unit", ["m", None]) def test_rgb_band(mocker, rgb, band, unit): @@ -55,12 +55,14 @@ def test_rgb_band(mocker, rgb, band, unit): data = mocker.sentinel.data mocked_read = mocker.MagicMock(return_value=data) transform = mocker.sentinel.transform - height, width = 2, 3 + height, width = pixels_shape = 2, 3 + n_pixels = height * width kwargs = { "count": band, "crs": crs, "height": height, "read": mocked_read, + "shape": pixels_shape, "transform": transform, "units": [unit] * band, "width": width, @@ -74,10 +76,13 @@ def test_rgb_band(mocker, rgb, band, unit): "numpy.dstack", return_value=mocker.MagicMock(reshape=mocked_reshape) ) - cols, rows = mocker.sentinel.cols, mocker.sentinel.rows + cols_flatten = mocker.sentinel.cols_flatten + rows_flatten = mocker.sentinel.rows_flatten + cols = mocker.MagicMock(flatten=mocker.MagicMock(return_value=cols_flatten)) + rows = mocker.MagicMock(flatten=mocker.MagicMock(return_value=rows_flatten)) mocked_meshgrid = mocker.patch("numpy.meshgrid", return_value=(cols, rows)) - xs, ys = mocker.sentinel.xs, mocker.sentinel.ys + xs, ys = np.arange(n_pixels), np.arange(n_pixels) mocked_xy = mocker.patch("rasterio.transform.xy", return_value=(xs, ys)) expected = mocker.sentinel.mesh @@ -102,19 +107,26 @@ def test_rgb_band(mocker, rgb, band, unit): assert len(args) == 2 np.testing.assert_array_equal(args[0], np.arange(width)) np.testing.assert_array_equal(args[1], np.arange(height)) + assert mocked_meshgrid.call_args.kwargs == {"indexing": "xy"} - mocked_xy.assert_called_once_with(transform, rows, cols) - - kwargs = {"radius": None, "zlevel": None, "zscale": None, "clean": None} - mocked_from_2d.assert_called_once_with( - xs, - ys, - data=data, - name=name.format(units=str(unit)), - crs=crs, - rgb=rgb, - **kwargs, - ) + mocked_xy.assert_called_once_with(transform, rows_flatten, cols_flatten) + + expected_kwargs = { + "data": data, + "name": name.format(units=str(unit)), + "crs": crs, + "rgb": rgb, + "radius": None, + "zlevel": None, + "zscale": None, + "clean": None, + } + mocked_from_2d.assert_called_once() + args = mocked_from_2d.call_args.args + assert len(args) == 2 + assert args[0].shape == pixels_shape + assert args[1].shape == pixels_shape + assert mocked_from_2d.call_args.kwargs == expected_kwargs @pytest.mark.parametrize("sieve", [True]) @@ -122,7 +134,8 @@ def test_rgb_band(mocker, rgb, band, unit): @pytest.mark.parametrize("masked", [False, True]) def test_extract(mocker, masked, rgb, sieve): """Test extract behaviour with and without image masking.""" - height, width = 2, 3 + pixels_shape = height, width = 2, 3 + n_pixels = height * width band = 3 if rgb else 1 crs = mocker.sentinel.crs dtypes = ["uint8"] * band @@ -145,16 +158,20 @@ def test_extract(mocker, masked, rgb, sieve): "dtypes": dtypes, "height": height, "read": mocked_read, + "shape": pixels_shape, "transform": transform, "width": width, } dataset = mocker.MagicMock(**kwargs) mocker.patch("rasterio.open").return_value.__enter__.return_value = dataset - cols, rows = mocker.sentinel.cols, mocker.sentinel.rows + cols_flatten = mocker.sentinel.cols_flatten + rows_flatten = mocker.sentinel.rows_flatten + cols = mocker.MagicMock(flatten=mocker.MagicMock(return_value=cols_flatten)) + rows = mocker.MagicMock(flatten=mocker.MagicMock(return_value=rows_flatten)) mocked_meshgrid = mocker.patch("numpy.meshgrid", return_value=(cols, rows)) - xs, ys = mocker.sentinel.xs, mocker.sentinel.ys + xs, ys = np.arange(n_pixels), np.arange(n_pixels) mocked_xy = mocker.patch("rasterio.transform.xy", return_value=(xs, ys)) expected_mesh = mocker.sentinel.mesh @@ -186,15 +203,14 @@ def test_extract(mocker, masked, rgb, sieve): assert len(args) == 2 np.testing.assert_array_equal(args[0], np.arange(width)) np.testing.assert_array_equal(args[1], np.arange(height)) + assert mocked_meshgrid.call_args.kwargs == {"indexing": "xy"} - mocked_xy.assert_called_once_with(transform, rows, cols) + mocked_xy.assert_called_once_with(transform, rows_flatten, cols_flatten) if rgb: data = np.dstack(data).reshape(-1, band) mask = mask[0] - mocked_from_2d.assert_called_once() - assert mocked_from_2d.call_args.args == (xs, ys) expected_kwargs = { "name": None, "crs": crs, @@ -204,6 +220,11 @@ def test_extract(mocker, masked, rgb, sieve): "zscale": None, "clean": None, } + mocked_from_2d.assert_called_once() + args = mocked_from_2d.call_args.args + assert len(args) == 2 + assert args[0].shape == pixels_shape + assert args[1].shape == pixels_shape kwargs = mocked_from_2d.call_args.kwargs actual = kwargs.pop("data") assert kwargs == expected_kwargs From 97fc909a6e179480153a2e501ba2b915cc533779 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 3 Oct 2024 00:09:44 +0100 Subject: [PATCH 2/3] remove broken link (#1065) --- src/geovista/examples/rectilinear/from_1d__oisst.py | 4 ++-- src/geovista/examples/rectilinear/from_1d__oisst_eqc.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/geovista/examples/rectilinear/from_1d__oisst.py b/src/geovista/examples/rectilinear/from_1d__oisst.py index aebc82d8..06954653 100755 --- a/src/geovista/examples/rectilinear/from_1d__oisst.py +++ b/src/geovista/examples/rectilinear/from_1d__oisst.py @@ -18,8 +18,8 @@ The resulting mesh contains quad cells. The example uses NOAA/NECI 1/4° Daily Optimum Interpolation Sea Surface Temperature -(OISST) v2.1 Advanced Very High Resolution Radiometer (AVHRR) gridded data -(https://doi.org/10.25921/RE9P-PT57). The data targets the mesh faces/cells. +(OISST) v2.1 Advanced Very High Resolution Radiometer (AVHRR) gridded data. +The data targets the mesh faces/cells. Note that, a threshold is also applied to remove land ``NaN`` cells, and a NASA Blue Marble base layer is rendered along with Natural Earth coastlines. diff --git a/src/geovista/examples/rectilinear/from_1d__oisst_eqc.py b/src/geovista/examples/rectilinear/from_1d__oisst_eqc.py index ff1e64d7..8291ed64 100755 --- a/src/geovista/examples/rectilinear/from_1d__oisst_eqc.py +++ b/src/geovista/examples/rectilinear/from_1d__oisst_eqc.py @@ -18,8 +18,8 @@ The resulting mesh contains quad cells. The example uses NOAA/NECI 1/4° Daily Optimum Interpolation Sea Surface Temperature -(OISST) v2.1 Advanced Very High Resolution Radiometer (AVHRR) gridded data -(https://doi.org/10.25921/RE9P-PT57). The data targets the mesh faces/cells. +(OISST) v2.1 Advanced Very High Resolution Radiometer (AVHRR) gridded data. +The data targets the mesh faces/cells. Note that, a threshold is also applied to remove land ``NaN`` cells, and a NASA Blue Marble base layer is rendered along with Natural Earth coastlines. From 22841f102aac258f2feec4bf4c448e307e06db34 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 3 Oct 2024 00:24:24 +0100 Subject: [PATCH 3/3] fix upload coverage (#1075) --- .github/workflows/ci-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 30ca93d5..3ed53098 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -50,7 +50,7 @@ jobs: posargs: ["--xvfb-backend xvfb --durations=10"] include: - version: "py312" - coverage: "--cov-report=xml --cov=geovista" + coverage: "--cov-report= --cov=geovista" steps: - uses: actions/checkout@v4 @@ -131,13 +131,13 @@ jobs: - name: "prepare coverage" if: ${{ matrix.coverage }} run: | - mv .coverage .coverage${{ strategy.job-index }} + mv .coverage ci-test-coverage${{ strategy.job-index }} - if: ${{ matrix.coverage }} uses: actions/upload-artifact@v4 with: name: coverage-artifacts-${{ github.job }}-${{ strategy.job-index }} - path: ${{ github.workspace }}/.coverage* + path: ${{ github.workspace }}/ci-test-coverage* coverage: @@ -169,7 +169,7 @@ jobs: - name: "create coverage report" run: | - coverage combine .coverage* + coverage combine ci-test-coverage* coverage xml --omit=*/_version.py - name: "upload coverage report"