Skip to content

Commit 9f9d256

Browse files
committed
EAMxx: calculate vertical divergence in diagnostics
1 parent d2e0273 commit 9f9d256

File tree

9 files changed

+404
-0
lines changed

9 files changed

+404
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# vertical divergence diagnostics
2+
3+
We can automatically calculate the vertical divergence of a given field
4+
with respect to the vertical coordinate (pressure or height).
5+
In other words, the vertical divergence is the derivative of the field
6+
along a vertical coordinate.
7+
To automatically get the divergence of a field X, we can request
8+
`dX_dp_vert_divergence` or `X_dz_vert_divergence`
9+
in the output yaml files.
10+
11+
## Brief description
12+
13+
For simplicity, the field `X` can only have two dimensions
14+
(column and level). This covers most fields of interest, but
15+
notably does not cover higher-dimensioned fields.
16+
We also assume that the highest level (top of the atmosphere)
17+
has a zero derivative.
18+
The input field `X` must be defined on the midpoints, and its
19+
difference is defined as the difference between two consecutive
20+
levels, pointing downwards. That is, for a pressure coordinate,
21+
22+
$$
23+
\nabla_{\text{vert}} X = \frac{X_{i, k} - X_{i, k-1}}{\Delta p_{k}}
24+
$$
25+
26+
for all $k>0$ (at $k=0$, the derivative is zero).
27+
28+
## Example
29+
30+
```yaml
31+
%YAML 1.1
32+
---
33+
filename_prefix: monthly.outputs
34+
averaging_type: average
35+
max_snapshots_per_file: 1
36+
fields:
37+
physics_pg2:
38+
field_names:
39+
# in this example, we use T_mid in units of K
40+
- dT_mid_dp_vert_divergence # K / Pa
41+
- dT_mid_dz_vert_divergence # K / m
42+
output_control:
43+
frequency: 1
44+
frequency_units: nmonths
45+
```

components/eamxx/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ nav:
1818
- 'Diagnostics':
1919
- 'Overview': 'user/diags/index.md'
2020
- 'Field contraction diagnostics': 'user/diags/field_contraction.md'
21+
- 'Vertical divergence diagnostics': 'user/diags/vert_divergence.md'
2122
- 'Presentations': 'user/presentations.md'
2223
- 'Developer Guide':
2324
- 'Quick-start Guide': 'developer/dev_quickstart.md'

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_divergence.cpp
2627
zonal_avg.cpp
2728
)
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_divergence.hpp"
2930
#include "diagnostics/zonal_avg.hpp"
3031

3132
namespace scream {
@@ -56,6 +57,7 @@ inline void register_diagnostics () {
5657
diag_factory.register_product("AtmBackTendDiag",&create_atmosphere_diagnostic<AtmBackTendDiag>);
5758
diag_factory.register_product("HorizAvgDiag",&create_atmosphere_diagnostic<HorizAvgDiag>);
5859
diag_factory.register_product("VertContractDiag",&create_atmosphere_diagnostic<VertContractDiag>);
60+
diag_factory.register_product("VertDivergenceDiag",&create_atmosphere_diagnostic<VertDivergenceDiag>);
5961
diag_factory.register_product("ZonalAvgDiag",&create_atmosphere_diagnostic<ZonalAvgDiag>);
6062
}
6163

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,8 @@ CreateDiagTest(horiz_avg "horiz_avg_test.cpp")
7878
# Test for vertical contraction
7979
CreateDiagTest(vert_contract "vert_contract_test.cpp")
8080

81+
# Test for vertical divergence
82+
CreateDiagTest(vert_divergence "vert_divergence_test.cpp")
83+
8184
# Test zonal averaging
8285
CreateDiagTest(zonal_avg "zonal_avg_test.cpp" MPI_RANKS 1 ${SCREAM_TEST_MAX_RANKS})
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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,
12+
const int nlevs) {
13+
const int num_global_cols = ncols * comm.size();
14+
15+
using vos_t = std::vector<std::string>;
16+
ekat::ParameterList gm_params;
17+
gm_params.set("grids_names", vos_t{"Point Grid"});
18+
auto &pl = gm_params.sublist("Point Grid");
19+
pl.set<std::string>("type", "point_grid");
20+
pl.set("aliases", vos_t{"Physics"});
21+
pl.set<int>("number_of_global_columns", num_global_cols);
22+
pl.set<int>("number_of_vertical_levels", nlevs);
23+
24+
auto gm = create_mesh_free_grids_manager(comm, gm_params);
25+
gm->build_grids();
26+
27+
return gm;
28+
}
29+
30+
TEST_CASE("vert_divergence") {
31+
using namespace ShortFieldTagsNames;
32+
using namespace ekat::units;
33+
34+
// A world comm
35+
ekat::Comm comm(MPI_COMM_WORLD);
36+
37+
// A time stamp
38+
util::TimeStamp t0({2024, 1, 1}, {0, 0, 0});
39+
40+
// Create a grids manager - single column for these tests
41+
constexpr int nlevs = 150;
42+
const int ngcols = 195 * comm.size();
43+
44+
auto gm = create_gm(comm, ngcols, nlevs);
45+
auto grid = gm->get_grid("Physics");
46+
47+
// Input (randomized) qc
48+
FieldLayout scalar1d_layout{{LEV}, {nlevs}};
49+
FieldLayout scalar2d_layout{{COL, LEV}, {ngcols, nlevs}};
50+
51+
FieldIdentifier fin2_fid("qc", scalar2d_layout, kg / kg, grid->name());
52+
FieldIdentifier dp_fid("pseudo_density", scalar2d_layout, Pa, grid->name());
53+
FieldIdentifier dz_fid("dz", scalar2d_layout, m, grid->name());
54+
55+
Field fin2(fin2_fid);
56+
Field dp(dp_fid);
57+
// Field dz(dz_fid);
58+
59+
fin2.allocate_view();
60+
dp.allocate_view();
61+
// dz.allocate_view();
62+
63+
// Construct random number generator stuff
64+
using RPDF = std::uniform_real_distribution<Real>;
65+
RPDF pdf(sp(0.0), sp(200.0));
66+
auto engine = scream::setup_random_test();
67+
68+
// Construct the diagnostics factory
69+
std::map<std::string, std::shared_ptr<AtmosphereDiagnostic>> diags;
70+
auto &diag_factory = AtmosphereDiagnosticFactory::instance();
71+
register_diagnostics();
72+
73+
ekat::ParameterList params;
74+
// instantiation works because we don't do anything in the constructor
75+
auto bad_diag = diag_factory.create("VertDivergenceDiag", comm, params);
76+
SECTION("bad_diag") {
77+
// this will throw because no field_name was provided
78+
REQUIRE_THROWS(bad_diag->set_grids(gm));
79+
}
80+
81+
fin2.get_header().get_tracking().update_time_stamp(t0);
82+
dp.get_header().get_tracking().update_time_stamp(t0);
83+
// dz.get_header().get_tracking().update_time_stamp(t0);
84+
randomize(fin2, engine, pdf);
85+
randomize(dp, engine, pdf);
86+
// randomize(dz, engine, pdf);
87+
88+
// Create and set up the diagnostic
89+
params.set("grid_name", grid->name());
90+
params.set<std::string>("field_name", "qc");
91+
SECTION("bad_diag_2") {
92+
// this will throw because no divergence_method was provided
93+
auto bad_diag_2 = diag_factory.create("VertDivergenceDiag", comm, params);
94+
REQUIRE_THROWS(bad_diag_2->set_grids(gm));
95+
}
96+
97+
SECTION("bad_diag_3") {
98+
// this will throw because bad divergence_method was provided
99+
params.set<std::string>("divergence_method", "xyz");
100+
auto bad_diag_3 = diag_factory.create("VertDivergenceDiag", comm, params);
101+
REQUIRE_THROWS(bad_diag_3->set_grids(gm));
102+
}
103+
104+
// dp_vert_divergence
105+
params.set<std::string>("divergence_method", "dp");
106+
auto dp_vert_divergence = diag_factory.create("VertDivergenceDiag", comm, params);
107+
108+
109+
// TODO: for some reason the dz field keeps getting set to 0
110+
// TODO: as a workaround, just calculate dz here (sigh...)
111+
// TODO: commenting out all the dz stuff for now
112+
// // dz_vert_divergence
113+
// params.set<std::string>("divergence_method", "dz");
114+
// auto dz_vert_divergence = diag_factory.create("VertDivergenceDiag", comm, params);
115+
116+
dp_vert_divergence->set_grids(gm);
117+
// dz_vert_divergence->set_grids(gm);
118+
119+
// Fields for manual calculation
120+
FieldIdentifier diag1_fid("qc_vert_divergence_manual", scalar2d_layout, kg / kg, grid->name());
121+
Field diag1_m(diag1_fid);
122+
diag1_m.allocate_view();
123+
124+
SECTION("dp_vert_divergence") {
125+
// calculate dp_vert_div manually
126+
dp.sync_to_host();
127+
fin2.sync_to_host();
128+
diag1_m.sync_to_host();
129+
130+
auto dp_v = dp.get_view<const Real**, Host>();
131+
auto fin2_v = fin2.get_view<const Real**, Host>();
132+
auto diag1_m_v = diag1_m.get_view<Real**, Host>();
133+
134+
for (int icol = 0; icol < ngcols; ++icol) {
135+
// first one is zero by design
136+
diag1_m_v(icol, 0) = 0;
137+
for (int ilev = 1; ilev < nlevs; ++ilev) {
138+
diag1_m_v(icol, ilev) = (fin2_v(icol, ilev) - fin2_v(icol, ilev-1)) / dp_v(icol, ilev);
139+
}
140+
}
141+
diag1_m.sync_to_dev();
142+
143+
// Calculate weighted avg through diagnostics
144+
dp_vert_divergence->set_required_field(fin2);
145+
dp_vert_divergence->set_required_field(dp);
146+
dp_vert_divergence->initialize(t0, RunType::Initial);
147+
dp_vert_divergence->compute_diagnostic();
148+
auto dp_vert_divergence_f = dp_vert_divergence->get_diagnostic();
149+
150+
REQUIRE(views_are_equal(dp_vert_divergence_f, diag1_m));
151+
}
152+
153+
// TODO: add SECTION("dz_vert_divergence")
154+
155+
}
156+
157+
} // namespace scream
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#include "diagnostics/vert_divergence.hpp"
2+
3+
#include "share/util/eamxx_common_physics_functions.hpp"
4+
5+
namespace scream {
6+
7+
VertDivergenceDiag::VertDivergenceDiag(const ekat::Comm &comm,
8+
const ekat::ParameterList &params)
9+
: AtmosphereDiagnostic(comm, params) {}
10+
11+
void VertDivergenceDiag::set_grids(
12+
const std::shared_ptr<const GridsManager> grids_manager) {
13+
using namespace ShortFieldTagsNames;
14+
using namespace ekat::units;
15+
16+
const auto &fn = m_params.get<std::string>("field_name");
17+
const auto &gn = m_params.get<std::string>("grid_name");
18+
const auto g = grids_manager->get_grid(gn);
19+
20+
add_field<Required>(fn, gn);
21+
22+
// we support dp or dz
23+
m_divergence_method = m_params.get<std::string>("divergence_method");
24+
EKAT_REQUIRE_MSG(
25+
m_divergence_method == "dp" || m_divergence_method == "dz",
26+
"Error! VertDivergenceDiag only supports 'dp' or 'dz' as divergence_method.\n"
27+
" - divergence_method: " + m_divergence_method + "\n");
28+
m_diag_name = "d" + fn + m_divergence_method + "_vert_divergence";
29+
30+
auto scalar3d = g->get_3d_scalar_layout(true);
31+
if (m_divergence_method == "dp") {
32+
add_field<Required>("pseudo_density", scalar3d, Pa, gn);
33+
}
34+
else if (m_divergence_method == "dz")
35+
{
36+
add_field<Required>("pseudo_density", scalar3d, Pa, gn);
37+
add_field<Required>("qv", scalar3d, kg / kg, gn);
38+
add_field<Required>("p_mid", scalar3d, Pa, gn);
39+
add_field<Required>("T_mid", scalar3d, K, gn);
40+
}
41+
}
42+
43+
void VertDivergenceDiag::initialize_impl(const RunType /*run_type*/) {
44+
using namespace ShortFieldTagsNames;
45+
using namespace ekat::units;
46+
47+
const auto &f = get_fields_in().front();
48+
const auto &fid = f.get_header().get_identifier();
49+
const auto &layout = fid.get_layout();
50+
51+
// TODO: support higher-dimensioned input fields
52+
EKAT_REQUIRE_MSG(layout.rank() >= 2 && layout.rank() <= 2,
53+
"Error! Field rank not supported by VertDivergenceDiag.\n"
54+
" - field name: " + fid.name() + "\n"
55+
" - field layout: " + layout.to_string() + "\n");
56+
EKAT_REQUIRE_MSG(layout.tags().back() == LEV,
57+
"Error! VertDivergenceDiag diagnostic expects a layout ending "
58+
"with the 'LEV' tag.\n"
59+
" - field name : " + fid.name() + "\n"
60+
" - field layout: " + layout.to_string() + "\n");
61+
62+
ekat::units::Units diag_units = fid.get_units();
63+
64+
m_denominator = get_field_in("pseudo_density").clone("denominator");
65+
66+
if (m_divergence_method == "dp") {
67+
diag_units = fid.get_units() / Pa;
68+
}
69+
else if (m_divergence_method == "dz") {
70+
diag_units = fid.get_units() / m;
71+
}
72+
73+
FieldIdentifier d_fid(m_diag_name, layout, diag_units, fid.get_grid_name());
74+
m_diagnostic_output = Field(d_fid);
75+
m_diagnostic_output.allocate_view();
76+
}
77+
78+
79+
void VertDivergenceDiag::compute_diagnostic_impl() {
80+
const auto &f = get_fields_in().front();
81+
const auto &d = m_diagnostic_output;
82+
83+
// TODO: support higher-dimensioned input fields
84+
auto f2d = f.get_view<const Real**>();
85+
auto d2d = d.get_view<Real**>();
86+
87+
using CO = scream::ColumnOps<DefaultDevice,Real>;
88+
using KT = KokkosTypes<DefaultDevice>;
89+
using MT = typename KT::MemberType;
90+
using ESU = ekat::ExeSpaceUtils<typename KT::ExeSpace>;
91+
using PF = scream::PhysicsFunctions<DefaultDevice>;
92+
const int ncols = m_denominator.get_header().get_identifier().get_layout().dim(0);
93+
const int nlevs = m_denominator.get_header().get_identifier().get_layout().dim(1);
94+
const auto policy = ESU::get_default_team_policy(ncols, nlevs);
95+
96+
// get the denominator first
97+
if (m_divergence_method == "dp") {
98+
m_denominator.update(get_field_in("pseudo_density"), sp(1.0), sp(0.0));
99+
}
100+
else if (m_divergence_method == "dz") {
101+
// TODO: for some reason the dz field keeps getting set to 0
102+
// TODO: as a workaround, just calculate dz here (sigh...)
103+
// m_denominator.update(get_field_in("dz"), 1.0, 0.0);
104+
105+
auto dz_v = m_denominator.get_view<Real**>();
106+
auto dp_v = get_field_in("pseudo_density").get_view<const Real**>();
107+
auto pm_v = get_field_in("p_mid").get_view<const Real**>();
108+
auto tm_v = get_field_in("T_mid").get_view<const Real**>();
109+
auto qv_v = get_field_in("qv").get_view<const Real**>();
110+
Kokkos::parallel_for(
111+
"Compute dz for " + m_diagnostic_output.name(), policy, KOKKOS_LAMBDA(const MT &team) {
112+
const int icol = team.league_rank();
113+
auto dz_icol = ekat::subview(dz_v, icol);
114+
auto dp_icol = ekat::subview(dp_v, icol);
115+
auto pm_icol = ekat::subview(pm_v, icol);
116+
auto tm_icol = ekat::subview(tm_v, icol);
117+
auto qv_icol = ekat::subview(qv_v, icol);
118+
PF::calculate_dz(team, dp_icol, pm_icol, tm_icol, qv_icol, dz_icol);
119+
});
120+
}
121+
122+
auto d_v = m_denominator.get_view<Real**>();
123+
// calculate df / denominator (setting the first level to be zero)
124+
// TODO: perhaps we could calculate the values of the field at interfaces using col ops?
125+
// TODO: though not sure it will be worth it...
126+
Kokkos::parallel_for(
127+
"Compute df / denominator for " + m_diagnostic_output.name(), policy, KOKKOS_LAMBDA(const MT &team) {
128+
const int icol = team.league_rank();
129+
auto df_icol = ekat::subview(f2d, icol);
130+
auto dd_icol = ekat::subview(d2d, icol);
131+
auto dv_icol = ekat::subview(d_v, icol);
132+
133+
Kokkos::parallel_for(Kokkos::TeamVectorRange(team, nlevs), [&](const int ilev) {
134+
if (ilev == 0) {
135+
dd_icol(ilev) = 0;
136+
}
137+
else {
138+
dd_icol(ilev) = (df_icol(ilev) - df_icol(ilev-1)) / dv_icol(ilev);
139+
}
140+
});
141+
});
142+
}
143+
144+
} // namespace scream

0 commit comments

Comments
 (0)