# Integrators An integrator takes a particle state, a grid and a flux buffer, and moves the particles through one or more GCM windows. There are four: | Function | Windows | Flux data per window | Kernel loop | |---|---|---|---| | `steady_state` | 1, frozen field | whole grid | `lax.while_loop` | | `time_dependent` | many | whole grid (or the `subset`) | `lax.while_loop` | | `time_dependent_patched` | many | bounding box of the active particles | `lax.while_loop` | | `time_dependent_streaming` | many | the six faces of each particle's cell | Python `for` loop | All of them use the same advection equations (see [Advection schemes](schemes.md)). They differ in how much flux data they read, which decides memory use and speed. ## Windows and sub-steps A **window** is one GCM timestep, the interval between two snapshots (`grid.timestep`, the Fortran `ngcm_step`). Inside a window: - Particle time is kept in seconds (`tt`) and in normalised form (`ts = tt / timestep`), so window `n` runs from `ts = n` to `ts = n + 1`. - The window is split into `iter_sub` sub-steps. A sub-cycle advances each active particle until it hits a wall or reaches its next sub-step boundary, whichever comes first. A particle can need many sub-cycles per sub-step if it crosses many cells. - The loop runs until no active particle is left inside the window. Particles that reach the window end are *paused* (`active=False`, `reason == 0`) and resumed at the start of the next window. The number of windows is `n_windows` if given, else `lazy.n_windows` (from the `dtm_end` passed to `new_flux`), else 1. ## Safety limits Some particles never reach the end of a window: they are trapped in a convergence zone, oscillate at a wall, or move so slowly that they hold everyone else up. The integrators stop them and record a [reason](../tutorials/stopping.md): | Check | Reason | |---|---| | A sub-cycle made no progress in time | `no_progress` | | A particle falls behind a pace of one window per `(max_sub_iter_per_window - 2) / pace_factor` sub-cycles | `too_slow` | | The window used `max_sub_iter_per_window` sub-cycles (default 10 000) | `iter_limit` | | `tt - t0` exceeded the trajectory lifetime | `time_limit` | ## `time_dependent` The basic multi-window integrator. Each window it reads both snapshots of the whole grid (or the `[env.gcm] subset`) with `lazy.compute()`, runs the compiled loop, and moves on with `update_flux`. Use it for the synthetic test cases, for regional subsets, and whenever particles cover a large part of the grid. Options of its own: - `scheme`: `"tstep"` or `"analytical"`. - `n_particle_batches`: run the particles in this many groups, one compiled loop per group, to limit GPU memory. Between windows the particle state is kept on the host. - `rampup_windows`, `rampup_substeps`: extra saves in the first windows (see [Output](../output.md#ramp-up-saves)). ## `time_dependent_patched` Reads only a rectangular **patch**: the bounding box of the active particles plus `patch_buffer` cells on each side. The patch is snapped to the chunk boundaries of the source Zarr store, so no chunk is read partially. At each window the patch is re-centred on the particles, which costs nothing until it is read. It only ever grows, never shrinks, so the kernel (`piecewise_local`, which takes the patch's `kmt` and `dxdy` as arguments) is recompiled only when the particle cloud has spread beyond the current patch size. This is several times faster than `time_dependent` for **clustered particles** on a large grid, such as a regional release in a global reanalysis. It is not a good fit when the particles spread over most of the globe, because the patch then grows to cover it. ## `time_dependent_streaming` Reads only what each particle needs: the six face fluxes and the geometry of its current cell (a `FluxStencil`), gathered with one batched Dask read per sub-cycle (`gcm.extract_stencil`). Because the data a particle needs depends on where it is, the sub-cycle loop runs in Python instead of `lax.while_loop`. Two optimisations keep the number of reads down: - only **active** particles are read; - a **stencil cache** keeps each particle's stencil until it moves to another cell, and is cleared when a new window starts. Use it for **widely scattered particles** and very long runs, where any bounding box would cover most of the grid. Its cost grows with the number of sub-cycles, not with the size of the grid. It always uses the time-stepping scheme and does not support ramp-up saves. ## Choosing an integrator ```{list-table} :header-rows: 1 :widths: 40 60 * - Situation - Integrator * - Synthetic tests, small or subset grids - `time_dependent` * - A regional release in a global model, run for weeks to months - `time_dependent_patched` * - Particles all over the globe, or runs of many years - `time_dependent_streaming` * - Comparing with Fortran time-analytical runs - `time_dependent(scheme="analytical")` * - Many particles on a GPU that runs out of memory - `time_dependent` with `n_particle_batches`, or a `subset` ``` See [Memory-efficient integration](../tutorials/memory.md) for examples and timing advice. ## Common arguments All three multi-window integrators accept: ```python integrator(state, grid, lazy, fluxes_fn=None, *, iter_sub=1, max_iter=100_000, n_windows=None, max_sub_iter_per_window=10_000, timax_seconds=..., traj_max_time=..., recorder=..., monitor=..., no_wflux=..., pace_factor=5, n_particle_batches=..., rampup_windows=..., rampup_substeps=..., seed_freq=...) ``` Arguments shown as `...` default to the matching setting (see [Settings](../configuration/settings.md)). `fluxes_fn` defaults to `lazy.fluxes_fn`. `monitor` and `recorder` take an instance (which you keep ownership of), a string (`"rich"`, `"traj.parquet"`, built and closed for you), `None` to turn them off, or nothing to use the settings.