Source code for tracmass.trajs

"""Trajectory analysis utilities for TRACMASS-JAX.

Provides functions for computing physical properties from trajectory
DataFrames produced by :class:`~tracmass.particle.ParticleRecorder`.
"""

from __future__ import annotations

import numpy as np
import pandas as pd

from tracmass.trajs.coords import lonlat_to_xy, xy_to_lonlat

__all__ = ["speeds", "xy_to_lonlat", "lonlat_to_xy"]


[docs] def speeds( df: pd.DataFrame, dxdy, ) -> pd.DataFrame: """Compute inter-snapshot speeds for all particles in a trajectory DataFrame. For each consecutive pair of snapshots per particle, computes the physical displacement in metres and the speed in m/s. The effective cell width used for displacement conversion is ``sqrt(dxdy_cell)``, which is exact for square cells and the geometric-mean approximation for non-square cells. Parameters ---------- df : pd.DataFrame Trajectory data as produced by :class:`~tracmass.particle.ParticleRecorder`. Required columns: ``id``, ``x``, ``y``, ``tt``, ``window``. If an ``event_type`` column is present, only ``"snapshot"`` rows are used. If *dxdy* is a 2-D array, ``ia`` and ``ja`` must also be present. dxdy : float, array-like of shape (imt, jmt), or GridFields Cell area in m².\n - **scalar** — uniform grid; cell width is ``sqrt(dxdy)`` in both directions. Pass e.g. ``250.0 ** 2`` for 250 m Cartesian cells. - **2-D array** — spatially varying grid; the cell area at the *starting* position of each interval is looked up using the 1-based ``ia``/``ja`` columns. Pass ``np.asarray(grid.dxdy)`` or any array-like of the right shape. - **GridFields** (or any object with a ``.dxdy`` attribute) — the ``.dxdy`` attribute is used as the 2-D array. Pass ``grid`` directly. Returns ------- pandas.DataFrame One row per consecutive snapshot pair per particle, with columns: ``id`` Particle identifier. ``window`` Window index of the *later* snapshot. ``dt`` Time elapsed between snapshots in seconds. ``dx_m`` x-displacement in metres (signed). ``dy_m`` y-displacement in metres (signed). ``speed`` Euclidean speed in m/s. Examples -------- Uniform grid (e.g. Fabboni test case, 250 m cells): >>> import pandas as pd >>> from tracmass.trajs import speeds >>> df = pd.read_parquet("traj.parquet") >>> sp = speeds(df, dxdy=250.0 ** 2) >>> sp["speed"].max() Spatially varying grid from a GridFields object: >>> import tracmass >>> grid, lazy = tracmass.new_flux(tracmass.gcm.glorys.fluxes) >>> sp = speeds(df, grid) """ # Accept GridFields or anything with a .dxdy attribute if hasattr(dxdy, "dxdy"): dxdy = np.asarray(dxdy.dxdy) scalar = np.ndim(dxdy) == 0 if not scalar: dxdy = np.asarray(dxdy) # Filter to snapshot events if the column is present if "event_type" in df.columns: df = df[df["event_type"] == "snapshot"] if df.empty: return pd.DataFrame(columns=["id", "window", "dt", "dx_m", "dy_m", "speed"]) df = df.sort_values(["id", "window"]) grp = df.groupby("id", sort=False) dx = grp["x"].diff() dy = grp["y"].diff() dt = grp["tt"].diff() if scalar: cell_size = np.sqrt(float(dxdy)) else: ia_s = grp["ia"].shift(1).fillna(1).astype(int).values - 1 # 1-based → 0-based ja_s = grp["ja"].shift(1).fillna(1).astype(int).values - 1 cell_size = np.sqrt(dxdy[ia_s, ja_s]) dx_m = dx * cell_size dy_m = dy * cell_size mask = (dt > 0).values if not mask.any(): return pd.DataFrame(columns=["id", "window", "dt", "dx_m", "dy_m", "speed"]) return pd.DataFrame({ "id": df["id"].values[mask], "window": df["window"].values[mask], "dt": dt.values[mask], "dx_m": dx_m.values[mask], "dy_m": dy_m.values[mask], "speed": (np.sqrt(dx_m.values[mask] ** 2 + dy_m.values[mask] ** 2) / dt.values[mask]), })