CESM2 LENS with Tracking + Measures

This tutorial demonstrates how to use Ocetrac for:

  • Identifying and tracking spatial-temporal objects in climate data (one ensemble member of CESM2 LENS)

  • Calculates characteristics of detected objects

First import the necessary libraries and modules:

[1]:
import numpy as np
import xarray as xr

import cmocean
import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from cartopy.mpl.ticker import LongitudeFormatter,LatitudeFormatter
from cartopy.util import add_cyclic_point
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import Rectangle
import matplotlib.dates as mdates
[2]:
import ocetrac

from utils import cesm2_lens_utils
from utils import cesm_anomalies
from measures.shape_measures import ShapeMeasures
from measures.motion_measures import MotionMeasures
from measures.temporal_measures import get_initial_detection_time, get_duration
from measures.intensity_measures import calculate_intensity_metrics
from measures.plotting import plot_displacement
from measures.measures_utils import convert_lon, get_object_masks, run_shape_measures, run_motion_measures, run_temporal_measures, run_intensity_measures, process_objects_and_calculate_measures

Input specifications and preprocessing

[3]:
# Define variable name and component
var = 'SST'        # Variable of interest (Sea Surface Temperature)
comp = 'atm'       # Model component (atmosphere)

# Construct directory path using f-string
directory = f'/glade/campaign/cgd/cesm/CESM2-LE/{comp}/proc/tseries/month_1/{var}/'

# Ensemble member index (0 = first member)
ens_memb_index = 0

# Load historical and future datasets using utility function
ds_var_hist_var, ds_var_fut_var = cesm2_lens_utils.get_ds_var(
    directory,
    var,
    comp,
    ens_memb_index)

# Compute variable data (load into memory) and mask land points
var_ds = ds_var_hist_var[var].compute()
# Replace 0s with NaN (land masking)
var_ds_noland = var_ds.where(var_ds != 0, np.nan)

ds_var_hist_var[var]
[3]:
<xarray.DataArray 'SST' (time: 1980, lat: 192, lon: 288)> Size: 438MB
dask.array<concatenate, shape=(1980, 192, 288), dtype=float32, chunksize=(1, 192, 288), chunktype=numpy.ndarray>
Coordinates:
  * lat      (lat) float64 2kB -90.0 -89.06 -88.12 -87.17 ... 88.12 89.06 90.0
  * lon      (lon) float64 2kB 0.0 1.25 2.5 3.75 5.0 ... 355.0 356.2 357.5 358.8
  * time     (time) object 16kB 1850-02-01 00:00:00 ... 2015-01-01 00:00:00
Attributes:
    units:         K
    long_name:     sea surface temperature
    cell_methods:  time: mean
[4]:
# Decompose into mean, trend, seasonal cycle, and residuals
# For this example, we will only look at the years 1979 to 2015.
# Looking at the 90th_percentile threshold
mean, trend, seas, features_notrend, var_notrend = cesm_anomalies.calculate_anomalies_trend_features(
    ds=var_ds_noland.sel(time=slice('1979-01','2014-12')),
    threshold_perc=0.9)

# Thresholded anomaly fields
full_mask_land = features_notrend

# Mask out zeros (likely land/ice) by setting them to NaN
full_masked = full_mask_land.where(full_mask_land != 0)

# Binary mask: True = valid ocean data, False = NaN (land/invalid)
binary_out_afterlandmask=np.isfinite(full_masked)
# Secondary mask based on mean field (e.g., climatology or time-mean)
newmask = np.isfinite(mean)

# Force evaluation if using lazy arrays (Dask)
binary_out_afterlandmask = binary_out_afterlandmask.compute()
newmask = newmask.compute()
/glade/u/home/cassiacai/.conda/envs/test_ocetrac/lib/python3.9/site-packages/numpy/lib/nanfunctions.py:1563: RuntimeWarning: All-NaN slice encountered
  return function_base._ureduce(a,

Tracking

[5]:
obj_Tracker = ocetrac.Tracker(
    binary_out_afterlandmask,
    newmask,
    radius=3, #user-defined parameter
    min_size_quartile= 0.75, #user-defined parameter
    timedim = 'time',
    xdim = 'lon',
    ydim='lat',
    positive=True)

blobs = obj_Tracker.track()
mo = obj_Tracker._morphological_operations()
minimum area: 311.0
initial objects identified       5150
final objects tracked    386

Measures

For this example, we can select an ID that has a footprint at some point over its lifetime in the Pacific Ocean:

[6]:
# Defining our region (Pacific Ocean)
upper_lat = 65
lower_lat = 5
left_lon = 150
right_lon = 250

anomalies_NP = var_notrend.sel(lon=slice(left_lon, right_lon),
                                       lat=slice(lower_lat,upper_lat))
mhw_objs_NP = blobs.sel(lon=slice(left_lon, right_lon),
                                       lat=slice(lower_lat,upper_lat))
print(np.unique(mhw_objs_NP.data)[:-1])
[  4.   8.  11.  16.  18.  24.  27.  28.  35.  38.  59.  62.  63.  71.
  82.  84.  85.  88. 105. 111. 121. 124. 128. 130. 131. 137. 145. 149.
 153. 154. 168. 174. 175. 177. 178. 194. 201. 207. 208. 212. 217. 221.
 225. 226. 227. 231. 232. 235. 240. 253. 254. 258. 259. 260. 262. 267.
 272. 275. 281. 282. 283. 289. 292. 293. 294. 301. 304. 305. 308. 312.
 313. 314. 318. 325. 327. 330. 332. 337. 339. 346. 350. 369. 378. 379.
 381. 382.]

3.1 One event

[63]:
event_binary, event_intensity = get_object_masks(blobs, var_notrend, object_id=226.)
[64]:
fig, axes = plt.subplots(5, 1, figsize=(4, 8),
                        subplot_kw={'projection': ccrs.PlateCarree(central_longitude=180)})
axes = axes.flatten()

for i, ax in enumerate(axes[:11]):  # Only plot first 5
    contour = event_intensity.sel(lat=slice(-65, 65))[i,:,:].plot.contourf(
        ax=ax,
        transform=ccrs.PlateCarree(),
        cmap=cmocean.cm.balance,
        levels=17,
        vmin=-4,
        vmax=4,
        add_colorbar=False,
        extend='neither'
    )

    ax.add_feature(cfeature.COASTLINE, linewidth=0.5)

    gl = ax.gridlines(draw_labels=True, linewidth=0.3, color='gray', alpha=0.5, linestyle='--')
    gl.top_labels = False
    gl.right_labels = False

    time_str = str(event_intensity.time[i].values)[:10]
    ax.set_title(time_str, fontsize=10, pad=4)

cbar_ax = fig.add_axes([0.15, -0.03, 0.7, 0.02])  # [left, bottom, width, height]
cbar = fig.colorbar(contour, cax=cbar_ax, orientation='horizontal')
cbar.set_label('Intensity', fontsize=10)

plt.tight_layout()
plt.show()
/glade/derecho/scratch/cassiacai/tmp/ipykernel_106795/3397323041.py:30: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
  plt.tight_layout()
../_images/examples_cesm2lens_with_measures_14_1.png

Shape

[65]:
# Define spatial resolutions
lon_resolution_value = 111.320  # km per degree longitude
lat_resolution_value = 110.574  # km per degree latitude

# Instantiate the class
metrics = ShapeMeasures(lat_resolution=lat_resolution_value,
                       lon_resolution=lon_resolution_value,
                       use_decorators=False)

# Area-related metrics
spatial_extent_data = metrics.calc_spatial_extents(event_binary)

# Perimeter
perimeters = metrics.calc_perimeter(event_binary)

# Complement to deformation
coords_full = spatial_extent_data['coords_full']
spatial_extents = spatial_extent_data['spatial_extents']
comp_to_deform = metrics.calc_complement_to_deformation(coords_full, spatial_extents)

# Deformation
deform = metrics.calc_deformation(comp_to_deform)

# Convex hull area vs. object area
convex_hull_areas, ratio_convexhullarea_vs_area = metrics.calc_ratio_convexhullarea_vs_area(event_binary)

# Circularity
circularity = metrics.calc_circularity(spatial_extents, perimeters)
[66]:
print(perimeters)
print(spatial_extents)
print(spatial_extent_data['max_spatial_extent'])
print(spatial_extent_data['mean_spatial_extent'])
print(comp_to_deform)
print(deform)
print(convex_hull_areas)
print(circularity)
[12840.466685413652, 19877.813417377176, 21557.817559868552, 40139.67288988289, 62383.97281866282, 20879.48653034418, 18256.04348638295, 42787.0662891526, 42552.186868265606, 28239.780415804853, 29001.529284735137]
[5566139.569472478, 9954003.884386927, 8429047.029220862, 15338648.677684981, 29880430.636823323, 7619839.288714777, 9934519.26527951, 16395461.811288461, 15681878.02342612, 11373152.14366585, 9559966.063241929]
29880430.636823323
12703007.853927748
[0.28291897 0.25376353 0.15305238 0.27424141 0.13374565 0.37958403
 0.26929119 0.24446706 0.30225025 0.36259685]
[0.71708103 0.74623647 0.84694762 0.72575859 0.86625435 0.62041597
 0.73070881 0.75553294 0.69774975 0.63740315]
[6717623.950409378, 14062736.655204404, 14348913.240474159, 39167391.661781386, 55414750.97351513, 11516622.0136969, 11553863.508841246, 25611463.36680384, 24426431.802653592, 14033875.710066216, 13861783.47039088]
[0.4242309398748281, 0.31657050595140324, 0.227918068861202, 0.11963253525012466, 0.09648296895552338, 0.21964233254309426, 0.3745789752217847, 0.11254041131552377, 0.10883389356962263, 0.17921241078988423, 0.14283163772085983]

Motion

[67]:
event_binary_lon = convert_lon(event_binary)
event_binary['lon'] = event_binary_lon['lon_180']
event_intensity['lon'] = event_binary_lon['lon_180']
[68]:
motion = MotionMeasures(use_decorators=False)

# Calculate centroids
num_centroids = []
centroid_coords = []

for timestep in range(event_binary.time.shape[0]):
    centroids = motion.calculate_centroids(event_binary, timestep)
    num_centroids.append(len(centroids))
    centroid_coords.append(centroids)

print("Number of centroids per timestep:", num_centroids)

# Calculate centroid displacements
centroid_path, centroid_displacements = motion.calculate_centroid_displacement(event_binary)
print("Centroid path:", centroid_path)
print("Displacements (km):", centroid_displacements)

# Calculate centers of mass
num_coms = []
com_coords = []

for timestep in range(event_binary.time.shape[0]):
    coms = motion.calculate_coms(
        event_binary,
        event_intensity, timestep)
    num_coms.append(len(coms))
    com_coords.append(coms)

print("Number of COMs per timestep:", num_coms)

# Calculate COM displacements
com_path, com_displacements = motion.calculate_com_displacement(event_intensity)
print("COM path:", com_path)
print("Displacements (km):", com_displacements)

# Calculate directionality
com_dir = motion.calculate_directionality(com_path)
centroid_dir = motion.calculate_directionality(centroid_path)

print("\nCOM Directionality:")
for k, v in com_dir.items():
    print(f"{k}: {v}")

print("\nCentroid Directionality:")
for k, v in centroid_dir.items():
    print(f"{k}: {v}")
Number of centroids per timestep: [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1]
Centroid path: [(-48.53403141361257, 68.75), (-48.53403141361257, 77.5), (-54.18848167539267, 62.5), (-25.916230366492147, 70.0), (-24.973821989528798, 85.0), (-3.2984293193717282, 70.0), (-1.4136125654450267, 73.75), (7.0680628272251305, 87.5), (5.183246073298429, 112.5), (-4.240837696335079, 111.25), (-2.3560209424083776, 120.0)]
Displacements (km): [643.9159909517981, 1212.9228472124582, 3203.7696422105755, 1508.9501219600338, 2896.007441149724, 466.35683999533256, 1793.6277553728703, 2771.556364008446, 1057.0683232357997, 993.651900724373]
Number of COMs per timestep: [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1]
COM path: [(-47.59162303664922, 70.0), (-46.64921465968587, 80.0), (-55.13089005235602, 62.5), (-25.916230366492147, 70.0), (-24.973821989528798, 85.0), (-3.2984293193717282, 70.0), (-1.4136125654450267, 73.75), (8.01047120418848, 87.5), (5.183246073298429, 111.25), (-5.18324607329843, 111.25), (-2.3560209424083776, 118.75)]
Displacements (km): [763.3106070618832, 1540.7598609644376, 3305.4942055336974, 1508.9501219600338, 2896.007441149724, 466.35683999533256, 1850.044458144025, 2641.6411673494895, 1152.7029259811266, 889.4786592982161]

COM Directionality:
mean_delta_lon: 4.523560209424084
mean_delta_lat: 4.875
mean_angle: 47.141459886374875
direction: northward (meridional-dominated)
movement_count: 10

Centroid Directionality:
mean_delta_lon: 4.617801047120419
mean_delta_lat: 5.125
mean_angle: 47.98006183791712
direction: northward (meridional-dominated)
movement_count: 10
[70]:
plot_displacement(centroid_path, event_intensity)
../_images/examples_cesm2lens_with_measures_21_0.png

Temporal

[71]:
initial_detection_date = get_initial_detection_time(
    event_binary,
    units="days since 1850-01-01",
    calendar="noleap")
duration = get_duration(event_binary)

print(initial_detection_date)
print(duration)
1998-12-01 00:00:00
11

Intensity

[72]:
intensity_metrics = calculate_intensity_metrics(
    event_intensity)

intensity_metrics = calculate_intensity_metrics(
    event_intensity, quantile_threshold=0.90)

print(f"Maximum intensity: {intensity_metrics['max_intensity']}")
print(f"90th percentile timeseries: {intensity_metrics['percentile_90_intensity_timeseries']}")
Maximum intensity: 2.1457733522070725
90th percentile timeseries: <xarray.DataArray (time: 11)> Size: 88B
array([0.82347254, 1.10577674, 1.08106423, 1.19724209, 1.10096886,
       0.99251082, 1.21014004, 1.12043086, 1.1920588 , 1.48183421,
       1.17666015])
Coordinates:
  * time      (time) object 88B 1998-12-01 00:00:00 ... 1999-10-01 00:00:00
    quantile  float64 8B 0.9

3.2 Multiple events and multiple measures

[73]:
# One event, multiple measures
run_shape = True
run_motion = True
run_temporal = True
run_intensity = True

measure_results = {}

event_binary, event_intensity = get_object_masks(blobs, var_notrend, object_id=226.)

if run_shape:
    measure_results['shape_measures'] = run_shape_measures(
        event_binary,
        lon_resolution_value,
        lat_resolution_value)
    print("Shape Measures complete.")

if run_motion:
    measure_results['motion_measures'] = run_motion_measures(
        event_binary,
        event_intensity)
    print("Motion Measures complete.")

if run_temporal:
    measure_results['temporal_measures'] = run_temporal_measures(
        event_binary)
    print("Temporal Measures complete.")

if run_intensity:
    measure_results['intensity_measures'] = run_intensity_measures(
        event_intensity)
    print("Intensity Measures complete.")
Shape Measures complete.
Motion Measures complete.
Temporal Measures complete.
Intensity Measures complete.
[74]:
# Multiple events, multiple measures
my_object_ids = [8., 11., 16.] #

run_shape_flag = True
run_motion_flag = False # Example: turn off motion measures
run_temporal_flag = True
run_intensity_flag = True

results_for_my_objects = process_objects_and_calculate_measures(
    my_object_ids,
    blob_data = blobs,
    intensity_data = var_notrend,
    run_shape=run_shape_flag,
    run_motion=run_motion_flag,
    run_temporal=run_temporal_flag,
    run_intensity=run_intensity_flag,
    lon_resolution_value=lon_resolution_value,
    lat_resolution_value=lat_resolution_value
)
Shape Measures complete.
Temporal Measures complete.
Intensity Measures complete.
Shape Measures complete.
Temporal Measures complete.
Intensity Measures complete.
Shape Measures complete.
Temporal Measures complete.
Intensity Measures complete.
[75]:
results_for_my_objects
[75]:
{8.0: {'shape_measures': {'perimeters': [9747.292089650471],
   'spatial_extents': [2584050.3227785327],
   'max_spatial_extent': 2584050.3227785327,
   'mean_spatial_extent': 2584050.3227785327,
   'complement_to_deformation': array([], dtype=float64),
   'deformation': array([], dtype=float64),
   'convex_hull_areas': [3513587.4055634644],
   'ratio_convexhullarea_vs_area': [0.7354450094757599],
   'circularity': [0.3417770289684867]},
  'temporal_measures': {'initial_detection_date': cftime.DatetimeNoLeap(1979, 4, 1, 0, 0, 0, 0, has_year_zero=True),
   'duration': 1},
  'intensity_measures': {'max_intensity': 2.8889924833117675,
   'percentile_90_intensity_timeseries': <xarray.DataArray (time: 1)> Size: 8B
   array([2.22573869])
   Coordinates:
     * time      (time) object 8B 1979-04-01 00:00:00
       quantile  float64 8B 0.9}},
 11.0: {'shape_measures': {'perimeters': [12328.482711578845],
   'spatial_extents': [4068020.4550980856],
   'max_spatial_extent': 4068020.4550980856,
   'mean_spatial_extent': 4068020.4550980856,
   'complement_to_deformation': array([], dtype=float64),
   'deformation': array([], dtype=float64),
   'convex_hull_areas': [4534322.453004645],
   'ratio_convexhullarea_vs_area': [0.8971617032666993],
   'circularity': [0.3363362913350825]},
  'temporal_measures': {'initial_detection_date': cftime.DatetimeNoLeap(1979, 7, 1, 0, 0, 0, 0, has_year_zero=True),
   'duration': 1},
  'intensity_measures': {'max_intensity': 1.561326204174918,
   'percentile_90_intensity_timeseries': <xarray.DataArray (time: 1)> Size: 8B
   array([1.4500794])
   Coordinates:
     * time      (time) object 8B 1979-07-01 00:00:00
       quantile  float64 8B 0.9}},
 16.0: {'shape_measures': {'perimeters': [11498.378854624789],
   'spatial_extents': [3539317.4219226753],
   'max_spatial_extent': 3539317.4219226753,
   'mean_spatial_extent': 3539317.4219226753,
   'complement_to_deformation': array([], dtype=float64),
   'deformation': array([], dtype=float64),
   'convex_hull_areas': [4502545.207764898],
   'ratio_convexhullarea_vs_area': [0.7860703798862294],
   'circularity': [0.3364001262181062]},
  'temporal_measures': {'initial_detection_date': cftime.DatetimeNoLeap(1980, 1, 1, 0, 0, 0, 0, has_year_zero=True),
   'duration': 1},
  'intensity_measures': {'max_intensity': 2.445347845526044,
   'percentile_90_intensity_timeseries': <xarray.DataArray (time: 1)> Size: 8B
   array([2.15074526])
   Coordinates:
     * time      (time) object 8B 1980-01-01 00:00:00
       quantile  float64 8B 0.9}}}
[ ]: