Skip to content

Commit 31f3d60

Browse files
committed
Implement Wheeler-Kiladis diagnostic
1 parent aedc108 commit 31f3d60

15 files changed

Lines changed: 1377 additions & 0 deletions

.zenodo.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@
260260
"affiliation": "Met Office, UK",
261261
"name": "Little, Bill"
262262
},
263+
{
264+
"affiliation": "DLR, Germany",
265+
"name": "Londoño Castillo, Santiago",
266+
"orcid": "0000-0002-8437-5031"
267+
},
263268
{
264269
"affiliation": "BSC, Spain",
265270
"name": "Loosveldt-Tomas, Saskia"

CITATION.cff

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,11 @@ authors:
452452
affiliation: "Met Office, UK"
453453
family-names: Ellis
454454
given-names: Hannah
455+
-
456+
affiliation: "DLR, Germany"
457+
family-names: Londoño Castillo
458+
given-names: Santiago
459+
orcid: "https://orcid.org/0000-0002-8437-5031"
455460

456461

457462
cff-version: 1.2.0
309 KB
Loading
323 KB
Loading
313 KB
Loading

doc/sphinx/source/recipes/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Atmosphere
6666
recipe_aod_aeronet_assess
6767
recipe_surface_trace_gas
6868
recipe_weathertyping
69+
recipe_wheeler_kiladis_diagnostic
6970

7071
Climate metrics
7172
^^^^^^^^^^^^^^^
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
.. _recipe_wheeler_kiladis_diagnostic:
2+
3+
Wheeler-Kiladis tropical wave spectra
4+
=====================================
5+
6+
Overview
7+
--------
8+
9+
This recipe computes normalized Wheeler-Kiladis wavenumber-frequency
10+
spectra for tropical precipitation and outgoing longwave radiation.
11+
12+
The diagnostic is intended to evaluate tropical wave variability,
13+
including Kelvin waves, equatorial Rossby waves, and broad intraseasonal
14+
variability such as the Madden-Julian Oscillation.
15+
16+
The recipe uses standard ESMValTool preprocessors to convert units,
17+
compute daily means, extract the tropical belt between 15S and 15N, and
18+
regrid the data to a regular latitude-longitude grid. The diagnostic then
19+
computes a cosine-latitude-weighted equatorial mean, removes leap days,
20+
removes the time mean, linear trend, and annual harmonics, optionally
21+
removes the zonal mean, computes two-dimensional Fourier spectra in time
22+
and longitude, estimates a smoothed background spectrum, and plots
23+
normalized spectra with optional shallow-water dispersion curves.
24+
25+
The diagnostic saves raw, background, and normalized Wheeler-Kiladis
26+
spectra as NetCDF files and records provenance information for both data
27+
outputs and figures.
28+
29+
Available recipes and diagnostics
30+
---------------------------------
31+
32+
Recipes are stored in ``esmvaltool/recipes/``:
33+
34+
* ``recipe_wheeler_kiladis_diagnostic.yml``
35+
36+
Diagnostics are stored in ``esmvaltool/diag_scripts/wheeler_kiladis/``:
37+
38+
* ``wheeler_kiladis.py``: ESMValTool wrapper for running the
39+
Wheeler-Kiladis diagnostic.
40+
* ``spectra.py``: utilities for preparing equatorial fields and
41+
computing wavenumber-frequency spectra.
42+
* ``plot.py``: plotting utilities for normalized Wheeler-Kiladis
43+
spectra and theoretical dispersion curves.
44+
45+
User settings in recipe
46+
-----------------------
47+
48+
#. Script ``wheeler_kiladis/wheeler_kiladis.py``
49+
50+
*Optional settings for script*
51+
52+
* ``annual_harmonics``: Number of annual harmonics removed during
53+
seasonal-cycle correction. Default: 3.
54+
55+
* ``remove_zonal_mean``: If ``True``, remove the zonal mean before
56+
the wavenumber-frequency transform. This emphasizes propagating
57+
equatorial disturbances. Default: ``True``.
58+
59+
* ``segment_length``: Length of each spectral segment in days.
60+
Default: 180.
61+
62+
* ``segment_overlap``: Overlap between consecutive spectral segments
63+
in days. Default: 90.
64+
65+
* ``sampling_frequency_per_day``: Sampling frequency of the input
66+
data in samples per day. For daily data, this should be 1.0.
67+
Default: 1.0.
68+
69+
* ``sigma_freq``: Gaussian smoothing width in the frequency direction
70+
used to estimate the background spectrum. Default: 4.0.
71+
72+
* ``sigma_wn``: Gaussian smoothing width in the zonal-wavenumber
73+
direction used to estimate the background spectrum. Default: 4.0.
74+
75+
* ``max_wavenumber``: Maximum zonal wavenumber shown in the plot.
76+
Default: 15.
77+
78+
* ``max_frequency``: Maximum frequency shown in the plot in cycles
79+
per day. Default: 0.5.
80+
81+
* ``period_ticks``: Periods in days shown as labels on the frequency
82+
axis. Default: ``[2, 3, 5, 10, 20, 30, 60, 100]``.
83+
84+
* ``equivalent_depths``: Equivalent depths in metres used to draw
85+
theoretical shallow-water dispersion curves. Default:
86+
``[8, 12, 25, 50]``.
87+
88+
* ``show_dispersion``: If ``True``, overlay theoretical Kelvin and
89+
equatorial Rossby wave dispersion curves. Default: ``True``.
90+
91+
* ``mask_zero_wavenumber``: If ``True``, mask the
92+
zonal-wavenumber-zero column in the normalized spectrum plot. This
93+
is useful when the zonal mean has been removed. Default: ``True``.
94+
95+
Variables
96+
---------
97+
98+
* pr (atmos, daily, longitude, latitude, time)
99+
* rlut (atmos, daily, longitude, latitude, time)
100+
101+
Example plots
102+
-------------
103+
104+
.. _wk_era5_pr_example:
105+
.. figure:: /recipes/figures/wheeler_kiladis/ERA5_pr_wk_normalized_example.png
106+
:align: center
107+
108+
Normalized Wheeler-Kiladis spectrum for ERA5 precipitation.
109+
110+
.. _wk_persiann_cdr_pr_example:
111+
.. figure:: /recipes/figures/wheeler_kiladis/PERSIANN-CDR_pr_wk_normalized_example.png
112+
:align: center
113+
114+
Normalized Wheeler-Kiladis spectrum for PERSIANN-CDR precipitation.
115+
116+
.. _wk_mpi_esm1_2_lr_rlut_example:
117+
.. figure:: /recipes/figures/wheeler_kiladis/MPI-ESM1-2-LR_rlut_wk_normalized_example.png
118+
:align: center
119+
120+
Normalized Wheeler-Kiladis spectrum for MPI-ESM1-2-LR outgoing
121+
longwave radiation.
122+
123+
References
124+
----------
125+
126+
* Wheeler, M. C. and Kiladis, G. N. (1999): Convectively Coupled
127+
Equatorial Waves: Analysis of Clouds and Temperature in the
128+
Wavenumber-Frequency Domain.
129+
* Kiladis, G. N. et al. (2009): Convectively Coupled Equatorial Waves.
130+
* Hannah et al. (2020): Evaluation of tropical variability using
131+
Wheeler-Kiladis-type spectra.

esmvaltool/config-references.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,11 @@ authors:
402402
name: Lledo, Llorenc
403403
institute: BSC, Spain
404404
orcid:
405+
londono-castillo_santiago:
406+
name: Londono Castillo, Santiago
407+
institute: DLR, Germany
408+
orcid: https://orcid.org/0000-0002-8437-5031
409+
github: ApexCapybara
405410
loosveldt-tomas_saskia:
406411
name: Loosveldt-Tomas, Saskia
407412
institute: BSC, Spain
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Wheeler-Kiladis diagnostic."""
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Plot utilities for Wheeler-Kiladis diagnostic."""
2+
3+
import matplotlib.pyplot as plt
4+
import numpy as np
5+
6+
7+
def kelvin_curve(wavenumber, depth_m):
8+
"""
9+
Kelvin wave dispersion curve in cycles per day.
10+
11+
Parameters
12+
----------
13+
wavenumber : array-like
14+
Dimensionless zonal wavenumber.
15+
depth_m : float
16+
Equivalent depth [m].
17+
"""
18+
gravity = 9.80665 # m s^-2
19+
earth_radius = 6.371e6 # m
20+
21+
phase_speed = np.sqrt(gravity * depth_m) # m s^-1
22+
zonal_wavenumber = np.asarray(wavenumber) / earth_radius # m^-1
23+
24+
angular_frequency = phase_speed * zonal_wavenumber # rad s^-1
25+
frequency = angular_frequency / (2.0 * np.pi) * 86400.0 # cycles day^-1
26+
27+
return frequency
28+
29+
30+
def equatorial_rossby_curve(wavenumber, depth_m, meridional_mode=1):
31+
"""
32+
Compute the dispersion relation for equatorial Rossby waves.
33+
34+
Parameters
35+
----------
36+
wavenumber : array-like
37+
Dimensionless zonal wavenumber.
38+
depth_m : float
39+
Equivalent depth [m].
40+
meridional_mode : int, optional
41+
Meridional mode number of the equatorial Rossby wave.
42+
Default is 1.
43+
44+
Returns
45+
-------
46+
numpy.ndarray
47+
Wave frequency [cycles day^-1] corresponding to the input
48+
zonal wavenumbers.
49+
"""
50+
earth_rotation = 7.2921159e-5 # rad s^-1
51+
earth_radius = 6.371e6 # m
52+
gravity = 9.80665 # m s^-2
53+
54+
beta = 2.0 * earth_rotation / earth_radius # m^-1 s^-1
55+
phase_speed = np.sqrt(gravity * depth_m) # m s^-1
56+
57+
wavenumber = np.asarray(wavenumber) # dimensionless zonal wavenumber
58+
zonal_wavenumber = wavenumber / earth_radius # m^-1
59+
60+
denominator = (
61+
zonal_wavenumber**2
62+
+ (2 * meridional_mode + 1) * beta / phase_speed # m^-2
63+
)
64+
65+
angular_frequency = -beta * zonal_wavenumber / denominator # rad s^-1
66+
frequency = angular_frequency / (2.0 * np.pi) * 86400.0 # cycles day^-1
67+
68+
return frequency
69+
70+
71+
def plot_wk_spectrum(
72+
spectrum,
73+
title,
74+
output_file,
75+
max_wn=15,
76+
max_freq=0.5,
77+
levels=None,
78+
period_ticks=(2, 3, 5, 10, 20, 30, 60, 100),
79+
depths=(8, 12, 25, 50),
80+
show_dispersion=True,
81+
caption=None,
82+
):
83+
"""Plot normalized Wheeler-Kiladis spectrum."""
84+
if levels is None:
85+
levels = np.linspace(0.8, 4.0, 12)
86+
87+
plot_data = spectrum.sel(
88+
wavenumber=slice(-max_wn, max_wn),
89+
frequency=slice(0, max_freq),
90+
)
91+
92+
fig, ax = plt.subplots(figsize=(12, 11.0))
93+
94+
plot_data.plot.contourf(
95+
ax=ax,
96+
x="wavenumber",
97+
y="frequency",
98+
levels=levels,
99+
cmap="Spectral_r",
100+
extend="both",
101+
cbar_kwargs={"label": "normalized power"},
102+
)
103+
104+
ax.axvline(0, color="k", linewidth=0.8)
105+
106+
period_ticks = np.asarray(period_ticks)
107+
freq_ticks = 1.0 / period_ticks
108+
valid_ticks = (freq_ticks > 0) & (freq_ticks <= max_freq)
109+
110+
ax.set_yticks(freq_ticks[valid_ticks])
111+
ax.set_yticklabels([f"{period:g}" for period in period_ticks[valid_ticks]])
112+
113+
ax.set_ylabel("Frequency [cycles day$^{-1}$]")
114+
ax.set_xlabel("Zonal wavenumber")
115+
116+
ax.set_xlim(-max_wn, max_wn)
117+
ax.set_ylim(0, max_freq)
118+
119+
ax.set_title(title)
120+
121+
for period in [30, 100]:
122+
frequency = 1.0 / period
123+
124+
if frequency <= max_freq:
125+
ax.axhline(
126+
frequency,
127+
color="k",
128+
linestyle="--",
129+
linewidth=0.8,
130+
alpha=0.7,
131+
)
132+
133+
if show_dispersion:
134+
kelvin_wn = np.linspace(0.05, max_wn, 800)
135+
rossby_wn = -np.linspace(0.05, max_wn, 800)
136+
137+
for depth in depths:
138+
kelvin_freq = kelvin_curve(kelvin_wn, depth)
139+
rossby_freq = np.abs(
140+
equatorial_rossby_curve(
141+
rossby_wn,
142+
depth,
143+
meridional_mode=1,
144+
)
145+
)
146+
147+
kelvin_valid = (
148+
np.isfinite(kelvin_freq)
149+
& (kelvin_freq >= 0)
150+
& (kelvin_freq <= max_freq)
151+
)
152+
153+
rossby_valid = (
154+
np.isfinite(rossby_freq)
155+
& (rossby_freq >= 0)
156+
& (rossby_freq <= max_freq)
157+
)
158+
159+
ax.plot(
160+
kelvin_wn[kelvin_valid],
161+
kelvin_freq[kelvin_valid],
162+
color="k",
163+
linewidth=1.0,
164+
)
165+
166+
ax.plot(
167+
rossby_wn[rossby_valid],
168+
rossby_freq[rossby_valid],
169+
color="k",
170+
linewidth=1.0,
171+
)
172+
if caption is not None:
173+
fig.text(
174+
0.5,
175+
0.01,
176+
caption,
177+
ha="center",
178+
va="bottom",
179+
fontsize=8,
180+
wrap=True,
181+
)
182+
output_file = str(output_file)
183+
fig.savefig(output_file, bbox_inches="tight", dpi=150)
184+
plt.close(fig)
185+
186+
return output_file

0 commit comments

Comments
 (0)