GCM adapters#

In Fortran TRACMASS each GCM has a project folder (projects/NEMO, projects/ROMS, …) with code that sets up the grid (setup_grid.F90), reads the fields (read_field.F90) and defines kill zones (kill_zones.F90). You choose one in Makefile.prj and compile.

In TRACMASS-JAX a GCM is supported by an adapter, a Python module in tracmass.gcm with a fluxes function. You choose an adapter at run time, by name ([env.gcm] name = "glorys", new_flux("glorys", ...)) or by passing the function itself.

Available adapters#

Adapter

Model / data

Grid

Timestep

Vertical motion

Data

theoretical

Synthetic double gyre with noise

100 × 80 × 30, 250 m cells, zonally periodic

1 h

yes (random w)

none, generated

fabboni

Fabboni (2009) analytical decaying inertial oscillation, as in the Fortran Theoretical project

300 × 300 × 1, 250 m cells

1 h

no

none, generated

glorys

Copernicus Marine GLORYS12 reanalysis (NEMO)

4318 × 3059 × 50 (1/12°, ORCA tripolar), halo removed

1 day

yes

local daily Zarr stores

ecco2

ECCO2 cube92 state estimate (MITgcm)

0.25° regular lat/lon, global, re-centred on 0°

1 day

yes

OPeNDAP (APDRC), cached as Zarr

scrippsgog

Scripps Gulf of Guinea regional MITgcm, top 200 m

1/64°, 11° W–10.5° E, 2–7° N, 25 levels

1 day

no (no w in the files)

HTTPS, cached as NetCDF

theoretical#

A double-gyre flux field with small random perturbations, slowly evolving from 2007-06-01 by day-by-day random increments, plus random vertical fluxes. The first and last rows are land, and the domain is periodic in x. It needs no data, so it is the standard case for tests and for the Quick start. gcm.theoretical.initial_state(n_particles) draws random interior starting positions.

fabboni#

The spatially uniform test case of the Fortran distribution (Fabbroni, 2009): \(u\) starts at 0.5 m s⁻¹ and decays with an inertial oscillation at 45° N. It has an analytical solution, and gcm.fabboni.initial_state() reproduces the Fortran seeding, so it is used to validate the schemes against the Fortran code (tests/test_fortran_comparison.py).

glorys#

GLORYS12 daily means, read from per-day, per-variable Zarr stores under /data/{disk}/mercator/{product} (tracmass.seagrid.glorys). Notes:

  • The NEMO halo columns are removed. Velocity stores and mesh files carry different halos and are trimmed differently, so that uflux[i] is the east face of T-cell i. tests/test_glorys_alignment.py checks this.

  • Volume fluxes are uvel · dzt · dyu, vvel · dzt · dxv and wvel · dxdy.

  • Land fill values are masked (velocities become 0) after the subset is cut out, which is about 7 times faster than masking whole chunks.

  • [env.gcm] subset limits the region and the levels. An x-subset turns off zonal periodicity.

  • lonlat_grid, lonlat_to_xy and xy_to_lonlat convert between model positions and geographic coordinates.

  • seagrid.glorys.rechunk_wvel(dtm1, dtm2) rewrites wvel stores downloaded before 2026-10-08 with 10-level chunks, which makes reading a few levels much faster. Don’t rechunk days that a running job is about to read.

ecco2#

ECCO2 cube92 daily fields from the APDRC OPeNDAP server, one remote dataset per variable for the whole 1992–2025 record. Each requested day is cached locally as a Zarr store. The grid is regular lat/lon, so cell areas are computed analytically. kmt and dzt are derived from the cells where the velocity is exactly zero, because the source mislabels its fill value. Junk values in wvel (|w| > 10¹⁰) are set to zero, and longitudes are rolled to −180…180. fluxes(dtm, include_wflux=None) reads w only when move_vertically is true.

scrippsgog#

The daily Scripps Gulf of Guinea MITgcm run, downloaded from https://sose.ucsd.edu/GoG/DATA/PRELIM/daily/ on first use and cached unchanged under /data/raid/scrippsgog/PRELIM/daily/YYYY/:

from tracmass.seagrid import scrippsgog

scrippsgog.available_dates()          # dates on the server
scrippsgog.retrieve("2024-01-02")     # download one day (no-op if cached)
grid, lazy = tracmass.new_flux("scrippsgog", dtm_start="2024-01-02",
                               dtm_end="2024-01-31")

MITgcm puts UVEL on the west face and VVEL on the south face of each T-cell. The adapter shifts both by one face onto TRACMASS’s east and north faces, and uses the files’ own face metrics and open fractions (dyG, dxG, drF, hFacW, hFacS). kmt and dzt come from MITgcm’s partial-cell rule (hFacMin = 0.2) applied to the bathymetry. The western and southern open boundaries are closed in the tracking, and there is no vertical velocity, so tracking is horizontal only.

What an adapter returns#

An adapter function has the signature

def fluxes(dtm: pd.Timestamp | str | None = None) -> xr.Dataset: ...

It returns one snapshot, the fields at time dtm (with dtm=None meaning the adapter’s own default date), as an xarray.Dataset with:

Variable

Dims

Description

uflux

(x, y, z[, t])

Zonal volume flux through east faces, m³ s⁻¹

vflux

(x, y, z[, t])

Meridional volume flux through north faces, m³ s⁻¹

wflux

(x, y, z[, t])

Vertical volume flux through the top face of each level, positive upward, m³ s⁻¹ (optional; leave out for 2-D runs)

dzt

(x, y, z[, t])

Layer thickness, m

zstot

(x, y[, t])

Free-surface stretching factor (1 for fixed levels)

kmt

(x, y)

Number of ocean levels; 0 on land

dxdy

(x, y)

Horizontal cell area, m²

imt, jmt, km

scalar

Grid size

iperio, jperio

scalar

1 if periodic in x / north fold, else 0

timestep

scalar Timedelta

Output interval of the model

dtm

scalar Timestamp

Time of this snapshot

reverse

scalar

Always False (backward runs are handled by new_flux)

A trailing size-1 t dimension is removed automatically. The fields can, and for real models should, be lazy Dask arrays: new_flux and update_flux never read them, and LazyFluxFields.compute() reads them one window at a time. Adapters that support subsets set the dataset attributes _i_offset and _j_offset (the subset start), and may set _zarr_chunk_x and _zarr_chunk_y so the patched integrator can line its patches up with the storage chunks.

Optional functions add features:

Function

Used by

lonlat_grid(grid)

seed.latlon_box, latlon_point, latlon_list

lonlat_to_xy(lon, lat), xy_to_lonlat(x, y)

geographic seeding; tracmass.load(lonlat=True)

settings["subset"] dict

[env.gcm] subset (applied by new_flux before the first call)

The tutorial Adding a GCM writes an adapter from scratch.

The seagrid layer#

Data access is kept apart from flux computation. tracmass.seagrid.<model> modules download, cache and open the raw model output (open_dataset, grid_definition, retrieve), and know nothing about TRACMASS. tracmass.gcm.<model> turns that into fluxes. The seagrid modules also give every model a land mask and a distance-to-land field (see Land and coast).