Setting up a case¶
A case is a directory that holds everything specific to one problem: the compile-time choices,
the problem parameters, the initial condition and any user hooks, and the run-time inputs. The
solver itself lives in source/ and include/ and is shared by every case.
The files in a case¶
MyCase/
├── GNUmakefile
└── prob/
├── Make.package
├── Parm.H
├── ProblemICBC.H
└── inputs
File |
Contents |
|---|---|
|
Compile-time choices: the model, the dimension and the parallel backends |
|
The problem headers for the build. Rarely changed |
|
|
|
The initial condition, and optional hooks for source terms, boundary conditions, refinement and output |
|
Everything chosen at run time: domain, grid, methods, output and problem values |
Starting a new case¶
Copy the closest existing case in exec/ or exec/_Tests/ and edit it. The
Examples page lists them. For example,
cd COMPAS/exec
cp -r ShockBubble-AirHelium-5Eq MyCase
cd MyCase
make -j4
A copy in exec/ or exec/_Tests/ builds as is, because its GNUmakefile ends with
include ../Make.exec, which finds the build rules. For a case kept outside the repository,
change that line to
include $(COMPAS_HOME)/exec/Make.exec
and build with
make -j4 COMPAS_HOME=/path/to/COMPAS
AMReX is expected next to COMPAS. AMREX_HOME=/path/to/amrex points the build at another
checkout.
Change amr.case_name in the new prob/inputs, so that the copy writes its output to its own
folder. bash ../clean.sh (from a case in exec/) or bash ../../clean.sh (from a case in
exec/_Tests/) removes the plotfiles and checkpoints of previous runs.
Some cases of exec/_Tests/ hold one build per model (BUILD in the GNUmakefile), with one
prob/ for all of them, #if (PHYSICS == ...) where the models differ, and an inputs file per
base run, prob/inputs.<label>
(Testing). A copy of one keeps all its
builds, or only the branch of the model it needs.
GNUmakefile¶
The GNUmakefile sets the model (-DPHYSICS=), the number of phases for the N-phase models
(-DNPHASE=), the dimension (DIM) and the backends (USE_MPI, USE_OMP, USE_CUDA,
USE_HIP, USE_SYCL). Every option is described in Compile-time options.
Make.package¶
prob/Make.package lists the headers of the case:
CEXE_headers += ProblemICBC.H
CEXE_HEADERS += Parm.H
Add a line for any new header the case includes, as exec/GuderleyImplosion-5Eq does for
prob/Guderley.H.
Parm.H¶
Parm.H defines struct Parm, which holds the parameters of the problem. COMPAS fills it on the
host at startup and copies it to the GPU, and every kernel and hook receives it as parm.
Parm contains the parameter structures of the physics, finite-volume, Phase-Field and linear
solver modules, and the EOS and transport constants of each material. The member function
DynamicInit() reads the constants from prob.EOS.* and then calls Initialize(), which reads
the Physics.*, FiniteVolume.* and PhaseField.* entries. The two-phase models name the
constants per material (prob.EOS.gamma_1, prob.EOS.pinf_2, …), and the N-phase models read
one list per property (prob.EOS.gamma = 50.0 1.66667 1.4). The default EOS of a case is the
initial value of EOS in its Parm.H.
To add a problem parameter, add a member with a default value and read it from prob.* at the
end of DynamicInit(). From exec/_Tests/RichtmyerMeshkov-5Eq/prob/Parm.H:
struct Parm
{
// ... physics, EOS and solver members ...
amrex::Real rho1 = 1.0; // light-gas (material 1) density ahead of the shock
amrex::Real Mach = 1.5; // shock Mach number in material 1
void Initialize();
#define DYNAMIC_INIT
void DynamicInit();
};
AMREX_FORCE_INLINE
void Parm::DynamicInit ()
{
amrex::ParmParse pp("prob.EOS");
// ... EOS constants ...
Initialize();
amrex::ParmParse ppp("prob");
ppp.query("rho1", rho1);
ppp.query("Mach", Mach);
}
The member is then prob.Mach in the inputs file and parm.Mach in the kernels (parm->Mach
in the boundary functions, which receive a pointer).
Parm is copied to the GPU byte for byte, so its members must be plain values and fixed-size
arrays. An array whose size is known only at run time goes in device memory, with a pointer to
it in Parm. exec/_Tests/RichtmyerMeshkov-Multimode-5Eq/prob/Parm.H does this for its list of
interface modes.
ProblemICBC.H¶
Initial condition¶
Every case defines initial_condition:
AMREX_GPU_DEVICE
AMREX_FORCE_INLINE
void initial_condition(amrex::Array<amrex::Real, NSTATE>& Conservative,
bool& sharp,
amrex::Real x,
amrex::Real y,
amrex::Real z,
AMREX_D_DECL(amrex::Real dx,
amrex::Real dy,
amrex::Real dz),
AMREX_D_DECL(amrex::Real dx_FinestLevel,
amrex::Real dy_FinestLevel,
amrex::Real dz_FinestLevel),
Parm const& parm)
It returns the conservative state at the point \((x, y, z)\). dx is the cell size of the level
being filled and dx_FinestLevel that of level amr.max_level, even before that level exists,
which cases use to set the width of a diffuse interface. The cases set sharp = false, and the
core does not read it.
By default the function is evaluated at 4 Gauss-Legendre points in each direction of every cell
and the results are averaged, so the initial data are cell averages. run.QuadratureIC = 0
evaluates it once, at the cell center.
The state is easiest to build from primitive variables with State2Primitive and
Primitive2Conservative, so that the same code works with any EOS. Their arguments depend on the
model (the six-equation models take a pressure for each phase), so copy the calls from a case of
the same model.
Optional hooks¶
Each hook is enabled by defining a macro in ProblemICBC.H just before the function; without it,
the core uses its default behavior.
Macro |
Function |
Called |
Example |
|---|---|---|---|
|
|
In every cell at every stage of the time step |
|
|
|
Ghost cells of faces with boundary code 1 |
|
|
|
Ghost cells of faces with boundary code 1 |
|
|
|
Ghost cells outside the domain |
|
|
|
In every cell of each level being tagged, when the grids are rebuilt |
|
|
|
|
|
|
|
At the end of the run, for error norms |
|
include/BoundaryCondition.H holds templates of the three boundary functions with every argument
in place.
Source term¶
#define USER_SOURCE_TERM
AMREX_GPU_HOST_DEVICE
AMREX_FORCE_INLINE
void user_source_term (
amrex::Array<amrex::Real, NSTATE>& Residual,
amrex::Array4<const amrex::Real> const& statein,
int i, int j, int k,
amrex::Real x, amrex::Real y, amrex::Real z,
amrex::Real t,
amrex::Real dt,
AMREX_D_DECL(amrex::Real dx,
amrex::Real dy,
amrex::Real dz),
Parm const& parm)
Residual is added to the right-hand side of the conservative equations in cell (i,j,k), whose
state is statein(i,j,k,n). The array is zeroed before the call, so a component the function
leaves unset adds nothing. exec/_Tests/RayleighTaylor adds gravity to the momentum and energy
equations this way.
Boundary conditions¶
Boundary code 1 (run.lo_bc, run.hi_bc) is an inflow whose ghost cells are filled by a user
function, so code 1 is used together with one of the boundary macros below.
With USER_BOUNDARY_FUNC_ROBIN, BoundaryFunction_Robin is called for each ghost cell
(i,j,k) and each state component n, and sets three coefficients of the condition
where \(q_b\) is the value of component n on the boundary face and \(\partial q/\partial n\)
its outward normal derivative there:
#define USER_BOUNDARY_FUNC_ROBIN
AMREX_GPU_DEVICE
AMREX_FORCE_INLINE
void BoundaryFunction_Robin(int i, int j, int k, int n,
Real& BC_Dirichlet, // a
Real& BC_Neumann, // b
Real& BC_Function, // f
Real const& xBC,
Real const& yBC,
Real const& zBC,
Real const& time,
Array4<Real const> const& q,
AMREX_D_DECL(int ilo, int jlo, int klo),
AMREX_D_DECL(int ihi, int jhi, int khi),
AMREX_D_DECL(Real const& dx,
Real const& dy,
Real const& dz),
Parm const* parm)
(xBC, yBC, zBC) is the point on the boundary face, and comparing i with ilo and ihi (and
likewise for j and k) tells which face the ghost cell is on. exec/_Tests/Jet-Inflow-5Eq
imposes a jet state through part of the \(x = 0\) face and a slip wall on the rest.
USER_BOUNDARY_FUNC_DIRICHLET and USER_BOUNDARY_FUNC_NEUMANN are the two special cases. The
case defines BoundaryFunction_User, with the same arguments except that only BC_Function is
set, and it is the boundary value or the normal derivative.
With USER_BOUNDARY_FUNC, the case fills the ghost cells itself:
#define USER_BOUNDARY_FUNC
AMREX_GPU_DEVICE
AMREX_FORCE_INLINE
void BoundaryFill_User(const IntVect& iv,
Array4<Real> const& q,
const int dcomp,
const int numcomp,
GeometryData const& geom,
const Real time,
const BCRec* bcr,
const int bcomp,
const int orig_comp,
Parm const* parm)
It is called for ghost cells on every face, so it checks bcr[bcomp] for BCType::ext_dir
(code 1) before acting. It is also called for fields other than the state, such as the
Phase-Field buffers. exec/GuderleyImplosion-5Eq tells these apart with dcomp, orig_comp and
numcomp, and fills the state with the exact similarity solution at the current time.
Refinement in space and time¶
spatio_temporal_tag tags cells for refinement by position, level and time, in addition to the
run.refine.* criteria of the inputs file. SPATIO_TEMPORAL_TAG_USER_FUNC_ replaces the empty
default function and makes the tagging routine call it. exec/_Tests/NonsphericalCollapse-6Eq
also defines SPACE_TIME_REF, which is not required.
#define SPATIO_TEMPORAL_TAG_USER_FUNC_
AMREX_GPU_HOST_DEVICE
AMREX_FORCE_INLINE
void spatio_temporal_tag(int& TagVal,
int lev,
amrex::Real x,
amrex::Real y,
amrex::Real z,
amrex::Real t,
Parm const& parm)
{
if (x < 1.25 && y < 1.25) { TagVal = TagBox::SET; }
}
(x, y, z) is the cell center. Leave TagVal unchanged to leave the cell to the other criteria.
Exact solution¶
A case that defines CONVERGENCE as true and provides exact_solution gets the error norms of
every conservative variable against it at the end of the run. scripts/convergence.py uses them;
see Testing.
#undef CONVERGENCE // Macros.H defines it as false by default
#define CONVERGENCE true
AMREX_GPU_DEVICE
AMREX_FORCE_INLINE
void exact_solution(amrex::Array<amrex::Real, NSTATE>& Conservative,
amrex::Real x, amrex::Real y, amrex::Real z,
amrex::Real t,
AMREX_D_DECL(amrex::Real dx, amrex::Real dy, amrex::Real dz),
AMREX_D_DECL(amrex::Real dx_FinestLevel,
amrex::Real dy_FinestLevel,
amrex::Real dz_FinestLevel),
Parm const& parm)
inputs¶
prob/inputs is a list of name = value lines grouped by prefix, and the header at its top
describes the case. Every entry can be overridden on the command line. The entries under prob.
are the ones read by the case’s Parm.H. The rest are described in
Run-time options.
In-situ diagnostics¶
A quantity needed at every step, such as a shock position, a mixing width or a peak pressure, is
better computed in memory during the run than from plotfiles, which are written far less often. A
case does this in UserOutputFunction(T& o), where o is the solver object, and defines
USER_OUTPUT_FUNC to enable it. COMPAS calls the function once before the first step, and then
every N coarse steps with
run.user_output_int = N, orwith each plotfile with
run.user_output_int = 0, the default.
It is also called once before a run stops on a NaN in the step sum. Every rank calls it, and the
helpers in include/Tools/UserOutput.H (namespace UserOutput) do the shared work, including
the MPI reductions and the file output.
Measure¶
Values<N> Measure(o, {op_1, ..., op_N}, ngrow, f);
Values<N> Measure<N>(o, op, ngrow, f);
Values<N> Measure(o, {op_1, ..., op_N}, ngrow, f, FinestLevel);
Measure evaluates the GPU lambda f in each cell of the composite grid, the cells of each
level that no finer level covers. f returns N values, and output n is combined over the
cells and the MPI ranks with op_n, which is Sum, Min or Max. The second form applies one
operation to every output. With FinestLevel as the last argument, f is evaluated on the
cells of the finest level only, which may cover only part of the domain. The result is a
Values<N>, an amrex::GpuArray<Real, N>, and is the same on every rank.
The lambda has the form
[=] AMREX_GPU_DEVICE (Cell const& c, State const& U, Parm const& parm) -> Values<N> { ... }
Argument |
Contents |
|---|---|
|
the index of the cell on its level |
|
its center and size, one value per direction |
|
its volume, an area in 2D |
|
the index box of the domain on its level |
|
|
|
the conservative state, |
|
the case’s |
A Sum adds the values themselves, so an integral returns its integrand times c.dV. A cell
with nothing to contribute returns 0 for a Sum, a large value such as 1e30 for a Min, and a
large negative value such as -1e30 for a Max.
ngrow is the number of ghost cells of U. With ngrow = 0, f may read only U at
(c.i, c.j, c.k). A lambda that reads neighbors, for a gradient or a crossing, sets ngrow to
the distance it reaches, and Measure fills those cells from the same level, the coarser levels
and the boundary conditions.
WriteRow and the accessors¶
WriteRow(o, file, {{"name_1", value_1}, ...}) appends one line with the time, the coarse step
and the values to file in the plot directory of the case, for example
./plot/Advection5Eq/advection.csv. Only the I/O rank writes. At step 0 it starts a new file with
the header t,step,name_1,..., and a restarted run appends to the existing file.
Accessor |
Returns |
|---|---|
|
the coarse step |
|
the time |
|
the case’s |
|
the |
|
the plot directory, where |
Anything the helpers do not cover can use the solver’s members directly, such as
o.dof_new[lev], o.Geom(lev) and o.finestLevel().
Example¶
exec/_Tests/ShuOsher-5Eq records the shock position, the largest \(x\) at which the pressure
crosses a threshold between a cell and its \(+x\) neighbor. The neighbor needs one ghost cell, and
the crossing is taken on the finest level:
#define USER_OUTPUT_FUNC
template <class T>
AMREX_GPU_HOST
AMREX_FORCE_INLINE
void UserOutputFunction(T& o)
{
using namespace UserOutput;
const Real P_thr = 1.5 * 1.0;
// Finest level only, with one ghost cell for the crossing with the +x neighbor
const auto v = Measure(o, {Max}, 1,
[=] AMREX_GPU_DEVICE (Cell const& c, State const& U, Parm const& parm) -> Values<1>
{
amrex::Array<Real, NSTATE> Uc, Ue;
for (int n = 0; n < NSTATE; ++n) { Uc[n] = U(c.i,c.j,c.k,n); Ue[n] = U(c.i+1,c.j,c.k,n); }
Real Pc, Pe;
Conservative2Pressure(Pc, Uc, parm);
Conservative2Pressure(Pe, Ue, parm);
const bool cross = (Pc - P_thr) * (Pe - P_thr) <= 0.0 && Pc != Pe;
return {cross ? c.x[0] + c.dx[0] * (P_thr - Pc) / (Pe - Pc) : -1.0e30};
}, FinestLevel);
WriteRow(o, "shock.csv", {{"x_shock", v[0]}});
}
Every case in exec/ and exec/_Tests/ except IsentropicVortex-5Eq, which reports error
norms instead (Exact solution), defines a UserOutputFunction with these
helpers, with a comment above it that describes what it writes.