Discrete tripolar north-fold exchange (Murray 1996), LOCAL to the tile
that holds the whole fold row. Pure seam operators only (state
orchestration lives in rdb_ocean_fold_apply). Free procedures,
explicit-shape dummies, j-outer / i-inner do concurrent.
Every map below reads the mirror point from the SAME array, so it is
exact only on a tile that holds the whole fold row: the north-edge
rank of a north-south split (px = 1, any py), where nx_phys
is the global ni and ny_phys counts the tile’s rows up to the
fold line (every map is relative to the tile’s own last row, so no
global j offset enters). Callers apply it only there — the gate is
the rank-local bc%north_fold (tag .and. has_north); on the other
ranks the north ghosts are an MPI seam the halo exchange fills. An
east-west split (px > 1) needs the mirror of column i, column
ni+1-i, from another rank: every fold site routes that case
through the owner-routed exchange of rdb_ocean_fold_exchange
instead of these kernels — the state-level dispatchers of
rdb_ocean_fold_apply, the barotropic fast loop (two exchanges per
substep instead of its inline folds, see barotropic_substep) and the
setup-time folds (engine_setup, configure_ocean_land_mask). The
fold also reads the nghost rows below the fold line, so the north
tile must be at least nghost+1 rows tall, and under px > 1 every
tile at least nghost+1 columns wide (both refused at configure).
Continuous grid coordinates: T-cell (i,j), physical i ∈ 1..ni,
j ∈ 1..nj, occupies [i-1,i]×[j-1,j]; storage index = nghost + physical.
* T h(i,j) centre (i-1/2, j-1/2)
* Cu u(i,j) WEST face (i-1, j-1/2) extent nx+1
* Cv v(i,j) SOUTH face (i-1/2, j-1) extent ny+1
* Bu q(i,j) SW corner (i-1, j-1) extent (nx+1,ny+1)
(rdb_multilayer_state u_face_x_layer/v_face_y_layer docstrings;
coriolis_adv “zeta_corner(i,j) sits at (i-1/2,j-1/2)” relative to
T(i,j); metrics%wet_q “SW corner of T-cell (i,j)”.)
The fold line is y = nj, the NORTH edge of T-row nj. A point (x, y) north of it is the point (ni - x, 2nj - y) (i-periodic, period ni), reached through a 180° rotation of the local frame: both unit vectors reverse, so a true-vector component (u, v, a face flux) NEGATES and a scalar — and the pseudoscalar vorticity / PV (rotation preserves orientation) — COPIES.
Solving x’ = ni - x, y’ = 2nj - y for each stagger’s storage index: | Stagger | x(i) | y(j) | i-map (storage) | j-map (storage) | fold-line row | |---------|-------|-------|-----------------------|-----------------------|---------------| | T | i-½ | j-½ | i’ = 2ng+ni+1 - i | j’ = 2ng+2nj+1 - j | none | | u (Cu) | i-1 | j-½ | i’ = 2ng+ni+2 - i | j’ = 2ng+2nj+1 - j | none | | v (Cv) | i-½ | j-1 | i’ = 2ng+ni+1 - i | j’ = 2ng+2nj+2 - j | ng+nj+1 | | corner | i-1 | j-1 | i’ = 2ng+ni+2 - i | j’ = 2ng+2nj+2 - j | ng+nj+1 |
T and u points never lie on y = nj, so their exchange is a pure
halo fill of rows j > ng+nj. v and corners have a row ON the fold
line: storage row ng+nj+1 — the south face of the first ghost row,
i.e. the NORTH face of the last physical T-row. (The pre-2026-09
code used jsum = 2ng+2nj, j_fold = ng+nj: MOM6’s NORTH-face /
NE-corner rule applied to roundabout’s SOUTH-face / SW-corner
storage. It antisymmetrised the south face of the last T-row — an
ordinary interior face — and filled the true fold-line face from
-v(i', ng+nj-1), so every cell of the last row took an unrelated
flux through its north face: the global-tripolar B1 mass leak.)
Cross-check against MOM6 (symmetric memory, pass_vector /
FMS mpp_update_domains with a folded north edge, CGRID_NE):
MOM6 v(i,J) is the north face of cell j = roundabout v(i,J+1),
MOM6 q(I,J) NE corner = roundabout q(I+1,J+1), MOM6 u(I,j) east
face = roundabout u(I+1,j). MOM6’s fold maps v(i, nj+d) ←
-v(ni+1-i, nj-d), q(I, nj+d) ← q(ni-I, nj-d), u(I, nj+d) ←
-u(ni-I, nj+1-d); shifting by the index offsets gives exactly the
table above (MOM6’s fold-line row J = nj ↔ roundabout row nj+1).
On row ng+nj+1 the storage slots i and i’ (v: i’ = 2ng+ni+1-i; corner: i’ = 2ng+ni+2-i) are the SAME physical face / vertex seen from the two sides of the fold, with opposite orientation. So a single DOF is stored twice and must satisfy v(i) = -v(i’) (a true normal velocity / normal flux through one edge, which leaves cell (i,nj) northward and ENTERS cell (i’,nj) from its north) and q(i) = q(i’) for scalars/vorticity. The dynamics updates both slots independently; the projection overwrites the WEST half from the (negated) east mirror so the row is exactly (anti)symmetric. This is what makes the cross-fold mass flux telescope: Σ_i F(i, ng+nj+1) pairs off to zero. Self-conjugate slots (i = i’): * v: only when ni is odd (column (ni+1)/2) → 0 (a normal velocity equal to minus itself). ni even (every real tripolar grid) has none. * corner: c = ni/2+1 and c = 1 ≡ ni+1 (periodic images) — the two bipoles. Vector corner components → 0; scalars copy. Rows above the fold line (j > ng+nj+1) are pure mirrored images.
Caller ordering (MANDATORY): periodic-x wrap FIRST, then the fold, so the fold reads already cyclically-wrapped ghost columns at the corners.
All helpers stay pure + do concurrent so they run both on
device-mapped arrays (stdpar) and during host setup of metric ghosts.
Fill the north halo of a 2D cell-centred field by the T-fold.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | fld(nx_total,ny_total) |
Cell-centred field, shape (nx_total, ny_total). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost |
3D T-fold halo-fill — identical per level.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | fld(nx_total,ny_total,nz) |
Cell-centred 3D field, shape (nx_total, ny_total, nz). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost |
2D Bu-corner fold: north-halo fill + on-line projection.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | fld(nx_face,ny_face) |
Corner field, shape (nx_total+1, ny_total+1). |
||
| integer, | intent(in) | :: | nx_face | |||
| integer, | intent(in) | :: | ny_face | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in) | :: | negate |
.true. → negate (true-vector component); .false. → copy (scalar / pseudoscalar vorticity). |
Fill the north halo of a 2D x-face (Cu) field. Sign-flipped by
default (negate absent / .true., the true-vector contract —
wind stress, velocity). negate=.false. copies instead: for a
SCALAR carried on a u-face (e.g. the viscous remnant visc_rem_u
— a fraction, not a flux component), the 180-degree fold rotation
still swaps which side of the seam the value sits on, but the
value itself does not change sign (see fold_north_corner_2d’s
negate for the matching corner-stagger contract).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | u(nx_face,ny_total) |
x-face field, shape (nx_total+1, ny_total). |
||
| integer, | intent(in) | :: | nx_face | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|
3D x-face (Cu) north-halo fill, per-level identical. See the 2D
twin for the negate (vector vs. scalar) contract.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | u(nx_face,ny_total,nz) |
x-face 3D field, shape (nx_total+1, ny_total, nz). |
||
| integer, | intent(in) | :: | nx_face | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|
2D y-face (Cv) fold: north-halo fill + on-line antisymmetric
projection at the fold row. Used for the barotropic bt_vbt
field in the BT fast loop. negate (default .true., see the
u-face twin) selects true-vector (sign flip + self-conjugate
zero) vs. scalar (copy + self-conjugate left unchanged, matching
fold_north_corner_2d’s scalar contract) — a scalar on a v-face
(e.g. visc_rem_v) is the SAME physical attribute of the SAME
face seen from both sides of the seam, so the duplicated DOF at
the fold line must agree, not cancel.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | v(nx_total,ny_face) |
y-face 2D field, shape (nx_total, ny_total+1). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_face | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|
3D y-face (Cv) fold: north-halo fill + on-line antisymmetric
projection at the fold row. See the 2D twin for the negate
(vector vs. scalar) contract.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | v(nx_total,ny_face,nz) |
y-face 3D field, shape (nx_total, ny_total+1, nz). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_face | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|
Fill the north halo of a 2D cell-centred field by the T-fold.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | fld(nx_total,ny_total) |
Cell-centred field, shape (nx_total, ny_total). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost |
3D T-fold halo-fill — identical per level.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | fld(nx_total,ny_total,nz) |
Cell-centred 3D field, shape (nx_total, ny_total, nz). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost |
2D Bu-corner fold: north-halo fill + on-line projection.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | fld(nx_face,ny_face) |
Corner field, shape (nx_total+1, ny_total+1). |
||
| integer, | intent(in) | :: | nx_face | |||
| integer, | intent(in) | :: | ny_face | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in) | :: | negate |
.true. → negate (true-vector component); .false. → copy (scalar / pseudoscalar vorticity). |
Fill the north halo of a 2D x-face (Cu) field. Sign-flipped by
default (negate absent / .true., the true-vector contract —
wind stress, velocity). negate=.false. copies instead: for a
SCALAR carried on a u-face (e.g. the viscous remnant visc_rem_u
— a fraction, not a flux component), the 180-degree fold rotation
still swaps which side of the seam the value sits on, but the
value itself does not change sign (see fold_north_corner_2d’s
negate for the matching corner-stagger contract).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | u(nx_face,ny_total) |
x-face field, shape (nx_total+1, ny_total). |
||
| integer, | intent(in) | :: | nx_face | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|
3D x-face (Cu) north-halo fill, per-level identical. See the 2D
twin for the negate (vector vs. scalar) contract.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | u(nx_face,ny_total,nz) |
x-face 3D field, shape (nx_total+1, ny_total, nz). |
||
| integer, | intent(in) | :: | nx_face | |||
| integer, | intent(in) | :: | ny_total | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|
2D y-face (Cv) fold: north-halo fill + on-line antisymmetric
projection at the fold row. Used for the barotropic bt_vbt
field in the BT fast loop. negate (default .true., see the
u-face twin) selects true-vector (sign flip + self-conjugate
zero) vs. scalar (copy + self-conjugate left unchanged, matching
fold_north_corner_2d’s scalar contract) — a scalar on a v-face
(e.g. visc_rem_v) is the SAME physical attribute of the SAME
face seen from both sides of the seam, so the duplicated DOF at
the fold line must agree, not cancel.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | v(nx_total,ny_face) |
y-face 2D field, shape (nx_total, ny_total+1). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_face | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|
3D y-face (Cv) fold: north-halo fill + on-line antisymmetric
projection at the fold row. See the 2D twin for the negate
(vector vs. scalar) contract.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(inout) | :: | v(nx_total,ny_face,nz) |
y-face 3D field, shape (nx_total, ny_total+1, nz). |
||
| integer, | intent(in) | :: | nx_total | |||
| integer, | intent(in) | :: | ny_face | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx_phys | |||
| integer, | intent(in) | :: | ny_phys | |||
| integer, | intent(in) | :: | nghost | |||
| logical, | intent(in), | optional | :: | negate |
|