Plot one frame of SyCLoPS data overlain on ERA5 weatherΒΆ

Script to make a single frame:

#!/usr/bin/env python

# Atmospheric state - near-surface wind and precip.

import os
import sys
from get_data.ERA5_hourly.ERA5_hourly import load, get_land_mask
import datetime
import pickle

import iris
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
import cmocean

from pandas import qcut

# Fix dask SPICE bug
import dask

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

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/ERA+SYCLoPS" % os.getenv("SCRATCH"),
    type=str,
    required=False,
)
parser.add_argument(
    "--zfile",
    help="Noise pickle file name",
    default="%s/AnimH/visualizations/z.pkl" % os.getenv("SCRATCH"),
    type=str,
    required=False,
)
parser.add_argument(
    "--debug",
    action="store_true",
)

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)
)


# Remap the precipitation to standardise the distribution
# Normalise a precip field to fixed quantiles
def normalise_precip(p):
    res = p.copy()
    res.data[res.data <= 6.68e-5] = 0.79
    res.data[res.data < 7.77e-5] = 0.81
    res.data[res.data < 9.25e-5] = 0.83
    res.data[res.data < 1.11e-4] = 0.85
    res.data[res.data < 1.35e-4] = 0.87
    res.data[res.data < 1.68e-4] = 0.89
    res.data[res.data < 2.16e-4] = 0.91
    res.data[res.data < 2.94e-4] = 0.93
    res.data[res.data < 4.30e-4] = 0.95
    res.data[res.data < 7.11e-4] = 0.97
    res.data[res.data < 0.1] = 0.99
    return res


u10m = load("10m_u_component_of_wind", dte.year, dte.month, dte.day, dte.hour)
v10m = load("10m_v_component_of_wind", dte.year, dte.month, dte.day, dte.hour)
precip = load("total_precipitation", dte.year, dte.month, dte.day, dte.hour)
precip = normalise_precip(precip)

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=False,  # Don't draw a frame
    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


# Make the wind noise
def wind_field(uw, vw, zf, sequence=None, iterations=50, epsilon=0.003, sscale=1):
    # Random field as the source of the distortions
    z = pickle.load(open(zf, "rb"))
    z = z.regrid(uw, iris.analysis.Linear())
    (width, height) = z.data.shape
    # Each point in this field has an index location (i,j)
    #  and a real (x,y) position
    xmin = np.min(uw.coords()[0].points)
    xmax = np.max(uw.coords()[0].points)
    ymin = np.min(uw.coords()[1].points)
    ymax = np.max(uw.coords()[1].points)

    # Convert between index and real positions
    def i_to_x(i):
        return xmin + (i / width) * (xmax - xmin)

    def j_to_y(j):
        return ymin + (j / height) * (ymax - ymin)

    def x_to_i(x):
        return np.minimum(
            width - 1,
            np.maximum(0, np.floor((x - xmin) / (xmax - xmin) * (width - 1))),
        ).astype(int)

    def y_to_j(y):
        return np.minimum(
            height - 1,
            np.maximum(0, np.floor((y - ymin) / (ymax - ymin) * (height - 1))),
        ).astype(int)

    i, j = np.mgrid[0:width, 0:height]
    x = i_to_x(i)
    y = j_to_y(j)
    # Result is a distorted version of the random field
    result = z.copy()
    # Repeatedly, move the x,y points according to the vector field
    #  and update result with the random field at their locations
    ss = uw.copy()
    ss.data = np.sqrt(uw.data**2 + vw.data**2)
    if sequence is not None:
        startsi = np.arange(0, iterations, 3)
        endpoints = np.tile(startsi, 1 + (width * height) // len(startsi))
        endpoints += sequence % iterations
        endpoints[endpoints >= iterations] -= iterations
        startpoints = endpoints - 25
        startpoints[startpoints < 0] += iterations
        endpoints = endpoints[0 : (width * height)].reshape(width, height)
        startpoints = startpoints[0 : (width * height)].reshape(width, height)
    else:
        endpoints = iterations + 1
        startpoints = -1
    for k in range(iterations):
        x += epsilon * vw.data[i, j]
        x[x > xmax] = xmax
        x[x < xmin] = xmin
        y += epsilon * uw.data[i, j]
        y[y > ymax] = y[y > ymax] - ymax + ymin
        y[y < ymin] = y[y < ymin] - ymin + ymax
        i = x_to_i(x)
        j = y_to_j(y)
        update = z.data * ss.data / sscale
        update[(endpoints > startpoints) & ((k > endpoints) | (k < startpoints))] = 0
        update[(startpoints > endpoints) & ((k > endpoints) & (k < startpoints))] = 0
        result.data[i, j] += update
    return result


wind_pc = plot_cube(
    0.2, -180 / args.zoom, 180 / args.zoom, -90 / args.zoom, 90 / args.zoom
)
rw = iris.analysis.cartography.rotate_winds(u10m, v10m, cs)
u10m = rw[0].regrid(wind_pc, iris.analysis.Linear())
v10m = rw[1].regrid(wind_pc, iris.analysis.Linear())
seq = (dte - datetime.datetime(2000, 1, 1)).total_seconds() / 3600
wind_noise_field = wind_field(
    u10m, v10m, args.zfile, sequence=int(seq * 5), epsilon=0.01
)

# 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
ax.add_patch(  # Background
    Rectangle((-180, -90), 360, 180, facecolor=(0.9, 0.9, 0.9, 1), fill=True, zorder=1)
)

# 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((0, 0), 1, 1, facecolor=(0.6, 0.6, 0.6, 1), fill=True, zorder=1))

# Draw lines of latitude and longitude
for lat in range(-90, 95, 5):
    lwd = 0.75
    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.75
    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
ax.imshow(
    mask.data,
    extent=[lons.min(), lons.max(), lats.min(), lats.max()],
    cmap=matplotlib.colors.ListedColormap(((0.4, 0.4, 0.4, 0), (0.4, 0.4, 0.4, 1))),
    vmin=0,
    vmax=1,
    alpha=1.0,
    zorder=20,
    origin="lower",
    aspect="auto",
)


# Plot the Wind
wind_pc = plot_cube(
    0.05, -180 / args.zoom, 180 / args.zoom, -90 / args.zoom, 90 / args.zoom
)
wscale = 200
s = wind_noise_field.data.shape
wind_noise_field.data = (
    qcut(
        wind_noise_field.data.flatten(), wscale, labels=False, duplicates="drop"
    ).reshape(s)
    - (wscale - 1) / 2
)

# Plot as a colour map
wnf = wind_noise_field.regrid(wind_pc, iris.analysis.Linear())
t2m_img = ax.imshow(
    wnf.data,
    extent=[lons.min(), lons.max(), lats.min(), lats.max()],
    cmap=cmocean.cm.gray,
    alpha=0.6,
    zorder=100,
    vmin=-150,
    vmax=150,
    origin="lower",
    aspect="auto",
)

# Plot the precip
precip_pc = plot_cube(
    0.25, -180 / args.zoom, 180 / args.zoom, -90 / args.zoom, 90 / args.zoom
)
precip = precip.regrid(precip_pc, iris.analysis.Linear())
wnf = wind_noise_field.regrid(precip, iris.analysis.Linear())
precip.data[precip.data > 0.8] += wnf.data[precip.data > 0.8] / 3000
precip.data[precip.data < 0.8] = 0.8
cols = []
for ci in range(100):
    cols.append([0.0, 0.3, 0.0, ci / 100])
precip_img = ax.imshow(
    precip.data,
    extent=[lons.min(), lons.max(), lats.min(), lats.max()],
    cmap=matplotlib.colors.ListedColormap(cols),
    alpha=0.7,
    zorder=200,
    origin="lower",
    aspect="auto",
)


# 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,
)


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

# 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


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 = "red"
    ax.add_patch(
        matplotlib.patches.Circle(
            (track["LON"], track["LAT"]),
            radius=pointsize(track["WS"]),
            facecolor=color,
            edgecolor=color,
            linewidth=0.0,
            alpha=alpha,
            zorder=580,
        )
    )
    # Add the trails
    trail = make_trail(tracks, tid, dte)
    for i in range(len(trail) - 1):
        color = "red"
        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=550,
            )
        )


# 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)


# Render the figure as a png
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"
fig.savefig(opfile)