[Git][debian-gis-team/pyresample][upstream] New upstream version 1.25.1

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Fri Aug 5 10:35:37 BST 2022



Antonio Valentino pushed to branch upstream at Debian GIS Project / pyresample


Commits:
f72c4ee9 by Antonio Valentino at 2022-08-05T09:21:45+00:00
New upstream version 1.25.1
- - - - -


14 changed files:

- CHANGELOG.md
- pyresample/_spatial_mp.py
- pyresample/bilinear/_base.py
- pyresample/bilinear/xarr.py
- pyresample/bucket/__init__.py
- pyresample/geo_filter.py
- pyresample/geometry.py
- pyresample/grid.py
- pyresample/test/conftest.py
- pyresample/test/test_bilinear.py
- pyresample/test/test_geometry.py
- pyresample/test/test_resamplers/test_nearest.py
- pyresample/utils/proj4.py
- pyresample/version.py


Changes:

=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,24 @@
+## Version 1.25.0 (2022/07/29)
+
+### Issues Closed
+
+* [Issue 428](https://github.com/pytroll/pyresample/issues/428) - Add more flexible antimeridian handling to DynamicAreaDefinition ([PR 431](https://github.com/pytroll/pyresample/pull/431) by [@djhoese](https://github.com/djhoese))
+
+In this release 1 issue was closed.
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 446](https://github.com/pytroll/pyresample/pull/446) - Fix incorrect extents for DynamicAreaDefinition with 'modify_crs' antimeridian mode
+
+#### Features added
+
+* [PR 431](https://github.com/pytroll/pyresample/pull/431) - Add 'antimeridian_mode' to DynamicAreaDefinition ([428](https://github.com/pytroll/pyresample/issues/428))
+
+In this release 2 pull requests were closed.
+
+
 ## Version 1.24.1 (2022/07/06)
 
 ### Pull Requests Merged


=====================================
pyresample/_spatial_mp.py
=====================================
@@ -102,43 +102,16 @@ class cKDTree_MP(object):
         return _d.copy(), _i.copy()
 
 
-class BaseProj(pyproj.Proj):
-    """Helper class for easier backwards compatibility."""
-
-    def __init__(self, projparams=None, preserve_units=True, **kwargs):
-        # have to have this because pyproj uses __new__
-        # subclasses would fail when calling __init__ otherwise
-        super(BaseProj, self).__init__(projparams=projparams,
-                                       preserve_units=preserve_units,
-                                       **kwargs)
-
-
-class Proj(BaseProj):
-    """Helper class to skip transforming lon/lat projection coordinates."""
-
-    def __call__(self, data1, data2, inverse=False, radians=False,
-                 errcheck=False, nprocs=1):
-        """Transform coordinates to coordinate system except for geographic coordinate systems."""
-        if self.crs.is_geographic:
-            return data1, data2
-        return super(Proj, self).__call__(data1, data2, inverse=inverse,
-                                          radians=radians, errcheck=errcheck)
-
-
-class Proj_MP(BaseProj):
+class Proj_MP:
     """Multi-processing version of the pyproj Proj class."""
 
     def __init__(self, *args, **kwargs):
         self._args = args
         self._kwargs = kwargs
-        super(Proj_MP, self).__init__(*args, **kwargs)
 
     def __call__(self, data1, data2, inverse=False, radians=False,
                  errcheck=False, nprocs=2, chunk=None, schedule='guided'):
         """Transform coordinates to coordinates in the current coordinate system."""
-        if self.crs.is_geographic:
-            return data1, data2
-
         grid_shape = data1.shape
         n = data1.size
 


=====================================
pyresample/bilinear/_base.py
=====================================
@@ -34,9 +34,9 @@ import warnings
 
 import numpy as np
 from pykdtree.kdtree import KDTree
+from pyproj import Proj
 
 from pyresample import data_reduce, geometry
-from pyresample._spatial_mp import Proj
 
 from ..future.resamplers._transform_utils import lonlat2xyz
 


=====================================
pyresample/bilinear/xarr.py
=====================================
@@ -30,10 +30,10 @@ import dask.array as da
 import numpy as np
 import zarr
 from dask import delayed
+from pyproj import Proj
 from xarray import DataArray, Dataset
 
 from pyresample import CHUNK_SIZE
-from pyresample._spatial_mp import Proj
 from pyresample.bilinear._base import (
     BilinearBase,
     array_slice_for_multiple_arrays,


=====================================
pyresample/bucket/__init__.py
=====================================
@@ -24,8 +24,7 @@ import dask
 import dask.array as da
 import numpy as np
 import xarray as xr
-
-from pyresample._spatial_mp import Proj
+from pyproj import Proj
 
 LOG = logging.getLogger(__name__)
 


=====================================
pyresample/geo_filter.py
=====================================
@@ -18,6 +18,7 @@
 """Filters based on geolocation validity."""
 
 import numpy as np
+from pyproj import Proj
 
 from . import _spatial_mp, geometry
 
@@ -60,12 +61,14 @@ class GridFilter(object):
         lats = geometry_def.lats[:]
 
         # Get projection coords
+        proj_kwargs = {}
         if self.nprocs > 1:
             proj = _spatial_mp.Proj_MP(**self.area_def.proj_dict)
+            proj_kwargs["nprocs"] = self.nprocs
         else:
-            proj = _spatial_mp.Proj(**self.area_def.proj_dict)
+            proj = Proj(**self.area_def.proj_dict)
 
-        x_coord, y_coord = proj(lons, lats, nprocs=self.nprocs)
+        x_coord, y_coord = proj(lons, lats, **proj_kwargs)
 
         # Find array indices of coordinates
         target_x = ((x_coord / self.area_def.pixel_size_x) +


=====================================
pyresample/geometry.py
=====================================
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """Classes for geometry operations."""
+from __future__ import annotations
 
 import hashlib
 import math
@@ -24,14 +25,15 @@ from collections import OrderedDict
 from functools import partial, wraps
 from logging import getLogger
 from pathlib import Path
-from typing import Optional
+from typing import Optional, Sequence, Union
 
 import numpy as np
 import yaml
-from pyproj import Geod, transform
+from pyproj import Geod, Proj, transform
+from pyproj.aoi import AreaOfUse
 
 from pyresample import CHUNK_SIZE
-from pyresample._spatial_mp import Cartesian, Cartesian_MP, Proj, Proj_MP
+from pyresample._spatial_mp import Cartesian, Cartesian_MP, Proj_MP
 from pyresample.area_config import create_area_def
 from pyresample.boundary import AreaDefBoundary, Boundary, SimpleBoundary
 from pyresample.utils import (
@@ -1017,26 +1019,59 @@ class DynamicAreaDefinition(object):
             return None
         return self.resolution[1]
 
-    def compute_domain(self, corners, resolution=None, shape=None):
+    def compute_domain(
+            self,
+            corners: Sequence,
+            resolution: Optional[Union[float, tuple[float, float]]] = None,
+            shape: Optional[tuple[int, int]] = None,
+            projection: Optional[Union[CRS, dict, str, int]] = None
+    ):
         """Compute shape and area_extent from corners and [shape or resolution] info.
 
-        Corners represents the center of pixels, while area_extent represents the edge of pixels.
+        Args:
+            corners:
+                4-element sequence representing the outer corners of the
+                region. Note that corners represents the center of pixels,
+                while area_extent represents the edge of pixels. The four
+                values are (xmin_corner, ymin_corner, xmax_corner, ymax_corner).
+                If the x corners are ``None`` then the full extent (area of use)
+                of the projection will be used. When needed, area of use is taken
+                from the PROJ library or in the case of a geographic lon/lat
+                projection -180/180 is used. A RuntimeError is raised if the
+                area of use is needed (when x corners are ``None``) and area
+                of use can't be determined.
+            resolution:
+                Spatial resolution in projection units (typically meters or
+                degrees). If not specified then shape must be provided.
+                If a scalar then it is treated as the x and y resolution. If
+                a tuple then x resolution is the first element, y is the
+                second.
+            shape:
+                Number of pixels in the area as a 2-element tuple. The first
+                is number of rows, the second number of columns.
+            projection:
+                PROJ.4 definition string, dictionary, integer EPSG code, or
+                pyproj CRS object.
 
         Note that ``shape`` is (rows, columns) and ``resolution`` is
         (x_size, y_size); the dimensions are flipped.
+
         """
         if resolution is not None and shape is not None:
             raise ValueError("Both resolution and shape can't be provided.")
         elif resolution is None and shape is None:
             raise ValueError("Either resolution or shape must be provided.")
+        if resolution is not None and isinstance(resolution, (int, float)):
+            resolution = (resolution, resolution)
+        if projection is None:
+            projection = self._projection
 
+        corners = self._update_corners_for_full_extent(corners, shape, resolution, projection)
         if shape:
             height, width = shape
             x_resolution = (corners[2] - corners[0]) * 1.0 / (width - 1)
             y_resolution = (corners[3] - corners[1]) * 1.0 / (height - 1)
         else:
-            if isinstance(resolution, (int, float)):
-                resolution = (resolution, resolution)
             x_resolution, y_resolution = resolution
             width = int(np.rint((corners[2] - corners[0]) * 1.0 / x_resolution + 1))
             height = int(np.rint((corners[3] - corners[1]) * 1.0 / y_resolution + 1))
@@ -1047,7 +1082,33 @@ class DynamicAreaDefinition(object):
                        corners[3] + y_resolution / 2)
         return area_extent, width, height
 
-    def freeze(self, lonslats=None, resolution=None, shape=None, proj_info=None):
+    def _update_corners_for_full_extent(self, corners, shape, resolution, projection):
+        corners = list(corners)
+        if corners[0] is not None:
+            return corners
+        aou = self._get_crs_area_of_use(projection)
+        if shape is not None:
+            width = shape[1]
+            x_resolution = (aou.east - aou.west) / width
+            corners[0] = aou.west + x_resolution / 2.0
+            corners[2] = aou.east - x_resolution / 2.0
+        else:
+            x_resolution = resolution[0]
+            corners[0] = aou.west + x_resolution / 2.0
+            corners[2] = aou.east - x_resolution / 2.0
+        return corners
+
+    def _get_crs_area_of_use(self, projection):
+        crs = CRS(projection)
+        aou = crs.area_of_use
+        if aou is None:
+            if crs.is_geographic:
+                return AreaOfUse(west=-180.0, south=-90.0, east=180.0, north=90.0)
+            raise RuntimeError("Projection has no defined area of use")
+        return aou
+
+    def freeze(self, lonslats=None, resolution=None, shape=None, proj_info=None,
+               antimeridian_mode=None):
         """Create an AreaDefinition from this area with help of some extra info.
 
         Parameters
@@ -1061,6 +1122,31 @@ class DynamicAreaDefinition(object):
           the shape of the resulting area.
         proj_info:
           complementing parameters to the projection info.
+        antimeridian_mode:
+            How to handle lon/lat data crossing the anti-meridian of the
+            projection. This currently only affects lon/lat geographic
+            projections and data cases not covering the north or south pole.
+            The possible options are:
+
+            * "modify_extents": Set the X bounds to the edges of the data, but
+                add 360 to the right-most bound. This has the effect of making
+                the area coordinates continuous from the left side to the
+                right side. However, this means that some coordinates will be
+                outside the coordinate space of the projection. Although most
+                PROJ and pyresample functionality can handle this there may be
+                some edge cases.
+            * "modify_crs": Change the prime meridian of the projection
+                from 0 degrees longitude to 180 degrees longitude. This has
+                the effect of putting the data on a continuous coordinate
+                system. However, this means that comparing data resampled to
+                this resulting area and an area not over the anti-meridian
+                would be more difficult.
+            * "global_extents": Ignore the bounds of the data and use -180/180
+                degrees as the west and east bounds of the data. This will
+                generate a large output area, but with the benefit of keeping
+                the data on the original projection. Note that some resampling
+                methods may produce artifacts when resampling on the edge of
+                the area (the anti-meridian).
 
         Shape parameters are ignored if the instance is created
         with the `optimize_projection` flag set to True.
@@ -1082,76 +1168,59 @@ class DynamicAreaDefinition(object):
         shape = None if None in shape else shape
         area_extent = self.area_extent
         if not area_extent or not width or not height:
-            corners = self._compute_bound_centers(proj_dict, lonslats)
-            area_extent, width, height = self.compute_domain(corners, resolution, shape)
+            projection, corners = self._compute_bound_centers(proj_dict, lonslats, antimeridian_mode=antimeridian_mode)
+            area_extent, width, height = self.compute_domain(corners, resolution, shape, projection)
         return AreaDefinition(self.area_id, self.description, '',
                               projection, width, height,
                               area_extent, self.rotation)
 
-    def _compute_bound_centers(self, proj_dict, lonslats):
+    def _compute_bound_centers(self, proj_dict, lonslats, antimeridian_mode):
+        from pyresample.utils.proj4 import DaskFriendlyTransformer
+
         lons, lats = self._extract_lons_lats(lonslats)
-        if hasattr(lons, 'compute'):
-            return self._compute_bound_centers_dask(proj_dict, lons, lats)
-        return self._compute_bound_centers_numpy(proj_dict, lons, lats)
-
-    def _compute_bound_centers_numpy(self, proj_dict, lons, lats):
-        # TODO: Do more dask-friendly things here
-        proj4 = Proj(proj_dict)
-        xarr, yarr = proj4(np.asarray(lons), np.asarray(lats))
+        crs = CRS(proj_dict)
+        transformer = DaskFriendlyTransformer.from_crs(CRS(4326), crs, always_xy=True)
+        xarr, yarr = transformer.transform(lons, lats)
         xarr[xarr > 9e29] = np.nan
         yarr[yarr > 9e29] = np.nan
         xmin = np.nanmin(xarr)
         xmax = np.nanmax(xarr)
         ymin = np.nanmin(yarr)
         ymax = np.nanmax(yarr)
-        x_passes_antimeridian = (xmax - xmin) > 355
-        epsilon = 0.1
-        y_is_pole = (ymax >= 90 - epsilon) or (ymin <= -90 + epsilon)
-        if proj4.crs.is_geographic and x_passes_antimeridian and not y_is_pole:
-            # cross anti-meridian of projection
-            xmin = np.nanmin(xarr[xarr >= 0])
-            xmax = np.nanmax(xarr[xarr < 0]) + 360
-        return xmin, ymin, xmax, ymax
-
-    def _compute_bound_centers_dask(self, proj_dict, lons, lats):
-        import dask.array as da
-
-        from pyresample.utils.proj4 import DaskFriendlyTransformer
-        crs = CRS(proj_dict)
-        transformer = DaskFriendlyTransformer.from_crs(CRS(4326), crs,
-                                                       always_xy=True)
-        xarr, yarr = transformer.transform(lons, lats)
-        xarr = da.where(xarr > 9e29, np.nan, xarr)
-        yarr = da.where(yarr > 9e29, np.nan, yarr)
-        _xmin = np.nanmin(xarr)
-        _xmax = np.nanmax(xarr)
-        _ymin = np.nanmin(yarr)
-        _ymax = np.nanmax(yarr)
-        xmin, xmax, ymin, ymax = da.compute(
-            _xmin,
-            _xmax,
-            _ymin,
-            _ymax)
-
+        if hasattr(lons, "compute"):
+            xmin, xmax, ymin, ymax = da.compute(xmin, xmax, ymin, ymax)
         x_passes_antimeridian = (xmax - xmin) > 355
         epsilon = 0.1
         y_is_pole = (ymax >= 90 - epsilon) or (ymin <= -90 + epsilon)
         if crs.is_geographic and x_passes_antimeridian and not y_is_pole:
             # cross anti-meridian of projection
-            xarr_pos = da.where(xarr >= 0, xarr, np.nan)
-            xarr_neg = da.where(xarr < 0, xarr, np.nan)
-            xmin = np.nanmin(xarr_pos)
-            xmax = np.nanmax(xarr_neg) + 360
-            xmin, xmax = da.compute(xmin, xmax)
-        return xmin, ymin, xmax, ymax
-
-    def _extract_lons_lats(self, lonslats):
+            xmin, xmax = self._compute_new_x_corners_for_antimeridian(xarr, antimeridian_mode)
+            if antimeridian_mode == "modify_crs":
+                proj_dict.update({"pm": 180.0})
+        return proj_dict, (xmin, ymin, xmax, ymax)
+
+    @staticmethod
+    def _extract_lons_lats(lonslats):
         try:
             lons, lats = lonslats
         except (TypeError, ValueError):
             lons, lats = lonslats.get_lonlats()
         return lons, lats
 
+    def _compute_new_x_corners_for_antimeridian(self, xarr, antimeridian_mode):
+        if antimeridian_mode == "global_extents":
+            xmin, xmax = (None, None)
+        else:
+            wrapped_array = xarr % 360
+            xmin = np.nanmin(wrapped_array)
+            xmax = np.nanmax(wrapped_array)
+            if hasattr(wrapped_array, "compute"):
+                xmin, xmax = da.compute(xmin, xmax)
+            if antimeridian_mode == "modify_crs":
+                xmin -= 180
+                xmax -= 180
+        return xmin, xmax
+
 
 def _invproj(data_x, data_y, proj_dict):
     """Perform inverse projection."""
@@ -2373,13 +2442,15 @@ class AreaDefinition(_ProjectionDefinition):
             lons, lats = res[0], res[1]
             return lons, lats
 
+        proj_kwargs = {}
         if nprocs > 1:
             target_proj = Proj_MP(self.crs)
+            proj_kwargs["nprocs"] = nprocs
         else:
             target_proj = Proj(self.crs)
 
         # Get corresponding longitude and latitude values
-        lons, lats = target_proj(target_x, target_y, inverse=True, nprocs=nprocs)
+        lons, lats = target_proj(target_x, target_y, inverse=True, **proj_kwargs)
         lons = np.asanyarray(lons, dtype=dtype)
         lats = np.asanyarray(lats, dtype=dtype)
 


=====================================
pyresample/grid.py
=====================================
@@ -20,6 +20,7 @@
 from __future__ import absolute_import
 
 import numpy as np
+from pyproj import Proj
 
 from pyresample import _spatial_mp, geometry
 
@@ -107,13 +108,15 @@ def get_linesample(lons, lats, source_area_def, nprocs=1):
         Arrays for resampling area by array indexing
     """
     # Proj.4 definition of source area projection
+    proj_kwargs = {}
     if nprocs > 1:
         source_proj = _spatial_mp.Proj_MP(**source_area_def.proj_dict)
+        proj_kwargs["nprocs"] = nprocs
     else:
-        source_proj = _spatial_mp.Proj(**source_area_def.proj_dict)
+        source_proj = Proj(**source_area_def.proj_dict)
 
     # get cartesian projection values from longitude and latitude
-    source_x, source_y = source_proj(lons, lats, nprocs=nprocs)
+    source_x, source_y = source_proj(lons, lats, **proj_kwargs)
 
     # Find corresponding pixels (element by element conversion of ndarrays)
     source_pixel_x = (source_area_def.pixel_offset_x +


=====================================
pyresample/test/conftest.py
=====================================
@@ -51,7 +51,7 @@ def _euro_lonlats_dask():
 
 def _antimeridian_lonlats():
     lons = create_test_longitude(172.0, 190.0, SRC_SWATH_2D_SHAPE)
-    lons[lons > 180.0] = lons - 360.0
+    lons[lons > 180.0] -= 360.0
     lats = create_test_latitude(25.0, 33.0, SRC_SWATH_2D_SHAPE)
     return lons, lats
 
@@ -99,6 +99,19 @@ def swath_def_2d_numpy_antimeridian():
     return SwathDefinition(lons, lats)
 
 
+ at pytest.fixture(scope="session")
+def swath_def_2d_xarray_dask_antimeridian():
+    """Create a SwathDefinition with DataArrays(dask) arrays (200, 1500) over the antimeridian.
+
+    Longitude values go from positive values to negative values as they cross -180/180.
+
+    """
+    lons, lats = _antimeridian_lonlats()
+    lons = xr.DataArray(lons, dims=("y", "x"))
+    lats = xr.DataArray(lats, dims=("y", "x"))
+    return SwathDefinition(lons, lats)
+
+
 @pytest.fixture(scope="session")
 def area_def_lcc_conus_1km():
     """Create an AreaDefinition with an LCC projection over CONUS (1500, 2000)."""
@@ -150,6 +163,22 @@ def area_def_stere_target():
     )
 
 
+ at pytest.fixture(scope="session")
+def area_def_lonlat_pm180_target():
+    """Create an AreaDefinition with a geographic lon/lat projection with prime meridian at 180 (800, 850)."""
+    return AreaDefinition(
+        'lonlat_pm180', '', '',
+        {
+            'proj': 'longlat',
+            'pm': '180.0',
+            'datum': 'WGS84',
+            'no_defs': None,
+        },
+        DST_AREA_SHAPE[1], DST_AREA_SHAPE[0],
+        [-20.0, 20.0, 20.0, 35.0]
+    )
+
+
 @pytest.fixture(scope="session")
 def coord_def_2d_float32_dask():
     """Create a 2D CoordinateDefinition of dask arrays (4, 3)."""


=====================================
pyresample/test/test_bilinear.py
=====================================
@@ -27,6 +27,7 @@ import unittest
 from unittest import mock
 
 import numpy as np
+from pyproj import Proj
 
 
 class TestNumpyBilinear(unittest.TestCase):
@@ -226,7 +227,6 @@ class TestNumpyBilinear(unittest.TestCase):
 
     def test_get_input_xy(self):
         """Test calculation of input xy-coordinates."""
-        from pyresample._spatial_mp import Proj
         from pyresample.bilinear._base import _get_input_xy
 
         proj = Proj(self.target_def.proj_str)
@@ -237,7 +237,6 @@ class TestNumpyBilinear(unittest.TestCase):
 
     def test_get_four_closest_corners(self):
         """Test calculation of bounding corners."""
-        from pyresample._spatial_mp import Proj
         from pyresample.bilinear._base import (
             _get_four_closest_corners,
             _get_input_xy,
@@ -842,7 +841,6 @@ class TestXarrayBilinear(unittest.TestCase):
 
     def test_get_input_xy(self):
         """Test computation of input X and Y coordinates in target proj."""
-        from pyresample._spatial_mp import Proj
         from pyresample.bilinear.xarr import _get_input_xy
 
         proj = Proj(self.target_def.proj_str)
@@ -860,7 +858,6 @@ class TestXarrayBilinear(unittest.TestCase):
         import dask.array as da
 
         from pyresample import CHUNK_SIZE
-        from pyresample._spatial_mp import Proj
         from pyresample.bilinear._base import _get_four_closest_corners
         from pyresample.bilinear.xarr import _get_input_xy
 
@@ -891,7 +888,6 @@ class TestXarrayBilinear(unittest.TestCase):
         import dask.array as da
 
         from pyresample import CHUNK_SIZE
-        from pyresample._spatial_mp import Proj
         from pyresample.bilinear._base import _get_corner, _get_input_xy
 
         proj = Proj(self.target_def.proj_str)


=====================================
pyresample/test/test_geometry.py
=====================================
@@ -26,7 +26,7 @@ import dask.array as da
 import numpy as np
 import pytest
 import xarray as xr
-from pyproj import CRS
+from pyproj import CRS, Proj
 
 from pyresample import geo_filter, geometry, parse_area_file
 from pyresample.geometry import (
@@ -1134,7 +1134,6 @@ class Test(unittest.TestCase):
                                       proj_dict,
                                       x_size, y_size,
                                       area_extent)
-        from pyresample._spatial_mp import Proj
         p__ = Proj(proj_dict)
         lon_ul, lat_ul = p__(1000000, 50000, inverse=True)
         lon_ur, lat_ur = p__(1050000, 50000, inverse=True)
@@ -2172,6 +2171,10 @@ class TestStackedAreaDefinition:
         adef.assert_called_once_with(area1.area_id, area1.description, area1.proj_id,
                                      area1.crs, area1.width, y_size, area_extent)
 
+
+class TestCreateAreaDef:
+    """Test the 'create_area_def' utility function."""
+
     @staticmethod
     def _compare_area_defs(actual, expected, use_proj4=False):
         if use_proj4:
@@ -2524,6 +2527,54 @@ class TestDynamicAreaDefinition:
         assert x_size == 5
         assert y_size == 5
 
+    @pytest.mark.parametrize(
+        (
+            "antimeridian_mode",
+            "expected_shape",
+            "expected_extents",
+            "include_proj_components",
+            "exclude_proj_components"
+        ),
+        [
+            (None, (21, 59), (164.75, 24.75, 194.25, 35.25), tuple(), ("+pm=180",)),
+            ("modify_extents", (21, 59), (164.75, 24.75, 194.25, 35.25), tuple(), ("+pm=180",)),
+            ("modify_crs", (21, 59), (164.75 - 180.0, 24.75, 194.25 - 180.0, 35.25), ("+pm=180",), tuple()),
+            ("global_extents", (21, 720), (-180.0, 24.75, 180.0, 35.25), tuple(), ("+pm=180",)),
+        ],
+    )
+    @pytest.mark.parametrize("use_dask", [False, True])
+    def test_antimeridian_mode(self,
+                               use_dask,
+                               antimeridian_mode,
+                               expected_shape,
+                               expected_extents,
+                               include_proj_components,
+                               exclude_proj_components):
+        """Test that antimeridian_mode affects the result."""
+        dyn_area = geometry.DynamicAreaDefinition('test_area', '', {'proj': 'longlat'})
+        lons, lats = _get_fake_antimeridian_lonlats(use_dask)
+        area = dyn_area.freeze(lonslats=(lons, lats), resolution=0.5, antimeridian_mode=antimeridian_mode)
+        proj_str = area.crs.to_proj4()
+
+        assert area.shape == expected_shape
+        np.testing.assert_allclose(area.area_extent, expected_extents)
+        for include_comp in include_proj_components:
+            assert include_comp in proj_str
+        for exclude_comp in exclude_proj_components:
+            assert exclude_comp not in proj_str
+
+
+def _get_fake_antimeridian_lonlats(use_dask: bool) -> tuple:
+    lon_min = 165
+    lon_max = 195
+    lons = np.arange(lon_min, lon_max, dtype=np.float64)
+    lons[lons >= 180] -= 360.0
+    lats = np.linspace(25.0, 35.0, lons.size, dtype=np.float64)
+    if use_dask:
+        lons = da.from_array(lons, chunks=lons.size // 3)
+        lats = da.from_array(lats, chunks=lons.size // 3)
+    return lons, lats
+
 
 class TestCrop(unittest.TestCase):
     """Test the area helpers."""


=====================================
pyresample/test/test_resamplers/test_nearest.py
=====================================
@@ -128,6 +128,21 @@ class TestNearestNeighborResampler:
         assert cross_sum == expected
         assert res.shape == resampler.target_geo_def.shape
 
+    def test_nearest_swath_2d_to_area_1n_pm180(self, swath_def_2d_xarray_dask_antimeridian, data_2d_float32_xarray_dask,
+                                               area_def_lonlat_pm180_target):
+        """Test 2D swath definition to 2D area definition; 1 neighbor; output prime meridian at 180 degrees."""
+        resampler = KDTreeNearestXarrayResampler(
+            swath_def_2d_xarray_dask_antimeridian, area_def_lonlat_pm180_target)
+        res = resampler.resample(data_2d_float32_xarray_dask, radius_of_influence=50000)
+        assert isinstance(res, xr.DataArray)
+        assert isinstance(res.data, da.Array)
+        _check_common_metadata(res, isinstance(area_def_lonlat_pm180_target, AreaDefinition))
+        res = res.values
+        cross_sum = float(np.nansum(res))
+        expected = 115591.0
+        assert cross_sum == expected
+        assert res.shape == resampler.target_geo_def.shape
+
     def test_nearest_area_2d_to_area_1n_no_roi(self, area_def_stere_source, data_2d_float32_xarray_dask,
                                                area_def_stere_target):
         """Test 2D area definition to 2D area definition; 1 neighbor, no radius of influence."""


=====================================
pyresample/utils/proj4.py
=====================================
@@ -105,7 +105,12 @@ def _transform_dask_chunk(x, y, crs_from, crs_to, kwargs, transform_kwargs):
 
 
 class DaskFriendlyTransformer:
-    """Wrapper around the pyproj Transformer class that uses dask."""
+    """Wrapper around the pyproj Transformer class that uses dask.
+
+    If the provided arrays are not dask arrays, they are converted to numpy
+    arrays and pyproj will be called directly (dask is not used).
+
+    """
 
     def __init__(self, src_crs, dst_crs, **kwargs):
         """Initialize the transformer with CRS objects.
@@ -128,6 +133,12 @@ class DaskFriendlyTransformer:
         import dask.array as da
         crs_from = self.src_crs
         crs_to = self.dst_crs
+
+        if not hasattr(x, "compute"):
+            x = np.asarray(x)
+            y = np.asarray(y)
+            return self._transform_numpy(x, y, **kwargs)
+
         # CRS objects aren't thread-safe until pyproj 3.1+
         # convert to WKT strings to be safe
         result = da.map_blocks(_transform_dask_chunk, x, y,
@@ -139,3 +150,7 @@ class DaskFriendlyTransformer:
         x = result[..., 0]
         y = result[..., 1]
         return x, y
+
+    def _transform_numpy(self, x, y, **kwargs):
+        transformer = PROJTransformer.from_crs(self.src_crs, self.dst_crs, **self.kwargs)
+        return transformer.transform(x, y, **kwargs)


=====================================
pyresample/version.py
=====================================
@@ -25,9 +25,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.24.1)"
-    git_full = "c930465a94f63c9d6a9db1b0c3db033d0369a396"
-    git_date = "2022-07-06 19:04:53 -0500"
+    git_refnames = " (tag: v1.25.1)"
+    git_full = "164b714a9d00a2b5e103a2214400248047dc8424"
+    git_date = "2022-08-02 07:46:40 -0500"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 



View it on GitLab: https://salsa.debian.org/debian-gis-team/pyresample/-/commit/f72c4ee988c57a8756880501b8eac661d1116a33

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyresample/-/commit/f72c4ee988c57a8756880501b8eac661d1116a33
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/20220805/d41d9e81/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list