Implemented semi-lagrangian advection solver - #125
Conversation
|
A couple thoughts:
|
|
@speth Thanks for this! On each:
|
|
Hi Seb, I agree with Ray that we need new doc, and a before / after (maybe the before should have the fixes to #109 and #110) would be super helpful. |
|
Thanks @lrobion ! The implementation is much, much simpler than that. Frankly, calling it "semi-Lagrangian" isn't strictly correct but I'm not aware of a better term. The basic idea is:
This only works because the speed is always constant in a given row or column. This is more or less described by Ritchie (1986) (https://doi.org/10.1175/1520-0493(1986)114%3C0135:ETIAWT%3E2.0.CO;2). Hopefully that (somewhat) helps! Will get started on that doc. I'm on leave next week so that might be a good time.. 😂 |
|
I think there might be a small mistake in the forward Euler step because it does not always preserve monotonicity even though we use a flux limiter for that purpose. If you add this test to the test suite this will fail (the first failure being that we create an artificial negative value). // In test_adv_diff_solver.cpp
TEST_CASE("Semi-Lagrangian 1D Advection preserves monotonicity", "[advection]"){
// Monotone non-decreasing profile
const std::vector<double> initial = {0.0, 1.0, 3.0, 4.0, 5.0, 5.0};
const double dt = 1.0;
const double ds = 1.0;
const double bc_left = 0.0;
const double bc_right = 5.0;
auto checkMonotonicity = [&](const std::vector<double>& slice){
// 1. Values should stay in the range spanned by the initial data and the BCs.
for (std::size_t m = 0; m < slice.size(); m++) {
INFO("cell " << m << " = " << slice[m]);
REQUIRE(slice[m] >= 0.0 - 1e-12);
REQUIRE(slice[m] <= 5.0 + 1e-12);
}
// 2. A monotone profile must stay monotone
for (std::size_t m = 1; m < slice.size(); m++) {
INFO("cells " << m - 1 << ", " << m << " = " << slice[m-1] << ", " << slice[m]);
REQUIRE(slice[m] >= slice[m-1] - 1e-12);
}
// 3. Total variation must not increase
double tv_initial = 0.0;
double tv_final = 0.0;
for (std::size_t m = 1; m < slice.size(); m++) {
tv_initial += std::abs(initial[m] - initial[m-1]);
tv_final += std::abs(slice[m] - slice[m-1]);
}
INFO("TV before = " << tv_initial << ", TV after = " << tv_final);
REQUIRE(tv_final <= tv_initial + 1e-12);
};
SECTION("Fractional CFL 0.80, above the 2/3 TVD limit"){
std::vector<double> slice = initial;
double velocity = 0.8; // dt = ds = 1 -> no integer shift, fractional CFL = 0.8
semiLagrangianAdvection1D(slice, velocity, dt, ds, bc_left, bc_right);
checkMonotonicity(slice);
}
SECTION("Fractional CFL 0.60, below the 2/3 TVD limit"){
std::vector<double> slice = initial;
double velocity = 0.6; // dt = ds = 1 -> no integer shift, fractional CFL = 0.6
semiLagrangianAdvection1D(slice, velocity, dt, ds, bc_left, bc_right);
checkMonotonicity(slice);
}
}I think this is because the face flux for cell For a cell which is correct ( We can derive that value by considering the mass that crosses the cell face Because Then to get and substituting This is different from line 591 (and other places of the SL solver in Rerunning the test with this fix makes the tests pass. I am not an expert in this so I maybe be wrong, this cropped up because I wanted to add tests for properties we know about the solver here. |
lrobion
left a comment
There was a problem hiding this comment.
This looks good, I am happy to discuss any thing I've flagged. Some of the allocation stuff is just for performance and I made not sure how much of a difference it would make to hoist them out of the hot loops.
|
@lrobion - thanks so much for your review! I've tried to make limited-scope edits which I think address the issues you raised, but would value your thoughts. @speth - there's now a docstring, but the goal is to generate a much more thorough set of documentation which will try to cover recent major changes (including this one). I'll get that resolution study together too, and will make that available for you to review before going any further with the PR. |
|
From my end I think we resolved everything for the PR.
|
…lti-step Eulerian
@lrobion identified an incorrect definition for the upwind calculation at the boundary. This has no effect outside of exceptional cases but was misleading and made the code hard to understand. Now corrected - zero-diff for almost all cases so should have no effect on the user.
4be1af4 to
5427df3
Compare
|
Fantastic - thank you so much @lrobion ! I've added your proposed test (which I'm happy to say the code passes). I'm now working on documentation. I am also performing a resolution sweep, although for that I'm using the code in #126 - it's a PR which will be on top of this one, but which has a couple of additional improvements. They're zero-diff for the standard case though. I'll put the results of the resolution testing there, and I'm happy to hold off on merging both PRs until review is complete for that. |
|
That works for me. #126 looks very promising, but I'll look into it in detail once you're done with the resolution sweep! |
The Eulerian advection scheme caused massive diffusion, slow simulations, and had multiple bugs (see #109 and #110). This PR replaces it with a semi-Lagrangian approach, shifting the local vector by floor(CFL) and then performing a single forward Euler step for the remaining fractional timestep. Credit to @coco-yeung for the implementation on which this is based!
This is NOT zero-diff. It dramatically reduces numerical diffusion, which has the side effect of quite severely changing the simulation; in a reference case (issl_rhi140) the lifetime is decreased (see attached). It also increases run speed by 1.2-1.5x in terms of simulation hours per real-time hour.


The reason for this decrease in lifetime seems to be that, with the high degree of numerical diffusion in the Eulerian code, the contrail core is artificially preserved:
A review of the code would be greatly appreciated (nominating @lrobion if possible, and input from @Calebsakhtar would also be greatly appreciated). I believe that this code change does resolve several outstanding issues, but it would be good to verify that the new behaviour is expected.