Skip to content

Commit 13b1d61

Browse files
committed
added p,T,S limits (with CtFreezing calc) to eos Poly75t with a check on CtFreezing calculation and updated docs
1 parent 1ba2778 commit 13b1d61

File tree

4 files changed

+149
-16
lines changed

4 files changed

+149
-16
lines changed

components/omega/doc/devGuide/EOS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ Eos.computeSpecVolDisp(ConsrvTemp, AbsSalinity, Pressure, KDisp);
5252
where `KDisp` is the number of `k` levels you want to displace each specific volume level to.
5353
For example, to displace each level to one below, set `KDisp = 1`.
5454

55-
## Removal of Eos
55+
## Bounds check (and truncation) for the state variables (under Teos-10)
56+
57+
The implemented 75-term polynomial for the calculation of the specific volume under Teos-10 is considered valid for ocean states contained in 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 T given the pressure. The conservative temperature lower bound depends 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 erros in the (-5e-4 K, 6e-4 K) range. Once we calculate the upper and lower bounds of validity, 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.
58+
59+
## Removal of Eos
5660

5761
To clear the Eos instance do:
5862

components/omega/doc/userGuide/EOS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ Eos:
2020
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$).
2121

2222
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.
23+
24+
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)). If the values are outside of the accepted values, we use the valid bounds for the specific volume calculation. Note that the state variable values themselves are not modified, only that they are not used as is in the calculation.

components/omega/src/ocn/Eos.h

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ enum class EosType {
2626
Teos10Eos /// Roquet et al. 2015 75 term expansion
2727
};
2828

29+
struct Range {
30+
Real lo;
31+
Real hi;
32+
};
33+
2934
/// TEOS10 75-term Polynomial Equation of State
3035
class Teos10Eos {
3136
public:
@@ -52,7 +57,7 @@ class Teos10Eos {
5257
/// Calculate the local specific volume polynomial pressure
5358
/// coefficients
5459
calcPCoeffs(LocSpecVolPCoeffs, KVec, ConservTemp(ICell, K),
55-
AbsSalinity(ICell, K));
60+
AbsSalinity(ICell, K), Pressure(ICell, K));
5661

5762
/// Calculate the specific volume at the given pressure
5863
/// If KDisp is 0, we use the current pressure, otherwise we use the
@@ -78,12 +83,17 @@ class Teos10Eos {
7883
/// TEOS-10 helpers
7984
/// Calculate pressure polynomial coefficients for TEOS-10
8085
KOKKOS_FUNCTION void calcPCoeffs(Array2DReal SpecVolPCoeffs, const I4 K,
81-
const Real Ct, const Real Sa) const {
86+
const Real Ct, const Real Sa,
87+
const Real P) const {
8288
constexpr Real SAu = 40.0 * 35.16504 / 35.0;
8389
constexpr Real CTu = 40.0;
8490
constexpr Real DeltaS = 24.0;
85-
Real Ss = Kokkos::sqrt((Sa + DeltaS) / SAu);
86-
Real Tt = Ct / CTu;
91+
Range Srange = calcSLimits(P);
92+
Range Trange = calcTLimits(Sa, P);
93+
Real SaInFunnel = Kokkos::clamp(
94+
Sa, Srange.lo, Srange.hi); // Salt limited to Poly75t valid range
95+
Real Ss = Kokkos::sqrt((SaInFunnel + DeltaS) / SAu);
96+
Real Tt = Kokkos::clamp(Ct, Trange.lo, Trange.hi) / CTu;
8797

8898
/// Coefficients for the polynomial expansion
8999
constexpr Real V000 = 1.0769995862e-03;
@@ -205,8 +215,9 @@ class Teos10Eos {
205215
KOKKOS_FUNCTION Real calcDelta(const Array2DReal &SpecVolPCoeffs, const I4 K,
206216
const Real P) const {
207217

208-
constexpr Real Pu = 1e4;
209-
Real Pp = P / Pu;
218+
constexpr Real Pu = 1e4;
219+
constexpr Real Pmax = 8000.0;
220+
Real Pp = Kokkos::min(P, Pmax) / Pu; // P limited to Poly75t valid range
210221

211222
Real Delta = ((((SpecVolPCoeffs(5, K) * Pp + SpecVolPCoeffs(4, K)) * Pp +
212223
SpecVolPCoeffs(3, K)) *
@@ -220,22 +231,109 @@ class Teos10Eos {
220231
}
221232

222233
/// Calculate reference profile for TEOS-10
223-
KOKKOS_FUNCTION Real calcRefProfile(Real P) const {
224-
constexpr Real Pu = 1e4;
225-
constexpr Real V00 = -4.4015007269e-05;
226-
constexpr Real V01 = 6.9232335784e-06;
227-
constexpr Real V02 = -7.5004675975e-07;
228-
constexpr Real V03 = 1.7009109288e-08;
229-
constexpr Real V04 = -1.6884162004e-08;
230-
constexpr Real V05 = 1.9613503930e-09;
231-
Real Pp = P / Pu;
234+
KOKKOS_FUNCTION Real calcRefProfile(const Real P) const {
235+
constexpr Real Pu = 1e4;
236+
constexpr Real V00 = -4.4015007269e-05;
237+
constexpr Real V01 = 6.9232335784e-06;
238+
constexpr Real V02 = -7.5004675975e-07;
239+
constexpr Real V03 = 1.7009109288e-08;
240+
constexpr Real V04 = -1.6884162004e-08;
241+
constexpr Real V05 = 1.9613503930e-09;
242+
constexpr Real Pmax = 8000.0;
243+
Real Pp = Kokkos::min(P, Pmax) / Pu; // P limited to Poly75t valid range
232244

233245
Real V0 =
234246
(((((V05 * Pp + V04) * Pp + V03) * Pp + V02) * Pp + V01) * Pp + V00) *
235247
Pp;
236248
return V0;
237249
}
238250

251+
/// Calculate S limits of validity given pressure p
252+
KOKKOS_FUNCTION Range calcSLimits(const Real P) const {
253+
Real lo = 0.0;
254+
Real hi = 42.0;
255+
Real lo2 = P * 5e-3 - 2.5;
256+
Real lo3 = 30.0;
257+
258+
if (P >= 500.0 && P < 6500.0) {
259+
lo = Kokkos::max(lo, lo2);
260+
} else if (P >= 6500.0) {
261+
lo = Kokkos::max(lo, lo3);
262+
}
263+
return {lo, hi};
264+
}
265+
266+
/// Calculate T limits of validity given p,Sa
267+
KOKKOS_FUNCTION Range calcTLimits(const Real Sa, const Real P) const {
268+
Real lo = -15.0;
269+
Real hi = 95.0;
270+
271+
if (P < 500.0) {
272+
lo = calcCtFreezing(Sa, P, Real{0.0});
273+
} else if (P < 6500.0) {
274+
lo = calcCtFreezing(Sa, Real{500.0}, Real{0.0});
275+
hi = 31.66666666666667 - P * 3.333333333333334e-3;
276+
} else {
277+
lo = calcCtFreezing(Sa, Real{500.0}, Real{0.0});
278+
hi = 10.0;
279+
}
280+
return {lo, hi};
281+
}
282+
283+
/// Calculates freezing CTemperature (polynomial error in [-5e-4,6e-4] K)
284+
KOKKOS_FUNCTION Real calcCtFreezing(const Real Sa, const Real P,
285+
const Real SaturationFract) const {
286+
constexpr Real Sso = 35.16504;
287+
constexpr Real c0 = 0.017947064327968736;
288+
constexpr Real c1 = -6.076099099929818;
289+
constexpr Real c2 = 4.883198653547851;
290+
constexpr Real c3 = -11.88081601230542;
291+
constexpr Real c4 = 13.34658511480257;
292+
constexpr Real c5 = -8.722761043208607;
293+
constexpr Real c6 = 2.082038908808201;
294+
constexpr Real c7 = -7.389420998107497;
295+
constexpr Real c8 = -2.110913185058476;
296+
constexpr Real c9 = 0.2295491578006229;
297+
constexpr Real c10 = -0.9891538123307282;
298+
constexpr Real c11 = -0.08987150128406496;
299+
constexpr Real c12 = 0.3831132432071728;
300+
constexpr Real c13 = 1.054318231187074;
301+
constexpr Real c14 = 1.065556599652796;
302+
constexpr Real c15 = -0.7997496801694032;
303+
constexpr Real c16 = 0.3850133554097069;
304+
constexpr Real c17 = -2.078616693017569;
305+
constexpr Real c18 = 0.8756340772729538;
306+
constexpr Real c19 = -2.079022768390933;
307+
constexpr Real c20 = 1.596435439942262;
308+
constexpr Real c21 = 0.1338002171109174;
309+
constexpr Real c22 = 1.242891021876471;
310+
311+
// Note: a = 0.502500117621 / Sso
312+
constexpr Real a = 0.014289763856964;
313+
constexpr Real b = 0.057000649899720;
314+
315+
Real CtFreez;
316+
317+
Real Sar = Sa * 1.0e-2;
318+
Real X = Kokkos::sqrt(Sar);
319+
Real Pr = P * 1.0e-4;
320+
321+
CtFreez =
322+
c0 + Sar * (c1 + X * (c2 + X * (c3 + X * (c4 + X * (c5 + c6 * X))))) +
323+
Pr * (c7 + Pr * (c8 + c9 * Pr)) +
324+
Sar * Pr *
325+
(c10 + Pr * (c12 + Pr * (c15 + c21 * Sar)) +
326+
Sar * (c13 + c17 * Pr + c19 * Sar) +
327+
X * (c11 + Pr * (c14 + c18 * Pr) +
328+
Sar * (c16 + c20 * Pr + c22 * Sar)));
329+
330+
/* Adjust for the effects of dissolved air */
331+
CtFreez = CtFreez - SaturationFract * (1e-3) * (2.4 - a * Sa) *
332+
(1.0 + b * (1.0 - Sa / Sso));
333+
334+
return CtFreez;
335+
}
336+
239337
private:
240338
const int NVertLayers;
241339
};

components/omega/test/ocn/EosTest.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,34 @@ int checkValueGswcSpecVol() {
315315
return Err;
316316
}
317317

318+
/// Test that the external GSW-C library returns the expected specific volume
319+
int checkValueCtFreezing() {
320+
int Err = 0;
321+
const Real RTol = 1e-10;
322+
Teos10Eos TestEos(1);
323+
// TestEos->EosChoice = EosType::Teos10Eos;
324+
constexpr Real SaturationFrac = 0.0;
325+
constexpr Real P = 500.0;
326+
constexpr Real Sa = 32.0;
327+
328+
///
329+
/// Get specific volume from GSW-C library
330+
double CtFreezGswc = gsw_ct_freezing_poly(Sa, P, SaturationFrac);
331+
double CtFreez = TestEos.calcCtFreezing(Sa, P, SaturationFrac);
332+
/// Check the value against the expected TEOS-10 value
333+
bool Check = isApprox(CtFreezGswc, CtFreez, RTol);
334+
if (!Check) {
335+
Err++;
336+
LOG_ERROR(
337+
"checkValueCtFreezing: CtFreez isApprox FAIL, expected {}, got {}",
338+
CtFreezGswc, CtFreez);
339+
}
340+
if (Err == 0) {
341+
LOG_INFO("checkValueCtFreezing: PASS");
342+
}
343+
return Err;
344+
}
345+
318346
// the main test (all in one to have the same log)
319347
// Single value test:
320348
// --> test calls the external GSW-C library
@@ -333,6 +361,7 @@ int eosTest(const std::string &MeshFile = "OmegaMesh.nc") {
333361

334362
LOG_INFO("Single value checks:");
335363
Err += checkValueGswcSpecVol();
364+
Err += checkValueCtFreezing();
336365

337366
LOG_INFO("Full array checks:");
338367
Err += testEosLinear();

0 commit comments

Comments
 (0)