Ocean pressure-force kernel state + the layered hydrostatic PGF variants.
!! Ocean pressure-force kernel state + the layered hydrostatic PGF variants. module rdb_ocean_pressure_force !! Carries scheme-variant flags + reusable workspace for the !! layered hydrostatic pressure-force kernels on C-grid face arrays: !! Montgomery potential, finite-volume (lite / Wright / MOM6) and the !! NK=2 reduced-gravity form. !! !! The FV variants integrate pressure per column from the surface !! (p=0) down to the bed, !! p_edge(k) = p_edge(k+1) + g * rho_layer(k) * h_layer(k) !! p_centre(k) = 0.5 * (p_edge(k) + p_edge(k+1)) !! and difference cell-centred pressure across each face *at constant z* !! (pressure difference + `g*rho_face*dz_centre` correction) for the !! acceleration `du/dt = -(1/rho0) * dp/dx`. !! !! The Montgomery variant instead builds the Boussinesq Montgomery !! potential `M = p/rho0 + (g*rho/rho0)*z` — which is CONSTANT within a !! layer of uniform density — by a vertical recursion, and takes ONE !! horizontal difference of `M`. See the OPGF_VARIANT_* constants below. !! !! MONT, FV_LITE and FV_WRIGHT are measured from the FREE SURFACE (z = 0 !! at the surface, negative below), so they carry no barotropic !! `-g*grad(eta)` term: the split-explicit barotropic substep owns that. !! FV_MOM6 (`pa(nz+1) = rho_ref*g*eta_geo`) and GPRIME (`-g_FS*grad(eta)` !! in the top layer) DO carry it; the split sheds exactly that term from !! the barotropic forcing (`pgf_free_surface_gravity`, !! `set_fast_forcing_eta_pf`) and keeps the rest of the depth mean. #ifdef LFORTRAN_PASSING use rdb_constants, only: wp, GRAVITY, H_VANISHED, H_DIV_EPS #else use rdb_constants, only: wp, GRAVITY, H_VANISHED, H_DIV_EPS, NZ_STACK_MAX #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_scratch_3d, only: scratch_3d_buffer_t, & scratch_3d_buffer_enter_data_impl, & scratch_3d_buffer_exit_data_impl use rdb_eos, only: eos_wright_pgf_column_sweep_impl, eos_t, & EOS_VARIANT_WRIGHT_97, EOS_VARIANT_ROQUET_SPV, & WRIGHT_A0, WRIGHT_A1, WRIGHT_A2, & WRIGHT_B0, WRIGHT_B1, WRIGHT_B2, WRIGHT_B3, WRIGHT_B4, WRIGHT_B5, & WRIGHT_C0, WRIGHT_C1, WRIGHT_C2, WRIGHT_C3, WRIGHT_C4, WRIGHT_C5, & ROQ_V00, ROQ_V01, ROQ_V02, ROQ_V03, ROQ_V04, ROQ_V05, & ROQ_CP0, ROQ_CT_SFAC, ROQ_R1_S0, ROQ_RDELTAS, ROQ_SR_FACTOR, & SPV000, SPV001, SPV002, SPV003, SPV010, SPV011, SPV012, SPV013, & SPV020, SPV021, SPV022, SPV030, SPV031, SPV040, SPV041, SPV050, & SPV060, SPV100, SPV101, SPV102, SPV103, SPV110, SPV111, SPV112, & SPV120, SPV121, SPV130, SPV131, SPV140, SPV150, SPV200, SPV201, & SPV202, SPV210, SPV211, SPV220, SPV221, SPV230, SPV240, SPV300, & SPV301, SPV310, SPV311, SPV320, SPV330, SPV400, SPV401, SPV410, & SPV420, SPV500, SPV510, SPV600 ! (The Roquet coefficient table feeds `rdb_roquet_spv.inc`, included in ! `contains` for a module-local, inlinable copy of the SpV value.) use rdb_ocean_pgf_reconstruct, only: boole_dpa_intz_layer, boole_dpa_face, & PGF_RECON_PLM, PGF_RECON_PPM 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_pressure_force_t public :: ocean_pressure_force_compute public :: ocean_pressure_force_apply public :: parse_opgf_variant public :: gprime_nz_is_supported public :: nonoverlap_vanish_tol_for public :: wright_pcm_dpa_intz public :: wright_pcm_dpa_face public :: roquet_pcm_dpa_intz public :: roquet_pcm_dpa_face public :: plm_edges_layer public :: ppm_edges_layer public :: boole_dpa_intz_layer_wright public :: boole_dpa_face_wright public :: roquet_recon_dpa_intz public :: roquet_recon_dpa_face integer, parameter :: N_BOOLE = 5 !! Sub-points of the in-layer Boole (5-point closed Newton-Cotes) rule; !! the same value as `rdb_ocean_pgf_reconstruct`'s. ! Pressure-force variant tags. integer, parameter, public :: OPGF_VARIANT_MONT = 1 !! Boussinesq Montgomery-potential form (cheapest, layered, no EOS !! calls along the integration path, and no pressure stack at all). !! !! For a layer of horizontally uniform density the Boussinesq !! Montgomery potential !! M = p/rho0 + rho_star*z, rho_star = g*rho_layer/rho0 !! is CONSTANT through the layer (moving up by `dz` costs !! `-g*rho*dz/rho0` of `p/rho0` and gains exactly `rho_star*dz`), and !! -(1/rho0) * dp/dx|_z == -dM/dx along the layer. !! That is what makes ONE horizontal difference of `M` legitimate: !! `M` already carries the geopotential. !! !! Built by a vertical recursion, BOTTOM-UP here (k=1 bed, k=nz !! surface — MOM6 runs the same recursion top-down). Seeded at the !! free surface, where the surface-relative interface height and the !! pressure are both zero: !! !! e_edge(k) = z_centre(k) + 0.5*h_layer(k) ! TOP of layer k !! M(nz) = 0 !! M(k) = M(k+1) !! + (rho_star(k) - rho_star(k+1)) * e_edge(k+1) !! !! (`e_edge(k+1)` is the interface SHARED by layers k and k+1, where !! `p` and `z` agree, so the whole jump in `M` is the jump in !! `rho_star`.) Then, per face, !! !! PGF_x = -(M_R - M_L)*idxCu + (rho_star_R - rho_star_L)*z_eff*idxCu !! z_eff = (e_L*h_R + e_R*h_L - h_L*h_R) / (h_L + h_R) !! !! There is NO `1/rho0` multiplying `dM`: `M` already has units of !! geopotential (m^2 s^-2), so its horizontal gradient IS an !! acceleration. The `1/rho0` lives inside `rho_star`. !! !! The second term is load-bearing, not a refinement. `-dM/dx` is !! the PGF only where `rho_layer` is horizontally uniform *within the !! layer*; where it is not, the exact relation picks up !! `+ z * d(rho_star)/dx`, and `z_eff` is the thickness-weighted !! height at which to evaluate it. Drop it and the scheme gets the !! SIGN of a horizontal density contrast wrong. On aligned columns !! (h_L == h_R) `z_eff` collapses to the arithmetic mean layer centre !! and the whole expression reduces ALGEBRAICALLY to FV_LITE. !! !! Exact at rest in an isopycnal (VCOORD_LAGRANGIAN) column over ANY !! bathymetry as long as the layer is present on both sides of the !! face: flat isopycnals make every `e_edge` horizontally uniform and !! every `rho_star` difference zero, so `M` is uniform and the PGF is !! zero to round-off. Where an isopycnal layer has GROUNDED on one !! side the two columns share no common depth for that layer and the !! residual returns — `skip_nonoverlap` gates exactly that face, and !! honours `mont` for the same geometric reason it honours the FV !! variants. integer, parameter, public :: OPGF_VARIANT_FV_LITE = 2 !! Finite-volume PGF with the z-position correction !! PGF_x = -(1/rho_0) * [(p_R - p_L)/dx + g*rho_face*(z_R - z_L)/dx] !! that differences pressure *at constant z* rather than at constant !! layer index. Reduces to MONT for aligned columns; cancels the !! spurious bottom-current PGF when h_layer varies across columns. !! Reuses the layer-mean rho_layer from the EOS (no in-layer !! quadrature — use FV_WRIGHT for that). integer, parameter, public :: OPGF_VARIANT_GPRIME = 4 !! Reduced-gravity / gprime PGF (reduced-gravity analogue). !! Per-layer acceleration !! a_k = -g_FS · ∇η - Σ_{j>k} g'_j · ∇η_j_interface. !! NK = 2 only (asserted at init): !! a_top = -g_FS · ∂(h_1 + h_2)/∂x !! a_bot = -g_FS · ∂(h_1 + h_2)/∂x - g'_int · ∂h_1/∂x !! `pgf%gprime_gfs` is g_FS (m/s²), `pgf%gprime_gint` the single !! internal g'. No EOS, no Picard — densities fixed by GFS/GINT. integer, parameter, public :: OPGF_VARIANT_FV_WRIGHT = 3 !! FV_LITE z-correction *plus* in-situ density along the !! integration path: each layer's centre density is re-evaluated by !! Wright at the pressure from a single Picard step (`ms%rho_layer` !! seeds the half-layer pressure). The pressure stack and `rho_face` !! both use the in-situ value, adding the compressibility piece !! FV_LITE misses; reduces to FV_LITE for incompressible water. !! !! Caller must have run `ocean_eos_compute` with !! `EOS_VARIANT_WRIGHT_97` first (the Picard seed is the nonlinear !! surface ρ, not a Boussinesq constant). integer, parameter, public :: OPGF_VARIANT_FV_MOM6 = 5 !! Faithful layer-integrated FV-Bouss PGF (Boussinesq, per-layer !! `Rlay` density). Unlike FV_LITE (layer-centre pressure with a !! z-correction), uses layer-integrated pressure differences divided !! by face-averaged thickness: !! !! PFu(I,j,k) = [ (pa·h + intz_dpa)_L − (pa·h + intz_dpa)_R !! + (h_R − h_L) · intx_pa !! − (e_bot_R − e_bot_L) · intx_dpa ] !! · (2 · I_Rho0 · IdxCu) / (h_L + h_R + h_neglect) !! !! `pa` is the pressure anomaly stack relative to `rho_ref·g·z`, !! `intz_dpa` the in-layer vertical integral, `intx_pa/intx_dpa` the !! horizontal face integrals. The face-thickness divisor self- !! regulates bed-layer force at thin shelf-break cells (the mechanism !! FV_LITE+rho_init lacked, driving a 33× WBC bed overshoot). !! !! Density per layer from `ms%rho_layer`. `rho_ref` defaults to !! `rho0` so the surface layer's anomaly contribution vanishes. !! per-cell convention: our `k=1` is bed, MOM6's is surf. type :: ocean_pressure_force_t logical :: is_init = .false. !! True between `init` and `destroy`. Prefer this to !! `allocated(...)` — tracks GPU device attachment too. integer :: variant = OPGF_VARIANT_MONT !! Active pressure-force scheme variant. MONT is the default !! here AND the `&ocean_pgf_nml form` default, so a state built !! straight from this type and one built through the config !! agree. MONT is general-purpose: valid over variable !! bathymetry and every vcoord. logical :: use_eos_along_path = .false. !! Convenience flag — re-evaluate density at each integration !! sample rather than at layer centres. Phase 5d. logical :: scratch_gated = .false. !! ALLOCATION GATE for the variant-specific scratch buffers. !! !! `.false.` (default): `init` allocates all 16 buffers, whatever !! `variant` is — the historical behaviour every direct !! `pgf%init(grid, nz_ml=...)` call site (tests, benchmarks) relies !! on, since those set `variant` AFTER `init`. !! !! `.true.`: `init` allocates ONLY the buffers the active !! `variant` / `reconstruct_for_pressure` actually touch (see the !! per-buffer gate comments in `ocean_pressure_force_init`), saving !! up to 11 of the 16 (~3.5 GB at 1000x800x50). Latched together !! with `variant` + `reconstruct_for_pressure` BEFORE `init(grid)` !! by `ocean_state_init_from_config` — the same conditional- !! allocation contract as the default-off closures and !! `continuity_t%windowed_advection`. !! !! Safety contract: every gated-off buffer is unreachable on its !! gated-off path because `ocean_pressure_force_compute` dispatches !! on `variant` at the TOP and `return`s out of each branch, and the !! one external `pgf%e_face` consumer (`compute_pbce`) `error stop`s !! unless `variant == OPGF_VARIANT_FV_MOM6`. `enter_data` / !! `exit_data` / `bytes` all key off `allocated(...)`, so a gated-off !! buffer is neither mapped nor counted. real(wp) :: rho0 = 1035.0_wp !! BOUSSINESQ reference density (kg/m³) — the `ρ₀` that divides the !! pressure gradient into an acceleration, `du/dt = −(1/ρ₀)·∂p/∂x`. !! Read by EVERY variant (it is `inv_rho0` in the face passes and !! `g_over_rho0` in the Montgomery recursion), and by !! `compute_pbce` in the barotropic coupling. !! !! ASSIGNED FROM CONFIG by `configure_ocean_pgf` !! (`&ocean_ic_nml rho_0` → `eos%rho0`, the single ρ₀ of record — !! the EOS, the `eta_ib` surface-pressure seam, EPBL, kappa-shear, !! tidal mixing, MEKE/GM and the isopycnal slopes all take the same !! scalar). The literal here is only the pre-configure default for !! direct `pgf%init(...)` call sites (tests, benchmarks) that never !! run the configure pass; it matches the `&ocean_ic_nml rho_0` !! default so a state built either way agrees. !! !! Host scalar: every read is host-side (into a local `inv_rho0`, !! or passed by value into a `*_impl`), so `configure_ocean_pgf` !! needs no `!$acc update device` — nothing reads it through the !! device-mapped `pgf` handle. ! ---- gprime / reduced-gravity knobs (OPGF_VARIANT_GPRIME) ---- real(wp) :: gprime_gfs = 9.81_wp !! Free-surface reduced gravity (m/s²). Full physical g reduces to !! a standard free-surface model; reducing it slows the BT mode !! (wave speed `sqrt(gfs · H)`) for a slower BT CFL. real(wp) :: gprime_gint = 0.0098_wp !! Internal reduced gravity (m/s²) at the single layer-1/layer-2 !! interface. Only used by `OPGF_VARIANT_GPRIME`. ! ---- FV_MOM6 reference state (OPGF_VARIANT_FV_MOM6) ---- real(wp) :: rho_ref = 1035.0_wp !! ANOMALY reference density (kg/m³) subtracted from layer densities !! when building the `pa` pressure-anomaly stack (FV_MOM6), and the !! surface-layer `g·ρ_ref/ρ₀` in `compute_pbce`. !! `rho_ref = rho0 = ρ_surf` makes the surface layer's anomaly !! vanish; only denser bed layers contribute. !! !! A DISTINCT ROLE from `rho0` — `rho0` scales the gradient into an !! acceleration, `rho_ref` only shifts the baseline the anomaly is !! measured from — kept as a separate member so the two are never !! silently interchanged (MOM6 carries the same pair, `GV%Rho0` vs !! the `PressureForce_FV` `rho_ref`). Both are nonetheless SOURCED !! FROM THE SAME CONFIGURED ρ₀ by `configure_ocean_pgf` !! (`&ocean_ic_nml rho_0` → `eos%rho0`): roundabout has no separate !! anomaly-reference knob, and `rho_ref ≠ rho0` would put a constant !! `g·(ρ_ref−ρ₀)/ρ₀` offset in `pa(top)` that the Boussinesq !! divisor no longer cancels. Same host-scalar note as `rho0` — !! no device update needed. real(wp) :: h_neglect = 1.0e-10_wp !! Face-thickness floor in the FV_MOM6 denominator !! `(h_L + h_R + h_neglect)`. Prevents division by zero when both !! adjacent cells have vanishing bed layers. logical :: skip_nonoverlap = .false. !! Grounded-layer PGF gate (`&ocean_isopycnal_nml pgf_skip_nonoverlap`, !! set by the driver ONLY under VCOORD_LAGRANGIAN). When `.true.` the !! face PGF is zeroed wherever the layer's z-extents in the two !! abutting columns do NOT overlap — a layer that has wedged out !! against the bed on one side, where the two-point Jacobian !! `Δp_centre + g·ρ_layer·Δz_centre` has no common depth to difference !! across and leaves `g·(ρ_layer − ρ̄_ambient)·∂z/∂x` of acceleration on !! a RESTING ocean. `.false.` ⇒ the gate branch is never taken ⇒ !! bit-identical. !! !! Honoured by `mont`, `fv_lite`, `fv_wright` and `fv_mom6`. The gate !! reads `z_centre`; `ocean_pressure_force_init`'s allocation gate !! provides that buffer unconditionally for MONT and the two !! FV_LITE-family variants (all three also use it in the face passes) !! and, for FV_MOM6, exactly when this flag is set — so it MUST be !! latched before `init` (the ocean path does that in !! `ocean_state_init_from_config`, and `configure_ocean_pgf` !! re-checks that the latch did not drift). `gprime` differences !! interface positions directly and has no such Jacobian, so it is !! the one variant left N/A (the driver warns). !! !! `mont` needs the gate for the SAME geometric reason the FV forms !! do, even though its face expression is not a two-point Jacobian: !! a grounded layer sits at the bed on the shallow side and at its !! flat-isopycnal height on the deep side, so the two `e_edge` !! values entering the `M` recursion are hundreds of metres apart !! and `M` stops being horizontally uniform at rest. real(wp) :: nonoverlap_vanish_tol = 2.0_wp*H_VANISHED !! Thickness (m) at or below which a layer counts as GROUNDED for the !! `skip_nonoverlap` gate: a face is zeroed only where the layer's !! z-extents do not overlap AND the layer is this thin on at least !! one side. Non-overlap alone is not grounding — a layer that is !! MASSIVE on both sides but sits at different depths (a !! sigma-seeded stack over a bathymetric step, where a 10 m layer at !! 90-100 m faces a 25 m layer at 225-250 m) is a steep coordinate !! surface the FV forms are built for, exactly as under !! `vcoord_type="sigma"`. Zeroing its PGF while continuity keeps !! moving its mass across the face breaks the PGF-work / PE !! exchange and grows energy without bound (the compat-matrix !! staircase, MaxCFL panic at step 178). Set by the driver from !! `nonoverlap_vanish_tol_for(angstrom_h)`; the default matches !! `angstrom_h = 0`. logical :: mass_weight = .false. !! FV_MOM6 shelf-break `hWght` mass-weighting toggle. When `.true.` !! Pass-2's horizontal pressure integral biases the face density !! toward the thinner column at unequal-depth faces, cancelling the !! spurious bottom-layer shelf-break PGF. Equal-depth columns ⇒ !! reduces exactly to the midpoint average ⇒ bit-identical. logical :: reconstruct_for_pressure = .false. !! FV_MOM6 in-layer T/S reconstruction toggle. `.false.` (default): !! layer-mean (PCM) density ⇒ bit-identical. `.true.`: per-layer !! `dpa` / `intz_dpa` from a 5-point Boole quadrature of a monotone !! PLM/PPM sub-layer T/S profile (Adcroft, Hallberg & Harrison 2008; !! White, Adcroft & Hallberg 2009), removing the spurious-PGF error !! on thick/sloped layers. Only consulted by FV_MOM6. integer :: recon_scheme = PGF_RECON_PLM !! In-layer reconstruction scheme: 1 = PLM, 2 = PPM. Only consulted !! when `reconstruct_for_pressure = .true.`. logical :: insitu_density = .true. !! FV_MOM6 constant-by-layer (PCM) density at its IN-SITU pressure !! (`&ocean_pgf_nml insitu_density`, MOM6 parity). `.true.` !! (default): each layer's `dpa`/`intz_dpa` and the cross-face !! `intx_dpa`/`inty_dpa` integrate `EOS(T, S, p = -g·rho0·z)` with !! the layer-mean T/S (`compute_fv_mom6_insitu_pcm_impl`). Under !! Wright the vertical integral is ANALYTIC (MOM6 !! `int_density_dz_wright`: one polynomial evaluation per layer and !! per lateral sub-column); under Roquet the 5-point Boole rule of !! MOM6 `int_density_dz_generic_pcm` with the (T, S) part of the !! EOS hoisted out of the pressure points (`roquet_pcm_dpa_intz`: 1 !! T/S polynomial + 5 pressure Horners per layer, 3 + 15 per face). !! `.false.`: the legacy PCM integral of `ms%rho_layer`, a !! POTENTIAL density at the single `&ocean_eos_nml p_ref`, which !! drops the pressure dependence of the horizontal density !! gradient below the reference level. Consulted only by FV_MOM6 !! with `reconstruct_for_pressure = .false.`, an EOS handle and !! T/S, and only for a PRESSURE-DEPENDENT EOS (Wright, Roquet): !! for the linear EOS in-situ and potential density coincide, so !! the legacy path runs and answers are bit-identical. logical :: p_top_in_bc = .false. !! FV_MOM6 top-of-column pressure in the surface boundary !! condition (`&ocean_pgf_nml p_top_in_bc`). `.false.` (default): !! `pa(nz+1) = rho_ref·g·eta_geo`, bit-identical. `.true.`: the !! load `multilayer_state_t%p_top` (Pa) is ADDED there, so the !! pressure stack measures down from the loaded surface — !! `pa(nz+1) = rho_ref·g·eta_geo + p_top`. Only consulted by !! FV_MOM6 (both the PCM and the `reconstruct_for_pressure` !! branch); fail-loud at configure for any other variant, which !! carries no injectable `pa` stack. !! !! A DEPTH-UNIFORM `p_top` perturbs every layer's `PFu` by the !! SAME `−(1/ρ₀)∇p_top` (Theorem 1 in the Pass-1 docstring !! below), and the split solver replaces the depth mean of the !! layer PGF with the barotropic solution, so the baroclinic !! operator does not see it and this does NOT double-count the !! `eta_forcing` seam. See the `p_top` seam contract in !! `src/core/ocean/README.md`. real(wp) :: gfs_scale = 1.0_wp !! Free-surface gravity scaling (= GFS / G_EARTH). Default 1.0 ⇒ !! pure FV_MOM6, bit-identical. When < 1, Pass 5 applies the !! Montgomery `dM` correction subtracting !! `(1 - gfs_scale)·(g/ρ₀)·ρ_surf·∇η` from every layer's PGF !! (depth-independent, so the BT mass-flux invariant survives); the !! driver also drops `bt_work%g_bt` to `gfs_scale·GRAVITY`. !! Combined ⇒ wave speed `sqrt(gfs_scale · g · H)`. ! ---- Bathymetry (copy of state%barotropic%b) ---- ! Used by the gprime PGF to recover ∇η from ∇(sum h_layer) - ∇b; ! without it variable-bathymetry runs see a spurious PGF dominated ! by ∇H_bathy (~10⁴× larger than ∇η at shelf-breaks). Default zero = ! flat bed. Set by the driver via `set_bathymetry()`. real(wp), allocatable :: b(:, :) ! ---- Workspace ---- type(scratch_3d_buffer_t) :: p_edge !! Layer-edge pressure stack at cell centres. Shape !! (nx, ny, nz_ml+1). k=1 bed, k=nz_ml+1 surface. !! Reused across the two horizontal-gradient passes. type(scratch_3d_buffer_t) :: z_centre !! Layer-centre z at cell centres, from the free surface downward !! (z=0 surface, negative below). Shape (nx, ny, nz_ml). Filled for !! MONT and the FV_LITE-family branches. Surface-relative (NOT !! bed-relative) so the σ-coord Jacobian cancels the spurious !! cross-bathymetry pressure gradient — and so that neither form !! double-counts the barotropic `-g·∇η` the BT substep already !! carries. MONT reads it as the interface height !! `e_edge(k) = z_centre(k) + 0.5·h_layer(k)`. type(scratch_3d_buffer_t) :: mont_M !! Boussinesq Montgomery potential `M` at layer centres (m² s⁻²). !! Shape (nx, ny, nz_ml). Written by the MONT column recursion and !! read by its two face passes; no other variant touches it. type(scratch_3d_buffer_t) :: rho_insitu !! In-situ layer-centre density from the Wright !! column-sweep Picard step. Shape (nx, ny, nz_ml). !! Populated only by the FV_WRIGHT branch; the other !! variants source ρ from `ms%rho_layer`. type(scratch_3d_buffer_t) :: dpdx_face !! East-face PGF acceleration: -(1/rho0) * dp/dx at !! u-face. Shape (nx+1, ny, nz_ml). Apply adds dt*dpdx !! to u_face_x_layer. type(scratch_3d_buffer_t) :: dpdy_face !! North-face PGF acceleration. Shape (nx, ny+1, nz_ml). ! ---- FV_MOM6 scratch (OPGF_VARIANT_FV_MOM6) ---- !! Interface heights `e` (positive-up). `e(:,:,1)` is the bed !! (= -b), `e(:,:,nz+1)` is the free surface (= η). Same !! sign convention as MOM6, but bottom-up indexing to match the !! Roundabout bed-up layer convention. type(scratch_3d_buffer_t) :: e_face !! Interface heights at cell centres, shape (nx, ny, nz+1). !! Pressure anomaly stack at interfaces, units Pa. Relative to !! the `rho_ref · g · z` baseline so the surface-pressure !! contribution is just `pa(top) = rho_ref · g · η`. Built by !! marching down from the surface; `pa(k) − pa(k+1) = dpa(k)` !! where `dpa(k) = (Rlay(k) − rho_ref) · g · h(k)`. type(scratch_3d_buffer_t) :: pa !! Pressure anomaly at interfaces, shape (nx, ny, nz+1). !! Per-layer vertical integral of `dpa` from layer top inward. !! For Boussinesq Rlay path: `intz_dpa(k) = 0.5 · (Rlay(k) − !! rho_ref) · g · h(k)²` (mid-point rule). type(scratch_3d_buffer_t) :: intz_dpa !! Per-layer ∫ dpa dz, shape (nx, ny, nz), units Pa·m. ! Horizontal integrals at faces — average of the two adjacent ! cells. `intx_pa(K) = 0.5·(pa_L + pa_R)` at interface K; ! `intx_dpa(k) = 0.5·(Rlay(k) − rho_ref) · g · (h_L + h_R)` ! within layer k. Surface BC fixes intx_pa at top; deeper ! values come from `intx_pa(K+1) = intx_pa(K) + intx_dpa(k)`. type(scratch_3d_buffer_t) :: intx_pa !! u-face ∫ pa across x, shape (nx+1, ny, nz+1), units Pa·m. type(scratch_3d_buffer_t) :: inty_pa !! v-face ∫ pa across y, shape (nx, ny+1, nz+1), units Pa·m. type(scratch_3d_buffer_t) :: intx_dpa !! u-face ∫ dpa, shape (nx+1, ny, nz), units Pa·m. type(scratch_3d_buffer_t) :: inty_dpa !! v-face ∫ dpa, shape (nx, ny+1, nz), units Pa·m. type(scratch_3d_buffer_t) :: conc_T !! Layer-mean temperature the in-situ PCM and reconstruction !! kernels read, shape (nx, ny, nz): `hT/h` on a live layer, the !! I1′ donor's on a vanished one (Pass C of both kernels). Filled !! once per call by a per-column pass, so no later pass divides by !! a thickness or walks a column. Unused by the `rho_layer` path. type(scratch_3d_buffer_t) :: conc_S !! Layer-mean salinity, as `conc_T`. ! ---- In-layer reconstruction scratch (reconstruct_for_pressure) ---- !! Per-column PLM/PPM top (shallower) and bottom (deeper) edge !! values of the layer-mean T and S, shape (nx, ny, nz). Filled !! by `compute_fv_mom6_reconstruct_impl`'s edge-build pass; consumed !! by the 5-point Boole quadrature. Allocated only when !! `reconstruct_for_pressure` (or `scratch_gated = .false.`). type(scratch_3d_buffer_t) :: recon_T_t type(scratch_3d_buffer_t) :: recon_T_b type(scratch_3d_buffer_t) :: recon_S_t type(scratch_3d_buffer_t) :: recon_S_b contains procedure, non_overridable :: init => ocean_pressure_force_init procedure, non_overridable :: destroy => ocean_pressure_force_destroy procedure, non_overridable :: enter_data => ocean_pressure_force_enter_data procedure, non_overridable :: exit_data => ocean_pressure_force_exit_data procedure, non_overridable :: set_bathymetry => ocean_pressure_force_set_bathymetry procedure, non_overridable :: bytes => ocean_pressure_force_bytes end type ocean_pressure_force_t contains pure function nonoverlap_vanish_tol_for(angstrom_h) result(tol) !! The grounded-layer gate's "vanished on one side" threshold, !! `2·max(angstrom_h, H_VANISHED)`: a layer within a factor two of the !! floor it can rest on. The factor is the margin that keeps every !! floor the Lagrangian path parks a grounded layer on inside the band !! — the `uniform_z` seed's `max(angstrom_h, 2·H_VANISHED)` collapse, !! the conservative-floor borrow's `angstrom_h` (to round-off), and the !! positive-definite continuity's `[angstrom_h, angstrom_h + H_VANISHED]` !! at-floor band — while staying orders of magnitude below any layer !! that carries real mass. real(wp), intent(in) :: angstrom_h !! `&ocean_isopycnal_nml angstrom_h` (m); 0 when the floor is off. real(wp) :: tol tol = 2.0_wp*max(angstrom_h, H_VANISHED) end function nonoverlap_vanish_tol_for pure function gprime_nz_is_supported(variant, nz) result(ok) !! `.true.` unless `variant == OPGF_VARIANT_GPRIME` with `nz /= 2` !! (PR-6 fail-loud). The reduced-gravity gprime PGF hard-writes !! ONLY k=1 (bottom) and k=2 (top) — with `nz > 2` layers k=3..nz !! carry zero pressure gradient AND the "top" branch lands on layer !! 2 of nz (the abyss under the bottom-up convention); with `nz < 2` !! the kernel early-returns leaving the whole PGF zero. The guard !! is gprime-specific — every other variant supports general nz, so !! this returns `.true.` for them regardless of `nz`. Wired into !! `validate_config` against `cfg%nz_layers` (config-time). integer, intent(in) :: variant, nz logical :: ok ok = (variant /= OPGF_VARIANT_GPRIME) .or. (nz == 2) end function gprime_nz_is_supported subroutine ocean_pressure_force_init(this, grid, nz_ml) !! Allocate the scratch buffers. Default nz_ml=1 keeps the !! barotropic-only path constructible; passing nz_ml sizes them !! for the multilayer kernel. !! !! ALLOCATION GATE (`this%scratch_gated`, see the type docstring): !! when `.true.` only the buffers the active `variant` / !! `reconstruct_for_pressure` can actually reach are allocated. The !! gate defaults `.false.` so a bare `pgf%init(...)` (every direct !! test/benchmark call site) keeps the historical allocate-everything !! behaviour. Each gate's unreachability proof is stated inline. class(ocean_pressure_force_t), intent(inout) :: this type(hgrid_t), intent(in) :: grid integer, intent(in), optional :: nz_ml integer :: nx, ny, nz logical :: gate, need_p_edge, need_z_centre, need_rho_insitu logical :: need_mont_M, need_fv_mom6, need_recon nx = grid%nx_total ny = grid%ny_total nz = 1 if (present(nz_ml)) nz = nz_ml gate = this%scratch_gated ! `p_edge`: filled by Pass 1 and read by Pass 2/3. The MONT, GPRIME and ! FV_MOM6 branches of `ocean_pressure_force_compute` `return` or branch ! away BEFORE Pass 1, so none of them ever touches it. MONT builds the ! Montgomery potential straight from `rho_layer` + interface heights and ! needs no pressure stack at all. No other module references it. need_p_edge = .not. gate .or. & (this%variant == OPGF_VARIANT_FV_LITE .or. & this%variant == OPGF_VARIANT_FV_WRIGHT) ! `z_centre`: written by Pass 1b and read by the MONT / FV_LITE / ! FV_WRIGHT face passes — every one of those sites sits inside a branch ! testing for exactly those three variants. The Pass-4 grounded-layer ! gate reads it too, and that pass runs for FV_MOM6 as well — but ONLY ! when `skip_nonoverlap` is on, which is a pure config-time decision ! (vcoord + namelist) that the caller latches before `init`. So FV_MOM6 ! gets the buffer exactly when something will read it, and an ungated ! FV_MOM6 run still pays nothing. No other module references it. need_z_centre = .not. gate .or. & (this%variant == OPGF_VARIANT_MONT .or. & this%variant == OPGF_VARIANT_FV_LITE .or. & this%variant == OPGF_VARIANT_FV_WRIGHT .or. & (this%variant == OPGF_VARIANT_FV_MOM6 .and. & this%skip_nonoverlap)) ! `mont_M`: written and read by the MONT branch alone. No other ! variant, and no other module, references it. need_mont_M = .not. gate .or. (this%variant == OPGF_VARIANT_MONT) ! `rho_insitu`: written by the Pass-1 FV_WRIGHT branch (both the EOS ! column sweep and its no-tracer fallback) and read by the Pass 2/3 ! FV_WRIGHT branches only. No other module references it. need_rho_insitu = .not. gate .or. (this%variant == OPGF_VARIANT_FV_WRIGHT) ! FV_MOM6 stack: passed only to `compute_fv_mom6_impl` / ! `compute_fv_mom6_reconstruct_impl`, both inside the ! `if (variant == FV_MOM6)` branch that `return`s. The one external ! reader, `compute_pbce` (rdb_barotropic_coupling), opens with an ! `error stop` unless `variant == OPGF_VARIANT_FV_MOM6`. need_fv_mom6 = .not. gate .or. (this%variant == OPGF_VARIANT_FV_MOM6) ! Reconstruction edge scratch: passed only to ! `compute_fv_mom6_reconstruct_impl`, whose call site additionally ! requires `reconstruct_for_pressure` (configure_ocean_pgf `error ! stop`s if that knob is set with any variant other than FV_MOM6). need_recon = .not. gate .or. this%reconstruct_for_pressure ! Cell-centred edge stack: (nx, ny, nz+1) if (need_p_edge) call this%p_edge%init(nx, ny, nz + 1, "ocean_pgf_p_edge") ! Layer-centre z-coordinate: (nx, ny, nz) if (need_z_centre) call this%z_centre%init(nx, ny, nz, "ocean_pgf_z_centre") ! Montgomery potential at layer centres: (nx, ny, nz) if (need_mont_M) call this%mont_M%init(nx, ny, nz, "ocean_pgf_mont_M") ! In-situ density at layer centres: (nx, ny, nz) if (need_rho_insitu) call this%rho_insitu%init(nx, ny, nz, "ocean_pgf_rho_insitu") ! East-face: (nx+1, ny, nz) — same shape as u_face_x_layer. Every ! variant writes these (they ARE the PGF output), so never gated. call this%dpdx_face%init(nx + 1, ny, nz, "ocean_pgf_dpdx_face") ! North-face: (nx, ny+1, nz) call this%dpdy_face%init(nx, ny + 1, nz, "ocean_pgf_dpdy_face") ! FV_MOM6 scratch (interface heights, pa stack, per-layer ! integrals, per-face horizontal integrals). if (need_fv_mom6) then call this%e_face%init(nx, ny, nz + 1, "ocean_pgf_fv_mom6_e_face") call this%pa%init(nx, ny, nz + 1, "ocean_pgf_fv_mom6_pa") call this%intz_dpa%init(nx, ny, nz, "ocean_pgf_fv_mom6_intz_dpa") call this%intx_pa%init(nx + 1, ny, nz + 1, "ocean_pgf_fv_mom6_intx_pa") call this%inty_pa%init(nx, ny + 1, nz + 1, "ocean_pgf_fv_mom6_inty_pa") call this%intx_dpa%init(nx + 1, ny, nz, "ocean_pgf_fv_mom6_intx_dpa") call this%inty_dpa%init(nx, ny + 1, nz, "ocean_pgf_fv_mom6_inty_dpa") call this%conc_T%init(nx, ny, nz, "ocean_pgf_fv_mom6_conc_T") call this%conc_S%init(nx, ny, nz, "ocean_pgf_fv_mom6_conc_S") end if ! In-layer reconstruction edge scratch (nx, ny, nz). if (need_recon) then call this%recon_T_t%init(nx, ny, nz, "ocean_pgf_recon_T_t") call this%recon_T_b%init(nx, ny, nz, "ocean_pgf_recon_T_b") call this%recon_S_t%init(nx, ny, nz, "ocean_pgf_recon_S_t") call this%recon_S_b%init(nx, ny, nz, "ocean_pgf_recon_S_b") end if ! Bathymetry copy. Default zero = flat bed. Driver overwrites ! via `set_bathymetry` after `state%barotropic%b` is populated. allocate (this%b(nx, ny), source=0.0_wp) this%is_init = .true. end subroutine ocean_pressure_force_init subroutine ocean_pressure_force_destroy(this) class(ocean_pressure_force_t), intent(inout) :: this this%is_init = .false. call this%p_edge%destroy() call this%z_centre%destroy() call this%mont_M%destroy() call this%rho_insitu%destroy() call this%dpdx_face%destroy() call this%dpdy_face%destroy() call this%e_face%destroy() call this%pa%destroy() call this%intz_dpa%destroy() call this%intx_pa%destroy() call this%inty_pa%destroy() call this%intx_dpa%destroy() call this%inty_dpa%destroy() call this%conc_T%destroy() call this%conc_S%destroy() call this%recon_T_t%destroy() call this%recon_T_b%destroy() call this%recon_S_t%destroy() call this%recon_S_b%destroy() if (allocated(this%b)) deallocate (this%b) end subroutine ocean_pressure_force_destroy subroutine ocean_pressure_force_set_bathymetry(this, b) !! Copy `b(:, :)` into `this%b` on the host. Must be called BEFORE !! `enter_data` (the device copy is taken from the host values). !! Issues no `update device`; to refresh post `enter_data` the !! caller must issue `!$acc update device(this%b)` itself. class(ocean_pressure_force_t), intent(inout) :: this real(wp), intent(in) :: b(:, :) integer :: nx, ny, i, j nx = size(this%b, 1) ny = size(this%b, 2) if (size(b, 1) /= nx .or. size(b, 2) /= ny) then error stop "ocean_pressure_force_set_bathymetry: shape mismatch" end if do j = 1, ny do i = 1, nx this%b(i, j) = b(i, j) end do end do end subroutine ocean_pressure_force_set_bathymetry subroutine ocean_pressure_force_enter_data(this) class(ocean_pressure_force_t), intent(inout) :: this select type (this) type is (ocean_pressure_force_t) call ocean_pressure_force_enter_data_impl(this) end select end subroutine ocean_pressure_force_enter_data subroutine ocean_pressure_force_enter_data_impl(this) type(ocean_pressure_force_t), intent(inout) :: this call scratch_3d_buffer_enter_data_impl(this%p_edge) call scratch_3d_buffer_enter_data_impl(this%z_centre) call scratch_3d_buffer_enter_data_impl(this%mont_M) call scratch_3d_buffer_enter_data_impl(this%rho_insitu) call scratch_3d_buffer_enter_data_impl(this%dpdx_face) call scratch_3d_buffer_enter_data_impl(this%dpdy_face) call scratch_3d_buffer_enter_data_impl(this%e_face) call scratch_3d_buffer_enter_data_impl(this%pa) call scratch_3d_buffer_enter_data_impl(this%intz_dpa) call scratch_3d_buffer_enter_data_impl(this%intx_pa) call scratch_3d_buffer_enter_data_impl(this%inty_pa) call scratch_3d_buffer_enter_data_impl(this%intx_dpa) call scratch_3d_buffer_enter_data_impl(this%inty_dpa) call scratch_3d_buffer_enter_data_impl(this%conc_T) call scratch_3d_buffer_enter_data_impl(this%conc_S) call scratch_3d_buffer_enter_data_impl(this%recon_T_t) call scratch_3d_buffer_enter_data_impl(this%recon_T_b) call scratch_3d_buffer_enter_data_impl(this%recon_S_t) call scratch_3d_buffer_enter_data_impl(this%recon_S_b) !$acc enter data copyin(this%b) end subroutine ocean_pressure_force_enter_data_impl subroutine ocean_pressure_force_exit_data(this) class(ocean_pressure_force_t), intent(inout) :: this select type (this) type is (ocean_pressure_force_t) call ocean_pressure_force_exit_data_impl(this) end select end subroutine ocean_pressure_force_exit_data subroutine ocean_pressure_force_exit_data_impl(this) type(ocean_pressure_force_t), intent(inout) :: this call scratch_3d_buffer_exit_data_impl(this%p_edge) call scratch_3d_buffer_exit_data_impl(this%z_centre) call scratch_3d_buffer_exit_data_impl(this%mont_M) call scratch_3d_buffer_exit_data_impl(this%rho_insitu) call scratch_3d_buffer_exit_data_impl(this%dpdx_face) call scratch_3d_buffer_exit_data_impl(this%dpdy_face) call scratch_3d_buffer_exit_data_impl(this%e_face) call scratch_3d_buffer_exit_data_impl(this%pa) call scratch_3d_buffer_exit_data_impl(this%intz_dpa) call scratch_3d_buffer_exit_data_impl(this%intx_pa) call scratch_3d_buffer_exit_data_impl(this%inty_pa) call scratch_3d_buffer_exit_data_impl(this%intx_dpa) call scratch_3d_buffer_exit_data_impl(this%inty_dpa) call scratch_3d_buffer_exit_data_impl(this%conc_T) call scratch_3d_buffer_exit_data_impl(this%conc_S) call scratch_3d_buffer_exit_data_impl(this%recon_T_t) call scratch_3d_buffer_exit_data_impl(this%recon_T_b) call scratch_3d_buffer_exit_data_impl(this%recon_S_t) call scratch_3d_buffer_exit_data_impl(this%recon_S_b) !$acc exit data delete(this%b) end subroutine ocean_pressure_force_exit_data_impl pure subroutine ocean_pressure_force_compute(grid, metrics, pgf, ms, eos) !! Compute the hydrostatic pressure-gradient acceleration at every !! C-grid face. Variants (see the OPGF_VARIANT_* / GPRIME / FV_MOM6 !! constants for the per-variant formulas): MONT (layer-mean ρgh), !! FV_LITE (+ z-correction), FV_WRIGHT (+ in-situ Wright density), !! GPRIME, FV_MOM6. !! !! Pass layout: (1b) z_centre from h_layer (MONT + FV variants); !! then either (M1) the Montgomery column recursion and (M2/M3) its !! face passes, or (1) the column pressure sweep filling p_edge !! (+ rho_insitu for FV_WRIGHT) and (2/3) the FV east/north-face !! acceleration; finally (4) the grounded-layer gate. !! !! `ms%rho_layer` must be up to date — call the EOS kernel first. !! FV_WRIGHT needs `EOS_VARIANT_WRIGHT_97` for a good Picard seed. type(hgrid_t), intent(in) :: grid type(ocean_metrics_t), intent(in) :: metrics !! Curvilinear horizontal metrics. The u-face gradient divides by !! `idxCu(i,j)`, the v-face by `idyCv(i,j)`. On uniform Cartesian !! `idxCu == 1/dx` bitwise (byte-identical to scalar inv_dx/inv_dy). type(ocean_pressure_force_t), intent(inout) :: pgf type(multilayer_state_t), intent(in) :: ms type(eos_t), intent(in), optional :: eos !! EOS handle — REQUIRED when `pgf%reconstruct_for_pressure` is !! on (the in-layer Boole quadrature evaluates the EOS at each !! sub-point). Optional so the legacy PCM call sites (and the !! non-reconstruct variant tests) need not thread it through. integer :: i, j, k, nx, ny, nz real(wp) :: inv_rho0, g_over_rho0 real(wp) :: p_centre_left, p_centre_right real(wp) :: p_centre_below, p_centre_above real(wp) :: rho_face, z_correction, z_running real(wp) :: h_l, h_r, e_l, e_r, z_eff, drho_star real(wp) :: vtol nx = grid%nx_total ny = grid%ny_total nz = ms%nz_ml inv_rho0 = 1.0_wp/pgf%rho0 ! ---- gprime / reduced-gravity branch (Tier-1: NK = 2 only) ---- if (pgf%variant == OPGF_VARIANT_GPRIME) then call compute_gprime_impl(ms%h_layer, pgf%b, & pgf%dpdx_face%data, pgf%dpdy_face%data, & pgf%gprime_gfs, pgf%gprime_gint, & metrics%idxCu, metrics%idyCv, nx, ny, nz) return end if ! ---- Pass 1b: per-column z_centre from h_layer ---- ! z_centre(:, :, k) = physical z of the layer-k mid-depth, ! measured from the free surface downward (z=0 at surface, ! z negative below). Computed by summing h_layer top-down so ! the surface is the reference point regardless of column ! bathymetry. This is the load-bearing change that makes the ! FV_LITE / FV_WRIGHT Jacobian cancel the cross-bathymetry ! pressure gradient: a bed-relative z_centre would inject a ! spurious `-g·ρ·dH/dx` residual at every shelf-break face. ! ! Hoisted AHEAD of the variant branches because it depends on nothing ! but `h_layer`. FV_LITE / FV_WRIGHT consume it in Passes 2/3; MONT ! recovers the layer-TOP interface height `z_centre + h/2` from it for ! both the `M` recursion and the face `z_eff`. FV_MOM6's own face ! assembly never touches it, so on that path it is filled ONLY to feed ! the Pass-4 grounded-layer gate — which is also exactly when ! `ocean_pressure_force_init`'s allocation gate provides the buffer. if (pgf%variant == OPGF_VARIANT_MONT .or. & pgf%variant == OPGF_VARIANT_FV_LITE .or. & pgf%variant == OPGF_VARIANT_FV_WRIGHT .or. & (pgf%variant == OPGF_VARIANT_FV_MOM6 .and. pgf%skip_nonoverlap)) then do concurrent(j=1:ny, i=1:nx) local(z_running) z_running = 0.0_wp do k = nz, 1, -1 pgf%z_centre%data(i, j, k) = z_running - 0.5_wp*ms%h_layer(i, j, k) z_running = z_running - ms%h_layer(i, j, k) end do end do end if ! ---- MONT branch — Boussinesq Montgomery potential ---------------- ! Pass M1 (per column): the vertical recursion that builds `M`. Pass ! M2/M3 (per face): ONE horizontal difference of `M`, plus the ! horizontal-density term. No pressure stack is built on this path. ! See the OPGF_VARIANT_MONT docstring for the derivation. if (pgf%variant == OPGF_VARIANT_MONT) then g_over_rho0 = GRAVITY*inv_rho0 ! ---- Pass M1: Montgomery potential per column ------------------- ! Seeded at the free surface: `e_edge(nz+1) = 0` (z_centre is ! surface-relative) and the surface pressure is zero, so ! `M(nz) = p/rho0 + rho_star(nz)*e_edge(nz+1)` is identically zero ! in EVERY column. That is not a loss: the barotropic `-g*grad(eta)` ! it would otherwise carry is the BT substep's job, and adding it ! here would double-count it. Accumulated straight into the array ! (no `local` reassigned in the k-loop), mirroring the Pass-1 ! pressure sweep. do concurrent(j=1:ny, i=1:nx) pgf%mont_M%data(i, j, nz) = 0.0_wp do k = nz - 1, 1, -1 ! `e_edge(k+1)` — the interface SHARED by layers k and k+1, ! i.e. the TOP of layer k — is where `p` and `z` agree between ! the two layers, so the whole jump in M is the jump in ! rho_star. Recovered from the layer-k centre. pgf%mont_M%data(i, j, k) = pgf%mont_M%data(i, j, k + 1) + & g_over_rho0*(ms%rho_layer(i, j, k) - & ms%rho_layer(i, j, k + 1))* & (pgf%z_centre%data(i, j, k) + & 0.5_wp*ms%h_layer(i, j, k)) end do end do ! ---- Pass M2: east-face acceleration ---------------------------- ! `-dM/dx` plus the horizontal-density term `+ z_eff * d(rho_star)/dx`. ! `z_eff` is the thickness-weighted height at which `M`'s two ! column anchors are reconciled; on aligned columns (h_L == h_R) it ! is exactly the mean layer centre and the pair reduces ! ALGEBRAICALLY to FV_LITE. `H_DIV_EPS` is pure 1/0 armour for a ! face between two fully-vanished layers (numerator is then zero ! too, so the face value is a clean zero). do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(h_l, h_r, e_l, e_r, z_eff, drho_star) h_l = ms%h_layer(i - 1, j, k) h_r = ms%h_layer(i, j, k) e_l = pgf%z_centre%data(i - 1, j, k) + 0.5_wp*h_l e_r = pgf%z_centre%data(i, j, k) + 0.5_wp*h_r z_eff = (e_l*h_r + e_r*h_l - h_l*h_r)/(h_l + h_r + H_DIV_EPS) drho_star = g_over_rho0*(ms%rho_layer(i, j, k) - ms%rho_layer(i - 1, j, k)) pgf%dpdx_face%data(i, j, k) = & (-(pgf%mont_M%data(i, j, k) - pgf%mont_M%data(i - 1, j, k)) & + drho_star*z_eff)*metrics%idxCu(i, j) end do do concurrent(k=1:nz, j=1:ny) pgf%dpdx_face%data(1, j, k) = 0.0_wp pgf%dpdx_face%data(nx + 1, j, k) = 0.0_wp end do ! ---- Pass M3: north-face acceleration --------------------------- do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(h_l, h_r, e_l, e_r, z_eff, drho_star) h_l = ms%h_layer(i, j - 1, k) h_r = ms%h_layer(i, j, k) e_l = pgf%z_centre%data(i, j - 1, k) + 0.5_wp*h_l e_r = pgf%z_centre%data(i, j, k) + 0.5_wp*h_r z_eff = (e_l*h_r + e_r*h_l - h_l*h_r)/(h_l + h_r + H_DIV_EPS) drho_star = g_over_rho0*(ms%rho_layer(i, j, k) - ms%rho_layer(i, j - 1, k)) pgf%dpdy_face%data(i, j, k) = & (-(pgf%mont_M%data(i, j, k) - pgf%mont_M%data(i, j - 1, k)) & + drho_star*z_eff)*metrics%idyCv(i, j) end do do concurrent(k=1:nz, i=1:nx) pgf%dpdy_face%data(i, 1, k) = 0.0_wp pgf%dpdy_face%data(i, ny + 1, k) = 0.0_wp end do ! ---- FV_MOM6 branch — faithful port of MOM6 PressureForce_FV_Bouss ---- ! Layer-integrated pressure differences with face-thickness ! divisor. See OPGF_VARIANT_FV_MOM6 doc above. else if (pgf%variant == OPGF_VARIANT_FV_MOM6) then if (pgf%reconstruct_for_pressure .and. present(eos) .and. & ms%idx_salinity > 0 .and. ms%idx_temperature > 0) then ! In-layer PLM/PPM reconstruction: build per-layer Boole ! `dpa`/`intz_dpa` from a monotone sub-layer T/S profile, then ! reuse the unchanged FV_MOM6 face assembly. call compute_fv_mom6_reconstruct_impl(ms%h_layer, & ms%tracers(ms%idx_salinity)%hTr, & ms%tracers(ms%idx_temperature)%hTr, & pgf%b, eos, & pgf%recon_S_t%data, pgf%recon_S_b%data, & pgf%recon_T_t%data, pgf%recon_T_b%data, & pgf%conc_T%data, pgf%conc_S%data, & pgf%e_face%data, pgf%pa%data, & pgf%intz_dpa%data, & pgf%intx_pa%data, pgf%inty_pa%data, & pgf%intx_dpa%data, pgf%inty_dpa%data, & pgf%dpdx_face%data, pgf%dpdy_face%data, & pgf%rho0, pgf%rho_ref, pgf%h_neglect, & pgf%gfs_scale, pgf%recon_scheme, & ms%p_top, pgf%p_top_in_bc, & metrics%idxCu, metrics%idyCv, nx, ny, nz) else if (use_insitu_pcm(pgf, ms, eos)) then ! Constant-by-layer T/S, density at the in-situ pressure ! (MOM6 `int_density_dz_generic_pcm`). See `insitu_density`. call compute_fv_mom6_insitu_pcm_impl(ms%h_layer, & ms%tracers(ms%idx_salinity)%hTr, & ms%tracers(ms%idx_temperature)%hTr, & pgf%b, pgf%conc_T%data, pgf%conc_S%data, & pgf%e_face%data, pgf%pa%data, & pgf%intz_dpa%data, & pgf%intx_pa%data, pgf%inty_pa%data, & pgf%intx_dpa%data, pgf%inty_dpa%data, & pgf%dpdx_face%data, pgf%dpdy_face%data, & pgf%rho0, pgf%rho_ref, pgf%h_neglect, & pgf%gfs_scale, pgf%mass_weight, & eos%variant, & ms%p_top, pgf%p_top_in_bc, & metrics%idxCu, metrics%idyCv, nx, ny, nz) else call compute_fv_mom6_impl(ms%h_layer, ms%rho_layer, pgf%b, & pgf%e_face%data, pgf%pa%data, & pgf%intz_dpa%data, & pgf%intx_pa%data, pgf%inty_pa%data, & pgf%intx_dpa%data, pgf%inty_dpa%data, & pgf%dpdx_face%data, pgf%dpdy_face%data, & pgf%rho0, pgf%rho_ref, pgf%h_neglect, & pgf%gfs_scale, pgf%mass_weight, & ms%p_top, pgf%p_top_in_bc, & metrics%idxCu, metrics%idyCv, nx, ny, nz) end if else ! ---- Pass 1: hydrostatic integration per column ---- ! Surface boundary condition: p_edge at the top of the ! water column is zero (atmospheric absorbed into Boussinesq). ! March down: each layer adds rho*g*h_layer to the pressure ! at the layer below. FV_WRIGHT also computes rho_insitu ! per layer with a single Picard step. if (pgf%variant == OPGF_VARIANT_FV_WRIGHT) then if (ms%idx_salinity > 0 .and. ms%idx_temperature > 0) then call eos_wright_pgf_column_sweep_impl( & ms%h_layer, & ms%tracers(ms%idx_salinity)%hTr, & ms%tracers(ms%idx_temperature)%hTr, & ms%rho_layer, & ms%p_top, & pgf%p_edge%data, & pgf%rho_insitu%data, & GRAVITY, pgf%rho0, & nx, ny, nz) else ! No S, T registered: fall through to rho_layer. Keeps ! the test scaffolding (which sets rho_layer directly ! without registering tracers) workable. do concurrent(j=1:ny, i=1:nx) pgf%p_edge%data(i, j, nz + 1) = 0.0_wp do k = nz, 1, -1 pgf%p_edge%data(i, j, k) = pgf%p_edge%data(i, j, k + 1) + & GRAVITY*ms%rho_layer(i, j, k)*ms%h_layer(i, j, k) pgf%rho_insitu%data(i, j, k) = ms%rho_layer(i, j, k) end do end do end if else do concurrent(j=1:ny, i=1:nx) pgf%p_edge%data(i, j, nz + 1) = 0.0_wp do k = nz, 1, -1 pgf%p_edge%data(i, j, k) = pgf%p_edge%data(i, j, k + 1) + & GRAVITY*ms%rho_layer(i, j, k)*ms%h_layer(i, j, k) end do end do end if ! ---- Pass 2: east-face acceleration ---- ! `idxCu(i,j)` replaces the scalar `inv_dx` (D4) — bit-identical on ! uniform Cartesian. if (pgf%variant == OPGF_VARIANT_FV_WRIGHT) then do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(p_centre_left, p_centre_right, rho_face, z_correction) p_centre_right = 0.5_wp*(pgf%p_edge%data(i, j, k) + pgf%p_edge%data(i, j, k + 1)) p_centre_left = 0.5_wp*(pgf%p_edge%data(i - 1, j, k) + pgf%p_edge%data(i - 1, j, k + 1)) rho_face = 0.5_wp*(pgf%rho_insitu%data(i - 1, j, k) + pgf%rho_insitu%data(i, j, k)) z_correction = GRAVITY*rho_face* & (pgf%z_centre%data(i, j, k) - pgf%z_centre%data(i - 1, j, k))*metrics%idxCu(i, j) pgf%dpdx_face%data(i, j, k) = -inv_rho0*( & (p_centre_right - p_centre_left)*metrics%idxCu(i, j) + z_correction) end do else ! FV_LITE — the only variant that still reaches this pass ! (GPRIME returned, MONT and FV_MOM6 branched above). do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(p_centre_left, p_centre_right, rho_face, z_correction) p_centre_right = 0.5_wp*(pgf%p_edge%data(i, j, k) + pgf%p_edge%data(i, j, k + 1)) p_centre_left = 0.5_wp*(pgf%p_edge%data(i - 1, j, k) + pgf%p_edge%data(i - 1, j, k + 1)) rho_face = 0.5_wp*(ms%rho_layer(i - 1, j, k) + ms%rho_layer(i, j, k)) z_correction = GRAVITY*rho_face* & (pgf%z_centre%data(i, j, k) - pgf%z_centre%data(i - 1, j, k))*metrics%idxCu(i, j) pgf%dpdx_face%data(i, j, k) = -inv_rho0*( & (p_centre_right - p_centre_left)*metrics%idxCu(i, j) + z_correction) end do end if do concurrent(k=1:nz, j=1:ny) pgf%dpdx_face%data(1, j, k) = 0.0_wp pgf%dpdx_face%data(nx + 1, j, k) = 0.0_wp end do ! ---- Pass 3: north-face acceleration ---- ! `idyCv(i,j)` replaces the scalar `inv_dy` (D4). if (pgf%variant == OPGF_VARIANT_FV_WRIGHT) then do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(p_centre_below, p_centre_above, rho_face, z_correction) p_centre_above = 0.5_wp*(pgf%p_edge%data(i, j, k) + pgf%p_edge%data(i, j, k + 1)) p_centre_below = 0.5_wp*(pgf%p_edge%data(i, j - 1, k) + pgf%p_edge%data(i, j - 1, k + 1)) rho_face = 0.5_wp*(pgf%rho_insitu%data(i, j - 1, k) + pgf%rho_insitu%data(i, j, k)) z_correction = GRAVITY*rho_face* & (pgf%z_centre%data(i, j, k) - pgf%z_centre%data(i, j - 1, k))*metrics%idyCv(i, j) pgf%dpdy_face%data(i, j, k) = -inv_rho0*( & (p_centre_above - p_centre_below)*metrics%idyCv(i, j) + z_correction) end do else ! FV_LITE — see the Pass-2 comment. do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(p_centre_below, p_centre_above, rho_face, z_correction) p_centre_above = 0.5_wp*(pgf%p_edge%data(i, j, k) + pgf%p_edge%data(i, j, k + 1)) p_centre_below = 0.5_wp*(pgf%p_edge%data(i, j - 1, k) + pgf%p_edge%data(i, j - 1, k + 1)) rho_face = 0.5_wp*(ms%rho_layer(i, j - 1, k) + ms%rho_layer(i, j, k)) z_correction = GRAVITY*rho_face* & (pgf%z_centre%data(i, j, k) - pgf%z_centre%data(i, j - 1, k))*metrics%idyCv(i, j) pgf%dpdy_face%data(i, j, k) = -inv_rho0*( & (p_centre_above - p_centre_below)*metrics%idyCv(i, j) + z_correction) end do end if do concurrent(k=1:nz, i=1:nx) pgf%dpdy_face%data(i, 1, k) = 0.0_wp pgf%dpdy_face%data(i, ny + 1, k) = 0.0_wp end do end if ! ---- Pass 4: grounded-layer gate (VCOORD_LAGRANGIAN only) ---- ! Passes 2/3 form a two-point Jacobian: the `Δp_centre` and ! `g·ρ_layer·Δz_centre` terms cancel AT REST only while the two abutting ! layer centres lie in a common z-interval whose ambient density IS ! `ρ_layer`. Where an isopycnal layer has wedged out against the bed on ! one side, the centres are hundreds of metres apart, the interval ! between them holds OTHER density classes, and what survives is ! `g·(ρ_layer − ρ̄_ambient)·∂z/∂x` — a pressure gradient on a motionless ! ocean. There is no common depth to difference the pressure across ! there, so the honest face value is ZERO; the layer is still free to be ! re-wetted by continuity's upwind flux and the barotropic correction. ! ! FV_MOM6 is gated by the SAME test. Its face assembly is not the ! two-point Jacobian but the layer-integrated FV-Bouss form, yet the ! defect is the geometry, not the quadrature: where the layer occupies ! disjoint z-intervals in the two columns the layer-integrated pressure ! difference is likewise being taken between depths that share no water ! of that density class, and the `1/(h_L + h_R + h_neglect)` divisor ! does NOT suppress it — the deep side keeps the thickness up while the ! grounded side contributes the whole `e_bot` offset. Measured on ! `seamount_conservative_floor.nml` (form='fv_mom6'): En 2.17e-03 → ! 1.39e-26 at day 2. ! ! MONT is gated by the same test for the same reason. Its face ! expression is neither the two-point Jacobian nor the layer-integrated ! form, but a grounded layer still puts the two columns' `e_edge` values ! hundreds of metres apart, so the `M` recursion stops producing a ! horizontally uniform potential at rest and the residual reappears. ! ! GROUNDED, not merely non-overlapping: the face is zeroed only where ! the layer is also vanished (`<= nonoverlap_vanish_tol`) on at least ! one side. A layer massive on BOTH sides that merely sits at different ! depths (a sigma-seeded stack over a step) carries real mass flux ! across the face; its FV PGF is the ordinary steep-coordinate one ! (`vcoord_type="sigma"` runs the same geometry), and zeroing it breaks ! the PGF-work / PE exchange — see `nonoverlap_vanish_tol`. ! ! A separate guarded pass on purpose: when the gate is off (every vcoord ! but LAGRANGIAN) not one extra load is issued ⇒ bit-identical. ! Reads `z_centre`, so the driver only ever sets the flag for the four ! variants whose allocation gate provides it (MONT / FV_LITE / ! FV_WRIGHT, and FV_MOM6 — where `z_centre` is allocated + filled for ! this gate alone). if (pgf%skip_nonoverlap) then vtol = pgf%nonoverlap_vanish_tol do concurrent(k=1:nz, j=1:ny, i=2:nx) if (min(ms%h_layer(i - 1, j, k), ms%h_layer(i, j, k)) <= vtol) then if (min(pgf%z_centre%data(i - 1, j, k) + 0.5_wp*ms%h_layer(i - 1, j, k), & pgf%z_centre%data(i, j, k) + 0.5_wp*ms%h_layer(i, j, k)) <= & max(pgf%z_centre%data(i - 1, j, k) - 0.5_wp*ms%h_layer(i - 1, j, k), & pgf%z_centre%data(i, j, k) - 0.5_wp*ms%h_layer(i, j, k))) then pgf%dpdx_face%data(i, j, k) = 0.0_wp end if end if end do do concurrent(k=1:nz, j=2:ny, i=1:nx) if (min(ms%h_layer(i, j - 1, k), ms%h_layer(i, j, k)) <= vtol) then if (min(pgf%z_centre%data(i, j - 1, k) + 0.5_wp*ms%h_layer(i, j - 1, k), & pgf%z_centre%data(i, j, k) + 0.5_wp*ms%h_layer(i, j, k)) <= & max(pgf%z_centre%data(i, j - 1, k) - 0.5_wp*ms%h_layer(i, j - 1, k), & pgf%z_centre%data(i, j, k) - 0.5_wp*ms%h_layer(i, j, k))) then pgf%dpdy_face%data(i, j, k) = 0.0_wp end if end if end do end if end subroutine ocean_pressure_force_compute subroutine ocean_pressure_force_apply(pgf, ms, dt, no_wait) !! Forward-Euler accumulation of the PGF acceleration onto the face !! velocities. Additive (not overwriting), so apply ordering vs the !! Coriolis apply doesn't matter before the next tendency-compute. !! `no_wait` (optional, default .false. ⇒ blocking): when .true. the !! apply loops run on OpenACC queue 1 and return WITHOUT syncing, so !! the batched velocity-apply chain can `!$acc wait(1)` ONCE. Not !! `pure` (async/wait directives); still functionally pure. type(ocean_pressure_force_t), intent(in) :: pgf type(multilayer_state_t), intent(inout) :: ms real(wp), intent(in) :: dt logical, intent(in), optional :: no_wait integer :: i, j, k, nx_face, ny_uface, nx_vface, ny_face, nz logical :: lwait lwait = .true. if (present(no_wait)) lwait = .not. no_wait nx_face = size(ms%u_face_x_layer, 1) ny_uface = size(ms%u_face_x_layer, 2) nx_vface = size(ms%v_face_y_layer, 1) ny_face = size(ms%v_face_y_layer, 2) nz = ms%nz_ml !$acc kernels async(1) do concurrent(k=1:nz, j=1:ny_uface, i=1:nx_face) ms%u_face_x_layer(i, j, k) = ms%u_face_x_layer(i, j, k) + & dt*pgf%dpdx_face%data(i, j, k) end do do concurrent(k=1:nz, j=1:ny_face, i=1:nx_vface) ms%v_face_y_layer(i, j, k) = ms%v_face_y_layer(i, j, k) + & dt*pgf%dpdy_face%data(i, j, k) end do !$acc end kernels if (lwait) then !$acc wait(1) end if end subroutine ocean_pressure_force_apply pure subroutine compute_gprime_impl(h_layer, b, dpdx_face, dpdy_face, & gfs, gint, idxCu, idyCv, nx, ny, nz) !! Reduced-gravity / gprime PGF for NK = 2. !! !! Convention: k = 1 bottom (heavier), k = nz = 2 surface (lighter). !! `b(i, j)` is bathymetric depth (positive-down), used to recover ∇η: !! sum_h = h_1 + h_2 = b + η; η = sum_h - b; ∇η = ∇(h_1+h_2) - ∇b. !! The ∇b subtraction matters: without it ∇H_bathy dominates ∇η by !! 10³–10⁴ on shelf-break/spoon configs, over-driving the gyre. !! !! a_top = -g_FS · ∇η !! a_bot = -g_FS · ∇η - g'_int · ∇h_1 !! !! u-face gradient `(f(i,j) - f(i-1,j)) · idxCu(i,j)`, mirror for !! v-face. Higher k stays zero ⇒ no-op on NK > 2. integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: gfs, gint real(wp), intent(in) :: idxCu(nx + 1, ny), idyCv(nx, ny + 1) real(wp), intent(in) :: h_layer(nx, ny, nz) real(wp), intent(in) :: b(nx, ny) real(wp), intent(inout) :: dpdx_face(nx + 1, ny, nz) real(wp), intent(inout) :: dpdy_face(nx, ny + 1, nz) integer :: i, j, k real(wp) :: sum_h_left, sum_h_right, sum_h_below, sum_h_above real(wp) :: db_dx, db_dy real(wp) :: dssh_dx, dssh_dy, dhbot_dx, dhbot_dy ! Zero everything first. Then fill k = 1 and k = 2 explicitly. do concurrent(k=1:nz, j=1:ny, i=1:nx + 1) dpdx_face(i, j, k) = 0.0_wp end do do concurrent(k=1:nz, j=1:ny + 1, i=1:nx) dpdy_face(i, j, k) = 0.0_wp end do if (nz < 2) return ! u-face (east): two adjacent cells (i-1, j) and (i, j). do concurrent(j=1:ny, i=2:nx) & local(sum_h_left, sum_h_right, db_dx, dssh_dx, dhbot_dx) sum_h_left = h_layer(i - 1, j, 1) + h_layer(i - 1, j, 2) sum_h_right = h_layer(i, j, 1) + h_layer(i, j, 2) db_dx = (b(i, j) - b(i - 1, j))*idxCu(i, j) dssh_dx = (sum_h_right - sum_h_left)*idxCu(i, j) - db_dx dhbot_dx = (h_layer(i, j, 1) - h_layer(i - 1, j, 1))*idxCu(i, j) dpdx_face(i, j, 2) = -gfs*dssh_dx ! top layer (k=nz=2) dpdx_face(i, j, 1) = -gfs*dssh_dx - gint*dhbot_dx ! bottom layer (k=1) end do ! v-face (north): two adjacent cells (i, j-1) and (i, j). do concurrent(j=2:ny, i=1:nx) & local(sum_h_below, sum_h_above, db_dy, dssh_dy, dhbot_dy) sum_h_below = h_layer(i, j - 1, 1) + h_layer(i, j - 1, 2) sum_h_above = h_layer(i, j, 1) + h_layer(i, j, 2) db_dy = (b(i, j) - b(i, j - 1))*idyCv(i, j) dssh_dy = (sum_h_above - sum_h_below)*idyCv(i, j) - db_dy dhbot_dy = (h_layer(i, j, 1) - h_layer(i, j - 1, 1))*idyCv(i, j) dpdy_face(i, j, 2) = -gfs*dssh_dy dpdy_face(i, j, 1) = -gfs*dssh_dy - gint*dhbot_dy end do end subroutine compute_gprime_impl pure subroutine compute_fv_mom6_impl(h_layer, rho_layer, b, & e_face, pa, intz_dpa, & intx_pa, inty_pa, & intx_dpa, inty_dpa, & dpdx_face, dpdy_face, & rho0, rho_ref, h_neglect, & gfs_scale, mass_weight, & p_top, p_top_in_bc, & idxCu, idyCv, nx, ny, nz) !! Faithful port of MOM6's `PressureForce_FV_Bouss` per-layer PGF !! for the Boussinesq + per-layer Rlay path. !! !! Pass layout (matches MOM6's `PressureForce_FV_Bouss`): !! !! Pass 1 (per column): !! e_face(i, j, k_face) — interface heights, positive up. !! e_face(1) = -b (bed); e_face(k+1) = e_face(k) + h_layer(k); !! e_face(nz+1) = -b + sum(h_layer) = η (free surface). !! pa(i, j, nz+1) = rho_ref · g · η (surface BC), plus !! `p_top(i, j)` when `p_top_in_bc` (see the theorem below). !! pa(i, j, k) = pa(i, j, k+1) + (rho_layer(k) − rho_ref) · g · h(k) !! (marching down). !! intz_dpa(i, j, k) = 0.5 · (rho_layer(k) − rho_ref) · g · h(k)² !! (mid-point rule). !! !! Pass 2 (per face, march down): !! intx_pa(i, j, nz+1) = 0.5 · (pa(i-1, ., nz+1) + pa(i, ., nz+1)) !! (surface BC). !! intx_dpa(i, j, k) = 0.5 · g · ((rho(i-1, k) − rho_ref) · h(i-1, k) !! + (rho(i, k) − rho_ref) · h(i, k)) !! (generalised to per-cell rho). !! intx_pa(i, j, k) = intx_pa(i, j, k+1) + intx_dpa(i, j, k) !! (face pressure recurrence). !! Symmetric on v-face. !! !! When `mass_weight = .true.`, at hydrostatically-inconsistent !! unequal-depth faces (`hWght > 0`, !! `hWght = max(0, e_bed_R − e_top_L,k, e_bed_L − e_top_R,k)`) !! the layer density entering `dpa_L`/`dpa_R` is replaced by !! the MOM6 `hWt_LL/LR/RR/RL` blend biased toward the !! thinner column, AND the integral uses the face-interpolated !! thickness `dz = 0.5·(h_L + h_R)` for BOTH samples (MOM6 !! `dz_x · rho_anom`). The interpolated !! thickness is load-bearing: the plain per-cell form !! `0.5·g·(ρ_L'·h_L + ρ_R'·h_R)` is invariant under the !! (Σρh-conserving) hWt blend, so the shelf-break cancellation !! only appears when one `dz` multiplies both blended !! densities. `hWght = 0` (aligned / equal-depth) ⇒ the exact !! per-cell layer-midpoint average ⇒ bit-identical. !! !! Pass 3 (PFu/PFv assembly): !! numer = ((pa(L, k+1) · h(L, k) + intz_dpa(L, k)) !! − (pa(R, k+1) · h(R, k) + intz_dpa(R, k))) !! + (h(R, k) − h(L, k)) · intx_pa(face, k+1) !! − (e_face(R, k) − e_face(L, k)) · intx_dpa(face, k) !! denom = h(L, k) + h(R, k) + h_neglect !! PFu(face, k) = numer · (2 · I_Rho0 · IdxCu) / denom !! !! Convention map (MOM6 → Roundabout): !! MOM6 K (top of layer k_mom6) → ours k+1 (top of layer k) !! MOM6 K+1 (bottom of layer k_mom6) → ours k (bottom of layer k) !! MOM6 i (left of u-face I) → ours i-1 (west of u-face i) !! MOM6 i+1 (right of u-face I) → ours i (east of u-face i) !! MOM6 k_mom6 = 1 (surface layer) → ours k = nz !! MOM6 k_mom6 = nz_mom6 (bed layer) → ours k = 1 !! !! Wall faces (face index 1 and N+1) get zero by convention — the !! BT-substep / slow continuity already enforces u=0 there. !! !! ## Theorem — a depth-uniform top load is baroclinically inert here !! !! Perturb the TOP boundary condition only: `pa(·,nz+1) → pa(·,nz+1) !! + δp` with `δp(i,j)` independent of `k`. Every `dpa`, `intz_dpa` !! and `intx_dpa` is unchanged, and the Pass-2 recurrence shifts !! `intx_pa(K) → intx_pa(K) + ½(δp_L + δp_R)` for EVERY `K`. The !! Pass-3 numerator therefore moves by !! !! δnumer = δp_L·h_L − δp_R·h_R + (h_R − h_L)·½(δp_L + δp_R) !! = ½(h_L + h_R)·(δp_L − δp_R) !! δPFu(k) = −(1/ρ₀)·(δp_R − δp_L)·IdxCu · (h_L+h_R)/(h_L+h_R+h_neglect) !! !! — i.e. exactly `−(1/ρ₀)·∂δp/∂x`, **the same in every layer**, up !! to the `h_neglect` divisor (a relative `h_n/h_k ≈ 1e-10` for a !! metre-thick layer, `≈7e-7` for one at `H_VANISHED`). !! !! Consequence for the SPLIT solver (`&ocean_bt_nml bc_pgf_forcing`, !! default, MOM6 `BT_force`): the depth mean of the layer PGF FORCES !! the barotropic substep, so a depth-uniform `δPFu` reaches the !! barotropic mode. The part of `p_top` that is the atmospheric / !! anomaly load `sf%p_surf` is ALSO on the `eta_forcing` seam, and !! `set_fast_forcing_eta_pf` sheds `g·∇η_ib` from the forcing so it !! is counted once; the static ice load `p_ice_ref` cancels inside !! `pa(nz+1)` against the datum-shifted `η_geo`, and what survives !! of it is the physical reference-density shortfall. (The legacy !! split, `bc_pgf_forcing = .false.`, subtracted the whole depth !! mean, so there a depth-uniform `δPFu` cancelled identically.) !! The UNSPLIT driver has no seam, so there this term is the load's !! only path into the momentum. integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: h_layer(nx, ny, nz) real(wp), intent(in) :: rho_layer(nx, ny, nz) real(wp), intent(in) :: b(nx, ny) real(wp), intent(inout) :: e_face(nx, ny, nz + 1) real(wp), intent(inout) :: pa(nx, ny, nz + 1) real(wp), intent(inout) :: intz_dpa(nx, ny, nz) real(wp), intent(inout) :: intx_pa(nx + 1, ny, nz + 1) real(wp), intent(inout) :: inty_pa(nx, ny + 1, nz + 1) real(wp), intent(inout) :: intx_dpa(nx + 1, ny, nz) real(wp), intent(inout) :: inty_dpa(nx, ny + 1, nz) real(wp), intent(inout) :: dpdx_face(nx + 1, ny, nz) real(wp), intent(inout) :: dpdy_face(nx, ny + 1, nz) real(wp), intent(in) :: rho0, rho_ref, h_neglect, gfs_scale logical, intent(in) :: mass_weight real(wp), intent(in) :: p_top(nx, ny) !! Top-of-column pressure (Pa, `>= 0`), `multilayer_state_t%p_top`. !! Consulted only when `p_top_in_bc`; the zero array otherwise. logical, intent(in) :: p_top_in_bc !! Add `p_top` to the Pass-1 surface BC. `.false.` ⇒ the !! assignment is character-for-character the pre-knob one ⇒ !! bit-identical (same branch-on-a-scalar-knob shape as !! `mass_weight` in Pass 2). real(wp), intent(in) :: idxCu(nx + 1, ny), idyCv(nx, ny + 1) integer :: i, j, k real(wp) :: inv_rho0, eta, rho_anom, dpa_kk real(wp) :: dpa_L, dpa_R, h_L, h_R, e_bot_L, e_bot_R real(wp) :: pa_h_intz_L, pa_h_intz_R, numer, denom real(wp) :: dM_coeff, ddM_dx, ddM_dy real(wp) :: hwght, hwl, hwr, idenom_hw, hwt_ll, hwt_lr, hwt_rr, hwt_rl real(wp) :: rho_face_l, rho_face_r inv_rho0 = 1.0_wp/rho0 ! ---- Pass 1: per-column build of e_face, pa, intz_dpa ---- do concurrent(j=1:ny, i=1:nx) local(k, eta, rho_anom, dpa_kk) e_face(i, j, 1) = -b(i, j) do k = 1, nz e_face(i, j, k + 1) = e_face(i, j, k) + h_layer(i, j, k) end do eta = e_face(i, j, nz + 1) if (p_top_in_bc) then pa(i, j, nz + 1) = rho_ref*GRAVITY*eta + p_top(i, j) else pa(i, j, nz + 1) = rho_ref*GRAVITY*eta end if do k = nz, 1, -1 rho_anom = rho_layer(i, j, k) - rho_ref dpa_kk = rho_anom*GRAVITY*h_layer(i, j, k) pa(i, j, k) = pa(i, j, k + 1) + dpa_kk intz_dpa(i, j, k) = 0.5_wp*dpa_kk*h_layer(i, j, k) end do end do ! ---- Pass 2a: u-face horizontal integrals ---- do concurrent(j=1:ny, i=2:nx) local(k, dpa_L, dpa_R, hwght, hwl, hwr, & idenom_hw, hwt_ll, hwt_lr, hwt_rr, hwt_rl, & rho_face_l, rho_face_r) intx_pa(i, j, nz + 1) = 0.5_wp*(pa(i - 1, j, nz + 1) + pa(i, j, nz + 1)) do k = nz, 1, -1 ! hWght: hydrostatic-inconsistency measure at this u-face for ! layer k. Bed height = e_face(.,1); layer-k top = e_face(.,k+1). ! MOM6 form: max(0, bed_R - top_L, bed_L - top_R). hwght = 0.0_wp if (mass_weight) then hwght = max(0.0_wp, & e_face(i, j, 1) - e_face(i - 1, j, k + 1), & e_face(i - 1, j, 1) - e_face(i, j, k + 1)) end if if (hwght > 0.0_wp) then ! Hydrostatically-inconsistent face: blend the layer ! density toward the thinner column (MOM6 hWt_*) and ! integrate with the face-interpolated thickness ! dz = 0.5·(h_L + h_R). The interpolated thickness is ! what makes the blend non-trivial — the plain per-cell ! form `0.5·g·(ρ_L'·h_L + ρ_R'·h_R)` is invariant under ! the (Σρh-conserving) hWt blend, so the cancellation ! only appears when the SAME dz multiplies both samples ! (MOM6 `dz_x · rho_anom`). hwl = h_layer(i - 1, j, k) + h_neglect hwr = h_layer(i, j, k) + h_neglect hwght = hwght*((hwl - hwr)/(hwl + hwr))**2 idenom_hw = 1.0_wp/(hwght*(hwr + hwl) + hwl*hwr) hwt_ll = (hwght*hwl + hwr*hwl)*idenom_hw hwt_lr = (hwght*hwr)*idenom_hw hwt_rr = (hwght*hwr + hwr*hwl)*idenom_hw hwt_rl = (hwght*hwl)*idenom_hw rho_face_l = hwt_ll*rho_layer(i - 1, j, k) + hwt_lr*rho_layer(i, j, k) rho_face_r = hwt_rl*rho_layer(i - 1, j, k) + hwt_rr*rho_layer(i, j, k) dpa_L = (rho_face_l - rho_ref)*GRAVITY* & (0.5_wp*(h_layer(i - 1, j, k) + h_layer(i, j, k))) dpa_R = (rho_face_r - rho_ref)*GRAVITY* & (0.5_wp*(h_layer(i - 1, j, k) + h_layer(i, j, k))) else ! Aligned / equal-depth face: exact layer-midpoint ! average (bit-identical to the pre-knob FV_MOM6 form). dpa_L = (rho_layer(i - 1, j, k) - rho_ref)*GRAVITY*h_layer(i - 1, j, k) dpa_R = (rho_layer(i, j, k) - rho_ref)*GRAVITY*h_layer(i, j, k) end if intx_dpa(i, j, k) = 0.5_wp*(dpa_L + dpa_R) intx_pa(i, j, k) = intx_pa(i, j, k + 1) + intx_dpa(i, j, k) end do end do do concurrent(k=1:nz, j=1:ny) intx_dpa(1, j, k) = 0.0_wp intx_dpa(nx + 1, j, k) = 0.0_wp end do do concurrent(k=1:nz + 1, j=1:ny) intx_pa(1, j, k) = 0.0_wp intx_pa(nx + 1, j, k) = 0.0_wp end do ! ---- Pass 2b: v-face horizontal integrals ---- do concurrent(j=2:ny, i=1:nx) local(k, dpa_L, dpa_R, hwght, hwl, hwr, & idenom_hw, hwt_ll, hwt_lr, hwt_rr, hwt_rl, & rho_face_l, rho_face_r) inty_pa(i, j, nz + 1) = 0.5_wp*(pa(i, j - 1, nz + 1) + pa(i, j, nz + 1)) do k = nz, 1, -1 hwght = 0.0_wp if (mass_weight) then hwght = max(0.0_wp, & e_face(i, j, 1) - e_face(i, j - 1, k + 1), & e_face(i, j - 1, 1) - e_face(i, j, k + 1)) end if if (hwght > 0.0_wp) then ! Hydrostatically-inconsistent face: hWt density blend + ! face-interpolated thickness (see Pass 2a comment). hwl = h_layer(i, j - 1, k) + h_neglect hwr = h_layer(i, j, k) + h_neglect hwght = hwght*((hwl - hwr)/(hwl + hwr))**2 idenom_hw = 1.0_wp/(hwght*(hwr + hwl) + hwl*hwr) hwt_ll = (hwght*hwl + hwr*hwl)*idenom_hw hwt_lr = (hwght*hwr)*idenom_hw hwt_rr = (hwght*hwr + hwr*hwl)*idenom_hw hwt_rl = (hwght*hwl)*idenom_hw rho_face_l = hwt_ll*rho_layer(i, j - 1, k) + hwt_lr*rho_layer(i, j, k) rho_face_r = hwt_rl*rho_layer(i, j - 1, k) + hwt_rr*rho_layer(i, j, k) dpa_L = (rho_face_l - rho_ref)*GRAVITY* & (0.5_wp*(h_layer(i, j - 1, k) + h_layer(i, j, k))) dpa_R = (rho_face_r - rho_ref)*GRAVITY* & (0.5_wp*(h_layer(i, j - 1, k) + h_layer(i, j, k))) else ! Aligned / equal-depth face: exact layer-midpoint average. dpa_L = (rho_layer(i, j - 1, k) - rho_ref)*GRAVITY*h_layer(i, j - 1, k) dpa_R = (rho_layer(i, j, k) - rho_ref)*GRAVITY*h_layer(i, j, k) end if inty_dpa(i, j, k) = 0.5_wp*(dpa_L + dpa_R) inty_pa(i, j, k) = inty_pa(i, j, k + 1) + inty_dpa(i, j, k) end do end do do concurrent(k=1:nz, i=1:nx) inty_dpa(i, 1, k) = 0.0_wp inty_dpa(i, ny + 1, k) = 0.0_wp end do do concurrent(k=1:nz + 1, i=1:nx) inty_pa(i, 1, k) = 0.0_wp inty_pa(i, ny + 1, k) = 0.0_wp end do ! ---- Pass 3: PFu assembly ---- do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(h_L, h_R, e_bot_L, e_bot_R, pa_h_intz_L, pa_h_intz_R, numer, denom) h_L = h_layer(i - 1, j, k) h_R = h_layer(i, j, k) e_bot_L = e_face(i - 1, j, k) e_bot_R = e_face(i, j, k) pa_h_intz_L = pa(i - 1, j, k + 1)*h_L + intz_dpa(i - 1, j, k) pa_h_intz_R = pa(i, j, k + 1)*h_R + intz_dpa(i, j, k) numer = (pa_h_intz_L - pa_h_intz_R) & + (h_R - h_L)*intx_pa(i, j, k + 1) & - (e_bot_R - e_bot_L)*intx_dpa(i, j, k) denom = h_L + h_R + h_neglect dpdx_face(i, j, k) = numer*(2.0_wp*inv_rho0*idxCu(i, j))/denom end do do concurrent(k=1:nz, j=1:ny) dpdx_face(1, j, k) = 0.0_wp dpdx_face(nx + 1, j, k) = 0.0_wp end do ! ---- Pass 4: PFv assembly ---- do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(h_L, h_R, e_bot_L, e_bot_R, pa_h_intz_L, pa_h_intz_R, numer, denom) h_L = h_layer(i, j - 1, k) h_R = h_layer(i, j, k) e_bot_L = e_face(i, j - 1, k) e_bot_R = e_face(i, j, k) pa_h_intz_L = pa(i, j - 1, k + 1)*h_L + intz_dpa(i, j - 1, k) pa_h_intz_R = pa(i, j, k + 1)*h_R + intz_dpa(i, j, k) numer = (pa_h_intz_L - pa_h_intz_R) & + (h_R - h_L)*inty_pa(i, j, k + 1) & - (e_bot_R - e_bot_L)*inty_dpa(i, j, k) denom = h_L + h_R + h_neglect dpdy_face(i, j, k) = numer*(2.0_wp*inv_rho0*idyCv(i, j))/denom end do do concurrent(k=1:nz, i=1:nx) dpdy_face(i, 1, k) = 0.0_wp dpdy_face(i, ny + 1, k) = 0.0_wp end do ! ---- Pass 5: Montgomery dM correction (MOM6 GFS_scale) ---- ! Subtracts `(1 - gfs_scale)·(g/ρ₀)·ρ_surf·∇η` from every layer's ! PGF so the slow tendency only carries `gfs_scale · g · ∇η`. ! The BT substep evolves η with `g_bt = gfs_scale · GRAVITY` ! (set by the driver); combined they reproduce the physical ! surface-gravity coupling at the chosen reduced value. ! Depth-independent → BT mass-flux invariant intact. Mirrors ! MOM6's Boussinesq non-EOS branch ! (`rho_surf = Rlay(top)`). No-op when `gfs_scale = 1` ! (modulo the small tolerance below) so the default ! configuration is bit-identical to the pre-knob FV_MOM6 port. if (gfs_scale < 1.0_wp - 1.0e-12_wp) then dM_coeff = (gfs_scale - 1.0_wp)*GRAVITY*inv_rho0 do concurrent(k=1:nz, j=1:ny, i=2:nx) local(ddM_dx) ddM_dx = dM_coeff*(rho_layer(i, j, nz)*e_face(i, j, nz + 1) & - rho_layer(i - 1, j, nz)*e_face(i - 1, j, nz + 1))*idxCu(i, j) dpdx_face(i, j, k) = dpdx_face(i, j, k) - ddM_dx end do do concurrent(k=1:nz, j=2:ny, i=1:nx) local(ddM_dy) ddM_dy = dM_coeff*(rho_layer(i, j, nz)*e_face(i, j, nz + 1) & - rho_layer(i, j - 1, nz)*e_face(i, j - 1, nz + 1))*idyCv(i, j) dpdy_face(i, j, k) = dpdy_face(i, j, k) - ddM_dy end do end if end subroutine compute_fv_mom6_impl pure subroutine compute_fv_mom6_reconstruct_impl(h_layer, hS, hT, b, eos, & S_t, S_b, T_t, T_b, & conc_T, conc_S, & e_face, pa, intz_dpa, & intx_pa, inty_pa, & intx_dpa, inty_dpa, & dpdx_face, dpdy_face, & rho0, rho_ref, h_neglect, & gfs_scale, recon_scheme, & p_top, p_top_in_bc, & idxCu, idyCv, nx, ny, nz) !! FV_MOM6 pressure-gradient with in-layer T/S reconstruction. !! !! Same Pass 3-5 face assembly as `compute_fv_mom6_impl`, but the !! two integrals that assembly consumes are BOTH taken from the !! reconstructed sub-layer T/S profile rather than a layer mean: !! !! * Pass 1 replaces the PCM `dpa(k)` / `intz_dpa(k)` with the !! 5-point VERTICAL Boole quadrature of the monotone PLM/PPM !! profile (edges from Pass 0) — the side integrals of the !! control volume. !! * Pass 2 (per face) replaces the two-column trapezoid !! `0.5*(dpa_L + dpa_R)` with the 5-point HORIZONTAL Boole !! quadrature `boole_dpa_face` — the top/bottom (tilted) edges. !! !! Passes 0-2 each run one GPU thread per CELL (3-D `do concurrent` !! over k, j, i, plus a cheap per-column scan for the `pa` / `intx_pa` !! / `inty_pa` recurrences) with every per-EOS helper inlined: the !! same operations in the same order as the column-serial form they !! replaced, so bit-identical to it. Global 1-degree PPM, 5 days, one !! V100: `ocean_pgf` 5.97 -> 3.45 s under Wright, 12.42 -> 6.55 s under !! Roquet (`[stats]` identical to the digit). !! !! Both are required for the defining property: with a linear EOS !! and T/S linear in z, the PGF then vanishes to round-off for ANY !! layer geometry (Adcroft, Hallberg & Harrison 2008; Yung, !! Hallberg, Adcroft & Morrison 2026 §2.4). Correcting the vertical !! integral alone leaves the horizontal trapezoid's curvature !! residual `g*(-drho/dz)*Delta_e^2/12` at every tilted interface, !! which is the sigma "second-kind" pressure-gradient error. !! !! `mass_weight` (hWght blend) is NOT applied here: it needs a !! per-cell density, whereas reconstruction works on column T/S !! edges. Boundary layers take the linear-exact one-sided edge pair !! in the edge helper (`boundary_edges_linear`). !! !! `p_top_in_bc` injects the top load into the SAME Pass-1 surface !! BC as the PCM twin, and the Theorem in `compute_fv_mom6_impl` !! carries over verbatim: the reconstruction only changes `dpa` / !! `intz_dpa`, never the `pa(nz+1)` seed or the `intx_pa` !! recurrence, so a depth-uniform `p_top` still perturbs every !! layer's `PFu` by the same `−(1/ρ₀)∇p_top`. NOTE that this branch !! builds its OWN in-layer EOS pressure inside !! `boole_dpa_intz_layer` (`p = −g·ρ₀·z` from the surface-relative !! interface height) and that one is NOT offset by `p_top` — which !! is exactly why `validate_config` refuses !! `&ocean_psurf_nml in_eos` together with !! `reconstruct_for_pressure`. The BC injection here is a PRESSURE !! boundary condition, not an EOS argument; the two are independent !! seams. integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: h_layer(nx, ny, nz) real(wp), intent(in) :: hS(nx, ny, nz) !! Salinity * thickness (PSU*m) — layer-mean S = hS / h. real(wp), intent(in) :: hT(nx, ny, nz) !! Temperature * thickness (degC*m) — layer-mean T = hT / h. real(wp), intent(in) :: b(nx, ny) type(eos_t), intent(in) :: eos real(wp), intent(inout) :: S_t(nx, ny, nz), S_b(nx, ny, nz) real(wp), intent(inout) :: T_t(nx, ny, nz), T_b(nx, ny, nz) real(wp), intent(inout) :: conc_T(nx, ny, nz), conc_S(nx, ny, nz) !! Layer-mean T / S as every pass below reads them (Pass C). real(wp), intent(inout) :: e_face(nx, ny, nz + 1) real(wp), intent(inout) :: pa(nx, ny, nz + 1) real(wp), intent(inout) :: intz_dpa(nx, ny, nz) real(wp), intent(inout) :: intx_pa(nx + 1, ny, nz + 1) real(wp), intent(inout) :: inty_pa(nx, ny + 1, nz + 1) real(wp), intent(inout) :: intx_dpa(nx + 1, ny, nz) real(wp), intent(inout) :: inty_dpa(nx, ny + 1, nz) real(wp), intent(inout) :: dpdx_face(nx + 1, ny, nz) real(wp), intent(inout) :: dpdy_face(nx, ny + 1, nz) real(wp), intent(in) :: rho0, rho_ref, h_neglect, gfs_scale integer, intent(in) :: recon_scheme real(wp), intent(in) :: p_top(nx, ny) !! Top-of-column pressure (Pa, `>= 0`), `multilayer_state_t%p_top`. logical, intent(in) :: p_top_in_bc !! Add `p_top` to the Pass-1 surface BC (`.false.` ⇒ bit-identical). real(wp), intent(in) :: idxCu(nx + 1, ny), idyCv(nx, ny + 1) integer :: i, j, k real(wp) :: inv_rho0, dpa_kk, intz_kk real(wp) :: h_L, h_R, e_bot_L, e_bot_R real(wp) :: t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R real(wp) :: pa_h_intz_L, pa_h_intz_R, numer, denom real(wp) :: dM_coeff, ddM_dx, ddM_dy logical :: parabolic integer :: km2, km1, kp1, kp2, k_top_c, k_don_c integer :: eos_variant inv_rho0 = 1.0_wp/rho0 parabolic = (recon_scheme == PGF_RECON_PPM) eos_variant = eos%variant ! ---- Pass C (per column): the layer-mean T/S every pass reads ---- ! `hT/h` on a live layer and, on a vanished one, its I1′ DONOR's: ! the nearest live layer above it, or for a run of fillers reaching ! the top of the column the topmost live layer; 0 in a column with ! no live layer. The per-column form of `rdb_vl_column_conc`, read ! off the donor so no near-zero thickness is ever a divisor. MOM6 ! carries T/S as concentrations, so its vanished layers hold the ! remapped value its `int_density_dz_*` reads; `c_live` is that value ! here. NOT the floored `hT/max(h, H_VANISHED)` this replaced: that ! is `h/H_VANISHED` of the truth on a filler (2/3 at the default ! `zstar_h_min = 1e-4 m`), harmless in the vertical `pa` stack where ! it multiplies the filler's own thickness, but the cross-face Boole ! integral interpolates T/S over the INTERPOLATED -- live -- ! thickness and the PLM/PPM stencil reads its neighbours, so at an ! OPEN z-like step (`zstar`, closed-faces-off `z_fixed`, a `z_fixed` ! cell whose liveness flipped with eta under a static closed-face ! mask) it integrated the wrong salinity over tens of metres of live ! water: 2.9e-3 m/s^2 at rest on a live|filler face against 1.4e-6 ! (`test_ocean_pgf_insitu :: open_step_filler_faces_*`). One O(nz) ! sweep per column, the shape of Pass 1a: a per-cell donor walk made ! `ocean_pgf` 4x slower on the global 1-degree grid (long bed-filler ! runs). A live layer reads `hT/h` exactly as before (bit-identical ! on a column without fillers). do concurrent(j=1:ny, i=1:nx) local(k, k_top_c, k_don_c) k_top_c = 0 do k = nz, 1, -1 if (rdb_vl_is_live(h_layer(i, j, k))) then k_top_c = k exit end if end do if (k_top_c == 0) then do k = 1, nz conc_T(i, j, k) = 0.0_wp conc_S(i, j, k) = 0.0_wp end do else k_don_c = k_top_c do k = nz, 1, -1 if (rdb_vl_is_live(h_layer(i, j, k))) k_don_c = k conc_T(i, j, k) = rdb_vl_conc(hT(i, j, k_don_c), h_layer(i, j, k_don_c)) conc_S(i, j, k) = rdb_vl_conc(hS(i, j, k_don_c), h_layer(i, j, k_don_c)) end do end if end do ! ---- Pass 0: PLM/PPM T/S edge values, one thread per cell ---- ! A layer's edges come from its own short vertical stencil of layer ! means (k-1..k+1 for PLM, k-2..k+2 for PPM), so this is a 3-D ! `do concurrent`; the per-column form built seven NZ_STACK_MAX ! stacks per thread in device local memory. The means are Pass C's ! `conc_T`/`conc_S` (the I1′ donor's on a vanished layer). Stencil ! indices outside the column are clamped into it; the boundary ! branches that would read them do not. Done as its own pass so the ! quadrature passes below read clean edge arrays. do concurrent(k=1:nz, j=1:ny, i=1:nx) local(km2, km1, kp1, kp2) km2 = max(k - 2, 1) km1 = max(k - 1, 1) kp1 = min(k + 1, nz) kp2 = min(k + 2, nz) if (parabolic) then call ppm_edges_layer(k, nz, h_layer(i, j, km2), h_layer(i, j, km1), & h_layer(i, j, k), h_layer(i, j, kp1), h_layer(i, j, kp2), & conc_S(i, j, km2), & conc_S(i, j, km1), & conc_S(i, j, k), & conc_S(i, j, kp1), & conc_S(i, j, kp2), & conc_T(i, j, km2), & conc_T(i, j, km1), & conc_T(i, j, k), & conc_T(i, j, kp1), & conc_T(i, j, kp2), & S_t(i, j, k), S_b(i, j, k), T_t(i, j, k), T_b(i, j, k)) else call plm_edges_layer(k, nz, h_layer(i, j, km1), h_layer(i, j, k), h_layer(i, j, kp1), & conc_S(i, j, km1), & conc_S(i, j, k), & conc_S(i, j, kp1), & S_t(i, j, k), S_b(i, j, k)) call plm_edges_layer(k, nz, h_layer(i, j, km1), h_layer(i, j, k), h_layer(i, j, kp1), & conc_T(i, j, km1), & conc_T(i, j, k), & conc_T(i, j, kp1), & T_t(i, j, k), T_b(i, j, k)) end if end do ! ---- Pass 1: e_face, pa, intz_dpa via Boole quadrature ---- ! e_top = e_face(k+1) is the shallower interface of layer k. The ! reconstructed dpa(k) marches the pa stack; intz_dpa(k) is the ! first-moment piece. Both replace the PCM forms. ! ! Pass 1a (per column): interface heights and the surface seed of the ! pressure-anomaly stack. Pass 1b (3-D `do concurrent` over k, j, i): ! every layer's `dpa` / `intz_dpa`, `dpa` parked in `pa(i, j, k)`. ! Pass 1c (per column): the stack sum, top down, in place. The same ! operations in the same order as the column-serial march, so ! bit-identical to it, but one GPU thread per CELL -- the per-column ! form ran one thread per column through nz layers x 5 EOS ! evaluations (115k threads on the global 1-degree grid). Pass 2 ! likewise: 3-D face integrals, then a per-column scan. ! ! Passes 1b and 2 come in one loop copy per EOS, selected here, outside ! the loops (same sub-points, weights and summation order in every ! copy): Wright calls its Boole twins (density inline, no `eos_t` ! handle); Roquet calls this module's twins `roquet_recon_dpa_intz` / ! `roquet_recon_dpa_face`, whose SpV value comes from the included ! `rdb_roquet_spv.inc` and is inlined into the kernel (the generic ! chain's out-of-line `eos_density_point` calls were this path's ! device cost); anything else (the linear EOS) takes the generic ! `eos_density_point` rule. do concurrent(j=1:ny, i=1:nx) local(k) e_face(i, j, 1) = -b(i, j) do k = 1, nz e_face(i, j, k + 1) = e_face(i, j, k) + h_layer(i, j, k) end do if (p_top_in_bc) then pa(i, j, nz + 1) = rho_ref*GRAVITY*e_face(i, j, nz + 1) + p_top(i, j) else pa(i, j, nz + 1) = rho_ref*GRAVITY*e_face(i, j, nz + 1) end if end do if (eos_variant == EOS_VARIANT_WRIGHT_97) then do concurrent(k=1:nz, j=1:ny, i=1:nx) local(dpa_kk, intz_kk) call boole_dpa_intz_layer_wright(rho0, rho_ref, & e_face(i, j, k + 1), h_layer(i, j, k), & T_t(i, j, k), T_b(i, j, k), & conc_T(i, j, k), & S_t(i, j, k), S_b(i, j, k), & conc_S(i, j, k), & parabolic, dpa_kk, intz_kk) pa(i, j, k) = dpa_kk intz_dpa(i, j, k) = intz_kk end do else if (eos_variant == EOS_VARIANT_ROQUET_SPV) then do concurrent(k=1:nz, j=1:ny, i=1:nx) local(dpa_kk, intz_kk) call roquet_recon_dpa_intz(rho0, rho_ref, & e_face(i, j, k + 1), h_layer(i, j, k), & T_t(i, j, k), T_b(i, j, k), & conc_T(i, j, k), & S_t(i, j, k), S_b(i, j, k), & conc_S(i, j, k), & parabolic, dpa_kk, intz_kk) pa(i, j, k) = dpa_kk intz_dpa(i, j, k) = intz_kk end do else do concurrent(k=1:nz, j=1:ny, i=1:nx) local(dpa_kk, intz_kk) call boole_dpa_intz_layer(eos, rho0, rho_ref, & e_face(i, j, k + 1), h_layer(i, j, k), & T_t(i, j, k), T_b(i, j, k), & conc_T(i, j, k), & S_t(i, j, k), S_b(i, j, k), & conc_S(i, j, k), & parabolic, dpa_kk, intz_kk) pa(i, j, k) = dpa_kk intz_dpa(i, j, k) = intz_kk end do end if do concurrent(j=1:ny, i=1:nx) local(k) do k = nz, 1, -1 pa(i, j, k) = pa(i, j, k + 1) + pa(i, j, k) end do end do ! ---- Pass 2a: u-face horizontal integrals ---- ! The along-face mean of the layer pressure increment, by the 5-point ! cross-face Boole quadrature of `boole_dpa_face` (sub-columns at the ! INTERPOLATED interface height with interpolated T/S). The ! two-column trapezoid `0.5*(dpa_L + dpa_R)` this replaces is exact ! only for a pressure linear in x along the edge; under a tilted ! interface it leaves the sigma second-kind curvature residual at ! every interface — see the `boole_dpa_face` docstring. if (eos_variant == EOS_VARIANT_WRIGHT_97) then do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R) t_m_L = conc_T(i - 1, j, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i - 1, j, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i - 1, j, k) - pa(i - 1, j, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call boole_dpa_face_wright(rho0, rho_ref, & e_face(i - 1, j, k + 1), e_face(i, j, k + 1), & h_layer(i - 1, j, k), h_layer(i, j, k), & T_t(i - 1, j, k), T_b(i - 1, j, k), t_m_L, & T_t(i, j, k), T_b(i, j, k), t_m_R, & S_t(i - 1, j, k), S_b(i - 1, j, k), s_m_L, & S_t(i, j, k), S_b(i, j, k), s_m_R, & dpa_L, dpa_R, parabolic, dpa_kk) intx_dpa(i, j, k) = dpa_kk end do else if (eos_variant == EOS_VARIANT_ROQUET_SPV) then do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R) t_m_L = conc_T(i - 1, j, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i - 1, j, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i - 1, j, k) - pa(i - 1, j, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call roquet_recon_dpa_face(rho0, rho_ref, & e_face(i - 1, j, k + 1), e_face(i, j, k + 1), & h_layer(i - 1, j, k), h_layer(i, j, k), & T_t(i - 1, j, k), T_b(i - 1, j, k), t_m_L, & T_t(i, j, k), T_b(i, j, k), t_m_R, & S_t(i - 1, j, k), S_b(i - 1, j, k), s_m_L, & S_t(i, j, k), S_b(i, j, k), s_m_R, & dpa_L, dpa_R, parabolic, dpa_kk) intx_dpa(i, j, k) = dpa_kk end do else do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R) t_m_L = conc_T(i - 1, j, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i - 1, j, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i - 1, j, k) - pa(i - 1, j, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call boole_dpa_face(eos, rho0, rho_ref, & e_face(i - 1, j, k + 1), e_face(i, j, k + 1), & h_layer(i - 1, j, k), h_layer(i, j, k), & T_t(i - 1, j, k), T_b(i - 1, j, k), t_m_L, & T_t(i, j, k), T_b(i, j, k), t_m_R, & S_t(i - 1, j, k), S_b(i - 1, j, k), s_m_L, & S_t(i, j, k), S_b(i, j, k), s_m_R, & dpa_L, dpa_R, parabolic, dpa_kk) intx_dpa(i, j, k) = dpa_kk end do end if ! Column scan of the face integrals (cheap; the EOS work above is ! 3-D parallel). do concurrent(j=1:ny, i=2:nx) local(k) intx_pa(i, j, nz + 1) = 0.5_wp*(pa(i - 1, j, nz + 1) + pa(i, j, nz + 1)) do k = nz, 1, -1 intx_pa(i, j, k) = intx_pa(i, j, k + 1) + intx_dpa(i, j, k) end do end do do concurrent(k=1:nz, j=1:ny) intx_dpa(1, j, k) = 0.0_wp intx_dpa(nx + 1, j, k) = 0.0_wp end do do concurrent(k=1:nz + 1, j=1:ny) intx_pa(1, j, k) = 0.0_wp intx_pa(nx + 1, j, k) = 0.0_wp end do ! ---- Pass 2b: v-face horizontal integrals ---- if (eos_variant == EOS_VARIANT_WRIGHT_97) then do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R) t_m_L = conc_T(i, j - 1, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i, j - 1, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i, j - 1, k) - pa(i, j - 1, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call boole_dpa_face_wright(rho0, rho_ref, & e_face(i, j - 1, k + 1), e_face(i, j, k + 1), & h_layer(i, j - 1, k), h_layer(i, j, k), & T_t(i, j - 1, k), T_b(i, j - 1, k), t_m_L, & T_t(i, j, k), T_b(i, j, k), t_m_R, & S_t(i, j - 1, k), S_b(i, j - 1, k), s_m_L, & S_t(i, j, k), S_b(i, j, k), s_m_R, & dpa_L, dpa_R, parabolic, dpa_kk) inty_dpa(i, j, k) = dpa_kk end do else if (eos_variant == EOS_VARIANT_ROQUET_SPV) then do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R) t_m_L = conc_T(i, j - 1, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i, j - 1, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i, j - 1, k) - pa(i, j - 1, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call roquet_recon_dpa_face(rho0, rho_ref, & e_face(i, j - 1, k + 1), e_face(i, j, k + 1), & h_layer(i, j - 1, k), h_layer(i, j, k), & T_t(i, j - 1, k), T_b(i, j - 1, k), t_m_L, & T_t(i, j, k), T_b(i, j, k), t_m_R, & S_t(i, j - 1, k), S_b(i, j - 1, k), s_m_L, & S_t(i, j, k), S_b(i, j, k), s_m_R, & dpa_L, dpa_R, parabolic, dpa_kk) inty_dpa(i, j, k) = dpa_kk end do else do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R) t_m_L = conc_T(i, j - 1, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i, j - 1, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i, j - 1, k) - pa(i, j - 1, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call boole_dpa_face(eos, rho0, rho_ref, & e_face(i, j - 1, k + 1), e_face(i, j, k + 1), & h_layer(i, j - 1, k), h_layer(i, j, k), & T_t(i, j - 1, k), T_b(i, j - 1, k), t_m_L, & T_t(i, j, k), T_b(i, j, k), t_m_R, & S_t(i, j - 1, k), S_b(i, j - 1, k), s_m_L, & S_t(i, j, k), S_b(i, j, k), s_m_R, & dpa_L, dpa_R, parabolic, dpa_kk) inty_dpa(i, j, k) = dpa_kk end do end if do concurrent(j=2:ny, i=1:nx) local(k) inty_pa(i, j, nz + 1) = 0.5_wp*(pa(i, j - 1, nz + 1) + pa(i, j, nz + 1)) do k = nz, 1, -1 inty_pa(i, j, k) = inty_pa(i, j, k + 1) + inty_dpa(i, j, k) end do end do do concurrent(k=1:nz, i=1:nx) inty_dpa(i, 1, k) = 0.0_wp inty_dpa(i, ny + 1, k) = 0.0_wp end do do concurrent(k=1:nz + 1, i=1:nx) inty_pa(i, 1, k) = 0.0_wp inty_pa(i, ny + 1, k) = 0.0_wp end do ! ---- Pass 3: PFu assembly (identical to compute_fv_mom6_impl) ---- do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(h_L, h_R, e_bot_L, e_bot_R, pa_h_intz_L, pa_h_intz_R, numer, denom) h_L = h_layer(i - 1, j, k) h_R = h_layer(i, j, k) e_bot_L = e_face(i - 1, j, k) e_bot_R = e_face(i, j, k) pa_h_intz_L = pa(i - 1, j, k + 1)*h_L + intz_dpa(i - 1, j, k) pa_h_intz_R = pa(i, j, k + 1)*h_R + intz_dpa(i, j, k) numer = (pa_h_intz_L - pa_h_intz_R) & + (h_R - h_L)*intx_pa(i, j, k + 1) & - (e_bot_R - e_bot_L)*intx_dpa(i, j, k) denom = h_L + h_R + h_neglect dpdx_face(i, j, k) = numer*(2.0_wp*inv_rho0*idxCu(i, j))/denom end do do concurrent(k=1:nz, j=1:ny) dpdx_face(1, j, k) = 0.0_wp dpdx_face(nx + 1, j, k) = 0.0_wp end do ! ---- Pass 4: PFv assembly ---- do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(h_L, h_R, e_bot_L, e_bot_R, pa_h_intz_L, pa_h_intz_R, numer, denom) h_L = h_layer(i, j - 1, k) h_R = h_layer(i, j, k) e_bot_L = e_face(i, j - 1, k) e_bot_R = e_face(i, j, k) pa_h_intz_L = pa(i, j - 1, k + 1)*h_L + intz_dpa(i, j - 1, k) pa_h_intz_R = pa(i, j, k + 1)*h_R + intz_dpa(i, j, k) numer = (pa_h_intz_L - pa_h_intz_R) & + (h_R - h_L)*inty_pa(i, j, k + 1) & - (e_bot_R - e_bot_L)*inty_dpa(i, j, k) denom = h_L + h_R + h_neglect dpdy_face(i, j, k) = numer*(2.0_wp*inv_rho0*idyCv(i, j))/denom end do do concurrent(k=1:nz, i=1:nx) dpdy_face(i, 1, k) = 0.0_wp dpdy_face(i, ny + 1, k) = 0.0_wp end do ! ---- Pass 5: Montgomery dM correction (MOM6 GFS_scale) ---- ! rho_surf for the dM term is the reconstructed top-edge density of ! the surface layer; here we reuse the layer-mean surface density ! recovered from dpa(nz) = pa(nz) - pa(nz+1) divided by g*h, which ! equals (rho_surf - rho_ref). Keep the same depth-independent form. if (gfs_scale < 1.0_wp - 1.0e-12_wp) then dM_coeff = (gfs_scale - 1.0_wp)*GRAVITY*inv_rho0 do concurrent(k=1:nz, j=1:ny, i=2:nx) local(ddM_dx) ddM_dx = dM_coeff*(recon_rho_surf(pa(i, j, nz), pa(i, j, nz + 1), & h_layer(i, j, nz), rho_ref) & *e_face(i, j, nz + 1) & - recon_rho_surf(pa(i - 1, j, nz), pa(i - 1, j, nz + 1), & h_layer(i - 1, j, nz), rho_ref) & *e_face(i - 1, j, nz + 1))*idxCu(i, j) dpdx_face(i, j, k) = dpdx_face(i, j, k) - ddM_dx end do do concurrent(k=1:nz, j=2:ny, i=1:nx) local(ddM_dy) ddM_dy = dM_coeff*(recon_rho_surf(pa(i, j, nz), pa(i, j, nz + 1), & h_layer(i, j, nz), rho_ref) & *e_face(i, j, nz + 1) & - recon_rho_surf(pa(i, j - 1, nz), pa(i, j - 1, nz + 1), & h_layer(i, j - 1, nz), rho_ref) & *e_face(i, j - 1, nz + 1))*idyCv(i, j) dpdy_face(i, j, k) = dpdy_face(i, j, k) - ddM_dy end do end if end subroutine compute_fv_mom6_reconstruct_impl pure function use_insitu_pcm(pgf, ms, eos) result(yes) !! Does the FV_MOM6 constant-by-layer branch take the IN-SITU !! density path (`compute_fv_mom6_insitu_pcm_impl`)? Only when it !! can change the answer: the knob is on, there is an EOS handle and !! T/S to evaluate it on, and the EOS depends on pressure. For the !! linear EOS `ms%rho_layer` already IS the in-situ density, so the !! legacy path runs, bit-identical. type(ocean_pressure_force_t), intent(in) :: pgf type(multilayer_state_t), intent(in) :: ms type(eos_t), intent(in), optional :: eos logical :: yes yes = .false. if (.not. pgf%insitu_density) return if (.not. present(eos)) return if (ms%idx_salinity <= 0 .or. ms%idx_temperature <= 0) return yes = eos%variant == EOS_VARIANT_WRIGHT_97 .or. & eos%variant == EOS_VARIANT_ROQUET_SPV end function use_insitu_pcm pure subroutine compute_fv_mom6_insitu_pcm_impl(h_layer, hS, hT, b, conc_T, conc_S, & e_face, pa, intz_dpa, & intx_pa, inty_pa, & intx_dpa, inty_dpa, & dpdx_face, dpdy_face, & rho0, rho_ref, h_neglect, & gfs_scale, mass_weight, & eos_variant, & p_top, p_top_in_bc, & idxCu, idyCv, nx, ny, nz) !! FV_MOM6 pressure gradient, constant-by-layer (PCM) T/S, density at !! the IN-SITU pressure — MOM6 `PressureForce_FV_Bouss` with !! `RECONSTRUCT_FOR_PRESSURE = False` (`int_density_dz_generic_pcm`). !! !! The PCM twin `compute_fv_mom6_impl` integrates `ms%rho_layer`, a !! POTENTIAL density at the one horizontally uniform `p_ref`. Its !! horizontal difference at depth is then the difference at the !! REFERENCE pressure, not at the local one: the thermal expansion !! coefficient roughly doubles between the surface and 4000 dbar !! (thermobaricity), so with `p_ref = 0` the deep baroclinic !! pressure gradient — the bottom-pressure gradient that forces the !! barotropic mode over topography — is systematically too weak. !! On the global 1-degree WOA13 spin-up it held Drake Passage at !! ~80 Sv where MOM6 on the same protocol adjusts to ~155 Sv, and !! the transport tracked `p_ref` (0 / 2000 / 4000 dbar: 83 / 143 / !! 203 Sv) — the tell of a reference-pressure artefact. !! !! Here every density is `EOS(T, S, p = −g·rho0·z)` at the point it !! is used, integrated (Roquet; Wright takes the closed form below) !! by the same 5-point Boole rules as the reconstruction branch, with !! the sub-layer profile flat: !! !! * Pass 1 (per column): `dpa(k)`, `intz_dpa(k)` from !! `boole_dpa_intz_layer` with top = bottom = mean T/S. !! * Pass 2 (per face): `intx_dpa` / `inty_dpa` from !! `boole_dpa_face_pcm` — end points are the columns' own `dpa`, !! the three interior sub-columns interpolate `z` linearly and !! T/S with MOM6's near-bottom mass weighting (`hWght`, the same !! measure and blend as `compute_fv_mom6_impl`) when !! `mass_weight`. !! * Passes 3–5: the face assembly, identical to the other two !! FV_MOM6 branches. !! !! Under Wright (`wright_analytic`) Passes 1–2 replace each vertical !! Boole rule by the closed-form integral `wright_pcm_dpa_intz` (MOM6 !! `int_density_dz_wright`), keeping the 5-point cross-face Boole !! rule and the same sub-columns (`wright_pcm_dpa_face`). The !! vertical Boole rule it replaces is accurate to !! `(g·rho0·dz/(p + p0 + lambda/alpha0))^6` — round-off for any !! realistic layer — so the answers move at round-off, but each layer !! costs one polynomial evaluation instead of 5 generic-EOS calls and !! each face 3 instead of 15. Measured on the global 1° run (5 !! days, one V100): `ocean_pgf` 10.26 s → 1.90 s, against 1.09 s for !! `insitu_density = .false.`. !! !! Under Roquet the vertical rule stays 5-point Boole (no closed form !! for `int dz/SV(p)`), factored: `roquet_pcm_dpa_intz` evaluates the !! (T, S) part of the SpV polynomial once per sub-column and only the !! pressure Horner per point — the same five densities as !! `boole_dpa_intz_layer`, so answers are unchanged (to the digit on !! the global 1° run's `[stats]` and En). `ocean_pgf` 10.73 s → !! 3.64 s on the same run, → 2.55 s (1.36x Wright) with the SpV value !! from the module-local include `rdb_roquet_spv.inc`: as a call into !! `rdb_eos` the (T, S) part was NOT inlined on the device (a real !! `call`, its four results through the stack, 130+ registers in the !! face kernels against 88 inlined). !! !! Pass 1 and Pass 2 each run their integrals as a 3-D `do concurrent` !! over (k, j, i) — every layer's and every face's integral is !! independent — followed by a cheap per-column scan for the `pa` / !! `intx_pa` / `inty_pa` recurrences (Pass 1 parks `dpa` in `pa(k)` !! and sums it in place). Same operations in the same order, so !! bit-identical to the column-serial form; one GPU thread per CELL !! instead of per column. !! !! The trapezoid `0.5·(dpa_L + dpa_R)` of the potential-density twin !! is NOT kept: an in-situ density carries the compressibility !! gradient (`~4.4e-3 kg m⁻⁴`), and the trapezoid's curvature !! residual `g·(∂ρ/∂z)·Δe²/12` at a tilted interface (a partial-cell !! bed step) would be of the size of the signal. integer, intent(in) :: nx, ny, nz real(wp), intent(in) :: h_layer(nx, ny, nz) real(wp), intent(in) :: hS(nx, ny, nz) !! Salinity * thickness (PSU*m) — layer-mean S = hS / h. real(wp), intent(in) :: hT(nx, ny, nz) !! Temperature * thickness (degC*m) — layer-mean T = hT / h. real(wp), intent(in) :: b(nx, ny) real(wp), intent(inout) :: conc_T(nx, ny, nz), conc_S(nx, ny, nz) !! Layer-mean T / S as every pass below reads them (Pass C). real(wp), intent(inout) :: e_face(nx, ny, nz + 1) real(wp), intent(inout) :: pa(nx, ny, nz + 1) real(wp), intent(inout) :: intz_dpa(nx, ny, nz) real(wp), intent(inout) :: intx_pa(nx + 1, ny, nz + 1) real(wp), intent(inout) :: inty_pa(nx, ny + 1, nz + 1) real(wp), intent(inout) :: intx_dpa(nx + 1, ny, nz) real(wp), intent(inout) :: inty_dpa(nx, ny + 1, nz) real(wp), intent(inout) :: dpdx_face(nx + 1, ny, nz) real(wp), intent(inout) :: dpdy_face(nx, ny + 1, nz) real(wp), intent(in) :: rho0, rho_ref, h_neglect, gfs_scale logical, intent(in) :: mass_weight !! MOM6 `MASS_WEIGHT_IN_PRESSURE_GRADIENT` (near-bottom `hWght`). integer, intent(in) :: eos_variant !! `eos%variant`, selected ONCE here, outside the loops -- each !! variant has its own loop copies with its integrals inlined, no !! generic `eos_t` dispatch in the hot loop. `use_insitu_pcm` !! admits exactly two: !! * `EOS_VARIANT_WRIGHT_97`: the ANALYTIC Wright layer integral !! (`wright_pcm_dpa_intz` / `wright_pcm_dpa_face`, MOM6 !! `int_density_dz_wright`). !! * `EOS_VARIANT_ROQUET_SPV` (the `else` copies): the 5-point !! Boole rule of MOM6 `int_density_dz_generic_pcm`, with the !! (T, S) part of the EOS evaluated once per sub-column !! (`roquet_pcm_dpa_intz` / `roquet_pcm_dpa_face`). real(wp), intent(in) :: p_top(nx, ny) !! Top-of-column pressure (Pa, `>= 0`), `multilayer_state_t%p_top`. logical, intent(in) :: p_top_in_bc !! Add `p_top` to the Pass-1 surface BC (`.false.` ⇒ the plain !! `rho_ref·g·eta` seed). real(wp), intent(in) :: idxCu(nx + 1, ny), idyCv(nx, ny + 1) integer :: i, j, k real(wp) :: inv_rho0, dpa_kk, intz_kk, t_m, s_m real(wp) :: t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R real(wp) :: hwt_ll, hwt_lr, hwt_rr, hwt_rl real(wp) :: h_L, h_R, e_bot_L, e_bot_R real(wp) :: pa_h_intz_L, pa_h_intz_R, numer, denom real(wp) :: dM_coeff, ddM_dx, ddM_dy logical :: wright_analytic integer :: k_top_c, k_don_c inv_rho0 = 1.0_wp/rho0 wright_analytic = (eos_variant == EOS_VARIANT_WRIGHT_97) ! ---- Pass C (per column): the layer-mean T/S every pass reads ---- ! `hT/h` on a live layer and, on a vanished one, its I1′ DONOR's: ! the nearest live layer above it, or for a run of fillers reaching ! the top of the column the topmost live layer; 0 in a column with ! no live layer. The per-column form of `rdb_vl_column_conc`, read ! off the donor so no near-zero thickness is ever a divisor. MOM6 ! carries T/S as concentrations, so its vanished layers hold the ! remapped value its `int_density_dz_*` reads; `c_live` is that value ! here. NOT the floored `hT/max(h, H_VANISHED)` this replaced: that ! is `h/H_VANISHED` of the truth on a filler (2/3 at the default ! `zstar_h_min = 1e-4 m`), harmless in the vertical `pa` stack where ! it multiplies the filler's own thickness, but the cross-face Boole ! integral interpolates T/S over the INTERPOLATED -- live -- ! thickness and the PLM/PPM stencil reads its neighbours, so at an ! OPEN z-like step (`zstar`, closed-faces-off `z_fixed`, a `z_fixed` ! cell whose liveness flipped with eta under a static closed-face ! mask) it integrated the wrong salinity over tens of metres of live ! water: 2.9e-3 m/s^2 at rest on a live|filler face against 1.4e-6 ! (`test_ocean_pgf_insitu :: open_step_filler_faces_*`). One O(nz) ! sweep per column, the shape of Pass 1a: a per-cell donor walk made ! `ocean_pgf` 4x slower on the global 1-degree grid (long bed-filler ! runs). A live layer reads `hT/h` exactly as before (bit-identical ! on a column without fillers). do concurrent(j=1:ny, i=1:nx) local(k, k_top_c, k_don_c) k_top_c = 0 do k = nz, 1, -1 if (rdb_vl_is_live(h_layer(i, j, k))) then k_top_c = k exit end if end do if (k_top_c == 0) then do k = 1, nz conc_T(i, j, k) = 0.0_wp conc_S(i, j, k) = 0.0_wp end do else k_don_c = k_top_c do k = nz, 1, -1 if (rdb_vl_is_live(h_layer(i, j, k))) k_don_c = k conc_T(i, j, k) = rdb_vl_conc(hT(i, j, k_don_c), h_layer(i, j, k_don_c)) conc_S(i, j, k) = rdb_vl_conc(hS(i, j, k_don_c), h_layer(i, j, k_don_c)) end do end if end do ! ---- Pass 1: per-column e_face, pa, intz_dpa (in-situ) ---- ! Pass 1a (per column): interface heights and the surface seed of ! the pressure-anomaly stack. do concurrent(j=1:ny, i=1:nx) local(k) e_face(i, j, 1) = -b(i, j) do k = 1, nz e_face(i, j, k + 1) = e_face(i, j, k) + h_layer(i, j, k) end do if (p_top_in_bc) then pa(i, j, nz + 1) = rho_ref*GRAVITY*e_face(i, j, nz + 1) + p_top(i, j) else pa(i, j, nz + 1) = rho_ref*GRAVITY*e_face(i, j, nz + 1) end if end do ! Pass 1b (3-D parallel): every layer's `dpa` / `intz_dpa`. `dpa` ! is parked in `pa(i, j, k)` and summed by Pass 1c. Layer by layer ! the EOS work is independent; only the stack sum is a recurrence, so ! the expensive part no longer runs one thread per COLUMN (115k ! threads, latency-bound on the global 1-degree grid) but one per ! CELL. ! Two loop copies (not a branch inside one kernel) so the analytic ! Wright kernel carries none of the Boole path's register pressure. if (wright_analytic) then do concurrent(k=1:nz, j=1:ny, i=1:nx) local(dpa_kk, intz_kk, t_m, s_m) t_m = conc_T(i, j, k) s_m = conc_S(i, j, k) call wright_pcm_dpa_intz(t_m, s_m, e_face(i, j, k + 1), h_layer(i, j, k), & rho0, rho_ref, dpa_kk, intz_kk) pa(i, j, k) = dpa_kk intz_dpa(i, j, k) = intz_kk end do else do concurrent(k=1:nz, j=1:ny, i=1:nx) local(dpa_kk, intz_kk, t_m, s_m) t_m = conc_T(i, j, k) s_m = conc_S(i, j, k) call roquet_pcm_dpa_intz(t_m, s_m, e_face(i, j, k + 1), h_layer(i, j, k), & rho0, rho_ref, dpa_kk, intz_kk) pa(i, j, k) = dpa_kk intz_dpa(i, j, k) = intz_kk end do end if ! Pass 1c (per column): the pressure-anomaly stack, top down. do concurrent(j=1:ny, i=1:nx) local(k) do k = nz, 1, -1 pa(i, j, k) = pa(i, j, k + 1) + pa(i, j, k) end do end do ! ---- Pass 2a: u-face horizontal integrals ---- if (wright_analytic) then do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, hwt_ll, hwt_lr, hwt_rr, hwt_rl) call fv_mom6_mass_weights(mass_weight, e_face(i - 1, j, 1), e_face(i, j, 1), & e_face(i - 1, j, k + 1), e_face(i, j, k + 1), & h_layer(i - 1, j, k), h_layer(i, j, k), h_neglect, & hwt_ll, hwt_lr, hwt_rr, hwt_rl) t_m_L = conc_T(i - 1, j, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i - 1, j, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i - 1, j, k) - pa(i - 1, j, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call wright_pcm_dpa_face(e_face(i - 1, j, k + 1), e_face(i, j, k + 1), & h_layer(i - 1, j, k), h_layer(i, j, k), & t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, & hwt_ll, hwt_lr, hwt_rr, hwt_rl, rho0, rho_ref, dpa_kk) intx_dpa(i, j, k) = dpa_kk end do else do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, hwt_ll, hwt_lr, hwt_rr, hwt_rl) call fv_mom6_mass_weights(mass_weight, e_face(i - 1, j, 1), e_face(i, j, 1), & e_face(i - 1, j, k + 1), e_face(i, j, k + 1), & h_layer(i - 1, j, k), h_layer(i, j, k), h_neglect, & hwt_ll, hwt_lr, hwt_rr, hwt_rl) t_m_L = conc_T(i - 1, j, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i - 1, j, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i - 1, j, k) - pa(i - 1, j, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call roquet_pcm_dpa_face(e_face(i - 1, j, k + 1), e_face(i, j, k + 1), & h_layer(i - 1, j, k), h_layer(i, j, k), & t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, & hwt_ll, hwt_lr, hwt_rr, hwt_rl, rho0, rho_ref, dpa_kk) intx_dpa(i, j, k) = dpa_kk end do end if ! Column scan of the face integrals (cheap; the EOS work above is ! 3-D parallel). do concurrent(j=1:ny, i=2:nx) local(k) intx_pa(i, j, nz + 1) = 0.5_wp*(pa(i - 1, j, nz + 1) + pa(i, j, nz + 1)) do k = nz, 1, -1 intx_pa(i, j, k) = intx_pa(i, j, k + 1) + intx_dpa(i, j, k) end do end do do concurrent(k=1:nz, j=1:ny) intx_dpa(1, j, k) = 0.0_wp intx_dpa(nx + 1, j, k) = 0.0_wp end do do concurrent(k=1:nz + 1, j=1:ny) intx_pa(1, j, k) = 0.0_wp intx_pa(nx + 1, j, k) = 0.0_wp end do ! ---- Pass 2b: v-face horizontal integrals ---- if (wright_analytic) then do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, hwt_ll, hwt_lr, hwt_rr, hwt_rl) call fv_mom6_mass_weights(mass_weight, e_face(i, j - 1, 1), e_face(i, j, 1), & e_face(i, j - 1, k + 1), e_face(i, j, k + 1), & h_layer(i, j - 1, k), h_layer(i, j, k), h_neglect, & hwt_ll, hwt_lr, hwt_rr, hwt_rl) t_m_L = conc_T(i, j - 1, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i, j - 1, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i, j - 1, k) - pa(i, j - 1, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call wright_pcm_dpa_face(e_face(i, j - 1, k + 1), e_face(i, j, k + 1), & h_layer(i, j - 1, k), h_layer(i, j, k), & t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, & hwt_ll, hwt_lr, hwt_rr, hwt_rl, rho0, rho_ref, dpa_kk) inty_dpa(i, j, k) = dpa_kk end do else do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(dpa_kk, t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, hwt_ll, hwt_lr, hwt_rr, hwt_rl) call fv_mom6_mass_weights(mass_weight, e_face(i, j - 1, 1), e_face(i, j, 1), & e_face(i, j - 1, k + 1), e_face(i, j, k + 1), & h_layer(i, j - 1, k), h_layer(i, j, k), h_neglect, & hwt_ll, hwt_lr, hwt_rr, hwt_rl) t_m_L = conc_T(i, j - 1, k) t_m_R = conc_T(i, j, k) s_m_L = conc_S(i, j - 1, k) s_m_R = conc_S(i, j, k) dpa_L = pa(i, j - 1, k) - pa(i, j - 1, k + 1) dpa_R = pa(i, j, k) - pa(i, j, k + 1) call roquet_pcm_dpa_face(e_face(i, j - 1, k + 1), e_face(i, j, k + 1), & h_layer(i, j - 1, k), h_layer(i, j, k), & t_m_L, t_m_R, s_m_L, s_m_R, dpa_L, dpa_R, & hwt_ll, hwt_lr, hwt_rr, hwt_rl, rho0, rho_ref, dpa_kk) inty_dpa(i, j, k) = dpa_kk end do end if ! Column scan of the face integrals (cheap; the EOS work above is ! 3-D parallel). do concurrent(j=2:ny, i=1:nx) local(k) inty_pa(i, j, nz + 1) = 0.5_wp*(pa(i, j - 1, nz + 1) + pa(i, j, nz + 1)) do k = nz, 1, -1 inty_pa(i, j, k) = inty_pa(i, j, k + 1) + inty_dpa(i, j, k) end do end do do concurrent(k=1:nz, i=1:nx) inty_dpa(i, 1, k) = 0.0_wp inty_dpa(i, ny + 1, k) = 0.0_wp end do do concurrent(k=1:nz + 1, i=1:nx) inty_pa(i, 1, k) = 0.0_wp inty_pa(i, ny + 1, k) = 0.0_wp end do ! ---- Pass 3: PFu assembly (identical to compute_fv_mom6_impl) ---- do concurrent(k=1:nz, j=1:ny, i=2:nx) & local(h_L, h_R, e_bot_L, e_bot_R, pa_h_intz_L, pa_h_intz_R, numer, denom) h_L = h_layer(i - 1, j, k) h_R = h_layer(i, j, k) e_bot_L = e_face(i - 1, j, k) e_bot_R = e_face(i, j, k) pa_h_intz_L = pa(i - 1, j, k + 1)*h_L + intz_dpa(i - 1, j, k) pa_h_intz_R = pa(i, j, k + 1)*h_R + intz_dpa(i, j, k) numer = (pa_h_intz_L - pa_h_intz_R) & + (h_R - h_L)*intx_pa(i, j, k + 1) & - (e_bot_R - e_bot_L)*intx_dpa(i, j, k) denom = h_L + h_R + h_neglect dpdx_face(i, j, k) = numer*(2.0_wp*inv_rho0*idxCu(i, j))/denom end do do concurrent(k=1:nz, j=1:ny) dpdx_face(1, j, k) = 0.0_wp dpdx_face(nx + 1, j, k) = 0.0_wp end do ! ---- Pass 4: PFv assembly ---- do concurrent(k=1:nz, j=2:ny, i=1:nx) & local(h_L, h_R, e_bot_L, e_bot_R, pa_h_intz_L, pa_h_intz_R, numer, denom) h_L = h_layer(i, j - 1, k) h_R = h_layer(i, j, k) e_bot_L = e_face(i, j - 1, k) e_bot_R = e_face(i, j, k) pa_h_intz_L = pa(i, j - 1, k + 1)*h_L + intz_dpa(i, j - 1, k) pa_h_intz_R = pa(i, j, k + 1)*h_R + intz_dpa(i, j, k) numer = (pa_h_intz_L - pa_h_intz_R) & + (h_R - h_L)*inty_pa(i, j, k + 1) & - (e_bot_R - e_bot_L)*inty_dpa(i, j, k) denom = h_L + h_R + h_neglect dpdy_face(i, j, k) = numer*(2.0_wp*inv_rho0*idyCv(i, j))/denom end do do concurrent(k=1:nz, i=1:nx) dpdy_face(i, 1, k) = 0.0_wp dpdy_face(i, ny + 1, k) = 0.0_wp end do ! ---- Pass 5: Montgomery dM correction (MOM6 GFS_scale) ---- ! Same depth-independent form as the reconstruction branch, with the ! surface layer's mean in-situ density recovered from its `dpa`. if (gfs_scale < 1.0_wp - 1.0e-12_wp) then dM_coeff = (gfs_scale - 1.0_wp)*GRAVITY*inv_rho0 do concurrent(k=1:nz, j=1:ny, i=2:nx) local(ddM_dx) ddM_dx = dM_coeff*(recon_rho_surf(pa(i, j, nz), pa(i, j, nz + 1), & h_layer(i, j, nz), rho_ref) & *e_face(i, j, nz + 1) & - recon_rho_surf(pa(i - 1, j, nz), pa(i - 1, j, nz + 1), & h_layer(i - 1, j, nz), rho_ref) & *e_face(i - 1, j, nz + 1))*idxCu(i, j) dpdx_face(i, j, k) = dpdx_face(i, j, k) - ddM_dx end do do concurrent(k=1:nz, j=2:ny, i=1:nx) local(ddM_dy) ddM_dy = dM_coeff*(recon_rho_surf(pa(i, j, nz), pa(i, j, nz + 1), & h_layer(i, j, nz), rho_ref) & *e_face(i, j, nz + 1) & - recon_rho_surf(pa(i, j - 1, nz), pa(i, j - 1, nz + 1), & h_layer(i, j - 1, nz), rho_ref) & *e_face(i, j - 1, nz + 1))*idyCv(i, j) dpdy_face(i, j, k) = dpdy_face(i, j, k) - ddM_dy end do end if end subroutine compute_fv_mom6_insitu_pcm_impl pure subroutine wright_pcm_dpa_intz(t, s, e_top, dz, rho0, rho_ref, dpa, intz_dpa) !$acc routine seq !! ANALYTIC vertical integral of the Wright (1997) in-situ density !! anomaly over one constant-T/S (PCM) layer — MOM6 !! `int_density_dz_wright` (reduced-range coefficients, MOM6 !! `EQN_OF_STATE = "WRIGHT"` / `"WRIGHT_RED"`, the set `rdb_eos` !! carries). Replaces the 5-point Boole quadrature of !! `boole_dpa_intz_layer` (5 generic-EOS evaluations) with one !! polynomial evaluation, one division pair and a short series. !! !! With `P = p + p0(T,S)`, `L = lambda/alpha0` and the Boussinesq !! pressure `p = -g*rho0*z`, Wright's density is !! rho = P/(lambda + alpha0*P) = (1/alpha0)*(1 - L/(P + L)), !! so along the layer (T, S fixed) !! int rho dz = dz/alpha0 - (lambda/alpha0^2)/(g*rho0) * ln((1+eps)/(1-eps)), !! `eps = (g*rho0*dz/2)/(P_mid + L)` the half-layer pressure change !! over the layer-mean `P + L`. Expanding the log about the !! layer midpoint, `ln((1+eps)/(1-eps)) = 2*(eps + eps^3/3 + eps^5/5 !! + ...)`, the leading term is exactly `dz*rho(P_mid)`, leaving the !! remainder `rem = (lambda/alpha0^2)/rho0 * eps^2*(1/3 + eps^2/5 + !! eps^4/7 + eps^6/9)`: !! dpa = g*(rho(P_mid) - rho_ref)*dz - 2*eps*rem !! intz_dpa = 0.5*g*(rho(P_mid) - rho_ref)*dz^2 - dz*(1 + eps)*rem !! (`intz_dpa` = the layer integral of the pressure anomaly relative !! to its value at the layer TOP — the same moment !! `boole_dpa_intz_layer` returns). The series is truncated after !! `eps^8` inside `rem`: `P + L >= ~8e8 Pa` for sea water, so even a !! 6000 m layer has `eps < 0.04` and the dropped `eps^10/11` term is !! ~1e-15 of `rem` — round-off. MOM6 states the truncation valid for !! `|eps| < 0.34`. real(wp), intent(in) :: t !! Layer temperature (degC), constant through the layer. real(wp), intent(in) :: s !! Layer salinity (PSU), constant through the layer. real(wp), intent(in) :: e_top !! Height of the SHALLOWER interface (m, geopotential, negative !! below the datum). real(wp), intent(in) :: dz !! Layer thickness (m, >= 0); the layer spans `[e_top - dz, e_top]`. real(wp), intent(in) :: rho0 !! Boussinesq reference density of the pressure estimate (kg/m^3). real(wp), intent(in) :: rho_ref !! Anomaly reference subtracted from the in-situ density (kg/m^3). real(wp), intent(out) :: dpa !! `g * int (rho - rho_ref) dz` over the layer (Pa). real(wp), intent(out) :: intz_dpa !! First moment from the top (Pa*m). real(wp), parameter :: C1_3 = 1.0_wp/3.0_wp, C1_7 = 1.0_wp/7.0_wp real(wp), parameter :: C1_9 = 1.0_wp/9.0_wp real(wp) :: al0, p0, lam, gxrho, p_ave, big_p, i_d, half_dp_d, eps, eps2 real(wp) :: rho_anom, rem al0 = WRIGHT_A0 + (WRIGHT_A1*t + WRIGHT_A2*s) p0 = WRIGHT_B0 + (WRIGHT_B4*s + t*(WRIGHT_B1 + (t*(WRIGHT_B2 + WRIGHT_B3*t) & + WRIGHT_B5*s))) lam = WRIGHT_C0 + (WRIGHT_C4*s + t*(WRIGHT_C1 + (t*(WRIGHT_C2 + WRIGHT_C3*t) & + WRIGHT_C5*s))) ! One division: with D = alpha0*P + lambda (P = p0 + p_ave), ! rho(P) = P/D, 1/(P + L) = alpha0/D, ! (lambda/alpha0^2)*eps^2 = lambda*(half_dp/D)^2, ! algebraically MOM6's `I_al0`/`I_Lzz` form without 1/alpha0. gxrho = GRAVITY*rho0 p_ave = -gxrho*(e_top - 0.5_wp*dz) big_p = p0 + p_ave i_d = 1.0_wp/(al0*big_p + lam) half_dp_d = 0.5_wp*(gxrho*dz)*i_d eps = al0*half_dp_d eps2 = eps*eps rho_anom = big_p*i_d - rho_ref rem = (lam/rho0)*(half_dp_d*half_dp_d) & *(C1_3 + eps2*(0.2_wp + eps2*(C1_7 + C1_9*eps2))) dpa = (GRAVITY*rho_anom)*dz - 2.0_wp*eps*rem intz_dpa = 0.5_wp*(GRAVITY*rho_anom)*dz*dz - dz*((1.0_wp + eps)*rem) end subroutine wright_pcm_dpa_intz pure subroutine wright_pcm_dpa_face(e_top_l, e_top_r, dz_l, dz_r, & t_l, t_r, s_l, s_r, dpa_l, dpa_r, & hwt_ll, hwt_lr, hwt_rr, hwt_rl, & rho0, rho_ref, dpa_face) !$acc routine seq !! Cross-face 5-point Boole quadrature of the layer `dpa` for a PCM !! column pair with the ANALYTIC Wright vertical integral at every !! lateral sub-column (MOM6 `int_density_dz_wright`, `intx_dpa` / !! `inty_dpa`). The Wright twin of `boole_dpa_face_pcm`, with the !! same sub-column definition: the end points are the columns' own !! `dpa_l` / `dpa_r`; the three interior sub-columns interpolate the !! interface height and thickness linearly in the cross-face !! fraction and T/S with the near-bottom mass-weighted fractions !! `wtT_L = wl*hwt_ll + wr*hwt_rl`, `wtT_R = wl*hwt_lr + wr*hwt_rr`. !! !! (MOM6 interpolates the Wright polynomial TERMS `alpha0, p0, !! lambda` rather than T and S. Interpolating T/S keeps the !! sub-column identical to the Boole path's, so switching the vertical !! rule is the only change; the two differ by the polynomials' !! curvature, `O(wl*wr*dT^2)`, which is zero on a resting column.) real(wp), intent(in) :: e_top_l, e_top_r !! Shallower-interface heights in the left / right column (m). real(wp), intent(in) :: dz_l, dz_r !! Layer thicknesses in the left / right column (m, >= 0). real(wp), intent(in) :: t_l, t_r, s_l, s_r !! Layer-mean temperature / salinity in the left / right column. real(wp), intent(in) :: dpa_l, dpa_r !! The columns' own `g * int rho' dz` over the layer (Pa). real(wp), intent(in) :: hwt_ll, hwt_lr, hwt_rr, hwt_rl !! MOM6 `hWt_LL/LR/RR/RL` mass-weighting fractions. real(wp), intent(in) :: rho0, rho_ref !! Boussinesq pressure-estimate density / anomaly reference. real(wp), intent(out) :: dpa_face !! Along-face mean of `g * int rho' dz` over the layer (Pa). real(wp) :: wr, wl, wtt_l, wtt_r, dpa_m, intz_m, acc integer :: m acc = 7.0_wp*(dpa_l + dpa_r) ! (`GCC$ unroll`: gfortran unrolls the three sub-columns completely ! before vectorising, so the face loops over this vectorise on the CPU; ! nvfortran keeps the loop, which costs fewer registers on the device.) !GCC$ unroll 3 do m = 2, 4 wr = 0.25_wp*real(m - 1, wp) ! 0 at the left column .. 1 at the right wl = 1.0_wp - wr wtt_l = wl*hwt_ll + wr*hwt_rl wtt_r = wl*hwt_lr + wr*hwt_rr call wright_pcm_dpa_intz(wtt_l*t_l + wtt_r*t_r, wtt_l*s_l + wtt_r*s_r, & wl*e_top_l + wr*e_top_r, wl*dz_l + wr*dz_r, & rho0, rho_ref, dpa_m, intz_m) if (m == 3) then acc = acc + 12.0_wp*dpa_m else acc = acc + 32.0_wp*dpa_m end if end do dpa_face = acc/90.0_wp end subroutine wright_pcm_dpa_face pure subroutine roquet_pcm_dpa_intz(t, s, e_top, dz, rho0, rho_ref, dpa, intz_dpa) !$acc routine seq !! Vertical integral of the Roquet et al. (2015) SpV in-situ density !! anomaly over one constant-T/S (PCM) layer: the 5-point Boole rule !! of `boole_dpa_intz_layer` (MOM6 `int_density_dz_generic_pcm`, which !! is also what MOM6 runs for `ROQUET_SPV` under a Boussinesq PGF), !! with the EOS FACTORED. !! !! WHY NOT A CLOSED FORM. The Boussinesq FV PGF integrates DENSITY in !! height, `int rho dz` with `p = -g*rho0*z`. Roquet's polynomial is !! in SPECIFIC VOLUME, `SV(T, S, p)` -- a degree-6 polynomial in `p` !! -- so the integrand is `1/SV(p)`, a rational function of `z` with !! no useful antiderivative (partial fractions over six complex !! roots). The integral that IS exact in closed form, `int SV dp`, is !! the NON-Boussinesq one (MOM6 `int_spec_vol_dp`); roundabout does not !! integrate it. MOM6 sets `EOS_QUADRATURE = True` by default for !! every Roquet/TEOS-10 form for the same reason. !! !! WHAT IS FACTORED. In a PCM layer T and S are the same at all five !! Boole points, so everything expensive in the EOS -- two sqrt, the !! PT->CT polynomial, the ~50-term (T, S) sums -- is the same at all !! five. `rdb_roq_ts_coeffs` computes it ONCE; each point then !! costs the degree-6 pressure Horner and one division. The five !! densities are the numbers `boole_dpa_intz_layer` evaluates (up to !! its `wt_t*t + wt_b*t` blend of equal edge values, which can move !! T by an ulp), and the Boole weights and moment are its own, so !! `dpa` / `intz_dpa` agree with the generic path to round-off. !! Cost per layer: 1 T/S polynomial + 5 pressure Horners, against 5 !! full `roquet_spv_point` calls (each also computing the derivatives !! the density throws away) through the generic `eos_t` dispatch. !! !! ACCURACY. The rule's truncation error is set by the pressure !! curvature of `1/SV` over the layer: at most 1.1e-13 of `g*rho0*dz` !! for any layer up to 1000 m thick and 8e-10 for the whole 6000 m !! column as one layer (`test_ocean_pgf_eos_fast`, against 64-panel !! Gauss-Legendre of the generic EOS). real(wp), intent(in) :: t !! Layer potential temperature (degC), constant through the layer. real(wp), intent(in) :: s !! Layer practical salinity (PSU), constant through the layer. real(wp), intent(in) :: e_top !! Height of the SHALLOWER interface (m, geopotential). real(wp), intent(in) :: dz !! Layer thickness (m, >= 0); the layer spans `[e_top - dz, e_top]`. real(wp), intent(in) :: rho0 !! Boussinesq reference density of the pressure estimate (kg/m^3). real(wp), intent(in) :: rho_ref !! Anomaly reference subtracted from the in-situ density (kg/m^3). real(wp), intent(out) :: dpa !! `g * int (rho - rho_ref) dz` over the layer (Pa). real(wp), intent(out) :: intz_dpa !! First moment from the top (Pa*m), as `boole_dpa_intz_layer`. real(wp) :: sv0, sv1, sv2, sv3, gxrho, rho_anom real(wp) :: r1, r2, r3, r4, r5 call rdb_roq_ts_coeffs(t, s, sv0, sv1, sv2, sv3) gxrho = GRAVITY*rho0 ! The five Boole points, top (n = 1) to bottom (n = 5), at the same ! `z5 = e_top - 0.25*(n-1)*dz` as `boole_dpa_intz_layer`. r1 = 1.0_wp/rdb_roq_spv_p(sv0, sv1, sv2, sv3, -gxrho*e_top) - rho_ref r2 = 1.0_wp/rdb_roq_spv_p(sv0, sv1, sv2, sv3, -gxrho*(e_top - 0.25_wp*dz)) - rho_ref r3 = 1.0_wp/rdb_roq_spv_p(sv0, sv1, sv2, sv3, -gxrho*(e_top - 0.5_wp*dz)) - rho_ref r4 = 1.0_wp/rdb_roq_spv_p(sv0, sv1, sv2, sv3, -gxrho*(e_top - 0.75_wp*dz)) - rho_ref r5 = 1.0_wp/rdb_roq_spv_p(sv0, sv1, sv2, sv3, -gxrho*(e_top - dz)) - rho_ref rho_anom = (1.0_wp/90.0_wp)*(7.0_wp*(r1 + r5) + 32.0_wp*(r2 + r4) + 12.0_wp*r3) dpa = GRAVITY*dz*rho_anom intz_dpa = 0.5_wp*GRAVITY*dz*dz*(rho_anom & - (1.0_wp/90.0_wp)*(16.0_wp*(r4 - r2) + 7.0_wp*(r5 - r1))) end subroutine roquet_pcm_dpa_intz pure subroutine roquet_pcm_dpa_face(e_top_l, e_top_r, dz_l, dz_r, & t_l, t_r, s_l, s_r, dpa_l, dpa_r, & hwt_ll, hwt_lr, hwt_rr, hwt_rl, & rho0, rho_ref, dpa_face) !$acc routine seq !! Cross-face 5-point Boole quadrature of the layer `dpa` for a PCM !! column pair under Roquet SpV (`intx_dpa` / `inty_dpa`): the Roquet !! twin of `wright_pcm_dpa_face`, and `boole_dpa_face_pcm` with the !! factored vertical rule `roquet_pcm_dpa_intz` at the three interior !! sub-columns (same sub-column T/S / height / thickness !! interpolation, same weights and summation order). 3 T/S !! polynomials + 15 pressure Horners per face per layer, against 15 !! full generic-EOS calls. real(wp), intent(in) :: e_top_l, e_top_r !! Shallower-interface heights in the left / right column (m). real(wp), intent(in) :: dz_l, dz_r !! Layer thicknesses in the left / right column (m, >= 0). real(wp), intent(in) :: t_l, t_r, s_l, s_r !! Layer-mean temperature / salinity in the left / right column. real(wp), intent(in) :: dpa_l, dpa_r !! The columns' own `g * int rho' dz` over the layer (Pa). real(wp), intent(in) :: hwt_ll, hwt_lr, hwt_rr, hwt_rl !! MOM6 `hWt_LL/LR/RR/RL` mass-weighting fractions. real(wp), intent(in) :: rho0, rho_ref !! Boussinesq pressure-estimate density / anomaly reference. real(wp), intent(out) :: dpa_face !! Along-face mean of `g * int rho' dz` over the layer (Pa). real(wp) :: wr, wl, wtt_l, wtt_r, dpa_m, intz_m, acc integer :: m acc = 7.0_wp*dpa_l + 7.0_wp*dpa_r ! (`GCC$ unroll`: gfortran unrolls the three sub-columns completely ! before vectorising, so the face loops over this vectorise on the CPU; ! nvfortran keeps the loop, which costs fewer registers on the device.) !GCC$ unroll 3 do m = 2, 4 wr = 0.25_wp*real(m - 1, wp) ! 0 at the left column .. 1 at the right wl = 1.0_wp - wr wtt_l = wl*hwt_ll + wr*hwt_rl wtt_r = wl*hwt_lr + wr*hwt_rr call roquet_pcm_dpa_intz(wtt_l*t_l + wtt_r*t_r, wtt_l*s_l + wtt_r*s_r, & wl*e_top_l + wr*e_top_r, wl*dz_l + wr*dz_r, & rho0, rho_ref, dpa_m, intz_m) if (m == 3) then acc = acc + 12.0_wp*dpa_m else acc = acc + 32.0_wp*dpa_m end if end do dpa_face = acc/90.0_wp end subroutine roquet_pcm_dpa_face ! ---- Reconstruct-for-pressure PLM / PPM edge values, per layer. They ! live HERE, next to the kernel whose 3-D Pass 0 calls them, so they ! inline into it (across the module boundary they were real calls with ! their ~12 scalar arguments through the device stack: measured ~1.7x ! slower on the global 1-degree Pass 0). pure subroutine boundary_edges_linear(h_self, h_nbr, q_self, dq_up, q_t, q_b) !$acc routine seq !! Linear-exact one-sided edge pair for a BOUNDARY layer (k=1 or !! k=nz), where a centred slope has no second neighbour. !! !! `dq_up` is the layer-mean increment toward the SURFACE across the !! two cell centres (`q(2)-q(1)` at the bed, `q(nz)-q(nz-1)` at the !! surface). The centres are `(h_self + h_nbr)/2` apart, so the !! per-metre slope is `dq_up/((h_self+h_nbr)/2)` and the half-jump !! across this layer is !! !! d = dq_up * h_self / (h_self + h_nbr) !! !! giving `q_t = q + d` (shallower edge) and `q_b = q - d`. For a !! profile that is linear in z this reproduces the true edge values !! EXACTLY, for any thickness pair — which is the property the FV !! pressure-gradient quadrature needs (Adcroft, Hallberg & Harrison !! 2008; White, Adcroft & Hallberg 2009 §2): a PCM flatten here !! leaves the full terrain-following truncation error in the layers !! next to the tilted boundary. !! !! Limiter: `|d| <= |dq_up|`, i.e. the edge never leaves the !! interval the two cell means span on the other side. Since !! `h_self/(h_self+h_nbr) < 1` it never bites on a real thickness !! pair — it is armour against a degenerate `h_nbr <= 0`, and it !! keeps the extrapolation from manufacturing a density inversion. real(wp), intent(in) :: h_self !! Thickness of the boundary layer itself (m). real(wp), intent(in) :: h_nbr !! Thickness of its single interior neighbour (m). real(wp), intent(in) :: q_self !! Layer mean of the boundary layer. real(wp), intent(in) :: dq_up !! Layer-mean increment toward the surface, neighbour -> self at !! the surface layer, self -> neighbour at the bed layer. real(wp), intent(out) :: q_t !! Top (shallower) edge value. real(wp), intent(out) :: q_b !! Bottom (deeper) edge value. real(wp), parameter :: H_TINY = 1.0e-30_wp real(wp) :: d d = dq_up*h_self/max(h_self + h_nbr, H_TINY) d = sign(min(abs(d), abs(dq_up)), d) q_t = q_self + d q_b = q_self - d end subroutine boundary_edges_linear pure subroutine plm_edges_layer(k, nz, h_dn, h_c, h_up, q_dn, q_c, q_up, q_t, q_b) !$acc routine seq !! PLM top/bottom edge values of ONE layer `k` of a layer-mean field !! `q`, via a two-stage h-weighted van-Leer slope (White, Adcroft & !! Hallberg 2009 §2). Returns the SHALLOWER edge in `q_t` (toward !! k+1) and the DEEPER edge in `q_b` (toward k-1), bottom-up. !! Boundary layers (k=1, k=nz) -> `boundary_edges_linear`, the !! linear-exact one-sided pair. !! !! Every layer's edges depend on its two neighbours only, so the !! caller runs this one thread per CELL (the FV-MOM6 reconstruct !! kernel's Pass 0 is a 3-D `do concurrent`); the neighbour arguments !! of a boundary layer that has none are not referenced. integer, intent(in) :: k !! Layer index (1 = bed, nz = surface). integer, intent(in) :: nz !! Number of layers in the column. real(wp), intent(in) :: h_dn, h_c, h_up !! Thicknesses (m) of layers k-1 (deeper), k, k+1 (shallower). real(wp), intent(in) :: q_dn, q_c, q_up !! Layer means of layers k-1, k, k+1. real(wp), intent(out) :: q_t !! Top (shallower) edge value of layer k. real(wp), intent(out) :: q_b !! Bottom (deeper) edge value of layer k. real(wp) :: slp, sig_c, sig_l, sig_r, slp_max, e_t, e_b, q_lo, q_hi ! Single-layer column: PCM is the only option (no neighbour). if (nz <= 1) then q_t = q_c q_b = q_c return end if if (k == 1) then call boundary_edges_linear(h_c, h_up, q_c, q_up - q_c, q_t, q_b) return end if if (k == nz) then call boundary_edges_linear(h_c, h_dn, q_c, q_c - q_dn, q_t, q_b) return end if ! ---- Stage 1: h-weighted limited central slope ---- ! sig_c is the change ACROSS the layer measured deeper->shallower: ! positive sig_c means q increases toward the surface (k+1). ! h-weighted central slope (van-Leer / White-Adcroft-Hallberg): ! sig_c = (q(k+1)-q(k-1)) * h(k) / (h(k-1)+2 h(k)+h(k+1)) * 2 ! then limited to 2*min(|q(k)-q_deeper|,|q_shallower-q(k)|), zeroed ! at extrema. sig_l = q_c - q_dn ! deeper one-sided (toward k-1) sig_r = q_up - q_c ! shallower one-sided (toward k+1) if (sig_l*sig_r <= 0.0_wp) then slp = 0.0_wp ! local extremum -> flatten else sig_c = 2.0_wp*(q_up - q_dn)*h_c/(h_dn + 2.0_wp*h_c + h_up) slp_max = 2.0_wp*min(abs(sig_l), abs(sig_r)) slp = sign(min(abs(sig_c), slp_max), sig_c) end if ! ---- Stage 2: monotonized edges bounded against neighbour means ---- ! Clamp each edge between the cell mean and the adjacent cell mean ! (White, Adcroft & Hallberg 2009 §2 monotonization — prevents the ! reconstructed edge from over/undershooting the neighbour mean, ! which would manufacture a density inversion under the EOS). e_t = q_c + 0.5_wp*slp ! shallower edge (toward k+1) e_b = q_c - 0.5_wp*slp ! deeper edge (toward k-1) q_lo = min(q_c, q_up) q_hi = max(q_c, q_up) q_t = max(q_lo, min(q_hi, e_t)) q_lo = min(q_c, q_dn) q_hi = max(q_c, q_dn) q_b = max(q_lo, min(q_hi, e_b)) end subroutine plm_edges_layer pure subroutine ppm_interface_values(m, nz, h0, h1, h2, h3, s0, s1, s2, s3, & t0, t1, t2, t3, edge_s, edge_t) !$acc routine seq !! The PPM estimates of salinity and temperature at the interface !! between layer `m` (deeper) and `m+1` (shallower), `1 <= m <= nz-1`, !! from the four-layer stencil `m-1 .. m+2` (thicknesses `h0..h3`, !! layer means `s0..s3`, `t0..t3`). Interior interfaces !! (`2 <= m <= nz-2`) take the implicit-h4 estimate in its explicit !! form (White & Adcroft 2008 non-uniform stencil, exactly 4th-order !! on non-uniform layers; matches `remap_column_ppm_h4` Step 1 to !! round-off). The two near-boundary interfaces (`m = 1`, !! `m = nz-1`) take the thickness-weighted (h2) estimate — the !! linear-exact value at the shared face of two piecewise-linear !! cells, `(q_m h_{m+1} + q_{m+1} h_m)/(h_m + h_{m+1})` (a plain mean !! biases the thick interior layer's edge on non-uniform !! thicknesses); layer `m-1` (resp. `m+2`) is then not referenced. !! !! Both tracers at once: the stencil's thickness factors (five of the !! six divisions) are the same for S and T, so they are formed once. !! Each tracer's value is the single-tracer expression, operation for !! operation. integer, intent(in) :: m !! Interface index: between layers m and m+1. integer, intent(in) :: nz !! Number of layers in the column. real(wp), intent(in) :: h0, h1, h2, h3 !! Thicknesses of layers m-1, m, m+1, m+2 (m). real(wp), intent(in) :: s0, s1, s2, s3 !! Salinity layer means of layers m-1 .. m+2. real(wp), intent(in) :: t0, t1, t2, t3 !! Temperature layer means of layers m-1 .. m+2. real(wp), intent(out) :: edge_s, edge_t !! Interface salinity / temperature. real(wp) :: g0, g1, g2, g3, hf, h_sum real(wp) :: h01, h12, h23, h012, h123, h0123 real(wp) :: f1, f3, w2, w3 real(wp), parameter :: H_NEGLECT = 1.0e-30_wp real(wp), parameter :: H_MIN_FRAC = 1.0e-5_wp if (m == 1 .or. m == nz - 1) then edge_s = (s1*h2 + s2*h1)/(h1 + h2) edge_t = (t1*h2 + t2*h1)/(h1 + h2) return end if g0 = h0 g1 = h1 g2 = h2 g3 = h3 h_sum = g0 + g1 + g2 + g3 if (g0 + g1 <= 0.0_wp .or. g1 + g2 <= 0.0_wp .or. g2 + g3 <= 0.0_wp) then hf = H_MIN_FRAC*max(H_NEGLECT, h_sum) g0 = max(g0, hf) g1 = max(g1, hf) g2 = max(g2, hf) g3 = max(g3, hf) end if h01 = g0 + g1 h12 = g1 + g2 h23 = g2 + g3 h012 = g0 + g1 + g2 h123 = g1 + g2 + g3 h0123 = g0 + g1 + g2 + g3 f1 = h01*h23/h12 f3 = 1.0_wp/h012 + 1.0_wp/h123 w2 = g2*h23/(h012*h01) w3 = g1*h01/(h123*h23) edge_s = (f1*(g2*s1 + g1*s2)*f3 + w2*((g0 + 2.0_wp*g1)*s1 - g1*s0) & + w3*((2.0_wp*g2 + g3)*s2 - g2*s3))/h0123 edge_t = (f1*(g2*t1 + g1*t2)*f3 + w2*((g0 + 2.0_wp*g1)*t1 - g1*t0) & + w3*((2.0_wp*g2 + g3)*t2 - g2*t3))/h0123 end subroutine ppm_interface_values pure subroutine ppm_limit_edges(q_m1, q_c, q_p1, ql_raw, qr_raw, q_t, q_b) !$acc routine seq !! The PPM edge limiter of one interior layer: clip both interface !! estimates into the monotone bounds of the three adjacent means, !! flatten a local extremum to PCM, and apply the Colella & Woodward !! (1984) parabola limiter. real(wp), intent(in) :: q_m1, q_c, q_p1 !! Layer means of layers k-1, k, k+1. real(wp), intent(in) :: ql_raw, qr_raw !! Interface estimates at the layer's deeper / shallower interface. real(wp), intent(out) :: q_t, q_b !! Limited top (shallower) / bottom (deeper) edge values. real(wp) :: q_lo, q_hi, ql, qr, dq, dq_l, dq_r, q6 q_lo = min(q_m1, q_c, q_p1) q_hi = max(q_m1, q_c, q_p1) ql = max(q_lo, min(q_hi, ql_raw)) qr = max(q_lo, min(q_hi, qr_raw)) dq = qr - ql dq_l = q_c - ql dq_r = qr - q_c if (dq_l*dq_r <= 0.0_wp) then ! Local extremum -> flatten to PCM. ql = q_c qr = q_c else q6 = 6.0_wp*q_c - 3.0_wp*(ql + qr) if (abs(q6) > abs(dq)) then if (q6*dq > 0.0_wp) then ql = 3.0_wp*q_c - 2.0_wp*qr else qr = 3.0_wp*q_c - 2.0_wp*ql end if end if end if q_b = ql ! deeper edge q_t = qr ! shallower edge end subroutine ppm_limit_edges pure subroutine ppm_edges_layer(k, nz, h_m2, h_m1, h_c, h_p1, h_p2, & s_m2, s_m1, s_c, s_p1, s_p2, & t_m2, t_m1, t_c, t_p1, t_p2, & s_top, s_bot, t_top, t_bot) !$acc routine seq !! PPM top/bottom edge values of salinity and temperature in ONE layer !! `k`: the implicit-h4 interface estimates (`ppm_interface_values`) !! + the Colella & Woodward (1984) limiter (`ppm_limit_edges`). !! Returns the SHALLOWER edges in `*_top`, the DEEPER in `*_bot` !! (bottom-up). !! !! NOTE the PGF integrand on top of these edges is PARABOLIC (see !! `boole_dpa_intz_layer`): q6 = 3*(2*q_mean - (q_t + q_b)) is the !! in-layer curvature — why PPM differs from PLM at the density !! integral even at identical edge values. !! !! Boundary layers (k=1, k=nz) -> `boundary_edges_linear`. The pair !! is symmetric about the layer mean, so `q6 = 3*(2*q - (q_t+q_b))` !! is identically zero there: the boundary layer carries a straight !! line, which is the exact profile whenever `q(z)` is linear. !! !! A layer's edges depend on the five layers `k-2 .. k+2` only, so the !! caller runs this one thread per CELL (the FV-MOM6 reconstruct !! kernel's Pass 0 is a 3-D `do concurrent`); arguments of layers !! outside the column are not referenced. integer, intent(in) :: k !! Layer index (1 = bed, nz = surface). integer, intent(in) :: nz !! Number of layers in the column. real(wp), intent(in) :: h_m2, h_m1, h_c, h_p1, h_p2 !! Thicknesses (m) of layers k-2 .. k+2. real(wp), intent(in) :: s_m2, s_m1, s_c, s_p1, s_p2 !! Salinity layer means of layers k-2 .. k+2. real(wp), intent(in) :: t_m2, t_m1, t_c, t_p1, t_p2 !! Temperature layer means of layers k-2 .. k+2. real(wp), intent(out) :: s_top, s_bot, t_top, t_bot !! Top (shallower) / bottom (deeper) edge values of layer k. real(wp) :: sl, sr, tl, tr if (nz <= 1) then s_top = s_c s_bot = s_c t_top = t_c t_bot = t_c return end if ! Boundary layers: linear-exact one-sided pair (q6 == 0 there). if (k == 1) then call boundary_edges_linear(h_c, h_p1, s_c, s_p1 - s_c, s_top, s_bot) call boundary_edges_linear(h_c, h_p1, t_c, t_p1 - t_c, t_top, t_bot) return end if if (k == nz) then call boundary_edges_linear(h_c, h_m1, s_c, s_c - s_m1, s_top, s_bot) call boundary_edges_linear(h_c, h_m1, t_c, t_c - t_m1, t_top, t_bot) return end if call ppm_interface_values(k - 1, nz, h_m2, h_m1, h_c, h_p1, s_m2, s_m1, s_c, s_p1, & t_m2, t_m1, t_c, t_p1, sl, tl) call ppm_interface_values(k, nz, h_m1, h_c, h_p1, h_p2, s_m1, s_c, s_p1, s_p2, & t_m1, t_c, t_p1, t_p2, sr, tr) call ppm_limit_edges(s_m1, s_c, s_p1, sl, sr, s_top, s_bot) call ppm_limit_edges(t_m1, t_c, t_p1, tl, tr, t_top, t_bot) end subroutine ppm_edges_layer ! ---- Reconstruct-for-pressure per-EOS twins of the generic Boole rules ! (`rdb_ocean_pgf_reconstruct :: boole_dpa_intz_layer` / `boole_dpa_face`). ! They live HERE, in the kernel's module, so nvfortran inlines them into ! the 3-D Pass 1b / Pass 2 loops of `compute_fv_mom6_reconstruct_impl` ! (measured: across the module boundary they were real calls, their ! arguments and sub-point arrays through the device stack). pure subroutine boole_layer_points(rho0, e_top, dz, t_t, t_b, t_mean, & s_t, s_b, s_mean, parabolic, t5, s5, p5) !$acc routine seq !! The five sub-point (T, S, p) triples of the in-layer Boole rule, !! top (n = 1) to bottom (n = 5), for the per-EOS twins -- the same !! points `boole_dpa_intz_layer` writes out in place. real(wp), intent(in) :: rho0 !! Boussinesq reference density used in the pressure estimate. real(wp), intent(in) :: e_top !! Surface-relative height of the SHALLOWER interface. real(wp), intent(in) :: dz !! Layer thickness (m), dz >= 0. real(wp), intent(in) :: t_t, t_b, t_mean !! Temperature: top edge, bottom edge, layer mean. real(wp), intent(in) :: s_t, s_b, s_mean !! Salinity: top edge, bottom edge, layer mean. logical, intent(in) :: parabolic !! .true. -> add the PPM curvature (s6/t6) term. real(wp), intent(out) :: t5(N_BOOLE), s5(N_BOOLE), p5(N_BOOLE) !! Sub-point temperature, salinity and Boussinesq pressure (Pa). real(wp) :: gxrho, wt_t, wt_b, t6, s6, z5 integer :: n gxrho = GRAVITY*rho0 ! PPM curvature (zero for PLM). t6 = 0.0_wp s6 = 0.0_wp if (parabolic) then t6 = 3.0_wp*(2.0_wp*t_mean - (t_t + t_b)) s6 = 3.0_wp*(2.0_wp*s_mean - (s_t + s_b)) end if !GCC$ unroll 5 do n = 1, N_BOOLE wt_t = 0.25_wp*real(N_BOOLE - n, wp) ! 1, .75, .5, .25, 0 wt_b = 1.0_wp - wt_t ! Linear blend + parabolic correction. At wt_t in [0,1] the ! parabola through (_t at wt_t=1, _b at wt_t=0, mean) is ! q(wt_t) = wt_t*q_t + wt_b*q_b + q6*wt_t*wt_b. t5(n) = wt_t*t_t + wt_b*t_b + t6*wt_t*wt_b s5(n) = wt_t*s_t + wt_b*s_b + s6*wt_t*wt_b z5 = e_top - 0.25_wp*real(n - 1, wp)*dz ! marches DOWN from top p5(n) = -gxrho*z5 end do end subroutine boole_layer_points pure subroutine boole_layer_combine(r5, dz, dpa, intz_dpa) !$acc routine seq !! Boole weights of the five sub-point density anomalies `r5` (top !! to bottom): `dpa = g*dz*<rho'>` and the first moment from the top. real(wp), intent(in) :: r5(N_BOOLE) !! Density anomaly at the five sub-points (kg/m^3). real(wp), intent(in) :: dz !! Layer thickness (m). real(wp), intent(out) :: dpa !! g * int rho' dz over the layer (Pa). real(wp), intent(out) :: intz_dpa !! 0.5 * g * dz^2 * bracket (Pa*m), first moment from the top. real(wp) :: rho_anom rho_anom = (1.0_wp/90.0_wp)*(7.0_wp*(r5(1) + r5(5)) & + 32.0_wp*(r5(2) + r5(4)) + 12.0_wp*r5(3)) dpa = GRAVITY*dz*rho_anom intz_dpa = 0.5_wp*GRAVITY*dz*dz*(rho_anom & - (1.0_wp/90.0_wp)*(16.0_wp*(r5(4) - r5(2)) + 7.0_wp*(r5(5) - r5(1)))) end subroutine boole_layer_combine pure subroutine boole_dpa_intz_layer_wright(rho0, rho_ref, e_top, dz, & t_t, t_b, t_mean, s_t, s_b, s_mean, & parabolic, dpa, intz_dpa) !$acc routine seq !! `boole_dpa_intz_layer` specialised to Wright (1997): the same five !! sub-points and weights, with the density written inline !! (`wright_rho`, the expression `eos_density_point` evaluates) -- !! no `eos_t` handle, no per-point variant dispatch. The caller !! selects this twin ONCE, outside its loops. real(wp), intent(in) :: rho0, rho_ref !! Pressure-estimate density / anomaly reference (kg/m^3). real(wp), intent(in) :: e_top, dz !! Shallower-interface height / layer thickness (m). real(wp), intent(in) :: t_t, t_b, t_mean, s_t, s_b, s_mean !! T / S top edge, bottom edge, layer mean. logical, intent(in) :: parabolic !! .true. -> PPM curvature. real(wp), intent(out) :: dpa, intz_dpa !! As `boole_dpa_intz_layer`. real(wp) :: t5(N_BOOLE), s5(N_BOOLE), p5(N_BOOLE), r5(N_BOOLE) integer :: n call boole_layer_points(rho0, e_top, dz, t_t, t_b, t_mean, s_t, s_b, s_mean, & parabolic, t5, s5, p5) !GCC$ unroll 5 do n = 1, N_BOOLE r5(n) = wright_rho(t5(n), s5(n), p5(n)) - rho_ref end do call boole_layer_combine(r5, dz, dpa, intz_dpa) end subroutine boole_dpa_intz_layer_wright pure function wright_rho(t, s, p) result(rho) !$acc routine seq !! Wright (1997) in-situ density (kg/m^3) -- term for term the !! Wright branch of `eos_density_point`, without the handle. real(wp), intent(in) :: t, s, p !! Temperature (degC), salinity (PSU), pressure (Pa). real(wp) :: rho real(wp) :: T_sq, alpha_0, p_0, lambda, p_plus_p0 T_sq = t*t alpha_0 = WRIGHT_A0 + WRIGHT_A1*t + WRIGHT_A2*s p_0 = WRIGHT_B0 + WRIGHT_B1*t + WRIGHT_B2*T_sq + WRIGHT_B3*T_sq*t + & WRIGHT_B4*s + WRIGHT_B5*s*t lambda = WRIGHT_C0 + WRIGHT_C1*t + WRIGHT_C2*T_sq + WRIGHT_C3*T_sq*t + & WRIGHT_C4*s + WRIGHT_C5*s*t p_plus_p0 = p + p_0 rho = p_plus_p0/(lambda + alpha_0*p_plus_p0) end function wright_rho pure subroutine boole_dpa_face_wright(rho0, rho_ref, & e_top_l, e_top_r, dz_l, dz_r, & t_t_l, t_b_l, t_m_l, t_t_r, t_b_r, t_m_r, & s_t_l, s_b_l, s_m_l, s_t_r, s_b_r, s_m_r, & dpa_l, dpa_r, parabolic, dpa_face) !$acc routine seq !! `boole_dpa_face` with the Wright (1997) vertical rule !! `boole_dpa_intz_layer_wright` at each of the five sub-columns -- !! identical sub-columns, weights and summation order, no `eos_t` !! handle. 15 inline Wright density evaluations per face per layer !! (the end points are the columns' own `dpa`). real(wp), intent(in) :: rho0, rho_ref !! Pressure-estimate density / anomaly reference (kg/m^3). real(wp), intent(in) :: e_top_l, e_top_r, dz_l, dz_r !! Left / right shallower-interface heights and thicknesses (m). real(wp), intent(in) :: t_t_l, t_b_l, t_m_l, t_t_r, t_b_r, t_m_r !! Left / right temperature triples (top, bottom, mean). real(wp), intent(in) :: s_t_l, s_b_l, s_m_l, s_t_r, s_b_r, s_m_r !! Left / right salinity triples. real(wp), intent(in) :: dpa_l, dpa_r !! The left / right columns' own layer `dpa` (Pa), the end points. logical, intent(in) :: parabolic !! .true. -> PPM curvature. real(wp), intent(out) :: dpa_face !! Along-face mean of `g * int rho' dz` over the layer (Pa). real(wp) :: wr, wl, dpa_m, intz_m, acc integer :: m real(wp), parameter :: BOOLE_W(N_BOOLE) = & [7.0_wp, 32.0_wp, 12.0_wp, 32.0_wp, 7.0_wp] acc = BOOLE_W(1)*dpa_l + BOOLE_W(N_BOOLE)*dpa_r !GCC$ unroll 3 do m = 2, N_BOOLE - 1 wr = 0.25_wp*real(m - 1, wp) ! 0 at the left column .. 1 at the right wl = 1.0_wp - wr call boole_dpa_intz_layer_wright(rho0, rho_ref, & wl*e_top_l + wr*e_top_r, & wl*dz_l + wr*dz_r, & wl*t_t_l + wr*t_t_r, & wl*t_b_l + wr*t_b_r, & wl*t_m_l + wr*t_m_r, & wl*s_t_l + wr*s_t_r, & wl*s_b_l + wr*s_b_r, & wl*s_m_l + wr*s_m_r, & parabolic, dpa_m, intz_m) acc = acc + BOOLE_W(m)*dpa_m end do dpa_face = acc/90.0_wp end subroutine boole_dpa_face_wright pure subroutine roquet_recon_dpa_intz(rho0, rho_ref, e_top, dz, & t_t, t_b, t_mean, s_t, s_b, s_mean, & parabolic, dpa, intz_dpa) !$acc routine seq !! `boole_dpa_intz_layer` (the reconstruct-for-pressure in-layer 5-point !! Boole rule over a PLM / PPM T/S profile) specialised to Roquet SpV: !! the same five sub-points, weights and summation order, with the !! density `1/SV` from this module's copy of the SpV value !! (`rdb_roq_ts_coeffs` + `rdb_roq_spv_p`, `rdb_roquet_spv.inc`) -- no !! `eos_t` handle, no per-point variant dispatch, and a body the !! compiler inlines into the kernel. T and S vary through the layer, !! so unlike `roquet_pcm_dpa_intz` there is no (T, S) hoist: each point !! is a full EOS evaluation. real(wp), intent(in) :: rho0, rho_ref !! Pressure-estimate density / anomaly reference (kg/m^3). real(wp), intent(in) :: e_top, dz !! Shallower-interface height / layer thickness (m). real(wp), intent(in) :: t_t, t_b, t_mean, s_t, s_b, s_mean !! T / S top edge, bottom edge, layer mean. logical, intent(in) :: parabolic !! .true. -> PPM curvature. real(wp), intent(out) :: dpa, intz_dpa !! As `boole_dpa_intz_layer`. real(wp) :: gxrho, wt_t, wt_b, t6, s6, t5, s5, p5, rho_anom real(wp) :: sv0, sv1, sv2, sv3 real(wp) :: r5(N_BOOLE) integer :: n gxrho = GRAVITY*rho0 ! PPM curvature (zero for PLM). t6 = 0.0_wp s6 = 0.0_wp if (parabolic) then t6 = 3.0_wp*(2.0_wp*t_mean - (t_t + t_b)) s6 = 3.0_wp*(2.0_wp*s_mean - (s_t + s_b)) end if !GCC$ unroll 5 do n = 1, N_BOOLE wt_t = 0.25_wp*real(N_BOOLE - n, wp) ! 1, .75, .5, .25, 0 wt_b = 1.0_wp - wt_t t5 = wt_t*t_t + wt_b*t_b + t6*wt_t*wt_b s5 = wt_t*s_t + wt_b*s_b + s6*wt_t*wt_b p5 = -gxrho*(e_top - 0.25_wp*real(n - 1, wp)*dz) call rdb_roq_ts_coeffs(t5, s5, sv0, sv1, sv2, sv3) r5(n) = 1.0_wp/rdb_roq_spv_p(sv0, sv1, sv2, sv3, p5) - rho_ref end do rho_anom = (1.0_wp/90.0_wp)*(7.0_wp*(r5(1) + r5(5)) & + 32.0_wp*(r5(2) + r5(4)) + 12.0_wp*r5(3)) dpa = GRAVITY*dz*rho_anom intz_dpa = 0.5_wp*GRAVITY*dz*dz*(rho_anom & - (1.0_wp/90.0_wp)*(16.0_wp*(r5(4) - r5(2)) + 7.0_wp*(r5(5) - r5(1)))) end subroutine roquet_recon_dpa_intz pure subroutine roquet_recon_dpa_face(rho0, rho_ref, & e_top_l, e_top_r, dz_l, dz_r, & t_t_l, t_b_l, t_m_l, t_t_r, t_b_r, t_m_r, & s_t_l, s_b_l, s_m_l, s_t_r, s_b_r, s_m_r, & dpa_l, dpa_r, parabolic, dpa_face) !$acc routine seq !! `boole_dpa_face` (the reconstruct-for-pressure cross-face 5-point !! Boole rule) with the Roquet vertical rule `roquet_recon_dpa_intz` at !! the three interior sub-columns -- identical sub-columns, weights and !! summation order; the end points are the columns' own `dpa`. 15 !! inlined Roquet evaluations per face per layer. real(wp), intent(in) :: rho0, rho_ref !! Pressure-estimate density / anomaly reference (kg/m^3). real(wp), intent(in) :: e_top_l, e_top_r, dz_l, dz_r !! Left / right shallower-interface heights and thicknesses (m). real(wp), intent(in) :: t_t_l, t_b_l, t_m_l, t_t_r, t_b_r, t_m_r !! Left / right temperature triples (top, bottom, mean). real(wp), intent(in) :: s_t_l, s_b_l, s_m_l, s_t_r, s_b_r, s_m_r !! Left / right salinity triples. real(wp), intent(in) :: dpa_l, dpa_r !! The left / right columns' own layer `dpa` (Pa), the end points. logical, intent(in) :: parabolic !! .true. -> PPM curvature. real(wp), intent(out) :: dpa_face !! Along-face mean of `g * int rho' dz` over the layer (Pa). real(wp) :: wr, wl, dpa_m, intz_m, acc integer :: m real(wp), parameter :: BOOLE_W(N_BOOLE) = & [7.0_wp, 32.0_wp, 12.0_wp, 32.0_wp, 7.0_wp] acc = BOOLE_W(1)*dpa_l + BOOLE_W(N_BOOLE)*dpa_r !GCC$ unroll 3 do m = 2, N_BOOLE - 1 wr = 0.25_wp*real(m - 1, wp) ! 0 at the left column .. 1 at the right wl = 1.0_wp - wr call roquet_recon_dpa_intz(rho0, rho_ref, & wl*e_top_l + wr*e_top_r, & wl*dz_l + wr*dz_r, & wl*t_t_l + wr*t_t_r, & wl*t_b_l + wr*t_b_r, & wl*t_m_l + wr*t_m_r, & wl*s_t_l + wr*s_t_r, & wl*s_b_l + wr*s_b_r, & wl*s_m_l + wr*s_m_r, & parabolic, dpa_m, intz_m) acc = acc + BOOLE_W(m)*dpa_m end do dpa_face = acc/90.0_wp end subroutine roquet_recon_dpa_face pure subroutine fv_mom6_mass_weights(mass_weight, e_bed_l, e_bed_r, e_top_l, e_top_r, & h_l, h_r, h_neglect, & hwt_ll, hwt_lr, hwt_rr, hwt_rl) !$acc routine seq !! MOM6 near-bottom `hWght` mass-weighting fractions of one face !! (`MASS_WEIGHT_IN_PRESSURE_GRADIENT`): the distance by which the !! layer top of one column sits below the other column's bed, scaled !! by the squared relative thickness contrast. Zero ⇒ plain linear !! interpolation (`hwt_ll = hwt_rr = 1`, `hwt_lr = hwt_rl = 0`). logical, intent(in) :: mass_weight !! Mass weighting on? real(wp), intent(in) :: e_bed_l, e_bed_r !! Bed heights of the left / right column (m). real(wp), intent(in) :: e_top_l, e_top_r !! Layer top heights of the left / right column (m). real(wp), intent(in) :: h_l, h_r !! Layer thicknesses of the left / right column (m). real(wp), intent(in) :: h_neglect !! Negligible thickness (m). real(wp), intent(out) :: hwt_ll, hwt_lr, hwt_rr, hwt_rl !! MOM6 `hWt_LL/LR/RR/RL`. real(wp) :: hwght, hwl, hwr, idenom_hw logical :: use_hw ! Branch-free: the weighted fractions are formed unconditionally and ! SELECTED where `hwght > 0` (else plain linear interpolation) -- the ! same numbers as the branched form, and a CPU compiler can vectorise ! the face loops that call this. `h_neglect > 0` keeps the unselected ! arithmetic finite. hwght = 0.0_wp if (mass_weight) hwght = max(0.0_wp, e_bed_r - e_top_l, e_bed_l - e_top_r) use_hw = hwght > 0.0_wp hwl = h_l + h_neglect hwr = h_r + h_neglect hwght = hwght*((hwl - hwr)/(hwl + hwr))**2 idenom_hw = 1.0_wp/(hwght*(hwr + hwl) + hwl*hwr) hwt_ll = merge((hwght*hwl + hwr*hwl)*idenom_hw, 1.0_wp, use_hw) hwt_lr = merge((hwght*hwr)*idenom_hw, 0.0_wp, use_hw) hwt_rr = merge((hwght*hwr + hwr*hwl)*idenom_hw, 1.0_wp, use_hw) hwt_rl = merge((hwght*hwl)*idenom_hw, 0.0_wp, use_hw) end subroutine fv_mom6_mass_weights pure function recon_rho_surf(pa_k, pa_kp1, h_surf, rho_ref) result(rho_surf) !$acc routine seq !! Recover the layer-mean surface density from the reconstructed !! pressure-anomaly stack: dpa(nz) = pa(nz) - pa(nz+1) = !! (rho_surf - rho_ref)*g*h_surf, so rho_surf = rho_ref + dpa/(g*h). !! Used only by the gfs_scale Montgomery correction (Pass 5). real(wp), intent(in) :: pa_k, pa_kp1, h_surf, rho_ref real(wp) :: rho_surf real(wp), parameter :: H_FLOOR = 1.0e-10_wp real(wp) :: hh hh = max(h_surf, H_FLOOR) rho_surf = rho_ref + (pa_k - pa_kp1)/(GRAVITY*hh) end function recon_rho_surf pure function parse_opgf_variant(name) result(code) !! Translate a namelist string into an `OPGF_VARIANT_*` code. !! Unrecognised values fall back to `OPGF_VARIANT_FV_LITE` (the !! production default). character(len=*), intent(in) :: name integer :: code select case (trim(adjustl(name))) case ("mont", "MONT", "montgomery") code = OPGF_VARIANT_MONT case ("fv_lite", "FV_LITE", "fv-lite", "") code = OPGF_VARIANT_FV_LITE case ("fv_wright", "FV_WRIGHT", "fv-wright", "wright") code = OPGF_VARIANT_FV_WRIGHT case ("gprime", "GPRIME", "reduced_gravity") code = OPGF_VARIANT_GPRIME case ("fv_mom6", "FV_MOM6", "fv-mom6", "mom6_fv", "MOM6_FV") code = OPGF_VARIANT_FV_MOM6 case default code = OPGF_VARIANT_FV_LITE end select end function parse_opgf_variant pure function ocean_pressure_force_bytes(this) result(nbytes) !! Counted allocatable footprint of the pressure force slot !! (0 when unallocated). One arr_bytes term per array — add a !! term here when a new allocatable joins the type. class(ocean_pressure_force_t), intent(in) :: this integer(int64) :: nbytes nbytes = arr_bytes(this%b) & + this%p_edge%bytes() & + this%z_centre%bytes() & + this%mont_M%bytes() & + this%rho_insitu%bytes() & + this%dpdx_face%bytes() & + this%dpdy_face%bytes() & + this%e_face%bytes() & + this%pa%bytes() & + this%intz_dpa%bytes() & + this%intx_pa%bytes() & + this%inty_pa%bytes() & + this%intx_dpa%bytes() & + this%inty_dpa%bytes() & + this%conc_T%bytes() & + this%conc_S%bytes() & + this%recon_T_t%bytes() & + this%recon_T_b%bytes() & + this%recon_S_t%bytes() & + this%recon_S_b%bytes() end function ocean_pressure_force_bytes #include "rdb_roquet_spv.inc" #include "rdb_vanished_layer.inc" end module rdb_ocean_pressure_force