Skip to content

Commit da2e9c4

Browse files
committed
adds test for eos clamping
1 parent 80dd51f commit da2e9c4

File tree

4 files changed

+192
-70
lines changed

4 files changed

+192
-70
lines changed

components/omega/doc/devGuide/EOS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ An enumeration listing all implemented schemes is provided. It needs to be exten
1515
EOS is added. It is used to identify which EOS method is to be used at run time.
1616

1717
```c++
18-
enum class EosType { Linear, Teos10Poly75t };
18+
enum class EosType { LinearEos, Teos10Eos };
1919
```
2020
2121
## Initialization
@@ -52,9 +52,9 @@ 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-
## Bounds check (and truncation) for the state variables (under Teos-10)
55+
## Bounds check (and truncation) for the state variables (under TEOS-10)
5656

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.
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 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.
5858

5959
## Removal of Eos
6060

components/omega/doc/userGuide/EOS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ where `DRhoDT` is the thermal expansion coefficient ($\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.
2323

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.
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: 58 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ enum class EosType {
2525
Teos10Eos /// Roquet et al. 2015 75 term expansion
2626
};
2727

28-
struct Range {
29-
Real lo;
30-
Real hi;
28+
struct EosRange {
29+
Real Lo;
30+
Real Hi;
3131
};
3232

3333
/// TEOS10 75-term Polynomial Equation of State
@@ -87,12 +87,12 @@ class Teos10Eos {
8787
constexpr Real SAu = 40.0 * 35.16504 / 35.0;
8888
constexpr Real CTu = 40.0;
8989
constexpr Real DeltaS = 24.0;
90-
Range Srange = calcSLimits(P);
91-
Range Trange = calcTLimits(Sa, P);
90+
EosRange SRange = calcSLimits(P);
91+
EosRange TRange = calcTLimits(Sa, P);
9292
Real SaInFunnel = Kokkos::clamp(
93-
Sa, Srange.lo, Srange.hi); // Salt limited to Poly75t valid range
93+
Sa, SRange.Lo, SRange.Hi); // Salt limited to Poly75t valid range
9494
Real Ss = Kokkos::sqrt((SaInFunnel + DeltaS) / SAu);
95-
Real Tt = Kokkos::clamp(Ct, Trange.lo, Trange.hi) / CTu;
95+
Real Tt = Kokkos::clamp(Ct, TRange.Lo, TRange.Hi) / CTu;
9696

9797
/// Coefficients for the polynomial expansion
9898
constexpr Real V000 = 1.0769995862e-03;
@@ -248,87 +248,85 @@ class Teos10Eos {
248248
}
249249

250250
/// Calculate S limits of validity given pressure p
251-
KOKKOS_FUNCTION Range calcSLimits(const Real P) const {
252-
Real lo = 0.0;
253-
Real hi = 42.0;
254-
Real lo2 = P * 5e-3 - 2.5;
255-
Real lo3 = 30.0;
251+
KOKKOS_FUNCTION EosRange calcSLimits(const Real P) const {
252+
Real Lo = 0.0;
253+
Real Hi = 42.0;
254+
Real Lo2 = P * 5e-3 - 2.5;
255+
Real Lo3 = 30.0;
256256

257257
if (P >= 500.0 && P < 6500.0) {
258-
lo = Kokkos::max(lo, lo2);
258+
Lo = Kokkos::max(Lo, Lo2);
259259
} else if (P >= 6500.0) {
260-
lo = Kokkos::max(lo, lo3);
260+
Lo = Kokkos::max(Lo, Lo3);
261261
}
262-
return {lo, hi};
262+
return {Lo, Hi};
263263
}
264264

265265
/// Calculate T limits of validity given p,Sa
266-
KOKKOS_FUNCTION Range calcTLimits(const Real Sa, const Real P) const {
267-
Real lo = -15.0;
268-
Real hi = 95.0;
266+
KOKKOS_FUNCTION EosRange calcTLimits(const Real Sa, const Real P) const {
267+
Real Lo = -15.0;
268+
Real Hi = 95.0;
269269

270270
if (P < 500.0) {
271-
lo = calcCtFreezing(Sa, P, Real{0.0});
271+
Lo = calcCtFreezing(Sa, P, 0.0_Real);
272272
} else if (P < 6500.0) {
273-
lo = calcCtFreezing(Sa, Real{500.0}, Real{0.0});
274-
hi = 31.66666666666667 - P * 3.333333333333334e-3;
273+
Lo = calcCtFreezing(Sa, 500.0_Real, 0.0_Real);
274+
Hi = 31.66666666666667 - P * 3.333333333333334e-3;
275275
} else {
276-
lo = calcCtFreezing(Sa, Real{500.0}, Real{0.0});
277-
hi = 10.0;
276+
Lo = calcCtFreezing(Sa, 500.0_Real, 0.0_Real);
277+
Hi = 10.0;
278278
}
279-
return {lo, hi};
279+
return {Lo, Hi};
280280
}
281281

282282
/// Calculates freezing CTemperature (polynomial error in [-5e-4,6e-4] K)
283283
KOKKOS_FUNCTION Real calcCtFreezing(const Real Sa, const Real P,
284284
const Real SaturationFract) const {
285285
constexpr Real Sso = 35.16504;
286-
constexpr Real c0 = 0.017947064327968736;
287-
constexpr Real c1 = -6.076099099929818;
288-
constexpr Real c2 = 4.883198653547851;
289-
constexpr Real c3 = -11.88081601230542;
290-
constexpr Real c4 = 13.34658511480257;
291-
constexpr Real c5 = -8.722761043208607;
292-
constexpr Real c6 = 2.082038908808201;
293-
constexpr Real c7 = -7.389420998107497;
294-
constexpr Real c8 = -2.110913185058476;
295-
constexpr Real c9 = 0.2295491578006229;
296-
constexpr Real c10 = -0.9891538123307282;
297-
constexpr Real c11 = -0.08987150128406496;
298-
constexpr Real c12 = 0.3831132432071728;
299-
constexpr Real c13 = 1.054318231187074;
300-
constexpr Real c14 = 1.065556599652796;
301-
constexpr Real c15 = -0.7997496801694032;
302-
constexpr Real c16 = 0.3850133554097069;
303-
constexpr Real c17 = -2.078616693017569;
304-
constexpr Real c18 = 0.8756340772729538;
305-
constexpr Real c19 = -2.079022768390933;
306-
constexpr Real c20 = 1.596435439942262;
307-
constexpr Real c21 = 0.1338002171109174;
308-
constexpr Real c22 = 1.242891021876471;
286+
constexpr Real C0 = 0.017947064327968736;
287+
constexpr Real C1 = -6.076099099929818;
288+
constexpr Real C2 = 4.883198653547851;
289+
constexpr Real C3 = -11.88081601230542;
290+
constexpr Real C4 = 13.34658511480257;
291+
constexpr Real C5 = -8.722761043208607;
292+
constexpr Real C6 = 2.082038908808201;
293+
constexpr Real C7 = -7.389420998107497;
294+
constexpr Real C8 = -2.110913185058476;
295+
constexpr Real C9 = 0.2295491578006229;
296+
constexpr Real C10 = -0.9891538123307282;
297+
constexpr Real C11 = -0.08987150128406496;
298+
constexpr Real C12 = 0.3831132432071728;
299+
constexpr Real C13 = 1.054318231187074;
300+
constexpr Real C14 = 1.065556599652796;
301+
constexpr Real C15 = -0.7997496801694032;
302+
constexpr Real C16 = 0.3850133554097069;
303+
constexpr Real C17 = -2.078616693017569;
304+
constexpr Real C18 = 0.8756340772729538;
305+
constexpr Real C19 = -2.079022768390933;
306+
constexpr Real C20 = 1.596435439942262;
307+
constexpr Real C21 = 0.1338002171109174;
308+
constexpr Real C22 = 1.242891021876471;
309309

310310
// Note: a = 0.502500117621 / Sso
311-
constexpr Real a = 0.014289763856964;
312-
constexpr Real b = 0.057000649899720;
313-
314-
Real CtFreez;
311+
constexpr Real A = 0.014289763856964;
312+
constexpr Real B = 0.057000649899720;
315313

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

320-
CtFreez =
321-
c0 + Sar * (c1 + X * (c2 + X * (c3 + X * (c4 + X * (c5 + c6 * X))))) +
322-
Pr * (c7 + Pr * (c8 + c9 * Pr)) +
318+
Real CtFreez =
319+
C0 + Sar * (C1 + X * (C2 + X * (C3 + X * (C4 + X * (C5 + C6 * X))))) +
320+
Pr * (C7 + Pr * (C8 + C9 * Pr)) +
323321
Sar * Pr *
324-
(c10 + Pr * (c12 + Pr * (c15 + c21 * Sar)) +
325-
Sar * (c13 + c17 * Pr + c19 * Sar) +
326-
X * (c11 + Pr * (c14 + c18 * Pr) +
327-
Sar * (c16 + c20 * Pr + c22 * Sar)));
322+
(C10 + Pr * (C12 + Pr * (C15 + C21 * Sar)) +
323+
Sar * (C13 + C17 * Pr + C19 * Sar) +
324+
X * (C11 + Pr * (C14 + C18 * Pr) +
325+
Sar * (C16 + C20 * Pr + C22 * Sar)));
328326

329327
/* Adjust for the effects of dissolved air */
330-
CtFreez = CtFreez - SaturationFract * (1e-3) * (2.4 - a * Sa) *
331-
(1.0 + b * (1.0 - Sa / Sso));
328+
CtFreez = CtFreez - SaturationFract * (1e-3) * (2.4 - A * Sa) *
329+
(1.0 + B * (1.0 - Sa / Sso));
332330

333331
return CtFreez;
334332
}

components/omega/test/ocn/EosTest.cpp

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ constexpr int NVertLevels = 60;
3737

3838
/// Published values (TEOS-10 and linear) to test against
3939
const Real TeosExpValue = 0.0009732819628; // Expected value for TEOS-10 eos
40+
const Real TeosClampValue1 =
41+
0.0009714522912320203; // Expected value for TEOS-10 eos clamping test 1
42+
const Real TeosClampValue2 =
43+
0.0009662964459162306; // Expected value for TEOS-10 eos clamping test 2
44+
const Real TeosClampValue3 =
45+
0.0010086299185825206; // Expected value for TEOS-10 eos clamping test 3
4046
const Real LinearExpValue =
4147
0.0009784735812133072; // Expected value for Linear eos
4248

@@ -285,6 +291,117 @@ int testEosTeos10Displaced() {
285291
return Err;
286292
}
287293

294+
int testEosTeos10Clamping() {
295+
int Err = 0;
296+
const auto *Mesh = HorzMesh::getDefault();
297+
/// Get Eos instance to test
298+
Eos *TestEos = Eos::getInstance();
299+
TestEos->EosChoice = EosType::Teos10Eos;
300+
301+
/// Create and fill ocean state arrays
302+
Array2DReal SArray = Array2DReal("SArray", Mesh->NCellsAll, NVertLevels);
303+
Array2DReal TArray = Array2DReal("TArray", Mesh->NCellsAll, NVertLevels);
304+
Array2DReal PArray = Array2DReal("PArray", Mesh->NCellsAll, NVertLevels);
305+
/// Use Kokkos::deep_copy to fill the entire view with the ref value
306+
/// Test with valid poly75t values first.
307+
deepCopy(SArray, 35.0);
308+
deepCopy(TArray, 5.0);
309+
deepCopy(PArray, 400.0);
310+
deepCopy(TestEos->SpecVol, 0.0);
311+
312+
/// Compute specific volume
313+
TestEos->computeSpecVol(TArray, SArray, PArray);
314+
315+
/// Check all array values against expected value
316+
int numMismatches = 0;
317+
Array2DReal SpecVol = TestEos->SpecVol;
318+
parallelReduce(
319+
"CheckSpecVolMatrix-Teos", {Mesh->NCellsAll, NVertLevels},
320+
KOKKOS_LAMBDA(int i, int j, int &localCount) {
321+
if (!isApprox(SpecVol(i, j), TeosClampValue1, RTol)) {
322+
localCount++;
323+
}
324+
},
325+
numMismatches);
326+
327+
auto SpecVolH = createHostMirrorCopy(SpecVol);
328+
if (numMismatches != 0) {
329+
Err++;
330+
LOG_ERROR("EosTest: TEOS SpecVolClampingNone isApprox FAIL, "
331+
"expected {}, got {} with {} mismatches",
332+
TeosClampValue1, SpecVolH(1, 1), numMismatches);
333+
}
334+
if (Err == 0) {
335+
LOG_INFO("EosTest SpecVolClampingNone TEOS-10: PASS");
336+
}
337+
338+
/// Test with an ivalid poly75t salinity value second
339+
deepCopy(SArray, 45.0);
340+
deepCopy(TArray, 5.0);
341+
deepCopy(PArray, 400.0);
342+
deepCopy(TestEos->SpecVol, 0.0);
343+
344+
/// Compute specific volume
345+
TestEos->computeSpecVol(TArray, SArray, PArray);
346+
347+
/// Check all array values against expected value
348+
numMismatches = 0;
349+
SpecVol = TestEos->SpecVol;
350+
parallelReduce(
351+
"CheckSpecVolMatrix-Teos", {Mesh->NCellsAll, NVertLevels},
352+
KOKKOS_LAMBDA(int i, int j, int &localCount) {
353+
if (!isApprox(SpecVol(i, j), TeosClampValue2, RTol)) {
354+
localCount++;
355+
}
356+
},
357+
numMismatches);
358+
359+
SpecVolH = createHostMirrorCopy(SpecVol);
360+
if (numMismatches != 0) {
361+
Err++;
362+
LOG_ERROR("EosTest: TEOS SpecVolClampingSalt isApprox FAIL, "
363+
"expected {}, got {} with {} mismatches",
364+
TeosClampValue2, SpecVolH(1, 1), numMismatches);
365+
}
366+
if (Err == 0) {
367+
LOG_INFO("EosTest SpecVolClampingSalt TEOS-10: PASS");
368+
}
369+
370+
/// Test with an ivalid poly75t temperature value third
371+
deepCopy(SArray, 35.0);
372+
deepCopy(TArray, 100.0);
373+
deepCopy(PArray, 400.0);
374+
deepCopy(TestEos->SpecVol, 0.0);
375+
376+
/// Compute specific volume
377+
TestEos->computeSpecVol(TArray, SArray, PArray);
378+
379+
/// Check all array values against expected value
380+
numMismatches = 0;
381+
SpecVol = TestEos->SpecVol;
382+
parallelReduce(
383+
"CheckSpecVolMatrix-Teos", {Mesh->NCellsAll, NVertLevels},
384+
KOKKOS_LAMBDA(int i, int j, int &localCount) {
385+
if (!isApprox(SpecVol(i, j), TeosClampValue3, RTol)) {
386+
localCount++;
387+
}
388+
},
389+
numMismatches);
390+
391+
SpecVolH = createHostMirrorCopy(SpecVol);
392+
if (numMismatches != 0) {
393+
Err++;
394+
LOG_ERROR("EosTest: TEOS SpecVolClampingTemp isApprox FAIL, "
395+
"expected {}, got {} with {} mismatches",
396+
TeosClampValue3, SpecVolH(1, 1), numMismatches);
397+
}
398+
if (Err == 0) {
399+
LOG_INFO("EosTest SpecVolClampingTemp TEOS-10: PASS");
400+
}
401+
402+
return Err;
403+
}
404+
288405
/// Finalize and clean up all test infrastructure
289406
void finalizeEosTest() {
290407
HorzMesh::clear();
@@ -296,8 +413,7 @@ void finalizeEosTest() {
296413

297414
/// Test that the external GSW-C library returns the expected specific volume
298415
int checkValueGswcSpecVol() {
299-
int Err = 0;
300-
const Real RTol = 1e-10;
416+
int Err = 0;
301417

302418
/// Get specific volume from GSW-C library
303419
double SpecVol = gsw_specvol(Sa, Ct, P);
@@ -317,8 +433,8 @@ int checkValueGswcSpecVol() {
317433

318434
/// Test that the external GSW-C library returns the expected specific volume
319435
int checkValueCtFreezing() {
320-
int Err = 0;
321-
const Real RTol = 1e-10;
436+
int Err = 0;
437+
322438
Teos10Eos TestEos(1);
323439
// TestEos->EosChoice = EosType::Teos10Eos;
324440
constexpr Real SaturationFrac = 0.0;
@@ -344,14 +460,19 @@ int checkValueCtFreezing() {
344460
}
345461

346462
// the main test (all in one to have the same log)
347-
// Single value test:
348-
// --> test calls the external GSW-C library
463+
// Single value tests:
464+
// --> one test calls the external GSW-C library for specific volume
465+
// --> next test calls the external GSW-C library for freezing value
349466
// and compares the specific volume to the published value
350467
// Full array tests:
351468
// --> one tests the value on a Eos with linear option
352469
// --> next checks the value on a Eos with linear displaced option
353470
// --> next checks the value on a Eos with TEOS-10 option
354471
// --> next checks the value on a Eos with TEOS-10 displaced option
472+
// Clamping test:
473+
// --> test check the clamping works for values that (1) don't need
474+
// clamping, (2) need claiming on the salinity, and (3) need clamping
475+
// on the temperature
355476
int eosTest(const std::string &MeshFile = "OmegaMesh.nc") {
356477
int Err = initEosTest(MeshFile);
357478
if (Err != 0) {
@@ -369,6 +490,9 @@ int eosTest(const std::string &MeshFile = "OmegaMesh.nc") {
369490
Err += testEosTeos10();
370491
Err += testEosTeos10Displaced();
371492

493+
LOG_INFO("Clamping check:");
494+
Err += testEosTeos10Clamping();
495+
372496
if (Err == 0) {
373497
LOG_INFO("EosTest: Successful completion");
374498
}

0 commit comments

Comments
 (0)