TimeIntegration
These tutorials advance a MultiFab in time with
amrex::TimeIntegrator, which takes a right-hand side function and
provides native explicit Runge-Kutta methods and, when AMReX is built with
SUNDIALS, the explicit, implicit, IMEX and multirate methods of ARKODE. The
method is selected with integration.* runtime parameters. RungeKutta has
no extra requirements; SUNDIALS requires -DAMReX_SUNDIALS=ON (SUNDIALS is
downloaded and built unless SUNDIALS_ROOT is given). Build only this
topic with -DAMReXTutorials_TOPICS=TimeIntegration; see
Getting Started.
AMReX also has built-in Runge-Kutta functions, RungeKutta::RK2,
RK3 and RK4 in AMReX_RungeKutta.H, which advance a
MultiFab by one step given a right-hand side function and a function
that fills ghost cells. They are designed for AMR: together with
FillPatcher, they fill the ghost cells of a fine level at the
coarse/fine boundary at the intermediate times of the RK stages, using the
coarse level’s stage data. For codes
built on AmrLevel, AmrLevel::RK does all of this; see
WaveAmrLevel.
Note
TimeIntegrator advances a single MultiFab. In an AMR code, the
right-hand side function of a fine level must fill the ghost cells at the
coarse/fine boundary at each stage time, and TimeIntegrator leaves
that to the user. Interpolating the coarse data in time between the old
and new states is in general not consistent with the RK stages, so for
methods of order higher than two it can reduce the accuracy.
Both tutorials solve the same advection-diffusion problem on the periodic unit domain,
with second-order centered differences. For this initial condition the semi-discrete system has an exact solution (each sine is advected at a slightly modified speed and decays at a modified rate), so the measured error is the time integration error alone.
RungeKutta
- Level:
★★
- Source:
- Key APIs:
TimeIntegrator<MultiFab>,set_rhs,set_post_step_action,set_time_step,evolve,integration.type,integration.rk.type,ParmParse::queryAdd
Advance the problem with the native explicit Runge-Kutta methods of
TimeIntegrator, chosen at run time. The tutorial runs the problem
nruns times, halving the time step each time, and prints the error and
the observed order of accuracy, which it checks against the order of the
method.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
|
|
4 |
1 forward Euler, 2 trapezoid, 3 SSPRK3, 4 RK4, 0 user Butcher tableau
( |
|
0.01, 3 |
step of the first run; each run halves it |
|
16, 0.25 |
problem size and final time |
The right-hand side function receives the integrator’s stage data, so it
fills the ghost cells before applying the stencil in a ParallelFor:
// rhs = -vel.grad(phi) + kappa*lap(phi) with second-order centered differences
void compute_rhs (MultiFab& rhs, MultiFab& phi, Geometry const& geom, Problem const& prob)
{
// The integrator passes its stage data here; fill its ghost cells first.
phi.FillBoundary(geom.periodicity());
The integrator is constructed from the state, given the right-hand side and a time step, and advanced to the final time:
// The integrator allocates its stage data like phi.
TimeIntegrator<MultiFab> integrator(phi, time);
int nrhs = 0;
integrator.set_rhs([&] (MultiFab& rhs, MultiFab& state, Real /*t*/)
{
++nrhs;
compute_rhs(rhs, state, geom, prob);
});
// Called after each step (set_post_stage_action: after each stage),
// e.g., for applying limiters. Here we count the steps.
int nsteps = 0;
integrator.set_post_step_action([&] (MultiFab& /*state*/, Real /*t*/) { ++nsteps; });
integrator.set_time_step(dt_run);
integrator.set_max_steps(static_cast<int>(std::ceil(stop_time/dt_run)) + 1);
// Take as many steps as needed to reach stop_time. The last step is
// shortened if needed.
integrator.evolve(phi, stop_time);
queryAdd stores the defaults in ParmParse when they are not given in
the inputs, where TimeIntegrator reads them:
std::string int_type = "RungeKutta";
int rk_type = 4;
ParmParse("integration").queryAdd("type", int_type);
ParmParse("integration.rk").queryAdd("type", rk_type);
Run (from the top of the repository, see Getting Started):
cmake --build build --target TimeIntegration_RungeKutta -j 8
cd build/TimeIntegration/RungeKutta
mpiexec -n 2 ./RungeKutta inputs integration.rk.type=3
With the default RK4:
dt steps RHS evals max error rate
1.000e-02 25 100 6.794e-07
5.000e-03 50 200 4.212e-08 4.01
2.500e-03 100 400 2.635e-09 4.00
Expected order of accuracy: 4
PASSED
The explicit methods need dt below about \(\Delta x^2/(2 d \kappa)\)
in \(d\) dimensions.
SUNDIALS
- Level:
★★★
- Source:
- Key APIs:
TimeIntegrator<MultiFab>withIntegratorTypes::Sundials,set_rhs,set_imex_rhs,set_fast_rhs,set_time_step,set_fast_time_step,set_adaptive_step,set_post_step_action,set_post_fast_step_action,integration.sundials.*
Use the SUNDIALS (ARKODE) backend of TimeIntegrator for explicit,
implicit, implicit-explicit (IMEX) and multirate (MRI) time integration of
the problem above, here with a larger diffusion coefficient. Diffusion is
stiff: explicit methods need dt below about
\(\Delta x^2/(2 d \kappa)\). The tutorial runs each method type listed in
methods in turn and compares the number of steps, right-hand side
evaluations and the error against the exact solution.
|
Right-hand side |
Step |
|---|---|---|
|
|
adaptive, limited by stability |
|
|
|
|
|
|
|
|
|
The integrator is created with
TimeIntegrator<MultiFab> integrator(IntegratorTypes::Sundials, phi, time),
which selects the SUNDIALS backend in the code instead of with
integration.type. The method type is set with
ParmParse("integration.sundials").add("type", method), and the
right-hand side functions and step sizes depend on it:
if (method == "ERK" || method == "DIRK") {
// A single right-hand side, treated explicitly (ERK) or implicitly (DIRK)
integrator.set_rhs(make_rhs(true, true));
} else if (method == "IMEX-RK") {
// Stiff diffusion implicit, advection explicit
integrator.set_imex_rhs(make_rhs(false, true), make_rhs(true, false));
} else if (method == "EX-MRI") {
// Slow advection with step dt; fast diffusion with substeps of
// fast_dt, explicit (integration.sundials.fast_type = ERK) or
// implicit (DIRK).
integrator.set_rhs(make_rhs(true, false));
integrator.set_fast_rhs(make_rhs(false, true));
integrator.set_fast_time_step(fast_dt);
} else {
amrex::Abort("This tutorial supports ERK, DIRK, IMEX-RK and EX-MRI");
}
if (method == "ERK") {
// The stable explicit step is limited by diffusion; let SUNDIALS
// choose the step from the error estimate.
integrator.set_adaptive_step();
} else {
integrator.set_time_step(dt);
}
Other SUNDIALS settings, such as integration.sundials.method or
integration.sundials.fast_type = DIRK, are read from the inputs file
(see the comments in inputs and the Time Integration
chapter of the AMReX documentation). integration.rel_tol and
integration.abs_tol control the adaptive step and the implicit solves.
Increase n_cell to make diffusion stiffer and the implicit, IMEX and MRI
methods more attractive.
Run (from the top of the repository, with the build configured with
-DAMReX_SUNDIALS=ON, see Getting Started):
cmake --build build --target TimeIntegration_SUNDIALS -j 8
cd build/TimeIntegration/SUNDIALS
mpiexec -n 2 ./SUNDIALS inputs
After the SUNDIALS statistics of each run (silenced with
amrex.verbose=0):
method steps fast steps RHS evals relative error
ERK 49 0 199 2.646e-08
DIRK 5 0 236 6.983e-06
IMEX-RK 5 0 160 1.054e-06
EX-MRI 5 60 275 3.529e-04
PASSED
With GNU Make, the GNUmakefile sets USE_SUNDIALS = TRUE; pass the
SUNDIALS installation with SUNDIALS_DIR=/path/to/sundials/install.