From e6e1574b03487212d3e8e6167c9773d8f3671862 Mon Sep 17 00:00:00 2001 From: "tomos.evans" Date: Thu, 12 Mar 2026 16:25:39 +0000 Subject: [PATCH 1/2] updated collapse and utility functions for area average, with example recipe. --- src/CSET/operators/collapse.py | 16 +++++++++ src/CSET/operators/misc.py | 36 +++++++++++++++++++ .../generic_area_average_time_series.yaml | 35 ++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/CSET/recipes/surface_fields/generic_area_average_time_series.yaml diff --git a/src/CSET/operators/collapse.py b/src/CSET/operators/collapse.py index dcd299484..88838e19b 100644 --- a/src/CSET/operators/collapse.py +++ b/src/CSET/operators/collapse.py @@ -29,6 +29,7 @@ from CSET._common import iter_maybe from CSET.operators.aggregate import add_hour_coordinate +from CSET.operators.misc import guess_bounds def collapse( @@ -112,6 +113,21 @@ def collapse( cube_max = cube.collapsed(coordinate, iris.analysis.MAX) cube_min = cube.collapsed(coordinate, iris.analysis.MIN) collapsed_cubes.append(cube_max - cube_min) + elif method == "AREA_AVG": + # for now, just deal with forecast_period, valid time and hour plot types can come later + # drop scalar coords cube.coord('forecast_reference_time') + + # Mask nans + if np.count_nonzero(np.isnan(cube.data)) > 0: + cube.data = np.ma.masked_invalid(cube.data) + + # Compute grid cell areas + cube = guess_bounds(cube) + grid_areas = iris.analysis.cartography.area_weights(cube) + + collapsed_cubes.append( + cube.collapsed(coordinate, iris.analysis.MEAN, weights=grid_areas) + ) else: collapsed_cubes.append( cube.collapsed(coordinate, getattr(iris.analysis, method)) diff --git a/src/CSET/operators/misc.py b/src/CSET/operators/misc.py index 651eb5d34..ffdd59966 100644 --- a/src/CSET/operators/misc.py +++ b/src/CSET/operators/misc.py @@ -428,3 +428,39 @@ def convert_units(cubes: iris.cube.Cube | iris.cube.CubeList, units: str): return new_cubelist[0] else: return new_cubelist + + +def guess_bounds(cube): + """ + Guess bounds for x and y coordinates on a cube. + + Arguments + --------- + cube: iris.cube.Cube + Input cube whose x and y coordinate bounds will be guessed if missing. + + Returns + ------- + iris.cube.Cube + The same cube with bounds added to x and y coordinates where absent. + + Raises + ------ + ValueError + If the cube uses a variable resolution grid where bounds cannot be + guessed reliably. + """ + # Loop over spatial coordinates + for axis in ["x", "y"]: + coord = cube.coord(axis=axis) + try: + _ = iris.util.regular_step(coord) + except ValueError as e: + logging.warning( + "Cannot guess bounds for a variable resolution (non-regular) grid: %s", + e, + ) + # Guess bounds if there aren't any + if coord.bounds is None: + coord.guess_bounds() + return cube diff --git a/src/CSET/recipes/surface_fields/generic_area_average_time_series.yaml b/src/CSET/recipes/surface_fields/generic_area_average_time_series.yaml new file mode 100644 index 000000000..b705b5e44 --- /dev/null +++ b/src/CSET/recipes/surface_fields/generic_area_average_time_series.yaml @@ -0,0 +1,35 @@ +category: Surface Time Series +title: Domain area average $VARNAME time series +description: Plots a time series of the domain area average $VARNAME. + +steps: + - operator: read.read_cubes + file_paths: $INPUT_PATHS + model_names: $MODEL_NAME + constraint: + operator: constraints.combine_constraints + varname_constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + cell_methods_constraint: + operator: constraints.generate_cell_methods_constraint + cell_methods: [] + varname: $VARNAME + pressure_level_constraint: + operator: constraints.generate_level_constraint + coordinate: "pressure" + levels: [] + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + + # Area-weighted average + - operator: collapse.collapse + coordinate: [grid_latitude, grid_longitude] + method: AREA_AVG + + # Make a single NetCDF with all the data inside it. + - operator: write.write_cube_to_nc + overwrite: True + + # Plot the data. + - operator: plot.plot_line_series From be0733f3bf6eebdd5af98a78ae0ce59e91f1c544 Mon Sep 17 00:00:00 2001 From: "tomos.evans" Date: Fri, 13 Mar 2026 11:29:05 +0000 Subject: [PATCH 2/2] remove unnecessary comments --- src/CSET/operators/collapse.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/CSET/operators/collapse.py b/src/CSET/operators/collapse.py index 88838e19b..9682fc0a6 100644 --- a/src/CSET/operators/collapse.py +++ b/src/CSET/operators/collapse.py @@ -114,9 +114,6 @@ def collapse( cube_min = cube.collapsed(coordinate, iris.analysis.MIN) collapsed_cubes.append(cube_max - cube_min) elif method == "AREA_AVG": - # for now, just deal with forecast_period, valid time and hour plot types can come later - # drop scalar coords cube.coord('forecast_reference_time') - # Mask nans if np.count_nonzero(np.isnan(cube.data)) > 0: cube.data = np.ma.masked_invalid(cube.data)