Mesoscale eddy kinetic energy (MEKE) prognostic slot.
!! Mesoscale eddy kinetic energy (MEKE) prognostic slot. module rdb_ocean_meke !! Mesoscale eddy kinetic energy parameterization. Carries a single 2D !! vertically-averaged eddy-energy field `meke(nx,ny)` [m^2/s^2], sourced !! by the GM potential-energy release, damped by an implicit !! (backward-Euler) bottom drag, transported laterally (harmonic-mass !! Laplacian + optional biharmonic + advection), and fed back as a !! thickness/tracer diffusivity `kh = khcoeff·sqrt(2·gamma_t²·E)·Lmix` !! added (geom mean) into VarMix's per-face KhTh before GM's CFL clamp — !! closing the GM↔eddy-energy loop. Updated via a Strang split each !! thermo step. !! !! Clean-room (no source ported). `meke` is PROGNOSTIC (restart-persistent). !! Default off (`&ocean_meke_nml enable = .false.`) ⇒ `meke_step` never !! called ⇒ bit-identical. Bottom-up convention (k=1 bed, k=nz surface); !! only the column mass sum touches k and it is orientation-independent. !! !! References: Jansen, Adcroft, Hallberg & Held (2015); Eden & Greatbatch !! (2008); Marshall, Maddison & Berloff (2012). #ifdef LFORTRAN_PASSING use rdb_constants, only: wp, H_VANISHED, H_DIV_EPS, GRAVITY #else use rdb_constants, only: NZ_STACK_MAX, wp, H_VANISHED, H_DIV_EPS, GRAVITY #endif use rdb_grid, only: hgrid_t use rdb_ocean_metrics, only: ocean_metrics_t use rdb_multilayer_state, only: multilayer_state_t use rdb_ocean_gm, only: ocean_gm_t use rdb_ocean_varmix, only: ocean_varmix_t use rdb_ocean_wave_speed, only: ocean_wave_speed_t use, intrinsic :: iso_fortran_env, only: int64 use rdb_mem_report, only: arr_bytes implicit none private #ifdef LFORTRAN_PASSING integer, parameter :: NZ_STACK_MAX = 64 !! LFortran 0.64 workaround: module-local copy of the rdb_constants value !! (an imported parameter used as an explicit-shape dummy bound inside a !! PURE call becomes an impure getter under LFortran). Keep in sync (=64). #endif public :: ocean_meke_t public :: meke_step public :: meke_backscatter_apply real(wp), parameter :: MASS_NEGLECT = 1.0e-30_wp !! Floor in the harmonic-mass denominator (MOM6 `mass_neglect`). real(wp), parameter :: BACKSCATTER_CFL = 0.8_wp !! Forward-Euler viscous-CFL safety coefficient for the backscatter !! lower bound (MOM6 `BACKSCATTER_UNDERBOUND` analogue). The NET !! (resolved − Ku) harmonic viscosity is floored at !! `−BACKSCATTER_CFL·0.5/(dt·(idx²+idy²))` so the negative mode's !! growth RATE is bounded — NOT so the operator is stable on its own !! (a negative Laplacian always amplifies; a positive biharmonic !! backstop, mandatory at configure, dissipates the fed grid-scale !! mode). Matches the `bound_coef = 0.8` convention the resolved !! hvisc CFL limiter uses. type :: ocean_meke_t !! Mesoscale eddy kinetic energy state. All fields default to the !! inert (`enable=.false.`) configuration so an ocean run that never !! sets `&ocean_meke_nml` is bit-identical. logical :: is_init = .false. !! True between `init` and `destroy`; gate on this (never on !! `allocated`, which misses the GPU mapping). logical :: enable = .false. !! Master switch. Off ⇒ `meke_step` is never called. Requires !! `&ocean_gm_nml enable` (needs `gm%gm_src`); loud configure check. ! ---- Source / sink coefficients ---- real(wp) :: gmcoeff = -1.0_wp !! Efficiency of PE->MEKE conversion (nondim). < 0 ⇒ GM source off. real(wp) :: frcoeff = -1.0_wp !! Efficiency of mean->eddy frictional conversion (nondim); < 0 !! (default) ⇒ off ⇒ bit-identical. When >= 0, adds the frictional !! source `-frcoeff*i_mass*ke_diss` from the lateral-viscosity KE !! dissipation (`hvisc%ke_diss`). real(wp) :: bgsrc = 0.0_wp !! Background energy source (m^2/s^3). real(wp) :: damping = 0.0_wp !! Local depth-independent linear MEKE dissipation rate (1/s). ! ---- Lateral transport ---- real(wp) :: kh = -1.0_wp !! Background lateral diffusion of MEKE (m^2/s). < 0 ⇒ diffusion !! stage off. real(wp) :: k4 = -1.0_wp !! Background biharmonic diffusion of MEKE (m^4/s). < 0 ⇒ off. real(wp) :: khmeke_fac = 0.0_wp !! Factor relating `meke%kh` (the derived diffusivity) to the !! diffusivity used for MEKE's own lateral spreading (nondim). real(wp) :: advection_factor = 0.0_wp !! Scaling on the barotropic-transport advection of MEKE (nondim); !! 0 (default) ⇒ the advection stage is skipped ⇒ bit-identical. ! ---- KhTh closure ---- real(wp) :: khcoeff = 1.0_wp !! Scaling converting MEKE into Kh (nondim). <= 0 ⇒ closure off. real(wp) :: cd_scale = 0.0_wp !! Ratio of bottom eddy velocity to column-mean eddy velocity !! (nondim); enters bottomFac2. real(wp) :: cb = 25.0_wp !! Coefficient in the gamma_bot (bottomFac2) expression (nondim). real(wp) :: ct = 50.0_wp !! Coefficient in the gamma_bt (barotrFac2) expression (nondim). real(wp) :: min_gamma2 = 1.0e-4_wp !! Floor on gamma_b^2 / gamma_t^2 (nondim). real(wp) :: uscale = 0.0_wp !! Background (e.g. tidal) eddy velocity scale for bottom drag (m/s). real(wp) :: dtscale = 1.0_wp !! Scale factor accelerating MEKE time-stepping (nondim). real(wp) :: cdrag = 2.5e-3_wp !! Bottom drag coefficient for MEKE (nondim). Copied from !! `&ocean_bdrag_nml cdrag_side` at configure if that is > 0, !! else this default; enters drag_rate + Lfrict. logical :: use_bbl_drag = .false. !! Add the resolved bed-layer eddy velocity `|u_bed|²` to the MEKE !! bottom-drag rate `drag_rate = rho0·i_mass·sqrt(cdrag²·(2·bf2·E + !! |u_bed|² + uscale²))` (MOM6 `drag_rate_visc`). Default off ⇒ the !! `u_bbl²` workspace stays 0 ⇒ bit-identical to the prior drag. ! ---- Length-scale alpha weights (default all 0 ⇒ Lmix path inert) ---- real(wp) :: alpha_deform = 0.0_wp !! Weight on the deformation length scale Ldeform (nondim). real(wp) :: alpha_rhines = 0.0_wp !! Weight on the Rhines length scale Lrhines = sqrt(Ueddy/beta) !! (nondim). Default 0 ⇒ beta term inert; > 0 activates the !! Rhines scale (needs `f_centre` filled, done at setup). real(wp) :: alpha_eady = 0.0_wp !! Weight on the Eady length scale Leady (needs VarMix SN) (nondim). real(wp) :: alpha_frict = 0.0_wp !! Weight on the frictional-arrest length scale Lfrict (nondim). real(wp) :: alpha_grid = 0.0_wp !! Weight on the grid length scale Lgrid (nondim). ! ---- Feedback seam factors ---- real(wp) :: khth_fac = 0.0_wp !! Factor on the geometric-mean kh added into VarMix's KhTh face !! field (nondim). 0 (default) ⇒ feedback inert ⇒ bit-identical. real(wp) :: khtr_fac = 0.0_wp !! Factor on the geometric-mean kh added into VarMix's KhTr face !! field (nondim). 0 (default) ⇒ inert. ! ---- Backscatter (negative-viscosity momentum energy return, v1) ---- logical :: backscatter = .false. !! Master switch for the MEKE → momentum negative-viscosity !! energy return (capability Gap 2). Default `.false.` ⇒ the !! `ku` field stays 0 and `meke_backscatter_apply` adds nothing !! ⇒ bit-identical. When `.true.`, `meke_step` fills `ku` and the !! driver subtracts it from the per-face harmonic viscosity, !! returning eddy energy to the resolved flow. HARMONIC only in !! v1 (the biharmonic `Au` return + EBT/SQG vertical structure !! `BS_struct` are deferred). Needs `enable=.true.` (so `meke_step` !! runs and refreshes `ku`) — inert otherwise. real(wp) :: visc_coeff_ku = 0.0_wp !! MOM6 `MEKE_VISCOSITY_COEFF_KU` — the nondimensional efficiency !! of the harmonic backscatter viscosity !! `Ku = visc_coeff_ku·sqrt(2·gamma_t²·E)·Lmix` (m²/s). May be !! negative in MOM6 (negative viscosity); here it is the magnitude !! coefficient and the SIGN of the momentum effect is set by the !! SUBTRACTION in `meke_backscatter_apply` (`A_net = A − Ku`), so a !! positive `visc_coeff_ku` returns energy. 0 (default) ⇒ inert. ! ---- Cached extents ---- integer :: nx_total = 0 integer :: ny_total = 0 integer :: nz_ml = 0 ! ---- Prognostic + derived 2D fields ---- real(wp), allocatable :: meke(:, :) !! Eddy kinetic energy E (m^2/s^2), `(nx,ny)`. PROGNOSTIC — !! restart-persistent. real(wp), allocatable :: kh_diff(:, :) !! Derived MEKE diffusivity kh (m^2/s), `(nx,ny)`; fed (geom-mean) !! into VarMix's face KhTh/KhTr. real(wp), allocatable :: le(:, :) !! Mixing length scale Lmix (m), `(nx,ny)` (diagnostic). real(wp), allocatable :: ku(:, :) !! Derived harmonic backscatter viscosity Ku (m²/s), `(nx,ny)`; !! `ku = visc_coeff_ku·sqrt(2·gamma_t²·E)·Lmix`. Zero unless !! `backscatter`. Subtracted (face-averaged, stability-floored) !! from the resolved harmonic viscosity by `meke_backscatter_apply`. ! ---- Strang-stage workspace ---- real(wp), allocatable :: i_mass(:, :) !! 1 / column mass (m^2/kg), `(nx,ny)`. real(wp), allocatable :: depth_tot(:, :) !! Total column thickness (m), `(nx,ny)`; used in Lfrict. real(wp), allocatable :: bottom_fac2(:, :) !! gamma_b^2 (nondim), `(nx,ny)`. real(wp), allocatable :: barotr_fac2(:, :) !! gamma_t^2 (nondim), `(nx,ny)`. real(wp), allocatable :: src(:, :) !! Aggregate source (m^2/s^3), `(nx,ny)`. real(wp), allocatable :: uflux(:, :) !! u-face MEKE flux workspace, `(nx+1,ny)`. real(wp), allocatable :: vflux(:, :) !! v-face MEKE flux workspace, `(nx,ny+1)`. real(wp), allocatable :: del2(:, :) !! Laplacian of MEKE workspace (biharmonic), `(nx,ny)`. real(wp), allocatable :: mass_ws(:, :) !! Column mass (kg/m^2), `(nx,ny)`; the harmonic-mass input for the !! lateral flux (= 1/i_mass where i_mass>0). real(wp), allocatable :: u_bbl2(:, :) !! Resolved bed-layer speed² (m²/s²) at cell centres, `(nx,ny)`; !! filled from `ms` when `use_bbl_drag`, else 0. Feeds `meke_drag`. real(wp), allocatable :: ke_diss_ws(:, :) !! Staged hvisc KE-dissipation rate (kg/s³, ≤0), `(nx,ny)`; copied !! from `hv%ke_diss` when the frictional source is wired, else 0. !! Feeds `meke_source` as `-frcoeff·i_mass·ke_diss`. real(wp), allocatable :: rd_ws(:, :) !! Staged copy of `wavespeed%rd_over_dx` (nondim), `(nx,ny)`; zero !! when wavespeed absent ⇒ Ldeform→0. real(wp), allocatable :: f_centre(:, :) !! |f| at cell centres (1/s), `(nx,ny)`; filled by `set_f_centre` !! from the same beta-plane the Coriolis slot uses (mirrors EPBL / !! kappa-shear). Drives `beta = |grad f|` for the Rhines length. !! Zero until filled ⇒ Rhines weight inert (alpha_rhines default 0). real(wp), allocatable :: sn_u_ws(:, :) !! Staged copy of `varmix%sn_u` (1/s), `(nx+1,ny)`; zero when VarMix !! absent ⇒ Eady scale inert. real(wp), allocatable :: sn_v_ws(:, :) !! Staged copy of `varmix%sn_v` (1/s), `(nx,ny+1)`. real(wp), allocatable :: baro_hu(:, :) !! Depth-integrated u-face mass transport (kg/s), `(nx+1,ny)`; !! the barotropic transport that advects E. Filled only when !! `advection_factor > 0`. real(wp), allocatable :: baro_hv(:, :) !! Depth-integrated v-face mass transport (kg/s), `(nx,ny+1)`. contains procedure, non_overridable :: init => ocean_meke_init procedure, non_overridable :: destroy => ocean_meke_destroy procedure, non_overridable :: enter_data => ocean_meke_enter_data procedure, non_overridable :: exit_data => ocean_meke_exit_data procedure, non_overridable :: set_f_centre => ocean_meke_set_f_centre procedure, non_overridable :: bytes => ocean_meke_bytes end type ocean_meke_t contains subroutine ocean_meke_init(this, grid, nz_ml) !! Allocate the prognostic field, the derived diffusivity, and the !! Strang-stage workspaces. Always allocates (configure runs after !! init); setup uses plain host allocation (no `do concurrent` before !! enter_data). class(ocean_meke_t), intent(inout) :: this type(hgrid_t), intent(in) :: grid integer, intent(in), optional :: nz_ml integer :: nx, ny, nz nx = grid%nx_total ny = grid%ny_total nz = 1 if (present(nz_ml)) nz = nz_ml if (nz < 1) nz = 1 if (nz > NZ_STACK_MAX) then error stop "ocean_meke_init: nz_ml exceeds NZ_STACK_MAX "// & "(raise NZ_STACK_MAX in rdb_constants)" end if this%nx_total = nx this%ny_total = ny this%nz_ml = nz allocate (this%meke(nx, ny), source=0.0_wp) allocate (this%kh_diff(nx, ny), source=0.0_wp) allocate (this%le(nx, ny), source=0.0_wp) allocate (this%ku(nx, ny), source=0.0_wp) allocate (this%i_mass(nx, ny), source=0.0_wp) allocate (this%depth_tot(nx, ny), source=0.0_wp) allocate (this%bottom_fac2(nx, ny), source=0.0_wp) allocate (this%barotr_fac2(nx, ny), source=0.0_wp) allocate (this%src(nx, ny), source=0.0_wp) allocate (this%uflux(nx + 1, ny), source=0.0_wp) allocate (this%vflux(nx, ny + 1), source=0.0_wp) allocate (this%del2(nx, ny), source=0.0_wp) allocate (this%mass_ws(nx, ny), source=0.0_wp) allocate (this%u_bbl2(nx, ny), source=0.0_wp) allocate (this%ke_diss_ws(nx, ny), source=0.0_wp) allocate (this%rd_ws(nx, ny), source=0.0_wp) allocate (this%f_centre(nx, ny), source=0.0_wp) allocate (this%sn_u_ws(nx + 1, ny), source=0.0_wp) allocate (this%sn_v_ws(nx, ny + 1), source=0.0_wp) allocate (this%baro_hu(nx + 1, ny), source=0.0_wp) allocate (this%baro_hv(nx, ny + 1), source=0.0_wp) this%is_init = .true. end subroutine ocean_meke_init subroutine ocean_meke_destroy(this) class(ocean_meke_t), intent(inout) :: this this%is_init = .false. if (allocated(this%meke)) deallocate (this%meke) if (allocated(this%kh_diff)) deallocate (this%kh_diff) if (allocated(this%le)) deallocate (this%le) if (allocated(this%ku)) deallocate (this%ku) if (allocated(this%i_mass)) deallocate (this%i_mass) if (allocated(this%depth_tot)) deallocate (this%depth_tot) if (allocated(this%bottom_fac2)) deallocate (this%bottom_fac2) if (allocated(this%barotr_fac2)) deallocate (this%barotr_fac2) if (allocated(this%src)) deallocate (this%src) if (allocated(this%uflux)) deallocate (this%uflux) if (allocated(this%vflux)) deallocate (this%vflux) if (allocated(this%del2)) deallocate (this%del2) if (allocated(this%mass_ws)) deallocate (this%mass_ws) if (allocated(this%u_bbl2)) deallocate (this%u_bbl2) if (allocated(this%ke_diss_ws)) deallocate (this%ke_diss_ws) if (allocated(this%rd_ws)) deallocate (this%rd_ws) if (allocated(this%f_centre)) deallocate (this%f_centre) if (allocated(this%sn_u_ws)) deallocate (this%sn_u_ws) if (allocated(this%sn_v_ws)) deallocate (this%sn_v_ws) if (allocated(this%baro_hu)) deallocate (this%baro_hu) if (allocated(this%baro_hv)) deallocate (this%baro_hv) this%nx_total = 0 this%ny_total = 0 this%nz_ml = 0 end subroutine ocean_meke_destroy subroutine ocean_meke_enter_data(this) ! Poly TBP delegating to a `type(...)`-arg `_impl` (AMD-crash rule). class(ocean_meke_t), intent(inout) :: this select type (this) type is (ocean_meke_t) call ocean_meke_enter_data_impl(this) end select end subroutine ocean_meke_enter_data subroutine ocean_meke_enter_data_impl(this) type(ocean_meke_t), intent(inout) :: this if (.not. this%is_init) return !$acc enter data copyin(this%meke, this%kh_diff, this%le, this%ku) !$acc enter data copyin(this%i_mass, this%depth_tot) !$acc enter data copyin(this%bottom_fac2, this%barotr_fac2, this%src) !$acc enter data copyin(this%uflux, this%vflux, this%del2) !$acc enter data copyin(this%mass_ws, this%rd_ws, this%f_centre) !$acc enter data copyin(this%u_bbl2, this%ke_diss_ws) !$acc enter data copyin(this%sn_u_ws, this%sn_v_ws) !$acc enter data copyin(this%baro_hu, this%baro_hv) end subroutine ocean_meke_enter_data_impl subroutine ocean_meke_exit_data(this) class(ocean_meke_t), intent(inout) :: this select type (this) type is (ocean_meke_t) call ocean_meke_exit_data_impl(this) end select end subroutine ocean_meke_exit_data subroutine ocean_meke_exit_data_impl(this) type(ocean_meke_t), intent(inout) :: this if (.not. this%is_init) return !$acc exit data delete(this%baro_hu, this%baro_hv) !$acc exit data delete(this%sn_u_ws, this%sn_v_ws) !$acc exit data delete(this%u_bbl2, this%ke_diss_ws) !$acc exit data delete(this%mass_ws, this%rd_ws, this%f_centre) !$acc exit data delete(this%uflux, this%vflux, this%del2) !$acc exit data delete(this%bottom_fac2, this%barotr_fac2, this%src) !$acc exit data delete(this%i_mass, this%depth_tot) !$acc exit data delete(this%meke, this%kh_diff, this%le, this%ku) end subroutine ocean_meke_exit_data_impl subroutine ocean_meke_set_f_centre(this, grid, f_centre) !! Copy a pre-filled cell-centre Coriolis magnitude |f| (1/s) onto the !! MEKE slot, so `beta = |grad f|` for the Rhines length is live. The !! caller (setup) builds `f_centre` with the same `metrics_fill_coriolis` !! path the Coriolis / VarMix / EPBL slots use (handles beta-plane AND !! spherical). Host loop — call after `init`, before `enter_data`. class(ocean_meke_t), intent(inout) :: this type(hgrid_t), intent(in) :: grid real(wp), intent(in) :: f_centre(grid%nx_total, grid%ny_total) integer :: i, j if (.not. allocated(this%f_centre)) return do j = 1, grid%ny_total do i = 1, grid%nx_total this%f_centre(i, j) = f_centre(i, j) end do end do end subroutine ocean_meke_set_f_centre ! ================================================================= ! Compute driver ! ================================================================= subroutine meke_step(grid, metrics, this, gm, varmix, wavespeed, ms, dt, ke_diss_ext) !! Advance the MEKE field one thermo step (Strang split), update the !! derived diffusivity `kh_diff`, and feed the geometric-mean kh into !! VarMix's per-face KhTh/KhTr (the GM↔MEKE feedback). Run once per !! outer step at thermo cadence, after `varmix_compute` and before !! `gm_compute_transports` (MEKE reads `gm%gm_src` from the previous !! thermo step — a one-step lag). No-op when `enable=.false.`, !! uninitialised, or the GM slot is absent. type(hgrid_t), intent(in) :: grid type(ocean_metrics_t), intent(in) :: metrics type(ocean_meke_t), intent(inout) :: this type(ocean_gm_t), intent(in) :: gm type(ocean_varmix_t), intent(inout), optional :: varmix type(ocean_wave_speed_t), intent(in), optional :: wavespeed type(multilayer_state_t), intent(in) :: ms real(wp), intent(in) :: dt real(wp), intent(in), optional :: ke_diss_ext(:, :) !! hvisc KE-dissipation rate `(nx,ny)` for the frictional source; !! absent ⇒ the source is inert (staged to 0). integer :: nx, ny, nz real(wp) :: sdt, sdt_damp, damp_step logical :: have_rho, kh_flux_enabled, have_ws, have_vm if (.not. this%is_init) return if (.not. this%enable) return if (.not. gm%is_init) return if (.not. allocated(ms%h_layer)) return if (.not. allocated(gm%gm_src)) return nx = grid%nx_total ny = grid%ny_total nz = ms%nz_ml if (this%nz_ml /= nz) return sdt = dt*this%dtscale damp_step = 1.0_wp if (this%kh >= 0.0_wp .or. this%k4 >= 0.0_wp) damp_step = 0.5_wp sdt_damp = sdt*damp_step kh_flux_enabled = (this%kh >= 0.0_wp) have_rho = allocated(ms%rho_layer) have_ws = .false. if (present(wavespeed)) have_ws = wavespeed%is_init .and. allocated(wavespeed%rd_over_dx) have_vm = .false. if (present(varmix)) have_vm = varmix%is_init .and. allocated(varmix%sn_u) ! ---- 0. stage the optional inputs into device-resident workspaces. ! Copy ON-DEVICE from the slot arrays (which are device-resident) so the ! length-scale kernel reads explicit-shape slot args, never an ! optional-or-host-staged actual. When the source slot is absent the ! workspace is zeroed (Ldeform->0 / Eady inert). `f_centre` is the ! slot's own |f| (filled at setup via set_f_centre); when never filled ! it is zero ⇒ beta=0 ⇒ Rhines inert (alpha_rhines default 0). if (have_ws) then call meke_stage_rd(nx, ny, wavespeed%rd_over_dx, this%rd_ws) else call meke_zero_2d(nx, ny, this%rd_ws) end if if (have_vm) then call meke_stage_sn(nx, ny, varmix%sn_u, varmix%sn_v, & this%sn_u_ws, this%sn_v_ws) else call meke_zero_2d(nx + 1, ny, this%sn_u_ws) call meke_zero_2d(nx, ny + 1, this%sn_v_ws) end if ! ---- 1. column mass + depth (I_mass, depth_tot, mass_ws). ---- ! Pass rho_layer only when allocated; otherwise feed h_layer as a ! harmless placeholder (gated off by have_rho ⇒ mass uses gm%rho0). if (have_rho) then call meke_mass(nx, ny, nz, gm%rho0, .true., ms%h_layer, ms%rho_layer, & this%i_mass, this%depth_tot, this%mass_ws) else call meke_mass(nx, ny, nz, gm%rho0, .false., ms%h_layer, ms%h_layer, & this%i_mass, this%depth_tot, this%mass_ws) end if ! ---- 2. structure factors (bottomFac2, barotrFac2) + Lmix into le. ---- call meke_length_scales(nx, ny, this%cd_scale, this%cb, this%ct, & this%min_gamma2, this%cdrag, & this%alpha_deform, this%alpha_rhines, & this%alpha_eady, this%alpha_frict, this%alpha_grid, & metrics%areaT, metrics%idxT, metrics%idyT, & this%f_centre, & this%depth_tot, this%meke, & this%rd_ws, this%sn_u_ws, this%sn_v_ws, & this%bottom_fac2, this%barotr_fac2, this%le) ! ---- 3. explicit source bump: E += sdt*src. ---- ! Stage the hvisc KE-dissipation rate for the frictional source ! (-frcoeff·i_mass·ke_diss); 0 when the seam is not wired ⇒ inert. if (present(ke_diss_ext)) then call meke_stage_rd(nx, ny, ke_diss_ext, this%ke_diss_ws) else call meke_zero_2d(nx, ny, this%ke_diss_ws) end if call meke_source(nx, ny, this%bgsrc, this%gmcoeff, this%frcoeff, sdt, & this%i_mass, gm%gm_src, this%ke_diss_ws, this%src, this%meke) ! ---- 4. implicit drag half: E <- E/(1+sdt_damp*damp_rate). ---- ! Resolved bed-layer eddy velocity (MOM6 drag_rate_visc); 0 when off. if (this%use_bbl_drag) then call meke_bbl_speed2(nx, ny, nz, ms%u_face_x_layer, ms%v_face_y_layer, & ms%k_bot_u, ms%k_bot_v, this%u_bbl2) end if call meke_drag(nx, ny, sdt_damp, this%damping, this%cdrag, this%uscale, gm%rho0, & this%i_mass, this%bottom_fac2, this%u_bbl2, this%meke) ! ---- 5. lateral diffusion (+ biharmonic). ---- if (kh_flux_enabled .or. this%k4 >= 0.0_wp) then call meke_lateral(nx, ny, sdt, this%kh, this%k4, this%khmeke_fac, & kh_flux_enabled, & metrics%dy_cu, metrics%dx_cv, metrics%idxCu, & metrics%idyCv, metrics%iareaT, & this%i_mass, this%mass_ws, & this%kh_diff, this%uflux, this%vflux, this%del2, & this%meke) end if ! ---- 5b. upwind advection of E by the barotropic transport. ---- ! Self-contained transport stage: baroHu = Sum_k (mass-weighted face ! transport), upwind flux E*baroHu, single conservative divergence. ! `advection_factor = 0` (default) ⇒ no-op ⇒ bit-identical. if (this%advection_factor > 0.0_wp) then if (have_rho) then call meke_baro_transport(nx, ny, nz, gm%rho0, .true., & ms%mass_flux_x_layer, ms%mass_flux_y_layer, & ms%rho_layer, this%baro_hu, this%baro_hv) else call meke_baro_transport(nx, ny, nz, gm%rho0, .false., & ms%mass_flux_x_layer, ms%mass_flux_y_layer, & ms%rho_layer, this%baro_hu, this%baro_hv) end if call meke_advect(nx, ny, sdt, this%advection_factor, metrics%iareaT, & this%i_mass, this%baro_hu, this%baro_hv, & this%uflux, this%vflux, this%meke) end if ! ---- 6. implicit drag half (only when Strang-split, damp_step=0.5). ---- ! `u_bbl2` already filled in step 4 (or held at 0 when off). if (this%kh >= 0.0_wp .or. this%k4 >= 0.0_wp) then call meke_drag(nx, ny, sdt_damp, this%damping, this%cdrag, this%uscale, gm%rho0, & this%i_mass, this%bottom_fac2, this%u_bbl2, this%meke) end if ! ---- 7. MEKE -> KhTh closure: kh = khcoeff*sqrt(2*gamma_t2*E)*Lmix. ---- call meke_kh_closure(nx, ny, this%khcoeff, & this%barotr_fac2, this%meke, this%le, this%kh_diff) ! ---- 7b. MEKE -> Ku backscatter coefficient (v1, harmonic only). ---- ! ku = visc_coeff_ku*sqrt(2*gamma_t2*E)*Lmix; same eddy-velocity × ! mixing-length form as kh. Filled only when `backscatter`; the ! driver subtracts a face-average of `ku` from the resolved harmonic ! viscosity (stability-floored) ⇒ a negative-viscosity energy return. ! Default off ⇒ ku stays 0 ⇒ bit-identical. call meke_ku_closure(nx, ny, this%backscatter, this%visc_coeff_ku, & this%meke, this%le, this%ku) ! ---- 8. feedback seam: add geom-mean kh into VarMix face KhTh/KhTr. ---- if (present(varmix)) then if (varmix%is_init .and. (this%khth_fac /= 0.0_wp .or. & this%khtr_fac /= 0.0_wp)) then call meke_feed_khth(nx, ny, this%khth_fac, this%khtr_fac, & this%kh_diff, varmix%khth_u, varmix%khth_v, & varmix%khtr_u, varmix%khtr_v) end if end if end subroutine meke_step pure subroutine meke_zero_2d(n1, n2, a) !! Zero a device-resident 2D workspace (absent-source fallback). integer, intent(in) :: n1, n2 real(wp), intent(out) :: a(n1, n2) integer :: i, j do concurrent(j=1:n2, i=1:n1) a(i, j) = 0.0_wp end do end subroutine meke_zero_2d pure subroutine meke_stage_rd(nx, ny, rd_in, rd_ws) !! Copy rd_over_dx onto the device workspace. Thermo-cadence; !! explicit-shape; on-device (source slot is device-resident). integer, intent(in) :: nx, ny real(wp), intent(in) :: rd_in(nx, ny) real(wp), intent(out) :: rd_ws(nx, ny) integer :: i, j do concurrent(j=1:ny, i=1:nx) rd_ws(i, j) = rd_in(i, j) end do end subroutine meke_stage_rd pure subroutine meke_stage_sn(nx, ny, sn_u_in, sn_v_in, sn_u_ws, sn_v_ws) !! Copy the SN faces onto the device workspaces. Thermo-cadence; !! explicit-shape; on-device (VarMix slot is device-resident). integer, intent(in) :: nx, ny real(wp), intent(in) :: sn_u_in(nx + 1, ny) real(wp), intent(in) :: sn_v_in(nx, ny + 1) real(wp), intent(out) :: sn_u_ws(nx + 1, ny) real(wp), intent(out) :: sn_v_ws(nx, ny + 1) integer :: i, j do concurrent(j=1:ny, i=1:nx + 1) sn_u_ws(i, j) = sn_u_in(i, j) end do do concurrent(j=1:ny + 1, i=1:nx) sn_v_ws(i, j) = sn_v_in(i, j) end do end subroutine meke_stage_sn ! ================================================================= ! Kernels ! ================================================================= pure subroutine meke_mass(nx, ny, nz, rho0, have_rho, h_layer, rho_layer, & i_mass, depth_tot, mass_ws) !! Column mass `mass = Sum_k rho*max(h,H_VANISHED)` (kg/m^2), its !! inverse `i_mass` (0 where mass<=0), `depth_tot = Sum_k h` (m), and !! `mass_ws = mass` (the harmonic-mass input for the lateral flux). integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: rho0 logical, intent(in) :: have_rho real(wp), intent(in) :: h_layer(nx, ny, nz) real(wp), intent(in) :: rho_layer(nx, ny, nz) real(wp), intent(out) :: i_mass(nx, ny) real(wp), intent(out) :: depth_tot(nx, ny) real(wp), intent(out) :: mass_ws(nx, ny) integer :: i, j, k real(wp) :: mass, dsum, hk, rhok do concurrent(j=1:ny, i=1:nx) local(k, mass, dsum, hk, rhok) mass = 0.0_wp dsum = 0.0_wp do k = 1, nz hk = max(h_layer(i, j, k), H_VANISHED) rhok = rho0 if (have_rho) rhok = rho_layer(i, j, k) mass = mass + rhok*hk dsum = dsum + h_layer(i, j, k) end do depth_tot(i, j) = dsum mass_ws(i, j) = mass if (mass > 0.0_wp) then i_mass(i, j) = 1.0_wp/mass else i_mass(i, j) = 0.0_wp end if end do end subroutine meke_mass pure subroutine meke_baro_transport(nx, ny, nz, rho0, have_rho, & mflux_x, mflux_y, rho_layer, & baro_hu, baro_hv) !! Depth-integrated, mass-weighted barotropic transport through each !! C-grid face: `baroHu(I,j) = Sum_k rho_face * mass_flux_x_layer`, !! where `mass_flux_*_layer` is the per-layer VOLUME transport (m^3/s, !! = u*h_face*dy) and `rho_face` the two-cell average density. The !! result is a MASS transport (kg/s) so the advective divergence pairs !! exactly with `IareaT*I_mass` (1/(area*mass)) ⇒ Sum E*area*mass is !! conserved. Array-edge faces carry zero transport (closed domain). integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: rho0 logical, intent(in) :: have_rho real(wp), intent(in) :: mflux_x(nx + 1, ny, nz) real(wp), intent(in) :: mflux_y(nx, ny + 1, nz) real(wp), intent(in) :: rho_layer(nx, ny, nz) real(wp), intent(out) :: baro_hu(nx + 1, ny) real(wp), intent(out) :: baro_hv(nx, ny + 1) integer :: i, j, k real(wp) :: tsum, rho_face ! u-faces: interior I=2..nx between cells I-1 and I. do concurrent(j=1:ny, i=1:nx + 1) local(k, tsum, rho_face) tsum = 0.0_wp if (i >= 2 .and. i <= nx) then do k = 1, nz rho_face = rho0 if (have_rho) rho_face = 0.5_wp*(rho_layer(i - 1, j, k) + rho_layer(i, j, k)) tsum = tsum + rho_face*mflux_x(i, j, k) end do end if baro_hu(i, j) = tsum end do ! v-faces: interior J=2..ny between cells J-1 and J. do concurrent(j=1:ny + 1, i=1:nx) local(k, tsum, rho_face) tsum = 0.0_wp if (j >= 2 .and. j <= ny) then do k = 1, nz rho_face = rho0 if (have_rho) rho_face = 0.5_wp*(rho_layer(i, j - 1, k) + rho_layer(i, j, k)) tsum = tsum + rho_face*mflux_y(i, j, k) end do end if baro_hv(i, j) = tsum end do end subroutine meke_baro_transport pure subroutine meke_advect(nx, ny, sdt, adv_fac, iareaT, i_mass, & baro_hu, baro_hv, uflux, vflux, meke) !! Upwind flux-form advection of E by the barotropic mass transport. !! advFac = adv_fac/sdt !! uflux(I) = baroHu(I)*advFac*E_upwind (E_{I-1} if baroHu>0 else E_I) !! E += sdt*IareaT*I_mass*((uflux_{i-1}-uflux_i)+(vflux_{j-1}-vflux_j)) !! Conservative on a closed domain (interior faces only; the !! divergence telescopes ⇒ Sum E*area*mass conserved). `adv_fac=0` !! never reaches here (gated by the caller) ⇒ default bit-identity. integer, intent(in) :: nx, ny real(wp), intent(in) :: sdt, adv_fac real(wp), intent(in) :: iareaT(nx, ny) real(wp), intent(in) :: i_mass(nx, ny) real(wp), intent(in) :: baro_hu(nx + 1, ny) real(wp), intent(in) :: baro_hv(nx, ny + 1) real(wp), intent(inout) :: uflux(nx + 1, ny) real(wp), intent(inout) :: vflux(nx, ny + 1) real(wp), intent(inout) :: meke(nx, ny) integer :: i, j real(wp) :: adv_per_t, bh, mke adv_per_t = 0.0_wp if (sdt > 0.0_wp) adv_per_t = adv_fac/sdt ! u-face upwind flux (array-edge faces carry zero ⇒ closed domain). do concurrent(j=1:ny, i=1:nx + 1) uflux(i, j) = 0.0_wp end do do concurrent(j=1:ny, i=2:nx) local(bh) bh = baro_hu(i, j) if (bh > 0.0_wp) then uflux(i, j) = bh*adv_per_t*meke(i - 1, j) else if (bh < 0.0_wp) then uflux(i, j) = bh*adv_per_t*meke(i, j) end if end do ! v-face upwind flux. do concurrent(j=1:ny + 1, i=1:nx) vflux(i, j) = 0.0_wp end do do concurrent(j=2:ny, i=1:nx) local(bh) bh = baro_hv(i, j) if (bh > 0.0_wp) then vflux(i, j) = bh*adv_per_t*meke(i, j - 1) else if (bh < 0.0_wp) then vflux(i, j) = bh*adv_per_t*meke(i, j) end if end do ! conservative divergence (uflux at face i is the LEFT face of cell i; ! face i+1 the RIGHT face). Inflow-left minus outflow-right. do concurrent(j=1:ny, i=1:nx) local(mke) mke = sdt*(iareaT(i, j)*i_mass(i, j))* & ((uflux(i, j) - uflux(i + 1, j)) + (vflux(i, j) - vflux(i, j + 1))) meke(i, j) = meke(i, j) + mke end do end subroutine meke_advect pure function meke_inv_lmix(ueddy, sn, beta, area, rd_over_dx, depth, cdrag, & a_deform, a_rhines, a_eady, a_frict, a_grid) result(inv_l) !$acc routine seq !! Harmonic inverse mixing length `1/Lmix = Sum aX/LX` over the five !! length scales (deformation, frictional, Rhines, Eady, grid). Each !! scale is gated `aX*LX > 0` so a zero weight or a degenerate scale !! contributes nothing. Returns 1/Lmix (0 ⇒ Lmix degenerate). real(wp), intent(in) :: ueddy, sn, beta, area, rd_over_dx, depth, cdrag real(wp), intent(in) :: a_deform, a_rhines, a_eady, a_frict, a_grid real(wp) :: inv_l real(wp) :: lgrid, ldeform, lfrict, lrhines, leady lgrid = sqrt(max(area, 0.0_wp)) ldeform = lgrid*rd_over_dx lfrict = 0.0_wp if (cdrag > 0.0_wp) lfrict = depth/cdrag lrhines = 0.0_wp if (beta > 0.0_wp) lrhines = sqrt(max(ueddy, 0.0_wp)/beta) leady = 0.0_wp if (sn > 1.0e-15_wp) leady = ueddy/sn inv_l = 0.0_wp if (a_deform*ldeform > 0.0_wp) inv_l = inv_l + 1.0_wp/(a_deform*ldeform) if (a_frict*lfrict > 0.0_wp) inv_l = inv_l + 1.0_wp/(a_frict*lfrict) if (a_rhines*lrhines > 0.0_wp) inv_l = inv_l + 1.0_wp/(a_rhines*lrhines) if (a_eady*leady > 0.0_wp) inv_l = inv_l + 1.0_wp/(a_eady*leady) if (a_grid*lgrid > 0.0_wp) inv_l = inv_l + 1.0_wp/(a_grid*lgrid) end function meke_inv_lmix pure subroutine meke_length_scales(nx, ny, cd_scale, cb, ct, min_gamma2, & cdrag, a_deform, a_rhines, & a_eady, a_frict, a_grid, & areaT, idxT, idyT, f_centre, & depth_tot, meke, & rd_over_dx, sn_u, sn_v, & bottom_fac2, barotr_fac2, le) !! Fill the structure factors gamma_b^2 (`bottom_fac2`) and gamma_t^2 !! (`barotr_fac2`) plus the mixing length `le` (Lmix) at each cell !! centre. `Ldeform/Lfrict` drives both gammas; `Lmix` is the harmonic !! sum of the alpha-weighted scales. `beta = |grad f|` from centred !! `f_centre` differences scaled by `idxT`/`idyT` (zero when f_centre !! is unfilled ⇒ Rhines inert). SN = 0.25*(sn_u(i)+sn_u(i-1)+ !! sn_v(j)+sn_v(j-1)) only when aEady>0. integer, intent(in) :: nx, ny real(wp), intent(in) :: cd_scale, cb, ct, min_gamma2, cdrag real(wp), intent(in) :: a_deform, a_rhines, a_eady, a_frict, a_grid real(wp), intent(in) :: areaT(nx, ny) real(wp), intent(in) :: idxT(nx, ny) real(wp), intent(in) :: idyT(nx, ny) real(wp), intent(in) :: f_centre(nx, ny) real(wp), intent(in) :: depth_tot(nx, ny) real(wp), intent(in) :: meke(nx, ny) real(wp), intent(in) :: rd_over_dx(nx, ny) real(wp), intent(in) :: sn_u(nx + 1, ny) real(wp), intent(in) :: sn_v(nx, ny + 1) real(wp), intent(out) :: bottom_fac2(nx, ny) real(wp), intent(out) :: barotr_fac2(nx, ny) real(wp), intent(out) :: le(nx, ny) integer :: i, j real(wp) :: lgrid, ldeform, lfrict, ratio, bf2, tf2 real(wp) :: ueddy, sn, beta, inv_l, rd do concurrent(j=1:ny, i=1:nx) & local(lgrid, ldeform, lfrict, ratio, bf2, tf2, ueddy, sn, beta, inv_l, rd) rd = rd_over_dx(i, j) lgrid = sqrt(max(areaT(i, j), 0.0_wp)) ldeform = lgrid*rd lfrict = 0.0_wp if (cdrag > 0.0_wp) lfrict = depth_tot(i, j)/cdrag ! gamma_b^2 = cd_scale^2 + 1/(1+Cb*Ldeform/Lfrict)^0.8 (floor). bf2 = cd_scale*cd_scale if (lfrict*cb > 0.0_wp) then ratio = ldeform/lfrict bf2 = bf2 + 1.0_wp/(1.0_wp + cb*ratio)**0.8_wp end if bf2 = max(bf2, min_gamma2) bottom_fac2(i, j) = bf2 ! gamma_t^2 = 1/(1+Ct*Ldeform/Lfrict)^0.25 (floor). tf2 = 1.0_wp if (lfrict*ct > 0.0_wp) then ratio = ldeform/lfrict tf2 = 1.0_wp/(1.0_wp + ct*ratio)**0.25_wp end if tf2 = max(tf2, min_gamma2) barotr_fac2(i, j) = tf2 ! Mixing length: harmonic sum of alpha-weighted scales. ueddy = sqrt(2.0_wp*max(0.0_wp, tf2*meke(i, j))) ! beta = |grad f|; centred f_centre differences scaled to a true ! gradient by the cell-centre inverse metrics (df/dx ~ (f_{i+1} - ! f_{i-1})*idxT/2). When f_centre is unfilled (default) beta=0 ⇒ ! Rhines weight inert (alpha_rhines default 0). beta = 0.0_wp if (i > 1 .and. i < nx) then beta = beta + (0.5_wp*(f_centre(i + 1, j) - f_centre(i - 1, j))*idxT(i, j))**2 end if if (j > 1 .and. j < ny) then beta = beta + (0.5_wp*(f_centre(i, j + 1) - f_centre(i, j - 1))*idyT(i, j))**2 end if beta = sqrt(beta) sn = 0.0_wp if (a_eady > 0.0_wp) sn = 0.25_wp*((sn_u(i, j) + sn_u(i + 1, j)) + & (sn_v(i, j) + sn_v(i, j + 1))) inv_l = meke_inv_lmix(ueddy, sn, beta, areaT(i, j), rd, & depth_tot(i, j), cdrag, & a_deform, a_rhines, a_eady, a_frict, a_grid) if (inv_l > 0.0_wp) then le(i, j) = 1.0_wp/inv_l else le(i, j) = 0.0_wp end if end do end subroutine meke_length_scales pure subroutine meke_source(nx, ny, bgsrc, gmcoeff, frcoeff, sdt, i_mass, & gm_src, ke_diss, src, meke) !! Aggregate source `src = bgsrc + gmcoeff*I_mass*gm_src !! - frcoeff*I_mass*ke_diss` and the explicit bump `E += sdt*src`. !! `gmcoeff<0` ⇒ GM source off; `frcoeff<0` ⇒ frictional source off. !! `ke_diss` is the lateral-viscosity KE dissipation rate (≤0), so !! `-frcoeff*I_mass*ke_diss ≥ 0` is a mean→eddy source (0 ⇒ inert). integer, intent(in) :: nx, ny real(wp), intent(in) :: bgsrc, gmcoeff, frcoeff, sdt real(wp), intent(in) :: i_mass(nx, ny) real(wp), intent(in) :: gm_src(nx, ny) real(wp), intent(in) :: ke_diss(nx, ny) real(wp), intent(inout) :: src(nx, ny) real(wp), intent(inout) :: meke(nx, ny) integer :: i, j real(wp) :: s do concurrent(j=1:ny, i=1:nx) local(s) s = bgsrc if (gmcoeff >= 0.0_wp) s = s + gmcoeff*i_mass(i, j)*gm_src(i, j) if (frcoeff >= 0.0_wp) s = s - frcoeff*i_mass(i, j)*ke_diss(i, j) src(i, j) = s meke(i, j) = meke(i, j) + sdt*s end do end subroutine meke_source pure subroutine meke_drag(nx, ny, sdt_damp, damping, cdrag, uscale, rho0, & i_mass, bottom_fac2, u_bbl2, meke) !! Implicit (backward-Euler) bottom-drag half-step. !! drag_rate = rho0*i_mass*sqrt(cdrag^2*(max(0,2*bf2*E)+u_bbl2+uscale^2)) [1/s] !! damp_rate = damping + drag_rate*bf2 ; =0 where E<0 !! E <- E/(1 + sdt_damp*damp_rate) !! `rho0*i_mass` = rho0/(Sum_k rho_k*h_k) ~= 1/depth_tot [1/m], so !! drag_rate ~= cdrag*|U_d|/H -- the MOM6 `GV%H_to_RZ * I_mass` factor. !! Without it drag_rate is m^3/(kg*s), not a !! rate. `i_mass=0` on dry columns still gives `drag_rate=0`. !! `u_bbl2` is the resolved bed-layer speed² (MOM6 `drag_rate_visc`); !! it is 0 unless `use_bbl_drag` is set, so the default is bit-identical. integer, intent(in) :: nx, ny real(wp), intent(in) :: sdt_damp, damping, cdrag, uscale, rho0 real(wp), intent(in) :: i_mass(nx, ny) real(wp), intent(in) :: bottom_fac2(nx, ny) real(wp), intent(in) :: u_bbl2(nx, ny) real(wp), intent(inout) :: meke(nx, ny) integer :: i, j real(wp) :: drag_rate, damp_rate, cd2, e cd2 = cdrag*cdrag do concurrent(j=1:ny, i=1:nx) local(drag_rate, damp_rate, e) e = meke(i, j) drag_rate = (rho0*i_mass(i, j))*sqrt(cd2*(max(0.0_wp, 2.0_wp*bottom_fac2(i, j)*e) & + u_bbl2(i, j) + uscale*uscale)) damp_rate = damping + drag_rate*bottom_fac2(i, j) if (e < 0.0_wp) damp_rate = 0.0_wp meke(i, j) = e/(1.0_wp + sdt_damp*damp_rate) end do end subroutine meke_drag pure subroutine meke_bbl_speed2(nx, ny, nz, u_face, v_face, k_bot_u, k_bot_v, u_bbl2) !! Resolved bed-layer speed² at cell centres: !! `u_bbl2 = u_c² + v_c²` with `u_c = ½(u_face(i)+u_face(i+1))`, !! `v_c = ½(v_face(j)+v_face(j+1))`, each face read on ITS OWN bed !! layer `k_bot_u/v` (the first layer live on both sides counting up; !! `1` off `z_fixed` ⇒ the historical k=1 read). Under `z_fixed` the !! layers below are inert fillers whose velocity the closed-face mask !! has zeroed, so a `k = 1` read reported a motionless bed. The !! bottom eddy velocity the MEKE drag law needs (MOM6 `drag_rate_visc`). integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: u_face(nx + 1, ny, nz) real(wp), intent(in) :: v_face(nx, ny + 1, nz) integer, intent(in) :: k_bot_u(nx + 1, ny) integer, intent(in) :: k_bot_v(nx, ny + 1) real(wp), intent(inout) :: u_bbl2(nx, ny) integer :: i, j real(wp) :: u_c, v_c do concurrent(j=1:ny, i=1:nx) local(u_c, v_c) u_c = 0.5_wp*(u_face(i, j, k_bot_u(i, j)) + u_face(i + 1, j, k_bot_u(i + 1, j))) v_c = 0.5_wp*(v_face(i, j, k_bot_v(i, j)) + v_face(i, j + 1, k_bot_v(i, j + 1))) u_bbl2(i, j) = u_c*u_c + v_c*v_c end do end subroutine meke_bbl_speed2 pure subroutine meke_kh_closure(nx, ny, khcoeff, barotr_fac2, meke, le, kh_diff) !! Derived diffusivity `kh = khcoeff*sqrt(2*max(0,gamma_t2*E))*Lmix`. !! `khcoeff<=0` ⇒ kh left at 0. integer, intent(in) :: nx, ny real(wp), intent(in) :: khcoeff real(wp), intent(in) :: barotr_fac2(nx, ny) real(wp), intent(in) :: meke(nx, ny) real(wp), intent(in) :: le(nx, ny) real(wp), intent(out) :: kh_diff(nx, ny) integer :: i, j real(wp) :: ueddy do concurrent(j=1:ny, i=1:nx) local(ueddy) if (khcoeff > 0.0_wp) then ueddy = sqrt(2.0_wp*max(0.0_wp, barotr_fac2(i, j)*meke(i, j))) kh_diff(i, j) = khcoeff*ueddy*le(i, j) else kh_diff(i, j) = 0.0_wp end if end do end subroutine meke_kh_closure pure subroutine meke_ku_closure(nx, ny, backscatter, visc_coeff_ku, & meke, le, ku) !! Derived harmonic backscatter viscosity !! `ku = visc_coeff_ku*sqrt(2*max(0,E))*Lmix` (m²/s), matching MOM6 !! `MEKE%Ku = MEKE_VISCOSITY_COEFF_KU*sqrt(2*MEKE)*Lmix`. Unlike the !! kh closure (which carries the barotropic-mode factor `gamma_t2` !! inside the eddy velocity), MOM6's Ku uses the PLAIN `sqrt(2*MEKE)` — !! no vertical-structure factor — so `gamma_t2` is deliberately absent !! here (vertical structure `BS_struct = 1`; EBT/SQG deferred). !! Off ⇒ ku left at 0 (bit-identical seam). Always ≥ 0; the SIGN of !! the momentum effect is set by the subtraction downstream. integer, intent(in) :: nx, ny logical, intent(in) :: backscatter real(wp), intent(in) :: visc_coeff_ku real(wp), intent(in) :: meke(nx, ny) real(wp), intent(in) :: le(nx, ny) real(wp), intent(out) :: ku(nx, ny) integer :: i, j real(wp) :: ueddy do concurrent(j=1:ny, i=1:nx) local(ueddy) if (backscatter .and. visc_coeff_ku /= 0.0_wp) then ueddy = sqrt(2.0_wp*max(0.0_wp, meke(i, j))) ku(i, j) = visc_coeff_ku*ueddy*le(i, j) else ku(i, j) = 0.0_wp end if end do end subroutine meke_ku_closure pure subroutine meke_backscatter_apply(grid, metrics, this, dt, ah_face_x, ah_face_y) !! Inject the MEKE harmonic backscatter into the per-face resolved !! harmonic viscosity (capability Gap 2, v1). Subtracts a face-average !! of the cell-centred `ku` field from `ah_face_x`/`ah_face_y` so the !! NET coefficient `A_net = A_resolved − Ku` can go NEGATIVE — that !! negative viscosity is the energy return into the momentum tendency !! (the hvisc Laplacian kernel reads these same face fields). !! !! STABILITY: a negative explicit Laplacian viscosity AMPLIFIES !! grid-scale modes — a pure negative Laplacian is unconditionally !! unstable (`g = 1 + |A|·dt·k² > 1`). The floor does NOT by itself !! make the operator stable; it bounds the negative mode's GROWTH !! RATE so a co-present POSITIVE biharmonic (Smag_AH / `nu_4` / !! `leith_biharm`) can dissipate the grid-scale mode the backscatter !! feeds. The net magnitude is floored at the forward-Euler bound !! `|A|·dt·(idx²+idy²) ≤ backscatter_cfl·0.5`, i.e. !! `A_floor = −backscatter_cfl·0.5/(dt·(idx²+idy²))` per face !! (`backscatter_cfl = 0.8` safety; MOM6 `BACKSCATTER_UNDERBOUND` !! analogue). Result: `ah_face <- max(A_resolved − Ku, A_floor)`. !! A biharmonic backstop is therefore MANDATORY when `backscatter` !! is on — enforced fail-loud at configure (`validate_config`). !! !! No-op (bit-identical) when `backscatter` is off or the slot is !! uninitialised. GPU-resident: explicit-shape flat-impl dispatch. !! Run AFTER `ocean_lateral_mix_compute` (which fills `ah_face_*`) and !! BEFORE `ocean_horizontal_viscosity_compute_tendencies` consumes them. !! Uses the PREVIOUS thermo step's `ku` (a one-step lag, like the !! kh→KhTh feedback) — `meke_step` refreshes `ku` once per outer step. !! !! REQUIRES a flow-aware lateral closure (`&ocean_hvisc_nml !! lateral_closure` = `leith`/`smag` ⇒ `LMIX_*`): the hvisc Laplacian !! only reads `ah_face_*` when `closure /= LMIX_NONE`. With the !! default scalar-`nu_h` path the modified faces are never consumed, !! so backscatter has no effect (it is still bit-identical when off). type(hgrid_t), intent(in) :: grid type(ocean_metrics_t), intent(in) :: metrics type(ocean_meke_t), intent(in) :: this real(wp), intent(in) :: dt real(wp), intent(inout) :: ah_face_x(:, :, :) real(wp), intent(inout) :: ah_face_y(:, :, :) integer :: nx, ny, nz if (.not. this%is_init) return if (.not. this%backscatter) return if (.not. allocated(this%ku)) return nx = grid%nx_total ny = grid%ny_total nz = size(ah_face_x, 3) if (this%nx_total /= nx .or. this%ny_total /= ny) return call meke_backscatter_apply_impl(nx, ny, nz, dt, BACKSCATTER_CFL, this%ku, & metrics%idxCu, metrics%idyCu, & metrics%idxCv, metrics%idyCv, & ah_face_x, ah_face_y) end subroutine meke_backscatter_apply pure subroutine meke_backscatter_apply_impl(nx, ny, nz, dt, cfl_safety, ku, & idxCu, idyCu, idxCv, idyCv, & ah_face_x, ah_face_y) !! Flat-impl: subtract the face-averaged `ku` from each per-face !! harmonic viscosity and floor the net at the CFL-stable minimum. !! Explicit-shape dummies for NVHPC stdpar (no descriptor walk). !! The backscatter coefficient is z-independent in v1 (`BS_struct=1`), !! so the cell-centred `ku(i,j)` is broadcast to every layer. integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: dt, cfl_safety real(wp), intent(in) :: ku(nx, ny) real(wp), intent(in) :: idxCu(nx + 1, ny), idyCu(nx + 1, ny) real(wp), intent(in) :: idxCv(nx, ny + 1), idyCv(nx, ny + 1) real(wp), intent(inout) :: ah_face_x(nx + 1, ny, nz), ah_face_y(nx, ny + 1, nz) integer :: i, j, k real(wp) :: ku_face, denom, a_floor, a_net ! u-faces (i-1/2, j): Ku averaged from the two adjacent T-cells ! (i-1, i). Interior faces only; wall faces (i=1, i=nx+1) keep the ! resolved background (no backscatter at the closed boundary, matching ! the lateral-mix wall convention). do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(ku_face, denom, a_floor, a_net) ku_face = 0.5_wp*(ku(i - 1, j) + ku(i, j)) denom = dt*(idxCu(i, j)*idxCu(i, j) + idyCu(i, j)*idyCu(i, j)) a_net = ah_face_x(i, j, k) - ku_face if (denom > 0.0_wp) then a_floor = -cfl_safety*0.5_wp/denom if (a_net < a_floor) a_net = a_floor end if ah_face_x(i, j, k) = a_net end do ! v-faces (i, j-1/2): Ku averaged from the two adjacent T-cells ! (j-1, j). Interior faces only. do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(ku_face, denom, a_floor, a_net) ku_face = 0.5_wp*(ku(i, j - 1) + ku(i, j)) denom = dt*(idxCv(i, j)*idxCv(i, j) + idyCv(i, j)*idyCv(i, j)) a_net = ah_face_y(i, j, k) - ku_face if (denom > 0.0_wp) then a_floor = -cfl_safety*0.5_wp/denom if (a_net < a_floor) a_net = a_floor end if ah_face_y(i, j, k) = a_net end do end subroutine meke_backscatter_apply_impl pure subroutine meke_feed_khth(nx, ny, khth_fac, khtr_fac, kh_diff, & khth_u, khth_v, khtr_u, khtr_v) !! Add the geometric mean of neighbour `kh_diff` into VarMix's per-face !! KhTh (and KhTr) base BEFORE GM's CFL clamp: !! khth_u(i,j) += khth_fac*sqrt(kh(i-1,j)*kh(i,j)) !! `khth_fac=0` ⇒ nothing added ⇒ bit-identical seam. integer, intent(in) :: nx, ny real(wp), intent(in) :: khth_fac, khtr_fac real(wp), intent(in) :: kh_diff(nx, ny) real(wp), intent(inout) :: khth_u(nx + 1, ny) real(wp), intent(inout) :: khth_v(nx, ny + 1) real(wp), intent(inout) :: khtr_u(nx + 1, ny) real(wp), intent(inout) :: khtr_v(nx, ny + 1) integer :: i, j real(wp) :: gm_u, gm_v ! u-faces: interior i=2..nx pairs (i-1,i). do concurrent(j=1:ny, i=2:nx) local(gm_u) gm_u = sqrt(max(0.0_wp, kh_diff(i - 1, j))*max(0.0_wp, kh_diff(i, j))) khth_u(i, j) = khth_u(i, j) + khth_fac*gm_u khtr_u(i, j) = khtr_u(i, j) + khtr_fac*gm_u end do ! v-faces: interior j=2..ny pairs (j-1,j). do concurrent(j=2:ny, i=1:nx) local(gm_v) gm_v = sqrt(max(0.0_wp, kh_diff(i, j - 1))*max(0.0_wp, kh_diff(i, j))) khth_v(i, j) = khth_v(i, j) + khth_fac*gm_v khtr_v(i, j) = khtr_v(i, j) + khtr_fac*gm_v end do end subroutine meke_feed_khth pure subroutine meke_lateral(nx, ny, sdt, kh_bg, k4, khmeke_fac, & kh_flux_enabled, dy_cu, dx_cv, idxCu, idyCv, & iareaT, i_mass, mass, kh_diff, & uflux, vflux, del2, meke) !! Harmonic-mass Laplacian diffusion of MEKE (+ optional biharmonic). !! Flux-form, conservative on a closed domain (interior faces only; !! array-edge faces carry zero flux). !! Kh_u = max(0,kh_bg) + khmeke_fac*0.5*(kh_i+kh_{i+1}), CFL-capped 0.25 !! uflux = Kh_u*(dy_cu*idxCu)*[2 m_i m_{i+1}/(m_i+m_{i+1}+eps)]*(E_i-E_{i+1}) !! E += sdt*iareaT*i_mass*((uflux_{i-1}-uflux_i)+(vflux_{j-1}-vflux_j)) !! Biharmonic: del2 = iareaT*(d uflux' + d vflux') with the bare-gradient !! flux uflux' = (dy_cu*idxCu)*(E_{i+1}-E_i); then a harmonic-mass flux !! of del2 with CFL cap 0.3 and E += that divergence (additive). integer, intent(in) :: nx, ny real(wp), intent(in) :: sdt, kh_bg, k4, khmeke_fac logical, intent(in) :: kh_flux_enabled real(wp), intent(in) :: dy_cu(nx + 1, ny) real(wp), intent(in) :: dx_cv(nx, ny + 1) real(wp), intent(in) :: idxCu(nx + 1, ny) real(wp), intent(in) :: idyCv(nx, ny + 1) real(wp), intent(in) :: iareaT(nx, ny) real(wp), intent(in) :: i_mass(nx, ny) real(wp), intent(in) :: mass(nx, ny) real(wp), intent(in) :: kh_diff(nx, ny) real(wp), intent(inout) :: uflux(nx + 1, ny) real(wp), intent(inout) :: vflux(nx, ny + 1) real(wp), intent(inout) :: del2(nx, ny) real(wp), intent(inout) :: meke(nx, ny) integer :: i, j real(wp) :: kh_u, kh_v, hm, geo, inv_max, k4_u, k4_v, mke ! ---------- Biharmonic (computed first; tendency added after diffusion). ---------- if (k4 >= 0.0_wp) then ! bare-gradient flux into uflux/vflux workspaces (units m^2/s^2). do concurrent(j=1:ny, i=1:nx + 1) uflux(i, j) = 0.0_wp end do do concurrent(j=1:ny, i=2:nx) uflux(i, j) = (dy_cu(i, j)*idxCu(i, j))*(meke(i, j) - meke(i - 1, j)) end do do concurrent(j=1:ny + 1, i=1:nx) vflux(i, j) = 0.0_wp end do do concurrent(j=2:ny, i=1:nx) vflux(i, j) = (dx_cv(i, j)*idyCv(i, j))*(meke(i, j) - meke(i, j - 1)) end do do concurrent(j=1:ny, i=1:nx) del2(i, j) = iareaT(i, j)*((uflux(i + 1, j) - uflux(i, j)) + & (vflux(i, j + 1) - vflux(i, j))) end do ! harmonic-mass flux of del2 with K4 (CFL cap 0.3). do concurrent(j=1:ny, i=1:nx + 1) uflux(i, j) = 0.0_wp end do do concurrent(j=1:ny, i=2:nx) local(k4_u, geo, hm, inv_max) geo = dy_cu(i, j)*idxCu(i, j) inv_max = 64.0_wp*sdt*(geo*max(iareaT(i - 1, j), iareaT(i, j)))**2 k4_u = k4 if (k4_u*inv_max > 0.3_wp) k4_u = 0.3_wp/inv_max hm = 2.0_wp*mass(i - 1, j)*mass(i, j)/((mass(i - 1, j) + mass(i, j)) + MASS_NEGLECT) uflux(i, j) = (k4_u*geo)*hm*(del2(i, j) - del2(i - 1, j)) end do do concurrent(j=1:ny + 1, i=1:nx) vflux(i, j) = 0.0_wp end do do concurrent(j=2:ny, i=1:nx) local(k4_v, geo, hm, inv_max) geo = dx_cv(i, j)*idyCv(i, j) inv_max = 64.0_wp*sdt*(geo*max(iareaT(i, j - 1), iareaT(i, j)))**2 k4_v = k4 if (k4_v*inv_max > 0.3_wp) k4_v = 0.3_wp/inv_max hm = 2.0_wp*mass(i, j - 1)*mass(i, j)/((mass(i, j - 1) + mass(i, j)) + MASS_NEGLECT) vflux(i, j) = (k4_v*geo)*hm*(del2(i, j) - del2(i, j - 1)) end do do concurrent(j=1:ny, i=1:nx) local(mke) mke = sdt*(iareaT(i, j)*i_mass(i, j))* & ((uflux(i, j) - uflux(i + 1, j)) + (vflux(i, j) - vflux(i, j + 1))) del2(i, j) = mke ! stash the biharmonic tendency in del2 end do end if ! ---------- Laplacian (harmonic-mass) diffusion. ---------- if (kh_flux_enabled) then do concurrent(j=1:ny, i=1:nx + 1) uflux(i, j) = 0.0_wp end do do concurrent(j=1:ny, i=2:nx) local(kh_u, geo, hm, inv_max) geo = dy_cu(i, j)*idxCu(i, j) kh_u = max(0.0_wp, kh_bg) + khmeke_fac*0.5_wp*(kh_diff(i - 1, j) + kh_diff(i, j)) inv_max = 2.0_wp*sdt*(geo*max(iareaT(i - 1, j), iareaT(i, j))) if (kh_u*inv_max > 0.25_wp) kh_u = 0.25_wp/inv_max hm = 2.0_wp*mass(i - 1, j)*mass(i, j)/((mass(i - 1, j) + mass(i, j)) + MASS_NEGLECT) uflux(i, j) = (kh_u*geo)*hm*(meke(i - 1, j) - meke(i, j)) end do do concurrent(j=1:ny + 1, i=1:nx) vflux(i, j) = 0.0_wp end do do concurrent(j=2:ny, i=1:nx) local(kh_v, geo, hm, inv_max) geo = dx_cv(i, j)*idyCv(i, j) kh_v = max(0.0_wp, kh_bg) + khmeke_fac*0.5_wp*(kh_diff(i, j - 1) + kh_diff(i, j)) inv_max = 2.0_wp*sdt*(geo*max(iareaT(i, j - 1), iareaT(i, j))) if (kh_v*inv_max > 0.25_wp) kh_v = 0.25_wp/inv_max hm = 2.0_wp*mass(i, j - 1)*mass(i, j)/((mass(i, j - 1) + mass(i, j)) + MASS_NEGLECT) vflux(i, j) = (kh_v*geo)*hm*(meke(i, j - 1) - meke(i, j)) end do do concurrent(j=1:ny, i=1:nx) local(mke) mke = sdt*(iareaT(i, j)*i_mass(i, j))* & ((uflux(i, j) - uflux(i + 1, j)) + (vflux(i, j) - vflux(i, j + 1))) meke(i, j) = meke(i, j) + mke end do end if ! add the biharmonic tendency (computed above, stashed in del2). if (k4 >= 0.0_wp) then do concurrent(j=1:ny, i=1:nx) meke(i, j) = meke(i, j) + del2(i, j) end do end if end subroutine meke_lateral pure function ocean_meke_bytes(this) result(nbytes) !! Counted allocatable footprint of the MEKE slot !! (0 when unallocated). One arr_bytes term per array — add a !! term here when a new allocatable joins the type. class(ocean_meke_t), intent(in) :: this integer(int64) :: nbytes nbytes = arr_bytes(this%meke) & + arr_bytes(this%kh_diff) & + arr_bytes(this%le) & + arr_bytes(this%ku) & + arr_bytes(this%i_mass) & + arr_bytes(this%depth_tot) & + arr_bytes(this%bottom_fac2) & + arr_bytes(this%barotr_fac2) & + arr_bytes(this%src) & + arr_bytes(this%uflux) & + arr_bytes(this%vflux) & + arr_bytes(this%del2) & + arr_bytes(this%mass_ws) & + arr_bytes(this%u_bbl2) & + arr_bytes(this%ke_diss_ws) & + arr_bytes(this%rd_ws) & + arr_bytes(this%f_centre) & + arr_bytes(this%sn_u_ws) & + arr_bytes(this%sn_v_ws) & + arr_bytes(this%baro_hu) & + arr_bytes(this%baro_hv) end function ocean_meke_bytes end module rdb_ocean_meke