[Git][debian-gis-team/python-geotiepoints][upstream] New upstream version 1.7.5

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Sun Oct 13 08:48:11 BST 2024



Antonio Valentino pushed to branch upstream at Debian GIS Project / python-geotiepoints


Commits:
ba0c3548 by Antonio Valentino at 2024-10-13T07:37:09+00:00
New upstream version 1.7.5
- - - - -


7 changed files:

- .github/workflows/ci.yaml
- .github/workflows/deploy.yaml
- CHANGELOG.md
- geotiepoints/geointerpolator.py
- geotiepoints/interpolator.py
- geotiepoints/tests/test_geointerpolator.py
- geotiepoints/version.py


Changes:

=====================================
.github/workflows/ci.yaml
=====================================
@@ -30,9 +30,7 @@ jobs:
       - name: Setup Conda Environment
         uses: conda-incubator/setup-miniconda at v3
         with:
-          miniforge-variant: Mambaforge
           miniforge-version: latest
-          use-mamba: true
           channel-priority: strict
           python-version: ${{ matrix.python-version }}
           activate-environment: test-environment


=====================================
.github/workflows/deploy.yaml
=====================================
@@ -59,7 +59,7 @@ jobs:
           platforms: all
 
       - name: Build wheels
-        uses: pypa/cibuildwheel at v2.18.1
+        uses: pypa/cibuildwheel at v2.21.1
         env:
           CIBW_SKIP: "cp36-* cp37-* cp38-* pp* *-manylinux_i686 *-musllinux_i686 *-musllinux_aarch64 *-win32"
           CIBW_ARCHS: "${{ matrix.cibw_archs }}"
@@ -101,14 +101,14 @@ jobs:
           path: dist
       - name: Publish package to Test PyPI
         if: github.event.action != 'published' && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
-        uses: pypa/gh-action-pypi-publish at v1.8.14
+        uses: pypa/gh-action-pypi-publish at v1.10.2
         with:
           user: __token__
           password: ${{ secrets.test_pypi_password }}
           repository_url: https://test.pypi.org/legacy/
       - name: Publish package to PyPI
         if: github.event.action == 'published'
-        uses: pypa/gh-action-pypi-publish at v1.8.14
+        uses: pypa/gh-action-pypi-publish at v1.10.2
         with:
           user: __token__
           password: ${{ secrets.pypi_password }}


=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,20 @@
+## Version 1.7.5 (2024/10/12)
+
+### Issues Closed
+
+* [Issue 79](https://github.com/pytroll/python-geotiepoints/issues/79) - Test failure with scipy 1.13.x
+
+In this release 1 issue was closed.
+
+### Pull Requests Merged
+
+#### Features added
+
+* [PR 85](https://github.com/pytroll/python-geotiepoints/pull/85) - Add a spline interpolator for 2d arrays
+
+In this release 1 pull request was closed.
+
+
 ## Version 1.7.4 (2024/06/26)
 
 ### Pull Requests Merged


=====================================
geotiepoints/geointerpolator.py
=====================================
@@ -18,7 +18,7 @@
 """Geographical interpolation (lon/lats)."""
 
 import numpy as np
-from geotiepoints.interpolator import Interpolator, MultipleGridInterpolator
+from geotiepoints.interpolator import Interpolator, MultipleGridInterpolator, MultipleSplineInterpolator
 
 
 EARTH_RADIUS = 6370997.0
@@ -92,26 +92,38 @@ def xyz2lonlat(x__, y__, z__, radius=EARTH_RADIUS, thr=0.8, low_lat_z=True):
     return lons, lats
 
 
-class GeoGridInterpolator(MultipleGridInterpolator):
-    """Interpolate geographical coordinates from a regular grid of tie points."""
-
-    def __init__(self, tie_points, *data, **kwargs):
-        """Set up the interpolator."""
-        if len(data) == 1:
-            xyz = data[0].get_cartesian_coords()
-            data = [xyz[:, :, 0], xyz[:, :, 1], xyz[:, :, 2]]
-        elif len(data) == 2:
-            data = lonlat2xyz(*data)
-        else:
-            raise ValueError("Either pass lon/lats or a pyresample definition.")
-        super().__init__(tie_points, *data, **kwargs)
-
-    def interpolate(self, fine_points, **kwargs):
-        """Interpolate to *fine_points*."""
-        x, y, z = super().interpolate(fine_points, **kwargs)
-        return xyz2lonlat(x, y, z)
-
-    def interpolate_to_shape(self, shape, **kwargs):
-        """Interpolate to a given *shape*."""
-        fine_points = [np.arange(size) for size in shape]
-        return self.interpolate(fine_points, **kwargs)
+def _work_with_lonlats(klass):
+    """Adapt MultipleInterpolator classes to work with geographical coordinates."""
+
+    class GeoKlass(klass):
+
+        def __init__(self, tie_points, *data, **interpolator_init_kwargs):
+            """Set up the interpolator."""
+            data = to_xyz(data)
+            super().__init__(tie_points, *data, **interpolator_init_kwargs)
+
+        def interpolate(self, fine_points, **interpolator_call_kwargs):
+            """Interpolate to *fine_points*."""
+            x, y, z = super().interpolate(fine_points, **interpolator_call_kwargs)
+            return xyz2lonlat(x, y, z)
+
+    return GeoKlass
+
+
+def to_xyz(data):
+    """Convert data to cartesian.
+
+    Data can be a class with a `get_cartesian_coords` method, or a tuple of (lon, lat) arrays.
+    """
+    if len(data) == 1:
+        xyz = data[0].get_cartesian_coords()
+        data = [xyz[:, :, 0], xyz[:, :, 1], xyz[:, :, 2]]
+    elif len(data) == 2:
+        data = lonlat2xyz(*data)
+    else:
+        raise ValueError("Either pass lon/lats or a pyresample definition.")
+    return data
+
+
+GeoGridInterpolator = _work_with_lonlats(MultipleGridInterpolator)
+GeoSplineInterpolator = _work_with_lonlats(MultipleSplineInterpolator)


=====================================
geotiepoints/interpolator.py
=====================================
@@ -14,6 +14,8 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """Generic interpolation routines."""
 
+from abc import ABC, abstractmethod
+from functools import partial
 import numpy as np
 from scipy.interpolate import RectBivariateSpline, splev, splrep, RegularGridInterpolator
 
@@ -241,37 +243,36 @@ class Interpolator:
         return self.new_data
 
 
-class SingleGridInterpolator:
-    """An interpolator for a single 2d data array."""
+class AbstractSingleInterpolator(ABC):
+    """An abstract interpolator for a single 2d data array."""
 
-    def __init__(self, points, values, **kwargs):
+    def __init__(self, points, values, scipy_interpolator, **interpolator_init_kwargs):
         """Set up the interpolator.
 
-        *kwargs* are passed to the underlying RegularGridInterpolator instance.
+        *kwargs* are passed to the underlying scipy interpolator instance.
         So for example, to allow extrapolation, the kwargs can be `bounds_error=False, fill_value=None`.
         """
-        self.interpolator = RegularGridInterpolator(points, values, **kwargs)
+        self.interpolator = scipy_interpolator(points, values, **interpolator_init_kwargs)
         self.points = points
         self.values = values
 
-    def interpolate(self, fine_points, method="linear", chunks=None):
+    def interpolate(self, fine_points, chunks=None, **interpolator_call_kwargs):
         """Interpolate the value points to the *fine_points* grid.
 
         Args:
             fine_points: the points on the target grid to use, as one dimensional vectors for each dimension.
-            method: the method to use for interpolation as described in RegularGridInterpolator's documentation.
-                    Default is "linear".
             chunks: If not None, a lazy (dask-based) interpolation will be performed using the chunk sizes specified.
                     The result will be a dask array in this case. Defaults to None.
+            interpolator_kwargs: The keyword arguments to pass to the underlying scipy interpolator.
         """
         if chunks is not None:
-            res = self.interpolate_dask(fine_points, method=method, chunks=chunks)
+            res = self.interpolate_dask(fine_points, chunks=chunks, **interpolator_call_kwargs)
         else:
-            res = self.interpolate_numpy(fine_points, method=method)
+            res = self.interpolate_numpy(fine_points, **interpolator_call_kwargs)
 
         return res
 
-    def interpolate_dask(self, fine_points, method, chunks):
+    def interpolate_dask(self, fine_points, chunks, **interpolator_call_kwargs):
         """Interpolate (lazily) to a dask array."""
         from dask.base import tokenize
         import dask.array as da
@@ -281,12 +282,13 @@ class SingleGridInterpolator:
 
         chunks = normalize_chunks(chunks, shape, dtype=self.values.dtype)
 
-        token = tokenize(chunks, self.points, self.values, fine_points, method)
+        token = tokenize(chunks, self.points, self.values, fine_points, interpolator_call_kwargs)
         name = 'interpolate-' + token
 
-        dskx = {(name, ) + position: (self.interpolate_slices,
-                                      slices,
-                                      method)
+        interpolate_slices = partial(self.interpolate_slices, **interpolator_call_kwargs)
+
+        dskx = {(name, ) + position: (interpolate_slices,
+                                      slices)
                 for position, slices in _enumerate_chunk_slices(chunks)}
 
         res = da.Array(dskx, name, shape=list(shape),
@@ -294,12 +296,12 @@ class SingleGridInterpolator:
                        dtype=self.values.dtype)
         return res
 
-    def interpolate_numpy(self, fine_points, method="linear"):
+    @abstractmethod
+    def interpolate_numpy(self, fine_points, **interpolator_call_kwargs):
         """Interpolate to a numpy array."""
-        fine_x, fine_y = np.meshgrid(*fine_points, indexing='ij')
-        return self.interpolator((fine_x, fine_y), method=method).astype(self.values.dtype)
+        raise NotImplementedError
 
-    def interpolate_slices(self, fine_points, method="linear"):
+    def interpolate_slices(self, fine_points, **interpolator_call_kwargs):
         """Interpolate using slices.
 
         *fine_points* are a tuple of slices for the y and x dimensions
@@ -309,7 +311,7 @@ class SingleGridInterpolator:
         points_x = np.arange(slice_x.start, slice_x.stop)
         fine_points = points_y, points_x
 
-        return self.interpolate_numpy(fine_points, method=method)
+        return self.interpolate_numpy(fine_points, **interpolator_call_kwargs)
 
 
 def _enumerate_chunk_slices(chunks):
@@ -324,14 +326,41 @@ def _enumerate_chunk_slices(chunks):
         yield (position, slices)
 
 
-class MultipleGridInterpolator:
-    """Interpolator that works on multiple data arrays."""
+class SingleGridInterpolator(AbstractSingleInterpolator):
+    """A regular grid interpolator for a single 2d data array."""
+
+    def __init__(self, *args, **interpolator_init_kwargs):
+        """Set up the grid interpolator."""
+        super().__init__(*args, scipy_interpolator=RegularGridInterpolator, **interpolator_init_kwargs)
+
+    def interpolate_numpy(self, fine_points, **interpolator_call_kwargs):
+        """Interpolate to a numpy array."""
+        fine_x, fine_y = np.meshgrid(*fine_points, indexing='ij')
+        return self.interpolator((fine_x, fine_y), **interpolator_call_kwargs).astype(self.values.dtype)
+
+
+class SingleSplineInterpolator(AbstractSingleInterpolator):
+    """An spline interpolator for a single 2d data array."""
+
+    def __init__(self, points, values, **interpolator_init_kwargs):
+        """Set up the spline interpolator."""
+        self.interpolator = RectBivariateSpline(*points, values, **interpolator_init_kwargs)
+        self.points = points
+        self.values = values
+
+    def interpolate_numpy(self, fine_points, **interpolator_call_kwargs):
+        """Interpolate to a numpy array."""
+        return self.interpolator(*fine_points, **interpolator_call_kwargs).astype(self.values.dtype)
+
+
+class AbstractMultipleInterpolator(ABC):  # noqa: B024
+    """Abstract interpolator that works on mulitple arrays."""
 
-    def __init__(self, tie_points, *data, **kwargs):
+    def __init__(self, interpolator, tie_points, *data, **interpolator_init_kwargs):
         """Set up the interpolator from the multiple `data` arrays."""
         self.interpolators = []
         for values in data:
-            self.interpolators.append(SingleGridInterpolator(tie_points, values, **kwargs))
+            self.interpolators.append(interpolator(tie_points, values, **interpolator_init_kwargs))
 
     def interpolate(self, fine_points, **kwargs):
         """Interpolate the data.
@@ -339,3 +368,24 @@ class MultipleGridInterpolator:
         The keyword arguments will be passed on to SingleGridInterpolator's interpolate function.
         """
         return (interpolator.interpolate(fine_points, **kwargs) for interpolator in self.interpolators)
+
+    def interpolate_to_shape(self, shape, **interpolator_call_kwargs):
+        """Interpolate to a given *shape*."""
+        fine_points = [np.arange(size) for size in shape]
+        return self.interpolate(fine_points, **interpolator_call_kwargs)
+
+
+class MultipleGridInterpolator(AbstractMultipleInterpolator):
+    """Grid interpolator that works on multiple data arrays."""
+
+    def __init__(self, tie_points, *data, **interpolator_init_kwargs):
+        """Set up the interpolator from the multiple `data` arrays."""
+        super().__init__(SingleGridInterpolator, tie_points, *data, **interpolator_init_kwargs)
+
+
+class MultipleSplineInterpolator(AbstractMultipleInterpolator):
+    """Spline interpolator that works on multiple data arrays."""
+
+    def __init__(self, tie_points, *data, **interpolator_init_kwargs):
+        """Set up the interpolator from the multiple `data` arrays."""
+        super().__init__(SingleSplineInterpolator, tie_points, *data, **interpolator_init_kwargs)


=====================================
geotiepoints/tests/test_geointerpolator.py
=====================================
@@ -22,7 +22,7 @@ import numpy as np
 import pytest
 from pyresample.geometry import SwathDefinition
 
-from geotiepoints.geointerpolator import GeoInterpolator, GeoGridInterpolator
+from geotiepoints.geointerpolator import GeoInterpolator, GeoGridInterpolator, GeoSplineInterpolator
 
 TIES_EXP1 = np.array([[6384905.78040055, 6381081.08333225, 6371519.34066148,
                        6328950.00792935, 6253610.69157758, 6145946.19489936,
@@ -291,3 +291,79 @@ class TestGeoGridInterpolator:
         lons, lats = interpolator.interpolate_to_shape((16, 16), method="cubic")
 
         assert lons.shape == (16, 16)
+
+
+class TestGeoSplineInterpolator:
+    """Test the GeoGridInterpolator."""
+
+    @pytest.mark.parametrize("args", ((TIE_LONS, TIE_LATS),
+                                      [SwathDefinition(TIE_LONS, TIE_LATS)]
+                                      ))
+    def test_geospline_interpolation(self, args):
+        """Test that the interpolator works with both explicit tie-point arrays and swath definition objects."""
+        x_points = np.array([0, 1, 3, 7])
+        y_points = np.array([0, 1, 3, 7, 15])
+
+        interpolator = GeoSplineInterpolator((y_points, x_points), *args, kx=1, ky=1)
+
+        fine_x_points = np.arange(8)
+        fine_y_points = np.arange(16)
+
+        lons, lats = interpolator.interpolate((fine_y_points, fine_x_points))
+
+        lons_expected = np.array([1., 2., 2.5, 3., 3.25, 3.5, 3.75, 4.])
+        lats_expected = np.array([1., 2., 2.5, 3., 3.25, 3.5, 3.75, 4., 4.125,
+                                  4.25, 4.375, 4.5, 4.625, 4.75, 4.875, 5.])
+
+        np.testing.assert_allclose(lons[0, :], lons_expected, rtol=5e-5)
+        np.testing.assert_allclose(lats[:, 0], lats_expected, rtol=5e-5)
+
+    def test_geospline_interpolation_to_shape(self):
+        """Test that the interpolator works with both explicit tie-point arrays and swath definition objects."""
+        x_points = np.array([0, 1, 3, 7])
+        y_points = np.array([0, 1, 3, 7, 15])
+
+        interpolator = GeoSplineInterpolator((y_points, x_points), TIE_LONS, TIE_LATS, kx=1, ky=1)
+
+        lons, lats = interpolator.interpolate_to_shape((16, 8))
+
+        lons_expected = np.array([1., 2., 2.5, 3., 3.25, 3.5, 3.75, 4.])
+        lats_expected = np.array([1., 2., 2.5, 3., 3.25, 3.5, 3.75, 4., 4.125,
+                                  4.25, 4.375, 4.5, 4.625, 4.75, 4.875, 5.])
+
+        np.testing.assert_allclose(lons[0, :], lons_expected, rtol=5e-5)
+        np.testing.assert_allclose(lats[:, 0], lats_expected, rtol=5e-5)
+
+    def test_geospline_interpolation_preserves_dtype(self):
+        """Test that the interpolator works with both explicit tie-point arrays and swath definition objects."""
+        x_points = np.array([0, 1, 3, 7])
+        y_points = np.array([0, 1, 3, 7, 15])
+
+        interpolator = GeoGridInterpolator((y_points, x_points),
+                                           TIE_LONS.astype(np.float32), TIE_LATS.astype(np.float32))
+
+        lons, lats = interpolator.interpolate_to_shape((16, 8))
+
+        assert lons.dtype == np.float32
+        assert lats.dtype == np.float32
+
+    def test_chunked_geospline_interpolation(self):
+        """Test that the interpolator works with both explicit tie-point arrays and swath definition objects."""
+        dask = pytest.importorskip("dask")
+
+        x_points = np.array([0, 1, 3, 7])
+        y_points = np.array([0, 1, 3, 7, 15])
+
+        interpolator = GeoGridInterpolator((y_points, x_points),
+                                           TIE_LONS.astype(np.float32), TIE_LATS.astype(np.float32))
+
+        lons, lats = interpolator.interpolate_to_shape((16, 8), chunks=4)
+
+        assert lons.chunks == ((4, 4, 4, 4), (4, 4))
+        assert lats.chunks == ((4, 4, 4, 4), (4, 4))
+
+        with dask.config.set({"array.chunk-size": 64}):
+
+            lons, lats = interpolator.interpolate_to_shape((16, 8), chunks="auto")
+            assert lons.chunks == ((4, 4, 4, 4), (4, 4))
+            assert lats.chunks == ((4, 4, 4, 4), (4, 4))


=====================================
geotiepoints/version.py
=====================================
@@ -26,9 +26,9 @@ def get_keywords():
     # setup.py/versioneer.py will grep for the variable names, so they must
     # each be defined on a line of their own. _version.py will just call
     # get_keywords().
-    git_refnames = " (HEAD -> main, tag: v1.7.4)"
-    git_full = "29587f81d55e044bae9583c6f47ff3d715cfb199"
-    git_date = "2024-06-26 11:18:44 -0500"
+    git_refnames = " (HEAD -> main, tag: v1.7.5)"
+    git_full = "44f8e87bba78e29b5c8dda325984e069cbd2092a"
+    git_date = "2024-10-12 10:07:53 +0200"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 



View it on GitLab: https://salsa.debian.org/debian-gis-team/python-geotiepoints/-/commit/ba0c3548ad1119b9b2be33c05ad7a76dbf693f06

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/python-geotiepoints/-/commit/ba0c3548ad1119b9b2be33c05ad7a76dbf693f06
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20241013/1258b6d9/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list