Skip to content

Commit 3960f80

Browse files
adding infrastructure for computing the mass matrices to be used by the implicit EM solvers (#5768)
This PR adds the basic infrastructure for computing the mass matrices needed by the implicit EM solvers to efficiently take time steps larger than the plasma period. The mass matrices are deposited to the grid from the particles in the same fashion as the current is deposited to the grid. The relationship between dJ and dE is written as dJx = MassMatrices_xx * dEx + MassMatrices_xy * dEy + MassMatrices_xz * dEz dJy = MassMatrices_yx * dEx + MassMatrices_yy * dEy + MassMatrices_yz * dEz dJz = MassMatrices_zx * dEx + MassMatrices_zy * dEy + MassMatrices_zz * dEz At each grid point, each element in the expressions above is a sum over all local grid points depending on the particle shape factor. **For this PR, we are only computing the diagonal elements of the diagonal mass matrices**: dJx(i,j,k) = **MassMatrices_xx(i,j,k)** * dEx(i,j,k) dJy(i,j,k) = **MassMatrices_yy(i,j,k)** * dEy(i,j,k) dJz(i,j,k) = **MassMatrices_zz(i,j,k)** * dEz(i,j,k) The diagonal elements are used in the preconditioner for capturing plasma response. The mass matrices are described in Justin Ray Angus, et. al., An implicit particle code with exact energy and charge conservation for electromagnetic studies of dense plasmas, Journal of Computational Physics, Volume 491, 2023, 112383, https://doi.org/10.1016/j.jcp.2023.112383. <img width="1029" alt="dane_gmres_planar_pinch_longer" src="https://github.com/user-attachments/assets/edf66b6a-a9ce-4eb1-a519-a183c14f0ce8" /> This figure illustrates the effectiveness of including the plasma response in the preconditioner. These results are from 1D planar pinch simulations as described in the above reference. The parameters are such that \omega_{pe}*\Delta_t ranges from 18 at t = 0 to 33 for t > 15 ns where the density is compressed. The CFL number is 4.2. The number of GMRES iterations per Newton iteration increases as the plasma density increases when not using a preconditioner. Using the curl curl preconditioner without the plasma response actually result in more iterations. This is not totally unexpected since the stiffness of the system is dominated by the plasma rather than light waves. When the diagonal mass matrices are included in the curl curl preconditioner, the number of iterations decreases and becomes relatively insensitive to the change in density. This behavior is similar to that for the same conditions as reported in the above reference. The number of GMRES iterations can be reduced further by including the first layer of off-diagonal elements of the mass matrices in the PC. That will be done in a future PR. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent f161db9 commit 3960f80

31 files changed

+1845
-78
lines changed

Docs/source/usage/parameters.rst

+4
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ Overall simulation parameters
126126
, this sets the relative tolerance for the iterative method used to obtain a self-consistent update of the particles at
127127
each iteration in the JFNK process.
128128

129+
* ``implicit_evolve.use_mass_matrices`` (`bool`, default: false)
130+
When `algo.evolve_scheme` is either `theta_implicit_em`, `strang_implicit_spectral_em`, or `semi_implicit_em` and `implicit_evolve.nonlinear_solver = newton` and a preconditioner is being used
131+
, the diagonal components of the diagonal mass matrices are used to capture the plasma response in the preconditioner.
132+
129133
* ``picard.verbose`` (`bool`, default: 1)
130134
When `implicit_evolve.nonlinear_solver = picard`, this sets the verbosity of the Picard solver. If true, then information
131135
on the nonlinear error are printed to screen at each nonlinear iteration.

Examples/Tests/implicit/CMakeLists.txt

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Add tests (alphabetical order) ##############################################
22
#
33

4+
add_warpx_test(
5+
test_1d_planar_pinch_withPC # name
6+
1 # dims
7+
2 # nprocs
8+
inputs_test_1d_planar_pinch_withPC # inputs
9+
"analysis_1d_pinch.py" # analysis
10+
"analysis_default_regression.py --path diags/diag1000020" # checksum
11+
OFF # dependency
12+
)
13+
414
add_warpx_test(
515
test_1d_semi_implicit_picard # name
616
1 # dims
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python3
2+
3+
# This file is part of WarpX.
4+
#
5+
# License: BSD-3-Clause-LBNL
6+
#
7+
# This is a script that analyses the simulation results from
8+
# the script `inputs_test_1d_planar_pinch_withPC`.
9+
# This simulates a 1D planar pinch using the implicit solver with
10+
# the curl curl PC including the diagonal response from mass matrices.
11+
12+
import numpy as np
13+
14+
newton_solver = np.loadtxt("diags/reduced_files/newton_solver.txt", skiprows=1)
15+
gmres_iters = newton_solver[:, 5]
16+
17+
field_energy = np.loadtxt("diags/reduced_files/field_energy.txt", skiprows=1)
18+
particle_energy = np.loadtxt("diags/reduced_files/particle_energy.txt", skiprows=1)
19+
poynting_flux = np.loadtxt("diags/reduced_files/poynting_flux.txt", skiprows=1)
20+
21+
E_energy = field_energy[:, 3]
22+
B_energy = field_energy[:, 4]
23+
Efields = E_energy + B_energy
24+
ele_energy = particle_energy[:, 3]
25+
ion_energy = particle_energy[:, 4]
26+
Eplasma = ele_energy + ion_energy
27+
28+
Eout_lo_x = poynting_flux[:, 4]
29+
Eout_hi_x = poynting_flux[:, 5]
30+
31+
# check that violation of energy conservation is below tolerance
32+
dE = Efields + Eplasma + Eout_hi_x + Eout_lo_x
33+
rel_net_energy = np.abs(dE - dE[0]) / Eplasma
34+
max_rel_net_energy = rel_net_energy.max()
35+
rel_net_energy_tol = 1.0e-9
36+
print(f"max relative delta energy : {max_rel_net_energy}")
37+
print(f"relative delta energy tolerance : {rel_net_energy_tol}")
38+
assert max_rel_net_energy < rel_net_energy_tol
39+
40+
# check that the maximum gmres iterations is below tolerance
41+
max_gmres_iters_tol = 30
42+
print(f"max gmres iters: {gmres_iters.max()}")
43+
print(f"gmres iters tolerance: {max_gmres_iters_tol}")
44+
assert gmres_iters.max() < max_gmres_iters_tol
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#################################
2+
########## CONSTANTS ############
3+
#################################
4+
5+
### characteristic plasma values
6+
my_constants.n0 = 1.e23 # plasma density [1/m^3]
7+
my_constants.T0 = 1. # plasma temperature [eV]
8+
my_constants.R0 = 1.5e-2 # plasma radius [m]
9+
my_constants.I0 = 200.e3 # pinch current [Amps]
10+
11+
my_constants.AD = 2.01410 # atomic number deuterium
12+
my_constants.mD = AD*m_u - m_e # deuterium mass [kg]
13+
14+
### derived characteristic pinch values
15+
my_constants.B0 = mu0*I0/(2*pi*R0) # B at r=R0 [Tesla]
16+
my_constants.PB0 = B0^2/(2.0*mu0) # B pressure at r=R0
17+
my_constants.rho0 = AD*m_u*n0 # mass density [kg/m^3]
18+
my_constants.u0 = sqrt(PB0/rho0) # implosion speed [m/s]
19+
my_constants.t0 = R0/u0 # implosion time [m/s]
20+
21+
### simulation parameters
22+
my_constants.tsim = 220.e-9 # sim time = 1.35*t0 [s]
23+
my_constants.rise_time = t0/20.0 # current rise time [s]
24+
my_constants.dt = 1.0e-3*1.0e-9 # time step [s]
25+
my_constants.Nppc = 1028 # particles per cell
26+
27+
#################################
28+
####### GENERAL PARAMETERS ######
29+
#################################
30+
stop_time = tsim
31+
max_step = 20
32+
amr.n_cell = 216
33+
amr.max_level = 0
34+
warpx.numprocs = 2
35+
geometry.dims = 1
36+
geometry.prob_lo = 0.0
37+
geometry.prob_hi = 1.54e-2
38+
39+
#################################
40+
####### Boundary condition ######
41+
#################################
42+
boundary.field_lo = pmc
43+
boundary.field_hi = pec_insulator
44+
boundary.particle_lo = reflecting
45+
boundary.particle_hi = absorbing
46+
47+
insulator.area_z_hi(x,y) = 1.0
48+
insulator.Bx_z_hi(x,y,t) = min(t/rise_time,1)*B0
49+
50+
#################################
51+
############ NUMERICS ###########
52+
#################################
53+
warpx.serialize_initial_conditions = 1
54+
warpx.verbose = 1
55+
warpx.const_dt = dt
56+
warpx.use_filter = 0
57+
58+
algo.maxwell_solver = Yee
59+
algo.evolve_scheme = "theta_implicit_em"
60+
61+
implicit_evolve.theta = 0.5
62+
implicit_evolve.max_particle_iterations = 21
63+
implicit_evolve.particle_tolerance = 1.0e-10
64+
65+
implicit_evolve.nonlinear_solver = "newton"
66+
newton.verbose = true
67+
newton.max_iterations = 100
68+
newton.relative_tolerance = 1.0e-8
69+
newton.absolute_tolerance = 0.0
70+
newton.require_convergence = false
71+
newton.diagnostic_file = "diags/reduced_files/newton_solver.txt"
72+
newton.diagnostic_interval = 10
73+
74+
gmres.verbose_int = 2
75+
gmres.max_iterations = 1000
76+
gmres.relative_tolerance = 1.0e-4
77+
gmres.absolute_tolerance = 0.0
78+
79+
implicit_evolve.use_mass_matrices = true
80+
jacobian.pc_type = "pc_curl_curl_mlmg"
81+
pc_curl_curl_mlmg.verbose = true
82+
pc_curl_curl_mlmg.max_iter = 10
83+
pc_curl_curl_mlmg.relative_tolerance = 1e-4
84+
85+
algo.particle_pusher = "boris"
86+
87+
algo.particle_shape = 2
88+
algo.current_deposition = "villasenor"
89+
90+
#################################
91+
############ PLASMA #############
92+
#################################
93+
particles.species_names = electrons deuterium
94+
95+
electrons.charge = -q_e
96+
electrons.mass = m_e
97+
electrons.injection_style = "NUniformPerCell"
98+
electrons.num_particles_per_cell_each_dim = Nppc
99+
electrons.profile = constant
100+
electrons.density = n0
101+
electrons.zmax = R0
102+
electrons.momentum_distribution_type = "gaussian"
103+
electrons.ux_th = sqrt(T0*q_e/m_e)/clight
104+
electrons.uy_th = sqrt(T0*q_e/m_e)/clight
105+
electrons.uz_th = sqrt(T0*q_e/m_e)/clight
106+
107+
deuterium.charge = q_e
108+
deuterium.mass = mD
109+
deuterium.injection_style = "NUniformPerCell"
110+
deuterium.num_particles_per_cell_each_dim = Nppc
111+
deuterium.profile = constant
112+
deuterium.density = n0
113+
deuterium.zmax = R0
114+
deuterium.momentum_distribution_type = "gaussian"
115+
deuterium.ux_th = sqrt(T0*q_e/mD)/clight
116+
deuterium.uy_th = sqrt(T0*q_e/mD)/clight
117+
deuterium.uz_th = sqrt(T0*q_e/mD)/clight
118+
119+
### Diagnostics
120+
diagnostics.diags_names = diag1
121+
diag1.intervals = 20
122+
diag1.diag_type = Full
123+
diag1.fields_to_plot = Ex Ey Ez Bx By Bz jx jy jz rho divE
124+
diag1.electrons.variables = z w ux uy uz
125+
diag1.deuterium.variables = z w ux uy uz
126+
127+
### reduced diagnostics
128+
warpx.reduced_diags_names = particle_energy field_energy poynting_flux
129+
130+
particle_energy.type = ParticleEnergy
131+
particle_energy.intervals = 10
132+
particle_energy.precision = 18
133+
particle_energy.path = diags/reduced_files/
134+
135+
field_energy.type = FieldEnergy
136+
field_energy.intervals = 10
137+
field_energy.precision = 18
138+
field_energy.path = diags/reduced_files/
139+
140+
poynting_flux.type = FieldPoyntingFlux
141+
poynting_flux.intervals = 10
142+
poynting_flux.precision = 18
143+
poynting_flux.path = diags/reduced_files/
144+
145+
#################################
146+
############ COLLISION ##########
147+
#################################
148+
collisions.collision_names = collision1 collision2 collision3
149+
150+
collision1.species = electrons electrons
151+
collision2.species = deuterium deuterium
152+
collision3.species = electrons deuterium
153+
collision1.CoulombLog = 10.0
154+
collision2.CoulombLog = 10.0
155+
collision3.CoulombLog = 10.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"lev=0": {
3+
"Bx": 0.6884832387863651,
4+
"By": 0.6521976230101406,
5+
"Bz": 0.0,
6+
"Ex": 146532976.36091387,
7+
"Ey": 149594679.4515056,
8+
"Ez": 220206653.62773055,
9+
"divE": 2463670936036.45,
10+
"jx": 5671672121.997759,
11+
"jy": 5780685336.896632,
12+
"jz": 3889054995.637104,
13+
"rho": 21.813805174064612
14+
},
15+
"deuterium": {
16+
"particle_momentum_x": 4.004247985489721e-18,
17+
"particle_momentum_y": 3.99862983197008e-18,
18+
"particle_momentum_z": 3.985428948884101e-18,
19+
"particle_position_x": 1622.111089442873,
20+
"particle_weight": 1.5000033326127676e+21
21+
},
22+
"electrons": {
23+
"particle_momentum_x": 6.586489162124888e-20,
24+
"particle_momentum_y": 6.583939351411483e-20,
25+
"particle_momentum_z": 6.595354090513763e-20,
26+
"particle_position_x": 1622.1110484337041,
27+
"particle_weight": 1.5000033326127676e+21
28+
}
29+
}

Source/Evolve/WarpXEvolve.cpp

+6-3
Original file line numberDiff line numberDiff line change
@@ -1139,18 +1139,20 @@ WarpX::doQEDEvents ()
11391139
#endif
11401140

11411141
void
1142-
WarpX::PushParticlesandDeposit (amrex::Real cur_time, bool skip_current, PushType push_type)
1142+
WarpX::PushParticlesandDeposit (amrex::Real cur_time, bool skip_current,
1143+
bool deposit_mass_matrices, PushType push_type)
11431144
{
11441145
// Evolve particles to p^{n+1/2} and x^{n+1}
11451146
// Deposit current, j^{n+1/2}
11461147
for (int lev = 0; lev <= finest_level; ++lev) {
1147-
PushParticlesandDeposit(lev, cur_time, DtType::Full, skip_current, push_type);
1148+
PushParticlesandDeposit(lev, cur_time, DtType::Full, skip_current,
1149+
deposit_mass_matrices, push_type);
11481150
}
11491151
}
11501152

11511153
void
11521154
WarpX::PushParticlesandDeposit (int lev, amrex::Real cur_time, DtType a_dt_type, bool skip_current,
1153-
PushType push_type)
1155+
bool deposit_mass_matrices, PushType push_type)
11541156
{
11551157
using ablastr::fields::Direction;
11561158
using warpx::fields::FieldType;
@@ -1178,6 +1180,7 @@ WarpX::PushParticlesandDeposit (int lev, amrex::Real cur_time, DtType a_dt_type,
11781180
dt[lev],
11791181
a_dt_type,
11801182
skip_current,
1183+
deposit_mass_matrices,
11811184
push_type
11821185
);
11831186
if (! skip_current) {

Source/FieldSolver/ImplicitSolvers/ImplicitSolver.H

+24-6
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,6 @@ public:
6666
amrex::Real a_dt,
6767
int a_step ) = 0;
6868

69-
70-
/**
71-
* \brief Return pointer to MultiFab array for mass matrix
72-
*/
73-
[[nodiscard]] virtual const amrex::Vector<amrex::Array<amrex::MultiFab*,3>>* GetSigmaCoeff() const { return nullptr; }
74-
7569
//
7670
// the following routines are called by the linear and nonlinear solvers
7771
//
@@ -102,6 +96,14 @@ public:
10296
[[nodiscard]] amrex::Array<amrex::LinOpBCType,AMREX_SPACEDIM> GetLinOpBCLo () const;
10397
[[nodiscard]] amrex::Array<amrex::LinOpBCType,AMREX_SPACEDIM> GetLinOpBCHi () const;
10498

99+
/**
100+
* \brief Return pointer to MultiFab array for mass matrix
101+
*/
102+
[[nodiscard]] const amrex::Vector<amrex::Array<amrex::MultiFab*,3>>* GetMassMatricesCoeff() const {
103+
if (m_use_mass_matrices) { return &m_mmpc_mfarrvec; }
104+
else { return nullptr; }
105+
}
106+
105107
[[nodiscard]] amrex::Real GetTheta () const { return m_theta; }
106108

107109
protected:
@@ -146,6 +148,16 @@ protected:
146148
*/
147149
int m_max_particle_iterations = 21;
148150

151+
/**
152+
* \brief Whether to use mass matrices for the implicit solver
153+
*/
154+
bool m_use_mass_matrices = false;
155+
156+
/**
157+
* \brief Array of multifab pointers to mass matrix preconditioner
158+
*/
159+
amrex::Vector<amrex::Array<amrex::MultiFab*,3>> m_mmpc_mfarrvec;
160+
149161
/**
150162
* \brief parse nonlinear solver parameters (if one is used)
151163
*/
@@ -166,6 +178,7 @@ protected:
166178
m_nlsolver = std::make_unique<NewtonSolver<WarpXSolverVec,ImplicitSolver>>();
167179
pp.query("max_particle_iterations", m_max_particle_iterations);
168180
pp.query("particle_tolerance", m_particle_tolerance);
181+
pp.query("use_mass_matrices", m_use_mass_matrices);
169182
}
170183
else {
171184
WARPX_ABORT_WITH_MESSAGE(
@@ -174,6 +187,11 @@ protected:
174187

175188
}
176189

190+
/**
191+
* \brief Initialize the Mass Matrices used for plasma response in nonlinear Newton solver
192+
*/
193+
void InitializeMassMatrices ();
194+
177195
/**
178196
* \brief Convert from WarpX FieldBoundaryType to amrex::LinOpBCType
179197
*/

0 commit comments

Comments
 (0)