Skip to content

Commit 818569f

Browse files
LisaBockschlunmaaxel-lauer
authored
Adding figures of Bock and Lauer (2024) (#3526)
Co-authored-by: Manuel Schlund <[email protected]> Co-authored-by: Axel Lauer <[email protected]> Co-authored-by: Manuel Schlund <[email protected]>
1 parent e4f9ec2 commit 818569f

12 files changed

+3173
-0
lines changed
53.1 KB
Loading
450 KB
Loading
236 KB
Loading

doc/sphinx/source/recipes/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ Future projections
9494
recipe_tcr
9595
recipe_tebaldi21esd
9696
recipe_climate_change_hotspot
97+
recipe_bock24acp
9798

9899
IPCC
99100
^^^^
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
.. _recipes_bock24acp:
2+
3+
Cloud properties and their projected changes in CMIP models with low to high climate sensitivity
4+
================================================================================================
5+
6+
Overview
7+
--------
8+
9+
The recipes recipe_bock24acp_*.yml reproduce figures (Fig. 3, 4, 6 and 7) from the publication `Bock and Lauer, 2024`_ investigating cloud properties and their projected changes in CMIP models with low to high climate sensitivity.
10+
11+
.. _`Bock and Lauer, 2024`: https://doi.org/10.5194/acp-24-1587-2024
12+
13+
Available recipes and diagnostics
14+
---------------------------------
15+
16+
Recipes are stored in recipes/clouds
17+
18+
* recipe_bock24acp_fig3-4_maps.yml
19+
* recipe_bock24acp_fig6_zonal.yml
20+
* recipe_bock24acp_fig7_boxplots.yml
21+
22+
Diagnostics are stored in diag_scripts/
23+
24+
Fig. 3 and 4:
25+
26+
* clouds/clouds_ecs_groups_maps.py: Geographical maps of the multi-year annual means for group means of historical CMIP simulations from all three ECS groups.
27+
28+
Fig. 6:
29+
30+
* clouds/clouds_ecs_groups_zonal.py: Zonally averaged group means.
31+
32+
Fig. 7:
33+
34+
* clouds/clouds_ecs_groups_boxplots.py: Boxplots of relative changes for all groups.
35+
36+
37+
User settings in recipe
38+
-----------------------
39+
40+
#. Script clouds_ecs_groups_maps.py
41+
42+
*Required settings (scripts)*
43+
44+
reference: if true, a reference dataset is given within 'variable_group' equal 'OBS'
45+
46+
*Optional settings (scripts)*
47+
48+
plot_each_model: one figure for each single model
49+
50+
51+
#. Script clouds/clouds_ecs_groups_zonal.py
52+
53+
*Required settings (scripts)*
54+
55+
group_by: list of 'variable_group's to have the order
56+
plot_type: 'zonal' and 'height' plots are available
57+
58+
*Optional settings (scripts)*
59+
60+
filename_attach: attachment to the output files
61+
62+
63+
#. Script clouds/clouds_ecs_groups_boxplots.py
64+
65+
*Required settings (scripts)*
66+
67+
exclude_datasets: list of datasets which are not used for the statistics, default is ['MultiModelMean', 'MultiModelP5', 'MultiModelP95']
68+
group_by: list of 'variable_group's to have the order
69+
plot_type: 'zonal' and 'height' plots are available
70+
71+
*Optional settings (scripts)*
72+
73+
filename_attach: attachment to the output files
74+
title: set title of figure
75+
y_range: set range of the y-axes
76+
77+
78+
Variables
79+
---------
80+
81+
* clt (atmos, monthly, longitude latitude time)
82+
* clivi (atmos, monthly, longitude latitude time)
83+
* clwvi (atmos, monthly, longitude latitude time)
84+
* rlut (atmos, monthly, longitude latitude time)
85+
* rsut (atmos, monthly, longitude latitude time)
86+
* rlutcs (atmos, monthly, longitude latitude time)
87+
* rsutcs (atmos, monthly, longitude latitude time)
88+
* tas (atmos, monthly, longitude latitude time)
89+
90+
91+
Observations and reformat scripts
92+
---------------------------------
93+
94+
* CERES-EBAF (Ed4.2) - TOA radiation fluxes (used for calculation of
95+
the cloud radiative effects)
96+
97+
*Reformat script:* cmorizers/data/formatters/datasets/ceres_ebaf.py
98+
99+
100+
References
101+
----------
102+
103+
* Bock, L. and Lauer, A.: Cloud properties and their projected changes in CMIP
104+
models with low to high climate sensitivity, Atmos. Chem. Phys., 24, 1587–1605,
105+
https://doi.org/10.5194/acp-24-1587-2024, 2024.
106+
107+
108+
Example plots
109+
-------------
110+
111+
.. _fig_bock24acp_1:
112+
.. figure:: /recipes/figures/bock24acp/map_netcre.png
113+
:align: center
114+
115+
Geographical map of the multi-year annual mean net cloud radiative effect from
116+
(a) CERES–EBAF Ed4.2 (OBS) and (b–d) group means of historical CMIP simulations
117+
from all three ECS groups (Fig. 4).
118+
119+
.. _fig_bock24acp_2:
120+
.. figure:: /recipes/figures/bock24acp/zonal_diff_clt_ssp585.png
121+
:align: center
122+
123+
The upper panel show the zonally averaged group means of total cloud
124+
fraction from historical simulations (solid lines)
125+
and RCP8.5/SSP5-8.5 scenarios (dashed lines) for the three different ECS groups.
126+
Lower panels show the corresponding relative differences of all zonally
127+
averaged group means between the RCP8.5/SSP5-8.5 scenarios and the corresponding
128+
historical simulations. Shading indicates the 5 % and 95 % quantiles of the single
129+
model results (Fig. 6a).
130+
131+
.. _fig_bock24acp_3:
132+
.. figure:: /recipes/figures/bock24acp/boxplot_ssp585_south_oc.png
133+
:align: center
134+
135+
Relative change (calculated as the difference between the scenario value and the
136+
historical value divided by the historical value) of total cloud fraction (clt),
137+
ice water path (iwp), liquid water path (lwp), and net cloud radiative effect
138+
(netcre) per degree of warming averaged over the Southern Ocean (30–65°S). In the
139+
box plot, each box indicates the range from the first
140+
quartile to the third quartile, the vertical line shows the median, and the
141+
whiskers the minimum and maximum values, excluding the outliers. Outliers are
142+
defined as being outside 1.5 times the interquartile range (Fig. 7b).
143+
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
"""Python diagnostic for plotting boxplots."""
2+
import logging
3+
from pathlib import Path
4+
5+
import iris
6+
import matplotlib.pyplot as plt
7+
import pandas as pd
8+
import seaborn as sns
9+
10+
from esmvaltool.diag_scripts.shared import (
11+
ProvenanceLogger,
12+
get_diagnostic_filename,
13+
get_plot_filename,
14+
group_metadata,
15+
run_diagnostic,
16+
select_metadata,
17+
)
18+
19+
logger = logging.getLogger(Path(__file__).stem)
20+
21+
VAR_NAMES = {
22+
'cl': 'cloud_fraction',
23+
'cli': 'ice_water_content',
24+
'clw': 'liquid_water_content',
25+
}
26+
PALETTE = {
27+
'high ECS': 'royalblue',
28+
'med ECS': 'green',
29+
'low ECS': 'orange',
30+
}
31+
32+
33+
def get_provenance_record(ancestor_files):
34+
"""Create a provenance record describing the diagnostic data and plot."""
35+
caption = ("Relative change per degree of warming averaged over the"
36+
"chosen region.")
37+
38+
record = {
39+
'caption': caption,
40+
'statistics': ['mean'],
41+
'domains': ['global'],
42+
'plot_types': ['zonal'],
43+
'authors': [
44+
'bock_lisa',
45+
],
46+
'references': [
47+
'bock24acp',
48+
],
49+
'ancestors': ancestor_files,
50+
}
51+
return record
52+
53+
54+
def read_data(filename):
55+
"""Compute an example diagnostic."""
56+
logger.debug("Loading %s", filename)
57+
cube = iris.load_cube(filename)
58+
59+
if cube.var_name == 'cli':
60+
cube.convert_units('g/kg')
61+
elif cube.var_name == 'clw':
62+
cube.convert_units('g/kg')
63+
64+
cube = iris.util.squeeze(cube)
65+
return cube
66+
67+
68+
def compute_diff(filename1, filename2):
69+
"""Compute difference between two cubes."""
70+
logger.debug("Loading %s", filename1)
71+
cube1 = iris.load_cube(filename1)
72+
cube2 = iris.load_cube(filename2)
73+
74+
if cube1.var_name == 'cli':
75+
cube1.convert_units('g/kg')
76+
cube2.convert_units('g/kg')
77+
elif cube1.var_name == 'clw':
78+
cube1.convert_units('g/kg')
79+
cube2.convert_units('g/kg')
80+
81+
cube = cube2 - cube1
82+
cube.metadata = cube1.metadata
83+
return cube
84+
85+
86+
def compute_diff_temp(input_data, group, var, dataset):
87+
"""Compute relative change per temperture change."""
88+
dataset_name = dataset['dataset']
89+
var = dataset['short_name']
90+
91+
input_file_1 = dataset['filename']
92+
93+
var_data_2 = select_metadata(input_data,
94+
short_name=var,
95+
dataset=dataset_name,
96+
variable_group=var + "_" + group[1])
97+
if not var_data_2:
98+
raise ValueError(
99+
f"No '{var}' data for '{dataset_name}' in '{group[1]}' available")
100+
101+
input_file_2 = var_data_2[0]['filename']
102+
103+
tas_data_1 = select_metadata(input_data,
104+
short_name='tas',
105+
dataset=dataset_name,
106+
variable_group='tas_' + group[0])
107+
tas_data_2 = select_metadata(input_data,
108+
short_name='tas',
109+
dataset=dataset_name,
110+
variable_group='tas_' + group[1])
111+
if not tas_data_1:
112+
raise ValueError(
113+
f"No 'tas' data for '{dataset_name}' in '{group[0]}' available")
114+
if not tas_data_2:
115+
raise ValueError(
116+
f"No 'tas' data for '{dataset_name}' in '{group[1]}' available")
117+
input_file_tas_1 = tas_data_1[0]['filename']
118+
input_file_tas_2 = tas_data_2[0]['filename']
119+
120+
cube = read_data(input_file_1)
121+
122+
cube_diff = compute_diff(input_file_1, input_file_2)
123+
cube_tas_diff = compute_diff(input_file_tas_1, input_file_tas_2)
124+
125+
cube_diff = (100. * (cube_diff / iris.analysis.maths.abs(cube)) /
126+
cube_tas_diff)
127+
128+
return cube_diff
129+
130+
131+
def create_data_frame(input_data, cfg):
132+
"""Create data frame."""
133+
data_frame = pd.DataFrame(columns=['Variable', 'Group', 'Dataset', 'Data'])
134+
135+
ifile = 0
136+
137+
all_vars = group_metadata(input_data, 'short_name')
138+
groups = group_metadata(input_data, 'variable_group', sort='dataset')
139+
140+
for var in all_vars:
141+
if var != 'tas':
142+
logger.info("Processing variable %s", var)
143+
144+
if var == 'clivi':
145+
varname = 'iwp'
146+
else:
147+
varname = var
148+
149+
for group_names in cfg['group_by']:
150+
logger.info("Processing group %s of variable %s",
151+
group_names[0], var)
152+
153+
for dataset in groups[var + "_" + group_names[0]]:
154+
dataset_name = dataset['dataset']
155+
156+
if dataset_name not in cfg['exclude_datasets']:
157+
cube_diff = compute_diff_temp(input_data, group_names,
158+
var, dataset)
159+
160+
group_name = group_names[0].split('_')[1] + " ECS"
161+
162+
data_frame.loc[ifile] = [
163+
varname, group_name, dataset_name, cube_diff.data
164+
]
165+
ifile = ifile + 1
166+
167+
data_frame['Data'] = data_frame['Data'].astype(str).astype(float)
168+
169+
return data_frame
170+
171+
172+
def plot_boxplot(data_frame, input_data, cfg):
173+
"""Create boxplot."""
174+
sns.set_style('darkgrid')
175+
sns.set(font_scale=2)
176+
sns.boxplot(data=data_frame,
177+
x='Variable',
178+
y='Data',
179+
hue='Group',
180+
palette=PALETTE)
181+
plt.ylabel('Relative change (%/K)')
182+
if 'y_range' in cfg:
183+
plt.ylim(cfg.get('y_range'))
184+
plt.title(cfg['title'])
185+
186+
provenance_record = get_provenance_record(
187+
ancestor_files=[d['filename'] for d in input_data])
188+
189+
# Save plot
190+
plot_path = get_plot_filename('boxplot' + '_' + cfg['filename_attach'],
191+
cfg)
192+
plt.savefig(plot_path)
193+
logger.info("Wrote %s", plot_path)
194+
plt.close()
195+
196+
with ProvenanceLogger(cfg) as provenance_logger:
197+
provenance_logger.log(plot_path, provenance_record)
198+
199+
200+
def main(cfg):
201+
"""Run diagnostic."""
202+
cfg.setdefault('exclude_datasets',
203+
['MultiModelMean', 'MultiModelP5', 'MultiModelP95'])
204+
cfg.setdefault('title', 'Test')
205+
206+
plt.figure(constrained_layout=True, figsize=(12, 8))
207+
208+
# Get input data
209+
input_data = list(cfg['input_data'].values())
210+
211+
# Create data frame
212+
data_frame = create_data_frame(input_data, cfg)
213+
214+
# Create plot
215+
plot_boxplot(data_frame, input_data, cfg)
216+
217+
# Save file
218+
basename = "boxplot_region_" + cfg['filename_attach']
219+
csv_path = get_diagnostic_filename(basename, cfg).replace('.nc', '.csv')
220+
data_frame.to_csv(csv_path)
221+
logger.info("Wrote %s", csv_path)
222+
223+
224+
if __name__ == '__main__':
225+
226+
with run_diagnostic() as config:
227+
main(config)

0 commit comments

Comments
 (0)