Skip to content

Commit f9781fd

Browse files
JurijSchoenfeldschlunmaLisaBock
authored
Add Hovmoeller MJO diagnostic (#4564)
Co-authored-by: Manuel Schlund <manuel.schlund@dlr.de> Co-authored-by: Manuel Schlund <32543114+schlunma@users.noreply.github.com> Co-authored-by: Lisa Bock <lisa.bock@dlr.de>
1 parent 7509b11 commit f9781fd

16 files changed

Lines changed: 777 additions & 6 deletions

File tree

147 KB
Loading

doc/sphinx/source/recipes/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Atmosphere
5151
recipe_iht_toa
5252
recipe_impact
5353
recipe_lifetime
54+
recipe_mjo_hovmoeller
5455
recipe_modes_of_variability
5556
recipe_mpqb_xch4
5657
recipe_quantilebias
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
.. _recipes_mjo_hovmoeller:
2+
3+
Madden-Julian Oscillation precipitation Hovmöller diagnostic
4+
==============================================================
5+
6+
Overview
7+
--------
8+
9+
This recipe computes lag-regression Hovmöller diagrams of the
10+
Madden-Julian Oscillation (MJO). For each dataset, daily tropical
11+
precipitation is averaged over a latitude band and its day-of-year
12+
climatology is removed. The resulting anomalies are Lanczos band-pass
13+
filtered to the 20-100 day MJO period range. A reference index is built
14+
by averaging the filtered field over a reference longitude sector, and
15+
the full filtered field is then regressed against this index at a range
16+
of lags.
17+
18+
The result is a longitude-lag diagram: an eastward-propagating diagonal
19+
band is the signature of MJO convection.
20+
21+
22+
Available recipes and diagnostics
23+
----------------------------------
24+
25+
Recipes are stored in recipes/
26+
27+
* recipe_mjo_hovmoeller.yml
28+
29+
Diagnostics are stored in diag_scripts/mjo/
30+
31+
* mjo_hovmoeller.py: compute the lag regression and plot the Hovmöller
32+
diagram.
33+
34+
35+
User settings in recipe
36+
------------------------
37+
38+
#. Script mjo_hovmoeller.py
39+
40+
*Required settings for script*
41+
42+
* ``reference_longitudes``: longitude sector ``[lon0, lon1]`` (in
43+
degrees East) used to build the MJO reference index that the
44+
filtered field is regressed against.
45+
* ``low_period``: lower period cutoff (in days) of the Lanczos
46+
band-pass filter.
47+
* ``high_period``: upper period cutoff (in days) of the Lanczos
48+
band-pass filter.
49+
* ``lanczos_weights``: number of weights of the Lanczos band-pass
50+
filter. Must be an odd integer greater than 1.
51+
* ``max_lag``: maximum lag (in days, in both directions) computed by
52+
the lag regression.
53+
54+
*Optional settings for script*
55+
56+
* ``longitude_limits``: longitude axis limits of the Hovmöller plot
57+
(default: ``[0.0, 360.0]``).
58+
* ``contour_levels``: number of contour levels in the Hovmöller plot.
59+
Must be at least 3 (default: ``21``).
60+
* ``colormap``: matplotlib colormap used for the Hovmöller contour
61+
plot (default: ``RdYlBu``).
62+
* ``plot_title``: title of the Hovmöller plot (default: ``MJO
63+
Hovmöller diagram``).
64+
* ``colorbar_label``: label for the figure's colorbar (default:
65+
``Precipitation regression coefficient``).
66+
67+
*Required settings for variables*
68+
69+
* none beyond the standard ``short_name``, ``mip``, ``preprocessor``
70+
and ``timerange``.
71+
72+
*Optional settings for variables*
73+
74+
* none
75+
76+
*Required settings for preprocessor*
77+
78+
* ``extract_region``: restrict the data to the tropical latitude
79+
band used for the diagnostic.
80+
* ``regrid``: regrid all datasets onto a common regular grid.
81+
* ``meridional_statistics``: average over the extracted latitude
82+
band (``operator: mean``).
83+
* ``daily_statistics``: reduce the data to daily means
84+
(``operator: mean``).
85+
* ``anomalies``: remove the day-of-year climatology
86+
(``period: day``).
87+
* ``convert_units``: convert precipitation to ``kg m-2 day-1``.
88+
89+
*Optional settings for preprocessor*
90+
91+
* none
92+
93+
*Color tables*
94+
95+
* none
96+
97+
98+
Variables
99+
---------
100+
101+
* pr (atmos, daily mean, longitude latitude time)
102+
103+
104+
Observations and reformat scripts
105+
----------------------------------
106+
107+
*Note: ERA5 is read directly through ESMValCore's native6 support; no
108+
separate reformat script needs to be run beforehand.*
109+
110+
* ERA5 (native6 project, tier 3, ``frequency: 1hr``)
111+
112+
113+
References
114+
----------
115+
116+
* Hannah, W. M., Jones, C. R., Hillman, B. R., Norman, M. R., Bader, D. C., Taylor, M. A., et al. (2020).
117+
Initial results from the super-parameterized E3SM. Journal of Advances in Modeling Earth Systems. 12,
118+
e2019MS001863. https://doi.org/10.1029/2019MS001863
119+
120+
121+
Example plots
122+
-------------
123+
124+
.. _fig_mjo_hovmoeller_1:
125+
.. figure:: /recipes/figures/mjo/era5_mjo_hovmoeller.png
126+
:align: center
127+
128+
Lag regression of 20-100 day filtered ERA5 precipitation against a
129+
precipitation index averaged over 80-100E, 1979-1983. Positive
130+
longitude-lag slope through the reference sector shows the
131+
eastward-propagating MJO precipitation signal.

esmvaltool/cmorizers/data/formatters/datasets/esacci_ozone.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@
8383

8484

8585
def _convert_units(
86-
cubes: iris.cube.CubeList, short_name: str, var: dict
86+
cubes: iris.cube.CubeList,
87+
short_name: str,
88+
var: dict,
8789
) -> iris.cube.Cube:
8890
"""Perform variable-specific conversion of units.
8991
@@ -187,7 +189,7 @@ def _extract_variable(in_files, var, cfg, out_dir, year, month):
187189
[
188190
timecoord.units.date2num(start_date),
189191
timecoord.units.date2num(end_date),
190-
]
192+
],
191193
)
192194

193195
# Add longitude coordinate to cube only for o3_sage_omps.

esmvaltool/config-references.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,10 @@ authors:
550550
institute: DLR, Germany
551551
orcid:
552552
github: ellensarauer
553+
schoenfeld_jurij:
554+
name: Schönfeld, Jurij
555+
institute: DLR, Germany
556+
orcid: https://orcid.org/0009-0000-7453-9517
553557
schulze_kirsten:
554558
name: Schulze, Kirsten
555559
institute: Uni Bremen, Germany
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Diagnostics for evaluating the Madden-Julian Oscillation."""
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Lanczos filtering utilities for MJO diagnostics."""
2+
3+
import numpy as np
4+
5+
6+
def lanczos_weights(low_period, high_period, weights):
7+
"""Return Lanczos band-pass weights for daily data."""
8+
if weights < 3 or weights % 2 == 0:
9+
msg = "weights must be an odd integer greater than 1"
10+
raise ValueError(msg)
11+
if low_period <= 0 or high_period <= low_period:
12+
msg = "Expected 0 < low_period < high_period, e.g. 20 and 100 days"
13+
raise ValueError(msg)
14+
15+
half_width = weights // 2
16+
n = np.arange(-half_width, half_width + 1, dtype=float)
17+
18+
# Cutoff frequencies in cycles per day.
19+
low_frequency = 1.0 / high_period
20+
high_frequency = 1.0 / low_period
21+
22+
coefficients = np.empty(weights, dtype=float)
23+
coefficients[half_width] = 2.0 * (high_frequency - low_frequency)
24+
25+
nonzero = n != 0
26+
coefficients[nonzero] = (
27+
np.sin(2.0 * np.pi * high_frequency * n[nonzero])
28+
- np.sin(2.0 * np.pi * low_frequency * n[nonzero])
29+
) / (np.pi * n[nonzero])
30+
31+
# Lanczos sigma window.
32+
coefficients *= np.sinc(n / (half_width + 1.0))
33+
34+
# Do not normalize by coefficients.sum(): a band-pass filter has
35+
# approximately zero response at zero frequency.
36+
return coefficients
37+
38+
39+
def _filter_series(series, coefficients):
40+
"""Filter one time series and mask invalid endpoints."""
41+
series = np.ma.asarray(series, dtype=float)
42+
values = series.filled(np.nan)
43+
44+
valid = np.isfinite(values)
45+
numerator = np.convolve(
46+
np.where(valid, values, 0.0),
47+
coefficients,
48+
mode="same",
49+
)
50+
51+
# Require every value in the filter window to be valid.
52+
valid_count = np.convolve(
53+
valid.astype(int),
54+
np.ones(coefficients.size, dtype=int),
55+
mode="same",
56+
)
57+
output = np.ma.masked_where(valid_count < coefficients.size, numerator)
58+
59+
half_width = coefficients.size // 2
60+
output[:half_width] = np.ma.masked
61+
output[-half_width:] = np.ma.masked
62+
return output
63+
64+
65+
def lanczos_bandpass(
66+
cube,
67+
low_period=20,
68+
high_period=100,
69+
weights=91,
70+
):
71+
"""Apply a Lanczos band-pass filter along the cube time dimension."""
72+
if not cube.coords("time"):
73+
msg = "Input cube has no time coordinate"
74+
raise ValueError(msg)
75+
76+
time_axis = cube.coord_dims("time")[0]
77+
coefficients = lanczos_weights(low_period, high_period, weights)
78+
79+
data = np.moveaxis(np.ma.asarray(cube.data), time_axis, 0)
80+
flattened = data.reshape(data.shape[0], -1)
81+
82+
filtered = np.ma.empty(flattened.shape, dtype=float)
83+
for column in range(flattened.shape[1]):
84+
filtered[:, column] = _filter_series(
85+
flattened[:, column],
86+
coefficients,
87+
)
88+
89+
filtered = filtered.reshape(data.shape)
90+
filtered = np.moveaxis(filtered, 0, time_axis)
91+
92+
result = cube.copy(data=filtered)
93+
result.long_name = (
94+
f"{cube.name()} {low_period}-{high_period} day filtered anomalies"
95+
)
96+
result.attributes["temporal_filter"] = (
97+
f"Lanczos band-pass; periods={low_period}-{high_period} days; "
98+
f"weights={weights}"
99+
)
100+
return result

0 commit comments

Comments
 (0)