Stopping trajectories#

Fortran TRACMASS stops (“kills”) trajectories when they exceed a maximum time (timax), reach the surface, or enter a kill zone (exitType 1–4, set up in kill_zones.F90). TRACMASS-JAX does not have kill zones yet. Particles stop when they exceed their lifetime, leave the domain or hit land, or when a safety check stops them. Every stopped particle records why.

Reason codes#

ParticleState.reason (int16) holds the reason. The constants are exported from tracmass:

Code

Constant

Name in tallies and files

When

0

REASON_ACTIVE

active

Still running, or paused at a window boundary

1

REASON_TIME

time_limit

tt - t0 exceeded the trajectory lifetime

2

REASON_LAND

land

Moved into a land cell (kmt == 0)

3

REASON_BOUNDS_H

out_of_bounds_h

Left the domain horizontally, or entered the edge ring of a non-periodic axis

4

REASON_PROGRESS

no_progress

A sub-cycle made no progress in time

5

REASON_SLOW

too_slow

Fell too far behind the window’s pace

6

REASON_ITERLIMIT

iter_limit

The window reached max_sub_iter_per_window sub-cycles

7

REASON_DXYZ

zero_volume

The cell volume was zero or negative

8

REASON_BOUNDS_V

out_of_bounds_v

Left the domain vertically

The monitor prints a tally per window, for example stopped:3 (land:2, out_of_bounds_h:1). The recorder writes one event_type="deactivated" row per stopped particle, with its position and reason (see Output).

import numpy as np
import tracmass

result = tracmass.time_dependent(state, grid, lazy)
reasons = np.asarray(result.reason)
print((reasons == tracmass.REASON_LAND).sum(), "particles hit land")

Maximum trajectory time — traj_max_time#

The counterpart of Fortran’s timax. traj_max_time stops each particle a fixed time after its own release (reason time_limit):

[myrun]
traj_max_time = "90D"      # Timedelta strings: "30D", "12h", "2W", "90min"
tracmass.time_dependent(state, grid, lazy, traj_max_time="90D")
tracmass.time_dependent(state, grid, lazy, timax_seconds=86400.0)  # in seconds

It also accepts calendar units: "1M", "6M", "1Q", "1Y". pandas does not allow M or Y in a Timedelta, so here they always mean months and years; use "min" for minutes. A calendar month has no fixed length, so its length is measured once, from the run’s start (backward for a backward run). "1M" from 2010-02-01 is 28 days, and every cohort of a repeat-seeded run gets those same 28 days.

The argument wins over the setting. timax_seconds and traj_max_time cannot both be passed. A particle stops at the first sub-step that ends past its limit, so it can overshoot by up to one sub-step (timestep / iter_sub). The file metadata gets traj_max_time and timax_seconds.

Leaving the domain#

On every axis that does not wrap around (iperio = 0, jperio = 0), a particle that enters the outermost ring of cells stops with out_of_bounds_h. The flux through the outer faces of those cells is not part of the grid, so they cannot be integrated correctly. This matters mostly for subsets:

from tracmass.gcm.flux_grid import edge_mask

edge = edge_mask(grid)        # (imt, jmt) bool, True on the edge ring

Leave a margin of at least one cell between the seeding box and the edges of the subset. Particles seeded in an edge cell stop on their first step.

Land#

A particle that moves into a cell with kmt == 0 stops with land. With correct fluxes this should not happen, because the flux into a land cell is zero. The kernel also forces zero flux on every face next to land. A small number of land stops (around 0.001–0.003 % of particles per daily window in a global GLORYS12 run) remains, from particles that leave a cell through a corner and from rounding. See the README section Coastline accuracy.

Safety stops#

These stop particles that would otherwise hold up the whole run. Fortran handles such particles one at a time. Here all particles share the sub-cycle loop, so a few stuck particles would make every particle wait.

  • no_progress: a sub-cycle that did not advance the particle’s time, usually a particle stuck exactly on a wall in a convergence.

  • too_slow: after two warm-up sub-cycles, a particle must keep up a pace of one window per (max_sub_iter_per_window - 2) / pace_factor sub-cycles. A larger pace_factor is stricter. The default, 5, means a particle has to finish the window within about a fifth of the sub-cycle limit.

  • iter_limit: the window reached max_sub_iter_per_window (default 10 000) sub-cycles. A RuntimeWarning is raised, and the monitor marks the window [LIMIT].

If many particles stop with these reasons, look at the fluxes near where they stopped, or try more sub-steps (iter_sub).

Kill zones#

Box and tracer kill zones (Fortran exitType 1–4) are not ported yet. Two workarounds:

  • Run on a subset whose edge is the box you want: particles stop with out_of_bounds_h when they reach it.

  • Post-process: load the trajectories and cut each one at the first time it enters your region:

    df = tracmass.load(env="myrun", lonlat=True)
    inside = df.lat > 60
    first = df[inside].groupby("id").dtm.min().rename("t_exit")
    df = df.join(first, on="id")
    df = df[df.t_exit.isna() | (df.dtm <= df.t_exit)]