From 80d9cc310b2f8009059f80222dd5cc19d3443a90 Mon Sep 17 00:00:00 2001 From: SGallagherMet Date: Thu, 18 Sep 2025 16:10:35 +0100 Subject: [PATCH 1/4] Use custom cylc triggers to skip 'baking' when 'parbake' didn't find any recipes to configure. --- .../app/parbake_recipes/bin/parbake.py | 26 ++++++++++++-- src/CSET/cset_workflow/flow.cylc | 15 ++++++-- tests/workflow_utils/test_parbake_recipes.py | 34 +++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py b/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py index 565901a27..3044cafae 100755 --- a/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py +++ b/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py @@ -17,13 +17,16 @@ import json import os +import subprocess from base64 import b64decode from pathlib import Path from CSET.recipes import load_recipes -def parbake_all(variables: dict, rose_datac: Path, share_dir: Path, aggregation: bool): +def parbake_all( + variables: dict, rose_datac: Path, share_dir: Path, aggregation: bool +) -> int: """Generate and parbake recipes from configuration.""" # Gather all recipes into a big list. recipes = list(load_recipes(variables)) @@ -31,9 +34,12 @@ def parbake_all(variables: dict, rose_datac: Path, share_dir: Path, aggregation: if not recipes: raise ValueError("At least one recipe should be enabled.") # Parbake all recipes remaining after filtering aggregation recipes. + recipe_count = 0 for recipe in filter(lambda r: r.aggregation == aggregation, recipes): print(f"Parbaking {recipe}", flush=True) recipe.parbake(rose_datac, share_dir) + recipe_count += 1 + return recipe_count def main(): @@ -44,7 +50,23 @@ def main(): share_dir = Path(os.environ["CYLC_WORKFLOW_SHARE_DIR"]) aggregation = bool(os.getenv("DO_CASE_AGGREGATION")) # Parbake recipes for cycle. - parbake_all(variables, rose_datac, share_dir, aggregation) + recipe_count = parbake_all(variables, rose_datac, share_dir, aggregation) + + # If running under cylc, notify cylc of task completion. + cylc_workflow_id = os.environ.get("CYLC_WORKFLOW_ID", None) + cylc_task_job = os.environ.get("CYLC_TASK_JOB", None) + if cylc_workflow_id and cylc_task_job: + message_command = [ + "cylc", + "message", + "--", + cylc_workflow_id, + cylc_task_job, + ] + if recipe_count > 0: + subprocess.run(message_command + ["start baking"]) + else: + subprocess.run(message_command + ["skip baking"]) if __name__ == "__main__": # pragma: no cover diff --git a/src/CSET/cset_workflow/flow.cylc b/src/CSET/cset_workflow/flow.cylc index ecfd22104..5d6dbc8f3 100644 --- a/src/CSET/cset_workflow/flow.cylc +++ b/src/CSET/cset_workflow/flow.cylc @@ -46,14 +46,18 @@ final cycle point = {{CSET_TRIAL_END_DATE}} # Analysis from each forecast. {{CSET_TRIAL_CYCLE_PERIOD}} = """ setup_complete[^] => FETCH_DATA:succeed-all => fetch_complete - fetch_complete & parbake_recipes => bake_recipes => cycle_complete + fetch_complete & parbake_recipes:start_baking? => bake_recipes? + + parbake_recipes:skip_baking? | bake_recipes? => cycle_complete """ {% endif %} # Only runs on the final cycle. R1/$ = """ # Run aggregation recipes. - fetch_complete & parbake_aggregation_recipes => bake_aggregation_recipes => cycle_complete + fetch_complete & parbake_aggregation_recipes:start_baking? => bake_aggregation_recipes? + + parbake_aggregation_recipes:skip_baking? | bake_aggregation_recipes? => cycle_complete # Finalise website and cleanup. cycle_complete => finish_website => send_email cycle_complete => housekeeping @@ -97,6 +101,11 @@ final cycle point = {{CSET_TRIAL_END_DATE}} [[[environment]]] ANALYSIS_LENGTH = {{ANALYSIS_LENGTH}} + [[PARBAKE]] + [[[outputs]]] + start_baking='start baking' + skip_baking='skip baking' + [[METPLUS]] [[[environment]]] {% if METPLUS_GRID_STAT|default(False) %} @@ -153,6 +162,7 @@ final cycle point = {{CSET_TRIAL_END_DATE}} [[parbake_recipes]] # Parbake all the recipes for this cycle. + inherit=PARBAKE script = rose task-run -v --app-key=parbake_recipes execution time limit = PT5M [[[directives]]] @@ -163,6 +173,7 @@ final cycle point = {{CSET_TRIAL_END_DATE}} [[parbake_aggregation_recipes]] # Parbake all the aggregation recipes. + inherit=PARBAKE script = rose task-run -v --app-key=parbake_recipes execution time limit = PT5M [[[directives]]] diff --git a/tests/workflow_utils/test_parbake_recipes.py b/tests/workflow_utils/test_parbake_recipes.py index 894f0655f..367b4441a 100644 --- a/tests/workflow_utils/test_parbake_recipes.py +++ b/tests/workflow_utils/test_parbake_recipes.py @@ -14,6 +14,7 @@ """Tests for parbake_recipe workflow utility.""" +import subprocess from pathlib import Path import pytest @@ -25,14 +26,28 @@ def test_main(monkeypatch): """Check parbake.main() invokes parbake_all correctly.""" function_ran = False + recipes_parbaked = 0 + cylc_message_ran = False + cylc_message = "" def mock_parbake_all(variables, rose_datac, share_dir, aggregation): nonlocal function_ran + nonlocal recipes_parbaked function_ran = True assert variables == {"variable": "value"} assert rose_datac == Path("/share/cycle/20000101T0000Z") assert share_dir == Path("/share") assert isinstance(aggregation, bool) + return recipes_parbaked + + def mock_run(cmd, **kwargs): + nonlocal cylc_message + nonlocal cylc_message_ran + cylc_message_ran = True + assert cmd[0:3] == ["cylc", "message", "--"] + assert cmd[3] == "test-workflow" + assert cmd[4] == "test-job" + assert cmd[5] == cylc_message monkeypatch.setattr(parbake, "parbake_all", mock_parbake_all) @@ -51,6 +66,25 @@ def mock_parbake_all(variables, rose_datac, share_dir, aggregation): parbake.main() assert function_ran, "Function did not run!" + # Retry with cylc environment variables set. + monkeypatch.setattr(subprocess, "run", mock_run) + monkeypatch.setenv("CYLC_WORKFLOW_ID", "test-workflow") + monkeypatch.setenv("CYLC_TASK_JOB", "test-job") + + # No recipes parbaked. + function_ran = False + recipes_parbaked = 0 + cylc_message = "skip baking" + parbake.main() + assert cylc_message_ran, "Cylc message function did not run!" + + # Some recipes parbaked. + function_ran = False + recipes_parbaked = 3 + cylc_message = "start baking" + parbake.main() + assert cylc_message_ran, "Cylc message function did not run!" + def test_parbake_all_none_enabled(tmp_working_dir, monkeypatch): """Error when no recipes are enabled.""" From aa94b1fbbb5f2285923b69297153f128cc64bd6c Mon Sep 17 00:00:00 2001 From: SGallagherMet Date: Wed, 24 Sep 2025 16:52:02 +0100 Subject: [PATCH 2/4] Update recipes that load a single parameter to constrain using only the stash code or field name during the initial call to iris.load. --- .../recipes/generic_level_domain_mean_time_series.yaml | 8 ++++++-- ...vel_domain_mean_time_series_case_aggregation_all.yaml | 4 ++++ ...in_mean_time_series_case_aggregation_hour_of_day.yaml | 4 ++++ ...main_mean_time_series_case_aggregation_lead_time.yaml | 4 ++++ ..._mean_time_series_case_aggregation_validity_time.yaml | 4 ++++ ...eneric_level_domain_mean_vertical_profile_series.yaml | 8 ++++++-- ...ean_vertical_profile_series_case_aggregation_all.yaml | 4 ++++ ...ical_profile_series_case_aggregation_hour_of_day.yaml | 8 ++++++-- ...rtical_profile_series_case_aggregation_lead_time.yaml | 8 ++++++-- ...al_profile_series_case_aggregation_validity_time.yaml | 8 ++++++-- src/CSET/recipes/generic_level_histogram_series.yaml | 8 ++++++-- ...eric_level_histogram_series_case_aggregation_all.yaml | 8 ++++++-- ...el_histogram_series_case_aggregation_hour_of_day.yaml | 8 ++++++-- ...evel_histogram_series_case_aggregation_lead_time.yaml | 8 ++++++-- ..._histogram_series_case_aggregation_validity_time.yaml | 8 ++++++-- .../recipes/generic_level_spatial_plot_sequence.yaml | 8 ++++++-- ..._spatial_plot_sequence_case_aggregation_mean_all.yaml | 4 ++++ ..._plot_sequence_case_aggregation_mean_hour_of_day.yaml | 8 ++++++-- ...al_plot_sequence_case_aggregation_mean_lead_time.yaml | 8 ++++++-- ...lot_sequence_case_aggregation_mean_validity_time.yaml | 8 ++++++-- .../recipes/generic_surface_domain_mean_time_series.yaml | 8 ++++++-- ...ace_domain_mean_time_series_case_aggregation_all.yaml | 4 ++++ ...in_mean_time_series_case_aggregation_hour_of_day.yaml | 8 ++++++-- ...main_mean_time_series_case_aggregation_lead_time.yaml | 8 ++++++-- ..._mean_time_series_case_aggregation_validity_time.yaml | 8 ++++++-- src/CSET/recipes/generic_surface_histogram_series.yaml | 8 ++++++-- ...ic_surface_histogram_series_case_aggregation_all.yaml | 8 ++++++-- ...ce_histogram_series_case_aggregation_hour_of_day.yaml | 8 ++++++-- ...face_histogram_series_case_aggregation_lead_time.yaml | 8 ++++++-- ..._histogram_series_case_aggregation_validity_time.yaml | 8 ++++++-- .../generic_surface_single_point_time_series.yaml | 4 ++++ .../recipes/generic_surface_spatial_plot_sequence.yaml | 9 ++++++--- ..._spatial_plot_sequence_case_aggregation_mean_all.yaml | 8 ++++++-- ..._plot_sequence_case_aggregation_mean_hour_of_day.yaml | 8 ++++++-- ...al_plot_sequence_case_aggregation_mean_lead_time.yaml | 8 ++++++-- ...lot_sequence_case_aggregation_mean_validity_time.yaml | 8 ++++++-- .../generic_surface_spatial_plot_sequence_regrid.yaml | 4 ++++ src/CSET/recipes/level_spatial_difference.yaml | 8 ++++++-- ...vel_spatial_difference_case_aggregation_mean_all.yaml | 4 ++++ ...ial_difference_case_aggregation_mean_hour_of_day.yaml | 4 ++++ ...atial_difference_case_aggregation_mean_lead_time.yaml | 4 ++++ ...l_difference_case_aggregation_mean_validity_time.yaml | 4 ++++ src/CSET/recipes/surface_spatial_difference.yaml | 8 ++++++-- ...ace_spatial_difference_case_aggregation_mean_all.yaml | 8 ++++++-- ...ial_difference_case_aggregation_mean_hour_of_day.yaml | 8 ++++++-- ...atial_difference_case_aggregation_mean_lead_time.yaml | 8 ++++++-- ...l_difference_case_aggregation_mean_validity_time.yaml | 8 ++++++-- src/CSET/recipes/transect.yaml | 4 ++++ 48 files changed, 260 insertions(+), 69 deletions(-) diff --git a/src/CSET/recipes/generic_level_domain_mean_time_series.yaml b/src/CSET/recipes/generic_level_domain_mean_time_series.yaml index aea234d54..a69670e5d 100644 --- a/src/CSET/recipes/generic_level_domain_mean_time_series.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_time_series.yaml @@ -6,6 +6,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -15,8 +21,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: collapse.collapse coordinate: [grid_latitude, grid_longitude] diff --git a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_all.yaml b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_all.yaml index e09a51bb8..06ed0ffe0 100644 --- a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_all.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_all.yaml @@ -6,6 +6,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_hour_of_day.yaml b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_hour_of_day.yaml index 222735f22..63d43a21d 100644 --- a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_hour_of_day.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_hour_of_day.yaml @@ -10,6 +10,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_lead_time.yaml b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_lead_time.yaml index 321277b89..7df64f02b 100644 --- a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_lead_time.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_lead_time.yaml @@ -10,6 +10,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_validity_time.yaml b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_validity_time.yaml index cbbadefb0..0d635ab1d 100644 --- a/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_validity_time.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_time_series_case_aggregation_validity_time.yaml @@ -10,6 +10,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series.yaml b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series.yaml index 0f530ce6a..73fc08329 100644 --- a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series.yaml @@ -9,6 +9,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -18,8 +24,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: "*" - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: collapse.collapse coordinate: [grid_latitude, grid_longitude] diff --git a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_all.yaml b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_all.yaml index c3e8cdbf1..727186d86 100644 --- a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_all.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_all.yaml @@ -10,6 +10,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: diff --git a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_hour_of_day.yaml b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_hour_of_day.yaml index abb010022..fbbffd10e 100644 --- a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_hour_of_day.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_hour_of_day.yaml @@ -11,6 +11,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -20,8 +26,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: "*" - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_lead_time.yaml b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_lead_time.yaml index fe87c060d..58969b96a 100644 --- a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_lead_time.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_lead_time.yaml @@ -11,6 +11,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -20,8 +26,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: "*" - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_validity_time.yaml b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_validity_time.yaml index 873466054..283f1c0ac 100644 --- a/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_validity_time.yaml +++ b/src/CSET/recipes/generic_level_domain_mean_vertical_profile_series_case_aggregation_validity_time.yaml @@ -11,6 +11,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -20,8 +26,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: "*" - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_histogram_series.yaml b/src/CSET/recipes/generic_level_histogram_series.yaml index c6275b07d..a1ec00487 100644 --- a/src/CSET/recipes/generic_level_histogram_series.yaml +++ b/src/CSET/recipes/generic_level_histogram_series.yaml @@ -13,6 +13,12 @@ steps: - operator: read.read_cubes model_names: $MODEL_NAME file_paths: $INPUT_PATHS + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -26,8 +32,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: plot.plot_histogram_series sequence_coordinate: $SEQUENCE diff --git a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_all.yaml b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_all.yaml index a82b5c0d9..a799fe2fb 100644 --- a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_all.yaml +++ b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_all.yaml @@ -15,6 +15,12 @@ steps: - operator: read.read_cubes model_names: $MODEL_NAME file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -28,8 +34,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_hour_of_day.yaml b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_hour_of_day.yaml index 23dc7a59b..028673ab7 100644 --- a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_hour_of_day.yaml +++ b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_hour_of_day.yaml @@ -15,6 +15,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -28,8 +34,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_lead_time.yaml b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_lead_time.yaml index 93cd960bf..da4fc8def 100644 --- a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_lead_time.yaml +++ b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_lead_time.yaml @@ -15,6 +15,12 @@ steps: - operator: read.read_cubes model_names: $MODEL_NAME file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -28,8 +34,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_validity_time.yaml b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_validity_time.yaml index 611e929c2..c3a21f4f5 100644 --- a/src/CSET/recipes/generic_level_histogram_series_case_aggregation_validity_time.yaml +++ b/src/CSET/recipes/generic_level_histogram_series_case_aggregation_validity_time.yaml @@ -15,6 +15,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -28,8 +34,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_spatial_plot_sequence.yaml b/src/CSET/recipes/generic_level_spatial_plot_sequence.yaml index 587f04a55..e8ca5c3cb 100644 --- a/src/CSET/recipes/generic_level_spatial_plot_sequence.yaml +++ b/src/CSET/recipes/generic_level_spatial_plot_sequence.yaml @@ -6,6 +6,12 @@ description: | steps: - operator: read.read_cube file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -15,8 +21,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: collapse.collapse coordinate: [time] diff --git a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_all.yaml b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_all.yaml index 7d81d6537..31fcb5450 100644 --- a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_all.yaml +++ b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_all.yaml @@ -7,6 +7,10 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: diff --git a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml index c6f873ea2..32c0a3e66 100644 --- a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml +++ b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml @@ -7,6 +7,12 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -16,8 +22,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml index f7d3b11bb..be54f293d 100644 --- a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml +++ b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml @@ -7,6 +7,12 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -16,8 +22,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml index 9e58615ec..dc550ced7 100644 --- a/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml +++ b/src/CSET/recipes/generic_level_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml @@ -7,6 +7,12 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -16,8 +22,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_domain_mean_time_series.yaml b/src/CSET/recipes/generic_surface_domain_mean_time_series.yaml index a6334c6c1..bc80a39e4 100644 --- a/src/CSET/recipes/generic_surface_domain_mean_time_series.yaml +++ b/src/CSET/recipes/generic_surface_domain_mean_time_series.yaml @@ -6,6 +6,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -19,8 +25,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: collapse.collapse coordinate: [grid_latitude, grid_longitude] diff --git a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_all.yaml b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_all.yaml index 54afe80c6..03c32b219 100644 --- a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_all.yaml +++ b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_all.yaml @@ -10,6 +10,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_hour_of_day.yaml b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_hour_of_day.yaml index 7e06a90b3..d1c7be7d4 100644 --- a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_hour_of_day.yaml +++ b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_hour_of_day.yaml @@ -10,6 +10,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -23,8 +29,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_lead_time.yaml b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_lead_time.yaml index d81b30fc6..03cd68f74 100644 --- a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_lead_time.yaml +++ b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_lead_time.yaml @@ -10,6 +10,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -23,8 +29,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_validity_time.yaml b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_validity_time.yaml index 9aed477d9..a67c72851 100644 --- a/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_validity_time.yaml +++ b/src/CSET/recipes/generic_surface_domain_mean_time_series_case_aggregation_validity_time.yaml @@ -10,6 +10,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -23,8 +29,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_histogram_series.yaml b/src/CSET/recipes/generic_surface_histogram_series.yaml index 0ded153ca..eb13f2c4f 100644 --- a/src/CSET/recipes/generic_surface_histogram_series.yaml +++ b/src/CSET/recipes/generic_surface_histogram_series.yaml @@ -16,6 +16,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -29,8 +35,6 @@ steps: operator: constraints.generate_level_constraint coordinate: pressure levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: write.write_cube_to_nc overwrite: True diff --git a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_all.yaml b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_all.yaml index 801a8e620..8e753cee3 100644 --- a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_all.yaml +++ b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_all.yaml @@ -16,6 +16,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -29,8 +35,6 @@ steps: operator: constraints.generate_level_constraint coordinate: pressure levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_hour_of_day.yaml b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_hour_of_day.yaml index 1685dd932..d5d1240e9 100644 --- a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_hour_of_day.yaml +++ b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_hour_of_day.yaml @@ -16,6 +16,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -29,8 +35,6 @@ steps: operator: constraints.generate_level_constraint coordinate: pressure levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_lead_time.yaml b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_lead_time.yaml index 7bb0e2d4d..e0b97c3bc 100644 --- a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_lead_time.yaml +++ b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_lead_time.yaml @@ -16,6 +16,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -29,8 +35,6 @@ steps: operator: constraints.generate_level_constraint coordinate: pressure levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_validity_time.yaml b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_validity_time.yaml index bcfed7647..d0ca2e75d 100644 --- a/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_validity_time.yaml +++ b/src/CSET/recipes/generic_surface_histogram_series_case_aggregation_validity_time.yaml @@ -16,6 +16,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints variable_constraint: @@ -29,8 +35,6 @@ steps: operator: constraints.generate_level_constraint coordinate: pressure levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_single_point_time_series.yaml b/src/CSET/recipes/generic_surface_single_point_time_series.yaml index 722fe7e69..7dd97fce9 100644 --- a/src/CSET/recipes/generic_surface_single_point_time_series.yaml +++ b/src/CSET/recipes/generic_surface_single_point_time_series.yaml @@ -6,6 +6,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/generic_surface_spatial_plot_sequence.yaml b/src/CSET/recipes/generic_surface_spatial_plot_sequence.yaml index 48c1d7b3e..eeca75f6e 100644 --- a/src/CSET/recipes/generic_surface_spatial_plot_sequence.yaml +++ b/src/CSET/recipes/generic_surface_spatial_plot_sequence.yaml @@ -5,7 +5,12 @@ description: Extracts and plots the $METHOD of $VARNAME from all times in $MODEL steps: - operator: read.read_cube file_paths: $INPUT_PATHS - + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -19,8 +24,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: collapse.collapse coordinate: [time] diff --git a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_all.yaml b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_all.yaml index 03cc3ba88..5c08c6afa 100644 --- a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_all.yaml +++ b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_all.yaml @@ -7,6 +7,12 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -20,8 +26,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml index 61b25676e..76f17b627 100644 --- a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml +++ b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_hour_of_day.yaml @@ -7,6 +7,12 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -20,8 +26,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml index e9af8cd79..6fe8c3436 100644 --- a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml +++ b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_lead_time.yaml @@ -7,6 +7,12 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -20,8 +26,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml index b2738a7a5..fb8ca6c05 100644 --- a/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml +++ b/src/CSET/recipes/generic_surface_spatial_plot_sequence_case_aggregation_mean_validity_time.yaml @@ -7,6 +7,12 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -20,8 +26,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/generic_surface_spatial_plot_sequence_regrid.yaml b/src/CSET/recipes/generic_surface_spatial_plot_sequence_regrid.yaml index a68880131..11a89cc05 100644 --- a/src/CSET/recipes/generic_surface_spatial_plot_sequence_regrid.yaml +++ b/src/CSET/recipes/generic_surface_spatial_plot_sequence_regrid.yaml @@ -5,6 +5,10 @@ description: Extracts and plots the surface $VARNAME for all times in $MODEL_NAM steps: - operator: read.read_cube file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/level_spatial_difference.yaml b/src/CSET/recipes/level_spatial_difference.yaml index 5d50945c8..7e1f219f1 100644 --- a/src/CSET/recipes/level_spatial_difference.yaml +++ b/src/CSET/recipes/level_spatial_difference.yaml @@ -6,6 +6,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -19,8 +25,6 @@ steps: operator: constraints.generate_level_constraint coordinate: $LEVELTYPE levels: $LEVEL - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: collapse.collapse coordinate: [time] diff --git a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_all.yaml b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_all.yaml index cbc9b92ea..1d7c96174 100644 --- a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_all.yaml +++ b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_all.yaml @@ -7,6 +7,10 @@ description: | steps: - operator: read.read_cubes file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_hour_of_day.yaml b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_hour_of_day.yaml index 2b71fe22e..8e593f4f6 100644 --- a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_hour_of_day.yaml +++ b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_hour_of_day.yaml @@ -8,6 +8,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_lead_time.yaml b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_lead_time.yaml index 2bacc8ce2..fedcf5a11 100644 --- a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_lead_time.yaml +++ b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_lead_time.yaml @@ -8,6 +8,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_validity_time.yaml b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_validity_time.yaml index e4d8c6e98..020366122 100644 --- a/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_validity_time.yaml +++ b/src/CSET/recipes/level_spatial_difference_case_aggregation_mean_validity_time.yaml @@ -8,6 +8,10 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: diff --git a/src/CSET/recipes/surface_spatial_difference.yaml b/src/CSET/recipes/surface_spatial_difference.yaml index bfae548e9..faece6312 100644 --- a/src/CSET/recipes/surface_spatial_difference.yaml +++ b/src/CSET/recipes/surface_spatial_difference.yaml @@ -6,6 +6,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -19,8 +25,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: misc.difference diff --git a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_all.yaml b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_all.yaml index 57d7f892f..54838c8ad 100644 --- a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_all.yaml +++ b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_all.yaml @@ -8,6 +8,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -21,8 +27,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_hour_of_day.yaml b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_hour_of_day.yaml index 45153ddef..aa0c69236 100644 --- a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_hour_of_day.yaml +++ b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_hour_of_day.yaml @@ -8,6 +8,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -21,8 +27,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_lead_time.yaml b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_lead_time.yaml index f4a732a61..1e0dfcf6e 100644 --- a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_lead_time.yaml +++ b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_lead_time.yaml @@ -8,6 +8,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -21,8 +27,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_validity_time.yaml b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_validity_time.yaml index beea9e6e4..318551473 100644 --- a/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_validity_time.yaml +++ b/src/CSET/recipes/surface_spatial_difference_case_aggregation_mean_validity_time.yaml @@ -8,6 +8,12 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: [$BASE_MODEL, $OTHER_MODEL] + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints varname_constraint: @@ -21,8 +27,6 @@ steps: operator: constraints.generate_level_constraint coordinate: "pressure" levels: [] - subarea_type: $SUBAREA_TYPE - subarea_extent: $SUBAREA_EXTENT - operator: aggregate.ensure_aggregatable_across_cases diff --git a/src/CSET/recipes/transect.yaml b/src/CSET/recipes/transect.yaml index 0da565aa2..a0c072736 100644 --- a/src/CSET/recipes/transect.yaml +++ b/src/CSET/recipes/transect.yaml @@ -10,6 +10,10 @@ description: | steps: - operator: read.read_cube file_paths: $INPUT_PATHS + constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME + - operator: filters.filter_cubes constraint: operator: constraints.combine_constraints cell_method_constraint: From a6bb7dd36e75afbf0d3ad9b304a76e65cdef5873 Mon Sep 17 00:00:00 2001 From: SGallagherMet Date: Wed, 24 Sep 2025 17:06:55 +0100 Subject: [PATCH 3/4] Revert "Use custom cylc triggers to skip 'baking' when 'parbake' didn't find any recipes to configure." as this should be part of a separate branch/PR. This reverts commit 80d9cc310b2f8009059f80222dd5cc19d3443a90. --- .../app/parbake_recipes/bin/parbake.py | 26 ++------------ src/CSET/cset_workflow/flow.cylc | 15 ++------ tests/workflow_utils/test_parbake_recipes.py | 34 ------------------- 3 files changed, 4 insertions(+), 71 deletions(-) diff --git a/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py b/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py index 3044cafae..565901a27 100755 --- a/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py +++ b/src/CSET/cset_workflow/app/parbake_recipes/bin/parbake.py @@ -17,16 +17,13 @@ import json import os -import subprocess from base64 import b64decode from pathlib import Path from CSET.recipes import load_recipes -def parbake_all( - variables: dict, rose_datac: Path, share_dir: Path, aggregation: bool -) -> int: +def parbake_all(variables: dict, rose_datac: Path, share_dir: Path, aggregation: bool): """Generate and parbake recipes from configuration.""" # Gather all recipes into a big list. recipes = list(load_recipes(variables)) @@ -34,12 +31,9 @@ def parbake_all( if not recipes: raise ValueError("At least one recipe should be enabled.") # Parbake all recipes remaining after filtering aggregation recipes. - recipe_count = 0 for recipe in filter(lambda r: r.aggregation == aggregation, recipes): print(f"Parbaking {recipe}", flush=True) recipe.parbake(rose_datac, share_dir) - recipe_count += 1 - return recipe_count def main(): @@ -50,23 +44,7 @@ def main(): share_dir = Path(os.environ["CYLC_WORKFLOW_SHARE_DIR"]) aggregation = bool(os.getenv("DO_CASE_AGGREGATION")) # Parbake recipes for cycle. - recipe_count = parbake_all(variables, rose_datac, share_dir, aggregation) - - # If running under cylc, notify cylc of task completion. - cylc_workflow_id = os.environ.get("CYLC_WORKFLOW_ID", None) - cylc_task_job = os.environ.get("CYLC_TASK_JOB", None) - if cylc_workflow_id and cylc_task_job: - message_command = [ - "cylc", - "message", - "--", - cylc_workflow_id, - cylc_task_job, - ] - if recipe_count > 0: - subprocess.run(message_command + ["start baking"]) - else: - subprocess.run(message_command + ["skip baking"]) + parbake_all(variables, rose_datac, share_dir, aggregation) if __name__ == "__main__": # pragma: no cover diff --git a/src/CSET/cset_workflow/flow.cylc b/src/CSET/cset_workflow/flow.cylc index 5d6dbc8f3..ecfd22104 100644 --- a/src/CSET/cset_workflow/flow.cylc +++ b/src/CSET/cset_workflow/flow.cylc @@ -46,18 +46,14 @@ final cycle point = {{CSET_TRIAL_END_DATE}} # Analysis from each forecast. {{CSET_TRIAL_CYCLE_PERIOD}} = """ setup_complete[^] => FETCH_DATA:succeed-all => fetch_complete - fetch_complete & parbake_recipes:start_baking? => bake_recipes? - - parbake_recipes:skip_baking? | bake_recipes? => cycle_complete + fetch_complete & parbake_recipes => bake_recipes => cycle_complete """ {% endif %} # Only runs on the final cycle. R1/$ = """ # Run aggregation recipes. - fetch_complete & parbake_aggregation_recipes:start_baking? => bake_aggregation_recipes? - - parbake_aggregation_recipes:skip_baking? | bake_aggregation_recipes? => cycle_complete + fetch_complete & parbake_aggregation_recipes => bake_aggregation_recipes => cycle_complete # Finalise website and cleanup. cycle_complete => finish_website => send_email cycle_complete => housekeeping @@ -101,11 +97,6 @@ final cycle point = {{CSET_TRIAL_END_DATE}} [[[environment]]] ANALYSIS_LENGTH = {{ANALYSIS_LENGTH}} - [[PARBAKE]] - [[[outputs]]] - start_baking='start baking' - skip_baking='skip baking' - [[METPLUS]] [[[environment]]] {% if METPLUS_GRID_STAT|default(False) %} @@ -162,7 +153,6 @@ final cycle point = {{CSET_TRIAL_END_DATE}} [[parbake_recipes]] # Parbake all the recipes for this cycle. - inherit=PARBAKE script = rose task-run -v --app-key=parbake_recipes execution time limit = PT5M [[[directives]]] @@ -173,7 +163,6 @@ final cycle point = {{CSET_TRIAL_END_DATE}} [[parbake_aggregation_recipes]] # Parbake all the aggregation recipes. - inherit=PARBAKE script = rose task-run -v --app-key=parbake_recipes execution time limit = PT5M [[[directives]]] diff --git a/tests/workflow_utils/test_parbake_recipes.py b/tests/workflow_utils/test_parbake_recipes.py index 367b4441a..894f0655f 100644 --- a/tests/workflow_utils/test_parbake_recipes.py +++ b/tests/workflow_utils/test_parbake_recipes.py @@ -14,7 +14,6 @@ """Tests for parbake_recipe workflow utility.""" -import subprocess from pathlib import Path import pytest @@ -26,28 +25,14 @@ def test_main(monkeypatch): """Check parbake.main() invokes parbake_all correctly.""" function_ran = False - recipes_parbaked = 0 - cylc_message_ran = False - cylc_message = "" def mock_parbake_all(variables, rose_datac, share_dir, aggregation): nonlocal function_ran - nonlocal recipes_parbaked function_ran = True assert variables == {"variable": "value"} assert rose_datac == Path("/share/cycle/20000101T0000Z") assert share_dir == Path("/share") assert isinstance(aggregation, bool) - return recipes_parbaked - - def mock_run(cmd, **kwargs): - nonlocal cylc_message - nonlocal cylc_message_ran - cylc_message_ran = True - assert cmd[0:3] == ["cylc", "message", "--"] - assert cmd[3] == "test-workflow" - assert cmd[4] == "test-job" - assert cmd[5] == cylc_message monkeypatch.setattr(parbake, "parbake_all", mock_parbake_all) @@ -66,25 +51,6 @@ def mock_run(cmd, **kwargs): parbake.main() assert function_ran, "Function did not run!" - # Retry with cylc environment variables set. - monkeypatch.setattr(subprocess, "run", mock_run) - monkeypatch.setenv("CYLC_WORKFLOW_ID", "test-workflow") - monkeypatch.setenv("CYLC_TASK_JOB", "test-job") - - # No recipes parbaked. - function_ran = False - recipes_parbaked = 0 - cylc_message = "skip baking" - parbake.main() - assert cylc_message_ran, "Cylc message function did not run!" - - # Some recipes parbaked. - function_ran = False - recipes_parbaked = 3 - cylc_message = "start baking" - parbake.main() - assert cylc_message_ran, "Cylc message function did not run!" - def test_parbake_all_none_enabled(tmp_working_dir, monkeypatch): """Error when no recipes are enabled.""" From 767062d027a7aca54bd3cc334fcc241b3095593c Mon Sep 17 00:00:00 2001 From: SGallagherMet Date: Fri, 26 Sep 2025 14:11:56 +0100 Subject: [PATCH 4/4] Add a per_model option to the filter_cubes operator so that it can be used on multiple models at once. --- src/CSET/loaders/spatial_difference_field.py | 2 +- src/CSET/operators/filters.py | 48 ++++++++++++++++---- tests/operators/test_filters.py | 46 +++++++++++++++++++ 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/CSET/loaders/spatial_difference_field.py b/src/CSET/loaders/spatial_difference_field.py index d90fa1702..525c59ea6 100644 --- a/src/CSET/loaders/spatial_difference_field.py +++ b/src/CSET/loaders/spatial_difference_field.py @@ -217,7 +217,7 @@ def load(conf: Config): ]: base_model = models[0] yield RawRecipe( - recipe=f"mlevel_spatial_difference_case_aggregation_mean_{atype}.yaml", + recipe=f"level_spatial_difference_case_aggregation_mean_{atype}.yaml", variables={ "VARNAME": field, "LEVELTYPE": "model_level_number", diff --git a/src/CSET/operators/filters.py b/src/CSET/operators/filters.py index 465292366..4485a95ac 100644 --- a/src/CSET/operators/filters.py +++ b/src/CSET/operators/filters.py @@ -15,6 +15,7 @@ """Operators to perform various kind of filtering.""" import logging +from typing import Literal import iris import iris.cube @@ -70,8 +71,9 @@ def apply_mask( def filter_cubes( cube: iris.cube.Cube | iris.cube.CubeList, constraint: iris.Constraint, + per_model: Literal[True] | Literal[False] = False, **kwargs, -) -> iris.cube.Cube: +) -> iris.cube.Cube | iris.cube.CubeList: """Filter a CubeList down to a single Cube based on a constraint. Arguments @@ -80,28 +82,54 @@ def filter_cubes( Cube(s) to filter constraint: iris.Constraint Constraint to extract + per_model: bool + If True, the filter is expected to return a CubeList with + one cube per model, otherwise a single cube is expected. Returns ------- - iris.cube.Cube + iris.cube.Cube | iris.cube.CubeList Raises ------ ValueError If the constraint doesn't produce a single cube. """ + model_names = set() + + if per_model: + for c in iter_maybe(cube): + model_names.add(c.attributes.get("model_name", None)) + if None in model_names: + raise ValueError( + "per_model mode can only be used if all cubes have a model_name attribute." + ) + filtered_cubes = cube.extract(constraint) - # Return directly if already a cube. - if isinstance(filtered_cubes, iris.cube.Cube): - return filtered_cubes - # Check filtered cubes is a CubeList containing one cube. - if isinstance(filtered_cubes, iris.cube.CubeList) and len(filtered_cubes) == 1: - return filtered_cubes[0] - else: + + if per_model: + if isinstance(filtered_cubes, iris.cube.Cube) and len(model_names) == 1: + return iris.cube.CubeList((filtered_cubes,)) + if isinstance(filtered_cubes, iris.cube.CubeList) and len( + filtered_cubes + ) == len(model_names): + return filtered_cubes raise ValueError( - f"Constraint doesn't produce single cube. Constraint: {constraint}" + f"Constraint doesn't produce one cube per model. Constraint: {constraint}" f"\nSource: {cube}\nResult: {filtered_cubes}" ) + else: + # Return directly if already a cube. + if isinstance(filtered_cubes, iris.cube.Cube): + return filtered_cubes + # Check filtered cubes is a CubeList containing one cube. + if isinstance(filtered_cubes, iris.cube.CubeList) and len(filtered_cubes) == 1: + return filtered_cubes[0] + else: + raise ValueError( + f"Constraint doesn't produce single cube. Constraint: {constraint}" + f"\nSource: {cube}\nResult: {filtered_cubes}" + ) def filter_multiple_cubes( diff --git a/tests/operators/test_filters.py b/tests/operators/test_filters.py index 8c2e8b86d..6fc0359b0 100644 --- a/tests/operators/test_filters.py +++ b/tests/operators/test_filters.py @@ -36,6 +36,28 @@ def test_filter_cubes_cubelist(cubes): assert repr(cube) == expected_cube +def test_filter_cubes_cubelist_per_model(cube): + """Test filtering a CubeList with multiple models.""" + constraint = constraints.generate_cell_methods_constraint([]) + model1 = cube.copy() + model1.attributes["model_name"] = "model_1" + model2 = cube.copy() + model2.attributes["model_name"] = "model_2" + + cube = filters.filter_cubes( + iris.cube.CubeList([model1, model2]), constraint, per_model=True + ) + + expected_cube = "" + assert [repr(c) for c in cube] == [expected_cube, expected_cube] + + model1.attributes.pop("model_name") + with pytest.raises(ValueError): + filters.filter_cubes( + iris.cube.CubeList([model1, model2]), constraint, per_model=True + ) + + def test_filter_cubes_cube(cube): """Test filtering a Cube.""" constraint = constraints.generate_cell_methods_constraint([]) @@ -43,6 +65,30 @@ def test_filter_cubes_cube(cube): assert repr(cube) == repr(single_cube) +def test_filter_cubes_single_cube_per_model(cube): + """Test filtering a single Cube in per_model mode.""" + constraint = constraints.generate_cell_methods_constraint([]) + cube = cube.copy() + cube.attributes["model_name"] = "model_1" + returned_cubes = filters.filter_cubes(cube, constraint, per_model=True) + assert isinstance(returned_cubes, iris.cube.CubeList) + assert len(returned_cubes) == 1 + assert repr(cube) == repr(returned_cubes[0]) + + +def test_filter_cubes_single_entry_cubelist_per_model(cube): + """Test filtering a CubeList with one entry in per_model mode.""" + constraint = constraints.generate_cell_methods_constraint([]) + cube = cube.copy() + cube.attributes["model_name"] = "model_1" + returned_cubes = filters.filter_cubes( + iris.cube.CubeList((cube,)), constraint, per_model=True + ) + assert isinstance(returned_cubes, iris.cube.CubeList) + assert len(returned_cubes) == 1 + assert repr(cube) == repr(returned_cubes[0]) + + def test_filter_cubes_multiple_returned_exception(cubes): """Test for exception when multiple cubes returned.""" constraint = constraints.generate_stash_constraint("m01s03i236")