diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst index 20a126de5c..0224f5937c 100644 --- a/doc/recipe/preprocessor.rst +++ b/doc/recipe/preprocessor.rst @@ -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` diff --git a/esmvalcore/preprocessor/_regrid.py b/esmvalcore/preprocessor/_regrid.py index 7ff8dde22d..b35c4b3a25 100644 --- a/esmvalcore/preprocessor/_regrid.py +++ b/esmvalcore/preprocessor/_regrid.py @@ -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. diff --git a/tests/unit/preprocessor/_regrid/test_regrid.py b/tests/unit/preprocessor/_regrid/test_regrid.py index fb6ec94232..b7beaca442 100644 --- a/tests/unit/preprocessor/_regrid/test_regrid.py +++ b/tests/unit/preprocessor/_regrid/test_regrid.py @@ -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): @@ -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( @@ -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()