diff --git a/components/omega/configs/Default.yml b/components/omega/configs/Default.yml index 620f790cdc65..d592acd1359a 100644 --- a/components/omega/configs/Default.yml +++ b/components/omega/configs/Default.yml @@ -59,6 +59,19 @@ Omega: DRhoDT: -0.2 DRhoDS: 0.8 RhoT0S0: 1000.0 + VertMix: + Background: + Diffusivity: 1.0e-5 + Viscosity: 1.0e-4 + Convective: + Enable: true + Diffusivity: 1.0 + TriggerBVF: 0.0 + Shear: + Enable: true + NuZero: 0.005 + Alpha: 5.0 + Exponent: 2.0 IOStreams: InitialVertCoord: UsePointerFile: false diff --git a/components/omega/doc/design/images/ocean.jpg b/components/omega/doc/design/images/ocean.jpg new file mode 100644 index 000000000000..70607c7b7a41 Binary files /dev/null and b/components/omega/doc/design/images/ocean.jpg differ diff --git a/components/omega/doc/devGuide/EOS.md b/components/omega/doc/devGuide/EOS.md index 2cef706e9382..0762727a25b5 100644 --- a/components/omega/doc/devGuide/EOS.md +++ b/components/omega/doc/devGuide/EOS.md @@ -2,12 +2,19 @@ # Equation of State (EOS) -Omega includes an `Eos` class that provides functions that compute `SpecVol` and `SpecVolDisplaced`. -Current EOS options are a linear EOS or an EOS computed using the TEOS-10 75 term expansion from -[Roquet et al. 2015](https://www.sciencedirect.com/science/article/pii/S1463500315000566). If -`SpecVolDisplaced` is calculated with the linear EOS option, it will be equal to `SpecVol` as there +Omega includes an `Eos` class that provides functions that compute `SpecVol`, `SpecVolDisplaced`, +and `BruntVaisalaFreqSq`. Current EOS options are a linear EOS or an EOS computed using the TEOS-10 +75 term expansion from [Roquet et al. 2015](https://www.sciencedirect.com/science/article/pii/S1463500315000566). +If `SpecVolDisplaced` is calculated with the linear EOS option, it will be equal to `SpecVol` as there is no pressure/depth dependence for the linear EOS. `SpecVolDisplaced` computes specific volume -adiabatically displaced to `K + KDisp`. +adiabatically displaced to `K + KDisp` (where `K` counted positive downward, ie `K+1` is one layer below `K`). Note: `SpecVol` must be calculated before `BruntVaisalaFreqSq`, as +`SpecVol` is an input for the `BruntVaisalaFreqSq` calculation. If the linear EOS option is used, then the `BruntVaisalaFreqSq` +is calculated using linear coefficients. If the TEOS-10 option is used, the `BruntVaisalaFreqSq` is calculated with non-linear +coefficients according to the [TEOS-10 toolbox](https://www.teos-10.org/software.htm). Note: two assumption for ease of computation and efficiency have been made +for the `BruntVaisalaFreqSq` TEOS-10 option that differ from how it is calculated in the TEOS-10 toolbox: +(1) gravity is assumed to be constant and not a function of depth and latitude, and (2) the interface value of the specific volume is +calculated as the average between two layer values, rather than being recalculated using the interface values of temperature, +salinity, and pressure. Both of these assumptions incur less than a 1% error. ## Eos type @@ -15,7 +22,7 @@ An enumeration listing all implemented schemes is provided. It needs to be exten EOS is added. It is used to identify which EOS method is to be used at run time. ```c++ -enum class EosType { Linear, Teos10Poly75t }; +enum class EosType { LinearEos, Teos10Eos }; ``` ## Initialization @@ -49,9 +56,16 @@ and pressure arrays and displaced vertical index level, do Eos.computeSpecVolDisp(ConsrvTemp, AbsSalinity, Pressure, KDisp); ``` -where `KDisp` is the number of `k` levels you want to displace each specific volume level to. +where `KDisp` is the number of `k` layers you want to displace each specific volume layer to. For example, to displace each level to one below, set `KDisp = 1`. +To compute `BruntVaisalaFreqSq` for a particular set of temperature, salinity, pressure, and specific +volume arrays, do + +```c++ +Eos.computeBruntVaisalaFreqSq(ConservTemp, AbsSalinity, Pressure, SpecVol); +``` + ## Removal of Eos To clear the Eos instance do: diff --git a/components/omega/doc/devGuide/VerticalMixingCoeff.md b/components/omega/doc/devGuide/VerticalMixingCoeff.md new file mode 100644 index 000000000000..2b3fa0a86fa2 --- /dev/null +++ b/components/omega/doc/devGuide/VerticalMixingCoeff.md @@ -0,0 +1,82 @@ +(omega-dev-vertmix) = + +# Vertical Mixing Coefficients + +Omega includes a `VertMix` class that provides functions that compute `VertDiff` and `VertVisc`, the +vertical diffusivity and viscosity, where both are defined at the center of the cell and top of the layer. +Currently the values of `VertDiff` and `VertVisc` are calculated using the linear combination of three options: (1) a +constant background mixing value, (2) a convective instability mixing value, and (3) a Richardson +number dependent shear mixing value from the [Pacanowski and Philander (1981)](https://journals.ametsoc.org/view/journals/phoc/11/11/1520-0485_1981_011_1443_povmin_2_0_co_2.xml) parameterization. These options are linearly additive. In the future, additional additive options will be implemented, such as the K Profile Parameterization [(KPP; Large et al., 1994)](https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/94rg01872). For both the convective and shear mixing values `BruntVaisalaFreqSq` is needed, which +is calculated by the `EOS` class. + +## Initialization and Usage + +The primary class `VertMix` is implemented using the Singleton pattern to ensure a single instance manages all vertical mixing calculations. + +```c++ +// Initialize VertMix +VertMix* VMix = VertMix::getInstance(); + +// Compute mixing coefficients +VMix->computeVertMix(NormalVelocity, TangentialVelocity, BruntVaisalaFreqSq); +``` + +## Configuration + +The initialization process reads parameters from the yaml configuration file with the following structure and +default values: + +```yaml +VertMix: + Background: + Viscosity: 1e-4 + Diffusivity: 1e-5 + Convective: + Enable: true + Diffusivity: 1.0 + TriggerBVF: 0.0 + Shear: + Enable: true + NuZero: 0.005 + Alpha: 5.0 + Exponent: 2.0 +``` + +## Class Structure + +### Core Data Members + +- `VertDiff`: 2D array storing vertical diffusivity coefficients (m²/s) +- `VertVisc`: 2D array storing vertical viscosity coefficients (m²/s) + +### Mixing Parameters + +1. Background Mixing: + - `BackDiff`: Background vertical diffusivity (m²/s; Default: 1e-5) + - `BackVisc`: Background vertical viscosity (m²/s: Default: 1e-4) + +2. Convective Mixing: + - `EnableConvMix`: Flag to enable/disable convective mixing (Default: True) + - `ConvDiff`: Convective mixing coefficient (m²/s; Default: 1.0) + - `ConvTriggerBVF`: Trigger threshold for convective mixing (Default: 0.0) + +3. Shear Mixing: + - `EnableShearMix`: Flag to enable/disable shear mixing (Default: True) + - `ShearNuZero`: Base coefficient for Pacanowski-Philander scheme (Default: 0.005) + - `ShearAlpha`: Alpha parameter for P-P scheme (Default: 5.0) + - `ShearExponent`: Exponent parameter for P-P scheme (Default: 2.0) + +## Core Functionality (Vertical Mixing Coefficient Calculation) + +The main computation is handled by: + +```cpp +void computeVertMix(const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array2DReal &BruntVaisalaFreqSq); +``` + +This method combines the effects of: +- Background mixing (constant coefficients) +- Convective mixing (triggered by static instability) +- Shear instability driven mixing (Pacanowski-Philander scheme; to be changed to [Large et al., 1994](https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/94rg01872) shear mixing scheme in a later development) diff --git a/components/omega/doc/index.md b/components/omega/doc/index.md index 0e6175cc8738..121d65a0cbfd 100644 --- a/components/omega/doc/index.md +++ b/components/omega/doc/index.md @@ -50,6 +50,7 @@ userGuide/Tracers userGuide/TridiagonalSolvers userGuide/VertCoord userGuide/Timing +userGuide/VerticalMixingCoeff ``` ```{toctree} @@ -92,6 +93,7 @@ devGuide/Tracers devGuide/TridiagonalSolvers devGuide/VertCoord devGuide/Timing +devGuide/VerticalMixingCoeff ``` ```{toctree} diff --git a/components/omega/doc/userGuide/EOS.md b/components/omega/doc/userGuide/EOS.md index 3a7ca6b40097..7ab522c9761f 100644 --- a/components/omega/doc/userGuide/EOS.md +++ b/components/omega/doc/userGuide/EOS.md @@ -4,7 +4,7 @@ The equation of state (EOS) for the ocean describes the relationship between specific volume of seawater (in $\textrm{m}^3/\textrm{kg}$; the reciprocal of density) and temperature (in $^{\circ}\textrm{C}$), salinity (in $\textrm{g/kg}$), and pressure (in $\textrm{dbar}$). Through the hydrostatic balance (which relates density/specific volume gradients to pressure gradients), the equation of state provides a connection between active tracers (temperature and salinity) and the fluid dynamics. -Two choices of EOS are provided by Omega: a linear EOS and a TEOS-10 EOS. The linear EOS simplifies the relationship by excluding the influence of pressure and using constant expansion/contraction coefficients, making the specific volume a simple linear function of temperature and salinity. However, this option is only recommended for simpler idealized test cases as its accuracy is not sufficient for real ocean simulations. The TEOS-10 EOS is a 75-term polynomial expression from [Roquet et al. 2015](https://www.sciencedirect.com/science/article/pii/S1463500315000566) that approximates the [Thermodynamic Equation of Seawater 2010](https://www.teos-10.org/pubs/TEOS-10_Manual.pdf) , but in a less complex and more computationally efficient manner, and is the preferred EOS for real ocean simulations in Omega. +Two choices of EOS are provided by Omega: a linear EOS and a TEOS-10 EOS. The linear EOS simplifies the relationship by excluding the influence of pressure and using constant expansion/contraction coefficients, making the specific volume a simple linear function of temperature and salinity. However, this option should only be used for simpler idealized test cases as it is not appropriate for realistic ocean simulations. The TEOS-10 EOS is a 75-term polynomial expression from [Roquet et al. 2015](https://www.sciencedirect.com/science/article/pii/S1463500315000566) that approximates the [Thermodynamic Equation of Seawater 2010](https://www.teos-10.org/pubs/TEOS-10_Manual.pdf), but in a less complex and more computationally efficient manner, and is the preferred EOS for real ocean simulations in Omega. The user-configurable options are: `EosType` (choose either `Linear` or `Teos-10`), as well as the parameters needed for the linear EOS. @@ -19,4 +19,12 @@ Eos: where `DRhoDT` is the thermal expansion coefficient ($\textrm{kg}/(\textrm{m}^3 \cdot ^{\circ}\textrm{C})$), `DRhoDS` is the saline contraction coefficient ($\textrm{kg}/\textrm{m}^3$), and `RhoT0S0` is the reference density at (T,S)=(0,0) (in $\textrm{kg}/\textrm{m}^3$). -In addition to `SpecVol`, the displaced specific volume `SpecVolDisplaced` is also calculated by the EOS. This calculates the density of a parcel of fluid that is adiabatically displaced by a relative `k` levels, capturing the effects of pressure/depth changes. This is primarily used to calculate quantities for determining the water column stability (i.e. the stratification) and the vertical mixing coefficients (viscosity and diffusivity). Note: when using the linear EOS, `SpecVolDisplaced` will be the same as `SpecVol` since the specific volume calculation is independent of pressure/depth. +In addition to `SpecVol`, the displaced specific volume `SpecVolDisplaced` and `BruntVaisalaFreqSq` are also calculated by the EOS. + +## Displaced Specific Volume + +The `Eos` class calculates the density of a parcel of fluid that is adiabatically displaced by a relative `k` levels (`k` counted positive downward), capturing the effects of pressure/depth changes. This is primarily used to calculate quantities for determining the water column stability (i.e. the stratification) and the vertical mixing coefficients (viscosity and diffusivity). Note: when using the `Linear` EOS option, `SpecVolDisplaced` will be the same as `SpecVol` since the specific volume calculation is independent of pressure/depth. + +## Squared Brunt Vaisala Frequency + +The `Eos` class also calculates the squared Brunt Vaisala Frequency, which is used by the `VertMix` class to determine water column stability for both the convective adjustment and shear-instability driven mixing. When using the `Linear` EOS option, the `BruntVaisalaFreqSq` is calculated using linear coefficients. When using the `Teos-10` EOS option, the `BruntVaisalaFreqSq` is calculated with non-linear coefficients according to the TOES-10. For both options, `SpecVol` must be calculated prior to calculating `BruntVaisalaFreqSq`, as it is an input to the `BruntVaisalaFreqSq` calculation. diff --git a/components/omega/doc/userGuide/VerticalMixingCoeff.md b/components/omega/doc/userGuide/VerticalMixingCoeff.md new file mode 100644 index 000000000000..a7f92931f1dd --- /dev/null +++ b/components/omega/doc/userGuide/VerticalMixingCoeff.md @@ -0,0 +1,70 @@ +(omega-user-vertmix)= + +# Vertical Mixing Coefficients + +The vertical mixing module in Omega handles the parameterization of unresolved vertical mixing +processes in the ocean. It calculates vertical diffusivity and viscosity coefficients that +determine how properties (like momentum, heat, salt, and biogeochemical tracers) mix vertically +in the ocean model. Currently, Omega offers three different mixing processes within the water column: (1) a constant +background mixing value, (2) a convective instability mixing value, and (3) a Richardson number +dependent shear instability driven mixing value from the [Pacanowski and Philander (1981)](https://journals.ametsoc.org/view/journals/phoc/11/11/1520-0485_1981_011_1443_povmin_2_0_co_2.xml) parameterization. These are linearly additive and are describe a bit +more in detail below. Other mixing processes and parameterizations, such as the the K Profile Parameterization [(KPP; Large et al., 1994)](https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/94rg01872) will be added in the future. + +The user-configurable options are the following parameters in the yaml configuration file: + +```yaml +VertMix: + Background: + Viscosity: 1.0e-4 # Background vertical viscosity (m²/s) + Diffusivity: 1.0e-5 # Background vertical diffusivity (m²/s) + Convective: + Enable: true # Enables the convective-induced mixing option + Diffusivity: 1.0 # Convective mixing coefficient (m²/s) + TriggerBVF: 0.0 # Squared Brunt-Vaisala frequency threshold + Shear: + Enable: true # Enables the shear-instability driven mixing option + NuZero: 1.0e-2 # Base viscosity coefficient + Alpha: 5.0 # Stability parameter + Exponent: 2 # Richardson number exponent +``` + +## Vertical Mixing Processes/Types + +### 1. Background Mixing + +A constant background mixing value that represents small-scale mixing processes not explicitly resolved by the model. Typically, this is chosen to represent low values of vertical mixing +happening in the ocean's stratified interior. + +### 2. Convective Mixing + +Enhanced convective adjustment mixing that occurs in statically unstable regions of the water column to parameterize convection and homogenize properties. In Omega this is mixing is defaulted to occur when the squared Brunt Vaisala Frequency is less than 0.0 (unstable), and is off when equal to (neutral) or greater than (stable) 0.0. + +$$ +\kappa = +\begin{cases} ++\kappa_b + \kappa_{conv} \quad \text{ if } N^2 < N^2_{crit}\\ ++\kappa_b \quad \text{ if } N^2 \geq N^2_{crit} +\end{cases} +$$ + +This is different than some current implementations (i.e. in MPAS-Ocean and the CVMix package), where convective adjustment occurs both with unstable and neutral conditions ($N^2 \leq N^2_{crit}$). $\kappa_{conv}$ and $N^2_{crit}$ are constant parameters set in the `VertMix` section of the yaml file (`Diffusivity` and `TriggerBVF` under the `Convective` header). + +### 3. Shear Mixing + +Mixing induced by vertical velocity shear, implemented using the Pacanowski-Philander scheme, through the gradient Richardson number (ratio of buoyancy to shear). + +$$ +\nu = \frac{\nu_o}{(1+\alpha Ri)^n} + \nu_b\,, +$$ + +$$ +\kappa = \frac{\nu}{(1+\alpha Ri)} + \kappa_b\,. +$$ + +where $Ri$ is defined as: + +$$ +Ri = \frac{N^2}{\left|\frac{\partial \mathbf{U}}{\partial z}\right|^2}\,, +$$ + +where $\nu_o$, $\alpha$, $n$, $\nu_b$, $\kappa_b$ are constant parameters set in the `VertMix` section of the yaml file (`NuZero`, `Alpha`, `Exponent` under the `Shear` header and `Viscosity`, `Diffusivity` under the `Background` header). $N^2$ is calculated by the EOS based on the ocean state and $\mathbf{U}$ is the magnitude of the horizontal velocity. $N^2$, $\partial \mathbf{U}}{\partial z}\right|^2$ and $Ri$ of `K` are all defined at the cell center, top interface of layer `K`. $N^2$, $\nu_{shear}$ and $\kappa_{shear}$ are set to zero for the surface layer. In a later development, the shear mixing option will be changed to the interior shear mixing formulation in [Large et al., 1994](https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/94rg01872). diff --git a/components/omega/src/ocn/Eos.cpp b/components/omega/src/ocn/Eos.cpp index 2530583e738c..1b49fe400eea 100644 --- a/components/omega/src/ocn/Eos.cpp +++ b/components/omega/src/ocn/Eos.cpp @@ -15,24 +15,35 @@ namespace OMEGA { /// Constructor for Teos10Eos Teos10Eos::Teos10Eos(const VertCoord *VCoord) - : MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell) { - SpecVolPCoeffs = Array2DReal("SpecVolPCoeffs", 6, VecLength); -} + : MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell) {} /// Constructor for LinearEos LinearEos::LinearEos(const VertCoord *VCoord) : MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell) {} +/// Constructor for Teos10 squared Brunt-Vaisala frequency +Teos10BruntVaisalaFreqSq::Teos10BruntVaisalaFreqSq(const VertCoord *VCoord) + : MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell) {} + +/// Constructor for Linear squared Brunt-Vaisala frequency +LinearBruntVaisalaFreqSq::LinearBruntVaisalaFreqSq(const VertCoord *VCoord) + : MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell), + ZMid(VCoord->ZMid) {} + /// Constructor for Eos Eos::Eos(const std::string &Name, ///< [in] Name for eos object const HorzMesh *Mesh, ///< [in] Horizontal mesh const VertCoord *VCoord ///< [in] Vertical coordinate ) - : ComputeSpecVolLinear(VCoord), ComputeSpecVolTeos10(VCoord), Name(Name), - Mesh(Mesh), VCoord(VCoord) { + : ComputeSpecVolLinear(VCoord), ComputeSpecVolTeos10(VCoord), + ComputeBruntVaisalaFreqSqLinear(VCoord), + ComputeBruntVaisalaFreqSqTeos10(VCoord), Name(Name), Mesh(Mesh), + VCoord(VCoord) { SpecVol = Array2DReal("SpecVol", Mesh->NCellsAll, VCoord->NVertLayers); SpecVolDisplaced = Array2DReal("SpecVolDisplaced", Mesh->NCellsAll, VCoord->NVertLayers); + BruntVaisalaFreqSq = + Array2DReal("BruntVaisalaFreqSq", Mesh->NCellsAll, VCoord->NVertLayers); defineFields(); } @@ -104,7 +115,7 @@ void Eos::init() { (EosTypeStr == "TEOS-10")) { eos->EosChoice = EosType::Teos10Eos; } else { - LOG_ERROR("Eos::init: Unknown EosType requested"); + ABORT_ERROR("Eos::init: Unknown EosType requested"); } } // end init @@ -177,9 +188,6 @@ void Eos::computeSpecVolDisp(const Array2DReal &ConservTemp, /// If EosChoice is Linear, the displaced specific /// volume is the same as the specific volume if (EosChoice == EosType::LinearEos) { - LOG_INFO("Eos::computeSpecVolDisp called with Linear EOS. " - "SpecVol is independent of pressure/depth, so the " - "displaced value will be the same as SpecVol."); parallelForOuter( "eos-linear", {Mesh->NCellsAll}, KOKKOS_LAMBDA(I4 ICell, const TeamMember &Team) { @@ -209,19 +217,75 @@ void Eos::computeSpecVolDisp(const Array2DReal &ConservTemp, } } +/// Compute squared Brunt-Vaisala frequency for all cells/layers +void Eos::computeBruntVaisalaFreqSq(const Array2DReal &ConservTemp, + const Array2DReal &AbsSalinity, + const Array2DReal &Pressure, + const Array2DReal &SpecVol) { + OMEGA_SCOPE(LocBruntVaisalaFreqSq, + BruntVaisalaFreqSq); /// Local view for computation + OMEGA_SCOPE( + LocComputeBruntVaisalaFreqSqLinear, + ComputeBruntVaisalaFreqSqLinear); /// Local view for linear computation + OMEGA_SCOPE( + LocComputeBruntVaisalaFreqSqTeos10, + ComputeBruntVaisalaFreqSqTeos10); /// Local view for TEOS-10 computation + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + + deepCopy(LocBruntVaisalaFreqSq, + 0); /// Initialize local Brunt-Vaisala frequency to zero + + /// Dispatch to the correct squared Brunt-Vaisala frequency calculation + if (EosChoice == EosType::LinearEos) { + /// If Linear EOS, use linear squared Brunt-Vaisala frequency calculation + parallelForOuter( + "bvf-linear", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 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) { + LocComputeBruntVaisalaFreqSqLinear(LocBruntVaisalaFreqSq, + ICell, KChunk, SpecVol); + }); + }); + } else if (EosChoice == EosType::Teos10Eos) { + /// If TEOS-10 EOS, use TEOS-10 squared Brunt-Vaisala frequency + /// calculation + parallelForOuter( + "bvf-teos10", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 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) { + LocComputeBruntVaisalaFreqSqTeos10( + LocBruntVaisalaFreqSq, ICell, KChunk, ConservTemp, + AbsSalinity, Pressure, SpecVol); + }); + }); + } +} + /// Define IO fields and metadata for output void Eos::defineFields() { /// Set field names (append Name if not default) - SpecVolFldName = "SpecVol"; - SpecVolDisplacedFldName = "SpecVolDisplaced"; + SpecVolFldName = "SpecVol"; + SpecVolDisplacedFldName = "SpecVolDisplaced"; + BruntVaisalaFreqSqFldName = "BruntVaisalaFreqSq"; if (Name != "Default") { SpecVolFldName.append(Name); SpecVolDisplacedFldName.append(Name); + BruntVaisalaFreqSqFldName.append(Name); } /// Create fields for state variables - int NDims = 2; + const Real FillValue = -9.99e30; + int NDims = 2; std::vector DimNames(NDims); DimNames[0] = "NCells"; DimNames[1] = "NVertLayers"; @@ -233,8 +297,8 @@ void Eos::defineFields() { "m3 kg-1", // Units "sea_water_specific_volume", // CF-ish Name 0.0, // Min valid value - 9.99E+30, // Max valid value - -9.99E+30, // Scalar used for undefined entries + std::numeric_limits::max(), // Max valid value + FillValue, // Scalar used for undefined entries NDims, // Number of dimensions DimNames // Dimension names ); @@ -246,8 +310,20 @@ void Eos::defineFields() { "m3 kg-1", // Units "sea_water_specific_volume_displaced", // CF-ish Name 0.0, // Min valid value - 9.99E+30, // Max valid value - -9.99E+30, // Scalar used for undefined entried + std::numeric_limits::max(), // Max valid value + FillValue, // Scalar used for undefined entried + NDims, // Number of dimensions + DimNames // Dimension names + ); + /// Create and register the BruntVaisalaFreqSq field + auto BruntVaisalaFreqSqField = + Field::create(BruntVaisalaFreqSqFldName, // Field name + "Brunt-Vaisala frequency squared", // Long Name + "s-2", // Units + "sea_water_brunt_vaisala_frequency_squared", // CF-ish Name + std::numeric_limits::min(), // Min valid value + std::numeric_limits::max(), // Max valid value + FillValue, // Scalar used for undefined entries NDims, // Number of dimensions DimNames // Dimension names ); @@ -262,10 +338,12 @@ void Eos::defineFields() { // Add fields to the EOS group EosGroup->addField(SpecVolDisplacedFldName); EosGroup->addField(SpecVolFldName); + EosGroup->addField(BruntVaisalaFreqSqFldName); // Attach Kokkos views to the fields SpecVolDisplacedField->attachData(SpecVolDisplaced); SpecVolField->attachData(SpecVol); + BruntVaisalaFreqSqField->attachData(BruntVaisalaFreqSq); } // end defineIOFields diff --git a/components/omega/src/ocn/Eos.h b/components/omega/src/ocn/Eos.h index 9b69432f9713..707297f01113 100644 --- a/components/omega/src/ocn/Eos.h +++ b/components/omega/src/ocn/Eos.h @@ -30,8 +30,6 @@ enum class EosType { /// TEOS10 75-term Polynomial Equation of State class Teos10Eos { public: - Array2DReal SpecVolPCoeffs; - /// constructor declaration Teos10Eos(const VertCoord *VCoord); @@ -45,47 +43,49 @@ class Teos10Eos { const Array2DReal &Pressure, I4 KDisp) const { - OMEGA_SCOPE(LocSpecVolPCoeffs, SpecVolPCoeffs); + Real SpecVolPCoeffs[6 * VecLength]; + const I4 KStart = chunkStart(KChunk, MinLayerCell(ICell)); const I4 KLen = chunkLength(KChunk, KStart, MaxLayerCell(ICell)); + for (int KVec = 0; KVec < KLen; ++KVec) { const I4 K = KStart + KVec; - /// Calculate the local specific volume polynomial pressure - /// coefficients - calcPCoeffs(LocSpecVolPCoeffs, KVec, ConservTemp(ICell, K), + /// coefficients with cell center values + calcPCoeffs(SpecVolPCoeffs, KVec, ConservTemp(ICell, K), AbsSalinity(ICell, K)); /// Calculate the specific volume at the given pressure - /// If KDisp is 0, we use the current pressure, otherwise we use the - /// displaced pressure (K + KDisp) + /// If KDisp is 0, we use the current pressure, otherwise + /// we use the displaced pressure (K + KDisp) /// Note: KDisp is only used for TEOS-10, for Linear EOS it /// is always 0. if (KDisp == 0) { // No displacement SpecVol(ICell, K) = calcRefProfile(Pressure(ICell, K)) + - calcDelta(LocSpecVolPCoeffs, KVec, Pressure(ICell, K)); + calcDelta(SpecVolPCoeffs, KVec, Pressure(ICell, K)); } else { // Displacement, use the displaced pressure I4 KTmp = Kokkos::min(K + KDisp, MaxLayerCell(ICell)); KTmp = Kokkos::max(MinLayerCell(ICell), KTmp); SpecVol(ICell, K) = calcRefProfile(Pressure(ICell, KTmp)) + - calcDelta(LocSpecVolPCoeffs, KVec, Pressure(ICell, KTmp)); + calcDelta(SpecVolPCoeffs, KVec, Pressure(ICell, KTmp)); } } } /// TEOS-10 helpers /// Calculate pressure polynomial coefficients for TEOS-10 - KOKKOS_FUNCTION void calcPCoeffs(Array2DReal SpecVolPCoeffs, const I4 K, - const Real Ct, const Real Sa) const { - constexpr Real SAu = 40.0 * 35.16504 / 35.0; - constexpr Real CTu = 40.0; + KOKKOS_FUNCTION void calcPCoeffs(Real (&SpecVolPCoeffs)[6 * VecLength], + const I4 KVec, const Real Ct, + const Real Sa) const { + constexpr Real SaNorm = 40.0 * 35.16504 / 35.0; + constexpr Real CtNorm = 40.0; constexpr Real DeltaS = 24.0; - Real Ss = Kokkos::sqrt((Sa + DeltaS) / SAu); - Real Tt = Ct / CTu; + Real Ss = Kokkos::sqrt((Sa + DeltaS) / SaNorm); + Real Tt = Ct / CtNorm; /// Coefficients for the polynomial expansion constexpr Real V000 = 1.0769995862e-03; @@ -163,18 +163,18 @@ class Teos10Eos { constexpr Real V014 = 3.1454099902e-07; constexpr Real V005 = 4.2369007180e-09; - SpecVolPCoeffs(5, K) = V005; - SpecVolPCoeffs(4, K) = V014 * Tt + V104 * Ss + V004; - SpecVolPCoeffs(3, K) = + SpecVolPCoeffs[5 + 6 * KVec] = V005; + SpecVolPCoeffs[4 + 6 * KVec] = V014 * Tt + V104 * Ss + V004; + SpecVolPCoeffs[3 + 6 * KVec] = (V023 * Tt + V113 * Ss + V013) * Tt + (V203 * Ss + V103) * Ss + V003; - SpecVolPCoeffs(2, K) = + SpecVolPCoeffs[2 + 6 * KVec] = (((V042 * Tt + V132 * Ss + V032) * Tt + (V222 * Ss + V122) * Ss + V022) * Tt + ((V312 * Ss + V212) * Ss + V112) * Ss + V012) * Tt + (((V402 * Ss + V302) * Ss + V202) * Ss + V102) * Ss + V002; - SpecVolPCoeffs(1, K) = + SpecVolPCoeffs[1 + 6 * KVec] = ((((V051 * Tt + V141 * Ss + V041) * Tt + (V231 * Ss + V131) * Ss + V031) * Tt + @@ -184,7 +184,7 @@ class Teos10Eos { Tt + ((((V501 * Ss + V401) * Ss + V301) * Ss + V201) * Ss + V101) * Ss + V001; - SpecVolPCoeffs(0, K) = + SpecVolPCoeffs[0 + 6 * KVec] = (((((V060 * Tt + V150 * Ss + V050) * Tt + (V240 * Ss + V140) * Ss + V040) * Tt + @@ -199,36 +199,39 @@ class Teos10Eos { V100) * Ss + V000; - - // could insert a check here (abs(value)> 0 value or MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell), + ZMid(VCoord->ZMid), NEdgesOnCell(Mesh->NEdgesOnCell), + AreaCell(Mesh->AreaCell), EdgesOnCell(Mesh->EdgesOnCell), + DvEdge(Mesh->DvEdge), DcEdge(Mesh->DcEdge) {} + +ConvectiveMix::ConvectiveMix(const VertCoord *VCoord) + : MinLayerCell(VCoord->MinLayerCell), MaxLayerCell(VCoord->MaxLayerCell) {} + +/// Constructor for VertMix +VertMix::VertMix(const std::string &Name, ///< [in] Name for VertMix object + const HorzMesh *Mesh, ///< [in] Horizontal mesh + const VertCoord *VCoord ///< [in] Vertical coordinate + ) + : ComputeVertMixConv(VCoord), ComputeVertMixShear(Mesh, VCoord), Name(Name), + Mesh(Mesh), VCoord(VCoord) { + VertDiff = Array2DReal("VertDiff", Mesh->NCellsAll, VCoord->NVertLayers); + VertVisc = Array2DReal("VertVisc", Mesh->NCellsAll, VCoord->NVertLayers); + + defineFields(); +} + +/// Destructor for VertMix +VertMix::~VertMix() {} + +/// Instance management +VertMix *VertMix::Instance = nullptr; + +/// Get instance of VertMix +VertMix *VertMix::getInstance() { return Instance; } + +/// Destroy instance of VertMix +void VertMix::destroyInstance() { + delete Instance; + Instance = nullptr; +} + +/// Initializes the VertMix (Vertical Mixing Coefficients) class and its +/// options. it ASSUMES that HorzMesh was initialized and initializes the +/// VertMix class by using the default mesh, reading the config file, and +/// setting parameters for the background, convective, and/or shear mixing +/// routines. Returns 0 on success, or an error code if any required option is +/// missing. +void VertMix::init() { + + if (!Instance) { + Instance = new VertMix("Default", HorzMesh::getDefault(), + VertCoord::getDefault()); + } + + Error Err; // error code + + /// Retrieve default VertMix + VertMix *DefVertMix = VertMix::getInstance(); + + /// Get VertMixConfig group from Omega config + Config *OmegaConfig = Config::getOmegaConfig(); + Config VertMixConfig("VertMix"); + Err += OmegaConfig->get(VertMixConfig); + CHECK_ERROR_ABORT(Err, "VertMix::init: VertMix group not found in Config"); + + /// Get Background from VertMixConfig + /// and set associated parameters + Config BackConfig("Background"); + Err += VertMixConfig.get(BackConfig); + CHECK_ERROR_ABORT( + Err, "VertMix::init: Background subgroup not found in VertMixConfig"); + + /// Get diffusivity and viscosity parameters + Err += BackConfig.get("Viscosity", DefVertMix->BackVisc); + CHECK_ERROR_ABORT( + Err, + "VertMix::init: Parameter Background:Viscosity not found in BackConfig"); + + Err += BackConfig.get("Diffusivity", DefVertMix->BackDiff); + CHECK_ERROR_ABORT(Err, "VertMix::init: Parameter Background:Diffusivity not " + "found in BackConfig"); + + /// Get Convective from VertMixConfig + Config ConvConfig("Convective"); + Err += VertMixConfig.get(ConvConfig); + CHECK_ERROR_ABORT( + Err, "VertMix::init: Convective subgroup not found in VertMixConfig"); + + /// Get convective diffusivity and viscosity parameters + Err += ConvConfig.get("Enable", DefVertMix->ComputeVertMixConv.Enabled); + CHECK_ERROR_ABORT( + Err, + "VertMix::init: Parameter Convective:Enable not found in ConvConfig"); + + if (!DefVertMix->ComputeVertMixConv.Enabled) { + LOG_INFO("VertMix::init: Convective mixing is disabled."); + } else { + LOG_INFO("VertMix::init: Convective mixing is enabled."); + Err += ConvConfig.get("Diffusivity", + DefVertMix->ComputeVertMixConv.ConvDiff); + CHECK_ERROR_ABORT(Err, "VertMix::init: Parameter Convective:Diffusivity " + "not found in ConvConfig"); + + Err += ConvConfig.get("TriggerBVF", + DefVertMix->ComputeVertMixConv.ConvTriggerBVF); + CHECK_ERROR_ABORT(Err, "VertMix::init: Parameter Convective:TriggerBVF " + "not found in ConvConfig"); + } + + /// Get Shear from VertMixConfig + Config ShearConfig("Shear"); + Err += VertMixConfig.get(ShearConfig); + CHECK_ERROR_ABORT( + Err, "VertMix::init: Shear subgroup not found in VertMixConfig"); + + /// Get shear diffusivity and viscosity parameters + Err += ShearConfig.get("Enable", DefVertMix->ComputeVertMixShear.Enabled); + CHECK_ERROR_ABORT( + Err, "VertMix::init: Parameter Shear:Enable not found in ShearConfig"); + + if (!DefVertMix->ComputeVertMixShear.Enabled) { + LOG_INFO("VertMix::init: Shear mixing is disabled."); + } else { + LOG_INFO("VertMix::init: Shear mixing is enabled."); + Err += ShearConfig.get("NuZero", + DefVertMix->ComputeVertMixShear.ShearNuZero); + CHECK_ERROR_ABORT( + Err, + "VertMix::init: Parameter Shear:NuZero not found in ShearConfig"); + + Err += + ShearConfig.get("Alpha", DefVertMix->ComputeVertMixShear.ShearAlpha); + CHECK_ERROR_ABORT( + Err, "VertMix::init: Parameter Shear:Alpha not found in ShearConfig"); + + Err += ShearConfig.get("Exponent", + DefVertMix->ComputeVertMixShear.ShearExponent); + CHECK_ERROR_ABORT( + Err, + "VertMix::init: Parameter Shear:Exponent not found in ShearConfig"); + } +} // end init + +/// Compute diffusivity and viscosity for all cells/layers (no displacement) +void VertMix::computeVertMix(const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array2DReal &BruntVaisalaFreqSq) { + OMEGA_SCOPE(LocVertDiff, VertDiff); /// Create a local view for computation + OMEGA_SCOPE(LocVertVisc, VertVisc); /// Create a local view for computation + OMEGA_SCOPE( + LocComputeVertMixConv, + ComputeVertMixConv); /// Local view for Convective VertMix computation + OMEGA_SCOPE( + LocComputeVertMixShear, + ComputeVertMixShear); /// Local view for Shear VertMix computation + OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell); + OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell); + + /// Initialize VertDiff and VertVisc to background values + deepCopy(LocVertDiff, BackDiff); + deepCopy(LocVertVisc, BackVisc); + + /// Dispatch to the correct VertMix calculation + if (LocComputeVertMixShear.Enabled && LocComputeVertMixConv.Enabled) { + parallelForOuter( + "VertMix-ConvPlusShear", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 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) { + LocComputeVertMixShear( + LocVertDiff, LocVertVisc, ICell, KChunk, NormalVelocity, + TangentialVelocity, BruntVaisalaFreqSq); + LocComputeVertMixConv(LocVertDiff, LocVertVisc, ICell, + KChunk, BruntVaisalaFreqSq); + }); + }); + } else if (LocComputeVertMixShear.Enabled) { + parallelForOuter( + "VertMix-ShearOnly", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 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) { + LocComputeVertMixShear( + LocVertDiff, LocVertVisc, ICell, KChunk, NormalVelocity, + TangentialVelocity, BruntVaisalaFreqSq); + }); + }); + } else if (LocComputeVertMixConv.Enabled) { + parallelForOuter( + "VertMix-ConvOnly", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(I4 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) { + LocComputeVertMixConv(LocVertDiff, LocVertVisc, ICell, + KChunk, BruntVaisalaFreqSq); + }); + }); + } else { + parallelFor( + "VertMix-Background", {Mesh->NCellsAll}, KOKKOS_LAMBDA(I4 ICell) { + LocVertDiff(ICell, 0) = 0.0_Real; + LocVertVisc(ICell, 0) = 0.0_Real; + }); + } +} + +/// Define IO fields and metadata for output +void VertMix::defineFields() { + + /// Set field names (append Name if not default) + VertDiffFldName = "VertDiff"; + VertViscFldName = "VertVisc"; + if (Name != "Default") { + VertDiffFldName.append(Name); + VertViscFldName.append(Name); + } + + /// Create fields for state variables + const Real FillValue = -9.99e30; + int NDims = 2; + std::vector DimNames(NDims); + DimNames[0] = "NCells"; + DimNames[1] = "NVertLayers"; + + /// Create and register the Diffusivity field + auto VertDiffField = + Field::create(VertDiffFldName, // Field name + "Vertical diffusivity at center of" + " cell and top of layer", // Long Name + "m2 s-1", // Units + "vertical_diffusivity", // CF-ish Name + 0.0, // Min valid value + std::numeric_limits::max(), // Max valid value + FillValue, // Scalar used for undefined entries + NDims, // Number of dimensions + DimNames // Dimension names + ); + /// Create and register the VertVisc field + auto VertViscField = + Field::create(VertViscFldName, // Field name + "Vertical viscosity at center of" + " cell and top of layer", // Long Name + "m2 s-1", // Units + "vertical_viscosity", // CF-ish Name + 0.0, // Min valid value + std::numeric_limits::max(), // Max valid value + FillValue, // Scalar used for undefined entried + NDims, // Number of dimensions + DimNames // Dimension names + ); + + // Create a field group for the vertmix-specific state fields + VertMixGroupName = "VertMix"; + if (Name != "Default") { + VertMixGroupName.append(Name); + } + auto VertMixGroup = FieldGroup::create(VertMixGroupName); + + // Add fields to the VertMix group + VertMixGroup->addField(VertDiffFldName); + VertMixGroup->addField(VertViscFldName); + + // Attach Kokkos views to the fields + VertDiffField->attachData(VertDiff); + VertViscField->attachData(VertVisc); + +} // end defineIOFields + +} // namespace OMEGA diff --git a/components/omega/src/ocn/VertMix.h b/components/omega/src/ocn/VertMix.h new file mode 100644 index 000000000000..bd3abcd1b185 --- /dev/null +++ b/components/omega/src/ocn/VertMix.h @@ -0,0 +1,192 @@ +#ifndef OMEGA_VERTMIX_H +#define OMEGA_VERTMIX_H +//===-- ocn/VertMix.cpp - Vertical Mixing Coefficients -----------*- C++ +//-*-===// +// +/// \file +/// \brief Contains functors for calculating vertical diffusivity and viscosity +/// +/// This header defines functors to be called by the time-stepping scheme +/// to calculate the vertical diffusivity and viscosity +// +//===----------------------------------------------------------------------===// + +#include "AuxiliaryState.h" +#include "Config.h" +#include "HorzMesh.h" +#include "MachEnv.h" +#include "OmegaKokkos.h" +#include "TimeMgr.h" +#include "VertCoord.h" +#include + +namespace OMEGA { + +class ConvectiveMix { + public: + bool Enabled = true; ///< Enable convective mixing flag + + // Convective mixing parameters + Real ConvDiff = + 1.0; ///< Convective vertical viscosity and diffusivity (m^2 s^-1) + Real ConvTriggerBVF = 0.0; ///< Reference density (kg m^-3) at (T,S)=(0,0) + + /// Constructor for ConvectiveMix + ConvectiveMix(const VertCoord *VCoord); + + KOKKOS_FUNCTION void + operator()(Array2DReal VertDiff, Array2DReal VertVisc, I4 ICell, I4 KChunk, + const Array2DReal &BruntVaisalaFreqSq) const { + + const I4 KStart = chunkStart(KChunk, MinLayerCell(ICell)); + const I4 KLen = chunkLength(KChunk, KStart, MaxLayerCell(ICell)); + + for (int KVec = 0; KVec < KLen; ++KVec) { + const I4 K = KStart + KVec; + if (K == 0) { + VertVisc(ICell, K) = 0.0_Real; + VertDiff(ICell, K) = 0.0_Real; + } else { + if (BruntVaisalaFreqSq(ICell, K) < ConvTriggerBVF) { + VertDiff(ICell, K) += ConvDiff; + VertVisc(ICell, K) += ConvDiff; + } + } + } + } + + private: + Array1DI4 MinLayerCell; + Array1DI4 MaxLayerCell; +}; + +class ShearMix { + public: + bool Enabled = true; ///< Enable shear mixing flag + + // Shear mixing parameters + Real ShearNuZero = + 0.005; ///< Numerator of Pacanowski and Philander (1981) Eq (1). + Real ShearAlpha = 5.0; ///< Alpha value used in Pacanowski and Philander + ///< (1981) Eqs (1) and (2). + Real ShearExponent = + 2.0; /// Exponent value used in Pacanowski and Philander (1981) Eqs (1). + + /// Constructor for ShearMix + ShearMix(const HorzMesh *Mesh, const VertCoord *VCoord); + + KOKKOS_FUNCTION void + operator()(Array2DReal VertDiff, Array2DReal VertVisc, I4 ICell, I4 KChunk, + const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array2DReal &BruntVaisalaFreqSq) const { + + const I4 KStart = chunkStart(KChunk, MinLayerCell(ICell)); + const I4 KLen = chunkLength(KChunk, KStart, MaxLayerCell(ICell)); + + for (int KVec = 0; KVec < KLen; ++KVec) { + const I4 K = KStart + KVec; + if (K == 0) { + VertVisc(ICell, K) = 0.0_Real; + VertDiff(ICell, K) = 0.0_Real; + } else { + Real ShearViscVal = 0.0; + Real InvAreaCell = 1.0_Real / AreaCell(ICell); + Real ShearSquared = 0.0; + for (int J = 0; J < NEdgesOnCell(ICell); ++J) { + I4 JEdge = EdgesOnCell(ICell, J); + Real Factor = + 0.5_Real * DcEdge(JEdge) * DvEdge(JEdge) * InvAreaCell; + Real DelNormVel = + NormalVelocity(JEdge, K - 1) - NormalVelocity(JEdge, K); + Real DelTangVel = TangentialVelocity(JEdge, K - 1) - + TangentialVelocity(JEdge, K); + ShearSquared = ShearSquared + Factor * (DelNormVel * DelNormVel + + DelTangVel * DelTangVel); + } + Real DelZMid = ZMid(ICell, K - 1) - ZMid(ICell, K); + ShearSquared = ShearSquared / (DelZMid * DelZMid); + + Real RichardsonNum = BruntVaisalaFreqSq(ICell, K) / + Kokkos::max(1.0e-12_Real, ShearSquared); + + ShearViscVal = + ShearNuZero / Kokkos::pow(1.0_Real + ShearAlpha * RichardsonNum, + ShearExponent); + VertVisc(ICell, K) += ShearViscVal; + VertDiff(ICell, K) += + VertVisc(ICell, K) / (1.0_Real + ShearAlpha * RichardsonNum); + } + } + } + + private: + Array1DReal DcEdge; + Array1DReal DvEdge; + Array1DReal AreaCell; + Array2DReal ZMid; + Array1DI4 NEdgesOnCell; + Array2DI4 EdgesOnCell; + Array1DI4 MinLayerCell; + Array1DI4 MaxLayerCell; +}; + +/// Class for Vertical Mixing Coefficient (VertMix) calculations +class VertMix { + public: + /// Get instance of VertMix + static VertMix *getInstance(); + + /// Destroy instance (frees Kokkos views) + static void destroyInstance(); + + Array2DReal VertDiff; ///< Vertical diffusivity field (m^2 s^-1) + Array2DReal VertVisc; ///< Vertical viscosity field (m^2 s^-1) + + std::string VertDiffFldName; ///< Field name for vertical diffusivity + std::string VertViscFldName; ///< Field name for vertical viscosity + std::string VertMixGroupName; ///< VertMix group name (for config) + std::string Name; ///< Name of this VertMix instance + + // Background mixing parameters + Real BackDiff = 1.0e-5; ///< Background vertical diffusivity (m^2 s^-1) + Real BackVisc = 1.0e-4; ///< Background vertical viscosity (m^2 s^-1) + + ConvectiveMix + ComputeVertMixConv; ///< Functor for Convective VertMix calculation + ShearMix ComputeVertMixShear; ///< Functor for Shear VertMix calculation + + /// Compute vertical diffusivity and viscosity for all cells/layers + void computeVertMix(const Array2DReal &NormalVelocity, + const Array2DReal &TangentialVelocity, + const Array2DReal &BruntVaisalaFreqSq); + + /// Initialize VertMix from config and mesh + static void init(); + + private: + /// Private constructor + VertMix(const std::string &Name, const HorzMesh *Mesh, + const VertCoord *VCoord); + + /// Private destructor + ~VertMix(); + + static VertMix *Instance; ///< Instance pointer + + // Delete copy and move constructors and assignment operators + VertMix(const VertMix &) = delete; + VertMix &operator=(const VertMix &) = delete; + VertMix(VertMix &&) = delete; + VertMix &operator=(VertMix &&) = delete; + + const HorzMesh *Mesh; ///< Horizontal mesh + const VertCoord *VCoord; ///< Vertical coordinate + + // Define fields and metadata + void defineFields(); + +}; // End class VertMix + +} // namespace OMEGA +#endif diff --git a/components/omega/test/CMakeLists.txt b/components/omega/test/CMakeLists.txt index 4cdf78c28fd2..2f4b182f3ed0 100644 --- a/components/omega/test/CMakeLists.txt +++ b/components/omega/test/CMakeLists.txt @@ -465,3 +465,14 @@ add_omega_test( ocn/VertCoordTest.cpp "-n;8" ) + +########################## +# Vert Mix test +########################## + +add_omega_test( + VERTMIX_TEST + testVertMix.exe + ocn/VertMixTest.cpp + "-n;4" +) diff --git a/components/omega/test/ocn/EosTest.cpp b/components/omega/test/ocn/EosTest.cpp index 0c168646eaed..5202594abedb 100644 --- a/components/omega/test/ocn/EosTest.cpp +++ b/components/omega/test/ocn/EosTest.cpp @@ -37,22 +37,30 @@ using namespace OMEGA; constexpr int NVertLayers = 60; /// Published values (TEOS-10 and linear) to test against -const Real TeosExpValue = 0.0009732819628; // Expected value for TEOS-10 eos +const Real TeosSVExpValue = + 0.0009732819628; // Expected value for TEOS-10 specific volume const Real LinearExpValue = - 0.0009784735812133072; // Expected value for Linear eos + 0.0009784735812133072; // Expected value for Linear specific volume +const Real TeosBVFExpValue = + 0.020913834194283325; // Expected value for TEOS-10 squared Brunt-Vaisala + // frequency +const Real LinearBVFExpValue = + 0.017834796542017275; // Expected value for Linear squared Brunt-Vaisala + // frequency +const Real GswBVFExpValue = + 0.02081197958166906; // Expected value from GSW-C library /// Test input values -double Sa = 30.0; // Absolute Salinity in g/kg -double Ct = 10.0; // Conservative Temperature in degC -double P = 1000.0; // Pressure in dbar -const I4 KDisp = 1; // Displace parcel to K=1 for TEOS-10 eos -const Real RTol = 1e-10; // Relative tolerance for isApprox checks +const Real Sa = 30.0; // Absolute Salinity in g/kg +const Real Ct = 10.0; // Conservative Temperature in degC +const Real P = 1000.0; // Pressure in dbar + +const I4 KDisp = 1; // Displate parcel to K=1 for TEOS-10 eos +const Real RTol = 1e-10; // Relative tolerance for isApprox checks /// The initialization routine for Eos testing. It calls various /// init routines, including the creation of the default decomposition. -I4 initEosTest(const std::string &mesh) { - - I4 Err = 0; +void initEosTest(const std::string &mesh) { /// Initialize the Machine Environment class - this also creates /// the default MachEnv. Then retrieve the default environment and @@ -63,6 +71,7 @@ I4 initEosTest(const std::string &mesh) { /// Initialize logging initLogging(DefEnv); + LOG_INFO("------ EOS Unit Tests ------"); /// Open and read config file Config("Omega"); @@ -88,30 +97,25 @@ I4 initEosTest(const std::string &mesh) { /// Retrieve Eos Eos *DefEos = Eos::getInstance(); - if (DefEos) { - LOG_INFO("EosTest: Eos retrieval PASS"); - } else { - Err++; - LOG_INFO("EosTest: Eos retrieval FAIL"); - return -1; - } - - return Err; + if (!DefEos) + ABORT_ERROR("EosTest: Eos retrieval FAIL"); } /// Test Linear EOS calculation for all cells/layers -int testEosLinear() { - int Err = 0; - const auto *Mesh = HorzMesh::getDefault(); - const auto *VCoord = VertCoord::getDefault(); +void testEosLinear() { + /// Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; /// Get Eos instance to test Eos *TestEos = Eos::getInstance(); TestEos->EosChoice = EosType::LinearEos; /// Create and fill ocean state arrays - Array2DReal SArray = Array2DReal("SArray", Mesh->NCellsAll, NVertLayers); - Array2DReal TArray = Array2DReal("TArray", Mesh->NCellsAll, NVertLayers); - Array2DReal PArray = Array2DReal("PArray", Mesh->NCellsAll, NVertLayers); + Array2DReal SArray = Array2DReal("SArray", NCellsAll, NVertLayers); + Array2DReal TArray = Array2DReal("TArray", NCellsAll, NVertLayers); + Array2DReal PArray = Array2DReal("PArray", NCellsAll, NVertLayers); /// Use Kokkos::deep_copy to fill the entire view with the ref value deepCopy(SArray, Sa); deepCopy(TArray, Ct); @@ -149,33 +153,39 @@ int testEosLinear() { }, NumMismatches); - auto SpecVolH = createHostMirrorCopy(SpecVol); + // If test fails, print bad values and abort if (NumMismatches != 0) { - Err++; - LOG_ERROR("EosTest: SpecVol Linear isApprox FAIL, " - "expected {}, got {} with {} mismatches", - LinearExpValue, SpecVolH(1, 1), NumMismatches); - } - if (Err == 0) { - LOG_INFO("EosTest SpecVolCalc Linear: PASS"); + auto SpecVolH = createHostMirrorCopy(TestEos->SpecVol); + for (int I = 0; I < NCellsAll; ++I) { + for (int K = 0; K < NVertLayers; ++K) { + if (!isApprox(SpecVolH(I, K), LinearExpValue, RTol)) + LOG_ERROR("EosTest: SpecVol Linear Bad Value: " + "SpecVol({},{}) = {}; Expected {}", + I, K, SpecVolH(I, K), LinearExpValue); + } + } + ABORT_ERROR("EosTest: SpecVol Linear FAIL with {} bad values", + NumMismatches); } - return Err; + return; } /// Test Linear EOS calculation with vertical displacement -int testEosLinearDisplaced() { - int Err = 0; - const auto *Mesh = HorzMesh::getDefault(); - const auto *VCoord = VertCoord::getDefault(); +void testEosLinearDisplaced() { + /// Get mesh and coord info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; /// Get Eos instance to test Eos *TestEos = Eos::getInstance(); TestEos->EosChoice = EosType::LinearEos; /// Create and fill ocean state arrays - Array2DReal SArray = Array2DReal("SArray", Mesh->NCellsAll, NVertLayers); - Array2DReal TArray = Array2DReal("TArray", Mesh->NCellsAll, NVertLayers); - Array2DReal PArray = Array2DReal("PArray", Mesh->NCellsAll, NVertLayers); + Array2DReal SArray = Array2DReal("SArray", NCellsAll, NVertLayers); + Array2DReal TArray = Array2DReal("TArray", NCellsAll, NVertLayers); + Array2DReal PArray = Array2DReal("PArray", NCellsAll, NVertLayers); /// Use Kokkos::deep_copy to fill the entire view with the ref value deepCopy(SArray, Sa); deepCopy(TArray, Ct); @@ -214,33 +224,163 @@ int testEosLinearDisplaced() { }, NumMismatches); - auto SpecVolDisplacedH = createHostMirrorCopy(SpecVolDisplaced); + // If test fails, print bad values and abort if (NumMismatches != 0) { - Err++; - LOG_ERROR("EosTest: Linear SpecVolDisp isApprox FAIL, " - "expected {}, got {} with {} mismatches", - LinearExpValue, SpecVolDisplacedH(1, 1), NumMismatches); + auto SpecVolDisplacedH = createHostMirrorCopy(SpecVolDisplaced); + for (int I = 0; I < NCellsAll; ++I) { + for (int K = 0; K < NVertLayers; ++K) { + if (!isApprox(SpecVolDisplacedH(I, K), LinearExpValue, RTol)) + LOG_ERROR("EosTest: SpecVol Linear Displaced Bad Value: " + "SpecVol({},{}) = {}; Expected {}", + I, K, SpecVolDisplacedH(I, K), LinearExpValue); + } + } + ABORT_ERROR("EosTest: Linear SpecVolDisp FAIL with {} bad values ", + NumMismatches); } - if (Err == 0) { - LOG_INFO("EosTest SpecVolCalcDisp Linear: PASS"); + + return; +} + +/// Test linear squared Brunt-Vaisala frequency calculation for all cells/layers +void testBruntVaisalaFreqSqLinear() { + /// Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; + /// Get Eos instance to test + Eos *TestEos = Eos::getInstance(); + TestEos->EosChoice = EosType::LinearEos; + + /// Create and fill ocean state arrays + Array2DReal SArray = Array2DReal("SArray", NCellsAll, NVertLayers); + Array2DReal TArray = Array2DReal("TArray", NCellsAll, NVertLayers); + Array2DReal PArray = Array2DReal("PArray", NCellsAll, NVertLayers); + /// Use deep copy to initialize results to zero + deepCopy(TestEos->SpecVol, 0.0); + deepCopy(TestEos->BruntVaisalaFreqSq, 0.0); + + // fill remaining entries with sample values that should lead to ref result + // for K = 1. + OMEGA_SCOPE(ZMid, VCoord->ZMid); + parallelFor( + "populateArrays", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + if (K == 0) { + ZMid(ICell, 0) = -992.1173890198451_Real; + SArray(ICell, 0) = Sa - 1.0_Real; + TArray(ICell, 0) = Ct + 15.0_Real; + PArray(ICell, 0) = P; + } else if (K == 1) { + ZMid(ICell, 1) = -993.1071379053125_Real; + SArray(ICell, 1) = Sa; + TArray(ICell, 1) = Ct + 10.0_Real; + PArray(ICell, 1) = P + 1.0_Real; + } else if (K == 2) { + ZMid(ICell, 2) = -994.0968821072275_Real; + SArray(ICell, 2) = Sa + 1.0_Real; + TArray(ICell, 2) = Ct + 5.0_Real; + PArray(ICell, K) = P + 2.0_Real; + } else { // fill rest to valid junk to avoid NaNs or Inf + ZMid(ICell, K) = -994.0968821072275_Real - 0.1_Real * K; + SArray(ICell, K) = Sa + 1.0_Real + 0.1_Real * K; + TArray(ICell, K) = Ct + 5.0_Real - 0.01_Real * K; + PArray(ICell, K) = P + 2.0_Real + 0.1_Real * K; + } + }); + + /// Compute specific volume first + TestEos->computeSpecVol(TArray, SArray, PArray); + Array2DReal SpecVol = TestEos->SpecVol; + + /// Compute squared Brunt-Vaisala frequency + TestEos->computeBruntVaisalaFreqSq(TArray, SArray, PArray, SpecVol); + + const auto &MinLayerCell = VCoord->MinLayerCell; + const auto &MaxLayerCell = VCoord->MaxLayerCell; + + /// Check all array values against expected value + int NumMismatches = 0; + OMEGA_SCOPE(BruntVaisalaFreqSq, TestEos->BruntVaisalaFreqSq); + parallelReduceOuter( + "CheckBruntVaisalaSq-Linear", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { // should be zero + if (BruntVaisalaFreqSq(ICell, K) != 0.0) + InnerCount++; + } else if (K == 1) { // should be ref value + if (!isApprox(BruntVaisalaFreqSq(ICell, K), + LinearBVFExpValue, RTol)) + InnerCount++; + } else { // just check for unreasonable values + if (BruntVaisalaFreqSq(ICell, K) == 0.0 or + Kokkos::isnan(BruntVaisalaFreqSq(ICell, K)) or + Kokkos::isinf(BruntVaisalaFreqSq(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + // If test fails, print bad values and abort + if (NumMismatches != 0) { + auto BruntVaisalaFreqSqH = createHostMirrorCopy(BruntVaisalaFreqSq); + for (int I = 0; I < NCellsAll; ++I) { + // top layer should be zero + if (BruntVaisalaFreqSqH(I, 0) != 0.0) + LOG_ERROR("EosTest: Brunt-Vaisala Linear Bad Value: " + "BruntVaisala({},{}) = {}; Expected {}", + I, 0, BruntVaisalaFreqSqH(I, 0), 0.0); + // K = 1 should be ref value + if (!isApprox(BruntVaisalaFreqSqH(I, 1), LinearBVFExpValue, RTol)) + LOG_ERROR("EosTest: Brunt-Vaisala Linear Bad Value: " + "BruntVaisala({},{}) = {}; Expected {}", + I, 1, BruntVaisalaFreqSqH(I, 1), LinearBVFExpValue); + // remaining values just check for other conditions + for (int K = 2; K < NVertLayers; ++K) { + if (BruntVaisalaFreqSqH(I, K) == 0.0 or + Kokkos::isnan(BruntVaisalaFreqSqH(I, K)) or + Kokkos::isinf(BruntVaisalaFreqSqH(I, K))) + LOG_ERROR("EosTest: Brunt-Vaisala Linear Bad Value: " + "BruntVaisala({},{}) = {}", + I, K, BruntVaisalaFreqSqH(I, K)); + } + } + ABORT_ERROR("EosTest: BruntVaisala Linear FAIL with {} bad values", + NumMismatches); } - return Err; + return; } /// Test TEOS-10 EOS calculation for all cells/layers -int testEosTeos10() { - int Err = 0; - const auto *Mesh = HorzMesh::getDefault(); - const auto *VCoord = VertCoord::getDefault(); +void testEosTeos10() { + /// Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; /// Get Eos instance to test Eos *TestEos = Eos::getInstance(); TestEos->EosChoice = EosType::Teos10Eos; /// Create and fill ocean state arrays - Array2DReal SArray = Array2DReal("SArray", Mesh->NCellsAll, NVertLayers); - Array2DReal TArray = Array2DReal("TArray", Mesh->NCellsAll, NVertLayers); - Array2DReal PArray = Array2DReal("PArray", Mesh->NCellsAll, NVertLayers); + Array2DReal SArray = Array2DReal("SArray", NCellsAll, NVertLayers); + Array2DReal TArray = Array2DReal("TArray", NCellsAll, NVertLayers); + Array2DReal PArray = Array2DReal("PArray", NCellsAll, NVertLayers); /// Use Kokkos::deep_copy to fill the entire view with the ref value deepCopy(SArray, Sa); deepCopy(TArray, Ct); @@ -267,7 +407,7 @@ int testEosTeos10() { Team, KRange, INNER_LAMBDA(int KOff, int &InnerCount) { const int K = KMin + KOff; - if (!isApprox(SpecVol(ICell, K), TeosExpValue, RTol)) { + if (!isApprox(SpecVol(ICell, K), TeosSVExpValue, RTol)) { InnerCount++; } }, @@ -278,33 +418,39 @@ int testEosTeos10() { }, NumMismatches); - auto SpecVolH = createHostMirrorCopy(SpecVol); + // If test fails, print bad values and abort if (NumMismatches != 0) { - Err++; - LOG_ERROR("EosTest: TEOS SpecVol isApprox FAIL, " - "expected {}, got {} with {} mismatches", - TeosExpValue, SpecVolH(1, 1), NumMismatches); - } - if (Err == 0) { - LOG_INFO("EosTest SpecVolCalc TEOS-10: PASS"); + auto SpecVolH = createHostMirrorCopy(SpecVol); + for (int I = 0; I < NCellsAll; ++I) { + for (int K = 0; K < NVertLayers; ++K) { + if (!isApprox(SpecVolH(I, K), LinearExpValue, RTol)) + LOG_ERROR("EosTest: SpecVol TEOS Bad Value: " + "SpecVol({},{}) = {}; Expected {}", + I, K, SpecVolH(I, K), LinearExpValue); + } + } + ABORT_ERROR("EosTest: SpecVol TEOS FAIL with {} bad values", + NumMismatches); } - return Err; + return; } /// Test TEOS-10 EOS calculation with vertical displacement -int testEosTeos10Displaced() { - int Err = 0; - const auto *Mesh = HorzMesh::getDefault(); - const auto *VCoord = VertCoord::getDefault(); +void testEosTeos10Displaced() { + /// Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; /// Get Eos instance to test Eos *TestEos = Eos::getInstance(); TestEos->EosChoice = EosType::Teos10Eos; /// Create and fill ocean state arrays - Array2DReal SArray = Array2DReal("SArray", Mesh->NCellsAll, NVertLayers); - Array2DReal TArray = Array2DReal("TArray", Mesh->NCellsAll, NVertLayers); - Array2DReal PArray = Array2DReal("PArray", Mesh->NCellsAll, NVertLayers); + Array2DReal SArray = Array2DReal("SArray", NCellsAll, NVertLayers); + Array2DReal TArray = Array2DReal("TArray", NCellsAll, NVertLayers); + Array2DReal PArray = Array2DReal("PArray", NCellsAll, NVertLayers); /// Use Kokkos::deep_copy to fill the entire view with the ref value deepCopy(SArray, Sa); deepCopy(TArray, Ct); @@ -331,7 +477,7 @@ int testEosTeos10Displaced() { Team, KRange, INNER_LAMBDA(int KOff, int &InnerCount) { const int K = KMin + KOff; - if (!isApprox(SpecVolDisplaced(ICell, K), TeosExpValue, + if (!isApprox(SpecVolDisplaced(ICell, K), TeosSVExpValue, RTol)) { InnerCount++; } @@ -343,22 +489,150 @@ int testEosTeos10Displaced() { }, NumMismatches); - auto SpecVolDisplacedH = createHostMirrorCopy(SpecVolDisplaced); + // If test fails, print bad values and abort if (NumMismatches != 0) { - Err++; - LOG_ERROR("EosTest: TEOS SpecVolDisp isApprox FAIL, " - "expected {}, got {} with {} mismatches", - TeosExpValue, SpecVolDisplacedH(1, 1), NumMismatches); + auto SpecVolDisplacedH = createHostMirrorCopy(SpecVolDisplaced); + for (int I = 0; I < NCellsAll; ++I) { + for (int K = 0; K < NVertLayers; ++K) { + if (!isApprox(SpecVolDisplacedH(I, K), LinearExpValue, RTol)) + LOG_ERROR("EosTest: SpecVol Displaced TEOS Bad Value: " + "SpecVol({},{}) = {}; Expected {}", + I, K, SpecVolDisplacedH(I, K), LinearExpValue); + } + } + ABORT_ERROR("EosTest: SpecVol Displaced TEOS FAIL with {} bad values", + NumMismatches); } - if (Err == 0) { - LOG_INFO("EosTest SpecVolCalcDisp TEOS-10: PASS"); + + return; +} + +/// Test TEOS-10 squared Brunt-Vaisala frequency calculation for all cells/layer +void testBruntVaisalaFreqSqTeos10() { + /// Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; + /// Get Eos instance to test + Eos *TestEos = Eos::getInstance(); + TestEos->EosChoice = EosType::Teos10Eos; + + /// Create and fill ocean state arrays + Array2DReal SArray = Array2DReal("SArray", NCellsAll, NVertLayers); + Array2DReal TArray = Array2DReal("TArray", NCellsAll, NVertLayers); + Array2DReal PArray = Array2DReal("PArray", NCellsAll, NVertLayers); + /// Use deep copy to initialize results to zero + deepCopy(TestEos->BruntVaisalaFreqSq, 0.0); + deepCopy(TestEos->SpecVol, 0.0); + + /// Fill inputs with values that should lead to ref result for K=1 + OMEGA_SCOPE(ZMid, VCoord->ZMid); + parallelFor( + "populateArrays", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + if (K == 0) { + ZMid(ICell, 0) = -992.1173890198451_Real; + SArray(ICell, 0) = Sa - 1.0_Real; + TArray(ICell, 0) = Ct + 15.0_Real; + PArray(ICell, 0) = P; + } else if (K == 1) { + ZMid(ICell, 1) = -993.1071379053125_Real; + SArray(ICell, 1) = Sa; + TArray(ICell, 1) = Ct + 10.0_Real; + PArray(ICell, 1) = P + 1.0_Real; + } else if (K == 2) { + ZMid(ICell, 2) = -994.0968821072275_Real; + SArray(ICell, 2) = Sa + 1.0_Real; + TArray(ICell, 2) = Ct + 5.0_Real; + PArray(ICell, K) = P + 2.0_Real; + } else { // fill rest with valid junk to avoid Nans and Inf + ZMid(ICell, K) = -994.0968821072275_Real - 0.1_Real * K; + SArray(ICell, K) = Sa + 1.0_Real + 0.1_Real * K; + TArray(ICell, K) = Ct + 5.0_Real - 0.01_Real * K; + PArray(ICell, K) = P + 2.0_Real + 0.1_Real * K; + } + }); + + /// Compute specific volume first + TestEos->computeSpecVol(TArray, SArray, PArray); + Array2DReal SpecVol = TestEos->SpecVol; + + /// Compute Brunt-Vaisala frequency + TestEos->computeBruntVaisalaFreqSq(TArray, SArray, PArray, SpecVol); + + const auto &MinLayerCell = VCoord->MinLayerCell; + const auto &MaxLayerCell = VCoord->MaxLayerCell; + + /// Check all array values against expected value + int NumMismatches = 0; + OMEGA_SCOPE(BruntVaisalaFreqSq, TestEos->BruntVaisalaFreqSq); + parallelReduceOuter( + "CheckSpecVolMatrix-Teos", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { // should be zero at top + if (BruntVaisalaFreqSq(ICell, K) != 0.0) + InnerCount++; + } else if (K == 1) { // should be ref value + if (!isApprox(BruntVaisalaFreqSq(ICell, K), TeosBVFExpValue, + RTol)) + InnerCount++; + } else { // just check for unreasonable values + if (BruntVaisalaFreqSq(ICell, K) == 0.0 or + Kokkos::isnan(BruntVaisalaFreqSq(ICell, K)) or + Kokkos::isinf(BruntVaisalaFreqSq(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + // If test fails, print bad values and abort + if (NumMismatches != 0) { + auto BruntVaisalaFreqSqH = createHostMirrorCopy(BruntVaisalaFreqSq); + for (int ICell = 0; ICell < NCellsAll; ++ICell) { + // top layer should be zero + if (BruntVaisalaFreqSqH(ICell, 0) != 0.0) + LOG_ERROR("EosTest: Brunt-Vaisala TEOS Bad Value: " + "BruntVaisala({},{}) = {}; Expected {}", + ICell, 0, BruntVaisalaFreqSqH(ICell, 0), 0.0); + // K = 1 should be ref value + if (!isApprox(BruntVaisalaFreqSqH(ICell, 1), TeosBVFExpValue, RTol)) + LOG_ERROR("EosTest: Brunt-Vaisala TEOS Bad Value: " + "BruntVaisala({},{}) = {}; Expected {}", + ICell, 1, BruntVaisalaFreqSqH(ICell, 1), TeosBVFExpValue); + // remaining values just check for other conditions + for (int K = 2; K < NVertLayers; ++K) { + if (BruntVaisalaFreqSqH(ICell, K) == 0.0 or + Kokkos::isnan(BruntVaisalaFreqSqH(ICell, K)) or + Kokkos::isinf(BruntVaisalaFreqSqH(ICell, K))) + LOG_ERROR("EosTest: Brunt-Vaisala TEOS Bad Value: " + "BruntVaisala({},{}) = {}", + ICell, K, BruntVaisalaFreqSqH(ICell, K)); + } + } + ABORT_ERROR("EosTest: BruntVaisala TEOS FAIL with {} bad values", + NumMismatches); } - return Err; + return; } /// Finalize and clean up all test infrastructure void finalizeEosTest() { + Eos::destroyInstance(); VertCoord::clear(); HorzMesh::clear(); Halo::clear(); @@ -369,80 +643,102 @@ void finalizeEosTest() { } /// Test that the external GSW-C library returns the expected specific volume -int checkValueGswcSpecVol() { - int Err = 0; +void checkValueGswcSpecVol() { const Real RTol = 1e-10; /// Get specific volume from GSW-C library double SpecVol = gsw_specvol(Sa, Ct, P); /// Check the value against the expected TEOS-10 value - bool Check = isApprox(SpecVol, TeosExpValue, RTol); + bool Check = isApprox(SpecVol, TeosSVExpValue, RTol); if (!Check) { - Err++; - LOG_ERROR( - "checkValueGswcSpecVol: SpecVol isApprox FAIL, expected {}, got {}", - TeosExpValue, SpecVol); + ABORT_ERROR("checkValueGswcSpecVol: SpecVol FAIL, expected {}, got {}", + TeosSVExpValue, SpecVol); } - if (Err == 0) { - LOG_INFO("checkValueGswcSpecVol: PASS"); + return; +} + +/// Test that the external GSW-C library returns the expected N2 +void checkValueGswcN2() { + const Real RTol = 1e-10; + + // Number of intervals (nz) + int Nz = 2; + + // Input arrays: length nz+1 + double Salt[4] = {Sa - 1.0, Sa, Sa + 1.0}; // Absolute Salinity (g/kg) + double Temp[4] = {Ct + 15.0, Ct + 10.0, + Ct + 5.0}; // Conservative Temperature (deg C) + double Press[4] = {P, P + 1.0, P + 2.0}; // Pressure (dbar) + + // Latitude (degrees north) + double Latitude[4] = {0.0, 0.0, 0.0}; + + // Output arrays: length nz + double N2[Nz]; // Brunt–Väisälä frequency squared + double PMid[Nz]; // Midpoint pressure + + /// Get specific volume from GSW-C library + gsw_nsquared(Salt, Temp, Press, Latitude, Nz, N2, PMid); + + /// Check the value against the expected TEOS-10 value + bool Check = isApprox(N2[0], GswBVFExpValue, RTol); + if (!Check) { + ABORT_ERROR("checkValueGswcN2: N2 FAIL, expected {}, got {}", + GswBVFExpValue, N2[0]); } - return Err; + return; } -// the main test (all in one to have the same log) +// the main tests (all in one to have the same log): // Single value test: // --> test calls the external GSW-C library // and compares the specific volume to the published value // Full array tests: // --> one tests the value on a Eos with linear option // --> next checks the value on a Eos with linear displaced option +// --> next checks the value of the linear squared Brunt Vaisala Freq. +// calculation // --> next checks the value on a Eos with TEOS-10 option // --> next checks the value on a Eos with TEOS-10 displaced option -int eosTest(const std::string &MeshFile = "OmegaMesh.nc") { - int Err = initEosTest(MeshFile); - if (Err != 0) { - LOG_CRITICAL("EosTest: Error initializing"); - } +// --> last checks the value of the TOES-10 squared Brunt Vaisala Freq. +// calculation +void eosTest(const std::string &MeshFile = "OmegaMesh.nc") { + initEosTest(MeshFile); const auto &Mesh = HorzMesh::getDefault(); - LOG_INFO("Single value checks:"); - Err += checkValueGswcSpecVol(); + checkValueGswcSpecVol(); + checkValueGswcN2(); - LOG_INFO("Full array checks:"); - Err += testEosLinear(); - Err += testEosLinearDisplaced(); - Err += testEosTeos10(); - Err += testEosTeos10Displaced(); + testEosLinear(); + testEosLinearDisplaced(); + testBruntVaisalaFreqSqLinear(); + testEosTeos10(); + testEosTeos10Displaced(); + testBruntVaisalaFreqSqTeos10(); - if (Err == 0) { - LOG_INFO("EosTest: Successful completion"); - } finalizeEosTest(); - return Err; + return; } // The test driver for Eos testing int main(int argc, char *argv[]) { - int RetVal = 0; - MPI_Init(&argc, &argv); Kokkos::initialize(argc, argv); Pacer::initialize(MPI_COMM_WORLD); Pacer::setPrefix("Omega:"); - RetVal += eosTest(); + eosTest(); + + LOG_INFO("------ EOS Unit Tests Successful ------"); - Eos::destroyInstance(); Pacer::finalize(); Kokkos::finalize(); MPI_Finalize(); - if (RetVal >= 256) - RetVal = 255; - - return RetVal; + // If we made it here, test is successful + return 0; } // end of main //===-----------------------------------------------------------------------===/ diff --git a/components/omega/test/ocn/VertMixTest.cpp b/components/omega/test/ocn/VertMixTest.cpp new file mode 100644 index 000000000000..9c0cf2101867 --- /dev/null +++ b/components/omega/test/ocn/VertMixTest.cpp @@ -0,0 +1,804 @@ +//===-- Test driver for OMEGA Vertical Mixing Coefficients -------*- C++ -*-===/ +// +/// \file +/// \brief Test driver for OMEGA Vertical Mixing Coefficients +/// +/// This driver tests that VertMix can be called and returns expected values +/// of diffusivity, viscosity and Brunt-Vaisala frequency +/// +//===-----------------------------------------------------------------------===/ + +#include "VertMix.h" +#include "Config.h" +#include "DataTypes.h" +#include "Decomp.h" +#include "Dimension.h" +#include "Field.h" +#include "HorzMesh.h" +#include "IO.h" +#include "Logging.h" +#include "MachEnv.h" +#include "OceanTestCommon.h" +#include "OmegaKokkos.h" +#include "Pacer.h" +#include "VertCoord.h" +#include "mpi.h" + +using namespace OMEGA; + +/// Test constants and expected values +constexpr int NVertLayers = 60; + +/// Values to test against +const Real VertDiffExpValueN = + 1.0393923290498872; // Expected value for diffusivity for positive BVF +const Real VertViscExpValueN = + 1.0198269656595984; // Expected value for viscosity for positive BVF +const Real VertDiffExpValueP = + 0.0015685660274841844; // Expected value for diffusivity for negative BVF +const Real VertViscExpValueP = + 0.002332474675614262; // Expected value for viscosity for negative BVF +const Real VertDiffBackExp = + 1.0e-5; // Expected value for background diffusivity +const Real VertViscBackExp = 1.0e-4; // Expected value for background viscosity +const Real VertDiffConvExp = + 1.0; // Expected value for convective diffusivity/viscosity +const Real VertDiffShearExp = + 0.039183698912901; // Expected value for shear diffusivity +const Real VertViscShearExp = + 0.01972696565959843; // Expected value for shear viscosity + +/// Test input values +const Real BVFP = 0.1; // Positive Brunt-Vaisala frequency in s^-2 +const Real BVFN = -0.1; // Negative Brunt-Vaisala frequency in s^-2 +const Real NV = 1.0; // Normal velocity in m/s +const Real TV = 1.0; // Tangential velocity in m/s +const Real RTol = 1e-8; // Relative tolerance for isApprox checks + +/// The initialization routine for VertMix testing. It calls various +/// init routines, including the creation of the default decomposition. +void initVertMixTest() { + + /// Initialize the Machine Environment class - this also creates + /// the default MachEnv. Then retrieve the default environment and + /// some needed data members. + MachEnv::init(MPI_COMM_WORLD); + MachEnv *DefEnv = MachEnv::getDefault(); + MPI_Comm DefComm = DefEnv->getComm(); + + /// Initialize logging + initLogging(DefEnv); + LOG_INFO("------ Vertical Mixing Unit Tests ------"); + + /// Open and read config file + Config("Omega"); + Config::readAll("omega.yml"); + + // Initialize the IO system + IO::init(DefComm); + + /// Initialize decomposition + Decomp::init(); + + /// Initialize Halo + Halo::init(); + + /// Initialize mesh + HorzMesh::init(); + + /// Initialize vertical coordinate + VertCoord::init(false); + + /// Initialize VertMix + VertMix::init(); + + /// Retrieve VertMix + VertMix *DefVertMix = VertMix::getInstance(); + if (!DefVertMix) + ABORT_ERROR("VertMixTest: VertMix retrieval FAIL"); +} + +void testBackVertMix() { + // Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsSize = Mesh->NCellsSize; + I4 NEdgesSize = Mesh->NEdgesSize; + I4 NCellsAll = Mesh->NCellsAll; + I4 NEdgesAll = Mesh->NEdgesAll; + OMEGA_SCOPE(ZMid, VCoord->ZMid); + + /// Get VertMix instance to test + VertMix *TestVertMix = VertMix::getInstance(); + + /// Create and fill ocean state arrays + auto NormalVelEdge = Array2DReal("NormalVelEdge", NEdgesSize, NVertLayers); + auto TangVelEdge = Array2DReal("TangVelEdge", NEdgesSize, NVertLayers); + auto BruntVaisalaFreqSqCell = + Array2DReal("BruntVaisalaFreqSqCell", NCellsSize, NVertLayers); + + /// Use deep copy initialize with reference or zero values + deepCopy(NormalVelEdge, NV); + deepCopy(TangVelEdge, TV); + deepCopy(BruntVaisalaFreqSqCell, BVFN); + deepCopy(TestVertMix->VertDiff, 0.0); + deepCopy(TestVertMix->VertVisc, 0.0); + + parallelFor( + "populateArrays", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { ZMid(ICell, K) = -K; }); + + parallelFor( + "populateArrays", {NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + NormalVelEdge(IEdge, K) = NormalVelEdge(IEdge, K) + 0.5 * K; + TangVelEdge(IEdge, K) = TangVelEdge(IEdge, K) + 0.5 * K; + }); + + /// Compute only background vertical viscosity and diffusivity + TestVertMix->BackDiff = 1.0e-5; + TestVertMix->BackVisc = 1.0e-4; + TestVertMix->ComputeVertMixConv.Enabled = false; + TestVertMix->ComputeVertMixShear.Enabled = false; + TestVertMix->computeVertMix(NormalVelEdge, TangVelEdge, + BruntVaisalaFreqSqCell); + + const auto &MinLayerCell = VCoord->MinLayerCell; + const auto &MaxLayerCell = VCoord->MaxLayerCell; + + Array2DReal BackVertVisc = TestVertMix->VertVisc; + Array2DReal BackVertDiff = TestVertMix->VertDiff; + + /// Check total Visc against linear addition of components + int NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-BackgroundVisc", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + // Surface layer should be zero + if (BackVertVisc(ICell, K) != 0.0_Real) + InnerCount++; + } else { + if (!isApprox(BackVertVisc(ICell, K), VertViscBackExp, + RTol)) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) + ABORT_ERROR("TestVertMixBack: VertVisc FAIL, " + "expected {}, got {} with {} mismatches", + VertViscBackExp, BackVertVisc(1, 1), NumMismatches); + + /// Check total Diff against linear addition of components + NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-BackgroundDiff", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + // Surface layer should be zero + if (BackVertDiff(ICell, K) != 0.0_Real) + InnerCount++; + } else { + if (!isApprox(BackVertDiff(ICell, K), VertDiffBackExp, + RTol)) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto BackVertDiffH = createHostMirrorCopy(BackVertDiff); + ABORT_ERROR("TestVertMixBack: VertDiff FAIL, " + "expected {}, got {} with {} mismatches", + VertDiffBackExp, BackVertDiffH(1, 1), NumMismatches); + } + + return; +} + +void testConvVertMix() { + + // Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; + I4 NEdgesAll = Mesh->NEdgesAll; + OMEGA_SCOPE(ZMid, VCoord->ZMid); + + /// Get VertMix instance to test + VertMix *TestVertMix = VertMix::getInstance(); + + /// Create and fill ocean state arrays + auto NormalVelEdge = Array2DReal("NormalVelEdge", NEdgesAll, NVertLayers); + auto TangVelEdge = Array2DReal("TangVelEdge", NEdgesAll, NVertLayers); + auto BruntVaisalaFreqSqCell = + Array2DReal("BruntVaisalaFreqSqCell", NCellsAll, NVertLayers); + + /// Use deep copy to initialize with the ref value + deepCopy(NormalVelEdge, NV); + deepCopy(TangVelEdge, TV); + deepCopy(BruntVaisalaFreqSqCell, BVFN); + deepCopy(TestVertMix->VertDiff, 0.0); + deepCopy(TestVertMix->VertVisc, 0.0); + + parallelFor( + "populateArrays", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { ZMid(ICell, K) = -K; }); + + parallelFor( + "populateArrays", {NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + NormalVelEdge(IEdge, K) = NormalVelEdge(IEdge, K) + 0.5 * K; + TangVelEdge(IEdge, K) = TangVelEdge(IEdge, K) + 0.5 * K; + }); + + /// Compute only convective vertical viscosity and diffusivity + TestVertMix->BackDiff = 0.0; + TestVertMix->BackVisc = 0.0; + TestVertMix->ComputeVertMixConv.Enabled = true; + TestVertMix->ComputeVertMixShear.Enabled = false; + TestVertMix->computeVertMix(NormalVelEdge, TangVelEdge, + BruntVaisalaFreqSqCell); + + const auto &MinLayerCell = VCoord->MinLayerCell; + const auto &MaxLayerCell = VCoord->MaxLayerCell; + + Array2DReal ConvVertVisc = TestVertMix->VertVisc; + Array2DReal ConvVertDiff = TestVertMix->VertDiff; + + /// Check total Visc against linear addition of components + int NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-ConvectiveVisc", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + // Surface layer should be zero + if (ConvVertVisc(ICell, K) != 0.0_Real) + InnerCount++; + } else { + if (!isApprox(ConvVertVisc(ICell, K), VertDiffConvExp, + RTol)) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto ConvVertViscH = createHostMirrorCopy(ConvVertVisc); + ABORT_ERROR("TestVertMixConv: VertVisc FAIL, " + "expected {}, got {} with {} mismatches", + VertDiffConvExp, ConvVertViscH(1, 1), NumMismatches); + } + + /// Check total Diff against linear addition of components + NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-ConvectiveDiff", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + // Surface layer should be zero + if (ConvVertDiff(ICell, K) != 0.0_Real) + InnerCount++; + } else { + if (!isApprox(ConvVertDiff(ICell, K), VertDiffConvExp, + RTol)) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto ConvVertDiffH = createHostMirrorCopy(ConvVertDiff); + ABORT_ERROR("TestVertMixConv: VertDiff FAIL, " + "expected {}, got {} with {} mismatches", + VertDiffConvExp, ConvVertDiffH(1, 1), NumMismatches); + } + + return; +} + +void testShearVertMix() { + /// Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; + I4 NEdgesAll = Mesh->NEdgesAll; + OMEGA_SCOPE(ZMid, VCoord->ZMid); + OMEGA_SCOPE(NEdgesOnCell, Mesh->NEdgesOnCell); + OMEGA_SCOPE(AreaCell, Mesh->AreaCell); + OMEGA_SCOPE(DcEdge, Mesh->DcEdge); + OMEGA_SCOPE(DvEdge, Mesh->DvEdge); + + /// Get VertMix instance to test + VertMix *TestVertMix = VertMix::getInstance(); + + /// Create and fill ocean state arrays + auto NormalVelEdge = Array2DReal("NormalVelEdge", NEdgesAll, NVertLayers); + auto TangVelEdge = Array2DReal("TangVelEdge", NEdgesAll, NVertLayers); + auto BruntVaisalaFreqSqCell = + Array2DReal("BruntVaisalaFreqSqCell", NCellsAll, NVertLayers); + + /// Use Kokkos::deep_copy to fill the entire view with the ref value + deepCopy(NormalVelEdge, NV); + deepCopy(TangVelEdge, TV); + deepCopy(BruntVaisalaFreqSqCell, BVFN); + deepCopy(TestVertMix->VertDiff, 0.0); + deepCopy(TestVertMix->VertVisc, 0.0); + + parallelFor( + "populateArrays", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + ZMid(ICell, K) = -K; + NEdgesOnCell(ICell) = 5; + AreaCell(ICell) = 3.6e10_Real; + }); + + parallelFor( + "populateArrays", {NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + NormalVelEdge(IEdge, K) = NormalVelEdge(IEdge, K) + 0.5 * K; + TangVelEdge(IEdge, K) = TangVelEdge(IEdge, K) + 0.5 * K; + DcEdge(IEdge) = 2.0e5_Real; + DvEdge(IEdge) = 1.45e5_Real; + }); + + /// Compute only shear vertical viscosity and diffusivity + TestVertMix->BackDiff = 0.0; + TestVertMix->BackVisc = 0.0; + TestVertMix->ComputeVertMixConv.Enabled = false; + TestVertMix->ComputeVertMixShear.Enabled = true; + TestVertMix->computeVertMix(NormalVelEdge, TangVelEdge, + BruntVaisalaFreqSqCell); + + const auto &MinLayerCell = VCoord->MinLayerCell; + const auto &MaxLayerCell = VCoord->MaxLayerCell; + + Array2DReal ShearVertVisc = TestVertMix->VertVisc; + Array2DReal ShearVertDiff = TestVertMix->VertDiff; + + /// Check total Visc against linear addition of components + int NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-ShearVisc", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + if (ShearVertVisc(ICell, K) != 0.0_Real) + InnerCount++; + // K = 1 should have ref value + } else if (K == 1) { + if (!isApprox(ShearVertVisc(ICell, K), VertViscShearExp, + RTol)) + InnerCount++; + // otherwise check for invalid values + } else { + if (ShearVertVisc(ICell, K) == 0.0 or + Kokkos::isnan(ShearVertVisc(ICell, K)) or + Kokkos::isinf(ShearVertVisc(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto ShearVertViscH = createHostMirrorCopy(ShearVertVisc); + ABORT_ERROR("TestVertMixShear: VertVisc FAIL, " + "expected {}, got {} with {} mismatches", + VertViscShearExp, ShearVertViscH(1, 1), NumMismatches); + } + + /// Check total Diff against linear addition of components + NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-ShearVisc", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + if (ShearVertDiff(ICell, K) != 0.0_Real) + InnerCount++; + // K = 1 should have ref value + } else if (K == 1) { + if (!isApprox(ShearVertDiff(ICell, K), VertDiffShearExp, + RTol)) + InnerCount++; + // otherwise check for invalid values + } else { + if (ShearVertDiff(ICell, K) == 0.0 or + Kokkos::isnan(ShearVertDiff(ICell, K)) or + Kokkos::isinf(ShearVertDiff(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto ShearVertDiffH = createHostMirrorCopy(ShearVertDiff); + ABORT_ERROR("TestVertMixShear: VertDiff FAIL, " + "expected {}, got {} with {} mismatches", + VertDiffShearExp, ShearVertDiffH(1, 1), NumMismatches); + } + + return; +} + +/// Test vertical mixing coefficients calculation for all cells/layers +void testTotalVertMix() { + /// Get mesh and coordinate info + const auto Mesh = HorzMesh::getDefault(); + const auto VCoord = VertCoord::getDefault(); + VCoord->NVertLayers = NVertLayers; + I4 NCellsAll = Mesh->NCellsAll; + I4 NEdgesAll = Mesh->NEdgesAll; + OMEGA_SCOPE(ZMid, VCoord->ZMid); + OMEGA_SCOPE(NEdgesOnCell, Mesh->NEdgesOnCell); + OMEGA_SCOPE(AreaCell, Mesh->AreaCell); + OMEGA_SCOPE(DcEdge, Mesh->DcEdge); + OMEGA_SCOPE(DvEdge, Mesh->DvEdge); + + /// Get VertMix instance to test + VertMix *TestVertMix = VertMix::getInstance(); + + /// Create and fill ocean state arrays + auto NormalVelEdge = Array2DReal("NormalVelEdge", NEdgesAll, NVertLayers); + auto TangVelEdge = Array2DReal("TangVelEdge", NEdgesAll, NVertLayers); + auto BruntVaisalaFreqSqCell = + Array2DReal("BruntVaisalaFreqSqCell", NCellsAll, NVertLayers); + + /// Use deep copy to initialize with the ref value + deepCopy(NormalVelEdge, NV); + deepCopy(TangVelEdge, TV); + + // Test with positive BVF first + deepCopy(BruntVaisalaFreqSqCell, BVFP); + deepCopy(TestVertMix->VertDiff, 0.0); + deepCopy(TestVertMix->VertVisc, 0.0); + + parallelFor( + "populateArrays", {NCellsAll, NVertLayers}, + KOKKOS_LAMBDA(I4 ICell, I4 K) { + ZMid(ICell, K) = -K; + NEdgesOnCell(ICell) = 5; + AreaCell(ICell) = 3.6e10_Real; + }); + + parallelFor( + "populateArrays", {NEdgesAll, NVertLayers}, + KOKKOS_LAMBDA(I4 IEdge, I4 K) { + NormalVelEdge(IEdge, K) = NormalVelEdge(IEdge, K) + 0.5 * K; + TangVelEdge(IEdge, K) = TangVelEdge(IEdge, K) + 0.5 * K; + DcEdge(IEdge) = 2.0e5_Real; + DvEdge(IEdge) = 1.45e5_Real; + }); + + /// Compute vertical viscosity and diffusivity + TestVertMix->BackDiff = 1.0e-5; + TestVertMix->BackVisc = 1.0e-4; + TestVertMix->ComputeVertMixConv.Enabled = true; + TestVertMix->ComputeVertMixShear.Enabled = true; + TestVertMix->computeVertMix(NormalVelEdge, TangVelEdge, + BruntVaisalaFreqSqCell); + + const auto &MinLayerCell = VCoord->MinLayerCell; + const auto &MaxLayerCell = VCoord->MaxLayerCell; + + OMEGA_SCOPE(VertDiffP, TestVertMix->VertDiff); + OMEGA_SCOPE(VertViscP, TestVertMix->VertVisc); + + /// Check all VertDiff array values against expected value + int NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-TotalPosDiff", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + if (VertDiffP(ICell, K) != 0.0_Real) + InnerCount++; + // K = 1 should have ref value + } else if (K == 1) { + if (!isApprox(VertDiffP(ICell, K), VertDiffExpValueP, RTol)) + InnerCount++; + // otherwise check for invalid values + } else { + if (VertDiffP(ICell, K) == 0.0 or + Kokkos::isnan(VertDiffP(ICell, K)) or + Kokkos::isinf(VertDiffP(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto VertDiffPH = createHostMirrorCopy(VertDiffP); + ABORT_ERROR("TestVertMixTotal: VertDiffPositive FAIL, " + "expected {}, got {} with {} mismatches", + VertDiffExpValueP, VertDiffPH(1, 1), NumMismatches); + } + + /// Check all VertVisc array values against expected value + NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-TotalPosVisc", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + if (VertViscP(ICell, K) != 0.0_Real) + InnerCount++; + // K = 1 should have ref value + } else if (K == 1) { + if (!isApprox(VertViscP(ICell, K), VertViscExpValueP, RTol)) + InnerCount++; + // otherwise check for invalid values + } else { + if (VertViscP(ICell, K) == 0.0 or + Kokkos::isnan(VertViscP(ICell, K)) or + Kokkos::isinf(VertViscP(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto VertViscPH = createHostMirrorCopy(VertViscP); + ABORT_ERROR("TestVertMixTotal: VertViscPositive FAIL, " + "expected {}, got {} with {} mismatches", + VertViscExpValueP, VertViscPH(1, 1), NumMismatches); + } + + // Now test with negative BVF + deepCopy(BruntVaisalaFreqSqCell, BVFN); + deepCopy(TestVertMix->VertDiff, 0.0); + deepCopy(TestVertMix->VertVisc, 0.0); + + /// Compute vertical viscosity and diffusivity + TestVertMix->computeVertMix(NormalVelEdge, TangVelEdge, + BruntVaisalaFreqSqCell); + OMEGA_SCOPE(VertDiffN, TestVertMix->VertDiff); + OMEGA_SCOPE(VertViscN, TestVertMix->VertVisc); + + /// Check all VertDiff array values against expected value + NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-TotalNegDiff", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + if (VertDiffN(ICell, K) != 0.0_Real) + InnerCount++; + // K = 1 should have ref value + } else if (K == 1) { + if (!isApprox(VertDiffN(ICell, K), VertDiffExpValueN, RTol)) + InnerCount++; + // otherwise check for invalid values + } else { + if (VertDiffN(ICell, K) == 0.0 or + Kokkos::isnan(VertDiffN(ICell, K)) or + Kokkos::isinf(VertDiffN(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto VertDiffNH = createHostMirrorCopy(VertDiffN); + ABORT_ERROR("TestVertMix: VertDiffNegative FAIL, " + "expected {}, got {} with {} mismatches", + VertDiffExpValueN, VertDiffNH(1, 1), NumMismatches); + } + + /// Check all VertVisc array values against expected value + NumMismatches = 0; + parallelReduceOuter( + "CheckVertMixMatrix-TotalNegVisc", {Mesh->NCellsAll}, + KOKKOS_LAMBDA(int ICell, const TeamMember &Team, int &OuterCount) { + int NumMismatchesCol; + const int KMin = MinLayerCell(ICell); + const int KMax = MaxLayerCell(ICell); + const int KRange = vertRange(KMin, KMax); + parallelReduceInner( + Team, KRange, + INNER_LAMBDA(int KOff, int &InnerCount) { + const int K = KMin + KOff; + if (K == 0) { + if (VertViscN(ICell, K) != 0.0_Real) + InnerCount++; + // K = 1 should have ref value + } else if (K == 1) { + if (!isApprox(VertViscN(ICell, K), VertViscExpValueN, RTol)) + InnerCount++; + // otherwise check for invalid values + } else { + if (VertViscN(ICell, K) == 0.0 or + Kokkos::isnan(VertViscN(ICell, K)) or + Kokkos::isinf(VertViscN(ICell, K))) + InnerCount++; + } + }, + NumMismatchesCol); + + Kokkos::single(PerTeam(Team), + [&]() { OuterCount += NumMismatchesCol; }); + }, + NumMismatches); + + if (NumMismatches != 0) { + auto VertViscNH = createHostMirrorCopy(VertViscN); + ABORT_ERROR("TestVertMix: VertViscNegative FAIL, " + "expected {}, got {} with {} mismatches", + VertViscExpValueN, VertViscNH(1, 1), NumMismatches); + } + + return; +} + +/// Finalize and clean up all test infrastructure +void finalizeVertMixTest() { + VertMix::destroyInstance(); + HorzMesh::clear(); + Halo::clear(); + VertCoord::clear(); + Decomp::clear(); + Field::clear(); + Dimension::clear(); + MachEnv::removeAll(); +} + +// the main tests (all in one to have the same log): +// --> one tests the vertical diffusivity and viscosity +// with only background on +// --> next tests the vertical diffusivity and viscosity +// with only convective on +// --> next tests the vertical diffusivity and viscosity +// with only shear on +// --> next tests the linear superposition of the +// background, convective, and shear contributions +void vertMixTest() { + + // initialize vertical mix and other infrastructure + initVertMixTest(); + + // test each vertical mix option + testBackVertMix(); + testConvVertMix(); + testShearVertMix(); + testTotalVertMix(); + + // clean up + finalizeVertMixTest(); +} + +// The test driver for VertMix testing +int main(int argc, char *argv[]) { + + MPI_Init(&argc, &argv); + Kokkos::initialize(argc, argv); + Pacer::initialize(MPI_COMM_WORLD); + Pacer::setPrefix("Omega:"); + + vertMixTest(); + + LOG_INFO("------ Vertical Mixing Unit Tests Successful ------"); + Kokkos::finalize(); + MPI_Finalize(); + + // if we made it here, it is successful + return 0; + +} // end of main +//===-----------------------------------------------------------------------===/