Analysing trajectories#

Fortran TRACMASS ships its own post-processing (summaries, stream functions, divergences) and the pyTRAJ toolbox. TRACMASS-JAX writes trajectories to Parquet, so the usual Python tools work directly. This page collects common recipes.

Load a run#

import tracmass

df = tracmass.load(env="omi", lonlat=True)   # id, x, y, dtm, lon, lat
meta = df.attrs["tracmass.run"]
print(meta["gcm"], meta["direction"], meta["dtm_start"], meta["dtm_end"])

For large files, read only what you need:

df = tracmass.load(env="omi", columns=["id", "dtm", "x", "y"],
                   filters=[("window", "<", 30)])

Plot trajectories#

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
for _, traj in df.groupby("id"):
    ax.plot(traj.lon, traj.lat, lw=0.3, alpha=0.5)
start = df[df.dtm == df.dtm.min()]
ax.scatter(start.lon, start.lat, s=2, c="k", label="release")
ax.set_xlabel("longitude"); ax.set_ylabel("latitude"); ax.legend()

For millions of particles, a density map is more useful than lines:

import numpy as np

H, xe, ye = np.histogram2d(df.lon, df.lat, bins=(360, 180))
plt.pcolormesh(xe, ye, np.log10(H.T + 1))

Why particles stopped#

full = tracmass.load(env="omi", clean=False)
stops = full[full.event_type == "deactivated"]
print(stops.reason.value_counts())          # 2 = land, 3 = left the domain, ...

The codes are listed in Stopping trajectories.

Transit times#

The first time each particle enters a target region:

in_target = (df.lat > 60) & (df.lon.between(-30, 10))
first = df[in_target].groupby("id").dtm.min()
release = df.groupby("id").dtm.min()
transit_days = (first - release.loc[first.index]).dt.total_seconds() / 86400
transit_days.describe()

For a backward run, dtm decreases along each trajectory, so the same code gives the age of the water since it left the target region.

Cohorts of a repeat-seeded run#

df = tracmass.load(env="gog_upwelling", columns=["id", "dtm", "x", "y", "t0"])
df["age_days"] = (df.groupby("id").dtm.transform("min") - df.dtm).abs().dt.days
df.groupby("t0").id.nunique()                    # particles per release

Positions at fixed times across runs#

Positions are saved in the full-grid frame, so runs on different subsets can be compared directly:

a = tracmass.load(env="omi_small", filters={"window": 30})
b = tracmass.load(env="omi_large", filters={"window": 30})
merged = a.merge(b, on="id", suffixes=("_small", "_large"))

Geographic conversion without an adapter#

tracmass.trajs.xy_to_lonlat converts positions on any grid with longitude and latitude arrays (see Coordinates):

import types
from tracmass.trajs import xy_to_lonlat

grid = types.SimpleNamespace(lon=lon2d, lat=lat2d)
lon, lat = xy_to_lonlat(df.x, df.y, grid)