Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/recipe/preprocessor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,9 @@ inter-comparison or comparison with observational datasets). Regridding is
conceptually a very similar process to interpolation (in fact, the regridder
engine uses interpolation and extrapolation, with various schemes). The primary
difference is that interpolation is based on sample data points, while
regridding is based on the horizontal grid of another cube (the reference grid).
regridding is based on the horizontal grid of another cube (the reference
grid). If the horizontal grids of a cube and its reference grid are sufficiently
the same, regridding is automatically and silently skipped for performance reasons.

The underlying regridding mechanism in ESMValTool uses
:obj:`iris.cube.Cube.regrid`
Expand Down
51 changes: 46 additions & 5 deletions esmvalcore/preprocessor/_regrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,15 +467,56 @@ def regrid(cube, target_grid, scheme, lat_offset=True, lon_offset=True):
[coord] = coords
cube.remove_coord(coord)

# Perform the horizontal regridding.
if _attempt_irregular_regridding(cube, scheme):
cube = esmpy_regrid(cube, target_grid, scheme)
else:
cube = cube.regrid(target_grid, HORIZONTAL_SCHEMES[scheme])
# Return non-regridded cube if horizontal grid is the same.
if not _horizontal_grid_is_close(cube, target_grid):

# Perform the horizontal regridding.
if _attempt_irregular_regridding(cube, scheme):
cube = esmpy_regrid(cube, target_grid, scheme)
else:
cube = cube.regrid(target_grid, HORIZONTAL_SCHEMES[scheme])

return cube


def _horizontal_grid_is_close(cube1, cube2):
"""Check if two cubes have the same horizontal grid definition.

The result of the function is a boolean answer, if both cubes have the
same horizontal grid definition. The function checks both longitude and
latitude, based on extent and resolution.

Parameters
----------
cube1 : cube
The first of the cubes to be checked.
cube2 : cube
The second of the cubes to be checked.

Returns
-------
bool

.. note::

The current implementation checks if the bounds and the
grid shapes are the same.
Exits on first difference.
"""
# Go through the 2 expected horizontal coordinates longitude and latitude.
for coord in ['latitude', 'longitude']:
coord1 = cube1.coord(coord)
coord2 = cube2.coord(coord)

if not coord1.shape == coord2.shape:
return False

if not np.allclose(coord1.bounds, coord2.bounds):
return False

return True


def _create_cube(src_cube, data, src_levels, levels):
"""Generate a new cube with the interpolated data.

Expand Down
131 changes: 123 additions & 8 deletions tests/unit/preprocessor/_regrid/test_regrid.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
"""
Unit tests for the :func:`esmvalcore.preprocessor.regrid.regrid` function.

"""
"""Unit tests for the :func:`esmvalcore.preprocessor.regrid.regrid`
function."""

import unittest
from unittest import mock

import iris
import numpy as np
import pytest

import tests
from esmvalcore.preprocessor import regrid
from esmvalcore.preprocessor._regrid import _CACHE, HORIZONTAL_SCHEMES
from esmvalcore.preprocessor._regrid import (
_CACHE,
HORIZONTAL_SCHEMES,
_horizontal_grid_is_close,
)


class Test(tests.Test):
Expand Down Expand Up @@ -64,9 +68,17 @@ def setUp(self):
'unstructured_nearest'
]

def _return_mock_global_stock_cube(spec,
lat_offset=True,
lon_offset=True):
def _mock_horizontal_grid_is_close(src, tgt):
return False

self.patch('esmvalcore.preprocessor._regrid._horizontal_grid_is_close',
side_effect=_mock_horizontal_grid_is_close)

def _return_mock_global_stock_cube(
spec,
lat_offset=True,
lon_offset=True,
):
return self.tgt_grid

self.mock_stock = self.patch(
Expand Down Expand Up @@ -117,5 +129,108 @@ def test_regrid__cell_specification(self):
_CACHE.clear()


def _make_coord(start: float, stop: float, step: int, *, name: str):
"""Helper function for creating a coord."""
coord = iris.coords.DimCoord(
np.linspace(start, stop, step),
standard_name=name,
units='degrees',
)
coord.guess_bounds()
return coord


def _make_cube(*, lat: tuple, lon: tuple):
"""Helper function for creating a cube."""
lat_coord = _make_coord(*lat, name='latitude')
lon_coord = _make_coord(*lon, name='longitude')

return iris.cube.Cube(
np.empty([len(lat_coord.points),
len(lon_coord.points)]),
dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)],
)


# 10x10
LAT_SPEC1 = (-85, 85, 18)
LON_SPEC1 = (5, 355, 36)

# almost 10x10, but different shape
LAT_SPEC2 = (-85, 85, 17)
LON_SPEC2 = (5, 355, 35)

# 10x10, but different coords
LAT_SPEC3 = (-90, 90, 18)
LON_SPEC3 = (0, 360, 36)


@pytest.mark.parametrize(
'cube2_spec, expected',
(
# equal lat/lon
(
{
'lat': LAT_SPEC1,
'lon': LON_SPEC1,
},
True,
),
# different lon shape
(
{
'lat': LAT_SPEC1,
'lon': LON_SPEC2,
},
False,
),
# different lat shape
(
{
'lat': LAT_SPEC2,
'lon': LON_SPEC1,
},
False,
),
# different lon values
(
{
'lat': LAT_SPEC1,
'lon': LON_SPEC3,
},
False,
),
# different lat values
(
{
'lat': LAT_SPEC3,
'lon': LON_SPEC1,
},
False,
),
),
)
def test_horizontal_grid_is_close(cube2_spec: dict, expected: bool):
"""Test for `_horizontal_grid_is_close`."""
cube1 = _make_cube(lat=LAT_SPEC1, lon=LON_SPEC1)
cube2 = _make_cube(**cube2_spec)

assert _horizontal_grid_is_close(cube1, cube2) == expected


def test_regrid_is_skipped_if_grids_are_the_same():
"""Test that regridding is skipped if the grids are the same."""
cube = _make_cube(lat=LAT_SPEC1, lon=LON_SPEC1)
scheme = 'linear'

# regridding to the same spec returns the same cube
expected_same_cube = regrid(cube, target_grid='10x10', scheme=scheme)
assert expected_same_cube is cube

# regridding to a different spec returns a different cube
expected_different_cube = regrid(cube, target_grid='5x5', scheme=scheme)
assert expected_different_cube is not cube


if __name__ == '__main__':
unittest.main()