Plot one frame from the SyCLoPS videoΒΆ

The video shows each event in the data at each time-point, and the track of each event going back to its origin.

Script to make a single frame:

#!/usr/bin/env python

# Make a single frame showing the SyCLoPS tracks

import os
import sys
import datetime

import iris
import iris.cube
import iris.analysis.cartography
import numpy as np

import matplotlib
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle
from matplotlib.lines import Line2D

from get_data.SyCLoPS.SyCLoPS_load import (
    load_tracks,
    rotate_pole,
    interpolate_track,
    make_trail,
    last_date,
    first_date,
)
from get_data.ERA5_hourly.ERA5_hourly import get_land_mask

# Fix dask SPICE bug
import dask

dask.config.set(scheduler="single-threaded")

# Set colors
colors = {
    "HATHL": "red",
    "DOTHL": "green",
    "HAL": "blue",
    "TD(MD)": "black",
    "PL(PTLC)": "grey",
    "TLO": "green",
    "DST": "orange",
    "TC": "purple",
    "SC": "gold",
    "THL": "brown",
    "TD": "red",
    "DSE": "red",
    "TLO(ML)": "red",
    "DSD": "red",
    "EX": "red",
    "SS(STLC)": "red",
}

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--year", help="Year", type=int, required=True)
parser.add_argument("--month", help="Integer month", type=int, required=True)
parser.add_argument("--day", help="Day of month", type=int, required=True)
parser.add_argument(
    "--hour", help="Time of day (0 to 23.99)", type=float, required=True
)
parser.add_argument(
    "--pole_latitude",
    help="Latitude of projection pole",
    default=90,
    type=float,
    required=False,
)
parser.add_argument(
    "--pole_longitude",
    help="Longitude of projection pole",
    default=180,
    type=float,
    required=False,
)
parser.add_argument(
    "--npg_longitude",
    help="Longitude of view centre",
    default=0,
    type=float,
    required=False,
)
parser.add_argument(
    "--zoom",
    help="Scale factor for viewport (1=global)",
    default=1,
    type=float,
    required=False,
)
parser.add_argument(
    "--opdir",
    help="Directory for output files",
    default="%s/AnimH/visualizations/SyCLoPS" % os.getenv("SCRATCH"),
    type=str,
    required=False,
)
parser.add_argument(
    "--debug",
    help="Plot to debug image?",
    action="store_true",
    default=False,
    required=False,
)

args = parser.parse_args()
if not os.path.isdir(args.opdir):
    os.makedirs(args.opdir)


dte = datetime.datetime(
    args.year, args.month, args.day, int(args.hour), int(args.hour % 1 * 60)
)

# Load the tracks for the date
tracks = load_tracks()
# Rotate the pole
tracks = rotate_pole(
    tracks, args.pole_longitude, args.pole_latitude, args.npg_longitude
)
# Reduce the tracks to the date
itracks = {}
itracks_before = {}
itracks_after = {}
for tid, track in tracks.items():
    last_dte = last_date(track) - datetime.timedelta(minutes=1)
    first_dte = first_date(track)
    itr = interpolate_track(track, dte)
    if itr is not None:
        itracks[tid] = itr
    else:
        if last_dte < dte and last_dte > dte - datetime.timedelta(hours=12):
            itr_b = interpolate_track(track, last_dte)
            itr_b["time_offset"] = dte - last_dte
            itracks_before[tid] = itr_b
        if first_dte > dte and first_dte < dte + datetime.timedelta(hours=12):
            itr_a = interpolate_track(track, first_dte)
            itr_a["time_offset"] = first_dte - dte
            itracks_after[tid] = itr_a


# Find tracks
mask = get_land_mask()

# Define the figure (page size, background color, resolution, ...
fig = Figure(
    figsize=(19.2 * 2, 10.8 * 2),  # Width, Height (inches)
    dpi=100,
    facecolor=(0.5, 0.5, 0.5, 1),
    edgecolor=None,
    linewidth=0.0,
    frameon=True,
    subplotpars=None,
    tight_layout=None,
)
fig.set_frameon(False)
# Attach a canvas
canvas = FigureCanvas(fig)

# Projection for plotting
cs = iris.coord_systems.RotatedGeogCS(
    args.pole_latitude, args.pole_longitude, args.npg_longitude
)


def plot_cube(resolution, xmin, xmax, ymin, ymax):

    lat_values = np.arange(ymin, ymax + resolution, resolution)
    latitude = iris.coords.DimCoord(
        lat_values, standard_name="latitude", units="degrees_north", coord_system=cs
    )
    lon_values = np.arange(xmin, xmax + resolution, resolution)
    longitude = iris.coords.DimCoord(
        lon_values, standard_name="longitude", units="degrees_east", coord_system=cs
    )
    dummy_data = np.zeros((len(lat_values), len(lon_values)))
    plot_cube = iris.cube.Cube(
        dummy_data, dim_coords_and_dims=[(latitude, 0), (longitude, 1)]
    )
    return plot_cube


# Define an axes to contain the plot. In this case our axes covers
#  the whole figure
ax = fig.add_axes([0, 0, 1, 1])
ax.set_axis_off()  # Don't want surrounding x and y axis

# Lat and lon range (in rotated-pole coordinates) for plot
ax.set_xlim(-180 / args.zoom, 180 / args.zoom)
ax.set_ylim(-90 / args.zoom, 90 / args.zoom)
ax.set_aspect("auto")

# Background
ax.add_patch(
    Rectangle((-180, -90), 360, 180, facecolor=(0.9, 0.9, 0.9, 1), fill=True, zorder=1)
)

# Draw lines of latitude and longitude
for lat in range(-90, 95, 5):
    lwd = 0.25
    x = []
    y = []
    for lon in range(-180, 181, 1):
        rp = iris.analysis.cartography.rotate_pole(
            np.array(lon), np.array(lat), args.pole_longitude, args.pole_latitude
        )
        nx = rp[0] + args.npg_longitude
        if nx > 180:
            nx -= 360
        ny = rp[1]
        if len(x) == 0 or (abs(nx - x[-1]) < 10 and abs(ny - y[-1]) < 10):
            x.append(nx)
            y.append(ny)
        else:
            ax.add_line(
                Line2D(x, y, linewidth=lwd, color=(0.4, 0.4, 0.4, 1), zorder=10)
            )
            x = []
            y = []
    if len(x) > 1:
        ax.add_line(Line2D(x, y, linewidth=lwd, color=(0.4, 0.4, 0.4, 1), zorder=10))

for lon in range(-180, 185, 5):
    lwd = 0.25
    x = []
    y = []
    for lat in range(-90, 90, 1):
        rp = iris.analysis.cartography.rotate_pole(
            np.array(lon), np.array(lat), args.pole_longitude, args.pole_latitude
        )
        nx = rp[0] + args.npg_longitude
        if nx > 180:
            nx -= 360
        ny = rp[1]
        if len(x) == 0 or (abs(nx - x[-1]) < 10 and abs(ny - y[-1]) < 10):
            x.append(nx)
            y.append(ny)
        else:
            ax.add_line(
                Line2D(x, y, linewidth=lwd, color=(0.4, 0.4, 0.4, 1), zorder=10)
            )
            x = []
            y = []
    if len(x) > 1:
        ax.add_line(Line2D(x, y, linewidth=lwd, color=(0.4, 0.4, 0.4, 1), zorder=10))

# Plot the land mask
mask_pc = plot_cube(
    0.05, -180 / args.zoom, 180 / args.zoom, -90 / args.zoom, 90 / args.zoom
)
mask = mask.regrid(mask_pc, iris.analysis.Linear())
lats = mask.coord("latitude").points
lons = mask.coord("longitude").points
mask_img = ax.imshow(
    mask.data,
    extent=[lons.min(), lons.max(), lats.min(), lats.max()],
    cmap=matplotlib.colors.ListedColormap(((0.8, 0.8, 0.8, 0), (0.8, 0.8, 0.8, 1))),
    vmin=0,
    vmax=1,
    alpha=1.0,
    zorder=20,
    origin="lower",
    aspect="auto",
)


def pointsize(ws):  # Convert wind speed to point size
    return (np.sqrt(ws) / 5.0) * 2


def linewidth(ws):  # Convert wind speed to line width
    return (np.sqrt(ws) / 2.0) * 2


def plot_object(ax, track, tid, alpha=1):
    #    color = (1.0 * track["Tropical_Flag"], 0, 1.0 - track["Tropical_Flag"], 1)
    try:
        color = colors[track["Short_Label"]]
    except KeyError:
        color = "black"
    ax.add_patch(
        matplotlib.patches.Circle(
            (track["LON"], track["LAT"]),
            radius=pointsize(track["WS"]),
            facecolor=color,
            edgecolor=color,
            linewidth=0.0,
            alpha=alpha,
            zorder=180,
        )
    )
    # Add the trails
    trail = make_trail(tracks, tid, dte)
    for i in range(len(trail) - 1):
        #        color = (1.0 * trail[i]["Tropical_Flag"], 0, 1.0 - trail[i]["Tropical_Flag"], 1)
        try:
            color = colors[trail[i]["Short_Label"]]
        except KeyError:
            color = "black"
        if abs(trail[i]["LON"] - trail[i + 1]["LON"]) > 90:
            continue  # Skip lines that cross the 180th meridian
        ax.add_line(
            Line2D(
                [trail[i]["LON"], trail[i + 1]["LON"]],
                [trail[i]["LAT"], trail[i + 1]["LAT"]],
                linewidth=linewidth(trail[i]["WS"]),
                color=color,
                alpha=alpha * 0.5,
                zorder=150,
            )
        )


# Plot the objects
for tid, track in itracks.items():
    plot_object(ax, track, tid, alpha=1.0)

for tid, track in itracks_before.items():
    alpha = 1.0 - track["time_offset"].total_seconds() / 43200.0
    plot_object(ax, track, tid, alpha=alpha)
for tid, track in itracks_after.items():
    alpha = 1.0 - track["time_offset"].total_seconds() / 43200.0
    plot_object(ax, track, tid, alpha=alpha)

# Label with the date
ax.text(
    180 / args.zoom - (360 / args.zoom) * 0.009,
    90 / args.zoom - (180 / args.zoom) * 0.016,
    "%04d-%02d-%02d" % (args.year, args.month, args.day),
    horizontalalignment="right",
    verticalalignment="top",
    color="black",
    bbox=dict(
        facecolor=(0.6, 0.6, 0.6, 0.5), edgecolor="black", boxstyle="round", pad=0.5
    ),
    size=28,
    clip_on=True,
    zorder=500,
)

opfile = "%s/%04d%02d%02d%02d%02d.png" % (
    args.opdir,
    args.year,
    args.month,
    args.day,
    int(args.hour),
    int(args.hour % 1 * 60),
)
if args.debug:

    opfile = "debug.png"

# Render the figure as a png
fig.savefig(opfile)

For the functions to load the data, see the get-data page.