gm_column_x Subroutine

private pure subroutine gm_column_x(nx, ny, nz, i_smax2, i4dt, use_open, dy_cu, areaT, h_layer, bathy, slope_x, khth_u, open_u, uhD)

u-face GM streamfunction + bolus-transport column recurrence. Interior u-face (i=2..nx) pairs columns iw=i-1 (west) and i (east). Bottom-up sweep: interior interfaces Kr=2 (bed-most) -> nz (surface-most), uhtot=0 at the bed; the surface BC (Sfn=0 at Kr=nz+1) is closed after the loop by uhD(nz)=-uhtot, giving Sum_k uhD=0 exactly. Interface Kr straddles ka=Kr (above) and kb=Kr-1 (below) and fills LAYER kb; the rsum bound keys on ka, the donor h_frac and per-layer cap on kb.

OPEN COLUMN (use_open, &vcoord_nml zfixed_closed_faces): a layer is part of the face column only if ok(k) = open_u(i,j,k) and it is live on both sides. A not-ok layer gets h_avail = 0 and uhD = 0, leaving uhtot (the streamfunction at its top) unchanged, and the closure lands in the topmost ok layer ktop instead of nz — Sfn = 0 at the bottom AND the top of the open column (see the module docstring). use_open = .false. ⇒ ok all-true, ktop = nz: the full-column recurrence verbatim.

DIVERGENCE (MOM6 nk_linear): MOM6’s thickness_diffuse_full sets nk_linear = max(GV%nkml, 1); in ALE mode GV%nkml = 0, so MOM6 always runs nk_linear = 1 and it is NOT a namelist parameter — its top layer always takes a linear return-flow closure (comment: “Balance the deeper flow with a return flow uniformly distributed though the remaining near-surface layers”; MOM6 is top-down, k=1=surface, so k <= nk_linear selects the SURFACE region). Roundabout is bottom-up (k=1=bed, CLAUDE.md); this column’s kb = k - 1 is the layer BELOW the interface (bed side), so kb <= nk_linear would select the BED-most layers — porting MOM6’s nk_linear=1 as written would put its SURFACE return-flow region at the SEABED. The dead nk_linear field + its else branch here were removed (PR-8) rather than wired: with the field permanently 0 (the only value ever set), kb > nk_linear was always true for every valid kb >= 1, so the limited path below ran unconditionally and the deleted branch never executed. Adding a correct surface linear return-flow region is a live, unrecorded physics divergence from MOM6’s default GM — it changes GM answers and needs its own PR + an analytical test (Psi -> 0 through the top layer), not a same-PR wire-up.

Arguments

Type IntentOptional Attributes Name
integer, intent(in) :: nx
integer, intent(in) :: ny
integer, intent(in) :: nz
real(kind=wp), intent(in) :: i_smax2
real(kind=wp), intent(in) :: i4dt
logical, intent(in) :: use_open
real(kind=wp), intent(in) :: dy_cu(nx+1,ny)
real(kind=wp), intent(in) :: areaT(nx,ny)
real(kind=wp), intent(in) :: h_layer(nx,ny,nz)
real(kind=wp), intent(in) :: bathy(nx,ny)
real(kind=wp), intent(in) :: slope_x(nx+1,ny,nz+1)
real(kind=wp), intent(in) :: khth_u(nx+1,ny)
real(kind=wp), intent(in) :: open_u(nx+1,ny,nz)
real(kind=wp), intent(inout) :: uhD(nx+1,ny,nz)

Calls

proc~~gm_column_x~~CallsGraph proc~gm_column_x gm_column_x local local proc~gm_column_x->local proc~gm_block_below_bed gm_block_below_bed proc~gm_column_x->proc~gm_block_below_bed proc~gm_h_frac gm_h_frac proc~gm_column_x->proc~gm_h_frac rdb_vl_is_live rdb_vl_is_live proc~gm_column_x->rdb_vl_is_live

Called by

proc~~gm_column_x~~CalledByGraph proc~gm_column_x gm_column_x proc~gm_compute_impl gm_compute_impl proc~gm_compute_impl->proc~gm_column_x proc~gm_compute_transports gm_compute_transports proc~gm_compute_transports->proc~gm_compute_impl proc~run_gm_step run_gm_step proc~run_gm_step->proc~gm_compute_transports proc~ocean_dyn_step_split ocean_dyn_step_split proc~ocean_dyn_step_split->proc~run_gm_step proc~engine_step engine_step proc~engine_step->proc~ocean_dyn_step_split

Variables

Type Visibility Attributes Name Initial
real(kind=wp), private :: eL(NZ_STACK_MAX+1)
real(kind=wp), private :: eR(NZ_STACK_MAX+1)
real(kind=wp), private :: h_frac_d
real(kind=wp), private :: havL(NZ_STACK_MAX)
real(kind=wp), private :: havR(NZ_STACK_MAX)
integer, private :: i
integer, private :: iw
integer, private :: j
integer, private :: k
integer, private :: ka
integer, private :: kb
real(kind=wp), private :: kh
integer, private :: ktop
logical, private :: ok(NZ_STACK_MAX)
real(kind=wp), private :: rsumL(NZ_STACK_MAX+1)
real(kind=wp), private :: rsumR(NZ_STACK_MAX+1)
real(kind=wp), private :: s2r
real(kind=wp), private :: sfn_est
real(kind=wp), private :: sfn_in_h
real(kind=wp), private :: sfn_safe
real(kind=wp), private :: sfn_unlim
real(kind=wp), private :: slope
real(kind=wp), private :: uhd_k
real(kind=wp), private :: uhtot

Source Code

   pure subroutine gm_column_x(nx, ny, nz, i_smax2, i4dt, use_open, &
                               dy_cu, areaT, h_layer, bathy, slope_x, khth_u, open_u, uhD)
      !! u-face GM streamfunction + bolus-transport column recurrence.
      !! Interior u-face (i=2..nx) pairs columns iw=i-1 (west) and i (east).
      !! Bottom-up sweep: interior interfaces Kr=2 (bed-most) -> nz
      !! (surface-most), uhtot=0 at the bed; the surface BC (Sfn=0 at
      !! Kr=nz+1) is closed after the loop by `uhD(nz)=-uhtot`, giving
      !! `Sum_k uhD=0` exactly.  Interface Kr straddles ka=Kr (above) and
      !! kb=Kr-1 (below) and fills LAYER kb; the rsum bound keys on ka, the
      !! donor `h_frac` and per-layer cap on kb.
      !!
      !! OPEN COLUMN (`use_open`, `&vcoord_nml zfixed_closed_faces`): a
      !! layer is part of the face column only if `ok(k) = open_u(i,j,k)`
      !! and it is live on both sides.  A not-`ok` layer gets `h_avail = 0`
      !! and `uhD = 0`, leaving `uhtot` (the streamfunction at its top)
      !! unchanged, and the closure lands in the topmost `ok` layer
      !! `ktop` instead of `nz` — Sfn = 0 at the bottom AND the top of the
      !! open column (see the module docstring).  `use_open = .false.` ⇒
      !! `ok` all-true, `ktop = nz`: the full-column recurrence verbatim.
      !!
      !! DIVERGENCE (MOM6 nk_linear): MOM6's `thickness_diffuse_full`
      !! sets `nk_linear = max(GV%nkml, 1)`; in
      !! ALE mode `GV%nkml = 0`, so MOM6 always runs `nk_linear = 1` and it
      !! is NOT a namelist parameter — its top layer always takes a linear
      !! return-flow closure (comment: "Balance the deeper flow with a
      !! return flow uniformly distributed though the remaining
      !! near-surface layers"; MOM6 is top-down, k=1=surface, so
      !! `k <= nk_linear` selects the SURFACE region).  Roundabout is
      !! bottom-up (k=1=bed, CLAUDE.md); this column's `kb = k - 1` is the
      !! layer BELOW the interface (bed side), so `kb <= nk_linear` would
      !! select the BED-most layers — porting MOM6's `nk_linear=1` as
      !! written would put its SURFACE return-flow region at the SEABED.
      !! The dead `nk_linear` field + its `else` branch here were removed
      !! (PR-8) rather than wired: with the field permanently 0 (the only
      !! value ever set), `kb > nk_linear` was always true for every valid
      !! `kb >= 1`, so the limited path below ran unconditionally and the
      !! deleted branch never executed.  Adding a correct surface linear
      !! return-flow region is a live, unrecorded physics divergence from
      !! MOM6's default GM — it changes GM answers and needs its own PR +
      !! an analytical test (Psi -> 0 through the top layer), not a
      !! same-PR wire-up.
      integer, intent(in) :: nx, ny, nz
      real(wp), intent(in) :: i_smax2, i4dt
      real(wp), intent(in) :: dy_cu(nx + 1, ny)
      real(wp), intent(in) :: areaT(nx, ny)
      real(wp), intent(in) :: h_layer(nx, ny, nz)
      real(wp), intent(in) :: bathy(nx, ny)
      real(wp), intent(in) :: slope_x(nx + 1, ny, nz + 1)
      real(wp), intent(in) :: khth_u(nx + 1, ny)
      logical, intent(in) :: use_open
      real(wp), intent(in) :: open_u(nx + 1, ny, nz)
      real(wp), intent(inout) :: uhD(nx + 1, ny, nz)

      integer :: i, j, k, iw, ka, kb, ktop
      real(wp) :: havL(NZ_STACK_MAX), havR(NZ_STACK_MAX)
      real(wp) :: rsumL(NZ_STACK_MAX + 1), rsumR(NZ_STACK_MAX + 1)
      real(wp) :: eL(NZ_STACK_MAX + 1), eR(NZ_STACK_MAX + 1)
      logical :: ok(NZ_STACK_MAX)
      real(wp) :: uhtot, slope, s2r, sfn_unlim, sfn_safe, sfn_est, sfn_in_h
      real(wp) :: h_frac_d, uhd_k, kh

      do concurrent(j=1:ny, i=2:nx) &
         local(k, iw, ka, kb, ktop, havL, havR, rsumL, rsumR, eL, eR, ok, uhtot, slope, &
               s2r, sfn_unlim, sfn_safe, sfn_est, sfn_in_h, h_frac_d, uhd_k, kh)
         iw = i - 1
         kh = khth_u(i, j)
         ! Interface heights (bed-up from z = -D) of the two columns, for
         ! the bottom-blocking limiter (`gm_block_below_bed`).
         eL(1) = -bathy(iw, j)
         eR(1) = -bathy(i, j)
         do k = 1, nz
            eL(k + 1) = eL(k) + h_layer(iw, j, k)
            eR(k + 1) = eR(k) + h_layer(i, j, k)
         end do

         ! The face's OPEN column (all-true without closed faces).
         do k = 1, nz
            ok(k) = .true.
         end do
         if (use_open) then
            do k = 1, nz
               ok(k) = open_u(i, j, k) > 0.5_wp .and. &
                       rdb_vl_is_live(h_layer(iw, j, k)) .and. &
                       rdb_vl_is_live(h_layer(i, j, k))
            end do
         end if
         ktop = 0
         do k = nz, 1, -1
            if (ok(k)) then
               ktop = k
               exit
            end if
         end do

         ! Per-layer availability + cumulative rsum from the SURFACE down:
         ! rsum*(k) = Sum_{k'=k}^{nz} h_avail(k') (mass above interface k).
         ! A layer outside the open column has nothing available here.
         rsumL(nz + 1) = 0.0_wp
         rsumR(nz + 1) = 0.0_wp
         do k = nz, 1, -1
            if (ok(k)) then
               havL(k) = max(i4dt*areaT(iw, j)*(h_layer(iw, j, k) - H_VANISHED), 0.0_wp)
               havR(k) = max(i4dt*areaT(i, j)*(h_layer(i, j, k) - H_VANISHED), 0.0_wp)
            else
               havL(k) = 0.0_wp
               havR(k) = 0.0_wp
            end if
            rsumL(k) = rsumL(k + 1) + havL(k)
            rsumR(k) = rsumR(k + 1) + havR(k)
         end do

         ! Sweep interior interfaces bed-most (Kr=2) -> surface-most (Kr=nz).
         uhtot = 0.0_wp
         uhD(i, j, nz) = 0.0_wp
         do k = 2, nz       ! k is the interface index Kr
            ka = k           ! layer above interface (surface side)
            kb = k - 1       ! layer below interface (bed side) -> uhD(kb)
            if (.not. ok(kb) .or. kb >= ktop) then
               ! Outside the open column, or its top layer (closed below):
               ! no transport, Sfn carried unchanged across it.
               uhD(i, j, kb) = 0.0_wp
               cycle
            end if
            slope = slope_x(i, j, k)
            ! NaN-safe: a non-finite slope must not reach the min/max
            ! limiters below (they launder NaN into a bound, CLAUDE.md).
            if (.not. ieee_is_finite(slope)) slope = 0.0_wp
            s2r = slope*slope*i_smax2
            sfn_unlim = gm_block_below_bed(-(kh*dy_cu(i, j))*slope, &
                                           eL(k), eL(k - 1), eR(1), eR(k), eR(k - 1), eL(1))
            if (uhtot <= 0.0_wp) then
               h_frac_d = gm_h_frac(havL(kb), rsumL(kb))
            else
               h_frac_d = gm_h_frac(havR(kb), rsumR(kb))
            end if
            sfn_safe = uhtot*(1.0_wp - h_frac_d)
            sfn_est = (sfn_unlim + s2r*sfn_safe)/(1.0_wp + s2r)
            ! Mass above interface Kr bounds the streamfunction.
            sfn_in_h = min(max(sfn_est, -rsumL(ka)), rsumR(ka))
            uhd_k = max(min(sfn_in_h - uhtot, havL(kb)), -havR(kb))
            uhD(i, j, kb) = uhd_k
            uhtot = uhtot + uhd_k
         end do
         ! Top BC (Sfn=0 above the topmost open layer, `nz` without closed
         ! faces): close the column so Sum=0.  No open layer ⇒ all zero.
         if (ktop > 0) uhD(i, j, ktop) = -uhtot
      end do
   end subroutine gm_column_x