evp_u_momentum_impl Subroutine

private pure subroutine evp_u_momentum_impl(idxCu, idyCu, dy2h, dx2q, iareaCu, mask_u, mi_u, mi_v, q, str_d, str_t, str_s, uo, vo, tau_ax, ui, vi, fxoc, m_neglect, i_cdrhodt, cdrho, dt, nx_phys, ny_phys, nghost, nx, ny, a_u, a_face_on)

u-momentum (:1172-1231, requirement 1: fxic_now carries the FULL str_t force term). Loop over u-faces ng+1..ng+nxp+1 x ng+1..ng+nyp — each iteration writes only its own face.

PR 62 (a_face_on): weights BOTH the wind (tau_ax) AND the ice-ocean drag (drag_u) by the face ice concentration a_u, in the momentum balance ONLY — fxoc stays unweighted (per unit ice area) so ice_ocean_stress_flux_impl’s a_u*fxoc on the coupler side is the ocean’s share (see the module docstring D7 + the ice_ocean_stress_flux F5 caveat). Weighting the wind alone would convert today’s leak (zero at steady free drift) into a permanent one — do not “simplify” this to a single weighted term.

The a_fac > 0.0 branch is a MANDATORY 0/0 guard, not defensive tidiness: at an ice-free face mi_u = 0, so a naive a_fac*drag_u collapses the denominator to m_neglect alone against a generally-nonzero dt*fxic_now, producing O(1e30) on the first substep. The else branch (uio_c = 0 => ui = uo) is the SIS2 limit (set_wind_stresses_C’s else WindStr_x_Cu = 0.0) reached without the division hazard. Do NOT floor a_fac instead of branching — a max(a_fac, eps) floor reintroduces a (much smaller but nonzero) ghost-drift artefact.

The drag PREDICTOR (b_vel0/uio_pred, below) is deliberately NOT folded by a_u: conservation depends only on drag_u’s use in the uio_c/fxoc pair (§3.3 of the PR-62 plan), not on the predictor’s accuracy, and drag_u’s own max(uio_init**2, ...) converges to the exact quadratic drag as the substep loop converges regardless.

Arguments

Type IntentOptional Attributes Name
real(kind=wp), intent(in) :: idxCu(nx+1,ny)
real(kind=wp), intent(in) :: idyCu(nx+1,ny)
real(kind=wp), intent(in) :: dy2h(nx,ny)
real(kind=wp), intent(in) :: dx2q(nx+1,ny+1)
real(kind=wp), intent(in) :: iareaCu(nx+1,ny)
real(kind=wp), intent(in) :: mask_u(nx+1,ny)
real(kind=wp), intent(in) :: mi_u(nx+1,ny)
real(kind=wp), intent(in) :: mi_v(nx,ny+1)
real(kind=wp), intent(in) :: q(nx+1,ny+1)
real(kind=wp), intent(in) :: str_d(nx,ny)
real(kind=wp), intent(in) :: str_t(nx,ny)
real(kind=wp), intent(in) :: str_s(nx+1,ny+1)
real(kind=wp), intent(in) :: uo(nx+1,ny)
real(kind=wp), intent(in) :: vo(nx,ny+1)
real(kind=wp), intent(in) :: tau_ax(nx+1,ny)
real(kind=wp), intent(inout) :: ui(nx+1,ny)
real(kind=wp), intent(in) :: vi(nx,ny+1)
real(kind=wp), intent(inout) :: fxoc(nx+1,ny)
real(kind=wp), intent(in) :: m_neglect
real(kind=wp), intent(in) :: i_cdrhodt
real(kind=wp), intent(in) :: cdrho
real(kind=wp), intent(in) :: dt
integer, intent(in) :: nx_phys
integer, intent(in) :: ny_phys
integer, intent(in) :: nghost
integer, intent(in) :: nx
integer, intent(in) :: ny
real(kind=wp), intent(in) :: a_u(nx+1,ny)

PR 62: face ice concentration. Valid ONLY when a_face_on.

logical, intent(in) :: a_face_on

Calls

proc~~evp_u_momentum_impl~~CallsGraph proc~evp_u_momentum_impl evp_u_momentum_impl local local proc~evp_u_momentum_impl->local

Called by

proc~~evp_u_momentum_impl~~CalledByGraph proc~evp_u_momentum_impl evp_u_momentum_impl proc~ice_evp_dynamics_impl ice_evp_dynamics_impl proc~ice_evp_dynamics_impl->proc~evp_u_momentum_impl proc~ice_evp_dynamics ice_evp_dynamics proc~ice_evp_dynamics->proc~ice_evp_dynamics_impl proc~ice_evp_step ice_evp_step proc~ice_evp_step->proc~ice_evp_dynamics proc~engine_step_ice engine_step_ice proc~engine_step_ice->proc~ice_evp_step proc~driver_run_ocean driver_run_ocean proc~driver_run_ocean->proc~engine_step_ice proc~rdb_ocean_step rdb_ocean_step proc~rdb_ocean_step->proc~engine_step_ice

Variables

Type Visibility Attributes Name Initial
real(kind=wp), private :: a_fac
real(kind=wp), private :: azon
real(kind=wp), private :: b_vel0
real(kind=wp), private :: bzon
real(kind=wp), private :: cor
real(kind=wp), private :: czon
real(kind=wp), private :: drag_eff
real(kind=wp), private :: drag_u
real(kind=wp), private :: dzon
real(kind=wp), private :: f2dt_u
real(kind=wp), private :: fxic_now
integer, private :: i
real(kind=wp), private :: i1_f2dt2_u
integer, private :: i_hi
integer, private :: i_lo
integer, private :: j
integer, private :: j_hi
integer, private :: j_lo
real(kind=wp), private :: m_uio_explicit
real(kind=wp), private :: tau_eff
real(kind=wp), private :: uio_c
real(kind=wp), private :: uio_init
real(kind=wp), private :: uio_pred
real(kind=wp), private :: v2_at_u

Source Code

   pure subroutine evp_u_momentum_impl(idxCu, idyCu, dy2h, dx2q, iareaCu, mask_u, mi_u, mi_v, &
                                       q, str_d, str_t, str_s, uo, vo, tau_ax, ui, vi, &
                                       fxoc, m_neglect, i_cdrhodt, cdrho, dt, &
                                       nx_phys, ny_phys, nghost, nx, ny, a_u, a_face_on)
      !! u-momentum (:1172-1231, requirement 1: fxic_now carries the FULL
      !! str_t force term). Loop over u-faces `ng+1..ng+nxp+1` x
      !! `ng+1..ng+nyp` — each iteration writes only its own face.
      !!
      !! PR 62 (`a_face_on`): weights BOTH the wind (`tau_ax`) AND the
      !! ice-ocean drag (`drag_u`) by the face ice concentration `a_u`, in
      !! the momentum balance ONLY — `fxoc` stays unweighted (per unit ice
      !! area) so `ice_ocean_stress_flux_impl`'s `a_u*fxoc` on the coupler
      !! side is the ocean's share (see the module docstring D7 + the
      !! `ice_ocean_stress_flux` F5 caveat). Weighting the wind alone would
      !! convert today's leak (zero at steady free drift) into a permanent
      !! one — do not "simplify" this to a single weighted term.
      !!
      !! The `a_fac > 0.0` branch is a MANDATORY 0/0 guard, not defensive
      !! tidiness: at an ice-free face `mi_u = 0`, so a naive
      !! `a_fac*drag_u` collapses the denominator to `m_neglect` alone
      !! against a generally-nonzero `dt*fxic_now`, producing `O(1e30)` on
      !! the first substep. The `else` branch (`uio_c = 0` => `ui = uo`) is
      !! the SIS2 limit (`set_wind_stresses_C`'s `else WindStr_x_Cu = 0.0`)
      !! reached without the division hazard. Do NOT floor `a_fac` instead
      !! of branching — a `max(a_fac, eps)` floor reintroduces a
      !! (much smaller but nonzero) ghost-drift artefact.
      !!
      !! The drag PREDICTOR (`b_vel0`/`uio_pred`, below) is deliberately NOT
      !! folded by `a_u`: conservation depends only on `drag_u`'s use in the
      !! `uio_c`/`fxoc` pair (§3.3 of the PR-62 plan), not on the predictor's
      !! accuracy, and `drag_u`'s own `max(uio_init**2, ...)` converges to
      !! the exact quadratic drag as the substep loop converges regardless.
      integer, intent(in) :: nx_phys, ny_phys, nghost, nx, ny
      real(wp), intent(in) :: idxCu(nx + 1, ny), idyCu(nx + 1, ny)
      real(wp), intent(in) :: dy2h(nx, ny), dx2q(nx + 1, ny + 1)
      real(wp), intent(in) :: iareaCu(nx + 1, ny)
      real(wp), intent(in) :: mask_u(nx + 1, ny)
      real(wp), intent(in) :: mi_u(nx + 1, ny), mi_v(nx, ny + 1)
      real(wp), intent(in) :: q(nx + 1, ny + 1)
      real(wp), intent(in) :: str_d(nx, ny), str_t(nx, ny), str_s(nx + 1, ny + 1)
      real(wp), intent(in) :: uo(nx + 1, ny), vo(nx, ny + 1)
      real(wp), intent(in) :: tau_ax(nx + 1, ny)
      real(wp), intent(inout) :: ui(nx + 1, ny)
      real(wp), intent(in) :: vi(nx, ny + 1)
      real(wp), intent(inout) :: fxoc(nx + 1, ny)
      real(wp), intent(in) :: m_neglect, i_cdrhodt, cdrho, dt
      real(wp), intent(in) :: a_u(nx + 1, ny)
         !! PR 62: face ice concentration. Valid ONLY when `a_face_on`.
      logical, intent(in) :: a_face_on
      integer :: i, j, i_lo, i_hi, j_lo, j_hi
      real(wp) :: cor, f2dt_u, i1_f2dt2_u
      real(wp) :: azon, bzon, czon, dzon
      real(wp) :: fxic_now, v2_at_u, uio_init
      real(wp) :: m_uio_explicit, b_vel0, uio_pred, drag_u, uio_c
      real(wp) :: a_fac, tau_eff, drag_eff

      i_lo = nghost + 1
      i_hi = nghost + nx_phys + 1
      j_lo = nghost + 1
      j_hi = nghost + ny_phys

      do concurrent(j=j_lo:j_hi, i=i_lo:i_hi) &
         local(cor, f2dt_u, i1_f2dt2_u, azon, bzon, czon, dzon, fxic_now, v2_at_u, &
               uio_init, m_uio_explicit, b_vel0, uio_pred, drag_u, uio_c, &
               a_fac, tau_eff, drag_eff)
         azon = 0.25_wp*mi_v(i, j + 1)*q(i, j + 1)
         bzon = 0.25_wp*mi_v(i - 1, j + 1)*q(i, j + 1)
         czon = 0.25_wp*mi_v(i - 1, j)*q(i, j)
         dzon = 0.25_wp*mi_v(i, j)*q(i, j)

         cor = 0.25_wp*(q(i, j + 1)*(mi_v(i, j + 1)*vi(i, j + 1) + mi_v(i - 1, j + 1)*vi(i - 1, j + 1)) + &
                        q(i, j)*(mi_v(i - 1, j)*vi(i - 1, j) + mi_v(i, j)*vi(i, j)))

         f2dt_u = dt*4.0_wp*((azon**2 + czon**2) + (bzon**2 + dzon**2))
         i1_f2dt2_u = 1.0_wp/(1.0_wp + dt*f2dt_u)

         fxic_now = idxCu(i, j)*(str_d(i, j) - str_d(i - 1, j)) + &
                    (idyCu(i, j)*(dy2h(i, j)*str_t(i, j) - dy2h(i - 1, j)*str_t(i - 1, j)) + &
                     idxCu(i, j)*(dx2q(i, j + 1)*str_s(i, j + 1) - dx2q(i, j)*str_s(i, j)))* &
                    iareaCu(i, j)

         v2_at_u = 0.25_wp*(((vi(i - 1, j + 1) - vo(i - 1, j + 1))**2 + &
                             (vi(i, j) - vo(i, j))**2) + &
                            ((vi(i, j + 1) - vo(i, j + 1))**2 + &
                             (vi(i - 1, j) - vo(i - 1, j))**2))

         uio_init = ui(i, j) - uo(i, j)

         ! TWO FULLY SEPARATE ARMS, not `a_fac = 1.0_wp` feeding one shared
         ! expression: the off arm below is TEXTUALLY UNCHANGED from the
         ! pre-PR kernel, predictor included.  A shared `tau_eff`/`drag_eff`
         ! computed via `a_fac = 1.0_wp` is mathematically exact (1.0*x==x
         ! in IEEE) but NVHPC's GPU codegen does not guarantee identical FMA
         ! contraction/rounding across ~5000 chained substeps for two
         ! syntactically different expression trees that merely evaluate to
         ! the same VALUE — confirmed empirically (`a_face_full_cover_
         ! bitident` drifted ~1e-14 rel under exactly that construction
         ! before this fix; the `7d283fe8` GPU FMA precedent, CLAUDE.md
         ! Gotchas).  Keep the off arm untouched, full stop.
         if (a_face_on) then
            a_fac = a_u(i, j)
            tau_eff = a_fac*tau_ax(i, j)

            drag_u = 0.0_wp
            if (mask_u(i, j) > 0.0_wp) then
               m_uio_explicit = uio_init*mi_u(i, j) + dt*(cor*mi_u(i, j) + (fxic_now + tau_eff))
               b_vel0 = mi_u(i, j)*i_cdrhodt + (sqrt(uio_init**2 + v2_at_u) - abs(uio_init))
               if (b_vel0**2 > EVP_DRAG_LINEARIZE_THRESHOLD*i_cdrhodt*abs(m_uio_explicit)) then
                  if (b_vel0 /= 0.0_wp) then
                     uio_pred = m_uio_explicit*i_cdrhodt/b_vel0
                  else
                     uio_pred = 0.0_wp
                  end if
               else
                  uio_pred = 0.5_wp*(sqrt(b_vel0**2 + 4.0_wp*i_cdrhodt*abs(m_uio_explicit)) - b_vel0)
               end if
               drag_u = cdrho*sqrt(max(uio_init**2, uio_pred**2) + v2_at_u)
            end if

            drag_eff = a_fac*drag_u
            if (a_fac > 0.0_wp) then
               uio_c = mask_u(i, j)*(mi_u(i, j)*((ui(i, j) + dt*cor)*i1_f2dt2_u - uo(i, j)) + &
                                     dt*(fxic_now + tau_eff))/ &
                       (mi_u(i, j) + m_neglect + dt*drag_eff)
            else
               uio_c = 0.0_wp     ! no ice at either neighbour: no ice momentum here
            end if
         else
            drag_u = 0.0_wp
            if (mask_u(i, j) > 0.0_wp) then
               m_uio_explicit = uio_init*mi_u(i, j) + dt*(cor*mi_u(i, j) + (fxic_now + tau_ax(i, j)))
               b_vel0 = mi_u(i, j)*i_cdrhodt + (sqrt(uio_init**2 + v2_at_u) - abs(uio_init))
               if (b_vel0**2 > EVP_DRAG_LINEARIZE_THRESHOLD*i_cdrhodt*abs(m_uio_explicit)) then
                  if (b_vel0 /= 0.0_wp) then
                     uio_pred = m_uio_explicit*i_cdrhodt/b_vel0
                  else
                     uio_pred = 0.0_wp
                  end if
               else
                  uio_pred = 0.5_wp*(sqrt(b_vel0**2 + 4.0_wp*i_cdrhodt*abs(m_uio_explicit)) - b_vel0)
               end if
               drag_u = cdrho*sqrt(max(uio_init**2, uio_pred**2) + v2_at_u)
            end if

            uio_c = mask_u(i, j)*(mi_u(i, j)*((ui(i, j) + dt*cor)*i1_f2dt2_u - uo(i, j)) + &
                                  dt*(fxic_now + tau_ax(i, j)))/ &
                    (mi_u(i, j) + m_neglect + dt*drag_u)
         end if

         ui(i, j) = (uio_c + uo(i, j))*mask_u(i, j)
         fxoc(i, j) = fxoc(i, j) + drag_u*uio_c          ! UNWEIGHTED: the coupler applies a_u
      end do
   end subroutine evp_u_momentum_impl