Skip to content

Commit 66204fe

Browse files
roelof-groenewaldpre-commit-ci[bot]archermarx
authored
Add Dan Barnes' effective potential (semi-implicit) Poisson solver (#5079)
This PR adds a new electrostatic solver based on the semi-implicit scheme developed by [Barnes, Journal of Comp. Phys., 424 (2021), 109852](https://www.sciencedirect.com/science/article/pii/S0021999120306264?via%3Dihub) (see Appendix A of reference). The implementation was tested through the simulation of a Langmuir probe placed inside a uniform plasma. To this end a 2d simulation was performed with an initial uniform plasma with a conducting disk inserted in the center of the domain. The disk is kept at 0 V while Neumann boundary conditions are used for the domain boundary. Electrons and hydrogen ions were injected from all the domain boundaries using the `UniformFluxDistribution` in order to simulate the particle flux from a uniform plasma. Thermal boundaries were used for the particles with the same temperatures as the initial plasma particles. The expected outcome of this simulation is that a sheath develops around the "Langmuir probe" with value $V_s = 0.5T_e\ln\left(2\pi\frac{m_e}{2m_p}(1 + \frac{T_i}{T_e})\right) $. A pre-sheath of roughly $0.7T_e$ is also expected to form but the exact value of this depends on the domain size since the pre-sheath extends for many Debye lengths. The figure below shows an example outcome from a simulation as described above in which the semi-implicit factor was set to $C_{SI} = 10$. ![image](https://github.com/user-attachments/assets/de4c4f44-3eb1-4e4b-8297-4c047d1c0d98) Required before merging: - [x] Merging of AMReX-Codes/amrex#3968 - [x] Add example to CI tests - [x] Update documentation --------- Signed-off-by: roelof-groenewald <regroenewald@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Thomas Marks <marksta@umich.edu>
1 parent 7a7352f commit 66204fe

20 files changed

+1028
-7
lines changed

Docs/source/refs.bib

+12
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,18 @@ @article{VayFELB2009
480480
url = {https://doi.org/10.1063/1.3080930},
481481
}
482482

483+
@article{Barnes2021,
484+
title = {Improved C1 shape functions for simplex meshes},
485+
author = {D.C. Barnes},
486+
journal = {Journal of Computational Physics},
487+
volume = {424},
488+
pages = {109852},
489+
year = {2021},
490+
issn = {0021-9991},
491+
doi = {https://doi.org/10.1016/j.jcp.2020.109852},
492+
url = {https://www.sciencedirect.com/science/article/pii/S0021999120306264},
493+
}
494+
483495
@article{Rhee1987,
484496
author = {Rhee, M. J. and Schneider, R. F. and Weidman, D. J.},
485497
title = "{Simple time‐resolving Thomson spectrometer}",

Docs/source/usage/parameters.rst

+17
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,23 @@ Overall simulation parameters
236236
\boldsymbol{\nabla}^2 \phi = - \rho/\epsilon_0 \qquad \boldsymbol{E} = - \boldsymbol{\nabla}\phi \\
237237
\boldsymbol{\nabla}^2 \boldsymbol{A} = - \mu_0 \boldsymbol{j} \qquad \boldsymbol{B} = \boldsymbol{\nabla}\times\boldsymbol{A}
238238
239+
* ``labframe-effective-potential``: Poisson's equation is solved with a modified dielectric function
240+
(resulting in an "effective potential") to create a semi-implicit scheme which is robust to the
241+
numerical instability seen in explicit electrostatic PIC when :math:`\Delta t \omega_{pe} > 2`.
242+
If this option is used the additional parameter ``warpx.effective_potential_factor`` can also be
243+
specified to set the value of :math:`C_{EP}` (default 4). The method is stable for :math:`C_{EP} \geq 1`
244+
regardless of :math:`\Delta t`, however, the larger :math:`C_{EP}` is set, the lower the numerical plasma
245+
frequency will be and therefore care must be taken to not set it so high that the plasma mode
246+
hybridizes with other modes of interest.
247+
Details of the method can be found in Appendix A of :cite:t:`param-Barnes2021` (note that in that paper
248+
the method is referred to as "semi-implicit electrostatic" but here it has been renamed to "effective potential"
249+
to avoid confusion with the semi-implicit method of Chen et al.).
250+
In short, the code solves:
251+
252+
.. math::
253+
254+
\boldsymbol{\nabla}\cdot\left(1+\frac{C_{EP}}{4}\sum_{s \in \text{species}}(\omega_{ps}\Delta t)^2 \right)\boldsymbol{\nabla} \phi = - \rho/\epsilon_0 \qquad \boldsymbol{E} = - \boldsymbol{\nabla}\phi
255+
239256
* ``relativistic``: Poisson's equation is solved **for each species**
240257
in their respective rest frame. The corresponding field
241258
is mapped back to the simulation frame and will produce both E and B

Examples/Tests/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ add_subdirectory(restart)
7171
add_subdirectory(restart_eb)
7272
add_subdirectory(rigid_injection)
7373
add_subdirectory(scraping)
74+
add_subdirectory(effective_potential_electrostatic)
7475
add_subdirectory(silver_mueller)
7576
add_subdirectory(single_particle)
7677
add_subdirectory(space_charge_initialization)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Add tests (alphabetical order) ##############################################
2+
#
3+
4+
add_warpx_test(
5+
test_3d_effective_potential_electrostatic_picmi # name
6+
3 # dims
7+
2 # nprocs
8+
inputs_test_3d_effective_potential_electrostatic_picmi.py # inputs
9+
analysis.py # analysis
10+
diags/field_diag/ # output
11+
OFF # dependency
12+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
#
3+
# --- Analysis script for the effective potential Poisson solver test. This test
4+
# --- is based on the adiabatic plasma expansion benchmark from Connor et al. (2021)
5+
# --- doi.org/10.1109/TPS.2021.3072353.
6+
# --- The electron density distribution (as a function of radius) is compared
7+
# --- with the analytically calculated density based on the input parameters
8+
# --- of the test simulation at each output timestep.
9+
10+
import os
11+
import sys
12+
13+
import dill
14+
import matplotlib.pyplot as plt
15+
import numpy as np
16+
from openpmd_viewer import OpenPMDTimeSeries
17+
from scipy.interpolate import RegularGridInterpolator
18+
19+
from pywarpx import picmi
20+
21+
constants = picmi.constants
22+
23+
# load simulation parameters
24+
with open("sim_parameters.dpkl", "rb") as f:
25+
sim = dill.load(f)
26+
27+
# characteristic expansion time
28+
tau = sim["sigma_0"] * np.sqrt(sim["M"] / (constants.kb * (sim["T_e"] + sim["T_i"])))
29+
30+
31+
def get_analytic_density(r, t):
32+
expansion_factor = 1.0 + t**2 / tau**2
33+
T = sim["T_e"] / expansion_factor
34+
sigma = sim["sigma_0"] * np.sqrt(expansion_factor)
35+
return (
36+
sim["n_plasma"] * (T / sim["T_e"]) ** 1.5 * np.exp(-(r**2) / (2.0 * sigma**2))
37+
)
38+
39+
40+
def get_radial_function(field, info):
41+
"""Helper function to transform Cartesian data to polar data and average
42+
over theta and phi."""
43+
r_grid = np.linspace(0, np.max(info.z) - 1e-3, 30)
44+
theta_grid = np.linspace(0, 2 * np.pi, 40, endpoint=False)
45+
phi_grid = np.linspace(0, np.pi, 20)
46+
47+
r, t, p = np.meshgrid(r_grid, theta_grid, phi_grid, indexing="ij")
48+
x_sp = r * np.sin(p) * np.cos(t)
49+
y_sp = r * np.sin(p) * np.sin(t)
50+
z_sp = r * np.cos(p)
51+
52+
interp = RegularGridInterpolator((info.x, info.y, info.z), field)
53+
field_sp = interp((x_sp, y_sp, z_sp))
54+
return r_grid, np.mean(field_sp, axis=(1, 2))
55+
56+
57+
diag_dir = "diags/field_diag"
58+
ts = OpenPMDTimeSeries(diag_dir, check_all_files=True)
59+
60+
rms_errors = np.zeros(len(ts.iterations))
61+
62+
for ii, it in enumerate(ts.iterations):
63+
rho_e, info = ts.get_field(field="rho_electrons", iteration=it)
64+
r_grid, n_e = get_radial_function(-rho_e / constants.q_e, info)
65+
66+
n_e_analytic = get_analytic_density(r_grid, ts.t[ii])
67+
rms_errors[ii] = (
68+
np.sqrt(np.mean(np.sum((n_e - n_e_analytic) ** 2))) / n_e_analytic[0]
69+
)
70+
71+
plt.plot(r_grid, n_e_analytic, "k--", alpha=0.6)
72+
plt.plot(r_grid, n_e, label=f"t = {ts.t[ii]*1e6:.2f} $\mu$s")
73+
74+
print("RMS error (%) in density: ", rms_errors)
75+
assert np.all(rms_errors < 0.05)
76+
77+
plt.ylabel("$n_e$ (m$^{-3}$)")
78+
plt.xlabel("r (m)")
79+
plt.grid()
80+
plt.legend()
81+
plt.show()
82+
83+
if len(sys.argv) > 1:
84+
sys.path.insert(1, "../../../../warpx/Regression/Checksum/")
85+
import checksumAPI
86+
87+
filename = sys.argv[1]
88+
89+
test_name = os.path.split(os.getcwd())[1]
90+
checksumAPI.evaluate_checksum(test_name, filename, output_format="openpmd")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python3
2+
#
3+
# --- Test script for the effective potential Poisson solver. This test is based
4+
# --- on the adiabatic plasma expansion benchmark from Connor et al. (2021)
5+
# --- doi.org/10.1109/TPS.2021.3072353.
6+
# --- In the benchmark an expanding plasma ball with Gaussian density distribution
7+
# --- in the radial direction is simulated and the time evolution of the
8+
# --- density of the electron species is compared to an approximate analytic solution.
9+
# --- The example is modified slightly in the following ways:
10+
# --- 1) The original example used an electromagnetic solver with absorbing
11+
# --- boundaries while the present case encloses the plasma in a conducting
12+
# --- sphere.
13+
# --- 2) The domain and plasma parameters for this case has been modified to
14+
# --- set the cell-size and time step such that the explicit electrostatic
15+
# --- solver is unstable.
16+
17+
import dill
18+
import numpy as np
19+
from mpi4py import MPI as mpi
20+
from scipy.special import erf
21+
22+
from pywarpx import picmi
23+
24+
constants = picmi.constants
25+
26+
comm = mpi.COMM_WORLD
27+
28+
simulation = picmi.Simulation(warpx_serialize_initial_conditions=True, verbose=0)
29+
30+
m_ion = 25 # Ion mass (electron masses)
31+
32+
# Plasma parameters
33+
n_plasma = 5e12 # Plasma density (m^-3)
34+
sigma_0 = 22 # Initial Gaussian distribution standard deviation (Debye lengths)
35+
T_e = 100.0 # Electron temperature (K)
36+
T_i = 10.0 # Ion temperature (K)
37+
38+
# Spatial domain
39+
R = 86 # Radius of metallic sphere (Debye lengths)
40+
NZ = 72 # Number of cells in each direction
41+
42+
# Temporal domain (if not run as a CI test)
43+
LT = 0.6e-6 # Simulation temporal length (s)
44+
45+
# Numerical parameters
46+
NPARTS = 500000 # Seed number of particles
47+
DT = 0.8 # Time step (electron streaming)
48+
49+
# Solver parameter
50+
C_EP = 1.0 # Effective potential factor
51+
52+
#######################################################################
53+
# Calculate various plasma parameters based on the simulation input #
54+
#######################################################################
55+
56+
# Ion mass (kg)
57+
M = m_ion * constants.m_e
58+
59+
# Electron plasma frequency (Hz)
60+
f_pe = np.sqrt(constants.q_e**2 * n_plasma / (constants.m_e * constants.ep0)) / (
61+
2.0 * np.pi
62+
)
63+
64+
# Debye length (m)
65+
lambda_e = np.sqrt(constants.ep0 * constants.kb * T_e / (n_plasma * constants.q_e**2))
66+
67+
# Thermal velocities (m/s) from v_th = np.sqrt(kT / m)
68+
v_ti = np.sqrt(T_i * constants.kb / M)
69+
v_te = np.sqrt(T_e * constants.kb / constants.m_e)
70+
71+
R *= lambda_e
72+
sigma_0 *= lambda_e
73+
74+
dz = 2.0 * R / (NZ - 4)
75+
Lz = dz * NZ
76+
dt = DT * dz / v_te
77+
78+
total_steps = int(LT / dt)
79+
diag_steps = total_steps // 3
80+
total_steps = diag_steps * 3
81+
82+
# dump attributes needed for analysis to a dill pickle file
83+
if comm.rank == 0:
84+
parameter_dict = {
85+
"sigma_0": sigma_0,
86+
"M": M,
87+
"T_i": T_i,
88+
"T_e": T_e,
89+
"n_plasma": n_plasma,
90+
}
91+
with open("sim_parameters.dpkl", "wb") as f:
92+
dill.dump(parameter_dict, f)
93+
94+
# print out plasma parameters
95+
if comm.rank == 0:
96+
print(
97+
f"Initializing simulation with input parameters:\n"
98+
f"\tT_e = {T_e:.1f} K\n"
99+
f"\tT_i = {T_i:.1f} K\n"
100+
f"\tn = {n_plasma:.1e} m^-3\n"
101+
)
102+
print(
103+
f"Plasma parameters:\n"
104+
f"\tlambda_e = {lambda_e:.1e} m\n"
105+
f"\tt_pe = {1.0/f_pe:.1e} s\n"
106+
f"\tv_ti = {v_ti:.1e} m/s\n"
107+
)
108+
print(
109+
f"Numerical parameters:\n"
110+
f"\tdz/lambda_e = {dz/lambda_e:.2f}\n"
111+
f"\tdt*w_pe = {dt*f_pe*2.0*np.pi:.2f}\n"
112+
f"\tdiag steps = {diag_steps:d}\n"
113+
f"\ttotal steps = {total_steps:d}\n"
114+
)
115+
116+
117+
#######################################################################
118+
# Set geometry and boundary conditions #
119+
#######################################################################
120+
121+
grid = picmi.Cartesian3DGrid(
122+
number_of_cells=[NZ] * 3,
123+
lower_bound=[-Lz / 2.0] * 3,
124+
upper_bound=[Lz / 2.0] * 3,
125+
lower_boundary_conditions=["neumann"] * 3,
126+
upper_boundary_conditions=["neumann"] * 3,
127+
lower_boundary_conditions_particles=["absorbing"] * 3,
128+
upper_boundary_conditions_particles=["absorbing"] * 3,
129+
warpx_max_grid_size=NZ // 2,
130+
)
131+
simulation.time_step_size = dt
132+
simulation.max_steps = total_steps
133+
simulation.current_deposition_algo = "direct"
134+
simulation.particle_shape = 1
135+
simulation.verbose = 1
136+
137+
#######################################################################
138+
# Insert spherical boundary as EB #
139+
#######################################################################
140+
141+
embedded_boundary = picmi.EmbeddedBoundary(
142+
implicit_function=f"(x**2+y**2+z**2-{R**2})",
143+
potential=0.0,
144+
)
145+
simulation.embedded_boundary = embedded_boundary
146+
147+
#######################################################################
148+
# Field solver #
149+
#######################################################################
150+
151+
solver = picmi.ElectrostaticSolver(
152+
grid=grid,
153+
method="Multigrid",
154+
warpx_effective_potential=True,
155+
warpx_effective_potential_factor=C_EP,
156+
warpx_self_fields_verbosity=0,
157+
)
158+
simulation.solver = solver
159+
160+
#######################################################################
161+
# Particle types setup #
162+
#######################################################################
163+
164+
total_parts = (
165+
n_plasma
166+
* sigma_0**2
167+
* (
168+
(2.0 * np.pi) ** 1.5 * sigma_0 * erf(R / (np.sqrt(2) * sigma_0))
169+
+ 4.0 * np.pi * R * np.exp(-(R**2) / (2.0 * sigma_0**2))
170+
)
171+
)
172+
173+
electrons = picmi.Species(
174+
name="electrons",
175+
particle_type="electron",
176+
initial_distribution=picmi.GaussianBunchDistribution(
177+
n_physical_particles=total_parts,
178+
rms_bunch_size=[sigma_0] * 3,
179+
rms_velocity=[v_te] * 3,
180+
),
181+
)
182+
simulation.add_species(
183+
electrons,
184+
layout=picmi.PseudoRandomLayout(grid=grid, n_macroparticles=NPARTS),
185+
)
186+
187+
ions = picmi.Species(
188+
name="ions",
189+
charge="q_e",
190+
mass=M,
191+
initial_distribution=picmi.GaussianBunchDistribution(
192+
n_physical_particles=total_parts,
193+
rms_bunch_size=[sigma_0] * 3,
194+
rms_velocity=[v_ti] * 3,
195+
),
196+
)
197+
simulation.add_species(
198+
ions,
199+
layout=picmi.PseudoRandomLayout(grid=grid, n_macroparticles=NPARTS),
200+
)
201+
202+
#######################################################################
203+
# Add diagnostics #
204+
#######################################################################
205+
206+
field_diag = picmi.FieldDiagnostic(
207+
name="field_diag",
208+
grid=grid,
209+
period=diag_steps,
210+
data_list=[
211+
"E",
212+
"J",
213+
"T_electrons",
214+
"T_ions",
215+
"phi",
216+
"rho_electrons",
217+
"rho_ions",
218+
],
219+
write_dir="diags",
220+
warpx_format="openpmd",
221+
warpx_openpmd_backend="h5",
222+
)
223+
simulation.add_diagnostic(field_diag)
224+
225+
#######################################################################
226+
# Initialize simulation #
227+
#######################################################################
228+
229+
simulation.initialize_inputs()
230+
simulation.initialize_warpx()
231+
232+
#######################################################################
233+
# Execute simulation #
234+
#######################################################################
235+
236+
simulation.step()

0 commit comments

Comments
 (0)