Track on Radar data, Segment on satellite data

This notebooks shows: - how to input NEXRAD radar data and GOES satellite data from Amazon cloud service and prepare it for use in tobac - how to identify features on one data type (3D radar data) and to connect another data type (satellite data) to these features via segmentation

This is an idealized show case that ignores potential data mismatches, in our case the mismatch due to parallax effects.

Library Imports

Note: In addition to the normaltobac* requirements, this tutorial also requires that Py-ART and s3fs are installed.*

[1]:
# Import libraries:
import pyart
import xarray as xr
import s3fs
import numpy as np
import datetime
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from matplotlib.pyplot import cm as cmaps
import pandas as pd
from pathlib import Path
import iris
from pyproj import Proj, Geod

%matplotlib inline

## You are using the Python ARM Radar Toolkit (Py-ART), an open source
## library for working with weather radar data. Py-ART is partly
## supported by the U.S. Department of Energy as part of the Atmospheric
## Radiation Measurement (ARM) Climate Research Facility, an Office of
## Science user facility.
##
## If you use this software to prepare a publication, please cite:
##
##     JJ Helmus and SM Collis, JORS 2016, doi: 10.5334/jors.119

[2]:
# Disable a few warnings:
import warnings

warnings.filterwarnings("ignore", category=UserWarning, append=True)
warnings.filterwarnings("ignore", category=RuntimeWarning, append=True)
warnings.filterwarnings("ignore", category=FutureWarning, append=True)
warnings.filterwarnings("ignore", category=pd.io.pytables.PerformanceWarning)
[3]:
# Import tobac itself:
import tobac
import tobac.utils

print("using tobac version", str(tobac.__version__))
using tobac version 1.5.3

Data Input and Preparations

Reading radar data and satellite data from Amazon S3

[4]:
# read in radar data
radar = pyart.io.read_nexrad_archive(
    "s3://noaa-nexrad-level2/2021/05/26/KGLD/KGLD20210526_155623_V06"
)
[5]:
# read in satellite data
fs = s3fs.S3FileSystem(anon=True)
aws_url = "s3://noaa-goes16/ABI-L2-MCMIPC/2021/146/15/OR_ABI-L2-MCMIPC-M6_G16_s20211461556154_e20211461558539_c20211461559030.nc"

goes_data = xr.open_dataset(fs.open(aws_url), engine="h5netcdf")
[6]:
#Set up directory to save output and plots:
savedir=Path("Save")
if not savedir.is_dir():
    savedir.mkdir()
plot_dir=Path("Plot")
if not plot_dir.is_dir():
    plot_dir.mkdir()

Preparing GOES satellite data

In the next steps, we - calculate longitude and latitude values using geostationary satellite projection - construct a GOES dataset with lon/lat as coordinates - finally, convert the dataset into an Iris Cube (which is taken as tobac input)

[7]:
"""
Because the GOES data comes in without latitude/longitude values, we need to calculate those.
"""


def lat_lon_reproj(g16nc):
    # GOES-R projection info and retrieving relevant constants
    proj_info = g16nc["goes_imager_projection"]
    lon_origin = proj_info.attrs["longitude_of_projection_origin"]
    H = proj_info.attrs["perspective_point_height"] + proj_info.attrs["semi_major_axis"]
    r_eq = proj_info.attrs["semi_major_axis"]
    r_pol = proj_info.attrs["semi_minor_axis"]

    # grid info
    lat_rad_1d = g16nc.variables["x"][:]
    lon_rad_1d = g16nc.variables["y"][:]

    # create meshgrid filled with radian angles
    lat_rad, lon_rad = np.meshgrid(lat_rad_1d, lon_rad_1d)

    # lat/lon calc routine from satellite radian angle vectors

    lambda_0 = (lon_origin * np.pi) / 180.0

    a_var = np.power(np.sin(lat_rad), 2.0) + (
        np.power(np.cos(lat_rad), 2.0)
        * (
            np.power(np.cos(lon_rad), 2.0)
            + (((r_eq * r_eq) / (r_pol * r_pol)) * np.power(np.sin(lon_rad), 2.0))
        )
    )
    b_var = -2.0 * H * np.cos(lat_rad) * np.cos(lon_rad)
    c_var = (H**2.0) - (r_eq**2.0)

    r_s = (-1.0 * b_var - np.sqrt((b_var**2) - (4.0 * a_var * c_var))) / (2.0 * a_var)

    s_x = r_s * np.cos(lat_rad) * np.cos(lon_rad)
    s_y = -r_s * np.sin(lat_rad)
    s_z = r_s * np.cos(lat_rad) * np.sin(lon_rad)

    lat = (180.0 / np.pi) * (
        np.arctan(
            ((r_eq * r_eq) / (r_pol * r_pol))
            * ((s_z / np.sqrt(((H - s_x) * (H - s_x)) + (s_y * s_y))))
        )
    )
    lon = (lambda_0 - np.arctan(s_y / (H - s_x))) * (180.0 / np.pi)

    return lon, lat
[8]:
llons, llats = lat_lon_reproj(goes_data)

full_goes_data = goes_data["CMI_C14"].values
in_goes_for_tobac = xr.Dataset(
    data_vars={
        "C14_brightness_temperature": (
            ("time", "Y", "X"),
            [full_goes_data],
        )
    },
    coords={
        "time": [goes_data["time_bounds"][0].values],
        "longitude": (["Y", "X"], llons),
        "latitude": (["Y", "X"], llats),
    },
)
[9]:
def enhanced_colormap(vmin=200.0, vmed=240.0, vmax=300.0):
    '''
    Creates enhanced colormap typical of IR BTs.
    '''
    nfull = 256

    ngray = int(nfull * (vmax - vmed) / (vmax - vmin))
    ncol = nfull - ngray

    colors1 = plt.cm.gray_r(np.linspace(0.0, 1.0, ngray))
    colors2 = plt.cm.Spectral(np.linspace(0., 0.95, ncol))

    # combine them and build a new colormap
    colors = np.vstack((colors2, colors1))
    mymap = plt.matplotlib.colors.LinearSegmentedColormap.from_list(
        "enhanced_colormap", colors
    )

    return mymap
[10]:
in_goes_for_tobac["C14_brightness_temperature"].plot(
    yincrease=False, vmin=200, vmax=300.0, cmap=enhanced_colormap(), figsize=(12, 8)
)
[10]:
<matplotlib.collections.QuadMesh at 0x13c0c8bd0>
../../_images/examples4doc_Example_Track_on_Radar_Segment_on_Satellite_Example_Track_on_Radar_Segment_on_Satellite_17_1.png
[11]:
goes_array_iris = in_goes_for_tobac["C14_brightness_temperature"].to_iris()

Preparing Radar Data

In the next steps, we - map radar data onto a regular grid - construct a 3D radar dataset and add longitude, latitude and altitude as coordinates - finally, convert the dataset into an Iris Cube (which is taken as tobac input)

[12]:
# First, we need to grid the input radar data.
radar_grid_pyart = pyart.map.grid_from_radars(
    radar,
    grid_shape=(41, 401, 401),
    grid_limits=(
        (
            0.0,
            20000,
        ),
        (-200000.0, 200000.0),
        (-200000, 200000.0),
    ),
)
[13]:
# In Py-ART's graphing suite, there is a display class similar to RadarMapDisplay,
# but for grids. To plot the grid:
fig = plt.figure(figsize=[12, 8])
plt.axis("off")

display = pyart.graph.GridMapDisplay(radar_grid_pyart)
display.plot_grid("reflectivity", level=3, vmin=0, vmax=60, fig=fig)
../../_images/examples4doc_Example_Track_on_Radar_Segment_on_Satellite_Example_Track_on_Radar_Segment_on_Satellite_22_0.png
[14]:
radar_xarray_grid = radar_grid_pyart.to_xarray()
[15]:
radar_xr_grid_full = radar_xarray_grid["reflectivity"][:, 4]
radar_xr_grid_full["z"] = radar_xr_grid_full.z.assign_attrs(
    {"standard_name": "altitude"}
)
radar_xr_grid_full["lat"] = radar_xr_grid_full.lat.assign_attrs(
    {"standard_name": "latitude"}
)
radar_xr_grid_full["lon"] = radar_xr_grid_full.lon.assign_attrs(
    {"standard_name": "longitude"}
)
[16]:
radar_grid_iris = radar_xr_grid_full.to_iris()

Combined Plot

[17]:
# first we cut satellite data a little bit
rlon_min, rlon_max = radar_xarray_grid.lon.min().data, radar_xarray_grid.lon.max().data
rlat_min, rlat_max = radar_xarray_grid.lat.min().data, radar_xarray_grid.lat.max().data

lon_mask = (llons >= rlon_min) & (llons <= rlon_max)
lat_mask = (llats >= rlat_min) & (llats <= rlat_max)
mask = lon_mask & lat_mask


goes_cutted = (
    in_goes_for_tobac.where(mask).dropna("X", how="all").dropna("Y", how="all")
)
[18]:
Zmax = radar_xarray_grid['reflectivity'].max('z').squeeze()
[19]:
goes_cutted["C14_brightness_temperature"].plot(
    x="longitude", y="latitude", cmap=enhanced_colormap(), figsize=(16, 8)
)

Zmax.where(Zmax > 34).plot.contourf(
    x="lon",
    y="lat",
    cmap=plt.cm.Purples,
    levels=[35, 45, 56],
    alpha=0.5,
)

Zmax.where(Zmax > 20).plot.contour(
    x="lon", y="lat", colors="k", levels=[35, 45, 56], linewidths=2
)

plt.xlim(rlon_min, rlon_max)
plt.ylim(rlat_min, rlat_max)
[19]:
(37.54600438517133, 41.16558741816462)
../../_images/examples4doc_Example_Track_on_Radar_Segment_on_Satellite_Example_Track_on_Radar_Segment_on_Satellite_29_1.png

Tobac Cloud Tracking

Feature detection

We use multiple thresholds to detect features in the radar data.

[20]:
feature_detection_params = dict()
feature_detection_params["threshold"] = [30, 40, 50]
feature_detection_params["target"] = "maximum"
feature_detection_params["position_threshold"] = "weighted_diff"
feature_detection_params["n_erosion_threshold"] = 2
feature_detection_params["sigma_threshold"] = 1
feature_detection_params["n_min_threshold"] = 4
[21]:
# Perform feature detection:
print('starting feature detection')
radar_features = tobac.feature_detection.feature_detection_multithreshold(
    radar_grid_iris, 0, **feature_detection_params
)
radar_features.to_hdf(savedir / 'Features.h5', 'table')
print('feature detection performed and saved')
starting feature detection
feature detection performed and saved
[22]:
radar_features
[22]:
frame idx hdim_1 hdim_2 num threshold_value feature time timestr projection_y_coordinate projection_x_coordinate altitude latitude longitude
0 0 5 258.862284 271.851509 19 30 1 2021-05-26 15:56:23 2021-05-26 15:56:23 58862.283732 71851.509127 71851.509127 39.893281 -100.858070
1 0 9 250.469609 240.740283 6 40 2 2021-05-26 15:56:23 2021-05-26 15:56:23 50469.608638 40740.283179 40740.283179 39.819857 -101.223255
2 0 11 198.778758 225.305252 54 50 3 2021-05-26 15:56:23 2021-05-26 15:56:23 -1221.242078 25305.251764 25305.251764 39.355590 -101.405959
3 0 12 243.316368 217.254572 7 50 4 2021-05-26 15:56:23 2021-05-26 15:56:23 43316.368018 17254.571781 17254.571781 39.756323 -101.498434
[23]:
# we now have 4 detected features

fig = plt.figure(figsize=[10, 8])
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent([-101.7, -100.7, 39, 40], crs=ccrs.PlateCarree())


Z = radar_xarray_grid["reflectivity"][0, 4]
cm = ax.pcolormesh(
    radar_xarray_grid["lon"],
    radar_xarray_grid["lat"],
    Z.where(Z> 0),
    vmin=0,
    vmax=65,
    transform=ccrs.PlateCarree(),
    cmap="Spectral_r",
)
plt.xlim(-101.7, -100.7)
plt.ylim(39.0, 40)
cb = plt.colorbar(cm)
cb.set_label("Reflectivity (dBZ)", size=14)
cb.ax.tick_params(labelsize=14)
plt.title("KGLD Reflectivity, 2 km AGL, 2021-05-26 15:56", size=15)
plt.scatter(
    radar_features["longitude"],
    radar_features["latitude"],
    70,
    transform=ccrs.PlateCarree(),
    color="grey",
)
[23]:
<matplotlib.collections.PathCollection at 0x13913f150>
../../_images/examples4doc_Example_Track_on_Radar_Segment_on_Satellite_Example_Track_on_Radar_Segment_on_Satellite_36_1.png
[24]:
# maximum space away in m,  maximum time away as Python Datetime object
goes_adj_features = tobac.utils.transform_feature_points(
    radar_features,
    goes_array_iris,
    max_time_away=datetime.timedelta(minutes=1),
    max_space_away=20 * 1000,
)
[25]:
# the transformed dataframe needs to have identical time to the data to segment on.
# replacement_dt = np.datetime64(in_goes_for_tobac['time'][0].values, 's')
# however, iris cannot deal with times in ms, so we need to drop the ms values.

# goes_adj_features['time'] = replacement_dt
[26]:
goes_adj_features
[26]:
frame idx hdim_1 hdim_2 num threshold_value feature time timestr projection_y_coordinate projection_x_coordinate altitude latitude longitude
index
0 0.0 5.0 372.0 806.0 19.0 30.0 1 2021-05-26 15:56:15 2021-05-26 15:56:23 58862.283732 71851.509127 71851.509127 39.893281 -100.858070
1 0.0 9.0 376.0 791.0 6.0 40.0 2 2021-05-26 15:56:15 2021-05-26 15:56:23 50469.608638 40740.283179 40740.283179 39.819857 -101.223255
2 0.0 11.0 393.0 777.0 54.0 50.0 3 2021-05-26 15:56:15 2021-05-26 15:56:23 -1221.242078 25305.251764 25305.251764 39.355590 -101.405959
3 0.0 12.0 379.0 781.0 7.0 50.0 4 2021-05-26 15:56:15 2021-05-26 15:56:23 43316.368018 17254.571781 17254.571781 39.756323 -101.498434

Segmentation:

[27]:
parameters_segmentation = dict()
parameters_segmentation["method"] = "watershed"
parameters_segmentation["threshold"] = 235
parameters_segmentation["target"] = "minimum"
[28]:
# Perform segmentation and save results:
print('Starting segmentation.')
seg_data, seg_feats = tobac.segmentation.segmentation(
    goes_adj_features, goes_array_iris, dxy=2000, **parameters_segmentation
)
print('segmentation performed, start saving results to files')
iris.save([seg_data], savedir / 'Mask_Segmentation_sat.nc', zlib=True, complevel=4)
seg_feats.to_hdf(savedir / 'Features_sat.h5', 'table')
print('segmentation performed and saved')
Starting segmentation.
segmentation performed, start saving results to files
segmentation performed and saved
[29]:
seg_data_xr = xr.DataArray.from_iris(seg_data)
[30]:
# In Py-ART's graphing suite, there is a display class similar to RadarMapDisplay,
# but for grids. To plot the grid:
fig = plt.figure(figsize=[10, 8])
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent([-101.7, -100.7, 39, 40], crs=ccrs.PlateCarree())

contoured = ax.contourf(
    in_goes_for_tobac["longitude"],
    in_goes_for_tobac["latitude"],
    in_goes_for_tobac["C14_brightness_temperature"][0],
    transform=ccrs.PlateCarree(),
    cmap=enhanced_colormap(), vmin = 200, vmax = 300, levels = 31
)
plt.xlim(-101.7, -100.7)
plt.ylim(39.0, 40)
unique_seg = np.unique(seg_data_xr)
color_map = cmaps.plasma(np.linspace(0, 1, len(unique_seg)))

# we have one feature without a segmented area
curr_feat = goes_adj_features[goes_adj_features["feature"] == 1]
plt.scatter(
    curr_feat["longitude"],
    curr_feat["latitude"],
    70,
    transform=ccrs.PlateCarree(),
    color="grey",
)


for seg_num, color in zip(unique_seg, color_map):
    if seg_num == 0 or seg_num == -1:
        continue
    curr_seg = (seg_data_xr == seg_num).astype(int)
    ax.contour(
        seg_data_xr["longitude"],
        seg_data_xr["latitude"],
        curr_seg,
        colors=[
            color,
        ],
        levels=[
            0.9,1
        ],
        linewidths=3,
    )
    curr_feat = goes_adj_features[goes_adj_features["feature"] == seg_num]
    plt.scatter(
        curr_feat["longitude"],
        curr_feat["latitude"],
        70,
        transform=ccrs.PlateCarree(),
        color=color,
        edgecolors = 'black',
        zorder = 10
    )

cb = plt.colorbar(contoured)
cb.set_label("C14 Brightness Temperature (K)", size=14)
cb.ax.tick_params(labelsize=14)
# plt.title("KGLD Reflectivity, 2 km AGL, 2021-05-26 15:56", size=15)
# plt.savefig("./radar_example_2/satellite_wseg.png", facecolor='w', bbox_inches='tight')
../../_images/examples4doc_Example_Track_on_Radar_Segment_on_Satellite_Example_Track_on_Radar_Segment_on_Satellite_44_0.png

result: - convective anvils could be connected to two respective radar cells - ignoring parallax shifts can cause assignment problems for smaller anvils (see purple radar cell)