Source code for esmvalcore.preprocessor._regrid_esmpy

# -*- coding: utf-8 -*-
"""Provides regridding for irregular grids."""

try:
    import esmpy
except ImportError as exc:
    # Prior to v8.4.0, `esmpy`` could be imported as `ESMF`.
    try:
        import ESMF as esmpy  # noqa: N811
    except ImportError:
        raise exc from None
import warnings

import iris
import numpy as np
from iris.cube import Cube

from esmvalcore.exceptions import ESMValCoreDeprecationWarning

from ._mapping import get_empty_data, map_slices, ref_to_dims_index

ESMF_MANAGER = esmpy.Manager(debug=False)

ESMF_LON, ESMF_LAT = 0, 1

ESMF_REGRID_METHODS = {
    "linear": esmpy.RegridMethod.BILINEAR,
    "area_weighted": esmpy.RegridMethod.CONSERVE,
    "nearest": esmpy.RegridMethod.NEAREST_STOD,
}

MASK_REGRIDDING_MASK_VALUE = {
    esmpy.RegridMethod.BILINEAR: np.array([1]),
    esmpy.RegridMethod.CONSERVE: np.array([1]),
    esmpy.RegridMethod.NEAREST_STOD: np.array([]),
}

# ESMF_REGRID_METHODS = {
#     'bilinear': esmpy.RegridMethod.BILINEAR,
#     'patch': esmpy.RegridMethod.PATCH,
#     'conserve': esmpy.RegridMethod.CONSERVE,
#     'nearest_stod': esmpy.RegridMethod.NEAREST_STOD,
#     'nearest_dtos': esmpy.RegridMethod.NEAREST_DTOS,
# }


[docs] class ESMPyRegridder: """General ESMPy regridder. Does not support lazy regridding nor weights caching. .. deprecated:: 2.12.0 This regridder has been deprecated and is scheduled for removal in version 2.14.0. Please use :class:`~esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` to create an :doc:`esmf_regrid:index` regridder instead. Parameters ---------- src_cube: Cube defining the source grid. tgt_cube: Cube defining the target grid. method: Regridding algorithm. Must be one of `linear`, `area_weighted`, `nearest`. mask_threshold: Threshold used to regrid mask of input cube. """ def __init__( self, src_cube: Cube, tgt_cube: Cube, method: str = "linear", mask_threshold: float = 0.99, ): """Initialize class instance.""" # These regridders are not lazy, so load source and target data once. src_cube.data # # noqa: B018 pylint: disable=pointless-statement tgt_cube.data # # noqa: B018 pylint: disable=pointless-statement self.src_cube = src_cube self.tgt_cube = tgt_cube self.method = method self.mask_threshold = mask_threshold def __call__(self, cube: Cube) -> Cube: """Perform regridding. Parameters ---------- cube: Cube to be regridded. Returns ------- Cube Regridded cube. """ # These regridders are not lazy, so load source data once. cube.data # # noqa: B018 pylint: disable=pointless-statement src_rep, dst_rep = get_grid_representants(cube, self.tgt_cube) regridder = build_regridder( src_rep, dst_rep, self.method, mask_threshold=self.mask_threshold ) result = map_slices(cube, regridder, src_rep, dst_rep) return result
class _ESMPyScheme: """General irregular regridding scheme. This class can be used in :meth:`iris.cube.Cube.regrid`. Note ---- See `ESMPy <http://www.earthsystemmodeling.org/ esmf_releases/non_public/ESMF_7_0_0/esmpy_doc/html/ RegridMethod.html#ESMF.api.constants.RegridMethod>`__ for more details on this. Parameters ---------- mask_threshold: Threshold used to regrid mask of source cube. """ _METHOD = "" def __init__(self, mask_threshold: float = 0.99): """Initialize class instance.""" msg = ( "The `esmvalcore.preprocessor.regrid_schemes." f"{self.__class__.__name__}' regridding scheme has been " "deprecated in ESMValCore version 2.12.0 and is scheduled for " "removal in version 2.14.0. Please use " "`esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` " "instead." ) warnings.warn(msg, ESMValCoreDeprecationWarning, stacklevel=2) self.mask_threshold = mask_threshold def __repr__(self) -> str: """Return string representation of class.""" return ( f"{self.__class__.__name__}(mask_threshold={self.mask_threshold})" ) def regridder(self, src_cube: Cube, tgt_cube: Cube) -> ESMPyRegridder: """Get regridder. Parameters ---------- src_cube: Cube defining the source grid. tgt_cube: Cube defining the target grid. Returns ------- ESMPyRegridder Regridder instance. """ return ESMPyRegridder( src_cube, tgt_cube, method=self._METHOD, mask_threshold=self.mask_threshold, )
[docs] class ESMPyAreaWeighted(_ESMPyScheme): """ESMPy area-weighted regridding scheme. This class can be used in :meth:`iris.cube.Cube.regrid`. Does not support lazy regridding. .. deprecated:: 2.12.0 This scheme has been deprecated and is scheduled for removal in version 2.14.0. Please use :class:`~esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` instead. """ _METHOD = "area_weighted"
[docs] class ESMPyLinear(_ESMPyScheme): """ESMPy bilinear regridding scheme. This class can be used in :meth:`iris.cube.Cube.regrid`. Does not support lazy regridding. .. deprecated:: 2.12.0 This scheme has been deprecated and is scheduled for removal in version 2.14.0. Please use :class:`~esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` instead. """ _METHOD = "linear"
[docs] class ESMPyNearest(_ESMPyScheme): """ESMPy nearest-neighbor regridding scheme. This class can be used in :meth:`iris.cube.Cube.regrid`. Does not support lazy regridding. .. deprecated:: 2.12.0 This scheme has been deprecated and is scheduled for removal in version 2.14.0. Please use :class:`~esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` instead. """ _METHOD = "nearest"
def cf_2d_bounds_to_esmpy_corners(bounds, circular): """Convert cf style 2d bounds to normal (esmpy style) corners.""" no_lat_points, no_lon_points = bounds.shape[:2] no_lat_bounds = no_lat_points + 1 if circular: no_lon_bounds = no_lon_points else: no_lon_bounds = no_lon_points + 1 esmpy_corners = np.empty((no_lon_bounds, no_lat_bounds)) esmpy_corners[:no_lon_points, :no_lat_points] = bounds[:, :, 0].T esmpy_corners[:no_lon_points, no_lat_points:] = bounds[-1:, :, 3].T esmpy_corners[no_lon_points:, :no_lat_points] = bounds[:, -1:, 1].T esmpy_corners[no_lon_points:, no_lat_points:] = bounds[-1:, -1:, 2].T return esmpy_corners def coords_iris_to_esmpy(lat, lon, circular): """Build ESMF compatible coordinate information from iris coords.""" dim = lat.ndim if lon.ndim != dim: msg = "Different dimensions in latitude({}) and longitude({}) coords." raise ValueError(msg.format(lat.ndim, lon.ndim)) if dim == 1: for coord in [lat, lon]: if not coord.has_bounds(): coord.guess_bounds() esmpy_lat, esmpy_lon = np.meshgrid(lat.points, lon.points) lat_corners = np.concatenate([lat.bounds[:, 0], lat.bounds[-1:, 1]]) if circular: lon_corners = lon.bounds[:, 0] else: lon_corners = np.concatenate( [lon.bounds[:, 0], lon.bounds[-1:, 1]] ) esmpy_lat_corners, esmpy_lon_corners = np.meshgrid( lat_corners, lon_corners ) elif dim == 2: esmpy_lat, esmpy_lon = lat.points.T.copy(), lon.points.T.copy() esmpy_lat_corners = cf_2d_bounds_to_esmpy_corners(lat.bounds, circular) esmpy_lon_corners = cf_2d_bounds_to_esmpy_corners(lon.bounds, circular) else: raise NotImplementedError( f"Coord dimension is {dim}. Expected 1 or 2." ) return esmpy_lat, esmpy_lon, esmpy_lat_corners, esmpy_lon_corners def get_grid( esmpy_lat, esmpy_lon, esmpy_lat_corners, esmpy_lon_corners, circular ): """Build EMSF grid from given coordinate information.""" if circular: num_peri_dims = 1 else: num_peri_dims = 0 grid = esmpy.Grid( np.vstack(esmpy_lat.shape), num_peri_dims=num_peri_dims, staggerloc=[esmpy.StaggerLoc.CENTER], ) grid.get_coords(ESMF_LON)[...] = esmpy_lon grid.get_coords(ESMF_LAT)[...] = esmpy_lat grid.add_coords([esmpy.StaggerLoc.CORNER]) grid_lon_corners = grid.get_coords( ESMF_LON, staggerloc=esmpy.StaggerLoc.CORNER ) grid_lat_corners = grid.get_coords( ESMF_LAT, staggerloc=esmpy.StaggerLoc.CORNER ) grid_lon_corners[...] = esmpy_lon_corners grid_lat_corners[...] = esmpy_lat_corners grid.add_item(esmpy.GridItem.MASK, esmpy.StaggerLoc.CENTER) return grid def is_lon_circular(lon): """Determine if longitudes are circular.""" if isinstance(lon, iris.coords.DimCoord): circular = lon.circular elif isinstance(lon, iris.coords.AuxCoord): if lon.ndim == 1: seam = lon.bounds[-1, 1] - lon.bounds[0, 0] elif lon.ndim == 2: seam = lon.bounds[1:-1, -1, (1, 2)] - lon.bounds[1:-1, 0, (0, 3)] else: raise NotImplementedError( "AuxCoord longitude is higher dimensional than 2d. Giving up." ) circular = np.all(abs(seam) % 360.0 < 1.0e-3) else: raise ValueError( "longitude is neither DimCoord nor AuxCoord. Giving up." ) return circular def cube_to_empty_field(cube): """Build an empty ESMF field from a cube.""" lat = cube.coord("latitude") lon = cube.coord("longitude") circular = is_lon_circular(lon) esmpy_coords = coords_iris_to_esmpy(lat, lon, circular) grid = get_grid(*esmpy_coords, circular=circular) field = esmpy.Field( grid, name=cube.long_name, staggerloc=esmpy.StaggerLoc.CENTER ) return field def get_representant(cube, ref_to_slice): """Get a representative slice from a cube.""" slice_dims = ref_to_dims_index(cube, ref_to_slice) rep_ind = [0] * cube.ndim for dim in slice_dims: rep_ind[dim] = slice(None, None) rep_ind = tuple(rep_ind) return cube[rep_ind] def regrid_mask_2d(src_data, regridding_arguments, mask_threshold): """Regrid the mask from the source field to the destination grid.""" src_field = regridding_arguments["srcfield"] dst_field = regridding_arguments["dstfield"] regrid_method = regridding_arguments["regrid_method"] original_src_mask = np.ma.getmaskarray(src_data) src_field.data[...] = ~original_src_mask.T src_mask = src_field.grid.get_item( esmpy.GridItem.MASK, esmpy.StaggerLoc.CENTER ) src_mask[...] = original_src_mask.T center_mask = dst_field.grid.get_item( esmpy.GridItem.MASK, esmpy.StaggerLoc.CENTER ) center_mask[...] = 0 mask_regridder = esmpy.Regrid( src_mask_values=MASK_REGRIDDING_MASK_VALUE[regrid_method], dst_mask_values=np.array([]), **regridding_arguments, ) regr_field = mask_regridder(src_field, dst_field) dst_mask = regr_field.data[...].T < mask_threshold center_mask[...] = dst_mask.T if not dst_mask.any(): dst_mask = np.ma.nomask return dst_mask def build_regridder_2d(src_rep, dst_rep, regrid_method, mask_threshold): """Build regridder for 2d regridding.""" dst_field = cube_to_empty_field(dst_rep) src_field = cube_to_empty_field(src_rep) regridding_arguments = { "srcfield": src_field, "dstfield": dst_field, "regrid_method": regrid_method, "unmapped_action": esmpy.UnmappedAction.IGNORE, "ignore_degenerate": True, } dst_mask = regrid_mask_2d( src_rep.data, regridding_arguments, mask_threshold ) field_regridder = esmpy.Regrid( src_mask_values=np.array([1]), dst_mask_values=np.array([1]), **regridding_arguments, ) def regridder(src): """Regrid 2d for irregular grids.""" res = get_empty_data(dst_rep.shape, src.dtype) data = src.data if np.ma.is_masked(data): data = data.data src_field.data[...] = data.T regr_field = field_regridder(src_field, dst_field) res.data[...] = regr_field.data[...].T res.mask[...] = dst_mask return res return regridder def build_regridder_3d(src_rep, dst_rep, regrid_method, mask_threshold): # The necessary refactoring will be done for the full 3d regridding. """Build regridder for 2.5d regridding.""" esmf_regridders = [] no_levels = src_rep.shape[0] for level in range(no_levels): esmf_regridders.append( build_regridder_2d( src_rep[level], dst_rep[level], regrid_method, mask_threshold ) ) def regridder(src): """Regrid 2.5d for irregular grids.""" res = get_empty_data(dst_rep.shape, src.dtype) for i, esmf_regridder in enumerate(esmf_regridders): res[i, ...] = esmf_regridder(src[i]) return res return regridder def build_regridder(src_rep, dst_rep, method, mask_threshold=0.99): """Build regridders from representants.""" regrid_method = ESMF_REGRID_METHODS[method] if src_rep.ndim == 2: regridder = build_regridder_2d( src_rep, dst_rep, regrid_method, mask_threshold ) elif src_rep.ndim == 3: regridder = build_regridder_3d( src_rep, dst_rep, regrid_method, mask_threshold ) return regridder def get_grid_representant(cube, horizontal_only=False): """Extract the spatial grid from a cube.""" horizontal_slice = ["latitude", "longitude"] ref_to_slice = horizontal_slice if not horizontal_only: try: cube_z_coord = cube.coord(axis="Z") n_zdims = len(cube.coord_dims(cube_z_coord)) if n_zdims == 0: # scalar z coordinate, go on with 2d regridding pass elif n_zdims == 1: ref_to_slice = [cube_z_coord] + horizontal_slice else: raise ValueError("Cube has multidimensional Z coordinate.") except iris.exceptions.CoordinateNotFoundError: # no z coordinate, go on with 2d regridding pass return get_representant(cube, ref_to_slice) def get_grid_representants(src, dst): """ Construct cubes representing the source and destination grid. This method constructs two new cubes that representant the grids, i.e. the spatial dimensions of the given cubes. Parameters ---------- src: :class:`iris.cube.Cube` Cube to be regridded. Typically a time series of 2d or 3d slices. dst: :class:`iris.cube.Cube` Cube defining the destination grid. Usually just a 2d or 3d cube. Returns ------- tuple of :class:`iris.cube.Cube`: A tuple containing two cubes, representing the source grid and the destination grid, respectively. """ src_rep = get_grid_representant(src) dst_horiz_rep = get_grid_representant(dst, horizontal_only=True) if src_rep.ndim == 3: dst_shape = (src_rep.shape[0],) dim_coords = [src_rep.coord(dimensions=[0], dim_coords=True)] else: dst_shape = tuple() dim_coords = [] dst_shape += dst_horiz_rep.shape dim_coords += dst_horiz_rep.coords(dim_coords=True) dim_coords_and_dims = [(c, i) for i, c in enumerate(dim_coords)] aux_coords_and_dims = [] for coord in dst_horiz_rep.aux_coords: dims = dst_horiz_rep.coord_dims(coord) if not dims: continue if src_rep.ndim == 3: dims = [dim + 1 for dim in dims] aux_coords_and_dims.append((coord, dims)) # Add scalar dimensions of source cube to target for scalar_coord in src.coords(dimensions=()): aux_coords_and_dims.append((scalar_coord, ())) dst_rep = iris.cube.Cube( data=get_empty_data(dst_shape, src.dtype), standard_name=src.standard_name, long_name=src.long_name, var_name=src.var_name, units=src.units, attributes=src.attributes, cell_methods=src.cell_methods, dim_coords_and_dims=dim_coords_and_dims, aux_coords_and_dims=aux_coords_and_dims, ) return src_rep, dst_rep