tracmass.gcm.flux_grid#
Grid field containers and utilities for TRACMASS JAX.
This module defines the core data structures and helper functions used to manage GCM grid geometry and velocity flux fields across two-slot time windows required by the TRACMASS Lagrangian particle tracker.
The grid is split into three objects:
GridFields— static geometry (kmt,dxdy) and metadata (dimensions, periodicity). These fields never change during a run and are closed over by the compiled advection kernels as XLA constants.LazyFluxFields— two time-slots of flux data held as raw xarray DataArrays (possibly Dask-backed), with no I/O triggered yet. Also carriesdtm, the timestamp of the past (slot 0) time level. Callcompute()to load data into device memory just before calling the advection kernel.update_flux()advances the window by a pure Python dict reference swap — no Dask compute, no JAX, no GC required until the nextcompute().FluxFields— the computed form: JAX arrays backed by device memory, registered as a pytree so the compiled kernel can receive it as a dynamic argument.
- class tracmass.gcm.flux_grid.GridFields(*, kmt, dxdy, imt, jmt, km, timestep, reverse, iperio, jperio, i_offset=0, j_offset=0)[source]#
Bases:
objectStatic grid geometry and metadata for a TRACMASS run.
These fields are fixed for the lifetime of a run. The advection kernels close over a
GridFieldsinstance so thatkmtanddxdybecome XLA constants in the compiled graph.- Parameters:
- kmt#
Bottom-level index (integer), shape
(imt, jmt). A value of 0 indicates a land cell.- Type:
jnp.ndarray
- dxdy#
Horizontal cell area, shape
(imt, jmt).- Type:
jnp.ndarray
- timestep#
Duration between consecutive time levels.
- Type:
pd.Timedelta
- i_offset, j_offset
Position of this grid’s cell
(0, 0)in the adapter’s full (un-subset) grid — thesubseti1/j1, else 0. Particle positions are relative to this grid; the trajectory recorder adds the offsets so savedx/y/ia/jaare absolute.- Type:
- tracmass.gcm.flux_grid.edge_mask(grid)[source]#
Cells on the edge of the domain, where particles leave it.
The outer ring of cells along every axis that does not wrap around: the first and last column when
grid.iperio == 0(e.g. asubsetin longitude) and the first and last row whengrid.jperio == 0. A particle that enters one of these cells stops withREASON_BOUNDS_H. The edge cells cannot be integrated correctly: the flux through their outer face is not part of the grid (the kernels treat the west / south one as a closed wall, and a particle crossing the east / north one would read fluxes outside the arrays).- Parameters:
grid (
GridFields) – Grid geometry; onlyimt,jmt,iperioandjperioare used.- Returns:
np.ndarray– Boolean array, shape(imt, jmt).- Return type:
Examples
>>> edge = edge_mask(grid) # subset grid: iperio == 0 >>> edge[0].all() and edge[-1].all() True
- class tracmass.gcm.flux_grid.FluxFields(*, dzt, zstot, uflux, vflux, wflux=None)[source]#
Bases:
objectTwo-slot time window of velocity flux data for the advection kernels.
Each array field has a trailing dimension of size 2, where slot 0 holds the ‘past’ time level and slot 1 holds the ‘present’ time level.
FluxFieldsis a JAX pytree and is passed as a dynamic argument to the compiled step functions. Obtain one from aLazyFluxFieldsby callingcompute().- dzt#
Layer thickness at tracer points, shape
(imt, jmt, km, 2).- Type:
jnp.ndarray
- zstot#
Total sea-surface height anomaly, shape
(imt, jmt, 2).- Type:
jnp.ndarray
- uflux#
Zonal volume flux, shape
(imt, jmt, km, 2).- Type:
jnp.ndarray
- vflux#
Meridional volume flux, shape
(imt, jmt+1, km, 2).- Type:
jnp.ndarray
- wflux#
Vertical volume flux in the kernel convention:
wflux[..., k, :]is the flux through the bottom face of levelk(level 0 at the surface), positive downward. Adapters supply the model convention (top face, positive upward); see_w_to_kernel().Nonedisables vertical particle motion.- Type:
jnp.ndarrayorNone
- class tracmass.gcm.flux_grid.LocalFluxFields(*, uflux, vflux, wflux, dzt, zstot, kmt, dxdy, edge=None)[source]#
Bases:
objectFlux data for a spatial sub-patch, with geometry included dynamically.
Unlike
FluxFields,kmtanddxdyare carried as ordinary JAX arrays rather than being closed over as XLA constants. This allows the advection kernel to be compiled once for a fixed patch shape and then reused across windows as the patch content changes — the kernel receives new values via the normal dynamic-argument path.- Parameters:
- uflux#
Zonal volume flux, shape
(imt_p, jmt_p, km, 2).- Type:
jnp.ndarray
- vflux#
Meridional volume flux, shape
(imt_p, jmt_p+1, km, 2).- Type:
jnp.ndarray
- dzt#
Layer thickness, shape
(imt_p, jmt_p, km, 2).- Type:
jnp.ndarray
- zstot#
Sea-surface height anomaly, shape
(imt_p, jmt_p, 2).- Type:
jnp.ndarray
- kmt#
Bottom-level index, shape
(imt_p, jmt_p). Dynamic — values differ per patch but shape is fixed for the lifetime of the run.- Type:
jnp.ndarray
- dxdy#
Horizontal cell area, shape
(imt_p, jmt_p). Dynamic.- Type:
jnp.ndarray
- edge#
edge_mask()of the full grid sliced to the patch, shape(imt_p, jmt_p): the patch cells on the domain edge, where particles stop as leaving the domain.None: no domain edge in the patch. Dynamic.- Type:
jnp.ndarrayorNone
- class tracmass.gcm.flux_grid.FluxStencil(*, u_east, u_west, v_north, v_south, w_top, w_bot, dzt, zstot, kmt, dxdy)[source]#
Bases:
objectPer-particle flux stencil extracted at each particle’s current cell.
Each array carries values for all N active particles, with a trailing slot dimension of size 2 (slot 0 = past, slot 1 = present).
Obtained via
extract_stencil()using vectorised xarray point indexing, which tells Dask to load only the Zarr chunks that contain the requested grid cells. This enables memory-efficient integration of widely scattered particles without loading a rectangular spatial patch.- Parameters:
- u_east#
Zonal flux at the east wall of each particle’s cell, shape
(N, 2).- Type:
jnp.ndarray
- u_west#
Zonal flux at the west wall, shape
(N, 2).- Type:
jnp.ndarray
- v_north#
Meridional flux at the north wall, shape
(N, 2).- Type:
jnp.ndarray
- v_south#
Meridional flux at the south wall, shape
(N, 2).- Type:
jnp.ndarray
- w_top#
Vertical flux at the top wall, shape
(N, 2).Nonewhen vertical motion is disabled (no_wflux=True).- Type:
jnp.ndarrayorNone
- dzt#
Layer thickness at the particle’s cell, shape
(N, 2).- Type:
jnp.ndarray
- zstot#
Total column depth at the particle’s position, shape
(N, 2).- Type:
jnp.ndarray
- kmt#
Bottom level index at the particle’s cell, shape
(N,).- Type:
jnp.ndarray
- dxdy#
Horizontal cell area at the particle’s position, shape
(N,).- Type:
jnp.ndarray
- tracmass.gcm.flux_grid.extract_stencil(lazy, grid, state, no_wflux=False, mask=None)[source]#
Extract per-particle flux stencils using vectorised xarray point indexing.
For each particle at grid position
(ia, ja, ka)(1-based), reads the six flux face values required for one advection sub-cycle (east/west for u, north/south for v, top/bottom for w) plus cell geometry (dzt,zstot,kmt,dxdy). All xarray DataArrays are fetched in a singledask.compute()call so Dask loads only the Zarr chunks that contain the requested points.- Parameters:
lazy (
LazyFluxFields) – Lazy flux data for the current time window.grid (
GridFields) – Static grid geometry (provideskmt,dxdy, andiperio).state (
ParticleState) – Current particle state. Onlyia,ja,kaare used.no_wflux (
bool, optional) – WhenTrue,w_topandw_botare set toNone.mask (
np.ndarrayorNone, optional) – Boolean array of shape(N,)selecting which particles to read Zarr data for. Positions where mask isFalsereceive zeros in the returnedFluxStencil(those values are never used by the kernel’sjnp.where(state.active, ...)guards). WhenNone(default), data is read only for active particles (state.active), skipping unnecessary I/O for deactivated ones.
- Returns:
FluxStencil– Per-particle flux stencils, shape(N, 2)/(N,). Only positions covered by mask (orstate.activewhen mask isNone) carry valid data; all other positions are zero.- Return type:
- class tracmass.gcm.flux_grid.LazyFluxFields(_past, _present, dtm=None, fluxes_fn=None, zarr_chunk_xy=(1, 1), timestep=None, reverse=False, dtm_end=None, _computed=<factory>)[source]#
Bases:
objectTwo time-slots of flux data held as raw xarray DataArrays.
The DataArrays may be backed by Dask (for GLORYS from Zarr) or by in-memory arrays (for the theoretical adapter). In either case, no
numpyor device memory is allocated untilcompute()is called.This object is not a JAX pytree. Pass the result of
compute()to the advection kernel instead.The two time slots are stored as separate DataArrays without a trailing time dimension. The shape of each DataArray matches the spatial shape of the variable (e.g.
(imt, jmt, km)foruflux).update_flux()advances the window by reassigning Python dict references — no Dask compute, no JAX, no GC.- Parameters:
- dtm#
Timestamp of the ‘past’ (slot 0) time level. Set by
new_flux()andupdate_flux();Nonewhen constructed via lower-level helpers likeswap_fields().- Type:
pd.TimestamporNone
- fluxes_fn#
The GCM adapter callable used to create this object, e.g.
gcm.glorys.fluxes. Stored bynew_flux()and propagated byupdate_flux()so thattime_dependent()can infer it automatically without the caller re-supplying it.- Type:
callableorNone
- timestep#
GCM output interval (always positive), copied from
grid.timestepbynew_flux().update_flux()stepsdtmby this amount.- Type:
pd.TimedeltaorNone
- reverse#
Truefor a backward run (copied fromgrid.reverse):update_flux()then walksdtmbackward and negates the velocity fluxes it loads.- Type:
- dtm_end#
The dtm_end passed to
new_flux(), orNone. Used byn_windowsto size a run.- Type:
pd.TimestamporNone
Notes
compute()keeps the present slot’s arrays (already read and cleaned) in_computed, keyed by the slot’s DataArray. Afterupdate_flux()that DataArray is the new past slot, so the nextcompute()reuses the arrays instead of reading the same snapshot again: every snapshot is read once, not twice._computedis shared by the copiesupdate_flux()makes and holds one slot per variable.- property n_windows: int | None#
Number of GCM windows left between
dtmanddtm_end.ceil(|dtm_end - dtm| / timestep), so a dtm_end that falls inside a window still has that window integrated. Decreases by one with everyupdate_flux()call.Nonewhendtm_end(ordtm/timestep) is not set.
- compute_patch(i0, i1, j0, j1, grid, include_wflux=True)[source]#
Load only the sub-patch
[i0:i1, j0:j1]into device memory.Slices the lazy xarray DataArrays before calling
.values, so Dask only computes (and reads from Zarr) the chunks that intersect the requested region.kmtanddxdyare sliced from grid and included in the returnedLocalFluxFieldsas dynamic JAX arrays, so the advection kernel can be compiled once for a fixed patch shape and reused without recompilation as patch content changes.- Parameters:
i0 (
int) – Zonal slice bounds (0-based, exclusive end).i1 (
int) – Zonal slice bounds (0-based, exclusive end).j0 (
int) – Meridional slice bounds (0-based, exclusive end).j1 (
int) – Meridional slice bounds (0-based, exclusive end).grid (
GridFields) – Full-grid static geometry;kmtanddxdyare sliced from here to match the patch.include_wflux (
bool, optional) – False skips readingwflux(returned asNone), for runs without vertical motion. Default True.
- Returns:
- Return type:
- preload(include_wflux=True)[source]#
Return a copy with all Dask arrays preloaded to numpy.
Issues a single
dask.compute()call for every variable in_pastand_present, replacing Dask-backed DataArrays with in-memory numpy-backed DataArrays. The returned object is otherwise identical and can be used anywhere aLazyFluxFieldsis expected.Calling this once per GCM window in
time_dependent_streaming()changes the I/O pattern from O(n_sub) small Dask computes (one per sub-cycle that has cell crossings) to O(1) per window. Subsequentextract_stencil()calls on the returned object use pure numpy fancy indexing — no further Zarr I/O regardless of how many sub-cycles or cell crossings occur.- Parameters:
include_wflux (
bool, optional) – False leaveswfluxout of the copy instead of reading it, for runs without vertical motion. Default True.- Returns:
LazyFluxFields– A copy with numpy-backed DataArrays and the same metadata (dtm,fluxes_fn,timestep,reverse, …).- Return type:
- compute(include_wflux=True)[source]#
Trigger Dask computation and transfer flux data to JAX device memory.
Variables are loaded one at a time (past then present) and stacked along a new trailing axis, so the peak extra allocation per variable is one numpy array of size
t0, one of sizet1, and the resulting JAX array of sizet0 + t1.NaN values in flux arrays (
uflux,vflux,wflux) are replaced with 0.0 before stacking. Land cells in GLORYS/NEMO are masked as NaN in the Zarr files; the physical no-flow boundary condition is 0.0, so NaN → 0 is the correct substitution. Without this replacement, NaN fluxes propagate through_calc_pos_1dto produce a NaN particle position, which afterastype(int32)conversion gives a garbage cell index that may point to a kmt=0 (land) cell, causing spuriousREASON_LANDdeactivations.- Parameters:
include_wflux (
bool, optional) – False skips readingwflux(returned asNone), for runs without vertical motion. Default True.- Returns:
FluxFields– Kernel-ready flux data with trailing slot dimension of size 2.- Return type:
- tracmass.gcm.flux_grid.subset_offset_attrs(subset)[source]#
Dataset attrs recording where an adapter’s x/y subset starts.
Subsetting adapters put these on the dataset
fluxes()returns;_static_from_ds()turns them intoGridFields.i_offset/j_offsetso recorded positions can be written in full-grid indices.- Parameters:
subset (
dict) – The adapter’ssettings["subset"](i1,j1,istep, …).- Returns:
dict[str,int]–{"_i_offset": i1 or 0, "_j_offset": j1 or 0}.- Warns:
UserWarning – If
istep/jstepis set: a strided subset’s cells are not full-grid cells, so the offset alone does not make saved positions absolute.- Return type:
- tracmass.gcm.flux_grid.swap_fields(lazy_fluxes)[source]#
Swap time levels: present (slot 1) becomes past (slot 0).
Mirrors
mod_swap.F90::swap_time(). After calling this function, the caller should provide new present-slot data (e.g. by callingupdate_flux()).This is a pure Python reference swap — no Dask compute, no JAX.
- Parameters:
lazy_fluxes (
LazyFluxFields) – Lazy flux data to swap.- Returns:
LazyFluxFields– NewLazyFluxFieldswhere the old present data is now the past slot. The present slot is left pointing at the old present data (stale) and should be overwritten by the caller.- Return type:
- tracmass.gcm.flux_grid.fluxes_to_grid(ds)[source]#
Convert a two-slot concatenated xarray Dataset to
(GridFields, LazyFluxFields).Static variables (
kmt,dxdy, dimensions, timestamps) are computed eagerly into aGridFields. Flux variables (uflux,vflux,wflux,dzt,zstot) are kept as xarray DataArrays inside aLazyFluxFields— no I/O is triggered for them untilcompute()is called.The DataArrays stored in the
LazyFluxFieldsare expected to already have a trailing time slot dimension of size 2 (as produced byxr.concat).new_flux()builds the same two-slot structure directly from two separatefluxes()calls.- Parameters:
ds (
xr.Dataset) – Two-slot concatenated dataset.- Returns:
tuple[GridFields,LazyFluxFields]- Return type:
- tracmass.gcm.flux_grid.new_grid(fluxes, dtm_start=None, dtm_end=None)[source]#
Build the static grid geometry for a run.
Calls fluxes once (at dtm_start) purely to read the time-invariant geometry —
kmt,dxdy,imt/jmt/km,iperio/jperio— and the GCM output interval (timestep). No flux data is loaded. The returnedGridFieldsis immutable for the lifetime of the run and can be reused across manynew_flux()buffers on the same grid (successive experiments, restart windows).Integration direction is inferred from dtm_start / dtm_end exactly as in
new_flux():dtm_end < dtm_startmarks a backward run (grid.reverse=True).grid.timestepis always positive.- Parameters:
fluxes (
callableorstr) –fluxes(dtm) -> xr.Dataset, or the name of atracmass.gcmsub-module (e.g."glorys"). When the adapter can be identified, the[env.gcm] subsettable from the activetracmass.tomlenvironment is applied first viatracmass.config.apply_gcm_subset().dtm_start (
pd.TimestamporstrorNone, optional) – Any valid time for the adapter; only the geometry is read. DefaultNone— fluxes is called with no arguments at all, so its own default applies (forgcm.glorys/gcm.ecco2this falls through totracmass.seagrid.glorys.open_dataset()/tracmass.seagrid.ecco2.open_dataset()’s owndtmdefault).dtm_end (
pd.TimestamporstrorNone, optional) – When earlier than the resolved dtm_start, marks the grid backward (reverse=True). DefaultNone(forward).
- Returns:
GridFields– Static geometry withtimestep(positive) andreverseset.- Return type:
See also
new_fluxbuild the flux buffer, optionally reusing a grid from here.
- tracmass.gcm.flux_grid.new_flux(grid_or_fluxes, fluxes=None, dtm_start=None, dtm_end=None)[source]#
Create the lazy two-slot flux buffer for a run.
Two call forms:
new_flux(fluxes, dtm_start=..., dtm_end=None) -> (GridFields, LazyFluxFields)Build the grid and the flux buffer together. Equivalent to
g = new_grid(fluxes, ...); return g, new_flux(g, fluxes, ...)but fetching slot 0 only once. This is the original signature and return type.new_flux(grid, fluxes, dtm_start=..., dtm_end=None) -> LazyFluxFieldsReuse a
GridFieldsfromnew_grid()(or an earliernew_flux); build only the flux buffer.grid.reverseandgrid.timestepdrive direction and step size; dtm_end, if given, is only checked for consistency withgrid.reverse.
Flux data (
uflux,vflux,wflux,dzt,zstot) is kept as lazy xarray DataArrays and not loaded into device memory untilcompute()is called. For a backward run (grid.reverse) slot 1 is onegrid.timestepfurther into the past, the velocity fluxes are negated lazily, and eachupdate_flux()call walkslazy.dtmbackward through the archive. Thefluxescallable is never passedreverse=.- Parameters:
grid_or_fluxes (
GridFieldsorcallableorstr) – AGridFieldsselects the grid-reuse form; anything else is thefluxescallable / adapter name of the combined form.fluxes (
callableorstr, optional) – Thefluxes(dtm) -> xr.Datasetcallable (or adapter name). Required — and only used — in the grid-reuse form.dtm_start (
pd.TimestamporstrorNone, optional) – Physical time of slot 0 (past). DefaultNone— fluxes is called with no arguments at all, so its own default applies (forgcm.glorys/gcm.ecco2this falls through totracmass.seagrid.glorys.open_dataset()/tracmass.seagrid.ecco2.open_dataset()’s owndtmdefault).dtm_end (
pd.TimestamporstrorNone, optional) – When earlier than the resolved dtm_start, activates backward tracking (combined form) or is checked againstgrid.reverse(grid-reuse form). Stored aslazy.dtm_end, solazy.n_windowsgives the number of windows to reach it and thetime_dependent*integrators need non_windows.
- Returns:
tuple[GridFields,LazyFluxFields]orLazyFluxFields– A(grid, lazy)tuple for the combined form; justlazyfor the grid-reuse form.lazy.dtmholds the slot-0 timestamp;lazy.timestep/lazy.reversemirror the grid.- Return type:
See also
new_gridbuild just the static grid.
- tracmass.gcm.flux_grid.update_flux(*args, fluxes_fn=None)[source]#
Advance the flux buffer by one timestep using a rolling two-slot window.
Two call forms:
update_flux(lazy, fluxes_fn=None) -> LazyFluxFieldsStep size and direction come from
lazy.timestep/lazy.reverse(set bynew_flux()), and fluxes_fn defaults tolazy.fluxes_fn.update_flux(grid, lazy, fluxes_fn) -> LazyFluxFieldsOlder form: step size and direction come from
grid.timestep/grid.reverse. Still needed for alazybuilt by lower-level helpers that do not recordtimestep.
The time-slot swap is a pure Python dict reference assignment — the old present slot’s DataArrays become the new past slot at zero cost. Only a single call to fluxes_fn is made to load the next timestep’s DataArrays (still lazy for Dask-backed sources).
No Dask computation, no JAX operations, and no GC are needed here; all I/O is deferred until the next
compute()call.The timestep is always positive; for backward runs (
reverseisTrue)lazy.dtmdecrements by one timestep per call. Thefluxes_fncallable is invoked withoutreverse=— flux negation is applied by this function after loading.- Parameters:
*args –
(lazy,),(lazy, fluxes_fn)or(grid, lazy, fluxes_fn); see the call forms above.lazy.dtmis the physical timestamp of the current past (slot 0) time level.fluxes_fn (
callable, optional) –fluxes_fn(dtm) -> xr.Datasetfor the next window. Defaults tolazy.fluxes_fn.
- Returns:
LazyFluxFields– New lazy flux data withdtmadvanced (forward) or decremented (backward) by one timestep; all other metadata is carried over.- Raises:
TypeError – On a wrong number of positional arguments.
ValueError – If no fluxes_fn is given and
lazy.fluxes_fnis unset, or if the timestep is unknown (alazywithouttimestepneeds the(grid, lazy, fluxes_fn)form).
- Return type:
Examples
>>> grid, lazy = new_flux(gcm.theoretical.fluxes) >>> lazy = update_flux(lazy)