# Copyright Iris contributors
#
# This file is part of Iris and is released under the BSD license.
# See LICENSE in the root of the repository for full licensing details.
"""Regridding functions.
Notes
-----
.. deprecated:: 3.2.0
This package will be removed in a future release.
The PointInCell class has now moved to :class:`iris.analysis.PointInCell`.
All the other content will be withdrawn.
If you still use any of this, please contact the Iris Developers to
discuss how to replace it or to retain it.
"""
import copy
import functools
import warnings
import cartopy.crs as ccrs
import numpy as np
import scipy.interpolate
from iris._deprecation import warn_deprecated
from iris.analysis._area_weighted import (
_regrid_area_weighted_rectilinear_src_and_grid__perform,
_regrid_area_weighted_rectilinear_src_and_grid__prepare,
)
from iris.analysis._interpolation import get_xy_coords, get_xy_dim_coords, snapshot_grid
from iris.analysis._regrid import (
_regrid_weighted_curvilinear_to_rectilinear__perform,
_regrid_weighted_curvilinear_to_rectilinear__prepare,
)
import iris.analysis.cartography
import iris.coord_systems
import iris.cube
from iris.util import _meshgrid
from iris.warnings import IrisImpossibleUpdateWarning
wmsg = (
"The 'iris.experimental.regrid' package is deprecated since version 3.2, "
"and will be removed in a future release. The PointInCell class has now "
"moved into iris.analysis. All its other content will be withdrawn. "
"If you still use any of this, please contact the Iris Developers to "
"discuss how to replace it or to retain it (reverse the deprecation)."
)
warn_deprecated(wmsg)
[docs]
def regrid_area_weighted_rectilinear_src_and_grid(src_cube, grid_cube, mdtol=0):
"""Regrid using the area weighted mean of data values.
Return a new cube with data values calculated using the area weighted
mean of data values from src_grid regridded onto the horizontal grid of
grid_cube.
This function requires that the horizontal grids of both cubes are
rectilinear (i.e. expressed in terms of two orthogonal 1D coordinates)
and that these grids are in the same coordinate system. This function
also requires that the coordinates describing the horizontal grids
all have bounds.
Parameters
----------
src_cube : :class:`iris.cube.Cube`
An instance of :class:`iris.cube.Cube` that supplies the data,
metadata and coordinates.
grid_cube : :class:`iris.cube.Cube`
An instance of :class:`iris.cube.Cube` that supplies the desired
horizontal grid definition.
mdtol : int, default=0
Tolerance of missing data. The value returned in each element of the
returned cube's data array will be masked if the fraction of masked
data in the overlapping cells of the source cube exceeds mdtol. This
fraction is calculated based on the area of masked cells within each
target cell. mdtol=0 means no missing data is tolerated while mdtol=1
will mean the resulting element will be masked if and only if all the
overlapping cells of the source cube are masked. Defaults to 0.
Returns
-------
A new :class:`iris.cube.Cube` instance.
Notes
-----
Elements in data array of the returned cube that lie either partially
or entirely outside of the horizontal extent of the src_cube will
be masked irrespective of the value of mdtol.
.. deprecated:: 3.2.0
This function is scheduled to be removed in a future release.
Please use :meth:`iris.cube.Cube.regrid` with the
:class:`iris.analysis.AreaWeighted` scheme instead : this is an exact
replacement.
For example :
.. code::
result = src_cube.regrid(grid_cube, AreaWeighted())
"""
wmsg = (
"The function "
"'iris.experimental.regrid."
"regrid_area_weighted_rectilinear_src_and_grid' "
"has been deprecated, and will be removed in a future release. "
"Please consult the docstring for details."
)
warn_deprecated(wmsg)
regrid_info = _regrid_area_weighted_rectilinear_src_and_grid__prepare(
src_cube, grid_cube
)
result = _regrid_area_weighted_rectilinear_src_and_grid__perform(
src_cube, regrid_info, mdtol
)
return result
[docs]
def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube):
r"""Regrid using the weighted mean and the weights.
Return a new cube with the data values calculated using the weighted
mean of data values from :data:`src_cube` and the weights from
:data:`weights` regridded onto the horizontal grid of :data:`grid_cube`.
This function requires that the :data:`src_cube` has a horizontal grid
defined by a pair of X- and Y-axis coordinates which are mapped over the
same cube dimensions, thus each point has an individually defined X and
Y coordinate value. The actual dimensions of these coordinates are of
no significance.
The :data:`src_cube` grid cube must have a normal horizontal grid,
i.e. expressed in terms of two orthogonal 1D horizontal coordinates.
Both grids must be in the same coordinate system, and the :data:`grid_cube`
must have horizontal coordinates that are both bounded and contiguous.
Note that, for any given target :data:`grid_cube` cell, only the points
from the :data:`src_cube` that are bound by that cell will contribute to
the cell result. The bounded extent of the :data:`src_cube` will not be
considered here.
A target :data:`grid_cube` cell result will be calculated as,
:math:`\sum (src\_cube.data_{ij} * weights_{ij}) / \sum weights_{ij}`, for
all :math:`ij` :data:`src_cube` points that are bound by that cell.
Warnings
--------
All coordinates that span the :data:`src_cube` that don't define
the horizontal curvilinear grid will be ignored.
Parameters
----------
src_cube : :class:`iris.cube.Cube`
A :class:`iris.cube.Cube` instance that defines the source
variable grid to be regridded.
weights : array or None
A :class:`numpy.ndarray` instance that defines the weights
for the source variable grid cells. Must have the same shape
as the X and Y coordinates. If weights is None, all-ones will be used.
grid_cube : :class:`iris.cube.Cube`
A :class:`iris.cube.Cube` instance that defines the target
rectilinear grid.
Returns
-------
A :class:`iris.cube.Cube` instance.
Notes
-----
.. deprecated:: 3.2.0
This function is scheduled to be removed in a future release.
Please use :meth:`iris.cube.Cube.regrid` with the
:class:`iris.analysis.PointInCell` scheme instead : this is an exact
replacement.
For example :
.. code::
result = src_cube.regrid(grid_cube, PointInCell())
"""
wmsg = (
"The function "
"'iris.experimental.regrid."
"regrid_weighted_curvilinear_to_rectilinear' "
"has been deprecated, and will be removed in a future release. "
"Please consult the docstring for details."
)
warn_deprecated(wmsg)
regrid_info = _regrid_weighted_curvilinear_to_rectilinear__prepare(
src_cube, weights, grid_cube
)
result = _regrid_weighted_curvilinear_to_rectilinear__perform(src_cube, regrid_info)
return result
[docs]
class PointInCell:
"""Describe the point-in-cell regridding scheme.
This class describes the point-in-cell regridding scheme for use
typically with :meth:`iris.cube.Cube.regrid()`.
.. warning::
This class is now **disabled**.
The functionality has been moved to
:class:`iris.analysis.PointInCell`.
"""
def __init__(self, weights=None):
"""Point-in-cell regridding scheme for regridding over one or more orthogonal coordinates.
.. warning::
This class is now **disabled**.
The functionality has been moved to
:class:`iris.analysis.PointInCell`.
"""
raise Exception(
'The class "iris.experimental.PointInCell" has been '
"moved, and is now in iris.analysis"
"\nPlease replace "
'"iris.experimental.PointInCell" with '
'"iris.analysis.PointInCell".'
)
class _ProjectedUnstructuredRegridder:
"""Regridding that uses scipy.interpolate.griddata."""
def __init__(self, src_cube, tgt_grid_cube, method, projection=None):
"""Create a regridder for conversions between the source and target grids.
Parameters
----------
src_cube : :class:`~iris.cube.Cube`
The :class:`~iris.cube.Cube` providing the source points.
tgt_grid_cube : :class:`~iris.cube.Cube`
The :class:`~iris.cube.Cube` providing the target grid.
method :
Either 'linear' or 'nearest'.
projection : optional
The projection in which the interpolation is performed. If None, a
PlateCarree projection is used. Defaults to None.
"""
# Validity checks.
if not isinstance(src_cube, iris.cube.Cube):
raise TypeError("'src_cube' must be a Cube")
if not isinstance(tgt_grid_cube, iris.cube.Cube):
raise TypeError("'tgt_grid_cube' must be a Cube")
# Snapshot the state of the target cube to ensure that the regridder
# is impervious to external changes to the original source cubes.
self._tgt_grid = snapshot_grid(tgt_grid_cube)
# Check the target grid units.
for coord in self._tgt_grid:
self._check_units(coord)
# Whether to use linear or nearest-neighbour interpolation.
if method not in ("linear", "nearest"):
msg = "Regridding method {!r} not supported.".format(method)
raise ValueError(msg)
self._method = method
src_x_coord, src_y_coord = get_xy_coords(src_cube)
if src_x_coord.coord_system != src_y_coord.coord_system:
raise ValueError(
"'src_cube' lateral geographic coordinates have "
"differing coordinate systems."
)
if src_x_coord.coord_system is None:
raise ValueError(
"'src_cube' lateral geographic coordinates have no coordinate system."
)
tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_grid_cube)
if tgt_x_coord.coord_system != tgt_y_coord.coord_system:
raise ValueError(
"'tgt_grid_cube' lateral geographic coordinates "
"have differing coordinate systems."
)
if tgt_x_coord.coord_system is None:
raise ValueError(
"'tgt_grid_cube' lateral geographic coordinates "
"have no coordinate system."
)
if projection is None:
globe = src_x_coord.coord_system.as_cartopy_globe()
projection = ccrs.Sinusoidal(globe=globe)
self._projection = projection
def _check_units(self, coord):
if coord.coord_system is None:
# No restriction on units.
pass
elif isinstance(
coord.coord_system,
(iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS),
):
# Units for lat-lon or rotated pole must be 'degrees'. Note
# that 'degrees_east' etc. are equal to 'degrees'.
if coord.units != "degrees":
msg = (
"Unsupported units for coordinate system. "
"Expected 'degrees' got {!r}.".format(coord.units)
)
raise ValueError(msg)
else:
# Units for other coord systems must be equal to metres.
if coord.units != "m":
msg = (
"Unsupported units for coordinate system. "
"Expected 'metres' got {!r}.".format(coord.units)
)
raise ValueError(msg)
@staticmethod
def _regrid(
src_data,
xy_dim,
src_x_coord,
src_y_coord,
tgt_x_coord,
tgt_y_coord,
projection,
method,
):
"""Regrid input data from the source to the target. Calculation is."""
# Transform coordinates into the projection the interpolation will be
# performed in.
src_projection = src_x_coord.coord_system.as_cartopy_projection()
projected_src_points = projection.transform_points(
src_projection, src_x_coord.points, src_y_coord.points
)
tgt_projection = tgt_x_coord.coord_system.as_cartopy_projection()
tgt_x, tgt_y = _meshgrid(tgt_x_coord.points, tgt_y_coord.points)
projected_tgt_grid = projection.transform_points(tgt_projection, tgt_x, tgt_y)
# Prepare the result data array.
# XXX TODO: Deal with masked src_data
(tgt_y_shape,) = tgt_y_coord.shape
(tgt_x_shape,) = tgt_x_coord.shape
tgt_shape = (
src_data.shape[:xy_dim]
+ (tgt_y_shape,)
+ (tgt_x_shape,)
+ src_data.shape[xy_dim + 1 :]
)
data = np.empty(tgt_shape, dtype=src_data.dtype)
iter_shape = list(src_data.shape)
iter_shape[xy_dim] = 1
for index in np.ndindex(tuple(iter_shape)):
src_index = list(index)
src_index[xy_dim] = slice(None)
src_subset = src_data[tuple(src_index)]
tgt_index = (
index[:xy_dim] + (slice(None), slice(None)) + index[xy_dim + 1 :]
)
data[tgt_index] = scipy.interpolate.griddata(
projected_src_points[..., :2],
src_subset,
(projected_tgt_grid[..., 0], projected_tgt_grid[..., 1]),
method=method,
)
data = np.ma.array(data, mask=np.isnan(data))
return data
def _create_cube(
self,
data,
src,
src_xy_dim,
src_x_coord,
src_y_coord,
grid_x_coord,
grid_y_coord,
regrid_callback,
):
"""Return a new Cube for the result of regridding the source Cube onto the new grid.
All the metadata and coordinates of the result Cube are copied from
the source Cube, with two exceptions:
* Grid dimension coordinates are copied from the grid Cube.
* Auxiliary coordinates which span the grid dimensions are
ignored, except where they provide a reference surface for an
:class:`iris.aux_factory.AuxCoordFactory`.
Parameters
----------
data :
The regridded data as an N-dimensional NumPy array.
src : :class:`~iris.cube.Cube`
The source Cube.
src_xy_dim :
The dimension the X and Y coord span within the source Cube.
src_x_coord :
The X coordinate (either :class:`iris.coords.AuxCoord` or
:class:`iris.coords.DimCoord`).
src_y_coord :
The Y coordinate (either :class:`iris.coords.AuxCoord` or
:class:`iris.coords.DimCoord`).
grid_x_coord :
The :class:`iris.coords.DimCoord` for the new grid's X
coordinate.
grid_y_coord :
The :class:`iris.coords.DimCoord` for the new grid's Y
coordinate.
regrid_callback :
The routine that will be used to calculate the interpolated
values of any reference surfaces.
Returns
-------
The new, regridded Cube.
"""
# Create a result cube with the appropriate metadata
result = iris.cube.Cube(data)
result.metadata = copy.deepcopy(src.metadata)
# Copy across all the coordinates which don't span the grid.
# Record a mapping from old coordinate IDs to new coordinates,
# for subsequent use in creating updated aux_factories.
coord_mapping = {}
def copy_coords(src_coords, add_method):
for coord in src_coords:
dims = src.coord_dims(coord)
if coord is src_x_coord:
coord = grid_x_coord
# Increase dimensionality to account for 1D coord being
# regridded onto 2D grid
dims = list(dims)
dims[0] += 1
dims = tuple(dims)
add_method = result.add_dim_coord
elif coord is src_y_coord:
coord = grid_y_coord
add_method = result.add_dim_coord
elif src_xy_dim in dims:
continue
result_coord = coord.copy()
add_method(result_coord, dims)
coord_mapping[id(coord)] = result_coord
copy_coords(src.dim_coords, result.add_dim_coord)
copy_coords(src.aux_coords, result.add_aux_coord)
def regrid_reference_surface(
src_surface_coord,
surface_dims,
src_xy_dim,
src_x_coord,
src_y_coord,
grid_x_coord,
grid_y_coord,
regrid_callback,
):
# Determine which of the reference surface's dimensions span the X
# and Y dimensions of the source cube.
surface_xy_dim = surface_dims.index(src_xy_dim)
surface = regrid_callback(
src_surface_coord.points,
surface_xy_dim,
src_x_coord,
src_y_coord,
grid_x_coord,
grid_y_coord,
)
surface_coord = src_surface_coord.copy(surface)
return surface_coord
# Copy across any AuxFactory instances, and regrid their reference
# surfaces where required.
for factory in src.aux_factories:
for coord in factory.dependencies.values():
if coord is None:
continue
dims = src.coord_dims(coord)
if src_xy_dim in dims:
result_coord = regrid_reference_surface(
coord,
dims,
src_xy_dim,
src_x_coord,
src_y_coord,
grid_x_coord,
grid_y_coord,
regrid_callback,
)
result.add_aux_coord(result_coord, (dims[0], dims[0] + 1))
coord_mapping[id(coord)] = result_coord
try:
result.add_aux_factory(factory.updated(coord_mapping))
except KeyError:
msg = (
"Cannot update aux_factory {!r} because of dropped"
" coordinates.".format(factory.name())
)
warnings.warn(msg, category=IrisImpossibleUpdateWarning)
return result
def __call__(self, src_cube):
"""Regrid to the target grid.
Regrid this :class:`~iris.cube.Cube` on to the target grid of
this :class:`UnstructuredProjectedRegridder`.
The given cube must be defined with the same grid as the source
grid used to create this :class:`UnstructuredProjectedRegridder`.
Parameters
----------
src_cube : :class:`~iris.cube.Cube`
A :class:`~iris.cube.Cube` to be regridded.
Returns
-------
:class:`~iris.cube.Cube`
A cube defined with the horizontal dimensions of the target
and the other dimensions from this cube. The data values of
this cube will be converted to values on the new grid using
either nearest-neighbour or linear interpolation.
"""
# Validity checks.
if not isinstance(src_cube, iris.cube.Cube):
raise TypeError("'src' must be a Cube")
src_x_coord, src_y_coord = get_xy_coords(src_cube)
tgt_x_coord, tgt_y_coord = self._tgt_grid
src_cs = src_x_coord.coord_system
if src_x_coord.coord_system != src_y_coord.coord_system:
raise ValueError(
"'src' lateral geographic coordinates have "
"differing coordinate systems."
)
if src_cs is None:
raise ValueError(
"'src' lateral geographic coordinates have no coordinate system."
)
# Check the source grid units.
for coord in (src_x_coord, src_y_coord):
self._check_units(coord)
(src_x_dim,) = src_cube.coord_dims(src_x_coord)
(src_y_dim,) = src_cube.coord_dims(src_y_coord)
if src_x_dim != src_y_dim:
raise ValueError(
"'src' lateral geographic coordinates should map the same dimension."
)
src_xy_dim = src_x_dim
# Compute the interpolated data values.
data = self._regrid(
src_cube.data,
src_xy_dim,
src_x_coord,
src_y_coord,
tgt_x_coord,
tgt_y_coord,
self._projection,
method=self._method,
)
# Wrap up the data as a Cube.
regrid_callback = functools.partial(
self._regrid, method=self._method, projection=self._projection
)
new_cube = self._create_cube(
data,
src_cube,
src_xy_dim,
src_x_coord,
src_y_coord,
tgt_x_coord,
tgt_y_coord,
regrid_callback,
)
return new_cube
[docs]
class ProjectedUnstructuredLinear:
"""Describe the linear regridding scheme.
This class describes the linear regridding scheme which uses the
scipy.interpolate.griddata to regrid unstructured data on to a grid.
The source cube and the target cube will be projected into a common
projection for the scipy calculation to be performed.
"""
def __init__(self, projection=None):
"""Linear regridding scheme.
Linear regridding scheme that uses scipy.interpolate.griddata on
projected unstructured data.
Parameters
----------
projection : `cartopy.crs` instance, optional
The projection that the scipy calculation is performed in.
If None is given, a PlateCarree projection is used. Defaults to
None.
Notes
-----
.. deprecated:: 3.2.0
This class is scheduled to be removed in a future release, and no
replacement is currently planned.
If you make use of this functionality, please contact the Iris
Developers to discuss how to retain it (which could include
reversing the deprecation).
"""
self.projection = projection
wmsg = (
"The class iris.experimental.regrid.ProjectedUnstructuredLinear "
"has been deprecated, and will be removed in a future release. "
"Please consult the docstring for details."
)
warn_deprecated(wmsg)
[docs]
def regridder(self, src_cube, target_grid):
"""Create a linear regridder to perform regridding.
Creates a linear regridder to perform regridding, using
scipy.interpolate.griddata from unstructured source points to the
target grid. The regridding calculation is performed in the given
projection.
Typically you should use :meth:`iris.cube.Cube.regrid` for
regridding a cube. There are, however, some situations when
constructing your own regridder is preferable. These are detailed in
the :ref:`user guide <caching_a_regridder>`.
Does not support lazy regridding.
Parameters
----------
src_cube : :class:`~iris.cube.Cube`
The :class:`~iris.cube.Cube` defining the unstructured source
points.
target_grid : :class:`~iris.cube.Cube`
The :class:`~iris.cube.Cube` defining the target grid.
Returns
-------
callable
A callable with the interface::
`callable(cube)`
where `cube` is a cube with the same grid as `src_cube`
that is to be regridded to the `target_grid`.
"""
return _ProjectedUnstructuredRegridder(
src_cube, target_grid, "linear", self.projection
)
[docs]
class ProjectedUnstructuredNearest:
"""Describe the nearest regridding scheme which uses scipy.interpolate.griddata.
This class describes the nearest regridding scheme which uses the
scipy.interpolate.griddata to regrid unstructured data on to a grid.
The source cube and the target cube will be projected into a common
projection for the scipy calculation to be performed.
.. note::
The :class:`iris.analysis.UnstructuredNearest` scheme performs
essentially the same job. That calculation is more rigorously
correct and may be applied to larger data regions (including global).
This one however, where applicable, is substantially faster.
"""
def __init__(self, projection=None):
"""Nearest regridding scheme that uses scipy.interpolate.griddata on projected unstructured data.
Parameters
----------
projection : `cartopy.crs instance`, optional
The projection that the scipy calculation is performed in.
If None is given, a PlateCarree projection is used. Defaults to
None.
Notes
-----
.. deprecated:: 3.2.0
This class is scheduled to be removed in a future release, and no
exact replacement is currently planned.
Please use :class:`iris.analysis.UnstructuredNearest` instead, if
possible. If you have a need for this exact functionality, please
contact the Iris Developers to discuss how to retain it (which
could include reversing the deprecation).
"""
self.projection = projection
wmsg = (
"iris.experimental.regrid.ProjectedUnstructuredNearest has been "
"deprecated, and will be removed in a future release. "
"Please use 'iris.analysis.UnstructuredNearest' instead, where "
"possible. Consult the docstring for details."
)
warn_deprecated(wmsg)
[docs]
def regridder(self, src_cube, target_grid):
"""Create a nearest-neighbour regridder to perform regridding.
Create a nearest-neighbour regridder to perform regridding, using
scipy.interpolate.griddata from unstructured source points to the
target grid. The regridding calculation is performed in the given
projection.
Typically you should use :meth:`iris.cube.Cube.regrid` for
regridding a cube. There are, however, some situations when
constructing your own regridder is preferable. These are detailed in
the :ref:`user guide <caching_a_regridder>`.
Does not support lazy regridding.
Parameters
----------
src_cube : :class:`~iris.cube.Cube`
The :class:`~iris.cube.Cube` defining the unstructured source
points.
target_grid : :class:`~iris.cube.Cube`
The :class:`~iris.cube.Cube` defining the target grid.
Returns
-------
callable
A callable with the interface::
`callable(cube)`
where `cube` is a cube with the same grid as `src_cube`
that is to be regridded to the `target_grid`.
"""
return _ProjectedUnstructuredRegridder(
src_cube, target_grid, "nearest", self.projection
)