From 4f0888ea977d6770de4491ba79b6e74ee9c87120 Mon Sep 17 00:00:00 2001 From: Helge Heuer Date: Wed, 6 Sep 2023 13:13:14 +0200 Subject: [PATCH 01/12] Add hovmoeller z vs time plot function --- .zenodo.json | 9 + CITATION.cff | 9 + .../monitor/hovmoeller_z_vs_time_with_ref.png | Bin 0 -> 89153 bytes doc/sphinx/source/recipes/recipe_monitor.rst | 7 + esmvaltool/config-references.yml | 9 + .../diag_scripts/monitor/multi_datasets.py | 616 ++++++++++++++---- .../monitor/recipe_monitor_with_refs.yml | 35 +- 7 files changed, 574 insertions(+), 111 deletions(-) create mode 100755 doc/sphinx/source/recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png diff --git a/.zenodo.json b/.zenodo.json index 083fa51356..0b196e0046 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -176,6 +176,11 @@ "name": "Hassler, Birgit", "orcid": "0000-0003-2724-709X" }, + { + "affiliation": "DLR, Germany", + "name": "Heuer, Helge", + "orcid": "0000-0003-2411-7150" + }, { "affiliation": "BSC, Spain", "name": "Hunter, Alasdair", @@ -194,6 +199,10 @@ "affiliation": "MPI for Biogeochemistry, Germany", "name": "Koirala, Sujan" }, + { + "affiliation": "DLR, Germany", + "name": "Kuehbacher, Birgit" + }, { "affiliation": "BSC, Spain", "name": "Lledó, Llorenç" diff --git a/CITATION.cff b/CITATION.cff index 49226a7fda..5674868d86 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -181,6 +181,11 @@ authors: family-names: Hassler given-names: Birgit orcid: "https://orcid.org/0000-0003-2724-709X" + - + affiliation: "DLR, Germany" + family-names: Heuer + given-names: Helge + orcid: "https://orcid.org/0000-0003-2411-7150" - affiliation: "BSC, Spain" family-names: Hunter @@ -199,6 +204,10 @@ authors: affiliation: "MPI for Biogeochemistry, Germany" family-names: Koirala given-names: Sujan + - + affiliation: "DLR, Germany" + family-names: Kuehbacher + given-names: Birgit - affiliation: "BSC, Spain" family-names: Lledó diff --git a/doc/sphinx/source/recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png b/doc/sphinx/source/recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png new file mode 100755 index 0000000000000000000000000000000000000000..734913c60b9c28abbca5c43c154a57daa0e8b6eb GIT binary patch literal 89153 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

`K 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 aW&YZ-B96^ZHc@LSBGBxPk!{%oO3FVV1`_fB literal 0 HcmV?d00001 diff --git a/doc/sphinx/source/recipes/recipe_monitor.rst b/doc/sphinx/source/recipes/recipe_monitor.rst index 0358ec36a7..45043a3318 100644 --- a/doc/sphinx/source/recipes/recipe_monitor.rst +++ b/doc/sphinx/source/recipes/recipe_monitor.rst @@ -209,4 +209,11 @@ Zonal mean profile of ta including a reference dataset. :align: center :width: 14cm +Hovmoeller plot (pressure vs time) of ta including a reference dataset. + +.. _fig_hovmoeller_z_vs_time_with_ref: +.. figure:: /recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png + :align: center + :width: 14cm + 1D profile of ta including a reference dataset. diff --git a/esmvaltool/config-references.yml b/esmvaltool/config-references.yml index baaaa44c96..ea416116d6 100644 --- a/esmvaltool/config-references.yml +++ b/esmvaltool/config-references.yml @@ -252,6 +252,11 @@ authors: name: Hempelmann, Nils institute: IPSL, France orcid: + heuer_helge: + name: Heuer, Helge + institute: DLR, Germany + email: helge.heuer@dlr.de + orcid: https://orcid.org/0000-0003-2411-7150 hogan_emma: name: Hogan, Emma institute: MetOffice, UK @@ -292,6 +297,10 @@ authors: name: Krasting, John institute: NOAA, USA orcid: https://orcid.org/0000-0002-4650-9844 + kuehbacher_birgit: + name: Kuehbacher, Birgit + institute: DLR, Germany + email: birgit.kuehbacher@dlr.de lejeune_quentin: name: Lejeune, Quentin institute: Climate Analytics, Germany diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index 6ac399652b..dbdd4fa7a0 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -32,6 +32,14 @@ can use the preprocessors :func:`esmvalcore.preprocessor.regrid` and :func:`esmvalcore.preprocessor.extract_levels` for this). Input data needs to be 2D with dimensions `latitude`, `height`/`air_pressure`. + - Hovmoeller altitude vs time (plot type ``hovmoeller_z_vs_time``): + for each variable and dataset, an individual profile is plotted. If a + reference dataset is defined, also include this dataset and a bias plot + into the figure. Note that if a reference dataset is defined, all input + datasets need to be given on the same horizontal and vertical grid (you + can use the preprocessors :func:`esmvalcore.preprocessor.regrid` and + :func:`esmvalcore.preprocessor.extract_levels` for this). Input data + needs to be 2D with dimensions `time`, `height`/`air_pressure`. .. warning:: @@ -119,6 +127,9 @@ ``{project}`` that vary between the different datasets will be transformed to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. +time_format: str, optional (default: None) + Matplotlib strftime format string for the time axis, if None defaults to + time units of iris cube Configuration options for plot type ``annual_cycle`` ---------------------------------------------------- @@ -312,6 +323,87 @@ coordinates. Can be adjusted to avoid overlap with the figure. Only relevant if ``show_stats: true``. +Configuration options for plot type ``hovmoeller_z_vs_time`` +---------------------------------------------------------- +cbar_label: str, optional (default: '{short_name} [{units}]') + Colorbar label. Can include facets in curly brackets which will be derived + from the corresponding dataset, e.g., ``{project}``, ``{short_name}``, + ``{exp}``. +cbar_label_bias: str, optional (default: 'Δ{short_name} [{units}]') + Colorbar label for plotting biases. Can include facets in curly brackets + which will be derived from the corresponding dataset, e.g., ``{project}``, + ``{short_name}``, ``{exp}``. This option has no effect if no reference + dataset is given. +cbar_kwargs: dict, optional + Optional keyword arguments for :func:`matplotlib.pyplot.colorbar`. By + default, uses ``orientation: vertical``. +cbar_kwargs_bias: dict, optional + Optional keyword arguments for :func:`matplotlib.pyplot.colorbar` for + plotting biases. These keyword arguments update (and potentially overwrite) + the ``cbar_kwargs`` for the bias plot. This option has no effect if no + reference dataset is given. +common_cbar: bool, optional (default: False) + Use a common colorbar for the top panels (i.e., plots of the dataset and + the corresponding reference dataset) when using a reference dataset. If + neither ``vmin`` and ``vmix`` nor ``levels`` is given in ``plot_kwargs``, + the colorbar bounds are inferred from the dataset in the top left panel, + which might lead to an inappropriate colorbar for the reference dataset + (top right panel). Thus, the use of the ``plot_kwargs`` ``vmin`` and + ``vmax`` or ``levels`` is highly recommend when using this ``common_cbar: + true``. This option has no effect if no reference dataset is given. +fontsize: int, optional (default: 10) + Fontsize used for ticks, labels and titles. For the latter, use the given + fontsize plus 2. Does not affect suptitles. +log_y: bool, optional (default: True) + Use logarithmic Y-axis. +plot_func: str, optional (default: 'contourf') + Plot function used to plot the profiles. Must be a function of + :mod:`iris.plot` that supports plotting of 2D cubes with coordinates + latitude and height/air_pressure. +plot_kwargs: dict, optional + Optional keyword arguments for the plot function defined by ``plot_func``. + Dictionary keys are elements identified by ``facet_used_for_labels`` or + ``default``, e.g., ``CMIP6`` if ``facet_used_for_labels: project`` or + ``historical`` if ``facet_used_for_labels: exp``. Dictionary values are + dictionaries used as keyword arguments for the plot function defined by + ``plot_func``. String arguments can include facets in curly brackets which + will be derived from the corresponding dataset, e.g., ``{project}``, + ``{short_name}``, ``{exp}``. Examples: ``default: {levels: 2}, CMIP6: + {vmin: 200, vmax: 250}``. +plot_kwargs_bias: dict, optional + Optional keyword arguments for the plot function defined by ``plot_func`` + for plotting biases. These keyword arguments update (and potentially + overwrite) the ``plot_kwargs`` for the bias plot. This option has no effect + if no reference dataset is given. See option ``plot_kwargs`` for more + details. By default, uses ``cmap: bwr``. +pyplot_kwargs: dict, optional + Optional calls to functions of :mod:`matplotlib.pyplot`. Dictionary keys + are functions of :mod:`matplotlib.pyplot`. Dictionary values are used as + single argument for these functions. String arguments can include facets in + curly brackets which will be derived from the corresponding dataset, e.g., + ``{project}``, ``{short_name}``, ``{exp}``. Examples: ``title: 'Awesome + Plot of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. +rasterize: bool, optional (default: True) + If ``True``, use `rasterization + `_ for + profile plots to produce smaller files. This is only relevant for vector + graphics (e.g., ``output_file_type=pdf,svg,ps``). +show_stats: bool, optional (default: True) + Show basic statistics on the plots. +show_y_minor_ticklabels: bool, optional (default: False) + Show tick labels for the minor ticks on the Y axis. +x_pos_stats_avg: float, optional (default: 0.01) + Text x-position of average (shown on the left) in Axes coordinates. Can be + adjusted to avoid overlap with the figure. Only relevant if ``show_stats: + true``. +x_pos_stats_bias: float, optional (default: 0.7) + Text x-position of bias statistics (shown on the right) in Axes + coordinates. Can be adjusted to avoid overlap with the figure. Only + relevant if ``show_stats: true``. +time_format: str, optional (default: %Y) + Matplotlib strftime format string for the time axis, if None defaults to + time units of iris cube + Configuration options for plot type ``1d_profile`` -------------------------------------------------- aspect_ratio: float, optional (default: 1.5) @@ -357,7 +449,6 @@ Extra arguments given to the recipe are ignored, so it is safe to use yaml anchors to share the configuration of common arguments with other monitor diagnostic script. - """ import logging from copy import deepcopy @@ -367,6 +458,7 @@ import cartopy.crs as ccrs import iris import matplotlib as mpl +import matplotlib.dates as mdates import matplotlib.pyplot as plt import numpy as np import seaborn as sns @@ -432,11 +524,8 @@ def __init__(self, config): # Check given plot types and set default settings for them self.supported_plot_types = [ - 'timeseries', - 'annual_cycle', - 'map', - 'zonal_mean_profile', - '1d_profile' + 'timeseries', 'annual_cycle', 'map', 'zonal_mean_profile', + '1d_profile', 'hovmoeller_z_vs_time' ] for (plot_type, plot_options) in self.plots.items(): if plot_type not in self.supported_plot_types: @@ -453,6 +542,7 @@ def __init__(self, config): self.plots[plot_type].setdefault('legend_kwargs', {}) self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('pyplot_kwargs', {}) + self.plots[plot_type].setdefault('time_format', None) if plot_type == 'annual_cycle': self.plots[plot_type].setdefault('gridline_kwargs', {}) @@ -461,13 +551,14 @@ def __init__(self, config): self.plots[plot_type].setdefault('pyplot_kwargs', {}) if plot_type == 'map': - self.plots[plot_type].setdefault( - 'cbar_label', '{short_name} [{units}]') - self.plots[plot_type].setdefault( - 'cbar_label_bias', 'Δ{short_name} [{units}]') - self.plots[plot_type].setdefault( - 'cbar_kwargs', {'orientation': 'horizontal', 'aspect': 30} - ) + self.plots[plot_type].setdefault('cbar_label', + '{short_name} [{units}]') + self.plots[plot_type].setdefault('cbar_label_bias', + 'Δ{short_name} [{units}]') + self.plots[plot_type].setdefault('cbar_kwargs', { + 'orientation': 'horizontal', + 'aspect': 30 + }) self.plots[plot_type].setdefault('cbar_kwargs_bias', {}) self.plots[plot_type].setdefault('common_cbar', False) self.plots[plot_type].setdefault('fontsize', 10) @@ -476,13 +567,11 @@ def __init__(self, config): self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('plot_kwargs_bias', {}) self.plots[plot_type]['plot_kwargs_bias'].setdefault( - 'cmap', 'bwr' - ) + 'cmap', 'bwr') if 'projection' not in self.plots[plot_type]: self.plots[plot_type].setdefault('projection', 'Robinson') - self.plots[plot_type].setdefault( - 'projection_kwargs', {'central_longitude': 10} - ) + self.plots[plot_type].setdefault('projection_kwargs', + {'central_longitude': 10}) else: self.plots[plot_type].setdefault('projection_kwargs', {}) self.plots[plot_type].setdefault('pyplot_kwargs', {}) @@ -492,13 +581,12 @@ def __init__(self, config): self.plots[plot_type].setdefault('x_pos_stats_bias', 0.92) if plot_type == 'zonal_mean_profile': - self.plots[plot_type].setdefault( - 'cbar_label', '{short_name} [{units}]') - self.plots[plot_type].setdefault( - 'cbar_label_bias', 'Δ{short_name} [{units}]') - self.plots[plot_type].setdefault( - 'cbar_kwargs', {'orientation': 'vertical'} - ) + self.plots[plot_type].setdefault('cbar_label', + '{short_name} [{units}]') + self.plots[plot_type].setdefault('cbar_label_bias', + 'Δ{short_name} [{units}]') + self.plots[plot_type].setdefault('cbar_kwargs', + {'orientation': 'vertical'}) self.plots[plot_type].setdefault('cbar_kwargs_bias', {}) self.plots[plot_type].setdefault('common_cbar', False) self.plots[plot_type].setdefault('fontsize', 10) @@ -507,14 +595,12 @@ def __init__(self, config): self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('plot_kwargs_bias', {}) self.plots[plot_type]['plot_kwargs_bias'].setdefault( - 'cmap', 'bwr' - ) + 'cmap', 'bwr') self.plots[plot_type].setdefault('pyplot_kwargs', {}) self.plots[plot_type].setdefault('rasterize', True) self.plots[plot_type].setdefault('show_stats', True) - self.plots[plot_type].setdefault( - 'show_y_minor_ticklabels', False - ) + self.plots[plot_type].setdefault('show_y_minor_ticklabels', + False) self.plots[plot_type].setdefault('x_pos_stats_avg', 0.01) self.plots[plot_type].setdefault('x_pos_stats_bias', 0.7) @@ -526,9 +612,33 @@ def __init__(self, config): self.plots[plot_type].setdefault('log_y', True) self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('pyplot_kwargs', {}) - self.plots[plot_type].setdefault( - 'show_y_minor_ticklabels', False - ) + self.plots[plot_type].setdefault('show_y_minor_ticklabels', + False) + + if plot_type == 'hovmoeller_z_vs_time': + self.plots[plot_type].setdefault('cbar_label', + '{short_name} [{units}]') + self.plots[plot_type].setdefault('cbar_label_bias', + 'Δ{short_name} [{units}]') + self.plots[plot_type].setdefault('cbar_kwargs', + {'orientation': 'vertical'}) + self.plots[plot_type].setdefault('cbar_kwargs_bias', {}) + self.plots[plot_type].setdefault('common_cbar', False) + self.plots[plot_type].setdefault('fontsize', 10) + self.plots[plot_type].setdefault('log_y', False) + self.plots[plot_type].setdefault('plot_func', 'contourf') + self.plots[plot_type].setdefault('plot_kwargs', {}) + self.plots[plot_type].setdefault('plot_kwargs_bias', {}) + self.plots[plot_type]['plot_kwargs_bias'].setdefault( + 'cmap', 'bwr') + self.plots[plot_type].setdefault('time_format', '%Y') + self.plots[plot_type].setdefault('pyplot_kwargs', {}) + self.plots[plot_type].setdefault('rasterize', True) + self.plots[plot_type].setdefault('show_stats', True) + self.plots[plot_type].setdefault('show_y_minor_ticklabels', + False) + self.plots[plot_type].setdefault('x_pos_stats_avg', 0.01) + self.plots[plot_type].setdefault('x_pos_stats_bias', 0.7) # Check that facet_used_for_labels is present for every dataset for dataset in self.input_data: @@ -554,7 +664,8 @@ def _add_colorbar(self, plot_type, plot_left, plot_right, axes_left, if self.plots[plot_type]['common_cbar']: if 'aspect' in cbar_kwargs: cbar_kwargs['aspect'] += 20.0 - cbar = plt.colorbar(plot_left, ax=[axes_left, axes_right], + cbar = plt.colorbar(plot_left, + ax=[axes_left, axes_right], **cbar_kwargs) cbar.set_label(cbar_label_left, fontsize=fontsize) cbar.ax.tick_params(labelsize=fontsize) @@ -568,7 +679,11 @@ def _add_colorbar(self, plot_type, plot_left, plot_right, axes_left, cbar_right.set_label(cbar_label_right, fontsize=fontsize) cbar_right.ax.tick_params(labelsize=fontsize) - def _add_stats(self, plot_type, axes, dim_coords, dataset, + def _add_stats(self, + plot_type, + axes, + dim_coords, + dataset, ref_dataset=None): """Add text to plot that describes basic statistics.""" if not self.plots[plot_type]['show_stats']: @@ -590,7 +705,7 @@ def _add_stats(self, plot_type, axes, dim_coords, dataset, if plot_type == 'map': x_pos_bias = self.plots[plot_type]['x_pos_stats_bias'] x_pos = self.plots[plot_type]['x_pos_stats_avg'] - elif plot_type in ['zonal_mean_profile']: + elif plot_type in ['zonal_mean_profile', 'hovmoeller_z_vs_time']: x_pos_bias = self.plots[plot_type]['x_pos_stats_bias'] x_pos = self.plots[plot_type]['x_pos_stats_avg'] else: @@ -614,7 +729,8 @@ def _add_stats(self, plot_type, axes, dim_coords, dataset, # Mean weights = area_weights(cube) if ref_cube is None: - mean = cube.collapsed(dim_coords, iris.analysis.MEAN, + mean = cube.collapsed(dim_coords, + iris.analysis.MEAN, weights=weights) logger.info( "Area-weighted mean of %s for %s = %f%s", @@ -624,7 +740,8 @@ def _add_stats(self, plot_type, axes, dim_coords, dataset, dataset['units'], ) else: - mean = (cube - ref_cube).collapsed(dim_coords, iris.analysis.MEAN, + mean = (cube - ref_cube).collapsed(dim_coords, + iris.analysis.MEAN, weights=weights) logger.info( "Area-weighted bias of %s for %s = %f%s", @@ -633,16 +750,23 @@ def _add_stats(self, plot_type, axes, dim_coords, dataset, mean.data, dataset['units'], ) - axes.text(x_pos, y_pos, f"{mean.data:.2f}{cube.units}", - fontsize=fontsize, transform=axes.transAxes) + axes.text(x_pos, + y_pos, + f"{mean.data:.2f}{cube.units}", + fontsize=fontsize, + transform=axes.transAxes) if ref_cube is None: return # Weighted RMSE - rmse = (cube - ref_cube).collapsed(dim_coords, iris.analysis.RMS, + rmse = (cube - ref_cube).collapsed(dim_coords, + iris.analysis.RMS, weights=weights) - axes.text(x_pos_bias, y_pos, f"RMSE={rmse.data:.2f}{cube.units}", - fontsize=fontsize, transform=axes.transAxes) + axes.text(x_pos_bias, + y_pos, + f"RMSE={rmse.data:.2f}{cube.units}", + fontsize=fontsize, + transform=axes.transAxes) logger.info( "Area-weighted RMSE of %s for %s = %f%s", dataset['short_name'], @@ -658,8 +782,11 @@ def _add_stats(self, plot_type, axes, dim_coords, dataset, ref_cube_data = ref_cube.data.ravel()[~mask] weights = weights.ravel()[~mask] r2_val = r2_score(cube_data, ref_cube_data, sample_weight=weights) - axes.text(x_pos_bias, y_pos - 0.1, rf"R$^2$={r2_val:.2f}", - fontsize=fontsize, transform=axes.transAxes) + axes.text(x_pos_bias, + y_pos - 0.1, + rf"R$^2$={r2_val:.2f}", + fontsize=fontsize, + transform=axes.transAxes) logger.info( "Area-weighted R2 of %s for %s = %f", dataset['short_name'], @@ -803,7 +930,9 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): # Create single figure with multiple axes with mpl.rc_context(self._get_custom_mpl_rc_params(plot_type)): fig = plt.figure(**self.cfg['figure_kwargs']) - gridspec = GridSpec(5, 4, figure=fig, + gridspec = GridSpec(5, + 4, + figure=fig, height_ratios=[1.0, 1.0, 0.4, 1.0, 1.0]) # Options used for all subplots @@ -847,7 +976,8 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): bias_cube = cube - ref_cube axes_bias = fig.add_subplot(gridspec[3:5, 1:3], projection=projection) - plot_kwargs_bias = self._get_plot_kwargs(plot_type, dataset, + plot_kwargs_bias = self._get_plot_kwargs(plot_type, + dataset, bias=True) plot_kwargs_bias['axes'] = axes_bias plot_bias = plot_func(bias_cube, **plot_kwargs_bias) @@ -859,7 +989,8 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): pad=3.0, ) cbar_kwargs_bias = self._get_cbar_kwargs(plot_type, bias=True) - cbar_bias = fig.colorbar(plot_bias, ax=axes_bias, + cbar_bias = fig.colorbar(plot_bias, + ax=axes_bias, **cbar_kwargs_bias) cbar_bias.set_label( self._get_cbar_label(plot_type, dataset, bias=True), @@ -880,9 +1011,8 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): # File paths plot_path = self.get_plot_path(plot_type, dataset) - netcdf_path = ( - get_diagnostic_filename(Path(plot_path).stem + "_{pos}", self.cfg) - ) + netcdf_path = (get_diagnostic_filename( + Path(plot_path).stem + "_{pos}", self.cfg)) netcdf_paths = { netcdf_path.format(pos='top_left'): cube, netcdf_path.format(pos='top_right'): ref_cube, @@ -918,7 +1048,8 @@ def _plot_map_without_ref(self, plot_func, dataset): # Setup colorbar fontsize = self.plots[plot_type]['fontsize'] - colorbar = fig.colorbar(plot_map, ax=axes, + colorbar = fig.colorbar(plot_map, + ax=axes, **self._get_cbar_kwargs(plot_type)) colorbar.set_label(self._get_cbar_label(plot_type, dataset), fontsize=fontsize) @@ -944,9 +1075,10 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, ref_dataset): """Plot zonal mean profile for single dataset with reference.""" plot_type = 'zonal_mean_profile' - logger.info("Plotting zonal mean profile with reference dataset" - " '%s' for '%s'", - self._get_label(ref_dataset), self._get_label(dataset)) + logger.info( + "Plotting zonal mean profile with reference dataset" + " '%s' for '%s'", self._get_label(ref_dataset), + self._get_label(dataset)) # Make sure that the data has the correct dimensions cube = dataset['cube'] @@ -957,7 +1089,9 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # Create single figure with multiple axes with mpl.rc_context(self._get_custom_mpl_rc_params(plot_type)): fig = plt.figure(**self.cfg['figure_kwargs']) - gridspec = GridSpec(5, 4, figure=fig, + gridspec = GridSpec(5, + 4, + figure=fig, height_ratios=[1.0, 1.0, 0.4, 1.0, 1.0]) # Options used for all subplots @@ -985,7 +1119,8 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # Plot reference dataset (top right) # Note: make sure to use the same vmin and vmax than the top left # plot if a common colorbar is desired - axes_ref = fig.add_subplot(gridspec[0:2, 2:4], sharex=axes_data, + axes_ref = fig.add_subplot(gridspec[0:2, 2:4], + sharex=axes_data, sharey=axes_data) plot_kwargs['axes'] = axes_ref if self.plots[plot_type]['common_cbar']: @@ -1002,9 +1137,11 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # Plot bias (bottom center) bias_cube = cube - ref_cube - axes_bias = fig.add_subplot(gridspec[3:5, 1:3], sharex=axes_data, + axes_bias = fig.add_subplot(gridspec[3:5, 1:3], + sharex=axes_data, sharey=axes_data) - plot_kwargs_bias = self._get_plot_kwargs(plot_type, dataset, + plot_kwargs_bias = self._get_plot_kwargs(plot_type, + dataset, bias=True) plot_kwargs_bias['axes'] = axes_bias plot_bias = plot_func(bias_cube, **plot_kwargs_bias) @@ -1015,7 +1152,8 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, axes_bias.set_xlabel('latitude [°N]') axes_bias.set_ylabel(f'{z_coord.long_name} [{z_coord.units}]') cbar_kwargs_bias = self._get_cbar_kwargs(plot_type, bias=True) - cbar_bias = fig.colorbar(plot_bias, ax=axes_bias, + cbar_bias = fig.colorbar(plot_bias, + ax=axes_bias, **cbar_kwargs_bias) cbar_bias.set_label( self._get_cbar_label(plot_type, dataset, bias=True), @@ -1036,9 +1174,8 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # File paths plot_path = self.get_plot_path(plot_type, dataset) - netcdf_path = ( - get_diagnostic_filename(Path(plot_path).stem + "_{pos}", self.cfg) - ) + netcdf_path = (get_diagnostic_filename( + Path(plot_path).stem + "_{pos}", self.cfg)) netcdf_paths = { netcdf_path.format(pos='top_left'): cube, netcdf_path.format(pos='top_right'): ref_cube, @@ -1050,9 +1187,9 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, def _plot_zonal_mean_profile_without_ref(self, plot_func, dataset): """Plot zonal mean profile for single dataset without reference.""" plot_type = 'zonal_mean_profile' - logger.info("Plotting zonal mean profile without reference dataset" - " for '%s'", - self._get_label(dataset)) + logger.info( + "Plotting zonal mean profile without reference dataset" + " for '%s'", self._get_label(dataset)) # Make sure that the data has the correct dimensions cube = dataset['cube'] @@ -1071,7 +1208,8 @@ def _plot_zonal_mean_profile_without_ref(self, plot_func, dataset): # Setup colorbar fontsize = self.plots[plot_type]['fontsize'] - colorbar = fig.colorbar(plot_zonal_mean_profile, ax=axes, + colorbar = fig.colorbar(plot_zonal_mean_profile, + ax=axes, **self._get_cbar_kwargs(plot_type)) colorbar.set_label(self._get_cbar_label(plot_type, dataset), fontsize=fontsize) @@ -1105,6 +1243,190 @@ def _plot_zonal_mean_profile_without_ref(self, plot_func, dataset): return (plot_path, {netcdf_path: cube}) + def _plot_hovmoeller_z_vs_time_without_ref(self, plot_func, dataset): + """Plot hovmoeller z vs time for single dataset without reference.""" + plot_type = 'hovmoeller_z_vs_time' + logger.info( + "Plotting homvoeller z vs time without reference dataset" + " for '%s'", self._get_label(dataset)) + + # Make sure that the data has the correct dimensions + cube = dataset['cube'] + dim_coords_dat = self._check_cube_dimensions(cube, plot_type) + + time_coord = cube.coord(axis='T') + + # Create plot with desired settings + with mpl.rc_context(self._get_custom_mpl_rc_params(plot_type)): + fig = plt.figure(**self.cfg['figure_kwargs']) + axes = fig.add_subplot() + plot_kwargs = self._get_plot_kwargs(plot_type, dataset) + plot_kwargs['axes'] = axes + plot_zonal_mean_profile = plot_func(cube, **plot_kwargs) + + # Print statistics if desired + self._add_stats(plot_type, axes, dim_coords_dat, dataset) + + # Setup colorbar + fontsize = self.plots[plot_type]['fontsize'] + colorbar = fig.colorbar(plot_zonal_mean_profile, + ax=axes, + **self._get_cbar_kwargs(plot_type)) + colorbar.set_label(self._get_cbar_label(plot_type, dataset), + fontsize=fontsize) + colorbar.ax.tick_params(labelsize=fontsize) + + # Customize plot + axes.set_title(self._get_label(dataset)) + fig.suptitle(f"{dataset['long_name']} ({dataset['start_year']}-" + f"{dataset['end_year']})") + z_coord = cube.coord(axis='Z') + axes.set_ylabel(f'{z_coord.long_name} [{z_coord.units}]') + if self.plots[plot_type]['log_y']: + axes.set_yscale('log') + axes.get_yaxis().set_major_formatter( + FormatStrFormatter('%.1f')) + if self.plots[plot_type]['show_y_minor_ticklabels']: + axes.get_yaxis().set_minor_formatter( + FormatStrFormatter('%.1f')) + else: + axes.get_yaxis().set_minor_formatter(NullFormatter()) + axes.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) + axes.set_xlabel(f'{time_coord.long_name}') + self._process_pyplot_kwargs(plot_type, dataset) + + # Rasterization + if self.plots[plot_type]['rasterize']: + self._set_rasterized([axes]) + + # File paths + plot_path = self.get_plot_path(plot_type, dataset) + netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) + + return (plot_path, {netcdf_path: cube}) + + def _plot_hovmoeller_z_vs_time_with_ref(self, plot_func, dataset, + ref_dataset): + """Plot hovmoeller z vs time for single dataset with reference.""" + plot_type = 'hovmoeller_z_vs_time' + logger.info( + "Plotting hovmoeller z vs time with reference dataset" + " '%s' for '%s'", self._get_label(ref_dataset), + self._get_label(dataset)) + + # Make sure that the data has the correct dimensions + cube = dataset['cube'] + ref_cube = ref_dataset['cube'] + dim_coords_dat = self._check_cube_dimensions(cube, plot_type) + dim_coords_ref = self._check_cube_dimensions(ref_cube, plot_type) + + time_coord = cube.coord(axis='T') + + # Create single figure with multiple axes + with mpl.rc_context(self._get_custom_mpl_rc_params(plot_type)): + fig = plt.figure(**self.cfg['figure_kwargs']) + gridspec = GridSpec(5, + 4, + figure=fig, + height_ratios=[1.0, 1.0, 0.4, 1.0, 1.0]) + + # Options used for all subplots + plot_kwargs = self._get_plot_kwargs(plot_type, dataset) + fontsize = self.plots[plot_type]['fontsize'] + + # Plot dataset (top left) + axes_data = fig.add_subplot(gridspec[0:2, 0:2]) + plot_kwargs['axes'] = axes_data + plot_data = plot_func(cube, **plot_kwargs) + axes_data.set_title(self._get_label(dataset), pad=3.0) + z_coord = cube.coord(axis='Z') + axes_data.set_ylabel(f'{z_coord.long_name} [{z_coord.units}]') + if self.plots[plot_type]['log_y']: + axes_data.set_yscale('log') + axes_data.get_yaxis().set_major_formatter( + FormatStrFormatter('%.1f')) + if self.plots[plot_type]['show_y_minor_ticklabels']: + axes_data.get_yaxis().set_minor_formatter( + FormatStrFormatter('%.1f')) + else: + axes_data.get_yaxis().set_minor_formatter(NullFormatter()) + axes_data.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) + self._add_stats(plot_type, axes_data, dim_coords_dat, dataset) + + # Plot reference dataset (top right) + # Note: make sure to use the same vmin and vmax than the top left + # plot if a common colorbar is desired + axes_ref = fig.add_subplot(gridspec[0:2, 2:4], + sharex=axes_data, + sharey=axes_data) + plot_kwargs['axes'] = axes_ref + if self.plots[plot_type]['common_cbar']: + plot_kwargs.setdefault('vmin', plot_data.get_clim()[0]) + plot_kwargs.setdefault('vmax', plot_data.get_clim()[1]) + plot_ref = plot_func(ref_cube, **plot_kwargs) + axes_ref.set_title(self._get_label(ref_dataset), pad=3.0) + plt.setp(axes_ref.get_yticklabels(), visible=False) + axes_ref.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) + self._add_stats(plot_type, axes_ref, dim_coords_ref, ref_dataset) + + # Add colorbar(s) + self._add_colorbar(plot_type, plot_data, plot_ref, axes_data, + axes_ref, dataset, ref_dataset) + + # Plot bias (bottom center) + bias_cube = cube - ref_cube + axes_bias = fig.add_subplot(gridspec[3:5, 1:3], + sharex=axes_data, + sharey=axes_data) + plot_kwargs_bias = self._get_plot_kwargs(plot_type, + dataset, + bias=True) + plot_kwargs_bias['axes'] = axes_bias + plot_bias = plot_func(bias_cube, **plot_kwargs_bias) + axes_bias.set_title( + f"{self._get_label(dataset)} - {self._get_label(ref_dataset)}", + pad=3.0, + ) + axes_bias.set_xlabel(time_coord.long_name) + axes_bias.set_ylabel(f'{z_coord.long_name} [{z_coord.units}]') + axes_bias.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) + cbar_kwargs_bias = self._get_cbar_kwargs(plot_type, bias=True) + cbar_bias = fig.colorbar(plot_bias, + ax=axes_bias, + **cbar_kwargs_bias) + cbar_bias.set_label( + self._get_cbar_label(plot_type, dataset, bias=True), + fontsize=fontsize, + ) + cbar_bias.ax.tick_params(labelsize=fontsize) + self._add_stats(plot_type, axes_bias, dim_coords_dat, dataset, + ref_dataset) + + # Customize plot + fig.suptitle(f"{dataset['long_name']} ({dataset['start_year']}-" + f"{dataset['end_year']})") + self._process_pyplot_kwargs(plot_type, dataset) + + # Rasterization + if self.plots[plot_type]['rasterize']: + self._set_rasterized([axes_data, axes_ref, axes_bias]) + + # File paths + plot_path = self.get_plot_path(plot_type, dataset) + netcdf_path = (get_diagnostic_filename( + Path(plot_path).stem + "_{pos}", self.cfg)) + netcdf_paths = { + netcdf_path.format(pos='top_left'): cube, + netcdf_path.format(pos='top_right'): ref_cube, + netcdf_path.format(pos='bottom'): bias_cube, + } + + return (plot_path, netcdf_paths) + def _process_pyplot_kwargs(self, plot_type, dataset): """Process functions for :mod:`matplotlib.pyplot`.""" pyplot_kwargs = self.plots[plot_type]['pyplot_kwargs'] @@ -1124,14 +1446,14 @@ def _process_pyplot_kwargs(self, plot_type, dataset): def _check_cube_dimensions(cube, plot_type): """Check that cube has correct dimensional variables.""" expected_dimensions_dict = { - 'annual_cycle': (['month_number'],), - 'map': (['latitude', 'longitude'],), - 'zonal_mean_profile': (['latitude', 'air_pressure'], - ['latitude', 'altitude']), - 'timeseries': (['time'],), - '1d_profile': (['air_pressure'], - ['altitude']), - + 'annual_cycle': (['month_number'], ), + 'map': (['latitude', 'longitude'], ), + 'zonal_mean_profile': (['latitude', + 'air_pressure'], ['latitude', 'altitude']), + 'timeseries': (['time'], ), + '1d_profile': (['air_pressure'], ['altitude']), + 'hovmoeller_z_vs_time': (['time', + 'air_pressure'], ['time', 'altitude']), } if plot_type not in expected_dimensions_dict: raise NotImplementedError(f"plot_type '{plot_type}' not supported") @@ -1141,8 +1463,7 @@ def _check_cube_dimensions(cube, plot_type): if all(cube_dims) and cube.ndim == len(dims): return dims expected_dims_str = ' or '.join( - [str(dims) for dims in expected_dimensions] - ) + [str(dims) for dims in expected_dimensions]) raise ValueError( f"Expected cube that exactly has the dimensional coordinates " f"{expected_dims_str}, got {cube.summary(shorten=True)}") @@ -1173,8 +1494,9 @@ def _get_multi_dataset_facets(datasets): @staticmethod def _get_reference_dataset(datasets, short_name): """Extract reference dataset.""" - ref_datasets = [d for d in datasets if - d.get('reference_for_monitor_diags', False)] + ref_datasets = [ + d for d in datasets if d.get('reference_for_monitor_diags', False) + ] if len(ref_datasets) > 1: raise ValueError( f"Expected at most 1 reference dataset (with " @@ -1226,7 +1548,13 @@ def create_timeseries_plot(self, datasets, short_name): # Default plot appearance multi_dataset_facets = self._get_multi_dataset_facets(datasets) axes.set_title(multi_dataset_facets['long_name']) - axes.set_xlabel('Time') + # apply time formatting + if self.plots[plot_type]['time_format'] is not None: + axes.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) + axes.set_xlabel(f"{short_name}") + else: + axes.set_xlabel(f"{short_name} [{multi_dataset_facets['units']}]") axes.set_ylabel(f"{short_name} [{multi_dataset_facets['units']}]") gridline_kwargs = self._get_gridline_kwargs(plot_type) if gridline_kwargs is not False: @@ -1249,7 +1577,8 @@ def create_timeseries_plot(self, datasets, short_name): # Save netCDF file netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) var_attrs = { - n: datasets[0][n] for n in ('short_name', 'long_name', 'units') + n: datasets[0][n] + for n in ('short_name', 'long_name', 'units') } io.save_1d_data(cubes, netcdf_path, 'time', var_attrs) @@ -1321,7 +1650,8 @@ def create_annual_cycle_plot(self, datasets, short_name): # Save netCDF file netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) var_attrs = { - n: datasets[0][n] for n in ('short_name', 'long_name', 'units') + n: datasets[0][n] + for n in ('short_name', 'long_name', 'units') } io.save_1d_data(cubes, netcdf_path, 'month_number', var_attrs) @@ -1366,25 +1696,21 @@ def create_map_plot(self, datasets, short_name): continue ancestors = [dataset['filename']] if ref_dataset is None: - (plot_path, netcdf_paths) = ( - self._plot_map_without_ref(plot_func, dataset) - ) + (plot_path, netcdf_paths) = (self._plot_map_without_ref( + plot_func, dataset)) caption = ( f"Map plot of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " - f"from {dataset['start_year']} to {dataset['end_year']}." - ) + f"from {dataset['start_year']} to {dataset['end_year']}.") else: - (plot_path, netcdf_paths) = ( - self._plot_map_with_ref(plot_func, dataset, ref_dataset) - ) + (plot_path, netcdf_paths) = (self._plot_map_with_ref( + plot_func, dataset, ref_dataset)) caption = ( f"Map plot of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " f"including bias relative to {ref_dataset['dataset']} " f"(project {ref_dataset['project']}) from " - f"{dataset['start_year']} to {dataset['end_year']}." - ) + f"{dataset['start_year']} to {dataset['end_year']}.") ancestors.append(ref_dataset['filename']) # If statistics are shown add a brief description to the caption @@ -1442,27 +1768,23 @@ def create_zonal_mean_profile_plot(self, datasets, short_name): continue ancestors = [dataset['filename']] if ref_dataset is None: - (plot_path, netcdf_paths) = ( - self._plot_zonal_mean_profile_without_ref(plot_func, - dataset) - ) + (plot_path, + netcdf_paths) = (self._plot_zonal_mean_profile_without_ref( + plot_func, dataset)) caption = ( f"Zonal mean profile of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " - f"from {dataset['start_year']} to {dataset['end_year']}." - ) + f"from {dataset['start_year']} to {dataset['end_year']}.") else: - (plot_path, netcdf_paths) = ( - self._plot_zonal_mean_profile_with_ref(plot_func, dataset, - ref_dataset) - ) + (plot_path, + netcdf_paths) = (self._plot_zonal_mean_profile_with_ref( + plot_func, dataset, ref_dataset)) caption = ( f"Zonal mean profile of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " f"including bias relative to {ref_dataset['dataset']} " f"(project {ref_dataset['project']}) from " - f"{dataset['start_year']} to {dataset['end_year']}." - ) + f"{dataset['start_year']} to {dataset['end_year']}.") ancestors.append(ref_dataset['filename']) # If statistics are shown add a brief description to the caption @@ -1532,11 +1854,9 @@ def create_1d_profile_plot(self, datasets, short_name): # apply logarithmic axes if self.plots[plot_type]['log_y']: axes.set_yscale('log') - axes.get_yaxis().set_major_formatter( - FormatStrFormatter('%.1f')) + axes.get_yaxis().set_major_formatter(FormatStrFormatter('%.1f')) if self.plots[plot_type]['show_y_minor_ticklabels']: - axes.get_yaxis().set_minor_formatter( - FormatStrFormatter('%.1f')) + axes.get_yaxis().set_minor_formatter(FormatStrFormatter('%.1f')) else: axes.get_yaxis().set_minor_formatter(NullFormatter()) if self.plots[plot_type]['log_x']: @@ -1576,7 +1896,8 @@ def create_1d_profile_plot(self, datasets, short_name): # Save netCDF file netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) var_attrs = { - n: datasets[0][n] for n in ('short_name', 'long_name', 'units') + n: datasets[0][n] + for n in ('short_name', 'long_name', 'units') } io.save_1d_data(cubes, netcdf_path, z_coord.standard_name, var_attrs) @@ -1595,6 +1916,82 @@ def create_1d_profile_plot(self, datasets, short_name): provenance_logger.log(plot_path, provenance_record) provenance_logger.log(netcdf_path, provenance_record) + def create_hovmoeller_z_vs_time_plot(self, datasets, short_name): + """Create hovmoeller z vs time plot.""" + plot_type = 'hovmoeller_z_vs_time' + if plot_type not in self.plots: + return + + if not datasets: + raise ValueError(f"No input data to plot '{plot_type}' given") + + # Get reference dataset if possible + ref_dataset = self._get_reference_dataset(datasets, short_name) + if ref_dataset is None: + logger.info("Plotting %s without reference dataset", plot_type) + else: + logger.info("Plotting %s with reference dataset '%s'", plot_type, + self._get_label(ref_dataset)) + + # Get plot function + plot_func = self._get_plot_func(plot_type) + + # Create a single plot for each dataset (incl. reference dataset if + # given) + for dataset in datasets: + if dataset == ref_dataset: + continue + ancestors = [dataset['filename']] + if ref_dataset is None: + (plot_path, + netcdf_paths) = (self._plot_hovmoeller_z_vs_time_without_ref( + plot_func, dataset)) + caption = ( + f"Hovmoeller z vs time of {dataset['long_name']}" + f"of dataset " + f"{dataset['dataset']} (project {dataset['project']}) " + f"from {dataset['start_year']} to {dataset['end_year']}.") + else: + (plot_path, + netcdf_paths) = (self._plot_hovmoeller_z_vs_time_with_ref( + plot_func, dataset, ref_dataset)) + caption = ( + f"Hovmoeller z vs time of {dataset['long_name']}" + f"of dataset " + f"{dataset['dataset']} (project {dataset['project']}) " + f"including bias relative to {ref_dataset['dataset']} " + f"(project {ref_dataset['project']}) from " + f"{dataset['start_year']} to {dataset['end_year']}.") + ancestors.append(ref_dataset['filename']) + + # If statistics are shown add a brief description to the caption + if self.plots[plot_type]['show_stats']: + caption += ( + " The number in the top left corner corresponds to the " + "spatial mean (weighted by grid cell areas).") + + # Save plot + plt.savefig(plot_path, **self.cfg['savefig_kwargs']) + logger.info("Wrote %s", plot_path) + plt.close() + + # Save netCDFs + for (netcdf_path, cube) in netcdf_paths.items(): + io.iris_save(cube, netcdf_path) + + # Provenance tracking + provenance_record = { + 'ancestors': ancestors, + 'authors': ['kuehbacher_birgit', 'heuer_helge'], + 'caption': caption, + 'plot_types': ['vert'], + 'long_names': [dataset['long_name']], + } + with ProvenanceLogger(self.cfg) as provenance_logger: + provenance_logger.log(plot_path, provenance_record) + for netcdf_path in netcdf_paths: + provenance_logger.log(netcdf_path, provenance_record) + def compute(self): """Plot preprocessed data.""" for (short_name, datasets) in self.grouped_input_data.items(): @@ -1604,6 +2001,7 @@ def compute(self): self.create_map_plot(datasets, short_name) self.create_zonal_mean_profile_plot(datasets, short_name) self.create_1d_profile_plot(datasets, short_name) + self.create_hovmoeller_z_vs_time_plot(datasets, short_name) def main(): diff --git a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml index 0d1415979a..3bc5fa686c 100644 --- a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml +++ b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml @@ -14,8 +14,8 @@ documentation: datasets: # Note: plot_label currently only used by diagnostic plot_multiple_annual_cycles - - {project: CMIP6, dataset: EC-Earth3, exp: historical, ensemble: r1i1p1f1, grid: gr, plot_label: 'EC-Earth3 historical'} - - {project: CMIP6, dataset: CanESM5, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'Reference (CanESM5 historical)', reference_for_monitor_diags: true} + - {project: CMIP6, dataset: MPI-ESM1-2-HR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'MPI-ESM1-2-HR historical'} + - {project: CMIP6, dataset: MPI-ESM1-2-LR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'MPI-ESM1-2-LR MPI historical', reference_for_monitor_diags: true} preprocessors: @@ -67,6 +67,18 @@ preprocessors: scheme: linear coordinate: air_pressure + global_mean_pressure: + custom_order: true + extract_levels: + levels: {cmor_table: CMIP6, coordinate: plev39} + scheme: linear + coordinate: air_pressure + regrid: + target_grid: 2x2 + scheme: + reference: esmf_regrid.schemes:ESMFAreaWeighted + area_statistics: + operator: mean diagnostics: @@ -168,3 +180,22 @@ diagnostics: color: C0 CanESM5: color: black + + pressure_vs_time_with_ref: + description: Plot hovmoeller z vs time including reference datasets. + variables: + ta: + preprocessor: global_mean_pressure + mip: Amon + timerange: '2000/2014' + scripts: + plot: + script: monitor/multi_datasets.py + plot_folder: '{plot_dir}' + plot_filename: '{plot_type}_{real_name}_{dataset}_{mip}' + plots: + hovmoeller_z_vs_time: + plot_func: 'contourf' + common_cbar: true + time_format: '%Y-%m' + log_y: true \ No newline at end of file From 29a93137f10154f2268063f62e3cfb3c8759ce44 Mon Sep 17 00:00:00 2001 From: Helge Heuer Date: Wed, 6 Sep 2023 13:35:35 +0200 Subject: [PATCH 02/12] Reset formatting from pre-commit --- .../diag_scripts/monitor/multi_datasets.py | 229 +++++++++--------- 1 file changed, 110 insertions(+), 119 deletions(-) diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index dbdd4fa7a0..049016e094 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -449,6 +449,7 @@ Extra arguments given to the recipe are ignored, so it is safe to use yaml anchors to share the configuration of common arguments with other monitor diagnostic script. + """ import logging from copy import deepcopy @@ -524,8 +525,12 @@ def __init__(self, config): # Check given plot types and set default settings for them self.supported_plot_types = [ - 'timeseries', 'annual_cycle', 'map', 'zonal_mean_profile', - '1d_profile', 'hovmoeller_z_vs_time' + 'timeseries', + 'annual_cycle', + 'map', + 'zonal_mean_profile', + '1d_profile', + 'hovmoeller_z_vs_time' ] for (plot_type, plot_options) in self.plots.items(): if plot_type not in self.supported_plot_types: @@ -551,14 +556,13 @@ def __init__(self, config): self.plots[plot_type].setdefault('pyplot_kwargs', {}) if plot_type == 'map': - self.plots[plot_type].setdefault('cbar_label', - '{short_name} [{units}]') - self.plots[plot_type].setdefault('cbar_label_bias', - 'Δ{short_name} [{units}]') - self.plots[plot_type].setdefault('cbar_kwargs', { - 'orientation': 'horizontal', - 'aspect': 30 - }) + self.plots[plot_type].setdefault( + 'cbar_label', '{short_name} [{units}]') + self.plots[plot_type].setdefault( + 'cbar_label_bias', 'Δ{short_name} [{units}]') + self.plots[plot_type].setdefault( + 'cbar_kwargs', {'orientation': 'horizontal', 'aspect': 30} + ) self.plots[plot_type].setdefault('cbar_kwargs_bias', {}) self.plots[plot_type].setdefault('common_cbar', False) self.plots[plot_type].setdefault('fontsize', 10) @@ -567,11 +571,13 @@ def __init__(self, config): self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('plot_kwargs_bias', {}) self.plots[plot_type]['plot_kwargs_bias'].setdefault( - 'cmap', 'bwr') + 'cmap', 'bwr' + ) if 'projection' not in self.plots[plot_type]: self.plots[plot_type].setdefault('projection', 'Robinson') - self.plots[plot_type].setdefault('projection_kwargs', - {'central_longitude': 10}) + self.plots[plot_type].setdefault( + 'projection_kwargs', {'central_longitude': 10} + ) else: self.plots[plot_type].setdefault('projection_kwargs', {}) self.plots[plot_type].setdefault('pyplot_kwargs', {}) @@ -581,12 +587,13 @@ def __init__(self, config): self.plots[plot_type].setdefault('x_pos_stats_bias', 0.92) if plot_type == 'zonal_mean_profile': - self.plots[plot_type].setdefault('cbar_label', - '{short_name} [{units}]') - self.plots[plot_type].setdefault('cbar_label_bias', - 'Δ{short_name} [{units}]') - self.plots[plot_type].setdefault('cbar_kwargs', - {'orientation': 'vertical'}) + self.plots[plot_type].setdefault( + 'cbar_label', '{short_name} [{units}]') + self.plots[plot_type].setdefault( + 'cbar_label_bias', 'Δ{short_name} [{units}]') + self.plots[plot_type].setdefault( + 'cbar_kwargs', {'orientation': 'vertical'} + ) self.plots[plot_type].setdefault('cbar_kwargs_bias', {}) self.plots[plot_type].setdefault('common_cbar', False) self.plots[plot_type].setdefault('fontsize', 10) @@ -595,12 +602,14 @@ def __init__(self, config): self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('plot_kwargs_bias', {}) self.plots[plot_type]['plot_kwargs_bias'].setdefault( - 'cmap', 'bwr') + 'cmap', 'bwr' + ) self.plots[plot_type].setdefault('pyplot_kwargs', {}) self.plots[plot_type].setdefault('rasterize', True) self.plots[plot_type].setdefault('show_stats', True) - self.plots[plot_type].setdefault('show_y_minor_ticklabels', - False) + self.plots[plot_type].setdefault( + 'show_y_minor_ticklabels', False + ) self.plots[plot_type].setdefault('x_pos_stats_avg', 0.01) self.plots[plot_type].setdefault('x_pos_stats_bias', 0.7) @@ -612,8 +621,9 @@ def __init__(self, config): self.plots[plot_type].setdefault('log_y', True) self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('pyplot_kwargs', {}) - self.plots[plot_type].setdefault('show_y_minor_ticklabels', - False) + self.plots[plot_type].setdefault( + 'show_y_minor_ticklabels', False + ) if plot_type == 'hovmoeller_z_vs_time': self.plots[plot_type].setdefault('cbar_label', @@ -664,8 +674,7 @@ def _add_colorbar(self, plot_type, plot_left, plot_right, axes_left, if self.plots[plot_type]['common_cbar']: if 'aspect' in cbar_kwargs: cbar_kwargs['aspect'] += 20.0 - cbar = plt.colorbar(plot_left, - ax=[axes_left, axes_right], + cbar = plt.colorbar(plot_left, ax=[axes_left, axes_right], **cbar_kwargs) cbar.set_label(cbar_label_left, fontsize=fontsize) cbar.ax.tick_params(labelsize=fontsize) @@ -679,11 +688,7 @@ def _add_colorbar(self, plot_type, plot_left, plot_right, axes_left, cbar_right.set_label(cbar_label_right, fontsize=fontsize) cbar_right.ax.tick_params(labelsize=fontsize) - def _add_stats(self, - plot_type, - axes, - dim_coords, - dataset, + def _add_stats(self, plot_type, axes, dim_coords, dataset, ref_dataset=None): """Add text to plot that describes basic statistics.""" if not self.plots[plot_type]['show_stats']: @@ -729,8 +734,7 @@ def _add_stats(self, # Mean weights = area_weights(cube) if ref_cube is None: - mean = cube.collapsed(dim_coords, - iris.analysis.MEAN, + mean = cube.collapsed(dim_coords, iris.analysis.MEAN, weights=weights) logger.info( "Area-weighted mean of %s for %s = %f%s", @@ -740,8 +744,7 @@ def _add_stats(self, dataset['units'], ) else: - mean = (cube - ref_cube).collapsed(dim_coords, - iris.analysis.MEAN, + mean = (cube - ref_cube).collapsed(dim_coords, iris.analysis.MEAN, weights=weights) logger.info( "Area-weighted bias of %s for %s = %f%s", @@ -750,23 +753,16 @@ def _add_stats(self, mean.data, dataset['units'], ) - axes.text(x_pos, - y_pos, - f"{mean.data:.2f}{cube.units}", - fontsize=fontsize, - transform=axes.transAxes) + axes.text(x_pos, y_pos, f"{mean.data:.2f}{cube.units}", + fontsize=fontsize, transform=axes.transAxes) if ref_cube is None: return # Weighted RMSE - rmse = (cube - ref_cube).collapsed(dim_coords, - iris.analysis.RMS, + rmse = (cube - ref_cube).collapsed(dim_coords, iris.analysis.RMS, weights=weights) - axes.text(x_pos_bias, - y_pos, - f"RMSE={rmse.data:.2f}{cube.units}", - fontsize=fontsize, - transform=axes.transAxes) + axes.text(x_pos_bias, y_pos, f"RMSE={rmse.data:.2f}{cube.units}", + fontsize=fontsize, transform=axes.transAxes) logger.info( "Area-weighted RMSE of %s for %s = %f%s", dataset['short_name'], @@ -782,11 +778,8 @@ def _add_stats(self, ref_cube_data = ref_cube.data.ravel()[~mask] weights = weights.ravel()[~mask] r2_val = r2_score(cube_data, ref_cube_data, sample_weight=weights) - axes.text(x_pos_bias, - y_pos - 0.1, - rf"R$^2$={r2_val:.2f}", - fontsize=fontsize, - transform=axes.transAxes) + axes.text(x_pos_bias, y_pos - 0.1, rf"R$^2$={r2_val:.2f}", + fontsize=fontsize, transform=axes.transAxes) logger.info( "Area-weighted R2 of %s for %s = %f", dataset['short_name'], @@ -930,9 +923,7 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): # Create single figure with multiple axes with mpl.rc_context(self._get_custom_mpl_rc_params(plot_type)): fig = plt.figure(**self.cfg['figure_kwargs']) - gridspec = GridSpec(5, - 4, - figure=fig, + gridspec = GridSpec(5, 4, figure=fig, height_ratios=[1.0, 1.0, 0.4, 1.0, 1.0]) # Options used for all subplots @@ -976,8 +967,7 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): bias_cube = cube - ref_cube axes_bias = fig.add_subplot(gridspec[3:5, 1:3], projection=projection) - plot_kwargs_bias = self._get_plot_kwargs(plot_type, - dataset, + plot_kwargs_bias = self._get_plot_kwargs(plot_type, dataset, bias=True) plot_kwargs_bias['axes'] = axes_bias plot_bias = plot_func(bias_cube, **plot_kwargs_bias) @@ -989,8 +979,7 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): pad=3.0, ) cbar_kwargs_bias = self._get_cbar_kwargs(plot_type, bias=True) - cbar_bias = fig.colorbar(plot_bias, - ax=axes_bias, + cbar_bias = fig.colorbar(plot_bias, ax=axes_bias, **cbar_kwargs_bias) cbar_bias.set_label( self._get_cbar_label(plot_type, dataset, bias=True), @@ -1011,8 +1000,9 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): # File paths plot_path = self.get_plot_path(plot_type, dataset) - netcdf_path = (get_diagnostic_filename( - Path(plot_path).stem + "_{pos}", self.cfg)) + netcdf_path = ( + get_diagnostic_filename(Path(plot_path).stem + "_{pos}", self.cfg) + ) netcdf_paths = { netcdf_path.format(pos='top_left'): cube, netcdf_path.format(pos='top_right'): ref_cube, @@ -1048,8 +1038,7 @@ def _plot_map_without_ref(self, plot_func, dataset): # Setup colorbar fontsize = self.plots[plot_type]['fontsize'] - colorbar = fig.colorbar(plot_map, - ax=axes, + colorbar = fig.colorbar(plot_map, ax=axes, **self._get_cbar_kwargs(plot_type)) colorbar.set_label(self._get_cbar_label(plot_type, dataset), fontsize=fontsize) @@ -1075,10 +1064,9 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, ref_dataset): """Plot zonal mean profile for single dataset with reference.""" plot_type = 'zonal_mean_profile' - logger.info( - "Plotting zonal mean profile with reference dataset" - " '%s' for '%s'", self._get_label(ref_dataset), - self._get_label(dataset)) + logger.info("Plotting zonal mean profile with reference dataset" + " '%s' for '%s'", + self._get_label(ref_dataset), self._get_label(dataset)) # Make sure that the data has the correct dimensions cube = dataset['cube'] @@ -1089,9 +1077,7 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # Create single figure with multiple axes with mpl.rc_context(self._get_custom_mpl_rc_params(plot_type)): fig = plt.figure(**self.cfg['figure_kwargs']) - gridspec = GridSpec(5, - 4, - figure=fig, + gridspec = GridSpec(5, 4, figure=fig, height_ratios=[1.0, 1.0, 0.4, 1.0, 1.0]) # Options used for all subplots @@ -1119,8 +1105,7 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # Plot reference dataset (top right) # Note: make sure to use the same vmin and vmax than the top left # plot if a common colorbar is desired - axes_ref = fig.add_subplot(gridspec[0:2, 2:4], - sharex=axes_data, + axes_ref = fig.add_subplot(gridspec[0:2, 2:4], sharex=axes_data, sharey=axes_data) plot_kwargs['axes'] = axes_ref if self.plots[plot_type]['common_cbar']: @@ -1137,11 +1122,9 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # Plot bias (bottom center) bias_cube = cube - ref_cube - axes_bias = fig.add_subplot(gridspec[3:5, 1:3], - sharex=axes_data, + axes_bias = fig.add_subplot(gridspec[3:5, 1:3], sharex=axes_data, sharey=axes_data) - plot_kwargs_bias = self._get_plot_kwargs(plot_type, - dataset, + plot_kwargs_bias = self._get_plot_kwargs(plot_type, dataset, bias=True) plot_kwargs_bias['axes'] = axes_bias plot_bias = plot_func(bias_cube, **plot_kwargs_bias) @@ -1152,8 +1135,7 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, axes_bias.set_xlabel('latitude [°N]') axes_bias.set_ylabel(f'{z_coord.long_name} [{z_coord.units}]') cbar_kwargs_bias = self._get_cbar_kwargs(plot_type, bias=True) - cbar_bias = fig.colorbar(plot_bias, - ax=axes_bias, + cbar_bias = fig.colorbar(plot_bias, ax=axes_bias, **cbar_kwargs_bias) cbar_bias.set_label( self._get_cbar_label(plot_type, dataset, bias=True), @@ -1174,8 +1156,9 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, # File paths plot_path = self.get_plot_path(plot_type, dataset) - netcdf_path = (get_diagnostic_filename( - Path(plot_path).stem + "_{pos}", self.cfg)) + netcdf_path = ( + get_diagnostic_filename(Path(plot_path).stem + "_{pos}", self.cfg) + ) netcdf_paths = { netcdf_path.format(pos='top_left'): cube, netcdf_path.format(pos='top_right'): ref_cube, @@ -1187,9 +1170,9 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, def _plot_zonal_mean_profile_without_ref(self, plot_func, dataset): """Plot zonal mean profile for single dataset without reference.""" plot_type = 'zonal_mean_profile' - logger.info( - "Plotting zonal mean profile without reference dataset" - " for '%s'", self._get_label(dataset)) + logger.info("Plotting zonal mean profile without reference dataset" + " for '%s'", + self._get_label(dataset)) # Make sure that the data has the correct dimensions cube = dataset['cube'] @@ -1208,8 +1191,7 @@ def _plot_zonal_mean_profile_without_ref(self, plot_func, dataset): # Setup colorbar fontsize = self.plots[plot_type]['fontsize'] - colorbar = fig.colorbar(plot_zonal_mean_profile, - ax=axes, + colorbar = fig.colorbar(plot_zonal_mean_profile, ax=axes, **self._get_cbar_kwargs(plot_type)) colorbar.set_label(self._get_cbar_label(plot_type, dataset), fontsize=fontsize) @@ -1446,14 +1428,16 @@ def _process_pyplot_kwargs(self, plot_type, dataset): def _check_cube_dimensions(cube, plot_type): """Check that cube has correct dimensional variables.""" expected_dimensions_dict = { - 'annual_cycle': (['month_number'], ), - 'map': (['latitude', 'longitude'], ), - 'zonal_mean_profile': (['latitude', - 'air_pressure'], ['latitude', 'altitude']), - 'timeseries': (['time'], ), - '1d_profile': (['air_pressure'], ['altitude']), - 'hovmoeller_z_vs_time': (['time', - 'air_pressure'], ['time', 'altitude']), + 'annual_cycle': (['month_number'],), + 'map': (['latitude', 'longitude'],), + 'zonal_mean_profile': (['latitude', 'air_pressure'], + ['latitude', 'altitude']), + 'timeseries': (['time'],), + '1d_profile': (['air_pressure'], + ['altitude']), + 'hovmoeller_z_vs_time': (['time', 'air_pressure'], + ['time', 'altitude']), + } if plot_type not in expected_dimensions_dict: raise NotImplementedError(f"plot_type '{plot_type}' not supported") @@ -1463,7 +1447,8 @@ def _check_cube_dimensions(cube, plot_type): if all(cube_dims) and cube.ndim == len(dims): return dims expected_dims_str = ' or '.join( - [str(dims) for dims in expected_dimensions]) + [str(dims) for dims in expected_dimensions] + ) raise ValueError( f"Expected cube that exactly has the dimensional coordinates " f"{expected_dims_str}, got {cube.summary(shorten=True)}") @@ -1494,9 +1479,8 @@ def _get_multi_dataset_facets(datasets): @staticmethod def _get_reference_dataset(datasets, short_name): """Extract reference dataset.""" - ref_datasets = [ - d for d in datasets if d.get('reference_for_monitor_diags', False) - ] + ref_datasets = [d for d in datasets if + d.get('reference_for_monitor_diags', False)] if len(ref_datasets) > 1: raise ValueError( f"Expected at most 1 reference dataset (with " @@ -1577,8 +1561,7 @@ def create_timeseries_plot(self, datasets, short_name): # Save netCDF file netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) var_attrs = { - n: datasets[0][n] - for n in ('short_name', 'long_name', 'units') + n: datasets[0][n] for n in ('short_name', 'long_name', 'units') } io.save_1d_data(cubes, netcdf_path, 'time', var_attrs) @@ -1650,8 +1633,7 @@ def create_annual_cycle_plot(self, datasets, short_name): # Save netCDF file netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) var_attrs = { - n: datasets[0][n] - for n in ('short_name', 'long_name', 'units') + n: datasets[0][n] for n in ('short_name', 'long_name', 'units') } io.save_1d_data(cubes, netcdf_path, 'month_number', var_attrs) @@ -1696,21 +1678,25 @@ def create_map_plot(self, datasets, short_name): continue ancestors = [dataset['filename']] if ref_dataset is None: - (plot_path, netcdf_paths) = (self._plot_map_without_ref( - plot_func, dataset)) + (plot_path, netcdf_paths) = ( + self._plot_map_without_ref(plot_func, dataset) + ) caption = ( f"Map plot of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " - f"from {dataset['start_year']} to {dataset['end_year']}.") + f"from {dataset['start_year']} to {dataset['end_year']}." + ) else: - (plot_path, netcdf_paths) = (self._plot_map_with_ref( - plot_func, dataset, ref_dataset)) + (plot_path, netcdf_paths) = ( + self._plot_map_with_ref(plot_func, dataset, ref_dataset) + ) caption = ( f"Map plot of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " f"including bias relative to {ref_dataset['dataset']} " f"(project {ref_dataset['project']}) from " - f"{dataset['start_year']} to {dataset['end_year']}.") + f"{dataset['start_year']} to {dataset['end_year']}." + ) ancestors.append(ref_dataset['filename']) # If statistics are shown add a brief description to the caption @@ -1768,23 +1754,27 @@ def create_zonal_mean_profile_plot(self, datasets, short_name): continue ancestors = [dataset['filename']] if ref_dataset is None: - (plot_path, - netcdf_paths) = (self._plot_zonal_mean_profile_without_ref( - plot_func, dataset)) + (plot_path, netcdf_paths) = ( + self._plot_zonal_mean_profile_without_ref(plot_func, + dataset) + ) caption = ( f"Zonal mean profile of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " - f"from {dataset['start_year']} to {dataset['end_year']}.") + f"from {dataset['start_year']} to {dataset['end_year']}." + ) else: - (plot_path, - netcdf_paths) = (self._plot_zonal_mean_profile_with_ref( - plot_func, dataset, ref_dataset)) + (plot_path, netcdf_paths) = ( + self._plot_zonal_mean_profile_with_ref(plot_func, dataset, + ref_dataset) + ) caption = ( f"Zonal mean profile of {dataset['long_name']} of dataset " f"{dataset['dataset']} (project {dataset['project']}) " f"including bias relative to {ref_dataset['dataset']} " f"(project {ref_dataset['project']}) from " - f"{dataset['start_year']} to {dataset['end_year']}.") + f"{dataset['start_year']} to {dataset['end_year']}." + ) ancestors.append(ref_dataset['filename']) # If statistics are shown add a brief description to the caption @@ -1854,9 +1844,11 @@ def create_1d_profile_plot(self, datasets, short_name): # apply logarithmic axes if self.plots[plot_type]['log_y']: axes.set_yscale('log') - axes.get_yaxis().set_major_formatter(FormatStrFormatter('%.1f')) + axes.get_yaxis().set_major_formatter( + FormatStrFormatter('%.1f')) if self.plots[plot_type]['show_y_minor_ticklabels']: - axes.get_yaxis().set_minor_formatter(FormatStrFormatter('%.1f')) + axes.get_yaxis().set_minor_formatter( + FormatStrFormatter('%.1f')) else: axes.get_yaxis().set_minor_formatter(NullFormatter()) if self.plots[plot_type]['log_x']: @@ -1896,8 +1888,7 @@ def create_1d_profile_plot(self, datasets, short_name): # Save netCDF file netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) var_attrs = { - n: datasets[0][n] - for n in ('short_name', 'long_name', 'units') + n: datasets[0][n] for n in ('short_name', 'long_name', 'units') } io.save_1d_data(cubes, netcdf_path, z_coord.standard_name, var_attrs) From 850efc173c35e9edefc3a7eb8b2826c13122e74b Mon Sep 17 00:00:00 2001 From: Helge Heuer Date: Wed, 6 Sep 2023 13:44:05 +0200 Subject: [PATCH 03/12] Time series plot x-axis label formatting --- esmvaltool/diag_scripts/monitor/multi_datasets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index 049016e094..e7a5f8e182 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -1532,13 +1532,11 @@ def create_timeseries_plot(self, datasets, short_name): # Default plot appearance multi_dataset_facets = self._get_multi_dataset_facets(datasets) axes.set_title(multi_dataset_facets['long_name']) + axes.set_xlabel("Time") # apply time formatting if self.plots[plot_type]['time_format'] is not None: axes.get_xaxis().set_major_formatter( mdates.DateFormatter(self.plots[plot_type]['time_format'])) - axes.set_xlabel(f"{short_name}") - else: - axes.set_xlabel(f"{short_name} [{multi_dataset_facets['units']}]") axes.set_ylabel(f"{short_name} [{multi_dataset_facets['units']}]") gridline_kwargs = self._get_gridline_kwargs(plot_type) if gridline_kwargs is not False: From d2a3f3418b9ca70e355732a691666fcfdc6df14f Mon Sep 17 00:00:00 2001 From: cubeme Date: Wed, 6 Sep 2023 13:48:43 +0200 Subject: [PATCH 04/12] Replace double quotes with single quotes --- esmvaltool/diag_scripts/monitor/multi_datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index e7a5f8e182..c979723b64 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -1532,7 +1532,7 @@ def create_timeseries_plot(self, datasets, short_name): # Default plot appearance multi_dataset_facets = self._get_multi_dataset_facets(datasets) axes.set_title(multi_dataset_facets['long_name']) - axes.set_xlabel("Time") + axes.set_xlabel('Time') # apply time formatting if self.plots[plot_type]['time_format'] is not None: axes.get_xaxis().set_major_formatter( From 01241f487c9e10f5daea78b320d9e0983d5a8046 Mon Sep 17 00:00:00 2001 From: helgehr <38046421+helgehr@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:13:50 +0200 Subject: [PATCH 05/12] Update multi_datasets.py Configuration options dashes --- esmvaltool/diag_scripts/monitor/multi_datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index c979723b64..0258abd1d7 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -324,7 +324,7 @@ relevant if ``show_stats: true``. Configuration options for plot type ``hovmoeller_z_vs_time`` ----------------------------------------------------------- +------------------------------------------------------------ cbar_label: str, optional (default: '{short_name} [{units}]') Colorbar label. Can include facets in curly brackets which will be derived from the corresponding dataset, e.g., ``{project}``, ``{short_name}``, From 950f072f650a305b0dd6f86df90eb27d6db9acc9 Mon Sep 17 00:00:00 2001 From: Helge Heuer Date: Wed, 6 Sep 2023 16:22:27 +0200 Subject: [PATCH 06/12] Add documentaion of hovmoeller_z_vst_time to configuration options in recipe --- esmvaltool/diag_scripts/monitor/multi_datasets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index e4a3788a18..3b4b3184ec 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -73,8 +73,8 @@ individual plot is created. plots: dict, optional Plot types plotted by this diagnostic (see list above). Dictionary keys - must be ``timeseries``, ``annual_cycle``, ``map``, ``zonal_mean_profile`` - or ``1d_profile``. + must be ``timeseries``, ``annual_cycle``, ``map``, ``zonal_mean_profile``, + ``1d_profile`` or ``hovmoeller_z_vs_time``. Dictionary values are dictionaries used as options for the corresponding plot. The allowed options for the different plot types are given below. plot_filename: str, optional From d349b9b68e77925d2792a1d1eb1fe1eb2cfc4539 Mon Sep 17 00:00:00 2001 From: Helge Heuer Date: Wed, 6 Sep 2023 16:32:53 +0200 Subject: [PATCH 07/12] Fixed docs figure description order --- doc/sphinx/source/recipes/recipe_monitor.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx/source/recipes/recipe_monitor.rst b/doc/sphinx/source/recipes/recipe_monitor.rst index 45043a3318..a1971fda09 100644 --- a/doc/sphinx/source/recipes/recipe_monitor.rst +++ b/doc/sphinx/source/recipes/recipe_monitor.rst @@ -209,11 +209,11 @@ Zonal mean profile of ta including a reference dataset. :align: center :width: 14cm -Hovmoeller plot (pressure vs time) of ta including a reference dataset. +1D profile of ta including a reference dataset. .. _fig_hovmoeller_z_vs_time_with_ref: .. figure:: /recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png :align: center :width: 14cm -1D profile of ta including a reference dataset. +Hovmoeller plot (pressure vs time) of ta including a reference dataset. \ No newline at end of file From d04c45a9d2c552c3b8b48471346445c36769ccda Mon Sep 17 00:00:00 2001 From: helgehr <38046421+helgehr@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:13:00 +0200 Subject: [PATCH 08/12] Apply suggestions from code review Co-authored-by: Manuel Schlund <32543114+schlunma@users.noreply.github.com> --- esmvaltool/config-references.yml | 1 + .../diag_scripts/monitor/multi_datasets.py | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/esmvaltool/config-references.yml b/esmvaltool/config-references.yml index ea416116d6..3bfe80e422 100644 --- a/esmvaltool/config-references.yml +++ b/esmvaltool/config-references.yml @@ -301,6 +301,7 @@ authors: name: Kuehbacher, Birgit institute: DLR, Germany email: birgit.kuehbacher@dlr.de + orcid: lejeune_quentin: name: Lejeune, Quentin institute: Climate Analytics, Germany diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index 3b4b3184ec..fa47c72e5f 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -131,8 +131,9 @@ to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. time_format: str, optional (default: None) - Matplotlib strftime format string for the time axis, if None defaults to - time units of iris cube + :func:`~datetime.strftime` format string that is used to format the time + axis using :class:`matplotlib.dates.DateFormatter`. If ``None``, use the + default formatting imposed by the iris plotting function. Configuration options for plot type ``annual_cycle`` ---------------------------------------------------- @@ -403,9 +404,10 @@ Text x-position of bias statistics (shown on the right) in Axes coordinates. Can be adjusted to avoid overlap with the figure. Only relevant if ``show_stats: true``. -time_format: str, optional (default: %Y) - Matplotlib strftime format string for the time axis, if None defaults to - time units of iris cube +time_format: str, optional (default: None) + :func:`~datetime.strftime` format string that is used to format the time + axis using :class:`matplotlib.dates.DateFormatter`. If ``None``, use the + default formatting imposed by the iris plotting function. Configuration options for plot type ``1d_profile`` -------------------------------------------------- @@ -1279,8 +1281,9 @@ def _plot_hovmoeller_z_vs_time_without_ref(self, plot_func, dataset): FormatStrFormatter('%.1f')) else: axes.get_yaxis().set_minor_formatter(NullFormatter()) - axes.get_xaxis().set_major_formatter( - mdates.DateFormatter(self.plots[plot_type]['time_format'])) + if self.plots[plot_type]['time_format'] is not None: + axes.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) axes.set_xlabel(f'{time_coord.long_name}') self._process_pyplot_kwargs(plot_type, dataset) @@ -1339,8 +1342,9 @@ def _plot_hovmoeller_z_vs_time_with_ref(self, plot_func, dataset, FormatStrFormatter('%.1f')) else: axes_data.get_yaxis().set_minor_formatter(NullFormatter()) - axes_data.get_xaxis().set_major_formatter( - mdates.DateFormatter(self.plots[plot_type]['time_format'])) + if self.plots[plot_type]['time_format'] is not None: + axes_data.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) self._add_stats(plot_type, axes_data, dim_coords_dat, dataset) # Plot reference dataset (top right) @@ -1356,8 +1360,9 @@ def _plot_hovmoeller_z_vs_time_with_ref(self, plot_func, dataset, plot_ref = plot_func(ref_cube, **plot_kwargs) axes_ref.set_title(self._get_label(ref_dataset), pad=3.0) plt.setp(axes_ref.get_yticklabels(), visible=False) - axes_ref.get_xaxis().set_major_formatter( - mdates.DateFormatter(self.plots[plot_type]['time_format'])) + if self.plots[plot_type]['time_format'] is not None: + axes_ref.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) self._add_stats(plot_type, axes_ref, dim_coords_ref, ref_dataset) # Add colorbar(s) @@ -1380,8 +1385,9 @@ def _plot_hovmoeller_z_vs_time_with_ref(self, plot_func, dataset, ) axes_bias.set_xlabel(time_coord.long_name) axes_bias.set_ylabel(f'{z_coord.long_name} [{z_coord.units}]') - axes_bias.get_xaxis().set_major_formatter( - mdates.DateFormatter(self.plots[plot_type]['time_format'])) + if self.plots[plot_type]['time_format'] is not None: + axes_bias.get_xaxis().set_major_formatter( + mdates.DateFormatter(self.plots[plot_type]['time_format'])) cbar_kwargs_bias = self._get_cbar_kwargs(plot_type, bias=True) cbar_bias = fig.colorbar(plot_bias, ax=axes_bias, @@ -1972,7 +1978,7 @@ def create_hovmoeller_z_vs_time_plot(self, datasets): if self.plots[plot_type]['show_stats']: caption += ( " The number in the top left corner corresponds to the " - "spatial mean (weighted by grid cell areas).") + "spatiotemporal mean.") # Save plot plt.savefig(plot_path, **self.cfg['savefig_kwargs']) From 3cea499b65b8caa0525339eed90aefb187cb828e Mon Sep 17 00:00:00 2001 From: Helge Heuer Date: Thu, 7 Sep 2023 10:38:31 +0200 Subject: [PATCH 09/12] Merge with main --- .zenodo.json | 9 ++ CITATION.cff | 9 ++ doc/sphinx/source/recipes/recipe_monitor.rst | 8 +- esmvaltool/config-references.yml | 10 ++ .../diag_scripts/monitor/multi_datasets.py | 119 +++++++++++++++++- .../monitor/recipe_monitor_with_refs.yml | 22 +++- 6 files changed, 171 insertions(+), 6 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 0b196e0046..df129dfcd1 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -86,6 +86,11 @@ "name": "Bock, Lisa", "orcid": "0000-0001-7058-5938" }, + { + "affiliation": "DLR, Germany", + "name": "Bonnet, Pauline", + "orcid": "0000-0003-3780-0784" + }, { "affiliation": "BSC, Spain", "name": "Caron, Louis-Philippe", @@ -272,6 +277,10 @@ "affiliation": "CICERO, Norway", "name": "Sandstad, Marit" }, + { + "affiliation": "DLR, Germany", + "name": "Sarauer, Ellen" + }, { "affiliation": "MetOffice, UK", "name": "Sellar, Alistair" diff --git a/CITATION.cff b/CITATION.cff index 5674868d86..b9c77159f8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -309,6 +309,10 @@ authors: family-names: Weigel given-names: Katja orcid: "https://orcid.org/0000-0001-6133-7801" + - + affiliation: "DLR, Germany" + family-names: Sarauer + given-names: Ellen - affiliation: "University of Reading, UK" family-names: Roberts @@ -363,6 +367,11 @@ authors: family-names: Beucher given-names: Romain orcid: "https://orcid.org/0000-0003-3891-5444" + - + affiliation: "DLR, Germany" + family-names: Bonnet + given-names: Pauline + orcid: "https://orcid.org/0000-0003-3780-0784" cff-version: 1.2.0 date-released: 2023-07-06 diff --git a/doc/sphinx/source/recipes/recipe_monitor.rst b/doc/sphinx/source/recipes/recipe_monitor.rst index a1971fda09..ac12fc1d1d 100644 --- a/doc/sphinx/source/recipes/recipe_monitor.rst +++ b/doc/sphinx/source/recipes/recipe_monitor.rst @@ -216,4 +216,10 @@ Zonal mean profile of ta including a reference dataset. :align: center :width: 14cm -Hovmoeller plot (pressure vs time) of ta including a reference dataset. \ No newline at end of file +Hovmoeller plot (pressure vs time) of ta including a reference dataset. +.. _fig_variable_vs_lat_with_ref: +.. figure:: /recipes/figures/monitor/variable_vs_lat_with_ref.png + :align: center + :width: 14cm + +1D profile of pr over latitude. diff --git a/esmvaltool/config-references.yml b/esmvaltool/config-references.yml index 3bfe80e422..8ad8bc1a50 100644 --- a/esmvaltool/config-references.yml +++ b/esmvaltool/config-references.yml @@ -124,6 +124,11 @@ authors: name: Bojovic, Dragana institute: BSC, Spain orcid: https://orcid.org/0000-0001-7354-1885 + bonnet_pauline: + name: Bonnet, Pauline + institute: DLR, Germany + orcid: https://orcid.org/0000-0003-3780-0784 + github: Paulinebonnet111 brunner_lukas: name: Brunner, Lukas institute: ETH Zurich, Switzerland @@ -456,6 +461,11 @@ authors: name: Sandstad, Marit institute: Cicero, Norway orcid: + sarauer_ellen: + name: Sarauer, Ellen + institute: DLR, Germany + orcid: + github: ellensarauer serva_federico: name: Serva, Federico institute: CNR, Italy diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index fa47c72e5f..7e52e39fad 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -40,6 +40,10 @@ can use the preprocessors :func:`esmvalcore.preprocessor.regrid` and :func:`esmvalcore.preprocessor.extract_levels` for this). Input data needs to be 2D with dimensions `time`, `height`/`air_pressure`. + - Variable vs. latitude plot (plot type ``variable_vs_lat``): + for each variable separately, all datasets are plotted in one + single figure. Input data needs to be 1D with single + dimension `latitude`. .. warning:: @@ -449,6 +453,35 @@ show_y_minor_ticklabels: bool, optional (default: False) Show tick labels for the minor ticks on the Y axis. +Configuration options for plot type ``variable_vs_lat`` +------------------------------------------------------- +gridline_kwargs: dict, optional + Optional keyword arguments for grid lines. By default, ``color: lightgrey, + alpha: 0.5`` are used. Use ``gridline_kwargs: false`` to not show grid + lines. +legend_kwargs: dict, optional + Optional keyword arguments for :func:`matplotlib.pyplot.legend`. Use + ``legend_kwargs: false`` to not show legends. +plot_kwargs: dict, optional + Optional keyword arguments for :func:`iris.plot.plot`. Dictionary keys are + elements identified by ``facet_used_for_labels`` or ``default``, e.g., + ``CMIP6`` if ``facet_used_for_labels: project`` or ``historical`` if + ``facet_used_for_labels: exp``. Dictionary values are dictionaries used as + keyword arguments for :func:`iris.plot.plot`. String arguments can include + facets in curly brackets which will be derived from the corresponding + dataset, e.g., ``{project}``, ``{short_name}``, ``{exp}``. Examples: + ``default: {linestyle: '-', label: '{project}'}, CMIP6: {color: red, + linestyle: '--'}, OBS: {color: black}``. +pyplot_kwargs: dict, optional + Optional calls to functions of :mod:`matplotlib.pyplot`. Dictionary keys + are functions of :mod:`matplotlib.pyplot`. Dictionary values are used as + single argument for these functions. String arguments can include facets in + curly brackets which will be derived from the datasets plotted in the + corresponding plot, e.g., ``{short_name}``, ``{exp}``. Facets like + ``{project}`` that vary between the different datasets will be transformed + to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot + of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. + .. hint:: Extra arguments given to the recipe are ignored, so it is safe to use yaml @@ -538,7 +571,8 @@ def __init__(self, config): 'map', 'zonal_mean_profile', '1d_profile', - 'hovmoeller_z_vs_time' + 'hovmoeller_z_vs_time', + 'variable_vs_lat' ] for (plot_type, plot_options) in self.plots.items(): if plot_type not in self.supported_plot_types: @@ -632,6 +666,11 @@ def __init__(self, config): self.plots[plot_type].setdefault( 'show_y_minor_ticklabels', False ) + if plot_type == 'variable_vs_lat': + self.plots[plot_type].setdefault('gridline_kwargs', {}) + self.plots[plot_type].setdefault('legend_kwargs', {}) + self.plots[plot_type].setdefault('plot_kwargs', {}) + self.plots[plot_type].setdefault('pyplot_kwargs', {}) if plot_type == 'hovmoeller_z_vs_time': self.plots[plot_type].setdefault('cbar_label', @@ -885,7 +924,8 @@ def _get_plot_kwargs(self, plot_type, dataset, bias=False): plot_kwargs[key] = val # Default settings for different plot types - if plot_type in ('timeseries', 'annual_cycle', '1d_profile'): + if plot_type in ('timeseries', 'annual_cycle', '1d_profile', + 'variable_vs_lat'): plot_kwargs.setdefault('label', label) return deepcopy(plot_kwargs) @@ -1449,7 +1489,7 @@ def _check_cube_dimensions(cube, plot_type): ['altitude']), 'hovmoeller_z_vs_time': (['time', 'air_pressure'], ['time', 'altitude']), - + 'variable_vs_lat': (['latitude'],) } if plot_type not in expected_dimensions_dict: raise NotImplementedError(f"plot_type '{plot_type}' not supported") @@ -1502,6 +1542,78 @@ def _get_reference_dataset(self, datasets): return ref_datasets[0] return None + def create_variable_vs_lat_plot(self, datasets): + """Create Variable as a function of latitude.""" + plot_type = 'variable_vs_lat' + if plot_type not in self.plots: + return + if not datasets: + raise ValueError(f"No input data to plot '{plot_type}' given") + logger.info("Plotting %s", plot_type) + fig = plt.figure(**self.cfg['figure_kwargs']) + axes = fig.add_subplot() + + # Plot all datasets in one single figure + ancestors = [] + cubes = {} + for dataset in datasets: + ancestors.append(dataset['filename']) + cube = dataset['cube'] + cubes[self._get_label(dataset)] = cube + self._check_cube_dimensions(cube, plot_type) + + # Plot data + plot_kwargs = self._get_plot_kwargs(plot_type, dataset) + plot_kwargs['axes'] = axes + iris.plot.plot(cube, **plot_kwargs) + + # Default plot appearance + multi_dataset_facets = self._get_multi_dataset_facets(datasets) + axes.set_title(multi_dataset_facets['long_name']) + axes.set_xlabel('latitude [°N]') + axes.set_ylabel( + f"{multi_dataset_facets[self.cfg['group_variables_by']]} " + f"[{multi_dataset_facets['units']}]" + ) + gridline_kwargs = self._get_gridline_kwargs(plot_type) + if gridline_kwargs is not False: + axes.grid(**gridline_kwargs) + + # Legend + legend_kwargs = self.plots[plot_type]['legend_kwargs'] + if legend_kwargs is not False: + axes.legend(**legend_kwargs) + + # Customize plot appearance + self._process_pyplot_kwargs(plot_type, multi_dataset_facets) + + # Save plot + plot_path = self.get_plot_path(plot_type, multi_dataset_facets) + fig.savefig(plot_path, **self.cfg['savefig_kwargs']) + logger.info("Wrote %s", plot_path) + plt.close() + + # Save netCDF file + netcdf_path = get_diagnostic_filename(Path(plot_path).stem, self.cfg) + var_attrs = { + n: datasets[0][n] for n in ('short_name', 'long_name', 'units') + } + io.save_1d_data(cubes, netcdf_path, 'latitude', var_attrs) + + # Provenance tracking + caption = (f"{multi_dataset_facets['long_name']} vs. latitude for " + f"various datasets.") + provenance_record = { + 'ancestors': ancestors, + 'authors': ['sarauer_ellen'], + 'caption': caption, + 'plot_types': ['line'], + 'long_names': [var_attrs['long_name']], + } + with ProvenanceLogger(self.cfg) as provenance_logger: + provenance_logger.log(plot_path, provenance_record) + provenance_logger.log(netcdf_path, provenance_record) + def create_timeseries_plot(self, datasets): """Create time series plot.""" plot_type = 'timeseries' @@ -2012,6 +2124,7 @@ def compute(self): self.create_zonal_mean_profile_plot(datasets) self.create_1d_profile_plot(datasets) self.create_hovmoeller_z_vs_time_plot(datasets) + self.create_variable_vs_lat_plot(datasets) def main(): diff --git a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml index 3bc5fa686c..f78594f40d 100644 --- a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml +++ b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml @@ -17,7 +17,6 @@ datasets: - {project: CMIP6, dataset: MPI-ESM1-2-HR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'MPI-ESM1-2-HR historical'} - {project: CMIP6, dataset: MPI-ESM1-2-LR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'MPI-ESM1-2-LR MPI historical', reference_for_monitor_diags: true} - preprocessors: timeseries_regular: @@ -80,6 +79,12 @@ preprocessors: area_statistics: operator: mean + var_vs_lat: + climate_statistics: + operator: mean + zonal_statistics: + operator: mean + diagnostics: plot_multiple_timeseries: @@ -198,4 +203,17 @@ diagnostics: plot_func: 'contourf' common_cbar: true time_format: '%Y-%m' - log_y: true \ No newline at end of file + log_y: true + + plot_variable_vs_latitude: + description: Creates a single-panel variable plot over latitude. + variables: + pr: + preprocessor: var_vs_lat + mip: Amon + timerange: '20000101/20030101' + scripts: + plot: + script: monitor/multi_datasets.py + plots: + variable_vs_lat: From 1be210694881acdf7926248053523090cd84a943 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Thu, 7 Sep 2023 11:30:09 +0200 Subject: [PATCH 10/12] Fixed plot type order and monitor recipe --- doc/sphinx/source/recipes/recipe_monitor.rst | 13 +- .../diag_scripts/monitor/multi_datasets.py | 201 +++++++++--------- .../monitor/recipe_monitor_with_refs.yml | 62 +++--- 3 files changed, 130 insertions(+), 146 deletions(-) diff --git a/doc/sphinx/source/recipes/recipe_monitor.rst b/doc/sphinx/source/recipes/recipe_monitor.rst index ac12fc1d1d..87ac9d0b17 100644 --- a/doc/sphinx/source/recipes/recipe_monitor.rst +++ b/doc/sphinx/source/recipes/recipe_monitor.rst @@ -211,15 +211,16 @@ Zonal mean profile of ta including a reference dataset. 1D profile of ta including a reference dataset. -.. _fig_hovmoeller_z_vs_time_with_ref: -.. figure:: /recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png +.. _fig_variable_vs_lat_with_ref: +.. figure:: /recipes/figures/monitor/variable_vs_lat_with_ref.png :align: center :width: 14cm -Hovmoeller plot (pressure vs time) of ta including a reference dataset. -.. _fig_variable_vs_lat_with_ref: -.. figure:: /recipes/figures/monitor/variable_vs_lat_with_ref.png +Zonal mean pr including a reference dataset. + +.. _fig_hovmoeller_z_vs_time_with_ref: +.. figure:: /recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png :align: center :width: 14cm -1D profile of pr over latitude. +Hovmoeller plot (pressure vs time) of ta including a reference dataset. diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index 2def700fc4..4a364e8c89 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -31,22 +31,7 @@ datasets need to be given on the same horizontal and vertical grid (you can use the preprocessors :func:`esmvalcore.preprocessor.regrid` and :func:`esmvalcore.preprocessor.extract_levels` for this). Input data - needs to be 2D with dimensions `latitude`, `height`/`air_pressure`. -<<<<<<< HEAD - - Hovmoeller altitude vs time (plot type ``hovmoeller_z_vs_time``): - for each variable and dataset, an individual profile is plotted. If a - reference dataset is defined, also include this dataset and a bias plot - into the figure. Note that if a reference dataset is defined, all input - datasets need to be given on the same horizontal and vertical grid (you - can use the preprocessors :func:`esmvalcore.preprocessor.regrid` and - :func:`esmvalcore.preprocessor.extract_levels` for this). Input data - needs to be 2D with dimensions `time`, `height`/`air_pressure`. -======= ->>>>>>> origin/main - - Variable vs. latitude plot (plot type ``variable_vs_lat``): - for each variable separately, all datasets are plotted in one - single figure. Input data needs to be 1D with single - dimension `latitude`. + needs to be 2D with dimensions `latitude`, `altitude`/`air_pressure`. .. warning:: @@ -57,7 +42,19 @@ - 1D profiles (plot type ``1d_profile``): for each variable separately, all datasets are plotted in one single figure. Input data needs to be 1D with - single dimension `height` / `air_pressure` + single dimension `altitude` / `air_pressure` + - Variable vs. latitude plot (plot type ``variable_vs_lat``): + for each variable separately, all datasets are plotted in one + single figure. Input data needs to be 1D with single + dimension `latitude`. + - Hovmoeller Z vs time (plot type ``hovmoeller_z_vs_time``): for each + variable and dataset, an individual profile is plotted. If a reference + dataset is defined, also include this dataset and a bias plot into the + figure. Note that if a reference dataset is defined, all input datasets + need to be given on the same horizontal and vertical grid (you can use + the preprocessors :func:`esmvalcore.preprocessor.regrid` and + :func:`esmvalcore.preprocessor.extract_levels` for this). Input data + needs to be 2D with dimensions `time`, `altitude`/`air_pressure`. Author ------ @@ -81,7 +78,7 @@ plots: dict, optional Plot types plotted by this diagnostic (see list above). Dictionary keys must be ``timeseries``, ``annual_cycle``, ``map``, ``zonal_mean_profile``, - ``1d_profile`` or ``hovmoeller_z_vs_time``. + ``1d_profile``, ``variable_vs_lat``, or ``hovmoeller_z_vs_time``. Dictionary values are dictionaries used as options for the corresponding plot. The allowed options for the different plot types are given below. plot_filename: str, optional @@ -138,9 +135,9 @@ to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. time_format: str, optional (default: None) - :func:`~datetime.strftime` format string that is used to format the time - axis using :class:`matplotlib.dates.DateFormatter`. If ``None``, use the - default formatting imposed by the iris plotting function. + :func:`~datetime.datetime.strftime` format string that is used to format + the time axis using :class:`matplotlib.dates.DateFormatter`. If ``None``, + use the default formatting imposed by the iris plotting function. Configuration options for plot type ``annual_cycle`` ---------------------------------------------------- @@ -292,7 +289,7 @@ plot_func: str, optional (default: 'contourf') Plot function used to plot the profiles. Must be a function of :mod:`iris.plot` that supports plotting of 2D cubes with coordinates - latitude and height/air_pressure. + latitude and altitude/air_pressure. plot_kwargs: dict, optional Optional keyword arguments for the plot function defined by ``plot_func``. Dictionary keys are elements identified by ``facet_used_for_labels`` or @@ -334,6 +331,75 @@ coordinates. Can be adjusted to avoid overlap with the figure. Only relevant if ``show_stats: true``. +Configuration options for plot type ``1d_profile`` +-------------------------------------------------- +aspect_ratio: float, optional (default: 1.5) + Aspect ratio of the plot. The default value results in a slender upright + plot. +gridline_kwargs: dict, optional + Optional keyword arguments for grid lines. By default, ``color: lightgrey, + alpha: 0.5`` are used. Use ``gridline_kwargs: false`` to not show grid + lines. +legend_kwargs: dict, optional + Optional keyword arguments for :func:`matplotlib.pyplot.legend`. Use + ``legend_kwargs: false`` to not show legends. +log_x: bool, optional (default: False) + Use logarithmic X-axis. Note that for the logarithmic x axis tickmarks are + set so that minor tickmarks show up. Setting of individual tickmarks by + pyplot_kwargs is not recommended in this case. +log_y: bool, optional (default: True) + Use logarithmic Y-axis. +plot_kwargs: dict, optional + Optional keyword arguments for :func:`iris.plot.plot`. Dictionary keys are + elements identified by ``facet_used_for_labels`` or ``default``, e.g., + ``CMIP6`` if ``facet_used_for_labels: project`` or ``historical`` if + ``facet_used_for_labels: exp``. Dictionary values are dictionaries used as + keyword arguments for :func:`iris.plot.plot`. String arguments can include + facets in curly brackets which will be derived from the corresponding + dataset, e.g., ``{project}``, ``{short_name}``, ``{exp}``. Examples: + ``default: {linestyle: '-', label: '{project}'}, CMIP6: {color: red, + linestyle: '--'}, OBS: {color: black}``. +pyplot_kwargs: dict, optional + Optional calls to functions of :mod:`matplotlib.pyplot`. Dictionary keys + are functions of :mod:`matplotlib.pyplot`. Dictionary values are used as + single argument for these functions. String arguments can include facets in + curly brackets which will be derived from the datasets plotted in the + corresponding plot, e.g., ``{short_name}``, ``{exp}``. Facets like + ``{project}`` that vary between the different datasets will be transformed + to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot + of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. +show_y_minor_ticklabels: bool, optional (default: False) + Show tick labels for the minor ticks on the Y axis. + +Configuration options for plot type ``variable_vs_lat`` +------------------------------------------------------- +gridline_kwargs: dict, optional + Optional keyword arguments for grid lines. By default, ``color: lightgrey, + alpha: 0.5`` are used. Use ``gridline_kwargs: false`` to not show grid + lines. +legend_kwargs: dict, optional + Optional keyword arguments for :func:`matplotlib.pyplot.legend`. Use + ``legend_kwargs: false`` to not show legends. +plot_kwargs: dict, optional + Optional keyword arguments for :func:`iris.plot.plot`. Dictionary keys are + elements identified by ``facet_used_for_labels`` or ``default``, e.g., + ``CMIP6`` if ``facet_used_for_labels: project`` or ``historical`` if + ``facet_used_for_labels: exp``. Dictionary values are dictionaries used as + keyword arguments for :func:`iris.plot.plot`. String arguments can include + facets in curly brackets which will be derived from the corresponding + dataset, e.g., ``{project}``, ``{short_name}``, ``{exp}``. Examples: + ``default: {linestyle: '-', label: '{project}'}, CMIP6: {color: red, + linestyle: '--'}, OBS: {color: black}``. +pyplot_kwargs: dict, optional + Optional calls to functions of :mod:`matplotlib.pyplot`. Dictionary keys + are functions of :mod:`matplotlib.pyplot`. Dictionary values are used as + single argument for these functions. String arguments can include facets in + curly brackets which will be derived from the datasets plotted in the + corresponding plot, e.g., ``{short_name}``, ``{exp}``. Facets like + ``{project}`` that vary between the different datasets will be transformed + to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot + of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. + Configuration options for plot type ``hovmoeller_z_vs_time`` ------------------------------------------------------------ cbar_label: str, optional (default: '{short_name} [{units}]') @@ -370,7 +436,7 @@ plot_func: str, optional (default: 'contourf') Plot function used to plot the profiles. Must be a function of :mod:`iris.plot` that supports plotting of 2D cubes with coordinates - latitude and height/air_pressure. + latitude and altitude/air_pressure. plot_kwargs: dict, optional Optional keyword arguments for the plot function defined by ``plot_func``. Dictionary keys are elements identified by ``facet_used_for_labels`` or @@ -412,78 +478,9 @@ coordinates. Can be adjusted to avoid overlap with the figure. Only relevant if ``show_stats: true``. time_format: str, optional (default: None) - :func:`~datetime.strftime` format string that is used to format the time - axis using :class:`matplotlib.dates.DateFormatter`. If ``None``, use the - default formatting imposed by the iris plotting function. - -Configuration options for plot type ``1d_profile`` --------------------------------------------------- -aspect_ratio: float, optional (default: 1.5) - Aspect ratio of the plot. The default value results in a slender upright - plot. -gridline_kwargs: dict, optional - Optional keyword arguments for grid lines. By default, ``color: lightgrey, - alpha: 0.5`` are used. Use ``gridline_kwargs: false`` to not show grid - lines. -legend_kwargs: dict, optional - Optional keyword arguments for :func:`matplotlib.pyplot.legend`. Use - ``legend_kwargs: false`` to not show legends. -log_x: bool, optional (default: False) - Use logarithmic X-axis. Note that for the logarithmic x axis tickmarks are - set so that minor tickmarks show up. Setting of individual tickmarks by - pyplot_kwargs is not recommended in this case. -log_y: bool, optional (default: True) - Use logarithmic Y-axis. -plot_kwargs: dict, optional - Optional keyword arguments for :func:`iris.plot.plot`. Dictionary keys are - elements identified by ``facet_used_for_labels`` or ``default``, e.g., - ``CMIP6`` if ``facet_used_for_labels: project`` or ``historical`` if - ``facet_used_for_labels: exp``. Dictionary values are dictionaries used as - keyword arguments for :func:`iris.plot.plot`. String arguments can include - facets in curly brackets which will be derived from the corresponding - dataset, e.g., ``{project}``, ``{short_name}``, ``{exp}``. Examples: - ``default: {linestyle: '-', label: '{project}'}, CMIP6: {color: red, - linestyle: '--'}, OBS: {color: black}``. -pyplot_kwargs: dict, optional - Optional calls to functions of :mod:`matplotlib.pyplot`. Dictionary keys - are functions of :mod:`matplotlib.pyplot`. Dictionary values are used as - single argument for these functions. String arguments can include facets in - curly brackets which will be derived from the datasets plotted in the - corresponding plot, e.g., ``{short_name}``, ``{exp}``. Facets like - ``{project}`` that vary between the different datasets will be transformed - to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot - of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. -show_y_minor_ticklabels: bool, optional (default: False) - Show tick labels for the minor ticks on the Y axis. - -Configuration options for plot type ``variable_vs_lat`` -------------------------------------------------------- -gridline_kwargs: dict, optional - Optional keyword arguments for grid lines. By default, ``color: lightgrey, - alpha: 0.5`` are used. Use ``gridline_kwargs: false`` to not show grid - lines. -legend_kwargs: dict, optional - Optional keyword arguments for :func:`matplotlib.pyplot.legend`. Use - ``legend_kwargs: false`` to not show legends. -plot_kwargs: dict, optional - Optional keyword arguments for :func:`iris.plot.plot`. Dictionary keys are - elements identified by ``facet_used_for_labels`` or ``default``, e.g., - ``CMIP6`` if ``facet_used_for_labels: project`` or ``historical`` if - ``facet_used_for_labels: exp``. Dictionary values are dictionaries used as - keyword arguments for :func:`iris.plot.plot`. String arguments can include - facets in curly brackets which will be derived from the corresponding - dataset, e.g., ``{project}``, ``{short_name}``, ``{exp}``. Examples: - ``default: {linestyle: '-', label: '{project}'}, CMIP6: {color: red, - linestyle: '--'}, OBS: {color: black}``. -pyplot_kwargs: dict, optional - Optional calls to functions of :mod:`matplotlib.pyplot`. Dictionary keys - are functions of :mod:`matplotlib.pyplot`. Dictionary values are used as - single argument for these functions. String arguments can include facets in - curly brackets which will be derived from the datasets plotted in the - corresponding plot, e.g., ``{short_name}``, ``{exp}``. Facets like - ``{project}`` that vary between the different datasets will be transformed - to something like ``ambiguous_project``. Examples: ``title: 'Awesome Plot - of {long_name}'``, ``xlabel: '{short_name}'``, ``xlim: [0, 5]``. + :func:`~datetime.datetime.strftime` format string that is used to format + the time axis using :class:`matplotlib.dates.DateFormatter`. If ``None``, + use the default formatting imposed by the iris plotting function. .. hint:: @@ -757,10 +754,10 @@ def _add_stats(self, plot_type, axes, dim_coords, dataset, # Different options for the different plots types fontsize = 6.0 y_pos = 0.95 - if plot_type == 'map': - x_pos_bias = self.plots[plot_type]['x_pos_stats_bias'] - x_pos = self.plots[plot_type]['x_pos_stats_avg'] - elif plot_type in ['zonal_mean_profile', 'hovmoeller_z_vs_time']: + if all([ + 'x_pos_stats_avg' in self.plots[plot_type], + 'x_pos_stats_bias' in self.plots[plot_type], + ]): x_pos_bias = self.plots[plot_type]['x_pos_stats_bias'] x_pos = self.plots[plot_type]['x_pos_stats_avg'] else: @@ -1295,14 +1292,14 @@ def _plot_hovmoeller_z_vs_time_without_ref(self, plot_func, dataset): axes = fig.add_subplot() plot_kwargs = self._get_plot_kwargs(plot_type, dataset) plot_kwargs['axes'] = axes - plot_zonal_mean_profile = plot_func(cube, **plot_kwargs) + plot_hovmoeller = plot_func(cube, **plot_kwargs) # Print statistics if desired self._add_stats(plot_type, axes, dim_coords_dat, dataset) # Setup colorbar fontsize = self.plots[plot_type]['fontsize'] - colorbar = fig.colorbar(plot_zonal_mean_profile, + colorbar = fig.colorbar(plot_hovmoeller, ax=axes, **self._get_cbar_kwargs(plot_type)) colorbar.set_label(self._get_cbar_label(plot_type, dataset), @@ -2126,8 +2123,8 @@ def compute(self): self.create_map_plot(datasets) self.create_zonal_mean_profile_plot(datasets) self.create_1d_profile_plot(datasets) - self.create_hovmoeller_z_vs_time_plot(datasets) self.create_variable_vs_lat_plot(datasets) + self.create_hovmoeller_z_vs_time_plot(datasets) def main(): diff --git a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml index 7deae3f5f2..a93a627274 100644 --- a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml +++ b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml @@ -14,13 +14,8 @@ documentation: datasets: # Note: plot_label currently only used by diagnostic plot_multiple_annual_cycles -<<<<<<< HEAD - - {project: CMIP6, dataset: MPI-ESM1-2-HR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'MPI-ESM1-2-HR historical'} - - {project: CMIP6, dataset: MPI-ESM1-2-LR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'MPI-ESM1-2-LR MPI historical', reference_for_monitor_diags: true} -======= - - {project: CMIP6, dataset: EC-Earth3, exp: historical, ensemble: r1i1p1f1, grid: gr, plot_label: 'EC-Earth3 historical'} - - {project: CMIP6, dataset: CanESM5, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'Reference (CanESM5 historical)', reference_for_monitor_diags: true} ->>>>>>> origin/main + - {project: CMIP6, dataset: MPI-ESM1-2-HR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'MPI-ESM1-2-HR historical'} + - {project: CMIP6, dataset: MPI-ESM1-2-LR, exp: historical, ensemble: r1i1p1f1, grid: gn, plot_label: 'Reference (MPI-ESM1-2-LR historical)', reference_for_monitor_diags: true} preprocessors: @@ -71,36 +66,27 @@ preprocessors: scheme: linear coordinate: air_pressure -<<<<<<< HEAD - global_mean_pressure: + var_vs_lat: + climate_statistics: + operator: mean + regrid: + target_grid: 2x2 + scheme: linear + zonal_statistics: + operator: mean + + global_mean_extract_levels: custom_order: true extract_levels: levels: {cmor_table: CMIP6, coordinate: plev39} scheme: linear coordinate: air_pressure -======= - var_vs_lat: - climate_statistics: - operator: mean ->>>>>>> origin/main regrid: target_grid: 2x2 - scheme: - reference: esmf_regrid.schemes:ESMFAreaWeighted -<<<<<<< HEAD + scheme: linear area_statistics: operator: mean - var_vs_lat: - climate_statistics: - operator: mean - zonal_statistics: - operator: mean -======= - zonal_statistics: - operator: mean - ->>>>>>> origin/main diagnostics: @@ -121,9 +107,9 @@ diagnostics: annual_mean_kwargs: linestyle: '--' plot_kwargs: - EC-Earth3: # = dataset since 'facet_used_for_labels' is 'dataset' by default + MPI-ESM1-2-HR: # = dataset since 'facet_used_for_labels' is 'dataset' by default color: C0 - CanESM5: + MPI-ESM1-2-LR: color: black plot_multiple_annual_cycles: @@ -142,9 +128,9 @@ diagnostics: legend_kwargs: loc: upper right plot_kwargs: - 'EC-Earth3 historical': # = plot_label since 'facet_used_for_labels: plot_label' + 'MPI-ESM1-2-HR historical': # = plot_label since 'facet_used_for_labels: plot_label' color: C0 - 'Reference (CanESM5 historical)': + 'Reference (MPI-ESM1-2-LR historical)': color: black pyplot_kwargs: title: Near-Surface Air Temperature on Northern Hemisphere @@ -198,26 +184,25 @@ diagnostics: plots: 1d_profile: plot_kwargs: - EC-Earth3: # = dataset since 'facet_used_for_labels' is 'dataset' by default + MPI-ESM1-2-HR: # = dataset since 'facet_used_for_labels' is 'dataset' by default color: C0 - CanESM5: + MPI-ESM1-2-LR: color: black pressure_vs_time_with_ref: description: Plot hovmoeller z vs time including reference datasets. variables: ta: - preprocessor: global_mean_pressure + preprocessor: global_mean_extract_levels mip: Amon - timerange: '2000/2014' + timerange: '2000/2004' scripts: plot: + <<: *plot_multi_dataset_default script: monitor/multi_datasets.py - plot_folder: '{plot_dir}' - plot_filename: '{plot_type}_{real_name}_{dataset}_{mip}' plots: hovmoeller_z_vs_time: - plot_func: 'contourf' + plot_func: contourf common_cbar: true time_format: '%Y-%m' log_y: true @@ -231,6 +216,7 @@ diagnostics: timerange: '20000101/20030101' scripts: plot: + <<: *plot_multi_dataset_default script: monitor/multi_datasets.py plots: variable_vs_lat: From 49ce7ad7b9bba085fff17f386c5ef03f323de869 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Thu, 7 Sep 2023 12:05:22 +0200 Subject: [PATCH 11/12] Final optimizations --- .../diag_scripts/monitor/multi_datasets.py | 30 +++++++++++-------- .../monitor/recipe_monitor_with_refs.yml | 15 +++++++--- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/esmvaltool/diag_scripts/monitor/multi_datasets.py b/esmvaltool/diag_scripts/monitor/multi_datasets.py index 4a364e8c89..2f4bce5176 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -47,12 +47,12 @@ for each variable separately, all datasets are plotted in one single figure. Input data needs to be 1D with single dimension `latitude`. - - Hovmoeller Z vs time (plot type ``hovmoeller_z_vs_time``): for each + - Hovmoeller Z vs. time (plot type ``hovmoeller_z_vs_time``): for each variable and dataset, an individual profile is plotted. If a reference dataset is defined, also include this dataset and a bias plot into the figure. Note that if a reference dataset is defined, all input datasets - need to be given on the same horizontal and vertical grid (you can use - the preprocessors :func:`esmvalcore.preprocessor.regrid` and + need to be given on the same temporal and vertical grid (you can use + the preprocessors :func:`esmvalcore.preprocessor.regrid_time` and :func:`esmvalcore.preprocessor.extract_levels` for this). Input data needs to be 2D with dimensions `time`, `altitude`/`air_pressure`. @@ -682,18 +682,18 @@ def __init__(self, config): self.plots[plot_type].setdefault('cbar_kwargs_bias', {}) self.plots[plot_type].setdefault('common_cbar', False) self.plots[plot_type].setdefault('fontsize', 10) - self.plots[plot_type].setdefault('log_y', False) + self.plots[plot_type].setdefault('log_y', True) self.plots[plot_type].setdefault('plot_func', 'contourf') self.plots[plot_type].setdefault('plot_kwargs', {}) self.plots[plot_type].setdefault('plot_kwargs_bias', {}) self.plots[plot_type]['plot_kwargs_bias'].setdefault( 'cmap', 'bwr') - self.plots[plot_type].setdefault('time_format', None) self.plots[plot_type].setdefault('pyplot_kwargs', {}) self.plots[plot_type].setdefault('rasterize', True) self.plots[plot_type].setdefault('show_stats', True) self.plots[plot_type].setdefault('show_y_minor_ticklabels', False) + self.plots[plot_type].setdefault('time_format', None) self.plots[plot_type].setdefault('x_pos_stats_avg', 0.01) self.plots[plot_type].setdefault('x_pos_stats_bias', 0.7) @@ -990,6 +990,7 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): axes_data.gridlines(**gridline_kwargs) axes_data.set_title(self._get_label(dataset), pad=3.0) self._add_stats(plot_type, axes_data, dim_coords_dat, dataset) + self._process_pyplot_kwargs(plot_type, dataset) # Plot reference dataset (top right) # Note: make sure to use the same vmin and vmax than the top left @@ -1006,6 +1007,7 @@ def _plot_map_with_ref(self, plot_func, dataset, ref_dataset): axes_ref.gridlines(**gridline_kwargs) axes_ref.set_title(self._get_label(ref_dataset), pad=3.0) self._add_stats(plot_type, axes_ref, dim_coords_ref, ref_dataset) + self._process_pyplot_kwargs(plot_type, ref_dataset) # Add colorbar(s) self._add_colorbar(plot_type, plot_data, plot_ref, axes_data, @@ -1149,6 +1151,7 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, else: axes_data.get_yaxis().set_minor_formatter(NullFormatter()) self._add_stats(plot_type, axes_data, dim_coords_dat, dataset) + self._process_pyplot_kwargs(plot_type, dataset) # Plot reference dataset (top right) # Note: make sure to use the same vmin and vmax than the top left @@ -1163,6 +1166,7 @@ def _plot_zonal_mean_profile_with_ref(self, plot_func, dataset, axes_ref.set_title(self._get_label(ref_dataset), pad=3.0) plt.setp(axes_ref.get_yticklabels(), visible=False) self._add_stats(plot_type, axes_ref, dim_coords_ref, ref_dataset) + self._process_pyplot_kwargs(plot_type, ref_dataset) # Add colorbar(s) self._add_colorbar(plot_type, plot_data, plot_ref, axes_data, @@ -1274,10 +1278,10 @@ def _plot_zonal_mean_profile_without_ref(self, plot_func, dataset): return (plot_path, {netcdf_path: cube}) def _plot_hovmoeller_z_vs_time_without_ref(self, plot_func, dataset): - """Plot hovmoeller z vs time for single dataset without reference.""" + """Plot Hovmoeller Z vs. time for single dataset without reference.""" plot_type = 'hovmoeller_z_vs_time' logger.info( - "Plotting homvoeller z vs time without reference dataset" + "Plotting Hovmoeller Z vs. time without reference dataset" " for '%s'", self._get_label(dataset)) # Make sure that the data has the correct dimensions @@ -1339,10 +1343,10 @@ def _plot_hovmoeller_z_vs_time_without_ref(self, plot_func, dataset): def _plot_hovmoeller_z_vs_time_with_ref(self, plot_func, dataset, ref_dataset): - """Plot hovmoeller z vs time for single dataset with reference.""" + """Plot Hovmoeller Z vs. time for single dataset with reference.""" plot_type = 'hovmoeller_z_vs_time' logger.info( - "Plotting hovmoeller z vs time with reference dataset" + "Plotting Hovmoeller z vs. time with reference dataset" " '%s' for '%s'", self._get_label(ref_dataset), self._get_label(dataset)) @@ -1386,6 +1390,7 @@ def _plot_hovmoeller_z_vs_time_with_ref(self, plot_func, dataset, axes_data.get_xaxis().set_major_formatter( mdates.DateFormatter(self.plots[plot_type]['time_format'])) self._add_stats(plot_type, axes_data, dim_coords_dat, dataset) + self._process_pyplot_kwargs(plot_type, dataset) # Plot reference dataset (top right) # Note: make sure to use the same vmin and vmax than the top left @@ -1404,6 +1409,7 @@ def _plot_hovmoeller_z_vs_time_with_ref(self, plot_func, dataset, axes_ref.get_xaxis().set_major_formatter( mdates.DateFormatter(self.plots[plot_type]['time_format'])) self._add_stats(plot_type, axes_ref, dim_coords_ref, ref_dataset) + self._process_pyplot_kwargs(plot_type, ref_dataset) # Add colorbar(s) self._add_colorbar(plot_type, plot_data, plot_ref, axes_data, @@ -2039,7 +2045,7 @@ def create_1d_profile_plot(self, datasets): provenance_logger.log(netcdf_path, provenance_record) def create_hovmoeller_z_vs_time_plot(self, datasets): - """Create hovmoeller z vs time plot.""" + """Create Hovmoeller Z vs. time plot.""" plot_type = 'hovmoeller_z_vs_time' if plot_type not in self.plots: return @@ -2069,7 +2075,7 @@ def create_hovmoeller_z_vs_time_plot(self, datasets): netcdf_paths) = (self._plot_hovmoeller_z_vs_time_without_ref( plot_func, dataset)) caption = ( - f"Hovmoeller z vs time of {dataset['long_name']}" + f"Hovmoeller Z vs. time plot of {dataset['long_name']} " f"of dataset " f"{dataset['dataset']} (project {dataset['project']}) " f"from {dataset['start_year']} to {dataset['end_year']}.") @@ -2078,7 +2084,7 @@ def create_hovmoeller_z_vs_time_plot(self, datasets): netcdf_paths) = (self._plot_hovmoeller_z_vs_time_with_ref( plot_func, dataset, ref_dataset)) caption = ( - f"Hovmoeller z vs time of {dataset['long_name']}" + f"Hovmoeller Z vs. time plot of {dataset['long_name']} " f"of dataset " f"{dataset['dataset']} (project {dataset['project']}) " f"including bias relative to {ref_dataset['dataset']} " diff --git a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml index a93a627274..37ff442290 100644 --- a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml +++ b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml @@ -8,6 +8,9 @@ documentation: authors: - schlund_manuel - winterstein_franziska + - sarauer_ellen + - kuehbacher_birgit + - heuer_helge maintainer: - schlund_manuel @@ -74,13 +77,15 @@ preprocessors: scheme: linear zonal_statistics: operator: mean + convert_units: + units: mm day-1 global_mean_extract_levels: custom_order: true extract_levels: - levels: {cmor_table: CMIP6, coordinate: plev39} + levels: {cmor_table: CMIP6, coordinate: alt16} scheme: linear - coordinate: air_pressure + coordinate: altitude regrid: target_grid: 2x2 scheme: linear @@ -204,8 +209,10 @@ diagnostics: hovmoeller_z_vs_time: plot_func: contourf common_cbar: true - time_format: '%Y-%m' - log_y: true + time_format: '%Y' + log_y: false + pyplot_kwargs: + ylim: [0, 20000] plot_variable_vs_latitude: description: Creates a single-panel variable plot over latitude. From 8591cb95fd8149307edd51771417b5c8d72ebc97 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Thu, 7 Sep 2023 12:07:41 +0200 Subject: [PATCH 12/12] Final recipe tweaks --- .../monitor/recipe_monitor_with_refs.yml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml index 37ff442290..3db01e6a4a 100644 --- a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml +++ b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml @@ -194,8 +194,22 @@ diagnostics: MPI-ESM1-2-LR: color: black - pressure_vs_time_with_ref: - description: Plot hovmoeller z vs time including reference datasets. + plot_variable_vs_latitude: + description: Creates a single-panel variable plot over latitude. + variables: + pr: + preprocessor: var_vs_lat + mip: Amon + timerange: '20000101/20030101' + scripts: + plot: + <<: *plot_multi_dataset_default + script: monitor/multi_datasets.py + plots: + variable_vs_lat: + + plot_hovmoeller_z_vs_time: + description: Plot Hovmoeller Z vs. time including reference datasets. variables: ta: preprocessor: global_mean_extract_levels @@ -213,17 +227,3 @@ diagnostics: log_y: false pyplot_kwargs: ylim: [0, 20000] - - plot_variable_vs_latitude: - description: Creates a single-panel variable plot over latitude. - variables: - pr: - preprocessor: var_vs_lat - mip: Amon - timerange: '20000101/20030101' - scripts: - plot: - <<: *plot_multi_dataset_default - script: monitor/multi_datasets.py - plots: - variable_vs_lat: