Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/omega/configs/Default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Omega:
DRhoDT: -0.2
DRhoDS: 0.8
RhoT0S0: 1000.0
ClampingEnable: false
EosLimits: Funnel
IOStreams:
InitialVertCoord:
UsePointerFile: false
Expand Down
34 changes: 29 additions & 5 deletions components/omega/doc/devGuide/EOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ An enumeration listing all implemented schemes is provided. It needs to be exten
EOS is added. It is used to identify which EOS method is to be used at run time.

```c++
enum class EosType { Linear, Teos10Poly75t };
enum class EosType { LinearEos, Teos10Eos };
```

## Initialization
Expand All @@ -35,24 +35,48 @@ OMEGA::Eos* DefEos = OMEGA::Eos::getInstance();

## Computation of Eos

To compute `SpecVol` for a particular set of temperature, salinity, and pressure arrays, do
To compute `SpecVol` for a particular set of temperature `Ct`, salinity `Sa`, and pressure `P` arrays, do

```c++
Eos.computeSpecVol(ConsrvTemp, AbsSalinity, Pressure);
Eos.computeSpecVol(Ct, Sa, P);
```

`SpecVolDisplaced` is calculated using local temperature and salinity values, but a pressure
value at `K + KDisp`. To compute `SpecVolDisplaced` for a particular set of temperature, salinity,
and pressure arrays and displaced vertical index level, do

```c++
Eos.computeSpecVolDisp(ConsrvTemp, AbsSalinity, Pressure, KDisp);
Eos.computeSpecVolDisp(Ct, Sa, P, KDisp);
```

where `KDisp` is the number of `k` levels you want to displace each specific volume level to.
For example, to displace each level to one below, set `KDisp = 1`.

## Removal of Eos
## Bounds check (and truncation) for the state variables (under TEOS-10)

The implemented 75-term polynomial for the calculation of the specific volume under TEOS-10 has been evaluated for ocean states in the ''cube" (-2-40 C; 0-42 g/kg; 0-10,000 dbar) and the ''oceanographic funnel'' defined in [McDougall et al., 2003](https://journals.ametsoc.org/view/journals/atot/20/5/1520-0426_2003_20_730_aaceaf_2_0_co_2.xml). When using TEOS-10, the Eos uses member methods `calcSLimits(P)` and `calcTLimits(Sa, P)` to calculate the valid ranges of Sa and Ct. When using the `Funnel` opion of `EosLimits`, the salinity limits are calculated as a function of pressure and the temperature limits as a function of pressure and salinity. The conservative temperature lower bound is set by the freezing temperature, using the member method `calcCtFreezing(Sa, P, SaturationFract)`. This method implements the polynomial approximation of the conservative freezing temperature (called `gsw_ct_freezing_poly` in the GSW package), which is known to produce errors in the (-5e-4 K, 6e-4 K) range. When using the `Cube` option of `EosLimits`, the bounds are constant. Once we calculate the upper and lower bounds of validity, warnings are issued when the state variables are outside the validity bounds. If `ClampingEnable` is true, the state variables are clipped to the valid range (if outside the bounds) before we run the specific volume calculation. The state fields themselves are not changed. If `ClampingEnable` is false, the warnings are issued but the specific volume is calculated based on the unchanged state variables.

## Underlying helper functions when using TEOS-10

The computation of the TEOS-10 specific volume relies on 3 underlying member functions:

```c++
calcPCoeffs(SpecVolPCoeffs, K, Ct, Sa, P);
```
which calculates the coefficents that will be applied to the pressure. These coefficients are dependent on the temperature and salinity variables but not the pressure. The pressure argument present in the function call is only used to set the bounds of Ct,Sa validity. This function is where the bulk of the polynomial calculation takes place and thus it is advised to reuse the `SpecVolPCoeffs` which are a class data member if possible to reduce computational expense.

```c++
calcRefProfile(Pressure);
```
which calculates the ocean reference profile as a function of pressure in the layers. This is a simple 6th order polynomial in P with constant coefficients. It could be reused within a timestep provided that the layer pressures do not change but is cheap to recalculate.

```c++
calcDelta(SpecVolPCoeffs, K, Pressure);
```
which applies the `SpecVolPCoeffs` calculated above to the pressure state variable. The resulting "delta" in specific volume is added to the reference profile calculated above to form the specific volume. This step is a simple 5th order polynomial in pressure, which combines the effects of T,S (pre-calculated in `calcPCoeffs`) and the pressure.


## Removal of Eos

To clear the Eos instance do:

Expand Down
6 changes: 5 additions & 1 deletion components/omega/doc/userGuide/EOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ Eos:
DRhoDT: -0.2
DRhoDS: 0.8
RhoT0S0: 1000.0
ClampingEnable: false
EosLimits: Funnel
```

where `DRhoDT` is the thermal expansion coefficient ($\textrm{kg}/(\textrm{m}^3 \cdot ^{\circ}\textrm{C})$), `DRhoDS` is the saline contraction coefficient ($\textrm{kg}/\textrm{m}^3$), and `RhoT0S0` is the reference density at (T,S)=(0,0) (in $\textrm{kg}/\textrm{m}^3$).
where `DRhoDT` is the thermal expansion coefficient ($\textrm{kg}/(\textrm{m}^3 \cdot ^{\circ}\textrm{C})$), `DRhoDS` is the saline contraction coefficient ($\textrm{kg}/\textrm{m}^3$), and `RhoT0S0` is the reference density at (T,S)=(0,0) (in $\textrm{kg}/\textrm{m}^3$). The `ClampingEnable` flag restricts the state variables to the ranges specified by the `EosLimits` options if turned on, or it issues a warning if off. Note that `ClampingEnable` and `EosLimits` are currently used only in the Teos-10 option.

In addition to `SpecVol`, the displaced specific volume `SpecVolDisplaced` is also calculated by the EOS. This calculates the density of a parcel of fluid that is adiabatically displaced by a relative `k` levels, capturing the effects of pressure/depth changes. This is primarily used to calculate quantities for determining the water column stability (i.e. the stratification) and the vertical mixing coefficients (viscosity and diffusivity). Note: when using the linear EOS, `SpecVolDisplaced` will be the same as `SpecVol` since the specific volume calculation is independent of pressure/depth.

When using TEOS-10, the state variables are checked against the range over which the polynomial is considered to be valid (see [Roquet et al. 2015](https://www.sciencedirect.com/science/article/pii/S1463500315000566)). The possible ranges are refered to as `Funnel` (by default) or `Cube` (wider range). If the values are outside of the accepted values, the code issues a warning. When `ClampingEnable=true`, the code uses the valid bounds for the specific volume calculation. Note that in that case, the state variable values themselves are not modified, only that they are not used as is in the calculation.
19 changes: 18 additions & 1 deletion components/omega/src/ocn/Eos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ void Eos::init() {
Err += EosConfig.get("EosType", EosTypeStr);
CHECK_ERROR_ABORT(Err, "Eos::init: EosType subgroup not found in EosConfig");

std::string EosLimStr;
Err += EosConfig.get("EosLimits", EosLimStr);
CHECK_ERROR_ABORT(Err,
"Eos::init: EosLimits subgroup not found in EosConfig");
/// EosLimits EosLimChoice;

/// Set EosChoice and parameters based on EosTypeStr
if (EosTypeStr == "Linear" or EosTypeStr == "linear") {
Config EosLinConfig("Linear");
Expand All @@ -104,12 +110,23 @@ void Eos::init() {
} else if ((EosTypeStr == "teos10") or (EosTypeStr == "teos-10") or
(EosTypeStr == "TEOS-10")) {
eos->EosChoice = EosType::Teos10Eos;
if (EosLimStr == "Funnel") {
eos->ComputeSpecVolTeos10.EosLimChoice = EosLimits::Funnel;
} else if (EosLimStr == "Cube") {
eos->ComputeSpecVolTeos10.EosLimChoice = EosLimits::Cube;
} else {
LOG_ERROR("Eos::init: Unknown EosLimits requested");
Err += EosConfig.get("ClampingEnable",
eos->ComputeSpecVolTeos10.ClampingEnable);
CHECK_ERROR_ABORT(
Err, "Eos::init: Parameter ClampingEnable not found in EosConfig");
}
} else {
LOG_ERROR("Eos::init: Unknown EosType requested");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to ABORT_ERROR

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Out of curiosity, is the use case for LOG_ERROR when it's an error but we don't want to exit? Can you think of a use case for it?
I understand that ABORT_ERROR is the most appropriate to exit when encountering a critical error?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, once we created the Error handler, some of the LOG macros are superseded (though the Error handler uses the LOG macros underneath). You can still use LOG_ERROR to write a message with an error prefix to the log and not abort/return. A possible use case might be when you have a lot of information to write and multiple messages might be cleaner than a single large message with multiple lines. And yes, ABORT_ERROR is preferred for critical errors. I've been going through and replacing LOG macros with ERROR macros when I can but haven't made it up to the ocn directory yet and just happened to see these.

}
} // end init

/// Compute specific volume for all cells/layers (no displacement)
/// Compute specific volume for all cells/levels (no displacement)
void Eos::computeSpecVol(const Array2DReal &ConservTemp,
const Array2DReal &AbsSalinity,
const Array2DReal &Pressure) {
Expand Down
155 changes: 149 additions & 6 deletions components/omega/src/ocn/Eos.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,29 @@ enum class EosType {
Teos10Eos /// Roquet et al. 2015 75 term expansion
};

enum class EosLimits {
Funnel, ///
Cube ///
};

struct EosRange {
Real Lo;
Real Hi;
};

/// TEOS10 75-term Polynomial Equation of State
class Teos10Eos {
public:
Array2DReal SpecVolPCoeffs;
EosLimits EosLimChoice; ///< EOS clamping option in use
bool ClampingEnable; ///< EOS clamping option in use

/// constructor declaration
Teos10Eos(int NVertLayers);

KOKKOS_INLINE_FUNCTION
void setClamping(bool Flag) { ClampingEnable = Flag; }

// The functor takes the full arrays of specific volume (inout),
// the indices ICell and KChunk, and the ocean tracers (conservative)
// temperature, and (absolute) salinity as inputs, and outputs the
Expand All @@ -52,7 +67,7 @@ class Teos10Eos {
/// Calculate the local specific volume polynomial pressure
/// coefficients
calcPCoeffs(LocSpecVolPCoeffs, KVec, ConservTemp(ICell, K),
AbsSalinity(ICell, K));
AbsSalinity(ICell, K), Pressure(ICell, K));

/// Calculate the specific volume at the given pressure
/// If KDisp is 0, we use the current pressure, otherwise we use the
Expand All @@ -78,12 +93,34 @@ class Teos10Eos {
/// TEOS-10 helpers
/// Calculate pressure polynomial coefficients for TEOS-10
KOKKOS_FUNCTION void calcPCoeffs(Array2DReal SpecVolPCoeffs, const I4 K,
const Real Ct, const Real Sa) const {
const Real Ct, const Real Sa,
const Real P) const {
constexpr Real SAu = 40.0 * 35.16504 / 35.0;
constexpr Real CTu = 40.0;
constexpr Real DeltaS = 24.0;
EosRange SRange = calcSLimits(P);
EosRange TRange = calcTLimits(Sa, P);
Real Ss = Kokkos::sqrt((Sa + DeltaS) / SAu);
Real Tt = Ct / CTu;
if (ClampingEnable) {
Real SaInRange = Kokkos::clamp(
Sa, SRange.Lo, SRange.Hi); // Salt limited to Poly75t valid range
Ss = Kokkos::sqrt((SaInRange + DeltaS) / SAu);
Tt = Kokkos::clamp(Ct, TRange.Lo, TRange.Hi) / CTu;
}

if (Sa > SRange.Hi) {
LOG_WARN("In calcPCoeffs: S={} exceeds Smax={}", Sa, SRange.Hi);
}
if (Sa < SRange.Lo) {
LOG_WARN("In calcPCoeffs: S={} lower than Smin={}", Sa, SRange.Lo);
}
if (Ct > TRange.Hi) {
LOG_WARN("In calcPCoeffs: T={} exceeds Tmax={}", Ct, TRange.Hi);
}
if (Ct < TRange.Lo) {
LOG_WARN("In calcPCoeffs: T={} lower than Tmin={}", Ct, TRange.Lo);
}

/// Coefficients for the polynomial expansion
constexpr Real V000 = 1.0769995862e-03;
Expand Down Expand Up @@ -206,8 +243,15 @@ class Teos10Eos {
const Real P) const {

constexpr Real Pu = 1e4;
Real Pp = P / Pu;

const Real Pmax = (EosLimChoice == EosLimits::Funnel) ? 8000.0 : 10000.0;
Real Pp = P / Pu;
if (ClampingEnable) {
Pp = Kokkos::min(P, Pmax) / Pu; // P limited to Poly75t valid range
LOG_INFO("In calcDelta: P={} exceeds Pmax={}; Clamping the pressure",
P, Pmax);
} else if (P > Pmax) {
LOG_WARN("In calcDelta: P={} exceeds Pmax={}", P, Pmax);
}
Real Delta = ((((SpecVolPCoeffs(5, K) * Pp + SpecVolPCoeffs(4, K)) * Pp +
SpecVolPCoeffs(3, K)) *
Pp +
Expand All @@ -220,22 +264,120 @@ class Teos10Eos {
}

/// Calculate reference profile for TEOS-10
KOKKOS_FUNCTION Real calcRefProfile(Real P) const {
KOKKOS_FUNCTION Real calcRefProfile(const Real P) const {
constexpr Real Pu = 1e4;
constexpr Real V00 = -4.4015007269e-05;
constexpr Real V01 = 6.9232335784e-06;
constexpr Real V02 = -7.5004675975e-07;
constexpr Real V03 = 1.7009109288e-08;
constexpr Real V04 = -1.6884162004e-08;
constexpr Real V05 = 1.9613503930e-09;
Real Pp = P / Pu;
const Real Pmax = (EosLimChoice == EosLimits::Funnel) ? 8000.0 : 10000.0;
Real Pp = P / Pu;
if (ClampingEnable) {
Pp = Kokkos::min(P, Pmax) / Pu; // P limited to Poly75t valid range
} else if (P > Pmax) {
LOG_WARN("In calcRefProfile: P={} exceeds Pmax={}", P, Pmax);
}

Real V0 =
(((((V05 * Pp + V04) * Pp + V03) * Pp + V02) * Pp + V01) * Pp + V00) *
Pp;
return V0;
}

/// Calculate S limits of validity given pressure p
KOKKOS_FUNCTION EosRange calcSLimits(const Real P) const {
Real Lo = 0.0;
Real Hi = 42.0;
if (EosLimChoice == EosLimits::Funnel) {
Real Lo2 = P * 5e-3 - 2.5;
Real Lo3 = 30.0;

if (P >= 500.0 && P < 6500.0) {
Lo = Kokkos::max(Lo, Lo2);
} else if (P >= 6500.0) {
Lo = Kokkos::max(Lo, Lo3);
}
}
return {Lo, Hi};
}

/// Calculate T limits of validity given p,Sa
KOKKOS_FUNCTION EosRange calcTLimits(const Real Sa, const Real P) const {
Real Lo = -15.0;
Real Hi = 95.0;

if (EosLimChoice == EosLimits::Funnel) {
if (P < 500.0) {
Lo = calcCtFreezing(Sa, P, 0.0_Real);
} else if (P < 6500.0) {
Lo = calcCtFreezing(Sa, 500.0_Real, 0.0_Real);
Hi = 31.66666666666667 - P * 3.333333333333334e-3;
} else {
Lo = calcCtFreezing(Sa, 500.0_Real, 0.0_Real);
Hi = 10.0;
}
}
if (EosLimChoice == EosLimits::Cube) {
Lo = -2.0;
Hi = 40.0;
}
return {Lo, Hi};
}

/// Calculates freezing CTemperature (polynomial error in [-5e-4,6e-4] K)
KOKKOS_FUNCTION Real calcCtFreezing(const Real Sa, const Real P,
const Real SaturationFract) const {
constexpr Real Sso = 35.16504;
constexpr Real C0 = 0.017947064327968736;
constexpr Real C1 = -6.076099099929818;
constexpr Real C2 = 4.883198653547851;
constexpr Real C3 = -11.88081601230542;
constexpr Real C4 = 13.34658511480257;
constexpr Real C5 = -8.722761043208607;
constexpr Real C6 = 2.082038908808201;
constexpr Real C7 = -7.389420998107497;
constexpr Real C8 = -2.110913185058476;
constexpr Real C9 = 0.2295491578006229;
constexpr Real C10 = -0.9891538123307282;
constexpr Real C11 = -0.08987150128406496;
constexpr Real C12 = 0.3831132432071728;
constexpr Real C13 = 1.054318231187074;
constexpr Real C14 = 1.065556599652796;
constexpr Real C15 = -0.7997496801694032;
constexpr Real C16 = 0.3850133554097069;
constexpr Real C17 = -2.078616693017569;
constexpr Real C18 = 0.8756340772729538;
constexpr Real C19 = -2.079022768390933;
constexpr Real C20 = 1.596435439942262;
constexpr Real C21 = 0.1338002171109174;
constexpr Real C22 = 1.242891021876471;

// Note: a = 0.502500117621 / Sso
constexpr Real A = 0.014289763856964;
constexpr Real B = 0.057000649899720;

Real Sar = Sa * 1.0e-2;
Real X = Kokkos::sqrt(Sar);
Real Pr = P * 1.0e-4;

Real CtFreez =
C0 + Sar * (C1 + X * (C2 + X * (C3 + X * (C4 + X * (C5 + C6 * X))))) +
Pr * (C7 + Pr * (C8 + C9 * Pr)) +
Sar * Pr *
(C10 + Pr * (C12 + Pr * (C15 + C21 * Sar)) +
Sar * (C13 + C17 * Pr + C19 * Sar) +
X * (C11 + Pr * (C14 + C18 * Pr) +
Sar * (C16 + C20 * Pr + C22 * Sar)));

/* Adjust for the effects of dissolved air */
CtFreez = CtFreez - SaturationFract * (1e-3) * (2.4 - A * Sa) *
(1.0 + B * (1.0 - Sa / Sso));

return CtFreez;
}

private:
const int NVertLayers;
};
Expand Down Expand Up @@ -277,6 +419,7 @@ class Eos {
/// Destroy instance (frees Kokkos views)
static void destroyInstance();

void setTeosClamping(bool Flag) { ComputeSpecVolTeos10.setClamping(Flag); }
EosType EosChoice; ///< Current EOS type in use
Array2DReal SpecVol; ///< Specific volume field
Array2DReal SpecVolDisplaced; ///< Displaced specific volume field
Expand Down
Loading
Loading