KF filter and Hovmöller Diagram

KF filter and Hovmöller Diagram#

The KF filter is a specialized spectral analysis technique designed to isolate and study tropical atmospheric waves by decomposing meteorological fields into their wavenumber-frequency components. Unlike traditional Fourier methods, the W-K filter applies a symmetric/antisymmetric separation to distinguish between different wave types, such as Kelvin waves, Rossby waves, and mixed Rossby-gravity waves, based on their theoretical dispersion relations. This approach is particularly effective in identifying convectively coupled waves, where tropical rainfall and large-scale circulation interact. In meteorology, the W-K filter is widely used to analyze Madden-Julian Oscillation (MJO) dynamics, monsoon variability, and other tropical wave disturbances, providing insights into their propagation characteristics and impacts on weather systems.

See also

Wheeler, M., & Kiladis, G. N. (1999). Convectively Coupled Equatorial Waves: Analysis of Clouds and Temperature in the Wavenumber–Frequency Domain. Journal of the Atmospheric Sciences, 56(3), 374-399. https://journals.ametsoc.org/view/journals/atsc/56/3/1520-0469_1999_056_0374_ccewao_2.0.co_2.xml

Before proceeding with all the steps, first import some necessary libraries and packages

import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import easyclimate as ecl

Preprocessed data

The example here is to avoid longer calculations, thus we open the pre-processed result data directly.

Tip

You can download following datasets here:

lats, latn = -20, 20

olr_data = xr.open_dataset('olr-daily_v01r02_19800101_20231231.nc', chunks='auto').sel(lat=slice(lats,latn)).olr
olr_daily_smoothed = ecl.variability.remove_smooth_daily_annual_cycle_mean(
    olr_data, extract_time_range = slice('2017-01-01','2018-12-31')
)

# target grid
target_grid = xr.Dataset()
target_grid['lat'] = olr_daily_smoothed.lat.data
target_grid['lon'] = olr_daily_smoothed.lon.thin(lon = 5).data

olr_data_interpolated = ecl.interp.interp_mesh2mesh(olr_daily_smoothed, target_grid = target_grid)
olr_data_interpolated = ecl.utility.get_compress_xarraydata(olr_data_interpolated)
olr_data_interpolated.to_netcdf("olr_smooth_data.nc")
<xarray.DataArray 'olr' (time: 730, lat: 40, lon: 72)> Size: 8MB
[2102400 values with dtype=float32]
Coordinates:
  * time       (time) datetime64[ns] 6kB 2017-01-01T12:00:00 ... 2018-12-31T1...
    dayofyear  (time) int64 6kB ...
  * lon        (lon) float32 288B 0.5 5.5 10.5 15.5 ... 340.5 345.5 350.5 355.5
  * lat        (lat) float32 160B -19.5 -18.5 -17.5 -16.5 ... 17.5 18.5 19.5
Attributes:
    standard_name:  toa_outgoing_longwave_flux
    long_name:      NOAA Climate Data Record of Daily Mean Upward Longwave Fl...
    units:          W m-2
    cell_methods:   time: mean area: mean


Filtering equatorial waves with kf_filter

lf_result = ecl.filter.kf_filter_lf_wave(olr_data_interpolated, steps_per_day = 1)
mjo_result = ecl.filter.kf_filter_mjo_wave(olr_data_interpolated, steps_per_day = 1)
er_result = ecl.filter.kf_filter_er_wave(olr_data_interpolated, steps_per_day = 1)
kelvin_result = ecl.filter.kf_filter_kelvin_wave(olr_data_interpolated, steps_per_day = 1)
mt_result = ecl.filter.kf_filter_mt_wave(olr_data_interpolated, steps_per_day = 1)
mrg_result = ecl.filter.kf_filter_mrg_wave(olr_data_interpolated, steps_per_day = 1)
td_result = ecl.filter.kf_filter_td_wave(olr_data_interpolated, steps_per_day = 1)
lf_result
/home/runner/work/easyclimate/easyclimate/src/easyclimate/filter/kf_filter.py:207: RuntimeWarning: invalid value encountered in sqrt
  freq = (beta * c) ** 0.5
/home/runner/work/easyclimate/easyclimate/src/easyclimate/filter/kf_filter.py:210: RuntimeWarning: invalid value encountered in sqrt
  freq = k * c * (0.5 + 0.5 * (1 + 4 * beta / (k**2 * c)) ** 0.5)
/home/runner/work/easyclimate/easyclimate/src/easyclimate/filter/kf_filter.py:212: RuntimeWarning: invalid value encountered in sqrt
  freq = k * c * (0.5 - 0.5 * (1 + 4 * beta / (k**2 * c)) ** 0.5)
<xarray.DataArray 'olr' (lat: 40, time: 730, lon: 72)> Size: 17MB
array([[[-2.32703094, -1.08666594, -2.40137572, ..., -2.88243589,
         -4.13795167, -3.8134417 ],
        [-2.44853239, -1.2718925 , -2.66297486, ..., -2.70843669,
         -3.98720919, -3.78270101],
        [-2.565229  , -1.45492335, -2.92488144, ..., -2.53113294,
         -3.8338216 , -3.74859289],
        ...,
        [-1.93592457, -0.52137343, -1.62279677, ..., -3.38149611,
         -4.57130514, -3.88459657],
        [-2.0704913 , -0.71104366, -1.88085224, ..., -3.21926823,
         -4.43029342, -3.86446764],
        [-2.20094079, -0.89959644, -2.1405228 , ..., -3.0528154 ,
         -4.2857455 , -3.84072468]],

       [[-2.85782587, -1.80721499, -2.43663707, ..., -3.89285024,
         -4.22983105, -3.13060803],
        [-3.00070127, -2.02241915, -2.74226962, ..., -3.70570675,
         -4.10835948, -3.14111651],
        [-3.13839696, -2.23468586, -3.04888902, ..., -3.51420666,
         -3.98473684, -3.1493203 ],
...
        [ 3.39455852, -1.33271312, -6.04567993, ...,  8.37708092,
          5.79219709,  2.71448836],
        [ 3.45290232, -1.43679532, -5.97934496, ...,  8.61588977,
          5.90021046,  2.77505596],
        [ 3.51140973, -1.53658624, -5.90499781, ...,  8.84323732,
          5.99844567,  2.82998422]],

       [[ 3.3085757 ,  0.76006205, -4.75053671, ..., 11.91868516,
          7.7514486 ,  4.53965341],
        [ 3.38972171,  0.68563329, -4.74060982, ..., 12.10472066,
          7.78897082,  4.52123228],
        [ 3.47230554,  0.61598577, -4.72311547, ..., 12.27653838,
          7.81478878,  4.49610418],
        ...,
        [ 3.07241953,  1.00717408, -4.73601331, ..., 11.27983002,
          7.56891284,  4.55223323],
        [ 3.15006811,  0.92133144, -4.74811349, ..., 11.50576357,
          7.64137377,  4.55538473],
        [ 3.22873756,  0.83879243, -4.75299874, ..., 11.71887733,
          7.70223766,  4.5511168 ]]], shape=(40, 730, 72))
Coordinates:
  * time       (time) datetime64[ns] 6kB 2017-01-01T12:00:00 ... 2018-12-31T1...
    dayofyear  (time) int64 6kB ...
  * lon        (lon) float32 288B 0.5 5.5 10.5 15.5 ... 340.5 345.5 350.5 355.5
  * lat        (lat) float32 160B -19.5 -18.5 -17.5 -16.5 ... 17.5 18.5 19.5
Attributes:
    wavenumber:  [-999, -999]
    period:      [120, -999]
    depth:       [-999, -999] m
    waveName:    None


Extract data in a specified time range

time1, time2 = '2017-12-01', '2018-02-28'
lon1, lon2, lats, latn = 39, 181, 5, 15

mjo_result_ave = mjo_result.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(dim = 'lat')
mrg_result_ave = mrg_result.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(dim = 'lat')
td_result_ave = td_result.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(dim = 'lat')
lf_result_ave = lf_result.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(dim = 'lat')
lf_result_ave
<xarray.DataArray 'olr' (time: 90, lon: 29)> Size: 21kB
array([[ 7.00596819,  4.20734533,  9.81327741, ...,  3.08308952,
         6.43874229,  7.21068365],
       [ 7.19987092,  4.34243567,  9.8258908 , ...,  3.17001833,
         6.49791331,  7.32667326],
       [ 7.38688198,  4.47578285,  9.82823522, ...,  3.25575246,
         6.55230979,  7.4364893 ],
       ...,
       [-1.72686133, -0.97142549, -0.15921378, ..., -6.98142383,
        -5.79331014, -4.62624251],
       [-2.0120113 , -1.33091801, -0.33071156, ..., -7.25684753,
        -5.98326832, -4.84648253],
       [-2.29662459, -1.69610103, -0.50888872, ..., -7.53098335,
        -6.17071102, -5.06496685]], shape=(90, 29))
Coordinates:
  * time       (time) datetime64[ns] 720B 2017-12-01T12:00:00 ... 2018-02-28T...
    dayofyear  (time) int64 720B ...
  * lon        (lon) float32 116B 40.5 45.5 50.5 55.5 ... 170.5 175.5 180.5
Attributes:
    wavenumber:  [-999, -999]
    period:      [120, -999]
    depth:       [-999, -999] m
    waveName:    None


The Hovmöller Diagram is a powerful visualization tool that plots atmospheric or oceanic variables along one spatial axis (e.g., longitude or latitude) against time, revealing propagating wave patterns and persistent anomalies. By compressing spatiotemporal data into a single image, it allows meteorologists to track the phase speed, direction, and life cycle of large-scale waves, such as equatorial Kelvin waves or extratropical Rossby wave trains. In tropical meteorology, Hovmöller diagrams are frequently used to study the eastward progression of the MJO or the westward movement of tropical cyclones, while in climate science, they help diagnose teleconnections like the El Niño-Southern Oscillation (ENSO) influence on global weather. This method is essential for validating model simulations and understanding the organization of atmospheric disturbances over time.

See also

Persson, Anders. “The Story of the Hovmöller Diagram: An (Almost) Eyewitness Account”. Bulletin of the American Meteorological Society 98.5 (2017): 949-957. https://doi.org/10.1175/BAMS-D-15-00234.1 Web.

The first is given for equatorial waves with a frequency greater than 120 days of slow variation

fig, ax = plt.subplots(figsize = (8, 5))

lf_result_ave.plot.contourf(
    yincrease = False,
    levels = np.linspace(-15, 15, 21),
    cbar_kwargs={"location": "right", "aspect": 30},
)
ecl.plot.set_lon_format_axis()
plot kf filter

The MJO wave is extracted here

fig, ax = plt.subplots(figsize = (8, 5))

mjo_result_ave.plot.contourf(
    yincrease = False,
    levels = np.linspace(-15, 15, 21),
    cbar_kwargs={"location": "right", "aspect": 30},
)
ecl.plot.set_lon_format_axis()
plot kf filter

The MJO waves are extracted here

fig, ax = plt.subplots(figsize = (8, 5))

mrg_result_ave.plot.contourf(
    yincrease = False,
    levels = np.linspace(-15, 15, 21),
    cbar_kwargs={"location": "right", "aspect": 30},
)
ecl.plot.set_lon_format_axis()
plot kf filter

The mixed Rossby-gravity (MRG) waves are extracted here

fig, ax = plt.subplots(figsize = (8, 5))

td_result_ave.plot.contourf(
    yincrease = False,
    levels = np.linspace(-15, 15, 21),
    cbar_kwargs={"location": "right", "aspect": 30},
)
ecl.plot.set_lon_format_axis()
plot kf filter

Finally we give the spatial planes of various equatorial waves

fig, ax = plt.subplots(
    nrows=5,
    figsize = (10, 12),
    subplot_kw={
        "projection": ccrs.PlateCarree(central_longitude=180)
    },
)
for axi in ax.flat:
    axi.gridlines(draw_labels=["bottom", "left"], color = "grey", alpha = 0.5, linestyle = "--")
    axi.coastlines(edgecolor='k', linewidths=0.5)

def get_date_str(data):
    return str(data['time'].data)[:10]

axi = ax[0]
data = mjo_result.isel(time = 18)
data.sel(lat = slice(-20, 20)).plot.contourf(
    ax = axi,
    levels = np.linspace(-30, 30, 21),
    transform = ccrs.PlateCarree(),
    add_colorbar = False,
)
axi.set_title("MJO")
axi.set_title(get_date_str(data), loc = 'right')

axi = ax[1]
data = mrg_result.isel(time = 18)
data.sel(lat = slice(-20, 20)).plot.contourf(
    ax = axi,
    levels = np.linspace(-30, 30, 21),
    transform = ccrs.PlateCarree(),
    add_colorbar = False,
)
axi.set_title("MRG")
axi.set_title(get_date_str(data), loc = 'right')

axi = ax[2]
data = td_result.isel(time = 18)
data.sel(lat = slice(-20, 20)).plot.contourf(
    ax = axi,
    levels = np.linspace(-30, 30, 21),
    transform = ccrs.PlateCarree(),
    add_colorbar = False,
)
axi.set_title("TD")
axi.set_title(get_date_str(data), loc = 'right')

axi = ax[3]
data = kelvin_result.isel(time = 18)
data.sel(lat = slice(-20, 20)).plot.contourf(
    ax = axi,
    levels = np.linspace(-30, 30, 21),
    transform = ccrs.PlateCarree(),
    add_colorbar = False,
)
axi.set_title("Kelvin")
axi.set_title(get_date_str(data), loc = 'right')

axi = ax[4]
data = lf_result.isel(time = 18)
bar_sample = data.sel(lat = slice(-20, 20)).plot.contourf(
    ax = axi,
    levels = np.linspace(-30, 30, 21),
    transform = ccrs.PlateCarree(),
    add_colorbar = False,
)
axi.set_title("Low")
axi.set_title(get_date_str(data), loc = 'right')

axi_item = ax.flatten()
cb1 = fig.colorbar(bar_sample, ax = axi_item, orientation = 'horizontal', pad = 0.08, aspect = 50, shrink = 0.8, extendrect = False)
cb1.set_label('OLR (W/$\\mathrm{m^2}$)')
MJO, 2017-01-19, MRG, 2017-01-19, TD, 2017-01-19, Kelvin, 2017-01-19, Low, 2017-01-19

Total running time of the script: (0 minutes 10.233 seconds)