From a9405e829815954a0ffa9db008aeec487d088521 Mon Sep 17 00:00:00 2001 From: daflack Date: Fri, 17 Jan 2025 10:28:53 +0000 Subject: [PATCH 1/8] Adds tests and function to ensure cube is aggregatable across multiple cases. Fixes #1048 --- src/CSET/operators/_utils.py | 84 +++++++++++++++++- tests/operators/test_utils.py | 68 +++++++++++++- .../long_forecast_air_temp_fcst_2.nc | Bin 0 -> 18027 bytes .../long_forecast_air_temp_fcst_3.nc | Bin 0 -> 18113 bytes .../long_forecast_air_temp_multi_day.nc | Bin 0 -> 24511 bytes 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 tests/test_data/long_forecast_air_temp_fcst_2.nc create mode 100644 tests/test_data/long_forecast_air_temp_fcst_3.nc create mode 100644 tests/test_data/long_forecast_air_temp_multi_day.nc diff --git a/src/CSET/operators/_utils.py b/src/CSET/operators/_utils.py index 0682dd804..bae065e45 100644 --- a/src/CSET/operators/_utils.py +++ b/src/CSET/operators/_utils.py @@ -1,4 +1,4 @@ -# © Crown copyright, Met Office (2022-2024) and CSET contributors. +# © Crown copyright, Met Office (2022-2025) and CSET contributors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -189,3 +189,85 @@ def fully_equalise_attributes(cubes: iris.cube.CubeList): logging.debug("Removed attributes from coordinate %s: %s", coord, removed) return cubes + + +def ensure_aggregatable_across_cases( + cube: iris.cube.Cube | iris.cube.CubeList, +) -> iris.cube.Cube: + """Ensure a Cube or CubeList can be aggregated across multiple cases. + + Arguments + --------- + cube: iris.cube.Cube | iris.cube.CubeList + If a Cube is provided a sub-operator is called to determine if the + cube has the necessary dimensional coordinates to be aggregateable. If + a CubeList is provided a Cube is created by slicing over all time + coordinates and resulting list is merged to create an aggregatable cube. + + Returns + ------- + cube: iris.cube.Cube + A time aggregatable cube with dimension coordinates including + 'forecast_period' and 'forecast_reference_time'. + + Raises + ------ + ValueError + If a Cube is provided and it is not aggregatable a ValueError is + raised. The user should then provide a CubeList to be turned into an + aggregatable cube to allow aggregation across multiple cases to occur. + """ + + def is_time_aggregatable(cube: iris.cube.Cube) -> bool: + """Determine whether a cube can be aggregated in time. + + If a cube is aggregatable it will contain both a 'forecast_reference_time' + and 'forecast_period' coordinate as dimensional coordinates. + + Arguments + --------- + cube: iris.cube.Cube + An iris cube which will be checked to see if it is aggregatable based + on a set of pre-defined dimensional time coordinates. + + Returns + ------- + bool + If true, then the cube is aggregatable and contains dimensional + coordinates including both 'forecast_reference_time' and + 'forecast_period'. + """ + # Acceptable time coordinate names for aggregatable cube. + TEMPORAL_COORD_NAMES = ["forecast_period", "forecast_reference_time"] + + # Coordinate names for the cube. + coord_names = [coord.name() for coord in cube.coords(dim_coords=True)] + + # Check which temporal coordinates we have. + temporal_coords = [ + coord for coord in coord_names if coord in TEMPORAL_COORD_NAMES + ] + if len(temporal_coords) != 2: + return False + + # Passed criterion so return True. + return True + + # Check to see if a cube is input and if that cube is iterable. + if isinstance(cube, iris.cube.Cube): + if is_time_aggregatable(cube): + return cube + else: + raise ValueError( + "Single Cube should have 'forecast_period' and" + "'forecast_reference_time' dimensional coordinates. " + "To make a time aggregatable Cube input a CubeList." + ) + # Create an aggregatable cube from the provided CubeList. + else: + new_cube_list = iris.cube.CubeList() + for x in cube: + for y in x.slices_over(["forecast_period", "forecast_reference_time"]): + new_cube_list.append(y) + new_list_merged = new_cube_list.merge()[0] + return new_list_merged diff --git a/tests/operators/test_utils.py b/tests/operators/test_utils.py index 3ccb8db9d..2a4820cac 100644 --- a/tests/operators/test_utils.py +++ b/tests/operators/test_utils.py @@ -1,4 +1,4 @@ -# © Crown copyright, Met Office (2022-2024) and CSET contributors. +# © Crown copyright, Met Office (2022-2025) and CSET contributors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,11 +17,36 @@ import iris import iris.coords import iris.cube +import numpy as np import pytest import CSET.operators._utils as operator_utils +@pytest.fixture() +def long_forecast() -> iris.cube.Cube: + """Get long_forecast to run tests on.""" + return iris.load_cube( + "tests/test_data/long_forecast_air_temp_fcst_1.nc", "air_temperature" + ) + + +@pytest.fixture() +def long_forecast_multi_day() -> iris.cube.Cube: + """Get long_forecast_multi_day to run tests on.""" + return iris.load_cube( + "tests/test_data/long_forecast_air_temp_multi_day.nc", "air_temperature" + ) + + +@pytest.fixture() +def long_forecast_many_cubes() -> iris.cube.Cube: + """Get long_forecast_may_cubes to run tests on.""" + return iris.load( + "tests/test_data/long_forecast_air_temp_fcst_*.nc", "air_temperature" + ) + + def test_missing_coord_get_cube_yxcoordname_x(regrid_rectilinear_cube): """Missing X coordinate raises error.""" regrid_rectilinear_cube.remove_coord("grid_longitude") @@ -137,3 +162,44 @@ def test_fully_equalise_attributes_equalise_coords(): fixed_cubes = operator_utils.fully_equalise_attributes([c1, c2]) for cube in fixed_cubes: assert "shared_attribute" not in cube.coord("foo").attributes + + +def test_ensure_aggregatable_across_cases_true_aggregateable_cube( + long_forecast_multi_day, +): + """Check that an aggregatable cube is returned with no changes.""" + assert np.allclose( + operator_utils.ensure_aggregatable_across_cases(long_forecast_multi_day).data, + long_forecast_multi_day.data, + rtol=1e-06, + atol=1e-02, + ) + + +def test_ensure_aggregatable_across_cases_false_aggregateable_cube(long_forecast): + """Check that a non-aggregatable cube raises an error.""" + with pytest.raises(ValueError): + operator_utils.ensure_aggregatable_across_cases(long_forecast) + + +def test_ensure_aggregatable_across_cases_cubelist( + long_forecast_many_cubes, long_forecast_multi_day +): + """Check that a CubeList turns into an aggregatable Cube.""" + # Check output is a Cube. + output_data = operator_utils.ensure_aggregatable_across_cases( + long_forecast_many_cubes + ) + assert isinstance(output_data, iris.cube.Cube) + # Check output can be aggregated in time. + assert isinstance( + operator_utils.ensure_aggregatable_across_cases(output_data), iris.cube.Cube + ) + # Check output is identical to a pre-calculated cube. + pre_calculated_data = long_forecast_multi_day + assert np.allclose( + pre_calculated_data.data, + output_data.data, + rtol=1e-06, + atol=1e-02, + ) diff --git a/tests/test_data/long_forecast_air_temp_fcst_2.nc b/tests/test_data/long_forecast_air_temp_fcst_2.nc new file mode 100644 index 0000000000000000000000000000000000000000..f96123241419cee15b5263c9a552d9ac0a0b566f GIT binary patch literal 18027 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 Nea=~zu^0LU{Rf#jG?4%R literal 0 HcmV?d00001 diff --git a/tests/test_data/long_forecast_air_temp_fcst_3.nc b/tests/test_data/long_forecast_air_temp_fcst_3.nc new file mode 100644 index 0000000000000000000000000000000000000000..96b73bc320b422e3841958b94da16cb081870a48 GIT binary patch literal 18113 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 literal 0 HcmV?d00001 diff --git a/tests/test_data/long_forecast_air_temp_multi_day.nc b/tests/test_data/long_forecast_air_temp_multi_day.nc new file mode 100644 index 0000000000000000000000000000000000000000..86f4c08c1454a52beaa35c5c70677dd8548a17c8 GIT binary patch literal 24511 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 literal 0 HcmV?d00001 From a19e17840c40b1f0a4ab2170c664a2b2120f6e68 Mon Sep 17 00:00:00 2001 From: daflack Date: Fri, 17 Jan 2025 14:42:33 +0000 Subject: [PATCH 2/8] Adds operator for collapse_by_lead_time --- src/CSET/operators/collapse.py | 50 ++++++++++++++++++++ tests/operators/test_collapse.py | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/src/CSET/operators/collapse.py b/src/CSET/operators/collapse.py index 71bb03a8c..d536f7b9e 100644 --- a/src/CSET/operators/collapse.py +++ b/src/CSET/operators/collapse.py @@ -21,6 +21,8 @@ import iris.coord_categorisation import iris.cube +from CSET.operators._utils import ensure_aggregatable_across_cases + def collapse( cube: iris.cube.Cube, @@ -76,6 +78,54 @@ def collapse( return collapsed_cube +def collapse_by_lead_time( + cube: iris.cube.Cube | iris.cube.CubeList, + method: str, + additional_percent: float = None, + **kwargs, +) -> iris.cube.Cube: + """Collapse a cube around lead time for multiple cases. + + First checks if the data can be aggregated by lead time easily. Then + collapses by lead time for a specified method using the collapse function. + + Arguments + --------- + cube: iris.cube.Cube | iris.cube.CubeList + Cube to collapse by lead time or CubeList that will be converted + to a cube before collapsing by lead time. + method: str + Type of collapse i.e. method: 'MEAN', 'MAX', 'MIN', 'MEDIAN', + 'PERCENTILE' getattr creates iris.analysis.MEAN, etc For PERCENTILE + YAML file requires i.e. method: 'PERCENTILE' additional_percent: 90 + + Returns + ------- + cube: iris.cube.Cube + Single variable collapsed by lead time based on chosen method. + + Raises + ------ + ValueError + If additional_percent wasn't supplied while using PERCENTILE method. + """ + if method == "PERCENTILE" and additional_percent is None: + raise ValueError("Must specify additional_percent") + # Ensure the cube can be aggregated over mutlipe cases. + cube_to_collapse = ensure_aggregatable_across_cases(cube) + # Collapse by lead time. + if method == "PERCENTILE": + collapsed_cube = collapse( + cube_to_collapse, + "forecast_period", + method, + additional_percent=additional_percent, + ) + else: + collapsed_cube = collapse(cube_to_collapse, "forecast_period", method) + return collapsed_cube + + def collapse_by_hour_of_day( cube: iris.cube.Cube, method: str, diff --git a/tests/operators/test_collapse.py b/tests/operators/test_collapse.py index 7b28660a2..dcd7f528f 100644 --- a/tests/operators/test_collapse.py +++ b/tests/operators/test_collapse.py @@ -16,6 +16,7 @@ import iris import iris.cube +import numpy as np import pytest from CSET.operators import collapse @@ -29,6 +30,22 @@ def long_forecast() -> iris.cube.Cube: ) +@pytest.fixture() +def long_forecast_multi_day() -> iris.cube.Cube: + """Get long_forecast_multi_day to run tests on.""" + return iris.load_cube( + "tests/test_data/long_forecast_air_temp_multi_day.nc", "air_temperature" + ) + + +@pytest.fixture() +def long_forecast_many_cubes() -> iris.cube.Cube: + """Get long_forecast_may_cubes to run tests on.""" + return iris.load( + "tests/test_data/long_forecast_air_temp_fcst_*.nc", "air_temperature" + ) + + def test_collapse(cube): """Reduces dimension of cube.""" # Test collapsing a single coordinate. @@ -79,3 +96,67 @@ def test_collapse_by_hour_of_day_percentile(long_forecast): ) expected_cube = "" assert repr(collapsed_cube) == expected_cube + + +def test_collapse_by_lead_time_single_cube(long_forecast_multi_day): + """Check cube collapse by lead time.""" + calculated_cube = collapse.collapse( + long_forecast_multi_day, "forecast_period", "MEAN" + ) + assert np.allclose( + calculated_cube.data, + collapse.collapse_by_lead_time(long_forecast_multi_day, "MEAN").data, + rtol=1e-06, + atol=1e-02, + ) + + +def test_collapse_by_lead_time_cube_list( + long_forecast_multi_day, long_forecast_many_cubes +): + """Check CubeList is made into an aggregatable cube and collapses by lead time.""" + calculated_cube = collapse.collapse( + long_forecast_multi_day, "forecast_period", "MEAN" + ) + assert np.allclose( + calculated_cube.data, + collapse.collapse_by_lead_time(long_forecast_many_cubes, "MEAN").data, + rtol=1e-06, + atol=1e-02, + ) + + +def test_collapse_by_lead_time_single_cube_percentile(long_forecast_multi_day): + """Check Cube collapse by lead time with percentiles.""" + calculated_cube = collapse.collapse( + long_forecast_multi_day, "forecast_period", "PERCENTILE", additional_percent=75 + ) + with pytest.raises(ValueError): + collapse.collapse_by_lead_time(long_forecast_multi_day, "PERCENTILE") + assert np.allclose( + calculated_cube.data, + collapse.collapse_by_lead_time( + long_forecast_multi_day, "PERCENTILE", additional_percent=75 + ).data, + rtol=1e-06, + atol=1e-02, + ) + + +def test_collapse_by_lead_time_cube_list_percentile( + long_forecast_multi_day, long_forecast_many_cubes +): + """Check CubeList is made into an aggregatable cube and collapses by lead time with percentiles.""" + calculated_cube = collapse.collapse( + long_forecast_multi_day, "forecast_period", "PERCENTILE", additional_percent=75 + ) + with pytest.raises(ValueError): + collapse.collapse_by_lead_time(long_forecast_many_cubes, "PERCENTILE") + assert np.allclose( + calculated_cube.data, + collapse.collapse_by_lead_time( + long_forecast_many_cubes, "PERCENTILE", additional_percent=75 + ).data, + rtol=1e-06, + atol=1e-02, + ) From e883f6d0490825d527566126cb3b11e716b00b52 Mon Sep 17 00:00:00 2001 From: David Flack <77390156+daflack@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:07:28 +0000 Subject: [PATCH 3/8] Apply suggestions from code review --- src/CSET/operators/collapse.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CSET/operators/collapse.py b/src/CSET/operators/collapse.py index d536f7b9e..41e7d0575 100644 --- a/src/CSET/operators/collapse.py +++ b/src/CSET/operators/collapse.py @@ -96,8 +96,7 @@ def collapse_by_lead_time( to a cube before collapsing by lead time. method: str Type of collapse i.e. method: 'MEAN', 'MAX', 'MIN', 'MEDIAN', - 'PERCENTILE' getattr creates iris.analysis.MEAN, etc For PERCENTILE - YAML file requires i.e. method: 'PERCENTILE' additional_percent: 90 + 'PERCENTILE'. For 'PERCENTILE' the additional_percent must be specified. Returns ------- From cb359f3c4766d9b814296445ebd7f0ffecfbb6c6 Mon Sep 17 00:00:00 2001 From: Sylvia Bohnenstengel <62748926+Sylviabohnenstengel@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:21:54 +0000 Subject: [PATCH 4/8] Update _utils.py --- src/CSET/operators/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSET/operators/_utils.py b/src/CSET/operators/_utils.py index bae065e45..455eb3510 100644 --- a/src/CSET/operators/_utils.py +++ b/src/CSET/operators/_utils.py @@ -202,7 +202,7 @@ def ensure_aggregatable_across_cases( If a Cube is provided a sub-operator is called to determine if the cube has the necessary dimensional coordinates to be aggregateable. If a CubeList is provided a Cube is created by slicing over all time - coordinates and resulting list is merged to create an aggregatable cube. + coordinates and the resulting list is merged to create an aggregatable cube. Returns ------- From 891b0e054141ce8b4a52100be40a848a41d0c2b0 Mon Sep 17 00:00:00 2001 From: David Flack <77390156+daflack@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:46:42 +0000 Subject: [PATCH 5/8] Apply suggestions from code review --- src/CSET/operators/_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CSET/operators/_utils.py b/src/CSET/operators/_utils.py index 455eb3510..b0f28d137 100644 --- a/src/CSET/operators/_utils.py +++ b/src/CSET/operators/_utils.py @@ -266,8 +266,8 @@ def is_time_aggregatable(cube: iris.cube.Cube) -> bool: # Create an aggregatable cube from the provided CubeList. else: new_cube_list = iris.cube.CubeList() - for x in cube: - for y in x.slices_over(["forecast_period", "forecast_reference_time"]): - new_cube_list.append(y) - new_list_merged = new_cube_list.merge()[0] - return new_list_merged + for sub_cube in cube: + for cube_slice in sub_cube.slices_over(["forecast_period", "forecast_reference_time"]): + new_cube_list.append(cube_slice) + new_merged_cube = new_cube_list.merge_cube() + return new_merged_cube From 54a00d73e49da89ab1dcf87208137d9dddfb27b1 Mon Sep 17 00:00:00 2001 From: Sylvia Bohnenstengel <62748926+Sylviabohnenstengel@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:48:15 +0000 Subject: [PATCH 6/8] Update collapse.py --- src/CSET/operators/collapse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSET/operators/collapse.py b/src/CSET/operators/collapse.py index 41e7d0575..574d2ce2b 100644 --- a/src/CSET/operators/collapse.py +++ b/src/CSET/operators/collapse.py @@ -110,7 +110,7 @@ def collapse_by_lead_time( """ if method == "PERCENTILE" and additional_percent is None: raise ValueError("Must specify additional_percent") - # Ensure the cube can be aggregated over mutlipe cases. + # Ensure the cube can be aggregated over mutiple cases. cube_to_collapse = ensure_aggregatable_across_cases(cube) # Collapse by lead time. if method == "PERCENTILE": From c2d19d8f98bce836f70dd6f4a586e9c228f834b9 Mon Sep 17 00:00:00 2001 From: daflack Date: Wed, 22 Jan 2025 16:07:27 +0000 Subject: [PATCH 7/8] Apply changes from code review --- src/CSET/operators/_utils.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/CSET/operators/_utils.py b/src/CSET/operators/_utils.py index b0f28d137..bdae972a7 100644 --- a/src/CSET/operators/_utils.py +++ b/src/CSET/operators/_utils.py @@ -200,9 +200,11 @@ def ensure_aggregatable_across_cases( --------- cube: iris.cube.Cube | iris.cube.CubeList If a Cube is provided a sub-operator is called to determine if the - cube has the necessary dimensional coordinates to be aggregateable. If - a CubeList is provided a Cube is created by slicing over all time - coordinates and the resulting list is merged to create an aggregatable cube. + cube has the necessary dimensional coordinates to be aggregateable. + These necessary coordinates are 'forecast_period' and + 'forecast_reference_time'.If a CubeList is provided a Cube is created + by slicing over all time coordinates and the resulting list is merged + to create an aggregatable cube. Returns ------- @@ -228,7 +230,8 @@ def is_time_aggregatable(cube: iris.cube.Cube) -> bool: --------- cube: iris.cube.Cube An iris cube which will be checked to see if it is aggregatable based - on a set of pre-defined dimensional time coordinates. + on a set of pre-defined dimensional time coordinates: + 'forecast_period' and 'forecast_reference_time'. Returns ------- @@ -247,11 +250,8 @@ def is_time_aggregatable(cube: iris.cube.Cube) -> bool: temporal_coords = [ coord for coord in coord_names if coord in TEMPORAL_COORD_NAMES ] - if len(temporal_coords) != 2: - return False - - # Passed criterion so return True. - return True + # Return whether both coordinates are in the temporal coordinates. + return len(temporal_coords) == 2 # Check to see if a cube is input and if that cube is iterable. if isinstance(cube, iris.cube.Cube): @@ -267,7 +267,9 @@ def is_time_aggregatable(cube: iris.cube.Cube) -> bool: else: new_cube_list = iris.cube.CubeList() for sub_cube in cube: - for cube_slice in sub_cube.slices_over(["forecast_period", "forecast_reference_time"]): + for cube_slice in sub_cube.slices_over( + ["forecast_period", "forecast_reference_time"] + ): new_cube_list.append(cube_slice) new_merged_cube = new_cube_list.merge_cube() return new_merged_cube From 469cacfb63e9426c5948528488d2c42097f99b17 Mon Sep 17 00:00:00 2001 From: daflack Date: Wed, 22 Jan 2025 16:09:50 +0000 Subject: [PATCH 8/8] Fix typo --- src/CSET/operators/collapse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSET/operators/collapse.py b/src/CSET/operators/collapse.py index 574d2ce2b..75a4566ef 100644 --- a/src/CSET/operators/collapse.py +++ b/src/CSET/operators/collapse.py @@ -110,7 +110,7 @@ def collapse_by_lead_time( """ if method == "PERCENTILE" and additional_percent is None: raise ValueError("Must specify additional_percent") - # Ensure the cube can be aggregated over mutiple cases. + # Ensure the cube can be aggregated over multiple cases. cube_to_collapse = ensure_aggregatable_across_cases(cube) # Collapse by lead time. if method == "PERCENTILE":