diff --git a/components/omega/configs/Default.yml b/components/omega/configs/Default.yml
index 93b4fe7f7bf3..d71f8538d4b8 100644
--- a/components/omega/configs/Default.yml
+++ b/components/omega/configs/Default.yml
@@ -23,13 +23,19 @@ Omega:
State:
NTimeLevels: 2
Advection:
+ Coef3rdOrder: 0.25
FluxThicknessType: Center
+ HorzTracerFluxOrder: 2
FluxTracerType: Center
+ VerticalTracerFluxLimiterEnable: true
+ VerticalTracerFluxOrder: 3
WindStress:
InterpType: Isotropic
VertCoord:
Density0: 1026.0
MovementWeightType: Uniform
+ PressureGrad:
+ PressureGradType: Centered
Tendencies:
ThicknessFluxTendencyEnable: true
PVTendencyEnable: true
@@ -50,6 +56,10 @@ Omega:
EddyDiff4: 0.0
UseCustomTendency: false
ManufacturedSolutionTendency: false
+ ThicknessVertAdvTendencyEnable: true
+ VelocityVertAdvTendencyEnable: true
+ TracerVertAdvTendencyEnable: true
+ PressureGradTendencyEnable: true
Tracers:
Base: [Temperature, Salinity]
Debug: [Debug1, Debug2, Debug3]
diff --git a/components/omega/doc/design/OmegaV1GoverningEqns.md b/components/omega/doc/design/OmegaV1GoverningEqns.md
index 2a972016e58d..2262a437fe6c 100644
--- a/components/omega/doc/design/OmegaV1GoverningEqns.md
+++ b/components/omega/doc/design/OmegaV1GoverningEqns.md
@@ -34,7 +34,7 @@ This document describes the governing equations for the layered ocean model, whi
The requirements in the [Omega-0 design document](OmegaV0ShallowWater) still apply. Additional design requirements for Omega-1 are:
### Omega will be a hydrostatic, non-Boussinesq ocean model.
-Omega will adopt a non-Boussinesq formulation, meaning it retains the full, spatially and temporally varying fluid density $\rho({\bf x}, t)$ in all governing equations. This ensures exact mass conservation, as opposed to volume conservation in Boussinesq models that approximate density as a constant reference value $\rho_0$ outside the pressure gradient. The equations are derived in terms of layer-integrated mass per unit area (see [layered equations](#Layer Equations)), and no approximation is made that filters out compressibility or density variations.
+Omega will adopt a non-Boussinesq formulation, meaning it retains the full, spatially and temporally varying fluid density $\rho({\bf x}, t)$ in all governing equations. This ensures exact mass conservation, as opposed to volume conservation in Boussinesq models that approximate density as a constant reference value $\rho_0$ outside the pressure gradient. The equations are derived in terms of layer-integrated mass per unit area (see [layered equations](#layer-equations)), and no approximation is made that filters out compressibility or density variations.
The model also assumes hydrostatic balance in the vertical momentum equation, which is a standard and well-justified simplification for large-scale geophysical flows. In such regimes, vertical accelerations are typically small compared to the vertical pressure gradient and gravitational forces. This assumption simplifies the dynamics and removes most sound waves while retaining fidelity for the mesoscale to planetary-scale ocean circulation that Omega is designed to simulate.
@@ -89,7 +89,7 @@ If the control surface moves with the fluid in a Lagrangian fashion, then ${\bf
The momentum equation is an expression of Newton's second law and includes two types of external forces.
The first is the body force, represented here as ${\bf b}({\bf x}, t)$, which encompasses any volumetric force acting throughout the fluid, such as gravitational acceleration or the Coriolis force. In some contexts, body forces may be expressible as the gradient of a potential, ${\bf b} = -\nabla_{3D} \Phi$, but this is not assumed in general.
-The second is the surface force ${\bf \tau}$, which acts on the boundary of the control volume and includes pressure and viscous stresses. These forces appear as surface integrals over the boundary and drive momentum exchange between adjacent fluid parcels or between the fluid and its environment.
+The second is the surface force or traction vector ${\bf \tau} = \sigma \cdot {\bf n}$, which acts on the boundary of the control volume and includes pressure and viscous stresses. These forces appear as surface integrals over the boundary and drive momentum exchange between adjacent fluid parcels or between the fluid and its environment.
The derivation of the momentum equation may also be found in [Leishman 2025](https://eaglepubs.erau.edu/introductiontoaerospaceflightvehicles/chapter/conservation-of-momentum-momentum-equation/#chapter-260-section-2), Chapter 21, equation 10.
## 4. Hydrostatic Approximation
@@ -155,6 +155,7 @@ In this formulation:
[Griffies 2018](https://doi.org/10.2307/j.ctv301gzg) p. 37 argues for the use of pseudo-velocities, which he calls the density-weighted velocity, for non-Boussinesq models.
Griffies recommends a value of $\rho_0=1035$ kg/m$^3$, following p. 47 of [Gill (1982)](https://doi.org/10.1016/S0074-6142(08)60028-5), because ocean density varies less than 2% from that value.
+However, for compatibility with the rest of E3SM, we have decided to use $\rho_0=1026$, the value of [SHR_CONST_RHOSW](https://github.com/E3SM-Project/E3SM/blob/935c7d24b1d7542337fab6f1e58443f18354f62c/share/util/shr_const_mod.F90#L47).
The use of a constant $\rho_0$ in defining pseudo-height does not imply the Boussinesq approximation. In Boussinesq models, $\rho$ is set to $\rho_0$ everywhere except in the buoyancy term (i.e., the vertical pressure gradient or gravitational forcing). Here, by contrast, we retain the full $\rho$ in all terms, and use $\rho_0$ only as a normalization constant—for example, so that $d\tilde{z} \approx dz$ when $\rho \approx \rho_0$. This preserves full mass conservation while making vertical units more intuitive.
@@ -207,7 +208,7 @@ To facilitate integration over a fixed horizontal domain, we introduce the follo
- $A$ is the horizontal footprint (in the $x$–$y$ plane) of the control volume $V(t)$.
- $dA$ is the horizontal area element.
- $\partial A$ is the boundary of $A$, and $dl$ is the line element along this boundary.
-- ${\bf n}_\perp$ is the outward-pointing unit normal vector in the horizontal plane, defined on $\partial A$. It lies in the $x$–$y$ plane and is orthogonal to $dl$.
+- $\mathbf{n}_\perp$ is the outward-pointing unit normal vector in the horizontal plane, defined on $\partial A$. It lies in the $x$–$y$ plane and is orthogonal to $dl$.
We now project the tracer equation [](#tr-vh-split-pseudo) onto a horizontal domain $A$ (the footprint of the control volume), over which the top and bottom boundaries vary in height. The side walls remain fixed in time and space, but not vertical extent.
@@ -250,7 +251,7 @@ $\tilde{W}_{tr}$ will be introduced and defined in [](#notational-simplification
$$
\frac{d}{dt} \int_A \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \, \varphi \, d\tilde{z} \, dA
-& + \int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \varphi \, {\bf u} \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl \\
+& + \int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \varphi \, {\bf u} \, d\tilde{z} \right) \cdot \mathbf{n}_\perp \, dl \\
& + \int_A \rho_0 \left\{\varphi \left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{top}}} dA \\
& - \int_A \rho_0 \left\{\varphi \left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{bot}}} dA
= 0
@@ -266,9 +267,9 @@ Similar expressions to [](#tr-vh-separation-pseudo) hold for mass and momentum:
$$
\frac{d}{dt} \int_A \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \, d\tilde{z} \, dA
-& + \int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \, {\bf u} \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl \\
-& + \int_A \rho_0 \left\{\left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{top}}} dA \\
-& - \int_A \rho_0 \left\{\left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{bot}}} dA
+& + \int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \, {\bf u} \, d\tilde{z} \right) \cdot \mathbf{n}_\perp \, dl \\
+& + \int_A \left\{\left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{top}}} dA \\
+& - \int_A \left\{\left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{bot}}} dA
= 0
$$ (vh-mass-pseudo)
@@ -276,9 +277,9 @@ $$ (vh-mass-pseudo)
$$
\frac{d}{dt} \int_A \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \varphi \, d\tilde{z} \, dA
-& + \int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \varphi \, {\bf u} \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl & \\
-& + \int_A \rho_0 \left\{\varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}_{\tilde{z} = \tilde{z}^{\text{top}}} \, dA & \\
-& - \int_A \rho_0 \left\{\varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}_{\tilde{z} = \tilde{z}^{\text{bot}}}\, dA
+& + \int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \varphi \, {\bf u} \, d\tilde{z} \right) \cdot \mathbf{n}_\perp \, dl & \\
+& + \int_A \left\{\varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}_{\tilde{z} = \tilde{z}^{\text{top}}} \, dA & \\
+& - \int_A \left\{\varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}_{\tilde{z} = \tilde{z}^{\text{bot}}}\, dA
& = 0
$$ (vh-tracer-pseudo)
@@ -287,21 +288,23 @@ $$ (vh-tracer-pseudo)
$$
\frac{d}{dt} \int_{A} \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \, {\bf u} \, d\tilde{z} \, dA
&+
-\int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \, {\bf u} \otimes {\bf u} \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl \\
+\int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \, {\bf u} \otimes {\bf u} \, d\tilde{z} \right) \cdot \mathbf{n}_\perp \, dl \\
&+
-\int_A \rho_0 \, \left\{{\bf u} \left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{top}}} \, dA
+\int_A \, \left\{{\bf u} \left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{top}}} \, dA
-
-\int_A \rho_0 \, \left\{{\bf u} \left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{bot}}} \, dA \\
+\int_A \, \left\{{\bf u} \left[ \tilde{w}_{tr} - \tilde{ u} \right]\right\}_{\tilde{z} = \tilde{z}^{\text{bot}}} \, dA \\
&=
\int_A \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} {\bf b}_{\perp} \, d\tilde{z} \, dA
+
-\int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \frac{{\bf \tau}}{\rho} \, d\tilde{z} \right) {\bf n}_\perp dl \\
+\int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}^{\text{top}}} \frac{{\bf \tau}_\perp}{\rho} \, d\tilde{z} \right) \mathbf{n}_\perp dl \\
&\quad
-+ \int_A \left[ \frac{{\bf \tau}}{\rho} \right]_{\tilde{z} = \tilde{z}^{\text{top}}} \, dA
-- \int_A \left[ \frac{{\bf \tau}}{\rho} \right]_{\tilde{z} = \tilde{z}^{\text{bot}}} \, dA
++ \int_A \left[ \frac{{\bf \tau}_\perp}{\rho} \right]_{\tilde{z} = \tilde{z}^{\text{top}}} \, dA
+- \int_A \left[ \frac{{\bf \tau}_\perp}{\rho} \right]_{\tilde{z} = \tilde{z}^{\text{bot}}} \, dA
$$ (vh-momentum-pseudo)
-In [](#vh-momentum-pseudo), $\mathbf{b}_\perp$ is defined similarly to $\mathbf{n}_\perp$, it is the body force normal to $dl$. These equations express horizontal and vertical fluxes naturally in terms of pseudo-height, enabling fully consistent discretization in the vertical coordinate used for prognostic evolution.
+Here, we have divided by $\rho_0$, since it occurs in every term.
+
+In [](#vh-momentum-pseudo), $\mathbf{b}_\perp$ is defined similarly to $\mathbf{n}_\perp$, it is the body force normal to $dl$, and ${\bf \tau}_\perp$ is the horizontal component of the traction vector. These equations express horizontal and vertical fluxes naturally in terms of pseudo-height, enabling fully consistent discretization in the vertical coordinate used for prognostic evolution.
## 7. Vertical Discretization for the Layered Equations
@@ -353,71 +356,91 @@ This relation is frequently used in discretized fluxes and conservation equation
### Decomposition
-In this document, two decompositions will be critical. The first decomposition is
+In this document, two decompositions will be critical.
+
+The first decomposition is
$$
\varphi(x,y,z,t) = \overline{\varphi}^{\tilde{z}}_k(x,y,t) + \delta \varphi(x,y,z,t)
$$ (vertical-decomposition)
-which is a density weighted vertical integral based upon [](#def-layer-average) and the deviation from this value within the layer.
+which is a density-weighted vertical layer average based upon [](#def-layer-average) and the deviation from this value within the layer.
-The second decomposition is
+The second decomposition separates each field into a resolved and unresolved component,
$$
-\varphi = \left<\varphi\right> + \varphi^\prime
-$$ (reynolds-definition)
+\varphi = \left<\varphi\right> + \varphi^\prime,
+$$ (resolved-unresolved-definition)
+
+where:
+
+- $\left<\varphi\right>$ denotes the **resolved (layer-mean / grid-resolved) component**, obtained by projection onto the resolved space,
+- $\varphi^\prime$ denotes the **unresolved component**, defined such that
+ $$
+ \left<\varphi^\prime\right> = 0.
+ $$
-which is the traditional Reynolds' average and deviation from this value.
+This decomposition is introduced to identify subgrid-scale (SGS) fluxes that require parameterization. It should be interpreted as a resolved/unresolved (coarse-grained) split consistent with the model’s prognostic layer-averaged variables, rather than strictly as a time or ensemble Reynolds average.
-The fundamental ocean model equations are most often Reynolds' averaged to derive the sub gridscale stresses. Each variable is decomposed into an average over a sufficiently large sample of turbulent processes, which allows for a meaningful fluctuating component [[](#reynolds-definition)]. Commonly, the Reynolds' average is thought of as a time average, but an ensemble average is equally valid. In fact, the ensemble and time averaging can be thought of as equivalent, given that a sufficiently long time average will effectively average over variations in the flow that could be thought of as an ensemble. Given the functions are continuous, the averaging operator can be passed through derivatives and integrals without corrections and an average of products with a single perturbation quantity is zero.
+The projection operator is linear and commutes with spatial derivatives and integrals in the same manner assumed for Reynolds averaging. Products satisfy
-The Reynolds' average is most commonly denoted by an overbar. However, to disambiguate from the definition of the bar as the vertical density weighted average, the Reynolds' average herein is denoted by $< . >$.
+$$
+\left< \varphi \psi \right>
+=
+\left< \varphi \right>\left< \psi \right>
++
+\left< \varphi^\prime \psi^\prime \right>,
+$$
-The Reynolds' approach is an attractive approach for Boussinesq ocean models since the fundamental equations do not include products of spatially variable density and tracer, pressure, or momentum. When the ocean model is non Boussinesq, products of spatially varying density and other fields (e.g., tracer) arise and create difficulties, producing a term like $\left<\rho^\prime {\mathbf u}^\prime\right>$ which is difficult to parameterize. However, given the definition of our chosen pseudo-height, the density terms are wrapped up in $\tilde{z}$ and once again a Reynolds' approach can be cleanly used.
+which serves to expose SGS flux terms.
### Averaging
-The quantity being averaged in [](#def-layer-average) is arbitrary. For example, this equation can apply equally to a Reynolds' averaged quantity, e.g.,
+The quantity being averaged in [](#def-layer-average) is arbitrary. For example, the layer average may be applied to resolved quantities,
$$
\overline{\left<\varphi\right>}^{\tilde{z}}_k(x,y,t) =
\frac{1}{\tilde{h}_k} \int_{\tilde{z}_{k}^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left<\varphi\right> d\tilde{z}.
$$ (def-layer-average-reynolds)
-Additionally, for the decomposition related to vertical averaging, we will encounter terms such as ${\overline \varphi}^{\tilde{z}}_k \delta\varphi$ that need to be vertical averaged. Using [](#def-layer-average) we have
+Additionally, for the decomposition related to vertical averaging, we will encounter terms such as ${\overline \varphi}^{\tilde{z}}_k \delta\varphi$ that need to be vertically averaged. Using [](#def-layer-average) we have
$$
-\overline{{\overline \varphi}^{\tilde{z}}_k \delta\varphi}^{\tilde{z}}_k &= \frac{\int_{{\tilde z}_{k}^{\text{bot}}}^{{\tilde z}_k^{\text{top}}} {\overline \varphi}^{\tilde{z}}_k \delta \varphi d{\tilde z}}
- {\int_{{\tilde z}_{k}^{\text{bot}}}^{{\tilde z}_k^{\text{top}}} d{\tilde z}} \\
- &= {\overline \varphi}^{\tilde{z}}_k \frac{\int_{{\tilde z}_{k}^{\text{bot}}}^{{\tilde z}_k^{\text{top}}} \delta \varphi d{\tilde z}}
- {\int_{{\tilde z}_{k}^{\text{bot}}}^{{\tilde z}_k^{\text{top}}} d{\tilde z}} \\
- &= 0,
+\overline{{\overline \varphi}^{\tilde{z}}_k \delta\varphi}^{\tilde{z}}_k
+=
+{\overline \varphi}^{\tilde{z}}_k
+\frac{\int_{{\tilde z}_{k}^{\text{bot}}}^{{\tilde z}_k^{\text{top}}} \delta \varphi d{\tilde z}}
+ {\int_{{\tilde z}_{k}^{\text{bot}}}^{{\tilde z}_k^{\text{top}}} d{\tilde z}}
+=
+0,
$$ (delta-vert-average)
where the last equality is true by definition.
+(layer-equations)=
## 9. Layer Equations
### Tracer & Mass
-We first Reynolds' average [](#vh-tracer-pseudo) and given the definition of the operator we can move the averaging past the derivatives and integrals without correction terms to yield
+We first apply the resolved/unresolved decomposition to [](#vh-tracer-pseudo).
+Because the projection operator is linear and commutes with spatial derivatives and integrals, we may move it through the spatial integrals without correction terms to yield
$$
-\frac{d}{dt} \int_A \int_{\tilde{z}_{k+1}^{\text{top}}}^{\tilde{z}_k^{\text{top}}} \left<\varphi\right> \, d\tilde{z} \, dA
-& + \int_{\partial A} \left( \int_{\tilde{z}_{k+1}^{\text{bot}}}^{\tilde{z}_k^{\text{bot}}} \left<\varphi \, {\bf u} \right> \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl & \\
-& + \int_A \left<\left\{ \rho_0 \varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}\right>_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
-& - \int_A \left<\left\{ \rho_0 \varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}\right>_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
+\frac{d}{dt} \int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left<\varphi\right> \, d\tilde{z} \, dA
+& + \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left<\varphi \, {\bf u} \right> \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl & \\
+& + \int_A \left<\left\{ \varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}\right>_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
+& - \int_A \left<\left\{ \varphi \left[\tilde{w}_{tr} - \tilde{ u} \right] \right\}\right>_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
& = 0.
$$ (Aintegral-tracer-first-reynolds)
-Next, we use a Reynolds' decomposition on any terms involving products inside a Reynolds' average,
+Next, we use the resolved/unresolved decomposition on any terms involving products inside a resolved component projection,
$$
\frac{d}{dt} \int_A \int_{\tilde{z}_{k}^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left<\varphi \right> \, d\tilde{z} \, dA
& +
-\int_{\partial A} \left( \int_{\tilde{z}_{k}^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left(\left<{\varphi}\right>\left<{\bf u}\right> + \left<\varphi^{\prime}{\bf u}^{\prime}\right> \right) d\tilde{z} \right) \, \cdot {\bf n}_\perp \, dl & \\
-& + \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
-& - \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
+\int_{\partial A} \left( \int_{\tilde{z}_{k}^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left(\left<{\varphi}\right>\left<{\bf u}\right> + \left<\varphi^{\prime}{\bf u}^{\prime}\right> \right) d\tilde{z} \right) \, \cdot \mathbf{n}_\perp \, dl & \\
+& + \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
+& - \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
& = 0.
$$ (Aintegral-tracer-reynolds)
@@ -425,9 +448,9 @@ We can rewrite the first two terms in [](#Aintegral-tracer-reynolds) using [](#d
$$
\frac{d}{dt} \int_A \tilde{h}_k \, \overline{\left<\varphi\right>}^{\tilde{z}}_k \, dA
-& + \int_{\partial A} \left( \tilde{h}_k \, \left[\overline{\left<\varphi\right> \left<{\bf u}\right> + \left<\varphi^{\prime}{\bf u}^{\prime}\right>}^{\tilde{z}}_k \right] \right) \cdot {\bf n}_\perp \, dl & \\
-& + \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
-& - \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
+& + \int_{\partial A} \left( \tilde{h}_k \, \left[\overline{\left<\varphi\right> \left<{\bf u}\right> + \left<\varphi^{\prime}{\bf u}^{\prime}\right>}^{\tilde{z}}_k \right] \right) \cdot \mathbf{n}_\perp \, dl & \\
+& + \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
+& - \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
& = 0.
$$ (Aintegral-tracer)
@@ -435,9 +458,9 @@ The second term is expanded utilizing [](#vertical-decomposition)
$$
\frac{d}{dt} \int_A \tilde{h}_k \, \overline{\left<\varphi\right>}^{\tilde{z}}_k \, dA
-& + \int_{\partial A} \left( \tilde{h}_k \, \overline{\left(\overline{\left<\varphi\right>}^{\tilde{z}}_k + \delta \varphi\right)\left(\overline{\left<{\bf u}\right>}^{\tilde{z}}_k + \delta {\bf u}\right)}^{\tilde{z}}_k + \tilde{h}_k \overline{\left<\varphi^{\prime}{\bf u}^{\prime}\right>}^{\tilde{z}}_k \right) \cdot {\bf n}_\perp \, dl & \\
-& + \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
-& - \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
+& + \int_{\partial A} \left( \tilde{h}_k \, \overline{\left(\overline{\left<\varphi\right>}^{\tilde{z}}_k + \delta \varphi\right)\left(\overline{\left<{\bf u}\right>}^{\tilde{z}}_k + \delta {\bf u}\right)}^{\tilde{z}}_k + \tilde{h}_k \overline{\left<\varphi^{\prime}{\bf u}^{\prime}\right>}^{\tilde{z}}_k \right) \cdot \mathbf{n}_\perp \, dl & \\
+& + \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
+& - \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
& = 0.
$$ (Aintegral-tracer2)
@@ -445,9 +468,9 @@ And then is simplified using [](#delta-vert-average)
$$
\frac{d}{dt} \int_A \tilde{h}_k \, \overline{\left<\varphi\right>}^{\tilde{z}}_k \, dA
-& + \int_{\partial A} \left[ \tilde{h}_k \, \left(\overline{\left<\varphi\right>}^{\tilde{z}}_k \overline{\left<{\bf u}\right>}^{\tilde{z}}_k + \overline{\delta \varphi \delta {\bf u}}^{\tilde{z}}_k + \overline{\left<\varphi^{\prime}{\bf u}^{\prime}\right>}^{\tilde{z}}_k \right)\right] \cdot {\bf n}_\perp \, dl & \\
-& + \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
-& - \int_A \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
+& + \int_{\partial A} \left[ \tilde{h}_k \, \left(\overline{\left<\varphi\right>}^{\tilde{z}}_k \overline{\left<{\bf u}\right>}^{\tilde{z}}_k + \overline{\delta \varphi \delta {\bf u}}^{\tilde{z}}_k + \overline{\left<\varphi^{\prime}{\bf u}^{\prime}\right>}^{\tilde{z}}_k \right)\right] \cdot \mathbf{n}_\perp \, dl & \\
+& + \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> -\left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA & \\
+& - \int_A \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\, dA
& = 0.
$$
@@ -456,8 +479,8 @@ Given that the horizontal area is not changing in time, we can move the time der
$$
\int_A \bigl\{ \frac{\partial \tilde{h}_k \, \overline{\left<\varphi\right>}^{\tilde{z}}_k}{\partial t}
& + \nabla \cdot \left[ \tilde{h}_k \, \left(\overline{\left<\varphi\right>}^{\tilde{z}}_k \overline{\left<{\bf u}\right>}^{\tilde{z}}_k + \overline{\delta \varphi \delta {\bf u}}^{\tilde{z}}_k + \overline{\left<\varphi^{\prime}{\bf u}^{\prime}\right>}^{\tilde{z}}_k \right)\right] & \\
-& + \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} & \\
-& - \left\{ \rho_0 \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \bigr\} \, dA
+& + \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{top}}} & \\
+& - \left\{ \left[ \left<\varphi\right>\left<\tilde{w}_{tr}\right> + \left<\varphi^{\prime} \tilde{w}_{tr}^\prime \right> - \left<\varphi\right> \left<\tilde{ u}\right> - \left<\varphi^{\prime} \tilde{ u}^{\prime}\right> \right] \right\}_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \bigr\} \, dA
& = 0.
$$ (reynolds-tracer-final)
@@ -470,12 +493,12 @@ Finally, given that the area integral operates on the entire equation, it is val
$$
\frac{\partial {\tilde h}_k \overline{\left<\varphi\right>}^{\tilde{z}}_k }{\partial t} & + \nabla \cdot \left({\tilde h}_k \overline{\left<\varphi\right>}^{\tilde{z}}_k \overline{\left<{\bf u}\right>}^{\tilde{z}}_k\right) \\
-& + \rho_0 \left\{ \left[ \left<\varphi\right> \left<{\tilde w}_{tr}\right> - \left<\varphi\right>\left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_k^{\text{top}}} - \left[ \left<\varphi\right> \left<{\tilde w}_{tr}\right> - \left<\varphi\right>\left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_k^{\text{bot}}} \right\} \\
+& + \left\{ \left[ \left<\varphi\right> \left<{\tilde w}_{tr}\right> - \left<\varphi\right>\left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_k^{\text{top}}} - \left[ \left<\varphi\right> \left<{\tilde w}_{tr}\right> - \left<\varphi\right>\left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_k^{\text{bot}}} \right\} \\
& = - \nabla \cdot \left({\tilde h}_k \overline{\left<\varphi^{\prime} {\bf u}^{\prime}\right>}^{\tilde{z}}_k \, + {\tilde h}_k \overline{\delta \varphi \delta {\bf u}}^{\tilde{z}}_k \right) \\
-& - \rho_0 \left\{ \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{top}}} - \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}\right\}.
+& - \left\{ \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{top}}} - \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}\right\}.
$$ (layer-tracer)
-A few notes on the layer-averaged tracer equation. In this complete form, it includes three types of fluctuating quantities that must be dealt with: (1) the layer averaged, Reynolds' averaged horizontal turbulent flux $\left( \overline{\left<\varphi^{\prime} {\bf u}^{\prime}\right>}^{\tilde{z}}_k \right)$, (2) the Reynolds' average vertical turbulent flux $\left( \left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{u}^{\prime}\right> \right)$, and (3) the layer-averaged product of deviations from the layer-integrated variables $\left(\overline{\delta \varphi \delta {\bf u}}^{\tilde{z}}_k \right)$. The details of the first two quantities will be discussed later in this document and follow-on design documents. The terms involving perturbations from the layer integrated quantity are necessary to extend beyond piecewise constant represenation of variables. In this equation, variables with no overline are the full field variable at the interfaces.
+A few notes on the layer-averaged tracer equation. In this complete form, it includes three types of fluctuating quantities that must be dealt with: (1) the layer averaged horizontal turbulent flux $\left( \overline{\left<\varphi^{\prime} {\bf u}^{\prime}\right>}^{\tilde{z}}_k \right)$, (2) the vertical turbulent flux $\left( \left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{u}^{\prime}\right> \right)$, and (3) the layer-averaged product of deviations from the layer-integrated variables $\left(\overline{\delta \varphi \delta {\bf u}}^{\tilde{z}}_k \right)$. The details of the first two quantities will be discussed later in this document and follow-on design documents. The terms involving perturbations from the layer integrated quantity are necessary to extend beyond piecewise constant represenation of variables. In this equation, variables with no overline are the full field variable at the interfaces.
The mass equation is identical to the tracer equation with $\varphi=1$.
@@ -484,98 +507,140 @@ The mass equation is identical to the tracer equation with $\varphi=1$.
$$
\frac{\partial {\tilde h}_k }{\partial t}
+ \nabla \cdot \left({\tilde h}_k \overline{\left<{\bf u}\right>}^{\tilde{z}}_k\right)
-+ \rho_0 \left[ \left<{\tilde w}_{tr}\right> - \left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_k^{\text{top}}}
-- \rho_0 \left[ \left<{\tilde w}_{tr}\right> - \left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}
++ \left[ \left<{\tilde w}_{tr}\right> - \left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_k^{\text{top}}}
+- \left[ \left<{\tilde w}_{tr}\right> - \left<\tilde{ u}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}
= 0.
$$ (layer-mass)
+### Pressure Gradient Force
+
+Before deriving the layered momentum equation, we collect the pressure and gravitational contributions into a single **pressure gradient force (PGF)** in our pseudo-height framework. This subsection establishes the continuous form of the horizontal PGF per unit mass and serves as the target for the subsequent finite-volume, layer-averaged derivation.
+
+Recall from [](#vh-momentum-pseudo) that the horizontal momentum equation is written in terms of
+
+- a **body force per unit mass** $\mathbf{b}_\perp$, and
+- a **traction** vector $\boldsymbol{\tau}$ that enters the weak form as $\boldsymbol{\tau}/\rho$,
+
+so that all terms in the equation have the units of acceleration.
+
+In the finite-volume formulation, pressure acts on the boundary of a control volume $\mathcal{R}$ through the traction
+$\boldsymbol{\tau}_p = -p\,\mathbf{n}$, where $\mathbf{n}$ is the outward unit normal. The net pressure force on $\mathcal{R}$ is therefore
+
+$$
+\mathbf{F}_p(\mathcal{R})
+=
+\int_{\partial\mathcal{R}} \boldsymbol{\tau}_p\,dS
+=
+-\int_{\partial\mathcal{R}} p\,\mathbf{n}\,dS .
+$$ (pgf-pressure-traction)
+
+Using the divergence theorem, this surface integral may be written equivalently as a volume integral of a force per unit volume,
+
+$$
+-\int_{\partial\mathcal{R}} p\,\mathbf{n}\,dS
+=
+-\int_{\mathcal{R}} \nabla p \, dV .
+$$ (pgf-pressure-volume)
+
+Interpreting $-\nabla p$ as a force per unit volume, the corresponding pressure acceleration (body force per unit mass) is
+
+$$
+\mathbf{b}_{p}
+=
+-\frac{1}{\rho}\,\nabla p .
+$$ (pgf-pressure-accel)
+
+Note that one should **not** apply the divergence theorem directly to $\boldsymbol{\tau}_p/\rho$; the divergence theorem applies to the physical traction $\boldsymbol{\tau}_p$ (force per unit area). The division by $\rho$ to obtain an acceleration is performed after forming the pressure force.
+
+Gravity acts as a body force per unit mass $-\nabla\Phi$, so the combined pressure–gravity acceleration is
+
+$$
+\mathbf{b}_{\text{PGF},\perp}
+=
+-\,\frac{1}{\rho}\,\nabla p \;-\; \nabla \Phi .
+$$ (pgf-continuous)
+
+In Omega, the operator $\nabla$ denotes the **horizontal gradient along layers**, following the convention adopted throughout this section. It should not be interpreted as a gradient taken at constant geometric height $z$ or constant pseudo-height $\tilde{z}$.
+
+Under the hydrostatic approximation [](#hydrostatic), the form of the horizontal pressure–gravity force in [](#pgf-continuous) is valid for *any* choice of vertical coordinate, provided the horizontal gradient is taken consistently with that coordinate. In particular, although the horizontal gradient of $\Phi$ vanishes in $z$-level coordinates in the absence of tides, self-attraction, and loading, the combined expression in [](#pgf-continuous) remains the correct horizontal pressure–gravity force when written using the appropriate along-layer gradient operator.
+
+When inserted into the weak, finite-volume form of the momentum equation, $\mathbf{b}_{\text{PGF},\perp}$ represents the combined effect of pressure traction and gravitational body force. In the next subsection, we will layer-average [](#pgf-continuous) over the pseudo-height thickness of each layer and combine it with the advective and Coriolis terms to obtain the layered horizontal momentum equation. The resulting horizontal pressure-gradient force in Omega must be a consistent finite-volume discretization of [](#pgf-continuous), together with additional metric terms associated with sloping layer interfaces.
+
### Momentum
We now derive the horizontal momentum equation in our non-Boussinesq, hydrostatic framework, following the same finite-volume approach used for mass and tracer conservation.
-We begin from [](#vh-momentum-pseudo) and specify the body forces ${\bf b}$ and surface forces ${\bf \tau}$
+As established in the preceding **Pressure Gradient Force** subsection, the combined horizontal pressure–gravity acceleration is
+$-\,\rho^{-1}\nabla p - \nabla \Phi$ when written using the appropriate along-layer horizontal gradient operator $\nabla$; in the finite-volume derivation below we retain the same physics by treating gravity as a body force and pressure as a surface traction.
+
+We specify the body forces, including the pressure term, as:
$$
-\frac{d}{dt} \int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} {\bf u} \, d\tilde{z} \, dA
-& + \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} {\bf u} \otimes {\bf u} \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl \\
-& + \int_A \rho_0 \, {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA
-- \int_A \rho_0 \, {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
-& = -\int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \rho_0 \left({\bf f} \times \mathbf{u} + \nabla \Phi \right) \, d\tilde{z} \, dA
-- \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \frac{\rho_0 p}{\rho} \, d\tilde{z} \right) {\bf n}_\perp dl \\
-& - \int_A \left[ \frac{\rho_0 p}{\rho} \nabla \tilde{z}_k^{\text{top}} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA
-+ \int_A \left[ \frac{\rho_0 p}{\rho} \nabla \tilde{z}_k^{\text{bot}} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA.
-$$ (vh-momentum-forces)
+\mathbf{b}_{\perp} = - \left({\bf f} \times \mathbf{u} + \frac{1}{\rho} \nabla p + \nabla \Phi \right).
+$$ (body-forces)
-In this equation:
+The first term on the right hand side is the **Coriolis force**, where $ \mathbf{f} $ is the vector Coriolis vector (e.g., $ f \hat{\mathbf{z}} $ on the sphere).
-- The first term on the right hand side is the **Coriolis force**, where $ \mathbf{f} $ is the vector Coriolis vector (e.g., $ f \hat{\mathbf{z}} $ on the sphere).
-- The second term on the right hand side represents the **gravitational force**, expressed in terms of the two-dimensional gradient of the gravitational potential $ \Phi(x, y, z, t) $, which may include effects such as tides and self-attraction and loading. This gradient is along layer (e.g. constant $\tilde{z}$) such that the gradient of $\Phi$ doesn't disappear.
-- The final terms are the **pressure force**, which acts on the boundary surfaces and is naturally expressed as a surface integral. It gives rise to both horizontal pressure gradients and contributions from sloping surfaces.
-- A negative sign is included for the pressure terms derived from the surface force terms. According to [Leishman 2025](https://eaglepubs.erau.edu/introductiontoaerospaceflightvehicles/chapter/conservation-of-momentum-momentum-equation/#chapter-260-section-2), Chapter 21, equation 2, "the negative sign indicates that the force is inward and in the opposite direction to the unit normal vector area, which always points *outward* by convention". Also see discussion in [Kundu et al. 2016](https://doi.org/10.1016/C2012-0-00611-4) page 104 and equation 4.26.
+The second and third terms are $\mathbf{b}_{\text{PGF},\perp}$ from the previous subsection.
-As with the tracer derivation, we next Reynolds' average [](#vh-momentum-forces),
+Substituting [](#body-forces) and the traction terms in [](#vh-momentum-pseudo), we arrive at:
+
+$$
+\frac{d}{dt} \int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} {\bf u} \, d\tilde{z} \, dA
+& + \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} {\bf u} \otimes {\bf u} \, d\tilde{z} \right) \cdot \mathbf{n}_\perp \, dl \\
+& + \int_A \, {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA
+- \int_A \, {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
+& = -\int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left({\bf f} \times \mathbf{u} + \frac{1}{\rho} \nabla p + \nabla \Phi \right) \, d\tilde{z} \, dA.
+$$ (vh-momentum-forces)
+
+As with the tracer derivation, we next apply [](#resolved-unresolved-definition) to [](#vh-momentum-forces) to identify subgrid-scale (SGS) fluxes,
$$
\frac{d}{dt} \int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left< {\bf u} \right> \, d\tilde{z} \, dA
-& + \int_{\partial A} \left( \int_{\tilde{z}^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left< {\bf u} \otimes {\bf u} \right> \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl \\
-& + \int_A \rho_0 \,\left< {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \right> \, dA
-- \int_A \rho_0 \, \left< {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right> \, dA \\
-& = -\int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \rho_0 \, \left<\left( {\bf f} \times \mathbf{u} + \nabla \Phi \right) \right> \, d\tilde{z} \, dA
-- \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \rho_0 \left< \frac{p}{\rho} \right> \, d\tilde{z} \right) {\bf n}_\perp dl \\
-& - \int_A \rho_0 \left[ \left< \frac{p}{\rho} \nabla \tilde{z}_k^{\text{top}} \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA
-+ \int_A \rho_0 \left[ \left< \frac{p}{\rho} \nabla \tilde{z}_k^{\text{bot}} \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA.
+& + \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left< {\bf u} \otimes {\bf u} \right> \, d\tilde{z} \right) \cdot \mathbf{n}_\perp \, dl \\
+& + \int_A \,\left< {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \right> \, dA
+- \int_A \, \left< {\bf u} \left[ \tilde{w}_{tr} - \tilde{\bf u} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right> \, dA \\
+& = -\int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \, \left< {\bf f} \times \mathbf{u} + \frac{1}{\rho} \nabla p + \nabla \Phi \right> \, d\tilde{z} \, dA.
$$ (vh-momentum-reynolds1)
-Here we have also moved the Reynolds' average through the spatial integrals given the properties of the averaging. Next we do a Reynolds' decomposition, this yields
+Here we have also moved the resolved component projection operator through the spatial integrals given the properties of the operator. Next we decompose the nonlinear products into resolved and unresolved components, this yields
$$
\frac{d}{dt} \int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left< {\bf u} \right> \, d\tilde{z} \, dA
-& + \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}^{\text{top}}} \left( \left< {\bf u} \right> \otimes \left< {\bf u} \right> + \left< {\bf u}^\prime \otimes {\bf u}^\prime \right> \right) \, d\tilde{z} \right) \cdot {\bf n}_\perp \, dl \\
-& + \int_A \rho_0 \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
-& - \int_A \rho_0 \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
-& = - \int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \rho_0 \, \left( {\bf f} \times \left<\mathbf{u}\right> + \nabla \left<\Phi\right> \right) \, d\tilde{z} \, dA \\
-& - \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \rho_0 \left(\left< \alpha \right> \left
+ \left<\alpha^\prime p^\prime\right> \right) \, d\tilde{z} \right) {\bf n}_\perp dl \\
-& - \int_A \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{top}}\right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
-& + \int_A \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{bot}}\right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA.
+& + \int_{\partial A} \left( \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \left( \left< {\bf u} \right> \otimes \left< {\bf u} \right> + \left< {\bf u}^\prime \otimes {\bf u}^\prime \right> \right) \, d\tilde{z} \right) \cdot \mathbf{n}_\perp dl \, dl \\
+& + \int_A \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
+& - \int_A \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
+& = - \int_A \int_{\tilde{z}_k^{\text{bot}}}^{\tilde{z}_k^{\text{top}}} \, \left( {\bf f} \times \left<\mathbf{u}\right> + \left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right> \right) \, d\tilde{z} \, dA.
$$ (vh-momentum-reynolds2)
-In [](#vh-momentum-reynolds2) we have also used $\alpha = \frac{1}{\rho}$ for notation conciseness. The definition of the layer average [](#def-layer-average-reynolds) is now utilized on terms with vertical integrals to yield
+In [](#vh-momentum-reynolds2), we have also used $\alpha = \frac{1}{\rho}$ for notation conciseness. The definition of the layer average [](#def-layer-average-reynolds) is now utilized on terms with vertical integrals to yield
$$
\frac{d}{dt} \int_A \tilde{h}_k \overline{\left< {\bf u} \right>}^{\tilde{z}}_k \, dA
& + \int_{\partial A} \tilde{h}_k \left( \overline{\left< {\bf u} \right> \otimes \left< {\bf u} \right>}^{\tilde{z}}_k + \overline{\left< {\bf u}^\prime \otimes {\bf u}^\prime \right>}^{\tilde{z}}_k \right) \cdot {\bf n}_\perp \, dl \\
-& + \int_A \rho_0 \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
-& - \int_A \rho_0 \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
-& = - \int_A \rho_0 \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \, dA \\
-& - \int_{\partial A} \rho_0 \tilde{h}_k \left( \overline{\left< \alpha \right> \left
}^{\tilde{z}}_k + \overline{\left<\alpha^\prime p^\prime\right>}^{\tilde{z}}_k \right) {\bf n}_\perp dl \\
-& - \int_A \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{top}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
-& + \int_A \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{bot}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA.
+& + \int_A \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
+& - \int_A \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
+& = - \int_A \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \, dA.
$$ (vh-momentum-reynolds-lay-avg)
-The next step is to decompose vertical averages of products. This yields
+The next step is to decompose vertical averages of products. However we retain the PGF terms as a product because approximating them as products of vertical averages can lead to large inaccuracies. The result is
$$
\frac{d}{dt} \int_{A} \tilde{h}_k \overline{\left< {\bf u} \right>}^{\tilde{z}}_k \, dA
& + \int_{\partial A} \tilde{h}_k \left( \overline{\left< {\bf u} \right>}^{\tilde{z}}_k \otimes \overline{\left< {\bf u} \right>}^{\tilde{z}}_k + \overline{\delta {\bf u} \otimes \delta {\bf u}}^{\tilde{z}}_k + \overline{\left< {\bf u}^\prime \otimes {\bf u}^\prime \right>}^{\tilde{z}}_k \right) \cdot {\bf n}_\perp \, dl \\
-& + \int_A \rho_0 \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
-& - \int_A \rho_0 \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
-& = - \int_A \rho_0 \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \, dA \\
-& - \int_{\partial A} \rho_0 \tilde{h}_k \left( \overline{\left< \alpha \right>}^{\tilde{z}}_k \overline{\left
}^{\tilde{z}}_k + \overline{\delta \alpha \delta p}^{\tilde{z}}_k+ \overline{\left<\alpha^\prime p^\prime\right>}^{\tilde{z}}_k \right) {\bf n}_\perp dl \\
-& - \int_A \rho_0 \left[\left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{top}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
-& + \int_A \rho_0 \left[\left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{bot}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA.
+& + \int_A \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \, dA \\
+& - \int_A \, \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \, dA \\
+& = - \int_A \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \, dA.
$$ (vh-momentum-reynolds-lay-avg2)
We next use the Divergence theorem on the surface integrals and combine terms into fewer integrals on each side of the equation.
$$
-\int_A \bigl\{ \frac{\partial\tilde{h}_k \overline{\left< {\bf u} \right>}^{\tilde{z}}_k}{\partial t}
+\int_A \Bigl\{ \frac{\partial\tilde{h}_k \overline{\left< {\bf u} \right>}^{\tilde{z}}_k}{\partial t}
& + \nabla \cdot \left[ \tilde{h}_k \left(\overline{\left< {\bf u} \right>}^{\tilde{z}}_k \otimes \overline{\left< {\bf u} \right>}^{\tilde{z}}_k + \overline{\delta {\bf u} \otimes \delta {\bf u}}^{\tilde{z}}_k + \overline{\left< {\bf u}^\prime \otimes {\bf u}^\prime \right>}^{\tilde{z}}_k \right) \right] \\
-& + \rho_0 \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}^{\text{top}}} \\
-& - \rho_0 \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}^{\text{bot}}} \bigr\} \, dA \\
-& = - \int_A \bigl\{ \rho_0 \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \\
-& - \nabla \left[ \rho_0 \tilde{h}_k \left( \overline{\left< \alpha \right>}^{\tilde{z}}_k \overline{\left
}^{\tilde{z}}_k + \overline{\delta \alpha \delta p}^{\tilde{z}}_k+ \overline{\left<\alpha^\prime p^\prime\right>}^{\tilde{z}}_k \right) \right] \\
-& - \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{top}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \\
-& + \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{bot}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}}\bigr\} \, dA.
+& + \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \\
+& - \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \Bigr\} \, dA \\
+& = - \int_A \left\{ \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \right\} \, dA.
$$ (vh-momentum-reynolds-lay-avg3)
Since the equation is fully inside the integral, the equation is true for any area and therefore we can write the layer averaged momentum equation as
@@ -583,12 +648,9 @@ Since the equation is fully inside the integral, the equation is true for any ar
$$
\frac{\partial\tilde{h}_k \overline{\left< {\bf u} \right>}^{\tilde{z}}_k}{\partial t}
& + \nabla \cdot \left[ \tilde{h}_k \left( \overline{\left< {\bf u} \right>}^{\tilde{z}}_k \otimes \overline{\left< {\bf u} \right>}^{\tilde{z}}_k + \overline{\delta {\bf u} \otimes \delta {\bf u}}^{\tilde{z}}_k + \overline{\left< {\bf u}^\prime \otimes {\bf u}^\prime \right>}^{\tilde{z}}_k \right) \right] \\
-& + \rho_0 \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \\
-& - \rho_0 \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \\
-& = - \rho_0 \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \\
-& - \nabla \left[ \rho_0 \tilde{h}_k \left( \overline{\left< \alpha \right>}^{\tilde{z}}_k \overline{\left
}^{\tilde{z}}_k + \overline{\delta \alpha \delta p}^{\tilde{z}}_k+ \overline{\left<\alpha^\prime p^\prime\right>}^{\tilde{z}}_k \right) \right] \\
-& - \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \cdot \nabla \tilde{z}_k^{\text{top}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \\
-& + \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \cdot \nabla \tilde{z}_k^{\text{bot}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}}.
+& + \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \\
+& - \left[ \left(\left< {\bf u} \right> \left< \tilde{w}_{tr} \right> + \left<{\bf u}^\prime \tilde{w}_{tr}^\prime \right> \right) - \left(\left<{\bf u}\right> \left<\tilde{\bf u}\right> + \left< {\bf u}^\prime \tilde{\bf u}^\prime \right> \right) \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \\
+& = - \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k.
$$ (vh-momentum-v1)
The product rule is used on the first two terms of [](#vh-momentum-v1) and then we multiply [](#layer-mass) by $\overline{\mathbf{u}}^{\tilde{z}}_k$ and subtract it from [](#vh-momentum-v1). This yields
@@ -596,12 +658,9 @@ The product rule is used on the first two terms of [](#vh-momentum-v1) and then
$$
\tilde{h}_k \frac{\partial \overline{\left< {\bf u} \right>}^{\tilde{z}}_k}{\partial t}
& + \tilde{h}_k \overline{\left< {\bf u} \right>}^{\tilde{z}}_k \cdot \nabla \overline{\left< {\bf u} \right>}^{\tilde{z}}_k + \nabla \cdot \left[ \tilde{h}_k \left(\overline{\delta {\bf u} \otimes \delta {\bf u}}^{\tilde{z}}_k + \overline{\left< {\bf u}^\prime \otimes {\bf u}^\prime \right>}^{\tilde{z}}_k \right) \right] \\
-& + \rho_0 \left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{ \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& + \rho_0 \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& = - \rho_0 \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \\
-& - \nabla \left[ \rho_0 \tilde{h}_k \left( \overline{\left< \alpha \right>}^{\tilde{z}}_k \overline{\left
}^{\tilde{z}}_k + \overline{\delta \alpha \delta p}^{\tilde{z}}_k+ \overline{\left<\alpha^\prime p^\prime\right>}^{\tilde{z}}_k \right) \right] \\
-& - \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{top}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \\
-& + \rho_0 \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{bot}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}}.
+& + \left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{ \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& + \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& = - \, \tilde{h}_k \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k.
$$ (vh-momentum-v2)
The previous equation is divided by $\tilde{h}_k$,
@@ -609,12 +668,9 @@ The previous equation is divided by $\tilde{h}_k$,
$$
\frac{\partial \overline{\left< {\bf u} \right>}^{\tilde{z}}_k}{\partial t}
& + \overline{\left< {\bf u} \right>}^{\tilde{z}}_k \cdot \nabla \overline{\left< {\bf u} \right>}^{\tilde{z}}_k + \frac{1}{\tilde{h}_k} \nabla \cdot \left[ \tilde{h}_k \left(\overline{\delta {\bf u} \otimes \delta {\bf u}}^{\tilde{z}}_k + \overline{\left< {\bf u}^\prime \otimes {\bf u}^\prime \right>}^{\tilde{z}}_k \right) \right] \\
-& + \frac{\rho_0}{\tilde{h}_k} \left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{ \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& + \frac{\rho_0}{\tilde{h}_k} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& = - \rho_0 \, \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k \\
-& - \frac{1}{\tilde{h}_k} \nabla \left[ \rho_0 \tilde{h}_k \left( \overline{\left< \alpha \right>}^{\tilde{z}}_k \overline{\left
}^{\tilde{z}}_k + \overline{\delta \alpha \delta p}^{\tilde{z}}_k+ \overline{\left<\alpha^\prime p^\prime\right>}^{\tilde{z}}_k \right) \right] \\
-& - \frac{\rho_0}{\tilde{h}_k} \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{top}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} \\
-& + \frac{\rho_0}{\tilde{h}_k} \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{bot}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}}.
+& + \frac{1}{\tilde{h}_k} \left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{ \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& + \frac{1}{\tilde{h}_k} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& = - \, \overline{\left( {\bf f} \times \left<\mathbf{u}\right> + \left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right> \right)}^{\tilde{z}}_k.
$$ (vh-momentum-v3)
@@ -645,13 +701,13 @@ Defining the absolute vorticity $\zeta_a$ as $\zeta + f$, where $f$ is the Corio
$$
\frac{\partial \overline{\left< {\bf u} \right>}^{\tilde{z}}_k}{\partial t}
& + \zeta_a {\overline{\left<{\bf u}\right>}^{\tilde{z}}_k}^{\perp} + \nabla K + \frac{1}{\tilde{h}_k} \nabla \cdot \left[ \tilde{h}_k \left(\overline{\delta {\bf u} \otimes \delta {\bf u}}^{\tilde{z}}_k + \overline{\left< {\bf u}^\prime \otimes {\bf u}^\prime \right>}^{\tilde{z}}_k \right) \right] \\
-& + \frac{\rho_0}{\tilde{h}_k} \left\{ \left[\left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{\left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{\left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& + \frac{\rho_0}{\tilde{h}_k} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& = - \rho_0 \, \overline{\nabla \left<\Phi\right>}^{\tilde{z}}_k - \frac{1}{\tilde{h}_k} \nabla \left[ \rho_0 \tilde{h}_k \left( \overline{\left< \alpha \right>}^{\tilde{z}}_k \overline{\left
}^{\tilde{z}}_k + \overline{\delta \alpha \delta p}^{\tilde{z}}_k+ \overline{\left<\alpha^\prime p^\prime\right>}^{\tilde{z}}_k \right) \right] \\
-& - \frac{\rho_0}{\tilde{h}_k} \left\{ \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{top}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}}
-- \left[ \left< \alpha \right> \left
+ \left<\alpha^\prime \left(p \nabla \tilde{z}_k^{\text{bot}} \right)^\prime\right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\}.
+& + \frac{1}{\tilde{h}_k} \left\{ \left[\left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{\left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left(\left<\mathbf{u}\right> - \overline{\left<\mathbf{u}\right>}^{\tilde{z}}_k\right) \left\{\left<\tilde{w}_{tr}\right> - \left<\tilde{\bf u}\right> \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& + \frac{1}{\tilde{h}_k} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{\bf u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& = - \, \overline{\left< \alpha \right> \nabla \left< p \right> + \left< \alpha' \nabla p' \right> + \nabla \left<\Phi\right>}^{\tilde{z}}_k.
$$ (layer-momentum-final)
+The term $\left< \alpha' \nabla p' \right>$ arises from decomposing the nonlinear product $\alpha \nabla p$ into resolved and unresolved components. In the present formulation, we do not introduce a separate parameterization for this term. It is assumed to be small relative to the resolved pressure–gravity acceleration under weak density variability and small dynamic pressure fluctuations. The pressure gradient force is instead computed directly from the hydrostatic pressure and equation of state, consistent with the finite-volume discretization.
+
The $\delta X$ terms in the layered equations are vertical deviations from the vertical layer average of a given quantity $X$. We will assume these deviations are small and products of these deviations are even smaller. Thus we will ignore most of these terms in Omega. The exception is the pressure gradient force. At this time we will assume piecewise constant approximations of the vertically continuous system, which is appropriate for the simple pressure gradient force targeted for early versions of Omega. This assumption will be revisited at a later date. We also note that these terms could potentially serve as a bridge to multiscale fluxes, as resolution is increased, these $\delta X$ terms would get larger, but likely only for significantly higher resolution (e.g. 10s of meters). However, these terms would have to be further analyzed and developed as these terms are only deviations from layer averages, not temporal averages as in the Reynolds' approach.
(notational-simplifications)=
@@ -659,7 +715,7 @@ The $\delta X$ terms in the layered equations are vertical deviations from the v
Throughout the rest of this document, we will
-1. Drop the $< >$ notation around single variables and note that all variables are assumed to be Reynolds' averaged. Turbulent correlations will retain the angle bracket notation.
+1. Drop the $< >$ notation around single variables and interpret all prognostic variables as resolved (layer-mean) quantities. Covariance terms of the form $\left< X' Y' \right>$ denote unresolved (subgrid-scale) fluxes that must be parameterized.
2. Change the vertical density weighted average notation from $\overline{\varphi}^{\tilde{z}}_k$ to the more simple $\varphi_k$. Thus, any subscript $k$ implies a layer average.
3. Define a total vertical velocity across the pseudo height surface as $\tilde{W}_{tr} \equiv \tilde{w}_{tr} - \tilde{u}$. As a reminder $\tilde{u}$ is the projection of the normal velocity onto the normal vector to the pseudo height surface, in many cases this can be a very small correction to $\tilde{w}_{tr}$.
@@ -668,9 +724,9 @@ With these three simplifications, the final continuous equations become
**Tracer:**
$$
-\frac{\partial {\tilde h}_k \varphi_k}{\partial t} & + \nabla \cdot \left({\tilde h}_k \varphi_k {\bf u}_k\right) + \rho_0 \left\{ \left[ \varphi \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_k^{\text{top}}} - \left[ \varphi \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}} \right\} \\
+\frac{\partial {\tilde h}_k \varphi_k}{\partial t} & + \nabla \cdot \left({\tilde h}_k \varphi_k {\bf u}_k\right) + \left\{ \left[ \varphi \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_k^{\text{top}}} - \left[ \varphi \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}} \right\} \\
& = - \nabla \cdot \left({\tilde h}_k \left<\varphi^{\prime} {\bf u}^{\prime}\right>_k\right) \\
-& - \rho_0 \left\{ \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{top}}} - \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}\right\}.
+& - \left\{ \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{top}}} - \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}\right\}.
$$ (layer-tracer-final-simple)
**Mass:**
@@ -678,8 +734,8 @@ $$ (layer-tracer-final-simple)
$$
\frac{\partial {\tilde h}_k }{\partial t}
+ \nabla \cdot \left({\tilde h}_k {\bf u}_k\right)
-+ \rho_0 \left[ \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_{k}^{\text{top}}}
-- \rho_0 \left[ \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}
++ \left[ \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_{k}^{\text{top}}}
+- \left[ \tilde{W}_{tr} \right]_{{\tilde z}={\tilde z}_{k}^{\text{bot}}}
= 0.
$$ (layer-mass-final-simple)
@@ -688,26 +744,24 @@ $$ (layer-mass-final-simple)
$$
\frac{\partial {\bf u}_k}{\partial t}
& + \zeta_a {{\bf u}_k}^{\perp} + \nabla K + \frac{1}{\tilde{h}_k} \nabla \cdot \left[ \tilde{h}_k \left< {\bf u}^\prime \otimes {\bf u}^\prime \right>_k \right] \\
-& + \frac{\rho_0}{\tilde{h}_k} \left\{ \left[\left(\mathbf{u} - \mathbf{u}_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left(\mathbf{u} - \mathbf{u}_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& + \frac{\rho_0}{\tilde{h}_k} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
-& = - \left(\nabla \Phi \right)_k - \frac{1}{\tilde{h}_k} \nabla \left[ \tilde{h}_k \alpha_k p_k \right] \\
-& - \frac{1}{\tilde{h}_k} \left\{ \left[ \alpha p \nabla \tilde{z}_k^{\text{top}}\right]_{\tilde{z} = \tilde{z}_k^{\text{top}}}
-- \left[ \alpha p \nabla \tilde{z}_k^{\text{bot}}\right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\}.
+& + \frac{1}{\tilde{h}_k} \left\{ \left[\left(\mathbf{u} - \mathbf{u}_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left(\mathbf{u} - \mathbf{u}_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& + \frac{1}{\tilde{h}_k} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{top}}} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{\tilde{z} = \tilde{z}_k^{\text{bot}}} \right\} \\
+& = - \left(\alpha \nabla p + \nabla \Phi \right)_k.
$$ (layer-momentum-final-simple)
In [](#layer-tracer-final-simple) and [](#layer-momentum-final-simple) we have not combined turbulent flux terms (e.g., $\left<\varphi^\prime \tilde{W}_{tr}^\prime \right>$) as the two terms that comprise this term ($\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right>$ and $\left< \varphi^\prime \tilde{ u}^{\prime}\right>$) are fundamentally different processes that must be modeled separately.
## 10. Discrete Equations
-In the following equations, the subscripts $i$, $e$, and $v$ indicate cell, edge, and vertex locations and subscript $k$ is the layer. Square brackets $[\cdot]_e$ and $[\cdot]_v$ are quantities that are interpolated to edge and vertex locations. For vector quantities, $u_{e,k}$ denotes the normal component at the center of the edge (where the normal component is the local ${\bf n}_\perp$ described in above sections), while $u_{e,k}^\perp$ denotes the tangential component. We have switched from $\varphi_{i,k}^{bot}$ to the identical $\varphi_{i,k+1}^{top}$ for all variables in order for the notation to match the array names in the code. It is important to note that any term without a subscript $k$ are quantities evaluated at that location and are ***not*** individual layer averages, but will be reconstructed as a function of neighboring layer average values. For example, Eq (44) of [White and Adcroft (2008)](https://www.sciencedirect.com/science/article/pii/S0021999108002593) shows a third order reconstruction. Finally, we note that in the equations below, it is assumed that $k$ increases away from the surface.
+In the following equations, the subscripts $i$, $e$, and $v$ indicate cell, edge, and vertex locations and subscript $k$ is the layer. Square brackets $[\cdot]_e$ and $[\cdot]_v$ are quantities that are interpolated to edge and vertex locations. For vector quantities, $u_{e,k}$ denotes the normal component at the center of the edge (where the normal component is the local $\mathbf{n}_\perp$ described in above sections), while $u_{e,k}^\perp$ denotes the tangential component. We have switched from $\varphi_{i,k}^{bot}$ to the identical $\varphi_{i,k+1}^{top}$ for all variables in order for the notation to match the array names in the code. It is important to note that any term without a subscript $k$ are quantities evaluated at that location and are ***not*** individual layer averages, but will be reconstructed as a function of neighboring layer average values. For example, Eq (44) of [White and Adcroft (2008)](https://www.sciencedirect.com/science/article/pii/S0021999108002593) shows a third order reconstruction. Finally, we note that in the equations below, it is assumed that $k$ increases away from the surface.
**Mass:**
$$
\frac{\partial {\tilde h}_{i,k} }{\partial t}
+ \nabla \cdot \left(\left[{\tilde h}_k\right]_e {\bf u}_{e,k}\right)
-+ \rho_0 \left[ \tilde{W}_{tr} \right]_k^\text{top}
-- \rho_0 \left[ \tilde{W}_{tr} \right]_{k+1}^\text{top}
++ \left[ \tilde{W}_{tr} \right]_k^\text{top}
+- \left[ \tilde{W}_{tr} \right]_{k+1}^\text{top}
= 0.
$$ (discrete-mass)
@@ -716,8 +770,8 @@ In this equation, mass source term ($Q$), such as sources like river runoff, sea
**Tracer:**
$$
-\frac{\partial {\tilde h}_{i,k} \varphi_{i,k}}{\partial t} & + \nabla \cdot \left(\left[{\tilde h}_{i,k} \varphi_{i,k}\right]_e {\bf u}_{e,k}\right) + \rho_0 \left\{ \left[ \varphi \tilde{W}_{tr} \right]_k^\text{top} - \left[ \varphi \tilde{W}_{tr} \right]_{k+1}^\text{top} \right\} \\
-& = - \nabla \cdot \left({\tilde h}_k \left<\varphi^{\prime} {\bf u}^{\prime}\right>_k \right) - \rho_0 \left\{ \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_k^\text{top} - \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{k+1}^\text{top} \right\}.
+\frac{\partial {\tilde h}_{i,k} \varphi_{i,k}}{\partial t} & + \nabla \cdot \left(\left[{\tilde h}_{i,k} \varphi_{i,k}\right]_e {\bf u}_{e,k}\right) + \left\{ \left[ \varphi \tilde{W}_{tr} \right]_k^\text{top} - \left[ \varphi \tilde{W}_{tr} \right]_{k+1}^\text{top} \right\} \\
+& = - \nabla \cdot \left({\tilde h}_k \left<\varphi^{\prime} {\bf u}^{\prime}\right>_k \right) - \left\{ \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_k^\text{top} - \left[\left< \varphi^\prime {\tilde w}_{tr}^{\prime} \right> - \left< \varphi^\prime \tilde{ u}^{\prime}\right> \right]_{k+1}^\text{top} \right\}.
$$ (discrete-tracer)
In the tracer equation, we note that surface fluxes (e.g. latent heat fluxes) will appear in the surface value of the vertical turbulent tracer flux (e.g., $\left[\left<\varphi^\prime \tilde{w}_{tr}^\prime\right>\right]_{k=0}^\text{top}$). The surface fluxes will also include effects from volumetric surface fluxes that carry a non zero tracer value. The terms on the second line represent the small scale turbulence and the form used in Omega to represent these terms will be discussed in the next section.
@@ -729,9 +783,9 @@ Omega will only predict the layer average normal velocity, so we drop the bold f
$$
\frac{\partial u_{e,k}}{\partial t}
& + \left[ {\bf k} \cdot \nabla \times u_{e,k} +f_v\right]_e\left(u_{e,k}^{\perp}\right) + \left[\nabla K\right]_e \\
-& + \frac{\rho_0}{\left[\tilde{h}_{i,k}\right]_e} \left\{ \left[\left(u - u_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{e,k}^\text{top} - \left[ \left(u - u_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{e,k+1}^\text{top} \right\} \\
-& = - \left(\nabla \Phi \right)_{e,k} - \frac{1}{\left[\tilde{h}_k\right]_e} \nabla \left( \tilde{h}_k \alpha_k p_k \right) - \frac{1}{\left[\tilde{h}_k\right]_e} \left\{ \left[ \alpha p \nabla \tilde{z}^{\text{top}}\right]_{e,k}^\text{top} - \left[ \alpha p \nabla \tilde{z}^{\text{top}}\right]_{e,k+1}^\text{top} \right\} \\
-& - \frac{1}{\left[\tilde{h}_{i,k}\right]_e} \nabla \cdot \left( \tilde{h}_k \left< {\bf u}^\prime \otimes {\bf u}^\prime \right>_k \right) - \frac{\rho_0}{\left[\tilde{h}_{i,k}\right]_e^\text{top}} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{e,k}^\text{top} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{e,k+1}^\text{top} \right\}.
+& + \frac{1}{\left[\tilde{h}_{i,k}\right]_e} \left\{ \left[\left(u - u_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{e,k}^\text{top} - \left[ \left(u - u_k\right) \left\{\tilde{W}_{tr} \right\} \right]_{e,k+1}^\text{top} \right\} \\
+& = - \left(\alpha \nabla p + \nabla \Phi \right)_{e,k} - \frac{1}{\left[\tilde{h}_{i,k}\right]_e} \nabla \cdot \left( \tilde{h}_k \left< {\bf u}^\prime \otimes {\bf u}^\prime \right>_k \right) \\
+& - \frac{1}{\left[\tilde{h}_{i,k}\right]_e^\text{top}} \left\{ \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{e,k}^\text{top} - \left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> - \left< \mathbf{u}^\prime \tilde{ u}^\prime \right> \right]_{e,k+1}^\text{top} \right\}.
$$ (discrete-momentum)
**Diagnostic Relations:**
@@ -748,7 +802,7 @@ $$
z_{i,k}^{top} = z_{i}^{floor} + \rho_0 \sum_{k'=k}^{K_{max}} \alpha_{i,k'} \tilde{h}_{i,k'}
$$ (discrete-z)
-We refer to these as the discrete equations, but time derivatives remain continuous. The time discretization is described in the [time stepping design document](TimeStepping.md). The velocity, mass-thickness, and tracers are solved prognostically using [](discrete-momentum), [](discrete-mass), [](discrete-tracer). At the new time, these variables are used to compute pressure [](discrete-pressure), specific volume [](discrete-eos), and z-locations [](discrete-z). Additional variables are computed diagnostically at the new time: $\mathbf{u}^{\perp}$, $K$, $\zeta_a$, $z^{mid}$, $\Phi$, etc. The initial geopotential is simply $\Phi=gz$, but additional gravitational terms may be added later.
+We refer to these as the discrete equations, but time derivatives remain continuous. The time discretization is described in the [time stepping design document](TimeStepping.md). The velocity, mass-thickness, and tracers are solved prognostically using [](#discrete-momentum), [](#discrete-mass), [](#discrete-tracer). At the new time, these variables are used to compute pressure [](#discrete-pressure), specific volume [](#discrete-eos), and z-locations [](#discrete-z). Additional variables are computed diagnostically at the new time: $\mathbf{u}^{\perp}$, $K$, $\zeta_a$, $z^{mid}$, $\Phi$, etc. The initial geopotential is simply $\Phi=gz$, but additional gravitational terms may be added later.
The horizontal operators $\nabla$, $\nabla\cdot$, and $\nabla \times$ are now in their discrete form. In the TRiSK design, gradients ($\nabla$) map cell centers to edges; divergence ($\nabla \cdot$) maps edge quantities to cells; and curl ($\nabla \times$) maps edges to vertices. The exact form of operators and interpolation stencils remain the same as those given in {ref}`Omega-0 operator formulation <33-operator-formulation>` The discrete version of terms common with Omega-0, such as advection, potential vorticity, and $\nabla K$, can be found in {ref}`Omega-0 Momentum Terms <34-momentum-terms>` and {ref}`Omega-0 Thickness and Tracer Terms <35-thickness-and-tracer-terms>`.
@@ -812,18 +866,30 @@ $$
in this relation, we have moved the subscript $k$ off the variable itself to prevent confusion with the layer average. With this definition, [](#discrete-mom-flux-sloping) goes to zero for flat layer surfaces.
#### Vertical velocity dissipation
-The vertical turbulent momentum stress is most commonly parameterized as a down-gradient process, i.e.,
+The vertical turbulent momentum stress is most commonly parameterized as a down-gradient process, which for a z-coordinate model is given as,
$$
-\left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> \right]_{e,k} = -\frac{\nu_v \rho}{\rho_0} \left[\frac{\partial u}{\partial \tilde{z}}\right]_{e,k}
+\left = -\nu_v \frac{\partial u}{\partial z}
$$
+where $\nu_v$ is the parameterized vertical viscosity.
+
+For our pseudo-height coordinate the momentum stress is,
+
+$$
+\left[ \left<\mathbf{u}^\prime \tilde{w}_{tr}^\prime \right> \right]_{e,k} = -\tilde{\nu_v} \left[\frac{\partial u}{\partial \tilde{z}}\right]_{e,k}
+$$
+
+where $\tilde{\nu}_v \equiv \frac{\nu_v \rho}{\rho_0}$.
+
Plugging this relation into the last part of [](#discrete-momentum)
$$
-\frac{1}{\left[\tilde{h}_{i,k}\right]_{e,k}} \left\{ \left[ \nu_v \left[\frac{\partial u}{\partial \tilde{z}}\right]_{e,k} \right]_{e,k} - \left[ \nu_v \left[\frac{\partial u}{\partial \tilde{z}}\right]_{e,k} \right]_{e,k+1} \right\}
+-\frac{1}{\left[\tilde{h}_k\right]_e} \left\{ \left[\left \right]_{e,k} - \left[\left \right]_{e,k+1} \right\} = \frac{1}{\left[\tilde{h}_{i,k}\right]_{e,k}} \left\{ \left[ \tilde{\nu_v} \right]_{e,k} \left[\frac{\partial u}{\partial \tilde{z}}\right]_{e,k} - \left[ \tilde{\nu_v} \right]_{e,k+1} \left[\frac{\partial u}{\partial \tilde{z}}\right]_{e,k} \right\}
$$ (discrete-mom-vert-diff)
+Here $\left[ \tilde{\nu_v} \right]_{e,k} $ is the vertical viscosity interpolated to a given edge.
+
##### Vertical derivatives in a finite volume framework
Since, Omega will predict layer average quantities (e.g., $u_k$), it's not immediately clear that a traditional discretization is appropriate as there is a discontinuity in the predicted variables at layer interfaces. To circumvent this problem, we turn to weak derivatives instead of traditional pointwise forms. A weak derivative of $\varphi$ is defined as
@@ -867,7 +933,7 @@ where $0.5 \left(\tilde{h}_k + \tilde{h}_{k+1}\right)$ is the distance between $
With this, we can now fully discretize [](#discrete-mom-vert-diff) as
$$
--\frac{1}{\left[\tilde{h}_k\right]_e} \left\{ \left[\left \right]_{e,k} - \left[\left \right]_{e,k+1} \right\} = -\frac{1}{\left[\tilde{h}_k\right]_e} \left\{ \frac{\left(u_{e,k-1} - u_{e,k}\right)}{0.5 \left(\tilde{h}_{k-1} + \tilde{h}_{k}\right)} - \frac{\left(u_{e,k} - u_{e,k+1}\right)}{0.5 \left(\tilde{h}_k + \tilde{h}_{k+1}\right)} \right\}.
+-\frac{1}{\left[\tilde{h}_k\right]_e} \left\{ \left[\left \right]_{e,k} - \left[\left \right]_{e,k+1} \right\} = -\frac{1}{\left[\tilde{h}_k\right]_{e,k}} \left\{ \left[\tilde{\nu_v} \right]_{e,k} \frac{\left(u_{e,k-1} - u_{e,k}\right)}{0.5 \left(\tilde{h}_{k-1} + \tilde{h}_{k}\right)} - \left[\tilde{\nu_v} \right]_{e,k+1} \frac{\left(u_{e,k} - u_{e,k+1}\right)}{0.5 \left(\tilde{h}_k + \tilde{h}_{k+1}\right)} \right\}.
$$ (final-vert-vel-dissipation)
This form can be interfaced with the Omega [tridiagonal solver](TridiagonalSolver.md) routine.
@@ -1031,7 +1097,7 @@ Table 1. Definition of variables. Geometric variables may be found in the {ref}`
|$K_{min}$ | shallowest active layer | |
|$K_{max}$ | deepest active layer | |
|$K_{i,k}$ | kinetic energy | m$^2$ s$^{-2}$ | cell | KineticEnergyCell |$K = \left\| {\bf u} \right\|^2 / 2$ |
-|$p_{i,k}$ | pressure | Pa | cell | Pressure | see [](discrete-pressure) |
+|$p_{i,k}$ | pressure | Pa | cell | Pressure | see [](#discrete-pressure) |
|$p^{floor}_i$ | bottom pressure | Pa | cell | PFloor | pressure at ocean floor
|$p^{surf}_i$ | surface pressure | Pa | cell | PSurface | due to atm. pressure, sea ice, ice shelves
|$q_{v,k}$ | potential vorticity | m$^{-1}$ s$^{-1}$ | vertex | PotentialVorticity |$q = \left(\zeta+f\right)/\tilde{h}$ |
@@ -1047,7 +1113,7 @@ Table 1. Definition of variables. Geometric variables may be found in the {ref}`
|$\tilde{w}_{tr\ i,k}$ | net vertical transport through a moving surface | m s$^{-1}$ | cell | NetVertTransportVelocity | volume transport per m$^2$ |
|$\tilde{W}_{tr\ i,k}$ | total vertical velocity across a pseudo height surface | m s$^{-1}$ | cell | TotalVertTransportVelocity | $\tilde{W}_{tr} \equiv \tilde{w}_{tr} - \tilde{u}$ |
|$\tilde{z}$ | vertical coordinate (pseudo-height) | m | - | | positive upward |
-|$\tilde{z}^{top}_{i,k}$ | layer top $\tilde{z}$-location | m | cell | ZTop | see [](discrete-z) |
+|$\tilde{z}^{top}_{i,k}$ | layer top $\tilde{z}$-location | m | cell | ZTop | see [](#discrete-z) |
|$\tilde{z}^{mid}_{i,k}$ | layer mid-depth $\tilde{z}$-location | m | cell | ZMid |
|$\tilde{z}^{surf}_{i}$ | ocean surface $\tilde{z}$-location | m | cell | ZSurface |
|$\tilde{z}^{floor}_{i}$ | ocean floor $\tilde{z}$-location | m | cell | ZFloor |
diff --git a/components/omega/doc/design/Tendencies.md b/components/omega/doc/design/Tendencies.md
index 9bceff5a0b38..3f392698c12b 100644
--- a/components/omega/doc/design/Tendencies.md
+++ b/components/omega/doc/design/Tendencies.md
@@ -39,14 +39,23 @@ retrieval.
```c++
class Tendencies{
public:
+ // Arrays for accumulating tendencies
Array2DReal LayerThicknessTend;
Array2DReal NormalVelocityTend;
+ Array3DReal TracerTend;
+ // Instances of tendency terms
ThicknessFluxDivOnCell ThicknessFluxDiv;
PotentialVortHAdvOnEdge PotientialVortHAdv;
KEGradOnEdge KEGrad;
SSHGradOnEdge SSHGrad;
VelocityDiffusionOnEdge VelocityDiffusion;
VelocityHyperDiffOnEdge VelocityHyperDiff;
+ WindForcingOnEdge WindForcing;
+ BottomDragOnEdge BottomDrag;
+ TracerDiffOnCell TracerDiffusion;
+ TracerHyperDiffOnCell TracerHyperDiff;
+ TracerHorzAdvOnCell TracerHorzAdv;
+ TracerHighOrderHorzAdvOnCell TracerHighOrderHorzAdv;
private:
static Tendencies *DefaultTendencies;
static std::map> AllTendencies;
@@ -79,29 +88,29 @@ Tendencies* Tendencies::create(const std::string &Name, const HorzMesh *Mesh, in
#### 4.2.2 Initialization
The init method will create the default tendencies and return an error code:
```c++
-int Tendencies::init();
+static void Tendencies::init();
```
#### 4.2.3 Retrieval
There will be methods for getting the default and non-default tendencies instances:
```c++
-Tendencies *Tendencies::getDefault();
-Tendencies *Tendencies::get(const std::string &Name);
+static Tendencies *Tendencies::getDefault();
+static Tendencies *Tendencies::get(const std::string &Name);
```
#### 4.2.4 Computation
The 'computeAll' method will compute and accumulate all layer thickness and normal velocity tendencies using the ocean
state and auxiliary state from a given time level:
```c++
-void Tendencies::computeAllTendencies(const OceanState *State, const AuxilaryState *AuxState, int TimeLevel);
+void Tendencies::computeAllTendencies(const OceanState *State, const AuxilaryState *AuxState, const Array3DReal &TracerArray, int TimeLevel, int VelTimeLevel, TimeInstant Time);
```
The layer thickness tendencies will be computed with a method:
```c++
-void Tendencies::computeThicknessTendencies(const OceanState *State, const AuxilaryState *AuxState, int TimeLevel);
+void Tendencies::computeThicknessTendencies(const OceanState *State, const AuxilaryState *AuxState, int TimeLevel, int VelTimeLevel, TimeInstant Time);
```
The normal velocity tendencies will be computed with a method:
```c++
-void Tendencies::computeVelocityTendencies(const OceanState *State, const AuxilaryState *AuxState, int TimeLevel);
+void Tendencies::computeVelocityTendencies(const OceanState *State, const AuxilaryState *AuxState, int TimeLevel, int VelTimeLevel, TimeInstant Time);
```
@@ -111,8 +120,8 @@ when they are no longer in scope. The erase method
will remove a named tendencies instance, whereas the clear method will remove all of
them. Both will call the destructor in the process.
```c++
-void Tendencies::erase(const std::string &Name);
-void Tendencies::clear();
+static void Tendencies::erase(const std::string &Name);
+static void Tendencies::clear();
```
## 5 Verification and Testing
diff --git a/components/omega/doc/design/Tendency.md b/components/omega/doc/design/Tendency.md
index a4f077638ce9..da797e057cf4 100644
--- a/components/omega/doc/design/Tendency.md
+++ b/components/omega/doc/design/Tendency.md
@@ -39,24 +39,25 @@ The class contains private member variables for any constant data that are defin
Each tendency term will have a `bool` variable which can be set to enable/disable the computation of the tendency.
```c++
-class ThicknessFluxDivergenceOnCell {
+class ThicknessFluxDivOnCell {
public:
bool enabled = false;
- ThicknessFluxDivergenceOnCell(const HorzMesh *Mesh, Config *Options);
+ ThicknessFluxDivOnCell(const HorzMesh *Mesh, Config *Options);
KOKKOS_FUNCTION void operator()(Array2DReal &Tend
int ICell,
int KChunk,
- const Array2DReal &ThicknessFlux);
+ const Array2DReal &ThicknessFlux,
+ const Array2DReal &NormalVelEdge);
private:
Array1DI4 NEdgesOnCell;
Array2DI4 EdgesOnCell;
- Array1DR8 DvEdge;
- Array1DR8 AreaCell;
- Array2DR8 EdgeSignOnCell;
+ Array1DReal DvEdge;
+ Array1DReal AreaCell;
+ Array2DReal EdgeSignOnCell;
};
```
@@ -66,13 +67,10 @@ class ThicknessFluxDivergenceOnCell {
Tendency functor constructors are responsible for initializing the private member variables:
```c++
-ThicknessFluxDivergenceOnCell::ThicknessFluxDivergenceOnCell(HorzMesh const *Mesh, Config *Options)
+ThicknessFluxDivOnCell::ThicknessFluxDivOnCell(HorzMesh const *Mesh)
: NEdgesOnCell(Mesh->NEdgesOnCell), EdgesOnCell(Mesh->EdgesOnCell),
DvEdge(Mesh->DvEdge), AreaCell(Mesh->AreaCell),
- EdgeSignOnCell(Mesh->EdgeSignOnCell) {
-
- enabled = Options->get("ThicknessFluxTendencyEnable")
-}
+ EdgeSignOnCell(Mesh->EdgeSignOnCell) { }
```
#### 4.2.2 operator
@@ -80,17 +78,27 @@ The operator method implements the tendency computation for a chunk of vertical
The inner loop over a chunk of vertical layers enables CPU vectorization.
```c++
-KOKKOS_FUNCTION void ThicknessFluxDivergenceOnCell::operator()(Array2DReal &Tend
- int ICell,
- int KChunk,
- const Array2DReal &ThicknessFlux) const {
- const Real InvAreaCell = 1. / AreaCell(iCell);
- for (int J = 0; J < NEdgesOnCell(ICell); ++J) {
- const int JEdge = EdgesOnCell(ICell, J);
- for (int K = KChunk * VecLength; K < (KChunk + 1) * VecLength; ++K) {
- Tend(ICell, K) -= DvEdge(JEdge) * EdgeSignOnCell(ICell, J) * ThicknessFlux(JEdge, K) * InvAreaCell;
- }
- }
+KOKKOS_FUNCTION void ThicknessFluxDivOnCell::operator()(Array2DReal &Tend
+ int ICell,
+ int KChunk,
+ const Array2DReal &ThicknessFlux,
+ const Array2DReal &NormalVelEdge) const {
+ const I4 KStart = KChunk * VecLength;
+ const Real InvAreaCell = 1._Real / AreaCell(ICell);
+ Real DivTmp[VecLength] = {0};
+ for (int J = 0; J < NEdgesOnCell(ICell); ++J) {
+ const I4 JEdge = EdgesOnCell(ICell, J);
+ for (int KVec = 0; KVec < VecLength; ++KVec) {
+ const I4 K = KStart + KVec;
+ DivTmp[KVec] -= DvEdge(JEdge) * EdgeSignOnCell(ICell, J) *
+ ThicknessFlux(JEdge, K) * NormalVelEdge(JEdge, K) *
+ InvAreaCell;
+ }
+ }
+ for (int KVec = 0; KVec < VecLength; ++KVec) {
+ const I4 K = KStart + KVec;
+ Tend(ICell, K) -= DivTmp[KVec];
+ }
}
```
diff --git a/components/omega/doc/devGuide/OceanState.md b/components/omega/doc/devGuide/OceanState.md
index d817b520e148..1d4463ef55b5 100644
--- a/components/omega/doc/devGuide/OceanState.md
+++ b/components/omega/doc/devGuide/OceanState.md
@@ -40,18 +40,19 @@ For now, member variables that are host arrays have variable names are appended
`H`. Array variable names not ending in `H` are device arrays. For a given time level,
host to device array is performed via:
```c++
-Err = State->copyToDevice(TimeLevel);
+State->copyToDevice(TimeLevel);
```
and a copy from device to host is performed by:
```c++
-Err = State->copyToHost(TimeLevel);
+State->copyToHost(TimeLevel);
```
+These functions validate the `TimeLevel` via `OMEGA_REQUIRE` and will abort if invalid.
Eventually, the host arrays will be eliminated when `IO` and `Halo` are extended to
handle host <-> device transfers.
A time level update to advance the solution at the end of a model timestep is done by:
```c++
-Err = State->updateTimeLevels();
+State->updateTimeLevels();
```
This shifts the time level indices within the `State` instance. A halo exchange is
also performed on these arrays and the IOFields data pointer is attached
@@ -59,19 +60,18 @@ to the current time level.
The arrays associated with a given time level can be accessed with the functions:
```c++
-Array2DReal LayerThick;
-Err = State->getLayerThickness(LayerThick, TimeLevel);
-Array2DReal NormVel;
-Err = State->getNormalVelocity(NormVel, TimeLevel);
+Array2DReal LayerThick = State->getLayerThickness(TimeLevel);
+Array2DReal NormVel = State->getNormalVelocity(TimeLevel);
```
for the device arrays and
```c++
-HostArray2DReal LayerThickH;
-Err = State->getLayerThicknessH(LayerThickH, TimeLevel);
-HostArray2DReal NormVelH;
-Err = State->getNormalVelocityH(NormVelH, TimeLevel);
+HostArray2DReal LayerThickH = State->getLayerThicknessH(TimeLevel);
+HostArray2DReal NormVelH = State->getNormalVelocityH(TimeLevel);
```
-for the host arrays. The time level convention is:
+for the host arrays. These functions return the arrays directly and will abort
+via `OMEGA_REQUIRE` if an invalid `TimeLevel` is provided.
+
+The time level convention is:
| time level | `TimeLevel` |
|------------|-------------|
| New | 1 |
diff --git a/components/omega/doc/devGuide/PGrad.md b/components/omega/doc/devGuide/PGrad.md
new file mode 100644
index 000000000000..cfbe45fcabd8
--- /dev/null
+++ b/components/omega/doc/devGuide/PGrad.md
@@ -0,0 +1,185 @@
+(omega-dev-pgrad)=
+
+# Pressure Gradient (PGrad)
+
+Omega includes a `PressureGrad` class that computes horizontal pressure gradient
+tendencies for the non-Boussinesq momentum equation. The implementation supports a
+centered difference scheme as the default, with a placeholder for future high-order
+methods. The class follows the same factory pattern used by other Omega modules.
+
+## PressureGradType enum
+
+An enumeration of the available pressure gradient schemes is defined in `PGrad.h`:
+
+```c++
+enum class PressureGradType { Centered, HighOrder1, HighOrder2 };
+```
+
+This is used to select which pressure gradient method is applied at runtime.
+
+## Initialization
+
+An instance of `PressureGrad` requires both a [`HorzMesh`](#omega-dev-horz-mesh) and
+a [`VertCoord`](#omega-dev-vert-coord), so these classes and all of their dependencies
+must be initialized before `PressureGrad` can be initialized. The static method:
+
+```c++
+OMEGA::PressureGrad::init();
+```
+
+initializes the default `PressureGrad` instance using the default `HorzMesh` and
+`VertCoord` instances and the global Omega configuration. A pointer to the default
+instance can be retrieved at any time using:
+
+```c++
+OMEGA::PressureGrad* DefPGrad = OMEGA::PressureGrad::getDefault();
+```
+
+## Creating additional instances
+
+Additional named instances can be created using:
+
+```c++
+OMEGA::PressureGrad* MyPGrad =
+ OMEGA::PressureGrad::create("MyPGrad", Mesh, VCoord, Options);
+```
+
+where `Mesh` is a pointer to a `HorzMesh`, `VCoord` is a pointer to a `VertCoord`,
+and `Options` is a pointer to the `Config`. A named instance can be retrieved later
+using:
+
+```c++
+OMEGA::PressureGrad* MyPGrad = OMEGA::PressureGrad::get("MyPGrad");
+```
+
+## Constructor behavior
+
+The constructor reads the `PressureGrad` section from the configuration, stores
+references to mesh and vertical coordinate data needed for computation, and enables
+the appropriate functor based on the configured `PressureGradType`. It also allocates
+placeholder arrays for tidal potential and self-attraction/loading, which are intended
+to be populated by a future tidal forcing module. Currently these arrays are initialized
+to zero.
+
+## Computing the pressure gradient
+
+To compute pressure gradient tendencies and accumulate them into a tendency array:
+
+```c++
+PGrad->computePressureGrad(Tend, State, VCoord, EqState, TimeLevel);
+```
+
+where:
+- `Tend` is a 2D array `(NEdgesAll × NVertLayers)` that the pressure gradient
+ tendency is accumulated into
+- `State` is the current `OceanState`, from which layer thickness is extracted
+ at the given `TimeLevel`
+- `VCoord` provides pressure, interface height, and geopotential fields
+- `EqState` provides the specific volume field
+- `TimeLevel` selects which time level of the state to use
+
+The method uses hierarchical Kokkos parallelism: an outer `parallelForOuter` loop
+iterates over edges, and an inner `parallelForInner` loop iterates over vertical
+chunks. The appropriate functor is dispatched based on `PressureGradChoice`.
+
+## Functors
+
+### PressureGradCentered
+
+This functor implements a centered difference approximation of the pressure gradient
+tendency. For each edge, it first computes the layer-invariant tidal and
+self-attraction/loading contribution:
+
+```
+GradGeoPot = grad(TidalPotential) + grad(SelfAttractionLoading)
+```
+
+Then, for each vertical layer `K`, it computes three terms:
+
+1. **Montgomery potential gradient**: The average of the horizontal gradients of the
+ Montgomery potential ($\alpha p + g z$) at the top (interface `K`) and bottom
+ (interface `K+1`) of the layer. This compactly represents the combined effect
+ of the pressure gradient and the geopotential contribution from tilted coordinate
+ surfaces.
+
+2. **Specific volume correction**: A correction term equal to the edge-averaged
+ pressure at mid-layer multiplied by the horizontal gradient of specific volume.
+ This accounts for horizontal density variations that are not captured by the
+ Montgomery potential form.
+
+3. **Tidal and geopotential forcing** (`GradGeoPot`): The external geopotential
+ contribution from tidal forcing and self-attraction/loading, applied uniformly
+ across all layers at an edge.
+
+The tendency update for each layer is:
+
+```
+Tend(IEdge, K) += EdgeMask(IEdge, K) * (-GradMontPot + PGradAlpha - GradGeoPot)
+```
+
+where `EdgeMask` is applied to enforce land boundary conditions. The functor operator
+signature is:
+
+```c++
+KOKKOS_FUNCTION void operator()(const Array2DReal &Tend, I4 IEdge, I4 KChunk,
+ const Array2DReal &PressureMid,
+ const Array2DReal &PressureInterface,
+ const Array2DReal &ZInterface,
+ const Array1DReal &TidalPotential,
+ const Array1DReal &SelfAttractionLoading,
+ const Array2DReal &SpecVol) const;
+```
+
+### PressureGradHighOrder
+
+This functor is a placeholder for a future high-order pressure gradient implementation
+suitable for ice shelf cavities and complex bathymetry. Currently it performs no
+computation (a no-op).
+
+## Configuration
+
+The pressure gradient type is selected in the input YAML file:
+
+```yaml
+PressureGrad:
+ PressureGradType: 'centered'
+```
+
+Valid options for `PressureGradType` are:
+- `'centered'` or `'Centered'`: centered difference approximation (default)
+- `'HighOrder1'`: first high-order method (placeholder, future implementation)
+
+If an unrecognized value is provided, the implementation falls back to the centered
+scheme and logs an informational message.
+
+## Data members
+
+The `PressureGrad` class stores the following key data:
+
+| Member | Type | Description |
+| ------ | ---- | ----------- |
+| `NEdgesAll` | `I4` | Total number of edges including halo |
+| `NChunks` | `I4` | Number of vertical chunks for vectorization |
+| `NVertLayers` | `I4` | Number of vertical layers |
+| `NVertLayersP1` | `I4` | Number of vertical layers plus one |
+| `MinLayerEdgeBot` | `Array1DI4` | Minimum active layer index for each edge |
+| `MaxLayerEdgeTop` | `Array1DI4` | Maximum active layer index for each edge |
+| `TidalPotential` | `Array1DReal` | Tidal potential (placeholder, currently zero) |
+| `SelfAttractionLoading` | `Array1DReal` | Self-attraction and loading term (placeholder, currently zero) |
+| `CenteredPGrad` | `PressureGradCentered` | Centered pressure gradient functor |
+| `HighOrderPGrad` | `PressureGradHighOrder` | High-order pressure gradient functor |
+| `PressureGradChoice` | `PressureGradType` | Selected pressure gradient method |
+
+## Removal
+
+To remove all `PressureGrad` instances:
+
+```c++
+OMEGA::PressureGrad::clear();
+```
+
+To remove a specific named instance:
+
+```c++
+OMEGA::PressureGrad::erase("MyPGrad");
+```
diff --git a/components/omega/doc/devGuide/TendencyTerms.md b/components/omega/doc/devGuide/TendencyTerms.md
index 536efbffb60c..e665fb03ed76 100644
--- a/components/omega/doc/devGuide/TendencyTerms.md
+++ b/components/omega/doc/devGuide/TendencyTerms.md
@@ -35,8 +35,9 @@ implemented:
- `SSHGradOnEdge`
- `VelocityDiffusionOnEdge`
- `VelocityHyperDiffOnEdge`
+- `WindForcingOnEdge`
+- `BottomDragOnEdge`
- `TracerHorzAdvOnCell`
+- `TracerHighOrderHorzAdvOnCell`
- `TracerDiffOnCell`
- `TracerHyperDiffOnCell`
-- `WindForcingOnEdge`
-- `BottomDragOnEdge`
diff --git a/components/omega/doc/devGuide/Tracers.md b/components/omega/doc/devGuide/Tracers.md
index 17f3d4fca937..5bb55c1e158a 100644
--- a/components/omega/doc/devGuide/Tracers.md
+++ b/components/omega/doc/devGuide/Tracers.md
@@ -85,11 +85,7 @@ I4 Err = Tracers::getGroupRange(GroupRange, GroupName);
auto [StartIndex, GroupLength] = GroupRange;
// Get all tracers at the current time level (0)
-Array3DReal TracerArray;
-Err = OMEGA::Tracers::getAll(TracerArray, 0);
-if (Err != 0)
- LOG_ERROR("getAll returns an error code: {}.", Err);
-
+Array3DReal TracerArray = OMEGA::Tracers::getAll(0);
OMEGA::parallelFor(
"ComputeGroupTendency",
@@ -112,12 +108,10 @@ previous time levels:
```c++
// Get all tracers at the current time level (0)
-Array3DReal CurrentTracerArray;
-I4 Err1 = OMEGA::Tracers::getAll(CurrentTracerArray, 0);
+Array3DReal CurrentTracerArray = OMEGA::Tracers::getAll(0);
// Get all tracers at the previous time level (-1)
-Array3DReal PreviousTracerArray;
-I4 Err2 = OMEGA::Tracers::getAll(PreviousTracerArray, -1);
+Array3DReal PreviousTracerArray = OMEGA::Tracers::getAll(-1);
```
## Initialization and Finalization
@@ -131,17 +125,16 @@ OMEGA itself, so users may not need to call them separately.
### `getAll` and `getAllHost`
-These functions return all device and host tracer arrays, respectively. If
-the specified `TimeLevel` does not exist, they return a negative integer.
+These functions return all device and host tracer arrays, respectively.
+If the specified `TimeLevel` does not exist, the program will abort with
+an error message.
```c++
static HostArray3DReal getAllHost(
- HostArray3DReal &TracerArrayH, ///< [out] tracer host array
const I4 TimeLevel ///< [in] Time level index
);
static Array3DReal getAll(
- Array3DReal &TracerArray, ///< [out] tracer device array
const I4 TimeLevel ///< [in] Time level index
);
```
@@ -163,17 +156,15 @@ static I4 getGroupRange(
These functions return device and host tracer arrays, respectively, based
on the `TracerIndex`. If the specified `TimeLevel` and/or `TracerIndex`
-does not exist, they return a negative integer.
+does not exist, the program will abort with an error message.
```c++
static Array2DReal getByIndex(
- Array2DReal &TracerArray, ///< [out] tracer device array
const I4 TimeLevel, ///< [in] Time level index
const I4 TracerIndex ///< [in] Global tracer index
);
static HostArray2DReal getHostByIndex(
- HostArray2DReal &TracerArrayH, ///< [out] tracer host array
const I4 TimeLevel, ///< [in] Time level index
const I4 TracerIndex ///< [in] Global tracer index
);
diff --git a/components/omega/doc/devGuide/VertAdv.md b/components/omega/doc/devGuide/VertAdv.md
new file mode 100644
index 000000000000..672a25ef550e
--- /dev/null
+++ b/components/omega/doc/devGuide/VertAdv.md
@@ -0,0 +1,103 @@
+(omega-dev-vert-adv)=
+
+## Vertical Advection
+
+The `VertAdv` class contains methods and arrays for calculating vertical
+velocities and the tendencies of thickness, horizontal velocity, and tracers due
+to vertical advection.
+
+### Initialization
+
+The default `VertAdv` instance is created by calling `VertAdv::init()` method.
+The default `HorzMesh`, `VertCoord`, and `Tracers` objects must be initialized
+first. The default instance is retrieved by:
+```
+VertAdv *DefVertAdv = VertAdv::getDefault();
+```
+
+Additional instances can be created with the `create` method, which calls the
+constructor and places the new instance in a map identified with the label
+`Name`:
+```
+VertAdv *VertAdv::create(const std::string &Name, ///< [in] name for new VertAdv
+ const HorzMesh *Mesh, ///< [in] associated HorzMesh
+ const VertCoord *VCoord, ///< [in] associated VertCoord
+ Config *Options ///< [in] configuration options
+)
+```
+This instance is retrieved with the get method:
+```
+VertAdv *NewVertAdv = VertAdv::get("Name");
+```
+The constructor reads configuration options, stores some info from the
+`HorzMesh`, `VertCoord`, and `Tracers` objects, allocates needed arrays,
+and defines `Field` metadata.
+
+### Variables
+
+The following arrays are stored as members of the `VertAdv` class.
+
+| Variable Name | Type | Dimensions |
+| ------------- | ---- | ---------- |
+| VerticalVelocity | Real | NCellsSize, NVertLayersP1 |
+| TotalVerticalVelocity | Real | NCellsSize, NVertLayersP1 |
+| VertFlux | Real | NTracers, NCellsSize, NVertLayersP1 |
+| LowOrderVertFlux | Real | NTracers, NCellsSize, NVertLayersP1 |
+
+The `VerticalVelocity` represents the raw vertical pseudovelocity through the
+top interfaces of cell layers, computed from the divergence of the horizontal
+velocity. The `TotalVerticalVelocity` is the transport velocity and includes
+corrections applied to the `VerticalVelocity`. The `VertFlux` and
+`LowOrderVertFlux` store tracer fluxes at layer interfaces. The specific
+numerical algorithms to compute these fluxes are chosen by the user through
+configuration options.
+
+### Methods
+
+The `VerticalVelocity` and `TotalVerticalVelocity` arrays are computed by
+calling the `computeVerticalVelocity` method:
+```
+VertAdv::computeVerticalVelocity(NormalVelocity, FluxLayerThickEdge);
+```
+This method takes as input the `NormalVelocity` field from the `OceanState`
+object, and the `FluxLayerThickEdge` field from the `AuxiliaryState`. At
+present, `TotalVerticalVelocity` is equivalent to `VerticalVelocity`;
+additional corrections will be added in subsequent updates. The
+`TotalVerticalVelocity` array is used by each of the tendency methods,
+therefore `computeVerticalVelocity` must be called before any tendency
+computations.
+
+There are three methods for computing vertical advection tendencies of
+thickness, horizontal velocity and tracers that are called from the time
+stepper: `computeThicknessVAdvTend`, `computeVelocityVAdvTend`, and
+`computeTracerVAdvTend`. Each method updates the corresponding tendency array
+in place (inout).
+
+Only the thickness tendency array is passed to the `computeThicknessVAdvTend`
+method:
+```
+VertAdv::computeThicknessVAdvTend(ThickTend);
+```
+
+The `computeVelocityVAdvTend` method requires the `NormalVelocity` and
+`FluxLayerThickEdge` fields, in addition to the velocity tendency array:
+```
+VertAdv::computeVelocityVAdvTend(VelTend, NormalVelocity, FluxLayerThickEdge);
+```
+
+The tracer vertical advection tendency depends on the configured settings.
+The `computeTracerVAdvTend` method takes as arguments the full 3D array of
+tracers, a thickness array (selected based on the configured advection
+algorithm) and a `TimeInterval` representing the time step, along with the
+tracer tendency array:
+```
+VertAdv::computeTracerVAdvTend(TracerTend, TracerArray, Thickness, TimeStep);
+```
+For the standard advection algorithm, `LayerThickness` from the `OceanState` is
+passed as the thickness argument; for the FCT algorithm, the `ProvThickness`
+from the `AuxiliaryState` is used instead.
+
+This method first calls `computeVerticalFluxes` to compute the tracer fluxes at
+layer interfaces, using the order of accuracy specified in the configuration
+file. It then dispatches to the selected advection algorithm (standard or FCT)
+to compute the tracer tendencies.
diff --git a/components/omega/doc/index.md b/components/omega/doc/index.md
index bac7b4633a60..41bd611140b3 100644
--- a/components/omega/doc/index.md
+++ b/components/omega/doc/index.md
@@ -49,8 +49,10 @@ userGuide/Reductions
userGuide/Tracers
userGuide/TridiagonalSolvers
userGuide/VertCoord
+userGuide/PGrad
userGuide/Timing
userGuide/VerticalMixingCoeff
+userGuide/VertAdv
```
```{toctree}
@@ -92,8 +94,10 @@ devGuide/Reductions
devGuide/Tracers
devGuide/TridiagonalSolvers
devGuide/VertCoord
+devGuide/PGrad
devGuide/Timing
devGuide/VerticalMixingCoeff
+devGuide/VertAdv
```
```{toctree}
diff --git a/components/omega/doc/userGuide/HorzMesh.md b/components/omega/doc/userGuide/HorzMesh.md
index 7d50db5c529e..2160b51c82ae 100644
--- a/components/omega/doc/userGuide/HorzMesh.md
+++ b/components/omega/doc/userGuide/HorzMesh.md
@@ -23,7 +23,6 @@ This includes the following variables:
| XCell, YCell, ZCell | Cartesian coordinates of cell centers | m |
| XEdge, YEdge, ZEdge | Cartesian coordinates of edge centers | m |
| XVertex, YVertex, ZVertex | Cartesian coordinates of vertices | m |
-| BottomDepth | Depth of the bottom of the ocean at cell centers | m |
| FCell, FEdge, FVertex | Coriolis parameter at cell centers/edges/vertices | radians/s |
| LonCell, LatCell | Longitude/latitude coordinates of cell centers | radians |
| LonEdge, LatEdge | Longitude/latitude coordinates of edge centers | radians |
diff --git a/components/omega/doc/userGuide/PGrad.md b/components/omega/doc/userGuide/PGrad.md
new file mode 100644
index 000000000000..0fc7bb5968a2
--- /dev/null
+++ b/components/omega/doc/userGuide/PGrad.md
@@ -0,0 +1,68 @@
+(omega-user-pgrad)=
+
+# Pressure Gradient
+
+The pressure gradient term in the momentum equation represents the force per unit
+mass due to horizontal variations in pressure and geopotential. This term is
+essential for capturing the dynamics of
+ocean circulation, including both barotropic and baroclinic motions.
+
+## Physical Background
+
+In the layered non-Boussinesq momentum equation solved in Omega, the pressure
+gradient tendency for each edge and layer includes three contributions:
+
+1. **Montgomery potential gradient**: The horizontal gradient of the Montgomery
+ potential ($\alpha p + g z$), averaged across the top and bottom interfaces of
+ each layer. The Montgomery potential combines the pressure gradient and the
+ geopotential, and its gradient along coordinate surfaces accounts for both the
+ direct pressure force and the effect of tilted layer interfaces that arise when
+ using a general vertical coordinate.
+
+2. **Specific volume correction**: A correction term proportional to the gradient
+ of specific volume (inverse density) at each edge. This term ensures that
+ horizontal density variations between the two cells sharing an edge are properly
+ represented in the pressure gradient force.
+
+3. **External geopotential forcing**: Contributions from the tidal potential and
+ the self-attraction and loading (SAL) terms. These represent gravitational
+ forcing from astronomical tides and the deformation of the solid Earth and ocean
+ surface in response to the ocean mass distribution. These terms are currently set
+ to zero and will be provided by a future tidal forcing module.
+
+## Configuration Options
+
+The pressure gradient method is configured in the input YAML file under the
+`PressureGrad` section:
+
+```yaml
+PressureGrad:
+ PressureGradType: 'centered'
+```
+
+### Available Methods
+
+**Centered Difference** (`'centered'` or `'Centered'`)
+- Computes the pressure gradient using a centered finite-difference approximation
+ of the Montgomery potential gradient and specific volume correction
+- Suitable for global ocean simulations without ice shelf cavities
+- Default and currently the only fully implemented option
+
+**High-Order** (`'HighOrder1'`)
+- Placeholder for a future high-order pressure gradient method based on volume
+ integral formulations
+- Intended for simulations with ice shelf cavities and steep bathymetry where the
+ centered scheme may be inaccurate
+- Not yet implemented; selecting this option produces zero pressure gradient tendency
+
+## Dependencies
+
+The pressure gradient calculation requires the following Omega components to be
+initialized first:
+
+- [**Horizontal Mesh**](omega-user-horz-mesh): provides mesh geometry including
+ distances between cell centers and edge connectivity
+- [**Vertical Coordinate**](omega-user-vert-coord): provides pressure at layer
+ mid-points and interfaces, interface heights ($z$), and geopotential
+- [**Equation of State**](omega-user-eos): provides the specific volume field
+- [**Ocean State**](omega-user-state): provides the current layer thicknesses
diff --git a/components/omega/doc/userGuide/TendencyTerms.md b/components/omega/doc/userGuide/TendencyTerms.md
index 69ad8437d210..5757e98ebe0f 100644
--- a/components/omega/doc/userGuide/TendencyTerms.md
+++ b/components/omega/doc/userGuide/TendencyTerms.md
@@ -15,6 +15,7 @@ tendency terms are currently implemented:
| VelocityDiffusionOnEdge | Laplacian horizontal mixing, defined on edges
| VelocityHyperDiffOnEdge | biharmonic horizontal mixing, defined on edges
| TracerHorzAdvOnCell | horizontal advection of thickness-weighted tracers
+| TracerHighOrderHorzAdvOnCell | second order horizontal advection of thickness-weighted tracers
| TracerDiffOnCell | horizontal diffusion of thickness-weighted tracers
| TracerHyperDiffOnCell | biharmonic horizontal mixing of thickness-weighted tracers
| WindForcingOnEdge | forcing by wind stress, defined on edges
@@ -43,6 +44,9 @@ the currently available tendency terms:
| | ViscDel4 | horizontal biharmonic mixing coefficient for normal velocity
| | DivFactor | scale factor for the divergence term
| TracerHorzAdvOnCell | TracerHorzAdvTendencyEnable | enable/disable term
+| | HorzTracerFluxOrder | 1 for standard linear advection
+| TracerHighOrderHorzAdvOnCell | TracerHorzAdvTendencyEnable | enable/disable term
+| | HorzTracerFluxOrder | 2 for second order advection algorithm
| TracerDiffOnCell | TracerDiffTendencyEnable | enable/disable term
| | EddyDiff2 | horizontal diffusion coefficient
| TracerHyperDiffOnCell | TracerHyperDiffTendencyEnable | enable/disable term
@@ -50,3 +54,85 @@ the currently available tendency terms:
| WindForcingOnEdge | WindForcingTendencyEnable | enable/disable term
| BottomDragOnEdge | BottomDragTendencyEnable | enable/disable term
| | BottomDragCoeff | bottom drag coefficient
+
+
+## Second Order Horizontal Advection Algorithm
+
+The horizontal advection is done independently within each ocean layer
+so a two dimensional scheme is required to advect mixing ratios.
+The second order horizontal advection scheme is described in the paper
+
+William C. Skamarock and Almut Gassmann,
+"Conservative Transport Schemes for Spherical Geodesic Grids: High-Order Flux Operators for ODE-Based Time Integration"
+Monthly Weather Review, Vol 139: Issue 9, pp 2962–2975, 2011. doi.org/10.1175/MWR-D-10-05056.1
+
+and only a summary of a few equations from that paper are reproduced here.
+
+The conservative form of the scalar transport equation
+within a fluid is given in equation (1) of Skamarock and Gassmann,
+$$
+ \frac{\partial(\rho\psi)}{\partial t} = -\nabla\cdot\mathbf{V}\rho\psi
+$$
+where $\rho$ is the fluid density, $\psi$ is the mixing ratio, and $\mathbf{V}$ is
+the fluid velocity. This is simplified by assuming that $\rho$ is constant and cancels out.
+The advection scheme for a finite volume method involves approximating this equation along an element
+edge at a certain time step and multiplying by the time step length to get an approximate scalar transport
+from element to element during the time step.
+The left hand side of this equation is a time derivative and for Omega this is handled by standard Runge—Kutta methods and
+not discussed here but can be found in the Skamarock and Gassmann paper.
+The right hand side of this equation is the spatial derivative that needs to be evaluated by the Omega horizontal advection scheme.
+Omega ocean uses unstructured Voronoi meshes and a
+finite volume scheme with $\psi$ defined on cell centers. A first order approximation
+to the spatial derivative of the left hand termacross a single edge of an element would be the difference of $\psi$ at
+element centroids divided by the distance from cell centroid to cell centroid. This is expressed in equation (5) of the paper.
+For edge $i$ between elements $i+1/2$ and $i-1/2$,
+$$
+ \frac{\partial(u\psi_i)}{\partial x} = \frac{1}{\Delta x}[F_{i+1/2}(u\psi) - F_{i-1/2}(u\psi)] + O(\Delta x^2).
+$$
+Where $u$ is $\mathbf{V}\cdot\mathbf{n}$ where $\mathbf{n}$ is the unit normal along the edge being evaluated and $F$ is the evaluation
+of $\psi$ for the elements on each side of the edge being evaluated.
+This is used when the TracerHorzAdvOnCell user option is active and HorzTracerFluxOrder is 1.
+To make the comparison to higher order methods explicit, this first order method is equivalent to defining $\psi$ as a linear function,
+$\psi = c_0 + c_x x + c_y y$, between the two elements sharing the edge $i$ and taking the directed derivative along the edge.
+
+
+For higher order flux calculations, higher order derivatives of $\psi$ are needed and for the user option of
+TracerHorzAdvOnCell along with HorzTracerFluxOrder set to 2, a quadratic approximation of $\psi$ is used,
+$$
+\psi = c_0 + c_x x + c_y y + c_{xx} x^2 + c_{xy} xy + c_{yy} y^2,
+$$
+defined for each edge in the mesh. The six coefficients in this quadratic equation are calculated from
+a least squares fit of $\psi$ in a neighborhood of elements around an edge. The
+least squares fit is defined in equation (12) of the paper,
+$$
+ \mathbf{f = (P^T P)^{-1}P^T s = Bs}
+$$
+where the vector $\mathbf{f} = [c_0,c_x,c_y,c_{xx},c_{xy},c_{yy}]$ and $\mathbf{P}$ is an $m\times 6$ matrix with each row,
+$(1,x_i,y_i,x_i^2,x_iy_i,y_i^2)$, based on the cell center coordinates where there is one row for each of the $m$ elements in the neighborhood.
+This results in a $6\times m$ matrix $\mathbf{B}$. The values of $\mathbf{s}$ are the mixing ratios,
+$\mathbf{s}=[\psi_0, \psi_1,...,\psi_m]$, of the elements in the neighborhood at the time step being evaluated.
+Note that $\mathbf{B}$ depends on the mesh geometry only, so only needs to
+be computed once for each edge during initialization. Since only the second order terms are used to determine the
+second order derivatives needed for the higher order flux corrections to the lower order linear flux given above
+and the derivatives are taken along a line connecting
+cell centers, the amount of data that needs to be stored is minimized and $\mathbf{B}$ reduces to a vector.
+
+The second order derivatives of $\psi$ are then combined as shown in equation (11) of Skamarock and Gassmann to get
+a second order horizontal transport algorithm.
+
+The computation of these second order fluxes is complicated by having to do these computations on a sphere.
+Figure 2 from the Skamarock and Gassmann paper describs the slight modifications needed to compute the entries
+of the $\mathbf{P}$ matrix for a spherical geometry where the distances need to be measured as arc lengths along
+the surface.
+
+In practice, the convergence of this higher order algorithm on a sphere is slightly below
+second order. For the advection of a $\psi$ in the shape of a cosine bell
+being advected around a sphere and compared with the analytic solution, the spatial convergence for grid refinement
+is about order 1.7 as shown in {numref}`tracer-higher-order-convergence`:
+
+```{figure} images/higher_order_tracer_convergence_on_sphere.jpeg
+:name: tracer-higher-order-convergence
+:align: center
+:width: 600 px
+Tracer higer order convergence example of a cosine bell advected on a sphere showing an order 1.71 convergence rate
+```
diff --git a/components/omega/doc/userGuide/VertAdv.md b/components/omega/doc/userGuide/VertAdv.md
new file mode 100644
index 000000000000..38511a201956
--- /dev/null
+++ b/components/omega/doc/userGuide/VertAdv.md
@@ -0,0 +1,47 @@
+(omega-user-vert-adv)=
+
+## Vertical Advection
+
+### Overview
+
+In Omega, vertical advection represents the transport of mass, momentum, and
+tracer quantities along the vertical coordinate due to vertical motion within a
+water column. In the context of Omega's
+[governing equations](omega-design-governing-eqns-omega1), vertical advection
+contributes to the tendencies of pseudo-thickness, horizontal velocity, and
+tracer fields. Vertical motion is inferred from the divergence of horizontal
+flow through the continuity equation and is necessary for accurately
+representing vertical transport in three-dimensional ocean simulations.
+
+The `VertAdv` class contains variables and functions for computing the vertical
+velocity and the tendencies of `LayerThickness`, `NormalVelocity`, and
+`Tracers`. The algorithms used are determined by options specified in the
+configuration file.
+
+### Configuration Options
+
+Within the `Tendencies` group of the configuration file, flags are used to
+enable or disable the contribution of vertical advection to the tendencies of
+thickness, velocity, and tracers:
+| Configuration name | Options |
+| ------------------ | ------- |
+| ThicknessVertAdvTendencyEnable | true / false |
+| VelocityVertAdvTendencyEnable | true / false |
+| TracerVertAdvTendencyEnable | true / false |
+
+Within the `Advection` group, options are provided to configure the algorithms
+used to compute the tracer tendencies:
+| Configuration name | Description | Options |
+| ------------------ | ----------- | ------- |
+| VerticalTracerFluxOrder | order of accuracy used for the tracer flux calculation | 2, 3, or 4 |
+| VerticalTracerFluxLimiterEnable | selects the tracer tendency algorithm | true / false |
+| Coef3rdOrder | coefficient used for blending 3rd- and 4th-order fluxes when `VerticalTracerFluxOrder == 3` | 0.0 - 1.0 |
+
+For `VerticalTracerFluxLimiterEnable`, `true` enables flux-corrected transport,
+and `false` selects the standard algorithm. For `Coef3rdOrder`, `1` corresponds
+to purely 3rd-order, `0` purely 4th-order.
+
+The `VertAdv` class provides four fields that can be written to output by
+adding them to the contents of an output stream in the configuration file:
+`VerticalVelocity`, `TotalVerticalVelocity`, `VertFlux`, and `LowOrderVertFlux`.
+These fields collectively make up the `VertAdv` output group.
diff --git a/components/omega/doc/userGuide/images/higher_order_tracer_convergence_on_sphere.jpeg b/components/omega/doc/userGuide/images/higher_order_tracer_convergence_on_sphere.jpeg
new file mode 100644
index 000000000000..fdcd0424a4c2
Binary files /dev/null and b/components/omega/doc/userGuide/images/higher_order_tracer_convergence_on_sphere.jpeg differ
diff --git a/components/omega/src/infra/Config.cpp b/components/omega/src/infra/Config.cpp
index 6e7077a60062..493ddc6eb744 100644
--- a/components/omega/src/infra/Config.cpp
+++ b/components/omega/src/infra/Config.cpp
@@ -33,17 +33,7 @@ int Config::NumReadGroups = 0;
MPI_Comm Config::ConfigComm;
Config Config::ConfigAll;
-//------------------------------------------------------------------------------
-// Constructor that creates configuration with a given name and an emtpy
-// YAML node that will be filled later with either a readAll or get call.
-Config::Config(const std::string &InName // [in] name of config, node
-) {
-
- // If this is the first Config created, set variables for reading the
- // input configuration file. In particular, to avoid too many MPI tasks
- // reading the same input stream, we divide the tasks into groups and
- // only one group at a time loads the full configuration from a stream
-
+void Config::Initialize() {
if (NotInitialized) {
// Determine MPI variables
MachEnv *DefEnv = MachEnv::getDefault();
@@ -58,6 +48,18 @@ Config::Config(const std::string &InName // [in] name of config, node
NotInitialized = false; // now initialized for future calls
}
+}
+//------------------------------------------------------------------------------
+// Constructor that creates configuration with a given name and an emtpy
+// YAML node that will be filled later with either a readAll or get call.
+Config::Config(const std::string &InName // [in] name of config, node
+) {
+
+ // If this is the first Config created, set variables for reading the
+ // input configuration file. In particular, to avoid too many MPI tasks
+ // reading the same input stream, we divide the tasks into groups and
+ // only one group at a time loads the full configuration from a stream
+ Initialize();
// Set the name - this is also used as the name of the root YAML node
// (a map node) in this configuration.
diff --git a/components/omega/src/infra/Config.h b/components/omega/src/infra/Config.h
index 54cd24dc878e..7dcb3b2f510e 100644
--- a/components/omega/src/infra/Config.h
+++ b/components/omega/src/infra/Config.h
@@ -59,6 +59,8 @@ class Config {
public:
// Methods
+ static void Initialize();
+
/// Constructor that creates a Configuration with a given name and an
/// empty YAML::Node. The node will be filled later with either a read
/// or get operation.
diff --git a/components/omega/src/infra/OmegaKokkos.h b/components/omega/src/infra/OmegaKokkos.h
index 5a3eb0b82499..a7ab8bca89da 100644
--- a/components/omega/src/infra/OmegaKokkos.h
+++ b/components/omega/src/infra/OmegaKokkos.h
@@ -78,6 +78,8 @@ using ScratchMemSpace = ExecSpace::scratch_memory_space;
using Kokkos::MemoryUnmanaged;
using Kokkos::PerTeam;
using Kokkos::TeamThreadRange;
+using RealScratchArray =
+ Kokkos::View;
/// team_size for hierarchical parallelism
#ifdef OMEGA_TARGET_DEVICE
@@ -321,7 +323,8 @@ KOKKOS_INLINE_FUNCTION void teamBarrier(const TeamMember &Team) {
// parallelForOuter: with label
template
inline void parallelForOuter(const std::string &Label,
- const int (&UpperBounds)[N], F &&Functor) {
+ const int (&UpperBounds)[N], F &&Functor,
+ int ScratchValsPerTeam = 0) {
auto LinFunctor = LinearIdxWrapper{std::forward(Functor), UpperBounds};
int LinBound = 1;
@@ -330,6 +333,12 @@ inline void parallelForOuter(const std::string &Label,
}
auto Policy = TeamPolicy(LinBound, OMEGA_TEAMSIZE);
+
+ if (ScratchValsPerTeam > 0) {
+ Policy.set_scratch_size(
+ 0, Kokkos::PerTeam(ScratchValsPerTeam * sizeof(Real)));
+ }
+
Kokkos::parallel_for(
Label, Policy, KOKKOS_LAMBDA(const TeamMember &Team) {
const int TeamId = Team.league_rank();
@@ -339,8 +348,10 @@ inline void parallelForOuter(const std::string &Label,
// parallelForOuter: without label
template
-inline void parallelForOuter(const int (&UpperBounds)[N], F &&Functor) {
- parallelForOuter("", UpperBounds, std::forward(Functor));
+inline void parallelForOuter(const int (&UpperBounds)[N], F &&Functor,
+ int ScratchValsPerTeam = 0) {
+ parallelForOuter("", UpperBounds, std::forward(Functor),
+ ScratchValsPerTeam);
}
// parallelForInner
diff --git a/components/omega/src/ocn/AuxiliaryState.cpp b/components/omega/src/ocn/AuxiliaryState.cpp
index 9643fe264cd5..135728e1b14f 100644
--- a/components/omega/src/ocn/AuxiliaryState.cpp
+++ b/components/omega/src/ocn/AuxiliaryState.cpp
@@ -3,6 +3,7 @@
#include "Field.h"
#include "Logging.h"
#include "Pacer.h"
+#include "TimeStepper.h"
namespace OMEGA {
@@ -19,9 +20,10 @@ static std::string stripDefault(const std::string &Name) {
// fields with IOStreams
AuxiliaryState::AuxiliaryState(const std::string &Name, const HorzMesh *Mesh,
Halo *MeshHalo, const VertCoord *VCoord,
- int NTracers)
- : Mesh(Mesh), MeshHalo(MeshHalo), VCoord(VCoord), Name(stripDefault(Name)),
- KineticAux(stripDefault(Name), Mesh, VCoord),
+ VertAdv *VAdv, int NTracers,
+ TimeInterval TimeStep)
+ : Mesh(Mesh), MeshHalo(MeshHalo), VCoord(VCoord), VAdv(VAdv),
+ Name(stripDefault(Name)), KineticAux(stripDefault(Name), Mesh, VCoord),
LayerThicknessAux(stripDefault(Name), Mesh, VCoord),
VorticityAux(stripDefault(Name), Mesh, VCoord),
VelocityDel2Aux(stripDefault(Name), Mesh, VCoord),
@@ -60,10 +62,8 @@ AuxiliaryState::~AuxiliaryState() {
// Compute the auxiliary variables needed for momentum equation
void AuxiliaryState::computeMomAux(const OceanState *State, int ThickTimeLevel,
int VelTimeLevel) const {
- Array2DReal LayerThickCell;
- Array2DReal NormalVelEdge;
- State->getLayerThickness(LayerThickCell, ThickTimeLevel);
- State->getNormalVelocity(NormalVelEdge, VelTimeLevel);
+ Array2DReal LayerThickCell = State->getLayerThickness(ThickTimeLevel);
+ Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel);
OMEGA_SCOPE(LocKineticAux, KineticAux);
OMEGA_SCOPE(LocLayerThicknessAux, LayerThicknessAux);
@@ -82,6 +82,9 @@ void AuxiliaryState::computeMomAux(const OceanState *State, int ThickTimeLevel,
OMEGA_SCOPE(MaxLayerEdgeBot, VCoord->MaxLayerEdgeBot);
OMEGA_SCOPE(MaxLayerEdgeTop, VCoord->MaxLayerEdgeTop);
+ R8 TimeStepSeconds;
+ TimeStep.get(TimeStepSeconds, TimeUnits::Seconds);
+
Pacer::start("AuxState:computeMomAux", 1);
Pacer::start("AuxState:vertexAuxState1", 2);
@@ -196,12 +199,20 @@ void AuxiliaryState::computeMomAux(const OceanState *State, int ThickTimeLevel,
parallelForInner(
Team, KRange, INNER_LAMBDA(int KChunk) {
- LocLayerThicknessAux.computeVarsOnCells(ICell, KChunk,
- LayerThickCell);
+ LocLayerThicknessAux.computeVarsOnCells(
+ ICell, KChunk, LayerThickCell, NormalVelEdge,
+ TimeStepSeconds);
});
});
Pacer::stop("AuxState:cellAuxState3", 2);
+ Pacer::start("AuxState:computeVerticalVelocity", 2);
+
+ const auto &FluxLayerThickEdge = LayerThicknessAux.FluxLayerThickEdge;
+ VAdv->computeVerticalVelocity(NormalVelEdge, FluxLayerThickEdge);
+
+ Pacer::stop("AuxState:computeVerticalVelocity", 2);
+
Pacer::stop("AuxState:computeMomAux", 1);
}
@@ -209,38 +220,41 @@ void AuxiliaryState::computeMomAux(const OceanState *State, int ThickTimeLevel,
void AuxiliaryState::computeAll(const OceanState *State,
const Array3DReal &TracerArray,
int ThickTimeLevel, int VelTimeLevel) const {
- Array2DReal LayerThickCell;
- Array2DReal NormalVelEdge;
- State->getLayerThickness(LayerThickCell, ThickTimeLevel);
- State->getNormalVelocity(NormalVelEdge, VelTimeLevel);
+ Array2DReal LayerThickCell = State->getLayerThickness(ThickTimeLevel);
+ Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel);
const int NTracers = TracerArray.extent_int(0);
+ OMEGA_SCOPE(LocLayerThicknessAux, LayerThicknessAux);
OMEGA_SCOPE(LocTracerAux, TracerAux);
OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
OMEGA_SCOPE(MinLayerEdgeBot, VCoord->MinLayerEdgeBot);
OMEGA_SCOPE(MaxLayerEdgeTop, VCoord->MaxLayerEdgeTop);
+ R8 TimeStepSeconds;
+ TimeStep.get(TimeStepSeconds, TimeUnits::Seconds);
+
Pacer::start("AuxState:computeAll", 1);
computeMomAux(State, ThickTimeLevel, VelTimeLevel);
- Pacer::start("AuxState:edgeAuxState4", 2);
+ Pacer::start("AuxState:cellAuxState3", 2);
parallelForOuter(
- "edgeAuxState4", {NTracers, Mesh->NEdgesAll},
- KOKKOS_LAMBDA(int LTracer, int IEdge, const TeamMember &Team) {
- const int KMin = MinLayerEdgeBot(IEdge);
- const int KMax = MaxLayerEdgeTop(IEdge);
+ "cellAuxState3", {Mesh->NCellsAll},
+ KOKKOS_LAMBDA(int ICell, const TeamMember &Team) {
+ const int KMin = MinLayerCell(ICell);
+ const int KMax = MaxLayerCell(ICell);
const int KRange = vertRangeChunked(KMin, KMax);
+
parallelForInner(
Team, KRange, INNER_LAMBDA(int KChunk) {
- LocTracerAux.computeVarsOnEdge(LTracer, IEdge, KChunk,
- NormalVelEdge, LayerThickCell,
- TracerArray);
+ LocLayerThicknessAux.computeVarsOnCells(
+ ICell, KChunk, LayerThickCell, NormalVelEdge,
+ TimeStepSeconds);
});
});
- Pacer::stop("AuxState:edgeAuxState4", 2);
+ Pacer::stop("AuxState:cellAuxState3", 2);
const auto &MeanLayerThickEdge = LayerThicknessAux.MeanLayerThickEdge;
@@ -272,8 +286,9 @@ void AuxiliaryState::computeAll(const OceanState *State,
// Create a non-default auxiliary state
AuxiliaryState *AuxiliaryState::create(const std::string &Name,
const HorzMesh *Mesh, Halo *MeshHalo,
- const VertCoord *VCoord,
- const int NTracers) {
+ const VertCoord *VCoord, VertAdv *VAdv,
+ const int NTracers,
+ TimeInterval TimeStep) {
if (AllAuxStates.find(Name) != AllAuxStates.end()) {
LOG_ERROR("Attempted to create a new AuxiliaryState with name {} but it "
"already exists",
@@ -281,24 +296,27 @@ AuxiliaryState *AuxiliaryState::create(const std::string &Name,
return nullptr;
}
- auto *NewAuxState =
- new AuxiliaryState(Name, Mesh, MeshHalo, VCoord, NTracers);
+ auto *NewAuxState = new AuxiliaryState(Name, Mesh, MeshHalo, VCoord, VAdv,
+ NTracers, TimeStep);
AllAuxStates.emplace(Name, NewAuxState);
return NewAuxState;
}
-// Create the default auxiliary state. Assumes that HorzMesh, VertCoord and
-// Halo have been initialized.
+// Create the default auxiliary state. Assumes that HorzMesh, VertCoord,
+// VertAdv, and Halo have been initialized.
void AuxiliaryState::init() {
- const HorzMesh *DefMesh = HorzMesh::getDefault();
- Halo *DefHalo = Halo::getDefault();
- const VertCoord *DefVCoord = VertCoord::getDefault();
+ const HorzMesh *DefMesh = HorzMesh::getDefault();
+ Halo *DefHalo = Halo::getDefault();
+ const VertCoord *DefVCoord = VertCoord::getDefault();
+ VertAdv *DefVAdv = VertAdv::getDefault();
+ const TimeStepper *DefTimeStepper = TimeStepper::getDefault();
- int NTracers = Tracers::getNumTracers();
+ int NTracers = Tracers::getNumTracers();
+ TimeInterval TimeStep = DefTimeStepper->getTimeStep();
- AuxiliaryState::DefaultAuxState =
- AuxiliaryState::create("Default", DefMesh, DefHalo, DefVCoord, NTracers);
+ AuxiliaryState::DefaultAuxState = AuxiliaryState::create(
+ "Default", DefMesh, DefHalo, DefVCoord, DefVAdv, NTracers, TimeStep);
Config *OmegaConfig = Config::getOmegaConfig();
DefaultAuxState->readConfigOptions(OmegaConfig);
@@ -357,19 +375,6 @@ void AuxiliaryState::readConfigOptions(Config *OmegaConfig) {
ABORT_ERROR("AuxiliaryState: Unknown FluxThicknessType requested");
}
- std::string FluxTracerTypeStr;
- Err += AdvectConfig.get("FluxTracerType", FluxTracerTypeStr);
- CHECK_ERROR_ABORT(
- Err, "AuxiliaryState: FluxTracerType not found in AdvectConfig");
-
- if (FluxTracerTypeStr == "Center") {
- this->TracerAux.TracersOnEdgeChoice = FluxTracerEdgeOption::Center;
- } else if (FluxTracerTypeStr == "Upwind") {
- this->TracerAux.TracersOnEdgeChoice = FluxTracerEdgeOption::Upwind;
- } else {
- ABORT_ERROR("AuxiliaryState: Unknown FluxTracerType requested");
- }
-
Config WindStressConfig("WindStress");
Err += OmegaConfig->get(WindStressConfig);
diff --git a/components/omega/src/ocn/AuxiliaryState.h b/components/omega/src/ocn/AuxiliaryState.h
index 313cc157f0f3..f3c14be20ac3 100644
--- a/components/omega/src/ocn/AuxiliaryState.h
+++ b/components/omega/src/ocn/AuxiliaryState.h
@@ -6,7 +6,9 @@
#include "Halo.h"
#include "HorzMesh.h"
#include "OceanState.h"
+#include "TimeMgr.h"
#include "Tracers.h"
+#include "VertAdv.h"
#include "VertCoord.h"
#include "auxiliaryVars/KineticAuxVars.h"
#include "auxiliaryVars/LayerThicknessAuxVars.h"
@@ -51,7 +53,8 @@ class AuxiliaryState {
// Create a non-default auxiliary state
static AuxiliaryState *create(const std::string &Name, const HorzMesh *Mesh,
Halo *MeshHalo, const VertCoord *VCoord,
- int NTracers);
+ VertAdv *VAdv, int NTracers,
+ TimeInterval TimeStep);
/// Get the default auxiliary state
static AuxiliaryState *getDefault();
@@ -84,7 +87,8 @@ class AuxiliaryState {
private:
AuxiliaryState(const std::string &Name, const HorzMesh *Mesh, Halo *MeshHalo,
- const VertCoord *VCoord, int NTracers);
+ const VertCoord *VCoord, VertAdv *VAdv, int NTracers,
+ TimeInterval TimeStep);
AuxiliaryState(const AuxiliaryState &) = delete;
AuxiliaryState(AuxiliaryState &&) = delete;
@@ -92,6 +96,9 @@ class AuxiliaryState {
const HorzMesh *Mesh;
Halo *MeshHalo;
const VertCoord *VCoord;
+ VertAdv *VAdv;
+ TimeInterval TimeStep;
+
static AuxiliaryState *DefaultAuxState;
static std::map> AllAuxStates;
};
diff --git a/components/omega/src/ocn/CustomTendencyTerms.cpp b/components/omega/src/ocn/CustomTendencyTerms.cpp
index ff9c17da6435..79ab03630912 100644
--- a/components/omega/src/ocn/CustomTendencyTerms.cpp
+++ b/components/omega/src/ocn/CustomTendencyTerms.cpp
@@ -10,6 +10,7 @@
#include "Config.h"
#include "GlobalConstants.h"
#include "TimeStepper.h"
+#include "VertCoord.h"
namespace OMEGA {
@@ -76,8 +77,8 @@ void ManufacturedSolution::init() {
/// This test case assumes that the restingThickness is horizontally uniform
/// and that only one vertical level is used so only one set of indices is
/// used here.
- HorzMesh *DefHorzMesh = HorzMesh::getDefault();
- R8 H0 = DefHorzMesh->BottomDepthH(0);
+ VertCoord *DefVCoord = VertCoord::getDefault();
+ R8 H0 = DefVCoord->BottomDepthH(0);
// Define and compute common constants
R8 Kx = TwoPi / WavelengthX; // Wave in X-dir
diff --git a/components/omega/src/ocn/CustomTendencyTerms.h b/components/omega/src/ocn/CustomTendencyTerms.h
index 7ae226d2a27a..0635e0483ed9 100644
--- a/components/omega/src/ocn/CustomTendencyTerms.h
+++ b/components/omega/src/ocn/CustomTendencyTerms.h
@@ -22,8 +22,8 @@
//
//===----------------------------------------------------------------------===//
-#include "HorzMesh.h"
-#include "TendencyTerms.h"
+#include "AuxiliaryState.h"
+#include "OceanState.h"
#include "TimeMgr.h"
namespace OMEGA {
diff --git a/components/omega/src/ocn/Eos.h b/components/omega/src/ocn/Eos.h
index 707297f01113..06c9695f7267 100644
--- a/components/omega/src/ocn/Eos.h
+++ b/components/omega/src/ocn/Eos.h
@@ -44,7 +44,6 @@ class Teos10Eos {
I4 KDisp) const {
Real SpecVolPCoeffs[6 * VecLength];
-
const I4 KStart = chunkStart(KChunk, MinLayerCell(ICell));
const I4 KLen = chunkLength(KChunk, KStart, MaxLayerCell(ICell));
@@ -63,15 +62,15 @@ class Teos10Eos {
if (KDisp == 0) {
// No displacement
SpecVol(ICell, K) =
- calcRefProfile(Pressure(ICell, K)) +
- calcDelta(SpecVolPCoeffs, KVec, Pressure(ICell, K));
+ calcRefProfile(Pressure(ICell, K) * Pa2Db) +
+ calcDelta(SpecVolPCoeffs, KVec, Pressure(ICell, K) * Pa2Db);
} 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(SpecVolPCoeffs, KVec, Pressure(ICell, KTmp));
+ calcRefProfile(Pressure(ICell, KTmp) * Pa2Db) +
+ calcDelta(SpecVolPCoeffs, KVec, Pressure(ICell, KTmp) * Pa2Db);
}
}
}
@@ -312,15 +311,15 @@ class Teos10BruntVaisalaFreqSq {
Real PInt =
0.5_Real * (Pressure(ICell, K) + Pressure(ICell, K - 1));
Real SpInt = 0.5_Real * (SpecVol(ICell, K) + SpecVol(ICell, K - 1));
- Real AlphaInt = calcAlpha(SaInt, CtInt, PInt, SpInt);
- Real BetaInt = calcBeta(SaInt, CtInt, PInt, SpInt);
+ Real AlphaInt = calcAlpha(SaInt, CtInt, PInt * Pa2Db, SpInt);
+ Real BetaInt = calcBeta(SaInt, CtInt, PInt * Pa2Db, SpInt);
Real DSa = AbsSalinity(ICell, K) - AbsSalinity(ICell, K - 1);
Real DCt = ConservTemp(ICell, K) - ConservTemp(ICell, K - 1);
Real DP = Pressure(ICell, K) - Pressure(ICell, K - 1);
BruntVaisalaFreqSq(ICell, K) = Gravity * Gravity *
(BetaInt * DSa - AlphaInt * DCt) /
- (SpInt * Db2Pa * DP);
+ (SpInt * DP);
}
}
}
@@ -568,7 +567,6 @@ class Eos {
const Array2DReal &AbsSalinity,
const Array2DReal &Pressure,
const Array2DReal &SpecVol);
-
/// Initialize EOS from config and mesh
static void init();
diff --git a/components/omega/src/ocn/HorzMesh.cpp b/components/omega/src/ocn/HorzMesh.cpp
index 0c626b49905e..f97d6379bae5 100644
--- a/components/omega/src/ocn/HorzMesh.cpp
+++ b/components/omega/src/ocn/HorzMesh.cpp
@@ -17,7 +17,6 @@
#include "Error.h"
#include "IO.h"
#include "Logging.h"
-#include "MachEnv.h"
#include "OmegaKokkos.h"
namespace OMEGA {
@@ -46,19 +45,21 @@ void HorzMesh::init() {
HorzMesh::HorzMesh(const std::string &Name, //< [in] Name for new mesh
Decomp *MeshDecomp //< [in] Decomp for the new mesh
-) {
-
+ )
+ : CellID(MeshDecomp->CellID) ///< global cell ID for each local cell
+{
MeshName = Name;
// Retrieve mesh files name from Decomp
MeshFileName = MeshDecomp->MeshFileName;
// Retrieve mesh cell/edge/vertex totals from Decomp
- NCellsHalo = MeshDecomp->NCellsHalo;
- NCellsHaloH = MeshDecomp->NCellsHaloH;
- NCellsOwned = MeshDecomp->NCellsOwned;
- NCellsAll = MeshDecomp->NCellsAll;
- NCellsSize = MeshDecomp->NCellsSize;
+ NCellsHalo = MeshDecomp->NCellsHalo;
+ NCellsHaloH = MeshDecomp->NCellsHaloH;
+ NCellsOwned = MeshDecomp->NCellsOwned;
+ NCellsAll = MeshDecomp->NCellsAll;
+ NCellsSize = MeshDecomp->NCellsSize;
+ NCellsGlobal = MeshDecomp->NCellsGlobal;
NEdgesHalo = MeshDecomp->NEdgesHalo;
NEdgesHaloH = MeshDecomp->NEdgesHaloH;
@@ -67,6 +68,7 @@ HorzMesh::HorzMesh(const std::string &Name, //< [in] Name for new mesh
NEdgesSize = MeshDecomp->NEdgesSize;
MaxCellsOnEdge = MeshDecomp->MaxCellsOnEdge;
MaxEdges = MeshDecomp->MaxEdges;
+ NEdgesGlobal = MeshDecomp->NEdgesGlobal;
NVerticesHalo = MeshDecomp->NVerticesHalo;
NVerticesHaloH = MeshDecomp->NVerticesHaloH;
@@ -111,9 +113,6 @@ HorzMesh::HorzMesh(const std::string &Name, //< [in] Name for new mesh
// Read x/y/z and lon/lat coordinates for cells, edges, and vertices
readCoordinates();
- // Read the cell-centered bottom depth
- readBottomDepth();
-
// Read the mesh areas, lengths, and angles
readMeasurements();
@@ -439,12 +438,6 @@ void HorzMesh::readCoordinates() {
} // end readCoordinates
-//------------------------------------------------------------------------------
-// Read the cell-centered bottom depth
-void HorzMesh::readBottomDepth() {
- readCellArray(BottomDepthH, "bottomDepth");
-} // end readDepth
-
//------------------------------------------------------------------------------
// Read the mesh areas (cell, triangle, and kite),
// lengths (between centers and vertices), and edge angles
@@ -600,7 +593,6 @@ void HorzMesh::copyToDevice() {
AngleEdge = createDeviceMirrorCopy(AngleEdgeH);
WeightsOnEdge = createDeviceMirrorCopy(WeightsOnEdgeH);
FVertex = createDeviceMirrorCopy(FVertexH);
- BottomDepth = createDeviceMirrorCopy(BottomDepthH);
FEdge = createDeviceMirrorCopy(FEdgeH);
XCell = createDeviceMirrorCopy(XCellH);
YCell = createDeviceMirrorCopy(YCellH);
diff --git a/components/omega/src/ocn/HorzMesh.h b/components/omega/src/ocn/HorzMesh.h
index 0b244c0e17a9..e36762730e57 100644
--- a/components/omega/src/ocn/HorzMesh.h
+++ b/components/omega/src/ocn/HorzMesh.h
@@ -42,16 +42,12 @@ class HorzMesh {
void readCoordinates();
- void readBottomDepth();
-
void readMeasurements();
void readWeights();
void readCoriolis();
- // void computeEdgeSign();
-
void copyToDevice();
// int computeMesh();
@@ -98,6 +94,7 @@ class HorzMesh {
I4 NCellsOwned; ///< Number of cells owned by this task
I4 NCellsAll; ///< Total number of local cells (owned + all halo)
I4 NCellsSize; ///< Array size (incl padding, bndy cell) for cell arrays
+ I4 NCellsGlobal;
Array1DI4 NEdgesHalo; ///< num cells owned+halo for halo layer
HostArray1DI4 NEdgesHaloH; ///< num cells owned+halo for halo layer
@@ -106,7 +103,8 @@ class HorzMesh {
I4 NEdgesSize; ///< Array length (incl padding, bndy) for edge dim
I4 MaxCellsOnEdge; ///< Max number of cells sharing an edge
I4 MaxEdges; ///< Max number of edges around a cell
- I4 MaxEdges2; ///< Max number of edges around a cell x2
+ I4 NEdgesGlobal;
+ I4 MaxEdges2; ///< Max number of edges around a cell x2
Array1DI4 NVerticesHalo; ///< num cells owned+halo for halo layer
HostArray1DI4 NVerticesHaloH; ///< num cells owned+halo for halo layer
@@ -147,6 +145,7 @@ class HorzMesh {
Array2DI4 EdgesOnVertex; ///< Indx of edges sharing vertex as endpoint
HostArray2DI4 EdgesOnVertexH; ///< Indx of edges sharing vertex as endpoint
+ Array1DI4 CellID; ///< global cell ID for each local cell
// Coordinates
Array1DReal XCell; ///< X Coordinates of cell centers (m)
@@ -232,11 +231,6 @@ class HorzMesh {
Array1DReal FVertex; ///< Coriolis parameter at vertices (radians s^-1)
HostArray1DReal FVertexH; ///< Coriolis parameter at vertices (radians s^-1)
- // Depth
-
- Array1DReal BottomDepth; ///< Depth of the bottom of the ocean (m)
- HostArray1DReal BottomDepthH; ///< Depth of the bottom of the ocean (m)
-
// Edge sign
Array2DReal EdgeSignOnCell; ///< Sign of vector connecting cells
diff --git a/components/omega/src/ocn/HorzOperators.cpp b/components/omega/src/ocn/HorzOperators.cpp
index b0f13dbe8d59..e182dd4f77ed 100644
--- a/components/omega/src/ocn/HorzOperators.cpp
+++ b/components/omega/src/ocn/HorzOperators.cpp
@@ -27,4 +27,46 @@ InterpCellToEdge::InterpCellToEdge(const HorzMesh *Mesh)
KiteAreasOnVertex(Mesh->KiteAreasOnVertex),
VertexDegree(Mesh->VertexDegree) {}
+SecondDerivativeOnCell::SecondDerivativeOnCell(HorzMesh const *Mesh)
+ : OnSphere(true), NCellsAll(Mesh->NCellsAll), MaxEdges(1 + Mesh->MaxEdges),
+ NEdgesOnCell(Mesh->NEdgesOnCell), EdgesOnCell(Mesh->EdgesOnCell),
+ CellsOnCell(Mesh->CellsOnCell), CellsOnEdge(Mesh->CellsOnEdge),
+ VerticesOnEdge(Mesh->VerticesOnEdge),
+
+ XCell(Mesh->XCell), YCell(Mesh->YCell),
+ ZCell(createDeviceMirrorCopy(Mesh->ZCellH)), DvEdge(Mesh->DvEdge),
+ DcEdge(Mesh->DcEdge), AngleEdge(Mesh->AngleEdge),
+ AreaCell(Mesh->AreaCell), EdgeSignOnCell(Mesh->EdgeSignOnCell),
+ XVertex(createDeviceMirrorCopy(Mesh->XVertexH)),
+ YVertex(createDeviceMirrorCopy(Mesh->YVertexH)),
+ ZVertex(createDeviceMirrorCopy(Mesh->ZVertexH)),
+
+ ThetaAbs("SphereAngle", NCellsAll), XPCell("XP", NCellsAll, MaxEdges),
+ YPCell("YP", NCellsAll, MaxEdges),
+ Angle2DCell("Angle2D", NCellsAll, MaxEdges),
+ BCell("WorkSpaceForLeastSquares", NCellsAll, 6, MaxEdges),
+ CellListCell("CellList", NCellsAll, MaxEdges) {
+ if (MaxMaxEdges <= Mesh->MaxEdges)
+ LOG_CRITICAL(
+ "SecondDerivativeOnCell::SecondDerivativeOnCell Max Edges exceeded:"
+ "Max Allowed: {} Found in Mesh:{}",
+ MaxMaxEdges - 1, Mesh->MaxEdges);
+}
+
+MasksAndCoefficients::MasksAndCoefficients(
+ HorzMesh const *Mesh, const Array3DReal DerivTwo,
+ Array1DI4 NAdvCellsForEdge, Array2DI4 AdvCellsForEdge,
+ Array1DI4 AdvMaskHighOrder, Array2DReal AdvCoefs, Array2DReal AdvCoefs3rd)
+ : NCellsGlobal(Mesh->NCellsGlobal), NCellsAll(Mesh->NCellsAll),
+ NAdvCellsMax(Mesh->MaxEdges2), // PatchCellLists("PatchCellLists",
+ // Mesh->NEdgesOwned, Mesh->NEdgesAll+1),
+ NAdvCellsForEdge(NAdvCellsForEdge), AdvCellsForEdge(AdvCellsForEdge),
+ NEdgesOnEdge(Mesh->NEdgesOnEdge), NEdgesOnCell(Mesh->NEdgesOnCell),
+ CellIndx("CellIndx", Mesh->NEdgesAll, Mesh->MaxEdges2 + 2),
+ CellIndxSorted("CellIndxSorted", Mesh->NEdgesAll, 2, Mesh->MaxEdges2 + 2),
+ CellID(Mesh->CellID), AdvMaskHighOrder(AdvMaskHighOrder),
+ EdgesOnEdge(Mesh->EdgesOnEdge), CellsOnCell(Mesh->CellsOnCell),
+ CellsOnEdge(Mesh->CellsOnEdge), DcEdge(Mesh->DcEdge),
+ DvEdge(Mesh->DvEdge), AdvCoefs(AdvCoefs), AdvCoefs3rd(AdvCoefs3rd),
+ DerivTwo(DerivTwo) {}
} // namespace OMEGA
diff --git a/components/omega/src/ocn/HorzOperators.h b/components/omega/src/ocn/HorzOperators.h
index 6e187a2976e4..6838ad071146 100644
--- a/components/omega/src/ocn/HorzOperators.h
+++ b/components/omega/src/ocn/HorzOperators.h
@@ -2,7 +2,9 @@
#define OMEGA_HORZOPERATORS_H
#include "DataTypes.h"
+#include "GlobalConstants.h"
#include "HorzMesh.h"
+#include "HorzUtil.h"
namespace OMEGA {
@@ -186,5 +188,461 @@ class InterpCellToEdge {
I4 VertexDegree;
};
+class SecondDerivativeOnCell {
+ /*
+ \brief deriv two computation
+ \author Doug Jacobsen, Bill Skamarock
+ \date 03/09/12
+ \details
+ This routine precomputes the second derivative values for tracer
+ advection. It computes cell coefficients for the polynomial fit
+ as described in:
+ Skamarock, W. C., & Gassmann, A. (2011).
+ Conservative Transport Schemes for Spherical Geodesic Meshs:
+ High-Order Flux Operators for ODE-Based Time Integration.
+ Monthly Weather Review, 139(9), 2962-2975.
+ doi:10.1175/MWR-D-10-05056.1
+ This is performed during model initialization.
+ */
+ public:
+ SecondDerivativeOnCell(HorzMesh const *Mesh);
+ KOKKOS_FUNCTION ~SecondDerivativeOnCell() {}
+ KOKKOS_FUNCTION void operator()(const Array3DReal &DerivTwo,
+ const int ICell) const {
+ const int NEdges = NEdgesOnCell(ICell);
+ if (MaxMaxEdges < NEdges)
+ printf("Error: Number of edges on cell:%d exceeds maximum "
+ "expected:%d for cell:%d",
+ NEdges, MaxMaxEdges, ICell);
+
+ // check to see if we are reaching outside the halo
+ auto CellList = Kokkos::subview(CellListCell, ICell, Kokkos::ALL);
+ bool doCell = true;
+ CellList[0] = ICell;
+ for (int J = 1; J <= NEdges; ++J)
+ CellList[J] = CellsOnCell(ICell, J - 1);
+
+ for (int I = 0; I <= NEdges; ++I)
+ if (NCellsAll <= CellList[I])
+ doCell = false;
+ if (!doCell)
+ return;
+
+ auto XP = Kokkos::subview(XPCell, ICell, Kokkos::ALL);
+ auto YP = Kokkos::subview(YPCell, ICell, Kokkos::ALL);
+ auto Angle2D = Kokkos::subview(Angle2DCell, ICell, Kokkos::ALL);
+ auto B = Kokkos::subview(BCell, ICell, Kokkos::ALL, Kokkos::ALL);
+ if (OnSphere) {
+ const Array1DI4 edgesOnCell =
+ Kokkos::subview(EdgesOnCell, ICell, Kokkos::ALL);
+ DetermineSphericalPatchGeometry(
+ NEdges, edgesOnCell, VerticesOnEdge, XCell, YCell, ZCell, XVertex,
+ YVertex, ZVertex, CellList, XP, YP, Angle2D, ThetaAbs[ICell]);
+ } else { // On an x-y plane
+ const Array1DI4 edgesOnCell =
+ Kokkos::subview(EdgesOnCell, ICell, Kokkos::ALL);
+ DeterminePlanerPatchGeometry(ICell, NEdges, edgesOnCell, CellsOnEdge,
+ AngleEdge, DcEdge, XP, YP, Angle2D);
+ }
+ LeastSquaresFit(XP, YP, NEdges, B);
+
+ // fill second derivative stencil for rk advection
+ for (int I = 0; I < NEdges; ++I) {
+ const I4 IEdge = EdgesOnCell(ICell, I);
+ const I4 Ind = (ICell == CellsOnEdge(IEdge, 0)) ? 0 : 1;
+ const Real Theta = Angle2D[I];
+ const Real x = Kokkos::cos(Theta);
+ const Real y = Kokkos::sin(Theta);
+ const Real xx = x * x;
+ const Real xy = x * y;
+ const Real yy = y * y;
+ for (int J = 0; J <= NEdges; ++J)
+ DerivTwo(J, Ind, IEdge) = 2._Real * xx * B(3, J) +
+ 2._Real * xy * B(4, J) +
+ 2._Real * yy * B(5, J);
+ }
+ }
+
+ private:
+ // MaxMaxEdges is used to dimention arrays that include ICell and the
+ // neighbor cells, so it is technically one more than MaxEdges.
+ static const I4 MaxMaxEdges = 10;
+ static constexpr R8 Pii = 3.141592653589793_Real;
+
+ const bool OnSphere;
+ const I4 NCellsAll;
+ const I4 MaxEdges;
+ Array1DI4 NEdgesOnCell;
+ Array2DI4 EdgesOnCell;
+ Array2DI4 CellsOnCell;
+ Array2DI4 CellsOnEdge;
+ Array2DI4 VerticesOnEdge;
+
+ Array1DReal XCell;
+ Array1DReal YCell;
+ Array1DReal ZCell;
+ Array1DReal DvEdge;
+ Array1DReal DcEdge;
+ Array1DReal AngleEdge;
+ Array1DReal AreaCell;
+ Array2DReal EdgeSignOnCell;
+ Array1DReal XVertex;
+ Array1DReal YVertex;
+ Array1DReal ZVertex;
+
+ Array1DReal ThetaAbs;
+ Array2DReal XPCell;
+ Array2DReal YPCell;
+ Array2DReal Angle2DCell;
+ Array3DReal BCell;
+ Array2DI4 CellListCell;
+
+ protected:
+ KOKKOS_INLINE_FUNCTION static void LeastSquaresFit(const Array1DReal XP,
+ const Array1DReal YP,
+ const int NEdges,
+ Array2DReal B) {
+ constexpr int NA = 6;
+ Real P[MaxMaxEdges][NA] = {};
+
+ // (polynomial_order == 2) is the only order supported
+ // The first row is for the origin (0,0) since the data
+ // is relative to the ICell centroid.
+ P[0][0] = 1._Real;
+ for (int I = 1; I <= NEdges; ++I) {
+ P[I][0] = 1._Real;
+ P[I][1] = XP[I - 1];
+ P[I][2] = YP[I - 1];
+ P[I][3] = XP[I - 1] * XP[I - 1];
+ P[I][4] = XP[I - 1] * YP[I - 1];
+ P[I][5] = YP[I - 1] * YP[I - 1];
+ }
+ poly_fit_2(P, B, 1 + NEdges);
+ }
+
+ KOKKOS_INLINE_FUNCTION static void DeterminePlanerPatchGeometry(
+ const int ICell, const int NEdges, const Array1DI4 EdgesOnCell,
+ const Array2DI4 CellsOnEdge, const Array1DReal AngleEdge,
+ const Array1DReal DcEdge, Array1DReal XP, Array1DReal YP,
+ Array1DReal Angle2D) {
+ for (int I = 0; I < NEdges; ++I) {
+ const auto IEdge = EdgesOnCell[I];
+ Angle2D[I] = AngleEdge(IEdge);
+ if (ICell != CellsOnEdge(IEdge, 0))
+ Angle2D[I] -= Pii;
+ XP[I] = DcEdge(IEdge) * Kokkos::cos(Angle2D[I]);
+ YP[I] = DcEdge(IEdge) * Kokkos::sin(Angle2D[I]);
+ }
+ }
+ KOKKOS_INLINE_FUNCTION static void DetermineSphericalPatchGeometry(
+ const int NEdges, const Array1DI4 EdgesOnCell,
+ const Array2DI4 VerticesOnEdge, const Array1DReal XCell,
+ const Array1DReal YCell, const Array1DReal ZCell,
+ const Array1DReal XVertex, const Array1DReal YVertex,
+ const Array1DReal ZVertex, const Array1DI4 CellList, Array1DReal XP,
+ Array1DReal YP, Array1DReal Angle2D, Real &ThetaAbs) {
+ const Real length_scale = 1._Real;
+ const Real sphereRadius = REarth;
+ Real XC[MaxMaxEdges] = {};
+ Real YC[MaxMaxEdges] = {};
+ Real ZC[MaxMaxEdges] = {};
+
+ Real Thetat_prev = 0;
+ Real Thetav_prev = 0;
+
+ for (int I = 0; I <= NEdges; ++I) {
+ const auto J = CellList[I];
+ XC[I] = XCell(J) / sphereRadius;
+ YC[I] = YCell(J) / sphereRadius;
+ ZC[I] = ZCell(J) / sphereRadius;
+ }
+ if (ZC[0] == 1.0_Real)
+ ThetaAbs = Pii / 2._Real;
+ else if (1 - ZC[0] < 1.0e-6)
+ ThetaAbs = Pii / 2._Real - (1 - ZC[0]) * std::atan2(YC[0], XC[0]);
+ else
+ ThetaAbs =
+ Pii / 2._Real - sphere_angle(XC[0], YC[0], ZC[0], XC[1], YC[1],
+ ZC[1], 0._Real, 0._Real, 1._Real);
+
+ for (int I = 0; I < NEdges; ++I) {
+ int Ip1 = I + 1, Ip2 = I + 2;
+ if (NEdges < Ip2)
+ Ip2 = 1;
+ // angles from cell center to neighbor centers (thetav)
+ const Real Thetav = sphere_angle(XC[0], YC[0], ZC[0], XC[Ip1], YC[Ip1],
+ ZC[Ip1], XC[Ip2], YC[Ip2], ZC[Ip2]);
+ Real Dl_sphere = sphereRadius * arc_length(XC[0], YC[0], ZC[0],
+ XC[Ip1], YC[Ip1], ZC[Ip1]);
+
+ Dl_sphere /= length_scale;
+ // Thetat = 0. this defines the x direction,
+ // cell center 0 -> this defines the x direction, longitude line
+ const Real Thetat = I ? Thetat_prev + Thetav_prev : ThetaAbs;
+ Thetat_prev = Thetat;
+ Thetav_prev = Thetav;
+
+ XP[I] = Kokkos::cos(Thetat) * Dl_sphere;
+ YP[I] = Kokkos::sin(Thetat) * Dl_sphere;
+
+ const I4 IEdge = EdgesOnCell[I];
+ Real XV[2] = {}, YV[2] = {}, ZV[2] = {}, EC[3] = {};
+ for (int J = 0; J < 2; ++J) {
+ const I4 vert = VerticesOnEdge(IEdge, J);
+ XV[J] = XVertex(vert) / sphereRadius;
+ YV[J] = YVertex(vert) / sphereRadius;
+ ZV[J] = ZVertex(vert) / sphereRadius;
+ }
+ arc_bisect(XV[0], YV[0], ZV[0], XV[1], YV[1], ZV[1], EC[0], EC[1],
+ EC[2]);
+ Real ThTmp = sphere_angle(XC[0], YC[0], ZC[0], XC[Ip1], YC[Ip1],
+ ZC[Ip1], EC[0], EC[1], EC[2]);
+ ThTmp += Thetat;
+ Angle2D[I] = ThTmp;
+ }
+ }
+};
+
+class MasksAndCoefficients {
+
+ KOKKOS_INLINE_FUNCTION static void swap(Array2DI4 &vec, const int m,
+ const int n) {
+ for (int i : {0, 1}) {
+ const I4 j = vec(i, m);
+ vec(i, m) = vec(i, n);
+ vec(i, n) = j;
+ }
+ }
+ // Sort the second dimension (values) of vec based on the first (keys):
+ // Array1DI4 keys = Kokkos::subview(vec, 0, Kokkos::ALL);
+ // Array1DI4 values = Kokkos::subview(vec, 1, Kokkos::ALL);
+ KOKKOS_INLINE_FUNCTION static int partition(Array2DI4 &vec, const int low,
+ const int high) {
+ // Selecting last element as the pivot
+ const I4 pivot = vec(0, high);
+ int i = (low - 1);
+ for (int j = low; j < high; ++j) {
+ if (vec(0, j) <= pivot) {
+ i++;
+ swap(vec, i, j);
+ }
+ }
+ // Put pivot to its position
+ swap(vec, i + 1, high);
+ // Return the point of partition
+ return (i + 1);
+ }
+
+ KOKKOS_INLINE_FUNCTION static void sort_by_key(Array2DI4 &vec, const I4 low,
+ const I4 high) {
+ // Base case: This part will be executed till the starting
+ // index low is lesser than the ending index high
+ if (low < high) {
+ // pi is Partitioning Index, vec[pi] is now at
+ // right place
+ const I4 pi = partition(vec, low, high);
+ // Separately sort elements before and after the
+ // Partition Index pi
+ sort_by_key(vec, low, pi - 1);
+ sort_by_key(vec, pi + 1, high);
+ }
+ }
+ KOKKOS_INLINE_FUNCTION static bool is_sorted(Array2DI4 &vec) {
+ bool sorted = true;
+ const I4 N = vec.extent(1) - 1;
+ for (I4 I = 0; I < N && sorted; ++I)
+ if (vec(0, I + 1) < vec(0, I))
+ sorted = false;
+ return sorted;
+ }
+ KOKKOS_INLINE_FUNCTION static I4 search(Array1DI4 &vec, const I4 x) {
+ I4 low = 0, high = vec.extent(0) - 1;
+ while (low <= high) {
+ const I4 mid = low + (high - low) / 2;
+ if (vec(mid) == x)
+ return mid;
+ else if (vec(mid) < x)
+ low = mid + 1;
+ else
+ high = mid - 1;
+ }
+ // If we reach here, then element was not present
+ return -1;
+ }
+ KOKKOS_INLINE_FUNCTION static bool found_in_list(const Array1DI4 &List,
+ const I4 N, const I4 X) {
+ bool found = false;
+ for (I4 I = 0; I < N && !found; ++I)
+ if (X == List(I))
+ found = true;
+ return found;
+ }
+
+ public:
+ MasksAndCoefficients(HorzMesh const *Mesh, const Array3DReal DerivTwo,
+ Array1DI4 NAdvCellsForEdge, Array2DI4 AdvCellsForEdge,
+ Array1DI4 AdvMaskHighOrder, Array2DReal AdvCoefs,
+ Array2DReal AdvCoefs3rd);
+
+ KOKKOS_FUNCTION void operator()(const int IEdge) const {
+
+ // Array1DI4 PatchCellList = Kokkos::subview(PatchCellLists, IEdge,
+ // Kokkos::ALL); for (I4 I=0; I< PatchCellList.extent(0))
+ // PatchCellList[I] = -1;
+ NAdvCellsForEdge(IEdge) = 0;
+ Array1DI4 CellIndex = Kokkos::subview(CellIndx, IEdge, Kokkos::ALL);
+ Array2DI4 CellIndexSorted =
+ Kokkos::subview(CellIndxSorted, IEdge, Kokkos::ALL, Kokkos::ALL);
+ for (unsigned I = 0; I < CellIndexSorted.extent(0); ++I)
+ for (unsigned K = 0; K < CellIndexSorted.extent(1); ++K)
+ CellIndexSorted(I, K) = MaxI4;
+ const int Cell1 = CellsOnEdge(IEdge, 0);
+ const int Cell2 = CellsOnEdge(IEdge, 1);
+ // at boundaries, must stay at low order
+ AdvMaskHighOrder(IEdge) = 1;
+ for (int K = 0; K < NEdgesOnCell(Cell1); ++K)
+ if (CellsOnCell(Cell1, K) == NCellsGlobal)
+ AdvMaskHighOrder(IEdge) = 0;
+
+ for (int K = 0; K < NEdgesOnCell(Cell2); ++K)
+ if (CellsOnCell(Cell2, K) == NCellsGlobal)
+ AdvMaskHighOrder(IEdge) = 0;
+ // do only if this edge flux is needed to update owned cells
+ if (Cell1 < NCellsAll && Cell2 < NCellsAll) {
+ // Insert cellsOnEdge to list of advection cells
+ // insert_into_list(PatchCellList,Cell1);
+ // insert_into_list(PatchCellList,Cell2);
+ CellIndex(0) = Cell1;
+ CellIndex(1) = Cell2;
+ CellIndexSorted(0, 0) = CellID(Cell1);
+ CellIndexSorted(1, 0) = Cell1;
+ CellIndexSorted(0, 1) = CellID(Cell2);
+ CellIndexSorted(1, 1) = Cell2;
+ int N = 2;
+ // Build unique list of cells used for advection on edge
+ // by expanding to the extended neighbor cells
+ for (int I = 0; I < NEdgesOnCell(Cell1); ++I) {
+ const I4 CellOnCell = CellsOnCell(Cell1, I);
+ if (!found_in_list(CellIndex, N, CellOnCell)) {
+ CellIndex(N) = CellOnCell;
+ CellIndexSorted(0, N) = CellID(CellOnCell);
+ CellIndexSorted(1, N) = CellOnCell;
+ ++N;
+ }
+ }
+ for (int I = 0; I < NEdgesOnCell(Cell2); ++I) {
+ const I4 CellOnCell = CellsOnCell(Cell2, I);
+ if (!found_in_list(CellIndex, N, CellOnCell)) {
+ CellIndex(N) = CellOnCell;
+ CellIndexSorted(0, N) = CellID(CellOnCell);
+ CellIndexSorted(1, N) = CellOnCell;
+ ++N;
+ }
+ }
+ // sort the cell indices by cellID
+ sort_by_key(CellIndexSorted, 0, CellIndexSorted.extent(1) - 1);
+
+ Array1DI4 keys = Kokkos::subview(CellIndexSorted, 0, Kokkos::ALL);
+ Array1DI4 values = Kokkos::subview(CellIndexSorted, 1, Kokkos::ALL);
+ // store local cell indices for high-order calculations
+ NAdvCellsForEdge(IEdge) = N;
+ for (int ICell = 0; ICell < N; ++ICell)
+ AdvCellsForEdge(IEdge, ICell) = values(ICell);
+ // equation 7 in Skamarock, W. C., & Gassmann, A. (2011):
+ // F(u,psi)_{i+1/2} = u_{i+1/2} *
+ // [ 1/2 (psi_{i+1} + psi_i) term 1
+ // - 1/12(dx^2psi_{i+1} + dx^2psi_i) term 2
+ // + sign(u) beta/12 (dx^2psi_{i+1} - dx^2psi_i)] term 3
+ // (note minus sign)
+ //
+ // advCoefs accounts for terms 1 and 2 in SG11 equation 7.
+ // Term 1 is the 2nd-order flux-function term. advCoefs
+ // accounts for this with the "+ 0.5" lines below. In the
+ // advection routines that use these coefficients, the
+ // 2nd-order flux loop is then skipped. Term 2 is the
+ // 4th-order flux-function term. advCoefs accounts for
+ // term 3, the beta term. beta > 0 corresponds to the
+ // third-order flux function. The - sign in the derivTwo
+ // accumulation is for the i+1 part of term 3, while
+ // the + sign is for the i part.
+
+ for (int I = 0; I < NAdvCellsMax; ++I) {
+ AdvCoefs(I, IEdge) = 0._Real;
+ AdvCoefs3rd(I, IEdge) = 0._Real;
+ }
+ // pull together third and fourth order contributions to the flux first
+ // from cell1
+ Array1DI4 keys_edge = Kokkos::subview(
+ keys, Kokkos::make_pair(0, NAdvCellsForEdge(IEdge)));
+ if (const I4 I = search(keys_edge, CellID(Cell1)); -1 != I) {
+ AdvCoefs(I, IEdge) += DerivTwo(0, 0, IEdge);
+ AdvCoefs3rd(I, IEdge) += DerivTwo(0, 0, IEdge);
+ }
+ for (int ICell = 0; ICell < NEdgesOnCell(Cell1); ++ICell) {
+ if (const I4 I =
+ search(keys_edge, CellID(CellsOnCell(Cell1, ICell)));
+ -1 != I) {
+ AdvCoefs(I, IEdge) += DerivTwo(ICell + 1, 0, IEdge);
+ AdvCoefs3rd(I, IEdge) += DerivTwo(ICell + 1, 0, IEdge);
+ }
+ }
+ // pull together third and fourth order contributions to the flux first
+ // from cell2
+ if (const I4 I = search(keys_edge, CellID(Cell2)); -1 != I) {
+ AdvCoefs(I, IEdge) += DerivTwo(0, 1, IEdge);
+ AdvCoefs3rd(I, IEdge) -= DerivTwo(0, 1, IEdge);
+ }
+ for (int ICell = 0; ICell < NEdgesOnCell(Cell2); ++ICell) {
+ if (const I4 I =
+ search(keys_edge, CellID(CellsOnCell(Cell2, ICell)));
+ -1 != I) {
+ AdvCoefs(I, IEdge) += DerivTwo(ICell + 1, 1, IEdge);
+ AdvCoefs3rd(I, IEdge) -= DerivTwo(ICell + 1, 1, IEdge);
+ }
+ }
+ for (int ICell = 0; ICell < NAdvCellsForEdge(IEdge); ++ICell) {
+ AdvCoefs(ICell, IEdge) *= -DcEdge(IEdge) * DcEdge(IEdge) / 12._Real;
+ AdvCoefs3rd(ICell, IEdge) *=
+ -DcEdge(IEdge) * DcEdge(IEdge) / 12._Real;
+ }
+ // 2nd order centered contribution place this in the main flux weights
+ if (const I4 I = search(keys_edge, CellID(Cell1)); -1 != I) {
+ AdvCoefs(I, IEdge) += 0.5_Real;
+ }
+ if (const I4 I = search(keys_edge, CellID(Cell2)); -1 != I) {
+ AdvCoefs(I, IEdge) += 0.5_Real;
+ }
+ // multiply by edge length - thus the flux is just dt*ru times the
+ // results of the vector-vector multiply
+ for (int ICell = 0; ICell < NAdvCellsForEdge(IEdge); ++ICell) {
+ AdvCoefs(ICell, IEdge) *= DvEdge(IEdge);
+ AdvCoefs3rd(ICell, IEdge) *= DvEdge(IEdge);
+ }
+ }
+ }
+
+ private:
+ const I4 MaxI4 = std::numeric_limits::max();
+ const I4 NCellsGlobal;
+ const I4 NCellsAll;
+ const I4 NAdvCellsMax;
+ Array2DI4 PatchCellLists;
+ Array1DI4 NAdvCellsForEdge;
+ Array2DI4 AdvCellsForEdge;
+ Array1DI4 NEdgesOnEdge;
+ Array1DI4 NEdgesOnCell;
+ Array2DI4 CellIndx;
+ Array3DI4 CellIndxSorted;
+ Array1DI4 CellID;
+ Array1DI4 AdvMaskHighOrder;
+ Array2DI4 EdgesOnEdge;
+ Array2DI4 CellsOnCell;
+ Array2DI4 CellsOnEdge;
+ Array1DReal DcEdge;
+ Array1DReal DvEdge;
+ Array2DReal AdvCoefs;
+ Array2DReal AdvCoefs3rd;
+ Array3DReal DerivTwo;
+};
} // namespace OMEGA
#endif
diff --git a/components/omega/src/ocn/HorzUtil.h b/components/omega/src/ocn/HorzUtil.h
new file mode 100644
index 000000000000..cc27927f8834
--- /dev/null
+++ b/components/omega/src/ocn/HorzUtil.h
@@ -0,0 +1,197 @@
+#ifndef OMEGA_HORZUTIL_H
+#define OMEGA_HORZUTIL_H
+
+#include "DataTypes.h"
+#include "Logging.h"
+namespace OMEGA {
+
+KOKKOS_INLINE_FUNCTION Real distance(const Real x, const Real y, const Real z) {
+ const Real dist = Kokkos::sqrt(x * x + y * y + z * z);
+ return dist;
+}
+
+KOKKOS_INLINE_FUNCTION Real sphere_angle(const Real ax, const Real ay,
+ const Real az, const Real bx,
+ const Real by, const Real bz,
+ const Real cx, const Real cy,
+ const Real cz) {
+ // Computes the angle between arcs AB and AC, given points A, B, and C
+ // Equation numbers w.r.t.
+ // http://mathworld.wolfram.com/SphericalTrigonometry.html
+ const Real one = 1._Real;
+ const Real neg_one = -1._Real;
+ const auto a =
+ Kokkos::acos(Kokkos::max(Kokkos::min(bx * cx + by * cy + bz * cz, one),
+ neg_one)); // Eqn. (3)
+ const auto b =
+ Kokkos::acos(Kokkos::max(Kokkos::min(ax * cx + ay * cy + az * cz, one),
+ neg_one)); // Eqn. (2)
+ const auto c =
+ Kokkos::acos(Kokkos::max(Kokkos::min(ax * bx + ay * by + az * bz, one),
+ neg_one)); // Eqn. (1)
+ const auto ABx = bx - ax;
+ const auto ABy = by - ay;
+ const auto ABz = bz - az;
+
+ const auto ACx = cx - ax;
+ const auto ACy = cy - ay;
+ const auto ACz = cz - az;
+
+ const auto Dx = (ABy * ACz) - (ABz * ACy);
+ const auto Dy = -((ABx * ACz) - (ABz * ACx));
+ const auto Dz = (ABx * ACy) - (ABy * ACx);
+
+ const auto s = 0.5_Real * (a + b + c);
+ const auto sin_angle = Kokkos::sqrt(Kokkos::min(
+ one, Kokkos::max(0._Real,
+ (Kokkos::sin(s - b) * Kokkos::sin(s - c)) /
+ (Kokkos::sin(b) * Kokkos::sin(c))))); // Eqn. (28)
+ Real sa = 2._Real *
+ Kokkos::asin(Kokkos::max(Kokkos::min(sin_angle, one), neg_one));
+ if ((Dx * ax + Dy * ay + Dz * az) < 0.0)
+ sa *= neg_one;
+ return sa;
+}
+KOKKOS_INLINE_FUNCTION Real arc_length(const Real ax, const Real ay,
+ const Real az, const Real bx,
+ const Real by, const Real bz) {
+ // Returns the length of the great circle arc from A=(ax, ay, az) to
+ // B=(bx, by, bz). It is assumed that both A and B lie on the surface of the
+ // same sphere centered at the origin.
+ const auto cx = bx - ax;
+ const auto cy = by - ay;
+ const auto cz = bz - az;
+ const auto r = Kokkos::sqrt(ax * ax + ay * ay + az * az);
+ const auto c = Kokkos::sqrt(cx * cx + cy * cy + cz * cz);
+ const auto al = r * 2.0 * Kokkos::asin(c / (2.0 * r));
+ return al;
+}
+template
+static KOKKOS_FUNCTION void mat_t_mul(Real AtA[NA][NA], const Real A[][NA],
+ const I4 Rows) {
+ // Compute AtA = A^t * A for the first N rows of A
+ const I4 Cols = NA;
+ for (int I = 0; I < Cols; ++I)
+ for (int J = 0; J < Cols; ++J)
+ for (int K = 0; K < Rows; ++K)
+ AtA[I][J] += A[K][I] * A[K][J]; // = A^t(I,K) * A(K,j)
+}
+template
+KOKKOS_INLINE_FUNCTION void elgs(Real A[NA][NA], I4 Indx[NA]) {
+ // subroutine to perform the partial-pivoting gaussian elimination.
+ // a(n,n) is the original matrix in the input and transformed matrix
+ // plus the pivoting element ratios below the diagonal in the output.
+ // indx(n) records the pivoting order. tao pang 2001.
+ //
+ // A is a square matrix NA by NA but only the first N rows,columns are
+ // pivoted.
+ const I4 N = NA;
+ for (int I = 0; I < N; ++I)
+ Indx[I] = I;
+ // find the rescaling factors, one from each row
+ Real C[NA] = {};
+ for (int I = 0; I < N; ++I)
+ for (int J = 0; J < N; ++J)
+ C[I] = Kokkos::max(C[I], Kokkos::abs(A[I][J]));
+
+ // search the pivoting (largest) element from each column
+ for (int J = 0; J < N - 1; ++J) {
+ Real pi1 = 0._Real;
+ I4 K = 0;
+ for (int I = J; I < N; ++I) {
+ const Real pi = Kokkos::abs(A[Indx[I]][J]) / C[Indx[I]];
+ if (pi1 < pi) {
+ pi1 = pi;
+ K = I;
+ }
+ }
+ // interchange the rows via indx(n) to record pivoting order
+ const I4 ITmp = Indx[J];
+ Indx[J] = Indx[K];
+ Indx[K] = ITmp;
+ for (int I = J + 1; I < N; ++I) {
+ const Real pj = A[Indx[I]][J] / A[Indx[J]][J];
+ // record pivoting ratios below the diagonal
+ A[Indx[I]][J] = pj;
+ // modify other elements accordingly
+ for (int K = J + 1; K < N; ++K)
+ A[Indx[I]][K] -= pj * A[Indx[J]][K];
+ }
+ }
+}
+template
+KOKKOS_INLINE_FUNCTION void migs(Real A[NA][NA], Real X[NA][NA]) {
+ // subroutine to invert matrix a(n,n) with the inverse stored
+ // in x(n,n) in the output. tao pang 2001.
+ //
+ // Matrix sizes are NA by NA but only the first N rowx & columns are
+ // inverted.
+ I4 Indx[NA] = {};
+ Real B[NA][NA] = {};
+ for (int I = 0; I < NA; ++I)
+ B[I][I] = 1._Real;
+ elgs(A, Indx);
+ for (int I = 0; I < NA - 1; ++I)
+ for (int J = I + 1; J < NA; ++J)
+ for (int K = 0; K < NA; ++K)
+ B[Indx[J]][K] -= A[Indx[J]][I] * B[Indx[I]][K];
+ for (int I = 0; I < NA; ++I) {
+ X[NA - 1][I] = B[Indx[NA - 1]][I] / A[Indx[NA - 1]][NA - 1];
+ for (int J = NA - 2; 0 <= J; --J) {
+ X[J][I] = B[Indx[J]][I];
+ for (int K = J + 1; K < NA; ++K)
+ X[J][I] -= A[Indx[J]][K] * X[K][I];
+ X[J][I] /= A[Indx[J]][J];
+ }
+ }
+}
+template
+KOKKOS_INLINE_FUNCTION void mat_mul_t(Array2DReal C, const Real B[NA][NA],
+ const Real A[][NA], const I4 Rows) {
+ // Compute C = B * A^t for first N columns of C
+ // C assumed to be initialized to 0.
+ // The number of rows in C is inferred from the number of column in A and B.
+ const I4 Cols = NA;
+ for (int I = 0; I < Cols; ++I)
+ for (int J = 0; J < Rows; ++J)
+ C(I, J) = 0;
+ for (int I = 0; I < Cols; ++I)
+ for (int J = 0; J < Rows; ++J)
+ for (int K = 0; K < Cols; ++K)
+ C(I, J) += B[I][K] * A[J][K]; // Transpose A
+}
+template
+KOKKOS_INLINE_FUNCTION void poly_fit_2(const Real A[][NA], Array2DReal B,
+ const I4 Rows) {
+ Real AtA[NA][NA] = {};
+ Real AtAInv[NA][NA] = {};
+ mat_t_mul(AtA, A, Rows);
+ migs(AtA, AtAInv);
+ mat_mul_t(B, AtAInv, A, Rows);
+}
+
+KOKKOS_INLINE_FUNCTION void arc_bisect(const Real ax, const Real ay,
+ const Real az, const Real bx,
+ const Real by, const Real bz, Real &cx,
+ Real &cy, Real &cz) {
+
+ // Returns the point C=(cx, cy, cz) that bisects the great circle arc from
+ // A=(ax, ay, az) to B=(bx, by, bz). It is assumed that A and B lie on the
+ // surface of a sphere centered at the origin.
+
+ cx = 0.5_Real * (ax + bx);
+ cy = 0.5_Real * (ay + by);
+ cz = 0.5_Real * (az + bz);
+ if (cx == 0. && cy == 0. && cz == 0.) {
+ printf("arc_bisect: A and B are diametrically opposite");
+ } else {
+ const Real d = Kokkos::sqrt(cx * cx + cy * cy + cz * cz);
+ const Real r = Kokkos::sqrt(ax * ax + ay * ay + az * az);
+ cx = r * cx / d;
+ cy = r * cy / d;
+ cz = r * cz / d;
+ }
+}
+
+} // namespace OMEGA
+#endif
diff --git a/components/omega/src/ocn/OceanFinal.cpp b/components/omega/src/ocn/OceanFinal.cpp
index bcadc327a324..a518db52a175 100644
--- a/components/omega/src/ocn/OceanFinal.cpp
+++ b/components/omega/src/ocn/OceanFinal.cpp
@@ -7,6 +7,7 @@
#include "AuxiliaryState.h"
#include "Decomp.h"
+#include "Eos.h"
#include "Field.h"
#include "Halo.h"
#include "HorzMesh.h"
@@ -14,6 +15,7 @@
#include "MachEnv.h"
#include "OceanDriver.h"
#include "OceanState.h"
+#include "PGrad.h"
#include "Tendencies.h"
#include "TimeMgr.h"
#include "TimeStepper.h"
@@ -33,9 +35,12 @@ int ocnFinalize(const TimeInstant &CurrTime ///< [in] current sim time
// clean up all objects
Tracers::clear();
TimeStepper::clear();
+ PressureGrad::clear();
+ Eos::destroyInstance();
Tendencies::clear();
AuxiliaryState::clear();
OceanState::clear();
+ VertAdv::clear();
VertCoord::clear();
Dimension::clear();
Field::clear();
diff --git a/components/omega/src/ocn/OceanInit.cpp b/components/omega/src/ocn/OceanInit.cpp
index 884a9cc51d7b..a22e3b3b0667 100644
--- a/components/omega/src/ocn/OceanInit.cpp
+++ b/components/omega/src/ocn/OceanInit.cpp
@@ -11,6 +11,7 @@
#include "Config.h"
#include "DataTypes.h"
#include "Decomp.h"
+#include "Eos.h"
#include "Error.h"
#include "Field.h"
#include "Halo.h"
@@ -21,11 +22,13 @@
#include "MachEnv.h"
#include "OceanDriver.h"
#include "OceanState.h"
+#include "PGrad.h"
#include "Pacer.h"
#include "Tendencies.h"
#include "TimeMgr.h"
#include "TimeStepper.h"
#include "Tracers.h"
+#include "VertAdv.h"
#include "VertCoord.h"
#include "mpi.h"
@@ -131,7 +134,10 @@ int initOmegaModules(MPI_Comm Comm) {
HorzMesh::init();
VertCoord::init();
Tracers::init();
+ VertAdv::init();
AuxiliaryState::init();
+ PressureGrad::init();
+ Eos::init();
Tendencies::init();
TimeStepper::init2();
@@ -193,10 +199,7 @@ int initOmegaModules(MPI_Comm Comm) {
if (Err != 0) {
ABORT_ERROR("Error updating tracer halo after restart");
}
- Err = Tracers::copyToHost(CurTimeLevel);
- if (Err != 0) {
- ABORT_ERROR("Error updating tracer device arrays after restart");
- }
+ Tracers::copyToHost(CurTimeLevel);
return Err;
diff --git a/components/omega/src/ocn/OceanState.cpp b/components/omega/src/ocn/OceanState.cpp
index fea924e84ca4..4d3e7ce8919a 100644
--- a/components/omega/src/ocn/OceanState.cpp
+++ b/components/omega/src/ocn/OceanState.cpp
@@ -10,6 +10,7 @@
#include "OceanState.h"
#include "DataTypes.h"
#include "Decomp.h"
+#include "Error.h"
#include "Field.h"
#include "Halo.h"
#include "Logging.h"
@@ -43,6 +44,9 @@ int OceanState::init() {
LOG_ERROR("TimeStepper needs to be initialized before OceanState");
}
int NTimeLevels = DefTimeStepper->getNTimeLevels();
+ LOG_INFO("OceanState: Initializing default state with {} vertical layers "
+ "and {} time levels",
+ NVertLayers, NTimeLevels);
if (NTimeLevels < 2) {
LOG_ERROR("OceanState: the number of time level is lower than 2");
@@ -234,8 +238,7 @@ void OceanState::defineFields() {
FieldGroup::addFieldToGroup(LayerThicknessFldName, "Restart");
// Associate Field with data
- I4 TimeIndex;
- int Err = getTimeIndex(TimeIndex, 0);
+ const I4 TimeIndex = getTimeIndex(0);
NormalVelocityField->attachData(NormalVelocity[TimeIndex]);
LayerThicknessField->attachData(LayerThickness[TimeIndex]);
@@ -244,103 +247,64 @@ void OceanState::defineFields() {
//------------------------------------------------------------------------------
// Get layer thickness device array
-I4 OceanState::getLayerThickness(Array2DReal &LayerThick,
- const I4 TimeLevel) const {
- I4 Err = 0;
- I4 TimeIndex;
-
- Err = getTimeIndex(TimeIndex, TimeLevel);
- LayerThick = LayerThickness[TimeIndex];
-
- return Err;
+Array2DReal OceanState::getLayerThickness(const I4 TimeLevel) const {
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return LayerThickness[TimeIndex];
}
//------------------------------------------------------------------------------
// Get layer thickness host array
-I4 OceanState::getLayerThicknessH(HostArray2DReal &LayerThickH,
- const I4 TimeLevel) const {
- I4 Err = 0;
- I4 TimeIndex;
-
- Err = getTimeIndex(TimeIndex, TimeLevel);
- LayerThickH = LayerThicknessH[TimeIndex];
-
- return Err;
+HostArray2DReal OceanState::getLayerThicknessH(const I4 TimeLevel) const {
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return LayerThicknessH[TimeIndex];
}
//------------------------------------------------------------------------------
// Get normal velocity device array
-I4 OceanState::getNormalVelocity(Array2DReal &NormVel,
- const I4 TimeLevel) const {
-
- I4 Err = 0;
- I4 TimeIndex;
-
- Err = getTimeIndex(TimeIndex, TimeLevel);
- NormVel = NormalVelocity[TimeIndex];
-
- return Err;
+Array2DReal OceanState::getNormalVelocity(const I4 TimeLevel) const {
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return NormalVelocity[TimeIndex];
}
//------------------------------------------------------------------------------
// Get normal velocity host array
-I4 OceanState::getNormalVelocityH(HostArray2DReal &NormVelH,
- const I4 TimeLevel) const {
- I4 Err = 0;
- I4 TimeIndex;
-
- Err = getTimeIndex(TimeIndex, TimeLevel);
- NormVelH = NormalVelocityH[TimeIndex];
-
- return Err;
+HostArray2DReal OceanState::getNormalVelocityH(const I4 TimeLevel) const {
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return NormalVelocityH[TimeIndex];
}
//------------------------------------------------------------------------------
// Perform copy to device for state variables
// TimeLevel == [1:new, 0:current, -1:previous, -2:two times ago, ...]
-I4 OceanState::copyToDevice(const I4 TimeLevel) {
-
- I4 Err;
- I4 TimeIndex;
+void OceanState::copyToDevice(const I4 TimeLevel) {
- Err = getTimeIndex(TimeIndex, TimeLevel);
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
deepCopy(LayerThickness[TimeIndex], LayerThicknessH[TimeIndex]);
deepCopy(NormalVelocity[TimeIndex], NormalVelocityH[TimeIndex]);
-
- return Err;
} // end copyToDevice
//------------------------------------------------------------------------------
// Perform copy to host for state variables
// TimeLevel == [1: new, 0:current, -1:previous, -2:two times ago, ...]
-I4 OceanState::copyToHost(const I4 TimeLevel) {
-
- I4 Err;
- I4 TimeIndex;
+void OceanState::copyToHost(const I4 TimeLevel) {
- Err = getTimeIndex(TimeIndex, TimeLevel);
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
deepCopy(LayerThicknessH[TimeIndex], LayerThickness[TimeIndex]);
deepCopy(NormalVelocityH[TimeIndex], NormalVelocity[TimeIndex]);
-
- return Err;
} // end copyToHost
//------------------------------------------------------------------------------
// Perform state halo exchange
// TimeLevel == [1:new, 0:current, -1:previous, -2:two times ago, ...]
-I4 OceanState::exchangeHalo(const I4 TimeLevel) {
+void OceanState::exchangeHalo(const I4 TimeLevel) {
- I4 Err;
- I4 TimeIndex;
- Err = getTimeIndex(TimeIndex, TimeLevel);
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
MeshHalo->exchangeFullArrayHalo(LayerThickness[TimeIndex], OnCell);
MeshHalo->exchangeFullArrayHalo(NormalVelocity[TimeIndex], OnEdge);
- return Err;
-
} // end exchangeHalo
//------------------------------------------------------------------------------
@@ -391,19 +355,14 @@ OceanState *OceanState::get(const std::string Name ///< [in] Name of state
//------------------------------------------------------------------------------
// Get time index from time level
// TimeLevel == [1:new, 0:current, -1:previous, -2:two times ago, ...]
-I4 OceanState::getTimeIndex(I4 &TimeIndex, const I4 TimeLevel) const {
-
- // Check if time level is valid
- if (NTimeLevels > 1 && (TimeLevel > 1 || (TimeLevel + NTimeLevels) <= 1)) {
- LOG_ERROR("OceanState: Time level {} is out of range for NTimeLevels {}",
- TimeLevel, NTimeLevels);
- return -1;
- }
-
- TimeIndex = (TimeLevel + CurTimeIndex + NTimeLevels) % NTimeLevels;
+I4 OceanState::getTimeIndex(const I4 TimeLevel) const {
- return 0;
+ OMEGA_REQUIRE(NTimeLevels <= 1 ||
+ !(TimeLevel > 1 || (TimeLevel + NTimeLevels) <= 1),
+ "OceanState: Time level {} is out of range for NTimeLevels {}",
+ TimeLevel, NTimeLevels);
+ return (TimeLevel + CurTimeIndex + NTimeLevels) % NTimeLevels;
} // end get time index
} // end namespace OMEGA
diff --git a/components/omega/src/ocn/OceanState.h b/components/omega/src/ocn/OceanState.h
index 27a398883667..b8e9316cd16a 100644
--- a/components/omega/src/ocn/OceanState.h
+++ b/components/omega/src/ocn/OceanState.h
@@ -54,7 +54,7 @@ class OceanState {
I4 CurTimeIndex; ///< Time dimension array index for current level
/// Get the current time level index associated with a time level
- I4 getTimeIndex(I4 &TimeIndex, const I4 TimeLevel) const;
+ I4 getTimeIndex(const I4 TimeLevel) const;
public:
// Variables
@@ -110,29 +110,28 @@ class OceanState {
);
/// Get layer thickness device array at given time level
- I4 getLayerThickness(Array2DReal &LayerThick, const I4 TimeLevel) const;
+ Array2DReal getLayerThickness(const I4 TimeLevel) const;
/// Get layer thickness host array at given time level
- I4 getLayerThicknessH(HostArray2DReal &LayerThickH,
- const I4 TimeLevel) const;
+ HostArray2DReal getLayerThicknessH(const I4 TimeLevel) const;
/// Get normal velocity device array at given time level
- I4 getNormalVelocity(Array2DReal &NormVel, const I4 TimeLevel) const;
+ Array2DReal getNormalVelocity(const I4 TimeLevel) const;
/// Get normal velocity host array at given time level
- I4 getNormalVelocityH(HostArray2DReal &NormVelH, const I4 TimeLevel) const;
+ HostArray2DReal getNormalVelocityH(const I4 TimeLevel) const;
/// Exchange halo
- I4 exchangeHalo(const I4 TimeLevel);
+ void exchangeHalo(const I4 TimeLevel);
/// Swap time levels to update state arrays
void updateTimeLevels();
/// Copy state variables from host to device
- I4 copyToDevice(const I4 TimeLevel);
+ void copyToDevice(const I4 TimeLevel);
/// Copy state variables from device to host
- I4 copyToHost(const I4 TimeLevel);
+ void copyToHost(const I4 TimeLevel);
/// Destructor - deallocates all memory and deletes an OceanState
~OceanState();
diff --git a/components/omega/src/ocn/PGrad.cpp b/components/omega/src/ocn/PGrad.cpp
new file mode 100644
index 000000000000..33f3320c7606
--- /dev/null
+++ b/components/omega/src/ocn/PGrad.cpp
@@ -0,0 +1,237 @@
+//===-- ocn/PGrad.cpp - Pressure Gradient Term -----------------*- C++ -*-===//
+//
+// Implements the PGrad manager and two discretizations: Centered and
+// HighOrder.
+//
+//===----------------------------------------------------------------------===//
+
+#include "PGrad.h"
+#include "Eos.h"
+#include "Error.h"
+#include "Field.h"
+#include "HorzMesh.h"
+#include "OmegaKokkos.h"
+#include "VertCoord.h"
+
+namespace OMEGA {
+
+PressureGrad *PressureGrad::DefaultPGrad = nullptr;
+std::map> PressureGrad::AllPGrads;
+
+//------------------------------------------------------------------------------
+// Initialize the PressureGrad. Assumes that HorzMesh and VertCoord have already
+// been initialized.
+void PressureGrad::init() {
+
+ // Retrieve default mesh and vertical coordinate
+ HorzMesh *DefMesh = HorzMesh::getDefault();
+ VertCoord *DefVCoord = VertCoord::getDefault();
+
+ // Retrieve omega config
+ Config *OmegaConfig = Config::getOmegaConfig();
+
+ // Create the default PressureGrad and set pointer to it
+ PressureGrad::DefaultPGrad =
+ PressureGrad::create("Default", DefMesh, DefVCoord, OmegaConfig);
+
+} // end init
+
+//------------------------------------------------------------------------------
+// Create a new PressureGrad object and add it to the map
+PressureGrad *
+PressureGrad::create(const std::string &Name, /// [in] Name for PressureGrad
+ const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ const VertCoord *VCoord, ///< [in] Vertical coordinate
+ Config *Options) { ///< [in] Configuration options
+
+ // Check to see if a PressureGrad of the same name already exists and
+ // if so, exit with an error
+ if (AllPGrads.find(Name) != AllPGrads.end()) {
+ LOG_ERROR("Attempted to create a PressureGrad with name {} but a "
+ "PressureGrad of that name already exists",
+ Name);
+ return nullptr;
+ }
+
+ // create a new PressureGrad on the heap and put it in a map of
+ // unique_ptrs, which will manage its lifetime
+ auto *NewPGrad = new PressureGrad(Mesh, VCoord, Options);
+ AllPGrads.emplace(Name, NewPGrad);
+
+ return NewPGrad;
+
+} // end create
+
+//------------------------------------------------------------------------------
+// Get the default pressure gradient instance
+PressureGrad *PressureGrad::getDefault() {
+
+ return DefaultPGrad;
+
+} // end get default
+
+//------------------------------------------------------------------------------
+// Constructor for PressureGrad
+PressureGrad::PressureGrad(
+ const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ const VertCoord *VCoord, ///< [in] Vertical coordinate
+ Config *Options) ///< [in] Configuration options
+ : MinLayerEdgeBot(VCoord->MinLayerEdgeBot),
+ MaxLayerEdgeTop(VCoord->MaxLayerEdgeTop), CenteredPGrad(Mesh, VCoord),
+ HighOrderPGrad(Mesh, VCoord) {
+
+ // store mesh sizes
+ NEdgesAll = Mesh->NEdgesAll;
+ NEdgesOwned = Mesh->NEdgesOwned;
+ NVertLayers = VCoord->NVertLayers;
+ NVertLayersP1 = NVertLayers + 1;
+
+ // Read config options for PressureGrad type
+ // and enable the appropriate functor
+ Config PGradConfig("PressureGrad");
+ Error Err;
+ Err += Options->get(PGradConfig);
+ CHECK_ERROR_ABORT(Err,
+ "PressureGrad: PressureGrad group not found in Config");
+ std::string PGradTypeStr;
+ Err += PGradConfig.get("PressureGradType", PGradTypeStr);
+
+ if (PGradTypeStr == "centered" || PGradTypeStr == "Centered") {
+ PressureGradChoice = PressureGradType::Centered;
+ this->CenteredPGrad.Enabled = true;
+ } else if (PGradTypeStr == "HighOrder1") {
+ PressureGradChoice = PressureGradType::HighOrder1;
+ this->HighOrderPGrad.Enabled = true;
+ } else {
+ LOG_INFO(
+ "PGrad: Unknown PressureGradType in config, defaulting to centered");
+ }
+
+ // Temporary: initialization of tidal potential and SAL
+ TidalPotential = Array1DReal("TidalPotential", Mesh->NCellsSize);
+ SelfAttractionLoading =
+ Array1DReal("SelfAttractionLoading", Mesh->NCellsSize);
+
+} // end constructor
+
+//------------------------------------------------------------------------------
+// Destructor for PressureGrad
+PressureGrad::~PressureGrad() {
+
+ // No operations needed, Kokkos arrays removed when no longer in scope
+
+} // end destructor
+
+//------------------------------------------------------------------------------
+// Remove PressureGrad instances before exit
+void PressureGrad::clear() {
+
+ AllPGrads.clear();
+
+ DefaultPGrad = nullptr; // prevent dangling pointe'r
+
+} // end clear
+
+//------------------------------------------------------------------------------
+// Remove PressureGrad from list by name
+void PressureGrad::erase(const std::string &Name) {
+
+ AllPGrads.erase(Name);
+
+} // end erase
+
+//------------------------------------------------------------------------------
+// Get pressure gradient instance by name
+PressureGrad *PressureGrad::get(const std::string &Name ///< [in] Name of
+) {
+
+ auto it = AllPGrads.find(Name);
+
+ if (it != AllPGrads.end()) {
+ return it->second.get();
+ } else {
+ LOG_ERROR("PressureGrad::get: Attempt to retrieve non-existent "
+ "PressureGrad:");
+ LOG_ERROR("{} has not been defined or has been removed", Name);
+ return nullptr;
+ }
+
+} // end get pressure gradient
+
+//------------------------------------------------------------------------------
+// Compute pressure gradient tendencies and add into Tend array
+void PressureGrad::computePressureGrad(Array2DReal &Tend,
+ const Array2DReal &PressureMid,
+ const Array2DReal &PressureInterface,
+ const Array2DReal &SpecVol,
+ const Array2DReal &ZInterface,
+ const Array2DReal &LayerThick) const {
+
+ OMEGA_SCOPE(LocCenteredPGrad, CenteredPGrad);
+ OMEGA_SCOPE(LocHighOrderPGrad, HighOrderPGrad);
+ OMEGA_SCOPE(LocMinLayerEdgeBot, MinLayerEdgeBot);
+ OMEGA_SCOPE(LocMaxLayerEdgeTop, MaxLayerEdgeTop);
+ OMEGA_SCOPE(LocTidalPotential, TidalPotential);
+ OMEGA_SCOPE(LocSelfAttractionLoading, SelfAttractionLoading);
+
+ if (PressureGradChoice == PressureGradType::Centered) {
+
+ // computes centered geopotential and pressure gradient tendency
+ parallelForOuter(
+ "pgrad-centered", {NEdgesAll},
+ KOKKOS_LAMBDA(I4 IEdge, const TeamMember &Team) {
+ const int KMin = LocMinLayerEdgeBot(IEdge);
+ const int KMax = LocMaxLayerEdgeTop(IEdge);
+ const int KRange = vertRangeChunked(KMin, KMax);
+
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ LocCenteredPGrad(Tend, IEdge, KChunk, PressureMid,
+ PressureInterface, ZInterface,
+ LocTidalPotential,
+ LocSelfAttractionLoading, SpecVol);
+ });
+ });
+
+ } else {
+
+ // computes high-order geopotential and pressure gradient tendency
+ parallelForOuter(
+ "pgrad-highorder", {NEdgesAll},
+ KOKKOS_LAMBDA(I4 IEdge, const TeamMember &Team) {
+ const int KMin = LocMinLayerEdgeBot(IEdge);
+ const int KMax = LocMaxLayerEdgeTop(IEdge);
+ const int KRange = vertRangeChunked(KMin, KMax);
+
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ LocHighOrderPGrad(Tend, IEdge, KChunk, PressureMid,
+ PressureInterface, ZInterface,
+ LocTidalPotential,
+ LocSelfAttractionLoading, SpecVol);
+ });
+ });
+ }
+} // end compute pressure gradient
+
+//------------------------------------------------------------------------------
+// Constructor for centered pressure gradient functor
+PressureGradCentered::PressureGradCentered(
+ const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ const VertCoord *VCoord ///< [in] Vertical coordinate
+ )
+ : CellsOnEdge(Mesh->CellsOnEdge), DcEdge(Mesh->DcEdge),
+ EdgeMask(VCoord->EdgeMask), MinLayerEdgeBot(VCoord->MinLayerEdgeBot),
+ MaxLayerEdgeTop(VCoord->MaxLayerEdgeTop) {}
+
+//------------------------------------------------------------------------------
+// Constructor for high order pressure gradient functor
+PressureGradHighOrder::PressureGradHighOrder(
+ const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ const VertCoord *VCoord ///< [in] Vertical coordinate
+ )
+ : CellsOnEdge(Mesh->CellsOnEdge), DcEdge(Mesh->DcEdge),
+ EdgeMask(VCoord->EdgeMask), MinLayerEdgeBot(VCoord->MinLayerEdgeBot),
+ MaxLayerEdgeTop(VCoord->MaxLayerEdgeTop) {}
+
+} // namespace OMEGA
diff --git a/components/omega/src/ocn/PGrad.h b/components/omega/src/ocn/PGrad.h
new file mode 100644
index 000000000000..9c64b4b86e82
--- /dev/null
+++ b/components/omega/src/ocn/PGrad.h
@@ -0,0 +1,204 @@
+#ifndef OMEGA_PGRAD_H
+#define OMEGA_PGRAD_H
+//===-- ocn/PGrad.h - Pressure Gradient -----------------*- C++ -*-===//
+///
+/// Implements the PressureGrad class which provides a centered and
+/// high-order pressure gradient option and dispatches computations to
+/// functor objects. This follows the patterns used in Eos.h/Eos.cpp.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Config.h"
+#include "Eos.h"
+#include "GlobalConstants.h"
+#include "HorzMesh.h"
+#include "OceanState.h"
+#include "OmegaKokkos.h"
+#include "VertCoord.h"
+#include
+
+namespace OMEGA {
+
+enum class PressureGradType { Centered, HighOrder1, HighOrder2 };
+
+// Centered pressure gradient functor
+class PressureGradCentered {
+ public:
+ bool Enabled;
+
+ // constructor declaration
+ PressureGradCentered(const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ const VertCoord *VCoord ///< [in] Vertical coordinate
+ );
+
+ // Compute centered pressure gradient contribution for given edge and
+ // vertical chunk. This appends results into the Tend array (in-place).
+ KOKKOS_FUNCTION void operator()(const Array2DReal &Tend, I4 IEdge, I4 KChunk,
+ const Array2DReal &PressureMid,
+ const Array2DReal &PressureInterface,
+ const Array2DReal &ZInterface,
+ const Array1DReal &TidalPotential,
+ const Array1DReal &SelfAttractionLoading,
+ const Array2DReal &SpecVol) const {
+
+ const I4 KStart = chunkStart(KChunk, MinLayerEdgeBot(IEdge));
+ const I4 KLen = chunkLength(KChunk, KStart, MaxLayerEdgeTop(IEdge));
+
+ const I4 ICell0 = CellsOnEdge(IEdge, 0);
+ const I4 ICell1 = CellsOnEdge(IEdge, 1);
+ const Real InvDcEdge = 1.0_Real / DcEdge(IEdge);
+
+ Real GradGeoPot =
+ (TidalPotential(ICell1) - TidalPotential(ICell0)) * InvDcEdge +
+ (SelfAttractionLoading(ICell1) - SelfAttractionLoading(ICell0)) *
+ InvDcEdge;
+
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ Real MontPotCell0K =
+ PressureInterface(ICell0, K) * SpecVol(ICell0, K) +
+ Gravity * ZInterface(ICell0, K);
+ Real MontPotCell1K =
+ PressureInterface(ICell1, K) * SpecVol(ICell1, K) +
+ Gravity * ZInterface(ICell1, K);
+ Real GradMontPotK = (MontPotCell1K - MontPotCell0K) * InvDcEdge;
+
+ Real MontPotCell0Kp1 =
+ PressureInterface(ICell0, K + 1) * SpecVol(ICell0, K) +
+ Gravity * ZInterface(ICell0, K + 1);
+ Real MontPotCell1Kp1 =
+ PressureInterface(ICell1, K + 1) * SpecVol(ICell1, K) +
+ Gravity * ZInterface(ICell1, K + 1);
+ Real GradMontPotKp1 = (MontPotCell1Kp1 - MontPotCell0Kp1) * InvDcEdge;
+ Real GradMontPot = 0.5_Real * (GradMontPotK + GradMontPotKp1);
+
+ Real PGradAlpha =
+ 0.5_Real * (PressureMid(ICell1, K) + PressureMid(ICell0, K)) *
+ (SpecVol(ICell1, K) - SpecVol(ICell0, K)) * InvDcEdge;
+ Tend(IEdge, K) +=
+ EdgeMask(IEdge, K) * (-GradMontPot + PGradAlpha - GradGeoPot);
+ }
+ }
+
+ private:
+ Array2DI4 CellsOnEdge;
+ Array1DReal DcEdge;
+ Array2DReal EdgeMask;
+ Array1DI4 MinLayerEdgeBot;
+ Array1DI4 MaxLayerEdgeTop;
+};
+
+// High-order pressure gradient functor (placeholder)
+class PressureGradHighOrder {
+ public:
+ bool Enabled;
+
+ // constructor declaration
+ PressureGradHighOrder(const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ const VertCoord *VCoord ///< [in] Vertical coordinate
+ );
+
+ KOKKOS_FUNCTION void operator()(const Array2DReal &Tend, I4 IEdge, I4 KChunk,
+ const Array2DReal &PressureMid,
+ const Array2DReal &PressureInterface,
+ const Array2DReal &ZInterface,
+ const Array1DReal &TidalPotential,
+ const Array1DReal &SelfAttractionLoading,
+ const Array2DReal &SpecVol) const {
+
+ // Placeholder: for now, no-op (future high-order implementation)
+ const I4 KStart = chunkStart(KChunk, MinLayerEdgeBot(IEdge));
+ const I4 KLen = chunkLength(KChunk, KStart, MaxLayerEdgeTop(IEdge));
+
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ Tend(IEdge, K) += 0.0_Real;
+ }
+ }
+
+ private:
+ Array2DI4 CellsOnEdge;
+ Array1DReal DcEdge;
+ Array2DReal EdgeMask;
+ Array1DI4 MinLayerEdgeBot;
+ Array1DI4 MaxLayerEdgeTop;
+};
+
+// Pressure gradient manager class
+class PressureGrad {
+ public:
+ // Flag to indicate if pressure gradient term is enabled
+ bool Enabled;
+
+ // Initialize the default instance
+ static void init();
+
+ // Create a new pressure gradient object and add to map
+ static PressureGrad *create(const std::string &Name, const HorzMesh *Mesh,
+ const VertCoord *VCoord, Config *Options);
+
+ // Get the default instance
+ static PressureGrad *getDefault();
+
+ // Get instance by name
+ static PressureGrad *
+ get(const std::string &Name ///< [in] Name of PressureGrad
+ );
+
+ // Deallocates arrays and deletes instance
+ static void clear();
+
+ // Remove pressure gradient object by name
+ static void erase(const std::string &Name ///< [in] Name of PressureGrad
+ );
+
+ // Destructor
+ ~PressureGrad();
+
+ // Compute pressure gradient tendencies and add into Tend array
+ void computePressureGrad(Array2DReal &Tend, const Array2DReal &PressureMid,
+ const Array2DReal &PressureInterface,
+ const Array2DReal &SpecVol,
+ const Array2DReal &ZInterface,
+ const Array2DReal &LayerThick) const;
+
+ private:
+ // Construct a new pressure gradient object
+ PressureGrad(const HorzMesh *Mesh, const VertCoord *VCoord, Config *Options);
+
+ // forbid copy and move construction
+ PressureGrad(const PressureGrad &) = delete;
+ PressureGrad(PressureGrad &&) = delete;
+
+ // Pointer to default pressure gradient object
+ static PressureGrad *DefaultPGrad;
+
+ // Mesh-related sizes
+ I4 NEdgesAll = 0;
+ I4 NEdgesOwned = 0;
+ I4 NVertLayers = 0;
+ I4 NVertLayersP1 = 0;
+
+ // Data required for computation (stored copies of VCoord arrays)
+ Array1DI4 MinLayerEdgeBot; ///< min vertical layer on each edge
+ Array1DI4 MaxLayerEdgeTop; ///< max vertical layer on each edge
+
+ // Temporary: to be moveed to tidal forcing module in future
+ Array1DReal TidalPotential; ///< Tidal potential for tidal forcing
+ Array1DReal
+ SelfAttractionLoading; ///< Self attraction and loading for tidal forcing
+
+ // Instances of functors
+ PressureGradCentered CenteredPGrad;
+ PressureGradHighOrder HighOrderPGrad;
+
+ // Choice from config
+ PressureGradType PressureGradChoice = PressureGradType::Centered;
+
+ // Map of all pressure gradient objects by name
+ static std::map> AllPGrads;
+
+}; // end class PressureGrad
+
+} // namespace OMEGA
+#endif
diff --git a/components/omega/src/ocn/Tendencies.cpp b/components/omega/src/ocn/Tendencies.cpp
index 7cf8a7a401a8..23259f36c379 100644
--- a/components/omega/src/ocn/Tendencies.cpp
+++ b/components/omega/src/ocn/Tendencies.cpp
@@ -10,9 +10,16 @@
#include "Tendencies.h"
#include "CustomTendencyTerms.h"
+#include "Eos.h"
#include "Error.h"
+#include "Field.h"
+#include "OceanState.h"
+#include "PGrad.h"
#include "Pacer.h"
+#include "TimeStepper.h"
#include "Tracers.h"
+#include "VertAdv.h"
+#include
namespace OMEGA {
@@ -20,13 +27,17 @@ Tendencies *Tendencies::DefaultTendencies = nullptr;
std::map> Tendencies::AllTendencies;
//------------------------------------------------------------------------------
-// Initialize the tendencies. Assumes that HorzMesh and VertCoord has alread
-// been initialized.
+// Initialize the tendencies. Assumes that HorzMesh, VertCoord, VertAdv, and
+// TimeStepper has already been initialized.
void Tendencies::init() {
Error Err; // error code
- HorzMesh *DefHorzMesh = HorzMesh::getDefault();
- VertCoord *DefVertCoord = VertCoord::getDefault();
+ HorzMesh *DefHorzMesh = HorzMesh::getDefault();
+ VertCoord *DefVertCoord = VertCoord::getDefault();
+ VertAdv *DefVertAdv = VertAdv::getDefault();
+ TimeStepper *DefTimeStepper = TimeStepper::getDefault();
+ Eos *DefEos = Eos::getInstance();
+ PressureGrad *DefPGrad = PressureGrad::getDefault();
I4 NTracers = Tracers::getNumTracers();
@@ -63,13 +74,17 @@ void Tendencies::init() {
} // end if UseCustomTendency
+ TimeInterval TimeStep = DefTimeStepper->getTimeStep();
+
// Ceate default tendencies
- Tendencies::DefaultTendencies =
- create("Default", DefHorzMesh, DefVertCoord, NTracers, &TendConfig,
- CustomThickTend, CustomVelTend);
+ Tendencies::DefaultTendencies = create(
+ "Default", DefHorzMesh, DefVertCoord, DefVertAdv, DefPGrad, DefEos,
+ NTracers, TimeStep, &TendConfig, CustomThickTend, CustomVelTend);
- DefaultTendencies->readTendConfig(&TendConfig);
+ DefaultTendencies->readConfig(OmegaConfig);
+ if (DefaultTendencies->TracerHorzAdv.Enabled)
+ DefaultTendencies->TracerHorzAdv.init();
} // end init
//------------------------------------------------------------------------------
@@ -120,112 +135,195 @@ Tendencies *Tendencies::get(const std::string &Name ///< [in] Name of tendencies
//------------------------------------------------------------------------------
// read and set config options
-void Tendencies::readTendConfig(
- Config *TendConfig ///< [in] Tendencies subconfig
+void Tendencies::readConfig(Config *OmegaConfig ///< [in] Omega config
) {
Error Err; // error code
- Err += TendConfig->get("ThicknessFluxTendencyEnable",
- this->ThicknessFluxDiv.Enabled);
+ Config TendConfig("Tendencies");
+ Err += OmegaConfig->get(TendConfig);
+ CHECK_ERROR_ABORT(Err, "Tendencies: Tendencies group not found in Config");
+
+ Err += TendConfig.get("ThicknessFluxTendencyEnable",
+ this->ThicknessFluxDiv.Enabled);
CHECK_ERROR_ABORT(
Err, "Tendencies: ThicknessFluxTendencyEnable not found in TendConfig");
- Err += TendConfig->get("PVTendencyEnable", this->PotientialVortHAdv.Enabled);
+ Err += TendConfig.get("PVTendencyEnable", this->PotientialVortHAdv.Enabled);
CHECK_ERROR_ABORT(Err,
"Tendencies: PVTendencyEnable not found in TendConfig");
- Err += TendConfig->get("KETendencyEnable", this->KEGrad.Enabled);
+ Err += TendConfig.get("KETendencyEnable", this->KEGrad.Enabled);
CHECK_ERROR_ABORT(Err,
"Tendencies: KETendencyEnable not found in TendConfig");
- Err += TendConfig->get("SSHTendencyEnable", this->SSHGrad.Enabled);
+ Err += TendConfig.get("SSHTendencyEnable", this->SSHGrad.Enabled);
CHECK_ERROR_ABORT(Err,
"Tendencies: SSHTendencyEnable not found in TendConfig");
- Err += TendConfig->get("VelDiffTendencyEnable",
- this->VelocityDiffusion.Enabled);
+ Err +=
+ TendConfig.get("VelDiffTendencyEnable", this->VelocityDiffusion.Enabled);
CHECK_ERROR_ABORT(
Err, "Tendencies: VelDiffTendencyEnable not found in TendConfig");
- Err += TendConfig->get("VelHyperDiffTendencyEnable",
- this->VelocityHyperDiff.Enabled);
+ Err += TendConfig.get("VelHyperDiffTendencyEnable",
+ this->VelocityHyperDiff.Enabled);
CHECK_ERROR_ABORT(
Err, "Tendencies: VelHyperDiffTendencyEnable not found in TendConfig");
if (this->VelocityDiffusion.Enabled) {
- Err += TendConfig->get("ViscDel2", this->VelocityDiffusion.ViscDel2);
+ Err += TendConfig.get("ViscDel2", this->VelocityDiffusion.ViscDel2);
CHECK_ERROR_ABORT(Err, "Tendencies: ViscDel2 not found in TendConfig");
}
if (this->VelocityHyperDiff.Enabled) {
- Err += TendConfig->get("ViscDel4", this->VelocityHyperDiff.ViscDel4);
+ Err += TendConfig.get("ViscDel4", this->VelocityHyperDiff.ViscDel4);
CHECK_ERROR_ABORT(Err, "Tendencies: ViscDel4 not found in TendConfig");
- Err += TendConfig->get("DivFactor", this->VelocityHyperDiff.DivFactor);
+ Err += TendConfig.get("DivFactor", this->VelocityHyperDiff.DivFactor);
CHECK_ERROR_ABORT(Err, "Tendencies: DivFactor not found in TendConfig");
}
-
- Err += TendConfig->get("TracerHorzAdvTendencyEnable",
- this->TracerHorzAdv.Enabled);
+ Err += TendConfig.get("TracerHorzAdvTendencyEnable",
+ this->TracerHorzAdv.Enabled);
CHECK_ERROR_ABORT(
Err, "Tendencies: TracerHorzAdvTendencyEnable not found in TendConfig");
-
- Err += TendConfig->get("TracerDiffTendencyEnable",
- this->TracerDiffusion.Enabled);
+ if (this->TracerHorzAdv.Enabled) {
+ Config AdvectConfig("Advection");
+ Err += OmegaConfig->get(AdvectConfig);
+ CHECK_ERROR_ABORT(Err, "Tendencies: Advection group not in Config");
+
+ I4 Order = 0;
+ Err += AdvectConfig.get("HorzTracerFluxOrder", Order);
+ CHECK_ERROR_ABORT(
+ Err, "Tendencies: HorzTracerFluxOrder not found in AdvectConfig");
+ OMEGA_REQUIRE(Order >= 2 && Order <= 4,
+ "HorzTracerFluxOrder: Only values are 2, 3, 4, found {}",
+ Order);
+
+ if (Order == 2) {
+ this->TracerHorzAdv.ForceLowOrder = true;
+ this->TracerHorzAdv.Coef3rdOrder = 0;
+ }
+ if (Order == 3) {
+ Err += AdvectConfig.get("Coef3rdOrder", TracerHorzAdv.Coef3rdOrder);
+ CHECK_ERROR_ABORT(
+ Err, "Tendencies: Coef3rdOrder not found in AdvectConfig");
+ }
+ if (Order == 4) {
+ this->TracerHorzAdv.Coef3rdOrder = 0;
+ }
+ }
+ Err += TendConfig.get("TracerDiffTendencyEnable",
+ this->TracerDiffusion.Enabled);
CHECK_ERROR_ABORT(
Err, "Tendencies: TracerDiffTendencyEnable not found in TendConfig");
Err +=
- TendConfig->get("WindForcingTendencyEnable", this->WindForcing.Enabled);
+ TendConfig.get("WindForcingTendencyEnable", this->WindForcing.Enabled);
CHECK_ERROR_ABORT(
Err, "Tendencies: WindForcingTendencyEnable not found in TendConfig");
- Err += TendConfig->get("BottomDragTendencyEnable", this->BottomDrag.Enabled);
+ Err += TendConfig.get("BottomDragTendencyEnable", this->BottomDrag.Enabled);
CHECK_ERROR_ABORT(
Err, "Tendencies: BottomDragTendencyEnable not found in TendConfig");
- Err += TendConfig->get("BottomDragCoeff", this->BottomDrag.Coeff);
+ Err += TendConfig.get("BottomDragCoeff", this->BottomDrag.Coeff);
CHECK_ERROR_ABORT(Err,
"Tendencies: BottomDragCoeff not found in TendConfig");
- Err += TendConfig->get("TracerHorzAdvTendencyEnable",
- this->TracerHorzAdv.Enabled);
- CHECK_ERROR_ABORT(
- Err, "Tendencies: TracerHorzAdvTendencyEnable not found in TendConfig");
-
if (this->TracerDiffusion.Enabled) {
- Err += TendConfig->get("EddyDiff2", this->TracerDiffusion.EddyDiff2);
+ Err += TendConfig.get("EddyDiff2", this->TracerDiffusion.EddyDiff2);
CHECK_ERROR_ABORT(Err, "Tendencies: EddyDiff2 not found in TendConfig");
}
- Err += TendConfig->get("TracerHyperDiffTendencyEnable",
- this->TracerHyperDiff.Enabled);
+ Err += TendConfig.get("TracerHyperDiffTendencyEnable",
+ this->TracerHyperDiff.Enabled);
CHECK_ERROR_ABORT(
Err,
"Tendencies: TracerHyperDiffTendencyEnable not found in TendConfig");
if (this->TracerHyperDiff.Enabled) {
- Err += TendConfig->get("EddyDiff4", this->TracerHyperDiff.EddyDiff4);
+ Err += TendConfig.get("EddyDiff4", this->TracerHyperDiff.EddyDiff4);
CHECK_ERROR_ABORT(Err, "Tendencies: EddyDiff4 not found in TendConfig");
}
+
+ Err += TendConfig.get("PressureGradTendencyEnable", this->PGrad->Enabled);
+ CHECK_ERROR_ABORT(
+ Err, "Tendencies: PressureGradTendencyEnable not found in TendConfig");
}
+//------------------------------------------------------------------------------
+// Define fields associated with tendencies
+void Tendencies::defineFields() {
+ std::string LayerThicknessTendFieldName = "LayerThicknessTend";
+ std::string NormalVelocityTendFieldName = "NormalVelocityTend";
+ std::string TracerTendFieldName = "TracerTend";
+ if (Name != "Default") {
+ LayerThicknessTendFieldName.append(Name);
+ NormalVelocityTendFieldName.append(Name);
+ TracerTendFieldName.append(Name);
+ }
+
+ int NDims = 2;
+ std::vector DimNamesThickness(NDims);
+ DimNamesThickness[0] = "NCells";
+ DimNamesThickness[1] = "NVertLayers";
+ auto LayerThicknessTendField =
+ Field::create(LayerThicknessTendFieldName, "Layer thickness tendency",
+ "m/s", "cell_thickness_tendency", -9.99E+10, 9.99E+10,
+ -9.99E+30, NDims, DimNamesThickness);
+ NDims = 3;
+ std::vector DimNamesTracer(NDims);
+ DimNamesTracer[0] = "NTracers";
+ DimNamesTracer[1] = "NCells";
+ DimNamesTracer[2] = "NVertLayers";
+ auto TracerTendField = Field::create(
+ TracerTendFieldName, "Tracer tendency", "kg/m^3/s", "tracer_tendency",
+ -9.99E+10, 9.99E+10, -9.99E+30, NDims, DimNamesTracer);
+ NDims = 2;
+ std::vector DimNamesVelocity(NDims);
+ DimNamesVelocity[0] = "NEdges";
+ DimNamesVelocity[1] = "NVertLayers";
+ auto NormalVelocityTendField =
+ Field::create(NormalVelocityTendFieldName, "Normal velocity tendency",
+ "m/s^2", "sea_water_velocity_tendency", -9.99E+10,
+ 9.99E+10, -9.99E+30, NDims, DimNamesVelocity);
+
+ std::string TendGroupName = "Tendencies";
+ if (Name != "Default") {
+ TendGroupName.append(Name);
+ }
+ auto TendGroup = FieldGroup::create(TendGroupName);
+
+ TendGroup->addField(LayerThicknessTendFieldName);
+ TendGroup->addField(NormalVelocityTendFieldName);
+ TendGroup->addField(TracerTendFieldName);
+
+ LayerThicknessTendField->attachData(LayerThicknessTend);
+ NormalVelocityTendField->attachData(NormalVelocityTend);
+ TracerTendField->attachData(TracerTend);
+
+} // end defineFields
+
//------------------------------------------------------------------------------
// Construct a new group of tendencies
-Tendencies::Tendencies(const std::string &Name, ///< [in] Name for tendencies
- const HorzMesh *Mesh, ///< [in] Horizontal mesh
- const VertCoord *VCoord, ///< [in] Vertical coordinate
- int NTracersIn, ///< [in] Number of tracers
- Config *Options, ///< [in] Configuration options
+Tendencies::Tendencies(const std::string &Name_, ///< [in] Name for tendencies
+ const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ VertCoord *VCoord, ///< [in] Vertical coordinate
+ VertAdv *VAdv, ///< [in] Vertical advection
+ PressureGrad *PGrad, ///< [in] Pressure gradient
+ Eos *EqState, ///< [in] Equation of state
+ int NTracersIn, ///< [in] Number of tracers
+ TimeInterval TimeStepIn, ///< [in] Time step
+ Config *Options, ///< [in] Configuration options
CustomTendencyType InCustomThicknessTend,
CustomTendencyType InCustomVelocityTend)
- : Mesh(Mesh), VCoord(VCoord), ThicknessFluxDiv(Mesh, VCoord),
+ : Mesh(Mesh), VCoord(VCoord), VAdv(VAdv), ThicknessFluxDiv(Mesh, VCoord),
PotientialVortHAdv(Mesh, VCoord), KEGrad(Mesh, VCoord),
SSHGrad(Mesh, VCoord), VelocityDiffusion(Mesh, VCoord),
VelocityHyperDiff(Mesh, VCoord), WindForcing(Mesh, VCoord),
- BottomDrag(Mesh, VCoord), TracerHorzAdv(Mesh, VCoord),
- TracerDiffusion(Mesh, VCoord), TracerHyperDiff(Mesh, VCoord),
+ BottomDrag(Mesh, VCoord), TracerDiffusion(Mesh, VCoord),
+ TracerHyperDiff(Mesh, VCoord), TracerHorzAdv(Mesh, VCoord),
CustomThicknessTend(InCustomThicknessTend),
- CustomVelocityTend(InCustomVelocityTend) {
+ CustomVelocityTend(InCustomVelocityTend), EqState(EqState), PGrad(PGrad) {
// Tendency arrays
LayerThicknessTend =
@@ -235,16 +333,26 @@ Tendencies::Tendencies(const std::string &Name, ///< [in] Name for tendencies
TracerTend = Array3DReal("TracerTend", NTracersIn, Mesh->NCellsSize,
VCoord->NVertLayers);
+ Name = Name_;
+
NTracers = NTracersIn;
+ TimeStep = TimeStepIn;
+
+ defineFields();
} // end constructor
-Tendencies::Tendencies(const std::string &Name, ///< [in] Name for tendencies
- const HorzMesh *Mesh, ///< [in] Horizontal mesh
- const VertCoord *VCoord, ///< [in] Vertical coordinate
- int NTracersIn, ///< [in] Number of tracers
- Config *Options) ///< [in] Configuration options
- : Tendencies(Name, Mesh, VCoord, NTracersIn, Options, CustomTendencyType{},
+Tendencies::Tendencies(const std::string &Name_, ///< [in] Name for tendencies
+ const HorzMesh *Mesh, ///< [in] Horizontal mesh
+ VertCoord *VCoord, ///< [in] Vertical coordinate
+ VertAdv *VAdv, ///< [in] Vertical advection
+ PressureGrad *PGrad, ///< [in] Pressure gradient
+ Eos *EqState, ///< [in] Equation of state
+ int NTracersIn, ///< [in] Number of tracers
+ TimeInterval TimeStepIn, ///< [in] Time step
+ Config *Options) ///< [in] Configuration options
+ : Tendencies(Name_, Mesh, VCoord, VAdv, PGrad, EqState, NTracersIn,
+ TimeStepIn, Options, CustomTendencyType{},
CustomTendencyType{}) {}
//------------------------------------------------------------------------------
@@ -262,8 +370,7 @@ void Tendencies::computeThicknessTendenciesOnly(
OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
- Array2DReal NormalVelEdge;
- State->getNormalVelocity(NormalVelEdge, VelTimeLevel);
+ Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel);
Pacer::start("Tend:computeThicknessTendenciesOnly", 1);
@@ -301,6 +408,11 @@ void Tendencies::computeThicknessTendenciesOnly(
Pacer::stop("Tend:thicknessFluxDiv", 2);
}
+ Pacer::start("Tend:computeThicknessVAdvTend", 2);
+ // Compute thickness tendency from vertical advection
+ VAdv->computeThicknessVAdvTend(LayerThicknessTend);
+ Pacer::stop("Tend:computeThicknessVAdvTend", 2);
+
if (CustomThicknessTend) {
Pacer::start("Tend:customThicknessTend", 2);
CustomThicknessTend(LocLayerThicknessTend, State, AuxState,
@@ -317,8 +429,10 @@ void Tendencies::computeThicknessTendenciesOnly(
void Tendencies::computeVelocityTendenciesOnly(
const OceanState *State, ///< [in] State variables
const AuxiliaryState *AuxState, ///< [in] Auxilary state variables
+ const Array3DReal &TracerArray, ///< [in] Tracer array
int ThickTimeLevel, ///< [in] Time level
int VelTimeLevel, ///< [in] Time level
+ int TracerTimeLevel, ///< [in] Time level
TimeInstant Time ///< [in] Time
) {
@@ -348,15 +462,12 @@ void Tendencies::computeVelocityTendenciesOnly(
});
});
- const Array2DReal &NormalVelEdge = State->NormalVelocity[VelTimeLevel];
-
// Compute potential vorticity horizontal advection
const Array2DReal &FluxLayerThickEdge =
AuxState->LayerThicknessAux.FluxLayerThickEdge;
const Array2DReal &NormRVortEdge = AuxState->VorticityAux.NormRelVortEdge;
const Array2DReal &NormFEdge = AuxState->VorticityAux.NormPlanetVortEdge;
- Array2DReal NormVelEdge;
- State->getNormalVelocity(NormVelEdge, VelTimeLevel);
+ Array2DReal NormVelEdge = State->getNormalVelocity(VelTimeLevel);
if (LocPotientialVortHAdv.Enabled) {
Pacer::start("Tend:potientialVortHAdv", 2);
parallelForOuter(
@@ -448,6 +559,12 @@ void Tendencies::computeVelocityTendenciesOnly(
Pacer::stop("Tend:velocityHyperDiff", 2);
}
+ Pacer::start("Tend:computeVelocityVAdvTend", 2);
+ // Compute velocity tendency from vertical advection
+ VAdv->computeVelocityVAdvTend(NormalVelocityTend, NormVelEdge,
+ FluxLayerThickEdge);
+ Pacer::stop("Tend:computeVelocityVAdvTend", 2);
+
// Compute wind forcing
const auto &NormalStressEdge = AuxState->WindForcingAux.NormalStressEdge;
const auto &MeanLayerThickEdge =
@@ -473,7 +590,7 @@ void Tendencies::computeVelocityTendenciesOnly(
Pacer::start("Tend:bottomDrag", 2);
parallelFor(
{Mesh->NEdgesAll}, KOKKOS_LAMBDA(int IEdge) {
- LocBottomDrag(LocNormalVelocityTend, IEdge, NormalVelEdge, KECell,
+ LocBottomDrag(LocNormalVelocityTend, IEdge, NormVelEdge, KECell,
MeanLayerThickEdge);
});
Pacer::stop("Tend:bottomDrag", 2);
@@ -486,6 +603,37 @@ void Tendencies::computeVelocityTendenciesOnly(
Pacer::stop("Tend:customVelocityTend", 2);
}
+ // Compute pressure gradient
+ if (PGrad->Enabled) {
+
+ // Temporary handling of surface pressure
+ Array1DReal SurfacePressure("SurfacePressure", Mesh->NCellsSize);
+ deepCopy(SurfacePressure, 0.0_Real);
+
+ Pacer::start("Tend:pressureGradTerm", 2);
+ Array2DReal LayerThick = State->getLayerThickness(ThickTimeLevel);
+ VCoord->computePressure(LayerThick, SurfacePressure);
+
+ const auto &PressureMid = VCoord->PressureMid;
+ const auto &PressureInterface = VCoord->PressureInterface;
+ Array2DReal Temp = Kokkos::subview(TracerArray, Tracers::IndxTemp,
+ Kokkos::ALL, Kokkos::ALL);
+ Array2DReal Salinity = Kokkos::subview(TracerArray, Tracers::IndxSalt,
+ Kokkos::ALL, Kokkos::ALL);
+ EqState->computeSpecVol(Temp, Salinity, PressureMid);
+
+ // Temporary: ensure vertical geometric/geopotential fields are updated
+ // for pressure-gradient tendency calculations.
+ const auto &SpecVol = EqState->SpecVol;
+ VCoord->computeZHeight(LayerThick, SpecVol);
+
+ const auto &ZInterface = VCoord->ZInterface;
+ PGrad->computePressureGrad(LocNormalVelocityTend, PressureMid,
+ PressureInterface, SpecVol, ZInterface,
+ LayerThick);
+ Pacer::stop("Tend:pressureGradTerm", 2);
+ }
+
Pacer::stop("Tend:computeVelocityTendenciesOnly", 1);
} // end velocity tendency compute
@@ -504,6 +652,8 @@ void Tendencies::computeTracerTendenciesOnly(
OMEGA_SCOPE(LocTracerHyperDiff, TracerHyperDiff);
OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
+ OMEGA_SCOPE(MinLayerEdgeBot, VCoord->MinLayerEdgeBot);
+ OMEGA_SCOPE(MaxLayerEdgeTop, VCoord->MaxLayerEdgeTop);
Pacer::start("Tend:computeTracerTendenciesOnly", 1);
@@ -521,21 +671,32 @@ void Tendencies::computeTracerTendenciesOnly(
});
// compute tracer horizotal advection
- const Array2DReal &NormalVelEdge = State->NormalVelocity[VelTimeLevel];
- const Array3DReal &HTracersEdge = AuxState->TracerAux.HTracersEdge;
+ Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel);
+ const Array2DReal &FluxLayerThickEdge =
+ AuxState->LayerThicknessAux.FluxLayerThickEdge;
if (LocTracerHorzAdv.Enabled) {
Pacer::start("Tend:tracerHorzAdv", 2);
+ parallelForOuter(
+ {NTracers, Mesh->NEdgesAll},
+ KOKKOS_LAMBDA(int L, int IEdge, const TeamMember &Team) {
+ const int KMin = MinLayerEdgeBot(IEdge);
+ const int KMax = MaxLayerEdgeTop(IEdge);
+ const int KRange = vertRangeChunked(KMin, KMax);
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ LocTracerHorzAdv(L, IEdge, KChunk, TracerArray,
+ FluxLayerThickEdge, NormalVelEdge);
+ });
+ });
parallelForOuter(
{NTracers, Mesh->NCellsAll},
KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) {
const int KMin = MinLayerCell(ICell);
const int KMax = MaxLayerCell(ICell);
const int KRange = vertRangeChunked(KMin, KMax);
-
parallelForInner(
Team, KRange, INNER_LAMBDA(int KChunk) {
- LocTracerHorzAdv(LocTracerTend, L, ICell, KChunk,
- NormalVelEdge, HTracersEdge);
+ LocTracerHorzAdv(LocTracerTend, L, ICell, KChunk);
});
});
Pacer::stop("Tend:tracerHorzAdv", 2);
@@ -582,6 +743,18 @@ void Tendencies::computeTracerTendenciesOnly(
Pacer::stop("Tend:tracerHyperDiff", 2);
}
+ Pacer::start("Tend:computeTracerVAdvTend", 2);
+ // compute tracer tendencies from vertical advection
+ Array2DReal ThicknessForVAdv;
+ if (VAdv->VertAdvChoice == VertAdvOption::Standard) {
+ ThicknessForVAdv = State->getLayerThickness(ThickTimeLevel);
+ } else if (VAdv->VertAdvChoice == VertAdvOption::FCT) {
+ ThicknessForVAdv = AuxState->LayerThicknessAux.ProvThickness;
+ }
+ VAdv->computeTracerVAdvTend(LocTracerTend, TracerArray, ThicknessForVAdv,
+ TimeStep);
+ Pacer::stop("Tend:computeTracerVAdvTend", 2);
+
Pacer::stop("Tend:computeTracerTendenciesOnly", 1);
} // end tracer tendency compute
@@ -593,10 +766,8 @@ void Tendencies::computeThicknessTendencies(
TimeInstant Time ///< [in] Time
) {
// only need LayerThicknessAux on edge
- Array2DReal LayerThick;
- Array2DReal NormVel;
- State->getLayerThickness(LayerThick, ThickTimeLevel);
- State->getNormalVelocity(NormVel, VelTimeLevel);
+ Array2DReal LayerThick = State->getLayerThickness(ThickTimeLevel);
+ Array2DReal NormVel = State->getNormalVelocity(VelTimeLevel);
OMEGA_SCOPE(LayerThicknessAux, AuxState->LayerThicknessAux);
OMEGA_SCOPE(LayerThickCell, LayerThick);
OMEGA_SCOPE(NormalVelEdge, NormVel);
@@ -630,15 +801,17 @@ void Tendencies::computeThicknessTendencies(
void Tendencies::computeVelocityTendencies(
const OceanState *State, ///< [in] State variables
const AuxiliaryState *AuxState, ///< [in] Auxilary state variables
+ const Array3DReal &TracerArray, ///< [in] Tracer array
int ThickTimeLevel, ///< [in] Time level
int VelTimeLevel, ///< [in] Time level
+ int TracerTimeLevel, ///< [in] Time level
TimeInstant Time ///< [in] Time
) {
Pacer::start("Tend:computeVelocityTendencies", 1);
AuxState->computeMomAux(State, ThickTimeLevel, VelTimeLevel);
- computeVelocityTendenciesOnly(State, AuxState, ThickTimeLevel, VelTimeLevel,
- Time);
+ computeVelocityTendenciesOnly(State, AuxState, TracerArray, ThickTimeLevel,
+ VelTimeLevel, TracerTimeLevel, Time);
Pacer::stop("Tend:computeVelocityTendencies", 1);
}
@@ -651,9 +824,9 @@ void Tendencies::computeTracerTendencies(
int VelTimeLevel, ///< [in] Time level
TimeInstant Time ///< [in] Time
) {
+ Array2DReal LayerThickCell = State->getLayerThickness(ThickTimeLevel);
+ Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel);
OMEGA_SCOPE(TracerAux, AuxState->TracerAux);
- OMEGA_SCOPE(LayerThickCell, State->LayerThickness[ThickTimeLevel]);
- OMEGA_SCOPE(NormalVelEdge, State->NormalVelocity[VelTimeLevel]);
OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
OMEGA_SCOPE(MinLayerEdgeBot, VCoord->MinLayerEdgeBot);
@@ -661,22 +834,6 @@ void Tendencies::computeTracerTendencies(
Pacer::start("Tend:computeTracerTendencies", 1);
- Pacer::start("Tend:computeTracerAuxEdge", 2);
- parallelForOuter(
- "computeTracerAuxEdge", {NTracers, Mesh->NEdgesAll},
- KOKKOS_LAMBDA(int LTracer, int IEdge, const TeamMember &Team) {
- const int KMin = MinLayerEdgeBot(IEdge);
- const int KMax = MaxLayerEdgeTop(IEdge);
- const int KRange = vertRangeChunked(KMin, KMax);
- parallelForInner(
- Team, KRange, INNER_LAMBDA(int KChunk) {
- TracerAux.computeVarsOnEdge(LTracer, IEdge, KChunk,
- NormalVelEdge, LayerThickCell,
- TracerArray);
- });
- });
- Pacer::stop("Tend:computeTracerAuxEdge", 2);
-
const auto &MeanLayerThickEdge =
AuxState->LayerThicknessAux.MeanLayerThickEdge;
Pacer::start("Tend:computeTracerAuxCell", 2);
@@ -709,21 +866,17 @@ void Tendencies::computeAllTendencies(
const Array3DReal &TracerArray, ///< [in] Tracer array
int ThickTimeLevel, ///< [in] Time level
int VelTimeLevel, ///< [in] Time level
+ int TracerTimeLevel, ///< [in] Time level
TimeInstant Time ///< [in] Time
) {
-
- Pacer::start("Tend:computeAllTendencies", 1);
-
AuxState->computeAll(State, TracerArray, ThickTimeLevel, VelTimeLevel);
+
computeThicknessTendenciesOnly(State, AuxState, ThickTimeLevel, VelTimeLevel,
Time);
- computeVelocityTendenciesOnly(State, AuxState, ThickTimeLevel, VelTimeLevel,
- Time);
+ computeVelocityTendenciesOnly(State, AuxState, TracerArray, ThickTimeLevel,
+ VelTimeLevel, TracerTimeLevel, Time);
computeTracerTendenciesOnly(State, AuxState, TracerArray, ThickTimeLevel,
VelTimeLevel, Time);
-
- Pacer::stop("Tend:computeAllTendencies", 1);
-
} // end all tendency compute
} // end namespace OMEGA
diff --git a/components/omega/src/ocn/Tendencies.h b/components/omega/src/ocn/Tendencies.h
index 851ce1fc646c..4ba53a313e82 100644
--- a/components/omega/src/ocn/Tendencies.h
+++ b/components/omega/src/ocn/Tendencies.h
@@ -33,10 +33,13 @@
#include "AuxiliaryState.h"
#include "Config.h"
+#include "Eos.h"
#include "HorzMesh.h"
#include "OceanState.h"
+#include "PGrad.h"
#include "TendencyTerms.h"
#include "TimeMgr.h"
+#include "VertAdv.h"
#include "VertCoord.h"
#include
@@ -69,6 +72,8 @@ class Tendencies {
TracerDiffOnCell TracerDiffusion;
TracerHyperDiffOnCell TracerHyperDiff;
+ std::string Name;
+
// Methods to compute tendency groups
void computeThicknessTendencies(const OceanState *State,
const AuxiliaryState *AuxState,
@@ -76,8 +81,9 @@ class Tendencies {
TimeInstant Time);
void computeVelocityTendencies(const OceanState *State,
const AuxiliaryState *AuxState,
+ const Array3DReal &TracerArray,
int ThickTimeLevel, int VelTimeLevel,
- TimeInstant Time);
+ int TracerTimeLevel, TimeInstant Time);
void computeTracerTendencies(const OceanState *State,
const AuxiliaryState *AuxState,
const Array3DReal &TracerArray,
@@ -86,15 +92,17 @@ class Tendencies {
void computeAllTendencies(const OceanState *State,
const AuxiliaryState *AuxState,
const Array3DReal &TracerArray, int ThickTimeLevel,
- int VelTimeLevel, TimeInstant Time);
+ int VelTimeLevel, int TracerTimeLevel,
+ TimeInstant Time);
void computeThicknessTendenciesOnly(const OceanState *State,
const AuxiliaryState *AuxState,
int ThickTimeLevel, int VelTimeLevel,
TimeInstant Time);
void computeVelocityTendenciesOnly(const OceanState *State,
const AuxiliaryState *AuxState,
+ const Array3DReal &TracerArray,
int ThickTimeLevel, int VelTimeLevel,
- TimeInstant Time);
+ int TracerTimeLevel, TimeInstant Time);
void computeTracerTendenciesOnly(const OceanState *State,
const AuxiliaryState *AuxState,
const Array3DReal &TracerArray,
@@ -144,32 +152,48 @@ class Tendencies {
);
// read and set config options
- void readTendConfig(Config *TendConfig);
+ void readConfig(Config *OmegaConfig);
private:
// Construct a new tendency object
Tendencies(const std::string &Name, ///< [in] Name for tendencies
const HorzMesh *Mesh, ///< [in] Horizontal mesh
- const VertCoord *VCoord, ///< [in] Vertical coordinate
+ VertCoord *VCoord, ///< [in] Vertical coordinate
+ VertAdv *VAdv, ///< [in] Vertical advection
+ PressureGrad *PGrad, ///< [in] Pressure gradient
+ Eos *EqState, ///< [in] Equation of state
int NTracersIn, ///< [in] Number of tracers
+ TimeInterval TimeStep, ///< [in] Time step
Config *Options, ///< [in] Configuration options
CustomTendencyType InCustomThicknessTend,
CustomTendencyType InCustomVelocityTend);
Tendencies(const std::string &Name, ///< [in] Name for tendencies
const HorzMesh *Mesh, ///< [in] Horizontal mesh
- const VertCoord *VCoord, ///< [in] Vertical coordinate
+ VertCoord *VCoord, ///< [in] Vertical coordinate
+ VertAdv *VAdv, ///< [in] Vertical advection
+ PressureGrad *PGrad, ///< [in] Pressure gradient
+ Eos *EqState, ///< [in] Equation of state
int NTracersIn, ///< [in] Number of tracers
+ TimeInterval TimeStep, ///< [in] Time step
Config *Options ///< [in] Configuration options
);
+ void defineFields();
+
// forbid copy and move construction
Tendencies(const Tendencies &) = delete;
Tendencies(Tendencies &&) = delete;
- const HorzMesh *Mesh; ///< Pointer to horizontal mesh
- const VertCoord *VCoord; ///< Pointer to vertical coordinate
- I4 NTracers; ///< Number of tracers
+ const HorzMesh *Mesh; ///< Pointer to horizontal mesh
+ VertCoord *VCoord; ///< Pointer to vertical coordinate
+ VertAdv *VAdv; ///< Pointer to vertical advection
+ CustomTendencyType CustomThicknessTend;
+ CustomTendencyType CustomVelocityTend;
+ Eos *EqState; ///< Pointer to equation of state
+ PressureGrad *PGrad; ///< Pointer to pressure gradient
+ I4 NTracers; ///< Number of tracers
+ TimeInterval TimeStep; ///< Time step
// Pointer to default tendencies
static Tendencies *DefaultTendencies;
@@ -177,9 +201,6 @@ class Tendencies {
// Map of all tendency objects
static std::map> AllTendencies;
- CustomTendencyType CustomThicknessTend;
- CustomTendencyType CustomVelocityTend;
-
}; // end class Tendencies
} // namespace OMEGA
diff --git a/components/omega/src/ocn/TendencyTerms.cpp b/components/omega/src/ocn/TendencyTerms.cpp
index 57631a856d08..9550bf01c0be 100644
--- a/components/omega/src/ocn/TendencyTerms.cpp
+++ b/components/omega/src/ocn/TendencyTerms.cpp
@@ -12,6 +12,7 @@
#include "AuxiliaryState.h"
#include "DataTypes.h"
#include "HorzMesh.h"
+#include "HorzOperators.h"
#include "OceanState.h"
#include "Tracers.h"
@@ -72,13 +73,23 @@ BottomDragOnEdge::BottomDragOnEdge(const HorzMesh *Mesh,
TracerHorzAdvOnCell::TracerHorzAdvOnCell(const HorzMesh *Mesh,
const VertCoord *VCoord)
- : NEdgesOnCell(Mesh->NEdgesOnCell), EdgesOnCell(Mesh->EdgesOnCell),
+ : HorzontalMesh(Mesh),
+ NAdvCellsForEdge("NumberOfCellsContribToAdvectionAtEdge",
+ Mesh->NEdgesAll),
+ AdvCellsForEdge("IndexOfCellsContributingToAdvection", Mesh->NEdgesAll,
+ Mesh->MaxEdges2 + 2),
+ AdvMaskHighOrder("MaskForHighOrderAdvectionTerms", Mesh->NEdgesAll),
+ AdvCoefs("CommonAdvectionCoefficients", Mesh->MaxEdges2 + 2,
+ Mesh->NEdgesAll),
+ AdvCoefs3rd("CommonAdvectionCoeffsForHighOrder", Mesh->MaxEdges2 + 2,
+ Mesh->NEdgesAll),
+ HighOrderFlxHorz("HigherOrderHorizontalFlux", Tracers::getNumTracers(),
+ Mesh->NEdgesAll, VCoord->NVertLayers),
+ NEdgesOnCell(Mesh->NEdgesOnCell), EdgesOnCell(Mesh->EdgesOnCell),
CellsOnEdge(Mesh->CellsOnEdge), EdgeSignOnCell(Mesh->EdgeSignOnCell),
- DvEdge(Mesh->DvEdge), AreaCell(Mesh->AreaCell),
- EdgeMask(VCoord->EdgeMask), MinLayerCell(VCoord->MinLayerCell),
- MaxLayerCell(VCoord->MaxLayerCell),
- MinLayerEdgeBot(VCoord->MinLayerEdgeBot),
- MaxLayerEdgeTop(VCoord->MaxLayerEdgeTop) {}
+ DvEdge(Mesh->DvEdge), AreaCell(Mesh->AreaCell) {
+ deepCopy(HighOrderFlxHorz, 0);
+}
TracerDiffOnCell::TracerDiffOnCell(const HorzMesh *Mesh,
const VertCoord *VCoord)
@@ -100,6 +111,28 @@ TracerHyperDiffOnCell::TracerHyperDiffOnCell(const HorzMesh *Mesh,
MinLayerEdgeBot(VCoord->MinLayerEdgeBot),
MaxLayerEdgeTop(VCoord->MaxLayerEdgeTop) {}
+void TracerHorzAdvOnCell::init() {
+ const HorzMesh *Mesh = this->HorzontalMesh;
+ const auto MaxEdges2 = Mesh->MaxEdges2;
+ const auto NEdgesAll = Mesh->NEdgesAll;
+ const auto NCellsAll = Mesh->NCellsAll;
+ // Allocate Kokkos arrays in member data
+
+ SecondDerivativeOnCell secondDerivativeOnCell(Mesh);
+ Array3DReal DerivTwo("DerivTwo", MaxEdges2 + 2, 2, NEdgesAll);
+ parallelFor(
+ {NCellsAll},
+ KOKKOS_LAMBDA(int ICell) { secondDerivativeOnCell(DerivTwo, ICell); });
+ // Compute masks and coefficients
+ Kokkos::fence();
+ MasksAndCoefficients masksAndCoefficients(Mesh, DerivTwo, NAdvCellsForEdge,
+ AdvCellsForEdge, AdvMaskHighOrder,
+ AdvCoefs, AdvCoefs3rd);
+ Kokkos::fence();
+ parallelFor(
+ {NEdgesAll}, KOKKOS_LAMBDA(int IEdge) { masksAndCoefficients(IEdge); });
+ Kokkos::fence();
+}
} // end namespace OMEGA
//===----------------------------------------------------------------------===//
diff --git a/components/omega/src/ocn/TendencyTerms.h b/components/omega/src/ocn/TendencyTerms.h
index cc9aa841b60c..5ccb1ec79ed3 100644
--- a/components/omega/src/ocn/TendencyTerms.h
+++ b/components/omega/src/ocn/TendencyTerms.h
@@ -17,8 +17,7 @@
#include "OceanState.h"
#include "VertCoord.h"
-#include
-#include
+#include // for std::copysign
namespace OMEGA {
@@ -26,7 +25,7 @@ namespace OMEGA {
/// arrays
class ThicknessFluxDivOnCell {
public:
- bool Enabled;
+ bool Enabled = false;
/// constructor declaration
ThicknessFluxDivOnCell(const HorzMesh *Mesh, const VertCoord *VCoord);
@@ -80,7 +79,7 @@ class ThicknessFluxDivOnCell {
/// momentum equation
class PotentialVortHAdvOnEdge {
public:
- bool Enabled;
+ bool Enabled = false;
/// constructor declaration
PotentialVortHAdvOnEdge(const HorzMesh *Mesh, const VertCoord *VCoord);
@@ -131,7 +130,7 @@ class PotentialVortHAdvOnEdge {
/// Gradient of kinetic energy defined on edges, for momentum equation
class KEGradOnEdge {
public:
- bool Enabled;
+ bool Enabled = false;
/// constructor declaration
KEGradOnEdge(const HorzMesh *Mesh, const VertCoord *VCoord);
@@ -166,7 +165,7 @@ class KEGradOnEdge {
/// acceleration, for momentum equation
class SSHGradOnEdge {
public:
- bool Enabled;
+ bool Enabled = false;
/// constructor declaration
SSHGradOnEdge(const HorzMesh *Mesh, const VertCoord *VCoord);
@@ -201,7 +200,7 @@ class SSHGradOnEdge {
/// Laplacian horizontal mixing, for momentum equation
class VelocityDiffusionOnEdge {
public:
- bool Enabled;
+ bool Enabled = false;
Real ViscDel2;
@@ -252,7 +251,7 @@ class VelocityDiffusionOnEdge {
/// Biharmonic horizontal mixing, for momentum equation
class VelocityHyperDiffOnEdge {
public:
- bool Enabled;
+ bool Enabled = false;
Real ViscDel4;
Real DivFactor;
@@ -305,7 +304,7 @@ class VelocityHyperDiffOnEdge {
/// Wind forcing
class WindForcingOnEdge {
public:
- bool Enabled;
+ bool Enabled = false;
Real LocRhoSw;
/// constructor declaration
@@ -333,7 +332,7 @@ class WindForcingOnEdge {
/// Bottom drag
class BottomDragOnEdge {
public:
- bool Enabled;
+ bool Enabled = false;
Real Coeff;
/// constructor declaration
@@ -369,58 +368,88 @@ class BottomDragOnEdge {
// Tracer horizontal advection term
class TracerHorzAdvOnCell {
public:
- bool Enabled;
-
+ bool Enabled = false;
+ bool ForceLowOrder = false;
+ // coefficient for blending high-order terms
+ Real Coef3rdOrder = 0.25;
TracerHorzAdvOnCell(const HorzMesh *Mesh, const VertCoord *VCoord);
+ void init();
+ KOKKOS_FUNCTION void operator()(const I4 L, const I4 IEdge, const I4 KChunk,
+ const Array3DReal &TracerCell,
+ const Array2DReal &FluxLayerThickEdge,
+ const Array2DReal &NormVelEdge) const {
+ const I4 KStart = KChunk * VecLength;
+ const I4 KEnd = KStart + VecLength;
+ for (int K = KStart; K < KEnd; ++K)
+ HighOrderFlxHorz(L, IEdge, K) = 0;
+ if (!ForceLowOrder && AdvMaskHighOrder(IEdge)) {
+ for (int I = 0; I < NAdvCellsForEdge(IEdge); ++I) {
+ const I4 ICell = AdvCellsForEdge(IEdge, I);
+ for (int K = KStart; K < KEnd; ++K) {
+ const Real NormalThicknessFlux =
+ FluxLayerThickEdge(IEdge, K) * NormVelEdge(IEdge, K);
+ const Real TracerWgt =
+ (AdvCoefs(I, IEdge) +
+ Coef3rdOrder * std::copysign(1._Real, NormalThicknessFlux) *
+ AdvCoefs3rd(I, IEdge)) *
+ NormalThicknessFlux;
+ HighOrderFlxHorz(L, IEdge, K) +=
+ TracerWgt * TracerCell(L, ICell, K);
+ }
+ }
+ } else {
+ for (int K = KStart; K < KEnd; ++K) {
+ const I4 JCell0 = CellsOnEdge(IEdge, 0);
+ const I4 JCell1 = CellsOnEdge(IEdge, 1);
+ const Real NormalThicknessFlux =
+ FluxLayerThickEdge(IEdge, K) * NormVelEdge(IEdge, K);
+ const Real TracerWgt =
+ DvEdge(IEdge) * 0.5_Real * NormalThicknessFlux;
+ HighOrderFlxHorz(L, IEdge, K) +=
+ TracerWgt *
+ (TracerCell(L, JCell1, K) + TracerCell(L, JCell0, K));
+ }
+ }
+ }
- KOKKOS_FUNCTION void operator()(const Array3DReal &Tend, I4 L, I4 ICell,
- I4 KChunk, const Array2DReal &NormVelEdge,
- const Array3DReal &HTracersOnEdge) const {
-
- const I4 KStartCell = chunkStart(KChunk, MinLayerCell(ICell));
- const I4 KLenCell = chunkLength(KChunk, KStartCell, MaxLayerCell(ICell));
- const I4 KEndCell = KStartCell + KLenCell - 1;
+ KOKKOS_FUNCTION void operator()(const Array3DReal &Tend, const I4 L,
+ const I4 ICell, const I4 KChunk) const {
+ const I4 KStart = KChunk * VecLength;
+ const I4 KEnd = KStart + VecLength;
const Real InvAreaCell = 1._Real / AreaCell(ICell);
-
- Real HAdvTmp[VecLength] = {0};
-
- for (int J = 0; J < NEdgesOnCell(ICell); ++J) {
- const I4 JEdge = EdgesOnCell(ICell, J);
- const I4 KStartEdge = Kokkos::max(KStartCell, MinLayerEdgeBot(JEdge));
- const I4 KEndEdge = Kokkos::min(KEndCell, MaxLayerEdgeTop(JEdge));
-
- for (int K = KStartEdge; K <= KEndEdge; ++K) {
- const I4 KVec = K - KStartCell;
- HAdvTmp[KVec] -= EdgeMask(JEdge, K) * DvEdge(JEdge) *
- EdgeSignOnCell(ICell, J) *
- HTracersOnEdge(L, JEdge, K) *
- NormVelEdge(JEdge, K) * InvAreaCell;
+ for (int K = KStart; K < KEnd; ++K)
+ Tend(L, ICell, K) = 0;
+
+ for (int I = 0; I < NEdgesOnCell(ICell); ++I) {
+ const I4 IEdge = EdgesOnCell(ICell, I);
+ for (int K = KStart; K < KEnd; ++K) {
+ Tend(L, ICell, K) += EdgeSignOnCell(ICell, I) *
+ HighOrderFlxHorz(L, IEdge, K) * InvAreaCell;
}
}
- for (int KVec = 0; KVec < KLenCell; ++KVec) {
- const I4 K = KStartCell + KVec;
- Tend(L, ICell, K) -= HAdvTmp[KVec];
- }
}
private:
+ const HorzMesh *HorzontalMesh;
+ Array1DI4 NAdvCellsForEdge;
+ Array2DI4 AdvCellsForEdge;
+ Array1DI4 AdvMaskHighOrder;
+ Array2DReal AdvCoefs;
+ Array2DReal AdvCoefs3rd;
+ Array3DReal HighOrderFlxHorz;
+
Array1DI4 NEdgesOnCell;
Array2DI4 EdgesOnCell;
Array2DI4 CellsOnEdge;
Array2DReal EdgeSignOnCell;
Array1DReal DvEdge;
Array1DReal AreaCell;
- Array2DReal EdgeMask;
- Array1DI4 MinLayerCell;
- Array1DI4 MaxLayerCell;
- Array1DI4 MinLayerEdgeBot;
- Array1DI4 MaxLayerEdgeTop;
};
// Tracer horizontal diffusion term
class TracerDiffOnCell {
public:
- bool Enabled;
+ bool Enabled = false;
Real EddyDiff2;
@@ -483,7 +512,7 @@ class TracerDiffOnCell {
// Tracer biharmonic horizontal mixing term
class TracerHyperDiffOnCell {
public:
- bool Enabled;
+ bool Enabled = false;
Real EddyDiff4;
diff --git a/components/omega/src/ocn/Tracers.cpp b/components/omega/src/ocn/Tracers.cpp
index 04cb5b022a9d..efa7b00b635f 100644
--- a/components/omega/src/ocn/Tracers.cpp
+++ b/components/omega/src/ocn/Tracers.cpp
@@ -266,92 +266,58 @@ I4 Tracers::getName(std::string &TracerName, const I4 TracerIndex) {
// get Tracer arrays
// TimeLevel == [0:current, -1:previous, -2:two times ago, ...]
//---------------------------------------------------------------------------
-I4 Tracers::getAll(Array3DReal &TracerArray, const I4 TimeLevel) {
- I4 Err = 0;
- I4 TimeIndex;
-
- Err = getTimeIndex(TimeIndex, TimeLevel);
- TracerArray = TracerArrays[TimeIndex];
-
- return Err;
+Array3DReal Tracers::getAll(const I4 TimeLevel) {
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return TracerArrays[TimeIndex];
}
-I4 Tracers::getByIndex(Array2DReal &TracerArray, const I4 TimeLevel,
- const I4 TracerIndex) {
+Array2DReal Tracers::getByIndex(const I4 TimeLevel, const I4 TracerIndex) {
// Check if tracer index is valid
- if (TracerIndex < 0 || TracerIndex >= NumTracers) {
- LOG_ERROR("Tracers: Tracer index {} is out of range", TracerIndex);
- return -2;
- }
-
- I4 Err = 0;
- I4 TimeIndex;
+ OMEGA_REQUIRE(TracerIndex >= 0 && TracerIndex < NumTracers,
+ "Tracers: Tracer index {} is out of range", TracerIndex);
- Err = getTimeIndex(TimeIndex, TimeLevel);
- TracerArray = Kokkos::subview(TracerArrays[TimeIndex], TracerIndex,
- Kokkos::ALL, Kokkos::ALL);
- return Err;
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return Kokkos::subview(TracerArrays[TimeIndex], TracerIndex, Kokkos::ALL,
+ Kokkos::ALL);
}
-I4 Tracers::getByName(Array2DReal &TracerArray, const I4 TimeLevel,
- const std::string &TracerName) {
- // Check if tracer exists
- if (TracerIndexes.find(TracerName) == TracerIndexes.end()) {
- LOG_ERROR("Tracers: Tracer '{}' does not exist", TracerName);
- return -1;
- }
+Array2DReal Tracers::getByName(const I4 TimeLevel,
+ const std::string &TracerName) {
- int Err = getByIndex(TracerArray, TimeLevel, TracerIndexes[TracerName]);
- if (Err != 0)
- return -2;
+ // Check if tracer exists
+ OMEGA_REQUIRE(TracerIndexes.find(TracerName) != TracerIndexes.end(),
+ "Tracers: Tracer '{}' does not exist", TracerName);
- return 0;
+ return getByIndex(TimeLevel, TracerIndexes[TracerName]);
}
-I4 Tracers::getAllHost(HostArray3DReal &TracerArrayH, const I4 TimeLevel) {
-
- I4 Err = 0;
- I4 TimeIndex;
-
- Err = getTimeIndex(TimeIndex, TimeLevel);
- TracerArrayH = TracerArraysH[TimeIndex];
-
- return Err;
+HostArray3DReal Tracers::getAllHost(const I4 TimeLevel) {
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return TracerArraysH[TimeIndex];
}
-I4 Tracers::getHostByIndex(HostArray2DReal &TracerArrayH, const I4 TimeLevel,
- const I4 TracerIndex) {
+HostArray2DReal Tracers::getHostByIndex(const I4 TimeLevel,
+ const I4 TracerIndex) {
// Check if tracer index is valid
- if (TracerIndex < 0 || TracerIndex >= NumTracers) {
- LOG_ERROR("Tracers: Tracer index {} is out of range", TracerIndex);
- return -2;
- }
+ OMEGA_REQUIRE(TracerIndex >= 0 && TracerIndex < NumTracers,
+ "Tracers: Tracer index {} is out of range", TracerIndex);
- I4 TimeIndex;
-
- I4 Err = getTimeIndex(TimeIndex, TimeLevel);
- TracerArrayH = Kokkos::subview(TracerArraysH[TimeIndex], TracerIndex,
- Kokkos::ALL, Kokkos::ALL);
- return Err;
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
+ return Kokkos::subview(TracerArraysH[TimeIndex], TracerIndex, Kokkos::ALL,
+ Kokkos::ALL);
}
-
-I4 Tracers::getHostByName(HostArray2DReal &TracerArrayH, const I4 TimeLevel,
- const std::string &TracerName) {
+HostArray2DReal Tracers::getHostByName(const I4 TimeLevel,
+ const std::string &TracerName) {
// Check if tracer exists
- if (TracerIndexes.find(TracerName) == TracerIndexes.end()) {
- LOG_ERROR("Tracers: Tracer '{}' does not exist", TracerName);
- return -1;
- }
+ OMEGA_REQUIRE(TracerIndexes.find(TracerName) != TracerIndexes.end(),
+ "Tracers: Tracer '{}' does not exist", TracerName);
// Get the index of the tracer
I4 TracerIndex = TracerIndexes[TracerName];
- I4 Err = getHostByIndex(TracerArrayH, TimeLevel, TracerIndex);
- if (Err != 0)
- return -2;
- return 0;
+ return getHostByIndex(TimeLevel, TracerIndex);
}
//---------------------------------------------------------------------------
@@ -429,25 +395,18 @@ bool Tracers::isGroupMemberByName(const std::string &TracerName,
// deep copy at a time level
// TimeLevel == [1:new, 0:current, -1:previous, -2:two times ago, ...]
//---------------------------------------------------------------------------
-I4 Tracers::copyToDevice(const I4 TimeLevel) {
+void Tracers::copyToDevice(const I4 TimeLevel) {
- I4 Err = 0;
- I4 TimeIndex;
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
- Err = getTimeIndex(TimeIndex, TimeLevel);
deepCopy(TracerArrays[TimeIndex], TracerArraysH[TimeIndex]);
-
- return Err;
}
-I4 Tracers::copyToHost(const I4 TimeLevel) {
+void Tracers::copyToHost(const I4 TimeLevel) {
- I4 TimeIndex;
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
- I4 Err = getTimeIndex(TimeIndex, TimeLevel);
deepCopy(TracerArraysH[TimeIndex], TracerArrays[TimeIndex]);
-
- return Err;
}
//---------------------------------------------------------------------------
@@ -456,10 +415,9 @@ I4 Tracers::copyToHost(const I4 TimeLevel) {
//---------------------------------------------------------------------------
I4 Tracers::exchangeHalo(const I4 TimeLevel) {
- I4 Err = 0;
- I4 TimeIndex;
+ I4 Err = 0;
+ const I4 TimeIndex = getTimeIndex(TimeLevel);
- Err = getTimeIndex(TimeIndex, TimeLevel);
Err = MeshHalo->exchangeFullArrayHalo(TracerArrays[TimeIndex], OnCell);
if (Err != 0)
return -1;
@@ -499,18 +457,13 @@ void Tracers::updateTimeLevels() {
// get index for time level
// TimeLevel == [1:new, 0:current, -1:previous, -2:two times ago, ...]
//---------------------------------------------------------------------------
-I4 Tracers::getTimeIndex(I4 &TimeIndex, const I4 TimeLevel) {
+I4 Tracers::getTimeIndex(const I4 TimeLevel) {
+ OMEGA_REQUIRE(NTimeLevels <= 1 ||
+ !(TimeLevel > 1 || (TimeLevel + NTimeLevels) <= 1),
+ "Tracers: Time level {} is out of range for NTimeLevels {}",
+ TimeLevel, NTimeLevels);
- // Check if time level is valid
- if (NTimeLevels > 1 && (TimeLevel > 1 || (TimeLevel + NTimeLevels) <= 1)) {
- LOG_ERROR("Tracers: Time level {} is out of range for NTimeLevels {}",
- TimeLevel, NTimeLevels);
- return -1;
- }
-
- TimeIndex = (TimeLevel + CurTimeIndex + NTimeLevels) % NTimeLevels;
-
- return 0;
+ return (TimeLevel + CurTimeIndex + NTimeLevels) % NTimeLevels;
}
} // namespace OMEGA
diff --git a/components/omega/src/ocn/Tracers.h b/components/omega/src/ocn/Tracers.h
index f0077a05ebb9..6480880a6235 100644
--- a/components/omega/src/ocn/Tracers.h
+++ b/components/omega/src/ocn/Tracers.h
@@ -71,7 +71,7 @@ class Tracers {
static I4 CurTimeIndex; ///< Time dimension array index for current level
/// get the time level index
- static I4 getTimeIndex(I4 &TimeIndex, const I4 TimeLevel);
+ static I4 getTimeIndex(const I4 TimeLevel);
/// locally defines all tracers but do not allocates memory
static I4
@@ -116,41 +116,36 @@ class Tracers {
);
/// get a device array for all tracers
- static I4 getAll(Array3DReal &TracerArray, ///< [out] tracer device array
- const I4 TimeLevel ///< [in] time level index
+ static Array3DReal getAll(const I4 TimeLevel ///< [in] time level index
);
/// get a device array by tracer index
- static I4 getByIndex(Array2DReal &TracerArray, ///< [out] tracer device array
- const I4 TimeLevel, ///< [in] time level index
- const I4 TracerIndex ///< [in] global tracer index
+ static Array2DReal
+ getByIndex(const I4 TimeLevel, ///< [in] time level index
+ const I4 TracerIndex ///< [in] global tracer index
);
/// get a device array by tracer name
- static I4
- getByName(Array2DReal &TracerArray, ///< [out] tracer device array
- const I4 TimeLevel, ///< [in] time level index
+ static Array2DReal
+ getByName(const I4 TimeLevel, ///< [in] time level index
const std::string &TracerName ///< [in] global tracer name
);
/// get a host array for all tracers
- static I4
- getAllHost(HostArray3DReal &TracerArrayH, ///< [out] tracer host array
- const I4 TimeLevel ///< [in] time level index
+ static HostArray3DReal
+ getAllHost(const I4 TimeLevel ///< [in] time level index
);
/// get a host array by tracer index
- static I4
- getHostByIndex(HostArray2DReal &TracerArrayH, ///< [out] tracer host array
- const I4 TimeLevel, ///< [in] time level index
- const I4 TracerIndex ///< [in] global tracer index
+ static HostArray2DReal
+ getHostByIndex(const I4 TimeLevel, ///< [in] time level index
+ const I4 TracerIndex ///< [in] global tracer index
);
/// get a host array by tracer name
- static I4
- getHostByName(HostArray2DReal &TracerArrayH, ///< [out] tracer host array
- const I4 TimeLevel, ///< [in] time level index
- const std::string &TracerName ///< [in] global tracer name
+ static HostArray2DReal getHostByName( ///< [out] tracer host array
+ const I4 TimeLevel, ///< [in] time level index
+ const std::string &TracerName ///< [in] global tracer name
);
/// get a field by tracer index. If not found, returns nullptr
@@ -203,11 +198,11 @@ class Tracers {
//---------------------------------------------------------------------------
/// Copy tracers variables from host to device
- static I4 copyToDevice(const I4 TimeLevel ///< [in] tracer time level
+ static void copyToDevice(const I4 TimeLevel ///< [in] tracer time level
);
/// Copy tracers variables from device to host
- static I4 copyToHost(const I4 TimeLevel ///< [in] tracer time level
+ static void copyToHost(const I4 TimeLevel ///< [in] tracer time level
);
//---------------------------------------------------------------------------
diff --git a/components/omega/src/ocn/VertAdv.cpp b/components/omega/src/ocn/VertAdv.cpp
new file mode 100644
index 000000000000..343136b10d34
--- /dev/null
+++ b/components/omega/src/ocn/VertAdv.cpp
@@ -0,0 +1,960 @@
+//===-- ocn/VertAdv.cpp - vertical advection --------------------*- C++ -*-===//
+//
+// The VertAdv class contains methods for calculating the vertical velocity and
+// the tendencies of thickness, horizontal velocity, and tracers due to vertical
+// advective transport, and member variables for storing the vertical velocity
+// and the fluxes of tracers at vertical interfaces in each column.
+//
+//===----------------------------------------------------------------------===//
+
+#include "VertAdv.h"
+#include "Field.h"
+#include "Tracers.h"
+
+#include
+
+namespace OMEGA {
+
+// create static class members
+VertAdv *VertAdv::DefaultVertAdv = nullptr;
+std::map> VertAdv::AllVertAdvs;
+
+//------------------------------------------------------------------------------
+// create the default VertAdv, requires prior initialization of default
+// HorzMesh, VertCoord, and Tracers
+void VertAdv::init() {
+
+ auto Mesh = HorzMesh::getDefault();
+ auto VCoord = VertCoord::getDefault();
+
+ Config *OmegaConfig = Config::getOmegaConfig();
+
+ VertAdv::DefaultVertAdv = create("Default", Mesh, VCoord, OmegaConfig);
+
+} // end init
+
+//------------------------------------------------------------------------------
+// constructor
+VertAdv::VertAdv(const std::string &Name_, //< [in] name for new VertAdv
+ const HorzMesh *InMesh, //< [in] associated HorzMesh
+ const VertCoord *InVCoord, //< [in] associated VertCoord
+ Config *Options //< [in] configuration options
+) {
+
+ // Read options from Config object
+ readConfigOptions(Options);
+
+ // Store name suffix
+ Name = Name_;
+
+ // Store pointers to Mesh and VertCoord objects
+ Mesh = InMesh;
+ VCoord = InVCoord;
+
+ // Store dimension sizes
+ NVertLayers = VCoord->NVertLayers;
+ NVertLayersP1 = VCoord->NVertLayersP1;
+ NCellsOwned = Mesh->NCellsOwned;
+ NCellsAll = Mesh->NCellsAll;
+ NCellsSize = Mesh->NCellsSize;
+ NEdgesOwned = Mesh->NEdgesOwned;
+ NEdgesAll = Mesh->NEdgesAll;
+ NEdgesSize = Mesh->NEdgesSize;
+ NTracers = Tracers::getNumTracers();
+
+ // Allocate member arrays
+ VerticalVelocity =
+ Array2DReal("VerticalVelocity", NCellsSize, NVertLayersP1);
+ TotalVerticalVelocity =
+ Array2DReal("TotalVerticalVelocity", NCellsSize, NVertLayersP1);
+ VertFlux = Array3DReal("VertFlux", NTracers, NCellsSize, NVertLayersP1);
+
+ // Allocate host copies
+ VerticalVelocityH = createHostMirrorCopy(VerticalVelocity);
+ TotalVerticalVelocityH = createHostMirrorCopy(TotalVerticalVelocity);
+ VertFluxH = createHostMirrorCopy(VertFlux);
+
+ // Low-order flux array only needed for flux-corrected transport
+ if (VertAdvChoice == VertAdvOption::FCT) {
+ LowOrderVertFlux =
+ Array3DReal("LowOrderVertFlux", NTracers, NCellsSize, NVertLayersP1);
+ LowOrderVertFluxH = createHostMirrorCopy(LowOrderVertFlux);
+ }
+
+ defineFields();
+
+} // end constructor
+
+//------------------------------------------------------------------------------
+// create a new VertAdv instance
+VertAdv *VertAdv::create(const std::string &Name, //< [in] name for new VertAdv
+ const HorzMesh *Mesh, //< [in] associated HorzMesh
+ const VertCoord *VCoord, //< [in] associated VertCoord
+ Config *Options //< [in] configuration options
+) {
+ // Check to see if a VertAdv of the same name already exists and, if so,
+ // exit with an error
+ if (AllVertAdvs.find(Name) != AllVertAdvs.end()) {
+ LOG_ERROR("Attempted to create a VertAdv with name {} but a VertAdv "
+ "of that name already exists",
+ Name);
+ return nullptr;
+ }
+
+ // create a new VertAdv on the heap and put it in a map of unique_ptrs,
+ // which will manage its lifetime
+ auto *NewVertAdv = new VertAdv(Name, Mesh, VCoord, Options);
+ AllVertAdvs.emplace(Name, NewVertAdv);
+
+ return NewVertAdv;
+
+} // end create
+
+void VertAdv::defineFields() {
+
+ // set field names (append Name if not default)
+ VerticalVelocityFldName = "VerticalVelocity";
+ TotalVertVelocityFldName = "TotalVerticalVelocity";
+ VertFluxFldName = "VertFlux";
+ LowOrderVertFluxFldName = "LowOrderVertFlux";
+
+ if (Name != "Default") {
+ VerticalVelocityFldName.append(Name);
+ TotalVertVelocityFldName.append(Name);
+ VertFluxFldName.append(Name);
+ LowOrderVertFluxFldName.append(Name);
+ }
+
+ const Real FillValueReal = -9.99e30;
+ I4 NDims = 2;
+ std::vector DimNames(NDims);
+ DimNames[0] = "NCells";
+ DimNames[1] = "NVertLayersP1";
+
+ auto VerticalVelocityField = Field::create(
+ VerticalVelocityFldName, // field name
+ "Vertical pseudovelocity across a pseudoheight surface", // long name or
+ // description
+ "m s^-1", // units
+ "", // CF standard Name
+ std::numeric_limits::min(), // min valid value
+ std::numeric_limits::max(), // max valid value
+ FillValueReal, // scalar for undefined entries
+ NDims, // number of dimensions
+ DimNames // dimension names
+ );
+
+ auto TotalVertVelocityField =
+ Field::create(TotalVertVelocityFldName, // field name
+ "Total vertical pseudovelocity across a moving, tilted "
+ "pseudoheight surface", // long name or description
+ "m s^-1", // units
+ "", // CF standard Name
+ std::numeric_limits::min(), // min valid value
+ std::numeric_limits::max(), // max valid value
+ FillValueReal, // scalar for undefined entries
+ NDims, // number of dimensions
+ DimNames // dimension names
+ );
+
+ NDims = 3;
+ DimNames.resize(NDims);
+ DimNames[2] = "NTracers";
+
+ auto VertFluxField = Field::create(
+ VertFluxFldName, // field name
+ "Vertical flux of tracers across a pseudoheight surface", // long name or
+ // description
+ "", // units
+ "", // CF standard Name
+ std::numeric_limits::min(), // min valid value
+ std::numeric_limits::max(), // max valid value
+ FillValueReal, // scalar for undefined entries
+ NDims, // number of dimensions
+ DimNames // dimension names
+ );
+
+ auto LowOrderVertFluxField =
+ Field::create(LowOrderVertFluxFldName, // field name
+ "Low-order vertical flux of tracers across a pseudoheight "
+ "surface", // long name or description
+ "", // units
+ "", // CF standard Name
+ std::numeric_limits::min(), // min valid value
+ std::numeric_limits::max(), // max valid value
+ FillValueReal, // scalar for undefined entries
+ NDims, // number of dimensions
+ DimNames // dimension names
+ );
+
+ GroupName = "VertAdv";
+ if (Name != "Default") {
+ GroupName.append(Name);
+ }
+
+ // Create a field group for VertAdv fields
+ auto VertAdvGroup = FieldGroup::create(GroupName);
+
+ VertAdvGroup->addField(VerticalVelocityFldName);
+ VertAdvGroup->addField(TotalVertVelocityFldName);
+ VertAdvGroup->addField(VertFluxFldName);
+ VertAdvGroup->addField(LowOrderVertFluxFldName);
+
+ // Associate Fields with data
+ VerticalVelocityField->attachData(VerticalVelocity);
+ TotalVertVelocityField->attachData(TotalVerticalVelocity);
+ VertFluxField->attachData(VertFlux);
+ LowOrderVertFluxField->attachData(LowOrderVertFlux);
+
+} // end defineFields
+
+//------------------------------------------------------------------------------
+// destructor
+VertAdv::~VertAdv() {
+
+ if (FieldGroup::exists(GroupName)) {
+ Field::destroy(VerticalVelocityFldName);
+ Field::destroy(TotalVertVelocityFldName);
+ Field::destroy(VertFluxFldName);
+ Field::destroy(LowOrderVertFluxFldName);
+ FieldGroup::destroy(GroupName);
+ }
+
+} // end destructor
+
+//------------------------------------------------------------------------------
+// Removes all VertAdvs to clean up before exit
+void VertAdv::clear() {
+
+ AllVertAdvs.clear(); // removes all VertAdvs from the list and in the
+ // process, calls the destructors for each
+
+} // end clear
+
+//------------------------------------------------------------------------------
+// Removes a VertAdv from map by name
+void VertAdv::erase(std::string Name) {
+ AllVertAdvs.erase(Name); // removes the VertAdv from the list and in the
+ // process, calls the destructor
+} // end erase
+
+//------------------------------------------------------------------------------
+// Perform deepCopy for each variable array from device to host
+void VertAdv::copyToHost() {
+
+ deepCopy(VerticalVelocityH, VerticalVelocity);
+ deepCopy(TotalVerticalVelocityH, TotalVerticalVelocity);
+ deepCopy(VertFluxH, VertFlux);
+ if (VertAdvChoice == VertAdvOption::FCT) {
+ deepCopy(LowOrderVertFluxH, LowOrderVertFlux);
+ }
+}
+
+//------------------------------------------------------------------------------
+// Perform deepCopy for each variable array from host to device
+void VertAdv::copyToDevice() {
+
+ deepCopy(VerticalVelocity, VerticalVelocityH);
+ deepCopy(TotalVerticalVelocity, TotalVerticalVelocityH);
+ deepCopy(VertFlux, VertFluxH);
+ if (VertAdvChoice == VertAdvOption::FCT) {
+ deepCopy(LowOrderVertFlux, LowOrderVertFluxH);
+ }
+}
+
+//------------------------------------------------------------------------------
+// Get default VertAdv
+VertAdv *VertAdv::getDefault() { return VertAdv::DefaultVertAdv; }
+
+//------------------------------------------------------------------------------
+// Get VertAdv by name
+VertAdv *VertAdv::get(const std::string Name //< [in] Name of VertAdv
+) {
+
+ // look for an instance of this name
+ auto it = AllVertAdvs.find(Name);
+
+ // if found, return the VertAdv pointer
+ if (it != AllVertAdvs.end()) {
+ return it->second.get();
+
+ // otherwise print error and return null pointer
+ } else {
+ LOG_ERROR("VertAdv::get: Attempt to retrieve non-existant VertAdv:");
+ LOG_ERROR("{} has not been defined or has been removed", Name);
+ return nullptr;
+ }
+
+} // end get VertAdv
+
+//------------------------------------------------------------------------------
+// Read and set config options
+void VertAdv::readConfigOptions(Config *OmegaConfig) {
+
+ Error Err; // Error code
+
+ Config TendConfig("Tendencies");
+ Err += OmegaConfig->get(TendConfig);
+ CHECK_ERROR_ABORT(Err, "VertAdv: Tendencies group not in Config");
+
+ Err += TendConfig.get("ThicknessVertAdvTendencyEnable",
+ this->ThickVertAdvEnabled);
+ CHECK_ERROR_ABORT(
+ Err, "VertAdv: ThicknessVertAdvTendencyEnable not found in TendConfig");
+
+ Err +=
+ TendConfig.get("VelocityVertAdvTendencyEnable", this->VelVertAdvEnabled);
+ CHECK_ERROR_ABORT(
+ Err, "VertAdv: ThicknessVertAdvTendencyEnable not found in TendConfig");
+
+ Err += TendConfig.get("TracerVertAdvTendencyEnable",
+ this->TracerVertAdvEnabled);
+ CHECK_ERROR_ABORT(
+ Err, "VertAdv: ThicknessVertAdvTendencyEnable not found in TendConfig");
+
+ Config AdvectConfig("Advection");
+ Err += OmegaConfig->get(AdvectConfig);
+ CHECK_ERROR_ABORT(Err, "VertAdv: Advection group not in Config");
+
+ bool FluxLimiterOn;
+ Err += AdvectConfig.get("VerticalTracerFluxLimiterEnable", FluxLimiterOn);
+ CHECK_ERROR_ABORT(
+ Err,
+ "VertAdv: VerticalTracerFluxLimiterEnable not found in AdvectConfig");
+ if (FluxLimiterOn) {
+ VertAdvChoice = VertAdvOption::FCT;
+ } else {
+ VertAdvChoice = VertAdvOption::Standard;
+ }
+
+ I4 VertFluxOrder;
+ Err += AdvectConfig.get("VerticalTracerFluxOrder", VertFluxOrder);
+ CHECK_ERROR_ABORT(
+ Err, "VertAdv: VerticalTracerFluxOrder not found in AdvectConfig");
+
+ switch (VertFluxOrder) {
+ case (2):
+ VertFluxChoice = VertFluxOption::Second;
+ break;
+ case (3):
+ VertFluxChoice = VertFluxOption::Third;
+ break;
+ case (4):
+ VertFluxChoice = VertFluxOption::Fourth;
+ break;
+ default:
+ ABORT_ERROR("VertAdv: Invalid option for VerticalTracerFluxOrder found "
+ "in AdvectConfig. Must be 2, 3, or 4");
+ }
+
+ Err += AdvectConfig.get("Coef3rdOrder", Coef3rdOrder);
+ CHECK_ERROR_ABORT(Err, "VertAdv: Coef3rdOrder not found in AdvectConfig");
+
+} // end readConfigOptions
+
+//------------------------------------------------------------------------------
+// Compute VerticalVelocity and TotalVerticalVelocity from the horizontal
+// velocity (NormalVelocity) and the layer thickness used for fluxes through
+// edges (FluxLayerThickEdge)
+void VertAdv::computeVerticalVelocity(
+ const Array2DReal &NormalVelocity, //< [in] horizontal velocity
+ const Array2DReal &FluxLayerThickEdge //< [in] layer thickness at edges
+) {
+
+ // Return if mesh only has a single vertical layer
+ if (NVertLayers == 1)
+ return;
+
+ OMEGA_SCOPE(LocVertVel, VerticalVelocity);
+ OMEGA_SCOPE(LocNVertLayers, NVertLayers);
+ OMEGA_SCOPE(LocAreaCell, Mesh->AreaCell);
+ OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
+ OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
+ OMEGA_SCOPE(LocNEOnC, Mesh->NEdgesOnCell);
+ OMEGA_SCOPE(LocEOnC, Mesh->EdgesOnCell);
+ OMEGA_SCOPE(LocDvE, Mesh->DvEdge);
+ OMEGA_SCOPE(LocESOnC, Mesh->EdgeSignOnCell);
+
+ // Loop over all cells owned by the task
+ parallelForOuter(
+ "computeVerticalVelocity", {NCellsOwned},
+ KOKKOS_LAMBDA(int ICell, const TeamMember &Team) {
+ RealScratchArray DivHU(Team.team_scratch(0), LocNVertLayers);
+
+ const Real InvAreaCell = 1._Real / LocAreaCell(ICell);
+
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ I4 KRange = vertRangeChunked(KMin, KMax);
+
+ // Compute thickness-weighted divergence of horizontal velocity
+ // in each layer
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ Real DivHUTmp[VecLength] = {0};
+ const I4 KStart = chunkStart(KChunk, KMin);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+
+ for (int J = 0; J < LocNEOnC(ICell); ++J) {
+ const I4 JEdge = LocEOnC(ICell, J);
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ DivHUTmp[KVec] -= LocDvE(JEdge) * LocESOnC(ICell, J) *
+ FluxLayerThickEdge(JEdge, K) *
+ NormalVelocity(JEdge, K) * InvAreaCell;
+ }
+ }
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ DivHU(K) = DivHUTmp[KVec];
+ }
+ });
+
+ Team.team_barrier();
+
+ // Set velocity through top and bottom interfaces to zero
+ Kokkos::single(
+ PerTeam(Team), INNER_LAMBDA() {
+ LocVertVel(ICell, KMin) = 0.;
+ LocVertVel(ICell, KMax + 1) = 0.;
+ });
+ KRange = vertRange(KMin + 1, KMax);
+
+ // Prefix sum of divergence to determine velocity through each
+ // interface
+ parallelScanInner(
+ Team, KRange, INNER_LAMBDA(int K, Real &Accum, bool IsFinal) {
+ const I4 KRev = KMax - K;
+ Accum -= DivHU(KRev);
+
+ if (IsFinal) {
+ LocVertVel(ICell, KRev) = Accum;
+ }
+ });
+ },
+ NVertLayers);
+
+ // TODO: currently assuming TotalVerticalVelocity = VerticalVelocity, i.e.
+ // purely from divergence of horizontal velocity. Need to add optional
+ // corrections to transport velocity from other contributions, e.g.
+ // movement of vertical interfaces, contribution of horizontal velocity
+ // through tilted interface.
+ deepCopy(TotalVerticalVelocity, VerticalVelocity);
+
+} // end computeVerticalVelocity
+
+//------------------------------------------------------------------------------
+// Compute thickness tendency due to vertical advection
+void VertAdv::computeThicknessVAdvTend(
+ const Array2DReal &ThickTend //< [inout] thickness tendency
+) {
+
+ // Return if vertical advection thickness tendency not enabled
+ if (!ThickVertAdvEnabled)
+ return;
+
+ // Return if mesh only has a single vertical layer
+ if (NVertLayers == 1)
+ return;
+
+ OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
+ OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
+ OMEGA_SCOPE(LocTotVertVelocity, TotalVerticalVelocity);
+
+ // Loop over every owned cell, pseudo thickness tendency is simply
+ // difference in pseudo velocity between bottom and top interface for
+ // each layer
+ parallelForOuter(
+ "computeThicknessVAdvTend", {NCellsOwned},
+ KOKKOS_LAMBDA(int ICell, const TeamMember &Team) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ const I4 KRange = vertRangeChunked(KMin, KMax);
+
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ ThickTend(ICell, K) += LocTotVertVelocity(ICell, K + 1) -
+ LocTotVertVelocity(ICell, K);
+ }
+ });
+ });
+
+} // end computeThicknessVAdvTend
+
+//------------------------------------------------------------------------------
+// Compute velocity tendency due to vertical advection
+void VertAdv::computeVelocityVAdvTend(
+ const Array2DReal &VelTend, //< [inout] horizontal velocity tendency
+ const Array2DReal &NormalVelocity, //< [in] horizontal velocity
+ const Array2DReal &FluxLayerThickEdge //< [in] layer thickness at edges
+) {
+
+ // Return if vertical advection velocity tendency not enabled
+ if (!VelVertAdvEnabled)
+ return;
+
+ // Return if mesh only has a single vertical layer
+ if (NVertLayers == 1)
+ return;
+
+ OMEGA_SCOPE(LocCOnE, Mesh->CellsOnEdge);
+ OMEGA_SCOPE(MinLayerEdgeBot, VCoord->MinLayerEdgeBot);
+ OMEGA_SCOPE(MaxLayerEdgeTop, VCoord->MaxLayerEdgeTop);
+ OMEGA_SCOPE(LocTotVertVelocity, TotalVerticalVelocity);
+ OMEGA_SCOPE(EdgeMask, VCoord->EdgeMask);
+ OMEGA_SCOPE(LocNVertLayersP1, NVertLayersP1);
+
+ // Loop over every owned edge
+ parallelForOuter(
+ "computeVelocityVAdvTend", {NEdgesOwned},
+ KOKKOS_LAMBDA(int IEdge, const TeamMember &Team) {
+ const I4 Cell1 = LocCOnE(IEdge, 0);
+ const I4 Cell2 = LocCOnE(IEdge, 1);
+ const I4 KMin = MinLayerEdgeBot(IEdge);
+ const I4 KMax = MaxLayerEdgeTop(IEdge);
+ I4 KRange = vertRangeChunked(KMin + 1, KMax);
+
+ // Allocate scratch space for W times Du/Dz at vertical interfaces
+ // between edges
+ RealScratchArray WDuDzEdge(Team.team_scratch(0), LocNVertLayersP1);
+
+ // Flux is zero at top and bottom
+ Kokkos::single(
+ PerTeam(Team), INNER_LAMBDA() {
+ WDuDzEdge(KMin) = 0._Real;
+ WDuDzEdge(KMax + 1) = 0._Real;
+ });
+
+ // Average vertical velocities from cell centers to edges and multiply
+ // by derivative of horizontal velocity to obtain flux of horizontal
+ // velocity at the vertical interfaces between edges
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin + 1);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ const Real WAvg = 0.5_Real * (LocTotVertVelocity(Cell1, K) +
+ LocTotVertVelocity(Cell2, K));
+ WDuDzEdge(K) =
+ WAvg *
+ (NormalVelocity(IEdge, K - 1) -
+ NormalVelocity(IEdge, K)) /
+ (0.5_Real * (FluxLayerThickEdge(IEdge, K - 1) +
+ FluxLayerThickEdge(IEdge, K)));
+ }
+ });
+
+ Team.team_barrier();
+
+ KRange = vertRangeChunked(KMin, KMax);
+ // Average W*Du/Dz from interfaces to layer midpoints
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ VelTend(IEdge, K) -= EdgeMask(IEdge, K) * 0.5_Real *
+ (WDuDzEdge(K) + WDuDzEdge(K + 1));
+ }
+ });
+ },
+ NVertLayersP1);
+
+} // end computeVelocityVAdvTend
+
+//------------------------------------------------------------------------------
+// Compute tracer tendency due to vertical advection, TimeStep is only needed
+// as an argument for flux-corrected transport
+void VertAdv::computeTracerVAdvTend(
+ const Array3DReal &TracerTend, //< [inout] tracer tendencies
+ const Array3DReal &Tracers, //< [in] tracer array
+ const Array2DReal &LayerThickness, //< [in] layer thickness
+ const TimeInterval TimeStep //< [in] time step
+) {
+
+ // Return if vertical advection tracer tendency not enabled
+ if (!TracerVertAdvEnabled)
+ return;
+
+ // Return if mesh only has a single vertical layer
+ if (NVertLayers == 1)
+ return;
+
+ // Compute tracer fluxes at the interfaces
+ computeVerticalFluxes(Tracers, LayerThickness);
+
+ // Dispatch to appropriate algorithm based on configuration settings
+ switch (VertAdvChoice) {
+ case VertAdvOption::Standard:
+ computeStdVAdvTend(TracerTend);
+ break;
+ case VertAdvOption::FCT:
+ R8 Dt;
+ TimeStep.get(Dt, TimeUnits::Seconds);
+ computeFCTVAdvTend(TracerTend, Tracers, LayerThickness, Dt);
+ break;
+ }
+
+} // end computeTracerVAdvTend
+
+//------------------------------------------------------------------------------
+// Compute tracer fluxes due to vertical advection, the particular scheme used
+// is chosen via configuration settings
+void VertAdv::computeVerticalFluxes(
+ const Array3DReal &Tracers, //< [in] tracer array
+ const Array2DReal &LayerThickness //< [in] layer thickness
+) {
+
+ OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
+ OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
+ OMEGA_SCOPE(LocTotVertVel, TotalVerticalVelocity);
+ OMEGA_SCOPE(LocVertFlux, VertFlux);
+ OMEGA_SCOPE(LocLowOrderVertFlux, LowOrderVertFlux);
+ OMEGA_SCOPE(LocCoef3rdOrder, Coef3rdOrder);
+
+ // Compute the fluxes used for the standard VAdv scheme, or the high-order
+ // fluxes used for the FCT VAdv scheme, store in VertFlux member array
+ switch (VertFluxChoice) {
+ // 2nd-order centered fluxes
+ case VertFluxOption::Second:
+ parallelForOuter(
+ "computeVerticalFluxes-Second", {NTracers, NCellsOwned},
+ KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ const I4 KRange = vertRangeChunked(KMin + 2, KMax - 1);
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin + 2);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax - 1);
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ const Real InvLayerThickSum =
+ 1._Real / (LayerThickness(ICell, K - 1) +
+ LayerThickness(ICell, K));
+ const Real VerticalWeightK =
+ LayerThickness(ICell, K - 1) * InvLayerThickSum;
+ const Real VerticalWeightKm1 =
+ LayerThickness(ICell, K) * InvLayerThickSum;
+ LocVertFlux(L, ICell, K) =
+ LocTotVertVel(ICell, K) *
+ (VerticalWeightK * Tracers(L, ICell, K) +
+ VerticalWeightKm1 * Tracers(L, ICell, K - 1));
+ }
+ });
+ });
+ break;
+ // 3rd-order upwind fluxes
+ case VertFluxOption::Third:
+ parallelForOuter(
+ "computeVerticalFluxes-Third", {NTracers, NCellsOwned},
+ KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ const I4 KRange = vertRangeChunked(KMin + 2, KMax - 1);
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin + 2);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax - 1);
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ LocVertFlux(L, ICell, K) =
+ (LocTotVertVel(ICell, K) *
+ (7._Real * (Tracers(L, ICell, K) +
+ Tracers(L, ICell, K - 1)) -
+ (Tracers(L, ICell, K + 1) +
+ Tracers(L, ICell, K - 2))) -
+ LocCoef3rdOrder *
+ std::abs(LocTotVertVel(ICell, K)) *
+ ((Tracers(L, ICell, K + 1) -
+ Tracers(L, ICell, K - 2)) -
+ 3._Real * (Tracers(L, ICell, K) -
+ Tracers(L, ICell, K - 1)))) /
+ 12._Real;
+ }
+ });
+ });
+ break;
+ // 4th-order centered fluxes
+ case VertFluxOption::Fourth:
+ parallelForOuter(
+ "computeVerticalFluxes-Fourth", {NTracers, NCellsOwned},
+ KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ const I4 KRange = vertRangeChunked(KMin + 2, KMax - 1);
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin + 2);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax - 1);
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ LocVertFlux(L, ICell, K) =
+ LocTotVertVel(ICell, K) *
+ (7._Real * (Tracers(L, ICell, K) +
+ Tracers(L, ICell, K - 1)) -
+ (Tracers(L, ICell, K + 1) +
+ Tracers(L, ICell, K - 2))) /
+ 12._Real;
+ }
+ });
+ });
+ break;
+ }
+
+ // Handle fluxes on interfaces near top and bottom layers. Fluxes are 0 on
+ // top-most (KMin) and bottom-most (KMax + 1) interfaces, use second order
+ // for fluxes on next-to-top (KMin + 1) and next-to-bottom (KMax) interfaces.
+ parallelFor(
+ "computeVerticalFluxes-TopBot", {NTracers, NCellsOwned},
+ KOKKOS_LAMBDA(int L, int ICell) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ for (int K : {KMin, KMax + 1}) {
+ LocVertFlux(L, ICell, K) = 0._Real;
+ }
+ for (int K : {KMin + 1, KMax}) {
+ const Real InvLayerThickSum =
+ 1._Real /
+ (LayerThickness(ICell, K - 1) + LayerThickness(ICell, K));
+ const Real VerticalWeightK =
+ LayerThickness(ICell, K - 1) * InvLayerThickSum;
+ const Real VerticalWeightKm1 =
+ LayerThickness(ICell, K) * InvLayerThickSum;
+ LocVertFlux(L, ICell, K) =
+ LocTotVertVel(ICell, K) *
+ (VerticalWeightK * Tracers(L, ICell, K) +
+ VerticalWeightKm1 * Tracers(L, ICell, K - 1));
+ }
+ });
+
+ // If using FCT scheme, compute 1st-order upwind fluxes for low-order and
+ // remove low-order flux from high-order flux
+ if (VertAdvChoice == VertAdvOption::FCT) {
+ parallelForOuter(
+ "computeVerticalFluxes-LowOrder", {NTracers, NCellsOwned},
+ KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ const I4 KRange = vertRangeChunked(KMin + 1, KMax);
+
+ for (int K : {KMin, KMax + 1}) {
+ LocLowOrderVertFlux(L, ICell, K) = 0._Real;
+ }
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin + 1);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ LocLowOrderVertFlux(L, ICell, K) =
+ Kokkos::min(0._Real, LocTotVertVel(ICell, K)) *
+ Tracers(L, ICell, K - 1) +
+ Kokkos::max(0._Real, LocTotVertVel(ICell, K)) *
+ Tracers(L, ICell, K);
+
+ LocVertFlux(L, ICell, K) -=
+ LocLowOrderVertFlux(L, ICell, K);
+ }
+ });
+ });
+ }
+
+} // end computeVerticalFluxes
+
+//------------------------------------------------------------------------------
+// Compute tracer tendencies due to vertical advection using standard advection
+// scheme
+void VertAdv::computeStdVAdvTend(
+ const Array3DReal &TracerTend //< [inout] tracer tendencies
+) {
+
+ OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
+ OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
+ OMEGA_SCOPE(LocVertFlux, VertFlux);
+
+ // Loop over owned cells, tracer tendency in each layer is computed from
+ // difference between fluxes through bottom and top interfaces
+ parallelForOuter(
+ "computeStdVAdvTend", {NTracers, NCellsOwned},
+ KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ const I4 KRange = vertRangeChunked(KMin, KMax);
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ TracerTend(L, ICell, K) +=
+ LocVertFlux(L, ICell, K + 1) - LocVertFlux(L, ICell, K);
+ }
+ });
+ });
+
+} // end computeStdVAdvTend
+
+//------------------------------------------------------------------------------
+// Compute tracer tendencies due to vertical advection using flux-corrected
+// transport scheme. ProvThickness input is provisional layer thickness after
+// horizontal thickness flux
+void VertAdv::computeFCTVAdvTend(
+ const Array3DReal &TracerTend, //< [inout] tracer tendencies
+ const Array3DReal &Tracers, //< [in] tracer array
+ const Array2DReal &ProvThickness, //< [in] provisional layer thickness
+ const Real Dt //< [in] time step
+) {
+
+ OMEGA_SCOPE(MinLayerCell, VCoord->MinLayerCell);
+ OMEGA_SCOPE(MaxLayerCell, VCoord->MaxLayerCell);
+ OMEGA_SCOPE(LocTotVertVel, TotalVerticalVelocity);
+ OMEGA_SCOPE(LocVertFlux, VertFlux);
+ OMEGA_SCOPE(LocLowOrderVertFlux, LowOrderVertFlux);
+ OMEGA_SCOPE(LocNVertLayers, NVertLayers);
+ OMEGA_SCOPE(LocEps, Eps);
+
+ parallelForOuter(
+ "computeFCTVAdvTend", {NTracers, NCellsOwned},
+ KOKKOS_LAMBDA(int L, int ICell, const TeamMember &Team) {
+ const I4 KMin = MinLayerCell(ICell);
+ const I4 KMax = MaxLayerCell(ICell);
+ I4 KRange = vertRangeChunked(KMin, KMax);
+
+ RealScratchArray InvNewProvThick(Team.team_scratch(0),
+ LocNVertLayers);
+ RealScratchArray WorkTend(Team.team_scratch(0), LocNVertLayers);
+ RealScratchArray FlxIn(Team.team_scratch(0), LocNVertLayers);
+ RealScratchArray FlxOut(Team.team_scratch(0), LocNVertLayers);
+ RealScratchArray RescaledFlux(Team.team_scratch(0),
+ LocNVertLayers + 1);
+
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ InvNewProvThick(K) =
+ 1._Real / (ProvThickness(ICell, K) +
+ Dt * (LocTotVertVel(ICell, K + 1) -
+ LocTotVertVel(ICell, K)));
+ Real TracerMax;
+ Real TracerMin;
+ // Determine bounds on tracer from neighbor values for
+ // limiting
+ if (K == KMin) {
+ TracerMax = Kokkos::max(Tracers(L, ICell, K),
+ Tracers(L, ICell, K + 1));
+ TracerMin = Kokkos::min(Tracers(L, ICell, K),
+ Tracers(L, ICell, K + 1));
+ } else if (K == KMax) {
+ TracerMax = Kokkos::max(Tracers(L, ICell, K - 1),
+ Tracers(L, ICell, K));
+ TracerMin = Kokkos::min(Tracers(L, ICell, K - 1),
+ Tracers(L, ICell, K));
+ } else {
+ TracerMax =
+ Kokkos::max(Tracers(L, ICell, K - 1),
+ Kokkos::max(Tracers(L, ICell, K),
+ Tracers(L, ICell, K + 1)));
+ TracerMin =
+ Kokkos::min(Tracers(L, ICell, K - 1),
+ Kokkos::min(Tracers(L, ICell, K),
+ Tracers(L, ICell, K + 1)));
+ }
+
+ // Accumulate upwind flux in WorkTend
+ WorkTend(K) = LocLowOrderVertFlux(L, ICell, K + 1) -
+ LocLowOrderVertFlux(L, ICell, K);
+ // Accumulate remaining high-order flux into layer
+ FlxIn(K) =
+ Kokkos::max(0._Real, LocVertFlux(L, ICell, K + 1)) -
+ Kokkos::min(0._Real, LocVertFlux(L, ICell, K));
+ // Accumulate remaining high-order flux out of layer
+ FlxOut(K) =
+ Kokkos::min(0._Real, LocVertFlux(L, ICell, K + 1)) -
+ Kokkos::max(0._Real, LocVertFlux(L, ICell, K));
+ // Build scale factors to limit flux for FCT using the
+ // bounds determined above and bounds on newly updated
+ // values. Factors are stored in FlxIn and FlxOut
+ // scratch space.
+ Real TracerMinNew =
+ (Tracers(L, ICell, K) * ProvThickness(ICell, K) +
+ Dt * (WorkTend(K) + FlxOut(K))) *
+ InvNewProvThick(K);
+ Real TracerMaxNew =
+ (Tracers(L, ICell, K) * ProvThickness(ICell, K) +
+ Dt * (WorkTend(K) + FlxIn(K))) *
+ InvNewProvThick(K);
+ Real TracerUpwindNew =
+ (Tracers(L, ICell, K) * ProvThickness(ICell, K) +
+ Dt * WorkTend(K)) *
+ InvNewProvThick(K);
+ Real ScaleFactor =
+ (TracerMax - TracerUpwindNew) /
+ (TracerMaxNew - TracerUpwindNew + LocEps);
+ FlxIn(K) =
+ Kokkos::min(1._Real, Kokkos::max(0._Real, ScaleFactor));
+ ScaleFactor = (TracerUpwindNew - TracerMin) /
+ (TracerUpwindNew - TracerMinNew + LocEps);
+ FlxOut(K) =
+ Kokkos::min(1._Real, Kokkos::max(0._Real, ScaleFactor));
+ }
+ });
+
+ Team.team_barrier();
+
+ KRange = vertRangeChunked(KMin + 1, KMax);
+
+ // Rescale the high-order vertical flux
+ RescaledFlux(KMin) = 0._Real;
+ RescaledFlux(KMax + 1) = 0._Real;
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin + 1);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+
+ RescaledFlux(K) =
+ Kokkos::max(0._Real, LocVertFlux(L, ICell, K)) *
+ Kokkos::min(FlxOut(K), FlxIn(K - 1)) +
+ Kokkos::min(0._Real, LocVertFlux(L, ICell, K)) *
+ Kokkos::min(FlxOut(K - 1), FlxIn(K));
+ }
+ });
+
+ Team.team_barrier();
+
+ // Accumulate total FCT vertical advection tendency
+ KRange = vertRangeChunked(KMin, KMax);
+ parallelForInner(
+ Team, KRange, INNER_LAMBDA(int KChunk) {
+ const I4 KStart = chunkStart(KChunk, KMin);
+ const I4 KLen = chunkLength(KChunk, KStart, KMax);
+
+ for (int KVec = 0; KVec < KLen; ++KVec) {
+ const I4 K = KStart + KVec;
+ WorkTend(K) += RescaledFlux(K + 1) - RescaledFlux(K);
+ TracerTend(L, ICell, K) += WorkTend(K);
+ }
+ });
+ // TODO: Monotonicity and diagnostic checks
+ },
+ 5 * NVertLayers + 1);
+
+} // end computeFTCVAdvTend
+
+} // end namespace OMEGA
+//===----------------------------------------------------------------------===//
diff --git a/components/omega/src/ocn/VertAdv.h b/components/omega/src/ocn/VertAdv.h
new file mode 100644
index 000000000000..6d74b91835ce
--- /dev/null
+++ b/components/omega/src/ocn/VertAdv.h
@@ -0,0 +1,222 @@
+#ifndef OMEGA_VERTADV_H
+#define OMEGA_VERTADV_H
+//===-- ocn/VertAdv.h - vertical advection definitions ----------*- C++ -*-===//
+//
+/// \file
+/// \brief Contains methods and variables for vertical advection
+///
+/// This header defines the VertAdv class which contains methods and variables
+/// for calculating the vertical velocity and for vertical transport of
+/// thickness, velocity, and tracers.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Config.h"
+#include "DataTypes.h"
+#include "Error.h"
+#include "HorzMesh.h"
+#include "OceanState.h"
+#include "OmegaKokkos.h"
+#include "TimeMgr.h"
+#include "VertCoord.h"
+
+#include