From dd263394f3403b4eaf375d1c7c5918f7445f526b Mon Sep 17 00:00:00 2001 From: rbeucher Date: Wed, 14 Aug 2024 16:43:05 +1000 Subject: [PATCH 01/21] Fix ERA5 native6 fix to handle single monthly-averaged NetCDF files. --- esmvalcore/cmor/_fixes/native6/era5.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index 6c67494aaa..e17b506a83 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -20,17 +20,21 @@ def get_frequency(cube): time = cube.coord(axis='T') except iris.exceptions.CoordinateNotFoundError: return 'fx' - + time.convert_units('days since 1850-1-1 00:00:00.0') - if len(time.points) == 1: - if cube.long_name != 'Geopotential': - raise ValueError('Unable to infer frequency of cube ' - f'with length 1 time dimension: {cube}') + + if (len(time.points) == 1 and + cube.long_name == 'Geopotential'): return 'fx' - - interval = time.points[1] - time.points[0] - if interval - 1 / 24 < 1e-4: - return 'hourly' + + if len(time.points) > 1: + interval = time.points[1] - time.points[0] + if interval - 1 / 24 < 1e-4: + return 'hourly' + else: + logger.warning(f"Unable to infer frequency of cube + with length 1 time dimension: {cube}, + assuming 'monthly' frequency") return 'monthly' From e87106ebb6d120e974424dd2c13246c209a78872 Mon Sep 17 00:00:00 2001 From: rbeucher Date: Wed, 14 Aug 2024 16:49:59 +1000 Subject: [PATCH 02/21] Fix string --- esmvalcore/cmor/_fixes/native6/era5.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index e17b506a83..99fe6fdc3e 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -32,9 +32,9 @@ def get_frequency(cube): if interval - 1 / 24 < 1e-4: return 'hourly' else: - logger.warning(f"Unable to infer frequency of cube - with length 1 time dimension: {cube}, - assuming 'monthly' frequency") + logger.warning("Unable to infer frequency of cube " + f"with length 1 time dimension: {cube}," + "assuming 'monthly' frequency") return 'monthly' From ebd55123857ce279f808ce7e09a54d88489eaf7d Mon Sep 17 00:00:00 2001 From: rbeucher Date: Wed, 14 Aug 2024 16:55:41 +1000 Subject: [PATCH 03/21] Fix cosmetics --- esmvalcore/cmor/_fixes/native6/era5.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index 99fe6fdc3e..f9cdbc1f46 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -20,13 +20,13 @@ def get_frequency(cube): time = cube.coord(axis='T') except iris.exceptions.CoordinateNotFoundError: return 'fx' - + time.convert_units('days since 1850-1-1 00:00:00.0') - + if (len(time.points) == 1 and - cube.long_name == 'Geopotential'): + cube.long_name == 'Geopotential'): return 'fx' - + if len(time.points) > 1: interval = time.points[1] - time.points[0] if interval - 1 / 24 < 1e-4: From a4b13a1917a6fc7d0ec95a2c6c6cf7c94aa195bd Mon Sep 17 00:00:00 2001 From: rbeucher Date: Wed, 14 Aug 2024 17:04:36 +1000 Subject: [PATCH 04/21] happy? --- esmvalcore/cmor/_fixes/native6/era5.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index f9cdbc1f46..4c938e29b6 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -24,8 +24,8 @@ def get_frequency(cube): time.convert_units('days since 1850-1-1 00:00:00.0') if (len(time.points) == 1 and - cube.long_name == 'Geopotential'): - return 'fx' + cube.long_name == 'Geopotential'): + return 'fx' if len(time.points) > 1: interval = time.points[1] - time.points[0] From ce1c78c221a75318fa4ac9f40e75a906c8b5ef95 Mon Sep 17 00:00:00 2001 From: rbeucher Date: Wed, 14 Aug 2024 17:13:36 +1000 Subject: [PATCH 05/21] Formatting --- esmvalcore/cmor/_fixes/native6/era5.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index 4c938e29b6..8375d5ac1c 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -23,9 +23,8 @@ def get_frequency(cube): time.convert_units('days since 1850-1-1 00:00:00.0') - if (len(time.points) == 1 and - cube.long_name == 'Geopotential'): - return 'fx' + if (len(time.points) == 1 and cube.long_name == 'Geopotential'): + return 'fx' if len(time.points) > 1: interval = time.points[1] - time.points[0] From 5bbae26949e81f37c4429b597ea9290db3a2df71 Mon Sep 17 00:00:00 2001 From: rbeucher Date: Wed, 14 Aug 2024 18:08:34 +1000 Subject: [PATCH 06/21] Fix test --- tests/integration/cmor/_fixes/native6/test_era5.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 70b432541d..553485cbf5 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -86,8 +86,7 @@ def test_get_frequency_fx(): ) assert get_frequency(cube) == 'fx' cube.long_name = 'Not geopotential' - with pytest.raises(ValueError): - get_frequency(cube) + assert get_frequency(cube) == 'monthly' def _era5_latitude(): From edd9564f31fd519d7fb29ba25a91180918c255ea Mon Sep 17 00:00:00 2001 From: rbeucher Date: Tue, 26 Nov 2024 08:14:47 +1000 Subject: [PATCH 07/21] Ruff fixing --- esmvalcore/cmor/_fixes/native6/era5.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index a30ee100a8..e8fc62d173 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -22,19 +22,22 @@ def get_frequency(cube): except iris.exceptions.CoordinateNotFoundError: return "fx" - time.convert_units('days since 1850-1-1 00:00:00.0') + time.convert_units("days since 1850-1-1 00:00:00.0") - if (len(time.points) == 1 and cube.long_name == 'Geopotential'): - return 'fx' + if len(time.points) == 1 and cube.long_name == "Geopotential": + return "fx" if len(time.points) > 1: interval = time.points[1] - time.points[0] if interval - 1 / 24 < 1e-4: - return 'hourly' + return "hourly" else: - logger.warning("Unable to infer frequency of cube " - f"with length 1 time dimension: {cube}," - "assuming 'monthly' frequency") + logger.warning( + "Unable to infer frequency of cube " + "with length 1 time dimension: %s," + "assuming 'monthly' frequency", + str(cube), + ) return "monthly" From 75e83461934a80626364be1901b85a09e9a84924 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:27:46 +0000 Subject: [PATCH 08/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/integration/cmor/_fixes/native6/test_era5.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 1dc4f28a7b..9ef2e3fa0c 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -87,9 +87,9 @@ def test_get_frequency_fx(): long_name="Geopotential", dim_coords_and_dims=[(time, 0)], ) - assert get_frequency(cube) == 'fx' - cube.long_name = 'Not geopotential' - assert get_frequency(cube) == 'monthly' + assert get_frequency(cube) == "fx" + cube.long_name = "Not geopotential" + assert get_frequency(cube) == "monthly" def _era5_latitude(): From 7b92edee110f3a4dc0b4bb603c9bf6ca807b116f Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Tue, 16 Sep 2025 14:36:03 +1000 Subject: [PATCH 09/21] update get time frequency --- esmvalcore/cmor/_fixes/native6/era5.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index d5da75b59c..d565e12249 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -16,6 +16,7 @@ safe_convert_units, ) + logger = logging.getLogger(__name__) @@ -27,22 +28,21 @@ def get_frequency(cube): return "fx" time.convert_units("days since 1850-1-1 00:00:00.0") - - if len(time.points) == 1 and cube.long_name == "Geopotential": + if len(time.points) == 1: + acceptable_long_names = ( + "Geopotential", + "Percentage of the Grid Cell Occupied by Land (Including Lakes)", + ) + if cube.long_name not in acceptable_long_names: + return "monthly" return "fx" - if len(time.points) > 1: - interval = time.points[1] - time.points[0] - if interval - 1 / 24 < 1e-4: - return "hourly" - else: - logger.warning( - "Unable to infer frequency of cube " - "with length 1 time dimension: %s," - "assuming 'monthly' frequency", - str(cube), - ) + interval = time.points[1] - time.points[0] + if interval - 1 / 24 < 1e-4: + return "hourly" + if interval - 1.0 < 1e-4: + return "daily" return "monthly" From ece7ca2c6f0d0edfaf3cd3614504546e70189377 Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Tue, 16 Sep 2025 15:35:42 +1000 Subject: [PATCH 10/21] remove blank line --- esmvalcore/cmor/_fixes/native6/era5.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index a5086d5621..ba7bbcf3ae 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -17,7 +17,6 @@ safe_convert_units, ) - logger = logging.getLogger(__name__) From 84d69949cac4cc80de44f7fdbaa4efc36d0ef51d Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Wed, 17 Sep 2025 13:25:22 +1000 Subject: [PATCH 11/21] log warning --- esmvalcore/cmor/_fixes/native6/era5.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index ba7bbcf3ae..70000a8ef7 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -34,6 +34,11 @@ def get_frequency(cube): "Percentage of the Grid Cell Occupied by Land (Including Lakes)", ) if cube.long_name not in acceptable_long_names: + logger.warning( + "Cube %s has length 1 time dimension, " + "assuming 'monthly' frequency", + str(cube), + ) return "monthly" return "fx" From 389ea2be7dcb5f6248c1acde668f8cf41eb71dbf Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Thu, 18 Sep 2025 11:14:21 +1000 Subject: [PATCH 12/21] revert get frequency, use when freq not specified --- esmvalcore/cmor/_fixes/native6/era5.py | 44 ++++++++++++++------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index 70000a8ef7..7acfc962dc 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -34,12 +34,13 @@ def get_frequency(cube): "Percentage of the Grid Cell Occupied by Land (Including Lakes)", ) if cube.long_name not in acceptable_long_names: - logger.warning( - "Cube %s has length 1 time dimension, " - "assuming 'monthly' frequency", - str(cube), + msg = ( + "Unable to infer frequency of cube " + f"with length 1 time dimension: {cube}" + ) + raise ValueError( + msg, ) - return "monthly" return "fx" interval = time.points[1] - time.points[0] @@ -566,7 +567,11 @@ def _fix_coordinates( # noqa: C901 ): coord.guess_bounds() - self._fix_monthly_time_coord(cube) + frequency = ( + get_frequency(cube) if self.frequency is None else self.frequency + ) + if frequency in ("monthly", "mon", "mo"): + self._fix_monthly_time_coord(cube) # Fix coordinate increasing direction if cube.coords("latitude") and not has_unstructured_grid(cube): @@ -583,20 +588,19 @@ def _fix_coordinates( # noqa: C901 @staticmethod def _fix_monthly_time_coord(cube): """Set the monthly time coordinates to the middle of the month.""" - if get_frequency(cube) == "monthly": - coord = cube.coord(axis="T") - end = [] - for cell in coord.cells(): - month = cell.point.month + 1 - year = cell.point.year - if month == 13: - month = 1 - year = year + 1 - end.append(cell.point.replace(month=month, year=year)) - end = date2num(end, coord.units) - start = coord.points - coord.points = 0.5 * (start + end) - coord.bounds = np.column_stack([start, end]) + coord = cube.coord(axis="T") + end = [] + for cell in coord.cells(): + month = cell.point.month + 1 + year = cell.point.year + if month == 13: + month = 1 + year = year + 1 + end.append(cell.point.replace(month=month, year=year)) + end = date2num(end, coord.units) + start = coord.points + coord.points = 0.5 * (start + end) + coord.bounds = np.column_stack([start, end]) def fix_metadata(self, cubes): """Fix metadata.""" From ac6eef69dd6897ffca0ad48d48970762fdf478ab Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Thu, 18 Sep 2025 11:23:13 +1000 Subject: [PATCH 13/21] revert test for fx --- tests/integration/cmor/_fixes/native6/test_era5.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 8e75f255d2..82c380383c 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -116,7 +116,8 @@ def test_get_frequency_fx(): assert get_frequency(cube) == "fx" cube.long_name = "Not geopotential" - assert get_frequency(cube) == "monthly" + with pytest.raises(ValueError): + get_frequency(cube) def test_fix_accumulated_units_fail(): From 5dd56cc722f2ad98b1f2278e5040a4bc2d157038 Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Thu, 18 Sep 2025 11:31:00 +1000 Subject: [PATCH 14/21] ruff check branches --- esmvalcore/cmor/_fixes/native6/era5.py | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index 7acfc962dc..5b654189cf 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -570,8 +570,7 @@ def _fix_coordinates( # noqa: C901 frequency = ( get_frequency(cube) if self.frequency is None else self.frequency ) - if frequency in ("monthly", "mon", "mo"): - self._fix_monthly_time_coord(cube) + self._fix_monthly_time_coord(cube, frequency) # Fix coordinate increasing direction if cube.coords("latitude") and not has_unstructured_grid(cube): @@ -586,21 +585,22 @@ def _fix_coordinates( # noqa: C901 return cube @staticmethod - def _fix_monthly_time_coord(cube): + def _fix_monthly_time_coord(cube, frequency): """Set the monthly time coordinates to the middle of the month.""" - coord = cube.coord(axis="T") - end = [] - for cell in coord.cells(): - month = cell.point.month + 1 - year = cell.point.year - if month == 13: - month = 1 - year = year + 1 - end.append(cell.point.replace(month=month, year=year)) - end = date2num(end, coord.units) - start = coord.points - coord.points = 0.5 * (start + end) - coord.bounds = np.column_stack([start, end]) + if frequency in ("monthly", "mon", "mo"): + coord = cube.coord(axis="T") + end = [] + for cell in coord.cells(): + month = cell.point.month + 1 + year = cell.point.year + if month == 13: + month = 1 + year = year + 1 + end.append(cell.point.replace(month=month, year=year)) + end = date2num(end, coord.units) + start = coord.points + coord.points = 0.5 * (start + end) + coord.bounds = np.column_stack([start, end]) def fix_metadata(self, cubes): """Fix metadata.""" From 3228c726a844cfb3ce9a900ba7b3109df805b850 Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Tue, 23 Sep 2025 14:09:33 +1000 Subject: [PATCH 15/21] remove mo, test mon freq --- esmvalcore/cmor/_fixes/native6/era5.py | 2 +- tests/integration/cmor/_fixes/native6/test_era5.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index 5b654189cf..d950d1da8e 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -587,7 +587,7 @@ def _fix_coordinates( # noqa: C901 @staticmethod def _fix_monthly_time_coord(cube, frequency): """Set the monthly time coordinates to the middle of the month.""" - if frequency in ("monthly", "mon", "mo"): + if frequency in ("monthly", "mon"): coord = cube.coord(axis="T") end = [] for cell in coord.cells(): diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 82c380383c..3ff5fab999 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -164,7 +164,7 @@ def _era5_time(frequency): timestamps = [788940, 788964, 788988] elif frequency == "hourly": timestamps = [788928, 788929, 788930] - elif frequency == "monthly": + elif frequency in ("monthly","mon"): # mon frequency when not run get_frequency timestamps = [788928, 789672, 790344] else: msg = f"Invalid frequency {frequency}" @@ -292,7 +292,7 @@ def _cmor_data(mip): def era5_2d(frequency): - if frequency == "monthly": + if frequency in ("monthly", "mon"): time = DimCoord( [-31, 0, 31], standard_name="time", @@ -1510,6 +1510,7 @@ def vas_cmor_e1hr(): (prsn_era5_hourly(), prsn_cmor_e1hr(), "prsn", "E1hr"), (era5_2d("monthly"), cmor_2d("Amon", "prw"), "prw", "Amon"), (era5_2d("monthly"), cmor_2d("Amon", "ps"), "ps", "Amon"), + (era5_2d("mon"), cmor_2d("Amon", "ps"), "ps", "Amon"), (ptype_era5_hourly(), ptype_cmor_e1hr(), "ptype", "E1hr"), ( era5_3d("monthly"), From dc00c12f691c306b9de2bae898f6bc35b3c316ca Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Tue, 23 Sep 2025 14:16:07 +1000 Subject: [PATCH 16/21] ruff --- tests/integration/cmor/_fixes/native6/test_era5.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 3ff5fab999..73580400bc 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -164,7 +164,10 @@ def _era5_time(frequency): timestamps = [788940, 788964, 788988] elif frequency == "hourly": timestamps = [788928, 788929, 788930] - elif frequency in ("monthly","mon"): # mon frequency when not run get_frequency + elif frequency in ( + "monthly", + "mon", + ): # mon frequency when not run get_frequency timestamps = [788928, 789672, 790344] else: msg = f"Invalid frequency {frequency}" From 030e5e5bddb83fd8b1f0a5ebbe7a82eb639ef3b0 Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Tue, 23 Sep 2025 14:18:39 +1000 Subject: [PATCH 17/21] remove comment --- tests/integration/cmor/_fixes/native6/test_era5.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 73580400bc..3659efc936 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -164,10 +164,7 @@ def _era5_time(frequency): timestamps = [788940, 788964, 788988] elif frequency == "hourly": timestamps = [788928, 788929, 788930] - elif frequency in ( - "monthly", - "mon", - ): # mon frequency when not run get_frequency + elif frequency in ("monthly", "mon"): timestamps = [788928, 789672, 790344] else: msg = f"Invalid frequency {frequency}" From 4948f387ff810ad6e7b6656943631fb36ec46526 Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Mon, 29 Sep 2025 12:31:39 +1000 Subject: [PATCH 18/21] remove get_frequency, tests freq for some vars --- esmvalcore/cmor/_fixes/native6/era5.py | 106 ++++-------- .../cmor/_fixes/native6/test_era5.py | 159 +++++------------- 2 files changed, 76 insertions(+), 189 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index d950d1da8e..41a6387984 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -20,53 +20,22 @@ logger = logging.getLogger(__name__) -def get_frequency(cube): - """Determine time frequency of input cube.""" - try: - time = cube.coord(axis="T") - except iris.exceptions.CoordinateNotFoundError: - return "fx" - - time.convert_units("days since 1850-1-1 00:00:00.0") - if len(time.points) == 1: - acceptable_long_names = ( - "Geopotential", - "Percentage of the Grid Cell Occupied by Land (Including Lakes)", - ) - if cube.long_name not in acceptable_long_names: - msg = ( - "Unable to infer frequency of cube " - f"with length 1 time dimension: {cube}" - ) - raise ValueError( - msg, - ) - return "fx" - interval = time.points[1] - time.points[0] - - if interval - 1 / 24 < 1e-4: - return "hourly" - if interval - 1.0 < 1e-4: - return "daily" - return "monthly" - - -def fix_hourly_time_coordinate(cube): +def fix_hourly_time_coordinate(cube, frequency): """Shift aggregated variables 30 minutes back in time.""" - if get_frequency(cube) == "hourly": + if frequency == "1hr": time = cube.coord(axis="T") time.points = time.points - 1 / 48 return cube -def fix_accumulated_units(cube): +def fix_accumulated_units(cube, frequency): """Convert accumulations to fluxes.""" - if get_frequency(cube) == "monthly": + if frequency == "mon": cube.units = cube.units * "d-1" - elif get_frequency(cube) == "hourly": + elif frequency == "1hr": cube.units = cube.units * "h-1" - elif get_frequency(cube) == "daily": + elif frequency == "day": msg = ( f"Fixing of accumulated units of cube " f"{cube.summary(shorten=True)} is not implemented for daily data" @@ -163,8 +132,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) # Correct sign to align with CMOR standards cube.data = cube.core_data() * -1.0 @@ -180,8 +149,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) # Correct sign to align with CMOR standards cube.data = cube.core_data() * -1.0 @@ -205,8 +174,8 @@ class Mrro(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) return cubes @@ -246,8 +215,8 @@ class Pr(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) return cubes @@ -265,8 +234,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) return cubes @@ -319,8 +288,8 @@ class Rlds(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -332,8 +301,8 @@ class Rlns(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -345,7 +314,7 @@ class Rls(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) + fix_hourly_time_coordinate(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -357,8 +326,8 @@ class Rlus(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "up" return cubes @@ -396,8 +365,8 @@ class Rsds(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -409,8 +378,8 @@ class Rsns(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -422,8 +391,8 @@ class Rsus(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "up" return cubes @@ -435,8 +404,8 @@ class Rsdt(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -448,8 +417,8 @@ class Rss(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -482,7 +451,7 @@ class Tasmax(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) + fix_hourly_time_coordinate(cube, self.frequency) return cubes @@ -492,7 +461,7 @@ class Tasmin(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) + fix_hourly_time_coordinate(cube, self.frequency) return cubes @@ -567,10 +536,7 @@ def _fix_coordinates( # noqa: C901 ): coord.guess_bounds() - frequency = ( - get_frequency(cube) if self.frequency is None else self.frequency - ) - self._fix_monthly_time_coord(cube, frequency) + self._fix_monthly_time_coord(cube, self.frequency) # Fix coordinate increasing direction if cube.coords("latitude") and not has_unstructured_grid(cube): diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 3659efc936..7bf48da3b5 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -15,7 +15,6 @@ Evspsbl, Zg, fix_accumulated_units, - get_frequency, ) from esmvalcore.cmor.fix import fix_metadata from esmvalcore.cmor.table import CMOR_TABLES, get_var_info @@ -41,84 +40,6 @@ def test_get_zg_fix(): assert fix == [Zg(vardef), AllVars(vardef), GenericFix(vardef)] -def test_get_frequency_hourly(): - """Test cubes with hourly frequency.""" - time = DimCoord( - [0, 1, 2], - standard_name="time", - units=Unit("hours since 1900-01-01"), - ) - cube = Cube( - [1, 6, 3], - var_name="random_var", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "hourly" - cube.coord("time").convert_units("days since 1850-1-1 00:00:00.0") - assert get_frequency(cube) == "hourly" - - -def test_get_frequency_daily(): - """Test cubes with daily frequency.""" - time = DimCoord( - [0, 1, 2], - standard_name="time", - units=Unit("days since 1900-01-01"), - ) - cube = Cube( - [1, 6, 3], - var_name="random_var", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "daily" - cube.coord("time").convert_units("hours since 1850-1-1 00:00:00.0") - assert get_frequency(cube) == "daily" - - -def test_get_frequency_monthly(): - """Test cubes with monthly frequency.""" - time = DimCoord( - [0, 31, 59], - standard_name="time", - units=Unit("hours since 1900-01-01"), - ) - cube = Cube( - [1, 6, 3], - var_name="random_var", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "monthly" - cube.coord("time").convert_units("days since 1850-1-1 00:00:00.0") - assert get_frequency(cube) == "monthly" - - -def test_get_frequency_fx(): - """Test cubes with time invariant frequency.""" - cube = Cube(1.0, long_name="Cube without time coordinate") - assert get_frequency(cube) == "fx" - - time = DimCoord( - 0, - standard_name="time", - units=Unit("hours since 1900-01-01"), - ) - cube = Cube( - [1], - var_name="cube_with_length_1_time_coord", - long_name="Geopotential", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "fx" - - cube.long_name = ( - "Percentage of the Grid Cell Occupied by Land (Including Lakes)" - ) - assert get_frequency(cube) == "fx" - - cube.long_name = "Not geopotential" - with pytest.raises(ValueError): - get_frequency(cube) - def test_fix_accumulated_units_fail(): """Test `fix_accumulated_units`.""" @@ -133,7 +54,7 @@ def test_fix_accumulated_units_fail(): dim_coords_and_dims=[(time, 0)], ) with pytest.raises(NotImplementedError): - fix_accumulated_units(cube) + fix_accumulated_units(cube, "day") def _era5_latitude(): @@ -162,7 +83,7 @@ def _era5_time(frequency): timestamps = [788928] # hours since 1900 at 1 january 1990 elif frequency == "daily": timestamps = [788940, 788964, 788988] - elif frequency == "hourly": + elif frequency == "1hr": timestamps = [788928, 788929, 788930] elif frequency in ("monthly", "mon"): timestamps = [788928, 789672, 790344] @@ -422,9 +343,9 @@ def cl_cmor_amon(): def clt_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="cloud cover fraction", var_name="cloud_cover", units="unknown", @@ -459,9 +380,9 @@ def clt_cmor_e1hr(): def evspsbl_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly") * -1.0, + _era5_data("1hr") * -1.0, long_name="total evapotranspiration", var_name="e", units="unknown", @@ -496,9 +417,9 @@ def evspsbl_cmor_e1hr(): def evspsblpot_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly") * -1.0, + _era5_data("1hr") * -1.0, long_name="potential evapotranspiration", var_name="epot", units="unknown", @@ -533,9 +454,9 @@ def evspsblpot_cmor_e1hr(): def mrro_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="runoff", var_name="runoff", units="m", @@ -647,9 +568,9 @@ def pr_cmor_amon(): def pr_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="total_precipitation", var_name="tp", units="m", @@ -684,9 +605,9 @@ def pr_cmor_e1hr(): def prsn_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="snow", var_name="snow", units="unknown", @@ -721,9 +642,9 @@ def prsn_cmor_e1hr(): def ptype_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="snow", var_name="snow", units="unknown", @@ -759,9 +680,9 @@ def ptype_cmor_e1hr(): def rlds_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="surface thermal radiation downwards", var_name="ssrd", units="J m**-2", @@ -796,7 +717,7 @@ def rlds_cmor_e1hr(): def rlns_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -837,7 +758,7 @@ def rlns_cmor_e1hr(): def rlus_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -954,9 +875,9 @@ def rlutcs_cmor_amon(): def rls_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="runoff", var_name="runoff", units="W m-2", @@ -991,9 +912,9 @@ def rls_cmor_e1hr(): def rsds_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="solar_radiation_downwards", var_name="rlwd", units="J m**-2", @@ -1028,7 +949,7 @@ def rsds_cmor_e1hr(): def rsns_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -1069,7 +990,7 @@ def rsns_cmor_e1hr(): def rsus_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -1108,9 +1029,9 @@ def rsus_cmor_e1hr(): def rsdt_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="thermal_radiation_downwards", var_name="strd", units="J m**-2", @@ -1145,9 +1066,9 @@ def rsdt_cmor_e1hr(): def rss_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="net_solar_radiation", var_name="ssr", units="J m**-2", @@ -1211,9 +1132,9 @@ def sftlf_cmor_fx(): def tas_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="2m_temperature", var_name="t2m", units="K", @@ -1335,9 +1256,9 @@ def zg_cmor_amon(): def tasmax_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="maximum 2m temperature", var_name="mx2t", units="K", @@ -1373,9 +1294,9 @@ def tasmax_cmor_e1hr(): def tasmin_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="minimum 2m temperature", var_name="mn2t", units="K", @@ -1411,9 +1332,9 @@ def tasmin_cmor_e1hr(): def uas_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="10m_u_component_of_wind", var_name="u10", units="m s-1", @@ -1449,9 +1370,9 @@ def uas_cmor_e1hr(): def vas_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="10m_v_component_of_wind", var_name="v10", units="m s-1", From 5de098ee0b9f847ccad547e9648141ca4e24ba24 Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Wed, 8 Oct 2025 08:38:50 +1100 Subject: [PATCH 19/21] Revert "remove get_frequency, tests freq for some vars" This reverts commit 4948f387ff810ad6e7b6656943631fb36ec46526. --- esmvalcore/cmor/_fixes/native6/era5.py | 106 ++++++++---- .../cmor/_fixes/native6/test_era5.py | 159 +++++++++++++----- 2 files changed, 189 insertions(+), 76 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index 41a6387984..d950d1da8e 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -20,22 +20,53 @@ logger = logging.getLogger(__name__) +def get_frequency(cube): + """Determine time frequency of input cube.""" + try: + time = cube.coord(axis="T") + except iris.exceptions.CoordinateNotFoundError: + return "fx" + + time.convert_units("days since 1850-1-1 00:00:00.0") + if len(time.points) == 1: + acceptable_long_names = ( + "Geopotential", + "Percentage of the Grid Cell Occupied by Land (Including Lakes)", + ) + if cube.long_name not in acceptable_long_names: + msg = ( + "Unable to infer frequency of cube " + f"with length 1 time dimension: {cube}" + ) + raise ValueError( + msg, + ) + return "fx" -def fix_hourly_time_coordinate(cube, frequency): + interval = time.points[1] - time.points[0] + + if interval - 1 / 24 < 1e-4: + return "hourly" + if interval - 1.0 < 1e-4: + return "daily" + return "monthly" + + +def fix_hourly_time_coordinate(cube): """Shift aggregated variables 30 minutes back in time.""" - if frequency == "1hr": + if get_frequency(cube) == "hourly": time = cube.coord(axis="T") time.points = time.points - 1 / 48 return cube -def fix_accumulated_units(cube, frequency): +def fix_accumulated_units(cube): """Convert accumulations to fluxes.""" - if frequency == "mon": + if get_frequency(cube) == "monthly": cube.units = cube.units * "d-1" - elif frequency == "1hr": + elif get_frequency(cube) == "hourly": cube.units = cube.units * "h-1" - elif frequency == "day": + elif get_frequency(cube) == "daily": msg = ( f"Fixing of accumulated units of cube " f"{cube.summary(shorten=True)} is not implemented for daily data" @@ -132,8 +163,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) multiply_with_density(cube) # Correct sign to align with CMOR standards cube.data = cube.core_data() * -1.0 @@ -149,8 +180,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) multiply_with_density(cube) # Correct sign to align with CMOR standards cube.data = cube.core_data() * -1.0 @@ -174,8 +205,8 @@ class Mrro(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) multiply_with_density(cube) return cubes @@ -215,8 +246,8 @@ class Pr(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) multiply_with_density(cube) return cubes @@ -234,8 +265,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) multiply_with_density(cube) return cubes @@ -288,8 +319,8 @@ class Rlds(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "down" return cubes @@ -301,8 +332,8 @@ class Rlns(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "down" return cubes @@ -314,7 +345,7 @@ class Rls(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) + fix_hourly_time_coordinate(cube) cube.attributes["positive"] = "down" return cubes @@ -326,8 +357,8 @@ class Rlus(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "up" return cubes @@ -365,8 +396,8 @@ class Rsds(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "down" return cubes @@ -378,8 +409,8 @@ class Rsns(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "down" return cubes @@ -391,8 +422,8 @@ class Rsus(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "up" return cubes @@ -404,8 +435,8 @@ class Rsdt(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "down" return cubes @@ -417,8 +448,8 @@ class Rss(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) - fix_accumulated_units(cube, self.frequency) + fix_hourly_time_coordinate(cube) + fix_accumulated_units(cube) cube.attributes["positive"] = "down" return cubes @@ -451,7 +482,7 @@ class Tasmax(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) + fix_hourly_time_coordinate(cube) return cubes @@ -461,7 +492,7 @@ class Tasmin(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube, self.frequency) + fix_hourly_time_coordinate(cube) return cubes @@ -536,7 +567,10 @@ def _fix_coordinates( # noqa: C901 ): coord.guess_bounds() - self._fix_monthly_time_coord(cube, self.frequency) + frequency = ( + get_frequency(cube) if self.frequency is None else self.frequency + ) + self._fix_monthly_time_coord(cube, frequency) # Fix coordinate increasing direction if cube.coords("latitude") and not has_unstructured_grid(cube): diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 7bf48da3b5..3659efc936 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -15,6 +15,7 @@ Evspsbl, Zg, fix_accumulated_units, + get_frequency, ) from esmvalcore.cmor.fix import fix_metadata from esmvalcore.cmor.table import CMOR_TABLES, get_var_info @@ -40,6 +41,84 @@ def test_get_zg_fix(): assert fix == [Zg(vardef), AllVars(vardef), GenericFix(vardef)] +def test_get_frequency_hourly(): + """Test cubes with hourly frequency.""" + time = DimCoord( + [0, 1, 2], + standard_name="time", + units=Unit("hours since 1900-01-01"), + ) + cube = Cube( + [1, 6, 3], + var_name="random_var", + dim_coords_and_dims=[(time, 0)], + ) + assert get_frequency(cube) == "hourly" + cube.coord("time").convert_units("days since 1850-1-1 00:00:00.0") + assert get_frequency(cube) == "hourly" + + +def test_get_frequency_daily(): + """Test cubes with daily frequency.""" + time = DimCoord( + [0, 1, 2], + standard_name="time", + units=Unit("days since 1900-01-01"), + ) + cube = Cube( + [1, 6, 3], + var_name="random_var", + dim_coords_and_dims=[(time, 0)], + ) + assert get_frequency(cube) == "daily" + cube.coord("time").convert_units("hours since 1850-1-1 00:00:00.0") + assert get_frequency(cube) == "daily" + + +def test_get_frequency_monthly(): + """Test cubes with monthly frequency.""" + time = DimCoord( + [0, 31, 59], + standard_name="time", + units=Unit("hours since 1900-01-01"), + ) + cube = Cube( + [1, 6, 3], + var_name="random_var", + dim_coords_and_dims=[(time, 0)], + ) + assert get_frequency(cube) == "monthly" + cube.coord("time").convert_units("days since 1850-1-1 00:00:00.0") + assert get_frequency(cube) == "monthly" + + +def test_get_frequency_fx(): + """Test cubes with time invariant frequency.""" + cube = Cube(1.0, long_name="Cube without time coordinate") + assert get_frequency(cube) == "fx" + + time = DimCoord( + 0, + standard_name="time", + units=Unit("hours since 1900-01-01"), + ) + cube = Cube( + [1], + var_name="cube_with_length_1_time_coord", + long_name="Geopotential", + dim_coords_and_dims=[(time, 0)], + ) + assert get_frequency(cube) == "fx" + + cube.long_name = ( + "Percentage of the Grid Cell Occupied by Land (Including Lakes)" + ) + assert get_frequency(cube) == "fx" + + cube.long_name = "Not geopotential" + with pytest.raises(ValueError): + get_frequency(cube) + def test_fix_accumulated_units_fail(): """Test `fix_accumulated_units`.""" @@ -54,7 +133,7 @@ def test_fix_accumulated_units_fail(): dim_coords_and_dims=[(time, 0)], ) with pytest.raises(NotImplementedError): - fix_accumulated_units(cube, "day") + fix_accumulated_units(cube) def _era5_latitude(): @@ -83,7 +162,7 @@ def _era5_time(frequency): timestamps = [788928] # hours since 1900 at 1 january 1990 elif frequency == "daily": timestamps = [788940, 788964, 788988] - elif frequency == "1hr": + elif frequency == "hourly": timestamps = [788928, 788929, 788930] elif frequency in ("monthly", "mon"): timestamps = [788928, 789672, 790344] @@ -343,9 +422,9 @@ def cl_cmor_amon(): def clt_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="cloud cover fraction", var_name="cloud_cover", units="unknown", @@ -380,9 +459,9 @@ def clt_cmor_e1hr(): def evspsbl_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr") * -1.0, + _era5_data("hourly") * -1.0, long_name="total evapotranspiration", var_name="e", units="unknown", @@ -417,9 +496,9 @@ def evspsbl_cmor_e1hr(): def evspsblpot_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr") * -1.0, + _era5_data("hourly") * -1.0, long_name="potential evapotranspiration", var_name="epot", units="unknown", @@ -454,9 +533,9 @@ def evspsblpot_cmor_e1hr(): def mrro_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="runoff", var_name="runoff", units="m", @@ -568,9 +647,9 @@ def pr_cmor_amon(): def pr_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="total_precipitation", var_name="tp", units="m", @@ -605,9 +684,9 @@ def pr_cmor_e1hr(): def prsn_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="snow", var_name="snow", units="unknown", @@ -642,9 +721,9 @@ def prsn_cmor_e1hr(): def ptype_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="snow", var_name="snow", units="unknown", @@ -680,9 +759,9 @@ def ptype_cmor_e1hr(): def rlds_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="surface thermal radiation downwards", var_name="ssrd", units="J m**-2", @@ -717,7 +796,7 @@ def rlds_cmor_e1hr(): def rlns_era5_hourly(): - freq = "1hr" + freq = "hourly" cube = Cube( _era5_data(freq), long_name=None, @@ -758,7 +837,7 @@ def rlns_cmor_e1hr(): def rlus_era5_hourly(): - freq = "1hr" + freq = "hourly" cube = Cube( _era5_data(freq), long_name=None, @@ -875,9 +954,9 @@ def rlutcs_cmor_amon(): def rls_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="runoff", var_name="runoff", units="W m-2", @@ -912,9 +991,9 @@ def rls_cmor_e1hr(): def rsds_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="solar_radiation_downwards", var_name="rlwd", units="J m**-2", @@ -949,7 +1028,7 @@ def rsds_cmor_e1hr(): def rsns_era5_hourly(): - freq = "1hr" + freq = "hourly" cube = Cube( _era5_data(freq), long_name=None, @@ -990,7 +1069,7 @@ def rsns_cmor_e1hr(): def rsus_era5_hourly(): - freq = "1hr" + freq = "hourly" cube = Cube( _era5_data(freq), long_name=None, @@ -1029,9 +1108,9 @@ def rsus_cmor_e1hr(): def rsdt_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="thermal_radiation_downwards", var_name="strd", units="J m**-2", @@ -1066,9 +1145,9 @@ def rsdt_cmor_e1hr(): def rss_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="net_solar_radiation", var_name="ssr", units="J m**-2", @@ -1132,9 +1211,9 @@ def sftlf_cmor_fx(): def tas_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="2m_temperature", var_name="t2m", units="K", @@ -1256,9 +1335,9 @@ def zg_cmor_amon(): def tasmax_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="maximum 2m temperature", var_name="mx2t", units="K", @@ -1294,9 +1373,9 @@ def tasmax_cmor_e1hr(): def tasmin_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="minimum 2m temperature", var_name="mn2t", units="K", @@ -1332,9 +1411,9 @@ def tasmin_cmor_e1hr(): def uas_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="10m_u_component_of_wind", var_name="u10", units="m s-1", @@ -1370,9 +1449,9 @@ def uas_cmor_e1hr(): def vas_era5_hourly(): - time = _era5_time("1hr") + time = _era5_time("hourly") cube = Cube( - _era5_data("1hr"), + _era5_data("hourly"), long_name="10m_v_component_of_wind", var_name="v10", units="m s-1", From 9dbf26c1c160091aa3cc5fb1af1fef62c8011ece Mon Sep 17 00:00:00 2001 From: Felicity Chun Date: Wed, 8 Oct 2025 14:34:59 +1100 Subject: [PATCH 20/21] add to error message --- esmvalcore/cmor/_fixes/native6/era5.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index d950d1da8e..e479884b89 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -36,7 +36,8 @@ def get_frequency(cube): if cube.long_name not in acceptable_long_names: msg = ( "Unable to infer frequency of cube " - f"with length 1 time dimension: {cube}" + f"with length 1 time dimension: {cube}. " + "Use frequency facet to specify manually." ) raise ValueError( msg, From 0b6af747be74d66d307f9c23c0247e226828e762 Mon Sep 17 00:00:00 2001 From: Bouwe Andela Date: Tue, 14 Oct 2025 06:03:39 +0200 Subject: [PATCH 21/21] Fix ERA5 without get_frequency (#2855) --- esmvalcore/cmor/_fixes/native6/era5.py | 140 ++++++-------- .../cmor/_fixes/native6/test_era5.py | 181 ++++++------------ 2 files changed, 122 insertions(+), 199 deletions(-) diff --git a/esmvalcore/cmor/_fixes/native6/era5.py b/esmvalcore/cmor/_fixes/native6/era5.py index e479884b89..26f2bc9e66 100644 --- a/esmvalcore/cmor/_fixes/native6/era5.py +++ b/esmvalcore/cmor/_fixes/native6/era5.py @@ -3,7 +3,6 @@ import datetime import logging -import iris import numpy as np from iris.cube import CubeList from iris.util import reverse @@ -20,61 +19,51 @@ logger = logging.getLogger(__name__) -def get_frequency(cube): - """Determine time frequency of input cube.""" - try: - time = cube.coord(axis="T") - except iris.exceptions.CoordinateNotFoundError: - return "fx" - - time.convert_units("days since 1850-1-1 00:00:00.0") - if len(time.points) == 1: - acceptable_long_names = ( - "Geopotential", - "Percentage of the Grid Cell Occupied by Land (Including Lakes)", - ) - if cube.long_name not in acceptable_long_names: - msg = ( - "Unable to infer frequency of cube " - f"with length 1 time dimension: {cube}. " - "Use frequency facet to specify manually." - ) - raise ValueError( - msg, - ) - return "fx" - - interval = time.points[1] - time.points[0] - - if interval - 1 / 24 < 1e-4: - return "hourly" - if interval - 1.0 < 1e-4: - return "daily" - return "monthly" - - -def fix_hourly_time_coordinate(cube): +def fix_hourly_time_coordinate(cube, frequency): """Shift aggregated variables 30 minutes back in time.""" - if get_frequency(cube) == "hourly": + # While the frequency for aggregated variables is "1hr", the most common frequency + # in the CMIP6 E1hr table is "1hrPt" and in the E1hrClimMon table is "1hrCM". + # We could set the frequency to "1hr" using the extra_facets_native6.yml configuration + # file, but this would be backward incompatible for users who have already + # stored the data under a directory with the name 1hrPt. Therefore, apply + # this fix to any frequency starting with "1hr". + # + # Note that comparing instantaneous variables from CMIP6 to averaged + # variables from ERA5 may lead to some differences. + if frequency.startswith("1hr"): time = cube.coord(axis="T") - time.points = time.points - 1 / 48 + if str(time.units).startswith("hours since"): + shift = 0.5 + elif str(time.units).startswith("days since"): + shift = 1.0 / 48.0 + else: + msg = f"Unexpected time units {time.units} encountered for ERA5 data." + raise ValueError(msg) + time.points = time.points - shift return cube -def fix_accumulated_units(cube): +def fix_accumulated_units(cube, frequency): """Convert accumulations to fluxes.""" - if get_frequency(cube) == "monthly": + # While the frequency for aggregated variables is "1hr", the most common frequency + # in the CMIP6 E1hr table is "1hrPt" and in the E1hrClimMon table is "1hrCM". + # We could set the frequency to "1hr" using the extra_facets_native6.yml configuration + # file, but this would be backward incompatible for users who have already + # stored the data under a directory with the name 1hrPt. Therefore, apply + # this fix to any frequency starting with "1hr". + # + # Note that comparing instantaneous variables from CMIP6 to averaged + # variables from ERA5 may lead to some differences. + if frequency == "mon": cube.units = cube.units * "d-1" - elif get_frequency(cube) == "hourly": + elif frequency.startswith("1hr"): cube.units = cube.units * "h-1" - elif get_frequency(cube) == "daily": + elif frequency == "day": msg = ( f"Fixing of accumulated units of cube " f"{cube.summary(shorten=True)} is not implemented for daily data" ) - raise NotImplementedError( - msg, - ) + raise NotImplementedError(msg) return cube @@ -164,8 +153,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) # Correct sign to align with CMOR standards cube.data = cube.core_data() * -1.0 @@ -181,8 +170,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) # Correct sign to align with CMOR standards cube.data = cube.core_data() * -1.0 @@ -206,8 +195,8 @@ class Mrro(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) return cubes @@ -247,8 +236,8 @@ class Pr(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) return cubes @@ -266,8 +255,8 @@ def fix_metadata(self, cubes): for cube in cubes: # Set input cube units for invalid units were ignored on load cube.units = "m" - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) multiply_with_density(cube) return cubes @@ -320,8 +309,8 @@ class Rlds(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -333,8 +322,8 @@ class Rlns(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -346,7 +335,7 @@ class Rls(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) + fix_hourly_time_coordinate(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -358,8 +347,8 @@ class Rlus(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "up" return cubes @@ -397,8 +386,8 @@ class Rsds(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -410,8 +399,8 @@ class Rsns(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -423,8 +412,8 @@ class Rsus(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "up" return cubes @@ -436,8 +425,8 @@ class Rsdt(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -449,8 +438,8 @@ class Rss(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) - fix_accumulated_units(cube) + fix_hourly_time_coordinate(cube, self.frequency) + fix_accumulated_units(cube, self.frequency) cube.attributes["positive"] = "down" return cubes @@ -483,7 +472,7 @@ class Tasmax(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) + fix_hourly_time_coordinate(cube, self.frequency) return cubes @@ -493,7 +482,7 @@ class Tasmin(Fix): def fix_metadata(self, cubes): """Fix metadata.""" for cube in cubes: - fix_hourly_time_coordinate(cube) + fix_hourly_time_coordinate(cube, self.frequency) return cubes @@ -568,10 +557,7 @@ def _fix_coordinates( # noqa: C901 ): coord.guess_bounds() - frequency = ( - get_frequency(cube) if self.frequency is None else self.frequency - ) - self._fix_monthly_time_coord(cube, frequency) + self._fix_monthly_time_coord(cube, self.frequency) # Fix coordinate increasing direction if cube.coords("latitude") and not has_unstructured_grid(cube): diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 3659efc936..b183bd811b 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -15,10 +15,10 @@ Evspsbl, Zg, fix_accumulated_units, - get_frequency, ) from esmvalcore.cmor.fix import fix_metadata from esmvalcore.cmor.table import CMOR_TABLES, get_var_info +from esmvalcore.dataset import Dataset from esmvalcore.preprocessor import cmor_check_metadata COMMENT = ( @@ -41,85 +41,6 @@ def test_get_zg_fix(): assert fix == [Zg(vardef), AllVars(vardef), GenericFix(vardef)] -def test_get_frequency_hourly(): - """Test cubes with hourly frequency.""" - time = DimCoord( - [0, 1, 2], - standard_name="time", - units=Unit("hours since 1900-01-01"), - ) - cube = Cube( - [1, 6, 3], - var_name="random_var", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "hourly" - cube.coord("time").convert_units("days since 1850-1-1 00:00:00.0") - assert get_frequency(cube) == "hourly" - - -def test_get_frequency_daily(): - """Test cubes with daily frequency.""" - time = DimCoord( - [0, 1, 2], - standard_name="time", - units=Unit("days since 1900-01-01"), - ) - cube = Cube( - [1, 6, 3], - var_name="random_var", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "daily" - cube.coord("time").convert_units("hours since 1850-1-1 00:00:00.0") - assert get_frequency(cube) == "daily" - - -def test_get_frequency_monthly(): - """Test cubes with monthly frequency.""" - time = DimCoord( - [0, 31, 59], - standard_name="time", - units=Unit("hours since 1900-01-01"), - ) - cube = Cube( - [1, 6, 3], - var_name="random_var", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "monthly" - cube.coord("time").convert_units("days since 1850-1-1 00:00:00.0") - assert get_frequency(cube) == "monthly" - - -def test_get_frequency_fx(): - """Test cubes with time invariant frequency.""" - cube = Cube(1.0, long_name="Cube without time coordinate") - assert get_frequency(cube) == "fx" - - time = DimCoord( - 0, - standard_name="time", - units=Unit("hours since 1900-01-01"), - ) - cube = Cube( - [1], - var_name="cube_with_length_1_time_coord", - long_name="Geopotential", - dim_coords_and_dims=[(time, 0)], - ) - assert get_frequency(cube) == "fx" - - cube.long_name = ( - "Percentage of the Grid Cell Occupied by Land (Including Lakes)" - ) - assert get_frequency(cube) == "fx" - - cube.long_name = "Not geopotential" - with pytest.raises(ValueError): - get_frequency(cube) - - def test_fix_accumulated_units_fail(): """Test `fix_accumulated_units`.""" time = DimCoord( @@ -133,7 +54,7 @@ def test_fix_accumulated_units_fail(): dim_coords_and_dims=[(time, 0)], ) with pytest.raises(NotImplementedError): - fix_accumulated_units(cube) + fix_accumulated_units(cube, "day") def _era5_latitude(): @@ -162,7 +83,7 @@ def _era5_time(frequency): timestamps = [788928] # hours since 1900 at 1 january 1990 elif frequency == "daily": timestamps = [788940, 788964, 788988] - elif frequency == "hourly": + elif frequency == "1hr": timestamps = [788928, 788929, 788930] elif frequency in ("monthly", "mon"): timestamps = [788928, 789672, 790344] @@ -422,9 +343,9 @@ def cl_cmor_amon(): def clt_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="cloud cover fraction", var_name="cloud_cover", units="unknown", @@ -459,9 +380,9 @@ def clt_cmor_e1hr(): def evspsbl_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly") * -1.0, + _era5_data("1hr") * -1.0, long_name="total evapotranspiration", var_name="e", units="unknown", @@ -496,9 +417,9 @@ def evspsbl_cmor_e1hr(): def evspsblpot_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly") * -1.0, + _era5_data("1hr") * -1.0, long_name="potential evapotranspiration", var_name="epot", units="unknown", @@ -533,9 +454,9 @@ def evspsblpot_cmor_e1hr(): def mrro_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="runoff", var_name="runoff", units="m", @@ -647,9 +568,9 @@ def pr_cmor_amon(): def pr_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="total_precipitation", var_name="tp", units="m", @@ -684,9 +605,9 @@ def pr_cmor_e1hr(): def prsn_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="snow", var_name="snow", units="unknown", @@ -721,9 +642,9 @@ def prsn_cmor_e1hr(): def ptype_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="snow", var_name="snow", units="unknown", @@ -759,9 +680,9 @@ def ptype_cmor_e1hr(): def rlds_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="surface thermal radiation downwards", var_name="ssrd", units="J m**-2", @@ -796,7 +717,7 @@ def rlds_cmor_e1hr(): def rlns_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -837,7 +758,7 @@ def rlns_cmor_e1hr(): def rlus_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -954,9 +875,9 @@ def rlutcs_cmor_amon(): def rls_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="runoff", var_name="runoff", units="W m-2", @@ -991,9 +912,9 @@ def rls_cmor_e1hr(): def rsds_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="solar_radiation_downwards", var_name="rlwd", units="J m**-2", @@ -1028,7 +949,7 @@ def rsds_cmor_e1hr(): def rsns_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -1069,7 +990,7 @@ def rsns_cmor_e1hr(): def rsus_era5_hourly(): - freq = "hourly" + freq = "1hr" cube = Cube( _era5_data(freq), long_name=None, @@ -1108,9 +1029,9 @@ def rsus_cmor_e1hr(): def rsdt_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="thermal_radiation_downwards", var_name="strd", units="J m**-2", @@ -1145,9 +1066,9 @@ def rsdt_cmor_e1hr(): def rss_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="net_solar_radiation", var_name="ssr", units="J m**-2", @@ -1211,9 +1132,9 @@ def sftlf_cmor_fx(): def tas_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="2m_temperature", var_name="t2m", units="K", @@ -1335,9 +1256,9 @@ def zg_cmor_amon(): def tasmax_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="maximum 2m temperature", var_name="mx2t", units="K", @@ -1373,9 +1294,9 @@ def tasmax_cmor_e1hr(): def tasmin_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="minimum 2m temperature", var_name="mn2t", units="K", @@ -1411,9 +1332,9 @@ def tasmin_cmor_e1hr(): def uas_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="10m_u_component_of_wind", var_name="u10", units="m s-1", @@ -1449,9 +1370,9 @@ def uas_cmor_e1hr(): def vas_era5_hourly(): - time = _era5_time("hourly") + time = _era5_time("1hr") cube = Cube( - _era5_data("hourly"), + _era5_data("1hr"), long_name="10m_v_component_of_wind", var_name="v10", units="m s-1", @@ -1551,14 +1472,30 @@ def vas_cmor_e1hr(): @pytest.mark.parametrize(("era5_cubes", "cmor_cubes", "var", "mip"), VARIABLES) def test_cmorization(era5_cubes, cmor_cubes, var, mip): """Verify that cmorization results in the expected target cube.""" - fixed_cubes = fix_metadata(era5_cubes, var, "native6", "era5", mip) + dataset = Dataset( + short_name=var, + mip=mip, + project="native6", + dataset="ERA5", + ) + dataset.augment_facets() + + # Call `fix_metadata` and `cmor_check_metadata` with the same arguments as + # in `esmvalcore.dataset.Dataset.load`. + fixed_cubes = fix_metadata(era5_cubes, **dataset.facets) assert len(fixed_cubes) == 1 fixed_cube = fixed_cubes[0] cmor_cube = cmor_cubes[0] # Test that CMOR checks are passing - fixed_cubes = cmor_check_metadata(fixed_cube, "native6", mip, var) + fixed_cubes = cmor_check_metadata( + fixed_cube, + cmor_table="native6", + mip=mip, + short_name=var, + frequency=dataset["frequency"], + ) if fixed_cube.coords("time"): for cube in [fixed_cube, cmor_cube]: