coriolis_adv_compute_tendencies_sadourny Subroutine

private pure subroutine coriolis_adv_compute_tendencies_sadourny(grid, metrics, this, ms, u, v, h)

Per-layer Sadourny Coriolis + advection tendency. Same algorithm as the barotropic counterpart, lifted with a k-axis on every loop. Each k-slice is independent (ζ stencil only reads same-k velocities; KE at centre only reads same-k face values), so the do-concurrent kernels parallelise over (k, j, i) for full GPU occupancy.

The Coriolis parameter is read from this%f_corner (2D field at C-grid corners, shared with the barotropic kernel). Uniform f_0 is the f-plane default; call this%set_beta_plane(grid, f_0, beta, y_ref) to switch to a f = f_0 + beta*(y - y_ref) profile.

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_sadourny~~CallsGraph proc~coriolis_adv_compute_tendencies_sadourny coriolis_adv_compute_tendencies_sadourny local local proc~coriolis_adv_compute_tendencies_sadourny->local proc~weno3_recon weno3_recon proc~coriolis_adv_compute_tendencies_sadourny->proc~weno3_recon proc~weno5_recon weno5_recon proc~coriolis_adv_compute_tendencies_sadourny->proc~weno5_recon proc~weno7_recon weno7_recon proc~coriolis_adv_compute_tendencies_sadourny->proc~weno7_recon proc~fac_weno fac_weno proc~weno3_recon->proc~fac_weno proc~beta5_0 beta5_0 proc~weno5_recon->proc~beta5_0 proc~beta5_1 beta5_1 proc~weno5_recon->proc~beta5_1 proc~beta5_2 beta5_2 proc~weno5_recon->proc~beta5_2 proc~weno5_recon->proc~fac_weno proc~beta7_0 beta7_0 proc~weno7_recon->proc~beta7_0 proc~beta7_1 beta7_1 proc~weno7_recon->proc~beta7_1 proc~beta7_2 beta7_2 proc~weno7_recon->proc~beta7_2 proc~beta7_3 beta7_3 proc~weno7_recon->proc~beta7_3 proc~weno7_recon->proc~fac_weno

Called by

proc~~coriolis_adv_compute_tendencies_sadourny~~CalledByGraph proc~coriolis_adv_compute_tendencies_sadourny coriolis_adv_compute_tendencies_sadourny proc~coriolis_adv_compute_tendencies coriolis_adv_compute_tendencies proc~coriolis_adv_compute_tendencies->proc~coriolis_adv_compute_tendencies_sadourny 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 :: f_at_u
real(kind=wp), private :: f_at_v
real(kind=wp), private :: h_eff_sum
real(kind=wp), private :: h_uf_NE
real(kind=wp), private :: h_uf_NW
real(kind=wp), private :: h_uf_SE
real(kind=wp), private :: h_uf_SW
real(kind=wp), private :: h_vf_NE
real(kind=wp), private :: h_vf_NW
real(kind=wp), private :: h_vf_SE
real(kind=wp), private :: h_vf_SW
integer, private :: i
integer, private :: j
integer, private :: k
real(kind=wp), private :: ke_grad_x
real(kind=wp), private :: ke_grad_y
real(kind=wp), private :: ns
integer, private :: nx
integer, private :: ny
integer, private :: nz
integer, private :: pv_scheme
real(kind=wp), private :: u_at_v
real(kind=wp), private :: uh_sum
real(kind=wp), private :: v_at_u
real(kind=wp), private :: vh_sum
real(kind=wp), private :: zeta_at_u
real(kind=wp), private :: zeta_at_v

Source Code

   pure subroutine coriolis_adv_compute_tendencies_sadourny(grid, metrics, this, ms, u, v, h)
      !! Per-layer Sadourny Coriolis + advection tendency.  Same
      !! algorithm as the barotropic counterpart, lifted with a
      !! k-axis on every loop.  Each k-slice is independent (ζ
      !! stencil only reads same-k velocities; KE at centre only
      !! reads same-k face values), so the do-concurrent kernels
      !! parallelise over (k, j, i) for full GPU occupancy.
      !!
      !! The Coriolis parameter is read from `this%f_corner` (2D
      !! field at C-grid corners, shared with the barotropic
      !! kernel).  Uniform `f_0` is the f-plane default; call
      !! `this%set_beta_plane(grid, f_0, beta, y_ref)` to switch to
      !! a `f = f_0 + beta*(y - y_ref)` profile.
      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, pv_scheme
      real(wp) :: v_at_u, u_at_v, zeta_at_u, zeta_at_v
      real(wp) :: f_at_u, f_at_v, ke_grad_x, ke_grad_y
      real(wp) :: h_vf_SW, h_vf_NW, h_vf_SE, h_vf_NE
      real(wp) :: h_uf_SW, h_uf_NW, h_uf_SE, h_uf_NE
      real(wp) :: vh_sum, uh_sum, h_eff_sum
      real(wp) :: ns

      nx = grid%nx_total
      ny = grid%ny_total
      nz = ms%nz_ml
      ! Hoist the loop-invariant PV face-interp scheme to a plain scalar so
      ! the do-concurrent kernels never touch a derived-type component.
      pv_scheme = this%pv_adv_scheme
      ns = merge(1.0_wp, 0.0_wp, this%no_slip)

      ! ---- Pass 1: relative vorticity at SW corners, per layer ----
      ! Inline twin of `rdb_rvc_zeta_corner` (shared_module_utilities/
      ! rdb_rel_vort_corner.inc, read by the `vorticity_z` diag): keep in step.
      ! Circulation/area (design §2) — see the barotropic kernel for the
      ! reduction to `(Δv)/dx - (Δu)/dy` on uniform square metrics.
      ! Slip factor `(1-2·ns)·wet_q + 2·ns` masks the rel-vort at land
      ! corners (C1, free-slip default); planetary f stays unmasked.
      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: KE at cell centres, per layer (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

      ! ---- Pass 3a: du/dt at interior east faces ----
      ! Thickness-weighted v at the u-face:
      !   v_at_u = Σ(v_face_y · h_at_v_face) / Σ(h_at_v_face)
      ! over the four abutting v-faces.  For uniform `h_layer` this
      ! reduces to the simple 4-point velocity average and is
      ! bit-identical to the previous code.  Under variable
      ! thickness it captures the mass-weighted advection the
      ! centred PV-advection form requires.  We compute `v · h_face`
      ! inline rather than reading `mass_flux_y_layer` so the kernel
      ! is self-contained — it doesn't matter whether continuity has
      ! run yet in this stage.
      ! ---- Pass 3a: u-face Coriolis-advection term (ζ+f)·v_at_u ----
      ! Writes (ζ+f)·v_at_u into pv_flux_x; the −∇KE term is subtracted
      ! in Pass 3c BELOW.  The split keeps the −∇KE subtraction in one
      ! place and future-proofs a BOUND_CORIOLIS clip on `CAu` BEFORE
      ! `−KEx` (mirroring MOM6's MOM_CoriolisAdv ordering — not yet ported).
      !
      ! This is the classical Sadourny (1975) ENSTROPHY-conserving form
      ! `CAu = (zeta_at_u + f_at_u)·v_at_u` (the default `form="sadourny"`,
      ! and `al81`).  The energy-conserving transport form
      ! (`form="sadourny_energy"`, MOM6 SADOURNY75_ENERGY) is a separate
      ! kernel, `coriolis_adv_compute_tendencies_sadourny_energy`.
      ! NOTE: the natural `associate (q_corner => this%q_corner%data,
      !       f_corner => this%f_corner)` shorthand is DELIBERATELY not used
      !       here.  ifx (2025.0 and 2026.0) miscompiles a reference to an
      !       ASSOCIATE name whose selector is an allocatable component of a
      !       derived-type dummy when the reference sits inside a `do
      !       concurrent` body that also contains a branch calling an inlined
      !       pure module function: under `-qopenmp` (which is how ifx maps
      !       `do concurrent` onto threads) the associate name reads as ZERO,
      !       so `f_corner` vanished from `zeta_at_u`/`zeta_at_v` and the whole
      !       Coriolis term silently went to 0.  gfortran 15.1 and nvfortran
      !       26.5 are correct; standalone repro +  writeup in
      !       the project wiki (ifx ASSOCIATE / do concurrent).  Spell the
      !       components out until Intel fixes it.
      do concurrent(k=1:nz, j=1:ny, i=2:nx) &
         local(f_at_u, h_vf_SW, h_vf_NW, h_vf_SE, h_vf_NE, &
               vh_sum, h_eff_sum)
         h_vf_SW = 0.5_wp*(h(i - 1, max(1, j - 1), k) + h(i - 1, j, k))
         h_vf_NW = 0.5_wp*(h(i - 1, j, k) + h(i - 1, min(ny, j + 1), k))
         h_vf_SE = 0.5_wp*(h(i, max(1, j - 1), k) + h(i, j, k))
         h_vf_NE = 0.5_wp*(h(i, j, k) + h(i, min(ny, j + 1), k))
         vh_sum = (v(i - 1, j, k)*h_vf_SW + &
                   v(i - 1, j + 1, k)*h_vf_NW) + &
                  (v(i, j, k)*h_vf_SE + &
                   v(i, j + 1, k)*h_vf_NE)
         h_eff_sum = (h_vf_SW + h_vf_NW) + (h_vf_SE + h_vf_NE)
         if (h_eff_sum > 0.0_wp) then
            v_at_u = vh_sum/h_eff_sum
         else
            v_at_u = 0.0_wp
         end if
         ! Absolute vorticity (f+zeta) interpolated onto the u-face along j,
         ! upwind on v_at_u.  WENO reconstructs it directly (f baked into the
         ! stencil, MOM6 reconstructs f+zeta); centred = the 2-point average.
         ! Each order falls back to centred within its stencil radius of the
         ! j=1 / j=ny array edges (the nghost gate keeps every PHYSICAL face
         ! inside the band, so only ghost faces degrade).
         if (pv_scheme == PV_ADV_WENO7 .and. j >= 4 .and. j <= ny - 3) then
            zeta_at_u = weno7_recon( &
                        this%q_corner%data(i, j - 3, k) + this%f_corner(i, j - 3), &
                        this%q_corner%data(i, j - 2, k) + this%f_corner(i, j - 2), &
                        this%q_corner%data(i, j - 1, k) + this%f_corner(i, j - 1), &
                        this%q_corner%data(i, j, k) + this%f_corner(i, j), &
                        this%q_corner%data(i, j + 1, k) + this%f_corner(i, j + 1), &
                        this%q_corner%data(i, j + 2, k) + this%f_corner(i, j + 2), &
                        this%q_corner%data(i, j + 3, k) + this%f_corner(i, j + 3), &
                        this%q_corner%data(i, j + 4, k) + this%f_corner(i, j + 4), v_at_u)
         else if (pv_scheme == PV_ADV_WENO5 .and. j >= 3 .and. j <= ny - 2) then
            zeta_at_u = weno5_recon( &
                        this%q_corner%data(i, j - 2, k) + this%f_corner(i, j - 2), &
                        this%q_corner%data(i, j - 1, k) + this%f_corner(i, j - 1), &
                        this%q_corner%data(i, j, k) + this%f_corner(i, j), &
                        this%q_corner%data(i, j + 1, k) + this%f_corner(i, j + 1), &
                        this%q_corner%data(i, j + 2, k) + this%f_corner(i, j + 2), &
                        this%q_corner%data(i, j + 3, k) + this%f_corner(i, j + 3), v_at_u)
         else if (pv_scheme == PV_ADV_WENO3 .and. j >= 2 .and. j <= ny - 1) then
            zeta_at_u = weno3_recon( &
                        this%q_corner%data(i, j - 1, k) + this%f_corner(i, j - 1), &
                        this%q_corner%data(i, j, k) + this%f_corner(i, j), &
                        this%q_corner%data(i, j + 1, k) + this%f_corner(i, j + 1), &
                        this%q_corner%data(i, j + 2, k) + this%f_corner(i, j + 2), v_at_u)
         else
            zeta_at_u = 0.5_wp*(this%q_corner%data(i, j, k) + this%q_corner%data(i, j + 1, k)) + &
                        0.5_wp*(this%f_corner(i, j) + this%f_corner(i, j + 1))
         end if
         this%pv_flux_x%data(i, j, k) = zeta_at_u*v_at_u
      end do
      ! ---- Pass 3c: subtract −∇KE from u-tendency ----
      do concurrent(k=1:nz, j=1:ny, i=2:nx) local(ke_grad_x)
         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) = this%pv_flux_x%data(i, j, k) - ke_grad_x
      end do
      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

      ! ---- Pass 4a: v-face Coriolis-advection term −(ζ+f)·u_at_v ----
      ! Mirror of Pass 3a.  Writes −(ζ+f)·u_at_v ONLY; ∇KE handled in 4c.
      do concurrent(k=1:nz, j=2:ny, i=1:nx) &
         local(f_at_v, h_uf_SW, h_uf_NW, h_uf_SE, h_uf_NE, &
               uh_sum, h_eff_sum)
         h_uf_SW = 0.5_wp*(h(max(1, i - 1), j - 1, k) + h(i, j - 1, k))
         h_uf_SE = 0.5_wp*(h(i, j - 1, k) + h(min(nx, i + 1), j - 1, k))
         h_uf_NW = 0.5_wp*(h(max(1, i - 1), j, k) + h(i, j, k))
         h_uf_NE = 0.5_wp*(h(i, j, k) + h(min(nx, i + 1), j, k))
         uh_sum = (u(i, j - 1, k)*h_uf_SW + &
                   u(i + 1, j - 1, k)*h_uf_SE) + &
                  (u(i, j, k)*h_uf_NW + &
                   u(i + 1, j, k)*h_uf_NE)
         h_eff_sum = (h_uf_SW + h_uf_SE) + (h_uf_NW + h_uf_NE)
         if (h_eff_sum > 0.0_wp) then
            u_at_v = uh_sum/h_eff_sum
         else
            u_at_v = 0.0_wp
         end if
         ! Absolute vorticity onto the v-face along i, upwind on u_at_v.
         ! Sign mirrors the centred form (CAv = -(f+zeta)*u_at_v).  Same
         ! per-order boundary fallback as the u-face.
         if (pv_scheme == PV_ADV_WENO7 .and. i >= 4 .and. i <= nx - 3) then
            zeta_at_v = weno7_recon( &
                        this%q_corner%data(i - 3, j, k) + this%f_corner(i - 3, j), &
                        this%q_corner%data(i - 2, j, k) + this%f_corner(i - 2, j), &
                        this%q_corner%data(i - 1, j, k) + this%f_corner(i - 1, j), &
                        this%q_corner%data(i, j, k) + this%f_corner(i, j), &
                        this%q_corner%data(i + 1, j, k) + this%f_corner(i + 1, j), &
                        this%q_corner%data(i + 2, j, k) + this%f_corner(i + 2, j), &
                        this%q_corner%data(i + 3, j, k) + this%f_corner(i + 3, j), &
                        this%q_corner%data(i + 4, j, k) + this%f_corner(i + 4, j), u_at_v)
         else if (pv_scheme == PV_ADV_WENO5 .and. i >= 3 .and. i <= nx - 2) then
            zeta_at_v = weno5_recon( &
                        this%q_corner%data(i - 2, j, k) + this%f_corner(i - 2, j), &
                        this%q_corner%data(i - 1, j, k) + this%f_corner(i - 1, j), &
                        this%q_corner%data(i, j, k) + this%f_corner(i, j), &
                        this%q_corner%data(i + 1, j, k) + this%f_corner(i + 1, j), &
                        this%q_corner%data(i + 2, j, k) + this%f_corner(i + 2, j), &
                        this%q_corner%data(i + 3, j, k) + this%f_corner(i + 3, j), u_at_v)
         else if (pv_scheme == PV_ADV_WENO3 .and. i >= 2 .and. i <= nx - 1) then
            zeta_at_v = weno3_recon( &
                        this%q_corner%data(i - 1, j, k) + this%f_corner(i - 1, j), &
                        this%q_corner%data(i, j, k) + this%f_corner(i, j), &
                        this%q_corner%data(i + 1, j, k) + this%f_corner(i + 1, j), &
                        this%q_corner%data(i + 2, j, k) + this%f_corner(i + 2, j), u_at_v)
         else
            zeta_at_v = 0.5_wp*(this%q_corner%data(i, j, k) + this%q_corner%data(i + 1, j, k)) + &
                        0.5_wp*(this%f_corner(i, j) + this%f_corner(i + 1, j))
         end if
         this%pv_flux_y%data(i, j, k) = -zeta_at_v*u_at_v
      end do

      ! ---- Pass 4c: subtract −∇KE from v-tendency ----
      do concurrent(k=1:nz, j=2:ny, i=1:nx) local(ke_grad_y)
         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) = this%pv_flux_y%data(i, j, k) - ke_grad_y
      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_sadourny