# 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)).