Physical constants and working precision for the tidal solver
!! Physical constants and working precision for the tidal solver module rdb_constants !! Working precision kind parameter and physical constants. use pic_types, only: dp, sp implicit none private public :: wp public :: GRAVITY, RHO_WATER, OMEGA_EARTH public :: CP_WATER public :: LATENT_HEAT_VAPORIZATION public :: DRY_TOLERANCE public :: NH_MIN_DEPTH public :: THIN_LAYER_THRESHOLD public :: H_VANISHED, H_DIV_EPS public :: LAND_DEPTH_THRESHOLD public :: FROUDE_CAP public :: PI, TWO_PI, DEG2RAD, RAD2DEG public :: NZ_STACK_MAX public :: nz_stack_required, nz_stack_is_sufficient public :: VCOORD_LAGRANGIAN, VCOORD_EULERIAN_Z, & VCOORD_SIGMA, VCOORD_ZSIGMA, VCOORD_ZSTAR, & VCOORD_ZSTAR_FULL, VCOORD_ZSTAR_SIGMA, VCOORD_Z_FIXED, & VCOORD_RHO, VCOORD_HYCOM public :: REMAP_PCM, REMAP_PLM, REMAP_PPM, REMAP_PPM_H4, REMAP_PQM public :: KEPS_STAB_CONSTANT, KEPS_STAB_GALPERIN, KEPS_STAB_CANUTO public :: FRHAT_ARITHMETIC, FRHAT_HYBRID, parse_frhat_scheme #ifdef RDB_DOUBLE_PRECISION integer, parameter :: wp = dp !! Working precision kind parameter (double) #else integer, parameter :: wp = sp !! Working precision kind parameter (single) #endif real(wp), parameter :: GRAVITY = 9.80665_wp !! Gravitational acceleration (m/s^2) real(wp), parameter :: RHO_WATER = 1035.0_wp !! Seawater density (kg/m^3) real(wp), parameter :: OMEGA_EARTH = 7.2921e-5_wp !! Earth angular velocity (rad/s) real(wp), parameter :: CP_WATER = 4000.0_wp !! Specific heat capacity of seawater (J/(kg·K)). Converts surface !! heat flux Q_net [W/m²] to kinematic heat flux Q_net/(rho_0·cp) [m·K/s]. real(wp), parameter :: LATENT_HEAT_VAPORIZATION = 2.5e6_wp !! Latent heat of vaporization of water (J/kg). A representative !! near-surface value (2.501e6 at 0 °C, weakly T-dependent). Used by !! the evaporative latent-heat surface cooling: a net evaporation of !! E [m/s] withdraws a heat flux ρ_water·E·L_v [W/m²] from the top of !! the water column as the evaporated mass changes phase. real(wp), parameter :: DRY_TOLERANCE = 1.0e-6_wp !! Minimum depth to consider a cell wet (m) real(wp), parameter :: NH_MIN_DEPTH = 1.0e-1_wp !! Minimum depth for non-hydrostatic pressure operations (m). !! Cells shallower than this are treated as hydrostatic to avoid !! 1/h singularities in the NH Poisson operator. real(wp), parameter :: THIN_LAYER_THRESHOLD = 1.0e-3_wp !! Below this depth, momentum is zeroed to prevent !! huge velocities at wet/dry fronts (m) ! ---- Thin-layer constants of record (ocean-path semantic split, D4 taxonomy) ---- real(wp), parameter :: H_VANISHED = 1.5e-4_wp !! Dynamic-vanish threshold (m). A layer thinner than this is !! dynamically VANISHED — skip / merge into a neighbour, do NOT clamp. !! When dividing by `h_layer` for a PHYSICAL result, gate on this. !! Tests of it are STRICT `>`: a layer sitting exactly ON the marker !! reads as vanished, which is what the geometric vcoord families !! rely on (`rdb_vcoord :: vcoord_h_min_role`). !! !! **Do not hand-roll the test.** Tracers are stored as CONTENT !! (`hTr = h·c`) and the tree holds one invariant about vanished !! layers — !! !! I1′: `h <= H_VANISHED ⇒ hTr = h·c_live`, every registered !! tracer, `c_live` the concentration of the filler's donor !! (nearest live layer above; topmost live layer for the !! fillers above it; `hTr = 0` with no live layer) !! !! — with ONE definition of the predicate, of "the concentration of !! layer k", and of the column merge that restores it: !! `src/shared_module_utilities/rdb_vanished_layer.inc` !! (`rdb_vl_is_live`, `rdb_vl_conc`, `rdb_vl_column_conc`, !! `rdb_vl_holds_live_conc`, `rdb_vl_merge_content`), ONE !! enforcement point (`multilayer_state_t%enforce_vanished_content`, !! at the tail of the outer step) and a fail-loud tripwire !! (`&vcoord_nml check_vanished_content`). The contract, and the !! table of consumers that deliberately substitute something ELSE !! (the EOS's reference T/S, the sponge's nearest massive layer, the !! diagnostics' NaN, the melt sampler's skip), is written down once !! in `src/core/ocean/README.md`, "The vanished-layer content rule". !! A new raw `hTr/h` divide or a new comparison against this constant !! outside the sanctioned modules is flagged by the `vanished-layer` !! pre-commit hook. real(wp), parameter :: H_DIV_EPS = 1.0e-20_wp !! Pure division-safety epsilon (m), far below any physical thickness: !! adding it (`1/(h+H_DIV_EPS)`) only prevents a 1/0, never changes a !! well-posed result. Use where a divisor is ALREADY guaranteed !! positive (MOM6 `H_subroundoff` role). real(wp), parameter :: LAND_DEPTH_THRESHOLD = 2.0_wp !! Cell bathymetry depth (m, positive-down) below which an ocean cell !! is treated as LAND for surface-forcing masking. Consumed by !! `ocean_state_seed_from_cfg` to populate `multilayer%wet_mask`. real(wp), parameter :: PI = 4.0_wp*atan(1.0_wp) !! Pi real(wp), parameter :: TWO_PI = 8.0_wp*atan(1.0_wp) !! 2*Pi real(wp), parameter :: DEG2RAD = PI/180.0_wp !! Degrees → radians conversion factor. real(wp), parameter :: RAD2DEG = 180.0_wp/PI !! Radians → degrees conversion factor. real(wp), parameter :: FROUDE_CAP = 10.0_wp !! Maximum Froude number allowed at wet/dry fronts. !! If |u|/sqrt(g*h) exceeds this, momentum is rescaled. !! Physical flows rarely exceed Fr=3; Fr=10 is very permissive. #ifndef RDB_NZ_STACK_MAX #define RDB_NZ_STACK_MAX 128 #endif integer, parameter :: NZ_STACK_MAX = RDB_NZ_STACK_MAX !! Maximum vertical layers for per-column stack workspace inside !! `do concurrent`. Stack arrays in the BPG, remap, kappa-shear, !! Redi and diag-remap kernels are dimensioned by this constant so !! the compiler can emit fixed-size thread-local storage on GPU. !! !! **The requirement is `NZ_STACK_MAX >= nz + 1`** — see !! `nz_stack_required` below for the derivation and the survey of !! every consumer. Enforced fail-loud by `validate_config`. !! !! History: 100 -> 256 (2026-06-19), on the belief that the Redi !! neutral-diffusion sweep needed `2*nz+2` *of this constant*. It !! does not: Redi's `nsurf` locals are declared `2*NZ_STACK_MAX+2` !! (`rdb_ocean_redi.F90` `PoLc`/`PoRc`/`KoLc`/`KoRc`/`hEc`), so they !! scale with the constant and only need `NZ_STACK_MAX >= nz`. !! Lowered 256 -> 128 (2026-08-05) after that measurement: 256 was !! ~2x larger than any consumer needed and cost ~4 GB of CUDA !! per-thread local memory, which the driver reserves at each !! kernel's first launch and never returns. 128 covers the largest !! shipped case (nz=90, `acc_channel_kitchensink_xl.nml`) and the !! largest test (nz=100) with headroom. !! !! Heap-allocated workspace arrays (PPM snapshots, remap snapshots) !! have no such limit — they are allocated at runtime. ! ---- Vertical coordinate type constants ---- integer, parameter :: VCOORD_LAGRANGIAN = -1 !! Pure Lagrangian / isopycnal — `h_layer` evolves freely and is !! never remapped. `compute_target_h` returns the current `h` !! unchanged and the ALE remap step is a no-op. Distinct from !! `VCOORD_EULERIAN_Z` (which targets the static `H · dsig` !! decomposition): LAGRANGIAN tracks material surfaces, so for !! adiabatic flow it reproduces MOM6's `COORD_CONFIG="gprime"` !! NK=2 isopycnal layering. Parsed from "lagrangian" / "isopycnal". integer, parameter :: VCOORD_EULERIAN_Z = 0 !! Ignore SSH when forming `target_h`; layer thicknesses follow !! the static `H · dsig(k)` decomposition. Used by the ocean !! path as the "do nothing, leave the IC layers alone" default !! before the user picks a remap-aware coord_type. Was originally !! declared in `rdb_ocean_vcoord` as an orphan; moved here so the !! VCOORD enum lives in one place. integer, parameter :: VCOORD_SIGMA = 1 !! Pure terrain-following sigma coordinate (default) integer, parameter :: VCOORD_ZSIGMA = 2 !! z-sigma hybrid: z-levels in deep/steep, sigma in shallow integer, parameter :: VCOORD_ZSTAR = 4 !! z-star "lite" (MOM6-like quasi-horizontal): single global !! `z_ref` pattern stretched proportionally per column by H/z_ref(nz) integer, parameter :: VCOORD_ZSTAR_FULL = 5 !! z-star "full" MOM6: per-column `z_ref` anchored to local !! bathymetry with vanishing layers below the bed. Surface !! layers stay at a fixed physical thickness independent of H integer, parameter :: VCOORD_ZSTAR_SIGMA = 6 !! z*/sigma hybrid: smoothstep blend from pure sigma in shallow !! water (H ≤ depth_transition) to z*-lite in deep water !! (H ≥ depth_transition + blend_width). Coastal wet/dry !! friendliness in shallow zones, SSH-tracking in deep zones integer, parameter :: VCOORD_Z_FIXED = 7 !! Fixed-z interfaces with vanishing layers in shallow water. !! ALE remap pulls `h_layer(k) → h_target(k)` where the targets !! come from absolute-depth interfaces `z_target = (0, h_nominal, !! 2·h_nominal, ..., h_ref)` with `h_nominal = h_ref/nz_ml` — or, !! under `&vcoord_nml z_fixed_profile = "list" | "tanh"`, from a !! stretched per-layer table (`ocean_vcoord_t%z_fixed_zi`). !! In deep cells the interfaces stay locked at those depths; in !! shallow cells the bed-side layers vanish to a minimum !! thickness `h_min` while the surface layer absorbs the residual. !! Mirrors MOM6's `COORD_CONFIG = "gprime"` layer structure and !! the existing `seed_h_layer_z_fixed_impl` IC algorithm — used !! as a per-step ALE target rather than an init-only seed so the !! interface stays anchored as the simulation evolves. integer, parameter :: VCOORD_RHO = 8 !! Isopycnal coordinate (P2). Layer interfaces are placed on !! prescribed potential-density surfaces `rho_target(0:nz)` !! (monotone-increasing, referenced to `rho_ref_pressure`). The !! ALE regrid inverts a PPM reconstruction of the column density !! profile for the depth where ρ equals each interior target, then !! remaps T/S/tracers/velocities onto the new grid. Bottom-up !! convention: `rho_target(0)` (lightest) anchors the surface !! interface (k=nz); `rho_target(nz)` (densest) the bed (k=1). !! Validation-grade alone (weakly-stratified columns collapse); !! HYCOM hybrid is the production follow-on (P3). See !! `rdb_ocean_vcoord :: ocean_vcoord_compute_target_h_rho_impl`. integer, parameter :: VCOORD_HYCOM = 9 !! Hybrid z*/isopycnal coordinate (P3, Bleck 2002 / MOM6 !! `coord_hycom`). Runs the exact `VCOORD_RHO` density-space !! inversion (same `ocean_vcoord_compute_target_h_rho` kernel, !! `hybrid=.true.`) but adds two HYCOM deltas around it: a !! bottom-up density monotonize before the inversion, and a z* !! nominal-floor sweep after it that pushes too-shallow interfaces !! down to a fixed-resolution surface band (`dsig·(H+η)`). Net !! effect: a fixed-res near-surface z* band (no surface collapse) !! with an isopycnal interior — the production GVC coordinate. !! Reuses `rho_target` / `rho_ref_pressure`; no new knobs. ! ---- Vertical remapping method constants ---- integer, parameter :: REMAP_PCM = 1 !! Piecewise constant (0th order, donor cell) integer, parameter :: REMAP_PLM = 2 !! Piecewise linear with minmod limiter (1st order) integer, parameter :: REMAP_PPM = 3 !! Piecewise parabolic (2nd order, Colella & Woodward) integer, parameter :: REMAP_PPM_H4 = 4 !! Piecewise parabolic with non-uniform 4th-order (H4) edge values — !! thickness-weighted edge estimate (White & Adcroft 2008) feeding the !! same Colella & Woodward parabola + monotonicity limiter as REMAP_PPM integer, parameter :: REMAP_PQM = 5 !! Piecewise quartic (4th-order, PQM_IH4IH3 — White & Adcroft 2008): !! implicit-h4 edge values + implicit-h3 edge slopes + a quartic per-cell !! reconstruction with the White & Adcroft monotonicity limiter and a !! conservative quartic overlap integral. Requires nz >= 5; falls back to !! REMAP_PPM for thinner columns. Boundary cells are PCM. ! ---- k-epsilon stability-function scheme constants ---- integer, parameter :: KEPS_STAB_CONSTANT = 0 !! Constant c_mu (=0.09) / Pr_t (=1) -- standard high-Re k-epsilon !! (default; bit-identical to the original closure). integer, parameter :: KEPS_STAB_GALPERIN = 1 !! Galperin et al. (1988) quasi-equilibrium stratification-aware !! stability functions (Mellor-Yamada level-2.5 algebraic form). integer, parameter :: KEPS_STAB_CANUTO = 2 !! Canuto et al. (2001) Model A second-moment-closure stability functions !! (rational alpha_M / alpha_N form per Umlauf & Burchard 2003). ! ---- Barotropic face-layer-weight ("frhat") scheme constants ---- integer, parameter :: FRHAT_ARITHMETIC = 0 !! `&ocean_bt_nml frhat_scheme = "arithmetic"` (DEFAULT pre-port !! behaviour). Every barotropic depth mean !! (`rdb_barotropic_coupling::derive_bt_from_layers`, !! `face_depth_mean_u/v`, `face_depth_mean_rem_u/v`, !! `apply_bt_correction`'s open/visc_rem folds, !! `compute_bt_rem_from_visc_rem`'s `av_rem`) weights each layer's !! face thickness by the plain two-abutting-cell arithmetic mean, !! `h_face(k) = 0.5*(h_L(k) + h_R(k))`. Bit-identical to the !! pre-frhat-port tree. integer, parameter :: FRHAT_HYBRID = 1 !! `&ocean_bt_nml frhat_scheme = "hybrid"` — MOM6's !! `HVEL_SCHEME = HYBRID` face-thickness closure (`btcalc`'s !! default scheme there). !! Above the shallower of the two abutting columns' bed depths the !! face thickness is the arithmetic mean (as `FRHAT_ARITHMETIC`); !! below it, the harmonic mean `h_L*h_R/(h_L+h_R)` — which !! vanishes with the thinner side instead of being dragged up by !! the thicker one — with a linear blend across the transition !! layer. Suppresses a thin partial-bed layer's weight next to a !! thick abutting layer, the mechanism `docs/ !! visc_rem_bt_rem_plan.md` Section 7 identifies as still missing !! from every roundabout barotropic depth mean. See !! `rdb_barotropic_coupling::frhat_h_face_step` !! (`src/shared_module_utilities/rdb_frhat_face.inc`) for the !! bottom-up port of MOM6's `e_u`/`D_shallow_u` recursion. contains pure integer function parse_frhat_scheme(str, default_code) result(scheme) !! Convert an `&ocean_bt_nml frhat_scheme` namelist string to a !! `FRHAT_*` constant. Unrecognised ⇒ `default_code` if given, !! else `FRHAT_ARITHMETIC`. character(len=*), intent(in) :: str integer, intent(in), optional :: default_code integer :: fallback fallback = FRHAT_ARITHMETIC if (present(default_code)) fallback = default_code select case (trim(adjustl(str))) case ("arithmetic", "ARITHMETIC") scheme = FRHAT_ARITHMETIC case ("hybrid", "HYBRID") scheme = FRHAT_HYBRID case default scheme = fallback end select end function parse_frhat_scheme pure function nz_stack_required(nz) result(req) !! Smallest `NZ_STACK_MAX` that safely covers a run of `nz` layers. !! !! `nz + 1`. Derived from a survey of every `NZ_STACK_MAX`-dimensioned !! array in the tree; the binding consumers are the ones that index a !! plainly-`NZ_STACK_MAX`-declared array to `nz+1` rather than the !! `NZ_STACK_MAX+1` an interface array would use: !! !! Interface-indexed column workspaces are the ones to watch: a !! tridiagonal over layer INTERFACES runs to `nz + 1`, so a !! plainly-`NZ_STACK_MAX`-declared scratch array must still admit !! that extra slot. The remap kernel's own `z_old(0:NZ_STACK_MAX)` / !! `q_L`/`q_R`/`q6` are sized on the same rule. !! * `rdb_ocean_diag_fills` `z_iface` — needs `n_bin + 2`, which the !! `n_bin <= NZ_STACK_MAX - 1` output-level guard bounds by `nz + 1`. !! !! NOT `2*nz + 2`: the Redi neutral-surface locals that motivated the !! 256 bump are declared `2*NZ_STACK_MAX + 2` and so scale with the !! constant — they need only `NZ_STACK_MAX >= nz`. integer, intent(in) :: nz integer :: req req = nz + 1 end function nz_stack_required pure function nz_stack_is_sufficient(nz) result(ok) !! `.true.` when the COMPILED `NZ_STACK_MAX` covers `nz` layers. !! Callers that get `.false.` must refuse the run — the overflow is !! a silent thread-local-storage overrun, not a crash. integer, intent(in) :: nz logical :: ok ok = nz_stack_required(nz) <= NZ_STACK_MAX end function nz_stack_is_sufficient end module rdb_constants