diff --git a/components/omega/configs/Default.yml b/components/omega/configs/Default.yml index 9f589f9466cf..bd30d4b2a781 100644 --- a/components/omega/configs/Default.yml +++ b/components/omega/configs/Default.yml @@ -58,6 +58,8 @@ Omega: Mode: Implicit Type: Constant BottomDragCoeff: 1.0e-3 + SfcThicknessForcingTendencyEnable: false + SfcTracerForcingTendencyEnable: false TracerHorzAdvTendencyEnable: true TracerDiffTendencyEnable: true EddyDiff2: 10.0 @@ -71,6 +73,8 @@ Omega: PressureGradTendencyEnable: true VelVertMixTendencyEnable: true TracerVertMixTendencyEnable: true + TracerNonLocalFluxTendencyEnable: true + TracerNonLocalDiagnosticsEnable: true ManufacturedSolution: WavelengthX: 5.0e6 WavelengthY: 4.33013e6 @@ -98,6 +102,22 @@ Omega: RiCrit: 0.7 Exponent: 3.0 RiSmoothLoops: 2 + KPP: + Enable: true + UseBLDSmoothing: true + UseLangmuirCirculation: true + CriticalBulkRichardsonNumber: 0.25 + SurfaceLayerExtent: 0.1 + # SimpleShapes or MatchBoth + MatchTechnique: SimpleShapes + InterpType2: LMD94 + UseEnhancedDiffusion: true + IceFractionThresholdForLangmuir: 0.05 + IceFractionThresholdForMinimumOBL: 0.15 + MinimumOBLUnderSeaIce: 5.0 + BackgroundViscosity: 1.0e-4 + BackgroundDiffusivity: 1.0e-5 + DebugDiagnostics: false IOStreams: HorzMeshIn: UsePointerFile: false diff --git a/components/omega/doc/design/KPPMix.md b/components/omega/doc/design/KPPMix.md new file mode 100644 index 000000000000..83fe2f1d6107 --- /dev/null +++ b/components/omega/doc/design/KPPMix.md @@ -0,0 +1,249 @@ +(omega-design-kppmix)= +# KPP Boundary Layer Mixing + +**Table of Contents** +1. [Overview](#1-overview) +2. [Requirements](#2-requirements) +3. [Algorithmic Formulation](#3-algorithmic-formulation) +4. [Design](#4-design) +5. [Verification and Testing](#5-verification-and-testing) + +## 1 Overview + +This document describes the Omega implementation of the K Profile Parameterization +(KPP) ocean boundary layer mixing. KPP computes a boundary-layer depth, vertical +viscosity, vertical diffusivity, and a non-local tracer flux shape implemented outside +the implicit vertical mixing routine. The implementation follows that in MPAS-Ocean and +uses direct ports of the functions defined in the [CVMix](https://github.com/CVMix/CVMix-src) +version of KPP. + +The implementation is in `KPPMix` and is integrated with the Omega tendency and +RK2, RK4, and Forward-backward stepping routines. Relative to broad vertical mixing documentation, this +page focuses specifically on KPP theory, algorithmic choices, and testing. + +## 2 Requirements + +### 2.1 Requirement: Boundary-layer depth from bulk Richardson criterion + +Following [Large et al (1994)](https://agupubs.onlinelibrary.wiley.com/doi/10.1029/94RG01872), +the OBL depth must be diagnosed from a bulk Richardson criterion so that +mixing depth responds to evolving stratification, shear, and surface forcing. It also +must include a unresolved turbulent shear contribution. + +### 2.2 Requirement: Coefficients must be computable in parallel over columns + +The KPP implementation must operate over many columns in parallel using Omega +array/kernels, rather than serial single-column calls. + +### 2.3 Requirement: Compatible with additive vertical-mixing framework + +KPP viscosity/diffusivity fields must be compatible with existing Omega vertical +mixing infrastructure so that other chosen vertical mixing sources can be merged +with KPP. + +### 2.4 Desired: Non-local flux and profile matching controls + +KPP will support a non-local tracer flux from LMD94 and include configurable +viscosity/diffusivity matching at the base of the boundary layer. + +## 3 Algorithmic Formulation + +The implementation follows a two-stage KPP structure. + +### 3.1 Stage 1: OBL depth search + +For each water column, OBL depth $h$ is diagnosed by searching downward until +bulk Richardson number reaches a critical value: + +$$ +Ri_b(z) = \frac{\Delta b(z)\, z}{|\Delta \mathbf{U}(z)|^2 + V_t^2(z)} +$$ + +with threshold + +$$ +Ri_b(h) = Ri_{crit}. +$$ + +Here, $\Delta b$ is buoyancy jump relative to a surface layer average, +$|\Delta \mathbf{U}|^2$ is shear contribution again computed relative to the +surface layer average, and $V_t^2$ is unresolved shear. + +When the bulk Richardson number falls between model layers, quadratic interpolation +is utilized to find the depth. In addition the boundary layer depth is constrained +to fall between a configurable minimum OBL under sea ice and a maximum set by the water +column depth. + +The boundary layer depth search is a growing inner loop. The outer loop iterates over all model layers. +The current model layer is set as a boundary layer depth candidate and $Ri_b$ is calculated for all +model layers shallower than the current depth. If any layer in the inner loop has an $Ri_b*StopOBL$ that +exceeds $Ri_{crit}$ the loop terminates. + +### 3.2 Stage 2: KPP coefficients and non-local flux + +Given a diagnosed $h$, KPP computes viscosity and diffusivity coefficients at the top of every +Omega cell except the surface and the bottom using shape functions +in normalized depth $\sigma = -d/h$, where $d$ is the depth relative to the sea surface height, not the physical depth: + +$$ +K_m(\sigma) = h\, w_m(\sigma)\, M_1(\sigma), +$$ + +$$ +K_s(\sigma) = h\, w_s(\sigma)\, S_1(\sigma), +$$ + +where $w_m$ and $w_s$ are turbulent velocity scales from Monin-Obukhov style +stability functions, see Appendix B of [Large et al, 1994](https://agupubs.onlinelibrary.wiley.com/doi/10.1029/94RG01872). $M_1$ and $S_1$ are shape functions. +The generic form of the shape function is given by + +$$ +X(\sigma) = c_1 \sigma^3 + c_2 \sigma^2 + c_3 \sigma + c_4 +$$ + +The coefficients are determined by various conditions, e.g., zero viscosity and diffusivity at the +surface, assuming a linear reduction of the turbulent flux with distance from the surface in the +surface layer. As in MPAS-Ocean, we include two options to determine the final coefficients. The original +version of KPP matches predicted viscosities and diffusivities to those predicted by other schemes +(e.g., shear instability driven mixing) and a second option where viscosities and diffusivities are +instead additive. In the latter case, the shape function greatly simplifies to $X(\sigma) = \sigma(1-\sigma)^2. + +For either shape function, enhanced diffusivity can be included near the boundary layer base. This can smooth boundary layer deepening in time. + +The non-local tracer flux uses the scalar shape function, scaled by the constant +$C_s$ from Eq. (20) of Large et al. (1994) rather than by $h\, w_s$: + +$$ +\gamma_s(\sigma) = C_s\, S_1(\sigma). +$$ + +Here $S_1$ is always the unmatched scalar shape $\sigma(1-\sigma)^2$, regardless +of the `MatchTechnique` setting. Matching is a property of the diffusivity +profile only: the matched shape is non-zero at $\sigma = -1$ by construction, so +reusing it for $\gamma_s$ would leave a finite non-local flux at the boundary +layer base that drops discontinuously to zero immediately below it. CVMix draws +the same distinction, exposing the non-local shape as a separate choice from the +matching option. + + + +## 4 Design + +### 4.1 Data types and parameters + +#### 4.1.1 Parameters + +KPP is configured from the `VertMix: KPP` YAML group. Key parameters include: + +- `Enable` +- `CriticalBulkRichardsonNumber` +- `MatchTechnique` (`SimpleShapes` or `MatchBoth`) +- `InterpType2` (`LMD94`, `Linear`, `Quadratic`, `Cubic`) +- `UseEnhancedDiffusion` +- `IceFractionThresholdForLangmuir` +- `IceFractionThresholdForMinimumOBL` +- `MinimumOBLUnderSeaIce` +- `BackgroundViscosity` +- `BackgroundDiffusivity` +- `DebugDiagnostics` + +Defaults and usage examples are documented in the user guide page: +[KPP in the User Guide](../userGuide/KPPMix.md). + +#### 4.1.2 Class/data structure + +`KPPMix` is a singleton that owns persistent output fields, including: + +- `BoundaryLayerDepth`, `IndexBoundaryLayerDepth` +- `VertDiff`, `VertVisc` +- `VertNonLocalFlux` +- diagnostics such as `BulkRichardsonNumber`, `BulkRichardsonShear`, + `UnresolvedShear`, `BuoyancyJump`, and `TurbulentVelocityScale` + +### 4.2 Methods + +Main interface: + +```c++ +void computeKPPMix(const Array2DReal &PotentialDensity, + const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, + const Array2DReal &BruntVaisalaFreqSq, + const Array1DReal &IceFraction, + const Array1DReal &WindSpeed10m = Array1DReal()); +``` + +Internal stages: +- `computeOBLDepth(...)` +- `computeMixingCoefficients(...)` + +### 4.3 Time stepper coupling behavior + +KPP is coupled to all three Omega time steppers -- Forward-Backward, +RungeKutta2, and RungeKutta4. For every stepper, KPP is evaluated exactly +once per time step, at the start of the step on the state at time $n$, before +any tendency is evaluated. The resulting boundary-layer depth, viscosity, +diffusivity, and non-local flux profile are then held fixed for the remainder +of the step. + +This design differs from MPAS-Ocean, where the boundary layer depth, diffusivity, and viscosity are computed at the end of the time step and the non local flux is applied on the following timestep. The primary advantage of this new approach is: + +1. **Consistency.** The non-local flux $\gamma_s$ applied in the tracer + tendency at each stage and the diffusivity $K_s$ used by the end-of-step + implicit vertical mixing solve are derived from the same OBL depth and the + same shape function $S_1(\sigma)$. Recomputing KPP at each stage would + pair a stage-dependent $\gamma_s$ with a different $K_s$, breaking the + correspondence described in section 3.2. + +Because KPP is evaluated before the step advances, the KPP diagnostics +written for a step describe the state at the beginning of that step. The +coefficients are lagged relative to the state during the implicit solve at the end of the +step. Full call-flow detail per stepper is described for developers and +users in: + +- [Developer KPP workflow](../devGuide/KPPMix.md) +- [User runtime notes](../userGuide/KPPMix.md) + +## 5 Verification and Testing + +### 5.1 Unit-level checks + +Use targeted tests and diagnostics to verify: + +- OBL depth search monotonicity and threshold crossing behavior +- Positive bounded coefficients and expected background behavior below OBL +- Correct enable/disable behavior for non-local flux and enhanced diffusion + +Tests cover requirements: 2.1, 2.2, 2.3, 2.4. + +### 5.2 Polaris testing + +The single column test case can be run across a wide range of surface forcing +(heat, evaporative, and momentum fluxes) and the following diagnostics will be +plotted over time + +- `BoundaryLayerDepth` +- `BulkRichardsonNumber` +- `VertDiff`, `VertVisc` +- `VertNonLocalFlux` + +For simple cases, such as free convection, boundary layer depth can be compared against +a semi-analytic solution (e.g., Appendix F, ([Van Roekel et al, 2018](https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2018MS001336)). + +The global test case, forced by annual averaged ERA-5 net surface heat, freshwater, and +momentum fluxes provides a qualitative assessment of KPP behavior. + +### 5.3 Configuration sensitivity checks + +Short single column and global test cases can be run varying critical parameters such as + +- `CriticalBulkRichardsonNumber` +- `MatchTechnique` +- `InterpType2` +- `UseEnhancedDiffusion` +- sea-ice thresholds + +to ensure expected qualitative and quantitative responses in OBL depth and +mixing intensity. diff --git a/components/omega/doc/devGuide/Forcing.md b/components/omega/doc/devGuide/Forcing.md index 97d38d8ae42d..7e69dc302bc6 100644 --- a/components/omega/doc/devGuide/Forcing.md +++ b/components/omega/doc/devGuide/Forcing.md @@ -6,7 +6,8 @@ This page describes design and implementation details for forcing-related pathways in Omega, currently this includes: - Surface stress forcing (e.g. wind stress) -- Surface tracer restoring +- Surface thickness and tracer flux forcing (actively coupled or data-forced) +- Surface tracer restoring (soon to be ported as a field originating from the coupler) ## Surface stress forcing design @@ -37,6 +38,70 @@ pathways in Omega, currently this includes: - `Omega.Tendencies.SfcStressForcingTendencyEnable` - gates execution of surface stress forcing tendency kernel +## Surface thickness and tracer flux forcing design + +### Surface thickness and tracer flux forcing data flow + +**Thickness equation pathway:** + +1. External fields provide freshwater and salt flux components: + - `SnowFlux`, `RainFlux`, `EvaporationFlux` + - `SeaIceFreshWaterFlux`, `IceRunoffFlux`, `RiverRunoffFlux` + - `SeaIceSaltFlux` +2. `Forcing` stores the flux fields in `TracerForcingVars` +3. The tendency term `SfcThicknessForcingOnCell` sums the freshwater and salt mass fluxes and applies them to +the surface layer pseudo-thickness. + +**Tracer equation pathway:** + +1. External fields provide heat and salt flux components: + - `LatentHeatFlux`, `SensibleHeatFlux` + - `LongWaveHeatFluxUp`, `LongWaveHeatFluxDown` + - `SeaIceHeatFlux`, `ShortWaveHeatFlux` + - mass fluxes which add energy changes (`SnowFlux`, `RainFlux`, `IceRunoffFlux`, `RiverRunoffFlux`) + - `SeaIceSaltFlux` +2. `Forcing` stores the flux fields in `TracerForcingVars` +3. The tendency term `SfcTracerForcingOnCell` converts the summed external heat fluxes to a conservative-temperature tendency, + and applies the external sea-ice salt flux to the top layer salt content thus impacting salinity. + +### Surface thickness and tracer flux forcing key classes/components + +- `TracerForcingVars` + - Stores 13 coupled flux cell-centered fields: 6 freshwater fluxes, 6 heat + fluxes, and 1 salt flux component + - Fields initialized to zero and registered in `Forcing` field group +- `SfcThicknessForcingOnCell` tendency term + - Computes the layer mass contribution (converted to pseudo-thickness): $\sum (\text{SnowFlux} + \text{RainFlux} + \text{EvaporationFlux} + \text{SeaIceFreshWaterFlux} + \text{IceRunoffFlux} + \text{RiverRunoffFlux} + \text{SeaIceSaltFlux}) / \rho_{sw}$ + - Applied only at surface layer (top active layer) using `MinLayerCell` +- `SfcTracerForcingOnCell` tendency term + - For temperature: adds the direct heat fluxes + $Q_{\text{latent}} + Q_{\text{sensible}} + Q_{\text{lw,up}} + Q_{\text{lw,down}} + Q_{\text{ice}} + Q_{\text{sw}}$ +, the phase change and enthalpy of added mass $(\text{RainFlux} + \text{RiverRunoffFlux}) c^0_{p,sw} C_T^{\text{top}} + (\text{SnowFlux} + \text{IceRunoffFlux})(c^0_{p,sw} C_T^{\text{frz}} - L_{\text{ice}})$, + (where $C_T^{\text{frz}}$ is from EOS at top-layer salinity and pressure), + and scales by $H_{\text{FluxFac}}$. + - For salinity: applies salt flux with unit conversion: $\text{SeaIceSaltFlux} \times S_{\text{FluxFac}}$ + - Applied only at surface layer using `MinLayerCell` + - Uses tracer index validation to apply to specific tracers only +- `Forcing` + - Manages `TracerForcingVars` instance +- `Tendencies` + - Calls `SfcThicknessForcingOnCell` in `computePseudoThicknessTendenciesOnly` + - Calls `SfcTracerForcingOnCell` in `computeTracerTendenciesOnly` after surface tracer restoring + +### Surface thickness and tracer flux forcing config coupling + +- `Omega.Tendencies.SfcThicknessForcingTendencyEnable` + - gates execution of coupled flux thickness kernel + - controls freshwater and salt flux forcing on sea surface height +- `Omega.Tendencies.SfcTracerForcingTendencyEnable` + - gates execution of coupled flux tracer kernel + - controls direct heat flux forcing on temperature and salt flux forcing on salinity + +## Notes + +- Currently all forcing is applied to the surface layer only. In the future, vertical spreading of river runoff contributions will be needed. +- `SeaIceFreshWaterFlux` is the pure freshwater mass from sea ice. The full mass flux from sea ice is `SeaIceFreshWaterFlux + SeaIceSaltFlux` + ## Surface tracer restoring design ### Surface tracer restoring data flow diff --git a/components/omega/doc/devGuide/HorzMesh.md b/components/omega/doc/devGuide/HorzMesh.md index c61f45299aaf..4c1596a9ec69 100644 --- a/components/omega/doc/devGuide/HorzMesh.md +++ b/components/omega/doc/devGuide/HorzMesh.md @@ -47,6 +47,15 @@ OMEGA::parallelFor({HMesh->NCellsOwned,HMesh->MaxEdges}, } ``` +`MaxEdges` is read from the mesh file and is therefore only known at run time. +Kernels that need a fixed-size per-thread array indexed by edge should size it +with the compile-time bound instead: +``` +OMEGA::Real Weights[OMEGA::HorzMesh::MaxEdgesBound]; +``` +`MaxEdgesBound` is a single shared upper bound on `MaxEdges`; meshes exceeding +it are rejected. Do not introduce a local copy of this limit. + For member variables that are host arrays, variable names are appended with an `H`. Array variable names not ending in `H` are device arrays. diff --git a/components/omega/doc/devGuide/KPPMix.md b/components/omega/doc/devGuide/KPPMix.md new file mode 100644 index 000000000000..947457541276 --- /dev/null +++ b/components/omega/doc/devGuide/KPPMix.md @@ -0,0 +1,243 @@ +(omega-dev-kppmix)= + +# KPP Boundary Layer Mixing + +This guide maps Omega KPP implementation details to runtime behavior and code +locations. It complements the design page by focusing on concrete APIs, +call flow, and developer test strategy. + +## Implementation Overview + +The Omega implementation of KPP mostly follows directly from the MPAS-Ocean implementation. +Notably, by default it does not match diffusivity and viscosity from interior mixing +(below the ocean surface boundary layer) sources at the base of the boundary +layer. Instead these separate sources are added directly to the KPP diagnosed +diffusivity and viscosity. Matching can be enabled via the `MatchTechnique` parameter to +`MatchBoth` in the Omega yaml file. Boundary layer depth is computed as the depth +where the bulk Richardson number exceeds a critical value. It is then smoothed horizontally. +KPP diffusivity, viscosity, and non local fluxes are computed based on this boundary layer. Unlike MPAS-Ocean, the boundary layer depth, vertical vicosity and vertical diffusivity are calculated at the beginning of a time step to ensure consistency of the nonlocal and local parts of the KPP scheme. + +Omega KPP is implemented in `KPPMix` as a singleton with two major compute +phases: + +1. OBL depth diagnosis (`computeOBLDepth`) +2. Coefficient/profile construction (`computeMixingCoefficients`) + +Main class/API surface is in `src/ocn/KPPMix.h` and implementation is in +`src/ocn/KPPMix.cpp`. + +### Depth convention + +All KPP depths are measured downward from the free surface, not from the geoid. +`VertCoord::GeomZInterface` and `GeomZMid` are geometric heights relative to +`z = 0`, with `GeomZInterface(ICell, MinLayerCell)` equal to +`VertCoord::SshCell`. KPP therefore forms depths as +`SshCell(ICell) - GeomZ...(ICell, K)`. Layer thicknesses are differences of +geometric heights and are unaffected by the sea surface height. + +## KPP Notation + +KPP splits the water column at the ocean boundary layer (OBL) depth `h`. Inside the OBL, diffusivity and viscosity are prescribed via a cubic shape function +scaled by `h` and a turbulent velocity scale; below it, only interior mixing + applies (e.g., shear instability driven mixing). + +| Symbol | Code name | Units | Meaning | +| --- | --- | --- | --- | +| `h` | `HOBL`, `BoundaryLayerDepth` | m | OBL depth below the free surface | +| `d` | `ZDepth`, `ZCenter` | m | depth below the free surface | +| `sigma` | `Sigma` | - | normalized depth in `[-1,0]`, Omega sign convention: 0 at the surface, -1 at the OBL base | +| `sigma_mu` | `SigmaMu` | - | `-Sigma`, in `[0,1]`; the CVMix/Large et al. convention | +| `u*` | `UStar` | m/s | surface friction velocity from wind stress | +| `B_0` | `BuoyFlux` | m^2/s^3 | surface buoyancy flux; **negative is destabilizing** (convection) | +| `L` | `LMoninObukhov` | m | Monin-Obukhov length, `u*^3 / (kappa B_0)` | +| `zeta` | `Zeta` | - | stability coordinate `d/L`; negative is unstable | +| `kappa` | `VonKar` | - | von Karman constant | +| `epsilon` | `SurfaceLayerExtent` | - | surface layer as a fraction of `h` | +| `w_m`, `w_s` | `WMTurb`, `WSTurb` | m/s | turbulent velocity scales for momentum and scalars | +| `Vt^2` | `Vt2`, `UnresolvedShear` | m^2/s^2 | unresolved turbulent shear, Large et al. (1994) Eq. 23 | +| `Ri_b` | `RiBulk`, `BulkRichardsonNumber` | - | bulk Richardson number, Eq. 21 | +| `G(sigma)` | `kppShape*` | - | non-dimensional profile shapes | +| `gamma` | `VertNonLocalFlux` | - | non-local (counter-gradient) tracer flux coefficient | + +The OBL depth is the shallowest `d` at which `Ri_b(d)` reaches the critical +value; the search is done per cell in `computeOBLDepth`, and the crossing depth +is refined by a quadratic fit through the three nearest cell-center `Ri_b` +values. + +Two properties of that search are worth knowing before editing it. The +surface-layer reference averages span the top `epsilon*d`, which grows +monotonically with the trial depth, so the running sums are carried across trial +depths in a two-pointer scan rather than rebuilt from the surface at each one; +resetting them inside the loop would make the search `O(K^2)`. The loop also +stops at the crossing, since only the crossing level and the two above it feed +the refinement. `DebugDiagnostics` suppresses that early exit so the `Ri_b` +diagnostic profiles are filled over the whole column; it has no effect on +`BoundaryLayerDepth` or on any mixing coefficient. + +### Shape and stability functions + +The non-dimensional functions live in `src/ocn/KPPConstants.h` in namespace +`OMEGA::KPP`. All shape functions take `Sigma` in `[-1,0]` and convert to +`SigmaMu` internally, so callers never flip signs: + +- `kppShapeMomentum`, `kppShapeScalar` -- gradient shapes for viscosity and + diffusivity (`SimpleShapes`) +- `kppShapeMatched` -- gradient shape that additionally reaches a prescribed + value at the OBL base, used by `MatchBoth` +- the non-local flux has no shape function of its own; it reuses whichever + scalar shape is in effect, scaled by `C_s` instead of `h*w_s` +- `kppPhiInvMomentum`, `kppPhiInvScalar` -- **inverse** Monin-Obukhov stability + functions `phi^-1(zeta)`; they already return the reciprocal, so do not + invert them again at the call site + +### Constants and defaults + +`src/ocn/KPPConstants.h` is the single authoritative source for KPP default +values. The runtime-configurable members of `KPPMix` (`SurfaceLayerExtent`, the +two ice-fraction thresholds and `MinimumOBLUnderSeaIce`) are initialized from +those constants rather than from inline literals, so a default is changed in +exactly one place. + +Per-thread edge scratch arrays in `computeOBLDepth` are sized from +`HorzMesh::MaxEdgesBound`, the shared compile-time bound on edges per cell; +KPP does not define its own maximum. + +## Runtime Call Flow + +### Tendency coupling + +KPP coupling into tendencies occurs through: + +- `Tendencies::computeKPPFields(...)` + +`computeKPPFields(...)` assembles required inputs: + +- potential density from EOS specific volume +- Brunt-Vaisala frequency squared +- edge normal and reconstructed tangential velocity +- surface friction velocity from wind stress +- surface buoyancy flux from heat/freshwater forcing + +Then it calls: + +```c++ +KPPInstance->computeKPPMix(...) +``` + +### Time stepper interaction + +KPP is evaluated exactly once per time step by every Omega time stepper +(`src/timeStepping/RungeKutta4Stepper.cpp`, +`src/timeStepping/RungeKutta2Stepper.cpp`, +`src/timeStepping/ForwardBackwardStepper.cpp`) through two hooks in +`src/timeStepping/TimeStepper.cpp`: + +1. **Start-of-step compute**: `TimeStepper::updateKPPFields(...)` fetches the + current tracer array and calls `Tendencies::computeKPPFields(...)`. Each + stepper calls it once at the top of `doStep()`, after `prescribeState` / + `prescribeVelocity` and before the first tendency evaluation: + + - **RungeKutta4Stepper**: in the `Stage == 0` branch, before the first + `computeAllTendencies(...)`. + - **RungeKutta2Stepper**: after the initial `prescribeState(...)`. + - **ForwardBackwardStepper**: after `prescribeVelocity(...)`. + +2. **End-of-step application**: + `TimeStepper::applyImplicitVerticalMixing(...)` is called by every + stepper's `doStep()` immediately after `State->updateTimeLevels()`. It + recomputes auxiliary state and calls `VertMix::VertMixImplicit(...)` if + enabled. It does **not** recompute KPP; the fields from the start of the + step are reused. + +The KPP fields are consumed in two places within the step: + +- `Tendencies::computeTracerTendenciesOnly(...)` adds the non-local tracer + tendency from `KPPMix::VertNonLocalFlux` at every stage. +- `VertMix::computeVertMix(...)`, called from `VertMixImplicit(...)`, merges + `KPPMix::VertDiff` / `VertVisc` into the final coefficients using + `KPPMix::IndexBoundaryLayerDepth`. + +Both therefore see the same boundary-layer depth and the same shape function, +so the non-local flux and the diffusivity it is paired with cannot become +inconsistent. KPP diagnostics written for a step describe the state at the +beginning of that step. + +Note that `VertMix::VertMixImplicit(...)` recomputes Brunt-Vaisala frequency +itself before calling `computeVertMix(...)`, so shear and convective mixing +still use end-of-step stratification; only the KPP contribution is lagged. + +## Configuration Mapping + +KPP reads configuration from the `VertMix: KPP` subgroup during `KPPMix::init`. +Important keys and class members: + +- `Enable` -> `Enabled` +- `CriticalBulkRichardsonNumber` -> `CriticalRichardson` +- `SurfaceLayerExtent` -> `SurfaceLayerExtent` +- `MatchTechnique` -> `MatchTechnique` (a `KPPMatchType` enum, not a string) +- `InterpType2` -> `InterpType2Str` +- `UseEnhancedDiffusion` -> `UseEnhancedDiffusion` +- `UseLangmuirCirculation` -> `UseLangmuirCirculation` +- `IceFractionThresholdForLangmuir` -> `IceFractionThresholdForLangmuir` +- `IceFractionThresholdForMinimumOBL` -> `IceFractionThresholdForMinimumOBL` +- `MinimumOBLUnderSeaIce` -> `MinimumOBLUnderSeaIce` +- `BackgroundViscosity` -> `BackgroundVisc` +- `BackgroundDiffusivity` -> `BackgroundDiff` +- `DebugDiagnostics` -> `DebugDiagnostics` + +See [User KPP guide](../userGuide/KPPMix.md) for defaults and runnable examples. + +## Output and Diagnostic Fields + +`KPPMix::defineFields()` registers KPP outputs for I/O. Frequently used outputs: + +- `BoundaryLayerDepth` +- `VertNonLocalFlux` +- `BulkRichardsonNumber` +- `BulkRichardsonShear` +- `UnresolvedShear` +- `BuoyancyJump` +- `TurbulentVelocityScale` +- `PotentialDensity` +- `SurfaceFrictionVelocity` +- `SurfaceBuoyancyFlux` + +These can be enabled in output stream contents to diagnose OBL and profile +behavior in experiments. + +## Developer Notes + +- `MatchTechnique` accepts only `SimpleShapes` and `MatchBoth`. Any other value + aborts at init rather than falling back, so a typo cannot silently change the + scheme being run. +- `MatchBoth` matches the interior coefficient *value* at the OBL base. The + shape derivative there is zero, so the gradient is not yet matched despite the + name. +- `MatchBoth` needs interior coefficients to be passed in; without them + `ShapeAtBase` is zero and it degenerates exactly to `SimpleShapes`. +- `Interptype2` accepts `LMD94`, `Linear`, `Quadratic`, and `Cubic`, but only `LMD94` is recommended strongly recommended. For `MatchBoth` other options can result in negative diffusivities and viscosities. +- When `DebugDiagnostics` is enabled in debug builds, targeted diagnostic + logging is available; behavior is compile/build-mode aware. + +## Testing Strategy + +### Code-level checks + +1. Verify KPP initialization with explicit and default YAML keys. +2. Verify the `computeKPPFields` path executes with KPP enabled and + early-returns when disabled. +3. Verify per-stepper sequencing: exactly one KPP evaluation per step for all + three steppers, occurring before the first tendency evaluation and reused + by the end-of-step implicit vertical mixing. The `Tend:computeKPPFields` + Pacer region can be used to confirm the call count. + +### Diagnostics-based checks + +1. Output `BoundaryLayerDepth`, `BulkRichardsonNumber`, and `VertDiff`. +2. Confirm OBL depth and coefficient evolution under changing forcing. +3. Validate optional outputs (`VertNonLocalFlux`) only when enabled. + +### Regression checks + +Run CTests and appropriate OMEGA regression workflows documented in: +[Testing Code](./Testing.md). diff --git a/components/omega/doc/devGuide/TendencyTerms.md b/components/omega/doc/devGuide/TendencyTerms.md index 5fa72197132f..2fd762e28b3e 100644 --- a/components/omega/doc/devGuide/TendencyTerms.md +++ b/components/omega/doc/devGuide/TendencyTerms.md @@ -41,9 +41,11 @@ implemented: - `TracerHighOrderHorzAdvOnCell` - `TracerDiffOnCell` - `TracerHyperDiffOnCell` +- `SfcThicknessForcingOnCell` +- `SfcTracerForcingOnCell` - `SurfaceTracerRestoringOnCell` ## See Also -Additional information on forcing (currently wind forcing and surface tracer -restoring) is detailed in [](omega-dev-forcing). +Additional information on forcing (surface stress, surface mass and tracer flux forcing, and +surface tracer restoring) is detailed in [](omega-dev-forcing). diff --git a/components/omega/doc/index.md b/components/omega/doc/index.md index dfc81a284670..af49963c74a7 100644 --- a/components/omega/doc/index.md +++ b/components/omega/doc/index.md @@ -52,6 +52,7 @@ userGuide/VertCoord userGuide/PGrad userGuide/Timing userGuide/VerticalMixingCoeff +userGuide/KPPMix userGuide/VertAdv userGuide/Forcing userGuide/SfcCoupling @@ -100,6 +101,7 @@ devGuide/VertCoord devGuide/PGrad devGuide/Timing devGuide/VerticalMixingCoeff +devGuide/KPPMix devGuide/VertAdv devGuide/Forcing devGuide/SfcCoupling @@ -143,6 +145,7 @@ design/Timers design/TimeStepping design/Tracers design/TridiagonalSolver +design/KPPMix design/VertAdv design/VertCoord design/VerticalMixingCoeff diff --git a/components/omega/doc/userGuide/Forcing.md b/components/omega/doc/userGuide/Forcing.md index d8fac4383730..01eeff4b5e3d 100644 --- a/components/omega/doc/userGuide/Forcing.md +++ b/components/omega/doc/userGuide/Forcing.md @@ -5,6 +5,7 @@ This page documents the user-facing configuration and behavior for current forcing in Omega: - Surface stress forcing (e.g. wind stress) +- Coupled tracer flux forcing (mass, energy and salt) - Surface tracer restoring ## Surface stress forcing @@ -39,6 +40,74 @@ Surface stress forcing uses surface stress input fields: These are stored in forcing variables and used to form edge-normal stress (`NormalStressEdge`) that enters momentum tendencies. +## Surface thickness and tracer flux forcing + +Surface thickness and tracer flux forcing applies ocean-atmosphere and ocean-sea ice fluxes from the other model +components (atmosphere, sea ice) to the thickness and tracer equations. This enables +the ocean to respond to heat, freshwater, and salt exchanges at the surface. These fluxes can be from data or (active) coupled components. + +### Surface thickness and tracer flux forcing configuration + +Surface thickness and tracer flux forcing is controlled by two configuration flags: + +```yaml +Omega: + Tendencies: + SfcThicknessForcingTendencyEnable: false + SfcTracerForcingTendencyEnable: false +``` + +- `Tendencies.SfcThicknessForcingTendencyEnable`: enables coupled freshwater and salt flux forcing on thickness +- `Tendencies.SfcTracerForcingTendencyEnable`: enables coupled heat and salt flux forcing on tracers + + +### Required input fields + +Coupled tracer flux forcing uses 13 auxiliary fields organized by type: + +**Freshwater mass fluxes (kg m⁻² s⁻¹):** +- `SnowFlux`: precipitation from snow +- `RainFlux`: precipitation from rain +- `EvaporationFlux`: evaporative water loss +- `SeaIceFreshWaterFlux`: freshwater mass flux from sea-ice melt or formation +- `IceRunoffFlux`: runoff from land ice +- `RiverRunoffFlux`: runoff from rivers + +**Heat fluxes (W m⁻²):** +- `LatentHeatFlux`: latent heat transfer +- `SensibleHeatFlux`: sensible heat transfer +- `LongWaveHeatFluxUp`: upward longwave radiation +- `LongWaveHeatFluxDown`: downward longwave radiation +- `SeaIceHeatFlux`: heat/energy from sea-ice interaction (incl. enthalpy of meltwater) +- `ShortWaveHeatFlux`: shortwave (solar) radiation + +**Salt mass flux (kg m⁻² s⁻¹):** +- `SeaIceSaltFlux`: salt flux from sea-ice formation/melt processes + +These fields are populated by external coupling components (typically atmosphere +and ice models). Omega assumes the incoming values match the documented units. +For now, there are assumed to come from a `forcing.nc` file, but later will be provided +by the equivalent `ocn_comp_mct.F`. + +### Notes + +- Coupled fluxes are applied only at the surface layer (top active layer) for each cell. In the future, vertical spreading of contributions from river runoff will be needed. +- Pseudo-thickness tendency is computed from the (six) freshwater mass fluxes and the salt mass flux + `SeaIceSaltFlux`, converted to a pseudo-thickness change. +- Temperature tendency is computed from direct heat flux plus + mass-flux enthalpy terms, converted to conservative-temperature tendency via + $H_{\text{FluxFac}} = 1.0 / (\rho_{sw} c^0_{p,sw})$ where $c^0_{p,sw}$ is the reference + specific heat of seawater defined by TEOS-10. The enthalpy associated with mass fluxes is currently hard-coded to SST for liquid fluxes and the freezing temperature for solid fluxes (which are melted using a constant latent heat of fusion). Note that the enthalpy of liquid meltwater from sea ice is already included in `SeaIceHeatFlux`. +- Salinity tendency from `SeaIceSaltFlux` is scaled by + $S_{\text{FluxFac}} = 1.0e3 / \rho_{sw}$ to account for unit conversion from + kg/(m²·s) to salinity units (g/kg). +- Fluxes are assumed to be in the documented units (i.e. net mass fluxes); + any unit conversion should be performed by the coupling component before providing flux + values to Omega. +- The reference density used here ($\rho_{sw}$) is not a Boussinesq density, it is the + conversion factor from mass to pseudo-thickness. +- No iceberg fluxes are included for now. + ## Surface tracer restoring Surface tracer restoring applies a piston-velocity tendency, or damping, at the ocean diff --git a/components/omega/doc/userGuide/KPPMix.md b/components/omega/doc/userGuide/KPPMix.md new file mode 100644 index 000000000000..df39a12d6345 --- /dev/null +++ b/components/omega/doc/userGuide/KPPMix.md @@ -0,0 +1,134 @@ +(omega-kppmix)= + +# KPP Boundary Layer Mixing + +This page explains how to enable, configure, and use Omega K-Profile +Parameterization (KPP) boundary layer mixing in runs. The implementation follows +directly from the MPAS-Ocean implementation. + +Related pages: +- KPP design/theory: [Design KPP document](../design/KPPMix.md) +- KPP implementation details: [Developer KPP document](../devGuide/KPPMix.md) +- Broader vertical mixing options: [Vertical Mixing Coefficients](./VerticalMixingCoeff.md) + +## What KPP Provides + +KPP computes: + +- Ocean boundary layer depth (`BoundaryLayerDepth`) +- Vertical viscosity (`VertVisc`) +- Vertical diffusivity (`VertDiff`) +- Non-local tracer flux profile (`VertNonLocalFlux`) + +It uses a bulk Richardson depth search followed by profile-based coefficient +construction. The non-local flux is a standard part of the KPP formulation and +is included by default. + +## How KPP Is Used in Time Stepping + +KPP is connected to all three Omega time steppers: Forward-Backward, RungeKutta2, and RungeKutta4. For +whichever stepper is active, KPP is computed **once per time step**, at the +start of the step (in contrast to MPAS-Ocean), before any tendency is evaluated. +Boundary-layer depth, viscosity, diffusivity, and the non-local flux profile +are then held fixed for the rest of the step and are used by: + +- the non-local tracer tendency at every internal stage of the stepper, and +- the implicit vertical mixing solve applied after the time levels are + advanced. + +Because both use the same KPP fields, the non-local flux and the diffusivity +it is paired with are always consistent with one another. + +The KPP diagnostics written to output for a step therefore describe the ocean +state at the **beginning** of that step, not the updated state at the end of +it. This is the same convention used by the MPAS-Ocean split-explicit +stepper. + +## Configuration + +KPP settings are under `VertMix: KPP` in `omega.yml`. + +### Example + +```yaml +VertMix: + KPP: + Enable: true + CriticalBulkRichardsonNumber: 0.25 + SurfaceLayerExtent: 0.1 + MatchTechnique: SimpleShapes + InterpType2: LMD94 + UseEnhancedDiffusion: true + UseBLDSmoothing: true + UseLangmuirCirculation: true + IceFractionThresholdForLangmuir: 0.05 + IceFractionThresholdForMinimumOBL: 0.15 + MinimumOBLUnderSeaIce: 5.0 + BackgroundViscosity: 1.0e-4 + BackgroundDiffusivity: 1.0e-5 + DebugDiagnostics: false +``` + +### Key Options + +| Key | Meaning | Typical default | +|---|---|---| +| `Enable` | Enable KPP mixing | `true` | +| `CriticalBulkRichardsonNumber` | OBL depth criterion threshold | `0.25` | +| `SurfaceLayerExtent` | Surface layer thickness as a fraction of the OBL depth ($\epsilon$ in Large et al. 1994) | `0.1` | +| `MatchTechnique` | How the K profile meets interior mixing at the OBL base: `SimpleShapes` or `MatchBoth` | `SimpleShapes` | +| `InterpType2` | Interpolation type used near OBL matching/base logic | `LMD94` | +| `UseEnhancedDiffusion` | Enable enhanced diffusion treatment near OBL base | `true` | +| `UseBLDSmoothing` | Apply horizontal smoothing to the boundary layer depth | `true` | +| `UseLangmuirCirculation` | Apply Langmuir enhancement to the turbulent velocity scale | `true` | +| `IceFractionThresholdForLangmuir` | Above this ice fraction, disable Langmuir enhancement | `0.05` | +| `IceFractionThresholdForMinimumOBL` | Above this ice fraction, enforce minimum OBL depth | `0.15` | +| `MinimumOBLUnderSeaIce` | Minimum OBL depth under sea ice (m) | `5.0` | +| `BackgroundViscosity` | Background viscosity below the OBL (m^2/s) | `1.0e-4` | +| `BackgroundDiffusivity` | Background diffusivity below the OBL (m^2/s) | `1.0e-5` | +| `DebugDiagnostics` | Enable additional KPP diagnostics/logging in debug workflows, and extend the Ri diagnostic profiles below the boundary layer base | `false` | + +Note that KPP reads its own `BackgroundViscosity` and `BackgroundDiffusivity` +from the `VertMix: KPP` group; these are separate from the `VertMix: Background` +values used by the other vertical mixing schemes. + +## Output and Diagnostics + +To diagnose KPP, include KPP fields in output stream contents. Common fields: + +- `BoundaryLayerDepth` +- `VertNonLocalFlux` +- `BulkRichardsonNumber` +- `BulkRichardsonShear` +- `UnresolvedShear` +- `BuoyancyJump` +- `TurbulentVelocityScale` +- `PotentialDensity` +- `SurfaceFrictionVelocity` +- `SurfaceBuoyancyFlux` + +The boundary layer search stops at the first level where the bulk Richardson +number reaches its critical value, so `BulkRichardsonNumber`, +`BulkRichardsonShear`, `UnresolvedShear` and `BuoyancyJump` are zero below the +boundary layer base. Set `DebugDiagnostics: true` to compute and output the +full water column profile of these four fields; it does not change +`BoundaryLayerDepth` or any mixing coefficient. + +## Typical Workflow + +1. Enable KPP and set baseline options in `omega.yml`. +2. Run a short case. +3. Inspect `BoundaryLayerDepth` and coefficient fields. +4. If needed, tune `CriticalBulkRichardsonNumber`, `MatchTechnique`, and + `InterpType2`. +5. Re-run and compare diagnostics. + +## Practical Notes + +- Non-local tracer transport is required for physically correct KPP boundary + layer tracer fluxes. It is applied through the + `Tendencies: TracerNonLocalFluxTendencyEnable` flag, which is on by default + and should be left enabled; see + [Tendency Terms](./TendencyTerms.md). +- Use `DebugDiagnostics` sparingly for troubleshooting targeted cases. +- When studying sea-ice regions, review minimum-OBL and ice-threshold options. diff --git a/components/omega/doc/userGuide/TendencyTerms.md b/components/omega/doc/userGuide/TendencyTerms.md index 3db2b4c10098..8293ea9362ac 100644 --- a/components/omega/doc/userGuide/TendencyTerms.md +++ b/components/omega/doc/userGuide/TendencyTerms.md @@ -20,6 +20,8 @@ tendency terms are currently implemented: | TracerHyperDiffOnCell | biharmonic horizontal mixing of thickness-weighted tracers | SfcStressForcingOnEdge | forcing by surface stress (e.g. wind), defined on edges | BottomDragOnEdge | bottom drag, defined on edges +| SfcThicknessForcingOnCell | surface pseudo-thickness forcing from coupled freshwater and salt fluxes, defined on cells +| SfcTracerForcingOnCell | surface tracer forcing from coupled heat and salt fluxes, with direct heat always and mass-flux enthalpy terms gated by thickness forcing, defined on cells | SurfaceTracerRestoringOnCell | surface tracer restoring, defined on cells Among the internal data stored by each functor is a `bool` which can enable or @@ -57,8 +59,34 @@ the currently available tendency terms: | | BottomDragTendency:Mode | bottom drag mode; `Implicit` or `Explicit` | | BottomDragTendency:Type | bottom drag type; `Constant` | | BottomDragTendency:BottomDragCoeff | bottom drag coefficient +| SfcThicknessForcingOnCell | SfcThicknessForcingTendencyEnable | enable/disable term +| SfcTracerForcingOnCell | SfcTracerForcingTendencyEnable | enable/disable term | SurfaceTracerRestoringOnCell | SurfaceTracerRestoringEnable | enable/disable term +The vertical advection, pressure gradient and vertical mixing tendencies are not +implemented as the functors above, but they are enabled from the same +`Tendencies` section of the configuration file: + +| Parameter | Description +| ------------ | ------------ | +| ThicknessVertAdvTendencyEnable | enable/disable vertical advection of thickness +| VelocityVertAdvTendencyEnable | enable/disable vertical advection of velocity +| TracerVertAdvTendencyEnable | enable/disable vertical advection of tracers +| PressureGradTendencyEnable | enable/disable the pressure gradient tendency +| VelVertMixTendencyEnable | enable/disable vertical mixing of velocity; required when bottom drag uses `Implicit` mode +| TracerVertMixTendencyEnable | enable/disable vertical mixing of tracers +| TracerNonLocalFluxTendencyEnable | enable/disable the KPP non-local tracer flux; see [KPP Boundary Layer Mixing](./KPPMix.md) +| TracerNonLocalDiagnosticsEnable | enable/disable diagnostic output of the non-local flux tendency + +The non-local options are only read when velocity or tracer vertical mixing is +enabled. If omitted, `TracerNonLocalFluxTendencyEnable` defaults to disabled and +`TracerNonLocalDiagnosticsEnable` defaults to enabled. + +Two further flags control tendency sources rather than individual terms: +`UseCustomTendency` enables user-supplied tendencies and +`ManufacturedSolutionTendency` adds the forcing used by the manufactured +solution test case. + ## Second Order Horizontal Advection Algorithm The horizontal advection is done independently within each ocean layer @@ -142,5 +170,6 @@ Tracer higer order convergence example of a cosine bell advected on a sphere sho ## See Also -Additional information on forcing (currently wind forcing and surface tracer -restoring) is detailed in [](omega-user-forcing). +Additional information on forcing, including surface stress forcing, +surface thickness and tracer flux forcing, and surface tracer restoring, is detailed in +[](omega-user-forcing). diff --git a/components/omega/src/ocn/Eos.cpp b/components/omega/src/ocn/Eos.cpp index 16ea4da9f5a2..ba36a2bb61ac 100644 --- a/components/omega/src/ocn/Eos.cpp +++ b/components/omega/src/ocn/Eos.cpp @@ -327,22 +327,6 @@ void Eos::computeBruntVaisalaFreqSq(const Array2DReal &ConservTemp, } } -Real Eos::calcPtFromCt(const Real &Sa, const Real &Ct) const { - if (EosChoice == EosType::Teos10Eos) { - return ComputeSpecVolTeos10.calcPtFromCt(Sa, Ct); - } - - return Ct; -} - -Real Eos::calcCtFromPt(const Real &Sa, const Real &Pt) const { - if (EosChoice == EosType::Teos10Eos) { - return ComputeSpecVolTeos10.calcCtFromPt(Sa, Pt); - } - - return Pt; -} - /// Define IO fields and metadata for output void Eos::defineFields() { diff --git a/components/omega/src/ocn/Eos.h b/components/omega/src/ocn/Eos.h index 5e4fd89cae21..b1b04f09b05e 100644 --- a/components/omega/src/ocn/Eos.h +++ b/components/omega/src/ocn/Eos.h @@ -358,8 +358,8 @@ class Teos10Eos { /// (polynomial error in [-5e-4, 6e-4] K, from GSW package). /// P is relative pressure (gauge pressure in Pa, i.e., absolute pressure /// minus the standard atmosphere). - KOKKOS_FUNCTION Real calcCtFreezing(const Real Sa, const Real P, - const Real SaturationFract) const { + static KOKKOS_FUNCTION Real calcCtFreezingTeos10( + const Real Sa, const Real P, const Real SaturationFract) { constexpr Real Sso = 35.16504; constexpr Real C0 = 0.017947064327968736; constexpr Real C1 = -6.076099099929818; @@ -756,10 +756,53 @@ class Eos { const Array2DReal &SpecVol); /// Convert Conservative Temperature to potential temperature - Real calcPtFromCt(const Real &Sa, const Real &Ct) const; + /// For TEOS-10, uses the TEOS-10 polynomial + /// For other EOS choices, conservative temperature is equal to potential + /// temperature + KOKKOS_FUNCTION Real calcPtFromCt(const Real &Sa, const Real &Ct) const { + if (EosChoice == EosType::Teos10Eos) { + return ComputeSpecVolTeos10.calcPtFromCt(Sa, Ct); + } + return Ct; + } + + /// Convert potential temperature to Conservative Temperature. + /// For TEOS-10, uses the TEOS-10 polynomial + /// For other EOS choices, potential temperature equals conservative + /// temperature + KOKKOS_FUNCTION Real calcCtFromPt(const Real &Sa, const Real &Pt) const { + if (EosChoice == EosType::Teos10Eos) { + return ComputeSpecVolTeos10.calcCtFromPt(Sa, Pt); + } + return Pt; + } - /// Convert potential temperature to Conservative Temperature - Real calcCtFromPt(const Real &Sa, const Real &Pt) const; + /// Get linear EOS density derivative with respect to temperature. + Real getLinearDRhodT() const { return ComputeSpecVolLinear.DRhodT; } + + /// Get linear EOS density derivative with respect to salinity. + Real getLinearDRhodS() const { return ComputeSpecVolLinear.DRhodS; } + + /// Calculate freezing temperature of seawater. + /// For TEOS-10, uses the Roquet et al. 75-term polynomial. + /// For LinearEos, uses a simple linear salinity-dependent approximation. + /// For ConstantEos, returns a constant approximate ocean freezing point. + static KOKKOS_FUNCTION Real calcCtFreezing(EosType Choice, const Real Sa, + const Real P, + const Real SaturationFract) { + if (Choice == EosType::Teos10Eos) { + return Teos10Eos::calcCtFreezingTeos10(Sa, P, SaturationFract); + } + if (Choice == EosType::LinearEos) { + // Linear salinity-dependent freezing point; coefficient -0.054 + // degC/PSU with absolute-to-practical salinity conversion (g/kg -> + // PSU). + constexpr Real Coeff = -0.054_Real; + return Coeff * Sa / Psu2Gpkg; + } + // ConstantEos: constant approximate ocean freezing point (degC) + return -1.9_Real; + } /// Initialize EOS from config and mesh static void init(); diff --git a/components/omega/src/ocn/Forcing.cpp b/components/omega/src/ocn/Forcing.cpp index 42643dba5385..46af26094113 100644 --- a/components/omega/src/ocn/Forcing.cpp +++ b/components/omega/src/ocn/Forcing.cpp @@ -29,18 +29,30 @@ static std::string stripDefault(const std::string &Name) { // mesh/halo. Forcing::Forcing(const std::string &Name, const HorzMesh *Mesh, Halo *MeshHalo) : Name(stripDefault(Name)), SfcStressForcing(stripDefault(Name), Mesh), - Mesh(Mesh), MeshHalo(MeshHalo) {} + TracerForcing(stripDefault(Name), Mesh), Mesh(Mesh), MeshHalo(MeshHalo) {} // Destructor. Unregisters fields from IO streams. Forcing::~Forcing() { unregisterFields(); } // Register surface stress fields with IO streams for a given mesh. void Forcing::registerFields(const std::string &MeshName) const { - SfcStressForcing.registerFields(MeshName); + if (SfcStressFieldsEnabled) { + SfcStressForcing.registerFields(MeshName); + } + if (TracerForcingFieldsEnabled) { + TracerForcing.registerFields(MeshName); + } } // Unregister surface stress fields from IO streams. -void Forcing::unregisterFields() const { SfcStressForcing.unregisterFields(); } +void Forcing::unregisterFields() const { + if (SfcStressFieldsEnabled) { + SfcStressForcing.unregisterFields(); + } + if (TracerForcingFieldsEnabled) { + TracerForcing.unregisterFields(); + } +} // Create and register a non-default forcing instance. Forcing *Forcing::create(const std::string &Name, const HorzMesh *Mesh, @@ -76,10 +88,9 @@ void Forcing::init() { ABORT_ERROR("Forcing: failed to initialize default forcing state"); } - DefaultForcing->registerFields(DefMesh->MeshName); - Config *OmegaConfig = Config::getOmegaConfig(); DefaultForcing->readConfigOptions(OmegaConfig); + DefaultForcing->registerFields(DefMesh->MeshName); // for now, forcing fields are read at start-up only. // to be extended to include switch from standalone to coupled. // to be moved to a Forcing->prepareForStep(SimTime) method later. @@ -136,10 +147,62 @@ void Forcing::readConfigOptions(Config *OmegaConfig) { } else { ABORT_ERROR("Forcing: Unknown InterpType requested"); } + + Config TendConfig("Tendencies"); + Err += OmegaConfig->get(TendConfig); + CHECK_ERROR_ABORT(Err, "Forcing: Tendencies group not found in Config"); + + Err += + TendConfig.get("SfcStressForcingTendencyEnable", SfcStressFieldsEnabled); + CHECK_ERROR_ABORT(Err, "Forcing: SfcStressForcingTendencyEnable not found " + "in Tendencies config"); + + bool SfcThicknessForcingEnabled = false; + Err += TendConfig.get("SfcThicknessForcingTendencyEnable", + SfcThicknessForcingEnabled); + CHECK_ERROR_ABORT(Err, "Forcing: SfcThicknessForcingTendencyEnable not " + "found in Tendencies config"); + + bool SfcTracerForcingEnabled = false; + Err += TendConfig.get("SfcTracerForcingTendencyEnable", + SfcTracerForcingEnabled); + CHECK_ERROR_ABORT(Err, "Forcing: SfcTracerForcingTendencyEnable not found " + "in Tendencies config"); + + TracerForcingFieldsEnabled = + SfcThicknessForcingEnabled || SfcTracerForcingEnabled; } // Compute all forcing variables (dispatches to specific computations). -void Forcing::computeAll() const { computeSfcStressForcingOnEdge(); } +void Forcing::computeAll() const { + exchangeHalo(); + computeSfcStressForcingOnEdge(); +} + +// Reset forcing arrays so omitted optional fields remain zero after read. +void Forcing::resetArrays() { + if (SfcStressFieldsEnabled) { + deepCopy(SfcStressForcing.NormalStressEdge, 0.0_Real); + deepCopy(SfcStressForcing.ZonalStressCell, 0.0_Real); + deepCopy(SfcStressForcing.MeridStressCell, 0.0_Real); + } + + if (TracerForcingFieldsEnabled) { + deepCopy(TracerForcing.SnowFluxCell, 0.0_Real); + deepCopy(TracerForcing.RainFluxCell, 0.0_Real); + deepCopy(TracerForcing.EvaporationFluxCell, 0.0_Real); + deepCopy(TracerForcing.SeaIceFreshWaterFluxCell, 0.0_Real); + deepCopy(TracerForcing.IceRunoffFluxCell, 0.0_Real); + deepCopy(TracerForcing.RiverRunoffFluxCell, 0.0_Real); + deepCopy(TracerForcing.LatentHeatFluxCell, 0.0_Real); + deepCopy(TracerForcing.SensibleHeatFluxCell, 0.0_Real); + deepCopy(TracerForcing.LongWaveHeatFluxUpCell, 0.0_Real); + deepCopy(TracerForcing.LongWaveHeatFluxDownCell, 0.0_Real); + deepCopy(TracerForcing.SeaIceHeatFluxCell, 0.0_Real); + deepCopy(TracerForcing.ShortWaveHeatFluxCell, 0.0_Real); + deepCopy(TracerForcing.SeaIceSaltFluxCell, 0.0_Real); + } +} // Compute edge-normal stress from cell-center zonal and meridional components. void Forcing::computeSfcStressForcingOnEdge() const { @@ -153,14 +216,17 @@ void Forcing::computeSfcStressForcingOnEdge() const { Pacer::stop("Forcing:edge1", 2); } -// Exchange halo for surface stress cell fields. +// Exchange halo for surface stress cell fields. Only needed for variables that +// need information beyond cell-centered values. I4 Forcing::exchangeHalo() const { I4 Err = 0; - Err += MeshHalo->exchangeFullArrayHalo(SfcStressForcing.ZonalStressCell, - OnCell); - Err += MeshHalo->exchangeFullArrayHalo(SfcStressForcing.MeridStressCell, - OnCell); + if (SfcStressFieldsEnabled) { + Err += MeshHalo->exchangeFullArrayHalo(SfcStressForcing.ZonalStressCell, + OnCell); + Err += MeshHalo->exchangeFullArrayHalo(SfcStressForcing.MeridStressCell, + OnCell); + } return Err; } @@ -172,13 +238,20 @@ void Forcing::readStreamIntoArrays() { std::string StreamName = "Forcing"; + resetArrays(); + + // Nothing to read if neither stress nor tracer forcing tendencies are + // enabled. + if (!SfcStressFieldsEnabled && !TracerForcingFieldsEnabled) { + return; + } + // Attempt to read stream; if unavailable, log and fall back to zero forcing. Err = IOStream::read(StreamName); if (Err.isFail()) { LOG_INFO("Forcing: Error while reading {} stream, using zero forcing", StreamName); - deepCopy(SfcStressForcing.ZonalStressCell, 0._Real); - deepCopy(SfcStressForcing.MeridStressCell, 0._Real); + resetArrays(); } I4 HaloErr = exchangeHalo(); diff --git a/components/omega/src/ocn/Forcing.h b/components/omega/src/ocn/Forcing.h index 5fdae7e550b8..b061bdcf25d7 100644 --- a/components/omega/src/ocn/Forcing.h +++ b/components/omega/src/ocn/Forcing.h @@ -17,6 +17,7 @@ #include "Halo.h" #include "HorzMesh.h" #include "forcingVars/SfcStressForcingVars.h" +#include "forcingVars/TracerForcingVars.h" #include #include @@ -32,6 +33,7 @@ class Forcing { std::string Name; ///< Name identifier for this forcing instance SfcStressForcingVars SfcStressForcing; ///< Surface stress forcing variables + TracerForcingVars TracerForcing; ///< Tracer forcing vars (thickness and T,S) ~Forcing(); @@ -69,6 +71,9 @@ class Forcing { /// Read forcing fields from input stream at startup void readStreamIntoArrays(); + /// Reset all forcing arrays to zero before reading optional fields + void resetArrays(); + /// Compute all forcing variables void computeAll() const; @@ -86,6 +91,8 @@ class Forcing { const HorzMesh *Mesh; Halo *MeshHalo; + bool SfcStressFieldsEnabled = false; + bool TracerForcingFieldsEnabled = false; static Forcing *DefaultForcing; static std::map> AllForcing; diff --git a/components/omega/src/ocn/GlobalConstants.h b/components/omega/src/ocn/GlobalConstants.h index d31bae11f489..0fe2adb1f54e 100644 --- a/components/omega/src/ocn/GlobalConstants.h +++ b/components/omega/src/ocn/GlobalConstants.h @@ -115,11 +115,12 @@ constexpr Real Pa2Db = 1.0e-4; // Pascal to Decibar constexpr Real Cm2M = 1.0e-2; // Centimeters to meters constexpr Real M2Cm = 1.0e2; // Meters to centimeters constexpr Real HFluxFac = - 1.0 / (RhoSw * CpSw); // Heat flux (W/m^2) to temp flux (C*m/s) + 1.0 / (RhoSw * Cp0Sw); // Heat flux (W/m^2) to Conserv Temp flux (C*m/s) constexpr Real FwFluxFac = 1.e-6; // Fw flux (kg/m^2/s) to salt((msu/psu)*m/s) constexpr Real SaltFac = - -OcnRefSal * FwFluxFac; // Fw flux (kg/m^2/s) to salt flux (msu*m/s) -constexpr Real SFluxFac = 1.0; // Salt flux (kg/m^2/s) to salt flux (msu*m/s) + -OcnRefSal * FwFluxFac; // Fw flux (kg/m^2/s) to salt flux (msu*m/s) +constexpr Real SFluxFac = + 1.e3 / RhoSw; // Salt flux (kg/m^2/s) to salinity flux (m*(g/kg)/s) } // namespace OMEGA #endif diff --git a/components/omega/src/ocn/HorzMesh.h b/components/omega/src/ocn/HorzMesh.h index 66617f629fc7..77b4f8380bed 100644 --- a/components/omega/src/ocn/HorzMesh.h +++ b/components/omega/src/ocn/HorzMesh.h @@ -107,6 +107,10 @@ class HorzMesh { I4 MaxEdges2; ///< Max number of edges around a cell x2 I4 NEdgesGlobal; ///< Total number of edges in global non-decomposed mesh + /// Compile-time upper bound on MaxEdges, for sizing on-stack (per-thread) + /// arrays in device kernels. Meshes with more edges per cell are rejected. + static constexpr I4 MaxEdgesBound = 10; + Array1DI4 NVerticesHalo; ///< num cells owned+halo for halo layer HostArray1DI4 NVerticesHaloH; ///< num cells owned+halo for halo layer I4 NVerticesOwned; ///< Number of vertices owned by this task diff --git a/components/omega/src/ocn/HorzOperators.h b/components/omega/src/ocn/HorzOperators.h index 70260564d873..6f44fbb3d568 100644 --- a/components/omega/src/ocn/HorzOperators.h +++ b/components/omega/src/ocn/HorzOperators.h @@ -266,8 +266,8 @@ class SecondDerivativeOnCell { private: // MaxMaxEdges is used to dimention arrays that include ICell and the // neighbor cells, so it is technically one more than MaxEdges. - static const I4 MaxMaxEdges = 10; - static constexpr R8 Pii = 3.141592653589793_Real; + static constexpr I4 MaxMaxEdges = HorzMesh::MaxEdgesBound; + static constexpr R8 Pii = 3.141592653589793_Real; const bool OnSphere; const I4 NCellsAll; @@ -372,8 +372,8 @@ class SecondDerivativeOnCell { // angles from cell center to neighbor centers (thetav) const Real Thetav = sphere_angle(XC[0], YC[0], ZC[0], XC[Ip1], YC[Ip1], ZC[Ip1], XC[Ip2], YC[Ip2], ZC[Ip2]); - Real Dl_sphere = sphereRadius * arc_length(XC[0], YC[0], ZC[0], - XC[Ip1], YC[Ip1], ZC[Ip1]); + Real Dl_sphere = sphereRadius * arc_length(XC[0], YC[0], ZC[0], + XC[Ip1], YC[Ip1], ZC[Ip1]); Dl_sphere /= length_scale; // Thetat = 0. this defines the x direction, diff --git a/components/omega/src/ocn/KPPConstants.h b/components/omega/src/ocn/KPPConstants.h new file mode 100644 index 000000000000..5377c9db0769 --- /dev/null +++ b/components/omega/src/ocn/KPPConstants.h @@ -0,0 +1,454 @@ +#ifndef OMEGA_KPP_CONSTANTS_H +#define OMEGA_KPP_CONSTANTS_H +//===-- ocn/KPPConstants.h - KPP Constants and Profiles --------*- C++ -*-===// +// +/// \file +/// \brief KPP-specific constants, parameters, and profile functions +/// +/// This header defines constants, parameters, and inline functions for the +/// K-Profile Parameterization boundary layer mixing scheme. Includes +/// non-dimensional profile functions used in KPP coefficient calculations. +// +//===----------------------------------------------------------------------===// + +#include "GlobalConstants.h" +#include "OmegaKokkos.h" + +namespace OMEGA::KPP { + +// ========================================================================== +// Monin-Obukhov stability function parameters (Large et al. 1994, App. B) +// +// The stability coordinate is zeta = d/L, where d is depth below the surface +// and L is the Monin-Obukhov length. zeta > 0 is stable (surface warming or +// salinification), zeta < 0 is unstable (convective). The values below set +// where the weakly-unstable branch hands off to the strongly-unstable branch. +// ========================================================================== + +/// Transition zeta for momentum: below this value, strongly-unstable formula +/// is used. Default: -0.2 (CVMix default). +constexpr Real ZetaM = -0.2_Real; + +/// Transition zeta for scalars: below this value, strongly-unstable formula +/// is used. Default: -1.0 (CVMix default). +constexpr Real ZetaS = -1.0_Real; + +// The four constants below are fixed by requiring the strongly-unstable +// branch to match the weakly-unstable branch in value at the transition zeta. + +/// a_m = (1-16*ZetaM)^{-0.25} * (1 - 4*ZetaM) +constexpr Real AMoM = 1.2573615702_Real; +/// c_m = (1-16*ZetaM)^{-0.25} * 12 +constexpr Real CMoM = 8.3824104679_Real; + +/// a_s = sqrt(1-16*ZetaS) * (1 + 8*ZetaS) (can be negative) +constexpr Real AMoS = -28.8617393793_Real; +/// c_s = 24 * sqrt(1-16*ZetaS) +constexpr Real CMoS = 98.9545350148_Real; + +/// Surface value of the momentum shape function (dimensionless) +constexpr Real HuOn = 0.03; + +/// Floor on friction velocity (m/s). Keeps the turbulent velocity scales and +/// the Monin-Obukhov length finite in near-calm conditions. +constexpr Real MinUStar = 1.0e-4; + +// ========================================================================== +// OBL Depth Computation Parameters +// ========================================================================== + +/// Minimum OBL depth under sea ice (m), applied above IceSuppressThresh +constexpr Real MinOBLUnderIce = 5.0; + +/// Ice fraction above which Langmuir enhancement is disabled +constexpr Real IceFracThresh = 0.05; + +/// Ice fraction above which the minimum OBL depth is enforced +constexpr Real IceSuppressThresh = 0.15; + +// ========================================================================== +// Surface Layer Parameters +// ========================================================================== + +/// Surface layer extent as a fraction of the trial OBL depth (epsilon in +/// Large et al. 1994). Reference values entering the bulk Richardson number +/// are averaged over the top SurfaceLayerExtent * d of the column. +constexpr Real SurfaceLayerExtent = 0.1; + +/// Empirical convective velocity coefficient in the turbulent velocity scale +/// w_s = (u*^3 + ConvectiveVelFac * max(-B_0,0) * h)^(1/3) +constexpr Real ConvectiveVelFac = 0.35; + +// ========================================================================== +// KPP Shape and Stability Functions +// +// Vertical position inside the OBL is expressed two ways: +// Sigma in [-1,0], the Omega convention: 0 at the surface, -1 at the +// OBL base (it follows the sign of the z coordinate). +// SigmaMu in [ 0,1], the CVMix/Large et al. convention: SigmaMu = -Sigma, +// so 0 at the surface and 1 at the OBL base. +// The shape functions below take Sigma and convert internally, so callers +// never need to flip signs. +// ========================================================================== + +/// @brief Momentum gradient shape function, multiplied by h and the turbulent +/// velocity scale to give the KPP viscosity: Kx = h * w_m * G(sigma). +/// REFERENCES: Large et al. (1994) Eq. (11) +/// +/// @param Sigma Normalized vertical position (-z/h) +/// @return Dimensionless shape value +KOKKOS_INLINE_FUNCTION +Real kppShapeMomentum(Real Sigma) { + Sigma = Kokkos::fmax(-1.0, Kokkos::fmin(0.0, Sigma)); + + const Real SigmaMu = -Sigma; + return SigmaMu * (1.0 - SigmaMu) * (1.0 - SigmaMu); +} + +/// @brief Matched KPP gradient shape function. +/// +/// Uses the SimpleShapes gradient shape plus a smooth correction that is +/// zero at the surface and equals ShapeAtBase at the OBL base. This lets +/// MatchBoth profiles meet pre-existing interior mixing at the BLD base while +/// preserving SimpleShapes behavior when ShapeAtBase is zero. +KOKKOS_INLINE_FUNCTION +Real kppShapeMatched(Real Sigma, Real ShapeAtBase) { + Sigma = Kokkos::fmax(-1.0, Kokkos::fmin(0.0, Sigma)); + + const Real SigmaMu = -Sigma; + const Real Simple = SigmaMu * (1.0 - SigmaMu) * (1.0 - SigmaMu); + const Real Smooth = SigmaMu * SigmaMu * (3.0 - 2.0 * SigmaMu); + return Simple + ShapeAtBase * Smooth; +} + +/// @brief phi_m^{-1}(zeta) - Inverse momentum Monin-Obukhov stability function +/// Multiplied by von Karman constant and friction velocity to give turbulent +/// momentum velocity scale: w_m = kappa * u* * phi_m^{-1}(zeta) +/// Three-regime formulation per Large et al. (1994) Appendix B and CVMix. +/// +/// @param Zeta Monin-Obukhov stability coordinate d/L (dimensionless) +/// @return phi_m^{-1} (dimensionless, > 0) +KOKKOS_INLINE_FUNCTION +Real kppPhiInvMomentum(Real Zeta) { + if (Zeta >= 0.0_Real) { + // Stable regime + return 1.0_Real / (1.0_Real + 5.0_Real * Zeta); + } else if (Zeta >= ZetaM) { + // Weakly unstable: (1 - 16*zeta)^{1/4} + return Kokkos::pow(1.0_Real - 16.0_Real * Zeta, 0.25_Real); + } else { + // Strongly unstable (convective): (a_m - c_m*zeta)^{1/3} + return Kokkos::pow(AMoM - CMoM * Zeta, 1.0_Real / 3.0_Real); + } +} + +/// @brief Scalar gradient shape function, multiplied by h and the turbulent +/// velocity scale to give the KPP diffusivity: Kx = h * w_s * G(sigma). +/// The non-local flux always uses this shape, scaled by C_s instead of h*w_s, +/// even under MatchBoth, so that gamma vanishes at the OBL base. +/// REFERENCES: Large et al. (1994) Eq. (11), Eq. (12)-(13), Large et al. (1997) +/// +/// @param Sigma Normalized vertical position (-z/h) +/// @return Dimensionless shape value +KOKKOS_INLINE_FUNCTION +Real kppShapeScalar(Real Sigma) { + Sigma = Kokkos::fmax(-1.0, Kokkos::fmin(0.0, Sigma)); + + const Real SigmaMu = -Sigma; + return SigmaMu * (1.0 - SigmaMu) * (1.0 - SigmaMu); +} + +/// @brief phi_s^{-1}(zeta) - Inverse scalar Monin-Obukhov stability function +/// Multiplied by von Karman constant and friction velocity to give turbulent +/// scalar velocity scale: w_s = kappa * u* * phi_s^{-1}(zeta) +/// Three-regime formulation per Large et al. (1994) Appendix B and CVMix. +/// Scalars mix more efficiently than momentum in unstable conditions, which +/// is why the weakly-unstable exponent is 1/2 here and 1/4 for momentum. +/// +/// @param Zeta Monin-Obukhov stability coordinate d/L (dimensionless) +/// @return phi_s^{-1} (dimensionless, > 0) +KOKKOS_INLINE_FUNCTION +Real kppPhiInvScalar(Real Zeta) { + if (Zeta >= 0.0_Real) { + // Stable regime + return 1.0_Real / (1.0_Real + 5.0_Real * Zeta); + } else if (Zeta >= ZetaS) { + // Weakly unstable: (1 - 16*zeta)^{1/2} + return Kokkos::sqrt(1.0_Real - 16.0_Real * Zeta); + } else { + // Strongly unstable (convective): (a_s - c_s*zeta)^{1/3} + return Kokkos::pow(AMoS - CMoS * Zeta, 1.0_Real / 3.0_Real); + } +} + +/// @brief Hu(sigma) - Momentum surface value scaling +/// Sets surface boundary condition for momentum mixing +/// REFERENCES: Large et al. (1994) +/// +/// @param Sigma Normalized vertical position +/// @return Normalized shape value +KOKKOS_INLINE_FUNCTION +Real kppSurfaceMomentumScale(Real Sigma) { + // Linear decay from HuOn at the surface to zero at the OBL base + return HuOn * (1.0 + Sigma); +} + +// ========================================================================== +// Langmuir Enhancement Factor (Theory-based Wave Model) +// +// Langmuir circulations are wind-and-wave driven counter-rotating vortices +// that deepen and strengthen boundary layer mixing beyond what wind stress +// alone produces. Their strength is measured by the turbulent Langmuir +// number La = sqrt(u* / u_stokes): small La means wave forcing dominates. +// With no wave model coupled, the Stokes drift is estimated from the 10 m +// wind following Li et al. (2016). +// ========================================================================== + +/// @brief Estimate the surface-layer Stokes drift velocity scale from wind +/// REFERENCES: Li et al. 2016, cvmix_kpp_ustokes_SL_model +/// +/// TODO: this is a placeholder empirical fit, not the full surface-layer +/// averaged Stokes drift of the reference model, which needs the wave +/// spectrum (or a wave component) to evaluate +/// u_s,BL = (U10/362) * sqrt(2*alpha*Cd) * lambda/HBL +/// with alpha = 0.84, Cd ~ 1.2e-3 and lambda ~ 2*pi*g/omega^2. Until those +/// inputs are available, HBL is accepted but unused. +/// +/// @param Wind10m Wind speed at 10 m height (m/s) +/// @param HBL Boundary layer depth (m) +/// @return Stokes drift velocity scale (m/s) +KOKKOS_INLINE_FUNCTION +Real estimateStokesDriftSL(Real Wind10m, Real HBL) { + Wind10m = Kokkos::fmax(0.0, Wind10m); + HBL = Kokkos::fmax(1.0, HBL); + + const Real UStokes = 0.016 * Wind10m; + + return Kokkos::fmax(0.0, UStokes); +} + +/// @brief Turbulent Langmuir number from friction velocity and Stokes drift +/// REFERENCES: Large et al. 2015 Eq. 6 +/// +/// @param UStar Friction velocity (m/s) +/// @param UStokes Stokes drift at surface (m/s) +/// @return Langmuir number (dimensionless) +KOKKOS_INLINE_FUNCTION +Real computeLangmuirNumber(Real UStar, Real UStokes) { + UStar = Kokkos::fmax(MinUStar, UStar); + UStokes = Kokkos::fmax(1.0e-8, UStokes); + + return Kokkos::sqrt(UStar / UStokes); +} + +/// @brief Langmuir enhancement factor applied to the KPP velocity scales +/// REFERENCES: Li et al. (2016) Eq. 1-3 +/// +/// @param Wind10m Wind speed at 10 m (m/s) +/// @param UStar Friction velocity (m/s) +/// @param HBL Boundary layer depth (m) +/// @return Enhancement factor R_L (dimensionless, > 1.0 enhances mixing) +KOKKOS_INLINE_FUNCTION +Real computeLangmuirEnhancement(Real Wind10m, Real UStar, Real HBL) { + UStar = Kokkos::fmax(MinUStar, UStar); + Wind10m = Kokkos::fmax(0.0, Wind10m); + HBL = Kokkos::fmax(1.0, HBL); + + const Real UStokes = estimateStokesDriftSL(Wind10m, HBL); + const Real La = computeLangmuirNumber(UStar, UStokes); + + // R_L = sqrt(1 + 0.5/La^2); La is floored at 0.5 so that the weak-wave + // limit returns the unenhanced scales rather than diverging. + const Real LaInv = 1.0 / Kokkos::fmax(0.5, La); + const Real RL = Kokkos::sqrt(1.0 + 0.5 * LaInv * LaInv); + + return Kokkos::fmin(2.0, Kokkos::fmax(1.0, RL)); +} + +// ========================================================================== +// Utility Functions for OBL Depth Computation +// ========================================================================== + +/// @brief Check if a point should be suppressed (e.g., under ice) +/// Sets OBL to minimum if ice coverage or land ice present +/// +/// @param IceFrac Sea ice coverage (0-1) +/// @param LandIceMask Land ice mask (0=ocean, non-zero=ice) +/// @return True if suppression applies +KOKKOS_INLINE_FUNCTION +bool shouldSuppressOBL(Real IceFrac, I4 LandIceMask) { + return (LandIceMask != 0) || (IceFrac > IceSuppressThresh); +} + +/// @brief Apply OBL depth constraints based on column properties +/// +/// @param HOBL Current OBL depth (m) +/// @param LayerThickness Surface layer thickness (m) +/// @param WaterDepth Total water depth (m) +/// @param IceFrac Sea ice coverage (0-1) +/// @return Constrained OBL depth (m) +KOKKOS_INLINE_FUNCTION +Real constrainOBLDepth(Real HOBL, Real LayerThickness, Real WaterDepth, + Real IceFrac) { + // Lower bound: at least half the surface layer thickness + HOBL = Kokkos::fmax(HOBL, LayerThickness * 0.5); + + // Enforce minimum under ice + if (IceFrac > IceSuppressThresh) { + HOBL = Kokkos::fmax(HOBL, MinOBLUnderIce); + } + + // Upper bound: cannot exceed water depth + HOBL = Kokkos::fmin(HOBL, WaterDepth * 0.95); + + return HOBL; +} + +// ========================================================================== +// Turbulent Velocity Scale Computation +// ========================================================================== + +/// @brief Compute the depth-independent turbulent velocity scale +/// Blends the shear-driven scale u* with the convective scale so that the +/// result stays finite in both the wind-driven and free-convection limits. +/// REFERENCES: Large et al. (1994) Eq. (9)-(10) +/// +/// @param UStar Friction velocity (m/s) +/// @param BuoyFlux Surface buoyancy flux (m^2/s^3), negative when convective +/// @param HOBL Boundary layer depth (m) +/// @return Turbulent velocity scale w_s (m/s) +KOKKOS_INLINE_FUNCTION +Real computeTurbVelocityScale(Real UStar, Real BuoyFlux, Real HOBL) { + UStar = Kokkos::fmax(0.0_Real, UStar); + HOBL = Kokkos::fmax(0.0_Real, HOBL); + + // Momentum contribution + const Real WMom = UStar * UStar * UStar; + + // Buoyancy contribution for unstable (cooling/densifying) forcing. + // In this sign convention, free convection corresponds to BuoyFlux < 0. + const Real WBuoy = + ConvectiveVelFac * Kokkos::fmax(0.0_Real, -BuoyFlux) * HOBL; + + return Kokkos::pow(Kokkos::fmax(0.0_Real, WMom + WBuoy), + 1.0_Real / 3.0_Real); +} + +/// @brief Momentum and scalar turbulent velocity scales at a point in the OBL +/// +/// CVMix-style scales: w = kappa * u* * phi^{-1}(zeta) in general, with +/// explicit free-convection limits when u* vanishes. Both scales are returned +/// together because they share the stability coordinate zeta. +/// REFERENCES: Large et al. (1994) Eq. (13), Appendix B +/// +/// @param UStar Friction velocity (m/s) +/// @param BuoyFlux Surface buoyancy flux (m^2/s^3), negative when convective +/// @param HOBL Boundary layer depth (m) +/// @param SigmaLoc Normalized depth [0,1], capped at SurfaceLayerExtent +/// @param Kappa von Karman constant +/// @param WMTurb [out] Momentum turbulent velocity scale w_m (m/s) +/// @param WSTurb [out] Scalar turbulent velocity scale w_s (m/s) +KOKKOS_INLINE_FUNCTION +void kppTurbScales(Real UStar, Real BuoyFlux, Real HOBL, Real SigmaLoc, + Real Kappa, Real &WMTurb, Real &WSTurb) { + WMTurb = 0.0_Real; + WSTurb = 0.0_Real; + + if (UStar > 0.0_Real) { + const Real U3 = UStar * UStar * UStar; + const Real Zeta = + SigmaLoc * HOBL * BuoyFlux * Kappa / Kokkos::max(U3, 1.0e-20_Real); + + // These return phi^{-1}; do not invert again. + WMTurb = Kappa * UStar * Kokkos::max(kppPhiInvMomentum(Zeta), 0.0_Real); + WSTurb = Kappa * UStar * Kokkos::max(kppPhiInvScalar(Zeta), 0.0_Real); + } else if (BuoyFlux < 0.0_Real) { + // Free-convection edge case (u*=0, unstable forcing). + const Real WM3 = -CMoM * SigmaLoc * HOBL * Kappa * BuoyFlux; + const Real WS3 = -CMoS * SigmaLoc * HOBL * Kappa * BuoyFlux; + WMTurb = + Kappa * Kokkos::pow(Kokkos::max(0.0_Real, WM3), 1.0_Real / 3.0_Real); + WSTurb = + Kappa * Kokkos::pow(Kokkos::max(0.0_Real, WS3), 1.0_Real / 3.0_Real); + } +} + +/// @brief Shape value the KPP profile must reach at the OBL base so that it +/// joins the pre-existing interior coefficient there (MatchBoth only). +/// +/// Callers must skip this when no interior mixing is supplied, since +/// InteriorCoeff is then read from an unallocated array. +/// +/// @param InteriorCoeff Interior diffusivity or viscosity at the OBL base +/// @param HOBL Boundary layer depth (m) +/// @param W Turbulent velocity scale matching InteriorCoeff (m/s) +/// @return Shape value at the OBL base (dimensionless) +KOKKOS_INLINE_FUNCTION +Real kppMatchShape(Real InteriorCoeff, Real HOBL, Real W) { + if (HOBL <= 0.0_Real || W <= 0.0_Real) { + return 0.0_Real; + } + + return InteriorCoeff / Kokkos::max(HOBL * W, 1.0e-20_Real); +} + +/// @brief Non-local flux normalization constant +/// C_s = C* * kappa * (c_s * kappa * epsilon)^(1/3), with C* = 10. +/// Evaluates to roughly 6.33 with the default constants. +/// REFERENCES: Large et al. (1994) Eq. (20) +/// +/// @param Kappa von Karman constant +/// @param SurfLayerExtent Surface layer extent epsilon (dimensionless) +/// @return C_s (dimensionless) +KOKKOS_INLINE_FUNCTION +Real kppNonLocalCs(Real Kappa, Real SurfLayerExtent) { + return 10.0_Real * Kappa * + Kokkos::pow(CMoS * Kappa * SurfLayerExtent, 1.0_Real / 3.0_Real); +} + +/// @brief Clamp a trial OBL depth to the range supported by the column +/// +/// @param OBLDepth Trial OBL depth (m) +/// @param MinOBLDepth Lower bound, typically half the top layer thickness (m) +/// @param MaxOBLDepth Upper bound, typically the deepest cell center (m) +/// @param ApplyIceMinimum Whether the sea-ice minimum depth applies +/// @param MinOBLUnderIce Minimum OBL depth under sea ice (m) +/// @return Clamped OBL depth (m) +KOKKOS_INLINE_FUNCTION +Real kppClampOBLDepth(Real OBLDepth, Real MinOBLDepth, Real MaxOBLDepth, + bool ApplyIceMinimum, Real MinOBLUnderIce) { + OBLDepth = Kokkos::fmax(OBLDepth, MinOBLDepth); + + if (ApplyIceMinimum) { + OBLDepth = Kokkos::fmax(OBLDepth, MinOBLUnderIce); + } + + return Kokkos::fmin(OBLDepth, MaxOBLDepth); +} + +/// @brief Index of the cell layer containing a given OBL depth +/// +/// @param ZInterface Geometric height of layer interfaces (m) +/// @param ICell Cell index +/// @param KMin Index of the top active layer +/// @param KMax Index of the bottom active layer +/// @param Ssh Sea surface height (m), since depths are measured below it +/// @param OBLDepth OBL depth (m) +/// @return Layer index bracketing OBLDepth, or KMax if none does +KOKKOS_INLINE_FUNCTION +I4 kppOBLIndex(const Array2DReal &ZInterface, I4 ICell, I4 KMin, I4 KMax, + Real Ssh, Real OBLDepth) { + for (I4 K = KMin; K < KMax; ++K) { + const Real ZAbove = Ssh - ZInterface(ICell, K); + const Real ZBelow = Ssh - ZInterface(ICell, K + 1); + if (OBLDepth >= ZAbove && OBLDepth <= ZBelow) { + return K; + } + } + + return KMax; +} + +} // namespace OMEGA::KPP + +#endif // OMEGA_KPP_CONSTANTS_H diff --git a/components/omega/src/ocn/KPPMix.cpp b/components/omega/src/ocn/KPPMix.cpp new file mode 100644 index 000000000000..05051fb2a233 --- /dev/null +++ b/components/omega/src/ocn/KPPMix.cpp @@ -0,0 +1,1391 @@ +//===-- ocn/KPPMix.cpp - KPP Boundary Layer Mixing Implementation --*- C++ +//-*-===// +// +/// \file +/// \brief Implementation of KPP boundary layer mixing scheme +/// +/// This file implements the KPPMix class for computing ocean boundary layer +/// mixing following Large et al. (1994) with optional Langmuir enhancement. +// +//===----------------------------------------------------------------------===// + +#include "KPPMix.h" +#include "DataTypes.h" +#include "Error.h" +#include "FillValues.h" +#include "GlobalConstants.h" +#include "KPPConstants.h" +#include "Logging.h" +#include "OmegaKokkos.h" +#include "VertCoord.h" +#include + +namespace OMEGA { + +// Singleton instance +KPPMix *KPPMix::Instance = nullptr; + +namespace { + +bool matchTypeFromString(const std::string &Name, KPPMatchType &Type) { + if (Name == "SimpleShapes") { + Type = KPPMatchType::SimpleShapes; + return true; + } + if (Name == "MatchBoth") { + Type = KPPMatchType::MatchBoth; + return true; + } + return false; +} + +const char *matchTypeName(KPPMatchType Type) { + switch (Type) { + case KPPMatchType::MatchBoth: + return "MatchBoth"; + default: + return "SimpleShapes"; + } +} + +} // anonymous namespace + +/// Constructor for KPPMix +KPPMix::KPPMix(const std::string &InName, const HorzMesh *InMesh, + const VertCoord *InVCoord) + : Name(InName), Mesh(InMesh), VCoord(InVCoord) { + + // Allocate output arrays + VertDiff = Array2DReal("VertDiff", Mesh->NCellsSize, VCoord->NVertLayersP1); + VertVisc = Array2DReal("VertVisc", Mesh->NCellsSize, VCoord->NVertLayersP1); + BoundaryLayerDepth = Array1DReal("BoundaryLayerDepth", Mesh->NCellsSize); + IndexBoundaryLayerDepth = + Array1DI4("IndexBoundaryLayerDepth", Mesh->NCellsSize); + VertNonLocalFlux = + Array2DReal("VertNonLocalFlux", Mesh->NCellsSize, VCoord->NVertLayersP1); + BulkRichardsonNumber = Array2DReal("BulkRichardsonNumber", Mesh->NCellsSize, + VCoord->NVertLayersP1); + BulkRichardsonShear = Array2DReal("BulkRichardsonShear", Mesh->NCellsSize, + VCoord->NVertLayersP1); + UnresolvedShear = + Array2DReal("UnresolvedShear", Mesh->NCellsSize, VCoord->NVertLayersP1); + BuoyancyJump = + Array2DReal("BuoyancyJump", Mesh->NCellsSize, VCoord->NVertLayersP1); + TurbulentVelocityScale = Array2DReal( + "TurbulentVelocityScale", Mesh->NCellsSize, VCoord->NVertLayersP1); + PotentialDensity = + Array2DReal("PotentialDensity", Mesh->NCellsSize, VCoord->NVertLayers); + SurfaceFrictionVelocity = + Array1DReal("SurfaceFrictionVelocity", Mesh->NCellsSize); + SurfaceBuoyancyFlux = Array1DReal("SurfaceBuoyancyFlux", Mesh->NCellsSize); + + // Set field names + VertDiffFldName = "VertDiff"; + VertViscFldName = "VertVisc"; + OBLDepthFldName = "BoundaryLayerDepth"; + OBLDepthIndexFldName = "BoundaryLayerDepthIndex"; + NonLocalFluxFldName = "VertNonLocalFlux"; + BulkRichardsonFldName = "BulkRichardsonNumber"; + BulkRichardsonShearFldName = "BulkRichardsonShear"; + UnresolvedShearFldName = "UnresolvedShear"; + BuoyancyJumpFldName = "BuoyancyJump"; + TurbulentVelScaleFldName = "TurbulentVelocityScale"; + PotentialDensityFldName = "PotentialDensity"; + SurfFricVelFldName = "SurfaceFrictionVelocity"; + SurfBuoyFluxFldName = "SurfaceBuoyancyFlux"; + + if (Name != "Default") { + VertDiffFldName.append(Name); + VertViscFldName.append(Name); + OBLDepthFldName.append(Name); + OBLDepthIndexFldName.append(Name); + NonLocalFluxFldName.append(Name); + BulkRichardsonFldName.append(Name); + BulkRichardsonShearFldName.append(Name); + UnresolvedShearFldName.append(Name); + BuoyancyJumpFldName.append(Name); + TurbulentVelScaleFldName.append(Name); + PotentialDensityFldName.append(Name); + SurfFricVelFldName.append(Name); + SurfBuoyFluxFldName.append(Name); + } + + defineFields(); +} + +/// Destructor for KPPMix +KPPMix::~KPPMix() {} + +/// Get singleton instance +KPPMix *KPPMix::getInstance() { return Instance; } + +/// Destroy singleton instance +void KPPMix::destroyInstance() { + delete Instance; + Instance = nullptr; +} + +/// Initialize KPPMix from configuration +void KPPMix::init() { + if (!Instance) { + Instance = new KPPMix("Default", HorzMesh::getDefault(), + VertCoord::getDefault()); + } + + Error Err; + KPPMix *DefKPPMix = KPPMix::getInstance(); + Config *OmegaConfig = Config::getOmegaConfig(); + + // Get VertMix config group + Config VertMixConfig("VertMix"); + Err += OmegaConfig->get(VertMixConfig); + CHECK_ERROR_ABORT(Err, "KPPMix::init: VertMix group not found in Config"); + + // Get KPP config subgroup + Config KPPConfig("KPP"); + Err += VertMixConfig.get(KPPConfig); + if (Err.isFail()) { + LOG_WARN("KPPMix::init: KPP subgroup not found, using defaults"); + return; // Continue with defaults + } + + // Read KPP parameters + bool Enable = true; + Err += KPPConfig.get("Enable", Enable); + DefKPPMix->Enabled = Enable; + + Err += KPPConfig.get("CriticalBulkRichardsonNumber", + DefKPPMix->CriticalRichardson); + Err += KPPConfig.get("SurfaceLayerExtent", DefKPPMix->SurfaceLayerExtent); + + // KPP matching/profile semantics. + std::string MatchStr = "SimpleShapes"; + Error MatchErr = KPPConfig.get("MatchTechnique", MatchStr); + if (!MatchErr.isSuccess()) { + MatchErr.reset(); + } + Error InterpErr = KPPConfig.get("InterpType2", DefKPPMix->InterpType2Str); + if (!InterpErr.isSuccess()) { + InterpErr.reset(); + } + Error EnhancedErr = + KPPConfig.get("UseEnhancedDiffusion", DefKPPMix->UseEnhancedDiffusion); + if (!EnhancedErr.isSuccess()) { + EnhancedErr.reset(); + } + Error BLDSmoothErr = + KPPConfig.get("UseBLDSmoothing", DefKPPMix->UseBLDSmoothing); + if (!BLDSmoothErr.isSuccess()) { + BLDSmoothErr.reset(); + } + + if (!matchTypeFromString(MatchStr, DefKPPMix->MatchTechnique)) { + ABORT_ERROR("KPPMix::init: Invalid MatchTechnique='{}', must be " + "SimpleShapes or MatchBoth", + MatchStr); + } + + // Wave and flux options + Err += KPPConfig.get("UseLangmuirCirculation", + DefKPPMix->UseLangmuirCirculation); + Err += KPPConfig.get("IceFractionThresholdForLangmuir", + DefKPPMix->IceFractionThresholdForLangmuir); + Err += KPPConfig.get("IceFractionThresholdForMinimumOBL", + DefKPPMix->IceFractionThresholdForMinimumOBL); + Err += + KPPConfig.get("MinimumOBLUnderSeaIce", DefKPPMix->MinimumOBLUnderSeaIce); + Error DebugErr = + KPPConfig.get("DebugDiagnostics", DefKPPMix->DebugDiagnostics); + if (!DebugErr.isSuccess()) { + DebugErr.reset(); + DefKPPMix->DebugDiagnostics = false; + } + if (DefKPPMix->DebugDiagnostics) { + LOG_WARN("KPP debug diagnostics enabled"); + } + + // Background mixing + Err += KPPConfig.get("BackgroundViscosity", DefKPPMix->BackgroundVisc); + Err += KPPConfig.get("BackgroundDiffusivity", DefKPPMix->BackgroundDiff); + + LOG_WARN("KPPMix::init: KPP initialized enabled={} debugDiagnostics={} " + "match={}", + DefKPPMix->Enabled, DefKPPMix->DebugDiagnostics, + matchTypeName(DefKPPMix->MatchTechnique)); +} + +/// Main computation routine +void KPPMix::computeKPPMix(const Array2DReal &PotentialDensity, + const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, + const Array2DReal &BruntVaisalaFreqSq, + const Array1DReal &IceFraction, + const Array1DReal &WindSpeed10m) { + + if (!Enabled) { + return; // Skip if disabled + } + + // Retain PotentialDensity for diagnostics/stream output. + deepCopy(this->PotentialDensity, PotentialDensity); + + // ======================================================================= + // Stage 1: Compute OBL Depth + // ======================================================================= + computeOBLDepth(PotentialDensity, NormalVelocity, TangentialVelocity, + SurfaceFrictionVelocity, SurfaceBuoyancyFlux, + BruntVaisalaFreqSq, IceFraction, WindSpeed10m); + + // ======================================================================= + // Stage 2: Compute Mixing Coefficients + // ======================================================================= + computeMixingCoefficients(PotentialDensity, SurfaceFrictionVelocity, + SurfaceBuoyancyFlux); + + if (DebugDiagnostics) { + logDiagnostics(PotentialDensity, NormalVelocity, TangentialVelocity, + SurfaceFrictionVelocity, SurfaceBuoyancyFlux, + WindSpeed10m); + } +} + +void KPPMix::logDiagnostics(const Array2DReal &PotentialDensity, + const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, + const Array1DReal &WindSpeed10m) { + + using namespace KPP; + + const auto MinLayerCellH = createHostMirrorCopy(VCoord->MinLayerCell); + const auto MaxLayerCellH = createHostMirrorCopy(VCoord->MaxLayerCell); + const auto ZInterfaceH = createHostMirrorCopy(VCoord->GeomZInterface); + const auto SshCellH = createHostMirrorCopy(VCoord->SshCell); + const auto DensityH = createHostMirrorCopy(PotentialDensity); + const auto UStarH = createHostMirrorCopy(SurfaceFrictionVelocity); + const auto B0H = createHostMirrorCopy(SurfaceBuoyancyFlux); + const auto OBLDepthH = createHostMirrorCopy(BoundaryLayerDepth); + const auto OBLIndexH = createHostMirrorCopy(IndexBoundaryLayerDepth); + const auto VertDiffH = createHostMirrorCopy(VertDiff); + const auto VertViscH = createHostMirrorCopy(VertVisc); + + // NormalVelocity and TangentialVelocity are edge-based; not accessed here + (void)NormalVelocity; + (void)TangentialVelocity; + + const int NCellsAll = Mesh->NCellsAll; + if (NCellsAll <= 0) { + return; + } + + // Domain-wide diagnostic to avoid misleading single-cell checks. + Real MaxAbsB0 = 0.0_Real; + Real MaxAbsUStar = 0.0_Real; + Real MaxVertDiff = 0.0_Real; + Real MaxVertVisc = 0.0_Real; + int MaxAbsB0Cell = -1; + int MaxAbsUStarCell = -1; + int MaxVertDiffCell = -1; + int MaxVertDiffK = -1; + int MaxVertViscCell = -1; + int MaxVertViscK = -1; + for (int C = 0; C < NCellsAll; ++C) { + const Real AbsB0 = Kokkos::abs(B0H(C)); + const Real AbsUStar = Kokkos::abs(UStarH(C)); + if (AbsB0 > MaxAbsB0) { + MaxAbsB0 = AbsB0; + MaxAbsB0Cell = C; + } + if (AbsUStar > MaxAbsUStar) { + MaxAbsUStar = AbsUStar; + MaxAbsUStarCell = C; + } + const int KCMin = MinLayerCellH(C); + const int KCMax = MaxLayerCellH(C) + 1; + for (int K = KCMin; K <= KCMax; ++K) { + const Real Diff = VertDiffH(C, K); + const Real Visc = VertViscH(C, K); + if (Diff > MaxVertDiff) { + MaxVertDiff = Diff; + MaxVertDiffCell = C; + MaxVertDiffK = K; + } + if (Visc > MaxVertVisc) { + MaxVertVisc = Visc; + MaxVertViscCell = C; + MaxVertViscK = K; + } + } + } + LOG_WARN("KPP debug domain post-coeff: max|b0|={} at cell={} max|u*|={} " + "at cell={} maxKPPDiff={} at cell={},k={} maxKPPVisc={} at " + "cell={},k={}", + MaxAbsB0, MaxAbsB0Cell, MaxAbsUStar, MaxAbsUStarCell, MaxVertDiff, + MaxVertDiffCell, MaxVertDiffK, MaxVertVisc, MaxVertViscCell, + MaxVertViscK); + + const int ICell = 0; + const int KMin = MinLayerCellH(ICell); + const int KMax = MaxLayerCellH(ICell); + const int NVertLayers = VCoord->NVertLayers; + + if (KMin > KMax) { + return; + } + + const int KSurf = Kokkos::min(KMin, NVertLayers - 1); + const Real RhoSurf = DensityH(ICell, KSurf); + const Real UStar = UStarH(ICell); + const Real UStarEff = Kokkos::fmax(KPP::MinUStar, UStar); + const Real BuoyFlux = B0H(ICell); + Real Wind10m = 0.0_Real; + if (WindSpeed10m.extent(0) > 0) { + const auto Wind10mH = createHostMirrorCopy(WindSpeed10m); + Wind10m = Wind10mH(ICell); + } + const Real LangmuirFactor = + UseLangmuirCirculation + ? computeLangmuirEnhancement(Wind10m, UStarEff, 50.0) + : 1.0_Real; + const Real BuoyFluxEff = BuoyFlux * LangmuirFactor; + + LOG_WARN("KPP debug: cell={} h_obl={} m k_obl={} u*={} b0={} b0_eff={} " + "langmuir={}", + ICell, OBLDepthH(ICell), OBLIndexH(ICell), UStar, BuoyFlux, + BuoyFluxEff, LangmuirFactor); + + const int KOblIface = Kokkos::min( + NVertLayers, Kokkos::max(KMin, static_cast(OBLIndexH(ICell)) + 1)); + LOG_WARN("KPP debug coeff target: cell={} k_obl={} iface={} diff={} " + "visc={}", + ICell, OBLIndexH(ICell), KOblIface, VertDiffH(ICell, KOblIface), + VertViscH(ICell, KOblIface)); + + const int KTop = Kokkos::min(KMax, KMin + 3); + const int KOBL = OBLIndexH(ICell); + const Real HOBL = OBLDepthH(ICell); + + for (int K = KMin; K <= KTop; ++K) { + const int KCell = Kokkos::min(K, NVertLayers - 1); + const int KIface = Kokkos::min(K + 1, NVertLayers); + const Real ZDepth = SshCellH(ICell) - ZInterfaceH(ICell, KIface); + + const Real RhoK = DensityH(ICell, KCell); + const Real DeltaRho = RhoK - RhoSurf; + const Real DeltaB = Gravity * DeltaRho / RhoSw; + const Real WTurb = + computeTurbVelocityScale(UStarEff, BuoyFluxEff, ZDepth); + const Real RiBulk = DeltaB * ZDepth / (WTurb * WTurb + 1.0e-12_Real); + + Real Sigma = 0.0_Real; + if (K <= KOBL) { + Sigma = -1.0_Real * static_cast(K - KMin) / + static_cast(KOBL - KMin + 1); + Sigma = Kokkos::fmax(-1.0_Real, Kokkos::fmin(0.0_Real, Sigma)); + } + + // Monin-Obukhov coordinate zeta = d/L at this depth + const Real ZLocal = -Sigma * HOBL; + Real Zeta = 0.0_Real; + const Real Denom = VonKar * BuoyFlux; + if (Kokkos::abs(Denom) > 1.0e-16_Real) { + const Real LMoninObukhov = (UStarEff * UStarEff * UStarEff) / Denom; + if (Kokkos::abs(LMoninObukhov) > 1.0e-16_Real) { + Zeta = ZLocal / LMoninObukhov; + } + } + + const Real PhiInvM = KPP::kppPhiInvMomentum(Zeta); + const Real PhiInvS = KPP::kppPhiInvScalar(Zeta); + + LOG_WARN( + "KPP debug top: cell={} k={} z={} ri_b={} zeta={} phi_m={} phi_s={}", + ICell, K, ZDepth, RiBulk, Zeta, PhiInvM, PhiInvS); + } +} + +/// Stage 1: Compute OBL depth using bulk Richardson search with edge-based +/// velocity shear following the MPAS CVMix reference implementation +/// (mpas_ocn_vmix_cvmix.F) +void KPPMix::computeOBLDepth(const Array2DReal &PotentialDensity, + const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, + const Array2DReal &BruntVaisalaFreqSq, + const Array1DReal &IceFraction, + const Array1DReal &WindSpeed10m) { + + using namespace KPP; + + I4 NVertLayers = VCoord->NVertLayers; + + // ======================================================================= + // Compute Langmuir enhancement factors if wind speed is available + // ======================================================================= + Array1DReal LangmuirFactor("LangmuirFactor", Mesh->NCellsSize); + const bool LocUseLangmuirCirculation = UseLangmuirCirculation; + const Real LocSurfaceLayerExtent = SurfaceLayerExtent; + const Real LocCriticalRichardson = CriticalRichardson; + const Real LocIceFracThresholdForLangmuir = IceFractionThresholdForLangmuir; + parallelFor( + "KPP-Langmuir", {Mesh->NCellsAll}, KOKKOS_LAMBDA(I4 ICell) { + const Real IceFrac = IceFraction(ICell); + if (LocUseLangmuirCirculation && + IceFrac < LocIceFracThresholdForLangmuir) { + const Real UStar = SurfaceFrictionVelocity(ICell); + const Real Wind10m = + (WindSpeed10m.extent(0) > 0) ? WindSpeed10m(ICell) : 0.0_Real; + LangmuirFactor(ICell) = + computeLangmuirEnhancement(Wind10m, UStar, 50.0); + } else { + LangmuirFactor(ICell) = 1.0; + } + }); + + // ======================================================================= + // Stage 1: Compute OBL depth using edge-based velocity shear + // ======================================================================= + + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + OMEGA_SCOPE(MinLayerEdgeBot, VCoord->MinLayerEdgeBot); + OMEGA_SCOPE(MaxLayerEdgeTop, VCoord->MaxLayerEdgeTop); + OMEGA_SCOPE(ZInterface, VCoord->GeomZInterface); + OMEGA_SCOPE(ZMid, VCoord->GeomZMid); + OMEGA_SCOPE(LocSshCell, VCoord->SshCell); + OMEGA_SCOPE(NEdgesOnCell, Mesh->NEdgesOnCell); + OMEGA_SCOPE(EdgesOnCell, Mesh->EdgesOnCell); + OMEGA_SCOPE(CellsOnCell, Mesh->CellsOnCell); + OMEGA_SCOPE(AreaCell, Mesh->AreaCell); + OMEGA_SCOPE(DcEdge, Mesh->DcEdge); + OMEGA_SCOPE(DvEdge, Mesh->DvEdge); + OMEGA_SCOPE(LocPotentialDensity, PotentialDensity); + OMEGA_SCOPE(LocNormalVelocity, NormalVelocity); + OMEGA_SCOPE(LocTangentialVelocity, TangentialVelocity); + OMEGA_SCOPE(LocBruntVaisalaFreqSq, BruntVaisalaFreqSq); + OMEGA_SCOPE(LocIceFraction, IceFraction); + OMEGA_SCOPE(LocLangmuirFactor, LangmuirFactor); + OMEGA_SCOPE(LocBoundaryLayerDepth, BoundaryLayerDepth); + OMEGA_SCOPE(LocIndexBoundaryLayerDepth, IndexBoundaryLayerDepth); + OMEGA_SCOPE(LocBulkRichardson, BulkRichardsonNumber); + OMEGA_SCOPE(LocBulkRichardsonShear, BulkRichardsonShear); + OMEGA_SCOPE(LocUnresolvedShear, UnresolvedShear); + OMEGA_SCOPE(LocBuoyancyJump, BuoyancyJump); + const bool LocUseBLDSmoothing = UseBLDSmoothing; + const bool LocFullRiProfile = DebugDiagnostics; + const Real LocIceFracThresholdForMinOBL = IceFractionThresholdForMinimumOBL; + const Real LocMinimumOBLUnderSeaIce = MinimumOBLUnderSeaIce; + + deepCopy(BulkRichardsonNumber, 0.0_Real); + deepCopy(BulkRichardsonShear, 0.0_Real); + deepCopy(UnresolvedShear, 0.0_Real); + deepCopy(BuoyancyJump, 0.0_Real); + + // Compile-time bound for the per-thread edge scratch arrays below + constexpr I4 MaxEdgesBound = HorzMesh::MaxEdgesBound; + + parallelFor( + "KPP-OBLDepth", {Mesh->NCellsAll}, KOKKOS_LAMBDA(I4 ICell) { + using namespace KPP; + + const Real UStar = + Kokkos::fmax(0.0_Real, SurfaceFrictionVelocity(ICell)); + const Real BuoyFlux = SurfaceBuoyancyFlux(ICell); + + const I4 KMin = MinLayerCell(ICell); + const I4 KMax = MaxLayerCell(ICell); + const I4 KIntTop = Kokkos::min(KMin + 1, NVertLayers); + const I4 KIntDeep = Kokkos::min(KMax + 1, NVertLayers); + + const Real IceFrac = LocIceFraction(ICell); + + // KPP depths are measured below the free surface, so geometric + // heights must be offset by the sea surface height. + const Real Ssh = LocSshCell(ICell); + + // Default to the full water column; overwritten if Ri crosses. + Real OBLDepth = Ssh - ZInterface(ICell, KIntDeep); + I4 KCross = -1; + const Real RiCritical = LocCriticalRichardson; + // Ri is evaluated at cell centers while the reference average spans + // the top epsilon*d; this factor corrects for that offset. + const Real RiScaling = 1.0_Real - 0.5_Real * LocSurfaceLayerExtent; + const Real BuoyFluxEff = BuoyFlux * LocLangmuirFactor(ICell); + + // Unresolved shear coefficient, Large et al. (1994) Eq. (23): + // Vt^2 = Cv * sqrt(-beta_T/(c_s*eps)) / (kappa^2 * Ri_crit) * d*N*w_s + // CSUnres is c_s for the strongly-unstable scalar branch and VtCoef + // collects the constant prefactor. + const Real CSUnres = 24.0_Real * Kokkos::sqrt(17.0_Real); + const Real VtCoef = + Kokkos::sqrt( + 0.2_Real / + Kokkos::max(1.0e-12_Real, CSUnres * LocSurfaceLayerExtent)) / + (VonKar * VonKar); + + // ------------------------------------------------------------------- + // Velocities live on edges (C-grid), so the shear entering Ri is a + // weighted average over the cell's edges. Weights are the MPAS kite + // areas (0.25*dc*dv) normalized by the cell area. + // ------------------------------------------------------------------- + const I4 NEdges = NEdgesOnCell(ICell); + const I4 NEdgesEff = Kokkos::min(NEdges, MaxEdgesBound); + bool EdgeValid[MaxEdgesBound] = {}; + Real EdgeWeights[MaxEdgesBound] = {}; + const Real InvAreaCell = + 1.0_Real / Kokkos::max(AreaCell(ICell), 1.0e-20_Real); + for (I4 J = 0; J < NEdgesEff; ++J) { + const I4 IEdge = EdgesOnCell(ICell, J); + const I4 KEMin = MinLayerEdgeBot(IEdge); + const I4 KEMax = MaxLayerEdgeTop(IEdge); + EdgeValid[J] = + (KEMax >= KEMin && KEMin >= 0 && KEMin < NVertLayers); + if (EdgeValid[J]) { + EdgeWeights[J] = + 0.25_Real * DcEdge(IEdge) * DvEdge(IEdge) * InvAreaCell; + } + } + if (NEdgesEff > 0) { + Real SumW = 0.0_Real; + for (I4 J = 0; J < NEdgesEff; ++J) { + if (EdgeValid[J]) { + SumW += EdgeWeights[J]; + } + } + if (SumW < 1.0e-20_Real) { + // Degenerate kite areas: fall back to equal weighting. + I4 NEdgesValid = 0; + for (I4 J = 0; J < NEdgesEff; ++J) { + if (EdgeValid[J]) { + ++NEdgesValid; + } + } + if (NEdgesValid > 0) { + const Real EqualW = + 1.0_Real / static_cast(NEdgesValid); + for (I4 J = 0; J < NEdgesEff; ++J) { + EdgeWeights[J] = EdgeValid[J] ? EqualW : 0.0_Real; + } + } + } else { + const Real InvSumW = 1.0_Real / SumW; + for (I4 J = 0; J < NEdgesEff; ++J) { + if (EdgeValid[J]) { + EdgeWeights[J] *= InvSumW; + } + } + } + } + + // ------------------------------------------------------------------- + // Bulk Richardson search, Large et al. (1994) Eq. (21): + // Ri_b(d) = (B_r - B(d)) d / (|V_r - V(d)|^2 + Vt^2(d)) + // where the reference values B_r, V_r are averaged over the top + // epsilon*d of the column. Since epsilon*d grows monotonically with + // the trial depth, the averaging window only ever extends downward, + // so the running sums are carried across trial depths rather than + // rebuilt from the surface at each one. + // The OBL base is the first d at which Ri_b reaches RiCritical, and + // the search stops there unless the full profile is wanted for the + // Ri diagnostics. + // ------------------------------------------------------------------- + + // Cell surface-layer density average + I4 KSurfaceAvg = KMin; + const Real ThickTop = Kokkos::abs(ZInterface(ICell, KMin + 1) - + ZInterface(ICell, KMin)); + Real SumThickness = Kokkos::max(ThickTop, 1.0e-12_Real); + Real SumRho = LocPotentialDensity(ICell, KMin) * SumThickness; + + // Per-edge surface-layer velocity averages + I4 KSurfE[MaxEdgesBound] = {}; + Real SumThickE[MaxEdgesBound] = {}; + Real SumUnE[MaxEdgesBound] = {}; + Real SumVtE[MaxEdgesBound] = {}; + + for (I4 J = 0; J < NEdgesEff; ++J) { + if (!EdgeValid[J]) { + continue; + } + const I4 IEdge = EdgesOnCell(ICell, J); + const I4 KEMin = MinLayerEdgeBot(IEdge); + KSurfE[J] = KEMin; + const I4 KIntE0 = Kokkos::min(KEMin + 1, NVertLayers); + const Real Thick0 = Kokkos::abs(ZInterface(ICell, KIntE0) - + ZInterface(ICell, KEMin)); + SumThickE[J] = Kokkos::max(Thick0, 1.0e-12_Real); + const I4 KE0 = Kokkos::min(KEMin, NVertLayers - 1); + SumUnE[J] = LocNormalVelocity(IEdge, KE0) * SumThickE[J]; + SumVtE[J] = LocTangentialVelocity(IEdge, KE0) * SumThickE[J]; + } + + for (I4 K = KMin; K <= KMax; ++K) { + const I4 KCell = Kokkos::min(K, NVertLayers - 1); + const I4 KInt = Kokkos::min(K + 1, NVertLayers); + const Real ZDepth = Ssh - ZInterface(ICell, KInt); + const Real ZCenter = Ssh - ZMid(ICell, KCell); + if (ZDepth < 1.0e-12) + continue; + + const Real SurfLayerDepth = LocSurfaceLayerExtent * ZDepth; + + // Advance cell surface average for density + while (KSurfaceAvg < K && + (Ssh - ZInterface(ICell, KSurfaceAvg + 1)) < + SurfLayerDepth) { + ++KSurfaceAvg; + const I4 KSA = Kokkos::min(KSurfaceAvg, NVertLayers - 1); + const Real DZ = Kokkos::abs(ZInterface(ICell, KSurfaceAvg + 1) - + ZInterface(ICell, KSurfaceAvg)); + const Real ThickK = Kokkos::max(DZ, 1.0e-12_Real); + SumThickness += ThickK; + SumRho += LocPotentialDensity(ICell, KSA) * ThickK; + } + + // Advance per-edge surface averages for velocity + for (I4 J = 0; J < NEdgesEff; ++J) { + if (!EdgeValid[J]) { + continue; + } + const I4 IEdge = EdgesOnCell(ICell, J); + const I4 KEMax = MaxLayerEdgeTop(IEdge); + while (KSurfE[J] < K && + (Ssh - ZInterface(ICell, KSurfE[J] + 1)) < + SurfLayerDepth) { + ++KSurfE[J]; + const I4 KE = Kokkos::min( + Kokkos::max(KSurfE[J], MinLayerEdgeBot(IEdge)), KEMax); + const Real DZ = + Kokkos::abs(ZInterface(ICell, KSurfE[J] + 1) - + ZInterface(ICell, KSurfE[J])); + const Real ThickK = Kokkos::max(DZ, 1.0e-12_Real); + SumThickE[J] += ThickK; + SumUnE[J] += LocNormalVelocity(IEdge, KE) * ThickK; + SumVtE[J] += LocTangentialVelocity(IEdge, KE) * ThickK; + } + } + + const Real InvSumThickness = + 1.0_Real / Kokkos::max(SumThickness, 1.0e-12_Real); + const Real RhoAvgSurf = SumRho * InvSumThickness; + + // Buoyancy jump B_r - B(d), positive for stable stratification + const Real RhoK = LocPotentialDensity(ICell, KCell); + const Real DeltaRho = RhoK - RhoAvgSurf; + const Real DeltaB = Gravity * DeltaRho / RhoSw; + LocBuoyancyJump(ICell, KInt) = DeltaB; + + // Resolved shear |V_r - V(d)|^2, averaged over the cell edges + Real DeltaVSq = 0.0_Real; + if (NEdges > 0) { + for (I4 J = 0; J < NEdgesEff; ++J) { + if (!EdgeValid[J]) { + continue; + } + const I4 IEdge = EdgesOnCell(ICell, J); + const I4 KEMin = MinLayerEdgeBot(IEdge); + const I4 KEMax = MaxLayerEdgeTop(IEdge); + const I4 KE = Kokkos::min(Kokkos::max(K, KEMin), KEMax); + const Real InvThickE = + 1.0_Real / Kokkos::max(SumThickE[J], 1.0e-12_Real); + const Real UnAvg = SumUnE[J] * InvThickE; + const Real VtAvg = SumVtE[J] * InvThickE; + const Real UnK = LocNormalVelocity(IEdge, KE); + const Real VtK = LocTangentialVelocity(IEdge, KE); + const Real DUn = UnK - UnAvg; + const Real DVt = VtK - VtAvg; + DeltaVSq += EdgeWeights[J] * (DUn * DUn + DVt * DVt); + } + } + LocBulkRichardsonShear(ICell, KInt) = + Kokkos::max(DeltaVSq, 1.0e-15_Real); + + const Real SigmaLoc = Kokkos::fmin( + 1.0_Real, Kokkos::fmax(0.0_Real, LocSurfaceLayerExtent)); + + // Turbulent scalar velocity scale w_s at the surface-layer depth + Real WTurb = 0.0_Real; + if (UStar > 1.0e-12_Real) { + const Real U3 = UStar * UStar * UStar; + const Real Zeta = SigmaLoc * ZDepth * VonKar * BuoyFluxEff / + Kokkos::max(U3, 1.0e-20_Real); + const Real PhiInvS = KPP::kppPhiInvScalar(Zeta); + WTurb = VonKar * UStar * Kokkos::max(PhiInvS, 0.0_Real); + } else if (BuoyFluxEff < 0.0_Real) { + // Free convection limit: u* drops out and w_s ~ (c_s d B_0)^1/3 + const Real CS = KPP::CMoS; + const Real WS3 = -CS * SigmaLoc * ZDepth * VonKar * BuoyFluxEff; + WTurb = VonKar * Kokkos::pow(Kokkos::max(WS3, 0.0_Real), + 1.0_Real / 3.0_Real); + } + + // Unresolved turbulent shear Vt^2 (m^2/s^2), Large et al. Eq. + // (23). Cv ramps from 2.1 to 1.7 as stratification strengthens. + const Real NCntr = Kokkos::sqrt( + Kokkos::max(0.0_Real, LocBruntVaisalaFreqSq(ICell, KInt))); + const Real Cv = (NCntr < 0.002_Real) + ? (2.1_Real - 200.0_Real * NCntr) + : 1.7_Real; + const Real Vt2 = Kokkos::max( + 1.0e-10_Real, Cv * VtCoef * ZCenter * NCntr * WTurb / + Kokkos::max(RiCritical, 1.0e-12_Real)); + LocUnresolvedShear(ICell, KInt) = Vt2; + + const Real VelScaleSq = DeltaVSq + Vt2; + + const Real RiBulk = RiScaling * DeltaB * ZCenter / + Kokkos::max(VelScaleSq, 1.0e-12_Real); + LocBulkRichardson(ICell, KInt) = RiBulk; + + if (KCross < 0 && RiBulk > RiCritical) { + KCross = K; + // Levels below the crossing only feed the Ri diagnostics + if (!LocFullRiProfile) + break; + } + } + + if (KCross >= KMin) { + if (KCross > KMin) { + // Ri values are defined at cell centers, so interpolate on + // center depths to keep the abscissa consistent. + const I4 KAbove = Kokkos::max(KMin, KCross - 1); + const I4 KBelow = Kokkos::min(KCross, NVertLayers - 1); + const I4 KAboveRi = Kokkos::min(KAbove + 1, NVertLayers); + const I4 KBelowRi = Kokkos::min(KBelow + 1, NVertLayers); + const Real ZAbove = Ssh - ZMid(ICell, KAbove); + const Real ZBelow = Ssh - ZMid(ICell, KBelow); + const Real RiAbove = LocBulkRichardson(ICell, KAboveRi); + const Real RiBelow = LocBulkRichardson(ICell, KBelowRi); + + const Real H = ZBelow - ZAbove; + if (H > 1.0e-12_Real) { + // CVMix-style QUAD interpolation for OBL crossing: + // - first interior crossing uses zero slope at top point + // - deeper crossings use upstream slope + Real SlopeAbove = 0.0_Real; + if (KCross > KMin + 1) { + const I4 KPrev = Kokkos::max(KMin, KAbove - 1); + const I4 KPrevRi = Kokkos::min(KPrev + 1, NVertLayers); + const Real ZPrev = Ssh - ZMid(ICell, KPrev); + const Real RiPrev = LocBulkRichardson(ICell, KPrevRi); + const Real DZPrev = ZAbove - ZPrev; + if (Kokkos::abs(DZPrev) > 1.0e-12_Real) { + SlopeAbove = (RiAbove - RiPrev) / DZPrev; + } + } + + // In local coordinate T = z - ZAbove: + // Ri(T) = QuadA T^2 + SlopeAbove T + RiAbove, with QuadA + // fixed by requiring Ri(H) = RiBelow. The OBL base is the + // root of Ri(T) = RiCritical. + const Real QuadA = + (RiBelow - RiAbove - SlopeAbove * H) / (H * H); + const Real QuadC = RiAbove - RiCritical; + + Real TCross = H; + if (Kokkos::abs(QuadA) < 1.0e-14_Real) { + // Degenerate quadratic -> linear fallback. + const Real DRi = RiBelow - RiAbove; + if (Kokkos::abs(DRi) > 1.0e-12_Real) { + const Real Frac = Kokkos::fmax( + 0.0_Real, + Kokkos::fmin(1.0_Real, + (RiCritical - RiAbove) / DRi)); + TCross = Frac * H; + } + } else { + const Real Disc = + SlopeAbove * SlopeAbove - 4.0_Real * QuadA * QuadC; + if (Disc >= 0.0_Real) { + const Real SqrtDisc = Kokkos::sqrt(Disc); + const Real T1 = + (-SlopeAbove + SqrtDisc) / (2.0_Real * QuadA); + const Real T2 = + (-SlopeAbove - SqrtDisc) / (2.0_Real * QuadA); + + const bool T1Ok = (T1 >= 0.0_Real && T1 <= H); + const bool T2Ok = (T2 >= 0.0_Real && T2 <= H); + if (T1Ok && T2Ok) { + // Both roots lie in the interval; prefer the one + // nearest mid-interval, as CVMix does. + const Real Mid = 0.5_Real * H; + TCross = + (Kokkos::abs(T1 - Mid) <= Kokkos::abs(T2 - Mid)) + ? T1 + : T2; + } else if (T1Ok) { + TCross = T1; + } else if (T2Ok) { + TCross = T2; + } else { + TCross = H; + } + } + } + + TCross = Kokkos::fmax(0.0_Real, Kokkos::fmin(H, TCross)); + OBLDepth = ZAbove + TCross; + } else { + OBLDepth = ZBelow; + } + } else { + // Match center-based OBL convention when crossing occurs in + // the top interval. + OBLDepth = Ssh - ZMid(ICell, KMin); + } + } else { + OBLDepth = Ssh - ZInterface(ICell, KIntDeep); + } + + const Real TopLayerThickness = + Kokkos::abs(ZInterface(ICell, KIntTop) - ZInterface(ICell, KMin)); + const Real MinOBLDepth = 0.5_Real * TopLayerThickness; + const Real MaxOBLDepth = Ssh - ZMid(ICell, KMax); + OBLDepth = KPP::kppClampOBLDepth( + OBLDepth, MinOBLDepth, MaxOBLDepth, + IceFrac > LocIceFracThresholdForMinOBL, LocMinimumOBLUnderSeaIce); + + const I4 KFinal = + KPP::kppOBLIndex(ZInterface, ICell, KMin, KMax, Ssh, OBLDepth); + + LocBoundaryLayerDepth(ICell) = OBLDepth; + LocIndexBoundaryLayerDepth(ICell) = KFinal; + }); + + if (LocUseBLDSmoothing) { + Array1DReal BoundaryLayerDepthSmooth("BoundaryLayerDepthSmooth", + Mesh->NCellsSize); + OMEGA_SCOPE(LocBoundaryLayerDepthSmooth, BoundaryLayerDepthSmooth); + OMEGA_SCOPE(LocNCellsAll, Mesh->NCellsAll); + + // Area-weighted smoothing of the BLD over each cell and its neighbors + // (MPAS-Ocean cvmix convention). This suppresses the grid-scale noise + // that the discrete Ri crossing search can introduce. + parallelFor( + "KPP-OBLDepth-Smooth", {Mesh->NCellsAll}, KOKKOS_LAMBDA(I4 ICell) { + const I4 KMin = MinLayerCell(ICell); + if (KMin < 0 || KMin >= NVertLayers) { + LocBoundaryLayerDepthSmooth(ICell) = + LocBoundaryLayerDepth(ICell); + return; + } + + const I4 NEdges = NEdgesOnCell(ICell); + Real AreaSum = 0.0_Real; + Real BLDSum = 0.0_Real; + I4 EdgeCount = 0; + + for (I4 J = 0; J < NEdges; ++J) { + const I4 INeighbor = CellsOnCell(ICell, J); + if (INeighbor == LocNCellsAll) { + continue; + } + + const I4 KMinNbr = MinLayerCell(INeighbor); + if (KMinNbr < 0 || KMinNbr >= NVertLayers) { + continue; + } + + const Real NbrArea = AreaCell(INeighbor); + BLDSum += 2.0_Real * NbrArea * LocBoundaryLayerDepth(INeighbor); + AreaSum += 2.0_Real * NbrArea; + ++EdgeCount; + } + + if (EdgeCount > 0) { + const Real SelfArea = AreaCell(ICell); + BLDSum += LocBoundaryLayerDepth(ICell) * + static_cast(EdgeCount) * SelfArea; + AreaSum += static_cast(EdgeCount) * SelfArea; + } + + if (AreaSum > 0.0_Real) { + LocBoundaryLayerDepthSmooth(ICell) = BLDSum / AreaSum; + } else { + LocBoundaryLayerDepthSmooth(ICell) = + LocBoundaryLayerDepth(ICell); + } + }); + + parallelFor( + "KPP-OBLDepth-CommitSmooth", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 ICell) { + const I4 KMin = MinLayerCell(ICell); + const I4 KMax = MaxLayerCell(ICell); + if (KMin < 0 || KMax < KMin || KMin >= NVertLayers) { + return; + } + + const Real Ssh = LocSshCell(ICell); + + const I4 KIntTop = Kokkos::min(KMin + 1, NVertLayers); + const Real TopLayerThickness = Kokkos::abs( + ZInterface(ICell, KIntTop) - ZInterface(ICell, KMin)); + const Real MinOBLDepth = 0.5_Real * TopLayerThickness; + const Real MaxOBLDepth = Ssh - ZMid(ICell, KMax); + + // The sea-ice minimum is deliberately not reapplied here; it was + // already enforced before smoothing. + const Real OBLDepth = KPP::kppClampOBLDepth( + LocBoundaryLayerDepthSmooth(ICell), MinOBLDepth, MaxOBLDepth, + false, 0.0_Real); + + const I4 KFinal = + KPP::kppOBLIndex(ZInterface, ICell, KMin, KMax, Ssh, OBLDepth); + + LocBoundaryLayerDepth(ICell) = OBLDepth; + LocIndexBoundaryLayerDepth(ICell) = KFinal; + }); + } +} + +/// Stage 2: Compute KPP mixing contribution or matched coefficients +void KPPMix::computeMixingCoefficients( + const Array2DReal &PotentialDensity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, const Array2DReal &InteriorVertDiff, + const Array2DReal &InteriorVertVisc) { + + using namespace KPP; + + (void)PotentialDensity; + + I4 NVertLayers = VCoord->NVertLayers; + + // ======================================================================= + // Capture data for use in lambda + // ======================================================================= + OMEGA_SCOPE(LocBoundaryLayerDepth, BoundaryLayerDepth); + OMEGA_SCOPE(LocIndexBoundaryLayerDepth, IndexBoundaryLayerDepth); + OMEGA_SCOPE(LocVertDiff, VertDiff); + OMEGA_SCOPE(LocVertVisc, VertVisc); + OMEGA_SCOPE(LocVertNonLocalFlux, VertNonLocalFlux); + OMEGA_SCOPE(LocTurbulentVelocityScale, TurbulentVelocityScale); + OMEGA_SCOPE(LocSurfaceFrictionVelocity, SurfaceFrictionVelocity); + OMEGA_SCOPE(LocSurfaceBuoyancyFlux, SurfaceBuoyancyFlux); + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + OMEGA_SCOPE(ZInterface, VCoord->GeomZInterface); + OMEGA_SCOPE(ZMid, VCoord->GeomZMid); + OMEGA_SCOPE(LocSshCell, VCoord->SshCell); + OMEGA_SCOPE(LocInteriorVertDiff, InteriorVertDiff); + OMEGA_SCOPE(LocInteriorVertVisc, InteriorVertVisc); + + // Capture member variables for use in lambda + const Real LocSurfaceLayerExtent = SurfaceLayerExtent; + const KPPMatchType LocMatch = MatchTechnique; + const Real LocNonLocalCs = KPP::kppNonLocalCs(VonKar, LocSurfaceLayerExtent); + bool LocUseEnhancedDiffusion = UseEnhancedDiffusion; + const Real LocKappa = VonKar; + const bool LocUseInteriorMix = + InteriorVertDiff.data() != nullptr && InteriorVertVisc.data() != nullptr; + const bool LocUseMatchedShapes = + LocUseInteriorMix && LocMatch == KPPMatchType::MatchBoth; + + // ======================================================================= + // Initialize with zero KPP contribution, or precomputed interior mixing for + // matched-coefficient construction. + // ======================================================================= + parallelFor( + "KPP-Coeffs-Init", {Mesh->NCellsAll, NVertLayers + 1}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + LocVertDiff(ICell, K) = + LocUseInteriorMix ? LocInteriorVertDiff(ICell, K) : 0.0_Real; + LocVertVisc(ICell, K) = + LocUseInteriorMix ? LocInteriorVertVisc(ICell, K) : 0.0_Real; + LocVertNonLocalFlux(ICell, K) = 0.0; + LocTurbulentVelocityScale(ICell, K) = 0.0; + }); + + // ======================================================================= + // Stage 2: Compute KPP profile-based mixing coefficients + // ======================================================================= + + parallelFor( + "KPP-MixingCoeffs", {Mesh->NCellsAll}, KOKKOS_LAMBDA(I4 ICell) { + // Get OBL information for this cell + Real HOBL = LocBoundaryLayerDepth(ICell); + + const I4 KMin = MinLayerCell(ICell); + const I4 KMax = MaxLayerCell(ICell); + if (KMin < 0 || KMin >= NVertLayers || KMax < KMin) { + return; + } + const I4 KMatch = + Kokkos::min(KMax + 1, LocIndexBoundaryLayerDepth(ICell) + 1); + + // KPP depths are measured below the free surface, so geometric + // heights must be offset by the sea surface height. + const Real Ssh = LocSshCell(ICell); + + // ============================================================= + // Compute turbulent velocity scales + // ============================================================= + Real UStar = LocSurfaceFrictionVelocity(ICell); + Real BuoyFlux = LocSurfaceBuoyancyFlux(ICell); + + // ============================================================= + // Compute mixing coefficients at each interface + // ============================================================= + for (I4 K = KMin; K <= KMax + 1; ++K) { + const I4 KIface = Kokkos::min(Kokkos::max(K, 0), NVertLayers); + const Real ZDepth = Ssh - ZInterface(ICell, KIface); + + // Check if within OBL using depth below the free surface. + if (ZDepth <= HOBL && HOBL > 0.0_Real) { + // Normalized depth in Omega sign convention: sigma in [-1,0]. + Real Sigma = -ZDepth / HOBL; + Sigma = Kokkos::fmax(-1.0_Real, Kokkos::fmin(0.0_Real, Sigma)); + + // CVMix-style turbulent scales: w = kappa*u*/phi in general, + // with explicit free-convection limits when u*=0. The scales + // are frozen at the surface-layer depth below the surface + // layer, so SigmaLoc is capped at SurfaceLayerExtent. + const Real SigmaCoord = -Sigma; // [0,1] + const Real SigmaLoc = Kokkos::fmin( + LocSurfaceLayerExtent, Kokkos::fmax(0.0_Real, SigmaCoord)); + + Real WMTurb = 0.0_Real; + Real WSTurb = 0.0_Real; + KPP::kppTurbScales(UStar, BuoyFlux, HOBL, SigmaLoc, LocKappa, + WMTurb, WSTurb); + + // For MatchBoth, the shape value the KPP profile must reach at + // the OBL base so that it joins the interior coefficient there. + const Real MatchViscShape = + LocUseMatchedShapes + ? KPP::kppMatchShape(LocInteriorVertVisc(ICell, KMatch), + HOBL, WMTurb) + : 0.0_Real; + const Real MatchDiffShape = + LocUseMatchedShapes + ? KPP::kppMatchShape(LocInteriorVertDiff(ICell, KMatch), + HOBL, WSTurb) + : 0.0_Real; + + // ======================================================== + // Momentum mixing contribution. + // ======================================================== + Real ShapeM = LocUseMatchedShapes + ? KPP::kppShapeMatched(Sigma, MatchViscShape) + : KPP::kppShapeMomentum(Sigma); + LocVertVisc(ICell, K) = HOBL * WMTurb * ShapeM; + + // ======================================================== + // Tracer mixing contribution. + // ======================================================== + Real ShapeS = LocUseMatchedShapes + ? KPP::kppShapeMatched(Sigma, MatchDiffShape) + : KPP::kppShapeScalar(Sigma); + LocVertDiff(ICell, K) = HOBL * WSTurb * ShapeS; + LocTurbulentVelocityScale(ICell, K) = WSTurb; + + // ======================================================== + // Non-local flux: C_s * G(sigma). + // C_s = C* * kappa * (c_s * kappa * epsilon)^(1/3) + // per Large et al. (1994) eq. 20 (~6.33 with default constants) + // ======================================================== + // The non-local shape is always the unmatched scalar shape, + // independent of MatchTechnique, so that gamma vanishes at the + // OBL base. The matched shape is non-zero there by + // construction, which would leave a non-local flux at the base + // that jumps to zero just below it. CVMix likewise keeps + // matching (a diffusivity choice) separate from the non-local + // shape. + const Real NonLocalShape = KPP::kppShapeScalar(Sigma); + + // Match CVMix behavior: apply non-local term only when + // surface buoyancy forcing is unstable/neutral. + if (BuoyFlux <= 0.0_Real) { + LocVertNonLocalFlux(ICell, K) = + LocNonLocalCs * NonLocalShape; + } else { + LocVertNonLocalFlux(ICell, K) = 0.0; + } + + } else { + // Below OBL: preserve interior values for MatchBoth, otherwise + // no KPP contribution. + LocVertDiff(ICell, K) = LocUseInteriorMix + ? LocInteriorVertDiff(ICell, K) + : 0.0_Real; + LocVertVisc(ICell, K) = LocUseInteriorMix + ? LocInteriorVertVisc(ICell, K) + : 0.0_Real; + LocVertNonLocalFlux(ICell, K) = 0.0; + LocTurbulentVelocityScale(ICell, K) = 0.0; + } + } + + // Optional enhanced diffusion/viscosity treatment at OBL base. + // Match CVMix Appendix D weighting at the interface nearest HOBL: + // the OBL base rarely lands on an interface, so the coefficient at + // the neighboring interface KTarget is replaced by a quadratic blend + // of the KPP value extrapolated from KKtup and the value already + // there, weighted by where HOBL falls between the two cell centers. + if (LocUseEnhancedDiffusion && HOBL > 0.0_Real) { + const I4 KOBL = Kokkos::max( + KMin, Kokkos::min(LocIndexBoundaryLayerDepth(ICell), KMax)); + const Real ZMidOBL = Ssh - ZMid(ICell, KOBL); + + const bool TargetOutsideOBL = HOBL >= ZMidOBL; + const I4 KKtup = + TargetOutsideOBL ? KOBL : Kokkos::max(KMin, KOBL - 1); + const I4 KTarget = TargetOutsideOBL + ? Kokkos::min(KOBL + 1, KMax + 1) + : Kokkos::max(KMin + 1, KOBL); + + const Real ZKtup = Ssh - ZMid(ICell, KKtup); + const Real ZNext = (KKtup < KMax) + ? (Ssh - ZMid(ICell, KKtup + 1)) + : (Ssh - ZInterface(ICell, KKtup + 1)); + const Real Delta = Kokkos::fmax( + 0.0_Real, + Kokkos::fmin(1.0_Real, + (HOBL - ZKtup) / + Kokkos::max(ZNext - ZKtup, 1.0e-12_Real))); + const Real OneMinusDelta = 1.0_Real - Delta; + + Real SigmaKtup = -ZKtup / HOBL; + SigmaKtup = + Kokkos::fmax(-1.0_Real, Kokkos::fmin(0.0_Real, SigmaKtup)); + const Real SigmaCoord = -SigmaKtup; + const Real SigmaLoc = Kokkos::fmin( + LocSurfaceLayerExtent, Kokkos::fmax(0.0_Real, SigmaCoord)); + + Real WMKtup = 0.0_Real; + Real WSKtup = 0.0_Real; + KPP::kppTurbScales(UStar, BuoyFlux, HOBL, SigmaLoc, LocKappa, + WMKtup, WSKtup); + + const Real MatchViscShape = + LocUseMatchedShapes + ? KPP::kppMatchShape(LocInteriorVertVisc(ICell, KMatch), + HOBL, WMKtup) + : 0.0_Real; + const Real MatchDiffShape = + LocUseMatchedShapes + ? KPP::kppMatchShape(LocInteriorVertDiff(ICell, KMatch), + HOBL, WSKtup) + : 0.0_Real; + + const Real ViscKtup = + HOBL * WMKtup * + (LocUseMatchedShapes + ? KPP::kppShapeMatched(SigmaKtup, MatchViscShape) + : KPP::kppShapeMomentum(SigmaKtup)); + const Real DiffKtup = + HOBL * WSKtup * + (LocUseMatchedShapes + ? KPP::kppShapeMatched(SigmaKtup, MatchDiffShape) + : KPP::kppShapeScalar(SigmaKtup)); + + const Real ViscProfile = LocVertVisc(ICell, KTarget); + const Real DiffProfile = LocVertDiff(ICell, KTarget); + + const Real EnhVisc = OneMinusDelta * OneMinusDelta * ViscKtup + + Delta * Delta * ViscProfile; + const Real EnhDiff = OneMinusDelta * OneMinusDelta * DiffKtup + + Delta * Delta * DiffProfile; + + const Real OldVisc = LocUseInteriorMix + ? LocInteriorVertVisc(ICell, KTarget) + : 0.0_Real; + const Real OldDiff = LocUseInteriorMix + ? LocInteriorVertDiff(ICell, KTarget) + : 0.0_Real; + const Real NewVisc = OneMinusDelta * OldVisc + Delta * EnhVisc; + const Real NewDiff = OneMinusDelta * OldDiff + Delta * EnhDiff; + + LocVertVisc(ICell, KTarget) = NewVisc; + LocVertDiff(ICell, KTarget) = NewDiff; + + // Keep the non-local term consistent with the rescaled diffusivity + if (!TargetOutsideOBL && DiffProfile != 0.0_Real) { + LocVertNonLocalFlux(ICell, KTarget) = + LocVertNonLocalFlux(ICell, KTarget) * NewDiff / DiffProfile; + } else if (!TargetOutsideOBL) { + LocVertNonLocalFlux(ICell, KTarget) = 0.0_Real; + } + } + }); +} + +/// Register fields with I/O system +void KPPMix::defineFields() { + // BoundaryLayerDepth on cells + std::vector CellDims(1); + CellDims[0] = "NCells"; + auto OBLDepthField = + Field::create(OBLDepthFldName, // field name + "ocean boundary layer depth", // long name + "m", // units + "", // CF standard name + 0.0, // min valid value + std::numeric_limits::max(), // max valid value + 1, // number of dims + CellDims); + + auto OBLDepthIndexField = + Field::create(OBLDepthIndexFldName, // field name + "ocean boundary layer depth index", // long name + "", // units + "", // CF standard name + -1, // min valid value + std::numeric_limits::max(), // max valid value + 1, // number of dims + CellDims); + + // KPP non-local tracer flux profile on cell-layer interfaces + std::vector FluxDims(2); + FluxDims[0] = "NCells"; + FluxDims[1] = "NVertLayersP1"; + auto NonLocalFluxField = + Field::create(NonLocalFluxFldName, // field name + "KPP non-local tracer flux profile", // long name + "1", // units + "", // CF standard name + std::numeric_limits::lowest(), // min valid value + std::numeric_limits::max(), // max valid value + 2, // number of dims + FluxDims); + + auto BulkRichardsonField = + Field::create(BulkRichardsonFldName, // field name + "bulk Richardson number", // long name + "1", // units + "", // CF standard name + std::numeric_limits::lowest(), // min valid value + std::numeric_limits::max(), // max valid value + 2, // number of dims + FluxDims); + + auto BulkRichardsonShearField = + Field::create(BulkRichardsonShearFldName, // field name + "bulk Richardson shear term", // long name + "m2 s-2", // units + "", // CF standard name + 0.0, // min valid value + std::numeric_limits::max(), // max valid value + 2, // number of dims + FluxDims); + + auto UnresolvedShearField = + Field::create(UnresolvedShearFldName, // field name + "KPP unresolved shear term Vt2", // long name + "m2 s-2", // units + "", // CF standard name + 0.0, // min valid value + std::numeric_limits::max(), // max valid value + 2, // number of dims + FluxDims); + + auto BuoyancyJumpField = + Field::create(BuoyancyJumpFldName, // field name + "KPP buoyancy jump (density anomaly)", // long name + "m s-2", // units + "", // CF standard name + std::numeric_limits::lowest(), // min valid value + std::numeric_limits::max(), // max valid value + 2, // number of dims + FluxDims); + + auto TurbulentVelScaleField = + Field::create(TurbulentVelScaleFldName, // field name + "KPP turbulent velocity scale", // long name + "m s-1", // units + "", // CF standard name + 0.0, // min valid value + std::numeric_limits::max(), // max valid value + 2, // number of dims + FluxDims); + + std::vector LayerDims(2); + LayerDims[0] = "NCells"; + LayerDims[1] = "NVertLayers"; + auto PotentialDensityField = + Field::create(PotentialDensityFldName, // field name + "KPP potential density", // long name + "kg m-3", // units + "", // CF standard name + std::numeric_limits::lowest(), // min valid value + std::numeric_limits::max(), // max valid value + 2, // number of dims + LayerDims); + + // Group KPP-specific outputs for convenient stream selection. + auto KPPGroup = FieldGroup::create("KPPMix"); + KPPGroup->addField(OBLDepthFldName); + KPPGroup->addField(OBLDepthIndexFldName); + KPPGroup->addField(NonLocalFluxFldName); + KPPGroup->addField(BulkRichardsonFldName); + KPPGroup->addField(BulkRichardsonShearFldName); + KPPGroup->addField(UnresolvedShearFldName); + KPPGroup->addField(BuoyancyJumpFldName); + KPPGroup->addField(TurbulentVelScaleFldName); + KPPGroup->addField(PotentialDensityFldName); + KPPGroup->addField(SurfFricVelFldName); + KPPGroup->addField(SurfBuoyFluxFldName); + + OBLDepthField->attachData(BoundaryLayerDepth, false); + OBLDepthIndexField->attachData(IndexBoundaryLayerDepth, false); + NonLocalFluxField->attachData(VertNonLocalFlux, false); + BulkRichardsonField->attachData(BulkRichardsonNumber, false); + BulkRichardsonShearField->attachData(BulkRichardsonShear, + false); + UnresolvedShearField->attachData(UnresolvedShear, false); + BuoyancyJumpField->attachData(BuoyancyJump, false); + TurbulentVelScaleField->attachData(TurbulentVelocityScale, + false); + PotentialDensityField->attachData(PotentialDensity, false); + + // Surface forcing fields on cells + auto SurfFricVelField = + Field::create(SurfFricVelFldName, // field name + "KPP surface friction velocity u*", // long name + "m s-1", // units + "", // CF standard name + 0.0, // min valid value + std::numeric_limits::max(), // max valid value + 1, // number of dims + CellDims); + SurfFricVelField->attachData(SurfaceFrictionVelocity, false); + + auto SurfBuoyFluxField = + Field::create(SurfBuoyFluxFldName, // field name + "KPP surface buoyancy flux", // long name + "m2 s-3", // units + "", // CF standard name + std::numeric_limits::lowest(), // min valid value + std::numeric_limits::max(), // max valid value + 1, // number of dims + CellDims); + SurfBuoyFluxField->attachData(SurfaceBuoyancyFlux, false); + + OBLDepthField->addMetadata("_FillValue", FillValueReal); + OBLDepthIndexField->addMetadata("_FillValue", FillValueI4); + NonLocalFluxField->addMetadata("_FillValue", FillValueReal); + BulkRichardsonField->addMetadata("_FillValue", FillValueReal); + BulkRichardsonShearField->addMetadata("_FillValue", FillValueReal); + UnresolvedShearField->addMetadata("_FillValue", FillValueReal); + BuoyancyJumpField->addMetadata("_FillValue", FillValueReal); + TurbulentVelScaleField->addMetadata("_FillValue", FillValueReal); + PotentialDensityField->addMetadata("_FillValue", FillValueReal); + SurfFricVelField->addMetadata("_FillValue", FillValueReal); + SurfBuoyFluxField->addMetadata("_FillValue", FillValueReal); + + LOG_INFO("KPPMix::defineFields: registered {}, {}, {}, {}, {}, {}, {}, {}, " + "{}, {}, {}", + OBLDepthFldName, OBLDepthIndexFldName, NonLocalFluxFldName, + BulkRichardsonFldName, BulkRichardsonShearFldName, + UnresolvedShearFldName, BuoyancyJumpFldName, + TurbulentVelScaleFldName, PotentialDensityFldName, + SurfFricVelFldName, SurfBuoyFluxFldName); +} + +} // namespace OMEGA diff --git a/components/omega/src/ocn/KPPMix.h b/components/omega/src/ocn/KPPMix.h new file mode 100644 index 000000000000..6042451cc5af --- /dev/null +++ b/components/omega/src/ocn/KPPMix.h @@ -0,0 +1,236 @@ +#ifndef OMEGA_KPP_MIX_H +#define OMEGA_KPP_MIX_H +//===-- ocn/KPPMix.h - K-Profile Parameterization --------*- C++ -*-===// +// +/// \file +/// \brief K-Profile Parameterization (KPP) boundary layer mixing scheme +/// +/// This header defines the KPPMix class for computing ocean boundary layer +/// mixing coefficients using the K-Profile Parameterization scheme. +/// Follows Large et al. (1994) formulation with optional Langmuir circulation +/// enhancement. +// +//===----------------------------------------------------------------------===// + +#include "AuxiliaryState.h" +#include "Config.h" +#include "DataTypes.h" +#include "HorzMesh.h" +#include "HorzOperators.h" +#include "KPPConstants.h" +#include "MachEnv.h" +#include "OmegaKokkos.h" +#include "TimeMgr.h" +#include "VertCoord.h" +#include + +namespace OMEGA { + +/// @brief How the KPP profile is matched to interior mixing at the OBL base. +/// Also selects the shape used for the non-local flux, which follows the +/// scalar diffusivity profile. +enum class KPPMatchType : I4 { + SimpleShapes = 0, ///< Unmatched Large et al. (1994) cubic shapes + MatchBoth = 1 ///< Match the interior coefficient at the OBL base +}; + +/// @brief KPP Boundary Layer Mixing Scheme +/// +/// Implements the K-Profile Parameterization following Large et al. (1994) +/// with optional Langmuir circulation enhancement. Computes vertical +/// diffusivity, viscosity, and non-local flux coefficients for the OBL. +/// +/// Two-stage computation: +/// 1. Stage 1: Compute OBL depth from bulk Richardson criterion +/// 2. Stage 2: Compute mixing coefficients within and below OBL +/// +class KPPMix { + + public: + /// @brief Singleton instance management + static KPPMix *getInstance(); + static void init(); + static void destroyInstance(); + + /// @brief Main computation routine + /// Calls Stage 1 and Stage 2 computation in sequence + /// + /// Input arrays should be pre-populated with current state. + /// NormalVelocity and TangentialVelocity are edge-based quantities + /// (C-grid convention): dimensions [NEdges × NVertLayers]. + /// Output arrays are computed in-place. + void computeKPPMix( + const Array2DReal + &PotentialDensity, ///< Density (kg/m³) [NCells×NLevels] + const Array2DReal &NormalVelocity, ///< Normal vel on edges (m/s) + const Array2DReal &TangentialVelocity, ///< Tangential vel on edges (m/s) + const Array1DReal &SurfaceFrictionVelocity, ///< u* (m/s) + const Array1DReal &SurfaceBuoyancyFlux, ///< B_0 (m²/s³) + const Array2DReal &BruntVaisalaFreqSq, ///< N² (s⁻²) + const Array1DReal &IceFraction, ///< Sea ice cover (0-1) + const Array1DReal &WindSpeed10m = + Array1DReal() ///< Wind for Langmuir (m/s) + ); + + // ======================================================================= + // Output Fields + // ======================================================================= + + /// @brief Vertical diffusivity at layer interfaces (m²/s) + /// Size: [nCells][nLevels+1] + Array2DReal VertDiff; + + /// @brief Vertical viscosity at layer interfaces (m²/s) + /// Size: [nCells][nLevels+1] + Array2DReal VertVisc; + + /// @brief Boundary layer depth (m) + /// Size: [nCells] + Array1DReal BoundaryLayerDepth; + + /// @brief OBL depth as layer index + /// Size: [nCells] + Array1DI4 IndexBoundaryLayerDepth; + + /// @brief Non-local flux coefficient profile G(σ) (dimensionless) + /// Size: [nCells][nLevels+1] + /// Applied to surface tracer fluxes to compute vertical transport + Array2DReal VertNonLocalFlux; + + /// @brief Bulk Richardson number profile used in OBL search (dimensionless) + /// Size: [nCells][nLevels+1] + Array2DReal BulkRichardsonNumber; + + /// @brief Shear contribution to bulk Richardson denominator (m^2/s^2) + /// Size: [nCells][nLevels+1] + Array2DReal BulkRichardsonShear; + + /// @brief Unresolved shear contribution Vt^2 (m^2/s^2) + /// Size: [nCells][nLevels+1] + Array2DReal UnresolvedShear; + + /// @brief Buoyancy jump (density anomaly converted to buoyancy) (m/s²) + /// Size: [nCells][nLevels+1] + /// Captures delta_b = g * delta_rho / rho_sw at each layer during OBL search + Array2DReal BuoyancyJump; + + /// @brief Turbulent velocity scale profile (m/s), tracer branch + /// Size: [nCells][nLevels+1] + Array2DReal TurbulentVelocityScale; + + /// @brief Potential density used by KPP OBL search (kg/m^3) + /// Size: [nCells][nLevels] + Array2DReal PotentialDensity; + + /// @brief Surface friction velocity u* (m/s) + /// Size: [nCells] + Array1DReal SurfaceFrictionVelocity; + + /// @brief Surface buoyancy flux B_0 (m²/s³) + /// Size: [nCells] + Array1DReal SurfaceBuoyancyFlux; + + // ======================================================================= + // Configuration Parameters + // ======================================================================= + + bool Enabled = true; ///< Enable/disable KPP mixing + + // Defaults below may be overridden from the Config file; where a value also + // appears in KPPConstants.h, that is the authoritative default. + Real CriticalRichardson = 0.25; ///< Ri_crit for OBL base + Real SurfaceLayerExtent = KPP::SurfaceLayerExtent; ///< Frac of OBL depth + + bool UseLangmuirCirculation = true; ///< Apply wave enhancement + bool DebugDiagnostics = false; ///< Print per-step KPP diagnostics + + // Ice/Langmuir controls (kept configurable to match reference semantics) + /// Disable Langmuir above this ice fraction + Real IceFractionThresholdForLangmuir = KPP::IceFracThresh; + /// Apply minimum OBL depth above this ice fraction + Real IceFractionThresholdForMinimumOBL = KPP::IceSuppressThresh; + /// Min OBL depth under sea ice (m) + Real MinimumOBLUnderSeaIce = KPP::MinOBLUnderIce; + + Real BackgroundVisc = 1.0e-4; ///< Background viscosity below OBL (m²/s) + Real BackgroundDiff = 1.0e-5; ///< Background diffusivity below OBL (m²/s) + + // KPP matching/profile controls (CVMix-style semantics) + KPPMatchType MatchTechnique = KPPMatchType::SimpleShapes; + std::string InterpType2Str = "LMD94"; ///< Linear, Quadratic, Cubic, LMD94 + bool UseEnhancedDiffusion = true; ///< Apply enhanced mixing at OBL base + bool UseBLDSmoothing = true; ///< Apply MPAS-style BLD horizontal smoothing + + // Field names for I/O + std::string BuoyancyJumpFldName; + std::string VertDiffFldName; + std::string VertViscFldName; + std::string OBLDepthFldName; + std::string OBLDepthIndexFldName; + std::string NonLocalFluxFldName; + std::string BulkRichardsonFldName; + std::string BulkRichardsonShearFldName; + std::string UnresolvedShearFldName; + std::string TurbulentVelScaleFldName; + std::string PotentialDensityFldName; + std::string SurfFricVelFldName; + std::string SurfBuoyFluxFldName; + std::string Name; + + private: + /// @brief Private constructor for singleton pattern + KPPMix(const std::string &InName, const HorzMesh *InMesh, + const VertCoord *InVCoord); + + /// @brief Private destructor + ~KPPMix(); + + /// @brief Static singleton instance + static KPPMix *Instance; + + /// @brief Mesh and coordinate references + const HorzMesh *Mesh; + const VertCoord *VCoord; + + public: + /// @brief Stage 1: Compute OBL depth using edge-based velocity shear + void computeOBLDepth(const Array2DReal &PotentialDensity, + const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, + const Array2DReal &BruntVaisalaFreqSq, + const Array1DReal &IceFraction, + const Array1DReal &WindSpeed10m); + + /// @brief Stage 2: Compute KPP mixing contribution or matched coefficients + void computeMixingCoefficients( + const Array2DReal &PotentialDensity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, + const Array2DReal &InteriorVertDiff = Array2DReal(), + const Array2DReal &InteriorVertVisc = Array2DReal()); + + private: + /// @brief Print targeted diagnostics for KPP troubleshooting + void logDiagnostics(const Array2DReal &PotentialDensity, + const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array1DReal &SurfaceFrictionVelocity, + const Array1DReal &SurfaceBuoyancyFlux, + const Array1DReal &WindSpeed10m); + + /// @brief Register fields with I/O system + void defineFields(); + + // Delete copy and move constructors/assignment + KPPMix(const KPPMix &) = delete; + KPPMix &operator=(const KPPMix &) = delete; + KPPMix(KPPMix &&) = delete; + KPPMix &operator=(KPPMix &&) = delete; + +}; // class KPPMix + +} // namespace OMEGA + +#endif // OMEGA_KPP_MIX_H diff --git a/components/omega/src/ocn/OceanInit.cpp b/components/omega/src/ocn/OceanInit.cpp index 6a50cb2c0b2b..b2b89e769de7 100644 --- a/components/omega/src/ocn/OceanInit.cpp +++ b/components/omega/src/ocn/OceanInit.cpp @@ -20,6 +20,7 @@ #include "HorzMesh.h" #include "IO.h" #include "IOStream.h" +#include "KPPMix.h" #include "Logging.h" #include "MachEnv.h" #include "OceanDriver.h" @@ -289,6 +290,7 @@ static int initOmegaModulesImpl(MPI_Comm Comm) { Eos::init(); PressureGrad::init(); VertMix::init(); + KPPMix::init(); Tendencies::init(); // Validate SurfaceTracerRestoring configuration diff --git a/components/omega/src/ocn/Tendencies.cpp b/components/omega/src/ocn/Tendencies.cpp index d8273a844de0..f96297aa6750 100644 --- a/components/omega/src/ocn/Tendencies.cpp +++ b/components/omega/src/ocn/Tendencies.cpp @@ -14,6 +14,8 @@ #include "Error.h" #include "Field.h" #include "Forcing.h" +#include "HorzOperators.h" +#include "KPPMix.h" #include "OceanState.h" #include "PGrad.h" #include "Pacer.h" @@ -283,6 +285,18 @@ void Tendencies::readConfig(Config *OmegaConfig ///< [in] Omega config } } + Err += TendConfig.get("SfcThicknessForcingTendencyEnable", + this->SfcThicknessForcing.Enabled); + CHECK_ERROR_ABORT( + Err, + "Tendencies: SfcThicknessForcingTendencyEnable not found in TendConfig"); + + Err += TendConfig.get("SfcTracerForcingTendencyEnable", + this->SfcTracerForcing.Enabled); + CHECK_ERROR_ABORT( + Err, + "Tendencies: SfcTracerForcingTendencyEnable not found in TendConfig"); + if (this->TracerDiffusion.Enabled) { Err += TendConfig.get("EddyDiff2", this->TracerDiffusion.EddyDiff2); CHECK_ERROR_ABORT(Err, "Tendencies: EddyDiff2 not found in TendConfig"); @@ -386,6 +400,21 @@ void Tendencies::readConfig(Config *OmegaConfig ///< [in] Omega config ABORT_ERROR("Tendencies: VertMix must be initialized when" "vertical mixing tendencies are enabled"); } + // Optional KPP non-local tracer tendency: no abort if missing + Error TracerNonLocalErr = TendConfig.get( + "TracerNonLocalFluxTendencyEnable", this->TracerNonLocalFluxEnabled); + if (!TracerNonLocalErr.isSuccess()) { + TracerNonLocalErr.reset(); + this->TracerNonLocalFluxEnabled = false; + } + + Error TracerDiagErr = + TendConfig.get("TracerNonLocalDiagnosticsEnable", + this->TracerNonLocalDiagnosticsEnable); + if (!TracerDiagErr.isSuccess()) { + TracerDiagErr.reset(); + this->TracerNonLocalDiagnosticsEnable = true; + } } } @@ -395,10 +424,16 @@ void Tendencies::defineFields() { std::string PseudoThicknessTendFieldName = "PseudoThicknessTend"; std::string NormalVelocityTendFieldName = "NormalVelocityTend"; std::string TracerTendFieldName = "TracerTend"; + std::string SurfaceTracerFluxFieldName = "SurfaceTracerFlux"; + std::string TempNonLocalDiagFieldName = "TempNonLocalTendDiag"; + std::string TempNonLocalColSumFieldName = "TempNonLocalColumnSumDiag"; if (Name != "Default") { PseudoThicknessTendFieldName.append(Name); NormalVelocityTendFieldName.append(Name); TracerTendFieldName.append(Name); + SurfaceTracerFluxFieldName.append(Name); + TempNonLocalDiagFieldName.append(Name); + TempNonLocalColSumFieldName.append(Name); } int NDims = 2; @@ -426,6 +461,31 @@ void Tendencies::defineFields() { "m/s^2", "sea_water_velocity_tendency", -9.99E+10, 9.99E+10, NDims, DimNamesVelocity); + NDims = 2; + std::vector DimNamesSurfaceFlux(NDims); + DimNamesSurfaceFlux[0] = "NTracers"; + DimNamesSurfaceFlux[1] = "NCells"; + auto SurfaceTracerFluxField = + Field::create(SurfaceTracerFluxFieldName, "Surface tracer flux", "1", "", + -9.99E+10, 9.99E+10, NDims, DimNamesSurfaceFlux); + + NDims = 2; + std::vector DimNamesTempDiag(NDims); + DimNamesTempDiag[0] = "NCells"; + DimNamesTempDiag[1] = "NVertLayers"; + auto TempNonLocalDiagField = + Field::create(TempNonLocalDiagFieldName, + "Temperature non-local KPP tendency diagnostic", "1", "", + -9.99E+10, 9.99E+10, NDims, DimNamesTempDiag); + + NDims = 1; + std::vector DimNamesCellOnly(NDims); + DimNamesCellOnly[0] = "NCells"; + auto TempNonLocalColSumField = + Field::create(TempNonLocalColSumFieldName, + "Temperature non-local tendency column-sum diagnostic", + "1", "", -9.99E+10, 9.99E+10, NDims, DimNamesCellOnly); + std::string TendGroupName = "Tendencies"; if (Name != "Default") { TendGroupName.append(Name); @@ -435,10 +495,17 @@ void Tendencies::defineFields() { TendGroup->addField(PseudoThicknessTendFieldName); TendGroup->addField(NormalVelocityTendFieldName); TendGroup->addField(TracerTendFieldName); + TendGroup->addField(SurfaceTracerFluxFieldName); + TendGroup->addField(TempNonLocalDiagFieldName); + TendGroup->addField(TempNonLocalColSumFieldName); PseudoThicknessTendField->attachData(PseudoThicknessTend); NormalVelocityTendField->attachData(NormalVelocityTend); TracerTendField->attachData(TracerTend); + SurfaceTracerFluxField->attachData(SurfaceTracerFlux, false); + TempNonLocalDiagField->attachData(TempNonLocalTendDiag, false); + TempNonLocalColSumField->attachData(TempNonLocalColumnSumDiag, + false); } // end defineFields @@ -461,8 +528,14 @@ Tendencies::Tendencies(const std::string &Name_, ///< [in] Name for tendencies KEGrad(Mesh, VCoord), SSHGrad(Mesh, VCoord), VelocityDiffusion(Mesh, VCoord), VelocityHyperDiff(Mesh, VCoord), SfcStressForcing(Mesh, VCoord), ExplicitBottomDrag(Mesh, VCoord), + SfcThicknessForcing(Mesh, VCoord), + SfcTracerForcing(Mesh, VCoord, Tracers::IndxTemp, Tracers::IndxSalt, + EqState), TracerDiffusion(Mesh, VCoord), TracerHyperDiff(Mesh, VCoord), TracerHorzAdv(Mesh, VCoord), SurfaceTracerRestoring(Mesh), + PotentialDensityCalc(Mesh, VCoord), + KPPSurfaceForcing(Mesh, VCoord, Tracers::IndxTemp, Tracers::IndxSalt, + EqState), CustomThicknessTend(InCustomThicknessTend), CustomVelocityTend(InCustomVelocityTend), EqState(EqState), PGrad(PGrad), VMix(VMix) { @@ -474,6 +547,30 @@ Tendencies::Tendencies(const std::string &Name_, ///< [in] Name for tendencies Array2DReal("NormalVelocityTend", Mesh->NEdgesSize, VCoord->NVertLayers); TracerTend = Array3DReal("TracerTend", NTracersIn, Mesh->NCellsSize, VCoord->NVertLayers); + SurfaceTracerFlux = + Array2DReal("SurfaceTracerFlux", NTracersIn, Mesh->NCellsAll); + TempNonLocalTendDiag = Array2DReal("TempNonLocalTendDiag", Mesh->NCellsSize, + VCoord->NVertLayers); + TempNonLocalColumnSumDiag = + Array1DReal("TempNonLocalColumnSumDiag", Mesh->NCellsSize); + deepCopy(SurfaceTracerFlux, 0.0_Real); + deepCopy(TempNonLocalTendDiag, 0.0_Real); + deepCopy(TempNonLocalColumnSumDiag, 0.0_Real); + + // KPP scratch. Extents must match the KPPMix members these are copied to + // and from, so all cell-indexed arrays use NCellsSize. + KPPConservTemp = + Array2DReal("KPP-ConservTemp", Mesh->NCellsSize, VCoord->NVertLayers); + KPPAbsSalinity = + Array2DReal("KPP-AbsSalinity", Mesh->NCellsSize, VCoord->NVertLayers); + KPPSurfacePressure = Array1DReal("KPP-SurfacePressure", Mesh->NCellsSize); + KPPPotentialDensity = Array2DReal("KPP-PotentialDensity", Mesh->NCellsSize, + VCoord->NVertLayers); + KPPRefPressure = Array2DReal("KPP-PotentialDensityPressure", + Mesh->NCellsSize, VCoord->NVertLayers); + KPPTangentialVelEdge = Array2DReal("KPP-TangentialVelEdge", Mesh->NEdgesSize, + VCoord->NVertLayers); + KPPIceFraction = Array1DReal("KPP-IceFraction", Mesh->NCellsSize); Name = Name_; @@ -510,6 +607,7 @@ void Tendencies::computePseudoThicknessTendenciesOnly( OMEGA_SCOPE(LocPseudoThicknessTend, PseudoThicknessTend); OMEGA_SCOPE(LocThicknessFluxDiv, PseudoThicknessFluxDiv); + OMEGA_SCOPE(LocSfcThicknessForcing, SfcThicknessForcing); OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); @@ -553,6 +651,32 @@ void Tendencies::computePseudoThicknessTendenciesOnly( VAdv->computePseudoThicknessVAdvTend(PseudoThicknessTend); Pacer::stop("Tend:computePseudoThicknessVAdvTend", 2); + if (LocSfcThicknessForcing.Enabled) { + Pacer::start("Tend:sfcThicknessForcing", 2); + const auto *ForcingState = Forcing::getDefault(); + + const auto &SnowFlux = ForcingState->TracerForcing.SnowFluxCell; + const auto &RainFlux = ForcingState->TracerForcing.RainFluxCell; + const auto &EvaporationFlux = + ForcingState->TracerForcing.EvaporationFluxCell; + const auto &SeaIceFreshWaterFlux = + ForcingState->TracerForcing.SeaIceFreshWaterFluxCell; + const auto &IceRunoffFlux = ForcingState->TracerForcing.IceRunoffFluxCell; + const auto &RiverRunoffFlux = + ForcingState->TracerForcing.RiverRunoffFluxCell; + const auto &SeaIceSaltFlux = + ForcingState->TracerForcing.SeaIceSaltFluxCell; + + parallelFor( + {Mesh->NCellsAll}, KOKKOS_LAMBDA(int ICell) { + LocSfcThicknessForcing(LocPseudoThicknessTend, ICell, SnowFlux, + RainFlux, EvaporationFlux, + SeaIceFreshWaterFlux, IceRunoffFlux, + RiverRunoffFlux, SeaIceSaltFlux); + }); + Pacer::stop("Tend:sfcThicknessForcing", 2); + } + if (CustomThicknessTend) { Pacer::start("Tend:customThicknessTend", 2); CustomThicknessTend(LocPseudoThicknessTend, State, AuxState, @@ -772,10 +896,20 @@ void Tendencies::computeTracerTendenciesOnly( OMEGA_SCOPE(LocTracerDiffusion, TracerDiffusion); OMEGA_SCOPE(LocTracerHyperDiff, TracerHyperDiff); OMEGA_SCOPE(LocSurfaceTracerRestoring, SurfaceTracerRestoring); + OMEGA_SCOPE(LocSurfaceTracerFlux, SurfaceTracerFlux); + OMEGA_SCOPE(LocSfcTracerForcing, SfcTracerForcing); OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); OMEGA_SCOPE(MinLayerEdgeBot, VCoord->MinLayerEdgeBot); OMEGA_SCOPE(MaxLayerEdgeTop, VCoord->MaxLayerEdgeTop); + OMEGA_SCOPE(LocTempNonLocalTendDiag, TempNonLocalTendDiag); + OMEGA_SCOPE(LocTempNonLocalColumnSumDiag, TempNonLocalColumnSumDiag); + const bool LocTracerNonLocalDiagnosticsEnable = + TracerNonLocalDiagnosticsEnable; + I4 TempTracerIndex = -1; + const bool LocHasTempTracer = + (Tracers::getIndex(TempTracerIndex, "Temperature") == 0); + const I4 LocTempTracerIndex = TempTracerIndex; Pacer::start("Tend:computeTracerTendenciesOnly", 1); @@ -789,6 +923,21 @@ void Tendencies::computeTracerTendenciesOnly( INNER_LAMBDA(int K) { LocTracerTend(L, ICell, K) = 0; }); }); + if (LocTracerNonLocalDiagnosticsEnable) { + parallelForOuter( + {Mesh->NCellsAll}, KOKKOS_LAMBDA(int ICell, const TeamMember &Team) { + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + parallelForInner( + Team, Range{KMin, KMax}, INNER_LAMBDA(int K) { + LocTempNonLocalTendDiag(ICell, K) = 0.0_Real; + }); + Kokkos::single(Kokkos::PerTeam(Team), [&]() { + LocTempNonLocalColumnSumDiag(ICell) = 0.0_Real; + }); + }); + } + // compute tracer horizotal advection Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel); const Array2DReal &FluxPseudoThickEdge = @@ -893,6 +1042,101 @@ void Tendencies::computeTracerTendenciesOnly( Pacer::stop("Tend:surfaceTracerRestoring", 2); } + // Compute KPP non-local tracer tendency + if (TracerNonLocalFluxEnabled) { + KPPMix *KPPInstance = KPPMix::getInstance(); + if (KPPInstance && KPPInstance->Enabled) { + Pacer::start("Tend:tracerNonLocalFlux", 2); + OMEGA_SCOPE(LocNonLocalFlux, KPPInstance->VertNonLocalFlux); + parallelForOuter( + {NTracers, Mesh->NCellsAll}, + KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) { + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRangeChunked(KMin, KMax); + parallelForInner( + Team, KRange, INNER_LAMBDA(int KChunk) { + const I4 KStart = chunkStart(KChunk, KMin); + const I4 KLen = chunkLength(KChunk, KStart, KMax); + for (int KVec = 0; KVec < KLen; ++KVec) { + const I4 K = KStart + KVec; + const Real NonLocalTend = + LocSurfaceTracerFlux(L, ICell) * + (LocNonLocalFlux(ICell, K) - + LocNonLocalFlux(ICell, K + 1)); + LocTracerTend(L, ICell, K) += NonLocalTend; + + if (LocTracerNonLocalDiagnosticsEnable && + LocHasTempTracer && L == LocTempTracerIndex) { + LocTempNonLocalTendDiag(ICell, K) += NonLocalTend; + } + } + }); + }); + + if (LocTracerNonLocalDiagnosticsEnable && LocHasTempTracer) { + parallelForOuter( + {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team) { + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + Real Sum = 0.0_Real; + parallelReduceInner( + Team, Range{KMin, KMax}, + [&](int K, Real &LocalSum) { + const Real NonLocalTend = + LocSurfaceTracerFlux(LocTempTracerIndex, ICell) * + (LocNonLocalFlux(ICell, K) - + LocNonLocalFlux(ICell, K + 1)); + LocalSum += NonLocalTend; + }, + Sum); + Kokkos::single(Kokkos::PerTeam(Team), [&]() { + LocTempNonLocalColumnSumDiag(ICell) = Sum; + }); + }); + } + + Pacer::stop("Tend:tracerNonLocalFlux", 2); + } + } + + // compute tracer forcing tendency + if (LocSfcTracerForcing.Enabled) { + Pacer::start("Tend:sfcTracerForcing", 2); + const auto *ForcingState = Forcing::getDefault(); + const auto &LatentHeatFlux = + ForcingState->TracerForcing.LatentHeatFluxCell; + const auto &SensibleHeatFlux = + ForcingState->TracerForcing.SensibleHeatFluxCell; + const auto &LongWaveHeatFluxUp = + ForcingState->TracerForcing.LongWaveHeatFluxUpCell; + const auto &LongWaveHeatFluxDown = + ForcingState->TracerForcing.LongWaveHeatFluxDownCell; + const auto &SeaIceHeatFlux = + ForcingState->TracerForcing.SeaIceHeatFluxCell; + const auto &ShortWaveHeatFlux = + ForcingState->TracerForcing.ShortWaveHeatFluxCell; + const auto &SnowFlux = ForcingState->TracerForcing.SnowFluxCell; + const auto &RainFlux = ForcingState->TracerForcing.RainFluxCell; + const auto &IceRunoffFlux = ForcingState->TracerForcing.IceRunoffFluxCell; + const auto &RiverRunoffFlux = + ForcingState->TracerForcing.RiverRunoffFluxCell; + const auto &SeaIceSaltFlux = + ForcingState->TracerForcing.SeaIceSaltFluxCell; + const auto &PressureMid = VCoord->PressureMid; + + parallelFor( + {Mesh->NCellsAll}, KOKKOS_LAMBDA(int ICell) { + LocSfcTracerForcing( + LocTracerTend, ICell, TracerArray, PressureMid, LatentHeatFlux, + SensibleHeatFlux, LongWaveHeatFluxUp, LongWaveHeatFluxDown, + SeaIceHeatFlux, ShortWaveHeatFlux, SnowFlux, RainFlux, + IceRunoffFlux, RiverRunoffFlux, SeaIceSaltFlux); + }); + Pacer::stop("Tend:sfcTracerForcing", 2); + } + Pacer::stop("Tend:computeTracerTendenciesOnly", 1); } // end tracer tendency compute @@ -1023,6 +1267,163 @@ void Tendencies::computeAllTendencies( VelTimeLevel, Time); } // end all tendency compute +//------------------------------------------------------------------------------ +// Set surface tracer flux for use by KPP non-local tracer tendency +void Tendencies::setSurfaceTracerFlux(const Array2DReal &Flux) { + OMEGA_REQUIRE(Flux.extent(0) == SurfaceTracerFlux.extent(0), + "Tendencies::setSurfaceTracerFlux: tracer dimension mismatch"); + OMEGA_REQUIRE(Flux.extent(1) == SurfaceTracerFlux.extent(1), + "Tendencies::setSurfaceTracerFlux: cell dimension mismatch"); + Kokkos::deep_copy(SurfaceTracerFlux, Flux); +} + +//------------------------------------------------------------------------------ +// Prepare KPP state for the current time step. Final VertDiff/VertVisc +// coefficient assembly is owned by VertMix::computeVertMix. +void Tendencies::computeKPPFields(const OceanState *State, + const Array3DReal &TracerArray, + int ThickTimeLevel, int VelTimeLevel) { + KPPMix *KPPInstance = KPPMix::getInstance(); + + if (!EqState || !KPPInstance || !KPPInstance->Enabled) + return; + + Pacer::start("Tend:computeKPPFields", 1); + + I4 TempIdx = -1; + I4 SaltIdx = -1; + if (Tracers::getIndex(TempIdx, "Temperature") != 0 || + Tracers::getIndex(SaltIdx, "Salinity") != 0) { + LOG_WARN("Tendencies::computeKPPFields: Temperature/Salinity " + "tracers not found, skipping KPP update"); + Pacer::stop("Tend:computeKPPFields", 1); + return; + } + + const I4 NCellsAll = Mesh->NCellsAll; + const I4 NVertLayers = VCoord->NVertLayers; + + OMEGA_SCOPE(ConservTemp, KPPConservTemp); + OMEGA_SCOPE(AbsSalinity, KPPAbsSalinity); + parallelFor( + "KPP-ExtractTS", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + ConservTemp(ICell, K) = TracerArray(TempIdx, ICell, K); + AbsSalinity(ICell, K) = TracerArray(SaltIdx, ICell, K); + }); + + Array2DReal LayerThickCell = State->getPseudoThickness(ThickTimeLevel); + Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel); + + deepCopy(KPPSurfacePressure, 1.0e5_Real); + const_cast(VCoord)->computePressure(LayerThickCell, + KPPSurfacePressure); + + OMEGA_SCOPE(PressureMid, VCoord->PressureMid); + + EqState->computeSpecVol(ConservTemp, AbsSalinity, PressureMid); + EqState->computeBruntVaisalaFreqSq(ConservTemp, AbsSalinity, PressureMid, + EqState->SpecVol); + + // Potential density referenced to each column's surface pressure + OMEGA_SCOPE(LocPotentialDensityCalc, PotentialDensityCalc); + OMEGA_SCOPE(RefPressure, KPPRefPressure); + parallelFor( + "KPP-PotentialDensityPressure", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + LocPotentialDensityCalc.computeRefPressure(RefPressure, ICell, K, + PressureMid); + }); + EqState->computeSpecVolDisp(ConservTemp, AbsSalinity, RefPressure, 0); + + OMEGA_SCOPE(SpecVolPotential, EqState->SpecVolDisplaced); + OMEGA_SCOPE(PotentialDensity, KPPPotentialDensity); + parallelFor( + "KPP-PotentialDensity", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + LocPotentialDensityCalc(PotentialDensity, ICell, K, SpecVolPotential); + }); + + { + TangentialReconOnEdge TanReconEdge(Mesh); + OMEGA_SCOPE(LocTangentialVelEdge, KPPTangentialVelEdge); + OMEGA_SCOPE(MinLayerEdgeTop, VCoord->MinLayerEdgeTop); + OMEGA_SCOPE(MaxLayerEdgeBot, VCoord->MaxLayerEdgeBot); + parallelForOuter( + {Mesh->NEdgesAll}, KOKKOS_LAMBDA(int IEdge, const TeamMember &Team) { + const int KMin = MinLayerEdgeTop(IEdge); + const int KMax = MaxLayerEdgeBot(IEdge); + const int KRange = vertRangeChunked(KMin, KMax); + parallelForInner( + Team, KRange, INNER_LAMBDA(int KChunk) { + TanReconEdge(LocTangentialVelEdge, IEdge, KChunk, + NormalVelEdge); + }); + }); + } + + const auto *ForcingState = Forcing::getDefault(); + if (!ForcingState) { + LOG_WARN("Tendencies::computeKPPFields: Forcing has not " + "been initialized, skipping KPP update"); + Pacer::stop("Tend:computeKPPFields", 1); + return; + } + + const auto &SfcStress = ForcingState->SfcStressForcing; + const auto &TracerForcing = ForcingState->TracerForcing; + + KPPSurfaceForcing.UpdateSurfaceTracerFlux = TracerNonLocalFluxEnabled; + KPPSurfaceForcing.UseTracerForcing = SfcTracerForcing.Enabled; + + if (TracerNonLocalFluxEnabled) { + deepCopy(SurfaceTracerFlux, 0.0_Real); + } + deepCopy(KPPInstance->SurfaceBuoyancyFlux, 0.0_Real); + + OMEGA_SCOPE(LocKPPSurfaceForcing, KPPSurfaceForcing); + OMEGA_SCOPE(LocFrictionVelocity, KPPInstance->SurfaceFrictionVelocity); + OMEGA_SCOPE(LocBuoyancyFlux, KPPInstance->SurfaceBuoyancyFlux); + OMEGA_SCOPE(LocSurfaceTracerFlux, SurfaceTracerFlux); + OMEGA_SCOPE(IceFraction, KPPIceFraction); + OMEGA_SCOPE(LocSpecVol, EqState->SpecVol); + OMEGA_SCOPE(ZonalStress, SfcStress.ZonalStressCell); + OMEGA_SCOPE(MeridStress, SfcStress.MeridStressCell); + OMEGA_SCOPE(LatentHeatFlux, TracerForcing.LatentHeatFluxCell); + OMEGA_SCOPE(SensibleHeatFlux, TracerForcing.SensibleHeatFluxCell); + OMEGA_SCOPE(LongWaveHeatFluxUp, TracerForcing.LongWaveHeatFluxUpCell); + OMEGA_SCOPE(LongWaveHeatFluxDown, TracerForcing.LongWaveHeatFluxDownCell); + OMEGA_SCOPE(SeaIceHeatFlux, TracerForcing.SeaIceHeatFluxCell); + OMEGA_SCOPE(ShortWaveHeatFlux, TracerForcing.ShortWaveHeatFluxCell); + OMEGA_SCOPE(SnowFlux, TracerForcing.SnowFluxCell); + OMEGA_SCOPE(RainFlux, TracerForcing.RainFluxCell); + OMEGA_SCOPE(EvaporationFlux, TracerForcing.EvaporationFluxCell); + OMEGA_SCOPE(SeaIceFreshWaterFlux, TracerForcing.SeaIceFreshWaterFluxCell); + OMEGA_SCOPE(IceRunoffFlux, TracerForcing.IceRunoffFluxCell); + OMEGA_SCOPE(RiverRunoffFlux, TracerForcing.RiverRunoffFluxCell); + OMEGA_SCOPE(SeaIceSaltFlux, TracerForcing.SeaIceSaltFluxCell); + + parallelFor( + "KPP-SurfaceForcing", {NCellsAll}, KOKKOS_LAMBDA(I4 ICell) { + LocKPPSurfaceForcing( + LocFrictionVelocity, LocBuoyancyFlux, LocSurfaceTracerFlux, + IceFraction, ICell, ConservTemp, AbsSalinity, PressureMid, + LocSpecVol, ZonalStress, MeridStress, LatentHeatFlux, + SensibleHeatFlux, LongWaveHeatFluxUp, LongWaveHeatFluxDown, + SeaIceHeatFlux, ShortWaveHeatFlux, SnowFlux, RainFlux, + EvaporationFlux, SeaIceFreshWaterFlux, IceRunoffFlux, + RiverRunoffFlux, SeaIceSaltFlux); + }); + + Array1DReal WindSpeed10m; + KPPInstance->computeKPPMix( + PotentialDensity, NormalVelEdge, KPPTangentialVelEdge, + KPPInstance->SurfaceFrictionVelocity, KPPInstance->SurfaceBuoyancyFlux, + EqState->BruntVaisalaFreqSq, IceFraction, WindSpeed10m); + + Pacer::stop("Tend:computeKPPFields", 1); +} + } // end namespace OMEGA //===----------------------------------------------------------------------===// diff --git a/components/omega/src/ocn/Tendencies.h b/components/omega/src/ocn/Tendencies.h index 877ba22faf2b..1962ed2462be 100644 --- a/components/omega/src/ocn/Tendencies.h +++ b/components/omega/src/ocn/Tendencies.h @@ -74,10 +74,38 @@ class Tendencies { VelocityHyperDiffOnEdge VelocityHyperDiff; SfcStressForcingOnEdge SfcStressForcing; BottomDragOnEdge ExplicitBottomDrag; + SfcThicknessForcingOnCell SfcThicknessForcing; + SfcTracerForcingOnCell SfcTracerForcing; TracerHorzAdvOnCell TracerHorzAdv; TracerDiffOnCell TracerDiffusion; TracerHyperDiffOnCell TracerHyperDiff; SurfaceTracerRestoringOnCell SurfaceTracerRestoring; + PotentialDensityOnCell PotentialDensityCalc; + KPPSurfaceForcingOnCell KPPSurfaceForcing; + + // Surface tracer flux used for KPP non-local tracer tendency [NTracers, + // NCellsAll] + Array2DReal SurfaceTracerFlux; + + // Diagnostics for temperature forcing pathways used in KPP comparison. + // These are raw contributions added to TracerTend before tracer update. + Array2DReal TempNonLocalTendDiag; + Array1DReal TempNonLocalColumnSumDiag; + + // Scratch used by computeKPPFields, allocated once rather than per step + Array2DReal KPPConservTemp; + Array2DReal KPPAbsSalinity; + Array1DReal KPPSurfacePressure; + Array2DReal KPPPotentialDensity; + Array2DReal KPPRefPressure; + Array2DReal KPPTangentialVelEdge; + Array1DReal KPPIceFraction; + + // Enables explicit non-local tracer tendency from KPP + bool TracerNonLocalFluxEnabled = false; + + // Enable diagnostics that isolate temperature non-local terms. + bool TracerNonLocalDiagnosticsEnable = true; std::string Name; @@ -118,6 +146,14 @@ class Tendencies { int ThickTimeLevel, int VelTimeLevel, TimeInstant Time); + void setSurfaceTracerFlux(const Array2DReal &Flux); + + // Computes KPP boundary layer depth, coefficients and non-local flux. + // Called once per time step by the active time stepper. + void computeKPPFields(const OceanState *State, + const Array3DReal &TracerArray, int ThickTimeLevel, + int VelTimeLevel); + // Create a non-default group of tendencies template static Tendencies *create(const std::string &Name, ArgTypes &&...Args) { diff --git a/components/omega/src/ocn/TendencyTerms.cpp b/components/omega/src/ocn/TendencyTerms.cpp index 5b142888142c..747604b42441 100644 --- a/components/omega/src/ocn/TendencyTerms.cpp +++ b/components/omega/src/ocn/TendencyTerms.cpp @@ -11,6 +11,7 @@ #include "TendencyTerms.h" #include "AuxiliaryState.h" #include "DataTypes.h" +#include "Eos.h" #include "HorzMesh.h" #include "HorzOperators.h" #include "OceanState.h" @@ -71,6 +72,34 @@ BottomDragOnEdge::BottomDragOnEdge(const HorzMesh *Mesh, NVertLayers(VCoord->NVertLayers), EdgeMask(VCoord->EdgeMask), MaxLayerEdgeTop(VCoord->MaxLayerEdgeTop) {} +SfcThicknessForcingOnCell::SfcThicknessForcingOnCell(const HorzMesh *Mesh, + const VertCoord *VCoord) + : MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell) {} + +SfcTracerForcingOnCell::SfcTracerForcingOnCell(const HorzMesh *Mesh, + const VertCoord *VCoord, + I4 TempTracerIndex, + I4 SaltTracerIndex, + const Eos *EosInst) + : TempIndex(TempTracerIndex), SaltIndex(SaltTracerIndex), + MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell), + EosChoice(EosInst->EosChoice) {} + +PotentialDensityOnCell::PotentialDensityOnCell(const HorzMesh *Mesh, + const VertCoord *VCoord) + : MinLayerCell(VCoord->MinLayerCell) {} + +KPPSurfaceForcingOnCell::KPPSurfaceForcingOnCell(const HorzMesh *Mesh, + const VertCoord *VCoord, + I4 TempTracerIndex, + I4 SaltTracerIndex, + const Eos *EosInst) + : TempIndex(TempTracerIndex), SaltIndex(SaltTracerIndex), + LinearDRhodT(EosInst->getLinearDRhodT()), + LinearDRhodS(EosInst->getLinearDRhodS()), + MinLayerCell(VCoord->MinLayerCell), EosChoice(EosInst->EosChoice), + Teos10Coeff(VCoord) {} + TracerHorzAdvOnCell::TracerHorzAdvOnCell(const HorzMesh *Mesh, const VertCoord *VCoord) : HorzontalMesh(Mesh), VerticalCoord(VCoord), diff --git a/components/omega/src/ocn/TendencyTerms.h b/components/omega/src/ocn/TendencyTerms.h index 548832b290bf..3131b601107e 100644 --- a/components/omega/src/ocn/TendencyTerms.h +++ b/components/omega/src/ocn/TendencyTerms.h @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// #include "AuxiliaryState.h" +#include "Eos.h" #include "GlobalConstants.h" #include "HorzMesh.h" #include "MachEnv.h" @@ -21,6 +22,50 @@ namespace OMEGA { +//------------------------------------------------------------------------------ +// Shared surface flux helpers +// +// Used by the thickness, tracer, and KPP surface forcing functors so the same +// flux definitions are not restated in each. +//------------------------------------------------------------------------------ + +/// Net surface freshwater mass flux (kg/m^2/s) +KOKKOS_INLINE_FUNCTION Real sfcFreshWaterFlux( + I4 ICell, const Array1DReal &SnowFlux, const Array1DReal &RainFlux, + const Array1DReal &EvaporationFlux, const Array1DReal &SeaIceFreshWaterFlux, + const Array1DReal &IceRunoffFlux, const Array1DReal &RiverRunoffFlux) { + return SnowFlux(ICell) + RainFlux(ICell) + EvaporationFlux(ICell) + + SeaIceFreshWaterFlux(ICell) + IceRunoffFlux(ICell) + + RiverRunoffFlux(ICell); +} + +/// Direct surface heat flux (radiative and turbulent), excluding any enthalpy +/// carried by surface mass fluxes +KOKKOS_INLINE_FUNCTION Real sfcDirectHeatFlux( + I4 ICell, const Array1DReal &LatentHeatFlux, + const Array1DReal &SensibleHeatFlux, const Array1DReal &LongWaveHeatFluxUp, + const Array1DReal &LongWaveHeatFluxDown, const Array1DReal &SeaIceHeatFlux, + const Array1DReal &ShortWaveHeatFlux) { + return LatentHeatFlux(ICell) + SensibleHeatFlux(ICell) + + LongWaveHeatFluxUp(ICell) + LongWaveHeatFluxDown(ICell) + + SeaIceHeatFlux(ICell) + ShortWaveHeatFlux(ICell); +} + +/// Enthalpy carried into the ocean by surface mass fluxes. +/// Liquid mass fluxes (rain, rivers) enter at the local SST. Solid mass fluxes +/// (snow, frozen runoff) are melted locally by the ocean, so they enter at the +/// freezing point less a constant latent heat of fusion. +/// +/// @param CtTop Conservative temperature of the top layer +/// @param CtFrz Freezing conservative temperature at the top layer +KOKKOS_INLINE_FUNCTION Real sfcMassFluxEnthalpy( + I4 ICell, Real CtTop, Real CtFrz, const Array1DReal &SnowFlux, + const Array1DReal &RainFlux, const Array1DReal &IceRunoffFlux, + const Array1DReal &RiverRunoffFlux) { + return (RainFlux(ICell) + RiverRunoffFlux(ICell)) * Cp0Sw * CtTop + + (SnowFlux(ICell) + IceRunoffFlux(ICell)) * (Cp0Sw * CtFrz - LatIce); +} + /// Divergence of pseudo-thickness flux at cell centers, for updating /// pseudo-thickness arrays class PseudoThicknessFluxDivOnCell { @@ -372,6 +417,227 @@ class BottomDragOnEdge { Array1DI4 MaxLayerEdgeTop; }; +/// Coupled freshwater flux forcing for thickness equation. +class SfcThicknessForcingOnCell { + public: + bool Enabled = false; + + SfcThicknessForcingOnCell(const HorzMesh *Mesh, const VertCoord *VCoord); + + KOKKOS_FUNCTION void operator()(const Array2DReal &Tend, I4 ICell, + const Array1DReal &SnowFlux, + const Array1DReal &RainFlux, + const Array1DReal &EvaporationFlux, + const Array1DReal &SeaIceFreshWaterFlux, + const Array1DReal &IceRunoffFlux, + const Array1DReal &RiverRunoffFlux, + const Array1DReal &SeaIceSaltFlux) const { + + const I4 KTop = MinLayerCell(ICell); + if (KTop > MaxLayerCell(ICell)) { + return; + } + + const Real FreshWaterFlux = sfcFreshWaterFlux( + ICell, SnowFlux, RainFlux, EvaporationFlux, SeaIceFreshWaterFlux, + IceRunoffFlux, RiverRunoffFlux); + + Tend(ICell, KTop) += (FreshWaterFlux + SeaIceSaltFlux(ICell)) / RhoSw; + } + + private: + Array1DI4 MinLayerCell; + Array1DI4 MaxLayerCell; +}; + +/// Coupled surface flux forcing for active tracers. +class SfcTracerForcingOnCell { + public: + bool Enabled = false; + + SfcTracerForcingOnCell(const HorzMesh *Mesh, const VertCoord *VCoord, + I4 TempTracerIndex, I4 SaltTracerIndex, + const Eos *EosInst); + + KOKKOS_FUNCTION void + operator()(const Array3DReal &Tend, I4 ICell, const Array3DReal &TracerCell, + const Array2DReal &PressureMid, const Array1DReal &LatentHeatFlux, + const Array1DReal &SensibleHeatFlux, + const Array1DReal &LongWaveHeatFluxUp, + const Array1DReal &LongWaveHeatFluxDown, + const Array1DReal &SeaIceHeatFlux, + const Array1DReal &ShortWaveHeatFlux, const Array1DReal &SnowFlux, + const Array1DReal &RainFlux, const Array1DReal &IceRunoffFlux, + const Array1DReal &RiverRunoffFlux, + const Array1DReal &SeaIceSaltFlux) const { + + const I4 KTop = MinLayerCell(ICell); + if (KTop > MaxLayerCell(ICell)) { + return; + } + + if (TempIndex >= 0) { + const Real PTopDb = PressureMid(ICell, KTop) * Pa2Db; + const Real SaTop = SaltIndex >= 0 + ? TracerCell(SaltIndex, ICell, KTop) + : 0.0_Real; // not sure we want zero here? + const Real CtFrz = + Eos::calcCtFreezing(EosChoice, SaTop, PTopDb, 0.0_Real); + const Real CtTop = TracerCell(TempIndex, ICell, KTop); + + // Heat tendencies are due to direct heat fluxes + enthalpy fluxes + const Real HeatFlux = + sfcDirectHeatFlux(ICell, LatentHeatFlux, SensibleHeatFlux, + LongWaveHeatFluxUp, LongWaveHeatFluxDown, + SeaIceHeatFlux, ShortWaveHeatFlux) + + sfcMassFluxEnthalpy(ICell, CtTop, CtFrz, SnowFlux, RainFlux, + IceRunoffFlux, RiverRunoffFlux); + + Tend(TempIndex, ICell, KTop) += HeatFlux * HFluxFac; + } + + if (SaltIndex >= 0) { + Tend(SaltIndex, ICell, KTop) += SeaIceSaltFlux(ICell) * SFluxFac; + } + } + + private: + I4 TempIndex; + I4 SaltIndex; + Array1DI4 MinLayerCell; + Array1DI4 MaxLayerCell; + EosType EosChoice; +}; + +/// Potential density referenced to the surface, used by the KPP boundary +/// layer depth search. +class PotentialDensityOnCell { + public: + bool Enabled = false; + + PotentialDensityOnCell(const HorzMesh *Mesh, const VertCoord *VCoord); + + /// Fills the reference pressure used for the displaced specific volume: + /// every layer in a column is referenced to that column's surface pressure + KOKKOS_FUNCTION void + computeRefPressure(const Array2DReal &RefPressure, I4 ICell, I4 K, + const Array2DReal &PressureMid) const { + RefPressure(ICell, K) = PressureMid(ICell, MinLayerCell(ICell)); + } + + /// Inverts the surface-referenced specific volume to give potential density + KOKKOS_FUNCTION void operator()(const Array2DReal &PotentialDensity, + I4 ICell, I4 K, + const Array2DReal &SpecVolDisplaced) const { + PotentialDensity(ICell, K) = + 1.0_Real / Kokkos::max(1.0e-12_Real, SpecVolDisplaced(ICell, K)); + } + + private: + Array1DI4 MinLayerCell; +}; + +/// Surface forcing inputs consumed by the KPP boundary layer scheme: friction +/// velocity, buoyancy flux, and the surface tracer fluxes that scale the +/// non-local term. +class KPPSurfaceForcingOnCell { + public: + bool Enabled = false; + + /// Store surface tracer fluxes for the KPP non-local tracer tendency + bool UpdateSurfaceTracerFlux = false; + + /// When false no coupled tracer forcing is active, so only the friction + /// velocity is set and the buoyancy and tracer fluxes stay zero + bool UseTracerForcing = false; + + KPPSurfaceForcingOnCell(const HorzMesh *Mesh, const VertCoord *VCoord, + I4 TempTracerIndex, I4 SaltTracerIndex, + const Eos *EosInst); + + KOKKOS_FUNCTION void operator()( + const Array1DReal &FrictionVelocity, const Array1DReal &BuoyancyFlux, + const Array2DReal &SurfaceTracerFlux, const Array1DReal &IceFraction, + I4 ICell, const Array2DReal &ConservTemp, const Array2DReal &AbsSalinity, + const Array2DReal &PressureMid, const Array2DReal &SpecVol, + const Array1DReal &ZonalStress, const Array1DReal &MeridStress, + const Array1DReal &LatentHeatFlux, const Array1DReal &SensibleHeatFlux, + const Array1DReal &LongWaveHeatFluxUp, + const Array1DReal &LongWaveHeatFluxDown, + const Array1DReal &SeaIceHeatFlux, const Array1DReal &ShortWaveHeatFlux, + const Array1DReal &SnowFlux, const Array1DReal &RainFlux, + const Array1DReal &EvaporationFlux, + const Array1DReal &SeaIceFreshWaterFlux, + const Array1DReal &IceRunoffFlux, const Array1DReal &RiverRunoffFlux, + const Array1DReal &SeaIceSaltFlux) const { + + const Real TauX = ZonalStress(ICell); + const Real TauY = MeridStress(ICell); + const Real TauMag = Kokkos::sqrt(TauX * TauX + TauY * TauY); + + FrictionVelocity(ICell) = + Kokkos::sqrt(Kokkos::max(0.0_Real, TauMag / RhoSw)); + BuoyancyFlux(ICell) = 0.0_Real; + if (UpdateSurfaceTracerFlux) { + SurfaceTracerFlux(TempIndex, ICell) = 0.0_Real; + SurfaceTracerFlux(SaltIndex, ICell) = 0.0_Real; + } + // Sea ice coupling is not wired in yet, so KPP sees an ice-free ocean + IceFraction(ICell) = 0.0_Real; + + if (!UseTracerForcing) { + return; + } + + const I4 KTop = MinLayerCell(ICell); + const Real SaTop = AbsSalinity(ICell, KTop); + const Real CtTop = ConservTemp(ICell, KTop); + const Real PTopDb = PressureMid(ICell, KTop) * Pa2Db; + const Real CtFrz = + Eos::calcCtFreezing(EosChoice, SaTop, PTopDb, 0.0_Real); + + // Mirrors the enthalpy treatment in SfcTracerForcingOnCell + const Real HeatFlux = sfcDirectHeatFlux( + ICell, LatentHeatFlux, SensibleHeatFlux, LongWaveHeatFluxUp, + LongWaveHeatFluxDown, SeaIceHeatFlux, ShortWaveHeatFlux); + const Real FreshWaterFlux = sfcFreshWaterFlux( + ICell, SnowFlux, RainFlux, EvaporationFlux, SeaIceFreshWaterFlux, + IceRunoffFlux, RiverRunoffFlux); + + const Real TempFlux = HeatFlux * HFluxFac; + const Real SaltFlux = + SeaIceSaltFlux(ICell) / RhoSw - FreshWaterFlux * SaTop / RhoSw; + + const Real SpVol = Kokkos::max(1.0e-12_Real, SpecVol(ICell, KTop)); + const Real RhoTop = 1.0_Real / SpVol; + + Real Alpha = 0.0_Real; + Real Beta = 0.0_Real; + if (EosChoice == EosType::Teos10Eos) { + Alpha = Teos10Coeff.calcAlpha(SaTop, CtTop, PTopDb, SpVol); + Beta = Teos10Coeff.calcBeta(SaTop, CtTop, PTopDb, SpVol); + } else if (EosChoice == EosType::LinearEos) { + Alpha = -LinearDRhodT / RhoTop; + Beta = LinearDRhodS / RhoTop; + } + + BuoyancyFlux(ICell) = Gravity * (Alpha * TempFlux - Beta * SaltFlux); + if (UpdateSurfaceTracerFlux) { + SurfaceTracerFlux(TempIndex, ICell) = TempFlux; + SurfaceTracerFlux(SaltIndex, ICell) = SaltFlux; + } + } + + private: + I4 TempIndex; + I4 SaltIndex; + Real LinearDRhodT; + Real LinearDRhodS; + Array1DI4 MinLayerCell; + EosType EosChoice; + Teos10BruntVaisalaFreqSq Teos10Coeff; +}; + // Tracer horizontal advection term class TracerHorzAdvOnCell { public: diff --git a/components/omega/src/ocn/VertMix.cpp b/components/omega/src/ocn/VertMix.cpp index 45431292ed16..46faf652fdc5 100644 --- a/components/omega/src/ocn/VertMix.cpp +++ b/components/omega/src/ocn/VertMix.cpp @@ -16,6 +16,7 @@ #include "GlobalConstants.h" #include "HorzMesh.h" #include "HorzOperators.h" +#include "KPPMix.h" #include "TimeStepper.h" #include "TriDiagSolvers.h" @@ -224,6 +225,17 @@ void VertMix::computeVertMix(const Array2DReal &NormalVelocity, OMEGA_SCOPE(LocBackVisc, BackVisc); OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + const Real LocConvDiff = LocComputeVertMixConv.ConvDiff; + const Real LocConvTriggerBVF = LocComputeVertMixConv.ConvTriggerBVF; + Array1DI4 KPPBoundaryLayerIndex("VertMix-KPPBoundaryLayerIndex", + Mesh->NCellsSize); + deepCopy(KPPBoundaryLayerIndex, -1); + KPPMix *KPPInstance = KPPMix::getInstance(); + const bool LocKPPEnabled = (KPPInstance && KPPInstance->Enabled); + if (LocKPPEnabled) { + deepCopy(KPPBoundaryLayerIndex, KPPInstance->IndexBoundaryLayerDepth); + } + OMEGA_SCOPE(LocKPPBoundaryLayerIndex, KPPBoundaryLayerIndex); /// First, initialize VertDiff and VertVisc to background values parallelForOuter( @@ -311,7 +323,34 @@ void VertMix::computeVertMix(const Array2DReal &NormalVelocity, }); }); } - /// Third, compute convective mixing if enabled + + /// Third, apply KPP mixing if enabled + if (LocKPPEnabled) { + const I4 NVertLayers = VCoord->NVertLayers; + I4 KPPMergeMode = 0; // 0=additive profile, 1=matched coefficients + if (KPPInstance->MatchTechnique == KPPMatchType::MatchBoth) { + KPPMergeMode = 1; + } + + OMEGA_SCOPE(LocKPPVertDiff, KPPInstance->VertDiff); + OMEGA_SCOPE(LocKPPVertVisc, KPPInstance->VertVisc); + + parallelFor( + "VertMix-KPP", {Mesh->NCellsAll, NVertLayers + 1}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + if (K <= LocKPPBoundaryLayerIndex(ICell) + 1) { + if (KPPMergeMode == 1) { + LocVertDiff(ICell, K) = LocKPPVertDiff(ICell, K); + LocVertVisc(ICell, K) = LocKPPVertVisc(ICell, K); + } else { + LocVertDiff(ICell, K) += LocKPPVertDiff(ICell, K); + LocVertVisc(ICell, K) += LocKPPVertVisc(ICell, K); + } + } + }); + } + + /// Fourth, compute convective mixing if enabled if (LocComputeVertMixConv.Enabled) { parallelForOuter( "VertMix-Conv", {Mesh->NCellsAll}, @@ -322,8 +361,19 @@ void VertMix::computeVertMix(const Array2DReal &NormalVelocity, parallelForInner( Team, KRange, INNER_LAMBDA(int KChunk) { - LocComputeVertMixConv(LocVertDiff, LocVertVisc, ICell, - KChunk, BruntVaisalaFreqSq); + const I4 KStart = chunkStart(KChunk, KMin); + const I4 KLen = chunkLength(KChunk, KStart, KMax); + for (int KVec = 0; KVec < KLen; ++KVec) { + const I4 K = KStart + KVec; + const bool ApplyConv = + (!LocKPPEnabled) || + (K > LocKPPBoundaryLayerIndex(ICell) + 1); + if (ApplyConv && + BruntVaisalaFreqSq(ICell, K) < LocConvTriggerBVF) { + LocVertDiff(ICell, K) += LocConvDiff; + LocVertVisc(ICell, K) += LocConvDiff; + } + } }); }); } diff --git a/components/omega/src/ocn/forcingVars/TracerForcingVars.cpp b/components/omega/src/ocn/forcingVars/TracerForcingVars.cpp new file mode 100644 index 000000000000..a6478bb612c4 --- /dev/null +++ b/components/omega/src/ocn/forcingVars/TracerForcingVars.cpp @@ -0,0 +1,155 @@ +#include "TracerForcingVars.h" +#include "Eos.h" +#include "Field.h" +#include "Tracers.h" +#include "VertCoord.h" + +#include + +namespace OMEGA { + +TracerForcingVars::TracerForcingVars(const std::string &Suffix, + const HorzMesh *Mesh) + : SnowFluxCell("SnowFlux" + Suffix, Mesh->NCellsSize), + RainFluxCell("RainFlux" + Suffix, Mesh->NCellsSize), + EvaporationFluxCell("EvaporationFlux" + Suffix, Mesh->NCellsSize), + SeaIceFreshWaterFluxCell("SeaIceFreshWaterFlux" + Suffix, + Mesh->NCellsSize), + IceRunoffFluxCell("IceRunoffFlux" + Suffix, Mesh->NCellsSize), + RiverRunoffFluxCell("RiverRunoffFlux" + Suffix, Mesh->NCellsSize), + LatentHeatFluxCell("LatentHeatFlux" + Suffix, Mesh->NCellsSize), + SensibleHeatFluxCell("SensibleHeatFlux" + Suffix, Mesh->NCellsSize), + LongWaveHeatFluxUpCell("LongWaveHeatFluxUp" + Suffix, Mesh->NCellsSize), + LongWaveHeatFluxDownCell("LongWaveHeatFluxDown" + Suffix, + Mesh->NCellsSize), + SeaIceHeatFluxCell("SeaIceHeatFlux" + Suffix, Mesh->NCellsSize), + ShortWaveHeatFluxCell("ShortWaveHeatFlux" + Suffix, Mesh->NCellsSize), + SeaIceSaltFluxCell("SeaIceSaltFlux" + Suffix, Mesh->NCellsSize) { + deepCopy(SnowFluxCell, 0.0_Real); + deepCopy(RainFluxCell, 0.0_Real); + deepCopy(EvaporationFluxCell, 0.0_Real); + deepCopy(SeaIceFreshWaterFluxCell, 0.0_Real); + deepCopy(IceRunoffFluxCell, 0.0_Real); + deepCopy(RiverRunoffFluxCell, 0.0_Real); + deepCopy(LatentHeatFluxCell, 0.0_Real); + deepCopy(SensibleHeatFluxCell, 0.0_Real); + deepCopy(LongWaveHeatFluxUpCell, 0.0_Real); + deepCopy(LongWaveHeatFluxDownCell, 0.0_Real); + deepCopy(SeaIceHeatFluxCell, 0.0_Real); + deepCopy(ShortWaveHeatFluxCell, 0.0_Real); + deepCopy(SeaIceSaltFluxCell, 0.0_Real); +} + +void TracerForcingVars::registerFields(const std::string &MeshName) const { + const int NDims = 1; + std::vector DimNames(NDims); + + std::string DimSuffix; + if (MeshName == "Default") { + DimSuffix = ""; + } else { + DimSuffix = MeshName; + } + + DimNames[0] = "NCells" + DimSuffix; + + auto SnowFluxField = + Field::create(SnowFluxCell.label(), "snow freshwater flux", + "kg m^-2 s^-1", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto RainFluxField = + Field::create(RainFluxCell.label(), "rain freshwater flux", + "kg m^-2 s^-1", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto EvaporationFluxField = + Field::create(EvaporationFluxCell.label(), "evaporation freshwater flux", + "kg m^-2 s^-1", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto SeaIceFreshWaterFluxField = Field::create( + SeaIceFreshWaterFluxCell.label(), "sea-ice freshwater flux", + "kg m^-2 s^-1", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto IceRunoffFluxField = + Field::create(IceRunoffFluxCell.label(), "ice runoff freshwater flux", + "kg m^-2 s^-1", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto RiverRunoffFluxField = Field::create( + RiverRunoffFluxCell.label(), "river runoff freshwater flux", + "kg m^-2 s^-1", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + + auto LatentHeatFluxField = + Field::create(LatentHeatFluxCell.label(), "latent heat flux", "W m^-2", + "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto SensibleHeatFluxField = + Field::create(SensibleHeatFluxCell.label(), "sensible heat flux", + "W m^-2", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto LongWaveHeatFluxUpField = Field::create( + LongWaveHeatFluxUpCell.label(), "upward longwave heat flux", "W m^-2", + "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto LongWaveHeatFluxDownField = Field::create( + LongWaveHeatFluxDownCell.label(), "downward longwave heat flux", + "W m^-2", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto SeaIceHeatFluxField = + Field::create(SeaIceHeatFluxCell.label(), "sea-ice heat flux", "W m^-2", + "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + auto ShortWaveHeatFluxField = + Field::create(ShortWaveHeatFluxCell.label(), "shortwave heat flux", + "W m^-2", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + + auto SeaIceSaltFluxField = + Field::create(SeaIceSaltFluxCell.label(), "sea-ice salt flux", + "kg m^-2 s^-1", "", std::numeric_limits::lowest(), + std::numeric_limits::max(), NDims, DimNames); + + FieldGroup::addFieldToGroup(SnowFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(RainFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(EvaporationFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(SeaIceFreshWaterFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(IceRunoffFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(RiverRunoffFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(LatentHeatFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(SensibleHeatFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(LongWaveHeatFluxUpCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(LongWaveHeatFluxDownCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(SeaIceHeatFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(ShortWaveHeatFluxCell.label(), "Forcing"); + FieldGroup::addFieldToGroup(SeaIceSaltFluxCell.label(), "Forcing"); + + SnowFluxField->attachData(SnowFluxCell); + RainFluxField->attachData(RainFluxCell); + EvaporationFluxField->attachData(EvaporationFluxCell); + SeaIceFreshWaterFluxField->attachData(SeaIceFreshWaterFluxCell); + IceRunoffFluxField->attachData(IceRunoffFluxCell); + RiverRunoffFluxField->attachData(RiverRunoffFluxCell); + LatentHeatFluxField->attachData(LatentHeatFluxCell); + SensibleHeatFluxField->attachData(SensibleHeatFluxCell); + LongWaveHeatFluxUpField->attachData(LongWaveHeatFluxUpCell); + LongWaveHeatFluxDownField->attachData(LongWaveHeatFluxDownCell); + SeaIceHeatFluxField->attachData(SeaIceHeatFluxCell); + ShortWaveHeatFluxField->attachData(ShortWaveHeatFluxCell); + SeaIceSaltFluxField->attachData(SeaIceSaltFluxCell); +} + +void TracerForcingVars::unregisterFields() const { + Field::destroy(SnowFluxCell.label()); + Field::destroy(RainFluxCell.label()); + Field::destroy(EvaporationFluxCell.label()); + Field::destroy(SeaIceFreshWaterFluxCell.label()); + Field::destroy(IceRunoffFluxCell.label()); + Field::destroy(RiverRunoffFluxCell.label()); + Field::destroy(LatentHeatFluxCell.label()); + Field::destroy(SensibleHeatFluxCell.label()); + Field::destroy(LongWaveHeatFluxUpCell.label()); + Field::destroy(LongWaveHeatFluxDownCell.label()); + Field::destroy(SeaIceHeatFluxCell.label()); + Field::destroy(ShortWaveHeatFluxCell.label()); + Field::destroy(SeaIceSaltFluxCell.label()); +} +} // namespace OMEGA diff --git a/components/omega/src/ocn/forcingVars/TracerForcingVars.h b/components/omega/src/ocn/forcingVars/TracerForcingVars.h new file mode 100644 index 000000000000..e38d9948f672 --- /dev/null +++ b/components/omega/src/ocn/forcingVars/TracerForcingVars.h @@ -0,0 +1,42 @@ +#ifndef OMEGA_TRACER_FORCING_H +#define OMEGA_TRACER_FORCING_H + +#include "DataTypes.h" +#include "HorzMesh.h" + +#include + +namespace OMEGA { + +// Forward declarations. Full definitions not needed in this header since only +// pointers are used. +class VertCoord; +class Eos; + +class TracerForcingVars { + public: + Array1DReal SnowFluxCell; + Array1DReal RainFluxCell; + Array1DReal EvaporationFluxCell; + Array1DReal SeaIceFreshWaterFluxCell; + Array1DReal IceRunoffFluxCell; + Array1DReal RiverRunoffFluxCell; + + Array1DReal LatentHeatFluxCell; + Array1DReal SensibleHeatFluxCell; + Array1DReal LongWaveHeatFluxUpCell; + Array1DReal LongWaveHeatFluxDownCell; + Array1DReal SeaIceHeatFluxCell; + Array1DReal ShortWaveHeatFluxCell; + + Array1DReal SeaIceSaltFluxCell; + + TracerForcingVars(const std::string &Suffix, const HorzMesh *Mesh); + + void registerFields(const std::string &MeshName) const; + void unregisterFields() const; +}; + +} // namespace OMEGA + +#endif diff --git a/components/omega/src/timeStepping/ForwardBackwardStepper.cpp b/components/omega/src/timeStepping/ForwardBackwardStepper.cpp index 6e1b2f2f47d9..82c1802d70e8 100644 --- a/components/omega/src/timeStepping/ForwardBackwardStepper.cpp +++ b/components/omega/src/timeStepping/ForwardBackwardStepper.cpp @@ -6,7 +6,6 @@ #include "ForwardBackwardStepper.h" #include "Pacer.h" -#include "VertMix.h" namespace OMEGA { @@ -38,13 +37,9 @@ void ForwardBackwardStepper::doStep( const int ThickNextLevel = 1; const int TracerNextLevel = 1; - int NTracers = Tracers::getNumTracers(); - Array3DReal CurTracerArray = Tracers::getAll(TracerCurLevel); Array3DReal NextTracerArray = Tracers::getAll(TracerNextLevel); - VertMix *VMix = VertMix::getInstance(); - if (State == nullptr) LOG_CRITICAL("Invalid State"); if (AuxState == nullptr) @@ -52,6 +47,8 @@ void ForwardBackwardStepper::doStep( prescribeVelocity(State, VelCurLevel, State, VelCurLevel, SimTime); + updateKPPFields(State, TracerCurLevel, ThickCurLevel, VelCurLevel); + // R_u^{n} = RHS_u(u^{n}, h^{n}, t^{n}) Tend->computeVelocityTendencies(State, AuxState, CurTracerArray, ThickCurLevel, VelCurLevel, TracerCurLevel, @@ -91,19 +88,8 @@ void ForwardBackwardStepper::doStep( Tracers::updateTimeLevels(); Pacer::stop("ForwardBackward:haloExch", 3); - // Apply implicit vertical mixing - CurTracerArray = Tracers::getAll(VelCurLevel); - if (VMix->VelVertMixSetup.Enabled or VMix->TracerVertMixSetup.Enabled) { - VMix->VertMixImplicit(State, AuxState, CurTracerArray, NTracers, - VelCurLevel); - - // Re-exchange halos after vertical mixing - Pacer::timingBarrier("ForwardBackward:vMixHaloExchBarrier", 3, Comm); - Pacer::start("ForwardBackward:vMixHaloExch", 3); - State->exchangeHalo(VelCurLevel); - Tracers::exchangeHalo(VelCurLevel); - Pacer::stop("ForwardBackward:vMixHaloExch", 3); - } + applyImplicitVerticalMixing(State, TracerCurLevel, ThickCurLevel, + VelCurLevel, "ForwardBackward"); validateOceanState(State, AuxState, VertCoord::getDefault(), 0); diff --git a/components/omega/src/timeStepping/RungeKutta2Stepper.cpp b/components/omega/src/timeStepping/RungeKutta2Stepper.cpp index dcf33ad611a2..26fb124f76f7 100644 --- a/components/omega/src/timeStepping/RungeKutta2Stepper.cpp +++ b/components/omega/src/timeStepping/RungeKutta2Stepper.cpp @@ -6,7 +6,6 @@ #include "RungeKutta2Stepper.h" #include "Pacer.h" -#include "VertMix.h" namespace OMEGA { @@ -32,15 +31,13 @@ void RungeKutta2Stepper::doStep(OceanState *State, // model state const int CurLevel = 0; const int NextLevel = 1; - int NTracers = Tracers::getNumTracers(); - Array3DReal CurTracerArray = Tracers::getAll(CurLevel); Array3DReal NextTracerArray = Tracers::getAll(NextLevel); - VertMix *VMix = VertMix::getInstance(); - prescribeState(State, CurLevel, State, CurLevel, SimTime); + updateKPPFields(State, CurLevel, CurLevel, CurLevel); + // q = (h,u,phi) // R_q^{n} = RHS_q(u^{n}, h^{n}, phi^{n}, t^{n}) Tend->computeAllTendencies(State, AuxState, CurTracerArray, CurLevel, @@ -75,19 +72,7 @@ void RungeKutta2Stepper::doStep(OceanState *State, // model state Tracers::updateTimeLevels(); Pacer::stop("RK2:haloExch", 3); - // Apply implicit vertical mixing - CurTracerArray = Tracers::getAll(CurLevel); - if (VMix->VelVertMixSetup.Enabled or VMix->TracerVertMixSetup.Enabled) { - VMix->VertMixImplicit(State, AuxState, CurTracerArray, NTracers, - CurLevel); - - // Re-exchange halos after vertical mixing - Pacer::timingBarrier("RK2:vMixHaloExchBarrier", 3, Comm); - Pacer::start("RK2:vMixHaloExch", 3); - State->exchangeHalo(CurLevel); - Tracers::exchangeHalo(CurLevel); - Pacer::stop("RK2:vMixHaloExch", 3); - } + applyImplicitVerticalMixing(State, CurLevel, CurLevel, CurLevel, "RK2"); validateOceanState(State, AuxState, VertCoord::getDefault(), CurLevel); diff --git a/components/omega/src/timeStepping/RungeKutta4Stepper.cpp b/components/omega/src/timeStepping/RungeKutta4Stepper.cpp index abd83b03043e..c0ed337b7b62 100644 --- a/components/omega/src/timeStepping/RungeKutta4Stepper.cpp +++ b/components/omega/src/timeStepping/RungeKutta4Stepper.cpp @@ -6,7 +6,6 @@ #include "RungeKutta4Stepper.h" #include "Pacer.h" -#include "VertMix.h" namespace OMEGA { @@ -79,14 +78,11 @@ void RungeKutta4Stepper::doStep(OceanState *State, // model state const int CurLevel = 0; const int NextLevel = 1; - int NTracers = Tracers::getNumTracers(); Array3DReal CurTracerArray = Tracers::getAll(CurLevel); Array3DReal NextTracerArray = Tracers::getAll(NextLevel); TimeInstant ForcingStageTime = SimTime; - VertMix *VMix = VertMix::getInstance(); - for (int Stage = 0; Stage < NStages; ++Stage) { const TimeInstant StageTime = SimTime + RKC[Stage] * TimeStep; // first stage does: @@ -95,6 +91,7 @@ void RungeKutta4Stepper::doStep(OceanState *State, // model state if (Stage == 0) { weightTracers(NextTracerArray, CurTracerArray, State, CurLevel); prescribeState(State, CurLevel, State, CurLevel, ForcingStageTime); + updateKPPFields(State, CurLevel, CurLevel, CurLevel); Tend->computeAllTendencies(State, AuxState, CurTracerArray, CurLevel, CurLevel, CurLevel, StageTime, RKProj[Stage] * TimeStep); @@ -139,19 +136,7 @@ void RungeKutta4Stepper::doStep(OceanState *State, // model state Tracers::updateTimeLevels(); Pacer::stop("RK4:haloExch", 3); - // Apply implicit vertical mixing - CurTracerArray = Tracers::getAll(CurLevel); - if (VMix->VelVertMixSetup.Enabled or VMix->TracerVertMixSetup.Enabled) { - VMix->VertMixImplicit(State, AuxState, CurTracerArray, NTracers, - CurLevel); - - // Re-exchange halos after vertical mixing - Pacer::timingBarrier("RK4:vMixHaloExchBarrier", 3, Comm); - Pacer::start("RK4:vMixHaloExch", 3); - State->exchangeHalo(CurLevel); - Tracers::exchangeHalo(CurLevel); - Pacer::stop("RK4:vMixHaloExch", 3); - } + applyImplicitVerticalMixing(State, CurLevel, CurLevel, CurLevel, "RK4"); validateOceanState(State, AuxState, VertCoord::getDefault(), CurLevel); diff --git a/components/omega/src/timeStepping/TimeStepper.cpp b/components/omega/src/timeStepping/TimeStepper.cpp index 4576f2946de2..e1d8abc50869 100644 --- a/components/omega/src/timeStepping/TimeStepper.cpp +++ b/components/omega/src/timeStepping/TimeStepper.cpp @@ -9,8 +9,10 @@ #include "Error.h" #include "ForwardBackwardStepper.h" #include "Logging.h" +#include "Pacer.h" #include "RungeKutta2Stepper.h" #include "RungeKutta4Stepper.h" +#include "VertMix.h" namespace OMEGA { //------------------------------------------------------------------------------ @@ -610,11 +612,11 @@ void TimeStepper::prescribeVelocity(OceanState *State1, int TimeLevel1, const R8 lon_p = LonEdge(IEdge) - 2.0 * Pi * TSim / Tau; const R8 u = (1 / Tau) * (10.0 * Kokkos::pow(sin(lon_p), 2) * - sin(2.0 * LatEdge(IEdge)) * - cos(Pi * TSim / Tau) + - 2.0 * Pi * cos(LatEdge(IEdge))); + sin(2.0 * LatEdge(IEdge)) * + cos(Pi * TSim / Tau) + + 2.0 * Pi * cos(LatEdge(IEdge))); const R8 v = (10.0 / Tau) * sin(2.0 * lon_p) * - cos(LatEdge(IEdge)) * cos(Pi * TSim / Tau); + cos(LatEdge(IEdge)) * cos(Pi * TSim / Tau); const R8 normalVel = REarth * (u * cos(AngleEdge(IEdge)) + v * sin(AngleEdge(IEdge))); @@ -790,4 +792,42 @@ void TimeStepper::finalizeTracersUpdate(const Array3DReal &NextTracers, }); } +//------------------------------------------------------------------------------ +// Compute KPP fields once per time step, before any tendency evaluation. +void TimeStepper::updateKPPFields(OceanState *State, int TracerTimeLevel, + int ThickTimeLevel, int VelTimeLevel) const { + + Array3DReal CurTracerArray = Tracers::getAll(TracerTimeLevel); + Tend->computeKPPFields(State, CurTracerArray, ThickTimeLevel, VelTimeLevel); +} + +//------------------------------------------------------------------------------ +// Apply implicit vertical mixing after state/tracer time levels are updated. +void TimeStepper::applyImplicitVerticalMixing( + OceanState *State, int TracerTimeLevel, int ThickTimeLevel, + int VelTimeLevel, const std::string &TimerPrefix) const { + + Array3DReal CurTracerArray = Tracers::getAll(TracerTimeLevel); + AuxState->computeAll(State, CurTracerArray, ThickTimeLevel, VelTimeLevel, + TimeStep); + + VertMix *VMix = VertMix::getInstance(); + if (!VMix) + return; + + if (VMix->VelVertMixSetup.Enabled or VMix->TracerVertMixSetup.Enabled) { + const int NTracers = Tracers::getNumTracers(); + VMix->VertMixImplicit(State, AuxState, CurTracerArray, NTracers, + VelTimeLevel); + + // Re-exchange halos after vertical mixing + const MPI_Comm Comm = MeshHalo->getComm(); + Pacer::timingBarrier(TimerPrefix + ":vMixHaloExchBarrier", 3, Comm); + Pacer::start(TimerPrefix + ":vMixHaloExch", 3); + State->exchangeHalo(VelTimeLevel); + Tracers::exchangeHalo(VelTimeLevel); + Pacer::stop(TimerPrefix + ":vMixHaloExch", 3); + } +} + } // namespace OMEGA diff --git a/components/omega/src/timeStepping/TimeStepper.h b/components/omega/src/timeStepping/TimeStepper.h index 7e95019da641..69c473455c55 100644 --- a/components/omega/src/timeStepping/TimeStepper.h +++ b/components/omega/src/timeStepping/TimeStepper.h @@ -294,6 +294,27 @@ class TimeStepper { int TimeLevel ///< [in] time level index ) const; + /// Compute KPP boundary layer depth, mixing coefficients and non-local + /// flux once for the current time step. Must be called before the first + /// tendency evaluation of the step so that every stage and the end-of-step + /// implicit mixing share the same KPP fields. + void + updateKPPFields(OceanState *State, ///< [in] model state + int TracerTimeLevel, ///< [in] tracer time level + int ThickTimeLevel, ///< [in] pseudo-thickness time level + int VelTimeLevel ///< [in] velocity time level + ) const; + + /// Apply implicit vertical mixing after state/tracer time levels are + /// updated, using the KPP fields computed at the start of the step. + void applyImplicitVerticalMixing( + OceanState *State, ///< [inout] model state + int TracerTimeLevel, ///< [in] tracer time level + int ThickTimeLevel, ///< [in] pseudo-thickness time level + int VelTimeLevel, ///< [in] velocity time level + const std::string &TimerPrefix ///< [in] timer name prefix + ) const; + protected: /// Name of time stepper std::string Name; diff --git a/components/omega/test/CMakeLists.txt b/components/omega/test/CMakeLists.txt index 20fe917bdd48..d8d00b057484 100644 --- a/components/omega/test/CMakeLists.txt +++ b/components/omega/test/CMakeLists.txt @@ -13,6 +13,12 @@ function(add_omega_test test_name exe_name source_files mpi_args) # Copy extra arguments to a local variable so they can be treated # as a list set(extra_args ${ARGN}) + set(test_arg "") + foreach(extra_arg ${extra_args}) + if(extra_arg MATCHES "^test_arg=(.*)") + set(test_arg "${CMAKE_MATCH_1}") + endif() + endforeach() # Create the executable add_executable(${exe_name} ${source_files}) @@ -58,19 +64,19 @@ function(add_omega_test test_name exe_name source_files mpi_args) if("${OMEGA_ARCH}" STREQUAL "SYCL") add_test( NAME ${test_name} - COMMAND ${OMEGA_MPI_EXEC} ${mpi_args} ${OMEGA_MPI_ARGS} ./${exe_name} + COMMAND ${OMEGA_MPI_EXEC} ${mpi_args} ${OMEGA_MPI_ARGS} ./${exe_name} ${test_arg} ) else() add_test( NAME ${test_name} - COMMAND ${OMEGA_MPI_EXEC} ${OMEGA_MPI_ARGS} ${mpi_args} -- ./${exe_name} + COMMAND ${OMEGA_MPI_EXEC} ${OMEGA_MPI_ARGS} ${mpi_args} -- ./${exe_name} ${test_arg} ) endif() else() add_test( NAME ${test_name} - COMMAND ../omega_env.sh ./${exe_name} + COMMAND ../omega_env.sh ./${exe_name} ${test_arg} ) endif() @@ -88,6 +94,31 @@ function(add_omega_test test_name exe_name source_files mpi_args) endfunction() +function(add_omega_ctest test_name exe_name mpi_args test_arg) + if (mpi_args) + + if("${OMEGA_ARCH}" STREQUAL "SYCL") + add_test( + NAME ${test_name} + COMMAND ${OMEGA_MPI_EXEC} ${mpi_args} ${OMEGA_MPI_ARGS} ./${exe_name} ${test_arg} + ) + else() + add_test( + NAME ${test_name} + COMMAND ${OMEGA_MPI_EXEC} ${OMEGA_MPI_ARGS} ${mpi_args} -- ./${exe_name} ${test_arg} + ) + endif() + + else() + add_test( + NAME ${test_name} + COMMAND ../omega_env.sh ./${exe_name} ${test_arg} + ) + endif() + + set_tests_properties(${test_name} PROPERTIES LABELS "${OMEGA_ARCH};Omega-0") +endfunction() + ################## # Data type test ################## @@ -568,6 +599,31 @@ add_omega_test( "-n;4" ) +########################## +# KPP tests +########################## + +add_omega_test( + KPP_PROFILES_TEST + testKPPMix.exe + ocn/KPPMixTest.cpp + "-n;1" + test_arg=profiles +) +add_omega_ctest(KPP_BLD_TEST testKPPMix.exe "-n;1" bld) +add_omega_ctest(KPP_VMIX_COEFF_TEST testKPPMix.exe "-n;1" vmix) +add_omega_ctest(KPP_INTEGRATION_TEST testKPPMix.exe "-n;1" integration) +# Each injects a rejected MatchTechnique and must abort in KPPMix::init. +add_omega_ctest(KPP_CONFIG_GRADIENT_TEST testKPPMix.exe "-n;1" config-gradient) +add_omega_ctest(KPP_CONFIG_UNSUPPORTED_TEST testKPPMix.exe "-n;1" config-unsupported) +add_omega_ctest(KPP_CONFIG_PARABOLIC_TEST testKPPMix.exe "-n;1" config-parabolic) +set_tests_properties( + KPP_CONFIG_GRADIENT_TEST + KPP_CONFIG_UNSUPPORTED_TEST + KPP_CONFIG_PARABOLIC_TEST + PROPERTIES WILL_FAIL TRUE +) + ################## # VAdv test ################## diff --git a/components/omega/test/ocn/EosTest.cpp b/components/omega/test/ocn/EosTest.cpp index 0e7eb2c5be76..f8ca084d4559 100644 --- a/components/omega/test/ocn/EosTest.cpp +++ b/components/omega/test/ocn/EosTest.cpp @@ -706,6 +706,42 @@ void testBruntVaisalaFreqSqTeos10() { return; } +/// Test all Eos::calcCtFreezing pathways (Teos10, Linear, Constant) +void testCalcCtFreezing() { + const Real RTol = 1e-10; + + constexpr Real SaturationFrac = 0.0; + constexpr Real PDb = 500.0; // pressure in dbar for GSW pathway + constexpr Real SaLocal = 32.0; + + const Real CtTeosExpected = + gsw_ct_freezing_poly(SaLocal, PDb, SaturationFrac); + const Real CtTeos = + Eos::calcCtFreezing(EosType::Teos10Eos, SaLocal, PDb, SaturationFrac); + if (!isApprox(CtTeos, CtTeosExpected, RTol)) { + ABORT_ERROR("testCalcCtFreezing: Teos10 FAIL, expected {}, got {}", + CtTeosExpected, CtTeos); + } + + const Real CtLinearExpected = -0.054_Real * SaLocal / Psu2Gpkg; + const Real CtLinear = + Eos::calcCtFreezing(EosType::LinearEos, SaLocal, PDb, SaturationFrac); + if (!isApprox(CtLinear, CtLinearExpected, RTol)) { + ABORT_ERROR("testCalcCtFreezing: Linear FAIL, expected {}, got {}", + CtLinearExpected, CtLinear); + } + + const Real CtConstExpected = -1.9_Real; + const Real CtConst = + Eos::calcCtFreezing(EosType::ConstantEos, SaLocal, PDb, SaturationFrac); + if (!isApprox(CtConst, CtConstExpected, RTol)) { + ABORT_ERROR("testCalcCtFreezing: Constant FAIL, expected {}, got {}", + CtConstExpected, CtConst); + } + + return; +} + /// Finalize and clean up all test infrastructure void finalizeEosTest() { Eos::destroyInstance(); @@ -767,22 +803,22 @@ void checkValueGswcN2() { } /// Test that the calcCtFreezing function returns the expected value -void checkValueCtFreezing() { +void checkValueGswcCtFreezing() { const Real RTol = 1e-10; - Teos10Eos TestEos(VertCoord::getDefault()); constexpr Real SaturationFrac = 0.0; constexpr Real P = 500.0 * Db2Pa; // Convert dbar to Pa constexpr Real Sa = 32.0; /// Get freezing temperature from GSW-C library double CtFreezGswc = gsw_ct_freezing_poly(Sa, P * Pa2Db, SaturationFrac); - double CtFreez = TestEos.calcCtFreezing(Sa, P * Pa2Db, SaturationFrac); + double CtFreez = + Teos10Eos::calcCtFreezingTeos10(Sa, P * Pa2Db, SaturationFrac); /// Check the value against the GSW-C value bool Check = isApprox(CtFreezGswc, CtFreez, RTol); if (!Check) { - ABORT_ERROR("checkValueCtFreezing: CtFreez FAIL, expected {}, got {}", + ABORT_ERROR("checkValueGswcCtFreezing: CtFreez FAIL, expected {}, got {}", CtFreezGswc, CtFreez); } return; @@ -837,7 +873,7 @@ void eosTest(const std::string &MeshFile = "OmegaMesh.nc") { checkValueGswcSpecVol(); checkValueGswcN2(); - checkValueCtFreezing(); + checkValueGswcCtFreezing(); checkValueGswcCtFromPt(); checkValueGswcPtFromCt(); @@ -848,6 +884,7 @@ void eosTest(const std::string &MeshFile = "OmegaMesh.nc") { testEosTeos10(); testEosTeos10Displaced(); testBruntVaisalaFreqSqTeos10(); + testCalcCtFreezing(); finalizeEosTest(); diff --git a/components/omega/test/ocn/KPPMixTest.cpp b/components/omega/test/ocn/KPPMixTest.cpp new file mode 100644 index 000000000000..8734f1d0e763 --- /dev/null +++ b/components/omega/test/ocn/KPPMixTest.cpp @@ -0,0 +1,2276 @@ +//===-- Test driver for OMEGA KPP mixing -------------------------*- C++ +//-*-===// +/// +/// \file +/// \brief Unit tests for KPP profiles, utilities, and mixing coefficients +/// +//===----------------------------------------------------------------------===// + +#include "KPPMix.h" +#include "Config.h" +#include "Decomp.h" +#include "Dimension.h" +#include "Field.h" +#include "Halo.h" +#include "HorzMesh.h" +#include "IO.h" +#include "IOStream.h" +#include "KPPConstants.h" +#include "Logging.h" +#include "MachEnv.h" +#include "OceanTestCommon.h" +#include "OmegaKokkos.h" +#include "Pacer.h" +#include "TimeMgr.h" +#include "VertCoord.h" +#include "mpi.h" + +#include +#include + +using namespace OMEGA; + +namespace { + +#ifdef SINGLE_PRECISION +constexpr Real RTol = 2.0e-5_Real; +constexpr Real ATol = 2.0e-6_Real; +constexpr Real BLDRTol = 2.0e-4_Real; +#else +constexpr Real RTol = 2.0e-10_Real; +constexpr Real ATol = 2.0e-12_Real; +constexpr Real BLDRTol = RTol; +#endif + +constexpr Real LayerThickness = 10.0_Real; +constexpr Real TestOBLDepth = 40.0_Real; +constexpr I4 TestOBLIndex = 3; + +std::unique_ptr TestClock; +Clock *TestClockPtr = nullptr; + +void initKPPMixTest(const std::string &TestGroup) { + MachEnv::init(MPI_COMM_WORLD); + MachEnv *DefEnv = MachEnv::getDefault(); + MPI_Comm DefComm = DefEnv->getComm(); + initLogging(DefEnv); + LOG_INFO("------ KPP Mixing Unit Tests ------"); + + Config("Omega"); + Config::readAll("omega.yml"); + // These groups inject a rejected MatchTechnique; KPPMix::init must abort, + // so the ctest entries for them are registered as expected failures. + if (TestGroup == "config-gradient" || TestGroup == "config-unsupported" || + TestGroup == "config-parabolic") { + Config VertMixConfig("VertMix"); + Config KPPConfig("KPP"); + Error Err; + Err += Config::getOmegaConfig()->get(VertMixConfig); + Err += VertMixConfig.get(KPPConfig); + CHECK_ERROR_ABORT(Err, "KPPMixTest: unable to access KPP configuration"); + const std::string MatchTechnique = + TestGroup == "config-gradient" ? "MatchGradient" + : TestGroup == "config-parabolic" ? "ParabolicNonLocal" + : "NotAKPPMode"; + KPPConfig.set("MatchTechnique", MatchTechnique); + } + IO::init(DefComm); + Decomp::init(); + Halo::init(); + + Calendar::init("No Leap"); + TimeInstant StartTime(0, 1, 1, 0, 0, 0.0); + TimeInterval TimeStep(1, TimeUnits::Hours); + TestClock = std::make_unique(StartTime, TimeStep); + TestClockPtr = TestClock.get(); + Field::init(TestClockPtr); + IOStream::init(TestClockPtr); + HorzMesh::init(TestClockPtr); + VertCoord::init(false); + KPPMix::init(); +} + +void finalizeKPPMixTest() { + KPPMix::destroyInstance(); + IOStream::finalize(); + VertCoord::clear(); + HorzMesh::clear(); + Halo::clear(); + Decomp::clear(); + Field::clear(); + Dimension::clear(); + TestClockPtr = nullptr; + TestClock.reset(); + MachEnv::removeAll(); +} + +void checkResult(const char *TestName, int NumErrors) { + if (NumErrors != 0) { + ABORT_ERROR("KPPMixTest: {} FAIL with {} errors", TestName, NumErrors); + } + LOG_INFO("KPPMixTest: {} PASS", TestName); +} + +void testStabilityFunctions() { + constexpr int NTests = 9; + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-StabilityFunctions", {NTests}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + Real Zeta = 0.0_Real; + switch (ITest) { + case 0: + Zeta = 2.0_Real; + break; + case 1: + Zeta = 0.1_Real; + break; + case 2: + Zeta = 0.0_Real; + break; + case 3: + Zeta = -0.1_Real; + break; + case 4: + Zeta = KPP::ZetaM; + break; + case 5: + Zeta = KPP::ZetaM - 1.0e-4_Real; + break; + case 6: + Zeta = KPP::ZetaS; + break; + case 7: + Zeta = KPP::ZetaS - 1.0e-4_Real; + break; + default: + Zeta = -10.0_Real; + break; + } + + Real ExpectedM; + if (Zeta >= 0.0_Real) { + ExpectedM = 1.0_Real / (1.0_Real + 5.0_Real * Zeta); + } else if (Zeta >= KPP::ZetaM) { + ExpectedM = Kokkos::pow(1.0_Real - 16.0_Real * Zeta, 0.25_Real); + } else { + ExpectedM = + Kokkos::pow(KPP::AMoM - KPP::CMoM * Zeta, 1.0_Real / 3.0_Real); + } + + Real ExpectedS; + if (Zeta >= 0.0_Real) { + ExpectedS = 1.0_Real / (1.0_Real + 5.0_Real * Zeta); + } else if (Zeta >= KPP::ZetaS) { + ExpectedS = Kokkos::sqrt(1.0_Real - 16.0_Real * Zeta); + } else { + ExpectedS = + Kokkos::pow(KPP::AMoS - KPP::CMoS * Zeta, 1.0_Real / 3.0_Real); + } + + const Real ActualM = KPP::kppPhiInvMomentum(Zeta); + const Real ActualS = KPP::kppPhiInvScalar(Zeta); + if (!isApprox(ActualM, ExpectedM, RTol, ATol) || ActualM <= 0.0_Real) + ++ErrorCount; + if (!isApprox(ActualS, ExpectedS, RTol, ATol) || ActualS <= 0.0_Real) + ++ErrorCount; + }, + NumErrors); + + checkResult("stability functions", NumErrors); + + NumErrors = 0; + parallelReduce( + "KPPMixTest-StabilityContinuity", {2}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + const Real Transition = ITest == 0 ? KPP::ZetaM : KPP::ZetaS; + const Real Epsilon = 1.0e-6_Real; + const Real Above = ITest == 0 + ? KPP::kppPhiInvMomentum(Transition + Epsilon) + : KPP::kppPhiInvScalar(Transition + Epsilon); + const Real Below = ITest == 0 + ? KPP::kppPhiInvMomentum(Transition - Epsilon) + : KPP::kppPhiInvScalar(Transition - Epsilon); + if (!isApprox(Above, Below, 2.0e-5_Real, 2.0e-5_Real)) + ++ErrorCount; + }, + NumErrors); + checkResult("stability transition continuity", NumErrors); +} + +void testShapeFunctions() { + constexpr int NTests = 7; + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-ShapeFunctions", {NTests}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + Real Sigma; + switch (ITest) { + case 0: + Sigma = 0.25_Real; + break; + case 1: + Sigma = 0.0_Real; + break; + case 2: + Sigma = -0.25_Real; + break; + case 3: + Sigma = -0.5_Real; + break; + case 4: + Sigma = -0.75_Real; + break; + case 5: + Sigma = -1.0_Real; + break; + default: + Sigma = -1.25_Real; + break; + } + + const Real SigmaClamped = + Kokkos::fmax(-1.0_Real, Kokkos::fmin(0.0_Real, Sigma)); + const Real SigmaMu = -SigmaClamped; + const Real OneMinus = 1.0_Real - SigmaMu; + const Real ExpectedSimple = SigmaMu * OneMinus * OneMinus; + constexpr Real ShapeAtBase = 0.125_Real; + const Real Smooth = + SigmaMu * SigmaMu * (3.0_Real - 2.0_Real * SigmaMu); + + if (!isApprox(KPP::kppShapeMomentum(Sigma), ExpectedSimple, RTol, + ATol)) + ++ErrorCount; + if (!isApprox(KPP::kppShapeScalar(Sigma), ExpectedSimple, RTol, ATol)) + ++ErrorCount; + if (!isApprox(KPP::kppShapeMatched(Sigma, ShapeAtBase), + ExpectedSimple + ShapeAtBase * Smooth, RTol, ATol)) + ++ErrorCount; + if (!isApprox(KPP::kppShapeMatched(Sigma, 0.0_Real), ExpectedSimple, + RTol, ATol)) + ++ErrorCount; + if (!isApprox(KPP::kppSurfaceMomentumScale(Sigma), + KPP::HuOn * (1.0_Real + Sigma), RTol, ATol)) + ++ErrorCount; + }, + NumErrors); + + checkResult("shape functions", NumErrors); +} + +void testLangmuirFunctions() { + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-LangmuirFunctions", {4}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + const Real Wind = ITest == 0 ? -5.0_Real + : ITest == 1 ? 0.0_Real + : ITest == 2 ? 10.0_Real + : 100.0_Real; + const Real UStar = ITest < 2 ? 0.0_Real : 0.01_Real; + const Real WindClamped = Kokkos::fmax(0.0_Real, Wind); + const Real ExpectedStokes = 0.016_Real * WindClamped; + const Real UStarClamped = Kokkos::fmax(KPP::MinUStar, UStar); + const Real StokesClamped = Kokkos::fmax(1.0e-8_Real, ExpectedStokes); + const Real ExpectedLa = Kokkos::sqrt(UStarClamped / StokesClamped); + const Real LaInv = 1.0_Real / Kokkos::fmax(0.5_Real, ExpectedLa); + const Real ExpectedEnhancement = Kokkos::fmin( + 2.0_Real, + Kokkos::fmax(1.0_Real, + Kokkos::sqrt(1.0_Real + 0.5_Real * LaInv * LaInv))); + + const Real Stokes = KPP::estimateStokesDriftSL(Wind, 50.0_Real); + const Real La = KPP::computeLangmuirNumber(UStar, Stokes); + const Real Enhancement = + KPP::computeLangmuirEnhancement(Wind, UStar, 50.0_Real); + if (!isApprox(Stokes, ExpectedStokes, RTol, ATol)) + ++ErrorCount; + if (!isApprox(La, ExpectedLa, RTol, ATol)) + ++ErrorCount; + if (!isApprox(Enhancement, ExpectedEnhancement, RTol, ATol) || + Enhancement < 1.0_Real || Enhancement > 2.0_Real) + ++ErrorCount; + }, + NumErrors); + + checkResult("Langmuir functions", NumErrors); +} + +void testOBLUtilities() { + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-OBLUtilities", {4}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + const Real IceFraction = ITest == 0 ? 0.0_Real + : ITest == 1 ? KPP::IceSuppressThresh + : ITest == 2 + ? KPP::IceSuppressThresh + 0.01_Real + : 0.0_Real; + const I4 LandIceMask = ITest == 3 ? 1 : 0; + const bool ExpectedSuppression = + LandIceMask != 0 || IceFraction > KPP::IceSuppressThresh; + if (KPP::shouldSuppressOBL(IceFraction, LandIceMask) != + ExpectedSuppression) + ++ErrorCount; + + const Real InputDepth = ITest == 0 ? 1.0_Real + : ITest == 1 ? 20.0_Real + : ITest == 2 ? 1.0_Real + : 200.0_Real; + Real ExpectedDepth = Kokkos::fmax(InputDepth, 2.0_Real); + if (IceFraction > KPP::IceSuppressThresh) + ExpectedDepth = Kokkos::fmax(ExpectedDepth, KPP::MinOBLUnderIce); + ExpectedDepth = Kokkos::fmin(ExpectedDepth, 95.0_Real); + if (!isApprox(KPP::constrainOBLDepth(InputDepth, 4.0_Real, 100.0_Real, + IceFraction), + ExpectedDepth, RTol, ATol)) + ++ErrorCount; + }, + NumErrors); + + checkResult("OBL utilities", NumErrors); +} + +void testTurbulentVelocityScale() { + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-TurbulentVelocityScale", {6}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + const Real UStar = ITest == 0 ? 0.02_Real + : ITest == 1 ? 0.0_Real + : ITest == 2 ? 0.02_Real + : ITest == 5 ? -0.02_Real + : 0.0_Real; + const Real B0 = ITest == 0 ? 0.0_Real + : ITest == 1 ? -1.0e-7_Real + : ITest == 2 ? -1.0e-7_Real + : ITest == 3 ? 1.0e-7_Real + : 0.0_Real; + const Real H = ITest == 5 ? -50.0_Real : 50.0_Real; + const Real UStarClamped = Kokkos::fmax(0.0_Real, UStar); + const Real HClamped = Kokkos::fmax(0.0_Real, H); + const Real Momentum = UStarClamped * UStarClamped * UStarClamped; + const Real Buoyancy = + KPP::ConvectiveVelFac * Kokkos::fmax(0.0_Real, -B0) * HClamped; + const Real Expected = + Kokkos::pow(Momentum + Buoyancy, 1.0_Real / 3.0_Real); + const Real Actual = KPP::computeTurbVelocityScale(UStar, B0, H); + if (!isApprox(Actual, Expected, RTol, ATol) || Actual < 0.0_Real) + ++ErrorCount; + }, + NumErrors); + + checkResult("turbulent velocity scale", NumErrors); +} + +void testTurbScales() { + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-TurbScales", {5}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + // 0: wind only, 1: wind + unstable, 2: wind + stable, + // 3: free convection (u*=0, B0<0), 4: calm and stable (both zero) + const Real UStar = (ITest == 3 || ITest == 4) ? 0.0_Real : 0.02_Real; + const Real B0 = ITest == 0 ? 0.0_Real + : ITest == 1 ? -1.0e-7_Real + : ITest == 2 ? 1.0e-7_Real + : ITest == 3 ? -1.0e-7_Real + : 1.0e-7_Real; + const Real HOBL = 50.0_Real; + const Real SigmaLoc = KPP::SurfaceLayerExtent; + + Real WM = -1.0_Real; + Real WS = -1.0_Real; + KPP::kppTurbScales(UStar, B0, HOBL, SigmaLoc, VonKar, WM, WS); + + Real ExpectedWM = 0.0_Real; + Real ExpectedWS = 0.0_Real; + if (UStar > 0.0_Real) { + const Real U3 = UStar * UStar * UStar; + const Real Zeta = + SigmaLoc * HOBL * B0 * VonKar / Kokkos::max(U3, 1.0e-20_Real); + ExpectedWM = VonKar * UStar * + Kokkos::max(KPP::kppPhiInvMomentum(Zeta), 0.0_Real); + ExpectedWS = VonKar * UStar * + Kokkos::max(KPP::kppPhiInvScalar(Zeta), 0.0_Real); + } else if (B0 < 0.0_Real) { + const Real WM3 = -KPP::CMoM * SigmaLoc * HOBL * VonKar * B0; + const Real WS3 = -KPP::CMoS * SigmaLoc * HOBL * VonKar * B0; + ExpectedWM = VonKar * Kokkos::pow(WM3, 1.0_Real / 3.0_Real); + ExpectedWS = VonKar * Kokkos::pow(WS3, 1.0_Real / 3.0_Real); + } + + if (!isApprox(WM, ExpectedWM, RTol, ATol) || + !isApprox(WS, ExpectedWS, RTol, ATol)) + ++ErrorCount; + + // Scales must never go negative, and scalars mix at least as + // efficiently as momentum under unstable forcing. + if (WM < 0.0_Real || WS < 0.0_Real) + ++ErrorCount; + if (B0 < 0.0_Real && WS < WM) + ++ErrorCount; + + // Calm and stable is fully quiescent. + if (ITest == 4 && (WM != 0.0_Real || WS != 0.0_Real)) + ++ErrorCount; + }, + NumErrors); + + checkResult("turbulent scales", NumErrors); +} + +void testMatchShape() { + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-MatchShape", {4}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + const Real Interior = 1.0e-4_Real; + const Real HOBL = ITest == 1 ? 0.0_Real : 50.0_Real; + const Real W = ITest == 2 ? 0.0_Real : 0.01_Real; + + const Real Shape = KPP::kppMatchShape(Interior, HOBL, W); + + // Degenerate HOBL or velocity scale switches matching off. + if (ITest == 1 || ITest == 2) { + if (Shape != 0.0_Real) + ++ErrorCount; + return; + } + + if (!isApprox(Shape, Interior / (HOBL * W), RTol, ATol)) + ++ErrorCount; + + // The matched shape must reproduce the interior coefficient when + // multiplied back by h*w, which is the whole point of matching. + if (!isApprox(HOBL * W * Shape, Interior, RTol, ATol)) + ++ErrorCount; + }, + NumErrors); + + checkResult("match shape", NumErrors); +} + +void testNonLocalCs() { + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-NonLocalCs", {1}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + const Real Cs = KPP::kppNonLocalCs(VonKar, KPP::SurfaceLayerExtent); + const Real Expected = + 10.0_Real * VonKar * + Kokkos::pow(KPP::CMoS * VonKar * KPP::SurfaceLayerExtent, + 1.0_Real / 3.0_Real); + if (!isApprox(Cs, Expected, RTol, ATol)) + ++ErrorCount; + + // Large et al. (1994) quote C_s ~ 6.33 for the default constants. + if (Kokkos::abs(Cs - 6.33_Real) > 0.05_Real) + ++ErrorCount; + }, + NumErrors); + + checkResult("non-local flux constant", NumErrors); +} + +void testClampOBLDepth() { + int NumErrors = 0; + + parallelReduce( + "KPPMixTest-ClampOBLDepth", {5}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + const Real MinDepth = 2.0_Real; + const Real MaxDepth = 95.0_Real; + const Real Input = ITest == 0 ? 40.0_Real + : ITest == 1 ? 1.0_Real + : ITest == 2 ? 200.0_Real + : ITest == 3 ? 1.0_Real + : 1.0_Real; + const bool ApplyIce = (ITest == 3); + + const Real Actual = KPP::kppClampOBLDepth( + Input, MinDepth, MaxDepth, ApplyIce, KPP::MinOBLUnderIce); + + Real Expected = Kokkos::fmax(Input, MinDepth); + if (ApplyIce) + Expected = Kokkos::fmax(Expected, KPP::MinOBLUnderIce); + Expected = Kokkos::fmin(Expected, MaxDepth); + + if (!isApprox(Actual, Expected, RTol, ATol)) + ++ErrorCount; + + // Result must always land inside the supported range. + if (Actual < MinDepth || Actual > MaxDepth) + ++ErrorCount; + }, + NumErrors); + + checkResult("OBL depth clamping", NumErrors); +} + +void testOBLIndex() { + int NumErrors = 0; + + // Single column of five 10 m layers, interfaces at 0,-10,...,-50 m. + constexpr I4 NLayers = 5; + Array2DReal ZInterface("KPPMixTest-OBLIndexZ", 1, NLayers + 1); + parallelFor( + "KPPMixTest-OBLIndexInit", {NLayers + 1}, + KOKKOS_LAMBDA(I4 K) { ZInterface(0, K) = -10.0_Real * K; }); + + parallelReduce( + "KPPMixTest-OBLIndex", {5}, + KOKKOS_LAMBDA(int ITest, int &ErrorCount) { + // Cases 0-3 bracket a layer; case 4 is deeper than the column. + const Real Depth = ITest == 0 ? 5.0_Real + : ITest == 1 ? 25.0_Real + : ITest == 2 ? 20.0_Real + : ITest == 3 ? 0.0_Real + : 100.0_Real; + const I4 Expected = ITest == 0 ? 0 + : ITest == 1 ? 2 + : ITest == 2 ? 1 + : ITest == 3 ? 0 + : NLayers - 1; + + const I4 Actual = + KPP::kppOBLIndex(ZInterface, 0, 0, NLayers - 1, 0.0_Real, Depth); + if (Actual != Expected) + ++ErrorCount; + }, + NumErrors); + + checkResult("OBL index lookup", NumErrors); +} + +// Builds a uniform column whose free surface sits at Ssh. All KPP results must +// be invariant to Ssh since depths are measured below the free surface. +void setCoefficientTestGeometry(Real Ssh = 0.0_Real) { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + + OMEGA_SCOPE(GeomZInterface, VCoord->GeomZInterface); + OMEGA_SCOPE(GeomZMid, VCoord->GeomZMid); + OMEGA_SCOPE(SshCell, VCoord->SshCell); + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + OMEGA_SCOPE(BoundaryLayerDepth, KPPInstance->BoundaryLayerDepth); + OMEGA_SCOPE(IndexBoundaryLayerDepth, KPPInstance->IndexBoundaryLayerDepth); + + parallelFor( + "KPPMixTest-SetGeometry", {Mesh->NCellsAll}, KOKKOS_LAMBDA(I4 ICell) { + MinLayerCell(ICell) = 0; + MaxLayerCell(ICell) = NVertLayers - 1; + SshCell(ICell) = Ssh; + BoundaryLayerDepth(ICell) = TestOBLDepth; + IndexBoundaryLayerDepth(ICell) = TestOBLIndex; + for (I4 K = 0; K <= NVertLayers; ++K) { + GeomZInterface(ICell, K) = Ssh - LayerThickness * K; + if (K < NVertLayers) { + GeomZMid(ICell, K) = Ssh - LayerThickness * (K + 0.5_Real); + } + } + }); +} + +Real nonLocalNormalization() { + return 10.0_Real * VonKar * + Kokkos::pow(KPP::CMoS * VonKar * KPP::SurfaceLayerExtent, + 1.0_Real / 3.0_Real); +} + +void testWindOnlyCoefficients() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-Density", Mesh->NCellsSize, + VCoord->NVertLayers); + Array1DReal UStar("KPPMixTest-UStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-B0", Mesh->NCellsSize); + deepCopy(Density, RhoSw); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + + const auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + const auto VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + const auto NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + const auto TurbVelH = + createHostMirrorCopy(KPPInstance->TurbulentVelocityScale); + + const Real Sigma = -0.5_Real; + const Real Shape = 0.125_Real; + const Real TurbVel = VonKar * 0.02_Real; + const Real ExpectedMix = TestOBLDepth * TurbVel * Shape; + const Real ExpectedNonLocal = nonLocalNormalization() * Shape; + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(VertDiffH(ICell, 2), ExpectedMix, RTol, ATol) || + !isApprox(VertViscH(ICell, 2), ExpectedMix, RTol, ATol) || + !isApprox(TurbVelH(ICell, 2), TurbVel, RTol, ATol) || + !isApprox(NonLocalH(ICell, 2), ExpectedNonLocal, RTol, ATol)) { + ++NumErrors; + } + if (!isApprox(KPP::kppShapeMomentum(Sigma), Shape, RTol, ATol) || + VertDiffH(ICell, 0) != 0.0_Real || VertViscH(ICell, 0) != 0.0_Real || + VertDiffH(ICell, 4) != 0.0_Real || VertViscH(ICell, 4) != 0.0_Real || + VertDiffH(ICell, 5) != 0.0_Real || NonLocalH(ICell, 5) != 0.0_Real) { + ++NumErrors; + } + } + checkResult("wind-only coefficients", NumErrors); +} + +void testConvectionOnlyCoefficients() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-ConvDensity", Mesh->NCellsSize, + VCoord->NVertLayers); + Array1DReal UStar("KPPMixTest-ConvUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-ConvB0", Mesh->NCellsSize); + deepCopy(Density, RhoSw); + deepCopy(UStar, 0.0_Real); + deepCopy(B0, -1.0e-7_Real); + + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + + const auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + const auto VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + const auto TurbVelH = + createHostMirrorCopy(KPPInstance->TurbulentVelocityScale); + const Real SigmaLoc = KPP::SurfaceLayerExtent; + const Real WM = VonKar * Kokkos::pow(KPP::CMoM * SigmaLoc * TestOBLDepth * + VonKar * 1.0e-7_Real, + 1.0_Real / 3.0_Real); + const Real WS = VonKar * Kokkos::pow(KPP::CMoS * SigmaLoc * TestOBLDepth * + VonKar * 1.0e-7_Real, + 1.0_Real / 3.0_Real); + constexpr Real Shape = 0.125_Real; + const Real ExpectedVisc = TestOBLDepth * WM * Shape; + const Real ExpectedDiff = TestOBLDepth * WS * Shape; + + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(VertViscH(ICell, 2), ExpectedVisc, RTol, ATol) || + !isApprox(VertDiffH(ICell, 2), ExpectedDiff, RTol, ATol) || + !isApprox(TurbVelH(ICell, 2), WS, RTol, ATol) || + !(VertDiffH(ICell, 2) > VertViscH(ICell, 2))) { + ++NumErrors; + } + } + checkResult("convection-only coefficients", NumErrors); +} + +void testNonLocalProfileModes() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-ProfileDensity", Mesh->NCellsSize, + VCoord->NVertLayers); + Array1DReal UStar("KPPMixTest-ProfileUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-ProfileB0", Mesh->NCellsSize); + deepCopy(Density, RhoSw); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + + KPPInstance->UseEnhancedDiffusion = false; + const Real Normalization = nonLocalNormalization(); + int NumErrors = 0; + + // The non-local flux follows the scalar diffusivity shape, so at sigma=-0.5 + // it is 0.125 * C_s and it vanishes at the surface and at the OBL base. + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + auto NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (NonLocalH(ICell, 0) != 0.0_Real || + !isApprox(NonLocalH(ICell, 2), 0.125_Real * Normalization, RTol, + ATol) || + NonLocalH(ICell, 4) != 0.0_Real) { + ++NumErrors; + } + } + + // Without interior coefficients there is nothing to match, so MatchBoth + // must reduce exactly to SimpleShapes. + const auto SimpleShapesH = NonLocalH; + KPPInstance->MatchTechnique = KPPMatchType::MatchBoth; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + for (I4 K = 0; K <= VCoord->NVertLayers; ++K) { + if (NonLocalH(ICell, K) != SimpleShapesH(ICell, K)) { + ++NumErrors; + } + } + } + + checkResult("non-local profile modes", NumErrors); +} + +void testMatchBothInteriorCoefficients() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-MatchDensity", Mesh->NCellsSize, + NVertLayers); + Array1DReal UStar("KPPMixTest-MatchUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-MatchB0", Mesh->NCellsSize); + Array2DReal InteriorDiff("KPPMixTest-InteriorDiff", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal InteriorVisc("KPPMixTest-InteriorVisc", Mesh->NCellsSize, + NVertLayers + 1); + constexpr Real ExpectedInteriorDiff = 2.0e-3_Real; + constexpr Real ExpectedInteriorVisc = 4.0e-3_Real; + deepCopy(Density, RhoSw); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + deepCopy(InteriorDiff, ExpectedInteriorDiff); + deepCopy(InteriorVisc, ExpectedInteriorVisc); + + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->MatchTechnique = KPPMatchType::MatchBoth; + KPPInstance->computeMixingCoefficients(Density, UStar, B0, InteriorDiff, + InteriorVisc); + + const auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + const auto VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + const auto NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + constexpr Real Sigma = -0.5_Real; + constexpr Real SimpleShape = 0.125_Real; + constexpr Real SmoothAtSigma = 0.5_Real; + const Real TurbVel = VonKar * 0.02_Real; + const Real ExpectedDiffMid = TestOBLDepth * TurbVel * SimpleShape + + SmoothAtSigma * ExpectedInteriorDiff; + const Real ExpectedViscMid = TestOBLDepth * TurbVel * SimpleShape + + SmoothAtSigma * ExpectedInteriorVisc; + const Real MatchDiffShape = ExpectedInteriorDiff / (TestOBLDepth * TurbVel); + // The non-local shape is independent of MatchTechnique, so gamma still + // follows the unmatched scalar shape and vanishes at the OBL base. + const Real ExpectedNonLocal = + nonLocalNormalization() * KPP::kppShapeScalar(Sigma); + + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(VertDiffH(ICell, 2), ExpectedDiffMid, RTol, ATol) || + !isApprox(VertViscH(ICell, 2), ExpectedViscMid, RTol, ATol) || + !isApprox(NonLocalH(ICell, 2), ExpectedNonLocal, RTol, ATol) || + !isApprox(VertDiffH(ICell, 4), ExpectedInteriorDiff, RTol, ATol) || + !isApprox(VertViscH(ICell, 4), ExpectedInteriorVisc, RTol, ATol) || + !isApprox(VertDiffH(ICell, 5), ExpectedInteriorDiff, RTol, ATol) || + !isApprox(VertViscH(ICell, 5), ExpectedInteriorVisc, RTol, ATol) || + NonLocalH(ICell, 4) != 0.0_Real || NonLocalH(ICell, 5) != 0.0_Real) { + ++NumErrors; + } + } + checkResult("MatchBoth interior coefficients", NumErrors); +} + +void testEnhancedDiffusion() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-EnhancedDensity", Mesh->NCellsSize, + NVertLayers); + Array1DReal UStar("KPPMixTest-EnhancedUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-EnhancedB0", Mesh->NCellsSize); + deepCopy(Density, RhoSw); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (VertDiffH(ICell, 4) != 0.0_Real) { + ++NumErrors; + } + } + + KPPInstance->UseEnhancedDiffusion = true; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + auto VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + auto NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + constexpr Real OutsideDelta = 0.5_Real; + const Real OutsideSigma = -35.0_Real / TestOBLDepth; + const Real OutsideProfile = + TestOBLDepth * VonKar * 0.02_Real * KPP::kppShapeScalar(OutsideSigma); + const Real ExpectedOutside = OutsideDelta * (1.0_Real - OutsideDelta) * + (1.0_Real - OutsideDelta) * OutsideProfile; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(VertDiffH(ICell, 4), ExpectedOutside, RTol, ATol) || + !isApprox(VertViscH(ICell, 4), ExpectedOutside, RTol, ATol) || + NonLocalH(ICell, 4) != 0.0_Real) { + ++NumErrors; + } + } + + constexpr Real InsideOBLDepth = 32.0_Real; + deepCopy(KPPInstance->BoundaryLayerDepth, InsideOBLDepth); + deepCopy(KPPInstance->IndexBoundaryLayerDepth, TestOBLIndex); + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + + constexpr Real InsideDelta = 0.7_Real; + constexpr Real OneMinusInsideDelta = 1.0_Real - InsideDelta; + const Real KtupSigma = -25.0_Real / InsideOBLDepth; + const Real TargetSigma = -30.0_Real / InsideOBLDepth; + const Real KtupProfile = + InsideOBLDepth * VonKar * 0.02_Real * KPP::kppShapeScalar(KtupSigma); + const Real TargetProfile = + InsideOBLDepth * VonKar * 0.02_Real * KPP::kppShapeScalar(TargetSigma); + const Real ExpectedInside = + InsideDelta * (OneMinusInsideDelta * OneMinusInsideDelta * KtupProfile + + InsideDelta * InsideDelta * TargetProfile); + const Real ExpectedInsideNonLocal = nonLocalNormalization() * + KPP::kppShapeScalar(TargetSigma) * + ExpectedInside / TargetProfile; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(VertDiffH(ICell, 3), ExpectedInside, RTol, ATol) || + !isApprox(VertViscH(ICell, 3), ExpectedInside, RTol, ATol) || + !isApprox(NonLocalH(ICell, 3), ExpectedInsideNonLocal, RTol, ATol)) { + ++NumErrors; + } + } + + Array2DReal InteriorDiff("KPPMixTest-EnhancedInteriorDiff", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal InteriorVisc("KPPMixTest-EnhancedInteriorVisc", Mesh->NCellsSize, + NVertLayers + 1); + constexpr Real InteriorDiffValue = 2.0e-3_Real; + constexpr Real InteriorViscValue = 4.0e-3_Real; + deepCopy(InteriorDiff, InteriorDiffValue); + deepCopy(InteriorVisc, InteriorViscValue); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + deepCopy(KPPInstance->BoundaryLayerDepth, TestOBLDepth); + deepCopy(KPPInstance->IndexBoundaryLayerDepth, TestOBLIndex); + KPPInstance->MatchTechnique = KPPMatchType::MatchBoth; + KPPInstance->computeMixingCoefficients(Density, UStar, B0, InteriorDiff, + InteriorVisc); + VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + const Real InteriorKtupSigma = -35.0_Real / TestOBLDepth; + const Real DiffMatchShape = + InteriorDiffValue / (TestOBLDepth * VonKar * 0.02_Real); + const Real ViscMatchShape = + InteriorViscValue / (TestOBLDepth * VonKar * 0.02_Real); + const Real DiffKtup = + TestOBLDepth * VonKar * 0.02_Real * + KPP::kppShapeMatched(InteriorKtupSigma, DiffMatchShape); + const Real ViscKtup = + TestOBLDepth * VonKar * 0.02_Real * + KPP::kppShapeMatched(InteriorKtupSigma, ViscMatchShape); + const Real ExpectedInteriorEnhancedDiff = + 0.625_Real * InteriorDiffValue + 0.125_Real * DiffKtup; + const Real ExpectedInteriorEnhancedVisc = + 0.625_Real * InteriorViscValue + 0.125_Real * ViscKtup; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(VertDiffH(ICell, 4), ExpectedInteriorEnhancedDiff, RTol, + ATol) || + !isApprox(VertViscH(ICell, 4), ExpectedInteriorEnhancedVisc, RTol, + ATol)) { + ++NumErrors; + } + } + + deepCopy(UStar, 0.0_Real); + deepCopy(B0, 0.0_Real); + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + deepCopy(KPPInstance->BoundaryLayerDepth, InsideOBLDepth); + deepCopy(KPPInstance->IndexBoundaryLayerDepth, TestOBLIndex); + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (VertDiffH(ICell, 3) != 0.0_Real || VertViscH(ICell, 3) != 0.0_Real || + NonLocalH(ICell, 3) != 0.0_Real) { + ++NumErrors; + } + } + checkResult("enhanced diffusion", NumErrors); +} + +void testStableAndZeroForcing() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-StableDensity", Mesh->NCellsSize, + VCoord->NVertLayers); + Array1DReal UStar("KPPMixTest-StableUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-StableB0", Mesh->NCellsSize); + deepCopy(Density, RhoSw); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 1.0e-7_Real); + + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + auto NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!(VertDiffH(ICell, 2) > 0.0_Real) || + NonLocalH(ICell, 2) != 0.0_Real) { + ++NumErrors; + } + } + + deepCopy(UStar, 0.0_Real); + deepCopy(B0, 0.0_Real); + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + const auto VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (VertDiffH(ICell, 2) != 0.0_Real || VertViscH(ICell, 2) != 0.0_Real) { + ++NumErrors; + } + } + checkResult("stable and zero forcing", NumErrors); +} + +void testCoefficientVerticalDomainEdges() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-EdgeDensity", Mesh->NCellsSize, NVertLayers); + Array1DReal UStar("KPPMixTest-EdgeUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-EdgeB0", Mesh->NCellsSize); + deepCopy(Density, -99.0_Real); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + OMEGA_SCOPE(BoundaryLayerDepth, KPPInstance->BoundaryLayerDepth); + OMEGA_SCOPE(IndexBoundaryLayerDepth, KPPInstance->IndexBoundaryLayerDepth); + parallelFor( + "KPPMixTest-SetPartialColumn", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 ICell) { + MinLayerCell(ICell) = 2; + MaxLayerCell(ICell) = 4; + BoundaryLayerDepth(ICell) = 40.0_Real; + IndexBoundaryLayerDepth(ICell) = 3; + }); + + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + auto VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + auto NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + const Real ExpectedPartial = + 40.0_Real * VonKar * 0.02_Real * KPP::kppShapeScalar(-0.5_Real); + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (VertDiffH(ICell, 0) != 0.0_Real || VertDiffH(ICell, 1) != 0.0_Real || + !isApprox(VertDiffH(ICell, 2), ExpectedPartial, RTol, ATol) || + !isApprox(VertViscH(ICell, 2), ExpectedPartial, RTol, ATol) || + !(NonLocalH(ICell, 2) > 0.0_Real) || + VertDiffH(ICell, 4) != 0.0_Real || VertDiffH(ICell, 5) != 0.0_Real) { + ++NumErrors; + } + } + + parallelFor( + "KPPMixTest-SetOneLayerColumn", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 ICell) { + MinLayerCell(ICell) = 2; + MaxLayerCell(ICell) = 2; + BoundaryLayerDepth(ICell) = 25.0_Real; + IndexBoundaryLayerDepth(ICell) = 2; + }); + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + const Real ExpectedOneLayer = + 25.0_Real * VonKar * 0.02_Real * KPP::kppShapeScalar(-0.8_Real); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(VertDiffH(ICell, 2), ExpectedOneLayer, RTol, ATol) || + !isApprox(VertViscH(ICell, 2), ExpectedOneLayer, RTol, ATol) || + !(NonLocalH(ICell, 2) > 0.0_Real) || + VertDiffH(ICell, 3) != 0.0_Real || VertViscH(ICell, 3) != 0.0_Real || + NonLocalH(ICell, 3) != 0.0_Real) { + ++NumErrors; + } + } + setCoefficientTestGeometry(); + checkResult("coefficient vertical-domain edges", NumErrors); +} + +void testCoefficientInvalidWetBounds() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + + Array2DReal Density("KPPMixTest-InvalidBoundsDensity", Mesh->NCellsSize, + NVertLayers); + Array1DReal UStar("KPPMixTest-InvalidBoundsUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-InvalidBoundsB0", Mesh->NCellsSize); + deepCopy(Density, -99.0_Real); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, -1.0e-7_Real); + deepCopy(KPPInstance->VertDiff, -7.0_Real); + deepCopy(KPPInstance->VertVisc, -7.0_Real); + deepCopy(KPPInstance->VertNonLocalFlux, -7.0_Real); + deepCopy(KPPInstance->TurbulentVelocityScale, -7.0_Real); + + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + parallelFor( + "KPPMixTest-SetInvalidWetBounds", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 ICell) { + MinLayerCell(ICell) = 2; + MaxLayerCell(ICell) = 1; + }); + + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + + const auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + const auto VertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + const auto NonLocalH = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + const auto TurbVelH = + createHostMirrorCopy(KPPInstance->TurbulentVelocityScale); + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + for (I4 K = 0; K <= NVertLayers; ++K) { + if (VertDiffH(ICell, K) != 0.0_Real || + VertViscH(ICell, K) != 0.0_Real || + NonLocalH(ICell, K) != 0.0_Real || + TurbVelH(ICell, K) != 0.0_Real) { + ++NumErrors; + } + } + } + + setCoefficientTestGeometry(); + checkResult("coefficient invalid wet bounds", NumErrors); +} + +void testConfiguredValues() { + Config VertMixConfig("VertMix"); + Config KPPConfig("KPP"); + Error Err; + Err += Config::getOmegaConfig()->get(VertMixConfig); + Err += VertMixConfig.get(KPPConfig); + + bool ExpectedEnabled = false; + bool ExpectedSmoothing = false; + bool ExpectedEnhanced = false; + bool ExpectedDebug = true; + Real ExpectedCriticalRi = 0.0_Real; + Real ExpectedLangmuirIce = 0.0_Real; + Real ExpectedMinimumOBLIce = 0.0_Real; + Real ExpectedMinimumOBL = 0.0_Real; + std::string ExpectedMatch; + std::string ExpectedInterp; + Err += KPPConfig.get("Enable", ExpectedEnabled); + Err += KPPConfig.get("UseBLDSmoothing", ExpectedSmoothing); + Err += KPPConfig.get("UseEnhancedDiffusion", ExpectedEnhanced); + Err += KPPConfig.get("DebugDiagnostics", ExpectedDebug); + Err += KPPConfig.get("CriticalBulkRichardsonNumber", ExpectedCriticalRi); + Err += KPPConfig.get("IceFractionThresholdForLangmuir", ExpectedLangmuirIce); + Err += KPPConfig.get("IceFractionThresholdForMinimumOBL", + ExpectedMinimumOBLIce); + Err += KPPConfig.get("MinimumOBLUnderSeaIce", ExpectedMinimumOBL); + Err += KPPConfig.get("MatchTechnique", ExpectedMatch); + Err += KPPConfig.get("InterpType2", ExpectedInterp); + CHECK_ERROR_ABORT(Err, "KPPMixTest: unable to read configured KPP values"); + + const KPPMatchType ExpectedMatchType = ExpectedMatch == "MatchBoth" + ? KPPMatchType::MatchBoth + : KPPMatchType::SimpleShapes; + + const KPPMix *KPPInstance = KPPMix::getInstance(); + int NumErrors = 0; + if (KPPInstance->Enabled != ExpectedEnabled || + KPPInstance->UseBLDSmoothing != ExpectedSmoothing || + KPPInstance->UseEnhancedDiffusion != ExpectedEnhanced || + KPPInstance->DebugDiagnostics != ExpectedDebug || + KPPInstance->MatchTechnique != ExpectedMatchType || + KPPInstance->InterpType2Str != ExpectedInterp || + !isApprox(KPPInstance->CriticalRichardson, ExpectedCriticalRi, RTol, + ATol) || + !isApprox(KPPInstance->IceFractionThresholdForLangmuir, + ExpectedLangmuirIce, RTol, ATol) || + !isApprox(KPPInstance->IceFractionThresholdForMinimumOBL, + ExpectedMinimumOBLIce, RTol, ATol) || + !isApprox(KPPInstance->MinimumOBLUnderSeaIce, ExpectedMinimumOBL, RTol, + ATol) || + !isApprox(KPPInstance->SurfaceLayerExtent, 0.1_Real, RTol, ATol) || + !KPPInstance->UseLangmuirCirculation || + !isApprox(KPPInstance->BackgroundVisc, 1.0e-4_Real, RTol, ATol) || + !isApprox(KPPInstance->BackgroundDiff, 1.0e-5_Real, RTol, ATol)) { + ++NumErrors; + } + checkResult("configured values and optional defaults", NumErrors); +} + +void testBoundaryLayerDepth() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); + + Array2DReal Density("KPPMixTest-BLDDensity", Mesh->NCellsSize, NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-BLDNormalVelocity", Mesh->NEdgesSize, + NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-BLDTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-BLDUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-BLDB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-BLDBVF", Mesh->NCellsSize, NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-BLDIce", Mesh->NCellsSize); + Array1DReal Wind; + + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + deepCopy(BVF, 1.0_Real); + deepCopy(IceFraction, 0.0_Real); + + constexpr Real RiScaling = 1.0_Real - 0.5_Real * KPP::SurfaceLayerExtent; + constexpr Real TestN = 1.0_Real; + const Real UnresolvedShearConstant = + Kokkos::sqrt(0.2_Real / (KPP::CMoS * KPP::SurfaceLayerExtent)) / + (VonKar * VonKar); + const Real WindTurbulentScale = VonKar * 0.02_Real; + parallelFor( + "KPPMixTest-SetRichardsonDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + Real TargetRi = 0.0_Real; + if (K == 1) { + TargetRi = 0.1_Real; + } else if (K >= 2) { + TargetRi = 0.4_Real; + } + const Real ZCenter = LayerThickness * (K + 0.5_Real); + const Real Vt2 = 1.7_Real * UnresolvedShearConstant * ZCenter * + TestN * WindTurbulentScale / 0.25_Real; + const Real DeltaRho = + TargetRi * Vt2 * RhoSw / (RiScaling * Gravity * ZCenter); + Density(ICell, K) = RhoSw + DeltaRho; + }); + + KPPInstance->CriticalRichardson = 0.25_Real; + KPPInstance->SurfaceLayerExtent = KPP::SurfaceLayerExtent; + KPPInstance->UseLangmuirCirculation = false; + KPPInstance->UseBLDSmoothing = false; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + + auto BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + auto BLDIndexH = createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + auto BulkRiH = createHostMirrorCopy(KPPInstance->BulkRichardsonNumber); + const auto BulkShearH = + createHostMirrorCopy(KPPInstance->BulkRichardsonShear); + const auto UnresolvedShearH = + createHostMirrorCopy(KPPInstance->UnresolvedShear); + const auto BuoyancyJumpH = createHostMirrorCopy(KPPInstance->BuoyancyJump); + + constexpr Real RiAbove = 0.1_Real; + constexpr Real RiBelow = 0.4_Real; + constexpr Real ZAbove = 15.0_Real; + constexpr Real Slope = 0.01_Real; + constexpr Real Quadratic = 0.002_Real; + const Real Discriminant = + Slope * Slope - 4.0_Real * Quadratic * (RiAbove - 0.25_Real); + const Real ExpectedBLD = + ZAbove + (-Slope + Kokkos::sqrt(Discriminant)) / (2.0_Real * Quadratic); + const Real ExpectedVt2 = 1.7_Real * UnresolvedShearConstant * 25.0_Real * + TestN * WindTurbulentScale / 0.25_Real; + const Real ExpectedDeltaB = 0.4_Real * ExpectedVt2 / (RiScaling * 25.0_Real); + + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BulkRiH(ICell, 2), RiAbove, BLDRTol, ATol) || + !isApprox(BulkRiH(ICell, 3), RiBelow, BLDRTol, ATol) || + !isApprox(BLDH(ICell), ExpectedBLD, BLDRTol, ATol) || + BLDIndexH(ICell) != 2 || + !isApprox(BulkShearH(ICell, 3), 1.0e-15_Real, RTol, 1.0e-16_Real) || + !isApprox(UnresolvedShearH(ICell, 3), ExpectedVt2, RTol, ATol) || + !isApprox(BuoyancyJumpH(ICell, 3), ExpectedDeltaB, BLDRTol, ATol)) { + ++NumErrors; + } + } + checkResult("analytic boundary-layer depth", NumErrors); + + parallelFor( + "KPPMixTest-SetLinearRichardsonDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + const Real TargetRi = K == 0 ? 0.0_Real + : K == 1 ? 0.1_Real + : K == 2 ? 0.2_Real + : 0.3_Real; + const Real ZCenter = LayerThickness * (K + 0.5_Real); + const Real Vt2 = 1.7_Real * UnresolvedShearConstant * ZCenter * + TestN * WindTurbulentScale / 0.25_Real; + const Real DeltaRho = + TargetRi * Vt2 * RhoSw / (RiScaling * Gravity * ZCenter); + Density(ICell, K) = RhoSw + DeltaRho; + }); + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + BLDIndexH = createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + BulkRiH = createHostMirrorCopy(KPPInstance->BulkRichardsonNumber); + NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BulkRiH(ICell, 3), 0.2_Real, BLDRTol, ATol) || + !isApprox(BulkRiH(ICell, 4), 0.3_Real, BLDRTol, ATol) || + !isApprox(BLDH(ICell), 30.0_Real, BLDRTol, ATol) || + BLDIndexH(ICell) != 2) { + ++NumErrors; + } + } + checkResult("boundary-layer linear interpolation", NumErrors); + + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + parallelFor( + "KPPMixTest-SetPartialBLDBounds", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 ICell) { + MinLayerCell(ICell) = 2; + MaxLayerCell(ICell) = 4; + }); + parallelFor( + "KPPMixTest-SetPartialBLDColumn", {Mesh->NCellsAll, NVertLayers + 1}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + if (K < NVertLayers) { + Density(ICell, K) = K >= 2 && K <= 4 ? RhoSw : -99.0_Real; + } + BVF(ICell, K) = K >= 3 && K <= 5 ? 1.0_Real : -99.0_Real; + }); + parallelFor( + "KPPMixTest-SetPartialBLDEdges", {Mesh->NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + const Real Value = K >= 2 && K <= 4 ? 0.0_Real : 99.0_Real; + NormalVelocity(IEdge, K) = Value; + TangentialVelocity(IEdge, K) = Value; + }); + VCoord->minMaxLayerEdge(Halo::getDefault()); + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + BLDIndexH = createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + BulkRiH = createHostMirrorCopy(KPPInstance->BulkRichardsonNumber); + NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BLDH(ICell), 45.0_Real, RTol, ATol) || + BLDIndexH(ICell) != 4 || BulkRiH(ICell, 1) != 0.0_Real || + BulkRiH(ICell, 2) != 0.0_Real) { + ++NumErrors; + } + } + checkResult("boundary-layer partial wet column", NumErrors); + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + deepCopy(BVF, 1.0_Real); + + parallelFor( + "KPPMixTest-RestoreRichardsonDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + Real TargetRi = 0.0_Real; + if (K == 1) { + TargetRi = 0.1_Real; + } else if (K >= 2) { + TargetRi = 0.4_Real; + } + const Real ZCenter = LayerThickness * (K + 0.5_Real); + const Real Vt2 = 1.7_Real * UnresolvedShearConstant * ZCenter * + TestN * WindTurbulentScale / 0.25_Real; + const Real DeltaRho = + TargetRi * Vt2 * RhoSw / (RiScaling * Gravity * ZCenter); + Density(ICell, K) = RhoSw + DeltaRho; + }); + + parallelFor( + "KPPMixTest-SetResolvedShear", {Mesh->NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + NormalVelocity(IEdge, K) = 0.1_Real * K; + TangentialVelocity(IEdge, K) = 0.2_Real * K; + }); + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + const auto ShearedBulkRiH = + createHostMirrorCopy(KPPInstance->BulkRichardsonNumber); + const auto ResolvedShearH = + createHostMirrorCopy(KPPInstance->BulkRichardsonShear); + NumErrors = 0; + constexpr Real ExpectedResolvedShear = 0.2_Real; + const Real ExpectedShearedRi = RiScaling * ExpectedDeltaB * 25.0_Real / + (ExpectedResolvedShear + ExpectedVt2); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(ResolvedShearH(ICell, 3), ExpectedResolvedShear, RTol, + ATol) || + !isApprox(ShearedBulkRiH(ICell, 3), ExpectedShearedRi, BLDRTol, + ATol) || + !(BLDH(ICell) > ExpectedBLD)) { + ++NumErrors; + } + } + checkResult("boundary-layer resolved shear", NumErrors); + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + + deepCopy(Density, RhoSw); + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + BLDIndexH = createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + NumErrors = 0; + const Real DeepestMidpoint = + LayerThickness * (static_cast(NVertLayers) - 0.5_Real); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BLDH(ICell), DeepestMidpoint, RTol, ATol) || + BLDIndexH(ICell) != NVertLayers - 1) { + ++NumErrors; + } + } + checkResult("boundary-layer no-crossing fallback", NumErrors); + + parallelFor( + "KPPMixTest-SetShallowCrossingDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + const Real ZCenter = LayerThickness * (K + 0.5_Real); + const Real TargetRi = K == 0 ? 0.0_Real : 1.0_Real; + const Real Vt2 = 1.7_Real * UnresolvedShearConstant * ZCenter * + TestN * WindTurbulentScale / 0.25_Real; + const Real DeltaRho = + TargetRi * Vt2 * RhoSw / (RiScaling * Gravity * ZCenter); + Density(ICell, K) = RhoSw + DeltaRho; + }); + deepCopy(IceFraction, 0.5_Real); + KPPInstance->IceFractionThresholdForMinimumOBL = 0.15_Real; + KPPInstance->MinimumOBLUnderSeaIce = 25.0_Real; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + BLDIndexH = createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BLDH(ICell), 25.0_Real, RTol, ATol) || + BLDIndexH(ICell) != 2) { + ++NumErrors; + } + } + checkResult("boundary-layer sea-ice minimum", NumErrors); +} + +void testBoundaryLayerNonuniformThickness() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + + Array2DReal Density("KPPMixTest-NonuniformDensity", Mesh->NCellsSize, + NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-NonuniformNormalVelocity", + Mesh->NEdgesSize, NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-NonuniformTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-NonuniformUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-NonuniformB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-NonuniformBVF", Mesh->NCellsSize, + NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-NonuniformIce", Mesh->NCellsSize); + Array1DReal Wind; + + OMEGA_SCOPE(GeomZInterface, VCoord->GeomZInterface); + OMEGA_SCOPE(GeomZMid, VCoord->GeomZMid); + OMEGA_SCOPE(SshCell, VCoord->SshCell); + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + + constexpr Real RiScaling = 1.0_Real - 0.5_Real * KPP::SurfaceLayerExtent; + constexpr Real TestN = 1.0_Real; + constexpr Real TestUStar = 0.02_Real; + const Real UnresolvedShearConstant = + Kokkos::sqrt(0.2_Real / (KPP::CMoS * KPP::SurfaceLayerExtent)) / + (VonKar * VonKar); + const Real WindTurbulentScale = VonKar * TestUStar; + + deepCopy(Density, RhoSw); + deepCopy(UStar, TestUStar); + deepCopy(B0, 0.0_Real); + deepCopy(BVF, TestN * TestN); + deepCopy(IceFraction, 0.0_Real); + parallelFor( + "KPPMixTest-SetNonuniformVelocity", {Mesh->NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + NormalVelocity(IEdge, K) = K == 0 ? 0.1_Real + : K == 1 ? 0.4_Real + : K == 2 ? 0.1_Real + : K == 3 ? 0.6_Real + : 0.6_Real; + TangentialVelocity(IEdge, K) = K == 0 ? 0.2_Real + : K == 1 ? 0.5_Real + : K == 2 ? 0.2_Real + : K == 3 ? 0.8_Real + : 0.8_Real; + }); + + parallelFor( + "KPPMixTest-SetNonuniformColumn", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 ICell) { + MinLayerCell(ICell) = 0; + MaxLayerCell(ICell) = NVertLayers - 1; + SshCell(ICell) = 0.0_Real; + + for (I4 K = 0; K <= NVertLayers; ++K) { + Real Depth = 25.0_Real + 25.0_Real * (K - 4); + if (K == 0) { + Depth = 0.0_Real; + } else if (K == 1) { + Depth = 1.0_Real; + } else if (K == 2) { + Depth = 3.0_Real; + } else if (K == 3) { + Depth = 10.0_Real; + } else if (K == 4) { + Depth = 25.0_Real; + } + GeomZInterface(ICell, K) = -Depth; + if (K < NVertLayers) { + Real NextDepth = 25.0_Real + 25.0_Real * (K - 3); + if (K == 0) { + NextDepth = 1.0_Real; + } else if (K == 1) { + NextDepth = 3.0_Real; + } else if (K == 2) { + NextDepth = 10.0_Real; + } else if (K == 3) { + NextDepth = 25.0_Real; + } + GeomZMid(ICell, K) = -0.5_Real * (Depth + NextDepth); + } + } + + constexpr Real Shear1 = 0.18_Real; + constexpr Real Shear3 = 0.25_Real; + const Real Vt2Layer1 = 1.7_Real * UnresolvedShearConstant * 2.0_Real * + TestN * WindTurbulentScale / 0.25_Real; + const Real Vt2Layer2 = 1.7_Real * UnresolvedShearConstant * 6.5_Real * + TestN * WindTurbulentScale / 0.25_Real; + const Real Vt2Layer3 = 1.7_Real * UnresolvedShearConstant * + 17.5_Real * TestN * WindTurbulentScale / + 0.25_Real; + const Real DeltaRho1 = 0.05_Real * (Shear1 + Vt2Layer1) * RhoSw / + (RiScaling * Gravity * 2.0_Real); + const Real DeltaRho2 = + 0.10_Real * Vt2Layer2 * RhoSw / (RiScaling * Gravity * 6.5_Real); + const Real DeltaRho3 = 0.40_Real * (Shear3 + Vt2Layer3) * RhoSw / + (RiScaling * Gravity * 17.5_Real); + Density(ICell, 0) = RhoSw; + Density(ICell, 1) = RhoSw + DeltaRho1; + Density(ICell, 2) = RhoSw + DeltaRho2; + + // At k=3, the 2.5 m surface layer contains the unequal 1 m and + // 2 m layers. Construct rho(3) relative to that weighted mean. + const Real WeightedSurfaceDensity = + (Density(ICell, 0) + 2.0_Real * Density(ICell, 1)) / 3.0_Real; + Density(ICell, 3) = WeightedSurfaceDensity + DeltaRho3; + }); + VCoord->minMaxLayerEdge(Halo::getDefault()); + + KPPInstance->CriticalRichardson = 0.25_Real; + KPPInstance->SurfaceLayerExtent = KPP::SurfaceLayerExtent; + KPPInstance->UseLangmuirCirculation = false; + KPPInstance->UseBLDSmoothing = false; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + + const auto BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + const auto BLDIndexH = + createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + const auto BulkRiH = createHostMirrorCopy(KPPInstance->BulkRichardsonNumber); + const auto BulkShearH = + createHostMirrorCopy(KPPInstance->BulkRichardsonShear); + const auto BuoyancyJumpH = createHostMirrorCopy(KPPInstance->BuoyancyJump); + + constexpr Real ZPrevious = 2.0_Real; + constexpr Real ZAbove = 6.5_Real; + constexpr Real ZBelow = 17.5_Real; + constexpr Real RiPrevious = 0.05_Real; + constexpr Real RiAbove = 0.10_Real; + constexpr Real RiBelow = 0.40_Real; + const Real Slope = (RiAbove - RiPrevious) / (ZAbove - ZPrevious); + const Real H = ZBelow - ZAbove; + const Real Quadratic = (RiBelow - RiAbove - Slope * H) / (H * H); + const Real Discriminant = + Slope * Slope - 4.0_Real * Quadratic * (RiAbove - 0.25_Real); + const Real ExpectedBLD = + ZAbove + (-Slope + Kokkos::sqrt(Discriminant)) / (2.0_Real * Quadratic); + const Real ExpectedDeltaB = + 0.40_Real * + (0.25_Real + 1.7_Real * UnresolvedShearConstant * ZBelow * TestN * + WindTurbulentScale / 0.25_Real) / + (RiScaling * ZBelow); + + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BulkRiH(ICell, 2), RiPrevious, BLDRTol, ATol) || + !isApprox(BulkRiH(ICell, 3), RiAbove, BLDRTol, ATol) || + !isApprox(BulkRiH(ICell, 4), RiBelow, BLDRTol, ATol) || + !isApprox(BulkShearH(ICell, 2), 0.18_Real, RTol, ATol) || + !isApprox(BulkShearH(ICell, 4), 0.25_Real, RTol, ATol) || + !isApprox(BuoyancyJumpH(ICell, 4), ExpectedDeltaB, BLDRTol, ATol) || + !isApprox(BLDH(ICell), ExpectedBLD, BLDRTol, ATol) || + BLDIndexH(ICell) != 3) { + ++NumErrors; + } + } + + checkResult("boundary-layer nonuniform thickness", NumErrors); + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); +} + +// KPP depths are measured below the free surface, so rigidly translating the +// whole column by the sea surface height must leave every KPP output unchanged. +void testSshOffsetInvariance() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + + Array2DReal Density("KPPMixTest-SshDensity", Mesh->NCellsSize, NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-SshNormalVelocity", Mesh->NEdgesSize, + NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-SshTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-SshUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-SshB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-SshBVF", Mesh->NCellsSize, NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-SshIce", Mesh->NCellsSize); + Array1DReal Wind; + + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, -1.0e-7_Real); + deepCopy(BVF, 1.0_Real); + deepCopy(IceFraction, 0.0_Real); + + constexpr Real RiScaling = 1.0_Real - 0.5_Real * KPP::SurfaceLayerExtent; + constexpr Real TestN = 1.0_Real; + const Real UnresolvedShearConstant = + Kokkos::sqrt(0.2_Real / (KPP::CMoS * KPP::SurfaceLayerExtent)) / + (VonKar * VonKar); + const Real WindTurbulentScale = VonKar * 0.02_Real; + parallelFor( + "KPPMixTest-SetSshDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + Real TargetRi = 0.0_Real; + if (K == 1) { + TargetRi = 0.1_Real; + } else if (K >= 2) { + TargetRi = 0.4_Real; + } + const Real ZCenter = LayerThickness * (K + 0.5_Real); + const Real Vt2 = 1.7_Real * UnresolvedShearConstant * ZCenter * + TestN * WindTurbulentScale / 0.25_Real; + const Real DeltaRho = + TargetRi * Vt2 * RhoSw / (RiScaling * Gravity * ZCenter); + Density(ICell, K) = RhoSw + DeltaRho; + }); + + KPPInstance->CriticalRichardson = 0.25_Real; + KPPInstance->SurfaceLayerExtent = KPP::SurfaceLayerExtent; + KPPInstance->UseLangmuirCirculation = false; + KPPInstance->UseBLDSmoothing = false; + KPPInstance->UseEnhancedDiffusion = true; + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + + auto runWithSsh = [&](Real Ssh) { + setCoefficientTestGeometry(Ssh); + VCoord->minMaxLayerEdge(Halo::getDefault()); + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + }; + + runWithSsh(0.0_Real); + const auto BLDRef = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + const auto BLDIndexRef = + createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + const auto DiffRef = createHostMirrorCopy(KPPInstance->VertDiff); + const auto ViscRef = createHostMirrorCopy(KPPInstance->VertVisc); + const auto NonLocalRef = createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + + // Large enough that an uncorrected geoid-referenced depth shifts the OBL + // search across the analytic crossing. + constexpr Real TestSsh = 0.75_Real; + runWithSsh(TestSsh); + const auto BLDShift = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + const auto BLDIndexShift = + createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + const auto DiffShift = createHostMirrorCopy(KPPInstance->VertDiff); + const auto ViscShift = createHostMirrorCopy(KPPInstance->VertVisc); + const auto NonLocalShift = + createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + // Guard against a degenerate all-zero comparison. + if (!(BLDRef(ICell) > 0.0_Real)) { + ++NumErrors; + continue; + } + if (!isApprox(BLDShift(ICell), BLDRef(ICell), BLDRTol, ATol) || + BLDIndexShift(ICell) != BLDIndexRef(ICell)) { + ++NumErrors; + continue; + } + for (I4 K = 0; K <= NVertLayers; ++K) { + if (!isApprox(DiffShift(ICell, K), DiffRef(ICell, K), BLDRTol, ATol) || + !isApprox(ViscShift(ICell, K), ViscRef(ICell, K), BLDRTol, ATol) || + !isApprox(NonLocalShift(ICell, K), NonLocalRef(ICell, K), BLDRTol, + ATol)) { + ++NumErrors; + break; + } + } + } + + checkResult("sea surface height offset invariance", NumErrors); + KPPInstance->UseBLDSmoothing = true; + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); +} + +void testBoundaryLayerEdgeFallbacks() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); + + Array2DReal Density("KPPMixTest-EdgeFallbackDensity", Mesh->NCellsSize, + NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-EdgeFallbackNormalVelocity", + Mesh->NEdgesSize, NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-EdgeFallbackTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-EdgeFallbackUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-EdgeFallbackB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-EdgeFallbackBVF", Mesh->NCellsSize, + NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-EdgeFallbackIce", Mesh->NCellsSize); + Array1DReal Wind; + Array1DReal OriginalDcEdge("KPPMixTest-OriginalDcEdge", + Mesh->DcEdge.extent(0)); + deepCopy(OriginalDcEdge, Mesh->DcEdge); + + deepCopy(Density, RhoSw); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, 0.0_Real); + deepCopy(BVF, 1.0_Real); + deepCopy(IceFraction, 0.0_Real); + parallelFor( + "KPPMixTest-SetEdgeFallbackVelocity", {Mesh->NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + NormalVelocity(IEdge, K) = 0.1_Real * K; + TangentialVelocity(IEdge, K) = 0.2_Real * K; + }); + + KPPInstance->CriticalRichardson = 0.25_Real; + KPPInstance->SurfaceLayerExtent = KPP::SurfaceLayerExtent; + KPPInstance->UseLangmuirCirculation = false; + KPPInstance->UseBLDSmoothing = false; + + // Zero geometric weights force the equal weighting fallback over all + // vertically valid edges. + deepCopy(Mesh->DcEdge, 0.0_Real); + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + auto BulkShearH = createHostMirrorCopy(KPPInstance->BulkRichardsonShear); + int NumErrors = 0; + constexpr Real ExpectedEqualWeightShear = + 0.2_Real; // (0.2)^2 + (0.4)^2 at k=2 + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BulkShearH(ICell, 3), ExpectedEqualWeightShear, RTol, + ATol)) { + ++NumErrors; + } + } + deepCopy(Mesh->DcEdge, OriginalDcEdge); + + OMEGA_SCOPE(MinLayerEdgeBot, VCoord->MinLayerEdgeBot); + OMEGA_SCOPE(MaxLayerEdgeTop, VCoord->MaxLayerEdgeTop); + deepCopy(MinLayerEdgeBot, -1); + deepCopy(MaxLayerEdgeTop, -1); + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + BulkShearH = createHostMirrorCopy(KPPInstance->BulkRichardsonShear); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(BulkShearH(ICell, 3), 1.0e-15_Real, RTol, 1.0e-16_Real)) { + ++NumErrors; + } + } + + VCoord->minMaxLayerEdge(Halo::getDefault()); + checkResult("boundary-layer edge fallbacks", NumErrors); +} + +void testBoundaryLayerLangmuir() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); + + Array2DReal Density("KPPMixTest-LangmuirDensity", Mesh->NCellsSize, + NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-LangmuirNormalVelocity", + Mesh->NEdgesSize, NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-LangmuirTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-LangmuirUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-LangmuirB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-LangmuirBVF", Mesh->NCellsSize, NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-LangmuirIce", Mesh->NCellsSize); + Array1DReal Wind("KPPMixTest-LangmuirWind", Mesh->NCellsSize); + + constexpr Real TestUStar = 0.02_Real; + constexpr Real TestB0 = -1.0e-7_Real; + constexpr Real TestN = 1.0_Real; + constexpr Real RiScaling = 1.0_Real - 0.5_Real * KPP::SurfaceLayerExtent; + const Real UnresolvedShearConstant = + Kokkos::sqrt(0.2_Real / (KPP::CMoS * KPP::SurfaceLayerExtent)) / + (VonKar * VonKar); + + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + deepCopy(UStar, TestUStar); + deepCopy(B0, TestB0); + deepCopy(BVF, TestN * TestN); + deepCopy(IceFraction, 0.0_Real); + deepCopy(Wind, 10.0_Real); + parallelFor( + "KPPMixTest-SetLangmuirDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + const Real ZDepth = LayerThickness * (K + 1.0_Real); + const Real ZCenter = LayerThickness * (K + 0.5_Real); + const Real Zeta = KPP::SurfaceLayerExtent * ZDepth * VonKar * TestB0 / + (TestUStar * TestUStar * TestUStar); + const Real PhiInv = Kokkos::sqrt(1.0_Real - 16.0_Real * Zeta); + const Real WTurb = VonKar * TestUStar * PhiInv; + const Real Vt2 = 1.7_Real * UnresolvedShearConstant * ZCenter * + TestN * WTurb / 0.25_Real; + const Real TargetRi = + K == 0 ? 0.0_Real : (K == 1 ? 0.1_Real : 0.26_Real); + const Real DeltaRho = + TargetRi * Vt2 * RhoSw / (RiScaling * Gravity * ZCenter); + Density(ICell, K) = RhoSw + DeltaRho; + }); + + KPPInstance->CriticalRichardson = 0.25_Real; + KPPInstance->SurfaceLayerExtent = KPP::SurfaceLayerExtent; + KPPInstance->UseBLDSmoothing = false; + KPPInstance->UseLangmuirCirculation = false; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + Array1DReal DisabledBLD("KPPMixTest-LangmuirDisabledBLD", Mesh->NCellsSize); + Array2DReal DisabledRi("KPPMixTest-LangmuirDisabledRi", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal DisabledVt2("KPPMixTest-LangmuirDisabledVt2", Mesh->NCellsSize, + NVertLayers + 1); + deepCopy(DisabledBLD, KPPInstance->BoundaryLayerDepth); + deepCopy(DisabledRi, KPPInstance->BulkRichardsonNumber); + deepCopy(DisabledVt2, KPPInstance->UnresolvedShear); + const auto DisabledBLDH = createHostMirrorCopy(DisabledBLD); + const auto DisabledRiH = createHostMirrorCopy(DisabledRi); + const auto DisabledVt2H = createHostMirrorCopy(DisabledVt2); + + KPPInstance->UseLangmuirCirculation = true; + KPPInstance->IceFractionThresholdForLangmuir = 0.05_Real; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + Array1DReal EnabledBLD("KPPMixTest-LangmuirEnabledBLD", Mesh->NCellsSize); + Array2DReal EnabledRi("KPPMixTest-LangmuirEnabledRi", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal EnabledVt2("KPPMixTest-LangmuirEnabledVt2", Mesh->NCellsSize, + NVertLayers + 1); + deepCopy(EnabledBLD, KPPInstance->BoundaryLayerDepth); + deepCopy(EnabledRi, KPPInstance->BulkRichardsonNumber); + deepCopy(EnabledVt2, KPPInstance->UnresolvedShear); + const auto EnabledBLDH = createHostMirrorCopy(EnabledBLD); + const auto EnabledRiH = createHostMirrorCopy(EnabledRi); + const auto EnabledVt2H = createHostMirrorCopy(EnabledVt2); + + deepCopy(IceFraction, 0.1_Real); + KPPInstance->IceFractionThresholdForMinimumOBL = 2.0_Real; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + const auto SuppressedBLDH = + createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + const auto SuppressedRiH = + createHostMirrorCopy(KPPInstance->BulkRichardsonNumber); + + constexpr Real ZDepth = 30.0_Real; + constexpr Real ZCenter = 25.0_Real; + const Real Enhancement = Kokkos::sqrt(3.0_Real); + const Real DisabledZeta = KPP::SurfaceLayerExtent * ZDepth * VonKar * + TestB0 / (TestUStar * TestUStar * TestUStar); + const Real EnabledZeta = DisabledZeta * Enhancement; + const Real DisabledWTurb = + VonKar * TestUStar * Kokkos::sqrt(1.0_Real - 16.0_Real * DisabledZeta); + const Real EnabledWTurb = + VonKar * TestUStar * Kokkos::sqrt(1.0_Real - 16.0_Real * EnabledZeta); + const Real ExpectedDisabledVt2 = 1.7_Real * UnresolvedShearConstant * + ZCenter * TestN * DisabledWTurb / 0.25_Real; + const Real ExpectedEnabledVt2 = 1.7_Real * UnresolvedShearConstant * + ZCenter * TestN * EnabledWTurb / 0.25_Real; + const Real ExpectedEnabledRi = + 0.26_Real * ExpectedDisabledVt2 / ExpectedEnabledVt2; + + int RiErrors = 0; + int Vt2Errors = 0; + int DepthErrors = 0; + int SuppressionErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(DisabledRiH(ICell, 3), 0.26_Real, BLDRTol, ATol) || + !isApprox(EnabledRiH(ICell, 3), ExpectedEnabledRi, BLDRTol, ATol)) { + ++RiErrors; + } + if (!isApprox(DisabledVt2H(ICell, 3), ExpectedDisabledVt2, RTol, ATol) || + !isApprox(EnabledVt2H(ICell, 3), ExpectedEnabledVt2, RTol, ATol)) { + ++Vt2Errors; + } + if (!(EnabledBLDH(ICell) > DisabledBLDH(ICell))) { + ++DepthErrors; + } + if (!isApprox(SuppressedRiH(ICell, 3), DisabledRiH(ICell, 3), RTol, + ATol) || + !isApprox(SuppressedBLDH(ICell), DisabledBLDH(ICell), RTol, ATol)) { + ++SuppressionErrors; + } + } + if (RiErrors != 0 || Vt2Errors != 0 || DepthErrors != 0 || + SuppressionErrors != 0) { + LOG_ERROR( + "Langmuir BLD failures: Ri={} Vt2={} depth={} suppression={}; " + "cell 0 disabled Ri={} Vt2={} BLD={}, enabled Ri={} Vt2={} " + "BLD={}, suppressed Ri={} BLD={}; expected enabled Ri={} Vt2={}", + RiErrors, Vt2Errors, DepthErrors, SuppressionErrors, + DisabledRiH(0, 3), DisabledVt2H(0, 3), DisabledBLDH(0), + EnabledRiH(0, 3), EnabledVt2H(0, 3), EnabledBLDH(0), + SuppressedRiH(0, 3), SuppressedBLDH(0), ExpectedEnabledRi, + ExpectedEnabledVt2); + } + KPPInstance->UseLangmuirCirculation = false; + checkResult("boundary-layer Langmuir enhancement and ice suppression", + RiErrors + Vt2Errors + DepthErrors + SuppressionErrors); +} + +void testBoundaryLayerSmoothing() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); + + Array2DReal Density("KPPMixTest-SmoothingDensity", Mesh->NCellsSize, + NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-SmoothingNormalVelocity", + Mesh->NEdgesSize, NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-SmoothingTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-SmoothingUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-SmoothingB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-SmoothingBVF", Mesh->NCellsSize, + NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-SmoothingIce", Mesh->NCellsSize); + Array1DReal Wind; + + constexpr Real TestUStar = 0.02_Real; + constexpr Real TestN = 1.0_Real; + constexpr Real RiScaling = 1.0_Real - 0.5_Real * KPP::SurfaceLayerExtent; + const Real UnresolvedShearConstant = + Kokkos::sqrt(0.2_Real / (KPP::CMoS * KPP::SurfaceLayerExtent)) / + (VonKar * VonKar); + const Real WTurb = VonKar * TestUStar; + + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + deepCopy(UStar, TestUStar); + deepCopy(B0, 0.0_Real); + deepCopy(BVF, TestN * TestN); + deepCopy(IceFraction, 0.0_Real); + parallelFor( + "KPPMixTest-SetSmoothingDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + Real TargetRi = 0.0_Real; + if (ICell % 2 == 0) { + TargetRi = K == 1 ? 0.1_Real : (K >= 2 ? 0.4_Real : 0.0_Real); + } else { + TargetRi = + K == 1 ? 0.05_Real + : (K == 2 ? 0.1_Real : (K >= 3 ? 0.4_Real : 0.0_Real)); + } + const Real ZCenter = LayerThickness * (K + 0.5_Real); + const Real Vt2 = 1.7_Real * UnresolvedShearConstant * ZCenter * + TestN * WTurb / 0.25_Real; + const Real DeltaRho = + TargetRi * Vt2 * RhoSw / (RiScaling * Gravity * ZCenter); + Density(ICell, K) = RhoSw + DeltaRho; + }); + + KPPInstance->CriticalRichardson = 0.25_Real; + KPPInstance->SurfaceLayerExtent = KPP::SurfaceLayerExtent; + KPPInstance->UseLangmuirCirculation = false; + KPPInstance->UseBLDSmoothing = false; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + Array1DReal UnsmoothedBLD("KPPMixTest-UnsmoothedBLD", Mesh->NCellsSize); + deepCopy(UnsmoothedBLD, KPPInstance->BoundaryLayerDepth); + const auto UnsmoothedBLDH = createHostMirrorCopy(UnsmoothedBLD); + + KPPInstance->UseBLDSmoothing = true; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + const auto SmoothedBLDH = + createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + const auto SmoothedIndexH = + createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + + int NumErrors = 0; + int NumChanged = 0; + const Real MinDepth = 0.5_Real * LayerThickness; + const Real MaxDepth = + LayerThickness * (static_cast(NVertLayers) - 0.5_Real); + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + Real WeightedDepth = 0.0_Real; + Real AreaSum = 0.0_Real; + I4 ValidNeighbors = 0; + for (I4 J = 0; J < Mesh->NEdgesOnCellH(ICell); ++J) { + const I4 INeighbor = Mesh->CellsOnCellH(ICell, J); + if (INeighbor == Mesh->NCellsAll) { + continue; + } + WeightedDepth += + 2.0_Real * Mesh->AreaCellH(INeighbor) * UnsmoothedBLDH(INeighbor); + AreaSum += 2.0_Real * Mesh->AreaCellH(INeighbor); + ++ValidNeighbors; + } + if (ValidNeighbors > 0) { + WeightedDepth += + UnsmoothedBLDH(ICell) * ValidNeighbors * Mesh->AreaCellH(ICell); + AreaSum += ValidNeighbors * Mesh->AreaCellH(ICell); + } + Real ExpectedDepth = + AreaSum > 0.0_Real ? WeightedDepth / AreaSum : UnsmoothedBLDH(ICell); + ExpectedDepth = Kokkos::fmax(MinDepth, ExpectedDepth); + ExpectedDepth = Kokkos::fmin(MaxDepth, ExpectedDepth); + + I4 ExpectedIndex = NVertLayers - 1; + for (I4 K = 0; K < NVertLayers - 1; ++K) { + const Real ZAbove = LayerThickness * K; + const Real ZBelow = LayerThickness * (K + 1); + if (ExpectedDepth >= ZAbove && ExpectedDepth <= ZBelow) { + ExpectedIndex = K; + break; + } + } + if (!isApprox(SmoothedBLDH(ICell), ExpectedDepth, BLDRTol, ATol) || + SmoothedIndexH(ICell) != ExpectedIndex) { + ++NumErrors; + } + if (!isApprox(SmoothedBLDH(ICell), UnsmoothedBLDH(ICell), BLDRTol, + ATol)) { + ++NumChanged; + } + } + if (NumChanged == 0) { + ++NumErrors; + } + KPPInstance->UseBLDSmoothing = false; + checkResult("boundary-layer horizontal smoothing", NumErrors); +} + +void testEnabledFullCall() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + setCoefficientTestGeometry(); + VCoord->minMaxLayerEdge(Halo::getDefault()); + + Array2DReal Density("KPPMixTest-FullCallDensity", Mesh->NCellsSize, + NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-FullCallNormalVelocity", + Mesh->NEdgesSize, NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-FullCallTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-FullCallUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-FullCallB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-FullCallBVF", Mesh->NCellsSize, NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-FullCallIce", Mesh->NCellsSize); + Array1DReal Wind; + + parallelFor( + "KPPMixTest-SetFullCallDensity", {Mesh->NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + Density(ICell, K) = RhoSw + 0.01_Real * K; + }); + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, -1.0e-7_Real); + deepCopy(BVF, 1.0e-4_Real); + deepCopy(IceFraction, 0.0_Real); + + KPPInstance->Enabled = true; + KPPInstance->UseLangmuirCirculation = false; + KPPInstance->UseBLDSmoothing = false; + KPPInstance->UseEnhancedDiffusion = false; + KPPInstance->MatchTechnique = KPPMatchType::SimpleShapes; + KPPInstance->computeOBLDepth(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + KPPInstance->computeMixingCoefficients(Density, UStar, B0); + + Array1DReal ExpectedBLD("KPPMixTest-ExpectedBLD", Mesh->NCellsSize); + Array1DI4 ExpectedBLDIndex("KPPMixTest-ExpectedBLDIndex", Mesh->NCellsSize); + Array2DReal ExpectedBulkRi("KPPMixTest-ExpectedBulkRi", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal ExpectedVertDiff("KPPMixTest-ExpectedVertDiff", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal ExpectedVertVisc("KPPMixTest-ExpectedVertVisc", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal ExpectedNonLocal("KPPMixTest-ExpectedNonLocal", Mesh->NCellsSize, + NVertLayers + 1); + Array2DReal ExpectedTurbVel("KPPMixTest-ExpectedTurbVel", Mesh->NCellsSize, + NVertLayers + 1); + deepCopy(ExpectedBLD, KPPInstance->BoundaryLayerDepth); + deepCopy(ExpectedBLDIndex, KPPInstance->IndexBoundaryLayerDepth); + deepCopy(ExpectedBulkRi, KPPInstance->BulkRichardsonNumber); + deepCopy(ExpectedVertDiff, KPPInstance->VertDiff); + deepCopy(ExpectedVertVisc, KPPInstance->VertVisc); + deepCopy(ExpectedNonLocal, KPPInstance->VertNonLocalFlux); + deepCopy(ExpectedTurbVel, KPPInstance->TurbulentVelocityScale); + + deepCopy(KPPInstance->BoundaryLayerDepth, -1.0_Real); + deepCopy(KPPInstance->IndexBoundaryLayerDepth, -1); + deepCopy(KPPInstance->BulkRichardsonNumber, -1.0_Real); + deepCopy(KPPInstance->VertDiff, -1.0_Real); + deepCopy(KPPInstance->VertVisc, -1.0_Real); + deepCopy(KPPInstance->VertNonLocalFlux, -1.0_Real); + deepCopy(KPPInstance->TurbulentVelocityScale, -1.0_Real); + deepCopy(KPPInstance->PotentialDensity, -1.0_Real); + + KPPInstance->computeKPPMix(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction, Wind); + + const auto ExpectedBLDH = createHostMirrorCopy(ExpectedBLD); + const auto ExpectedBLDIndexH = createHostMirrorCopy(ExpectedBLDIndex); + const auto ExpectedBulkRiH = createHostMirrorCopy(ExpectedBulkRi); + const auto ExpectedVertDiffH = createHostMirrorCopy(ExpectedVertDiff); + const auto ExpectedVertViscH = createHostMirrorCopy(ExpectedVertVisc); + const auto ExpectedNonLocalH = createHostMirrorCopy(ExpectedNonLocal); + const auto ExpectedTurbVelH = createHostMirrorCopy(ExpectedTurbVel); + const auto ActualBLDH = + createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + const auto ActualBLDIndexH = + createHostMirrorCopy(KPPInstance->IndexBoundaryLayerDepth); + const auto ActualBulkRiH = + createHostMirrorCopy(KPPInstance->BulkRichardsonNumber); + const auto ActualVertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + const auto ActualVertViscH = createHostMirrorCopy(KPPInstance->VertVisc); + const auto ActualNonLocalH = + createHostMirrorCopy(KPPInstance->VertNonLocalFlux); + const auto ActualTurbVelH = + createHostMirrorCopy(KPPInstance->TurbulentVelocityScale); + const auto RetainedDensityH = + createHostMirrorCopy(KPPInstance->PotentialDensity); + const auto InputDensityH = createHostMirrorCopy(Density); + + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (!isApprox(ActualBLDH(ICell), ExpectedBLDH(ICell), RTol, ATol) || + ActualBLDIndexH(ICell) != ExpectedBLDIndexH(ICell)) { + ++NumErrors; + } + for (I4 K = 0; K <= NVertLayers; ++K) { + if (!isApprox(ActualBulkRiH(ICell, K), ExpectedBulkRiH(ICell, K), RTol, + ATol) || + !isApprox(ActualVertDiffH(ICell, K), ExpectedVertDiffH(ICell, K), + RTol, ATol) || + !isApprox(ActualVertViscH(ICell, K), ExpectedVertViscH(ICell, K), + RTol, ATol) || + !isApprox(ActualNonLocalH(ICell, K), ExpectedNonLocalH(ICell, K), + RTol, ATol) || + !isApprox(ActualTurbVelH(ICell, K), ExpectedTurbVelH(ICell, K), + RTol, ATol)) { + ++NumErrors; + } + if (K < NVertLayers && + !isApprox(RetainedDensityH(ICell, K), InputDensityH(ICell, K), + RTol, ATol)) { + ++NumErrors; + } + } + } + checkResult("enabled full call", NumErrors); +} + +void testDisabledFullCall() { + const HorzMesh *Mesh = HorzMesh::getDefault(); + VertCoord *VCoord = VertCoord::getDefault(); + KPPMix *KPPInstance = KPPMix::getInstance(); + const I4 NVertLayers = VCoord->NVertLayers; + + Array2DReal Density("KPPMixTest-DisabledDensity", Mesh->NCellsSize, + NVertLayers); + Array2DReal NormalVelocity("KPPMixTest-DisabledNormalVelocity", + Mesh->NEdgesSize, NVertLayers); + Array2DReal TangentialVelocity("KPPMixTest-DisabledTangentialVelocity", + Mesh->NEdgesSize, NVertLayers); + Array1DReal UStar("KPPMixTest-DisabledUStar", Mesh->NCellsSize); + Array1DReal B0("KPPMixTest-DisabledB0", Mesh->NCellsSize); + Array2DReal BVF("KPPMixTest-DisabledBVF", Mesh->NCellsSize, NVertLayers + 1); + Array1DReal IceFraction("KPPMixTest-DisabledIce", Mesh->NCellsSize); + deepCopy(Density, RhoSw); + deepCopy(NormalVelocity, 0.0_Real); + deepCopy(TangentialVelocity, 0.0_Real); + deepCopy(UStar, 0.02_Real); + deepCopy(B0, -1.0e-7_Real); + deepCopy(BVF, 0.0_Real); + deepCopy(IceFraction, 0.0_Real); + deepCopy(KPPInstance->VertDiff, -7.0_Real); + deepCopy(KPPInstance->BoundaryLayerDepth, -9.0_Real); + + KPPInstance->Enabled = false; + KPPInstance->computeKPPMix(Density, NormalVelocity, TangentialVelocity, + UStar, B0, BVF, IceFraction); + const auto VertDiffH = createHostMirrorCopy(KPPInstance->VertDiff); + const auto BLDH = createHostMirrorCopy(KPPInstance->BoundaryLayerDepth); + int NumErrors = 0; + for (I4 ICell = 0; ICell < Mesh->NCellsAll; ++ICell) { + if (VertDiffH(ICell, 2) != -7.0_Real || BLDH(ICell) != -9.0_Real) { + ++NumErrors; + } + } + KPPInstance->Enabled = true; + checkResult("disabled full call", NumErrors); +} + +} // namespace + +int main(int argc, char *argv[]) { + const std::string TestGroup = argc > 1 ? argv[1] : "all"; + + MPI_Init(&argc, &argv); + Kokkos::initialize(argc, argv); + Pacer::initialize(MPI_COMM_WORLD); + Pacer::setPrefix("Omega:"); + + initKPPMixTest(TestGroup); + + if (TestGroup == "profiles" || TestGroup == "all") { + testStabilityFunctions(); + testShapeFunctions(); + testLangmuirFunctions(); + testOBLUtilities(); + testTurbulentVelocityScale(); + testTurbScales(); + testMatchShape(); + testNonLocalCs(); + testClampOBLDepth(); + testOBLIndex(); + } + if (TestGroup == "bld" || TestGroup == "all") { + testBoundaryLayerDepth(); + testBoundaryLayerNonuniformThickness(); + testSshOffsetInvariance(); + testBoundaryLayerEdgeFallbacks(); + testBoundaryLayerLangmuir(); + testBoundaryLayerSmoothing(); + } + if (TestGroup == "vmix" || TestGroup == "all") { + testWindOnlyCoefficients(); + testConvectionOnlyCoefficients(); + testNonLocalProfileModes(); + testMatchBothInteriorCoefficients(); + testEnhancedDiffusion(); + testStableAndZeroForcing(); + testCoefficientVerticalDomainEdges(); + testCoefficientInvalidWetBounds(); + } + if (TestGroup == "integration" || TestGroup == "all") { + testConfiguredValues(); + testEnabledFullCall(); + testDisabledFullCall(); + } + if (TestGroup == "config-gradient" || TestGroup == "config-unsupported" || + TestGroup == "config-parabolic") { + ABORT_ERROR("KPPMixTest: KPPMix::init should have rejected the injected " + "MatchTechnique for group '{}'", + TestGroup); + } + if (TestGroup != "profiles" && TestGroup != "bld" && TestGroup != "vmix" && + TestGroup != "integration" && TestGroup != "all") { + ABORT_ERROR("KPPMixTest: unknown test group '{}'", TestGroup); + } + + LOG_INFO("------ KPP {} Tests Successful ------", TestGroup); + finalizeKPPMixTest(); + Kokkos::finalize(); + MPI_Finalize(); + return 0; +} + +//===----------------------------------------------------------------------===// diff --git a/components/omega/test/ocn/TendenciesTest.cpp b/components/omega/test/ocn/TendenciesTest.cpp index 5268d0a436ea..13f8ed2ba2a4 100644 --- a/components/omega/test/ocn/TendenciesTest.cpp +++ b/components/omega/test/ocn/TendenciesTest.cpp @@ -54,6 +54,9 @@ struct TestSetup { constexpr Geometry Geom = Geometry::Spherical; constexpr int NVertLayers = 60; +int testSfcTracerForcing(); +int testSfcThicknessForcing(); + int initState() { int Err = 0; @@ -303,11 +306,26 @@ int testTendencies() { "NormalVelocityTend"); } + const Real NormVelTendSum = + sum(DefTendencies->NormalVelocityTend, Mesh->NEdgesOwned, + VCoord->MinLayerEdgeBot, VCoord->MaxLayerEdgeTop); + if (!Kokkos::isfinite(NormVelTendSum) || NormVelTendSum == 0) { + Err++; + LOG_ERROR("TendenciesTest: NormVelTendSum FAIL"); + } + DefTendencies->SfcStressForcing.Enabled = OrigSfcStressEnabled; + // Test surface tracer forcing with enthalpy terms (TEOS-10 CtFrz path) + const int TracerForcingErr = testSfcTracerForcing(); + Err += TracerForcingErr; + + // Test surface thickness forcing with freshwater terms + const int ThicknessForcingErr = testSfcThicknessForcing(); + Err += ThicknessForcingErr; + // check that everything got computed correctly int NCellsOwned = Mesh->NCellsOwned; - int NEdgesOwned = Mesh->NEdgesOwned; int NTracers = Tracers::getNumTracers(); const Real PseudoThickTendSum = @@ -318,14 +336,6 @@ int testTendencies() { LOG_ERROR("TendenciesTest: PseudoThickTend FAIL"); } - const Real NormVelTendSum = - sum(DefTendencies->NormalVelocityTend, NEdgesOwned, - VCoord->MinLayerEdgeBot, VCoord->MaxLayerEdgeTop); - if (!Kokkos::isfinite(NormVelTendSum) || NormVelTendSum == 0) { - Err++; - LOG_ERROR("TendenciesTest: NormVelTendSum FAIL"); - } - const Real TraceTendSum = sum(DefTendencies->TracerTend, NTracers, NCellsOwned, VCoord->MinLayerCell, VCoord->MaxLayerCell); @@ -335,6 +345,382 @@ int testTendencies() { } Tendencies::clear(); + return Err; +} + +int testSfcTracerForcing() { + int Err = 0; + + auto *VCoord = VertCoord::getDefault(); + auto *DefTendencies = Tendencies::getDefault(); + auto *State = OceanState::getDefault(); + auto *AuxState = AuxiliaryState::getDefault(); + auto *DefForcing = Forcing::getDefault(); + auto *EosInst = Eos::getInstance(); + + Array3DReal TracerArray = Tracers::getAll(0); + + const I4 TempIndex = Tracers::IndxTemp; + const I4 SaltIndex = Tracers::IndxSalt; + + if (TempIndex < 0 || SaltIndex < 0) { + LOG_ERROR("TendenciesTest: Invalid tracer indices for SfcTracerForcing"); + return -1; + } + + deepCopy(DefTendencies->TracerTend, 0._Real); + + // Set up single test cell at top layer + const I4 ICellTest = 0; + const I4 KTop = VCoord->MinLayerCellH(ICellTest); + + if (KTop > VCoord->MaxLayerCellH(ICellTest)) { + LOG_ERROR("TendenciesTest: Test cell has no layers"); + return -1; + } + + // Known tracer values for testing + const Real CtTopValue = 15.0_Real; // °C (conservative temperature) + const Real SaTopValue = 35.0_Real; // g/kg (salinity) + + // Set tracer values at test cell + OMEGA_SCOPE(LocTracerArray, TracerArray); + Kokkos::parallel_for( + "SetTestTracersForcing", 1, KOKKOS_LAMBDA(int i) { + LocTracerArray(TempIndex, ICellTest, KTop) = CtTopValue; + LocTracerArray(SaltIndex, ICellTest, KTop) = SaTopValue; + }); + + // Retrieve forcing field views + auto &SensibleHeatFlux = DefForcing->TracerForcing.SensibleHeatFluxCell; + auto &LatentHeatFlux = DefForcing->TracerForcing.LatentHeatFluxCell; + auto &LongWaveHeatFluxUp = DefForcing->TracerForcing.LongWaveHeatFluxUpCell; + auto &LongWaveHeatFluxDown = + DefForcing->TracerForcing.LongWaveHeatFluxDownCell; + auto &SeaIceHeatFlux = DefForcing->TracerForcing.SeaIceHeatFluxCell; + auto &ShortWaveHeatFlux = DefForcing->TracerForcing.ShortWaveHeatFluxCell; + auto &RainFlux = DefForcing->TracerForcing.RainFluxCell; + auto &RiverRunoffFlux = DefForcing->TracerForcing.RiverRunoffFluxCell; + auto &SnowFlux = DefForcing->TracerForcing.SnowFluxCell; + auto &IceRunoffFlux = DefForcing->TracerForcing.IceRunoffFluxCell; + auto &SeaIceSaltFlux = DefForcing->TracerForcing.SeaIceSaltFluxCell; + + // Initialize all fluxes to zero + deepCopy(SensibleHeatFlux, 0._Real); + deepCopy(LatentHeatFlux, 0._Real); + deepCopy(LongWaveHeatFluxUp, 0._Real); + deepCopy(LongWaveHeatFluxDown, 0._Real); + deepCopy(SeaIceHeatFlux, 0._Real); + deepCopy(ShortWaveHeatFlux, 0._Real); + deepCopy(RainFlux, 0._Real); + deepCopy(RiverRunoffFlux, 0._Real); + deepCopy(SnowFlux, 0._Real); + deepCopy(IceRunoffFlux, 0._Real); + deepCopy(SeaIceSaltFlux, 0._Real); + + // Set test forcing values + // Non-zero sensible heat: 100 W/m² + const Real TestSensibleHeat = 100.0_Real; + // Non-zero rain: 1e-8 kg/m²/s + const Real TestRain = 1.0e-8_Real; + // Non-zero snow: 5e-9 kg/m²/s + const Real TestSnow = 5.0e-9_Real; + // Sea ice salt flux: 1e-4 kg/m²/s + const Real TestSeaIceSaltFlux = 1.0e-4_Real; + + OMEGA_SCOPE(LocSensibleHeatFlux, SensibleHeatFlux); + OMEGA_SCOPE(LocRainFlux, RainFlux); + OMEGA_SCOPE(LocSnowFlux, SnowFlux); + OMEGA_SCOPE(LocSeaIceSaltFlux, SeaIceSaltFlux); + Kokkos::parallel_for( + "SetTestForcingTracer", 1, KOKKOS_LAMBDA(int i) { + LocSensibleHeatFlux(ICellTest) = TestSensibleHeat; + LocRainFlux(ICellTest) = TestRain; + LocSnowFlux(ICellTest) = TestSnow; + LocSeaIceSaltFlux(ICellTest) = TestSeaIceSaltFlux; + }); + + DefForcing->computeAll(); + + // Disable all tendencies except SfcTracerForcing + const bool OrigSfcStressEnabled = DefTendencies->SfcStressForcing.Enabled; + const bool OrigSfcThicknessEnabled = + DefTendencies->SfcThicknessForcing.Enabled; + const bool OrigSfcTracerEnabled = DefTendencies->SfcTracerForcing.Enabled; + const bool OrigPseudoThicknessDiv = + DefTendencies->PseudoThicknessFluxDiv.Enabled; + const bool OrigPotentialVortHAdv = DefTendencies->PotentialVortHAdv.Enabled; + const bool OrigKEGrad = DefTendencies->KEGrad.Enabled; + const bool OrigVelocityDiffusion = DefTendencies->VelocityDiffusion.Enabled; + const bool OrigVelocityHyperDiff = DefTendencies->VelocityHyperDiff.Enabled; + const bool OrigTracerHorzAdv = DefTendencies->TracerHorzAdv.Enabled; + const bool OrigTracerDiffusion = DefTendencies->TracerDiffusion.Enabled; + const bool OrigTracerHyperDiff = DefTendencies->TracerHyperDiff.Enabled; + const bool OrigSurfaceTracerRestoring = + DefTendencies->SurfaceTracerRestoring.Enabled; + + DefTendencies->SfcStressForcing.Enabled = false; + DefTendencies->SfcThicknessForcing.Enabled = false; + DefTendencies->SfcTracerForcing.Enabled = false; + DefTendencies->PseudoThicknessFluxDiv.Enabled = false; + DefTendencies->PotentialVortHAdv.Enabled = false; + DefTendencies->KEGrad.Enabled = false; + DefTendencies->VelocityDiffusion.Enabled = false; + DefTendencies->VelocityHyperDiff.Enabled = false; + DefTendencies->TracerHorzAdv.Enabled = false; + DefTendencies->TracerDiffusion.Enabled = false; + DefTendencies->TracerHyperDiff.Enabled = false; + DefTendencies->SurfaceTracerRestoring.Enabled = false; + + // Compute tendencies + int ThickTimeLevel = 0; + int VelTimeLevel = 0; + int TracerTimeLevel = 0; + TimeInstant Time; + TimeInterval Interval(1., TimeUnits::Seconds); + + // because vertical advection tendencies are always on, we need to compute a + // baseline first. the actual test is whether the total tendencies change + // with the flag toggling. + DefTendencies->computeAllTendencies(State, AuxState, TracerArray, + ThickTimeLevel, VelTimeLevel, + TracerTimeLevel, Time, Interval); + + HostArray3DReal TracerTendBaseH = + createHostMirrorCopy(DefTendencies->TracerTend); + deepCopy(TracerTendBaseH, DefTendencies->TracerTend); + const Real BaselineTempTend = TracerTendBaseH(TempIndex, ICellTest, KTop); + const Real BaselineSaltTend = TracerTendBaseH(SaltIndex, ICellTest, KTop); + + // Now enable SfcTracerForcing and compute again + DefTendencies->SfcTracerForcing.Enabled = true; + + DefTendencies->computeAllTendencies(State, AuxState, TracerArray, + ThickTimeLevel, VelTimeLevel, + TracerTimeLevel, Time, Interval); + + // Build a reference expectations for temperature tendency: + // using TEOS-10 freezing CT (expected to pass under strict tolerance). + + HostArray2DReal PressureMidH = createHostMirrorCopy(VCoord->PressureMid); + deepCopy(PressureMidH, VCoord->PressureMid); + const Real PTopDb = PressureMidH(ICellTest, KTop) * Pa2Db; + const Real CtFrz = + Eos::calcCtFreezing(EosInst->EosChoice, SaTopValue, PTopDb, 0.0_Real); + const Real ExpectedTempTend = + (TestSensibleHeat + TestRain * Cp0Sw * CtTopValue + + TestSnow * (Cp0Sw * CtFrz - LatIce)) * + HFluxFac; + + // SaltTend = SeaIceSaltFlux * SFluxFac + const Real ExpectedSaltTend = TestSeaIceSaltFlux * SFluxFac; + + HostArray3DReal TracerTendH = + createHostMirrorCopy(DefTendencies->TracerTend); + deepCopy(TracerTendH, DefTendencies->TracerTend); + const Real ComputedTempTend = + TracerTendH(TempIndex, ICellTest, KTop) - BaselineTempTend; + const Real ComputedSaltTend = + TracerTendH(SaltIndex, ICellTest, KTop) - BaselineSaltTend; + + constexpr Real RelTol = 1.0e-10_Real; + constexpr Real AbsTol = 1.0e-12_Real; // flux precision is ~e-15 + + // Expected-pass check with TEOS freezing CT reference. + if (!isApprox(ComputedTempTend, ExpectedTempTend, RelTol, AbsTol)) { + Err++; + LOG_ERROR("TendenciesTest: SfcTracerForcing temp tendency FAIL"); + LOG_ERROR(" with TEOS-CtFrz Expected: {}, Computed: {}, Diff: {}", + ExpectedTempTend, ComputedTempTend, + Kokkos::abs(ComputedTempTend - ExpectedTempTend)); + } else { + LOG_INFO("TendenciesTest: SfcTracerForcing temp tendency PASS"); + } + + // Check salinity tendency + if (!isApprox(ComputedSaltTend, ExpectedSaltTend, RelTol, AbsTol)) { + Err++; + LOG_ERROR("TendenciesTest: SfcTracerForcing salt tendency FAIL"); + LOG_INFO(" Expected: {}, Computed: {}, Diff: {}", ExpectedSaltTend, + ComputedSaltTend, + Kokkos::abs(ComputedSaltTend - ExpectedSaltTend)); + } else { + LOG_INFO("TendenciesTest: SfcTracerForcing salt tendency PASS"); + } + + DefTendencies->SfcStressForcing.Enabled = OrigSfcStressEnabled; + DefTendencies->SfcThicknessForcing.Enabled = OrigSfcThicknessEnabled; + DefTendencies->SfcTracerForcing.Enabled = OrigSfcTracerEnabled; + DefTendencies->PseudoThicknessFluxDiv.Enabled = OrigPseudoThicknessDiv; + DefTendencies->PotentialVortHAdv.Enabled = OrigPotentialVortHAdv; + DefTendencies->KEGrad.Enabled = OrigKEGrad; + DefTendencies->VelocityDiffusion.Enabled = OrigVelocityDiffusion; + DefTendencies->VelocityHyperDiff.Enabled = OrigVelocityHyperDiff; + DefTendencies->TracerHorzAdv.Enabled = OrigTracerHorzAdv; + DefTendencies->TracerDiffusion.Enabled = OrigTracerDiffusion; + DefTendencies->TracerHyperDiff.Enabled = OrigTracerHyperDiff; + DefTendencies->SurfaceTracerRestoring.Enabled = OrigSurfaceTracerRestoring; + + return Err; +} + +int testSfcThicknessForcing() { + int Err = 0; + + auto *VCoord = VertCoord::getDefault(); + auto *DefTendencies = Tendencies::getDefault(); + auto *State = OceanState::getDefault(); + auto *AuxState = AuxiliaryState::getDefault(); + auto *DefForcing = Forcing::getDefault(); + + Array3DReal TracerArray = Tracers::getAll(0); + + deepCopy(DefTendencies->PseudoThicknessTend, 0._Real); + + // Set up single test cell at top layer + const I4 ICellTest = 0; + const I4 KTop = VCoord->MinLayerCellH(ICellTest); + + if (KTop > VCoord->MaxLayerCellH(ICellTest)) { + LOG_ERROR("TendenciesTest: Test cell has no layers for thickness test"); + return -1; + } + + // Retrieve forcing field views for thickness + auto &SnowFlux = DefForcing->TracerForcing.SnowFluxCell; + auto &RainFlux = DefForcing->TracerForcing.RainFluxCell; + auto &EvaporationFlux = DefForcing->TracerForcing.EvaporationFluxCell; + auto &SeaIceFreshWater = DefForcing->TracerForcing.SeaIceFreshWaterFluxCell; + auto &IceRunoffFlux = DefForcing->TracerForcing.IceRunoffFluxCell; + auto &RiverRunoffFlux = DefForcing->TracerForcing.RiverRunoffFluxCell; + auto &SeaIceSaltFlux = DefForcing->TracerForcing.SeaIceSaltFluxCell; + + // Initialize all fluxes to zero + deepCopy(SnowFlux, 0._Real); + deepCopy(RainFlux, 0._Real); + deepCopy(EvaporationFlux, 0._Real); + deepCopy(SeaIceFreshWater, 0._Real); + deepCopy(IceRunoffFlux, 0._Real); + deepCopy(RiverRunoffFlux, 0._Real); + deepCopy(SeaIceSaltFlux, 0._Real); + + // Set test freshwater flux values + // Rain: 1e-8 kg/m²/s + const Real TestRain = 1.0e-8_Real; + // Snow: 5e-9 kg/m²/s + const Real TestSnow = 5.0e-9_Real; + // Ice runoff: 2e-9 kg/m²/s + const Real TestIceRunoff = 2.0e-9_Real; + // River runoff: 3e-9 kg/m²/s + const Real TestRiverRunoff = 3.0e-9_Real; + // Sea ice freshwater: 1e-9 kg/m²/s + const Real TestSeaIceFreshWater = 1.0e-9_Real; + // Sea ice salt flux: 1e-4 kg/m²/s (affects thickness via salt) + const Real TestSeaIceSaltFlux = 1.0e-4_Real; + + OMEGA_SCOPE(LocSnowFlux, SnowFlux); + OMEGA_SCOPE(LocRainFlux, RainFlux); + OMEGA_SCOPE(LocIceRunoffFlux, IceRunoffFlux); + OMEGA_SCOPE(LocRiverRunoffFlux, RiverRunoffFlux); + OMEGA_SCOPE(LocSeaIceFreshWater, SeaIceFreshWater); + OMEGA_SCOPE(LocSeaIceSaltFlux, SeaIceSaltFlux); + Kokkos::parallel_for( + "SetTestForcingThickness", 1, KOKKOS_LAMBDA(int i) { + LocRainFlux(ICellTest) = TestRain; + LocSnowFlux(ICellTest) = TestSnow; + LocIceRunoffFlux(ICellTest) = TestIceRunoff; + LocRiverRunoffFlux(ICellTest) = TestRiverRunoff; + LocSeaIceFreshWater(ICellTest) = TestSeaIceFreshWater; + LocSeaIceSaltFlux(ICellTest) = TestSeaIceSaltFlux; + }); + DefForcing->computeAll(); + + const bool OrigSfcStressEnabled = DefTendencies->SfcStressForcing.Enabled; + const bool OrigSfcThicknessEnabled = + DefTendencies->SfcThicknessForcing.Enabled; + const bool OrigSfcTracerEnabled = DefTendencies->SfcTracerForcing.Enabled; + const bool OrigPseudoThicknessDiv = + DefTendencies->PseudoThicknessFluxDiv.Enabled; + const bool OrigPotentialVortHAdv = DefTendencies->PotentialVortHAdv.Enabled; + const bool OrigKEGrad = DefTendencies->KEGrad.Enabled; + const bool OrigVelocityDiffusion = DefTendencies->VelocityDiffusion.Enabled; + const bool OrigVelocityHyperDiff = DefTendencies->VelocityHyperDiff.Enabled; + const bool OrigTracerHorzAdv = DefTendencies->TracerHorzAdv.Enabled; + const bool OrigTracerDiffusion = DefTendencies->TracerDiffusion.Enabled; + const bool OrigTracerHyperDiff = DefTendencies->TracerHyperDiff.Enabled; + const bool OrigSurfaceTracerRestoring = + DefTendencies->SurfaceTracerRestoring.Enabled; + + DefTendencies->SfcStressForcing.Enabled = false; + DefTendencies->SfcThicknessForcing.Enabled = false; + DefTendencies->SfcTracerForcing.Enabled = false; + DefTendencies->PseudoThicknessFluxDiv.Enabled = false; + DefTendencies->PotentialVortHAdv.Enabled = false; + DefTendencies->KEGrad.Enabled = false; + DefTendencies->VelocityDiffusion.Enabled = false; + DefTendencies->VelocityHyperDiff.Enabled = false; + DefTendencies->TracerHorzAdv.Enabled = false; + DefTendencies->TracerDiffusion.Enabled = false; + DefTendencies->TracerHyperDiff.Enabled = false; + DefTendencies->SurfaceTracerRestoring.Enabled = false; + + // Compute baseline tendencies (vertical advection is always on) + int ThickTimeLevel = 0; + int VelTimeLevel = 0; + TimeInstant Time; + DefTendencies->computePseudoThicknessTendenciesOnly( + State, AuxState, ThickTimeLevel, VelTimeLevel, Time); + + HostArray2DReal PseudoThicknessTendBaseH = + createHostMirrorCopy(DefTendencies->PseudoThicknessTend); + deepCopy(PseudoThicknessTendBaseH, DefTendencies->PseudoThicknessTend); + const Real BaselineThickTend = PseudoThicknessTendBaseH(ICellTest, KTop); + + // Now enable SfcThicknessForcing and compute again + DefTendencies->SfcThicknessForcing.Enabled = true; + DefTendencies->computePseudoThicknessTendenciesOnly( + State, AuxState, ThickTimeLevel, VelTimeLevel, Time); + + // Calculate expected thickness tendency + // ThickTend = (Rain + Snow + IceRunoff + RiverRunoff + SeaIceFreshWater + + // SeaIceSaltFlux) / RhoSw + const Real ExpectedThickTend = + (TestRain + TestSnow + TestIceRunoff + TestRiverRunoff + + TestSeaIceFreshWater + TestSeaIceSaltFlux) / + RhoSw; + + HostArray2DReal PseudoThicknessTendH = + createHostMirrorCopy(DefTendencies->PseudoThicknessTend); + deepCopy(PseudoThicknessTendH, DefTendencies->PseudoThicknessTend); + const Real ComputedThickTend = + PseudoThicknessTendH(ICellTest, KTop) - BaselineThickTend; + + constexpr Real RelTol = 1.0e-10_Real; + constexpr Real AbsTol = 1.0e-12_Real; + + // Check thickness tendency + if (!isApprox(ComputedThickTend, ExpectedThickTend, RelTol, AbsTol)) { + Err++; + LOG_ERROR("TendenciesTest: SfcThicknessForcing thickness tendency FAIL"); + LOG_INFO(" Expected: {}, Computed: {}, Diff: {}", ExpectedThickTend, + ComputedThickTend, + Kokkos::abs(ComputedThickTend - ExpectedThickTend)); + } else { + LOG_INFO("TendenciesTest: SfcThicknessForcing thickness tendency PASS"); + } + + DefTendencies->SfcStressForcing.Enabled = OrigSfcStressEnabled; + DefTendencies->SfcThicknessForcing.Enabled = OrigSfcThicknessEnabled; + DefTendencies->SfcTracerForcing.Enabled = OrigSfcTracerEnabled; + DefTendencies->PseudoThicknessFluxDiv.Enabled = OrigPseudoThicknessDiv; + DefTendencies->PotentialVortHAdv.Enabled = OrigPotentialVortHAdv; + DefTendencies->KEGrad.Enabled = OrigKEGrad; + DefTendencies->VelocityDiffusion.Enabled = OrigVelocityDiffusion; + DefTendencies->VelocityHyperDiff.Enabled = OrigVelocityHyperDiff; + DefTendencies->TracerHorzAdv.Enabled = OrigTracerHorzAdv; + DefTendencies->TracerDiffusion.Enabled = OrigTracerDiffusion; + DefTendencies->TracerHyperDiff.Enabled = OrigTracerHyperDiff; + DefTendencies->SurfaceTracerRestoring.Enabled = OrigSurfaceTracerRestoring; return Err; } diff --git a/components/omega/test/timeStepping/TimeStepperTest.cpp b/components/omega/test/timeStepping/TimeStepperTest.cpp index af84424217ff..d64b461896a5 100644 --- a/components/omega/test/timeStepping/TimeStepperTest.cpp +++ b/components/omega/test/timeStepping/TimeStepperTest.cpp @@ -255,6 +255,8 @@ int initTimeStepperTest(const std::string &mesh) { TestTendencies->TracerDiffusion.Enabled = false; TestTendencies->TracerHyperDiff.Enabled = false; TestTendencies->SfcStressForcing.Enabled = false; + TestTendencies->SfcTracerForcing.Enabled = false; + TestTendencies->SfcThicknessForcing.Enabled = false; TestTendencies->SurfaceTracerRestoring.Enabled = false; TestTendencies->ExplicitBottomDrag.Enabled = false; DefVAdv->ThickVertAdvEnabled = false;