# 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](../output.md)). ```python 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`): ```toml [myrun] traj_max_time = "90D" # Timedelta strings: "30D", "12h", "2W", "90min" ``` ```python 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](subsets.md): ```python 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: ```python 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)] ```