Skip to content

Commit 7d9061c

Browse files
committed
EAMxx: diagnose vertical derivatives
1 parent e09a648 commit 7d9061c

File tree

9 files changed

+442
-0
lines changed

9 files changed

+442
-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
@@ -19,6 +19,7 @@ nav:
1919
- 'Overview': 'user/diags/index.md'
2020
- 'Field contraction diagnostics': 'user/diags/field_contraction.md'
2121
- 'Conditional sampling': 'user/diags/conditional_sampling.md'
22+
- 'Vertical divergence diagnostics': 'user/diags/vert_derivative.md'
2223
- 'Presentations': 'user/presentations.md'
2324
- 'IO Aliases': 'user/io_aliases.md'
2425
- 'Developer Guide':

components/eamxx/src/diagnostics/CMakeLists.txt

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

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

@@ -57,6 +58,7 @@ inline void register_diagnostics () {
5758
diag_factory.register_product("AtmBackTendDiag",&create_atmosphere_diagnostic<AtmBackTendDiag>);
5859
diag_factory.register_product("HorizAvgDiag",&create_atmosphere_diagnostic<HorizAvgDiag>);
5960
diag_factory.register_product("VertContractDiag",&create_atmosphere_diagnostic<VertContractDiag>);
61+
diag_factory.register_product("VertDerivativeDiag",&create_atmosphere_diagnostic<VertDerivativeDiag>);
6062
diag_factory.register_product("ZonalAvgDiag",&create_atmosphere_diagnostic<ZonalAvgDiag>);
6163
diag_factory.register_product("ConditionalSampling",&create_atmosphere_diagnostic<ConditionalSampling>);
6264
}

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: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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 world comm
34+
ekat::Comm comm(MPI_COMM_WORLD);
35+
36+
// A time stamp
37+
util::TimeStamp t0({2024, 1, 1}, {0, 0, 0});
38+
39+
// Create a grids manager - single column for these tests
40+
constexpr int nlevs = 150;
41+
const int ngcols = 195 * comm.size();
42+
43+
auto gm = create_gm(comm, ngcols, nlevs);
44+
auto grid = gm->get_grid("Physics");
45+
46+
// Input (randomized) qc
47+
FieldLayout scalar1d_layout{{LEV}, {nlevs}};
48+
FieldLayout scalar2d_layout{{COL, LEV}, {ngcols, nlevs}};
49+
50+
FieldIdentifier fin2_fid("qc", scalar2d_layout, kg / kg, grid->name());
51+
FieldIdentifier dp_fid("pseudo_density", scalar2d_layout, Pa, grid->name());
52+
53+
Field fin2(fin2_fid);
54+
Field dp(dp_fid);
55+
56+
fin2.allocate_view();
57+
dp.allocate_view();
58+
59+
// Construct random number generator stuff
60+
using RPDF = std::uniform_real_distribution<Real>;
61+
RPDF pdf(sp(0.0), sp(200.0));
62+
auto engine = scream::setup_random_test();
63+
64+
// Construct the diagnostics factory
65+
std::map<std::string, std::shared_ptr<AtmosphereDiagnostic>> diags;
66+
auto &diag_factory = AtmosphereDiagnosticFactory::instance();
67+
register_diagnostics();
68+
69+
ekat::ParameterList params;
70+
// instantiation works because we don't do anything in the constructor
71+
auto bad_diag = diag_factory.create("VertDerivativeDiag", comm, params);
72+
SECTION("bad_diag") {
73+
// this will throw because no field_name was provided
74+
REQUIRE_THROWS(bad_diag->set_grids(gm));
75+
}
76+
77+
fin2.get_header().get_tracking().update_time_stamp(t0);
78+
dp.get_header().get_tracking().update_time_stamp(t0);
79+
randomize(fin2, engine, pdf);
80+
randomize(dp, engine, pdf);
81+
82+
// Create and set up the diagnostic
83+
params.set("grid_name", grid->name());
84+
params.set<std::string>("field_name", "qc");
85+
SECTION("bad_diag_2") {
86+
// this will throw because no derivative_method was provided
87+
auto bad_diag_2 = diag_factory.create("VertDerivativeDiag", comm, params);
88+
REQUIRE_THROWS(bad_diag_2->set_grids(gm));
89+
}
90+
91+
SECTION("bad_diag_3") {
92+
// this will throw because bad derivative_method was provided
93+
params.set<std::string>("derivative_method", "xyz");
94+
auto bad_diag_3 = diag_factory.create("VertDerivativeDiag", comm, params);
95+
REQUIRE_THROWS(bad_diag_3->set_grids(gm));
96+
}
97+
98+
// dp_vert_derivative
99+
params.set<std::string>("derivative_method", "p");
100+
auto dp_vert_derivative = diag_factory.create("VertDerivativeDiag", comm, params);
101+
102+
dp_vert_derivative->set_grids(gm);
103+
104+
// Fields for manual calculation
105+
FieldIdentifier diag1_fid("qc_vert_derivative_manual", scalar2d_layout, kg / kg, grid->name());
106+
Field diag1_m(diag1_fid);
107+
diag1_m.allocate_view();
108+
109+
SECTION("dp_vert_derivative") {
110+
// calculate dp_vert_div manually
111+
dp.sync_to_host();
112+
fin2.sync_to_host();
113+
diag1_m.sync_to_host();
114+
115+
auto dp_v = dp.get_view<const Real **, Host>();
116+
auto fin2_v = fin2.get_view<const Real **, Host>();
117+
auto diag1_m_v = diag1_m.get_view<Real **, Host>();
118+
119+
for (int icol = 0; icol < ngcols; ++icol) {
120+
// handle boundary point
121+
diag1_m_v(icol, 0) = (fin2_v(icol, 1) - fin2_v(icol, 0)) / dp_v(icol, 0);
122+
// interior points
123+
for (int ilev = 1; ilev < nlevs - 1; ++ilev) {
124+
auto fa1 = (fin2_v(icol, ilev + 1) * dp_v(icol, ilev) +
125+
fin2_v(icol, ilev) * dp_v(icol, ilev + 1)) /
126+
(dp_v(icol, ilev) + dp_v(icol, ilev + 1));
127+
auto fa0 = (fin2_v(icol, ilev) * dp_v(icol, ilev - 1) +
128+
fin2_v(icol, ilev - 1) * dp_v(icol, ilev)) /
129+
(dp_v(icol, ilev - 1) + dp_v(icol, ilev));
130+
diag1_m_v(icol, ilev) = (fa1 - fa0) / dp_v(icol, ilev);
131+
}
132+
// handle boundary point
133+
diag1_m_v(icol, nlevs - 1) =
134+
(fin2_v(icol, nlevs - 1) - fin2_v(icol, nlevs - 2)) / (dp_v(icol, nlevs - 1));
135+
}
136+
diag1_m.sync_to_dev();
137+
138+
// Calculate weighted avg through diagnostics
139+
dp_vert_derivative->set_required_field(fin2);
140+
dp_vert_derivative->set_required_field(dp);
141+
dp_vert_derivative->initialize(t0, RunType::Initial);
142+
dp_vert_derivative->compute_diagnostic();
143+
auto dp_vert_derivative_f = dp_vert_derivative->get_diagnostic();
144+
145+
REQUIRE(views_are_equal(dp_vert_derivative_f, diag1_m));
146+
}
147+
148+
// TODO: add SECTION("dz_vert_derivative")
149+
}
150+
151+
} // namespace scream

0 commit comments

Comments
 (0)