tidal_mixing_column_kernel Subroutine

private pure subroutine tidal_mixing_column_kernel(grid, this, ms, hT, hS)

Per-column St-Laurent flux-bookkeeping sweep. One do concurrent (j, i) over owned cells; each column is a serial upward sweep k=1(bed) -> nz(surface) over fixed-size local() arrays (register-resident). Bottom-up global indexing throughout — no surface-down flip (the decay anchors at the bed).

“The bed” is kbed = ms%k_bot(i,j), the first LIVE layer counting up (1 off z_fixed, ⇒ bit-identical there). Under z_fixed the layers below it are inert fillers carrying the donor’s T/S, so anchoring at k = 1 read N_bot = 0 across two fillers (killing the e_compute energy input on every column shallower than the nominal stack), exempted a FILLER from deposition instead of the bed layer, and handed the fillers a TKE share. The sweep, the N_bot sample, the bed-layer exclusion and the bed end-cap all start at kbed; layers below it get no Kd.

Pipeline per column: gather: h (floored), per-layer T,S = hTr/h. N^2(k): layer-centred buoyancy gradient (St-Laurent #4 — N2_lay, not interface N^2), clamped >= 0. E: prescribed e_in(i,j) (v1) or state-dependent TKE_coef*N_bot (v1.1, e_compute), masked where H < min_zbot. sweep: Inv_int normalization, TKE flux bookkeeping, per-layer Kd_add capped at kd_max, deposited 50/50 at the two bounding interfaces.

Arguments

Type IntentOptional Attributes Name
type(hgrid_t), intent(in) :: grid
type(ocean_tidal_mixing_t), intent(inout) :: this
type(multilayer_state_t), intent(in) :: ms
real(kind=wp), intent(in) :: hT(:,:,:)

Temperature tracer hTr (degC*m), host-dereferenced.

real(kind=wp), intent(in) :: hS(:,:,:)

Salinity tracer hTr (PSU*m), host-dereferenced.


Calls

proc~~tidal_mixing_column_kernel~~CallsGraph proc~tidal_mixing_column_kernel tidal_mixing_column_kernel local local proc~tidal_mixing_column_kernel->local proc~eos_specvol_derivs eos_specvol_derivs proc~tidal_mixing_column_kernel->proc~eos_specvol_derivs proc~roquet_spv_point roquet_spv_point proc~eos_specvol_derivs->proc~roquet_spv_point

Called by

proc~~tidal_mixing_column_kernel~~CalledByGraph proc~tidal_mixing_column_kernel tidal_mixing_column_kernel proc~tidal_mixing_compute tidal_mixing_compute proc~tidal_mixing_compute->proc~tidal_mixing_column_kernel proc~vmix_apply_in_stage vmix_apply_in_stage proc~vmix_apply_in_stage->proc~tidal_mixing_compute proc~run_stage run_stage proc~run_stage->proc~vmix_apply_in_stage proc~run_stage_split run_stage_split proc~run_stage_split->proc~vmix_apply_in_stage 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

Variables

Type Visibility Attributes Name Initial
real(kind=wp), private :: dbuoy_s
real(kind=wp), private :: dbuoy_t
real(kind=wp), private :: denom
real(kind=wp), private :: dsv_ds_k
real(kind=wp), private :: dsv_dt_k
real(kind=wp), private :: dz_eff
real(kind=wp), private :: e_cap
real(kind=wp), private :: e_col
real(kind=wp), private :: frac_top
real(kind=wp), private :: gamma_l
real(kind=wp), private :: h2c
real(kind=wp), private :: h_col(NZL)
real(kind=wp), private :: h_tot
real(kind=wp), private :: hz
integer, private :: i
real(kind=wp), private :: inv_int
real(kind=wp), private :: izeta
integer, private :: j
integer, private :: k
integer, private :: kbed
real(kind=wp), private :: kd_lay
real(kind=wp), private :: kd_lay_arr(NZL)
real(kind=wp), private :: kd_max_l
real(kind=wp), private :: mu_l
real(kind=wp), private :: n2_col(NZL)
real(kind=wp), private :: n_bot
integer, private :: nx
integer, private :: ny
integer, private :: nz
real(kind=wp), private :: omega2_l
real(kind=wp), private :: p_int
real(kind=wp), private :: rho0_l
real(kind=wp), private :: s_col(NZL)
real(kind=wp), private :: s_int
real(kind=wp), private :: t_col(NZL)
real(kind=wp), private :: t_int
real(kind=wp), private :: tke_bot
real(kind=wp), private :: tke_coef
real(kind=wp), private :: tke_lay
real(kind=wp), private :: tke_rem
real(kind=wp), private :: z_top
real(kind=wp), private :: zeta_l

Source Code

   pure subroutine tidal_mixing_column_kernel(grid, this, ms, hT, hS)
      !! Per-column St-Laurent flux-bookkeeping sweep.  One
      !! `do concurrent (j, i)` over owned cells; each column is a serial
      !! upward sweep k=1(bed) -> nz(surface) over fixed-size `local()`
      !! arrays (register-resident).  Bottom-up global indexing
      !! throughout — no surface-down flip (the decay anchors at the bed).
      !!
      !! "The bed" is `kbed = ms%k_bot(i,j)`, the first LIVE layer counting
      !! up (`1` off `z_fixed`, ⇒ bit-identical there).  Under `z_fixed`
      !! the layers below it are inert fillers carrying the donor's T/S, so
      !! anchoring at `k = 1` read `N_bot = 0` across two fillers (killing
      !! the `e_compute` energy input on every column shallower than the
      !! nominal stack), exempted a FILLER from deposition instead of the
      !! bed layer, and handed the fillers a TKE share.  The sweep, the
      !! `N_bot` sample, the bed-layer exclusion and the bed end-cap all
      !! start at `kbed`; layers below it get no `Kd`.
      !!
      !! Pipeline per column:
      !!   gather: h (floored), per-layer T,S = hTr/h.
      !!   N^2(k): layer-centred buoyancy gradient (St-Laurent #4 —
      !!           N2_lay, not interface N^2), clamped >= 0.
      !!   E:      prescribed `e_in(i,j)` (v1) or state-dependent
      !!           TKE_coef*N_bot (v1.1, `e_compute`), masked where
      !!           H < min_zbot.
      !!   sweep:  Inv_int normalization, TKE flux bookkeeping, per-layer
      !!           Kd_add capped at kd_max, deposited 50/50 at the two
      !!           bounding interfaces.
      type(hgrid_t), intent(in) :: grid
      type(ocean_tidal_mixing_t), intent(inout) :: this
      type(multilayer_state_t), intent(in) :: ms
      ! assumed-shape-ok: tracer registry outer-shim — caller host-dereferences
      ! ms%tracers(idx)%hTr before passing; size varies per tracer slot
      ! (see CLAUDE.md "outer-shim + flat-impl" pattern); thermo cadence.
      real(wp), intent(in) :: hT(:, :, :)
         !! Temperature tracer hTr (degC*m), host-dereferenced.
      real(wp), intent(in) :: hS(:, :, :)  ! assumed-shape-ok: tracer registry outer-shim; thermo cadence
         !! Salinity tracer hTr (PSU*m), host-dereferenced.

      integer :: i, j, k, nx, ny, nz, kbed
      real(wp) :: e_col, h_tot, n_bot, tke_coef, h2c, e_cap
      real(wp) :: inv_int, hz, tke_bot, tke_rem, z_top, frac_top, tke_lay
      real(wp) :: dz_eff, denom, kd_lay
      real(wp) :: h_col(NZL), t_col(NZL), s_col(NZL)
      real(wp) :: n2_col(NZL), kd_lay_arr(NZL)
      real(wp) :: dbuoy_t, dbuoy_s, p_int
      real(wp) :: dsv_dt_k, dsv_ds_k, t_int, s_int
      real(wp) :: gamma_l, mu_l, zeta_l, kd_max_l, omega2_l, rho0_l, izeta

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ms%nz_ml

      gamma_l = this%gamma
      mu_l = this%mu
      zeta_l = this%zeta
      kd_max_l = this%kd_max
      omega2_l = this%omega2
      rho0_l = this%rho0
      izeta = 1.0_wp/zeta_l

      do concurrent(j=1:ny, i=1:nx) &
         local(k, kbed, e_col, h_tot, n_bot, tke_coef, h2c, e_cap, &
               inv_int, hz, tke_bot, tke_rem, z_top, frac_top, tke_lay, &
               dz_eff, denom, kd_lay, h_col, t_col, s_col, n2_col, kd_lay_arr, &
               dbuoy_t, dbuoy_s, p_int, dsv_dt_k, dsv_ds_k, t_int, s_int)

         ! ---- Default output: zero contribution (overwritten if wet) ----
         do k = 1, nz + 1
            this%kd_int(i, j, k) = 0.0_wp
         end do

         if (ms%wet_mask(i, j) > 0.0_wp) then
            kbed = ms%k_bot(i, j)
            ! ---- Gather (bottom-up; floor thickness, back out T,S) ----
            ! `h_tot` is the LIVE column (the fillers below `kbed` are not
            ! water the internal tide can mix).
            h_tot = 0.0_wp
            do k = 1, nz
               dz_eff = max(ms%h_layer(i, j, k), H_VANISHED)
               h_col(k) = dz_eff
               denom = 1.0_wp/max(ms%h_layer(i, j, k), H_DIV_EPS)
               t_col(k) = hT(i, j, k)*denom
               s_col(k) = hS(i, j, k)*denom
               if (k >= kbed) h_tot = h_tot + ms%h_layer(i, j, k)
            end do

            ! ---- Layer-centred N^2 (St-Laurent #4: N2_lay) ----
            ! Buoyancy gradient between the centres of layer k and the
            ! layer above (k+1, bottom-up), over their centre spacing
            ! 0.5*(h(k)+h(k+1)).  Pressure accumulates downward from the
            ! surface for the EOS in-situ derivatives.  Surface layer
            ! (k=nz) has no overlying layer -> N2=0 there.
            p_int = 0.0_wp
            n2_col(nz) = 0.0_wp
            do k = nz - 1, 1, -1
               ! Walk pressure down from the surface to the k/k+1 interface.
               p_int = p_int + GRAVITY*rho0_l*h_col(k + 1)
               t_int = 0.5_wp*(t_col(k) + t_col(k + 1))
               s_int = 0.5_wp*(s_col(k) + s_col(k + 1))
               call eos_specvol_derivs(this%eos, t_int, s_int, p_int, &
                                       dsv_dt_k, dsv_ds_k)
               dbuoy_t = GRAVITY*rho0_l*dsv_dt_k
               dbuoy_s = GRAVITY*rho0_l*dsv_ds_k
               dz_eff = 0.5_wp*(h_col(k) + h_col(k + 1))
               n2_col(k) = (dbuoy_t*(t_col(k + 1) - t_col(k)) + &
                            dbuoy_s*(s_col(k + 1) - s_col(k)))/ &
                           max(dz_eff, H_VANISHED)
               if (n2_col(k) < 0.0_wp) n2_col(k) = 0.0_wp
            end do

            ! ---- Energy input E(x,y) ----
            ! v1: prescribed field.  v1.1 (`e_compute`): state-dependent
            ! internal-tide generation, Jayne & St Laurent (2001):
            !   E = min( 0.5*rho0*kappa_h2*kappa_itides*<h^2>*U_tide^2 * N_bot,
            !            e_max )
            ! with the roughness clamp <h^2> <= (frac_rough*H)^2.  The rho0
            ! factor sets E in W/m^2 (the same units as the prescribed e_in),
            ! so it lands correctly in the rho0*dz*(N^2+Omega^2) Kd divisor.
            ! N_bot = sqrt(N^2) at the bed-most interior interface
            ! (n2_col(kbed), the gradient across the two deepest LIVE layers).
            if (this%e_compute) then
               h2c = min(this%h2_rough, (this%frac_rough*h_tot)**2)
               tke_coef = 0.5_wp*rho0_l*this%kappa_h2*this%kappa_itides*h2c*this%utide**2
               n_bot = sqrt(max(n2_col(kbed), 0.0_wp))
               e_col = tke_coef*n_bot
               e_cap = this%e_max
               if (e_col > e_cap) e_col = e_cap
            else
               e_col = this%e_in(i, j)
            end if
            ! Mask off shallow columns (H < min_zbot).
            if (h_tot < this%min_zbot) e_col = 0.0_wp

            ! ---- Inv_int normalization (Adcroft reciprocal) ----
            ! L'Hospital degenerate-thin-column guard: H/zeta -> 0 => 1.
            hz = h_tot*izeta
            if (hz < 1.0e-14_wp) then
               inv_int = 1.0_wp
            else
               inv_int = 1.0_wp/(1.0_wp - exp(-hz))
            end if

            ! ---- Flux bookkeeping sweep (St-Laurent 2002), bed -> surf ----
            ! TKE_bot = q*mu*E is the total locally-dissipated power; the
            ! 1/rho0 Boussinesq factor lives in the TKE->Kd divisor
            ! (rho0*dz*(N^2+Omega^2)), so Sum(TKE_lay) == q*mu*E exactly
            ! (energy conservation is independent of the rho placement).
            tke_bot = gamma_l*mu_l*e_col
            tke_rem = inv_int*tke_bot
            z_top = 0.0_wp
            do k = 1, kbed - 1
               kd_lay_arr(k) = 0.0_wp   ! inert bed filler: no share of the TKE
            end do
            do k = kbed, nz
               z_top = z_top + h_col(k)
               frac_top = inv_int*exp(-z_top*izeta)
               tke_lay = tke_rem - tke_bot*frac_top
               tke_rem = tke_rem - tke_lay

               ! TKE_to_Kd = 1/(rho0*dz*(N^2+Omega^2)).  dz floored at
               ! H_VANISHED (the vdiff vanishing-layer mass-drop
               ! precedent) and the divisor armoured with H_DIV_EPS.
               dz_eff = max(h_col(k), H_VANISHED)
               denom = rho0_l*dz_eff*(n2_col(k) + omega2_l)
               denom = max(denom, H_DIV_EPS)
               kd_lay = tke_lay/denom
               ! Per-layer physical cap (St-Laurent).  DIVERGENCE D3:
               ! MOM6's max_TKE limiter re-injects the un-used power into
               ! the layer above (energy-conserving under clipping); this
               ! bare Kd_max clamp DISCARDS the clipped power.  v1 accepts
               ! the discard — it is a rarely-active safety cap, not the
               ! deposition mechanism.  The energy-conservation invariant
               ! is asserted on the PRE-clip TKE_lay accordingly.
               if (kd_max_l >= 0.0_wp .and. kd_lay > kd_max_l) kd_lay = kd_max_l
               kd_lay_arr(k) = kd_lay
            end do

            ! ---- D1: exclude the BED and SURFACE LAYERS from deposition ----
            ! MOM6 (St Laurent/Simmons; MOM_set_diffusivity find_TKE_to_Kd,
            ! MOM-inspired) sets TKE_to_Kd(i,1)=TKE_to_Kd(i,nz)=0 — BOTH
            ! endpoint layers get zero Kd_add, and the deposit loop runs the
            ! interior only (top-down do k=nz-1,2,-1).  In MOM6 top-down
            ! indexing layer 1 is the surface and layer nz the bottom; in
            ! Roundabout bottom-up k=nz is the surface and k=1 the bed, so we
            ! zero kd_lay_arr at BOTH ends.  The bed layer (`kbed`: k=1, or
            ! the first LIVE layer `k_bot` under z_fixed) is owned by the BBL
            ! drag; the surface layer (k=nz) is the mixed layer,
            ! owned downstream by EPBL/KPP, and additionally has no overlying
            ! layer so its kernel N^2 is 0 (its Omega^2-only inflated Kd would
            ! over-mix interface K=nz on shallow energetic columns).  Zeroing
            ! the deposited Kd here does NOT touch the TKE energy bookkeeping
            ! above (tke_lay/tke_rem are unchanged) — the energy invariant is
            ! asserted on the pre-deposit TKE_lay, mirroring the original
            ! surface-only exclusion.  Zero both before the 50/50 split.
            kd_lay_arr(kbed) = 0.0_wp
            kd_lay_arr(nz) = 0.0_wp

            ! ---- Deposit per-layer Kd_add 50/50 at the two bounding
            ! interfaces (St-Laurent 2002).  DIVERGENCE D2: MOM6 splits
            ! the layer power 0.5/0.5 across its bounding interfaces; we
            ! mirror that interface deposition.  Global interface K is the
            ! BOTTOM interface of layer K, so layer k contributes to
            ! interfaces k (its bed) and k+1 (its top).
            do k = 1, nz
               this%kd_int(i, j, k) = this%kd_int(i, j, k) + 0.5_wp*kd_lay_arr(k)
               this%kd_int(i, j, k + 1) = this%kd_int(i, j, k + 1) + 0.5_wp*kd_lay_arr(k)
            end do
            ! D1 (cont.): zero the bed (K=kbed) and surface (K=nz+1) interface
            ! end-caps — `vmix_assemble` / EPBL / BBL own them.  With BOTH
            ! the bed LAYER (k=kbed) and surface LAYER (k=nz) now excluded
            ! above, interface K=kbed+1 receives nothing from k=kbed (only
            ! k=kbed+1's bed half)
            ! and interface K=nz nothing from k=nz (only k=nz-1's top half) —
            ! the full MOM6 endpoint exclusion.  DIVERGENCE D4
            ! (BBL N^2 override): MOM6 replaces the near-bed N^2 with a
            ! roughness-height BBL average; v1 uses the raw per-layer N^2
            ! (a v1.1 refinement).
            this%kd_int(i, j, kbed) = 0.0_wp
            this%kd_int(i, j, nz + 1) = 0.0_wp
         end if
      end do
   end subroutine tidal_mixing_column_kernel