-
Notifications
You must be signed in to change notification settings - Fork 435
EAMxx: calculate vertical derivatives in diagnostics #7491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e316b9e
EAMxx: diagnose vertical derivatives
mahf708 b5c3b7a
EAMxx: make the vertical derivative better
mahf708 3631aee
EAMxx: use tol in vert deriv testing
mahf708 1278079
EAMxx: const extrap for BC in vert deriv
mahf708 389f6a7
EAMxx: use better pdf for dp in vert deriv testing
mahf708 49c5783
EAMxx: cop-out to avoid opt fails on ci
mahf708 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
components/eamxx/src/diagnostics/tests/vert_derivative_test.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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)
thenf'>0
(and viceversa)f(k+1) = a*f(k)
,dp(k+1)=b*dp(k)
with a,b>0, thenf'>1
if a>b,f'<1
otherwise (I think we should get something likef' ~ a/b
)f_mid(k) = g(p_mid(k))
, and verify thatf'(k) ~ g'(p_mid(k))
. This of course holds if dp_mid(k)<<1. So for instance, withp(k) =k/1000
,f(k)=e^p_mid
,f' ~ e^p_mid
(the computed value should bee^p_mid * (exp(0.001)-1)/0.001
, but the extra factor is ~1. Or maybe something withg(p)=sin(p)
, or a polynomial.