Skip to content

Commit 6f5ea50

Browse files
authored
Merge branch 'mahf708/eamxx/vert-divergence' (PR #7491)
[BFB]]
2 parents f798249 + 49c5783 commit 6f5ea50

File tree

9 files changed

+418
-0
lines changed

9 files changed

+418
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Vertical derivative diagnostics
2+
3+
We can automatically calculate the vertical derivative of a given field
4+
with respect to a vertical coordinate (pressure or height).
5+
In other words, the vertical derivative is the derivative of the field
6+
along a vertical coordinate.
7+
To automatically get the derivative of a field X, we can request
8+
`X_pvert_derivative` or `X_zvert_derivative`
9+
in the output yaml files.
10+
11+
*WARNING: support for the dz-based derivative is experimental!*
12+
13+
## Brief description of algorithm
14+
15+
For simplicity, the field `X` can only have two dimensions
16+
(column and level). This covers most fields of interest, but
17+
notably does not cover higher-dimensioned fields in radiation and MAM.
18+
We calculate the derivative by finding the difference in the
19+
input field `X`, which must be provided on midpoints, by interpolating
20+
its values at interfaces weighted by the pressure differential.
21+
That is, for a pressure-based derivative
22+
23+
$$
24+
\nabla_{p} X = \frac{\Delta X}{\Delta p}
25+
$$
26+
27+
and for height-based derivative
28+
29+
$$
30+
\nabla_{p} X = \frac{\Delta X}{\Delta z}
31+
$$
32+
33+
where
34+
35+
$$
36+
\Delta X = \tilde{X}_{i,k+1} - \tilde{X}_{i,k}
37+
$$
38+
39+
$$
40+
\tilde{X}_{i,k+1} = \frac{\left(
41+
X_{i,k} \times \Delta p_{i,k+1} +
42+
X_{i,k+1} \times \Delta p_{i,k}
43+
\right)}
44+
{\left(
45+
\Delta p_{i,k+1} + \Delta p_{i,k}
46+
\right)}
47+
$$
48+
49+
Here, $\Delta p$ is the "pseudo density" pressure differential in Pa;
50+
and $\Delta z$ is the height of the layer in m
51+
52+
Note that the definition $\tilde{X}_{i,k+1}$ assumes that all
53+
fields are weighted by the pressure differential, which is a
54+
prognostic parameter of the dynamics core solver. Also note that
55+
when $\Delta p_{i,k+1} \gg \Delta p_{i,k}$,
56+
$\tilde{X}_{i,k+1}$ is closer to $X_{i,k}$ than $X_{i,k+1}$.
57+
58+
## Example
59+
60+
```yaml
61+
%YAML 1.1
62+
---
63+
filename_prefix: monthly.outputs
64+
averaging_type: average
65+
max_snapshots_per_file: 1
66+
fields:
67+
physics_pg2:
68+
field_names:
69+
# in this example, we use T_mid in units of K
70+
- T_mid_pvert_derivative # K / Pa
71+
- T_mid_zvert_derivative # K / m
72+
output_control:
73+
frequency: 1
74+
frequency_units: nmonths
75+
```

components/eamxx/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ nav:
2020
- 'Field contraction': 'user/diags/field_contraction.md'
2121
- 'Conditional sampling': 'user/diags/conditional_sampling.md'
2222
- 'Binary arithmetics': 'user/diags/binary_ops.md'
23+
- 'Vertical derivative': 'user/diags/vert_derivative.md'
2324
- 'Presentations': 'user/presentations.md'
2425
- 'IO Aliases': 'user/io_aliases.md'
2526
- 'Developer Guide':

components/eamxx/src/diagnostics/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ set(DIAGNOSTIC_SRCS
2424
water_path.cpp
2525
wind_speed.cpp
2626
vert_contract.cpp
27+
vert_derivative.cpp
2728
zonal_avg.cpp
2829
conditional_sampling.cpp
2930
)

components/eamxx/src/diagnostics/register_diagnostics.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "diagnostics/atm_backtend.hpp"
2727
#include "diagnostics/horiz_avg.hpp"
2828
#include "diagnostics/vert_contract.hpp"
29+
#include "diagnostics/vert_derivative.hpp"
2930
#include "diagnostics/zonal_avg.hpp"
3031
#include "diagnostics/conditional_sampling.hpp"
3132
#include "diagnostics/binary_ops.hpp"
@@ -58,6 +59,7 @@ inline void register_diagnostics () {
5859
diag_factory.register_product("AtmBackTendDiag",&create_atmosphere_diagnostic<AtmBackTendDiag>);
5960
diag_factory.register_product("HorizAvgDiag",&create_atmosphere_diagnostic<HorizAvgDiag>);
6061
diag_factory.register_product("VertContractDiag",&create_atmosphere_diagnostic<VertContractDiag>);
62+
diag_factory.register_product("VertDerivativeDiag",&create_atmosphere_diagnostic<VertDerivativeDiag>);
6163
diag_factory.register_product("ZonalAvgDiag",&create_atmosphere_diagnostic<ZonalAvgDiag>);
6264
diag_factory.register_product("ConditionalSampling",&create_atmosphere_diagnostic<ConditionalSampling>);
6365
diag_factory.register_product("BinaryOpsDiag", &create_atmosphere_diagnostic<BinaryOpsDiag>);

components/eamxx/src/diagnostics/tests/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ CreateDiagTest(horiz_avg "horiz_avg_test.cpp" MPI_RANKS 1 ${SCREAM_TEST_MAX_RANK
7878
# Test for vertical contraction
7979
CreateDiagTest(vert_contract "vert_contract_test.cpp" MPI_RANKS 1 ${SCREAM_TEST_MAX_RANKS})
8080

81+
# Test for vertical derivative
82+
CreateDiagTest(vert_derivative "vert_derivative_test.cpp")
83+
8184
# Test zonal averaging
8285
CreateDiagTest(zonal_avg zonal_avg_test.cpp MPI_RANKS 1 ${SCREAM_TEST_MAX_RANKS})
8386

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#include "catch2/catch.hpp"
2+
#include "diagnostics/register_diagnostics.hpp"
3+
#include "physics/share/physics_constants.hpp"
4+
#include "share/field/field_utils.hpp"
5+
#include "share/grid/mesh_free_grids_manager.hpp"
6+
#include "share/util/eamxx_setup_random_test.hpp"
7+
#include "share/util/eamxx_universal_constants.hpp"
8+
9+
namespace scream {
10+
11+
std::shared_ptr<GridsManager> create_gm(const ekat::Comm &comm, const int ncols, const int nlevs) {
12+
const int num_global_cols = ncols * comm.size();
13+
14+
using vos_t = std::vector<std::string>;
15+
ekat::ParameterList gm_params;
16+
gm_params.set("grids_names", vos_t{"Point Grid"});
17+
auto &pl = gm_params.sublist("Point Grid");
18+
pl.set<std::string>("type", "point_grid");
19+
pl.set("aliases", vos_t{"Physics"});
20+
pl.set<int>("number_of_global_columns", num_global_cols);
21+
pl.set<int>("number_of_vertical_levels", nlevs);
22+
23+
auto gm = create_mesh_free_grids_manager(comm, gm_params);
24+
gm->build_grids();
25+
26+
return gm;
27+
}
28+
29+
TEST_CASE("vert_derivative") {
30+
using namespace ShortFieldTagsNames;
31+
using namespace ekat::units;
32+
33+
// A numerical tolerance
34+
// auto tol = std::numeric_limits<Real>::epsilon() * 100;
35+
36+
// A world comm
37+
ekat::Comm comm(MPI_COMM_WORLD);
38+
39+
// A time stamp
40+
util::TimeStamp t0({2024, 1, 1}, {0, 0, 0});
41+
42+
// Create a grids manager - single column for these tests
43+
constexpr int nlevs = 150;
44+
const int ngcols = 195 * comm.size();
45+
46+
auto gm = create_gm(comm, ngcols, nlevs);
47+
auto grid = gm->get_grid("Physics");
48+
49+
// Input (randomized) qc
50+
FieldLayout scalar1d_layout{{LEV}, {nlevs}};
51+
FieldLayout scalar2d_layout{{COL, LEV}, {ngcols, nlevs}};
52+
53+
FieldIdentifier fin2_fid("qc", scalar2d_layout, kg / kg, grid->name());
54+
FieldIdentifier dp_fid("pseudo_density", scalar2d_layout, Pa, grid->name());
55+
56+
Field fin2(fin2_fid);
57+
Field dp(dp_fid);
58+
59+
fin2.allocate_view();
60+
dp.allocate_view();
61+
62+
// Construct random number generator stuff
63+
using RPDF = std::uniform_real_distribution<Real>;
64+
RPDF dp_pdf(10,2000);
65+
auto engine = scream::setup_random_test();
66+
67+
// Construct the diagnostics factory
68+
std::map<std::string, std::shared_ptr<AtmosphereDiagnostic>> diags;
69+
auto &diag_factory = AtmosphereDiagnosticFactory::instance();
70+
register_diagnostics();
71+
72+
ekat::ParameterList params;
73+
// instantiation works because we don't do anything in the constructor
74+
auto bad_diag = diag_factory.create("VertDerivativeDiag", comm, params);
75+
SECTION("bad_diag") {
76+
// this will throw because no field_name was provided
77+
REQUIRE_THROWS(bad_diag->set_grids(gm));
78+
}
79+
80+
fin2.get_header().get_tracking().update_time_stamp(t0);
81+
dp.get_header().get_tracking().update_time_stamp(t0);
82+
// instead of random fin2, let's just a predictable one:
83+
fin2.sync_to_host();
84+
auto fin2_v_host_assign = fin2.get_view<Real **, Host>();
85+
for (int icol = 0; icol < ngcols; ++icol) {
86+
for (int ilev = 0; ilev < nlevs; ++ilev) {
87+
fin2_v_host_assign(icol, ilev) = 1.0 + icol + ilev;
88+
}
89+
}
90+
fin2.sync_to_dev();
91+
randomize(dp, engine, dp_pdf);
92+
93+
// Create and set up the diagnostic
94+
params.set("grid_name", grid->name());
95+
params.set<std::string>("field_name", "qc");
96+
SECTION("bad_diag_2") {
97+
// this will throw because no derivative_method was provided
98+
auto bad_diag_2 = diag_factory.create("VertDerivativeDiag", comm, params);
99+
REQUIRE_THROWS(bad_diag_2->set_grids(gm));
100+
}
101+
102+
SECTION("bad_diag_3") {
103+
// this will throw because bad derivative_method was provided
104+
params.set<std::string>("derivative_method", "xyz");
105+
auto bad_diag_3 = diag_factory.create("VertDerivativeDiag", comm, params);
106+
REQUIRE_THROWS(bad_diag_3->set_grids(gm));
107+
}
108+
109+
// dp_vert_derivative
110+
params.set<std::string>("derivative_method", "p");
111+
auto dp_vert_derivative = diag_factory.create("VertDerivativeDiag", comm, params);
112+
113+
dp_vert_derivative->set_grids(gm);
114+
115+
// Fields for manual calculation
116+
FieldIdentifier diag1_fid("qc_vert_derivative_manual", scalar2d_layout, kg / kg, grid->name());
117+
Field diag1_m(diag1_fid);
118+
diag1_m.allocate_view();
119+
120+
SECTION("dp_vert_derivative") {
121+
// calculate dp_vert_div manually
122+
dp.sync_to_host();
123+
fin2.sync_to_host();
124+
diag1_m.sync_to_host();
125+
126+
auto dp_v = dp.get_view<const Real **, Host>();
127+
auto fin2_v = fin2.get_view<const Real **, Host>();
128+
auto diag1_m_v = diag1_m.get_view<Real **, Host>();
129+
130+
for (int icol = 0; icol < ngcols; ++icol) {
131+
for (int ilev = 0; ilev < nlevs; ++ilev) {
132+
auto fa1 = (ilev < nlevs - 1) ? (fin2_v(icol, ilev + 1) * dp_v(icol, ilev) +
133+
fin2_v(icol, ilev) * dp_v(icol, ilev + 1)) /
134+
(dp_v(icol, ilev) + dp_v(icol, ilev + 1)) : fin2_v(icol, nlevs-1);
135+
auto fa0 = (ilev > 0 ) ? (fin2_v(icol, ilev) * dp_v(icol, ilev - 1) +
136+
fin2_v(icol, ilev - 1) * dp_v(icol, ilev)) /
137+
(dp_v(icol, ilev - 1) + dp_v(icol, ilev)) : fin2_v(icol, 0);
138+
diag1_m_v(icol, ilev) = (fa1 - fa0) / dp_v(icol, ilev);
139+
}
140+
}
141+
diag1_m.sync_to_dev();
142+
143+
// Calculate weighted avg through diagnostics
144+
dp_vert_derivative->set_required_field(fin2);
145+
dp_vert_derivative->set_required_field(dp);
146+
dp_vert_derivative->initialize(t0, RunType::Initial);
147+
dp_vert_derivative->compute_diagnostic();
148+
auto dp_vert_derivative_f = dp_vert_derivative->get_diagnostic();
149+
150+
// Check that the two are the same manually using the tolerance
151+
dp_vert_derivative_f.sync_to_host();
152+
auto view_deriv_f_d = dp_vert_derivative_f.get_view<Real **, Host>();
153+
auto view_deriv_f_m = diag1_m.get_view<Real **, Host>();
154+
for (int icol = 0; icol < ngcols; ++icol) {
155+
for (int ilev = 0; ilev < nlevs; ++ilev) {
156+
// TODO: the calculations are sometimes diverging because of fp issues in the test on h100 gcc13
157+
// TODO: so a cop-out, just test if the abs diff is within 1e-5
158+
REQUIRE(std::abs(view_deriv_f_d(icol, ilev) - view_deriv_f_m(icol, ilev)) < 1e-5);
159+
}
160+
}
161+
}
162+
163+
// TODO: add SECTION("dz_vert_derivative")
164+
}
165+
166+
} // namespace scream

0 commit comments

Comments
 (0)