An Imperial College London Undergraduate Research Opportunities Project by Sean Lim, supervised by Dr Aidan Crilly
In the simulation.py module, the main function 'simulation' is called with arguments (steps per snapshot, total steps, ICs, ext_fields,dx,dt,BCs). For neatness, the arguments in simulation should be wrapped when called, and then unwrapped within the simulation function.
For N particles and M cells (defined by an
- ICs is an initial conditions sequence containing 3 sequences, (box_size, particle_parameters, fields).
- box_size contains (
$L_x,L_y,L_z$ ), each an integer representing the$x$ ,$y$ and$z$ dimensions. - particle_parameters contains (particle_positions, velocities, qs, ms, q/ms, Number of each pseudospecies, Weights).
- particle_positions and velocities should both be
$N\times3$ arrays. - qs,ms and q/ms should all be Nx1 arrays. Note it has to be
$N\times1$ and not$N$ to be compatible with JAX's vmap function. Also note the use of$\frac{q}{m}$ to reduce floating point errors as JAX is single-precision. - Number of each pseudospecies should be a sequence of the number of each pseudospecies. It is used to split up the particles when outputting . E.g. if I had 5000 electrons, 1000 protons and 4000 Deuterium ions in that order when initialising the particles, it would be (5000,1000,4000).
- Weights should be a float.
- particle_positions and velocities should both be
- fields contains (array of E-fields,array of B-fields) where both are
$M\times3$ arrays specifying initial E- and B- fields. Note the staggered grid when dealing with E-fields, which are defined on the edges of cells. In EM_solver.py there is a function, find_E0_by_matrix to help check if the initial conditions are correct (this may provide the wrong answer by a constant, hence it is recommended to manually calculate the E-field values).
- box_size contains (
- ext_fields contains (array of E-fields,array of B-fields) where both are
$M\times3$ arrays specifying external E- and B- fields. - BCs is a 4-integer tuple representing (left particle BC, right particle BC, left field BC, right field BC). Particle BCs are 0 for periodic, 1 for reflective and 2 for destructive. Field BCs are 0 for periodic, 1 for reflective, 2 for trasnsmissive and 3 for laser. Detailed information on how these BCs work can be found below. If 3 for field BCs is selected, the laser magnitude and wavenumber must be specified with the arguments laser_mag and laser_k (both default 0).
In the Examples folder example_script.py gives a skeleton for the initialisation.
The simulation supports 2 forms of output, as a returned dictionary variable or by saving CSV files. This is defined with the write_to_file argument (default false).
For smaller simulations, the code saves all particle positions and velocities as a
For larger simulations, the
JAX is a Python module utilising the XLA (accelerated Linear Algebra) compiler to create efficient machine learning code. The github repository can be found here. So why are we using it to write PIC code?
-
JAX's compiler allows Python code to be passed onto GPUs to run. Given the parallel nature of PIC codes (advancing many particles at once with the same equations of motion), on top of JAX's vmap function to perform calculations vectorially, the code is well-suited to run on GPUs, utilising parallel computing to run quickly.
-
By writing our code in accordance with JAX's restrictions, we can use JAX's jit function to compile code efficiently and get large speed increases. As a quick test for how much of a speed increase we can get, I ran the current calculation code on 500/5000/50000/500000 particles and 100 grid cells on my local PC. After removing the
@jit
decorator from ourfind_j
function, running
import timeit
string = '''
import jax.numpy as jnp
import jax
from particles_to_grid import find_j
dx = 1
dt = dx/(2*3e8)
grid = jnp.arange(0.5,100.5,dx)
grid_start = grid[0]-dx/2
no_particles = 500/5000/50000/500000
xs_n = jax.random.uniform(jax.random.PRNGKey(100),shape=(no_particles,3),minval=0,maxval=100)
vs_n = jax.random.normal(jax.random.PRNGKey(100),shape=(no_particles,3))
xs_nminushalf = xs_n - vs_n*dt/2
xs_nplushalf = xs_n + vs_n*dt/2
qs = 1.6e-19*jnp.ones(shape=(no_particles,1))
'''
timeit.timeit(stmt='find_j(xs_nminushalf,xs_n,xs_nplushalf,vs_n,qs,dx,dt,grid,grid_start,0,0)',setup=string,number=100)
returns 44.4/44.5/46.6/67.5s.
Adding the @jit
decorator back and the .block_until_ready()
command behind find_j
, with the first 500 particle run taking 0.62s (due to compilation time), the output is now about 0.05/0.97/4.59/40.6s.
As another example, for the boris step with 500/5000/50000/500000 particles,
import timeit
string = '''
import jax.numpy as jnp
import jax
from particle_mover import boris_step
dx = 1
dt = dx/(2*3e8)
no_particles = 500/5000/50000/500000
xs_nplushalf = jax.random.uniform(jax.random.PRNGKey(100),shape=(no_particles,3),minval=0,maxval=100)
vs_n = jax.random.normal(jax.random.PRNGKey(100),shape=(no_particles,3))
q_ms = jnp.ones(shape=(no_particles,1))
E_fields_at_x = jnp.ones(shape=(no_particles,3))
B_fields_at_x = jnp.ones(shape=(no_particles,3))
'''
timeit.timeit(stmt='boris_step(dt,xs_nplushalf,vs_n,q_ms,E_fields_at_x,B_fields_at_x)',setup=string,number=100)
gave us outputs of 0.47/0.45/0.70/1.95s.
Jitting the function (and using jax.block_until_ready()
) gave us 0.0044/0.13/0.20/0.81s.
This is only on my local PC: While we are still trying to run it on GPUs, it would provide another speed boost to our simulation.
However, perhaps what this project best provides is a PIC code which is much more accessible, one which does not require knowledge of old and relatively unknown languages like Fortran. Even undergraduates can use or develop the code to their needs just by getting used to JAX's slightly different syntax.
The code could even be used as a learning tool to visualise plasma effects in Plasma Physics courses, albeit only 1D effects in its current iteration. Several of these effects are shown in the Examples folder (see below for more details).
The code uses many staples in PiC codes, such as the Boris Algorithm to push particles, a triangular shape function for the pseudoparticles, a staggered Yee Grid for the EM fields, and more. A detailed explanation is given below.
The core of the simulation consists of four parts:
- The particle pusher
- Copying the particles' properties to the grid
- The EM solver
- Returning the EM fields' values to the particles
The schematic of one cycle of the simulation is shown:
The Equations to be solved are:
$\frac{\partial B}{\partial t} = -\nabla\times E$ $\frac{\partial E}{\partial t} = c^2\nabla\times B-\frac{j}{\varepsilon_0}$
- (in
$x$ )$\nabla\cdot j = -\frac{\partial\rho}{\partial t}$ - (in
$y,z$ )$j=nqv$
$\frac{dv}{dt}=q(E+v\times B)$ $\frac{dx}{dt}=v$
The particle pusher functions are contained in the particle_mover.py module.
The Boris algorithm staggers the position and velocity in time. The equations used are:
To solve the second equation, if
These functions are contained in the particles_to_grid.py module.
Particles are taken as pseudoparticles with a weight
Thus when copying particle charges onto the grid, the charge density in cell
-For
-For
-For
The current density is found using the equation
The current in y and z direction use
The EM solver is contained in the EM_solver.py module. The equations to solve are
This increases the accuracy of the Curl calculations. We do not solve Gauss' Law directly, as Poisson solvers can lead to numerical issues, and Gauss' Law is automatically obeyed if we use the charge conservation equation, provided Gauss' Law was satisfied at the start.
In a 1D PiC code,
The solver takes 2 steps of
The function to return the fields to the particles is found in the particle_mover.py module. Taking into account the particle spanning several cells due to its shape, the total force it experiences adding each part is, where
Boundary conditions are found in the boundary_conditions.py module.
Boundary conditions are specified by moving the particles and changing their velocities as desired after they have left the box, and applying ghost cells for fields.
The code supports 3 particle BC modes, and 4 field BC modes, to be specified on each side.
For particles:
Mode | BC | Particle position | Particle velocity | Force experienced by particle in ghost cells GL1/GL2/GR |
---|---|---|---|---|
0 | Periodic | Move particle back to other side of box. This is done with the modulo function to find the distance left from the cell. | No change. | GL1 = 2nd last cell GL2 = Last cell GR = First cell |
1 | Reflective | Move particle back the excess distance. | Multiply |
GL1 = 2nd cell GL2 = First cell GR = Last cell |
2 | Destructive | Park particles on either side outside the box. JAX needs fixed array lengths, so removing particles causes it to recompile each time and increase the code runtime. Set their position outside of the box, currently at L-Δx for the left and R+2.5Δx for the right, where L/R is the left/right Also set q and q/m to 0 so they do not contribute any charge density/current. |
Set to 0. | GL1 = 0 GL2 = 0 GR = 0 |
Note the need to use 2 ghost cells on the left due to the leftmost edges of particles in the first half cell exceeding the grid when using the staggered grid. Also note
For fields:
Mode | BC | Ghost cells GL/GR |
---|---|---|
0 | Periodic | GL = Last cell GR = First cell |
1 | Reflective | GL = First cell GR = Last cell |
2 | Transmissive | Silver-Mueller BCs [6]. By applying conditions for a left-propagating wave for the left cell ( This gives us a zero-order approximation for transmissive BCs. |
3 | Laser | For laser amplitude A and wavenumber k defined at the start, |
Apart from the core solver, there is an additional diagnostics.py module for returning useful output. In it are functions to find the system's total kinetic energy, E-field density, B-field density, temperature at each cell and velocity histogram. These are returned in the output.
Temperature in each cell is calculated first by finding and subtracting any drift velocity
This module also contains a function to perform Fourier transforms on number density data.
Finally, the simulation.py module puts it all together. It defines one step in the cycle, which is called in an n_cycles function so we can take many steps before performing diagnosis (for long simulations where timescales of phenomenon are much longer than the dt required to maintain stability,
This outermost function n_cycles, as well as any other outermost functions in the simulation function, are decorated with @jit so jax compiles the function and any other function called inside it. block_until_ready statements are placed where necessary to run on GPUs.
In the examples folder there are some example simulations showing typical plasma behaviour, mostly set out by Langdon and Birdsall [7]. They are, with their approximate runtime on my local PC and some notes based on how far I got during the project:
- Plasma oscillations (16s). Frequency agrees with theoretical frequency of
$\omega=\sqrt{\frac{ne^2}{m_e\varepsilon_0}}=\sqrt{\frac{N_p\Omega e^2}{Lm_e\varepsilon_0}}$ . - Plasma waves (130s). A Fourier transform was performed to find the dominant modes in the simulation. While the FT plot takes the shape of the dispersion relation, there are strong modes in the entire area below the line as well. Note that
$\Delta x$ must be on the order of the Debye length to prevent numerical heating, as is the case for any thermal effects. - Hybrid oscillations (43s). Elliptical motion of particles can be seen, and frequency agrees with theoretical frequency of
$\omega_H^2=\omega_C^2+\omega_P^2$ where$\omega_C$ is cyclotron frequency and$\omega_P$ is plasma frequency. Note particles have to be initialised with a velocity based on their position to see the elliptical motion, and this velocity must be$<< c$ to ensure the system is electrostatic. - 2-stream instability (225s). A 2D histogram on the position and velocity was performed to plot the system in phase space. 2 configurations were tested, one with counterstreaming electrons in a sea of protons, and one with counterstreaming positrons and electrons. Changing the grid resolution changes the modes that can be captured by the simulation, leading to different patterns in phase space. The last 2 cells plot the system's energy, and the conversion of kinetic energy to electric field energy can be seen, as well as the point where the instability starts becoming saturated in the log plot (energy plots off by a factor for positron-electron example).
- Weibel instability (100s). With 2 groups of electrons, one moving in
$+z$ direction and one in$-z$ direction, B-fields can clearly be seen growing, and current filaments forming and merging. The last 2 cells show energy plots, and a log plot showing the growth and saturation of the instability. - Precursor (110s). A laser travels into an underdense plasma, and a small attenuation can be seen. However, a portion of the wave appears to be reflected. One can also try an overdense plasma and see that most of the wave is reflected and in the plasma, it is shorted out by the plasma.
References
[1] H. Qin, S. Zhang, J. Xiao, J. Liu, Y. Sun, W. M. Tang (2013, August). "Why is Boris algorithm so good." Physics of Plasmas [Online]. vol. 20, issue 8. Available: https://doi.org/10.1063/1.4818428.
[2] A. Hakim (2021). "Computational Methods in Plasma Physics Lecture II." PPPL Graduate Summer School 2021 [PowerPoint slides]. slide 19. Available: https://cmpp.readthedocs.io/en/latest/_static/lec2-2021.pdf.
[3] C. Brady, K. Bennett, H. Schmitz, C. Ridgers (2021, June). Section 4.3.1 "Particle Shape Functions" in "Developers Manual for the EPOCH PIC Codes." Version 4.17.0. Latest version available: https://github.com/Warwick-Plasma/EPOCH_manuals/releases.
[4] J. Villasenor, O. Buneman (1992, March). "Rigorous charge conservation for local electromagnetic field solvers." Computer Physics Communications [Online]. vol. 69, issues 2–3, pages 306-316. Available: https://doi.org/10.1016/0010-4655(92)90169-Y.
[5] C. Brady, K. Bennett, H. Schmitz, C. Ridgers (2021, June). Section 4.3.2 "Current Calculation" in "Developers Manual for the EPOCH PIC Codes." Version 4.17.0. Latest version available: https://github.com/Warwick-Plasma/EPOCH_manuals/releases.
[6] R. Lehe (2016, June). "Electromagnetic wave propagation in Particle-In-Cell codes." US Particle Accelerator School (USPAS) Summer Session 2016 [PowerPoint slides]. slides 18-24. Available: https://people.nscl.msu.edu/~lund/uspas/scs_2016/lec_adv/A1b_EM_Waves.pdf.
[7] C.K. Birdsall, A.B. Langdon. Plasma Physics via Computer Simulation (1st ed.). Bristol: IOP Publishing Ltd, 1991.