11. Reading MISO’s Time Evolution
The heat conduction calculation you wrote in Chapter 10 had these parts: determining the time step, the finite difference of the right-hand side, time integration, boundary handling, and output. MISO’s MHD calculation is built from the same parts. In this chapter, starting from the demo’s main(), we trace the contents of one step and the output through the MISO source, and relate the equations and finite-difference formulas to the code. Finally, we identify the places you rewrite when you create your own problem.
Follow the paths in the text from the folder where you cloned MISO. Even if you have not finished the GPU chapter (Chapter 9), you can read on with the CPU source alone. For the CUDA version, we show only how it corresponds to the CPU version.
Contents
1. Basic equations and MISO’s variables
1.1 State variables
MISO holds the following 9 variables at each grid point. Fields in miso/include/miso/mhd_fields.hpp bundles them as members of type Array3D (Chapter 4).
Member name |
Symbol |
Meaning |
|---|---|---|
|
\(\rho\) |
Density |
|
\(v_x, v_y, v_z\) |
Velocity |
|
\(B_x, B_y, B_z\) |
Magnetic field |
|
\(e\) |
Internal energy per unit mass |
|
\(\phi\) |
Auxiliary variable for reducing the divergence of the magnetic field (the potential of divB cleaning) |
The conserved quantities are the momentum \(\rho\boldsymbol{v}\), the total energy, and so on, but the arrays hold the velocity \(\boldsymbol{v}\) and the internal energy per unit mass \(e\). In the time integration, we first form the conserved quantities and advance them, and at the end divide by \(\rho\) to recover the velocity and \(e\) (Section 8).
1.2 Basic equations
MISO solves the ideal MHD equations in Gaussian units (the form in which the magnetic terms carry a factor \(1/4\pi\)). \(\partial_j\) is the partial derivative in the \(x, y, z\) directions, and when the same index \(j\) appears twice, it is summed over \(j = x, y, z\). \(S_\rho\) and the like are source terms (external forces and heating), which the user provides (Section 12).
The term \(-\partial_i\phi\) in the magnetic field equation and the equation for \(\phi\) form a mechanism that carries the error of \(\nabla\cdot\boldsymbol{B}\) generated in the numerical calculation away as a wave (with speed \(c_h\)) and damps it over a time \(\tau\). This method is called hyperbolic divergence cleaning (Dedner et al. 2002, J. Comput. Phys. 175, 645).
1.3 Equation of state
The pressure \(p\) and the sound speed \(c_s\) are obtained from the equation of state (EOS). The ideal-gas EOS that MISO provides is eos::IdealEOS in miso/include/miso/eos.hpp. With the ratio of specific heats \(\gamma\) (eos.gm in the configuration),
are computed by the two member functions gas_pressure and sound_speed. Both process all grid points with for_each (Chapter 7).
// miso/include/miso/eos.hpp: IdealEOS::gas_pressure (excerpt)
template <typename Backend>
void gas_pressure(Backend btag, mhd::FieldsView<const Real> qq,
Array3DView<Real> pr) const {
const Real gm_ = gm; // device lambdas must not capture `this`
Range1D range{0, qq.size()};
for_each(
btag, range,
MISO_LAMBDA(int i) { pr[i] = (gm_ - Real(1)) * qq.ro[i] * qq.ei[i]; });
}
The member variable gm is copied to the local variable gm_ before being used in the lambda in order to avoid the this capture explained in Chapter 9.
2. main() and Model
The main() in demo/mhd2d_vortex/src/main.cpp consists of just the following 4 lines.
// demo/mhd2d_vortex/src/main.cpp
int main(int argc, char **argv) {
// Initialize MPI and CUDA environments
Env env(argc, argv);
// Read configuration file
auto config_path = parse_config_filepath(argc, argv);
Config config(config_path.value_or("./config.yaml"));
// Run simulation
Model model(config);
model.run();
}
Line |
Role |
In Fortran |
|---|---|---|
|
Initializes MPI. At the end of |
|
|
Looks for |
Code that reads arguments with |
|
Reads the configuration file and fills in default values (Section 3). |
Code that reads a |
|
Creates the grid, arrays, and time-integration components together (Section 4) |
The calls to each |
|
Runs the time loop (Section 4) |
The |
Model is a struct that you write for each demo.
// demo/mhd2d_vortex/src/main.cpp
struct Model : public mhd::ModelBase<Model, Real, Backend> {
eos::IdealEOS<Real> eos;
InitialCondition ic;
mhd::EmptyBoundaryCondition<Real> bc;
mhd::EmptySourceTerm<Real> src;
Model(Config &config) : ModelBase(config), eos(config), ic(eos), bc(), src() {}
};
Model inherits from mhd::ModelBase and passes itself (Model) as the template argument. This is the CRTP you learned about in Chapter 5. The derived class holds the 4 components that change from problem to problem.
Member |
Role |
Type in this example |
|---|---|---|
|
Equation of state |
|
|
Initial condition |
|
|
Boundary condition |
|
|
Source term |
|
Real and Backend are decided at the top of the file by using Real = float; and #ifdef USE_CUDA. Real = float gives single precision, and the calculation runs on backend::CUDA (GPU) if USE_CUDA is defined and on backend::Host (CPU) otherwise (Chapter 2, Chapter 6).
3. Configuration file and default values
3.1 Sections of config.yaml
demo/mhd2d_vortex/config.yaml has the following sections.
Section |
Contents |
Where it is read |
|---|---|---|
|
Whether to write output, the output destination, and whether to restart ( |
|
|
End time |
|
|
Number of grid points |
|
|
Number of processes in each direction, |
|
|
Directions made periodic, |
|
|
CFL number |
|
|
Ratio of specific heats |
|
To read a value, specify the section name and the key in turn with [], as in config["time"]["tend"].as<Real>(), and extract it by choosing the type with as<Type>(). The <> in as<Real>() is a template argument (Chapter 6).
3.2 Default values and merge_yaml
So that you do not have to write every key each time, MISO keeps default values as a YAML string in miso/include/miso/config_defaults.hpp.
// miso/include/miso/config_defaults.hpp(excerpt; some comments omitted)
inline constexpr const char *config_default_yaml = R"yaml(
io:
enabled: true
continue: true
save_dir: data/
...
mhd:
# CFL number for time-stepping
cfl_number: 0.5
artificial_viscosity:
ep: 1.0
fh: 1.0
...
)yaml";
R"yaml( ... )yaml" is a raw string literal. It turns everything from R"delimiter( to )delimiter", including newlines and ", into a single string as it is. It is used to embed YAML directly in the C++ source.
The constructor of Config in miso/include/miso/config.hpp does the following in order.
Only rank 0 reads the file (
YAML::LoadFile), converts it to a string, and distributes it to all ranks withMPI_Bcast.All ranks read the default-value string (
YAML::Load(config_default_yaml)) and add it to the user’s settings withmerge_yaml.Interpret
io.save_diras a path relative to the folder containing the configuration file, and create the output destination.
// miso/include/miso/config.hpp
inline void merge_yaml(YAML::Node &target, const YAML::Node &defaults) {
if (!defaults.IsMap()) {
return;
}
if (!target || !target.IsMap()) {
target = YAML::Node(YAML::NodeType::Map);
}
for (const auto &it : defaults) {
const std::string key = it.first.as<std::string>();
const YAML::Node &default_value = it.second;
if (default_value.IsMap()) {
YAML::Node child = target[key];
merge_yaml(child, default_value); // fill nested sections recursively
target[key] = child;
} else {
if (!target[key]) { // fill only keys the user did not write
target[key] = default_value;
}
}
}
}
(The two comments in merge_yaml were added for explanation.) Values written by the user are not overwritten, and only the keys that were not written are filled with default values. Sections inside sections (such as mhd.artificial_viscosity) are also filled in key by key. The completed result is saved as config.yaml in the output destination (Section 11). You can check the values actually used in this saved file.
4. Constructing ModelBase and run()
4.1 Construction order of the members
ModelBase in miso/include/miso/mhd_model_base.hpp has the following members.
// miso/include/miso/mhd_model_base.hpp(excerpt)
template <class Derived, class Real, class Backend> class ModelBase {
public:
Config &config;
mpi::Shape mpi_shape;
Time<Real> time;
Grid<Real, backend::Host> grid;
ExecContext<Real, Backend> exec_ctx;
MHD<Real, Backend> mhd;
explicit ModelBase(Config &cfg)
: config(cfg), mpi_shape(cfg), time(cfg), grid(cfg, mpi_shape),
exec_ctx(mpi_shape, grid), mhd(cfg, grid, exec_ctx) {}
...
Members are constructed in the order of declaration (Chapter 4). Later members use members that were constructed earlier.
Order |
Member |
What is created |
|---|---|---|
1 |
|
Reference to the configuration |
2 |
|
The Cartesian process layout created by |
3 |
|
Time, step count, and output number |
4 |
|
The grid this rank is responsible for (coordinates including ghost cells, and grid spacing). Created on the CPU |
5 |
|
Extra information for each compute location (on the CPU, only a reference to |
6 |
|
The state variables |
The type MHD of mhd is in miso/include/miso/mhd_solver.hpp.
// miso/include/miso/mhd_solver.hpp(excerpt)
template <typename Real, typename Backend> struct MHD {
Grid<Real, Backend> grid;
Fields<Real, Backend> qq;
ExecContext<Real, Backend> &exec_ctx;
Integrator<Real, Backend> integrator;
Checkpoint<Real> checkpoint;
...
template <typename EOS, typename BoundaryCondition, typename Source>
void update(Real dt, const EOS &eos, const BoundaryCondition &bc,
const Source &src) {
integrator.update(dt, eos, bc, src, qq);
}
...
};
MHD holds the state variables qq and the computation components, and acts as the front end that hands the work to Integrator. Grid<Real, Backend> is a copy of the CPU grid grid_h placed on the compute location (Backend).
The members of the derived class Model (eos, ic, bc, src) are constructed after the construction of the base class ModelBase is finished. For this reason, the constructor of ModelBase does not use them, and run() receives them through derived().
4.2 run(): the time loop
// miso/include/miso/mhd_model_base.hpp(excerpt)
void run() {
// Ensure to call methods in the derived class
auto &d = derived();
// Assert that d has eos, ic, bc, and src as member variables.
static_assert(has_eos<Derived>::value,
"Derived must have member variable 'eos'.");
...
mhd.apply_initial_condition(d.ic, d.bc);
if (config["io"]["continue"].template as<bool>() &&
fs::exists(time.n_output_filepath())) {
load_state();
}
save_metadata();
save_if_needed();
while (time.time < time.tend) {
d.update();
save_if_needed();
}
}
Processing |
Contents |
|---|---|
|
Treats itself as the derived class |
|
Causes a compile-time error if |
|
Fills a temporary array on the CPU with initial values via |
|
If |
|
Writes out the configuration (with defaults filled in), the grid, and the MPI layout |
|
Writes out the state if the output time has been reached |
|
Advances one step |
The .template in config["io"]["continue"].template as<bool>() is the syntax required inside a template when calling a member template of a dependent type. You can read it as meaning the same as as<bool>().
d.update() calls update() if Model defines it, and otherwise calls the base class ModelBase::update(). The Model in demo/mhd3d_magnetosphere/src/main.cpp defines its own update() and changes how the time step is determined.
4.3 update(): the entry point of one step
// miso/include/miso/mhd_model_base.hpp(excerpt)
void update() {
auto &d = derived();
using Source = decltype(d.src);
...
auto dt = mhd.cfl(d.eos);
if constexpr (has_dt_limit<Source>::value) {
// dt_limit() returns the limit of this rank: take the global minimum.
...
Real dt_src = static_cast<Real>(d.src.dt_limit());
...
MPI_Allreduce(&dt_src, &dt_src_g, 1, mpi::data_type<Real>(), MPI_MIN,
mpi::comm());
...
dt = std::min(dt, dt_src_g);
}
mhd.update(dt, d.eos, d.bc, d.src);
time.update(dt);
}
Determine the time step from the CFL condition with
mhd.cfl(d.eos)(Section 6).If the source term has
dt_limit(), also apply that limit (the minimum over all ranks). If it does not, the inside of theif constexpris not compiled.Advance the state one step with
mhd.update(Section 5).Advance the time and step count with
time.update(dt).
5. Flow of one step
Integrator::update, called from MHD::update, is the body of one step. Integrator is declared in miso/include/miso/mhd_integrator.hpp, and the CPU and GPU versions are specialized in separate files (Chapter 6).
File |
Contents |
|---|---|
|
The declaration of |
|
|
|
|
The CPU version of update is 3 lines.
// miso/include/miso/mhd_integrator_host.hpp
template <typename EOS, typename BoundaryCondition, typename Source>
void update(Real dt, const EOS &eos, const BoundaryCondition &bc,
const Source &src, Fields<Real, backend::Host> &qq) {
divb_parameters_set(dt);
runge_kutta_4step(dt, eos, bc, src, qq);
apply_artificial_viscosity(dt, eos, bc, qq);
}
The whole step, arranged in the order of function calls, is as follows.
ModelBase::update
├─ MHD::cfl → Integrator::cfl time step dt (section 6)
├─ MHD::update → Integrator::update
│ ├─ divb_parameters_set(dt) set c_h and τ
│ ├─ runge_kutta_4step 4-stage time integration (section 7)
│ │ ├─ update_sc4(dt/4) → boundary conditions and halo exchange
│ │ ├─ update_sc4(dt/3) → boundary conditions and halo exchange
│ │ ├─ update_sc4(dt/2) → boundary conditions and halo exchange
│ │ └─ update_sc4(dt) → boundary conditions and halo exchange
│ └─ apply_artificial_viscosity artificial viscosity (section 10)
│ ├─ x direction → boundary conditions and halo exchange
│ ├─ y direction → boundary conditions and halo exchange
│ └─ z direction → boundary conditions and halo exchange
└─ Time::update advance the time
divb_parameters_set determines the divergence-cleaning speed \(c_h\) and the damping time \(\tau\) to match the time step.
// miso/include/miso/mhd_integrator_host.hpp
void divb_parameters_set(Real dt) {
ch_divb = 0.8 * cfl_number * grid.min_dxyz / dt;
ch_divb_square = ch_divb * ch_divb;
tau_divb = 2.0 * dt;
}
Since \(c_h \Delta t = 0.8\, C \min(\Delta x, \Delta y, \Delta z)\) (\(C\) is the CFL number), the waves of \(\phi\) also propagate at a speed that satisfies the CFL condition.
The CPU and GPU versions have the same function names and call order. The difference is whether the per-grid-point computation is written as a for statement or as a CUDA kernel.
Processing |
CPU version ( |
GPU version ( |
|---|---|---|
Preparing \(B^2\), \(h_t\), and \(\boldsymbol{v}\cdot\boldsymbol{B}\) |
The first triple loop of |
|
Update of each variable |
The second triple loop of |
|
CFL condition |
Take the minimum in a triple loop, then |
|
|
Same order |
Same order |
Artificial viscosity |
|
|
The CPU version of update_sc4 is written with an ordinary triple for statement, not for_each. With i as the outer loop and k as the inner loop, it reads memory along the layout of Array3D, in which the last index is contiguous (Chapter 7).
6. Time step from the CFL condition
The time step is chosen so that, at every grid point, the fastest wave does not cross the grid spacing in one step.
\(C\) is mhd.cfl_number in the configuration, and \(c_A\) is the Alfvén speed.
// miso/include/miso/mhd_integrator_host.hpp: Integrator::cfl (excerpt)
Real dt = 1.e10;
eos.sound_speed(backend::Host{}, qq.const_view(), cs.view());
for (int i = grid.i_margin; i < grid.i_total - grid.i_margin; ++i) {
for (int j = grid.j_margin; j < grid.j_total - grid.j_margin; ++j) {
for (int k = grid.k_margin; k < grid.k_total - grid.k_margin; ++k) {
...
Real total_vel = cs(i, j, k) + vv + ca;
dt = util::min2(dt, cfl_number
* util::min3<Real>(grid.dx[i], grid.dy[j], grid.dz[k])/total_vel);
}
}
}
Real dt_global;
MPI_Allreduce(&dt, &dt_global, 1, mpi::data_type<Real>(), MPI_MIN,
mpi::comm());
return dt_global;
The loop runs from i_margin up to just before i_total - i_margin, that is, over the grid points this rank is responsible for, excluding ghost cells. After each rank finds its minimum, MPI_MIN in MPI_Allreduce makes the global minimum common to all ranks (Chapter 8). This is because, unless all ranks advance with the same \(\Delta t\), the times of the values exchanged with neighbors would be inconsistent.
7. Four-stage Runge–Kutta method
7.1 runge_kutta_4step
Write the equation as \(\partial q/\partial t = R(q)\). \(q\) is the conserved quantities (\(\rho\), \(\rho\boldsymbol{v}\), \(\boldsymbol{B}\), \(\phi\), \(E\)), and \(R\) is the right-hand side (spatial derivatives and source terms). MISO builds \(q^{n+1}\) from \(q^n\) in the following 4 stages.
Each stage has the form “add the right-hand side, evaluated with the value from the previous stage, to the original value \(q^n\).” The only arrays needed are three sets: the original value, the value of the previous stage, and the result.
// miso/include/miso/mhd_integrator_host.hpp
template <typename EOS, typename BoundaryCondition, typename Source>
void runge_kutta_4step(const Real dt, const EOS &eos,
const BoundaryCondition &bc, const Source &src,
Fields<Real, backend::Host> &qq) {
update_sc4(dt / 4.0, eos, src, qq, qq, qq_rslt);
qq_argm.copy_from(qq_rslt);
apply_boundary_condition(bc, qq_argm);
update_sc4(dt / 3.0, eos, src, qq, qq_argm, qq_rslt);
qq_argm.copy_from(qq_rslt);
apply_boundary_condition(bc, qq_argm);
update_sc4(dt / 2.0, eos, src, qq, qq_argm, qq_rslt);
qq_argm.copy_from(qq_rslt);
apply_boundary_condition(bc, qq_argm);
update_sc4(dt, eos, src, qq, qq_argm, qq_rslt);
qq.copy_from(qq_rslt);
apply_boundary_condition(bc, qq);
}
The arguments of update_sc4(stage dt, eos, src, original value, value for evaluating the right-hand side, result) correspond to the formulas as follows.
Argument |
Array |
Symbol in the formula |
|---|---|---|
|
|
\(q^n\) (the value at the start of the step; unchanged during the 4 stages) |
|
|
The value from the previous stage that is fed into \(R(\cdot)\) |
|
|
The result of that stage |
The result of each stage is written to qq_rslt, copied to qq_argm with copy_from, and then the boundary condition is applied. update_sc4 updates only the grid points excluding the ghost cells, so the ghost cells must be refilled before the right-hand side of the next stage is computed. This is the same order as in the heat conduction calculation of Chapter 10, where the boundary values were prepared before each stage of the time integration. Only in the last stage is the result put back into qq, and this becomes \(q^{n+1}\).
7.2 Checking the stage coefficients
For the linear equation \(dq/dt = \lambda q\), we have \(R(q) = \lambda q\), so substituting in order with \(h = \lambda\Delta t\) gives
which agrees with the Taylor expansion of the exact solution \(e^{h} q^n\) up to the fourth-order term. The following short program solves \(dq/dt = -q\) up to \(t = 1\) with the same 4 stages and prints the difference from the exact solution \(e^{-1}\).
// rk4_linear.cpp: solve dq/dt = -q with the same 4-stage coefficients as MISO (illustrative example)
#include <cmath>
#include <iostream>
double solve(int n_step) {
const double dt = 1.0 / n_step;
double q = 1.0; // value at time t (qq in MISO)
for (int n = 0; n < n_step; ++n) {
double q_argm = q; // value used to evaluate the right-hand side (qq_argm in MISO)
q_argm = q + dt / 4.0 * (-q_argm); // stage 1
q_argm = q + dt / 3.0 * (-q_argm); // stage 2
q_argm = q + dt / 2.0 * (-q_argm); // stage 3
q = q + dt * (-q_argm); // stage 4 updates q
}
return q;
}
int main() {
for (int n : {10, 20, 40}) {
std::cout << "n_step = " << n
<< " error = " << std::abs(solve(n) - std::exp(-1.0)) << "\n";
}
}
g++ -std=c++17 -O2 -o rk4_linear rk4_linear.cpp
./rk4_linear
n_step = 10 error = 3.33241e-07
n_step = 20 error = 1.99761e-08
n_step = 40 error = 1.22274e-09
Doubling the number of steps reduces the error to about \(1/16 = 1/2^4\), which shows that the method is fourth-order accurate.
::This form of multistage method is known from its use by Jameson, Schmidt, and Turkel (1981, AIAA Paper 81-1259) for compressible flow calculations. Its advantage is that few arrays need to be kept at each stage.
The coefficients \(1/4, 1/3, 1/2, 1\) are chosen so that the method is fourth-order accurate for linear equations. For a nonlinear equation \(dq/dt = f(q)\), expanding and comparing the coefficient of \(f''f^2\) in the \(\Delta t^3\) term gives \(1/8\) for this method and \(1/6\) for the exact solution, which do not agree. The formal accuracy for nonlinear equations is therefore second order. On the other hand, the linear stability region is the same as that of the classical four-stage Runge–Kutta method. The “stability condition of RK4, \(\lambda\Delta t < 2.78\)” in the comment of miso/include/miso/mhd_resistivity.hpp also comes from this stability region.
8. Fourth-order central difference and the right-hand side
8.1 space_centered_4th
Spatial derivatives are approximated with a fourth-order central difference. On a uniformly spaced grid,
The error is proportional to \(\Delta x^4\). Because it uses 2 points on each side, the ghost cell width (grid.margin) must be at least 2.
// miso/include/miso/mhd_integrator.hpp
template <typename Real>
__host__ __device__ inline Real
space_centered_4th(Array3DView<const Real> qq, Real dxyzi, int i, int j, int k,
int is, int js, int ks) {
// clang-format off
return (
- qq(i + 2*is, j + 2*js, k + 2*ks)
+ 8.0*qq(i + is, j + js, k + ks)
- 8.0*qq(i - is, j - js, k - ks)
+ qq(i - 2*is, j - 2*js, k - 2*ks)
)*inv12<Real>*dxyzi;
// clang-format on
};
Argument |
Meaning |
|---|---|
|
The array to differentiate (a |
|
The reciprocal of the grid spacing, \(1/\Delta x\) ( |
|
The grid point at which the derivative is evaluated |
|
The direction of differentiation. For the \(x\) direction, |
Because the direction is selected with is, js, ks, one function can handle the derivatives in all 3 directions. grid.is is 1 if the number of grid points in the \(x\) direction is 2 or more, and 0 if it is 1. In a direction with one grid point, the index does not shift, and the derivative in that direction becomes 0 (Exercise 4). inv12<Real> is a variable template that represents \(1/12\) in type Real (miso/include/miso/constants.hpp, Chapter 6).
There are also functions with the same name that differentiate the product of two or three arrays (overloads, Chapter 2). space_centered_4th(c_ro, c_vx, dxi, ...) computes \(\partial_x(\rho v_x)\), and space_centered_4th(c_ro, c_vx, c_vy, dyi, ...) computes \(\partial_y(\rho v_x v_y)\). The product is formed at each grid point before the difference is taken.
8.2 Preparation in update_sc4
update_sc4 first prepares in arrays the quantities that are used many times on the right-hand side.
// miso/include/miso/mhd_integrator_host.hpp: update_sc4 (excerpt; formulas omitted, explanatory comments added)
eos.gas_pressure(backend::Host{}, qq_argm.const_view(), pr.view());
for (int i = 0; i < grid.i_total; ++i) {
for (int j = 0; j < grid.j_total; ++j) {
for (int k = 0; k < grid.k_total; ++k) {
bb(i, j, k) = ...; // B^2
ht(i, j, k) = ...; // ρe + p + B^2/4π + ρv^2/2
vb(i, j, k) = ...; // v·B
}
}
}
Array |
Formula |
Where it is used |
|---|---|---|
|
\(p = (\gamma-1)\rho e\) |
Pressure gradient in the equation of motion |
|
\(B^2\) |
Magnetic pressure \(B^2/8\pi\) in the equation of motion |
|
\(\rho e + p + \frac{1}{2}\rho v^2 + \frac{B^2}{4\pi}\) |
Energy flux |
|
\(\boldsymbol{v}\cdot\boldsymbol{B}\) |
Energy flux |
Because differences are taken, these are computed at all grid points including ghost cells. All of them are made from qq_argm (the value used to evaluate the right-hand side).
Next, read-only views are created with const_view() and given short names (c_ro, c_vx, … c_ph).
8.3 Correspondence between the equations and the code
The second triple loop updates the grid points excluding the ghost cells. We write the time step of a stage as \(\Delta t'\) (the dt argument; for example \(\Delta t/4\)) and space_centered_4th as \(D_x, D_y, D_z\).
Continuity equation
// miso/include/miso/mhd_integrator_host.hpp: update_sc4 (excerpt)
qq_rslt.ro(i, j, k) = qq_orgn.ro(i, j, k) + dt * (
-space_centered_4th(c_ro, c_vx, dxi, i, j, k, grid.is, 0, 0)
-space_centered_4th(c_ro, c_vy, dyi, i, j, k, 0, grid.js, 0)
-space_centered_4th(c_ro, c_vz, dzi, i, j, k, 0, 0, grid.ks)
+source_ro(src, c_qq, i, j, k)
);
The \(\rho v_x\) and so on on the right-hand side are values from qq_argm, and the \(\rho^n\) added to it is the value from qq_orgn. This matches the form of the formula in Section 7.
Equation of motion (\(x\) component)
// miso/include/miso/mhd_integrator_host.hpp: update_sc4 (excerpt)
qq_rslt.vx(i, j, k) = (
qq_orgn.ro(i, j, k) * qq_orgn.vx(i, j, k) + dt * (
-space_centered_4th(c_ro, c_vx, c_vx, dxi, i, j, k, grid.is, 0, 0)
-space_centered_4th(c_ro, c_vx, c_vy, dyi, i, j, k, 0, grid.js, 0)
-space_centered_4th(c_ro, c_vx, c_vz, dzi, i, j, k, 0, 0, grid.ks)
-space_centered_4th(c_pr, dxi, i, j, k, grid.is, 0, 0)
-space_centered_4th(c_bb, dxi, i, j, k, grid.is, 0, 0)*pii8<Real>
+pii4<Real>*(
+space_centered_4th(c_bx, c_bx, dxi, i, j, k, grid.is, 0, 0)
+space_centered_4th(c_bx, c_by, dyi, i, j, k, 0, grid.js, 0)
+space_centered_4th(c_bx, c_bz, dzi, i, j, k, 0, 0, grid.ks)
)
+source_vx(src, c_qq, i, j, k)
)
)/qq_rslt.ro(i, j, k);
The conserved quantity \(\rho v_x\) is advanced and then divided by the new density \(\rho^{\text{rslt}}\) to recover the velocity. For this reason, the continuity equation (qq_rslt.ro) must be computed first. pii4<Real> and pii8<Real> are \(1/4\pi\) and \(1/8\pi\) (miso/include/miso/constants.hpp). The \(y, z\) components have the same form.
Induction equation (\(x\) component)
// miso/include/miso/mhd_integrator_host.hpp: update_sc4 (excerpt)
qq_rslt.bx(i, j, k) = qq_orgn.bx(i, j, k) + dt * (
-space_centered_4th(c_vy, c_bx, dyi, i, j, k, 0, grid.js, 0)
-space_centered_4th(c_vz, c_bx, dzi, i, j, k, 0, 0, grid.ks)
+space_centered_4th(c_vx, c_by, dyi, i, j, k, 0, grid.js, 0)
+space_centered_4th(c_vx, c_bz, dzi, i, j, k, 0, 0, grid.ks)
-space_centered_4th(c_ph, dxi, i, j, k, grid.is, 0, 0)
+source_bx(src, c_qq, i, j, k)
);
This is the induction equation of Section 1.2 with \(i = x\) (the \(j = x\) term is not written because \(v_x B_x - v_x B_x = 0\)). The last term, \(-D_x\phi\), is the divergence-cleaning term.
The divergence-cleaning \(\phi\)
The formula for qq_rslt.ph takes the following form, using ch_divb_square (\(c_h^2\)) and util::exp(-dt/tau_divb).
The term \(-c_h^2\nabla\cdot\boldsymbol{B}\) is advanced with the finite difference, and the damping term \(-\phi/\tau\) is handled by multiplying by the exact-solution factor \(e^{-\Delta t'/\tau}\).
Energy equation
// miso/include/miso/mhd_integrator_host.hpp: update_sc4 (excerpt; parts elided with ...)
// Et: total energy per unit volume
// ei: is the internal energy per unit mass
const Real Et =
+ qq_orgn.ro(i,j,k)*qq_orgn.ei(i,j,k)
+ 0.5*qq_orgn.ro(i, j, k)*( ... ) // v^2 of qq_orgn
+ pii8<Real>*( ... ); // B^2 of qq_orgn
qq_rslt.ei(i, j, k) = ( Et + dt * (
-space_centered_4th(c_ht, c_vx, dxi, i, j, k, grid.is, 0, 0)
-space_centered_4th(c_ht, c_vy, dyi, i, j, k, 0, grid.js, 0)
-space_centered_4th(c_ht, c_vz, dzi, i, j, k, 0, 0, grid.ks)
+ pii4<Real>* (
+space_centered_4th(c_vb, c_bx, dxi, i, j, k, grid.is, 0, 0)
+space_centered_4th(c_vb, c_by, dyi, i, j, k, 0, grid.js, 0)
+space_centered_4th(c_vb, c_bz, dzi, i, j, k, 0, 0, grid.ks)
)
)
-0.5*qq_rslt.ro(i, j, k)*( ... ) // v^2 of qq_rslt (subtract the new kinetic energy)
- pii8<Real>*( ... ) // B^2 of qq_rslt (subtract the new magnetic energy)
+ dt * source_ei(src, c_qq, i, j, k)
)/qq_rslt.ro(i, j, k);
The total energy \(E\) is advanced, and the kinetic and magnetic energies of the new \(\rho\), \(\boldsymbol{v}\), and \(\boldsymbol{B}\) computed in the same stage are subtracted to recover the internal energy. Because it uses the new velocity and magnetic field, the energy equation is placed at the end of the loop. The source term source_ei is the right-hand side \(S_E\) of the total energy equation (per unit volume and unit time).
8.4 Extracting the source terms
source_ro(src, c_qq, i, j, k) and the like are functions generated by the macro MISO_MHD_DEFINE_SOURCE_TERM in miso/include/miso/mhd_integrator.hpp.
The following shows the body of the function that the macro generates for vy, with the macro argument name replaced by vy (expanded for explanation).
// miso/include/miso/mhd_integrator.hpp: MISO_MHD_DEFINE_SOURCE_TERM expanded for vy (excerpt)
if constexpr (impl_source::has_vy<Source, Real>::value) {
return src.vy(qq, i, j, k);
} else {
return Real(0);
}
If the source term type Source has a member function such as vy(FieldsView<const Real>, int, int, int) const, it is called; if not, 0 is returned (Chapter 6). mhd::EmptySourceTerm has nothing, so all 8 source terms are 0. The decision is made at compile time, so terms that are not defined add no computation. The source terms are called at each stage with c_qq (qq_argm), the value used to evaluate the right-hand side.
9. Boundary conditions and halo exchange
9.1 apply_boundary_condition
The following function is called after each stage of RK4 and after each direction of the artificial viscosity.
// miso/include/miso/mhd_integrator_host.hpp
template <typename BoundaryCondition>
void apply_boundary_condition(const BoundaryCondition &bc,
Fields<Real, backend::Host> &qq) {
bc.apply(qq.view(), grid.const_view());
halo_exchanger.apply(qq);
}
Apply the physical boundary condition given by the user with
bc.apply.Exchange ghost cell values with the neighboring ranks with
halo_exchanger.apply(halo exchange, Chapter 8).
The argument form of bc.apply is a convention that the bc of Model must follow.
void apply(mhd::FieldsView<Real> qq, GridView<const Real> grid) const;
9.2 Halo exchange and periodic boundaries
Halo exchange is in HaloExchanger in miso/include/miso/mhd_halo_exchange.hpp. The CPU version of apply does the following for each direction, in the order \(x\) → \(y\) → \(z\).
Pack the range to send (
marginlayers from the edge) of the 9 variables into a sendArray4D.Send and receive on the positive and negative sides at the same time with
MPI_IsendandMPI_Irecv, and wait withMPI_Waitall.Write the received values into the ghost cells.
Because the directions are exchanged one at a time, the range sent in the \(y\) direction includes the ghost cells received just before in the \(x\) direction. As a result, the corner ghost cells also receive correct values.
The neighboring ranks are x_procs_pos and the like, which mpi::Shape obtains with MPI_Cart_shift. In a direction made periodic with domain.periodic, the neighbor of the rank at one end of the domain is the rank at the opposite end. If the number of processes in that direction is 1, the neighbor is the rank itself. In other words, the halo exchange takes care of the periodic boundary. For a problem in which all directions are periodic, you can use mhd::EmptyBoundaryCondition, which does nothing, as bc.
At the end of a direction that is not periodic, the neighbor is MPI_PROC_NULL and no exchange takes place. Filling those ghost cells is the role of bc.apply.
9.3 Example of a physical boundary
The BoundaryCondition in demo/hd2d_rayleigh_taylor/src/main.cpp places symmetric boundaries at both ends in the \(y\) direction.
// demo/hd2d_rayleigh_taylor/src/main.cpp: BoundaryCondition::apply (excerpt)
namespace bc = miso::boundary_condition;
Backend btag{};
if (bc::is_physical_boundary(Direction::Y, Side::INNER, mpi_shape)) {
bc::symmetric(btag, qq.ro, grid, Sign::Pos, Direction::Y, Side::INNER);
bc::symmetric(btag, qq.vx, grid, Sign::Pos, Direction::Y, Side::INNER);
bc::symmetric(btag, qq.vy, grid, Sign::Neg, Direction::Y, Side::INNER);
...
}
is_physical_boundary (miso/include/miso/boundary_condition.hpp) checks whether this rank has no neighboring rank on the negative side in the \(y\) direction (Side::INNER), that is, whether it is responsible for the edge of the domain. When the domain is decomposed with MPI, some ranks are not responsible for an edge, so this check is necessary.
symmetric copies the value at the mirror-image position across the boundary into the ghost cell. With Sign::Neg, the sign is reversed. Only the velocity vy perpendicular to the boundary is reversed, which makes the flow through the boundary 0.
10. Artificial viscosity
With the central difference alone, numerical oscillations occur near sharp changes such as shock waves. After RK4, MISO applies artificial viscosity (artificial diffusion) once, selectively smoothing only the neighborhood of sharp changes. The implementation is impl_host::ArtificialViscosity in miso/include/miso/mhd_artificial_viscosity_host.hpp, and the method is based on Rempel (2014, ApJ 789, 132) (see the comment in config_defaults.hpp).
10.1 Update in each direction
// miso/include/miso/mhd_integrator_host.hpp(excerpt)
template <typename EOS, typename BoundaryCondition>
void apply_artificial_viscosity(const Real dt, const EOS &eos,
const BoundaryCondition &bc,
Fields<Real, backend::Host> &qq) {
artdiff.characteristic_velocity_eval(qq, eos);
// x direction
artdiff.update(qq, qq_rslt, Direction::X, dt);
qq.copy_from(qq_rslt);
apply_boundary_condition(bc, qq);
... // same 3 lines for y and z (Direction::Y, Direction::Z)
}
First, compute the characteristic speed \(c = f_{cs} c_s + f_{vv}|\boldsymbol{v}| + f_{ca} c_A\) at all grid points (the coefficients are mhd.artificial_viscosity.cs_fac and so on in the configuration). Then update one direction at a time in the order \(x, y, z\), applying the boundary condition and halo exchange each time.
10.2 Diffusive flux
The update in one direction is the difference of the fluxes \(F_{i+1/2}\) at the cell boundaries \(i+1/2\).
The targets are \(\rho\), \(\rho\boldsymbol{v}\), \(\boldsymbol{B}\), \(\phi\), and \(\rho e\). The flux is built by two functions in miso/include/miso/limiter.hpp.
Function |
Computation |
|---|---|
|
The slope \(\Delta q_i\). If \(\varepsilon(q_i - q_{i-1})\), \(\varepsilon(q_{i+1} - q_i)\), and \((q_{i+1} - q_{i-1})/2\) have the same sign, it is the one with the smallest absolute value; otherwise it is 0 (generalized minmod limiter). \(\varepsilon\) is |
|
Builds the values on the left and right of the cell boundary, \(q_L = q_i + \Delta q_i/2\) and \(q_R = q_{i+1} - \Delta q_{i+1}/2\), and returns \(F = -\frac{1}{2} c\, \Phi\, (q_R - q_L)\) |
\(\Phi\) is defined using \(r = \min\left(1, (q_R - q_L)/(q_{i+1} - q_i)\right)\): it is \(\max(0, 1 + h(r - 1))\) if \(r \ge 0\) and 0 if \(r < 0\). \(h\) is fh in the configuration. Where the variation is smooth, \(q_R \approx q_L\), so the flux is almost 0. Diffusion acts only where there is a sharp change and \(q_R - q_L\) remains large.
For the energy, the total energy is updated with the flux of \(\rho e\), and then the new kinetic and magnetic energies are subtracted to recover the internal energy. The kinetic and magnetic energy lost to artificial viscosity is transferred to the internal energy.
11. Output and restart
11.1 Output times
// miso/include/miso/mhd_model_base.hpp
void save_if_needed() {
if (time.time >= time.dt_output * time.n_output) {
save_state();
time.log();
time.n_output++;
}
}
Output is written at the first step at which the time reaches dt_output * n_output or more. The time step is not adjusted to match the output time, so the output time is not exactly a multiple of dt_output (Exercise 6). time.log() prints lines like the following on rank 0 (an example running demo/mhd2d_vortex with tend: 0.03 on 2 processes).
time = 0.00; n_step = 0; n_output = 0
time = 0.01; n_step = 4; n_output = 1
time = 0.02; n_step = 9; n_output = 2
time = 0.03; n_step = 13; n_output = 3
11.2 Files that are written
All of them are written under io.save_dir (by default, data/ in the same folder as the configuration file).
File |
Function that writes it |
Contents |
|---|---|---|
|
|
The configuration with defaults filled in |
|
|
Coordinates of the global grid |
|
|
MPI coordinates of each rank |
|
|
Time, output number, and step count, one per line |
|
|
The latest output number |
|
|
The 9 variables of each rank |
Checkpoint::save copies qq on the compute location to an array on the CPU (copy_from), writes the element size (4 for float) at the beginning as a std::uint32_t, and then writes out the arrays, including ghost cells, as they are, in the order ro, vx, vy, vz, bx, by, bz, ei, ph. The layout is the in-memory layout of Array3D (the last index k is contiguous). In the example above, the grid of each rank is \((64 + 2\cdot 2) \times (128 + 2\cdot 2) \times 1\), so the file size is \(4 + 68 \times 132 \times 9 \times 4 = 323140\) bytes. In Python, read it with pymiso.Data (Chapter 1).
11.3 Restart
If io.continue is true (the default is also true) and time/n_output.txt remains, run() creates the initial condition, then calls load_state() and continues the calculation from the latest output. Time::load reads the time file on rank 0 and distributes it with MPI_Bcast, and in Checkpoint::load each rank reads its own mhd.<n_output>.<rank>.bin. The design uses the output as is as the data for restarting (a checkpoint). To recompute from the beginning, delete data/ or set io.continue: false.
12. Where to make changes when creating your own problem
In most cases, you create a new problem by copying a demo’s main.cpp and rewriting the 4 components of Model and config.yaml. You do not need to modify anything inside miso/include/miso/.
What you want to change |
Where to write it |
Convention (the form in which it is called) |
Demo to refer to |
|---|---|---|---|
Initial condition |
Type of |
|
|
Boundary condition |
Type of |
|
|
Source term |
Type of |
Define only the terms you need, in the form |
|
Equation of state |
Type of |
Provide |
|
How the time step is determined |
|
Override it in the same form as |
|
Problem parameters |
A custom section in |
Read them in the constructor, as in |
|
Points to note are as follows.
In the GPU version, boundary conditions and source terms are called on arrays on the GPU. Attach
__host__ __device__to the member functions of the source term and use only values that the GPU can read (numeric members and views of arrays on the compute location) (Chapter 9).If you write a
forstatement directly in a boundary condition, it works only on the CPU. Use functions that take the compute-location tagbtag, such asboundary_condition::symmetric, or usefor_each.If you make all directions periodic, set all of
domain.periodicinconfig.yamltotrue, and you can usemhd::EmptyBoundaryCondition<Real>asbc.
How to write each component, with examples, is summarized in Customization in the MISO documentation.
13. Exercises: checking by reading the source
Exercise 1: How many times boundary conditions are called in one step
When advancing one step with the settings of demo/mhd2d_vortex, read miso/include/miso/mhd_integrator_host.hpp and count how many times bc.apply is called. Also count the number of calls made when creating the initial condition, and the number of times MPI_Allreduce is called in one step.
It is called 7 times in one step.
In the 4 stages of
runge_kutta_4step,apply_boundary_conditionis called once in each (4 times).In
apply_artificial_viscosity, it is called once in each of the \(x, y, z\) directions (3 times).
apply_boundary_condition calls halo_exchanger.apply after bc.apply, so the halo exchange also happens 7 times in one step. Even in a 2D calculation, the call for the artificial viscosity in the \(z\) direction is not skipped, so the count is the same.
When creating the initial condition, it is called once at the end of MHD::apply_initial_condition.
MPI_Allreduce is called once in Integrator::cfl. If the source term has dt_limit(), it is called once more in ModelBase::update.
You can also verify the counts by using, as bc, a boundary condition that counts the number of calls inside apply, and calling update() directly. Since apply is a const member function, it cannot modify member variables inside. Put the counting variable outside the boundary condition struct (for example, a variable declared outside any function in main.cpp). Counted this way, you get 1 for the initial condition, 7 for one step, and 14 for two steps.
Exercise 2: Filling in default values
mhd.cfl_number is not written in demo/hd2d_rayleigh_taylor/config.yaml. Read config_defaults.hpp and merge_yaml and answer what the CFL number and the artificial viscosity parameter ep are in this calculation. Also explain in which file, after the run, you can check the values that were actually used.
The CFL number is the default value, 0.5, and ep is 2.0, as written by the user.
merge_yaml descends into the mhd section of the defaults, and since cfl_number is not in the user’s settings, it adds 0.5. artificial_viscosity is a section (a YAML map), so it descends into it recursively and fills in key by key. ep is written by the user, so it is not overwritten. mhd.resistivity.cfl_number, which the user did not write, is also filled in with 0.5.
The completed result remains in data/config.yaml, which save_metadata() writes to the output destination. In the settings of demo/hd2d_rayleigh_taylor, io.save_dir is the default data/, so the file is demo/hd2d_rayleigh_taylor/data/config.yaml.
Exercise 3: Gravity source term
The SourceTerm in demo/hd2d_rayleigh_taylor/src/main.cpp provides a uniform gravity (magnitude \(g\)) in the \(-y\) direction. Explain which term of which formula in update_sc4 changes when this source term is used. Also, explain from the energy equation in Section 8.3 what must be returned as the source term for ei in order to conserve the total energy.
SourceTerm defines vx, vy, vz, and ei, and the only one that returns a nonzero value is \(-g\rho\) for vy.
// demo/hd2d_rayleigh_taylor/src/main.cpp
__host__ __device__ inline Real vy(mhd::FieldsView<const Real> qq, int i, int j,
int k) const noexcept {
return -g_grav * qq.ro(i, j, k);
}
What changes is the +source_vy(src, c_qq, i, j, k) term in the equation of motion in the \(y\) direction.
\(\rho\) is the value in c_qq, that is, qq_argm of that stage, and it is evaluated in each of the 4 RK4 stages. ro, bx, by, and bz are not defined, so they become 0 through if constexpr.
Regarding the energy: MISO’s energy equation is the equation for the total energy \(E\), and the internal energy is recovered with \(e = (E - \rho v^2/2 - B^2/8\pi)/\rho\). If the work that gravity does on the fluid, \(\rho\boldsymbol{v}\cdot\boldsymbol{g} = -g\rho v_y\), is not included in \(S_E\), the kinetic energy gained from gravity is subtracted without having been added to \(E\), and the internal energy decreases. To conserve the total energy, return \(-g\rho v_y\) in ei.
// example of the source term for ei (illustrative)
__host__ __device__ inline Real ei(mhd::FieldsView<const Real> qq, int i, int j,
int k) const noexcept {
return -g_grav * qq.ro(i, j, k) * qq.vy(i, j, k);
}
The ei in demo/hd2d_rayleigh_taylor returns 0, so in this demo the work done by gravity is deducted from the internal energy side.
When adding gravity, besides the source term you also need to consider making the initial condition satisfy hydrostatic equilibrium (in this demo, \(p = p_0 - g\rho y\)) and the boundary conditions at the ends in the \(y\) direction.
Exercise 4: Differences in the z direction in a 2D calculation
demo/mhd2d_vortex is a 2D calculation with k_size: 1. Read AxisGrid in miso/include/miso/grid.hpp and explain what happens in this case to the \(z\)-direction difference in update_sc4, the CFL condition, the halo exchange, and the \(z\)-direction update of the artificial viscosity.
In a direction where the number of grid points size is 1, AxisGrid sets stride = 0, margin = 0, and total = 1, sets the grid spacing ds to 1.e30, and sets its reciprocal dsi to 0. grid.ks is the stride, so it is 0; grid.k_margin is 0; and grid.k_total is 1.
Difference: In
space_centered_4th(..., 0, 0, grid.ks),ks = 0, so all 4 points are the same grid pointk, and the combination \((-1 + 8 - 8 + 1)\) gives 0. In addition,dziis also 0. The derivative terms in the \(z\) direction become 0.CFL condition: In
min3(dx[i], dy[j], dz[k]),dzis1.e30, so the \(z\) direction does not restrict the time step.Halo exchange: The \(z\) direction of
HaloExchangeris not performed becausegrid.k_size > 1is false (the GPU version decides withgrid.k_total > 1).Artificial viscosity: For
Direction::Z,ks = 0, so the left and right values are all the same, and the flux difference \(F_{i+1/2} - F_{i-1/2}\) is 0. Thedziit is multiplied by is also 0, so the values do not change.
In every case the loop range is just k = 0, and no out-of-range array access occurs.
Exercise 5: Checking the accuracy of the fourth-order central difference (advanced)
Write a program that uses MISO, compute the derivative of \(f(x) = \sin x\) with mhd::space_centered_4th, and find the maximum difference from \(\cos x\). Divide \(0 \le x < 2\pi\) into \(n\) cells and place the values at the cell centers \(x_i = (i + 1/2)\Delta x\). Check how the error changes for \(n = 16, 32, 64\). For the build, use the “minimal CMake configuration for using MISO from your own program” in Chapter 1.
What to build is as follows.
Write
#include <miso/core.hpp>and#include <miso/mhd.hpp>(space_centered_4this included frommhd.hpp).Create an
Array3D<double>with 2 ghost cells on each side (of sizen + 4, 1, 1), and put \(\sin x\) at the position \(x = (i - 2 + 1/2)\Delta x\) of indexi. If you fill the ghost cells with the same formula, the result is the same as a periodic boundary.For
i = 2ton + 1, find the maximum of the difference betweenmhd::space_centered_4th(f.const_view(), 1.0 / dx, i, 0, 0, 1, 0, 0)and \(\cos x\).
space_centered_4th does not use MPI, so this program works even without creating an Env.
An example of the result is as follows.
n = 16 error = 0.000763339
n = 32 error = 4.90805e-05
n = 64 error = 3.08927e-06
ratio 16/32 = 15.5528
ratio 32/64 = 15.8874
Doubling the number of grid points reduces the error to about \(1/16 = 1/2^4\), which shows that the scheme is fourth-order accurate. The leading term of the error of the difference formula is \(-\frac{\Delta x^4}{30} f^{(5)}\), and for \(f = \sin x\) its maximum is \(\Delta x^4/30\). For \(n = 16\), \(\Delta x = \pi/8\), so \((\pi/8)^4/30 \approx 7.9\times 10^{-4}\), which agrees with the result.
Exercise 6: Output times
When you run demo/mhd2d_vortex with dt_output: 0.01, the first line of data/time/time.00000001.txt is a value slightly off from 0.01, such as 0.0100069. Read ModelBase::save_if_needed and ModelBase::run and explain why this offset occurs.
run() calls save_if_needed() after every step and writes output at the first step at which time.time >= dt_output * n_output holds. The time step \(\Delta t\) is determined only by the CFL condition and is not shortened to land exactly on the output time. As a result, the time that is written is that of the step that first exceeds 0.01, which is later than 0.01 by at most \(\Delta t\).
The exact value of the output time is written on the first line of time/time.NNNNNNNN.txt. When you use the time in analysis, use the value in this file (the value that pymiso reads), not a multiple of dt_output.