Front Lifecycle Analysis¶
This notebook demonstrates how to use FrontTracker to follow the lifecycle of an oceanic front through its detected path.
The path is constructed by combining automated detection with manual corrections, ensuring continuity across consecutive timesteps.
In this demo, we will:
- Build the frontal path (lifecycle) by selecting the appropriate label for each day.
- Extract daily metrics for the tracked front, including
area,length,width,frontal_line, and gradient magnitude statistics (gm_mean,gm_min,gm_max). - Visualize the lifecycle by plotting the spatial evolution of the front on maps for each day.
- Summarize temporal variability with time series of:
- Frontal area to show changes in spatial extent.
- Gradient magnitude statistics to highlight variations in frontal intensity.
This notebook provides a step-by-step example of how a single front can be tracked, quantified, and visualized throughout its lifecycle, complementing the static front identification presented in previous demos.
In [1]:
Copied!
# -----------------------------------
# LIBRARY IMPORTS
# -----------------------------------
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from descartes import PolygonPatch
import cartopy
from matplotlib.colors import ListedColormap
from fronttracker import FrontTracker
# -----------------------------------
# LOAD DATA
# -----------------------------------
gm = np.transpose(np.load(".../gm.npy"), (2, 1, 0))
latitude = np.load(".../latitude.npy")
longitude = np.load(".../longitude.npy")
time = np.load(".../time.npy", allow_pickle=True)
time = np.arange(np.datetime64(time[0].strftime("%Y-%m-%d")), np.datetime64(time[-1].strftime("%Y-%m-%d")) + np.timedelta64(1, 'D'),np.timedelta64(1, 'D'))
# -----------------------------------
# FRONT THRESHOLD
# -----------------------------------
treshold = 0.08
# -----------------------------------
# FRONT IDENTIFICATION AND TRACKING
# -----------------------------------
# Initialize the front tracker
front_tracker = FrontTracker()
# Identify fronts in the GM field using the threshold defined earlier
front_tracker.identify_fronts(time, longitude, latitude, gm, treshold=treshold)
# Retrieve the tabular data of all detected front pixels
front_tracker.data
# -----------------------------------
# LIBRARY IMPORTS
# -----------------------------------
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from descartes import PolygonPatch
import cartopy
from matplotlib.colors import ListedColormap
from fronttracker import FrontTracker
# -----------------------------------
# LOAD DATA
# -----------------------------------
gm = np.transpose(np.load(".../gm.npy"), (2, 1, 0))
latitude = np.load(".../latitude.npy")
longitude = np.load(".../longitude.npy")
time = np.load(".../time.npy", allow_pickle=True)
time = np.arange(np.datetime64(time[0].strftime("%Y-%m-%d")), np.datetime64(time[-1].strftime("%Y-%m-%d")) + np.timedelta64(1, 'D'),np.timedelta64(1, 'D'))
# -----------------------------------
# FRONT THRESHOLD
# -----------------------------------
treshold = 0.08
# -----------------------------------
# FRONT IDENTIFICATION AND TRACKING
# -----------------------------------
# Initialize the front tracker
front_tracker = FrontTracker()
# Identify fronts in the GM field using the threshold defined earlier
front_tracker.identify_fronts(time, longitude, latitude, gm, treshold=treshold)
# Retrieve the tabular data of all detected front pixels
front_tracker.data
Out[1]:
| time | longitude | latitude | gm | labels | |
|---|---|---|---|---|---|
| 0 | 2023-11-27 | 100.375 | 9.125 | 0.082830 | 0 |
| 1 | 2023-11-27 | 100.375 | 9.375 | 0.087372 | 0 |
| 2 | 2023-11-27 | 100.375 | 9.625 | 0.082713 | 0 |
| 3 | 2023-11-27 | 100.625 | 9.375 | 0.083204 | 0 |
| 4 | 2023-11-27 | 103.125 | 9.875 | 0.080204 | 1 |
| ... | ... | ... | ... | ... | ... |
| 8177 | 2024-04-21 | 102.875 | 11.125 | 0.081668 | 817 |
| 8178 | 2024-04-21 | 102.875 | 11.375 | 0.133640 | 817 |
| 8179 | 2024-04-21 | 102.875 | 11.625 | 0.119224 | 817 |
| 8180 | 2024-04-21 | 103.625 | 10.375 | 0.088009 | 818 |
| 8181 | 2024-04-21 | 103.625 | 10.625 | 0.086561 | 818 |
8182 rows × 5 columns
In [2]:
Copied!
# Link fronts between consecutive time steps (temporal tracking)
front_tracker.track()
# Retrieve a DataFrame that maps each front label to its next label in the tracking path
front_tracker.data_labels
# Link fronts between consecutive time steps (temporal tracking)
front_tracker.track()
# Retrieve a DataFrame that maps each front label to its next label in the tracking path
front_tracker.data_labels
Out[2]:
| labels | next_labels | |
|---|---|---|
| 0 | 0 | None |
| 4 | 1 | None |
| 5 | 2 | 6 |
| 7 | 3 | None |
| 23 | 4 | 8 |
| ... | ... | ... |
| 8163 | 814 | None |
| 8165 | 815 | None |
| 8166 | 816 | None |
| 8172 | 817 | None |
| 8180 | 818 | None |
819 rows × 2 columns
In [3]:
Copied!
# Build the frontal path by combining automated detection with manual corrections
frontal_path = [None]
frontal_path += front_tracker.get_frontal_path(7)
frontal_path += front_tracker.get_frontal_path(548)[:2]
frontal_path += front_tracker.get_frontal_path(559)
frontal_path += front_tracker.get_frontal_path(579)[:3]
frontal_path += front_tracker.get_frontal_path(598)
frontal_path += front_tracker.get_frontal_path(700)
frontal_path += front_tracker.get_frontal_path(728)
frontal_path += front_tracker.get_frontal_path(789)
frontal_path.append(None)
print(f"Frontal path length: {len(frontal_path)}")
print(frontal_path)
# Build the frontal path by combining automated detection with manual corrections
frontal_path = [None]
frontal_path += front_tracker.get_frontal_path(7)
frontal_path += front_tracker.get_frontal_path(548)[:2]
frontal_path += front_tracker.get_frontal_path(559)
frontal_path += front_tracker.get_frontal_path(579)[:3]
frontal_path += front_tracker.get_frontal_path(598)
frontal_path += front_tracker.get_frontal_path(700)
frontal_path += front_tracker.get_frontal_path(728)
frontal_path += front_tracker.get_frontal_path(789)
frontal_path.append(None)
print(f"Frontal path length: {len(frontal_path)}")
print(frontal_path)
Frontal path length: 147 [None, 7, 9, 13, 19, 26, 31, 36, 40, 44, 50, 56, 58, 60, 61, 63, 68, 73, 77, 83, 88, 92, 97, 102, 107, 112, 117, 124, 131, 139, 144, 148, 154, 159, 165, 171, 178, 183, 191, 198, 205, 213, 220, 227, 234, 242, 249, 257, 264, 271, 281, 288, 294, 298, 305, 312, 319, 326, 332, 339, 345, 351, 356, 361, 366, 372, 377, 383, 387, 389, 394, 396, 398, 402, 408, 411, 415, 419, 421, 425, 429, 434, 437, 440, 444, 449, 452, 455, 458, 461, 465, 470, 477, 482, 488, 495, 500, 508, 515, 523, 530, 536, 542, 548, 553, 559, 567, 574, 579, 584, 590, 598, 609, 616, 624, 633, 641, 649, 658, 665, 672, 680, 687, 693, 700, 708, 715, 719, 723, 728, 732, 738, 744, 749, 754, 759, 766, 773, 778, 785, 789, 792, 795, 803, 810, 814, None]
In [4]:
Copied!
# -----------------------------------
# VISUALIZATION SETUP
# -----------------------------------
# Create a large figure with a grid of 11 rows × 14 columns
fig = plt.figure(figsize=(4.8*4, 6.2*4))
nrows, ncols = 11, 14
for k in range(len(time)):
ax = fig.add_subplot(nrows, ncols, k+1, projection=cartopy.crs.PlateCarree())
land = cartopy.feature.NaturalEarthFeature('physical', 'land', scale="10m", edgecolor='k', facecolor=cartopy.feature.COLORS['land'])
ax.add_feature(land, facecolor='lightgray', linewidth=0.25)
gl = ax.gridlines(draw_labels=True, linewidth=0.25, color='gray', alpha=0.5, linestyle='--')
gl.right_labels = False
if k % ncols != 0:
gl.left_labels = False
# Plot gradient magnitude (GM) field
pc = ax.pcolor(longitude, latitude, gm[k], vmin=0.0, vmax=0.3, cmap="jet")
# If a frontal path exists for this timestep
if frontal_path[k] is not None:
label = frontal_path[k]
# Get all detected fronts at this time
_df = front_tracker.data[front_tracker.data.time == time[k]]
uniq = _df.labels.unique()
for _label in uniq:
front = front_tracker.fronts[_label]
# Try to compute contours
try:
front.contouring()
except Exception:
continue
# Highlight the tracked front in pink, others in orange
color = "deeppink" if _label == label else "orange"
# Draw each polygon of the front contours
for poly in front.contours:
x, y = poly.exterior.xy
ax.plot(x, y, c=color, linewidth=2)
ax.text(102, 4.8, k, color="red", va="center", ha="center", fontsize=16)
plt.show()
# -----------------------------------
# VISUALIZATION SETUP
# -----------------------------------
# Create a large figure with a grid of 11 rows × 14 columns
fig = plt.figure(figsize=(4.8*4, 6.2*4))
nrows, ncols = 11, 14
for k in range(len(time)):
ax = fig.add_subplot(nrows, ncols, k+1, projection=cartopy.crs.PlateCarree())
land = cartopy.feature.NaturalEarthFeature('physical', 'land', scale="10m", edgecolor='k', facecolor=cartopy.feature.COLORS['land'])
ax.add_feature(land, facecolor='lightgray', linewidth=0.25)
gl = ax.gridlines(draw_labels=True, linewidth=0.25, color='gray', alpha=0.5, linestyle='--')
gl.right_labels = False
if k % ncols != 0:
gl.left_labels = False
# Plot gradient magnitude (GM) field
pc = ax.pcolor(longitude, latitude, gm[k], vmin=0.0, vmax=0.3, cmap="jet")
# If a frontal path exists for this timestep
if frontal_path[k] is not None:
label = frontal_path[k]
# Get all detected fronts at this time
_df = front_tracker.data[front_tracker.data.time == time[k]]
uniq = _df.labels.unique()
for _label in uniq:
front = front_tracker.fronts[_label]
# Try to compute contours
try:
front.contouring()
except Exception:
continue
# Highlight the tracked front in pink, others in orange
color = "deeppink" if _label == label else "orange"
# Draw each polygon of the front contours
for poly in front.contours:
x, y = poly.exterior.xy
ax.plot(x, y, c=color, linewidth=2)
ax.text(102, 4.8, k, color="red", va="center", ha="center", fontsize=16)
plt.show()
In [5]:
Copied!
# -----------------------------------
# EXTRACT FRONT METRICS
# -----------------------------------
# Dictionary used to collect daily metrics of the front over its lifecycle
metrics = { "area": [], "width": [], "length": [], "frontal_line": [], "gm_mean": [], "gm_min": [], "gm_max": []}
# Loop over frontal_path (excluding the None separators at start and end)
for k in range(1, len(frontal_path) - 1):
label = frontal_path[k]
front = front_tracker.fronts[label]
# Compute metrics for this front
front.metrics()
# Store values in the corresponding lists
metrics["area"].append(front.area)
metrics["width"].append(front.width)
metrics["length"].append(front.length)
metrics["frontal_line"].append(front.skeleton.length)
metrics["gm_mean"].append(front.gm.mean())
metrics["gm_min"].append(front.gm.min())
metrics["gm_max"].append(front.gm.max())
# Convert lists to NumPy arrays and pad with zeros at both ends
for key in metrics:
metrics[key] = np.array([0] + metrics[key] + [0])
# -----------------------------------
# EXTRACT FRONT METRICS
# -----------------------------------
# Dictionary used to collect daily metrics of the front over its lifecycle
metrics = { "area": [], "width": [], "length": [], "frontal_line": [], "gm_mean": [], "gm_min": [], "gm_max": []}
# Loop over frontal_path (excluding the None separators at start and end)
for k in range(1, len(frontal_path) - 1):
label = frontal_path[k]
front = front_tracker.fronts[label]
# Compute metrics for this front
front.metrics()
# Store values in the corresponding lists
metrics["area"].append(front.area)
metrics["width"].append(front.width)
metrics["length"].append(front.length)
metrics["frontal_line"].append(front.skeleton.length)
metrics["gm_mean"].append(front.gm.mean())
metrics["gm_min"].append(front.gm.min())
metrics["gm_max"].append(front.gm.max())
# Convert lists to NumPy arrays and pad with zeros at both ends
for key in metrics:
metrics[key] = np.array([0] + metrics[key] + [0])
In [6]:
Copied!
# -----------------------------------
# PLOT FRONT METRICS
# -----------------------------------
# Create a large figure with a grid of 2 rows × 1 column
fig, axs = plt.subplots(2, 1, figsize=(6.2*2, 4.8*0.75))
# --- Panel 1: Frontal area ---
axs[0].plot(np.arange(0, len(time)), metrics["area"])
axs[0].set_ylim(0,65000)
axs[0].set_yticks(np.arange(0, 60001, 15000))
axs[0].set_ylabel(r"Area (km$^2$)")
axs[0].set_xlim(0,len(time)-1)
axs[0].set_xticks(np.arange(0, 150, 5))
# --- Panel 2: Gradient magnitude statistics ---
axs[1].plot(time[1:-1], metrics["gm_mean"][1:-1])
axs[1].fill_between(time[1:-1], metrics["gm_min"][1:-1], metrics["gm_max"][1:-1], alpha=0.3)
axs[1].set_ylim(0.05,0.3)
axs[1].set_yticks(np.arange(0.05, 0.31, 0.05))
axs[1].set_ylabel(r"GM (°C (10 km)$^{-1}$)")
axs[1].set_xlim(time[0],time[-1])
axs[1].set_xticks(time[::5])
axs[1].tick_params(axis='x', rotation=90)
plt.show()
# -----------------------------------
# PLOT FRONT METRICS
# -----------------------------------
# Create a large figure with a grid of 2 rows × 1 column
fig, axs = plt.subplots(2, 1, figsize=(6.2*2, 4.8*0.75))
# --- Panel 1: Frontal area ---
axs[0].plot(np.arange(0, len(time)), metrics["area"])
axs[0].set_ylim(0,65000)
axs[0].set_yticks(np.arange(0, 60001, 15000))
axs[0].set_ylabel(r"Area (km$^2$)")
axs[0].set_xlim(0,len(time)-1)
axs[0].set_xticks(np.arange(0, 150, 5))
# --- Panel 2: Gradient magnitude statistics ---
axs[1].plot(time[1:-1], metrics["gm_mean"][1:-1])
axs[1].fill_between(time[1:-1], metrics["gm_min"][1:-1], metrics["gm_max"][1:-1], alpha=0.3)
axs[1].set_ylim(0.05,0.3)
axs[1].set_yticks(np.arange(0.05, 0.31, 0.05))
axs[1].set_ylabel(r"GM (°C (10 km)$^{-1}$)")
axs[1].set_xlim(time[0],time[-1])
axs[1].set_xticks(time[::5])
axs[1].tick_params(axis='x', rotation=90)
plt.show()