coriolis_adv_compute_tendencies_hk Subroutine

public pure subroutine coriolis_adv_compute_tendencies_hk(grid, metrics, this, ms, u, v, h)

Public only for the unit-test suite (no production module imports it); ignore when developing production code in other modules. Per-layer PV-conserving Coriolis + horizontal-advection tendency in the Arakawa-Hsu (1990) form (“HK correction”). The wider 3-corner PV stencil at each face suppresses the spurious Hollingsworth-Källén instability that biases the simpler Sadourny 2-corner form at eddy-resolving resolutions.

Algorithm — 6 passes per call: 1. Pass 1: relative vorticity ζ at interior corners (same stencil as the Sadourny multilayer kernel) into q_corner. Wall corners get ζ = 0 (free-slip BC). 2. Pass 2: per-mass PV q = (f + ζ) / h_at_corner rewritten into q_corner. h_at_corner is the AREA-WEIGHTED 4-cell mean (each T thickness weighted by its areaT, normalised by the summed areas), with min/max clamps so wall corners collapse onto the available cells. Reduces to the plain 4-cell mean on uniform Cartesian (equal areas). 3. Pass 3: per-face mass fluxes — mass_flux_u = u_face_x · h_at_u_face (face-averaged thickness) and mass_flux_v symmetrically. Wall faces fall through to the single available cell (velocities are zero there anyway). 4. Pass 4: KE at cell centres — identical to Sadourny. 5. Pass 5: u-face tendency CAu(i,j) = a · mass_flux_v(i, j+1) (NE) + b · mass_flux_v(i-1, j+1) (NW) + c · mass_flux_v(i-1, j) (SW) + d · mass_flux_v(i, j) (SE) - grad_KE_x where each coefficient combines the face’s two end corners + one diagonal corner: a = (q(i,j+1) + q(i+1,j+1) + q(i,j)) / 12 b = (q(i,j+1) + q(i-1,j+1) + q(i,j)) / 12 c = (q(i,j+1) + q(i-1,j) + q(i,j)) / 12 d = (q(i,j+1) + q(i+1,j) + q(i,j)) / 12 6. Pass 6: v-face tendency — symmetric construction; overall minus sign on the q-stencil sum (Coriolis on v is -f·u): CAv(i,j) = -[a’ · mass_flux_u(i+1, j) + b’ · mass_flux_u(i, j) + c’ · mass_flux_u(i, j-1) + d’ · mass_flux_u(i+1, j-1)] - grad_KE_y

Reduction property (uniform h, uniform v): each coefficient evaluates to q/4, so the sum of 4 mass-flux terms is q · vh and the kernel collapses to the Sadourny (f+ζ)·v form bit-identically. This is the basis for the regression test.

Under &vcoord_nml zfixed_closed_faces (metrics%use_closed_faces) Passes 5/6 run a PAIR-FLOORED twin: every PV in a pair coefficient is evaluated with a corner thickness of at least half the larger of the pair’s two face thicknesses (hk_pair_coef). Without it the “cross” pairs — a corner PV times a transport whose far cell lies outside that corner — carry an unbounded h_face/h_corner, which a z-level staircase (a live partial bottom cell as thin as H_VANISHED against a full-depth neighbour) turns into a runaway (1-degree Southern Ocean: NaN at step 11). The floor keeps the pair coefficients symmetric, so the energy-conserving antisymmetry is kept, and is inactive wherever no cell outweighs the other three of its corner. The same twin runs with the knob OFF on the coordinates that lay static bed fillers (hk_pair_floor): there the open step face pairs a live cell with a 1e-4 m filler, and without the floor the matrix staircase leaves the remap a negative thickness at step 2 (compat matrix vcoord=zstar x coriolis=sadourny_hk). Every other coordinate ⇒ the original passes, bit-identical.

Arguments

Type IntentOptional Attributes Name
type(hgrid_t), intent(in) :: grid
type(ocean_metrics_t), intent(in) :: metrics
type(coriolis_adv_t), intent(inout) :: this
type(multilayer_state_t), intent(in) :: ms
real(kind=wp), intent(in) :: u(grid%nx_total+1,grid%ny_total,ms%nz_ml)

Face-velocity / thickness source arrays (outer-shim; the dispatcher forwards either the prognostic components or the u_av time-mean family under split_scheme = "pred_corr").

real(kind=wp), intent(in) :: v(grid%nx_total,grid%ny_total+1,ms%nz_ml)
real(kind=wp), intent(in) :: h(grid%nx_total,grid%ny_total,ms%nz_ml)

Calls

proc~~coriolis_adv_compute_tendencies_hk~~CallsGraph proc~coriolis_adv_compute_tendencies_hk coriolis_adv_compute_tendencies_hk local local proc~coriolis_adv_compute_tendencies_hk->local proc~hk_corner_h hk_corner_h proc~coriolis_adv_compute_tendencies_hk->proc~hk_corner_h proc~hk_pair_coef hk_pair_coef proc~coriolis_adv_compute_tendencies_hk->proc~hk_pair_coef proc~porous_narrow_3d porous_narrow_3d proc~coriolis_adv_compute_tendencies_hk->proc~porous_narrow_3d

Called by

proc~~coriolis_adv_compute_tendencies_hk~~CalledByGraph proc~coriolis_adv_compute_tendencies_hk coriolis_adv_compute_tendencies_hk proc~coriolis_adv_compute_tendencies coriolis_adv_compute_tendencies proc~coriolis_adv_compute_tendencies->proc~coriolis_adv_compute_tendencies_hk proc~run_stage run_stage proc~run_stage->proc~coriolis_adv_compute_tendencies proc~run_stage_split run_stage_split proc~run_stage_split->proc~coriolis_adv_compute_tendencies proc~ocean_dyn_step ocean_dyn_step proc~ocean_dyn_step->proc~run_stage proc~ocean_dyn_step_split ocean_dyn_step_split proc~ocean_dyn_step_split->proc~run_stage_split proc~engine_step engine_step proc~engine_step->proc~ocean_dyn_step proc~engine_step->proc~ocean_dyn_step_split proc~driver_run_ocean driver_run_ocean proc~driver_run_ocean->proc~engine_step proc~rdb_ocean_step rdb_ocean_step proc~rdb_ocean_step->proc~engine_step

Variables

Type Visibility Attributes Name Initial
real(kind=wp), private, parameter :: C1_12 = 1.0_wp/12.0_wp
real(kind=wp), private, parameter :: H_MIN_PV = 1.0e-12_wp

Floor for the corner-h divide; vanishing-layer columns get q ≈ (f+ζ)/H_MIN_PV which is large but finite — paired with mass_flux ≈ 0 at the same column so the product decays cleanly toward zero rather than blowing up.

real(kind=wp), private :: aNE
real(kind=wp), private :: aNW
real(kind=wp), private :: aSE
real(kind=wp), private :: aSW
real(kind=wp), private :: a_NE
real(kind=wp), private :: b_NW
real(kind=wp), private :: c_SW
real(kind=wp), private :: d_SE
real(kind=wp), private :: h_E

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_N

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_NE

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_NW

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_S

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_SE

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_SW

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_W

Closed-face branch only: corner thicknesses of the stencil.

real(kind=wp), private :: h_corner
real(kind=wp), private :: h_face
real(kind=wp), private :: h_u

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: h_v

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hm_den
real(kind=wp), private :: hm_num
real(kind=wp), private :: hu_NE

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hu_NW

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hu_SE

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hu_SW

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hv_NE

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hv_NW

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hv_SE

Closed-face branch only: face thicknesses of the stencil.

real(kind=wp), private :: hv_SW

Closed-face branch only: face thicknesses of the stencil.

integer, private :: i
integer, private :: ie
integer, private :: iw
integer, private :: j
integer, private :: jn
integer, private :: js
integer, private :: k
real(kind=wp), private :: ke_grad_x
real(kind=wp), private :: ke_grad_y
real(kind=wp), private :: ns
integer, private :: nu
integer, private :: nv
integer, private :: nx
integer, private :: ny
integer, private :: nz
real(kind=wp), private :: q_E
real(kind=wp), private :: q_N
real(kind=wp), private :: q_NE
real(kind=wp), private :: q_NW
real(kind=wp), private :: q_S
real(kind=wp), private :: q_SE
real(kind=wp), private :: q_SW
real(kind=wp), private :: q_W
real(kind=wp), private :: zeta_corner

Source Code

   pure subroutine coriolis_adv_compute_tendencies_hk(grid, metrics, this, ms, u, v, h)
      !! Public only for the unit-test suite (no production module imports it);
      !! ignore when developing production code in other modules.
      !! Per-layer PV-conserving Coriolis + horizontal-advection tendency
      !! in the Arakawa-Hsu (1990) form ("HK correction").  The wider
      !! 3-corner PV stencil at each face suppresses the spurious
      !! Hollingsworth-Källén instability that biases the simpler
      !! Sadourny 2-corner form at eddy-resolving resolutions.
      !!
      !! Algorithm — 6 passes per call:
      !!   1. Pass 1: relative vorticity ζ at interior corners (same
      !!      stencil as the Sadourny multilayer kernel) into `q_corner`.
      !!      Wall corners get ζ = 0 (free-slip BC).
      !!   2. Pass 2: per-mass PV `q = (f + ζ) / h_at_corner` rewritten
      !!      into `q_corner`.  `h_at_corner` is the AREA-WEIGHTED 4-cell
      !!      mean (each T thickness weighted by its `areaT`, normalised
      !!      by the summed areas), with `min/max` clamps so wall corners
      !!      collapse onto the available cells.  Reduces to the plain
      !!      4-cell mean on uniform Cartesian (equal areas).
      !!   3. Pass 3: per-face mass fluxes — `mass_flux_u = u_face_x ·
      !!      h_at_u_face` (face-averaged thickness) and `mass_flux_v`
      !!      symmetrically.  Wall faces fall through to the single
      !!      available cell (velocities are zero there anyway).
      !!   4. Pass 4: KE at cell centres — identical to Sadourny.
      !!   5. Pass 5: u-face tendency
      !!        CAu(i,j) = a · mass_flux_v(i,   j+1)   (NE)
      !!                 + b · mass_flux_v(i-1, j+1)   (NW)
      !!                 + c · mass_flux_v(i-1, j)     (SW)
      !!                 + d · mass_flux_v(i,   j)     (SE)
      !!                 - grad_KE_x
      !!      where each coefficient combines the face's two end
      !!      corners + one diagonal corner:
      !!        a = (q(i,j+1) + q(i+1,j+1) + q(i,j))   / 12
      !!        b = (q(i,j+1) + q(i-1,j+1) + q(i,j))   / 12
      !!        c = (q(i,j+1) + q(i-1,j)   + q(i,j))   / 12
      !!        d = (q(i,j+1) + q(i+1,j)   + q(i,j))   / 12
      !!   6. Pass 6: v-face tendency — symmetric construction; overall
      !!      minus sign on the q-stencil sum (Coriolis on v is `-f·u`):
      !!        CAv(i,j) = -[a' · mass_flux_u(i+1, j)
      !!                   + b' · mass_flux_u(i,   j)
      !!                   + c' · mass_flux_u(i,   j-1)
      !!                   + d' · mass_flux_u(i+1, j-1)]
      !!                   - grad_KE_y
      !!
      !! Reduction property (uniform h, uniform v): each coefficient
      !! evaluates to `q/4`, so the sum of 4 mass-flux terms is `q · vh`
      !! and the kernel collapses to the Sadourny `(f+ζ)·v` form
      !! bit-identically.  This is the basis for the regression test.
      !!
      !! Under `&vcoord_nml zfixed_closed_faces` (`metrics%use_closed_faces`)
      !! Passes 5/6 run a PAIR-FLOORED twin: every PV in a pair coefficient
      !! is evaluated with a corner thickness of at least half the larger of
      !! the pair's two face thicknesses (`hk_pair_coef`).  Without it the
      !! "cross" pairs — a corner PV times a transport whose far cell lies
      !! outside that corner — carry an unbounded `h_face/h_corner`, which a
      !! z-level staircase (a live partial bottom cell as thin as
      !! `H_VANISHED` against a full-depth neighbour) turns into a runaway
      !! (1-degree Southern Ocean: NaN at step 11).  The floor keeps the
      !! pair coefficients symmetric, so the energy-conserving antisymmetry
      !! is kept, and is inactive wherever no cell outweighs the other three
      !! of its corner.  The same twin runs with the knob OFF on the
      !! coordinates that lay static bed fillers (`hk_pair_floor`): there
      !! the open step face pairs a live cell with a `1e-4 m` filler, and
      !! without the floor the matrix staircase leaves the remap a negative
      !! thickness at step 2 (compat matrix `vcoord=zstar` x
      !! `coriolis=sadourny_hk`).  Every other coordinate ⇒ the original
      !! passes, bit-identical.
      type(hgrid_t), intent(in) :: grid
      type(ocean_metrics_t), intent(in) :: metrics
      type(coriolis_adv_t), intent(inout) :: this
      type(multilayer_state_t), intent(in) :: ms
      real(wp), intent(in) :: u(grid%nx_total + 1, grid%ny_total, ms%nz_ml)
         !! Face-velocity / thickness source arrays (outer-shim; the
         !! dispatcher forwards either the prognostic components or the
         !! `u_av` time-mean family under `split_scheme = "pred_corr"`).
      real(wp), intent(in) :: v(grid%nx_total, grid%ny_total + 1, ms%nz_ml)
      real(wp), intent(in) :: h(grid%nx_total, grid%ny_total, ms%nz_ml)

      integer :: i, j, k, nx, ny, nz, nu, nv
      real(wp) :: zeta_corner, h_corner, h_face
      real(wp) :: aSW, aSE, aNW, aNE, hm_num, hm_den
      integer :: iw, ie, js, jn
      real(wp) :: q_S, q_N, q_W, q_E, q_NE, q_NW, q_SE, q_SW
      real(wp) :: a_NE, b_NW, c_SW, d_SE
      real(wp) :: ke_grad_x, ke_grad_y
      real(wp) :: h_S, h_N, h_W, h_E, h_NE, h_NW, h_SE, h_SW
         !! Closed-face branch only: corner thicknesses of the stencil.
      real(wp) :: h_u, h_v, hv_NE, hv_NW, hv_SW, hv_SE, hu_NE, hu_NW, hu_SW, hu_SE
         !! Closed-face branch only: face thicknesses of the stencil.
      real(wp) :: ns
      real(wp), parameter :: C1_12 = 1.0_wp/12.0_wp
      real(wp), parameter :: H_MIN_PV = 1.0e-12_wp
         !! Floor for the corner-h divide; vanishing-layer columns
         !! get q ≈ (f+ζ)/H_MIN_PV which is large but finite — paired
         !! with `mass_flux ≈ 0` at the same column so the product
         !! decays cleanly toward zero rather than blowing up.

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ms%nz_ml
      nu = size(u, 1)
      nv = size(v, 2)
      ns = merge(1.0_wp, 0.0_wp, this%no_slip)

      ! ---- Pass 1: relative vorticity at corners (circulation/area) ----
      ! Inline twin of `rdb_rvc_zeta_corner` (shared_module_utilities/
      ! rdb_rel_vort_corner.inc, read by the `vorticity_z` diag): keep in step.
      ! Slip factor masks the rel-vort at land corners (C1); Pass 2 reads
      ! this back as `zeta_corner` and adds the UNMASKED planetary f.
      do concurrent(k=1:nz, j=2:ny, i=2:nx)
         this%q_corner%data(i, j, k) = &
            ((1.0_wp - 2.0_wp*ns)*metrics%wet_q(i, j) + 2.0_wp*ns)* &
            ((v(i, j, k)*metrics%dyCv(i, j) - &
              v(i - 1, j, k)*metrics%dyCv(i - 1, j)) - &
             (u(i, j, k)*metrics%dxCu(i, j) - &
              u(i, j - 1, k)*metrics%dxCu(i, j - 1)))* &
            metrics%iareaBu(i, j)
      end do
      do concurrent(k=1:nz, j=1:ny + 1)
         this%q_corner%data(1, j, k) = 0.0_wp
         this%q_corner%data(nx + 1, j, k) = 0.0_wp
      end do
      do concurrent(k=1:nz, i=1:nx + 1)
         this%q_corner%data(i, 1, k) = 0.0_wp
         this%q_corner%data(i, ny + 1, k) = 0.0_wp
      end do

      ! ---- Pass 2: PV q = (f + ζ) / h_at_corner ----
      ! h_at_corner is the AREA-WEIGHTED mean of the four surrounding
      ! T cells, with min/max wall clamps — wall corners collapse onto
      ! the available cells (single cell at the four grid corners;
      ! two-cell mean along an edge).  Area weighting (each T thickness
      ! weighted by its own areaT, normalised by the summed areas) is the
      ! conservative corner thickness on curvilinear grids where
      ! areaT varies cell-to-cell; on uniform Cartesian every areaT is
      ! equal so it reduces to the plain 4-cell mean (same value).
      do concurrent(k=1:nz, j=1:ny + 1, i=1:nx + 1) &
         local(zeta_corner, h_corner, iw, ie, js, jn, &
               aSW, aSE, aNW, aNE, hm_num, hm_den)
         zeta_corner = this%q_corner%data(i, j, k)
         iw = max(1, i - 1)
         ie = min(nx, i)
         js = max(1, j - 1)
         jn = min(ny, j)
         ! Mask-weighted corner area (C1, MOM6 `Area_h = mask2dT·areaT`):
         ! a blocked T-column contributes zero area + zero thickness, so
         ! `h_corner` is the wet-column mean only.  All-wet ⇒ `wet_T≡1` ⇒
         ! plain area-weighted mean (bit-identical).  `hm_den` floored at
         ! H_DIV_EPS for a fully-land corner (zeta=0 there, mass fluxes 0).
         !
         ! This is the MOM6 SADOURNY75_ENERGY (transport/energy-form) land
         ! treatment, NOT a divergence from it.  MOM6 masks corner area
         ! UNCONDITIONALLY at land (`Area_h = mask2dT·areaT`, zero on a land
         ! T-column) then forms `q = abs_vort·Area_q/(hArea_q + vol_neglect)`
         ! with `Area_q = Σ Area_h` over the four corners — algebraically
         ! `abs_vort·Σ(wet·areaT) / Σ(wet·areaT·h)`, IDENTICAL to our
         ! `(f+ζ)/h_corner` with `h_corner = Σ(wet·areaT·h)/Σ(wet·areaT)`.
         ! MOM6's *additional* `Area_h` area-mirroring across a boundary is
         ! OBC-SEGMENT-ONLY (inside `if (associated(OBC))`), not a land-coast
         ! rule — it does not apply here.  No HK-specific corner liberty.
         aSW = metrics%wet_T(iw, js)*metrics%areaT(iw, js)
         aSE = metrics%wet_T(ie, js)*metrics%areaT(ie, js)
         aNW = metrics%wet_T(iw, jn)*metrics%areaT(iw, jn)
         aNE = metrics%wet_T(ie, jn)*metrics%areaT(ie, jn)
         hm_num = aSW*h(iw, js, k) + aSE*h(ie, js, k) + &
                  aNW*h(iw, jn, k) + aNE*h(ie, jn, k)
         hm_den = aSW + aSE + aNW + aNE
         h_corner = hm_num/max(hm_den, H_DIV_EPS)
         h_corner = max(h_corner, H_MIN_PV)
         this%q_corner%data(i, j, k) = (this%f_corner(i, j) + zeta_corner)/h_corner
      end do

      ! ---- Pass 3a: u-face transport uh = u·h·dy_cu ----
      do concurrent(k=1:nz, j=1:ny, i=1:nu) local(h_face)
         if (i == 1) then
            h_face = h(1, j, k)
         else if (i == nu) then
            h_face = h(nx, j, k)
         else
            h_face = 0.5_wp*(h(i - 1, j, k) + h(i, j, k))
         end if
         this%mass_flux_u%data(i, j, k) = u(i, j, k)*h_face*metrics%dy_cu(i, j)
      end do

      ! ---- Pass 3b: v-face transport vh = v·h·dx_cv ----
      do concurrent(k=1:nz, j=1:nv, i=1:nx) local(h_face)
         if (j == 1) then
            h_face = h(i, 1, k)
         else if (j == nv) then
            h_face = h(i, ny, k)
         else
            h_face = 0.5_wp*(h(i, j - 1, k) + h(i, j, k))
         end if
         this%mass_flux_v%data(i, j, k) = v(i, j, k)*h_face*metrics%dx_cv(i, j)
      end do

      ! ---- Porous barriers (Adcroft 2013) ----
      ! The PV/advection transports must carry the SAME narrowed face
      ! width continuity uses, or the two mass-flux definitions disagree.
      ! Host-side gate => no kernel launch and no textual change to the
      ! passes above when the knob is off (byte-identical).
      if (metrics%use_porous) then
         call porous_narrow_3d(nu, ny, nz, metrics%por_face_area_u, &
                               this%mass_flux_u%data)
         call porous_narrow_3d(nx, nv, nz, metrics%por_face_area_v, &
                               this%mass_flux_v%data)
      end if

      ! ---- z-level closed faces (&vcoord_nml zfixed_closed_faces) ----
      ! The SAME argument as the porous pass above, and a SEPARATE factor:
      ! the transport-Coriolis PV flux must see the same per-layer walls
      ! continuity does, or a closed layer contributes transport the mass
      ! budget never moved.  See the composition rule on
      ! `ocean_metrics_t%open_v`.
      if (metrics%use_closed_faces) then
         call porous_narrow_3d(nu, ny, nz, metrics%open_u, &
                               this%mass_flux_u%data)
         call porous_narrow_3d(nx, nv, nz, metrics%open_v, &
                               this%mass_flux_v%data)
      end if

      ! ---- Pass 4: KE at cell centres (area-weighted) ----
      do concurrent(k=1:nz, j=1:ny, i=1:nx)
         this%ke_centre%data(i, j, k) = 0.25_wp*metrics%iareaT(i, j)*( &
                                        metrics%areaCu(i, j)*u(i, j, k)**2 + &
                                        metrics%areaCu(i + 1, j)*u(i + 1, j, k)**2 + &
                                        metrics%areaCv(i, j)*v(i, j, k)**2 + &
                                        metrics%areaCv(i, j + 1)*v(i, j + 1, k)**2)
      end do

      if (metrics%use_closed_faces .or. this%hk_pair_floor) then
         ! ---- Passes 5f/6f: the same stencil, PAIR-FLOORED PV ----
         ! `&vcoord_nml zfixed_closed_faces`, or an OPEN staircase of static
         ! bed fillers (`hk_pair_floor`: z_fixed / zstar / zstar_full);
         ! otherwise the ELSE branch, the original Passes 5/6, textually
         ! untouched ⇒ bit-identical.  Inline,
         ! not a call: a host-gated call handing the tendency buffers to another
         ! procedure pessimises every loop of this routine on nvfortran.
         ! Each pair coefficient sums three corner PVs; the "cross" ones meet a
         ! transport whose far cell lies outside the corner, so `h_face/h_corner`
         ! is unbounded there (a thin live partial cell next to a full level).
         ! `hk_pair_coef` re-evaluates each PV at a corner thickness of at least
         ! half the pair's larger face thickness — the bound `sadourny_energy`
         ! has by construction — and the floor belongs to the PAIR, so the
         ! u- and v-tendencies share the coefficient (HK energy antisymmetry).
         ! See the routine docstring and `hk_pair_coef`.
         ! ---- Pass 5f: u-face ----
         do concurrent(k=1:nz, j=1:ny, i=2:nx) &
            local(q_S, q_N, q_NE, q_NW, q_SE, q_SW, &
                  a_NE, b_NW, c_SW, d_SE, ke_grad_x, &
                  h_S, h_N, h_NE, h_NW, h_SE, h_SW, h_u, &
                  hv_NE, hv_NW, hv_SW, hv_SE)
            q_S = this%q_corner%data(i, j, k)
            q_N = this%q_corner%data(i, j + 1, k)
            q_NE = this%q_corner%data(i + 1, j + 1, k)
            q_NW = this%q_corner%data(i - 1, j + 1, k)
            q_SE = this%q_corner%data(i + 1, j, k)
            q_SW = this%q_corner%data(i - 1, j, k)
            h_S = hk_corner_h(i, j, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_N = hk_corner_h(i, j + 1, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_NE = hk_corner_h(i + 1, j + 1, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_NW = hk_corner_h(i - 1, j + 1, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_SE = hk_corner_h(i + 1, j, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_SW = hk_corner_h(i - 1, j, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            ! Face thicknesses exactly as Pass 3 builds the transports
            ! (one-sided at the array edge: 0.5·(a+a) == a).
            h_u = 0.5_wp*(h(i - 1, j, k) + h(i, j, k))
            hv_NE = 0.5_wp*(h(i, j, k) + h(i, min(ny, j + 1), k))
            hv_NW = 0.5_wp*(h(i - 1, j, k) + h(i - 1, min(ny, j + 1), k))
            hv_SW = 0.5_wp*(h(i - 1, max(1, j - 1), k) + h(i - 1, j, k))
            hv_SE = 0.5_wp*(h(i, max(1, j - 1), k) + h(i, j, k))
            a_NE = hk_pair_coef(q_N, h_N, q_NE, h_NE, q_S, h_S, max(h_u, hv_NE))
            b_NW = hk_pair_coef(q_N, h_N, q_NW, h_NW, q_S, h_S, max(h_u, hv_NW))
            c_SW = hk_pair_coef(q_N, h_N, q_SW, h_SW, q_S, h_S, max(h_u, hv_SW))
            d_SE = hk_pair_coef(q_N, h_N, q_SE, h_SE, q_S, h_S, max(h_u, hv_SE))
            ke_grad_x = (this%ke_centre%data(i, j, k) - &
                         this%ke_centre%data(i - 1, j, k))*metrics%idxCu(i, j)
            this%pv_flux_x%data(i, j, k) = &
               (a_NE*this%mass_flux_v%data(i, j + 1, k) + &
                b_NW*this%mass_flux_v%data(i - 1, j + 1, k) + &
                c_SW*this%mass_flux_v%data(i - 1, j, k) + &
                d_SE*this%mass_flux_v%data(i, j, k))*metrics%idxCu(i, j) - ke_grad_x
         end do
         ! ---- Pass 6f: v-face ----
         do concurrent(k=1:nz, j=2:ny, i=1:nx) &
            local(q_W, q_E, q_NE, q_NW, q_SE, q_SW, &
                  a_NE, b_NW, c_SW, d_SE, ke_grad_y, &
                  h_W, h_E, h_NE, h_NW, h_SE, h_SW, h_v, &
                  hu_NE, hu_NW, hu_SW, hu_SE)
            q_W = this%q_corner%data(i, j, k)
            q_E = this%q_corner%data(i + 1, j, k)
            q_NE = this%q_corner%data(i + 1, j + 1, k)
            q_NW = this%q_corner%data(i, j + 1, k)
            q_SE = this%q_corner%data(i + 1, j - 1, k)
            q_SW = this%q_corner%data(i, j - 1, k)
            h_W = hk_corner_h(i, j, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_E = hk_corner_h(i + 1, j, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_NE = hk_corner_h(i + 1, j + 1, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_NW = hk_corner_h(i, j + 1, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_SE = hk_corner_h(i + 1, j - 1, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_SW = hk_corner_h(i, j - 1, k, nx, ny, nz, h, metrics%wet_T, metrics%areaT)
            h_v = 0.5_wp*(h(i, j - 1, k) + h(i, j, k))
            hu_NE = 0.5_wp*(h(i, j, k) + h(min(nx, i + 1), j, k))
            hu_NW = 0.5_wp*(h(max(1, i - 1), j, k) + h(i, j, k))
            hu_SW = 0.5_wp*(h(max(1, i - 1), j - 1, k) + h(i, j - 1, k))
            hu_SE = 0.5_wp*(h(i, j - 1, k) + h(min(nx, i + 1), j - 1, k))
            a_NE = hk_pair_coef(q_W, h_W, q_NE, h_NE, q_E, h_E, max(h_v, hu_NE))
            b_NW = hk_pair_coef(q_W, h_W, q_NW, h_NW, q_E, h_E, max(h_v, hu_NW))
            c_SW = hk_pair_coef(q_W, h_W, q_SW, h_SW, q_E, h_E, max(h_v, hu_SW))
            d_SE = hk_pair_coef(q_W, h_W, q_SE, h_SE, q_E, h_E, max(h_v, hu_SE))
            ke_grad_y = (this%ke_centre%data(i, j, k) - &
                         this%ke_centre%data(i, j - 1, k))*metrics%idyCv(i, j)
            this%pv_flux_y%data(i, j, k) = &
               -(a_NE*this%mass_flux_u%data(i + 1, j, k) + &
                 b_NW*this%mass_flux_u%data(i, j, k) + &
                 c_SW*this%mass_flux_u%data(i, j - 1, k) + &
                 d_SE*this%mass_flux_u%data(i + 1, j - 1, k))*metrics%idyCv(i, j) - ke_grad_y
         end do
      else
         ! ---- Pass 5: u-face HK tendency ----
         do concurrent(k=1:nz, j=1:ny, i=2:nx) &
            local(q_S, q_N, q_NE, q_NW, q_SE, q_SW, &
                  a_NE, b_NW, c_SW, d_SE, ke_grad_x)
            q_S = this%q_corner%data(i, j, k)
            q_N = this%q_corner%data(i, j + 1, k)
            q_NE = this%q_corner%data(i + 1, j + 1, k)
            q_NW = this%q_corner%data(i - 1, j + 1, k)
            q_SE = this%q_corner%data(i + 1, j, k)
            q_SW = this%q_corner%data(i - 1, j, k)
            a_NE = (q_N + q_NE + q_S)*C1_12
            b_NW = (q_N + q_NW + q_S)*C1_12
            c_SW = (q_N + q_SW + q_S)*C1_12
            d_SE = (q_N + q_SE + q_S)*C1_12
            ke_grad_x = (this%ke_centre%data(i, j, k) - &
                         this%ke_centre%data(i - 1, j, k))*metrics%idxCu(i, j)
            ! q·vh sum is a transport-weighted PV flux (m³/s); the u-face
            ! IdxCu closes it to a per-length acceleration (= /dx on uniform).
            this%pv_flux_x%data(i, j, k) = &
               (a_NE*this%mass_flux_v%data(i, j + 1, k) + &
                b_NW*this%mass_flux_v%data(i - 1, j + 1, k) + &
                c_SW*this%mass_flux_v%data(i - 1, j, k) + &
                d_SE*this%mass_flux_v%data(i, j, k))*metrics%idxCu(i, j) - ke_grad_x
         end do

         ! ---- Pass 6: v-face HK tendency ----
         do concurrent(k=1:nz, j=2:ny, i=1:nx) &
            local(q_W, q_E, q_NE, q_NW, q_SE, q_SW, &
                  a_NE, b_NW, c_SW, d_SE, ke_grad_y)
            q_W = this%q_corner%data(i, j, k)
            q_E = this%q_corner%data(i + 1, j, k)
            q_NE = this%q_corner%data(i + 1, j + 1, k)
            q_NW = this%q_corner%data(i, j + 1, k)
            q_SE = this%q_corner%data(i + 1, j - 1, k)
            q_SW = this%q_corner%data(i, j - 1, k)
            a_NE = (q_W + q_NE + q_E)*C1_12
            b_NW = (q_W + q_NW + q_E)*C1_12
            c_SW = (q_W + q_SW + q_E)*C1_12
            d_SE = (q_W + q_SE + q_E)*C1_12
            ke_grad_y = (this%ke_centre%data(i, j, k) - &
                         this%ke_centre%data(i, j - 1, k))*metrics%idyCv(i, j)
            this%pv_flux_y%data(i, j, k) = &
               -(a_NE*this%mass_flux_u%data(i + 1, j, k) + &
                 b_NW*this%mass_flux_u%data(i, j, k) + &
                 c_SW*this%mass_flux_u%data(i, j - 1, k) + &
                 d_SE*this%mass_flux_u%data(i + 1, j - 1, k))*metrics%idyCv(i, j) - ke_grad_y
         end do
      end if
      ! Array-edge faces carry no tendency (both branches).
      do concurrent(k=1:nz, j=1:ny)
         this%pv_flux_x%data(1, j, k) = 0.0_wp
         this%pv_flux_x%data(nx + 1, j, k) = 0.0_wp
      end do
      do concurrent(k=1:nz, i=1:nx)
         this%pv_flux_y%data(i, 1, k) = 0.0_wp
         this%pv_flux_y%data(i, ny + 1, k) = 0.0_wp
      end do
   end subroutine coriolis_adv_compute_tendencies_hk