diff --git a/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf b/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf index 77483988b..9785cf85a 100644 --- a/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf +++ b/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf @@ -1978,76 +1978,86 @@ title=Multi-variable plots ns=Diagnostics/Multi-variable title=SPATIAL_MULTI_VARIABLE description=Create spatially mapped plots for the specified surface fields. -help=This generates spatial maps of 3 variables over-plotted. - A colour-filled map of VARNAME_BASE cube is plotted for the chosen domain. - A masked colour-filled layer of VARNAME_OVERLAY cube is overlaid on top. - A contour of VARNAME_CONTOUR is then overlaid on top of both. + A list of variables can be provided to generate multiple different combinations of multi-variable outputs. +help=This generates spatial maps with masked color-filled overlay and/or contours over-plotted. + A colour-filled map of MULTI_BASE_FIELDS is plotted for the chosen domain. + A masked colour-filled layer of MULTI_OVERLAY_FIELDS is overlaid on top. + A contour of MULTI_CONTOUR_FIELDS is then overlaid on top of both. + If either MULTI_OVERLAY_FIELDS or MULTI_CONTOUR_FIELDS variable is not set, 2-layer outputs + are generated. type=python_boolean compulsory=true sort-key=multi1 -trigger=template variables=MULTI_BASE_FIELD: True; - template variables=MULTI_OVERLAY_FIELD: True; - template variables=MULTI_OVERLAY_MASK_CONDITION: True; - template variables=MULTI_OVERLAY_MASK_VALUE: True; - template variables=MULTI_CONTOUR_FIELD: True; +trigger=template variables=MULTI_BASE_FIELDS: True; + template variables=MULTI_OVERLAY_FIELDS: True; + template variables=MULTI_OVERLAY_MASK_CONDITIONS: True; + template variables=MULTI_OVERLAY_MASK_VALUES: True; + template variables=MULTI_CONTOUR_FIELDS: True; template variables=SPATIAL_MULTI_FIELD_METHOD: True; -[template variables=MULTI_BASE_FIELD] +[template variables=MULTI_BASE_FIELDS] ns=Diagnostics/Multi-variable -title=Base variable -description=Surface (2D) diagnostic variable to form base layer in output. +title=Base variables +description=Surface (2D) diagnostic variable(s) to form base layer in output. + A different multi-variable plot is generated for each entry. help=Quoted variable names for base surface (2D) variable. The names should follow CF naming conventions, and will be translated appropriately. Where applicable, a STASH code in the format "m??s??i???" may be used instead. compulsory=true -type=quoted +type=python_list sort-key=multi2 -[template variables=MULTI_OVERLAY_FIELD] +[template variables=MULTI_OVERLAY_FIELDS] ns=Diagnostics/Multi-variable -title=Overlay variable (masked) -description=Masked diagnostic variable to form overlay layer in output. +title=Overlay variables (masked) +description=Masked diagnostic variable name(s) to form overlay layer in output. + Provide an overlay variable entry for each base variable requested. + If blank or None set, no overlay variables are plotted. help=Quoted variable names for masked variable. The names should follow CF naming conventions, and will be translated appropriately. Where applicable, a STASH code in the format "m??s??i???" may be be used instead. compulsory=true -type=quoted +type=python_list sort-key=multi3 -[template variables=MULTI_OVERLAY_MASK_CONDITION] +[template variables=MULTI_OVERLAY_MASK_CONDITIONS] ns=Diagnostics/Multi-variable -title=Overlay variable mask condition +title=Overlay variable mask conditions description=Set logical condition to control overlay masking. See help for details. + Potential values: "ge", "gt", "eq", "lt", "le" + Provide an overlay condition entry for each overlay variable requested. help=For example, if set to "ge", only the overlay variable values greater than or equal to overlay variable mask value will be over-plotted. -values="ge", "gt", "eq", "lt", "le" compulsory=true +type=python_list sort-key=multi4 -[template variables=MULTI_OVERLAY_MASK_VALUE] +[template variables=MULTI_OVERLAY_MASK_VALUES] ns=Diagnostics/Multi-variable -title=Overlay variable mask value +title=Overlay variable mask values description=Set variable threshold value to control overlay masking. See help for details. -help=Overlay output controlled by mask condition and mask value. For example, to overplot all non-zero rainfall, set OVERLAY_FIELD="surface_microphysical_rainfall_rate"; OVERLAY_CONDITION="ge"; OVERLAY_MASK_VALUE="0.05" +help=Overlay output controlled by mask condition and mask value. For example, to overplot all non-zero rainfall, set OVERLAY_FIELDS=["surface_microphysical_rainfall_rate"]; OVERLAY_CONDITIONS=["ge"]; OVERLAY_MASK_VALUES=["0.05"] compulsory=true -type=real +type=python_list sort-key=multi5 -[template variables=MULTI_CONTOUR_FIELD] +[template variables=MULTI_CONTOUR_FIELDS] ns=Diagnostics/Multi-variable -title=Contour variable -description=Diagnostic variable to form contour layer in multi-variable output. +title=Contour variables +description=Diagnostic variable(s) to form contour layer in multi-variable output. + Provide a contour variable entry for each base variable requested. + If blank or None set, no overlay variables are plotted. help=Quoted variable names for contour variable. The names should follow CF naming conventions, and will be translated appropriately. Where applicable, a STASH code in the format "m??s??i???" may be be used instead. compulsory=true -type=quoted +type=python_list sort-key=multi6 [template variables=SPATIAL_MULTI_FIELD_METHOD] ns=Diagnostics/Multi-variable -description=Select analysis method(s) for output mapped plots. Add all options required. +description=Select analysis method(s) for all output mapped plots. Add all options required. Leave blank or set to "SEQ" (sequence) for plots each diagnostic output time. For time-collapsed outputs over each analysis period, set to "MEAN", "MAX", "MIN", "STD_DEV" etc. help=Quoted analysis methods. Settings should be based on available iris.analysis methods for collapsing cube dimensions. diff --git a/src/CSET/cset_workflow/rose-suite.conf.example b/src/CSET/cset_workflow/rose-suite.conf.example index 9e5ed42f1..e73c55a9a 100644 --- a/src/CSET/cset_workflow/rose-suite.conf.example +++ b/src/CSET/cset_workflow/rose-suite.conf.example @@ -90,11 +90,11 @@ MODERATE_RAIN_PRESENCE_SPATIAL_DIFFERENCE=False MODERATE_RAIN_PRESENCE_SPATIAL_PLOT=False !!MODULES_LIST= !!MODULES_PURGE=True -!!MULTI_BASE_FIELD="" -!!MULTI_CONTOUR_FIELD="" -!!MULTI_OVERLAY_FIELD="" -!!MULTI_OVERLAY_MASK_CONDITION="ge" -!!MULTI_OVERLAY_MASK_VALUE=0.0 +!!MULTI_BASE_FIELDS=[] +!!MULTI_CONTOUR_FIELDS=[] +!!MULTI_OVERLAY_FIELDS=[] +!!MULTI_OVERLAY_MASK_CONDITIONS=[] +!!MULTI_OVERLAY_MASK_VALUES=[] !!PLEVEL_TRANSECT_AGGREGATION=False,False,False,False !!PLEVEL_TRANSECT_AGGREGATION_DIFFERENCE=False,False,False,False !!PLEVEL_TRANSECT_DIFFERENCE=False diff --git a/src/CSET/loaders/spatial_field.py b/src/CSET/loaders/spatial_field.py index 1517c6816..d4bc28aaa 100644 --- a/src/CSET/loaders/spatial_field.py +++ b/src/CSET/loaders/spatial_field.py @@ -564,28 +564,49 @@ def load(conf: Config): aggregation=False, ) + # Multi-variable spatial plotting if conf.SPATIAL_MULTI_VARIABLE: for model, method in itertools.product(models, conf.SPATIAL_MULTI_FIELD_METHOD): - # Multi-variable spatial plotting. - yield RawRecipe( - recipe="multi_surface_spatial_plot_sequence.yaml", - variables={ - "VARNAME_BASE": conf.MULTI_BASE_FIELD, - "VARNAME_OVER": conf.MULTI_OVERLAY_FIELD, - "OVERLAY_MASK_CONDITION": conf.MULTI_OVERLAY_MASK_CONDITION, - "OVERLAY_MASK_VALUE": conf.MULTI_OVERLAY_MASK_VALUE, - "VARNAME_CONTOUR": conf.MULTI_CONTOUR_FIELD, - "MODEL_NAME": model["name"], - "METHOD": method, - "SUBAREA_TYPE": conf.SUBAREA_TYPE if conf.SELECT_SUBAREA else None, - "SUBAREA_EXTENT": conf.SUBAREA_EXTENT - if conf.SELECT_SUBAREA - else None, - "SUBAREA_NAME": conf.SUBAREA_NAME if conf.SELECT_SUBAREA else "", - }, - model_ids=model["id"], - aggregation=False, - ) + # Loop over all potential variable combinations, using zip to ensure inputs for all plots requested. + for base, overlay, mask_condition, mask_value, contour in zip( + conf.MULTI_BASE_FIELDS, + conf.MULTI_OVERLAY_FIELDS, + conf.MULTI_OVERLAY_MASK_CONDITIONS, + conf.MULTI_OVERLAY_MASK_VALUES, + conf.MULTI_CONTOUR_FIELDS, + strict=True, + ): + # Set recipe by selected input variable combinations + multi_recipe = "multi_surface_spatial_plot_sequence.yaml" + if not contour or contour.lower() == "none": + multi_recipe = "multi_overlay_spatial_plot_sequence.yaml" + if not overlay or overlay.lower() == "none": + multi_recipe = "multi_contour_spatial_plot_sequence.yaml" + + # Multi-variable spatial plotting - set same inputs for all recipes. + yield RawRecipe( + recipe=multi_recipe, + variables={ + "VARNAME_BASE": base, + "VARNAME_OVER": overlay, + "OVERLAY_MASK_CONDITION": mask_condition, + "OVERLAY_MASK_VALUE": mask_value, + "VARNAME_CONTOUR": contour, + "MODEL_NAME": model["name"], + "METHOD": method, + "SUBAREA_TYPE": conf.SUBAREA_TYPE + if conf.SELECT_SUBAREA + else None, + "SUBAREA_EXTENT": conf.SUBAREA_EXTENT + if conf.SELECT_SUBAREA + else None, + "SUBAREA_NAME": conf.SUBAREA_NAME + if conf.SELECT_SUBAREA + else "", + }, + model_ids=model["id"], + aggregation=False, + ) # Moist Absolutely Unstable Layer presence if conf.MAUL_PRESENCE: diff --git a/src/CSET/operators/plot.py b/src/CSET/operators/plot.py index 8bf1c54a4..48e0e25b5 100644 --- a/src/CSET/operators/plot.py +++ b/src/CSET/operators/plot.py @@ -1890,8 +1890,8 @@ def spatial_pcolormesh_plot( def spatial_multi_pcolormesh_plot( cube: iris.cube.Cube, - overlay_cube: iris.cube.Cube, - contour_cube: iris.cube.Cube, + overlay_cube: iris.cube.Cube | None = None, + contour_cube: iris.cube.Cube | None = None, filename: str = None, sequence_coordinate: str = "time", stamp_coordinate: str = "realization", @@ -1919,14 +1919,15 @@ def spatial_multi_pcolormesh_plot( Iris cube of the data to plot. It should have two spatial dimensions, such as lat and lon, and may also have a another two dimension to be plotted sequentially and/or as postage stamp plots. - overlay_cube: Cube + overlay_cube: Cube, optional Iris cube of the data to plot as an overlay on top of basis cube. It should have two spatial dimensions, such as lat and lon, and may also have a another two dimension to be plotted sequentially and/or as postage stamp plots. This is likely to be a masked cube in order not to hide the underlying basis cube. - contour_cube: Cube + If not provided, output plot generated without overlay cube. + contour_cube: Cube, optional Iris cube of the data to plot as a contour overlay on top of basis cube and overlay_cube. It should have two spatial dimensions, such as lat and lon, and may also have a another two dimension to be - plotted sequentially and/or as postage stamp plots. + plotted sequentially and/or as postage stamp plots. If not provided, output plot generated without contours. filename: str, optional Name of the plot to write, used as a prefix for plot sequences. Defaults to the recipe name. diff --git a/src/CSET/recipes/surface_fields/multi_contour_spatial_plot_sequence.yaml b/src/CSET/recipes/surface_fields/multi_contour_spatial_plot_sequence.yaml new file mode 100644 index 000000000..ffe2b74c0 --- /dev/null +++ b/src/CSET/recipes/surface_fields/multi_contour_spatial_plot_sequence.yaml @@ -0,0 +1,37 @@ +category: Multivar Spatial Plot +title: "$MODEL_NAME $VARNAME_BASE $METHOD $SUBAREA_NAME\n(contour: $VARNAME_CONTOUR)" +description: Extracts and overplots the $METHOD of $VARNAME_BASE and $VARNAME_CONTOUR from all times in $MODEL_NAME. + +steps: + + - operator: read.read_cubes + file_paths: $INPUT_PATHS + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + constraint: ['$VARNAME_BASE', '$VARNAME_CONTOUR'] + + - operator: filters.filter_multiple_cubes + constraint: + operator: constraints.generate_cell_methods_constraint + cell_methods: [] + varname: ['$VARNAME_BASE', '$VARNAME_CONTOUR'] + + - operator: collapse.collapse + coordinate: [time] + method: $METHOD + + - operator: plot.spatial_multi_pcolormesh_plot + cube: + operator: filters.filter_cubes + constraint: + operator: constraints.combine_constraints + variable_constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME_BASE + contour_cube: + operator: filters.filter_cubes + constraint: + operator: constraints.combine_constraints + variable_constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME_CONTOUR diff --git a/src/CSET/recipes/surface_fields/multi_overlay_spatial_plot_sequence.yaml b/src/CSET/recipes/surface_fields/multi_overlay_spatial_plot_sequence.yaml new file mode 100644 index 000000000..4aeac2e18 --- /dev/null +++ b/src/CSET/recipes/surface_fields/multi_overlay_spatial_plot_sequence.yaml @@ -0,0 +1,51 @@ +category: Multivar Spatial Plot +title: "$MODEL_NAME $VARNAME_BASE $METHOD $SUBAREA_NAME\n(overlay: $VARNAME_OVER $OVERLAY_MASK_CONDITION $OVERLAY_MASK_VALUE)" +description: Extracts and overplots the $METHOD of $VARNAME_BASE and $VARNAME_OVER from all times in $MODEL_NAME. + +steps: + + - operator: read.read_cubes + file_paths: $INPUT_PATHS + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + constraint: ['$VARNAME_BASE', '$VARNAME_OVER'] + + - operator: filters.filter_multiple_cubes + constraint: + operator: constraints.generate_cell_methods_constraint + cell_methods: [] + varname: ['$VARNAME_BASE', '$VARNAME_OVER'] + + - operator: collapse.collapse + coordinate: [time] + method: $METHOD + + - operator: plot.spatial_multi_pcolormesh_plot + cube: + operator: filters.filter_cubes + constraint: + operator: constraints.combine_constraints + variable_constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME_BASE + + overlay_cube: + operator: filters.apply_mask + original_field: + operator: filters.filter_cubes + constraint: + operator: constraints.combine_constraints + variable_constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME_OVER + mask: + operator: filters.generate_mask + mask_field: + operator: filters.filter_cubes + constraint: + operator: constraints.combine_constraints + variable_constraint: + operator: constraints.generate_var_constraint + varname: $VARNAME_OVER + condition: $OVERLAY_MASK_CONDITION + value: $OVERLAY_MASK_VALUE diff --git a/src/CSET/recipes/surface_fields/multi_surface_spatial_plot_sequence.yaml b/src/CSET/recipes/surface_fields/multi_surface_spatial_plot_sequence.yaml index ed5d709dd..289065929 100644 --- a/src/CSET/recipes/surface_fields/multi_surface_spatial_plot_sequence.yaml +++ b/src/CSET/recipes/surface_fields/multi_surface_spatial_plot_sequence.yaml @@ -1,5 +1,5 @@ category: Multivar Spatial Plot -title: "$MODEL_NAME multi-variable $VARNAME_BASE $METHOD $SUBAREA_NAME" +title: "$MODEL_NAME $VARNAME_BASE $METHOD $SUBAREA_NAME\n(overlay: $VARNAME_OVER $OVERLAY_MASK_CONDITION $OVERLAY_MASK_VALUE)\n(contour: $VARNAME_CONTOUR)" description: Extracts and overplots the $METHOD of $VARNAME_BASE, $VARNAME_OVER and $VARNAME_CONTOUR from all times in $MODEL_NAME. steps: diff --git a/tests/operators/test_plot.py b/tests/operators/test_plot.py index 2126119ab..5051369c5 100644 --- a/tests/operators/test_plot.py +++ b/tests/operators/test_plot.py @@ -206,6 +206,31 @@ def test_spatial_multi_variable_plot(cube, tmp_working_dir): assert Path("untitled_20220921050000.png").is_file() +def test_spatial_multi_variable_plot_nolayers(cube, tmp_working_dir): + """Plot spatial plot with single input cube only.""" + # Call spatial_multi_pcolormesh_plot with only cube as input. + plot.spatial_multi_pcolormesh_plot(cube[0], sequence_coordinate="time") + assert Path("untitled_20220921030000.png").is_file() + + +def test_spatial_multi_variable_plot_overlay_only(cube, tmp_working_dir): + """Plot spatial plot with based and overlay cube only.""" + # Call spatial_multi_pcolormesh_plot with only cube and overlay_cube. + plot.spatial_multi_pcolormesh_plot( + cube[0], overlay_cube=cube[0], sequence_coordinate="time" + ) + assert Path("untitled_20220921030000.png").is_file() + + +def test_spatial_multi_variable_plot_contour_only(cube, tmp_working_dir): + """Plot spatial plot with based and contour cube only.""" + # Call spatial_multi_pcolormesh_plot with only cube and contour_cube. + plot.spatial_multi_pcolormesh_plot( + cube[0], contour_cube=cube[0], sequence_coordinate="time" + ) + assert Path("untitled_20220921030000.png").is_file() + + @pytest.mark.slow def test_vector_plot_with_filename(vector_cubes, tmp_working_dir): """Plot a vector plot of u10 and v10 components."""