Conservative minimum-layer-thickness adjustment for the isopycnal
(VCOORD_LAGRANGIAN) ocean path.
Motivation: in the remap-free Lagrangian coordinate an eddy can displace
an interface so far that a layer thins to ~0 (“outcropping”), and then
u = hu/h blows up. The existing angstrom_h floor lifts a sub-floor
layer with max(h_new, angstrom_h), which INJECTS mass (non-conservative:
total column thickness, SSH, and tracer mass all drift up on every floored
step) and the injected volume shapes the eddy field.
This module instead performs a CONSERVATIVE adjustment: when a layer thins below the floor, the deficit is borrowed from the surplus layers of the SAME water column — the interface is moved, no mass is created. Per water column the following are conserved to round-off: * total thickness Sigma_k h_k (SSH unchanged) * momentum Sigma_k h_face_k . u_face_k (per C-grid face) * every tracer mass Sigma_k h_k . Tr_k
The adjustment is a STRICT no-op on any column where all layers already meet the floor: those columns (and their faces) are left byte-unchanged so the isopycnal interface structure — and hence the baroclinic mode we are modelling — is never pinned. This is the load-bearing constraint that distinguishes this from a z*/sigma remap.
Implementation: reuse the conservative per-column remap_column primitive
(the same engine the ALE remap uses) with a “floor-only” target column —
each sub-floor layer inflated to the floor, the excess drawn conservatively
from the surplus layers so the column total is preserved.
COST STRUCTURE (the 31%-of-runtime restructure, 2026-07-24): the borrow
is a no-op on every non-grounded column, but the original implementation
still paid ~8 full-field passes per call on a healthy domain
(unconditional target build + copy-back, per-tracer concentration
divisions before the gate, and per-face NEIGHBOUR COLUMN re-scans with
k-strided access). Now: ONE coalesced pass builds a 2D grounded mask +
a global count; zero grounded columns ⇒ the whole call returns after
that single read; otherwise every kernel consults the mask (2 loads) and
only grounded columns / active faces do work — and reads fall back to
h_old on non-grounded neighbours, where h_new ≡ h_old by
construction. Byte-identical results in all cases.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| real(kind=wp), | private, | parameter | :: | H_CONC_FLOOR | = | 1.0e-20_wp |
Pure 1/0 armour when forming a layer concentration c = q/h_old; only guards genuinely-zero source layers (never a physical thickness). |
Build the floor-only conservative target thickness column.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | h_old(nz) | |||
| real(kind=wp), | intent(in) | :: | h_floor | |||
| real(kind=wp), | intent(out) | :: | h_new(nz) | |||
| logical, | intent(out) | :: | grounded |
Apply the conservative minimum-thickness adjustment in place on ms.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(hgrid_t), | intent(in) | :: | grid | |||
| type(multilayer_state_t), | intent(inout) | :: | ms | |||
| real(kind=wp), | intent(inout) | :: | h_new(grid%nx_total,grid%ny_total,ms%nz_ml) | |||
| real(kind=wp), | intent(inout) | :: | grounded_mask(grid%nx_total,grid%ny_total,1) | |||
| real(kind=wp), | intent(in) | :: | h_floor |
Minimum layer thickness (m); the isopycnal |
Copy the target field into h_layer on grounded columns only — non-grounded columns’ h_new was never written, and their old h_layer is already the exact target.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | h_new(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | mask(nx,ny,1) | |||
| real(kind=wp), | intent(inout) | :: | h_layer(nx,ny,nz) |
ONE coalesced pass: mask(i,j,1) = 1.0 iff any layer of column (i,j)
is strictly below the floor; n_grounded counts them (explicit
OpenACC reduction — a sum() on a present-mapped array would run
host-side under NVHPC non-managed mode and read the stale shadow).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | h_old(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | h_floor | |||
| real(kind=wp), | intent(out) | :: | mask(nx,ny,1) | |||
| integer, | intent(out) | :: | n_grounded |
target. Non-grounded columns are SKIPPED — their h_new is never read downstream (every consumer falls back to h_old via the mask).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | h_old(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | h_floor | |||
| real(kind=wp), | intent(in) | :: | mask(nx,ny,1) | |||
| real(kind=wp), | intent(out) | :: | h_new(nx,ny,nz) |
Conservative tracer remap gated on the grounded mask. Non-grounded columns are skipped -> hTr byte-unchanged; the concentration divisions only happen on grounded columns.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | h_old(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | h_new(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | mask(nx,ny,1) | |||
| real(kind=wp), | intent(inout) | :: | hTr(nx,ny,nz) |
Conservative east-face velocity remap gated on the grounded mask. A
face is active iff either adjacent cell is grounded (2 mask loads —
no neighbour-column re-scan); inactive faces are left byte-unchanged.
Face thickness is the arithmetic mean of the two adjacent cells
(outer walls take the single interior cell); the target side reads
h_new only where the mask is set — elsewhere h_new ≡ h_old by
construction, so h_old is read directly. remap_column preserves
the per-face momentum Sigma h_face . u.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | h_old(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | h_new(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | mask(nx,ny,1) | |||
| real(kind=wp), | intent(inout) | :: | u_face_x(nx+1,ny,nz) |
Conservative north-face velocity remap, mirror of the x routine.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny | |||
| integer, | intent(in) | :: | nz | |||
| real(kind=wp), | intent(in) | :: | h_old(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | h_new(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | mask(nx,ny,1) | |||
| real(kind=wp), | intent(inout) | :: | v_face_y(nx,ny+1,nz) |