Gent & McWilliams (1990) / Griffies (1998) skew-flux thickness
diffusion. The eddy-induced (“bolus”) overturning is realized as a
layer THICKNESS flux (never an explicit velocity), applied as its OWN
sequential operator on the CURRENT thickness, after the dynamics of
every outer step (rdb_continuity :: continuity_gm_apply, driven by
rdb_ocean_dyn :: run_gm_step) — mirroring how MOM6 applies its
thickness_diffuse step after the split-RK2 dynamics step, updating
h in place.
The per-face availability cap h_avail = A·(h − H_VANISHED)/(4·dt)
bounds four faces’ outflow by
h − H_VANISHED, so h_new >= H_VANISHED — but ONLY for the h it
was computed from. Until 2026-10 this slot computed uhD from the
stage-entry h and FOLDED it into the resolved continuity sweeps
(gm_fold_x/y), so the cap bounded the stage-entry thickness and the
resolved outflow came on top: on the 1-degree Southern Ocean (z*,
open steps) an 8.6 cm partial bed cell with two open faces onto
fillers was drained by GM at the cap on all four faces and the
resolved flux took it to −8.2e-4 m. Computed here from the thickness
the dynamics LEFT, the cap bounds what is actually there (prototype:
python_prototypes/gm_sequential/gm_sequential.py).
At each u/v face the eddy streamfunction is
Sfn_unlim = -(KhTh * dy_Cu) * Slope
using the STORED isopycnal slope from the slopes slot. The per-layer
bolus transport is the vertical difference of the limited
streamfunction, formed by the column recurrence (surface->bed,
uhtot=0 at surface):
uhD(k) = max( min(Sfn_in_H - uhtot, h_avail_i), -h_avail_{i+1} )
uhtot = uhtot + uhD(k)
so Sum_k uhD = 0 (closes at the bed) — mass/tracer conservative.
Bottom-blocking (MOM6’s rule for the GM streamfunction) acts on the
UNLIMITED streamfunction first: no transport from a donor layer that
lies entirely below the receiving column’s bed, and a share scaled by
the fraction above it for a donor layer that straddles it
(gm_block_below_bed). On an open z-level step this is what keeps GM
from pouring deep water through the step into the fillers below the
shallow column’s bed.
Three limiters, in order: (1) safe-streamfunction blend toward a column-spread return flow where slope > slope_max; (2) mass- availability rsum bound (the conservation guard keeping each layer
= H_VANISHED); (3) per-layer donor cap. The donor side is keyed on the SIGN of
uhtot(column i when uhtot<=0, else i+1).
KhTh is a 2D face field, constant-filled from &ocean_gm_nml khth
(VarMix/MEKE seam populates it later via +=), CFL-clamped per face.
gm_src(nx,ny) carries the GM PE release (>= 0 for a stable tilted
column) for future MEKE coupling.
Bottom-up convention (k=1 bed, k=nz surface; interface K=1 bed, K=nz+1 surface); the slopes slot zeroes the slope at both caps so the streamfunction vanishes there and the recurrence closes.
&vcoord_nml zfixed_closed_faces)Under z_fixed a face column is not the whole water column: a layer
that is an inert FILLER on either side (inside the bed or the ice
draft) is a WALL for that layer at that face (metrics%open_u/open_v
== 0). Continuity applies that mask to the resolved flux, and GM
runs on its own (it does not pass through that mask), so GM must
build its overturning on the OPEN part of each face column itself. With the knob on, each face
first marks its open layers,
ok(k) = open(k) .and. live(h_W(k)) .and. live(h_E(k))
(rdb_vl_is_live, the one vanished-layer predicate), and the
recurrence then:
ok layer ZERO transport and zero availability
(h_avail = 0, so it adds nothing to rsum): the streamfunction
is carried UNCHANGED across it, so it is 0 at the BOTTOM of the
open column (it starts at 0 at the bed, and the bed-side closed /
filler layers cannot change it);k_top_open
(uhD(k_top_open) = -uhtot) instead of k = nz, so the
streamfunction is also 0 at the TOP of the open column — the free
surface, or the ice base with fillers above it. The full-column
closure into nz would push the residual through a filler under
the ice, or through a face whose top layer is closed.Hence uhD == 0 on every closed face-layer and every filler, and
Sum_k uhD = 0 at every face (the open-column integral IS the column
integral). With nothing closed ok is all-true, k_top_open = nz
and the arithmetic is the full-column form operation for operation.
The slopes slot zeroes slope / N^2 at every interface not strictly
inside a face’s open column on this path, so gm_src (the MEKE
source) sees only the open column as well. Knob OFF ⇒ use_open =
.false., ok all-true, the masks are never indexed ⇒ byte-identical.
Non-finite guard: the streamfunction limiters are min/max
clamps, which nvfortran’s relaxed FP lowers NaN-blind (CLAUDE.md) —
a NaN slope would come out as a plausible ±rsum transport. A
non-finite slope is therefore read as ZERO slope (no GM at that
interface) before any clamp sees it; finite inputs are untouched.
Default off (&ocean_gm_nml enable=.false.) => slot allocated but
gm_compute_transports no-ops => bit-identical.
Refs: Gent & McWilliams (1990); Gent et al. (1995); Griffies (1998).
Gent-McWilliams thickness-diffusion state. All fields default to
the inert (enable=.false.) configuration so an ocean run that
never sets &ocean_gm_nml is bit-identical.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| logical, | public | :: | enable | = | .false. |
Master switch. Off => |
|
| real(kind=wp), | public, | allocatable | :: | gm_src(:,:) |
Potential-energy release |
||
| logical, | public | :: | is_init | = | .false. |
True between |
|
| real(kind=wp), | public | :: | khth | = | 0.0_wp |
Thickness-diffusion coefficient KhTh (m^2/s); constant-filled into the 2D face fields (production 1e2-1e3). |
|
| real(kind=wp), | public | :: | khth_max_cfl | = | 0.1_wp |
Fraction of the diffusive CFL the face KH may use:
|
|
| real(kind=wp), | public | :: | khth_slope_max | = | 0.01_wp |
Slope magnitude (nondim) above which the safe-streamfunction
blend takes over ( |
|
| real(kind=wp), | public, | allocatable | :: | khth_u(:,:) |
Thickness diffusivity at u-faces (m^2/s), |
||
| real(kind=wp), | public, | allocatable | :: | khth_v(:,:) |
Thickness diffusivity at v-faces (m^2/s), |
||
| integer, | public | :: | nx_total | = | 0 | ||
| integer, | public | :: | ny_total | = | 0 | ||
| integer, | public | :: | nz_ml | = | 0 | ||
| real(kind=wp), | public | :: | rho0 | = | 1035.0_wp |
Reference density (kg/m^3) for the |
|
| real(kind=wp), | public, | allocatable | :: | uhD(:,:,:) |
GM x-face transport (m^3/s), |
||
| real(kind=wp), | public, | allocatable | :: | vhD(:,:,:) |
GM y-face transport (m^3/s), |
| procedure, public, non_overridable :: bytes => ocean_gm_bytes | |
| procedure, public, non_overridable :: destroy => ocean_gm_destroy | |
| procedure, public, non_overridable :: enter_data => ocean_gm_enter_data | |
| procedure, public, non_overridable :: exit_data => ocean_gm_exit_data | |
| procedure, public, non_overridable :: init => ocean_gm_init |
MOM6 bottom-blocking (“Avoid moving dense water upslope from below the
level of the bottom on the receiving side”). sfn is the unlimited
streamfunction at an interface: the transport of everything BELOW
it, > 0 from L to R. Its donor layer is the one just below the
interface on the donor side ([e_bot, e_top]).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | sfn | |||
| real(kind=wp), | intent(in) | :: | e_top_l | |||
| real(kind=wp), | intent(in) | :: | e_bot_l | |||
| real(kind=wp), | intent(in) | :: | bed_r | |||
| real(kind=wp), | intent(in) | :: | e_top_r | |||
| real(kind=wp), | intent(in) | :: | e_bot_r | |||
| real(kind=wp), | intent(in) | :: | bed_l |
Clamp a slope to +/- smax (bounded slope for the PE release).
A non-finite slope reads as 0 (no release) rather than being
laundered into ±smax by the NaN-blind clamp (CLAUDE.md).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | s | |||
| real(kind=wp), | intent(in) | :: | smax |
Donor mass fraction h_avail(k)/rsum_above(k) (0 when no mass is
available above). rsum_above(k) is the cumulative availability
from the surface down to and including layer k, so hf in [0,1].
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | h_avail_k | |||
| real(kind=wp), | intent(in) | :: | rsum_k |
max(N^2, 0) for the PE release, with a non-finite N^2 read as 0
(a bare max is NaN-blind under relaxed FP, CLAUDE.md).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | n2 |
Counted allocatable footprint of the GM slot (0 when unallocated). One arr_bytes term per array — add a term here when a new allocatable joins the type.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(ocean_gm_t), | intent(in) | :: | this |
Fill uhD/vhD (m^3/s) with the GM bolus thickness transport and
gm_src with the PE release, from the CURRENT ms%h_layer and the
stored slopes. Run every outer step AFTER the dynamics, and
immediately followed by continuity_gm_apply with the SAME dt
and the same, untouched h_layer: the availability cap
A·(h − H_VANISHED)/(4·dt) bounds that h and no other (see the
module docstring). No-op when disabled, uninitialised, or the
slopes slot is absent/disabled.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(hgrid_t), | intent(in) | :: | grid | |||
| type(ocean_metrics_t), | intent(in) | :: | metrics | |||
| type(ocean_gm_t), | intent(inout) | :: | this | |||
| type(ocean_slopes_t), | intent(in) | :: | slopes | |||
| type(multilayer_state_t), | intent(in) | :: | ms | |||
| real(kind=wp), | intent(in) | :: | dt | |||
| real(kind=wp), | intent(in), | optional | :: | khth_ext_u(:,:) | ||
| real(kind=wp), | intent(in), | optional | :: | khth_ext_v(:,:) |
Fill the 2D face KhTh fields from the per-face base, CFL-clamped
per face (native u-face idxCu/idyCu, v-face idxCv/idyCv) and zeroed
on wall faces. Base = VarMix khth_ext_* when use_ext, else the
scalar khth; the same diffusive-CFL min is applied either way.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| real(kind=wp), | intent(in) | :: | dt | |||
| real(kind=wp), | intent(in) | :: | khth | |||
| real(kind=wp), | intent(in) | :: | khth_max_cfl | |||
| logical, | intent(in) | :: | use_ext | |||
| real(kind=wp), | intent(in) | :: | khth_ext_u(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | khth_ext_v(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | idxCu(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | idyCu(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | idxCv(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | idyCv(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | wet_u(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | wet_v(nx,ny+1) | |||
| real(kind=wp), | intent(out) | :: | khth_u(nx+1,ny) | |||
| real(kind=wp), | intent(out) | :: | khth_v(nx,ny+1) |
u-face GM streamfunction + bolus-transport column recurrence.
Interior u-face (i=2..nx) pairs columns iw=i-1 (west) and i (east).
Bottom-up sweep: interior interfaces Kr=2 (bed-most) -> nz
(surface-most), uhtot=0 at the bed; the surface BC (Sfn=0 at
Kr=nz+1) is closed after the loop by uhD(nz)=-uhtot, giving
Sum_k uhD=0 exactly. Interface Kr straddles ka=Kr (above) and
kb=Kr-1 (below) and fills LAYER kb; the rsum bound keys on ka, the
donor h_frac and per-layer cap on kb.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | i_smax2 | |||
| real(kind=wp), | intent(in) | :: | i4dt | |||
| logical, | intent(in) | :: | use_open | |||
| real(kind=wp), | intent(in) | :: | dy_cu(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | areaT(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | h_layer(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | bathy(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | slope_x(nx+1,ny,nz+1) | |||
| real(kind=wp), | intent(in) | :: | khth_u(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | open_u(nx+1,ny,nz) | |||
| real(kind=wp), | intent(inout) | :: | uhD(nx+1,ny,nz) |
v-face GM column recurrence — mirror of gm_column_x with the
v-stagger. Interior v-face (j=2..ny) pairs columns js=j-1 (south)
and j (north). See gm_column_x for the open-column (use_open)
rule and the MOM6 nk_linear divergence note.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | i_smax2 | |||
| real(kind=wp), | intent(in) | :: | i4dt | |||
| logical, | intent(in) | :: | use_open | |||
| real(kind=wp), | intent(in) | :: | dx_cv(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | areaT(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | h_layer(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | bathy(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | slope_y(nx,ny+1,nz+1) | |||
| real(kind=wp), | intent(in) | :: | khth_v(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | open_v(nx,ny+1,nz) | |||
| real(kind=wp), | intent(inout) | :: | vhD(nx,ny+1,nz) |
Flat-impl GM kernel. Passes: CFL-clamp the 2D face KhTh, the
u-face and v-face column recurrences into uhD/vhD, the
gm_src PE release. Each face’s column sweep is serial in k (the
uhtot recurrence) but faces parallelise over (i,j).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | dt | |||
| real(kind=wp), | intent(in) | :: | khth | |||
| real(kind=wp), | intent(in) | :: | khth_max_cfl | |||
| real(kind=wp), | intent(in) | :: | slope_max | |||
| real(kind=wp), | intent(in) | :: | rho0 | |||
| logical, | intent(in) | :: | use_ext | |||
| logical, | intent(in) | :: | use_open |
z-level closed faces active ( |
||
| real(kind=wp), | intent(in) | :: | dy_cu(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | dx_cv(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | idxCu(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | idyCv(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | idyCu(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | idxCv(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | areaT(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | wet_u(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | wet_v(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | h_layer(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | bathy(nx,ny) |
Bed depth |
||
| real(kind=wp), | intent(in) | :: | slope_x(nx+1,ny,nz+1) | |||
| real(kind=wp), | intent(in) | :: | slope_y(nx,ny+1,nz+1) | |||
| real(kind=wp), | intent(in) | :: | n2_u(nx+1,ny,nz+1) | |||
| real(kind=wp), | intent(in) | :: | n2_v(nx,ny+1,nz+1) | |||
| real(kind=wp), | intent(in) | :: | khth_ext_u(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | khth_ext_v(nx,ny+1) | |||
| real(kind=wp), | intent(in) | :: | open_u(nx+1,ny,nz) |
Per-layer 0/1 u-face open mask ( |
||
| real(kind=wp), | intent(in) | :: | open_v(nx,ny+1,nz) |
v-face twin of |
||
| real(kind=wp), | intent(out) | :: | khth_u(nx+1,ny) | |||
| real(kind=wp), | intent(out) | :: | khth_v(nx,ny+1) | |||
| real(kind=wp), | intent(out) | :: | uhD(nx+1,ny,nz) | |||
| real(kind=wp), | intent(out) | :: | vhD(nx,ny+1,nz) | |||
| real(kind=wp), | intent(out) | :: | gm_src(nx,ny) |
GM potential-energy release at cell centres for the MEKE seam:
gm_src = 1/4 * Sum_k rho0 * (KHSlope^2N^2) * h
over the four straddling faces, summed over interior interfaces;
slope clamped to slope_max, N^2 floored at 0. gm_src >= 0 for
a stable tilted column. An interface value is attributed to the
layer below it (kb=K-1); bed + surface carry zero slope/N^2.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | slope_max | |||
| real(kind=wp), | intent(in) | :: | rho0 | |||
| real(kind=wp), | intent(in) | :: | h_layer(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | slope_x(nx+1,ny,nz+1) | |||
| real(kind=wp), | intent(in) | :: | slope_y(nx,ny+1,nz+1) | |||
| real(kind=wp), | intent(in) | :: | n2_u(nx+1,ny,nz+1) | |||
| real(kind=wp), | intent(in) | :: | n2_v(nx,ny+1,nz+1) | |||
| real(kind=wp), | intent(in) | :: | khth_u(nx+1,ny) | |||
| real(kind=wp), | intent(in) | :: | khth_v(nx,ny+1) | |||
| real(kind=wp), | intent(out) | :: | gm_src(nx,ny) |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(ocean_gm_t), | intent(inout) | :: | this |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(ocean_gm_t), | intent(inout) | :: | this |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(ocean_gm_t), | intent(inout) | :: | this |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(ocean_gm_t), | intent(inout) | :: | this |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(ocean_gm_t), | intent(inout) | :: | this |
Allocate the 2D face KhTh fields, per-layer face transports, and
the gm_src PE-release diagnostic. Always allocates (configure
runs after init); host allocation (no do concurrent before
enter_data).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(ocean_gm_t), | intent(inout) | :: | this | |||
| type(hgrid_t), | intent(in) | :: | grid | |||
| integer, | intent(in), | optional | :: | nz_ml |