Skip to content

Commit a35d50d

Browse files
Add an iris-esmf-regrid based regridding scheme (#2457)
Co-authored-by: Manuel Schlund <32543114+schlunma@users.noreply.github.com>
1 parent 343368e commit a35d50d

File tree

17 files changed

+790
-207
lines changed

17 files changed

+790
-207
lines changed

doc/conf.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -438,20 +438,21 @@
438438

439439
# Configuration for intersphinx
440440
intersphinx_mapping = {
441-
'cf_units': ('https://cf-units.readthedocs.io/en/latest/', None),
441+
'cf_units': ('https://cf-units.readthedocs.io/en/stable/', None),
442442
'cftime': ('https://unidata.github.io/cftime/', None),
443443
'esmvalcore':
444444
(f'https://docs.esmvaltool.org/projects/ESMValCore/en/{rtd_version}/',
445445
None),
446446
'esmvaltool': (f'https://docs.esmvaltool.org/en/{rtd_version}/', None),
447+
'esmpy': ('https://earthsystemmodeling.org/esmpy_doc/release/latest/html/',
448+
None),
447449
'dask': ('https://docs.dask.org/en/stable/', None),
448450
'distributed': ('https://distributed.dask.org/en/stable/', None),
449-
'iris': ('https://scitools-iris.readthedocs.io/en/latest/', None),
450-
'iris-esmf-regrid': ('https://iris-esmf-regrid.readthedocs.io/en/latest',
451-
None),
451+
'iris': ('https://scitools-iris.readthedocs.io/en/stable/', None),
452+
'esmf_regrid': ('https://iris-esmf-regrid.readthedocs.io/en/stable/', None),
452453
'matplotlib': ('https://matplotlib.org/stable/', None),
453454
'numpy': ('https://numpy.org/doc/stable/', None),
454-
'pyesgf': ('https://esgf-pyclient.readthedocs.io/en/latest/', None),
455+
'pyesgf': ('https://esgf-pyclient.readthedocs.io/en/stable/', None),
455456
'python': ('https://docs.python.org/3/', None),
456457
'scipy': ('https://docs.scipy.org/doc/scipy/', None),
457458
}

doc/quickstart/find_data.rst

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -398,32 +398,17 @@ ESMValCore can automatically make native ICON data `UGRID
398398
loading the data.
399399
The UGRID conventions provide a standardized format to store data on
400400
unstructured grids, which is required by many software packages or tools to
401-
work correctly.
401+
work correctly and specifically by Iris to interpret the grid as a
402+
:ref:`mesh <iris:ugrid>`.
402403
An example is the horizontal regridding of native ICON data to a regular grid.
403-
While the built-in :ref:`nearest scheme <built-in regridding
404-
schemes>` can handle unstructured grids not in UGRID format, using more complex
405-
regridding algorithms (for example provided by the
406-
:doc:`iris-esmf-regrid:index` package through :ref:`generic regridding
407-
schemes`) requires the input data in UGRID format.
408-
The following code snippet provides a preprocessor that regrids native ICON
409-
data to a 1°x1° grid using `ESMF's first-order conservative regridding
410-
algorithm <https://earthsystemmodeling.org/regrid/#regridding-methods>`__:
411-
412-
.. code-block:: yaml
413-
414-
preprocessors:
415-
regrid_icon:
416-
regrid:
417-
target_grid: 1x1
418-
scheme:
419-
reference: esmf_regrid.schemes:ESMFAreaWeighted
420-
404+
While the :ref:`built-in regridding schemes <built-in regridding schemes>`
405+
`linear` and `nearest` can handle unstructured grids (i.e., not UGRID-compliant) and meshes (i.e., UGRID-compliant),
406+
the `area_weighted` scheme requires the input data in UGRID format.
421407
This automatic UGRIDization is enabled by default, but can be switched off with
422408
the facet ``ugrid: false`` in the recipe or the extra facets (see below).
423-
This is useful for diagnostics that do not support input data in UGRID format
424-
(yet) like the :ref:`Psyplot diagnostic <esmvaltool:recipes_psyplot_diag>` or
425-
if you want to use the built-in :ref:`nearest scheme <built-in
426-
regridding schemes>` regridding scheme.
409+
This is useful for diagnostics that act on the native ICON grid and do not
410+
support input data in UGRID format (yet), like the
411+
:ref:`Psyplot diagnostic <esmvaltool:recipes_psyplot_diag>`.
427412

428413
For 3D ICON variables, ESMValCore tries to add the pressure level information
429414
(from the variables `pfull` and `phalf`) and/or altitude information (from the

doc/recipe/preprocessor.rst

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -890,15 +890,15 @@ The arguments are defined below:
890890
Regridding (interpolation, extrapolation) schemes
891891
-------------------------------------------------
892892

893-
ESMValCore has a number of built-in regridding schemes, which are presented in
894-
:ref:`built-in regridding schemes`. Additionally, it is also possible to use
895-
third party regridding schemes designed for use with :doc:`Iris
896-
<iris:index>`. This is explained in :ref:`generic regridding schemes`.
893+
ESMValCore provides three default regridding schemes, which are presented in
894+
:ref:`default regridding schemes`. Additionally, it is also possible to use
895+
third party regridding schemes designed for use with :meth:`iris.cube.Cube.regrid`.
896+
This is explained in :ref:`generic regridding schemes`.
897897

898898
Grid types
899899
~~~~~~~~~~
900900

901-
In ESMValCore, we distinguish between three grid types (note that these might
901+
In ESMValCore, we distinguish between various grid types (note that these might
902902
differ from other definitions):
903903

904904
* **Regular grid**: A rectilinear grid with 1D latitude and 1D longitude
@@ -907,30 +907,34 @@ differ from other definitions):
907907
longitude coordinates with common dimensions.
908908
* **Unstructured grid**: A grid with 1D latitude and 1D longitude coordinates
909909
with common dimensions (i.e., a simple list of points).
910+
* **Mesh**: A mesh as supported by Iris and described in :ref:`iris:ugrid`.
910911

911-
.. _built-in regridding schemes:
912+
.. _default regridding schemes:
912913

913-
Built-in regridding schemes
914-
~~~~~~~~~~~~~~~~~~~~~~~~~~~
914+
Default regridding schemes
915+
~~~~~~~~~~~~~~~~~~~~~~~~~~
915916

916917
* ``linear``: Bilinear regridding.
917918
For source data on a regular grid, uses :obj:`~iris.analysis.Linear` with
918919
`extrapolation_mode='mask'`.
919-
For source data on an irregular grid, uses
920-
:class:`~esmvalcore.preprocessor.regrid_schemes.ESMPyLinear`.
920+
For source and/or target data on an irregular grid or mesh, uses
921+
:class:`~esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` with
922+
`method='bilinear'`.
921923
For source data on an unstructured grid, uses
922924
:class:`~esmvalcore.preprocessor.regrid_schemes.UnstructuredLinear`.
923925
* ``nearest``: Nearest-neighbor regridding.
924926
For source data on a regular grid, uses :obj:`~iris.analysis.Nearest` with
925927
`extrapolation_mode='mask'`.
926-
For source data on an irregular grid, uses
927-
:class:`~esmvalcore.preprocessor.regrid_schemes.ESMPyNearest`.
928+
For source and/or target data on an irregular grid or mesh, uses
929+
:class:`~esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` with
930+
`method='nearest'`.
928931
For source data on an unstructured grid, uses
929932
:class:`~esmvalcore.preprocessor.regrid_schemes.UnstructuredNearest`.
930933
* ``area_weighted``: First-order conservative (area-weighted) regridding.
931934
For source data on a regular grid, uses :obj:`~iris.analysis.AreaWeighted`.
932-
For source data on an irregular grid, uses
933-
:class:`~esmvalcore.preprocessor.regrid_schemes.ESMPyAreaWeighted`.
935+
For source and/or target data on an irregular grid or mesh, uses
936+
:class:`~esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` with
937+
`method='conservative'`.
934938
Source data on an unstructured grid is not supported.
935939

936940
.. _generic regridding schemes:
@@ -950,7 +954,9 @@ afforded by the built-in schemes described above.
950954

951955
To facilitate this, the :func:`~esmvalcore.preprocessor.regrid` preprocessor
952956
allows the use of any scheme designed for Iris. The scheme must be installed
953-
and importable. To use this feature, the ``scheme`` key passed to the
957+
and importable. Several such schemes are provided by :mod:`iris.analysis` and
958+
:mod:`esmvalcore.preprocessor.regrid_schemes`.
959+
To use this feature, the ``scheme`` key passed to the
954960
preprocessor must be a dictionary instead of a simple string that contains all
955961
necessary information. That includes a ``reference`` to the desired scheme
956962
itself, as well as any arguments that should be passed through to the
@@ -996,10 +1002,13 @@ module, the second refers to the scheme, i.e. some callable that will be called
9961002
with the remaining entries of the ``scheme`` dictionary passed as keyword
9971003
arguments.
9981004

999-
One package that aims to capitalize on the :ref:`support for unstructured grids
1000-
introduced in Iris 3.2 <iris:ugrid>` is :doc:`iris-esmf-regrid:index`.
1005+
One package that aims to capitalize on the :ref:`support for meshes
1006+
introduced in Iris 3.2 <iris:ugrid>` is :doc:`esmf_regrid:index`.
10011007
It aims to provide lazy regridding for structured regular and irregular grids,
1002-
as well as unstructured grids.
1008+
as well as meshes. It is recommended to use these schemes through
1009+
the :obj:`esmvalcore.preprocessor.regrid_schemes.IrisESMFRegrid` scheme though,
1010+
as that provides more efficient handling of masks.
1011+
10031012
An example of its usage in a preprocessor is:
10041013

10051014
.. code-block:: yaml
@@ -1009,16 +1018,19 @@ An example of its usage in a preprocessor is:
10091018
regrid:
10101019
target_grid: 2.5x2.5
10111020
scheme:
1012-
reference: esmf_regrid.schemes:ESMFAreaWeighted
1021+
reference: esmvalcore.preprocessor.regrid_schemes:IrisESMFRegrid
1022+
method: conservative
10131023
mdtol: 0.7
1024+
use_src_mask: true
1025+
collapse_src_mask_along: ZT
10141026
10151027
Additionally, the use of generic schemes that take source and target grid cubes as
10161028
arguments is also supported. The call function for such schemes must be defined as
10171029
`(src_cube, grid_cube, **kwargs)` and they must return `iris.cube.Cube` objects.
10181030
The `regrid` module will automatically pass the source and grid cubes as inputs
10191031
of the scheme. An example of this usage is
10201032
the :func:`~esmf_regrid.schemes.regrid_rectilinear_to_rectilinear`
1021-
scheme available in :doc:`iris-esmf-regrid:index`:
1033+
scheme available in :doc:`esmf_regrid:index`:
10221034

10231035
.. code-block:: yaml
10241036

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies:
1919
- geopy
2020
- humanfriendly
2121
- iris >=3.10.0
22-
- iris-esmf-regrid >=0.10.0 # github.com/SciTools-incubator/iris-esmf-regrid/pull/342
22+
- iris-esmf-regrid >=0.11.0
2323
- iris-grib
2424
- isodate
2525
- jinja2

esmvalcore/preprocessor/_regrid.py

Lines changed: 58 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
from esmvalcore.cmor.table import CMOR_TABLES
3131
from esmvalcore.exceptions import ESMValCoreDeprecationWarning
3232
from esmvalcore.iris_helpers import has_irregular_grid, has_unstructured_grid
33+
from esmvalcore.preprocessor._shared import (
34+
get_dims_along_axes,
35+
)
3336
from esmvalcore.preprocessor._shared import (
3437
get_array_module,
3538
preserve_float_dtype,
@@ -39,10 +42,8 @@
3942
add_cell_measure,
4043
)
4144
from esmvalcore.preprocessor.regrid_schemes import (
42-
ESMPyAreaWeighted,
43-
ESMPyLinear,
44-
ESMPyNearest,
4545
GenericFuncScheme,
46+
IrisESMFRegrid,
4647
UnstructuredLinear,
4748
UnstructuredNearest,
4849
)
@@ -91,9 +92,17 @@
9192
# curvilinear grids; i.e., grids that can be described with 2D latitude and 2D
9293
# longitude coordinates with common dimensions)
9394
HORIZONTAL_SCHEMES_IRREGULAR = {
94-
'area_weighted': ESMPyAreaWeighted(),
95-
'linear': ESMPyLinear(),
96-
'nearest': ESMPyNearest(),
95+
'area_weighted': IrisESMFRegrid(method='conservative'),
96+
'linear': IrisESMFRegrid(method='bilinear'),
97+
'nearest': IrisESMFRegrid(method='nearest'),
98+
}
99+
100+
# Supported horizontal regridding schemes for meshes
101+
# https://scitools-iris.readthedocs.io/en/stable/further_topics/ugrid/index.html
102+
HORIZONTAL_SCHEMES_MESH = {
103+
'area_weighted': IrisESMFRegrid(method='conservative'),
104+
'linear': IrisESMFRegrid(method='bilinear'),
105+
'nearest': IrisESMFRegrid(method='nearest'),
97106
}
98107

99108
# Supported horizontal regridding schemes for unstructured grids (i.e., grids,
@@ -533,29 +542,7 @@ def _get_target_grid_cube(
533542
return target_grid_cube
534543

535544

536-
def _attempt_irregular_regridding(cube: Cube, scheme: str) -> bool:
537-
"""Check if irregular regridding with ESMF should be used."""
538-
if not has_irregular_grid(cube):
539-
return False
540-
if scheme not in HORIZONTAL_SCHEMES_IRREGULAR:
541-
raise ValueError(
542-
f"Regridding scheme '{scheme}' does not support irregular data, "
543-
f"expected one of {list(HORIZONTAL_SCHEMES_IRREGULAR)}")
544-
return True
545-
546-
547-
def _attempt_unstructured_regridding(cube: Cube, scheme: str) -> bool:
548-
"""Check if unstructured regridding should be used."""
549-
if not has_unstructured_grid(cube):
550-
return False
551-
if scheme not in HORIZONTAL_SCHEMES_UNSTRUCTURED:
552-
raise ValueError(
553-
f"Regridding scheme '{scheme}' does not support unstructured "
554-
f"data, expected one of {list(HORIZONTAL_SCHEMES_UNSTRUCTURED)}")
555-
return True
556-
557-
558-
def _load_scheme(src_cube: Cube, scheme: str | dict):
545+
def _load_scheme(src_cube: Cube, tgt_cube: Cube, scheme: str | dict):
559546
"""Return scheme that can be used in :meth:`iris.cube.Cube.regrid`."""
560547
loaded_scheme: Any = None
561548

@@ -586,23 +573,27 @@ def _load_scheme(src_cube: Cube, scheme: str | dict):
586573
logger.debug("Loaded regridding scheme %s", loaded_scheme)
587574
return loaded_scheme
588575

589-
# Scheme is a dict -> assume this describes a generic regridding scheme
590576
if isinstance(scheme, dict):
577+
# Scheme is a dict -> assume this describes a generic regridding scheme
591578
loaded_scheme = _load_generic_scheme(scheme)
592-
593-
# Scheme is a str -> load appropriate regridding scheme depending on the
594-
# type of input data
595-
elif _attempt_irregular_regridding(src_cube, scheme):
596-
loaded_scheme = HORIZONTAL_SCHEMES_IRREGULAR[scheme]
597-
elif _attempt_unstructured_regridding(src_cube, scheme):
598-
loaded_scheme = HORIZONTAL_SCHEMES_UNSTRUCTURED[scheme]
599579
else:
600-
loaded_scheme = HORIZONTAL_SCHEMES_REGULAR.get(scheme)
601-
602-
if loaded_scheme is None:
603-
raise ValueError(
604-
f"Got invalid regridding scheme string '{scheme}', expected one "
605-
f"of {list(HORIZONTAL_SCHEMES_REGULAR)}")
580+
# Scheme is a str -> load appropriate regridding scheme depending on
581+
# the type of input data
582+
if has_irregular_grid(src_cube) or has_irregular_grid(tgt_cube):
583+
grid_type = 'irregular'
584+
elif src_cube.mesh is not None or tgt_cube.mesh is not None:
585+
grid_type = 'mesh'
586+
elif has_unstructured_grid(src_cube):
587+
grid_type = 'unstructured'
588+
else:
589+
grid_type = 'regular'
590+
591+
schemes = globals()[f"HORIZONTAL_SCHEMES_{grid_type.upper()}"]
592+
if scheme not in schemes:
593+
raise ValueError(
594+
f"Regridding scheme '{scheme}' not available for {grid_type} "
595+
f"data, expected one of: {', '.join(schemes)}")
596+
loaded_scheme = schemes[scheme]
606597

607598
logger.debug("Loaded regridding scheme %s", loaded_scheme)
608599

@@ -676,14 +667,14 @@ def _get_regridder(
676667
return regridder
677668

678669
# Regridder is not in cached -> return a new one and cache it
679-
loaded_scheme = _load_scheme(src_cube, scheme)
670+
loaded_scheme = _load_scheme(src_cube, tgt_cube, scheme)
680671
regridder = loaded_scheme.regridder(src_cube, tgt_cube)
681672
_CACHED_REGRIDDERS.setdefault(name_shape_key, {})
682673
_CACHED_REGRIDDERS[name_shape_key][coord_key] = regridder
683674

684675
# (2) Weights caching disabled
685676
else:
686-
loaded_scheme = _load_scheme(src_cube, scheme)
677+
loaded_scheme = _load_scheme(src_cube, tgt_cube, scheme)
687678
regridder = loaded_scheme.regridder(src_cube, tgt_cube)
688679

689680
return regridder
@@ -860,36 +851,40 @@ def _cache_clear():
860851

861852
def _rechunk(cube: Cube, target_grid: Cube) -> Cube:
862853
"""Re-chunk cube with optimal chunk sizes for target grid."""
863-
if not cube.has_lazy_data() or cube.ndim < 3:
864-
# Only rechunk lazy multidimensional data
854+
if not cube.has_lazy_data():
855+
# Only rechunk lazy data
865856
return cube
866857

867-
lon_coord = target_grid.coord(axis='X')
868-
lat_coord = target_grid.coord(axis='Y')
869-
if lon_coord.ndim != 1 or lat_coord.ndim != 1:
870-
# This function only supports 1D lat/lon coordinates.
871-
return cube
858+
# Extract grid dimension information from source cube
859+
src_grid_indices = get_dims_along_axes(cube, ["X", "Y"])
860+
src_grid_shape = tuple(cube.shape[i] for i in src_grid_indices)
861+
src_grid_ndims = len(src_grid_indices)
872862

873-
lon_dim, = target_grid.coord_dims(lon_coord)
874-
lat_dim, = target_grid.coord_dims(lat_coord)
875-
grid_indices = sorted((lon_dim, lat_dim))
876-
target_grid_shape = tuple(target_grid.shape[i] for i in grid_indices)
863+
# Extract grid dimension information from target cube.
864+
tgt_grid_indices = get_dims_along_axes(target_grid, ["X", "Y"])
865+
tgt_grid_shape = tuple(target_grid.shape[i] for i in tgt_grid_indices)
866+
tgt_grid_ndims = len(tgt_grid_indices)
877867

878-
if 2 * np.prod(cube.shape[-2:]) > np.prod(target_grid_shape):
868+
if 2 * np.prod(src_grid_shape) > np.prod(tgt_grid_shape):
879869
# Only rechunk if target grid is more than a factor of 2 larger,
880870
# because rechunking will keep the original chunk in memory.
881871
return cube
882872

873+
# Compute a good chunk size for the target array
874+
# This uses the fact that horizontal dimension(s) are the last dimension(s)
875+
# of the input cube and also takes into account that iris regridding needs
876+
# unchunked data along the grid dimensions.
883877
data = cube.lazy_data()
878+
tgt_shape = data.shape[:-src_grid_ndims] + tgt_grid_shape
879+
tgt_chunks = data.chunks[:-src_grid_ndims] + tgt_grid_shape
884880

885-
# Compute a good chunk size for the target array
886-
tgt_shape = data.shape[:-2] + target_grid_shape
887-
tgt_chunks = data.chunks[:-2] + target_grid_shape
888-
tgt_data = da.empty(tgt_shape, dtype=data.dtype, chunks=tgt_chunks)
889-
tgt_data = tgt_data.rechunk({i: "auto" for i in range(cube.ndim - 2)})
881+
tgt_data = da.empty(tgt_shape, chunks=tgt_chunks, dtype=data.dtype)
882+
tgt_data = tgt_data.rechunk(
883+
{i: "auto"
884+
for i in range(tgt_data.ndim - tgt_grid_ndims)})
890885

891886
# Adjust chunks to source array and rechunk
892-
chunks = tgt_data.chunks[:-2] + data.shape[-2:]
887+
chunks = tgt_data.chunks[:-tgt_grid_ndims] + data.shape[-src_grid_ndims:]
893888
cube.data = data.rechunk(chunks)
894889

895890
return cube

0 commit comments

Comments
 (0)