Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions components/eamxx/docs/user/diags/vert_derivative.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Vertical derivative diagnostics

We can automatically calculate the vertical derivative of a given field
with respect to a vertical coordinate (pressure or height).
In other words, the vertical derivative is the derivative of the field
along a vertical coordinate.
To automatically get the derivative of a field X, we can request
`X_pvert_derivative` or `X_zvert_derivative`
in the output yaml files.

*WARNING: support for the dz-based derivative is experimental!*

## Brief description of algorithm

For simplicity, the field `X` can only have two dimensions
(column and level). This covers most fields of interest, but
notably does not cover higher-dimensioned fields in radiation and MAM.
We calculate the derivative by finding the difference in the
input field `X`, which must be provided on midpoints, by interpolating
its values at interfaces weighted by the pressure differential.
That is, for a pressure-based derivative

$$
\nabla_{p} X = \frac{\Delta X}{\Delta p}
$$

and for height-based derivative

$$
\nabla_{p} X = \frac{\Delta X}{\Delta z}
$$

where

$$
\Delta X = \tilde{X}_{i,k+1} - \tilde{X}_{i,k}
$$

$$
\tilde{X}_{i,k+1} = \frac{\left(
X_{i,k} \times \Delta p_{i,k+1} +
X_{i,k+1} \times \Delta p_{i,k}
\right)}
{\left(
\Delta p_{i,k+1} + \Delta p_{i,k}
\right)}
$$

Here, $\Delta p$ is the "pseudo density" pressure differential in Pa;
and $\Delta z$ is the height of the layer in m

Note that the definition $\tilde{X}_{i,k+1}$ assumes that all
fields are weighted by the pressure differential, which is a
prognostic parameter of the dynamics core solver. Also note that
when $\Delta p_{i,k+1} \gg \Delta p_{i,k}$,
$\tilde{X}_{i,k+1}$ is closer to $X_{i,k}$ than $X_{i,k+1}$.

## Example

```yaml
%YAML 1.1
---
filename_prefix: monthly.outputs
averaging_type: average
max_snapshots_per_file: 1
fields:
physics_pg2:
field_names:
# in this example, we use T_mid in units of K
- T_mid_pvert_derivative # K / Pa
- T_mid_zvert_derivative # K / m
output_control:
frequency: 1
frequency_units: nmonths
```
1 change: 1 addition & 0 deletions components/eamxx/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ nav:
- 'Field contraction': 'user/diags/field_contraction.md'
- 'Conditional sampling': 'user/diags/conditional_sampling.md'
- 'Binary arithmetics': 'user/diags/binary_ops.md'
- 'Vertical derivative': 'user/diags/vert_derivative.md'
- 'Presentations': 'user/presentations.md'
- 'IO Aliases': 'user/io_aliases.md'
- 'Developer Guide':
Expand Down
1 change: 1 addition & 0 deletions components/eamxx/src/diagnostics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ set(DIAGNOSTIC_SRCS
water_path.cpp
wind_speed.cpp
vert_contract.cpp
vert_derivative.cpp
zonal_avg.cpp
conditional_sampling.cpp
)
Expand Down
2 changes: 2 additions & 0 deletions components/eamxx/src/diagnostics/register_diagnostics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "diagnostics/atm_backtend.hpp"
#include "diagnostics/horiz_avg.hpp"
#include "diagnostics/vert_contract.hpp"
#include "diagnostics/vert_derivative.hpp"
#include "diagnostics/zonal_avg.hpp"
#include "diagnostics/conditional_sampling.hpp"
#include "diagnostics/binary_ops.hpp"
Expand Down Expand Up @@ -58,6 +59,7 @@ inline void register_diagnostics () {
diag_factory.register_product("AtmBackTendDiag",&create_atmosphere_diagnostic<AtmBackTendDiag>);
diag_factory.register_product("HorizAvgDiag",&create_atmosphere_diagnostic<HorizAvgDiag>);
diag_factory.register_product("VertContractDiag",&create_atmosphere_diagnostic<VertContractDiag>);
diag_factory.register_product("VertDerivativeDiag",&create_atmosphere_diagnostic<VertDerivativeDiag>);
diag_factory.register_product("ZonalAvgDiag",&create_atmosphere_diagnostic<ZonalAvgDiag>);
diag_factory.register_product("ConditionalSampling",&create_atmosphere_diagnostic<ConditionalSampling>);
diag_factory.register_product("BinaryOpsDiag", &create_atmosphere_diagnostic<BinaryOpsDiag>);
Expand Down
3 changes: 3 additions & 0 deletions components/eamxx/src/diagnostics/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ CreateDiagTest(horiz_avg "horiz_avg_test.cpp" MPI_RANKS 1 ${SCREAM_TEST_MAX_RANK
# Test for vertical contraction
CreateDiagTest(vert_contract "vert_contract_test.cpp" MPI_RANKS 1 ${SCREAM_TEST_MAX_RANKS})

# Test for vertical derivative
CreateDiagTest(vert_derivative "vert_derivative_test.cpp")

# Test zonal averaging
CreateDiagTest(zonal_avg zonal_avg_test.cpp MPI_RANKS 1 ${SCREAM_TEST_MAX_RANKS})

Expand Down
166 changes: 166 additions & 0 deletions components/eamxx/src/diagnostics/tests/vert_derivative_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#include "catch2/catch.hpp"
#include "diagnostics/register_diagnostics.hpp"
#include "physics/share/physics_constants.hpp"
#include "share/field/field_utils.hpp"
#include "share/grid/mesh_free_grids_manager.hpp"
#include "share/util/eamxx_setup_random_test.hpp"
#include "share/util/eamxx_universal_constants.hpp"

namespace scream {

std::shared_ptr<GridsManager> create_gm(const ekat::Comm &comm, const int ncols, const int nlevs) {
const int num_global_cols = ncols * comm.size();

using vos_t = std::vector<std::string>;
ekat::ParameterList gm_params;
gm_params.set("grids_names", vos_t{"Point Grid"});
auto &pl = gm_params.sublist("Point Grid");
pl.set<std::string>("type", "point_grid");
pl.set("aliases", vos_t{"Physics"});
pl.set<int>("number_of_global_columns", num_global_cols);
pl.set<int>("number_of_vertical_levels", nlevs);

auto gm = create_mesh_free_grids_manager(comm, gm_params);
gm->build_grids();

return gm;
}

TEST_CASE("vert_derivative") {
using namespace ShortFieldTagsNames;
using namespace ekat::units;

// A numerical tolerance
// auto tol = std::numeric_limits<Real>::epsilon() * 100;

// A world comm
ekat::Comm comm(MPI_COMM_WORLD);

// A time stamp
util::TimeStamp t0({2024, 1, 1}, {0, 0, 0});

// Create a grids manager - single column for these tests
constexpr int nlevs = 150;
const int ngcols = 195 * comm.size();

auto gm = create_gm(comm, ngcols, nlevs);
auto grid = gm->get_grid("Physics");

// Input (randomized) qc
FieldLayout scalar1d_layout{{LEV}, {nlevs}};
FieldLayout scalar2d_layout{{COL, LEV}, {ngcols, nlevs}};

FieldIdentifier fin2_fid("qc", scalar2d_layout, kg / kg, grid->name());
FieldIdentifier dp_fid("pseudo_density", scalar2d_layout, Pa, grid->name());

Field fin2(fin2_fid);
Field dp(dp_fid);

fin2.allocate_view();
dp.allocate_view();

// Construct random number generator stuff
using RPDF = std::uniform_real_distribution<Real>;
RPDF dp_pdf(10,2000);
auto engine = scream::setup_random_test();

// Construct the diagnostics factory
std::map<std::string, std::shared_ptr<AtmosphereDiagnostic>> diags;
auto &diag_factory = AtmosphereDiagnosticFactory::instance();
register_diagnostics();

ekat::ParameterList params;
// instantiation works because we don't do anything in the constructor
auto bad_diag = diag_factory.create("VertDerivativeDiag", comm, params);
SECTION("bad_diag") {
// this will throw because no field_name was provided
REQUIRE_THROWS(bad_diag->set_grids(gm));
}

fin2.get_header().get_tracking().update_time_stamp(t0);
dp.get_header().get_tracking().update_time_stamp(t0);
// instead of random fin2, let's just a predictable one:
fin2.sync_to_host();
auto fin2_v_host_assign = fin2.get_view<Real **, Host>();
for (int icol = 0; icol < ngcols; ++icol) {
for (int ilev = 0; ilev < nlevs; ++ilev) {
fin2_v_host_assign(icol, ilev) = 1.0 + icol + ilev;
}
}
fin2.sync_to_dev();
randomize(dp, engine, dp_pdf);

// Create and set up the diagnostic
params.set("grid_name", grid->name());
params.set<std::string>("field_name", "qc");
SECTION("bad_diag_2") {
// this will throw because no derivative_method was provided
auto bad_diag_2 = diag_factory.create("VertDerivativeDiag", comm, params);
REQUIRE_THROWS(bad_diag_2->set_grids(gm));
}

SECTION("bad_diag_3") {
// this will throw because bad derivative_method was provided
params.set<std::string>("derivative_method", "xyz");
auto bad_diag_3 = diag_factory.create("VertDerivativeDiag", comm, params);
REQUIRE_THROWS(bad_diag_3->set_grids(gm));
}

// dp_vert_derivative
params.set<std::string>("derivative_method", "p");
auto dp_vert_derivative = diag_factory.create("VertDerivativeDiag", comm, params);

dp_vert_derivative->set_grids(gm);

// Fields for manual calculation
FieldIdentifier diag1_fid("qc_vert_derivative_manual", scalar2d_layout, kg / kg, grid->name());
Field diag1_m(diag1_fid);
diag1_m.allocate_view();

SECTION("dp_vert_derivative") {
// calculate dp_vert_div manually
dp.sync_to_host();
fin2.sync_to_host();
diag1_m.sync_to_host();

auto dp_v = dp.get_view<const Real **, Host>();
auto fin2_v = fin2.get_view<const Real **, Host>();
auto diag1_m_v = diag1_m.get_view<Real **, Host>();

for (int icol = 0; icol < ngcols; ++icol) {
Copy link
Contributor

@bartgol bartgol Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this way of testing our diags (which, tbh, is a widespread issue, so it's not a critique to this PR per se) is great. I think it is only giving us a false sense of security. We are basically replicating the same piece of code twice, and verifying it gives the right answer. So if by chance we use a bad formula, all we are testing is that we used the same formula twice...

I would like more property based tests or analytical solution tests. Some examples:

  • f(k+1)>f(k) then f'>0 (and viceversa)
  • f(k+1) = a*f(k), dp(k+1)=b*dp(k) with a,b>0, then f'>1 if a>b, f'<1 otherwise (I think we should get something like f' ~ a/b)
  • set f_mid(k) = g(p_mid(k)), and verify that f'(k) ~ g'(p_mid(k)). This of course holds if dp_mid(k)<<1. So for instance, with p(k) =k/1000, f(k)=e^p_mid, f' ~ e^p_mid (the computed value should be e^p_mid * (exp(0.001)-1)/0.001, but the extra factor is ~1. Or maybe something with g(p)=sin(p), or a polynomial.

for (int ilev = 0; ilev < nlevs; ++ilev) {
auto fa1 = (ilev < nlevs - 1) ? (fin2_v(icol, ilev + 1) * dp_v(icol, ilev) +
fin2_v(icol, ilev) * dp_v(icol, ilev + 1)) /
(dp_v(icol, ilev) + dp_v(icol, ilev + 1)) : fin2_v(icol, nlevs-1);
auto fa0 = (ilev > 0 ) ? (fin2_v(icol, ilev) * dp_v(icol, ilev - 1) +
fin2_v(icol, ilev - 1) * dp_v(icol, ilev)) /
(dp_v(icol, ilev - 1) + dp_v(icol, ilev)) : fin2_v(icol, 0);
diag1_m_v(icol, ilev) = (fa1 - fa0) / dp_v(icol, ilev);
}
}
diag1_m.sync_to_dev();

// Calculate weighted avg through diagnostics
dp_vert_derivative->set_required_field(fin2);
dp_vert_derivative->set_required_field(dp);
dp_vert_derivative->initialize(t0, RunType::Initial);
dp_vert_derivative->compute_diagnostic();
auto dp_vert_derivative_f = dp_vert_derivative->get_diagnostic();

// Check that the two are the same manually using the tolerance
dp_vert_derivative_f.sync_to_host();
auto view_deriv_f_d = dp_vert_derivative_f.get_view<Real **, Host>();
auto view_deriv_f_m = diag1_m.get_view<Real **, Host>();
for (int icol = 0; icol < ngcols; ++icol) {
for (int ilev = 0; ilev < nlevs; ++ilev) {
// TODO: the calculations are sometimes diverging because of fp issues in the test on h100 gcc13
// TODO: so a cop-out, just test if the abs diff is within 1e-5
REQUIRE(std::abs(view_deriv_f_d(icol, ilev) - view_deriv_f_m(icol, ilev)) < 1e-5);
}
}
}

// TODO: add SECTION("dz_vert_derivative")
}

} // namespace scream
Loading