# 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](../configuration/quickstart.md).
`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
on first use and cached
unchanged under `/data/raid/scrippsgog/PRELIM/daily/YYYY/`:
```python
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
```python
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](../tutorials/new_gcm.md) writes an adapter from
scratch.
## The `seagrid` layer
Data access is kept apart from flux computation. `tracmass.seagrid.`
modules download, cache and open the raw model output (`open_dataset`,
`grid_definition`, `retrieve`), and know nothing about TRACMASS.
`tracmass.gcm.` turns that into fluxes. The `seagrid` modules also
give every model a land mask and a distance-to-land field (see
[Land and coast](../tools/coast.md)).