Langmuir Waves

These are examples of Plasma oscillations (Langmuir waves) in a uniform plasma in 1D, 2D, 3D, and RZ.

In each case, a uniform plasma is setup with a sinusoidal perturbation in the electron momentum along each axis. The plasma is followed for a short period of time, long enough so that E fields develop. The resulting fields can be compared to the analytic solutions.

Run

For MPI-parallel runs, prefix these lines with mpiexec -n 4 ... or srun -n 4 ..., depending on the system.

This example can be run as a Python script: python3 PICMI_inputs_3d.py.

Listing 43 You can copy this file from Examples/Tests/langmuir/PICMI_inputs_3d.py.
#!/usr/bin/env python3
#
# --- Simple example of Langmuir oscillations in a uniform plasma

from pywarpx import picmi

constants = picmi.constants

##########################
# physics parameters
##########################

plasma_density = 1.e25
plasma_xmin = 0.
plasma_x_velocity = 0.1*constants.c

##########################
# numerics parameters
##########################

# --- Number of time steps
max_steps = 40
diagnostic_interval = 10

# --- Grid
nx = 64
ny = 64
nz = 64

xmin = -20.e-6
ymin = -20.e-6
zmin = -20.e-6
xmax = +20.e-6
ymax = +20.e-6
zmax = +20.e-6

number_per_cell_each_dim = [2,2,2]

##########################
# physics components
##########################

uniform_plasma = picmi.UniformDistribution(density = 1.e25,
                                           upper_bound = [0., None, None],
                                           directed_velocity = [0.1*constants.c, 0., 0.])

electrons = picmi.Species(particle_type='electron', name='electrons', initial_distribution=uniform_plasma)

##########################
# numerics components
##########################

grid = picmi.Cartesian3DGrid(number_of_cells = [nx, ny, nz],
                             lower_bound = [xmin, ymin, zmin],
                             upper_bound = [xmax, ymax, zmax],
                             lower_boundary_conditions = ['periodic', 'periodic', 'periodic'],
                             upper_boundary_conditions = ['periodic', 'periodic', 'periodic'],
                             moving_window_velocity = [0., 0., 0.],
                             warpx_max_grid_size = 32)

solver = picmi.ElectromagneticSolver(grid=grid, cfl=1.)

##########################
# diagnostics
##########################

field_diag1 = picmi.FieldDiagnostic(name = 'diag1',
                                    grid = grid,
                                    period = diagnostic_interval,
                                    data_list = ['Ex', 'Jx'],
                                    write_dir = '.',
                                    warpx_file_prefix = 'Python_Langmuir_plt')

part_diag1 = picmi.ParticleDiagnostic(name = 'diag1',
                                      period = diagnostic_interval,
                                      species = [electrons],
                                      data_list = ['weighting', 'ux'])

##########################
# simulation setup
##########################

sim = picmi.Simulation(solver = solver,
                       max_steps = max_steps,
                       verbose = 1,
                       warpx_current_deposition_algo = 'direct')

sim.add_species(electrons,
                layout = picmi.GriddedLayout(n_macroparticle_per_cell=number_per_cell_each_dim, grid=grid))

sim.add_diagnostic(field_diag1)
sim.add_diagnostic(part_diag1)

##########################
# simulation run
##########################

# write_inputs will create an inputs file that can be used to run
# with the compiled version.
#sim.write_input_file(file_name = 'inputs_from_PICMI')

# Alternatively, sim.step will run WarpX, controlling it from Python
sim.step()

This example can be run as WarpX executable using an input file: warpx.3d inputs_3d

Listing 44 You can copy this file from Examples/Tests/langmuir/inputs_3d.
# Parameters for the plasma wave
my_constants.max_step = 40
my_constants.lx = 40.e-6 # length of sides
my_constants.dx = 6.25e-07 # grid cell size
my_constants.nx = lx/dx # number of cells in each dimension
my_constants.epsilon = 0.01
my_constants.n0 = 2.e24  # electron and positron densities, #/m^3
my_constants.wp = sqrt(2.*n0*q_e**2/(epsilon0*m_e))  # plasma frequency
my_constants.kp = wp/clight  # plasma wavenumber
my_constants.k = 2.*2.*pi/lx  # perturbation wavenumber
# Note: kp is calculated in SI for a density of 4e24 (i.e. 2e24 electrons + 2e24 positrons)
# k is calculated so as to have 2 periods within the 40e-6 wide box.

# Maximum number of time steps
max_step = max_step

# number of grid points
amr.n_cell =  nx nx nx

# Maximum allowable size of each subdomain in the problem domain;
#    this is used to decompose the domain for parallel calculations.
amr.max_grid_size = nx nx nx

# Maximum level in hierarchy (for now must be 0, i.e., one level in total)
amr.max_level = 0

# Geometry
geometry.dims = 3
geometry.prob_lo     = -lx/2.   -lx/2.   -lx/2.    # physical domain
geometry.prob_hi     =  lx/2.    lx/2.    lx/2.

# Boundary condition
boundary.field_lo = periodic periodic periodic
boundary.field_hi = periodic periodic periodic

warpx.serialize_initial_conditions = 1

# Verbosity
warpx.verbose = 1

# Algorithms
algo.current_deposition = esirkepov
algo.field_gathering = energy-conserving
warpx.use_filter = 0

# Order of particle shape factors
algo.particle_shape = 1

# CFL
warpx.cfl = 1.0

# Particles
particles.species_names = electrons positrons

electrons.charge = -q_e
electrons.mass = m_e
electrons.injection_style = "NUniformPerCell"
electrons.num_particles_per_cell_each_dim = 1 1 1
electrons.xmin = -20.e-6
electrons.xmax =  20.e-6
electrons.ymin = -20.e-6
electrons.ymax = 20.e-6
electrons.zmin = -20.e-6
electrons.zmax = 20.e-6

electrons.profile = constant
electrons.density = n0   # number of electrons per m^3
electrons.momentum_distribution_type = parse_momentum_function
electrons.momentum_function_ux(x,y,z) = "epsilon * k/kp * sin(k*x) * cos(k*y) * cos(k*z)"
electrons.momentum_function_uy(x,y,z) = "epsilon * k/kp * cos(k*x) * sin(k*y) * cos(k*z)"
electrons.momentum_function_uz(x,y,z) = "epsilon * k/kp * cos(k*x) * cos(k*y) * sin(k*z)"

positrons.charge = q_e
positrons.mass = m_e
positrons.injection_style = "NUniformPerCell"
positrons.num_particles_per_cell_each_dim = 1 1 1
positrons.xmin = -20.e-6
positrons.xmax =  20.e-6
positrons.ymin = -20.e-6
positrons.ymax = 20.e-6
positrons.zmin = -20.e-6
positrons.zmax = 20.e-6

positrons.profile = constant
positrons.density = n0   # number of positrons per m^3
positrons.momentum_distribution_type = parse_momentum_function
positrons.momentum_function_ux(x,y,z) = "-epsilon * k/kp * sin(k*x) * cos(k*y) * cos(k*z)"
positrons.momentum_function_uy(x,y,z) = "-epsilon * k/kp * cos(k*x) * sin(k*y) * cos(k*z)"
positrons.momentum_function_uz(x,y,z) = "-epsilon * k/kp * cos(k*x) * cos(k*y) * sin(k*z)"

# Diagnostics
diagnostics.diags_names = diag1
diag1.intervals = max_step
diag1.diag_type = Full
diag1.fields_to_plot = Ex Ey Ez Bx By Bz jx jy jz part_per_cell rho
diag1.electrons.variables = w ux
diag1.positrons.variables = uz

This example can be run as a Python script: python3 PICMI_inputs_2d.py.

Listing 45 You can copy this file from Examples/Tests/langmuir/PICMI_inputs_2d.py.
#!/usr/bin/env python3
#
# --- Simple example of Langmuir oscillations in a uniform plasma
# --- in two dimensions

from pywarpx import picmi

constants = picmi.constants

##########################
# physics parameters
##########################

plasma_density = 1.e25
plasma_xmin = 0.
plasma_x_velocity = 0.1*constants.c

##########################
# numerics parameters
##########################

# --- Number of time steps
max_steps = 40
diagnostic_intervals = "::10"

# --- Grid
nx = 64
ny = 64

xmin = -20.e-6
ymin = -20.e-6
xmax = +20.e-6
ymax = +20.e-6

number_per_cell_each_dim = [2,2]

##########################
# physics components
##########################

uniform_plasma = picmi.UniformDistribution(density = 1.e25,
                                           upper_bound = [0., None, None],
                                           directed_velocity = [0.1*constants.c, 0., 0.])

electrons = picmi.Species(particle_type='electron', name='electrons', initial_distribution=uniform_plasma)

##########################
# numerics components
##########################

grid = picmi.Cartesian2DGrid(number_of_cells = [nx, ny],
                             lower_bound = [xmin, ymin],
                             upper_bound = [xmax, ymax],
                             lower_boundary_conditions = ['periodic', 'periodic'],
                             upper_boundary_conditions = ['periodic', 'periodic'],
                             moving_window_velocity = [0., 0., 0.],
                             warpx_max_grid_size = 32)

solver = picmi.ElectromagneticSolver(grid=grid, cfl=1.)

##########################
# diagnostics
##########################

field_diag1 = picmi.FieldDiagnostic(name = 'diag1',
                                    grid = grid,
                                    period = diagnostic_intervals,
                                    data_list = ['Ex', 'Jx'],
                                    write_dir = '.',
                                    warpx_file_prefix = 'Python_Langmuir_2d_plt')

part_diag1 = picmi.ParticleDiagnostic(name = 'diag1',
                                      period = diagnostic_intervals,
                                      species = [electrons],
                                      data_list = ['weighting', 'ux'])

##########################
# simulation setup
##########################

sim = picmi.Simulation(solver = solver,
                       max_steps = max_steps,
                       verbose = 1,
                       warpx_current_deposition_algo = 'direct',
                       warpx_use_filter = 0)

sim.add_species(electrons,
                layout = picmi.GriddedLayout(n_macroparticle_per_cell=number_per_cell_each_dim, grid=grid))

sim.add_diagnostic(field_diag1)
sim.add_diagnostic(part_diag1)

##########################
# simulation run
##########################

# write_inputs will create an inputs file that can be used to run
# with the compiled version.
sim.write_input_file(file_name = 'inputs2d_from_PICMI')

# Alternatively, sim.step will run WarpX, controlling it from Python
sim.step()

This example can be run as WarpX executable using an input file: warpx.2d inputs_2d

Listing 46 You can copy this file from Examples/Tests/langmuir/inputs_2d.
# Maximum number of time steps
max_step = 80

# number of grid points
amr.n_cell =   128  128

# Maximum allowable size of each subdomain in the problem domain;
#    this is used to decompose the domain for parallel calculations.
amr.max_grid_size = 64

# Maximum level in hierarchy (for now must be 0, i.e., one level in total)
amr.max_level = 0

# Geometry
geometry.dims = 2
geometry.prob_lo     = -20.e-6   -20.e-6    # physical domain
geometry.prob_hi     =  20.e-6    20.e-6

# Boundary condition
boundary.field_lo = periodic periodic
boundary.field_hi = periodic periodic

warpx.serialize_initial_conditions = 1

# Verbosity
warpx.verbose = 1

# Algorithms
algo.field_gathering = energy-conserving
warpx.use_filter = 0

# Order of particle shape factors
algo.particle_shape = 1

# CFL
warpx.cfl = 1.0

# Parameters for the plasma wave
my_constants.epsilon = 0.01
my_constants.n0 = 2.e24  # electron and positron densities, #/m^3
my_constants.wp = sqrt(2.*n0*q_e**2/(epsilon0*m_e))  # plasma frequency
my_constants.kp = wp/clight  # plasma wavenumber
my_constants.k = 2.*pi/20.e-6  # perturbation wavenumber
# Note: kp is calculated in SI for a density of 4e24 (i.e. 2e24 electrons + 2e24 positrons)
# k is calculated so as to have 2 periods within the 40e-6 wide box.

# Particles
particles.species_names = electrons positrons

electrons.charge = -q_e
electrons.mass = m_e
electrons.injection_style = "NUniformPerCell"
electrons.num_particles_per_cell_each_dim = 2 2
electrons.xmin = -20.e-6
electrons.xmax =  20.e-6
electrons.ymin = -20.e-6
electrons.ymax = 20.e-6
electrons.zmin = -20.e-6
electrons.zmax = 20.e-6

electrons.profile = constant
electrons.density = n0   # number of electrons per m^3
electrons.momentum_distribution_type = parse_momentum_function
electrons.momentum_function_ux(x,y,z) = "epsilon * k/kp * sin(k*x) * cos(k*y) * cos(k*z)"
electrons.momentum_function_uy(x,y,z) = "epsilon * k/kp * cos(k*x) * sin(k*y) * cos(k*z)"
electrons.momentum_function_uz(x,y,z) = "epsilon * k/kp * cos(k*x) * cos(k*y) * sin(k*z)"

positrons.charge = q_e
positrons.mass = m_e
positrons.injection_style = "NUniformPerCell"
positrons.num_particles_per_cell_each_dim = 2 2
positrons.xmin = -20.e-6
positrons.xmax =  20.e-6
positrons.ymin = -20.e-6
positrons.ymax = 20.e-6
positrons.zmin = -20.e-6
positrons.zmax = 20.e-6

positrons.profile = constant
positrons.density = n0   # number of positrons per m^3
positrons.momentum_distribution_type = parse_momentum_function
positrons.momentum_function_ux(x,y,z) = "-epsilon * k/kp * sin(k*x) * cos(k*y) * cos(k*z)"
positrons.momentum_function_uy(x,y,z) = "-epsilon * k/kp * cos(k*x) * sin(k*y) * cos(k*z)"
positrons.momentum_function_uz(x,y,z) = "-epsilon * k/kp * cos(k*x) * cos(k*y) * sin(k*z)"

# Diagnostics
diagnostics.diags_names = diag1
diag1.intervals = 40
diag1.diag_type = Full

This example can be run as a Python script: python3 PICMI_inputs_rz.py.

Listing 47 You can copy this file from Examples/Tests/langmuir/PICMI_inputs_rz.py.
#!/usr/bin/env python3
#
# This is a script that analyses the multimode simulation results.
# This simulates a RZ multimode periodic plasma wave.
# The electric field from the simulation is compared to the analytic value

import matplotlib

matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from pywarpx import fields, picmi

constants = picmi.constants

##########################
# physics parameters
##########################

density = 2.e24
epsilon0 = 0.001*constants.c
epsilon1 = 0.001*constants.c
epsilon2 = 0.001*constants.c
w0 = 5.e-6
n_osc_z = 3

# Plasma frequency
wp = np.sqrt((density*constants.q_e**2)/(constants.m_e*constants.ep0))
kp = wp/constants.c

##########################
# numerics parameters
##########################

nr = 64
nz = 200

rmin =  0.e0
zmin =  0.e0
rmax = +20.e-6
zmax = +40.e-6

# Wave vector of the wave
k0 = 2.*np.pi*n_osc_z/(zmax - zmin)

diagnostic_intervals = 40

##########################
# physics components
##########################

uniform_plasma = picmi.UniformDistribution(density = density,
                                           upper_bound = [+18e-6, +18e-6, None],
                                           directed_velocity = [0., 0., 0.])

momentum_expressions = ["""+ epsilon0/kp*2*x/w0**2*exp(-(x**2+y**2)/w0**2)*sin(k0*z)
                           - epsilon1/kp*2/w0*exp(-(x**2+y**2)/w0**2)*sin(k0*z)
                           + epsilon1/kp*4*x**2/w0**3*exp(-(x**2+y**2)/w0**2)*sin(k0*z)
                           - epsilon2/kp*8*x/w0**2*exp(-(x**2+y**2)/w0**2)*sin(k0*z)
                           + epsilon2/kp*8*x*(x**2-y**2)/w0**4*exp(-(x**2+y**2)/w0**2)*sin(k0*z)""",
                        """+ epsilon0/kp*2*y/w0**2*exp(-(x**2+y**2)/w0**2)*sin(k0*z)
                           + epsilon1/kp*4*x*y/w0**3*exp(-(x**2+y**2)/w0**2)*sin(k0*z)
                           + epsilon2/kp*8*y/w0**2*exp(-(x**2+y**2)/w0**2)*sin(k0*z)
                           + epsilon2/kp*8*y*(x**2-y**2)/w0**4*exp(-(x**2+y**2)/w0**2)*sin(k0*z)""",
                        """- epsilon0/kp*k0*exp(-(x**2+y**2)/w0**2)*cos(k0*z)
                           - epsilon1/kp*k0*2*x/w0*exp(-(x**2+y**2)/w0**2)*cos(k0*z)
                           - epsilon2/kp*k0*4*(x**2-y**2)/w0**2*exp(-(x**2+y**2)/w0**2)*cos(k0*z)"""]

analytic_plasma = picmi.AnalyticDistribution(density_expression = density,
                                             upper_bound = [+18e-6, +18e-6, None],
                                             epsilon0 = epsilon0,
                                             epsilon1 = epsilon1,
                                             epsilon2 = epsilon2,
                                             kp = kp,
                                             k0 = k0,
                                             w0 = w0,
                                             momentum_expressions = momentum_expressions)

electrons = picmi.Species(particle_type='electron', name='electrons', initial_distribution=analytic_plasma)
protons = picmi.Species(particle_type='proton', name='protons', initial_distribution=uniform_plasma)

##########################
# numerics components
##########################

grid = picmi.CylindricalGrid(number_of_cells = [nr, nz],
                             n_azimuthal_modes = 3,
                             lower_bound = [rmin, zmin],
                             upper_bound = [rmax, zmax],
                             lower_boundary_conditions = ['none', 'periodic'],
                             upper_boundary_conditions = ['none', 'periodic'],
                             lower_boundary_conditions_particles = ['absorbing', 'periodic'],
                             upper_boundary_conditions_particles = ['absorbing', 'periodic'],
                             moving_window_velocity = [0.,0.],
                             warpx_max_grid_size=64)

solver = picmi.ElectromagneticSolver(grid=grid, cfl=1.)

##########################
# diagnostics
##########################

field_diag1 = picmi.FieldDiagnostic(name = 'diag1',
                                    grid = grid,
                                    period = diagnostic_intervals,
                                    data_list = ['Er', 'Ez', 'Bt', 'Jr', 'Jz', 'part_per_cell'],
                                    write_dir = '.',
                                    warpx_file_prefix = 'Python_Langmuir_rz_multimode_plt')

part_diag1 = picmi.ParticleDiagnostic(name = 'diag1',
                                      period = diagnostic_intervals,
                                      species = [electrons],
                                      data_list = ['weighting', 'momentum'])

##########################
# simulation setup
##########################

sim = picmi.Simulation(solver = solver,
                       max_steps = 40,
                       verbose = 1,
                       warpx_current_deposition_algo = 'esirkepov',
                       warpx_field_gathering_algo = 'energy-conserving',
                       warpx_particle_pusher_algo = 'boris',
                       warpx_use_filter = 0)

sim.add_species(electrons, layout=picmi.GriddedLayout(n_macroparticle_per_cell=[2,16,2], grid=grid))
sim.add_species(protons, layout=picmi.GriddedLayout(n_macroparticle_per_cell=[2,16,2], grid=grid))

sim.add_diagnostic(field_diag1)
sim.add_diagnostic(part_diag1)

##########################
# simulation run
##########################

# write_inputs will create an inputs file that can be used to run
# with the compiled version.
#sim.write_input_file(file_name='inputsrz_from_PICMI')

# Alternatively, sim.step will run WarpX, controlling it from Python
sim.step()


# Below is WarpX specific code to check the results.

def calcEr( z, r, k0, w0, wp, t, epsilons) :
    """
    Return the radial electric field as an array
    of the same length as z and r, in the half-plane theta=0
    """
    Er_array = (
        epsilons[0] * constants.m_e*constants.c/constants.q_e * 2*r/w0**2 *
            np.exp( -r**2/w0**2 ) * np.sin( k0*z ) * np.sin( wp*t )
        - epsilons[1] * constants.m_e*constants.c/constants.q_e * 2/w0 *
            np.exp( -r**2/w0**2 ) * np.sin( k0*z ) * np.sin( wp*t )
        + epsilons[1] * constants.m_e*constants.c/constants.q_e * 4*r**2/w0**3 *
            np.exp( -r**2/w0**2 ) * np.sin( k0*z ) * np.sin( wp*t )
        - epsilons[2] * constants.m_e*constants.c/constants.q_e * 8*r/w0**2 *
            np.exp( -r**2/w0**2 ) * np.sin( k0*z ) * np.sin( wp*t )
        + epsilons[2] * constants.m_e*constants.c/constants.q_e * 8*r**3/w0**4 *
            np.exp( -r**2/w0**2 ) * np.sin( k0*z ) * np.sin( wp*t ))
    return( Er_array )

def calcEz( z, r, k0, w0, wp, t, epsilons) :
    """
    Return the longitudinal electric field as an array
    of the same length as z and r, in the half-plane theta=0
    """
    Ez_array = (
        - epsilons[0] * constants.m_e*constants.c/constants.q_e * k0 *
            np.exp( -r**2/w0**2 ) * np.cos( k0*z ) * np.sin( wp*t )
        - epsilons[1] * constants.m_e*constants.c/constants.q_e * k0 * 2*r/w0 *
            np.exp( -r**2/w0**2 ) * np.cos( k0*z ) * np.sin( wp*t )
        - epsilons[2] * constants.m_e*constants.c/constants.q_e * k0 * 4*r**2/w0**2 *
            np.exp( -r**2/w0**2 ) * np.cos( k0*z ) * np.sin( wp*t ))
    return( Ez_array )

# Current time of the simulation
t0 = sim.extension.warpx.gett_new(0)

# Get the raw field data. Note that these are the real and imaginary
# parts of the fields for each azimuthal mode.
Ex_sim_wrap = fields.ExWrapper()
Ez_sim_wrap = fields.EzWrapper()
Ex_sim_modes = Ex_sim_wrap[...]
Ez_sim_modes = Ez_sim_wrap[...]

rr_Er = Ex_sim_wrap.mesh('r')
zz_Er = Ex_sim_wrap.mesh('z')
rr_Ez = Ez_sim_wrap.mesh('r')
zz_Ez = Ez_sim_wrap.mesh('z')

rr_Er = rr_Er[:,np.newaxis]*np.ones(zz_Er.shape[0])[np.newaxis,:]
zz_Er = zz_Er[np.newaxis,:]*np.ones(rr_Er.shape[0])[:,np.newaxis]
rr_Ez = rr_Ez[:,np.newaxis]*np.ones(zz_Ez.shape[0])[np.newaxis,:]
zz_Ez = zz_Ez[np.newaxis,:]*np.ones(rr_Ez.shape[0])[:,np.newaxis]

# Sum the real components to get the field along x-axis (theta = 0)
Er_sim = Ex_sim_modes[:,:,0] + np.sum(Ex_sim_modes[:,:,1::2], axis=2)
Ez_sim = Ez_sim_modes[:,:,0] + np.sum(Ez_sim_modes[:,:,1::2], axis=2)

# The analytical solutions
Er_th = calcEr(zz_Er, rr_Er, k0, w0, wp, t0, [epsilon0, epsilon1, epsilon2])
Ez_th = calcEz(zz_Ez, rr_Ez, k0, w0, wp, t0, [epsilon0, epsilon1, epsilon2])

max_error_Er = abs(Er_sim - Er_th).max()/abs(Er_th).max()
max_error_Ez = abs(Ez_sim - Ez_th).max()/abs(Ez_th).max()
print("Max error Er %e"%max_error_Er)
print("Max error Ez %e"%max_error_Ez)

# Plot the last field from the loop (Er at iteration 40)
fig, ax = plt.subplots(3)
im = ax[0].imshow( Er_sim, aspect='auto', origin='lower' )
fig.colorbar(im, ax=ax[0], orientation='vertical')
ax[0].set_title('Er, last iteration (simulation)')
ax[1].imshow( Er_th, aspect='auto', origin='lower' )
fig.colorbar(im, ax=ax[1], orientation='vertical')
ax[1].set_title('Er, last iteration (theory)')
im = ax[2].imshow( (Er_sim - Er_th)/abs(Er_th).max(), aspect='auto', origin='lower' )
fig.colorbar(im, ax=ax[2], orientation='vertical')
ax[2].set_title('Er, last iteration (difference)')
plt.savefig('langmuir_multi_rz_multimode_analysis_Er.png')

fig, ax = plt.subplots(3)
im = ax[0].imshow( Ez_sim, aspect='auto', origin='lower' )
fig.colorbar(im, ax=ax[0], orientation='vertical')
ax[0].set_title('Ez, last iteration (simulation)')
ax[1].imshow( Ez_th, aspect='auto', origin='lower' )
fig.colorbar(im, ax=ax[1], orientation='vertical')
ax[1].set_title('Ez, last iteration (theory)')
im = ax[2].imshow( (Ez_sim - Ez_th)/abs(Ez_th).max(), aspect='auto', origin='lower' )
fig.colorbar(im, ax=ax[2], orientation='vertical')
ax[2].set_title('Ez, last iteration (difference)')
plt.savefig('langmuir_multi_rz_multimode_analysis_Ez.png')

assert max(max_error_Er, max_error_Ez) < 0.02

This example can be run as WarpX executable using an input file: warpx.rz inputs_rz

Listing 48 You can copy this file from Examples/Tests/langmuir/inputs_rz.
# Parameters for the plasma wave
my_constants.max_step = 80
my_constants.epsilon = 0.01
my_constants.n0 = 2.e24  # electron density, #/m^3
my_constants.wp = sqrt(n0*q_e**2/(epsilon0*m_e))  # plasma frequency
my_constants.kp = wp/clight  # plasma wavenumber
my_constants.k0 = 2.*pi/20.e-6  # longitudianl perturbation wavenumber
my_constants.w0 = 5.e-6  # transverse perturbation length
# Note: kp is calculated in SI for a density of 2e24
# k0 is calculated so as to have 2 periods within the 40e-6 wide box.

# Maximum number of time steps
max_step = max_step

# number of grid points
amr.n_cell =   64  128

# Maximum allowable size of each subdomain in the problem domain;
#    this is used to decompose the domain for parallel calculations.
amr.max_grid_size = 64

# Maximum level in hierarchy (for now must be 0, i.e., one level in total)
amr.max_level = 0

# Geometry
geometry.dims = RZ
geometry.prob_lo     =   0.e-6   -20.e-6    # physical domain
geometry.prob_hi     =  20.e-6    20.e-6
boundary.field_lo = none periodic
boundary.field_hi = none periodic

warpx.serialize_initial_conditions = 1

# Verbosity
warpx.verbose = 1

# Algorithms
algo.field_gathering = energy-conserving
algo.current_deposition = esirkepov
warpx.use_filter = 0

# Order of particle shape factors
algo.particle_shape = 1

# CFL
warpx.cfl = 1.0

# Having this turned on makes for a more sensitive test
warpx.do_dive_cleaning = 1

# Particles
particles.species_names = electrons ions

electrons.charge = -q_e
electrons.mass = m_e
electrons.injection_style = "NUniformPerCell"
electrons.num_particles_per_cell_each_dim = 2 2 2
electrons.xmin =   0.e-6
electrons.xmax =  18.e-6
electrons.zmin = -20.e-6
electrons.zmax = +20.e-6

electrons.profile = constant
electrons.density = n0   # number of electrons per m^3
electrons.momentum_distribution_type = parse_momentum_function
electrons.momentum_function_ux(x,y,z) = "epsilon/kp*2*x/w0**2*exp(-(x**2+y**2)/w0**2)*sin(k0*z)"
electrons.momentum_function_uy(x,y,z) = "epsilon/kp*2*y/w0**2*exp(-(x**2+y**2)/w0**2)*sin(k0*z)"
electrons.momentum_function_uz(x,y,z) = "-epsilon/kp*k0*exp(-(x**2+y**2)/w0**2)*cos(k0*z)"


ions.charge = q_e
ions.mass = m_p
ions.injection_style = "NUniformPerCell"
ions.num_particles_per_cell_each_dim = 2 2 2
ions.xmin =   0.e-6
ions.xmax =  18.e-6
ions.zmin = -20.e-6
ions.zmax = +20.e-6

ions.profile = constant
ions.density = n0   # number of ions per m^3
ions.momentum_distribution_type = at_rest

# Diagnostics
diagnostics.diags_names = diag1 diag_parser_filter diag_uniform_filter diag_random_filter
diag1.intervals = max_step/2
diag1.diag_type = Full
diag1.fields_to_plot = jr jz Er Ez Bt

## diag_parser_filter is a diag used to test the particle filter function.
diag_parser_filter.intervals = max_step:max_step:
diag_parser_filter.diag_type = Full
diag_parser_filter.species = electrons
diag_parser_filter.electrons.plot_filter_function(t,x,y,z,ux,uy,uz) = "(uy-uz < 0) *
                                                                 (sqrt(x**2+y**2)<10e-6) * (z > 0)"

## diag_uniform_filter is a diag used to test the particle uniform filter.
diag_uniform_filter.intervals = max_step:max_step:
diag_uniform_filter.diag_type = Full
diag_uniform_filter.species = electrons
diag_uniform_filter.electrons.uniform_stride = 3

## diag_random_filter is a diag used to test the particle random filter.
diag_random_filter.intervals = max_step:max_step:
diag_random_filter.diag_type = Full
diag_random_filter.species = electrons
diag_random_filter.electrons.random_fraction = 0.66

Note

TODO: This input file should be created, like the inputs_1d file.

This example can be run as WarpX executable using an input file: warpx.1d inputs_1d

Listing 49 You can copy this file from Examples/Tests/langmuir/inputs_1d.
# Maximum number of time steps
max_step = 80

# number of grid points
amr.n_cell =  128

# Maximum allowable size of each subdomain in the problem domain;
#    this is used to decompose the domain for parallel calculations.
amr.max_grid_size = 64

# Maximum level in hierarchy (for now must be 0, i.e., one level in total)
amr.max_level = 0

# Geometry
geometry.dims = 1
geometry.prob_lo     = -20.e-6    # physical domain
geometry.prob_hi     =  20.e-6

# Boundary condition
boundary.field_lo = periodic
boundary.field_hi = periodic

warpx.serialize_initial_conditions = 1

# Verbosity
warpx.verbose = 1

# Algorithms
algo.field_gathering = energy-conserving
warpx.use_filter = 0

# Order of particle shape factors
algo.particle_shape = 1

# CFL
warpx.cfl = 0.8

# Parameters for the plasma wave
my_constants.epsilon = 0.01
my_constants.n0 = 2.e24  # electron and positron densities, #/m^3
my_constants.wp = sqrt(2.*n0*q_e**2/(epsilon0*m_e))  # plasma frequency
my_constants.kp = wp/clight  # plasma wavenumber
my_constants.k = 2.*pi/20.e-6  # perturbation wavenumber
# Note: kp is calculated in SI for a density of 4e24 (i.e. 2e24 electrons + 2e24 positrons)
# k is calculated so as to have 2 periods within the 40e-6 wide box.

# Particles
particles.species_names = electrons positrons

electrons.charge = -q_e
electrons.mass = m_e
electrons.injection_style = "NUniformPerCell"
electrons.num_particles_per_cell_each_dim = 2
electrons.zmin = -20.e-6
electrons.zmax = 20.e-6

electrons.profile = constant
electrons.density = n0   # number of electrons per m^3
electrons.momentum_distribution_type = parse_momentum_function
electrons.momentum_function_ux(x,y,z) = "epsilon * k/kp * sin(k*x) * cos(k*y) * cos(k*z)"
electrons.momentum_function_uy(x,y,z) = "epsilon * k/kp * cos(k*x) * sin(k*y) * cos(k*z)"
electrons.momentum_function_uz(x,y,z) = "epsilon * k/kp * cos(k*x) * cos(k*y) * sin(k*z)"

positrons.charge = q_e
positrons.mass = m_e
positrons.injection_style = "NUniformPerCell"
positrons.num_particles_per_cell_each_dim = 2
positrons.zmin = -20.e-6
positrons.zmax = 20.e-6

positrons.profile = constant
positrons.density = n0   # number of positrons per m^3
positrons.momentum_distribution_type = parse_momentum_function
positrons.momentum_function_ux(x,y,z) = "-epsilon * k/kp * sin(k*x) * cos(k*y) * cos(k*z)"
positrons.momentum_function_uy(x,y,z) = "-epsilon * k/kp * cos(k*x) * sin(k*y) * cos(k*z)"
positrons.momentum_function_uz(x,y,z) = "-epsilon * k/kp * cos(k*x) * cos(k*y) * sin(k*z)"

# Diagnostics
diagnostics.diags_names = diag1 openpmd
diag1.intervals = 40
diag1.diag_type = Full

openpmd.intervals = 40
openpmd.diag_type = Full
openpmd.format = openpmd

Analyze

We run the following script to analyze correctness:

Script analysis_3d.py
Listing 50 You can copy this file from Examples/Tests/langmuir/analysis_3d.py.
#!/usr/bin/env python3

# Copyright 2019-2022 Jean-Luc Vay, Maxence Thevenet, Remi Lehe, Axel Huebl
#
#
# This file is part of WarpX.
#
# License: BSD-3-Clause-LBNL
#
# This is a script that analyses the simulation results from
# the script `inputs.multi.rt`. This simulates a 3D periodic plasma wave.
# The electric field in the simulation is given (in theory) by:
# $$ E_x = \epsilon \,\frac{m_e c^2 k_x}{q_e}\sin(k_x x)\cos(k_y y)\cos(k_z z)\sin( \omega_p t)$$
# $$ E_y = \epsilon \,\frac{m_e c^2 k_y}{q_e}\cos(k_x x)\sin(k_y y)\cos(k_z z)\sin( \omega_p t)$$
# $$ E_z = \epsilon \,\frac{m_e c^2 k_z}{q_e}\cos(k_x x)\cos(k_y y)\sin(k_z z)\sin( \omega_p t)$$
import os
import re
import sys

import matplotlib.pyplot as plt
import yt
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable

yt.funcs.mylog.setLevel(50)

import numpy as np
from scipy.constants import c, e, epsilon_0, m_e

sys.path.insert(1, '../../../../warpx/Regression/Checksum/')
import checksumAPI

# this will be the name of the plot file
fn = sys.argv[1]

# Parse test name and check if current correction (psatd.current_correction=1) is applied
current_correction = True if re.search( 'current_correction', fn ) else False

# Parse test name and check if Vay current deposition (algo.current_deposition=vay) is used
vay_deposition = True if re.search( 'Vay_deposition', fn ) else False

# Parse test name and check if div(E)/div(B) cleaning (warpx.do_div<e,b>_cleaning=1) is used
div_cleaning = True if re.search('div_cleaning', fn) else False

# Parameters (these parameters must match the parameters in `inputs.multi.rt`)
epsilon = 0.01
n = 4.e24
n_osc_x = 2
n_osc_y = 2
n_osc_z = 2
lo = [-20.e-6, -20.e-6, -20.e-6]
hi = [ 20.e-6,  20.e-6,  20.e-6]
Ncell = [64, 64, 64]

# Wave vector of the wave
kx = 2.*np.pi*n_osc_x/(hi[0]-lo[0])
ky = 2.*np.pi*n_osc_y/(hi[1]-lo[1])
kz = 2.*np.pi*n_osc_z/(hi[2]-lo[2])
# Plasma frequency
wp = np.sqrt((n*e**2)/(m_e*epsilon_0))

k = {'Ex':kx, 'Ey':ky, 'Ez':kz}
cos = {'Ex': (0,1,1), 'Ey':(1,0,1), 'Ez':(1,1,0)}

def get_contribution( is_cos, k, idim ):
    du = (hi[idim]-lo[idim])/Ncell[idim]
    u = lo[idim] + du*( 0.5 + np.arange(Ncell[idim]) )
    if is_cos[idim] == 1:
        return( np.cos(k*u) )
    else:
        return( np.sin(k*u) )

def get_theoretical_field( field, t ):
    amplitude = epsilon * (m_e*c**2*k[field])/e * np.sin(wp*t)
    cos_flag = cos[field]
    x_contribution = get_contribution( cos_flag, kx, 0 )
    y_contribution = get_contribution( cos_flag, ky, 1 )
    z_contribution = get_contribution( cos_flag, kz, 2 )

    E = amplitude * x_contribution[:, np.newaxis, np.newaxis] \
                  * y_contribution[np.newaxis, :, np.newaxis] \
                  * z_contribution[np.newaxis, np.newaxis, :]

    return( E )

# Read the file
ds = yt.load(fn)

# Check that the particle selective output worked:
species = 'electrons'
print('ds.field_list', ds.field_list)
for field in ['particle_weight',
              'particle_momentum_x']:
    print('assert that this is in ds.field_list', (species, field))
    assert (species, field) in ds.field_list
for field in ['particle_momentum_y',
              'particle_momentum_z']:
    print('assert that this is NOT in ds.field_list', (species, field))
    assert (species, field) not in ds.field_list
species = 'positrons'
for field in ['particle_momentum_x',
              'particle_momentum_y']:
    print('assert that this is NOT in ds.field_list', (species, field))
    assert (species, field) not in ds.field_list

t0 = ds.current_time.to_value()
data = ds.covering_grid(level = 0, left_edge = ds.domain_left_edge, dims = ds.domain_dimensions)
edge = np.array([(ds.domain_left_edge[2]).item(), (ds.domain_right_edge[2]).item(), \
                 (ds.domain_left_edge[0]).item(), (ds.domain_right_edge[0]).item()])

# Check the validity of the fields
error_rel = 0
for field in ['Ex', 'Ey', 'Ez']:
    E_sim = data[('mesh',field)].to_ndarray()
    E_th = get_theoretical_field(field, t0)
    max_error = abs(E_sim-E_th).max()/abs(E_th).max()
    print('%s: Max error: %.2e' %(field,max_error))
    error_rel = max( error_rel, max_error )

# Plot the last field from the loop (Ez at iteration 40)
fig, (ax1, ax2) = plt.subplots(1, 2, dpi = 100)
# First plot (slice at y=0)
E_plot = E_sim[:,Ncell[1]//2+1,:]
vmin = E_plot.min()
vmax = E_plot.max()
cax1 = make_axes_locatable(ax1).append_axes('right', size = '5%', pad = '5%')
im1 = ax1.imshow(E_plot, origin = 'lower', extent = edge, vmin = vmin, vmax = vmax)
cb1 = fig.colorbar(im1, cax = cax1)
ax1.set_xlabel(r'$z$')
ax1.set_ylabel(r'$x$')
ax1.set_title(r'$E_z$ (sim)')
# Second plot (slice at y=0)
E_plot = E_th[:,Ncell[1]//2+1,:]
vmin = E_plot.min()
vmax = E_plot.max()
cax2 = make_axes_locatable(ax2).append_axes('right', size = '5%', pad = '5%')
im2 = ax2.imshow(E_plot, origin = 'lower', extent = edge, vmin = vmin, vmax = vmax)
cb2 = fig.colorbar(im2, cax = cax2)
ax2.set_xlabel(r'$z$')
ax2.set_ylabel(r'$x$')
ax2.set_title(r'$E_z$ (theory)')
# Save figure
fig.tight_layout()
fig.savefig('Langmuir_multi_analysis.png', dpi = 200)

tolerance_rel = 5e-2

print("error_rel    : " + str(error_rel))
print("tolerance_rel: " + str(tolerance_rel))

assert( error_rel < tolerance_rel )

# Check relative L-infinity spatial norm of rho/epsilon_0 - div(E)
# with current correction (and periodic single box option) or with Vay current deposition
if current_correction:
    tolerance = 1e-9
elif vay_deposition:
    tolerance = 1e-3
if current_correction or vay_deposition:
    rho  = data[('boxlib','rho')].to_ndarray()
    divE = data[('boxlib','divE')].to_ndarray()
    error_rel = np.amax( np.abs( divE - rho/epsilon_0 ) ) / np.amax( np.abs( rho/epsilon_0 ) )
    print("Check charge conservation:")
    print("error_rel = {}".format(error_rel))
    print("tolerance = {}".format(tolerance))
    assert( error_rel < tolerance )

if div_cleaning:
    ds_old = yt.load('Langmuir_multi_psatd_div_cleaning_plt000038')
    ds_mid = yt.load('Langmuir_multi_psatd_div_cleaning_plt000039')
    ds_new = yt.load(fn) # this is the last plotfile

    ad_old = ds_old.covering_grid(level = 0, left_edge = ds_old.domain_left_edge, dims = ds_old.domain_dimensions)
    ad_mid = ds_mid.covering_grid(level = 0, left_edge = ds_mid.domain_left_edge, dims = ds_mid.domain_dimensions)
    ad_new = ds_new.covering_grid(level = 0, left_edge = ds_new.domain_left_edge, dims = ds_new.domain_dimensions)

    rho   = ad_mid['rho'].v.squeeze()
    divE  = ad_mid['divE'].v.squeeze()
    F_old = ad_old['F'].v.squeeze()
    F_new = ad_new['F'].v.squeeze()

    # Check max norm of error on dF/dt = div(E) - rho/epsilon_0
    # (the time interval between the old and new data is 2*dt)
    dt = 1.203645751e-15
    x = F_new - F_old
    y = (divE - rho/epsilon_0) * 2 * dt
    error_rel = np.amax(np.abs(x - y)) / np.amax(np.abs(y))
    tolerance = 1e-2
    print("Check div(E) cleaning:")
    print("error_rel = {}".format(error_rel))
    print("tolerance = {}".format(tolerance))
    assert(error_rel < tolerance)

test_name = os.path.split(os.getcwd())[1]

if re.search( 'single_precision', fn ):
    checksumAPI.evaluate_checksum(test_name, fn, rtol=1.e-3)
else:
    checksumAPI.evaluate_checksum(test_name, fn)
Script analysis_2d.py
Listing 51 You can copy this file from Examples/Tests/langmuir/analysis_2d.py.
#!/usr/bin/env python3

# Copyright 2019 Jean-Luc Vay, Maxence Thevenet, Remi Lehe
#
#
# This file is part of WarpX.
#
# License: BSD-3-Clause-LBNL
#
# This is a script that analyses the simulation results from
# the script `inputs.multi.rt`. This simulates a 3D periodic plasma wave.
# The electric field in the simulation is given (in theory) by:
# $$ E_x = \epsilon \,\frac{m_e c^2 k_x}{q_e}\sin(k_x x)\cos(k_y y)\cos(k_z z)\sin( \omega_p t)$$
# $$ E_y = \epsilon \,\frac{m_e c^2 k_y}{q_e}\cos(k_x x)\sin(k_y y)\cos(k_z z)\sin( \omega_p t)$$
# $$ E_z = \epsilon \,\frac{m_e c^2 k_z}{q_e}\cos(k_x x)\cos(k_y y)\sin(k_z z)\sin( \omega_p t)$$
import os
import re
import sys

import matplotlib.pyplot as plt
import yt
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable

yt.funcs.mylog.setLevel(50)

import numpy as np
from scipy.constants import c, e, epsilon_0, m_e

sys.path.insert(1, '../../../../warpx/Regression/Checksum/')
import checksumAPI

# this will be the name of the plot file
fn = sys.argv[1]

# Parse test name and check if current correction (psatd.current_correction=1) is applied
current_correction = True if re.search( 'current_correction', fn ) else False

# Parse test name and check if Vay current deposition (algo.current_deposition=vay) is used
vay_deposition = True if re.search( 'Vay_deposition', fn ) else False

# Parse test name and check if particle_shape = 4 is used
particle_shape_4 = True if re.search('particle_shape_4', fn) else False

# Parameters (these parameters must match the parameters in `inputs.multi.rt`)
epsilon = 0.01
n = 4.e24
n_osc_x = 2
n_osc_z = 2
xmin = -20e-6; xmax = 20.e-6; Nx = 128
zmin = -20e-6; zmax = 20.e-6; Nz = 128

# Wave vector of the wave
kx = 2.*np.pi*n_osc_x/(xmax-xmin)
kz = 2.*np.pi*n_osc_z/(zmax-zmin)
# Plasma frequency
wp = np.sqrt((n*e**2)/(m_e*epsilon_0))

k = {'Ex':kx, 'Ez':kz}
cos = {'Ex': (0,1,1), 'Ez':(1,1,0)}

def get_contribution( is_cos, k ):
    du = (xmax-xmin)/Nx
    u = xmin + du*( 0.5 + np.arange(Nx) )
    if is_cos == 1:
        return( np.cos(k*u) )
    else:
        return( np.sin(k*u) )

def get_theoretical_field( field, t ):
    amplitude = epsilon * (m_e*c**2*k[field])/e * np.sin(wp*t)
    cos_flag = cos[field]
    x_contribution = get_contribution( cos_flag[0], kx )
    z_contribution = get_contribution( cos_flag[2], kz )

    E = amplitude * x_contribution[:, np.newaxis ] \
                  * z_contribution[np.newaxis, :]

    return( E )

# Read the file
ds = yt.load(fn)
t0 = ds.current_time.to_value()
data = ds.covering_grid(level = 0, left_edge = ds.domain_left_edge, dims = ds.domain_dimensions)
edge = np.array([(ds.domain_left_edge[1]).item(), (ds.domain_right_edge[1]).item(), \
                 (ds.domain_left_edge[0]).item(), (ds.domain_right_edge[0]).item()])

# Check the validity of the fields
error_rel = 0
for field in ['Ex', 'Ez']:
    E_sim = data[('mesh',field)].to_ndarray()[:,:,0]
    E_th = get_theoretical_field(field, t0)
    max_error = abs(E_sim-E_th).max()/abs(E_th).max()
    print('%s: Max error: %.2e' %(field,max_error))
    error_rel = max( error_rel, max_error )

# Plot the last field from the loop (Ez at iteration 40)
fig, (ax1, ax2) = plt.subplots(1, 2, dpi = 100)
# First plot
vmin = E_sim.min()
vmax = E_sim.max()
cax1 = make_axes_locatable(ax1).append_axes('right', size = '5%', pad = '5%')
im1 = ax1.imshow(E_sim, origin = 'lower', extent = edge, vmin = vmin, vmax = vmax)
cb1 = fig.colorbar(im1, cax = cax1)
ax1.set_xlabel(r'$z$')
ax1.set_ylabel(r'$x$')
ax1.set_title(r'$E_z$ (sim)')
# Second plot
vmin = E_th.min()
vmax = E_th.max()
cax2 = make_axes_locatable(ax2).append_axes('right', size = '5%', pad = '5%')
im2 = ax2.imshow(E_th, origin = 'lower', extent = edge, vmin = vmin, vmax = vmax)
cb2 = fig.colorbar(im2, cax = cax2)
ax2.set_xlabel(r'$z$')
ax2.set_ylabel(r'$x$')
ax2.set_title(r'$E_z$ (theory)')
# Save figure
fig.tight_layout()
fig.savefig('Langmuir_multi_2d_analysis.png', dpi = 200)

if particle_shape_4:
# lower fidelity, due to smoothing
    tolerance_rel = 0.07
else:
    tolerance_rel = 0.05

print("error_rel    : " + str(error_rel))
print("tolerance_rel: " + str(tolerance_rel))

assert( error_rel < tolerance_rel )

# Check relative L-infinity spatial norm of rho/epsilon_0 - div(E)
# with current correction (and periodic single box option) or with Vay current deposition
if current_correction:
    tolerance = 1e-9
elif vay_deposition:
    tolerance = 1e-3
if current_correction or vay_deposition:
    rho  = data[('boxlib','rho')].to_ndarray()
    divE = data[('boxlib','divE')].to_ndarray()
    error_rel = np.amax( np.abs( divE - rho/epsilon_0 ) ) / np.amax( np.abs( rho/epsilon_0 ) )
    print("Check charge conservation:")
    print("error_rel = {}".format(error_rel))
    print("tolerance = {}".format(tolerance))
    assert( error_rel < tolerance )

test_name = os.path.split(os.getcwd())[1]
checksumAPI.evaluate_checksum(test_name, fn)
Script analysis_rz.py
Listing 52 You can copy this file from Examples/Tests/langmuir/analysis_rz.py.
#!/usr/bin/env python3

# Copyright 2019 David Grote, Maxence Thevenet
#
# This file is part of WarpX.
#
# License: BSD-3-Clause-LBNL
#
# This is a script that analyses the simulation results from
# the script `inputs.multi.rz.rt`. This simulates a RZ periodic plasma wave.
# The electric field in the simulation is given (in theory) by:
# $$ E_r = -\partial_r \phi = \epsilon \,\frac{mc^2}{e}\frac{2\,r}{w_0^2} \exp\left(-\frac{r^2}{w_0^2}\right) \sin(k_0 z) \sin(\omega_p t)
# $$ E_z = -\partial_z \phi = - \epsilon \,\frac{mc^2}{e} k_0 \exp\left(-\frac{r^2}{w_0^2}\right) \cos(k_0 z) \sin(\omega_p t)
# Unrelated to the Langmuir waves, we also test the plotfile particle filter function in this
# analysis script.
import os
import re
import sys

import matplotlib

matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yt

yt.funcs.mylog.setLevel(50)

import numpy as np
import post_processing_utils
from scipy.constants import c, e, epsilon_0, m_e

sys.path.insert(1, '../../../../warpx/Regression/Checksum/')
import checksumAPI

# this will be the name of the plot file
fn = sys.argv[1]

test_name = os.path.split(os.getcwd())[1]

# Parse test name and check if current correction (psatd.current_correction) is applied
current_correction = True if re.search('current_correction', fn) else False

# Parameters (these parameters must match the parameters in `inputs.multi.rz.rt`)
epsilon = 0.01
n = 2.e24
w0 = 5.e-6
n_osc_z = 2
rmin =   0e-6; rmax = 20.e-6; Nr = 64
zmin = -20e-6; zmax = 20.e-6; Nz = 128

# Wave vector of the wave
k0 = 2.*np.pi*n_osc_z/(zmax-zmin)
# Plasma frequency
wp = np.sqrt((n*e**2)/(m_e*epsilon_0))
kp = wp/c

def Er( z, r, epsilon, k0, w0, wp, t) :
    """
    Return the radial electric field as an array
    of the same length as z and r, in the half-plane theta=0
    """
    Er_array = \
        epsilon * m_e*c**2/e * 2*r/w0**2 * \
            np.exp( -r**2/w0**2 ) * np.sin( k0*z ) * np.sin( wp*t )
    return( Er_array )

def Ez( z, r, epsilon, k0, w0, wp, t) :
    """
    Return the longitudinal electric field as an array
    of the same length as z and r, in the half-plane theta=0
    """
    Ez_array = \
        - epsilon * m_e*c**2/e * k0 * \
            np.exp( -r**2/w0**2 ) * np.cos( k0*z ) * np.sin( wp*t )
    return( Ez_array )

# Read the file
ds = yt.load(fn)
t0 = ds.current_time.to_value()
data = ds.covering_grid(level=0, left_edge=ds.domain_left_edge,
                        dims=ds.domain_dimensions)

# Get cell centered coordinates
dr = (rmax - rmin)/Nr
dz = (zmax - zmin)/Nz
coords = np.indices([Nr, Nz],'d')
rr = rmin + (coords[0] + 0.5)*dr
zz = zmin + (coords[1] + 0.5)*dz

# Check the validity of the fields
overall_max_error = 0
Er_sim = data[('boxlib','Er')].to_ndarray()[:,:,0]
Er_th = Er(zz, rr, epsilon, k0, w0, wp, t0)
max_error = abs(Er_sim-Er_th).max()/abs(Er_th).max()
print('Er: Max error: %.2e' %(max_error))
overall_max_error = max( overall_max_error, max_error )

Ez_sim = data[('boxlib','Ez')].to_ndarray()[:,:,0]
Ez_th = Ez(zz, rr, epsilon, k0, w0, wp, t0)
max_error = abs(Ez_sim-Ez_th).max()/abs(Ez_th).max()
print('Ez: Max error: %.2e' %(max_error))
overall_max_error = max( overall_max_error, max_error )

# Plot the last field from the loop (Ez at iteration 40)
plt.subplot2grid( (1,2), (0,0) )
plt.imshow( Ez_sim )
plt.colorbar()
plt.title('Ez, last iteration\n(simulation)')
plt.subplot2grid( (1,2), (0,1) )
plt.imshow( Ez_th )
plt.colorbar()
plt.title('Ez, last iteration\n(theory)')
plt.tight_layout()
plt.savefig(test_name+'_analysis.png')

error_rel = overall_max_error

tolerance_rel = 0.12

print("error_rel    : " + str(error_rel))
print("tolerance_rel: " + str(tolerance_rel))

assert( error_rel < tolerance_rel )

# Check charge conservation (relative L-infinity norm of error) with current correction
if current_correction:
    divE = data[('boxlib','divE')].to_ndarray()
    rho  = data[('boxlib','rho')].to_ndarray() / epsilon_0
    error_rel = np.amax(np.abs(divE - rho)) / max(np.amax(divE), np.amax(rho))
    tolerance = 1.e-9
    print("Check charge conservation:")
    print("error_rel = {}".format(error_rel))
    print("tolerance = {}".format(tolerance))
    assert( error_rel < tolerance )


## In the final past of the test, we verify that the diagnostic particle filter function works as
## expected in RZ geometry. For this, we only use the last simulation timestep.

dim = "rz"
species_name = "electrons"

parser_filter_fn = "diags/diag_parser_filter000080"
parser_filter_expression = "(py-pz < 0) * (r<10e-6) * (z > 0)"
post_processing_utils.check_particle_filter(fn, parser_filter_fn, parser_filter_expression,
                                            dim, species_name)

uniform_filter_fn = "diags/diag_uniform_filter000080"
uniform_filter_expression = "ids%3 == 0"
post_processing_utils.check_particle_filter(fn, uniform_filter_fn, uniform_filter_expression,
                                            dim, species_name)

random_filter_fn = "diags/diag_random_filter000080"
random_fraction = 0.66
post_processing_utils.check_random_filter(fn, random_filter_fn, random_fraction,
                                          dim, species_name)

checksumAPI.evaluate_checksum(test_name, fn)
Script analysis_1d.py
Listing 53 You can copy this file from Examples/Tests/langmuir/analysis_1d.py.
#!/usr/bin/env python3

# Copyright 2019-2022 Jean-Luc Vay, Maxence Thevenet, Remi Lehe, Prabhat Kumar, Axel Huebl
#
#
# This file is part of WarpX.
#
# License: BSD-3-Clause-LBNL
#
# This is a script that analyses the simulation results from
# the script `inputs.multi.rt`. This simulates a 1D periodic plasma wave.
# The electric field in the simulation is given (in theory) by:
# $$ E_z = \epsilon \,\frac{m_e c^2 k_z}{q_e}\sin(k_z z)\sin( \omega_p t)$$
import os
import re
import sys

import matplotlib

matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yt

yt.funcs.mylog.setLevel(50)

import numpy as np
from scipy.constants import c, e, epsilon_0, m_e

sys.path.insert(1, '../../../../warpx/Regression/Checksum/')
import checksumAPI

# this will be the name of the plot file
fn = sys.argv[1]

# Parse test name and check if current correction (psatd.current_correction=1) is applied
current_correction = True if re.search( 'current_correction', fn ) else False

# Parse test name and check if Vay current deposition (algo.current_deposition=vay) is used
vay_deposition = True if re.search( 'Vay_deposition', fn ) else False

# Parameters (these parameters must match the parameters in `inputs.multi.rt`)
epsilon = 0.01
n = 4.e24
n_osc_z = 2
zmin = -20e-6; zmax = 20.e-6; Nz = 128

# Wave vector of the wave
kz = 2.*np.pi*n_osc_z/(zmax-zmin)
# Plasma frequency
wp = np.sqrt((n*e**2)/(m_e*epsilon_0))

k = {'Ez':kz}
cos = {'Ez':(1,1,0)}

def get_contribution( is_cos, k ):
    du = (zmax-zmin)/Nz
    u = zmin + du*( 0.5 + np.arange(Nz) )
    if is_cos == 1:
        return( np.cos(k*u) )
    else:
        return( np.sin(k*u) )

def get_theoretical_field( field, t ):
    amplitude = epsilon * (m_e*c**2*k[field])/e * np.sin(wp*t)
    cos_flag = cos[field]
    z_contribution = get_contribution( cos_flag[2], kz )

    E = amplitude * z_contribution

    return( E )

# Read the file
ds = yt.load(fn)
t0 = ds.current_time.to_value()
data = ds.covering_grid(level=0, left_edge=ds.domain_left_edge,
                                    dims=ds.domain_dimensions)
# Check the validity of the fields
error_rel = 0
for field in ['Ez']:
    E_sim = data[('mesh',field)].to_ndarray()[:,0,0]
    E_th = get_theoretical_field(field, t0)
    max_error = abs(E_sim-E_th).max()/abs(E_th).max()
    print('%s: Max error: %.2e' %(field,max_error))
    error_rel = max( error_rel, max_error )

# Plot the last field from the loop (Ez at iteration 80)
plt.subplot2grid( (1,2), (0,0) )
plt.plot( E_sim )
#plt.colorbar()
plt.title('Ez, last iteration\n(simulation)')
plt.subplot2grid( (1,2), (0,1) )
plt.plot( E_th )
#plt.colorbar()
plt.title('Ez, last iteration\n(theory)')
plt.tight_layout()
plt.savefig('langmuir_multi_1d_analysis.png')

tolerance_rel = 0.05

print("error_rel    : " + str(error_rel))
print("tolerance_rel: " + str(tolerance_rel))

assert( error_rel < tolerance_rel )

# Check relative L-infinity spatial norm of rho/epsilon_0 - div(E) when
# current correction (psatd.do_current_correction=1) is applied or when
# Vay current deposition (algo.current_deposition=vay) is used
if current_correction or vay_deposition:
    rho  = data[('boxlib','rho')].to_ndarray()
    divE = data[('boxlib','divE')].to_ndarray()
    error_rel = np.amax( np.abs( divE - rho/epsilon_0 ) ) / np.amax( np.abs( rho/epsilon_0 ) )
    tolerance = 1.e-9
    print("Check charge conservation:")
    print("error_rel = {}".format(error_rel))
    print("tolerance = {}".format(tolerance))
    assert( error_rel < tolerance )

test_name = os.path.split(os.getcwd())[1]
checksumAPI.evaluate_checksum(test_name, fn)

Visualize

Note

This section is TODO.