Sapphire++
 v1.3.0-121-g2682132
Loading...
Searching...
No Matches
Parallel shock

Introduction

In this example we simulate the time-dependent transport and acceleration of particles at a parallel shock. The setup is similar to the quick start, but we use an advanced and more realistic configuration. You can find a detailed description of the setup in [15].

The astrophysical shock is assumed to move along the \(x\)-axis. In the rest frame of the shock wave, the background plasma's velocity is represented as \(\mathbf{U}_1 = U_{\rm sh} \hat{\mathbf{e}}_x\) upstream ( \(x<0\)) and \(\mathbf{U}_1 = U_{\rm sh}/r \hat{\mathbf{e}}_x\) downstream ( \(x>0\)). Here, \(r\) is the compression ratio of the shock and \(U_{\rm sh}\) is its velocity. As this represents a simple plain shock wave, we only require a single dimension in configuration space for the simulation. We also presuppose a constant magnetic field that is parallel to the shock normal, i.e. \(\mathbf{B} = B_0 \hat{\mathbf{e}}_x\). This kind of setup is referred to as a parallel shock.

In astrophysical shocks, particles can be accelerated to high energies through the process of diffusive shock acceleration [6] [12]. This involves injecting particles at the shock with a momentum of \(p_{\rm inj}\) at a rate \(Q\). These particles originate from the thermal background plasma and propagate around the shock. These particles may be scattered by Alvén waves travelling through the background plasma, a process we model with the help of scattering frequency \(\nu\). In [15] a constant scattering frequency is assumed, i.e. it is independent of \(p\) and \(x\):

\[ \nu = \nu_0 \, . \]

For the case of a constant scattering frequency, an analytic solution for the steady-state distribution function in mixed coordinates is presented in [6] :

\[ f(x, p, \theta) = \frac{Q}{p_{\rm inj}U_{1}} \frac{3r}{r - 1} \left(\frac{p}{p_{\rm inj}}\right)^{-3r/(r - 1)} \times \begin{cases} e^{3U_{1} \nu / V^{2} x} \left[1 - 3 U_{1}/V \cos\theta\right] &\quad \text{ for } x < 0 \\ 1 &\quad \text{ for } x \geq 0 \end{cases} \]

where \(V = p/\gamma m\) is the particle velocity in mixed coordinates. Decomposing this solution into spherical harmonics, we find that the isotropic part of the distribution function is given by

\[ f_{000}(x, p) = \sqrt{4\pi} \frac{Q}{p_{\rm inj}U_{1}} \frac{3r}{r - 1} \left(\frac{p}{p_{\rm inj}}\right)^{-3r/(r - 1)} \times \begin{cases} e^{3U_{1} \nu / V^{2} x} &\quad \text{ for } x < 0 \\ 1 &\quad \text{ for } x \geq 0 \end{cases} \]

Moreover, in the upstream we have a non-trivial anisotropy, namely

\[ f_{100}(x, p) = -3 U_{1}/V \sqrt{\frac{4\pi}{3}} \frac{Q}{p_{\rm inj}U_{1}} \frac{3r}{r - 1} \left(\frac{p}{p_{\rm inj}}\right)^{-3r/(r - 1)} e^{3U_{1} \nu / V^{2} x} \quad \text{ for } x < 0 \,, \]

while the downstream is isotropic, i.e.

\[ f_{100} = 0 \quad \text{ for } x \geq 0 \,. \]

An approximate analytic expression for the temporal evolution of the isotropic part of the distribution function \(f\) has been given in [7] and [8] . At the shock wave

\[ F(t, x = 0, p) = f_{000}(x = 0, p) \phi(t) \]

where \(F\) is the time-dependent isotropic part of the distribution function and

\[ \phi(t) = \frac{1}{2}\left[\exp\left(\frac{2 c^{2}_{1}}{c_{2}}\right) \mathrm{erfc}\left(\sqrt{\frac{c^{3}_{1}}{2 t c_{2}}} + \sqrt{\frac{c^{\phantom{3}}_{1} t}{2 c_{2}}}\right) + \mathrm{erfc}\left(\sqrt{\frac{c^{3}_{1}}{2 t c_{2}}} - \sqrt{\frac{c^{\phantom{3}}_{1}t}{2 c_{2}}}\right)\right] \,. \]

The constants \(c_1\) and \(c_2\) are the mean and the variance of the acceleration time. They are

\[ \begin{align} c_1 &= \frac{r}{2 U^{2}_{1} \nu} \frac{r + 1}{r - 1} \ln\left(\frac{1 + p^{2}}{1 + p^{2}_{\rm inj}}\right) \, \\ c_2 &= \frac{1}{3\nu^{2}} \frac{r}{U^{4}_{1}} \frac{r^{3} + 1}{r - 1} \left[ \frac{1}{1 + p^{2}} - \frac{1}{1 + p^{2}_{\rm inj}} + \ln\left(\frac{1 + p^{2}}{1 + p^{2}_{\rm inj}}\right)\right] \,. \end{align} \]

More details can be found in [15] .

Parameter files

A feature of Sapphire+⁠+, not covered in the quick start guide, is the use of parameter files. Its implementation uses the ParameterHandler class of deal.II. Parameter files allow you to define simulation parameters that are set at runtime, referred to hereafter as runtime parameters. This is in contrast to compile-time parameters.

Runtime parameters are read from a parameter file, such as parameter.prm, at the start of the simulation. You can pass a parameter file to Sapphire+⁠+ as the first command-line argument, e.g.

./build/sapphirepp parameter-file.prm

This enables you to modify simulation parameters without recompiling the code, which is particularly useful for parameter studies. Sapphire+⁠+ includes several predefined runtime parameters, such as the grid dimensions and the expansion order of the spherical harmonic expansion of \(f\).

Parameters in Sapphire+⁠+ are organised into categories and subcategories, primarily focusing on Output and VFP equation parameters. Key parameters are:

Parameter Name Category Description
Results directory Output Specifies the directory for output files.
Format Output Defines the output format (e.g., vtu/pvtu/hdf5).
Output frequency Output Determines the frequency of output.
Simulation identifier Output Name of the simulation run, i.e. subfolder for the simulation.
Checkpoint frequency Output Time step frequency at which checkpoints are created.
Expansion order VFP:Expansion Sets the expansion order in spherical harmonics.
Polynomial degree VFP:Finite Elements Indicates the polynomial degree of the finite element method.
Grid type VFP:Mesh Specifies the grid type (e.g., Shock grid/Hypercube/File).
Final time VFP:Time stepping Sets the final time of the simulation.
Time step size VFP:Time stepping Determines the time step size of the simulation.
Method VFP:Time stepping Defines the time stepping method (e.g., CN/ERK4/...).

A comprehensive and descriptive sample parameter file, template-parameter.prm, is located in the sapphirepp folder. This file is automatically generated when running Sapphire+⁠+ without any arguments. Here's an example of the syntax used in .prm files:

subsection Output
set Results folder = ./results
set Format = pvtu
set Output frequency = 1
set Simulation identifier =
end
subsection VFP
subsection Expansion
set Expansion order = 1
end
subsection Finite element
set Polynomial degree = 1
end
subsection Mesh
set Grid type = Shock grid
end
subsection Time stepping
set Method = CN
set Final time = 200
set Time step size = 1.0
end
end
Note
Note that section and parameter names are case-sensitive and can contain spaces. Moreover, not all parameters listed in the template file are used. It depends on the activated sapphirepp::VFP::VFPFlags, which ones are actually relevant.

For more details on the predefined parameters in Sapphire+⁠+ we also refer to the documentation of the sapphirepp::Utils::OutputParameters and the sapphirepp::VFP::VFPParameters class.

Note
The default behaviour of Sapphire+⁠+ is to overwrite results from previous runs. We recommend using the Simulation identifier parameter to give each run a unique ID, e.g. parallel-shock-1.

Users can also define their own parameters to describe their physical setup. An example of this can be found in the Implementation section.

Implementation

The implementation of this example can be found in the example directory sapphirepp/examples/vfp/parallel-shock. The following sections explain it in detail. As shown in the quick start, there are only a few lines in the config.h file that need to be adjusted.

VFP equation

As can be seen in the analytic solution presented above, the distribution function \(f\) depends only on \((x, p, \theta)\). This means that we need a single dimension in configuration space. Together with the \(p\) dependence this results in a two-dimensional reduced phase space. The \(\theta\) dependence is handled by the spherical harmonics with \(l>0\). Because we don't have any dependence on \(\varphi\), spherical harmonics with \(m>0\) will be zero.

131 constexpr unsigned int dimension = 2;

Next, we need to specify which terms of the VFP equation are necessary to model the transport and acceleration of particles at a parallel shock. We actually have to solve the full 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 + q \mathbf{v} \cdot \left( \mathbf{B} \times \nabla_{p} f \right) = \frac{\nu}{2} \Delta_{\theta, \varphi} f + S \,. \]

We therefore need to activate all flags in the vfp_flags variable. In addition, we know that the velocity field and magnetic field are time independent. This also holds true for the source term.

User-defined runtime parameters

This section outlines the process of implementing user-defined runtime parameters that describe their physical setup. The example parameters include:

Name Symbol Code Description
Shock velocity \(U_{\rm sh}\) u_sh Velocity of the shock.
Compression ratio \(r\) compression_ratio Compression ratio of the shock.
Injection momentum \(p_{\rm inj}\) p_inj Momentum of the injected particles.
Injection rate \(Q\) Q Rate of injected particles.
Scattering frequency \(\nu_0\) nu0 Scattering frequency of the particles.
Magnetic field \(B_0\) B0 Magnetic field strength.

Additionally, there are several numerical parameters that are not part of the physical setup:

Name Symbol Code Description
Shock width \(d_{\rm sh}\) shock_width Width of the shock.
Injection width in \(p\) \(\sigma_p\) sig_p Width of the injection in momentum.
Injection width in \(x\) \(\sigma_x\) sig_x Width of the injection in configuration space.
Injection position \(x_{\rm inj}\) x_inj Position of the injection.

Implementing these parameters in Sapphire+⁠+ involves a two-step process:

  1. Define the parameters within the PhysicalParameters class in the config.h file. This step informs the compiler about their existence and sets default values to be used if the parameter is not specified in the parameter file.

    55 double u_sh = 0.1;
    56 double B0 = 1.;
    57 double compression_ratio = 4.;
    58 double shock_width = 0.04;
    59 double nu0 = 0.1;
    60
    61 // Source
    62 double Q = 0.1;
    63 double p_inj = 2.;
    64 double x_inj = 0.;
    65 double sig_p = 0.125;
    66 double sig_x = 0.125;
  2. Add the parameters to the parameter file to ensure that the parameter parser expects them and automatically sets their values. The deal.II class ParameterHandler prm and its add_parameter method are used for this purpose:

    prm.add_parameter("entry", parameter, "Description of the parameter");

    The declare_parameters() method is edited to include the following code:

    84 prm.add_parameter("u_sh", u_sh, "The shock velocity.");
    85 prm.add_parameter("B0", B0, "The magnetic field strength upstream.");
    86 prm.add_parameter("compression ratio",
    87 compression_ratio,
    88 "The compression ratio of the shock.");
    89 prm.add_parameter("shock width", shock_width, "The width of the shock.");
    90 prm.add_parameter("nu0", nu0, "The scattering frequency.");
    91
    92 // Source
    93 prm.add_parameter("Q", Q, "The injection rate.");
    94 prm.add_parameter("p_inj", p_inj, "The injection momentum.");
    95 prm.add_parameter("x_inj", x_inj, "The injection position.");
    96 prm.add_parameter("sig_p",
    97 sig_p,
    98 "The width of the source in momentum space.");
    99 prm.add_parameter("sig_x",
    100 sig_x,
    101 "The width of the source in configuration space.");
Note
An alternative approach for defining parameters, offering more fine-grained control over parsing, is introduced in the gyro motion with advection example.

Scattering frequency

We now implement the functions modelling the physical scenario. We start with the scattering frequency. As stated above, we assume a constant scattering frequency, i.e. \(\nu = \nu_0\). This translates to

247 for (unsigned int q_index = 0; q_index < points.size(); ++q_index)
248 {
249 // Constant scattering frequency
250 scattering_frequencies[q_index] = prm.nu0;
251 }

The function calculates the scattering frequency at multiple points in the \((x,\ln p)\)–domain. We use the index q_index to refer to an individual point. Notice that we access the runtime parameter \(\nu_0\) via the (previously modified) PhysicalParameters class prm.

Source term

Furthermore, we would like to inject mono-energetic particles at the shock. To model this we use a Gaussian profile centred at \((x_{\rm inj}, p_{\rm inj})\) with width \(\sigma_x\) and \(\sigma_p\),

\[ S(x, p) = \frac{Q}{2\pi\sigma_x\sigma_p} \exp\left[ -\frac{(x - x_{\rm inj})^2}{2\sigma_x^2} - \frac{(p - p_{\rm inj})^2}{2\sigma_p^2} \right] \, . \]

As already explained in the quick start, the source term needs to be decomposed into spherical harmonics. As we inject an isotropic distribution of particles, we can set all \(l>0\) components to zero. Notice that we use the index i to refer to the components of the spherical harmonic decomposition, \(i(l,m,s)\), at a single point in the \((x,\ln p)\)–domain.

286 if (i == 0)
287 {
288 // shifted Gaussian in x and p
289 const double p = std::exp(point[1]) - prm.p_inj;
290 const double x = point[0] - prm.x_inj;
291
292 // s_000 = sqrt(4 pi) * s
293 source_values[0] =
294 prm.Q /
295 (std::sqrt(std::numbers::pi) * prm.sig_p * prm.sig_x) *
296 std::exp(-p * p / (2. * prm.sig_p * prm.sig_p)) *
297 std::exp(-x * x / (2. * prm.sig_x * prm.sig_x));
298 }
299 else
300 source_values[i] = 0.;

We emphasise that the second component of point, i.e. point[1], corresponds to the momentum variable \(\ln p\).

Magnetic field

As described above, we use a constant magnetic field parallel to the shock, \(\mathbf{B} = B_0 \hat{\mathbf{e}}_x\). It does not influence the steady-state solution, but nevertheless we include it for completeness.

332 magnetic_field[0] = prm.B0; // B_x
333 magnetic_field[1] = 0.; // B_y
334 magnetic_field[2] = 0.; // B_z

Velocity field

The velocity field is the same as in the quick start example. We use a \(\tanh\) profile to model the transition at the shock wave. The only modification is, that we now use the user-defined runtime parameters \(U_{\rm sh}\), \(r\) and \(d_{\rm sh}\). Otherwise, \(\mathbf{U}(\mathbf{x})\), its divergence \(\nabla \cdot \mathbf{U}(\mathbf{x})\), the material derivative \(\mathrm{D} \mathbf{U}/\mathrm{D} t\) and the Jacobian \(\partial U_{x}/\partial x\) stay the same.

  1. Background velocity field value \(\mathbf{U}(\mathbf{x})\):

    364 // u(x) = u_sh/2r * ((1-r)*tanh(x/x_s) + (1+r))
    365
    366 // u_x
    367 velocity[0] =
    368 prm.u_sh / (2 * prm.compression_ratio) *
    369 ((1 - prm.compression_ratio) * std::tanh(point[0] / prm.shock_width) +
    370 (1 + prm.compression_ratio));
    371 velocity[1] = 0.; // u_y
    372 velocity[2] = 0.; // u_z
  2. Background velocity divergence \(\nabla \cdot \mathbf{U}(\mathbf{x})\):

    388 // u(x) = u_sh/2r * ((1-r)*tanh(x/x_s) + (1+r))
    389 // => d/dx u(x) = u_sh/2r 1/x_s (1-r) (1-tanh(x/x_s)^2)
    390
    391 // div u
    392 divergence[q_index] =
    393 prm.u_sh / (2 * prm.compression_ratio) *
    394 (1 - prm.compression_ratio) / prm.shock_width *
    395 (1 - std::tanh(points[q_index][0] / prm.shock_width) *
    396 std::tanh(points[q_index][0] / prm.shock_width));
  3. Background velocity material derivative \(\mathrm{D}\mathbf{U}/\mathrm{D} t\):

    414 // u(x) = u_sh/2r * ((1-r)*tanh(x/x_s) + (1+r))
    415 // => D/Dt u(x) = d/dt u(x) + u d/dx u(x)
    416 // = (u_sh/2r)^2 * ((1-r)*tanh(x/x_s) + (1+r)) *
    417 // 1/x_s (1-r) (1-tanh(x/x_s)^2)
    418
    419 // D/Dt u_x
    420 material_derivatives[q_index][0] =
    421 prm.u_sh * prm.u_sh /
    422 (4 * prm.compression_ratio * prm.compression_ratio) /
    423 prm.shock_width * (1 - prm.compression_ratio) *
    424 ((1 - prm.compression_ratio) *
    425 std::tanh(points[q_index][0] / prm.shock_width) +
    426 (1 + prm.compression_ratio)) *
    427 (1 - std::tanh(points[q_index][0] / prm.shock_width) *
    428 std::tanh(points[q_index][0] / prm.shock_width));
    429 material_derivatives[q_index][1] = 0.; // D/Dt u_y
    430 material_derivatives[q_index][2] = 0.; // D/Dt u_z
  4. Background velocity Jacobian \(\partial U_{x}/ \partial x\):

    448 // u(x) = u_sh/2r * ((1-r)*tanh(x/x_s) + (1+r))
    449 // => u_00 = du/dx = u_sh/2r 1/x_s (1-r) (1-tanh(x/x_s)^2)
    450
    451 // \partial u_x / \partial x
    452 jacobians[q_index][0][0] =
    453 prm.u_sh / (2 * prm.compression_ratio) *
    454 (1 - prm.compression_ratio) / prm.shock_width *
    455 (1 - std::tanh(points[q_index][0] / prm.shock_width) *
    456 std::tanh(points[q_index][0] / prm.shock_width));
    457 jacobians[q_index][0][1] = 0.; // \partial u_x / \partial y
    458 jacobians[q_index][0][2] = 0.; // \partial u_x / \partial z
    459
    460 jacobians[q_index][1][0] = 0.; // \partial u_y / \partial x
    461 jacobians[q_index][1][1] = 0.; // \partial u_y / \partial y
    462 jacobians[q_index][1][2] = 0.; // \partial u_y / \partial z
    463
    464 jacobians[q_index][2][0] = 0.; // \partial u_z / \partial x
    465 jacobians[q_index][2][1] = 0.; // \partial u_z / \partial y
    466 jacobians[q_index][2][2] = 0.; // \partial u_z / \partial z

Compile and run

To run the simulation, implement the above functions in the config.h file and recompile Sapphire+⁠+:

make --directory=build

Alternatively, you can use the implementation in the examples/vfp/parallel-shock folder. This executable will be named parallel-shock instead of sapphirepp. Note that for this to work, Sapphire+⁠+ must be configured with the -DEXAMPLES=ON option.

cmake -S . -B build -DEXAMPLES=ON
make --directory=build parallel-shock

We recommend running the simulation with the parameters given in examples/vfp/parallel-shock/parameter.prm:

subsection Output
set Results folder = ./results
set Simulation identifier = parallel-shock
set Format = pvtu
set Output frequency = 1
set Checkpoint frequency = 50
end
subsection VFP
subsection Expansion
set Expansion order = 1
end
subsection Finite element
set Polynomial degree = 1
end
subsection Mesh
set Grid type = Shock grid
set Point 1 = -100, -2.3
set Point 2 = 100, 4.6
set Number of cells = 120, 64
set Shock width = 0.5
set Number of shock cells = 10
set Scaling factor shock = 1.1
subsection Boundary conditions
set lower x = zero inflow
set upper x = continuous
set lower p = continuous
set upper p = continuous
end
end
subsection Time stepping
set Final time = 5e4
set Method = CN
set Time step size = 100
end
end
subsection Physical parameters
set B0 = 1.
set nu0 = 1.
set compression ratio = 4.
set shock width = 0.5
set u_sh = 0.03
set Q = 0.1
set p_inj = 1.
set x_inj = 0.0
set sig_p = 0.1
set sig_x = 0.1
end

Run the simulation with:

mpirun -n 4 ./build/examples/vfp/parallel-shock/parallel-shock examples/vfp/parallel-shock/parameter.prm

We provide a sapphireppplot script to generate the plots:

conda activate sapplot
export SAPPHIREPP_RESULTS=$(pwd)/results
python examples/vfp/parallel-shock/plot_parallel_shock.py

Resume simulation

Sometimes simulations fail. This can be related to a bug - in that case you probably need to rerun the simulation from start after fixing it. But it can also be due to hardware failure or because of time limits for large simulations on HPC clusters. In this case it is imperative to be able to restart simulation from an earlier time step. For this special output files are needed, called checkpoints. (The normal output is processed so it can be easily visualized, but it looses information on the underlying discontinuous Galerkin (dG) degrees of freedom (DoFs).)

To enable checkpointing, you need to set the Checkpoint frequency in the parameter file. In this example we use a Checkpoint frequency = 50, which means that a checkpoint will be created every 50 simulation time steps. This is quite excessive and chosen for educational purposes. A good checkpoint frequency is a compromise of how long it takes to run a time step, the expected failure rate/cluster time limit and the time it needs to write the checkpoints. See the deal.II checkpoint/restart tutorial for more details.

After running the simulation, you can find these checkpoints in the results folder:

ls results/parallel-shock

yields

checkpoint_vfp
checkpoint_vfp_fixed.data
checkpoint_vfp.info
checkpoint_vfp.metadata
log.prm
solution_0000.pvtu
...

These checkpoint files save basic metadata, the grid and solution DoFs. Only the most recent checkpoint is kept to limit storage usage.

Restarting a simulation is done with the --resume (or -r) command-line flag. Since the checkpoint does not save the parameters used for the simulation, you also have to provide a parameter file to Sapphire+⁠+. This allows you to change some parameters between restarts. But to keep parameters consistent, the log.prm file can be used:

mpirun -n 6 ./build/examples/vfp/parallel-shock/parallel-shock results/parallel-shock/log.prm --resume

Note, that the number of MPI processes can differ for the restart. In case of a successful restart, you should see the following message:

Sapphire::Start Sapphire++ v1.3.0 with 6 MPI process(es)
Sapphire:VFP::Restarting VFP equation solver.
Sapphire:VFP::Time step 50 at t = 5000.00
...
Note
The restart currently only works for 2D and 3D simulations. We assume most 1D simulations are fast and do not need checkpoints. Furthermore, checkpoints are only available for time-dependent simulations. In steady-state simulations there is no obvious point to save checkpoints.

Results

Before we analyse the results, we check that the solution has converged to a steady state. To demonstrate this, we show the time series of the 2D data in a logarithmic scale.

2D time series Plot of shock region

From the first figure we can conclude that the solution reached steady state even for large momenta. The second figure shows a cut-out of the solution in the shock region. This serves an illustrative purpose demonstrating the stretched Shock grid used in the simulation.

The analytic solution predicts that the energy spectrum of the particles at shock follows a \(p^{-3r/(r - 1)}\) power law for \(p > p_{\rm inj}\). Given a compression ratio \(r=4\), we anticipate a \(p^{-4}\) power law. We plot \(f_{000}(x = 0, \ln p)\) and compare it with our expectation.

Steady state f(ln(p)) plot

In the upstream region ( \(x<0\)), we expect an exponential cut-off, which is dependent on the scattering frequency \(\nu\). As discussed in the introduction, we also observe an upstream anisotropy, \(f_{100}\). Note that the anisotropic part disappears in the downstream region. When comparing our simulation results to the analytic solution, we find they are in good agreement. The figure below shows \(f_{000}(x, p = 10)\) and \(f_{100}(x, p = 10)\):

Steady state f(x) plot

In the last figure we present the temporal evolution of the energy spectrum at the shock for a specific \(p\), namely \(p=10\). The simulation results are compared to the approximate analytical solution mentioned in the introduction.

f(t) plot

We note that there exists an exact analytical solution for a specific choice of the scattering frequency in the up- and downstream, see, for example, eq. 16 and 21 in [7] . In [4] it is demonstrated that Sapphire+⁠+ reproduces it accurately.

Previous
Examples

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)
Nils Schween (nils..nosp@m.schw.nosp@m.een@m.nosp@m.pi-h.nosp@m.d.mpg.nosp@m..de)
Date
2025-03-11