Gent-McWilliams thickness diffusion as its OWN sequential operator:
move h_layer AND every tracer by the bolus transport gm%uhD/
gm%vhD, which gm_compute_transports has JUST filled from this
same, untouched h_layer with this same dt.
MOM6 parity: thickness_diffuse runs after step_MOM_dyn_split_RK2
and updates h in place, h -= dt·IareaT·(div uhD), while
adding uhD·dt to uhtr so the SAME tracer advection that carries
the resolved transport carries the bolus one. Here:
TR_MODE_ADVECT): the bolus flux goes
through the resolved path’s own PPM tracer kernels
(tracer_advect_{zonal,meridional}_one_impl), interleaved with
the two thickness applies exactly as in
continuity_tracer_step_split (x: advect at h, apply; seam
refresh; y: advect at h*, apply) — so a uniform tracer stays
uniform (CWC) and the content is conserved to round-off;TR_MODE_ACCUMULATE): uhD·dt/vhD·dt join
the window accumulator uhtr/vhtr (MOM6’s uhtr += uhD·dt)
and the concentration hold is re-weighted onto the new h, so
the drain spends the bolus transport with the resolved one.Positivity: uhD is capped per face by A·(h − H_VANISHED)/(4·dt) of
the DONOR at the h passed in, so the four faces of a cell remove at
most h − H_VANISHED over dt — no layer is taken below
min(h, H_VANISHED), whatever the dynamics left. (Folded into the
resolved sweeps, as until 2026-10, the cap bounded the stage-entry
h and the resolved outflow came on top — see rdb_ocean_gm.)
Edges: the bolus transport is ZEROED on every physical edge face
that is not periodic or a tripolar fold — walls, sponges and every
open-boundary type — as MOM6 masks the GM slopes / KhTh by
OBCmaskCu/Cv: no GM flux leaves the domain, so no budget term
and no ghost fill is needed. An MPI seam (has_* false) is
interior and keeps its transport.
I1′: fillers keep their donor’s concentration through this operator
— the PPM kernel reads a filler’s hTr/h, which IS c_live under
I1′, and a filler cannot DONATE (its availability is 0); what it
receives arrives at its live neighbour’s concentration. The pool
(multilayer_state_t%enforce_vanished_content) at the tail of the
outer step restores I1′ exactly, as after the resolved continuity.
budget_w multiplies the heat/salt horizontal-advection budget
increments: this operator runs AFTER the RK2 stage average, so it
records 1/ocean_budget_stage_weight (2 under ssp_rk2, 1 under
pred_corr) for the console’s per-step weight to recover it 1:1.
set_flux_h (optional, default false): leave the bolus thickness
divergence in ms%flux_h_layer for the eulerian_z vertical
advection (compute_w_from_continuity), which then cancels it per
layer exactly as it cancels the resolved divergence.
The caller refreshes the h / tracer ghosts afterwards
(ocean_halo_exchange_ml_state + periodic wrap + fold). No-op when
GM is uninitialised or disabled.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(hgrid_t), | intent(in) | :: | grid | |||
| type(ocean_metrics_t), | intent(in) | :: | metrics | |||
| type(continuity_t), | intent(inout) | :: | this | |||
| type(multilayer_state_t), | intent(inout) | :: | ms | |||
| type(ocean_gm_t), | intent(inout) | :: | gm |
|
||
| real(kind=wp), | intent(in) | :: | dt |
The step the transports were capped for (s). |
||
| real(kind=wp), | intent(in) | :: | budget_w |
Budget bookkeeping weight (see above). |
||
| integer, | intent(in) | :: | tracer_mode |
|
||
| type(ocean_bc_state_t), | intent(in), | optional | :: | bc |
Edge tags (absent ⇒ every edge a wall). |
|
| logical, | intent(in), | optional | :: | set_flux_h |
Leave the bolus divergence in |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| real(kind=wp), | private | :: | div | ||||
| integer, | private | :: | i | ||||
| integer, | private | :: | it | ||||
| integer, | private | :: | j | ||||
| integer, | private | :: | k | ||||
| logical, | private | :: | keep_e | ||||
| logical, | private | :: | keep_n | ||||
| logical, | private | :: | keep_s | ||||
| logical, | private | :: | keep_w | ||||
| integer, | private | :: | nghost | ||||
| integer, | private | :: | nx | ||||
| integer, | private | :: | nx_phys | ||||
| integer, | private | :: | ny | ||||
| integer, | private | :: | ny_phys | ||||
| integer, | private | :: | nz | ||||
| logical, | private | :: | per_x | ||||
| logical, | private | :: | per_y | ||||
| integer, | private | :: | tag_e | ||||
| integer, | private | :: | tag_n | ||||
| integer, | private | :: | tag_s | ||||
| integer, | private | :: | tag_w | ||||
| logical, | private | :: | want_flux_h |
subroutine continuity_gm_apply(grid, metrics, this, ms, gm, dt, budget_w, tracer_mode, bc, & set_flux_h) !! Gent-McWilliams thickness diffusion as its OWN sequential operator: !! move `h_layer` AND every tracer by the bolus transport `gm%uhD`/ !! `gm%vhD`, which `gm_compute_transports` has JUST filled from this !! same, untouched `h_layer` with this same `dt`. !! !! MOM6 parity: `thickness_diffuse` runs after `step_MOM_dyn_split_RK2` !! and updates `h` in place, `h -= dt·IareaT·(div uhD)`, while !! adding `uhD·dt` to `uhtr` so the SAME tracer advection that carries !! the resolved transport carries the bolus one. Here: !! !! * every-step tracers (`TR_MODE_ADVECT`): the bolus flux goes !! through the resolved path's own PPM tracer kernels !! (`tracer_advect_{zonal,meridional}_one_impl`), interleaved with !! the two thickness applies exactly as in !! `continuity_tracer_step_split` (x: advect at h, apply; seam !! refresh; y: advect at h*, apply) — so a uniform tracer stays !! uniform (CWC) and the content is conserved to round-off; !! * windowed tracers (`TR_MODE_ACCUMULATE`): `uhD·dt`/`vhD·dt` join !! the window accumulator `uhtr`/`vhtr` (MOM6's `uhtr += uhD·dt`) !! and the concentration hold is re-weighted onto the new `h`, so !! the drain spends the bolus transport with the resolved one. !! !! Positivity: `uhD` is capped per face by `A·(h − H_VANISHED)/(4·dt)` of !! the DONOR at the `h` passed in, so the four faces of a cell remove at !! most `h − H_VANISHED` over `dt` — no layer is taken below !! `min(h, H_VANISHED)`, whatever the dynamics left. (Folded into the !! resolved sweeps, as until 2026-10, the cap bounded the stage-entry !! `h` and the resolved outflow came on top — see `rdb_ocean_gm`.) !! !! Edges: the bolus transport is ZEROED on every physical edge face !! that is not periodic or a tripolar fold — walls, sponges and every !! open-boundary type — as MOM6 masks the GM slopes / KhTh by !! `OBCmaskCu/Cv`: no GM flux leaves the domain, so no budget term !! and no ghost fill is needed. An MPI seam (`has_*` false) is !! interior and keeps its transport. !! !! I1′: fillers keep their donor's concentration through this operator !! — the PPM kernel reads a filler's `hTr/h`, which IS `c_live` under !! I1′, and a filler cannot DONATE (its availability is 0); what it !! receives arrives at its live neighbour's concentration. The pool !! (`multilayer_state_t%enforce_vanished_content`) at the tail of the !! outer step restores I1′ exactly, as after the resolved continuity. !! !! `budget_w` multiplies the heat/salt horizontal-advection budget !! increments: this operator runs AFTER the RK2 stage average, so it !! records `1/ocean_budget_stage_weight` (2 under ssp_rk2, 1 under !! pred_corr) for the console's per-step weight to recover it 1:1. !! !! `set_flux_h` (optional, default false): leave the bolus thickness !! divergence in `ms%flux_h_layer` for the `eulerian_z` vertical !! advection (`compute_w_from_continuity`), which then cancels it per !! layer exactly as it cancels the resolved divergence. !! !! The caller refreshes the h / tracer ghosts afterwards !! (`ocean_halo_exchange_ml_state` + periodic wrap + fold). No-op when !! GM is uninitialised or disabled. type(hgrid_t), intent(in) :: grid type(ocean_metrics_t), intent(in) :: metrics type(continuity_t), intent(inout) :: this type(multilayer_state_t), intent(inout) :: ms type(ocean_gm_t), intent(inout) :: gm !! `uhD`/`vhD` (inout: the edge closure and the fold-line !! projection are applied to them in place). real(wp), intent(in) :: dt !! The step the transports were capped for (s). real(wp), intent(in) :: budget_w !! Budget bookkeeping weight (see above). integer, intent(in) :: tracer_mode !! `TR_MODE_ADVECT` or `TR_MODE_ACCUMULATE`. type(ocean_bc_state_t), intent(in), optional :: bc !! Edge tags (absent ⇒ every edge a wall). logical, intent(in), optional :: set_flux_h !! Leave the bolus divergence in `ms%flux_h_layer` (eulerian_z). integer :: nx, ny, nz, nx_phys, ny_phys, nghost, it integer :: i, j, k integer :: tag_w, tag_e, tag_s, tag_n logical :: keep_w, keep_e, keep_s, keep_n, per_x, per_y, want_flux_h real(wp) :: div if (.not. gm%is_init) return if (.not. gm%enable) return if (.not. allocated(gm%uhD)) return nx = grid%nx_total ny = grid%ny_total nz = ms%nz_ml nx_phys = grid%nx_phys ny_phys = grid%ny_phys nghost = grid%nghost want_flux_h = .false. if (present(set_flux_h)) want_flux_h = set_flux_h ! ---- Edge closure (MOM6 OBCmaskCu/Cv): keep only periodic / fold ! edges and MPI seams. tag_w = OBC_WALL tag_e = OBC_WALL tag_s = OBC_WALL tag_n = OBC_WALL per_x = .false. per_y = .false. if (present(bc)) then tag_w = ocean_bc_outer_face_tag(bc%west%bc_type) tag_e = ocean_bc_outer_face_tag(bc%east%bc_type) tag_s = ocean_bc_outer_face_tag(bc%south%bc_type) tag_n = ocean_bc_outer_face_tag(bc%north%bc_type) if (.not. bc%has_west) tag_w = OBC_PERIODIC if (.not. bc%has_east) tag_e = OBC_PERIODIC if (.not. bc%has_south) tag_s = OBC_PERIODIC if (.not. bc%has_north) tag_n = OBC_PERIODIC per_x = bc%periodic_x .and. .not. ocean_halo_is_decomposed_x() per_y = bc%periodic_y .and. .not. ocean_halo_is_decomposed_y() end if keep_w = tag_w == OBC_PERIODIC .or. tag_w == OBC_TRIPOLAR_FOLD keep_e = tag_e == OBC_PERIODIC .or. tag_e == OBC_TRIPOLAR_FOLD keep_s = tag_s == OBC_PERIODIC .or. tag_s == OBC_TRIPOLAR_FOLD keep_n = tag_n == OBC_PERIODIC .or. tag_n == OBC_TRIPOLAR_FOLD do concurrent(k=1:nz, j=1:ny) if (.not. keep_w) gm%uhD(nghost + 1, j, k) = 0.0_wp if (.not. keep_e) gm%uhD(nghost + nx_phys + 1, j, k) = 0.0_wp end do do concurrent(k=1:nz, i=1:nx) if (.not. keep_s) gm%vhD(i, nghost + 1, k) = 0.0_wp if (.not. keep_n) gm%vhD(i, nghost + ny_phys + 1, k) = 0.0_wp end do ! Tripolar fold-line projection (see `continuity_tracer_step_split`): ! the duplicated fold-line face must carry ONE antisymmetric flux so ! what leaves (i,nj) north is exactly what enters (ni+1-i,nj). if (present(bc)) then if (bc%north_fold) call ocean_fold_north_v_face(gm%vhD, nx, ny + 1, nz, & nx_phys, ny_phys, nghost) end if ! ---- x half: tracers at h^n, then h^n -> h*. if (tracer_mode == TR_MODE_ACCUMULATE) then call drain_copy_3d(nx, ny, nz, ms%h_layer, this%hprev_work) do concurrent(k=1:nz, j=1:ny, i=1:nx + 1) this%uhtr(i, j, k) = this%uhtr(i, j, k) + gm%uhD(i, j, k)*dt end do else call gm_tracer_advect_x(grid, metrics, this, ms, gm%uhD, dt, budget_w) end if do concurrent(k=1:nz, j=1:ny, i=1:nx) local(div) div = (gm%uhD(i + 1, j, k) - gm%uhD(i, j, k))*metrics%iareaT(i, j) ms%h_layer(i, j, k) = ms%h_layer(i, j, k) - dt*div end do if (want_flux_h) then do concurrent(k=1:nz, j=1:ny, i=1:nx) ms%flux_h_layer(i, j, k) = (gm%uhD(i + 1, j, k) - gm%uhD(i, j, k))*metrics%iareaT(i, j) end do end if ! ---- Mid-split seam refresh (the meridional PPM reads h / tracer ! ghosts the neighbour's x half just moved) — the same three steps as ! `continuity_tracer_step_split`. call profiler_start("ocean_comms_ml") call ocean_halo_centre(ms%h_layer, nz) if (allocated(ms%tracers)) then do it = 1, size(ms%tracers) if (.not. allocated(ms%tracers(it)%hTr)) cycle call ocean_halo_centre(ms%tracers(it)%hTr, nz) end do end if call profiler_stop("ocean_comms_ml") if (per_x .or. per_y) then call ocean_periodic_wrap_centre_3d(ms%h_layer, nx, ny, nz, & nx_phys, ny_phys, nghost, per_x, per_y, no_wait=.true.) if (allocated(ms%tracers)) then do it = 1, size(ms%tracers) if (.not. allocated(ms%tracers(it)%hTr)) cycle call ocean_periodic_wrap_centre_3d(ms%tracers(it)%hTr, nx, ny, nz, & nx_phys, ny_phys, nghost, per_x, per_y, no_wait=.true.) end do end if !$acc wait(1) end if if (present(bc)) call ocean_fold_wrap_centre_3d_state(grid, bc, ms) ! ---- y half: tracers at h*, then h* -> h^{n+1}. if (tracer_mode == TR_MODE_ACCUMULATE) then do concurrent(k=1:nz, j=1:ny + 1, i=1:nx) this%vhtr(i, j, k) = this%vhtr(i, j, k) + gm%vhD(i, j, k)*dt end do else call gm_tracer_advect_y(grid, metrics, this, ms, gm%vhD, dt, budget_w) end if do concurrent(k=1:nz, j=1:ny, i=1:nx) local(div) div = (gm%vhD(i, j + 1, k) - gm%vhD(i, j, k))*metrics%iareaT(i, j) ms%h_layer(i, j, k) = ms%h_layer(i, j, k) - dt*div end do if (want_flux_h) then do concurrent(k=1:nz, j=1:ny, i=1:nx) ms%flux_h_layer(i, j, k) = ms%flux_h_layer(i, j, k) + & (gm%vhD(i, j + 1, k) - gm%vhD(i, j, k))*metrics%iareaT(i, j) end do end if ! ---- Windowed mode: re-weight the held concentration onto the new h ! (the same hold `continuity_tracer_step_split` keeps per stage), with ! the post-average budget weight. if (tracer_mode == TR_MODE_ACCUMULATE .and. allocated(ms%tracers)) then do it = 1, size(ms%tracers) if (.not. allocated(ms%tracers(it)%hTr)) cycle if (.not. ms%tracers(it)%do_horizontal_advection) cycle select case (ms%tracers(it)%budget_id) case (TRACER_BUDGET_HEAT) call drain_rescale_hTr_budget(nx, ny, nz, ms%h_layer, this%hprev_work, & budget_w, ms%tracers(it)%hTr, ms%heat_budget_horiz_adv) case (TRACER_BUDGET_SALT) call drain_rescale_hTr_budget(nx, ny, nz, ms%h_layer, this%hprev_work, & budget_w, ms%tracers(it)%hTr, ms%salt_budget_horiz_adv) case default call drain_rescale_hTr(nx, ny, nz, ms%h_layer, this%hprev_work, & ms%tracers(it)%hTr) end select end do end if end subroutine continuity_gm_apply