diff --git a/.zenodo.json b/.zenodo.json index 348d2aa80f..df129dfcd1 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -181,6 +181,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", @@ -199,6 +204,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 64378d61ce..b9c77159f8 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 0000000000..734913c60b Binary files /dev/null and b/doc/sphinx/source/recipes/figures/monitor/hovmoeller_z_vs_time_with_ref.png differ diff --git a/doc/sphinx/source/recipes/recipe_monitor.rst b/doc/sphinx/source/recipes/recipe_monitor.rst index fa3899cde6..87ac9d0b17 100644 --- a/doc/sphinx/source/recipes/recipe_monitor.rst +++ b/doc/sphinx/source/recipes/recipe_monitor.rst @@ -216,4 +216,11 @@ Zonal mean profile of ta including a reference dataset. :align: center :width: 14cm -1D profile of pr over latitude. +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 + +Hovmoeller plot (pressure vs time) of ta including a reference dataset. diff --git a/esmvaltool/config-references.yml b/esmvaltool/config-references.yml index fe28d612b5..8ad8bc1a50 100644 --- a/esmvaltool/config-references.yml +++ b/esmvaltool/config-references.yml @@ -257,6 +257,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 @@ -297,6 +302,11 @@ 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 + 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 6fcedd9470..2f4bce5176 100644 --- a/esmvaltool/diag_scripts/monitor/multi_datasets.py +++ b/esmvaltool/diag_scripts/monitor/multi_datasets.py @@ -31,11 +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`. - - 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:: @@ -46,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 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`. Author ------ @@ -69,8 +77,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``, ``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 @@ -126,6 +134,10 @@ ``{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) + :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`` ---------------------------------------------------- @@ -277,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 @@ -388,6 +400,88 @@ 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}]') + 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 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 + ``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: None) + :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:: Extra arguments given to the recipe are ignored, so it is safe to use yaml @@ -403,6 +497,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 @@ -476,7 +571,8 @@ def __init__(self, config): 'map', 'zonal_mean_profile', '1d_profile', - 'variable_vs_lat', + '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: @@ -493,6 +589,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', {}) @@ -575,6 +672,31 @@ def __init__(self, config): 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', + '{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', 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('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) + # Check that facet_used_for_labels is present for every dataset for dataset in self.input_data: if self.cfg['facet_used_for_labels'] not in dataset: @@ -632,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']: + 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: @@ -868,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 @@ -884,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, @@ -1027,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 @@ -1041,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, @@ -1151,6 +1277,196 @@ 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 Hovmoeller 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_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_hovmoeller, + 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()) + 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) + + # 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()) + 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) + 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 + # 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) + 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) + self._process_pyplot_kwargs(plot_type, 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}]') + 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, + **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'] @@ -1177,8 +1493,9 @@ def _check_cube_dimensions(cube, plot_type): 'timeseries': (['time'],), '1d_profile': (['air_pressure'], ['altitude']), - 'variable_vs_lat': (['latitude'],), - + '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") @@ -1346,6 +1663,10 @@ def create_timeseries_plot(self, datasets): 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_ylabel( f"{multi_dataset_facets[self.cfg['group_variables_by']]} " f"[{multi_dataset_facets['units']}]" @@ -1723,6 +2044,82 @@ def create_1d_profile_plot(self, datasets): provenance_logger.log(plot_path, provenance_record) provenance_logger.log(netcdf_path, provenance_record) + def create_hovmoeller_z_vs_time_plot(self, datasets): + """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) + 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 plot 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 plot 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 " + "spatiotemporal mean.") + + # 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 (var_key, datasets) in self.grouped_input_data.items(): @@ -1733,6 +2130,7 @@ def compute(self): self.create_zonal_mean_profile_plot(datasets) self.create_1d_profile_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 b75d84df3e..3db01e6a4a 100644 --- a/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml +++ b/esmvaltool/recipes/monitor/recipe_monitor_with_refs.yml @@ -8,14 +8,17 @@ documentation: authors: - schlund_manuel - winterstein_franziska + - sarauer_ellen + - kuehbacher_birgit + - heuer_helge maintainer: - schlund_manuel 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: 'Reference (MPI-ESM1-2-LR historical)', reference_for_monitor_diags: true} preprocessors: @@ -71,10 +74,23 @@ preprocessors: operator: mean regrid: target_grid: 2x2 - scheme: - reference: esmf_regrid.schemes:ESMFAreaWeighted + 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: alt16} + scheme: linear + coordinate: altitude + regrid: + target_grid: 2x2 + scheme: linear + area_statistics: + operator: mean diagnostics: @@ -96,9 +112,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: @@ -117,9 +133,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 @@ -173,9 +189,9 @@ 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 plot_variable_vs_latitude: @@ -187,6 +203,27 @@ diagnostics: 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 + mip: Amon + timerange: '2000/2004' + scripts: + plot: + <<: *plot_multi_dataset_default + script: monitor/multi_datasets.py + plots: + hovmoeller_z_vs_time: + plot_func: contourf + common_cbar: true + time_format: '%Y' + log_y: false + pyplot_kwargs: + ylim: [0, 20000]