Sapphire++
 v1.3.0-4-gbe09da0
Loading...
Searching...
No Matches
Quick-start

Introduction

This quick-start guide will help you get started with Sapphire+⁠+. While it does not cover all the features of Sapphire+⁠+, it provides the basics to write your first application. We will demonstrate Sapphire+⁠+ using a simplified version of CR acceleration at a parallel shock. Note that this example is primarily intended as a technical demonstration. For a physically accurate model of CR acceleration in a parallel shock, refer to the parallel shock example. We strongly recommend going through the parallel shock example after finishing this quick-start, as it introduces additional concepts in Sapphire+⁠+, such as parameter files. Furthermore, we provide a tutorial for visualizing results using ParaView as well as general tips and tricks for visualization. More examples that demonstrate advanced features of Sapphire+⁠+ can be found in the examples section.

The example we are considering uses the following setup: We have a planar shock that the particles will cross, to gain energy by the first-order Fermi mechanism [6] [11] [13]. We will prescribe the background plasma in the shock frame, resulting in a stationary velocity field \(\mathbf{u}(\mathbf{x})\) with the shock compression ratio \(r\) at \(x = 0\):

\begin{align} \mathbf{u}(\mathbf{x}) = \hat{\mathbf{e}}_x \begin{cases} u_{\mathrm{sh}} & \text{if} & x < 0 \\ \frac{u_{\mathrm{sh}}}{r} & \text{if} & x > 0 \end{cases} \end{align}

The transport of energetic charged (test-)particles in this plasma is described by the Vlasov-Fokker-Planck (VFP) equation:

\[ \frac{\partial f}{\partial t} + (\mathbf{u} + \mathbf{v}) \cdot \nabla_{x} f - \gamma m \frac{\mathrm{D} \mathbf{u}}{\mathrm{D} t} \cdot \nabla_{p}f - \mathbf{p} \cdot\nabla_{x} \mathbf{u}\cdot \nabla_{p} f = \frac{\nu}{2} \Delta_{\theta, \varphi} f + S\, . \]

Here, \(f(\mathbf{x}, \mathbf{p})\) is the distribution function of the energetic particles with momentum \(\mathbf{p} = \gamma m \mathbf{v}\). \(\frac{\mathrm{D}}{\mathrm{D} t} = \frac{\partial}{\partial t} + \mathbf{u} \cdot \frac{\partial}{\partial \mathbf{x}}\) denotes the material derivative, \(\nu(p)\) and \(S(\mathbf{x}, \mathbf{p})\) the scattering frequency and source respectively. (We neglect the magnetic field, as the gyro-motion is perpendicular to the plane).

Sapphire+⁠+ efficiently solves this equation by exploiting that the distribution function \(f\) is in many physical environments almost isotropic. It thus expresses \(f\) as a series of spherical harmonics, i.e.

\[ f(t, \mathbf{x}, \mathbf{p}) = \sum^{l_{\rm max}}_{l = 0} \sum^{l}_{m = 0} \sum^{1}_{s = 0} f_{lms}(t, \mathbf{x}, p) Y_{lms}(\theta,\varphi) \,, \]

where \(\mathbf{p} = p \, (\cos\theta, \sin\theta \cos\varphi, \sin\theta \sin\varphi)^T\) is used. The zeroth order term is the isotropic part and higher order terms represent the anisotropies of the distribution function. Sapphire+⁠+ computes the expansion coefficients \(f_{lms}(\mathbf{x}, p, t)\) up to a maximum order \(l_{\rm max}\). Hence, it reduces the dimensionality of the problem from the full six dimensional phase space \((\mathbf{x}, \mathbf{p})\) to a four dimensional reduced phase space \(\mathbf{\xi} = (\mathbf{x}, p)^{T}\). We can reduce the dimensionality even further, noticing that our problem only depends on one spatial dimension. Our spatial domain for this problem \((\mathbf{x}) = (x)\), called configuration space, has therefore the dimensionality dim_cs = 1. The computational domain for this problem therefore has the dimensionality dim = dim_ps = dim_cs + 1 = 2, equalling the dimensionality of the reduced phase space dim_ps.

Note
The computational domain is always the reduced phase space. Therefore, the dimension of the grid and (most) quantities dim, equals the dimension of the reduced phase space, dim = dim_ps.

In practice this means, that if we refer to a point in Sapphire+⁠+, the first dim_cs components are the spatial coordinates \(\mathbf{x} = (x,y,z)\) and the last component corresponds to the momentum \(p\). Since Sapphire+⁠+ uses a logarithmic momentum variable \(\ln(p)\) by default, we have the following in this example:

const double x = point[0];
const double log_p = point[1];
Note
Due to limitations of deal.II, the maximum computational dimension is currently limited to dim = dim_ps = 3. This means that one either has to assume an at most 2D spatial domain (dim_cs = 2 \(\implies\) dim_ps = 3), or a three-dimensional but momentum independent problem (dim_ps = dim_cs = 3).

Implementation

Now that we have covered the introduction, we can implement the equation in Sapphire+⁠+. To do this, we need to modify the sapphirepp/include/config.h file. This file contains the physical setup for the simulation. You can find the complete source code on GitHub. Although the file is over 600 lines long, most of it is boilerplate code that you can ignore. The lines you need to adapt for your specific problem are marked with // !!!EDIT HERE!!!. In the following we go through these lines step by step.

Dimensionality and VFPFlags

First, we have to specify the dimension of the reduced phase space for the problem. As elaborated above, the configuration space for this problem is 1 dimensional, corresponding to a 2D reduced phase space. We therefore set the dimension of the computational domain to 2 in sapphirepp/include/config.h (see the corresponding line numbers below):

125 // !!!EDIT HERE!!!
127 constexpr unsigned int dimension = 2;

Next, we have to specify which terms of the VFP equation are used. Because we do not consider a magnetic field in this example, the VFP equation is,

\[ \frac{\partial f}{\partial t} + (\mathbf{u} + \mathbf{v}) \cdot \nabla_{x} f - \gamma m \frac{\mathrm{D} \mathbf{u}}{\mathrm{D} t} \cdot \nabla_{p}f - \mathbf{p} \cdot\nabla_{x} \mathbf{u}\cdot \nabla_{p} f = \frac{\nu}{2} \Delta_{\theta, \varphi} f + S\, . \]

The temporal evolution of \(f\), included in the computation via the time evolution flag, is determined by four effects: First, we have spatial advection, \((\mathbf{u} + \mathbf{v}) \cdot \nabla_{x} f\), due to the background plasma velocity \(\mathbf{u}(\mathbf{x},t)\) and the particle velocity \(\mathbf{v}(\mathbf{p})\). Next, we have a momentum term, \(\left( \gamma m \frac{\mathrm{D} \mathbf{u}}{\mathrm{D} t} + \mathbf{p} \cdot\nabla_{x} \mathbf{u} \right) \cdot \nabla_{p} f\), describing the (perceived) acceleration of particles in the mixed coordinate frame due to changes in background velocity. The third effect are collisions (i.e. elastic interactions of the particles with the magnetohydrodynamic turbulence of the background plasma) that we model with an effective scattering term, \(\frac{\nu}{2} \Delta_{\theta, \varphi} f\), which only changes the direction \(\theta, \varphi\) of the particles but not their energy. Last, we model the injection of particles with a source term \(S\). In Sapphire+⁠+, we activate these effects by listing them in the vfp_flags variable (separated by a bitwise-or | and prefixed with the namespace VFPFlags::):

As you can see, we added two additional flags, time_independent_fields and time_independent_source, specifying that both the velocity field and the source are time independent. This results in noticeable performance gains, as Sapphire+⁠+ only needs to calculate the corresponding DG-matrices once instead of every time step. A full list of all possible flags can be found in the sapphirepp::VFP::VFPFlags documentation.

Scattering frequency

Now that we have stated the general problem, we need to specify the scattering frequency \(\nu(p)\), the source \(\mathbf{S}(x,p)\), and the background velocity field \(\mathbf{u}(x)\). We start with the scattering frequency. For simplicity, we assume it is independent of momentum. Using dimensionless units, we set

\[ \nu = 100 \,. \]

It is worth mentioning the units used in Sapphire+⁠+. Sapphire+⁠+ employs dimensionless units throughout the code, meaning both the input (in terms of user-defined functions in config.h) and the output are dimensionless. Generally, length scales are defined in terms of the gyro-radius of a reference particle, \(x^{*} = x/r_{g,0}\), and time is given in multiples of the reference gyro-frequency, \(t^{*} = t \omega_{g,0}\). Momentum is defined in terms of the rest mass of a test particle, \(p^{*} = p/m_{0} c\), while velocity is given as a fraction of \(c\), \(v^{*} = v/c\). An overview of the reference values can be found in sapphirepp::VFP::ReferenceValues.

We note again, that this setup serves as a technical demonstration. A more realistic setup with a momentum-depend scattering frequency is shown in the parallel shock example. In this example, the implementation in Sapphire+⁠+ corresponds to only one line in sapphirepp/include/config.h:

419 // !!!EDIT HERE!!!
420 scattering_frequencies[q_index] = 100.;
Note
You might notice the index q_index in the line above. For performance reasons, the ScatteringFrequency function calculates the scattering frequencies for a list of points rather than for one individual point. Consequently, the function returns a list of scattering frequency values, each corresponding to a point in the list, indexed by q_index.

Source term

Next, we specify the injection of particles at the shock, \(x = 0\). In analytic models, particles are typically injected isotropically at a discrete momentum \(p_0\) [6] [11] [13],

\[ S(x, \mathbf{p}) = Q \delta(x) \delta(p - p_0) \,. \]

Here, \(Q\) represents the injection rate, and \(\delta\) denotes the delta distribution. Numerically, we approximate these delta distributions as Gaussians with standard deviations \(\sigma_x\) and \(\sigma_p\), respectively [13],

\[ S(x, \mathbf{p}) = \frac{Q}{2 \pi \sigma_p \sigma_x} e^{-\frac{\left(p - p_0 \right)^2}{2 \sigma_p^2}} e^{-\frac{x^2}{2 \sigma_x^2}} \,. \]

To implement this source function in Sapphire+⁠+, we decompose it into its coefficients \(S_{lms}\) using spherical harmonics, similar to the distribution function,

\[ S(t, \mathbf{x}, \mathbf{p}) = \sum^{l_{\rm max}}_{l = 0} \sum^{l}_{m = 0} \sum^{1}_{s = 0} S_{lms}(t, \mathbf{x}, p) Y_{lms}(\theta,\varphi) \,. \]

Since particles are injected isotropically in this example, only \(S_{000}\) contributes:

\begin{align} & S(x, \mathbf{p}) = S_{000}(x, p) Y_{000}(\theta,\varphi) = \frac{1}{\sqrt{4 \pi}} S_{000}(x, p) \\ \implies & S_{000}(x, p) = \sqrt{4 \pi} \frac{Q}{2 \pi \sigma_p \sigma_x} e^{-\frac{\left(p - p_0 \right)^2}{2 \sigma_p^2}} e^{-\frac{x^2}{2 \sigma_x^2}} \\ & S_{lms}(x,p) = 0 \,, \quad \text{otherwise} \end{align}

For this example, we use an injection momentum of \(p_0 = 1\), an amplitude of \(Q = 0.1\), and standard deviations \(\sigma_x = 0.1\) and \(\sigma_p = 0.001\), again using dimensionless units. In Sapphire+⁠+, this corresponds to the following implementation:

481 // !!!EDIT HERE!!!
482 // Define parameters
483 const double p0 = 1.;
484 const double Q = 0.1;
485 const double sig_p = 0.1;
486 const double sig_x = 0.001;
487
488 // Get x and p value
489 const double x = point[0];
490 const double p = std::exp(point[1]); // point[1] = log(p)
491
492 // Convert i -> l, m, s
493 const unsigned int l = lms_indices[i][0];
494 const unsigned int m = lms_indices[i][1];
495 const unsigned int s = lms_indices[i][2];
496
497 // isotropic part
498 if (l == 0 && m == 0 && s == 0)
499 {
500 // shifted Gaussian in x and p
501 // s_000 = sqrt(4 pi) * s
502 source_values[i] =
503 Q / (std::sqrt(std::numbers::pi) * sig_p * sig_x) *
504 std::exp(-(p - p0) * (p - p0) / (2. * sig_p * sig_p)) *
505 std::exp(-x * x / (2. * sig_x * sig_x));
506 }
507 // vanishing anisotropic part
508 else
509 {
510 source_values[i] = 0.;
511 }

Given the complexity of this implementation, additional comments may be helpful. First, we define the parameters \(p_0\), \(Q\), \(\sigma_p\), and \(\sigma_x\) as const double with their respective values. Next, we retrieve the values of \(x\) and \(p\) at the current point. As noted earlier, point[0] corresponds to \(x\), while point[1] corresponds to \(\ln(p)\).

In the next step, it is important to note that Sapphire+⁠+ collapses the three indices \(l\), \(m\), and \(s\) into a single index \(i(l,m,s)\). This approach is advantageous as it allows to store all coefficients \(S_{lms} = S_{i(l,m,s)}\) in a single Vector. To convert from the index i to the indices \(l\), \(m\), and \(s\), we can utilize the lms_indices map (see here for more details).

Finally, we implement the coefficients of the source term \(S_{lms}\) by separating the isotropic part, \(l = m = s = 0\), from the vanishing anisotropic part.

Velocity field

Last, we need to implement the background velocity field \(\mathbf{u}(\mathbf{x})\). Since the velocity field of a shock, as given above, is discontinuous, we instead parametrize it using a \(\tanh\) profile for numerical implementation [13],

\[ \mathbf{u}(x) = \frac{u_{\mathrm{sh}}}{2r} \left( (1-r) \tanh\left(\frac{x}{d_{\mathrm{sh}}}\right) + (1+r)\right) \hat{\mathbf{e}}_x\,. \]

Here, \(r\) is the compression ratio, \(u_{\mathrm{sh}}\) is the shock velocity, and \(d_{\mathrm{sh}}\) is the shock width. This profile smooths the shock over a width \(d_{\mathrm{sh}}\), recovering the discontinuous profile in the limit \(d_{\mathrm{sh}} \to 0\),

\begin{align} \mathbf{u}(\mathbf{x}) \overset{d_{\mathrm{sh}} \to 0}{\to} \frac{u_{\mathrm{sh}}}{2r} \hat{\mathbf{e}}_x \begin{cases} -(1-r) + (1+r) & \text{if} & x < 0 \\ (1-r) + (1+r) & \text{if} & x > 0 \end{cases} = \frac{u_{\mathrm{sh}}}{2r} \hat{\mathbf{e}}_x \begin{cases} 2r & \text{if} & x < 0 \\ 2 & \text{if} & x > 0 \end{cases} \,. \end{align}

In this example, we choose the following set of parameters in dimensionless units: \(r = 4\), corresponding to a strong shock, \(u_{\mathrm{sh}} = 0.1\), and \(d_{\mathrm{sh}} = 0.001\). The implementation in Sapphire+⁠+ is given by:

645 // !!!EDIT HERE!!!
646 // u(x) = u_sh/2r * ((1-r)*tanh(x/d_sh) + (1+r))
647 const double r = 4.;
648 const double d_sh = 0.001;
649 const double u_sh = 0.1;
650
651 // u_x
652 velocity[0] =
653 u_sh / (2 * r) * ((1 - r) * std::tanh(point[0] / d_sh) + (1 + r));
654 velocity[1] = 0.; // u_y
655 velocity[2] = 0.; // u_z

Note that even though we specified the problem as 1D in configuration space, Sapphire+⁠+ requires us to specify \(u_y\) and \(u_z\) components. These correspond to a homogeneous flow out of the plane. In this example, we set them to zero and only specify the \(u_x\) (i.e., velocity[0]) component. Again, we can access the \(x\) value via point[0].

Investigating the VFP equation above, we notice that it not only depends on the value of velocity but also on different combinations of its derivatives \(\frac{\partial u_{x_i}}{\partial x_j}\). Therefore, we also need to implement these expressions in Sapphire+⁠+. We start by prescribing the divergence, which can be computed as,

\[ \nabla \cdot \mathbf{u}(x) = \frac{u_{\mathrm{sh}}}{2 r} \frac{1-r}{d_{\mathrm{sh}}} \left(1 - \tanh\left(\frac{x}{d_{\mathrm{sh}}}\right)^2\right) \,. \]

683 // !!!EDIT HERE!!!
684 // u(x) = u_sh/2r * ((1-r)*tanh(x/d_sh) + (1+r))
685 // => d/dx u(x) = u_sh/2r 1/d_sh (1-r) (1-tanh(x/d_sh)^2)
686 const double r = 4.;
687 const double d_sh = 0.001;
688 const double u_sh = 0.1;
689
690 const double x = points[q_index][0];
691
692 // div u
693 divergence[q_index] =
694 u_sh / (2 * r) * (1 - r) / d_sh *
695 (1 - std::tanh(x / d_sh) * std::tanh(x / d_sh));
Note
The divergence_list function uses a list of points to calculate the divergence for multiple points at once. Therefore, we use the index q_index to access individual points and their corresponding divergence values. The \(x\) value is consequently given by points[q_index][0].

Similarly, we can calculate and implement the material derivative,

\[ \frac{\mathrm{D} \mathbf{u}}{\mathrm{D} t} = \frac{\partial \mathbf{u}}{\partial t} + \mathbf{u} \cdot \frac{\partial \mathbf{u}}{\partial \mathbf{x}} = \left(\frac{u_{\mathrm{sh}}}{2r}\right)^2 \left( (1-r) \tanh\left(\frac{x}{d_{\mathrm{sh}}}\right) + (1+r)\right) \frac{1-r}{d_{\mathrm{sh}}} \left(1 - \tanh\left(\frac{x}{d_{\mathrm{sh}}}\right)^2\right) \,. \]

727 // !!!EDIT HERE!!!
728 // u(x) = u_sh/2r * ((1-r)*tanh(x/d_sh) + (1+r))
729 // => D/Dt u(x) = d/dt u(x) + u d/dx u(x)
730 // = (u_sh/2r)^2 * ((1-r)*tanh(x/d_sh) + (1+r)) *
731 // 1/d_sh (1-r) (1-tanh(x/d_sh)^2)
732 const double r = 4.;
733 const double d_sh = 0.001;
734 const double u_sh = 0.1;
735
736 const double x = points[q_index][0];
737
738 // D/Dt u_x
739 material_derivatives[q_index][0] =
740 u_sh * u_sh / (4 * r * r) / d_sh * (1 - r) *
741 ((1 - r) * std::tanh(x / d_sh) + (1 + r)) *
742 (1 - std::tanh(x / d_sh) * std::tanh(x / d_sh));
743 material_derivatives[q_index][1] = 0.; // D/Dt u_y
744 material_derivatives[q_index][2] = 0.; // D/Dt u_z

Last, we also need the Jacobian \(\frac{\partial u_{x_i}}{\partial x_j}\) of the velocity field to compute the numerical flux. We only have a \(x\)- \(x\)-component in the Jacobian which is given by:

\[ \frac{\partial u_{x}}{\partial x} = \frac{u_{\mathrm{sh}}}{2 r} \frac{1-r}{d_{\mathrm{sh}}} \left(1 - \tanh\left(\frac{x}{d_{\mathrm{sh}}}\right)^2\right) \,. \]

775 // !!!EDIT HERE!!!
776 // u(x) = u_sh/2r * ((1-r)*tanh(x/d_sh) + (1+r))
777 // => u_00 = du/dx = u_sh/2r 1/d_sh (1-r) (1-tanh(x/d_sh)^2)
778 const double r = 4.;
779 const double d_sh = 0.001;
780 const double u_sh = 0.1;
781
782 const double x = points[q_index][0];
783
784 // \partial u_x / \partial x
785 jacobians[q_index][0][0] =
786 u_sh / (2 * r) * (1 - r) / d_sh *
787 (1 - std::tanh(x / d_sh) * std::tanh(x / d_sh));
788 jacobians[q_index][0][1] = 0.; // \partial u_x / \partial y
789 jacobians[q_index][0][2] = 0.; // \partial u_x / \partial z
790
791 jacobians[q_index][1][0] = 0.; // \partial u_y / \partial x
792 jacobians[q_index][1][1] = 0.; // \partial u_y / \partial y
793 jacobians[q_index][1][2] = 0.; // \partial u_y / \partial z
794
795 jacobians[q_index][2][0] = 0.; // \partial u_z / \partial x
796 jacobians[q_index][2][1] = 0.; // \partial u_z / \partial y
797 jacobians[q_index][2][2] = 0.; // \partial u_z / \partial z


This concludes the implementation of the example in Sapphire+⁠+. The next step is to compile the code to prepare it for running the simulation.

Compile and run

After modifying sapphirepp/include/config.h to implement the physical setup, we need to recompile Sapphire+⁠+. First, we use CMake to locate and link all dependencies and prepare the compilation process. To keep source code and executables separate, we create a sapphirepp/build directory to store the compiled executable and related files. To prepare for compilation, execute the following commands (this may be redundant if you have already followed the instructions in Compiling Sapphire+⁠+):

cd path/to/sapphirepp
mkdir build
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

Upon successful execution, you should see output similar to:

Sapphire++ version 1.1.0
-- The C compiler identification is GNU 11.4.0
-- The CXX compiler identification is GNU 11.4.0
...
-- Using the deal.II-9.5.2 installation found at /home/schulze/.local/lib/dealii
...
-- Preparing Sapphire++
-- Preparing VFP module
-- Set up sapphirepp for Sapphire++ using /home/schulze/Documents/PhD/Code/sapphirepp/include/config.h
-- Configuring done (0.3s)
-- Generating done (0.0s)
-- Build files have been written to: /home/schulze/Documents/PhD/Code/sapphirepp/build


Next, we have to compile Sapphire+⁠+ in the build directory:

make --directory=build

To speed up the compilation, you can use multiple processes in parallel by running:

make --directory=build -j N

where N is the number of processes to use. You should see output similar to the following:

[ 7%] Building CXX object src/utils/CMakeFiles/UtilsLib.dir/output-parameters.cpp.o
[ 15%] Building CXX object src/utils/CMakeFiles/UtilsLib.dir/sapphirepp-logstream.cpp.o
[ 23%] Building CXX object src/utils/CMakeFiles/UtilsLib.dir/tools.cpp.o
[ 30%] Linking CXX static library libUtilsLib.a
[ 30%] Built target UtilsLib
[ 38%] Building CXX object src/vfp/CMakeFiles/VFPLib.dir/particle-functions.cpp.o
[ 46%] Building CXX object src/vfp/CMakeFiles/VFPLib.dir/pde-system.cpp.o
[ 53%] Building CXX object src/vfp/CMakeFiles/VFPLib.dir/phase-space-reconstruction.cpp.o
[ 61%] Building CXX object src/vfp/CMakeFiles/VFPLib.dir/vfp-parameters.cpp.o
[ 69%] Linking CXX static library libVFPLib.a
[ 69%] Built target VFPLib
[ 76%] Building CXX object CMakeFiles/sapphirepp.dir/sapphirepp.cpp.o
[ 84%] Building CXX object CMakeFiles/sapphirepp.dir/src/vfp/upwind-flux.cpp.o
[ 92%] Building CXX object CMakeFiles/sapphirepp.dir/src/vfp/vfp-solver.cpp.o
[100%] Linking CXX executable sapphirepp
[100%] Built target sapphirepp


Once compiled, the sapphirepp executable is created in the build folder. We execute it using the following command:

./build/sapphirepp

A successful execution will produce output similar to the following:

Sapphire::Start Sapphire++ v1.1.0 with 1 MPI process(es) [2024/7/31 10:43:50]
Sapphire:VFP::Setup VFP equation solver. [10:43:50]
Sapphire:VFP::Time step 0 at t = 0.00000 [10:43:50]
Sapphire:VFP::Time step 1 at t = 1.00000 [10:43:51]
Sapphire:VFP::Time step 2 at t = 2.00000 [10:43:51]
Sapphire:VFP::Time step 3 at t = 3.00000 [10:43:51]
Sapphire:VFP::Time step 4 at t = 4.00000 [10:43:51]
Sapphire:VFP::Time step 5 at t = 5.00000 [10:43:52]
Sapphire:VFP::Time step 6 at t = 6.00000 [10:43:52]
Sapphire:VFP::Time step 7 at t = 7.00000 [10:43:52]
Sapphire:VFP::Time step 8 at t = 8.00000 [10:43:53]
Sapphire:VFP::Time step 9 at t = 9.00000 [10:43:53]
Sapphire:VFP::Time step 10 at t = 10.0000 [10:43:53]
...
Sapphire:VFP::Time step 195 at t = 195.000 [10:44:46]
Sapphire:VFP::Time step 196 at t = 196.000 [10:44:47]
Sapphire:VFP::Time step 197 at t = 197.000 [10:44:47]
Sapphire:VFP::Time step 198 at t = 198.000 [10:44:47]
Sapphire:VFP::Time step 199 at t = 199.000 [10:44:48]
Sapphire:VFP::Simulation ended at t = 200.000 [10:44:48]


Sapphire+⁠+ also supports high performance multiprocessing with Open-MPI. To activate parallel computation on N processors, use the mpirun command:

mpirun -np N ./build/sapphirepp

You can verify if the parallelization is successful by checking the startup message of Sapphire+⁠+:

Sapphire::Start Sapphire++ v1.1.0 with N MPI process(es) [2024/7/31 10:43:50]
...

Results

Running the simulation will create a results directory, with a subdirectory 01 identifying the current run. Listing the contents of this folder with:

ls results/01

we see the following files:

log.prm
solution_0000.pvtu solution_0000.0.vtu solution_0000.1.vtu ... solution_0000.N-1.vtu
solution_0001.pvtu solution_0001.0.vtu solution_0001.1.vtu ... solution_0001.N-1.vtu
solution_0002.pvtu solution_0002.0.vtu solution_0002.1.vtu ... solution_0002.N-1.vtu
solution_0003.pvtu solution_0003.0.vtu solution_0003.1.vtu ... solution_0003.N-1.vtu
...
solution_0199.pvtu solution_0199.0.vtu solution_0199.1.vtu ... solution_0199.N-1.vtu
solution_0200.pvtu solution_0200.0.vtu solution_0200.1.vtu ... solution_0200.N-1.vtu

The first file, log.prm, contains the parameters used for the run. An introduction to parameter files is given in the parallel shock example. The other files, solution_XXXX.*, contain the simulation results. The _XXXX suffix indicates the time step the result corresponds to. Each time step has multiple associated files:

  • solution_XXXX.pvtu: Contains metadata for the results at each time step, primarily collecting the distributed data from different processors in a multiprocessor run.
  • solution_XXXX.n.vtu: Contains part of the solution handled by processor number n. In a single-threaded run, you will only have the solution_XXXX.0.vtu files. Otherwise, the index n runs up to N-1, with N being the number of processors.
Note
Sapphire+⁠+ supports other output formats, like hdf5.
Sapphire+⁠+ overwrites previous results upon rerunning. We recommend using a unique identifier for each run. For more details, refer to the documentation on Parameter files and OutputParameters.


To visualize the results, we recommend using dedicated visualization software like ParaView or VisIt. A detailed tutorial on how to visualise the results using ParaView is given in the next section on Visualization. Here, we want to present the results to give a first impression.

When opening the files as a time series, the data is displayed on a 2D grid. The x-coordinate of this grid corresponds to \(x\), while the y-coordinate corresponds to \(\ln p\). The output includes a scalar field for each expansion coefficient \(f_{lms}(x, \ln p)\), labelled f_lms. The most significant component is the zeroth-order coefficient, \(f_{000}\), which represents the isotropic part of the distribution function. In the animation below, we illustrate how \(f_{000}(t, x, \ln p)\) evolves over time using a logarithmic colour scale:

2d time series

We can see, that the particles are injected at \(\ln p_0 = 0\) and gain energy through diffusive shock acceleration, populating the phase space \(\ln p > 0\). Since the particles do not suffer any losses, the low momentum phase space ( \(\ln p < 0\)) remains unpopulated. The particles are advected downstream ( \(x > 0\)) with the background flow, while their density in the upstream ( \(x < 0\)) is exponentially suppressed. As the injection rate is constant, the distribution function reaches a steady state by the end of the simulation.

Note
As mentioned above, the output of Sapphire+⁠+ is in dimensionless units.

We again want to emphasize, that this example mainly serves as technical demonstration and therefore only uses a low-fidelity simulation with coarse resolution. A physically accurate, high resolution simulation can be found in the parallel shock example. There, we also provide a detailed discussion of the steady state solution and comparison with analytic models.

To further investigate the \(x\) dependence of the solution, we can examine a cut-out along \(x\) at constant \(\ln p\):

Steady state f(x) plot

Again, we can see that \(f_{000}\) is constant for downstream ( \(x > 0\)) but decays exponentially upstream, \(f_{000}(x) \propto e^{x}\) for \(x < 0\). Additionally, the figure illustrates the non-vanishing dipole anisotropy \(f_{100}(x)\). It exhibits a discontinuity at \(x = 0\): For \(x > 0\), the dipole anisotropy vanishes ( \(f_{100}(x) = 0\)), indicating an isotropic particle distribution. For \(x < 0\), \(f_{100}\) is negative ( \(f_{100}(x) < 0\)), which corresponds to an excess of particles moving in negative \(x\) direction relative to the background flow. However, since Sapphire+⁠+ employs mixed coordinates, this does not necessarily mean that particles are preferably moving towards the upstream. Instead, it suggests that particles are "swimming against the stream".

Note
Sapphire+⁠+ uses mixed coordinates. This means that the momentum \(\mathbf{p}\) is defined in the frame of the background plasma \(\mathbf{u}(\mathbf{x})\). One can transform between the momentum in a laboratory frame \(\tilde{\mathbf{p}}\) and the mixed coordinate momentum \(\mathbf{p}\) using: \(\mathbf{p} = \tilde{\mathbf{p}} - m \mathbf{u}(\mathbf{x})\).

When comparing with analytic models of parallel shock acceleration, we expect the momentum ( \(p\)) dependence of the distribution function to follow a power-law for \(p > p_0\), specifically \(f_{000}(p) \propto p^{-4}\) [6] [11] [13]. To demonstrate that this example accurately captures this behaviour, we present a log-log plot of \(p^4 f_{000}\), using a cut-out at constant \(x\):

Steady state f(ln(p)) plot


To continue, we recommend reviewing the ParaView tutorial and the parallel shock example. In the ParaView tutorial, we provide a beginner-friendly guide on creating the plots shown above using ParaView. The parallel shock example discusses how to use and define runtime parameters, among other topics. Additionally, it serves as a high-fidelity physical model of acceleration in parallel shocks.

Previous Next
Home Visualization

Author
Florian Schulze (flori.nosp@m.an.s.nosp@m.chulz.nosp@m.e@mp.nosp@m.i-hd..nosp@m.mpg..nosp@m.de)
Date
2024-07-31