Skip to content

Commit 66a5803

Browse files
committed
Merge branch 'feature-eamxx-zonal_average_diagnostic' into next (PR #7261)
Adds a zonal average average diagnostic (zonal_avg), where the given field is averaged in the zonal direction over uniform latitude ranges. The user can specify how many ranges to use via the num_lat_vals parameter. The diagnostic uses the CMP tag with name "bin", so I/O is updated to specifying dimension name binN where N is the number of bins. [BFB]
2 parents a2b8811 + ad24954 commit 66a5803

File tree

11 files changed

+521
-10
lines changed

11 files changed

+521
-10
lines changed

components/eamxx/docs/user/diags/field_contraction.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
In EAMxx, we can automatically calculate field reductions
44
across the horizontal columns and across the model vertical levels.
55
We call these horizontal and vertical reductions.
6+
We can also automatically calculate zonal averages.
67

78
## Horizontal reduction
89

@@ -58,6 +59,24 @@ The supported weighting options for now are
5859
In the case of `pseudo_density`, the weighting is scaled by 1/g,
5960
where g is the gravitational acceleration, in units of m/s$^2$.
6061

62+
## Zonal reduction
63+
64+
We currently have a utility to calculate zonal averages online.
65+
To select the zonal average, you need to suffix
66+
a field name `X` with `_zonal_avg` and the
67+
number of bins `Y` as `_Y_bins`. All zonal averages are calculated
68+
using the area fraction in each bin as the weight.
69+
70+
For 180 latitude bins, the bins are defined
71+
as follows: [-90, -89), [-89, -88), ..., [89, 90).
72+
For 90 latitude bins, the bins are defined as follows:
73+
[-90, -88), [-88, -86), ..., [88, 90).
74+
And so on...
75+
76+
| Reduction | Weight | Description |
77+
| --------- | ------ | ----------- |
78+
| `X_zonal_avg_Y_bins` | Area fraction | Average across the zonal direction |
79+
6180
## Example
6281

6382
```yaml
@@ -77,6 +96,8 @@ fields:
7796
- T_mid_vert_sum_dz_weighted # K * m
7897
- T_mid_vert_avg # K
7998
- T_mid_vert_sum # K
99+
- T_mid_zonal_avg_180_bins # K
100+
- T_mid_zonal_avg_90_bins # K
80101
output_control:
81102
frequency: 1
82103
frequency_units: nmonths

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+
zonal_avg.cpp
2627
)
2728

2829
add_library(diagnostics ${DIAGNOSTIC_SRCS})

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/zonal_avg.hpp"
2930

3031
namespace scream {
3132

@@ -55,6 +56,7 @@ inline void register_diagnostics () {
5556
diag_factory.register_product("AtmBackTendDiag",&create_atmosphere_diagnostic<AtmBackTendDiag>);
5657
diag_factory.register_product("HorizAvgDiag",&create_atmosphere_diagnostic<HorizAvgDiag>);
5758
diag_factory.register_product("VertContractDiag",&create_atmosphere_diagnostic<VertContractDiag>);
59+
diag_factory.register_product("ZonalAvgDiag",&create_atmosphere_diagnostic<ZonalAvgDiag>);
5860
}
5961

6062
} // namespace scream

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,6 @@ CreateDiagTest(horiz_avg "horiz_avg_test.cpp")
7777

7878
# Test for vertical contraction
7979
CreateDiagTest(vert_contract "vert_contract_test.cpp")
80+
81+
# Test zonal averaging
82+
CreateDiagTest(zonal_avg "zonal_avg_test.cpp" MPI_RANKS 1 ${SCREAM_TEST_MAX_RANKS})
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#include "catch2/catch.hpp"
2+
#include "diagnostics/register_diagnostics.hpp"
3+
#include "share/field/field_utils.hpp"
4+
#include "share/grid/mesh_free_grids_manager.hpp"
5+
#include "share/util/eamxx_setup_random_test.hpp"
6+
#include "share/util/eamxx_universal_constants.hpp"
7+
8+
namespace scream {
9+
10+
std::shared_ptr<GridsManager> create_gm(const ekat::Comm &comm, const int ncols, const int nlevs) {
11+
const int num_global_cols = ncols * comm.size();
12+
13+
using vos_t = std::vector<std::string>;
14+
ekat::ParameterList gm_params;
15+
gm_params.set("grids_names", vos_t{"Point Grid"});
16+
auto &pl = gm_params.sublist("Point Grid");
17+
pl.set<std::string>("type", "point_grid");
18+
pl.set("aliases", vos_t{"Physics"});
19+
pl.set<int>("number_of_global_columns", num_global_cols);
20+
pl.set<int>("number_of_vertical_levels", nlevs);
21+
22+
auto gm = create_mesh_free_grids_manager(comm, gm_params);
23+
gm->build_grids();
24+
25+
return gm;
26+
}
27+
28+
TEST_CASE("zonal_avg") {
29+
using namespace ShortFieldTagsNames;
30+
using namespace ekat::units;
31+
32+
// A numerical tolerance
33+
auto tol = std::numeric_limits<Real>::epsilon() * 100;
34+
35+
// A world comm
36+
ekat::Comm comm(MPI_COMM_WORLD);
37+
38+
// A time stamp
39+
util::TimeStamp t0({2024, 1, 1}, {0, 0, 0});
40+
41+
// Create a grids manager - single column for these tests
42+
constexpr int nlevs = 3;
43+
constexpr int dim3 = 4;
44+
const int ngcols = 6 * comm.size();
45+
const int nlats = 4;
46+
47+
auto gm = create_gm(comm, ngcols, nlevs);
48+
auto grid = gm->get_grid("Physics");
49+
50+
Field area = grid->get_geometry_data("area");
51+
auto area_view_h = area.get_view<const Real *, Host>();
52+
53+
// Set latitude values
54+
Field lat = gm->get_grid_nonconst("Physics")->create_geometry_data(
55+
"lat", grid->get_2d_scalar_layout(), Units::nondimensional());
56+
auto lat_view_h = lat.get_view<Real *, Host>();
57+
const Real lat_delta = sp(180.0) / nlats;
58+
std::vector<Real> zonal_areas(nlats, 0.0);
59+
for (int i = 0; i < ngcols; i++) {
60+
lat_view_h(i) = sp(-90.0) + (i % nlats + sp(0.5)) * lat_delta;
61+
zonal_areas[i % nlats] += area_view_h[i];
62+
}
63+
lat.sync_to_dev();
64+
65+
// Input (randomized) qc
66+
FieldLayout scalar1d_layout{{COL}, {ngcols}};
67+
FieldLayout scalar2d_layout{{COL, LEV}, {ngcols, nlevs}};
68+
FieldLayout scalar3d_layout{{COL, CMP, LEV}, {ngcols, dim3, nlevs}};
69+
70+
FieldIdentifier qc1_id("qc", scalar1d_layout, kg / kg, grid->name());
71+
FieldIdentifier qc2_fid("qc", scalar2d_layout, kg / kg, grid->name());
72+
FieldIdentifier qc3_fid("qc", scalar3d_layout, kg / kg, grid->name());
73+
74+
Field qc1(qc1_id);
75+
Field qc2(qc2_fid);
76+
Field qc3(qc3_fid);
77+
78+
qc1.allocate_view();
79+
qc2.allocate_view();
80+
qc3.allocate_view();
81+
82+
// Construct random number generator stuff
83+
using RPDF = std::uniform_real_distribution<Real>;
84+
RPDF pdf(sp(0.0), sp(200.0));
85+
auto engine = scream::setup_random_test();
86+
87+
// Set time for qc and randomize its values
88+
qc1.get_header().get_tracking().update_time_stamp(t0);
89+
qc2.get_header().get_tracking().update_time_stamp(t0);
90+
qc3.get_header().get_tracking().update_time_stamp(t0);
91+
randomize(qc1, engine, pdf);
92+
randomize(qc2, engine, pdf);
93+
randomize(qc3, engine, pdf);
94+
95+
// Construct the Diagnostics
96+
std::map<std::string, std::shared_ptr<AtmosphereDiagnostic>> diags;
97+
auto &diag_factory = AtmosphereDiagnosticFactory::instance();
98+
register_diagnostics();
99+
100+
// Create and set up the diagnostic
101+
ekat::ParameterList params;
102+
REQUIRE_THROWS(diag_factory.create("ZonalAvgDiag", comm,
103+
params)); // Bad construction
104+
105+
params.set("grid_name", grid->name());
106+
REQUIRE_THROWS(diag_factory.create("ZonalAvgDiag", comm,
107+
params)); // Still no field_name
108+
109+
params.set<std::string>("field_name", "qc");
110+
REQUIRE_THROWS(diag_factory.create("ZonalAvgDiag", comm,
111+
params)); // Still no number_of_zonal_bins
112+
113+
params.set<std::string>("number_of_zonal_bins", std::to_string(nlats));
114+
// Now we should be good to go...
115+
auto diag1 = diag_factory.create("ZonalAvgDiag", comm, params);
116+
auto diag2 = diag_factory.create("ZonalAvgDiag", comm, params);
117+
auto diag3 = diag_factory.create("ZonalAvgDiag", comm, params);
118+
diag1->set_grids(gm);
119+
diag2->set_grids(gm);
120+
diag3->set_grids(gm);
121+
122+
// Test the zonal average of qc1
123+
diag1->set_required_field(qc1);
124+
diag1->initialize(t0, RunType::Initial);
125+
diag1->compute_diagnostic();
126+
auto diag1_field = diag1->get_diagnostic();
127+
128+
// Manual calculation
129+
const std::string bin_dim_name = diag1_field.get_header().get_identifier().get_layout().name(0);
130+
FieldLayout diag0_layout({CMP}, {nlats}, {bin_dim_name});
131+
FieldIdentifier diag0_id("qc_zonal_avg_manual", diag0_layout, kg / kg, grid->name());
132+
Field diag0_field(diag0_id);
133+
diag0_field.allocate_view();
134+
135+
// calculate the zonal average
136+
auto qc1_view_h = qc1.get_view<const Real *, Host>();
137+
auto diag0_view_h = diag0_field.get_view<Real *, Host>();
138+
for (int i = 0; i < ngcols; i++) {
139+
const int nlat = i % nlats;
140+
diag0_view_h(nlat) += area_view_h(i) / zonal_areas[nlat] * qc1_view_h(i);
141+
}
142+
diag0_field.sync_to_dev();
143+
144+
// Compare
145+
REQUIRE(views_are_equal(diag1_field, diag0_field));
146+
147+
// Try other known cases
148+
// Set qc1_v to 1.0 to get zonal averages of 1.0/nlats
149+
const Real zavg1 = sp(1.0);
150+
qc1.deep_copy(zavg1);
151+
diag1->compute_diagnostic();
152+
auto diag1_view_host = diag1_field.get_view<Real *, Host>();
153+
for (int nlat = 0; nlat < nlats; nlat++) {
154+
REQUIRE_THAT(diag1_view_host(nlat), Catch::Matchers::WithinRel(zavg1, tol));
155+
}
156+
157+
// other diags
158+
// Set qc2_v to 5.0 to get weighted average of 5.0
159+
const Real zavg2 = sp(5.0);
160+
qc2.deep_copy(zavg2);
161+
diag2->set_required_field(qc2);
162+
diag2->initialize(t0, RunType::Initial);
163+
diag2->compute_diagnostic();
164+
auto diag2_field = diag2->get_diagnostic();
165+
166+
auto diag2_view_host = diag2_field.get_view<Real **, Host>();
167+
for (int i = 0; i < nlevs; ++i) {
168+
for (int nlat = 0; nlat < nlats; nlat++) {
169+
REQUIRE_THAT(diag2_view_host(nlat, i), Catch::Matchers::WithinRel(zavg2, tol));
170+
}
171+
}
172+
173+
// Try a random case with qc3
174+
FieldLayout diag3m_layout({CMP, CMP, LEV}, {nlats, dim3, nlevs},
175+
{bin_dim_name, e2str(CMP), e2str(LEV)});
176+
FieldIdentifier diag3m_id("qc_zonal_avg_manual", diag3m_layout, kg / kg, grid->name());
177+
Field diag3m_field(diag3m_id);
178+
diag3m_field.allocate_view();
179+
auto qc3_view_h = qc3.get_view<Real ***, Host>();
180+
auto diag3m_view_h = diag3m_field.get_view<Real ***, Host>();
181+
for (int i = 0; i < ngcols; i++) {
182+
const int nlat = i % nlats;
183+
for (int j = 0; j < dim3; j++) {
184+
for (int k = 0; k < nlevs; k++) {
185+
diag3m_view_h(nlat, j, k) += area_view_h(i) / zonal_areas[nlat] * qc3_view_h(i, j, k);
186+
}
187+
}
188+
}
189+
diag3m_field.sync_to_dev();
190+
diag3->set_required_field(qc3);
191+
diag3->initialize(t0, RunType::Initial);
192+
diag3->compute_diagnostic();
193+
auto diag3_field = diag3->get_diagnostic();
194+
REQUIRE(views_are_equal(diag3_field, diag3m_field));
195+
}
196+
197+
} // namespace scream

0 commit comments

Comments
 (0)