Typhoon Tracking and Axisymmetric Analysis#

This document demonstrates the process of tracking a typhoon’s center and performing axisymmetric analysis using mean sea level pressure (MSL), temperature, and precipitation data. The provided functions in easyclimate, implement the core algorithms for cyclone tracking and axisymmetric decomposition.

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

loads meteorological reanalysis datasets (ERA5) for a typhoon. The datasets include:

  • Temperature (t_data): A multi-level atmospheric temperature field.

  • Mean Sea Level Pressure (slp_time, slp_data): MSL pressure data with time, latitude, and longitude dimensions. A single time slice (slp_data) is extracted for initial analysis.

  • Precipitation (tp_data): Total precipitation data, converted from its original units (m/h) to millimeters per hour (mm/h) by multiplying by 1000 and updating the units attribute.

t_data = xr.open_dataset("test_t_typhoon_201919.nc").t

slp_time = xr.open_dataset("test_slp_typhoon_201919.nc").msl
slp_data = slp_time.isel(time = 0)

tp_data = xr.open_dataset("test_pr_typhoon_201919.nc").tp *1000
tp_data.attrs["units"] = "mm/h"

Tracking#

We uses the easyclimate.field.typhoon.track_cyclone_center_msl_only function to locate the typhoon center at a single time step using MSL pressure data. The function takes the following inputs:

  • slp_data: A 2D xarray.DataArray containing MSL pressure with latitude and longitude dimensions.

  • sample_point: An initial guess for the cyclone center, like (longitude=140°, latitude=20°).

The function identifies local minima in the MSL pressure field using a 3x3 minimum filter (scipy.ndimage.minimum_filter) with nearest boundary conditions to account for the grid edges. This ensures robust detection of low-pressure centers, which are characteristic of cyclones.

To be specific, the function computes the great-circle distance between the provided sample_point and all detected minima using the formula:

\[\cos \alpha = \sin \theta_0 \sin \theta + \cos \theta_0 \cos \theta \cos (\lambda - \lambda_0)\]

where \(\alpha\) is the central angle, and the closest minimum is selected as the cyclone center.

To refine the cyclone center beyond the grid resolution, the function applies biquadratic interpolation around the identified minimum. It constructs a quadratic function:

\[f(x, y) = c_0 + c_1x + c_2y + c_3xy + c_4x^2 + c_5y^2 + c_6x^2y + c_7xy^2 + c_8x^2y^2\]

and solves for the extremum by setting the gradient to zero (i.e., \(\\nabla f = 0\)). The solution involves computing the inverse of the Hessian matrix:

\[\begin{split}\mathbf{A}^{-1} = \frac{1}{d} \begin{pmatrix} f_{yy} & -f_{xy} \\ -f_{xy} & f_{xx} \end{pmatrix}, \quad d = f_{xx}f_{yy} - f_{xy}^2\end{split}\]

to determine the offsets \(\Delta x\) and \(\Delta y\). If the determinant \(d = 0\), the grid-based minimum is used instead.

lon lat slp_min
0 140.197533 19.766941 96263.759953


Here, we extends the tracking to multiple time steps by iterating over the time dimension of the slp_time dataset. For each time step:

lon lat slp_min
0 140.197533 19.766941 96263.759953
1 140.033467 20.215966 95901.328523


Here, we visualizes the MSL pressure field and the tracked typhoon center on a map.

fig, ax = ecl.plot.quick_draw_spatial_basemap(central_longitude=120)
ax.set_extent([120, 150, 5, 30], crs = ccrs.PlateCarree())


c = (slp_data/100).plot.contour(
    levels = 11,
    transform=ccrs.PlateCarree(),
    zorder = 1
)
ax.clabel(c)

ax.scatter(
    df_track["lon"][0], df_track['lat'][0],
    c="red",
    transform=ccrs.PlateCarree(),
    zorder = 2
)
time = 2019-10-09
<matplotlib.collections.PathCollection object at 0x7fa8c76f21a0>

Axisymmetric Analysis#

Temperature (multi-level variable)#

This section performs axisymmetric analysis on the multi-level temperature data (t_data) using the easyclimate.field.typhoon.cyclone_axisymmetric_analysis function

The function transforms the data into a polar coordinate system centered on the typhoon (relocated to the North Pole) using spherical orthodrome transformation (Ritchie, 1987; Nakamura et al., 1997; Yamazaki, 2011). This involves:

  • Converting longitude-latitude coordinates to Cartesian coordinates:

    \[x = \cos \lambda \sin \theta, \quad y = \sin \lambda \sin \theta, \quad z = \cos \theta\]
  • Applying two rotations to align the cyclone center at the North Pole:

    • Rotate by \(-\theta_c\) around the y-axis.

    • Rotate by \(\lambda_c\) around the z-axis.

    • The combined rotation matrix is:

\[\begin{split}A = \begin{bmatrix} \cos \lambda_c \cos \theta_c & -\sin \lambda_c & \cos \lambda_c \sin \theta_c \\ \sin \lambda_c \cos \theta_c & \cos \lambda_c & \sin \lambda_c \sin \theta_c \\ -\sin \theta_c & 0 & \cos \theta_c \end{bmatrix}\end{split}\]
  • Interpolating the data onto a polar grid, e.g., polar_lon=np.arange(0, 360, 2) and polar_lat=np.arange(80, 90.1, 1).

See also

Finally, we decomposes the data into:

  • rotated: Interpolated data on the polar grid.

  • rotated_symmetric: Azimuthal mean (axisymmetric component).

  • rotated_asymmetric: Deviation from the azimuthal mean (asymmetric component).

easyclimate.DataNode
'root'
root
rotated:xarray.DataArray
rotated_asymmetric:xarray.DataArray
rotated_symmetric:xarray.DataArray


We processes the symmetric temperature component:

  • Anomaly Calculation: r1 computes the temperature anomaly by subtracting the value at the cyclone center (\(y=0\), corresponding to the North Pole in the transformed coordinates) from the symmetric component (ds_sym), highlighting radial variations in the warm core structure.

  • Unit Conversion: r2 converts the symmetric temperature from Kelvin ( \(\mathrm{K}\) ) to Celsius ( \(\mathrm{^\circC}\) ) .

ds_sym = temp_tc_polar_result["rotated_symmetric"]
r1 = (ds_sym - ds_sym.isel(y = 0))
r2 = ecl.utility.transfer_data_temperature_units(ds_sym, "K", "degC")

Here, we visualizes the symmetric temperature anomaly and absolute temperature.

fig, ax = plt.subplots()
ecl.plot.set_p_format_axis()

r1.plot.contourf(
    ax = ax,
    levels = np.linspace(-8, 8, 21),
    yincrease = False
)

c = r2.plot.contour(
    levels= np.arange(-80, 80, 10),
    colors = "k",
    yincrease = False
)
ax.clabel(c)
plot tc track axis
<a list of 11 text.Text objects>

In this part, we extracts and prepares the asymmetric temperature component at the 850 hPa level for visualization:

ds_asym = temp_tc_polar_result["rotated_asymmetric"]
ds_asym850 = ds_asym.sel(level = 850)

ds_asym850_latlon = ds_asym850.swap_dims({'y': 'lat', 'polar_lon': 'lon'})
ds_asym850_latlon_0360 = ecl.plot.add_lon_cyclic(ds_asym850_latlon, inter = 2)

And visualizes the asymmetric temperature component at 850 hPa on a polar stereographic map.

fig, ax = plt.subplots(subplot_kw={"projection": ccrs.NorthPolarStereo()})

ecl.plot.draw_Circlemap_PolarStereo(
    ax=ax,
    lon_step=30,
    lat_step=2.5,
    lat_range=[80, 90],
    draw_labels=False,
    set_map_boundary_kwargs={"north_pad": 0.3, "south_pad": 0.4},
    gridlines_kwargs={"color": "grey", "alpha": 0.8, "linestyle": "--"},
)

(ds_asym850_latlon_0360 + 1e-13).plot.contourf(
    x = "lon", y = "lat",
    transform=ccrs.PlateCarree(),
    cmap = "RdBu_r",
    levels = 21
)
level = 850
<cartopy.mpl.contour.GeoContourSet object at 0x7fa8c867a320>

Precipitation (single level variable)#

At first, we visualizes the precipitation field (tp_data) in its original longitude-latitude coordinates.

tp_data.plot.contourf(cmap = "Greens", levels = np.linspace(0, 12, 11))
time = 2019-10-09
<matplotlib.contour.QuadContourSet object at 0x7fa8c34e9960>

And then, we apply axisymmetric analysis to the single-level precipitation data.

easyclimate.DataNode
'root'
root
rotated:xarray.DataArray
rotated_asymmetric:xarray.DataArray
rotated_symmetric:xarray.DataArray


Next, we prepare the rotated precipitation data for visualization.

pr_polardata = pr_tc_polar_result["rotated"].swap_dims({'y': 'lat', 'polar_lon': 'lon'})
pr_polardata = ecl.plot.add_lon_cyclic(pr_polardata, inter = 2)

And we visualizes the asymmetric precipitation component.

fig, ax = plt.subplots(subplot_kw={"projection": ccrs.NorthPolarStereo()})

ecl.plot.draw_Circlemap_PolarStereo(
    ax=ax,
    lon_step=30,
    lat_step=2.5,
    lat_range=[80, 90],
    draw_labels=False,
    set_map_boundary_kwargs={"north_pad": 0.3, "south_pad": 0.4},
    gridlines_kwargs={"color": "grey", "alpha": 0.4, "linestyle": "--"},
)

(pr_polardata + 1e-13).plot.contourf(
    x = "lon", y = "lat",
    transform=ccrs.PlateCarree(),
    cmap = "Greens",
    levels = np.linspace(0, 12, 11)
)
plot tc track axis
<cartopy.mpl.contour.GeoContourSet object at 0x7fa8c33f15a0>

At this step, we will also perform coordinate dimension and data conversion.

ds_asym = pr_tc_polar_result["rotated_asymmetric"]

ds_asym_latlon = ds_asym.swap_dims({'y': 'lat', 'polar_lon': 'lon'})
ds_asym_latlon_0360 = ecl.plot.add_lon_cyclic(ds_asym_latlon, inter = 2)

At last, we visualize the asymmetric precipitation componen

fig, ax = plt.subplots(subplot_kw={"projection": ccrs.NorthPolarStereo()})

ecl.plot.draw_Circlemap_PolarStereo(
    ax=ax,
    lon_step=30,
    lat_step=2.5,
    lat_range=[80, 90],
    draw_labels=False,
    set_map_boundary_kwargs={"north_pad": 0.3, "south_pad": 0.4},
    gridlines_kwargs={"color": "grey", "alpha": 0.8, "linestyle": "--"},
)

(ds_asym_latlon_0360 + 1e-13).plot.contourf(
    x = "lon", y = "lat",
    transform=ccrs.PlateCarree(),
    cmap = "RdBu_r",
    levels = np.linspace(-4, 4, 21)
)
plot tc track axis
<cartopy.mpl.contour.GeoContourSet object at 0x7fa8c3298490>

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