Applications
Complete solvers that combine several topics, meant to be copied as the
starting point of a new application. They may use a Source/ directory
for the solver and Exec/<Problem>/ directories for problem setups.
CNS needs AMReX built with AmrLevel support (AMReX_AMRLEVEL,
on by default) and IncFlow needs the linear solvers
(AMReX_LINEAR_SOLVERS and AMReX_LINEAR_SOLVERS_INCFLO, on by
default). Both run in 2D and 3D only.
Build only this topic with -DAMReXTutorials_TOPICS=Applications; see
Getting Started.
CNS
- Level:
Blueprint
- Source:
- Key APIs:
Amr,AmrLevel,LevelBld,StateDescriptor(desc_lst),DeriveList(derive_lst),AmrLevel::RK,FluxRegister,average_down,AMRErrorTag,GpuBndryFuncFab,ParmParse::queryAdd,ParallelFor,ParReduce
A compressible Euler solver built on amrex::Amr and amrex::AmrLevel,
with adaptive mesh refinement, subcycling in time, refluxing and GPU
support. It uses a second-order Godunov method with characteristic slope
limiting and an approximate Riemann solver, advanced in time with
Runge-Kutta (cns.rk_order = 2 or 3). It comes with two problems: a Sod
shock tube (Exec/Sod) and a Rayleigh-Taylor instability with gravity
(Exec/RT). At the end of a run, the code checks that the total mass
changed only by round-off, if no mass can enter or leave the domain.
Compared with AdvectionAmrCore, which manages level data itself
on top of AmrCore, here AmrLevel provides the state data with old
and new time levels, FillPatch, checkpoint/restart and plotfile I/O.
Code structure
File |
Contents |
|---|---|
|
Initialize, build |
|
The |
|
State data, boundary conditions, derived variables, |
|
|
|
GPU kernels: primitive variables, slopes, Riemann solver |
|
GPU kernels: time step, temperature |
|
Derived plot variables (pressure, velocity) |
|
Component indices; physical parameters (EOS, gravity) |
|
|
|
Default parameters ( |
Amr owns the grid hierarchy and calls the virtual functions of CNS:
Function |
Called |
|---|---|
|
Once, to define the state data and derived variables |
|
For each level at start-up |
|
On level 0, to choose |
|
For each level and each (sub)step |
|
After a level has caught up with the next finer level: reflux and average down |
|
When regridding, to tag cells for refinement |
|
When regridding, to fill new grids from old data or from the coarser level |
|
After initialization and after each coarse step |
|
When restarting from a checkpoint |
Driver and setup
The driver builds Amr from a LevelBld and takes coarse time steps;
Amr recursively advances the finer levels and writes plotfiles and
checkpoints according to the amr.* parameters:
// Amr manages the hierarchy of CNS levels: it builds the initial
// grids, regrids, subcycles in time, and writes plotfiles and
// checkpoints according to the amr.* parameters.
Amr amr(getLevelBld());
amr.init(strt_time, stop_time);
const Real mass0 = dynamic_cast<CNS&>(amr.getLevel(0)).conservedTotals()[0];
while ( amr.okToContinue() &&
(amr.levelSteps(0) < max_step || max_step < 0) &&
(amr.cumTime() < stop_time || stop_time < Real(0.0)) )
{
// One coarse time step, which recursively advances finer levels.
amr.coarseTimeStep(stop_time);
}
The state is registered once in CNS::variableSetUp: one cell-centered
StateData with NUM_GROW ghost cells and conservative interpolation,
the component names with their BCRec boundary types and fill function,
and derived quantities computed on demand, e.g., for
amr.derive_plot_vars:
// One cell-centered StateData with NCONS components and NUM_GROW ghost
// cells, interpolated conservatively from coarse to fine.
desc_lst.addDescriptor(State_Type, IndexType::TheCellType(),
StateDescriptor::Point, NUM_GROW, NCONS,
&cell_cons_interp, /*extrap=*/ false,
/*store_in_checkpoint=*/ true);
Vector<std::string> names{"density", "xmom", "ymom", "zmom", "rho_E", "rho_e", "Temp"};
Vector<BCRec> bcs(NCONS);
for (int n = 0; n < NCONS; ++n) {
bcs[n] = component_bc(n, phys_bc);
}
StateDescriptor::BndryFunc bndryfunc(GpuBndryFuncFab<CnsFillExtDir>(CnsFillExtDir{}));
bndryfunc.setRunOnGPU(true); // the BC function launches GPU kernels itself
desc_lst.setComponent(State_Type, URHO, names, bcs, bndryfunc);
// Derived quantities, computed on demand for plotfiles
// (amr.derive_plot_vars) and tagging.
derive_lst.add("pressure", IndexType::TheCellType(), 1, cns_derpres,
&DeriveRec::TheSameBox);
derive_lst.addComponent("pressure", desc_lst, State_Type, UEINT, 1);
derive_lst.add("velocity", IndexType::TheCellType(), AMREX_SPACEDIM,
{AMREX_D_DECL("x_velocity","y_velocity","z_velocity")}, cns_dervel,
&DeriveRec::TheSameBox);
derive_lst.addComponent("velocity", desc_lst, State_Type, URHO, 1+AMREX_SPACEDIM);
Time step and synchronization
CNS::advance calls AmrLevel::RK, which runs the Runge-Kutta stages
and fills the ghost cells of each stage, including coarse/fine boundaries
interpolated in space and time. compute_dSdt computes the fluxes with
ParallelFor kernels and adds them to the flux registers:
// As the coarse side, this level adds its fluxes to the register owned by
// the next finer level. As the fine side, it adds to its own register.
FluxRegister* fr_as_crse = nullptr;
if (do_reflux && level < parent->finestLevel()) {
fr_as_crse = getLevel(level+1).flux_reg.get();
fr_as_crse->setVal(Real(0.0));
}
FluxRegister* fr_as_fine = nullptr;
if (do_reflux && level > 0) {
fr_as_fine = flux_reg.get();
}
// AmrLevel::RK fills the ghost cells of each stage, including
// coarse/fine boundaries interpolated in space and time.
RK(rk_order, State_Type, time, dt, iteration, ncycle,
// Given state S, compute dSdt. dtsub is this stage's weight times dt.
[&] (int /*stage*/, MultiFab& dSdt, MultiFab const& S, Real /*t*/, Real dtsub)
{
compute_dSdt(S, dSdt, dtsub, fr_as_crse, fr_as_fine);
},
// After each stage, update the derived components.
[&] (int /*stage*/, MultiFab& S) { computeTemp(S, 0); });
After the finer level has caught up, post_timestep corrects the coarse
level with the fine fluxes and overwrites the covered cells with the
average of the fine data, which together keep the scheme conservative:
if (level < parent->finestLevel()) {
// Replace the coarse fluxes at the coarse/fine interface with the
// sum of the fine fluxes, which keeps the scheme conservative.
if (do_reflux) {
MultiFab& S = get_new_data(State_Type);
getLevel(level+1).flux_reg->Reflux(S, Real(1.0), 0, 0, NCONS, geom);
}
avgDown();
}
Making your own code
New problem: copy
Exec/SodtoExec/MyProblem. EditProbParmandcns_initdataincns_prob.H, and the defaults andprob.*parameters incns_prob.cpp. Add the problem to the list inCMakeLists.txt.New physics: add components in
CNS_index.Hand their names and boundary conditions inCNS_setup.cpp, and change the kernels inCNS_hydro_K.Handcompute_dSdt. Inflow boundaries need values inCnsFillExtDirand removal of the check inCNS::read_params.New AmrLevel code: keep the structure of
main.cpp,CNS_setup.cpp(LevelBld) and theAmrLeveloverrides inCNS.cpp, and replaceadvance, the kernels and the tagging criteria. Kernels capture small parameter structs such asParmby value; large tables belong in GPU memory, e.g.,Gpu::DeviceVector.
Building and running
One executable is built per problem: targets Applications_CNS_Sod and
Applications_CNS_RT, binaries CNS_Sod and CNS_RT. Both problems
have an inputs file, which is read from the source tree:
cmake --build build --target Applications_CNS_Sod -j 8
cd build/Applications/CNS
mpiexec -n 2 ./CNS_Sod ../../../Applications/CNS/Exec/Sod/inputs
After copying Applications/CNS out of the repository, its
CMakeLists.txt builds it on its own and fetches AMReX (or uses a local
checkout given by -DFETCHCONTENT_SOURCE_DIR_AMREX):
cmake -S . -B build [-DAMReX_SPACEDIM=2] [-DAMReX_GPU_BACKEND=CUDA]
cmake --build build -j 8
With GNU Make, build in a problem directory:
cd Applications/CNS/Exec/Sod
make -j8
mpiexec -n 2 ./CNS3d.gnu.MPI.ex inputs
Without an inputs file, each problem runs to its stop_time with the
defaults in cns_prob_defaults. The inputs files stop after 20
steps; remove max_step and increase amr.n_cell and
amr.max_level for a real run. Boundary types (cns.lo_bc,
cns.hi_bc) are 0 interior (periodic), 1 inflow (not implemented),
2 outflow, 3 symmetry, 4 slip wall and 5 no-slip wall. cns.v=2 prints
the conserved totals after every coarse step.
IncFlow
- Level:
Blueprint
- Source:
- Key APIs:
AmrCore,FillPatchTwoLevels,InterpFromCoarseLevel,GpuBndryFuncFab,MLPoisson,MLNodeLaplacian,MLABecLaplacian,MLMG::solve,MLMG::getFluxes,MLMG::apply,computeDivergence,average_down_faces,amrex_calc_xslope_extdir,AMRErrorTag
A small incompressible Navier-Stokes solver built on amrex::AmrCore, in
the spirit of incflo but with
one method for each step and no options. It solves
with constant density, adaptive mesh refinement and periodic or no-slip wall boundaries. All levels advance together with the same time step. The projections are written directly with the MLMG linear solvers. It comes with three problems:
Exec/TaylorGreen: decaying Taylor-Green vortices on one level, checked against the exact solution.Exec/DoubleShearLayer: two periodic shear layers that roll up into vortices (Kelvin-Helmholtz), with a fine level that follows the vorticity. The mean velocity must be conserved to round-off, also across levels.Exec/LidDrivenCavity: no-slip walls and a moving lid, refined near the lid.
Compared with AdvectionAmrCore, which also manages its level data
on top of AmrCore, the solution here is a velocity that must stay
divergence free, so each step involves elliptic solves over all levels.
Code structure
File |
Contents |
|---|---|
|
Initialize, |
|
The |
|
Time step, advection and viscous terms |
|
GPU kernels: slopes, face values, upwinding |
|
MAC and nodal projections |
|
Plotfiles, checkpoint and restart |
|
The initial velocity |
|
Default parameters ( |
AmrCore calls MakeNewLevelFromScratch at start-up,
MakeNewLevelFromCoarse, RemakeLevel and ClearLevel when
regridding, and ErrorEst to tag cells where the vorticity exceeds
incflow.vort_tag. Ghost cells are filled by FillPatch from the same
level, the coarser level, and the wall velocity.
Time step
Each step is a predictor-corrector method. ComputeRHS returns
\(R(u) = -\nabla\cdot(u u) + \nu\nabla^2 u\) without the pressure
gradient, and the nodal projection removes the gradient part of the result:
// Predictor: u* = P(u^n + dt R(u^n))
ComputeRHS(rhs_old, t_old);
for (int lev = 0; lev < nlevs; ++lev) {
MultiFab::LinComb(vel_new[lev], 1.0_rt, vel_old[lev], 0, dt, rhs_old[lev], 0,
0, AMREX_SPACEDIM, 0);
}
NodalProjection(vel_new, dt);
// Corrector: u^{n+1} = P(u^n + dt/2 (R(u^n) + R(u*)))
ComputeRHS(rhs_new, t_new);
for (int lev = 0; lev < nlevs; ++lev) {
MultiFab::LinComb(vel_new[lev], 1.0_rt, vel_old[lev], 0, 0.5_rt*dt, rhs_old[lev], 0,
0, AMREX_SPACEDIM, 0);
MultiFab::Saxpy(vel_new[lev], 0.5_rt*dt, rhs_new[lev], 0, 0, AMREX_SPACEDIM, 0);
}
NodalProjection(vel_new, dt);
For the advection term, the velocity is extrapolated to both sides of each face with limited slopes, one-sided next to walls:
void face_values (amrex::IntVect const& iv, int n, int d,
amrex::Array4<amrex::Real const> const& q, DirInfo const& di,
amrex::Real& ql, amrex::Real& qr)
{
using namespace amrex::literals;
amrex::IntVect const im = iv - amrex::IntVect::TheDimensionVector(d);
if (di.wall_lo && iv[d] == di.dlo) {
ql = qr = q(im,n);
} else if (di.wall_hi && iv[d] == di.dhi+1) {
ql = qr = q(iv,n);
} else {
ql = q(im,n) + 0.5_rt * slope(im, n, d, q, di);
qr = q(iv,n) - 0.5_rt * slope(iv, n, d, q, di);
}
}
The upwinded normal component gives a face velocity umac. A MAC
projection makes it divergence free: solve
\(\nabla^2\phi = \nabla\cdot u_{mac}\) on all levels and add the fluxes
\(-\nabla\phi\) from MLMG::getFluxes to umac:
if (!mac_op) {
mac_op = std::make_unique<MLPoisson>(LevelGeom(), LevelGrids(), LevelDmap());
mac_op->setDomainBC(LinOpBC(LinOpBCType::Neumann), LinOpBC(LinOpBCType::Neumann));
for (int lev = 0; lev < nlevs; ++lev) {
mac_op->setLevelBC(lev, nullptr);
}
}
MLMG mlmg(*mac_op);
mlmg.setVerbose(mg_verbose);
mlmg.solve(GetVecOfPtrs(phi), GetVecOfConstPtrs(rhs), solver_tolerance(), 0.0_rt);
All velocity components are then upwinded with umac, and the fluxes on
faces covered by a finer level are replaced by the average of the fine
fluxes, so that the update is conservative. The viscous term is applied with
an MLABecLaplacian, which handles the walls (Dirichlet values in the
ghost cells) and coarse/fine boundaries:
if (!visc_op) {
// Only the operator is needed, no multigrid hierarchy.
LPInfo info;
info.setMaxCoarseningLevel(0);
visc_op = std::make_unique<MLABecLaplacian>(LevelGeom(), LevelGrids(), LevelDmap(),
info, Vector<FabFactory<FArrayBox> const*>{},
AMREX_SPACEDIM);
visc_op->setDomainBC(LinOpBC(LinOpBCType::Dirichlet), LinOpBC(LinOpBCType::Dirichlet));
// alpha a u - beta div(b grad(u)) with alpha = 0, beta = b = 1
visc_op->setScalars(0.0_rt, 1.0_rt);
for (int lev = 0; lev < nlevs; ++lev) {
visc_op->setBCoeffs(lev, 1.0_rt);
}
}
for (int lev = 0; lev < nlevs; ++lev) {
// The ghost cells outside walls hold the wall velocity.
visc_op->setLevelBC(lev, &vel[lev]);
}
MLMG mlmg(*visc_op);
mlmg.apply(GetVecOfPtrs(visc), GetVecOfPtrs(vel));
The nodal projection solves \(\nabla^2\phi = \nabla\cdot u\) with \(\phi\) on the nodes. Since the update has no pressure gradient, \(u = u_{div\,free} + \Delta t\,\nabla p\), so \(p = \phi/\Delta t\). Building a linear operator sets up its multigrid hierarchy, so all three operators are built when first needed and kept until the grids change:
if (!nodal_op) {
// Constant sigma = 1. The empty list of factories means no embedded
// boundaries.
Vector<FabFactory<FArrayBox> const*> const no_eb{};
nodal_op = std::make_unique<MLNodeLaplacian>(LevelGeom(), LevelGrids(), LevelDmap(),
LPInfo(), no_eb, 1.0_rt);
nodal_op->setDomainBC(LinOpBC(LinOpBCType::Neumann), LinOpBC(LinOpBCType::Neumann));
}
// rhs = div(vel) on the nodes, including the coarse/fine synchronization.
// The velocity in ghost cells outside the Neumann walls is not used.
nodal_op->compRHS(GetVecOfPtrs(rhs), GetVecOfPtrs(vel), {}, {});
MLMG mlmg(*nodal_op);
mlmg.setVerbose(mg_verbose);
mlmg.solve(GetVecOfPtrs(phi), GetVecOfConstPtrs(rhs), solver_tolerance(), 0.0_rt);
Making your own code
New problem: copy
Exec/TaylorGreentoExec/MyProblem. Editprob_initdatainprob.H, and the defaults and the check inprob.cpp. Add the problem to the list inCMakeLists.txt.Wall velocity:
xlo.velocity,yhi.velocity, … set the velocity of the walls in non-periodic directions; the normal component is zero.More physics: a scalar can be advected like a velocity component in
ComputeAdvection. Forces such as gravity are added to \(R\) inComputeRHS. Inflow and outflow need Dirichlet values inWallVelocityFilland Dirichlet pressure boundaries in the projections.
Building and running
One executable is built per problem: targets
Applications_IncFlow_<Problem>, binaries IncFlow_<Problem>:
cmake --build build --target Applications_IncFlow_TaylorGreen -j 8
cd build/Applications/IncFlow
mpiexec -n 2 ./IncFlow_TaylorGreen ../../../Applications/IncFlow/Exec/TaylorGreen/inputs
As with CNS, the directory can be copied out and built on its own with
CMake, and each problem directory has a GNUmakefile. The inputs
files stop after 10 steps; remove max_step for a longer run, e.g.,
the shear layers roll up by stop_time=1, and stop_time=20 brings
the cavity close to steady state. The nodal
projection needs amr.ref_ratio = 2. Restart with
amr.restart=chk00005.