MFEM v4.10.0
Finite element discretization library
Loading...
Searching...
No Matches
electrostatic-pic.cpp
Go to the documentation of this file.
1// Copyright (c) 2010-2026, Lawrence Livermore National Security, LLC. Produced
2// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
3// LICENSE and NOTICE for details. LLNL-CODE-806117.
4//
5// This file is part of the MFEM library. For more information and source code
6// availability visit https://mfem.org.
7//
8// MFEM is free software; you can redistribute it and/or modify it under the
9// terms of the BSD-3 license. We welcome feedback and contributions, see file
10// CONTRIBUTING.md for details.
11//
12// -----------------------------------------------------
13// Particle-In-Cell (PIC) Simulation (2D/3D)
14// -----------------------------------------------------
15//
16// This miniapp performs a Particle-In-Cell simulation (supports 2D or 3D
17// spatial dimensions) of multiple charged particles subject to electric
18// field forces.
19//
20// dp/dt = q E
21//
22// The method used is explicit time integration with a leap-frog scheme.
23//
24// The electric field is computed from the particle charge distribution using
25// a Poisson solver. The particle trajectories are computed within a periodic
26// domain (2D or 3D).
27//
28// Solution process (per timestep, repeating steps 1-6):
29// (1) Deposit charge from particles to grid via Dirac delta function
30// to form the RHS of the Poisson equation
31// (2) Solve Poisson equation (-Δφ = ρ - ρ_0) to compute potential φ, where
32// ρ_0 is a constant neutralizing term that enforces global charge
33// neutrality.
34// (3) Compute electric field E = -∇φ from the potential
35// (4) Interpolate E-field to particle positions
36// (5) Push particles using leap-frog scheme (update momentum and position)
37// (6) Redistribute particles across processors
38//
39// Compile with: make electrostatic-pic
40//
41// Sample runs:
42//
43// 2D2V Linear Landau damping test case (Ricketson & Hu, 2025):
44// mpirun -n 4 ./electrostatic-pic -rdi 1 -npt 409600 -k 0.2855993321 -a 0.05 -nt 200 -nx 32 -ny 32 -O 1 -q 0.001181640625 -m 0.001181640625 -oci 1000 -dt 0.1
45// 3D3V Linear Landau damping test case (Zheng et al., 2025):
46// * mpirun -n 128 ./electrostatic-pic -dim 3 -rdi 1 -npt 40960000 -k 0.5 -a 0.01 -nt 100 -nx 32 -ny 32 -nz 32 -O 1 -q 0.00004844730731 -m 0.00004844730731 -oci 1000 -dt 0.02 -no-vis
47
48#include "mfem.hpp"
53
54#include <ctime>
55#include <fstream>
56#include <iomanip>
57#include <iostream>
58#include <random>
59#include <string>
60#include <vector>
61
62#define EPSILON 1 // ε_0
63
64using namespace std;
65using namespace mfem;
66using namespace mfem::common;
67
68struct PICContext
69{
70 int dim = 2; ///< Spatial dimension.
71 int order = 1; ///< FE order for spatial discretization.
72 int nx = 100; ///< Number of grid cells in x-direction.
73 int ny = 100; ///< Number of grid cells in y-direction.
74 int nz = 100; ///< Number of grid cells in z-direction.
75 real_t L = 1.0; ///< Domain length.
76
77 int ordering = 1; ///< Ordering of particles.
78 int npt = 1000; ///< Number of particles.
79 real_t q = 1.0; ///< Particle charge.
80 real_t m = 1.0; ///< Particle mass.
81
82 real_t k = 1.0; ///< Wave number (Landau damping init).
83 real_t alpha = 0.1; ///< Perturbation amplitude (Landau damping init).
84
85 real_t dt = 1e-2; ///< Time step size.
86
87 int nt = 1000; ///< Number of time steps to run.
88 int redist_interval = 5; ///< Redistribution and update E_gf interval.
89 int output_csv_interval = 1000; ///< Interval for outputting CSV data files.
90
91 bool visualization = true; ///< Enable visualization.
92 int visport = 19916; ///< Port number for visualization server.
93 bool reproduce = true; ///< Enable reproducible results.
95
96/** This class implements explicit time integration for charged particles
97 in an electric field using ParticleSet. */
98class ParticleMover
99{
100public:
101 enum Fields
102 {
103 MASS, // vdim = 1
104 CHARGE, // vdim = 1
105 MOM, // vdim = dim
106 EFIELD // vdim = dim
107 };
108
109protected:
110 /// Pointers to E field GridFunctions
111 ParGridFunction* E_gf;
112
113 /// FindPointsGSLIB object for E field mesh
114 FindPointsGSLIB& E_finder;
115
116 /// ParticleSet of charged particles
117 std::unique_ptr<ParticleSet> charged_particles;
118
119 /// Temporary vectors for particle computation
120 mutable Vector pm_, pp_;
121
122public:
123 ParticleMover(MPI_Comm comm, ParGridFunction* E_gf_,
124 FindPointsGSLIB& E_finder_, int num_particles,
125 Ordering::Type pdata_ordering);
126
127 /// Initialize charged particles with given parameters
128 void InitializeChargedParticles(const real_t& k, const real_t& alpha,
129 real_t m, real_t q, real_t L,
130 bool reproduce = false);
131
132 /// Find Particles in mesh corresponding to E and field
133 void FindParticles();
134
135 /// Advance particles one time step using leap-frog scheme
136 void Step(real_t& t, real_t dt, real_t L, bool first_step = false);
137
138 /// Redistribute particles across processors
139 void Redistribute();
140
141 /// Get reference to ParticleSet
142 ParticleSet& GetParticles() { return *charged_particles; }
143
144 /// Compute (global) kinetic energy from particles
145 /** Optionally, advance the particle momenta by time step @a dt. */
146 real_t ComputeKineticEnergy(real_t dt = 0.) const;
147};
148
149/** Field solver responsible for updating the electrostatic potential and field
150 from the particle charge density. Assembles and solves the periodic Poisson
151 problem, computes the electric field via a discrete gradient operator, and
152 provides utilities for field diagnostics (e.g. global field energy). */
153class FieldSolver
154{
155private:
156 real_t domain_volume;
157 real_t neutralizing_const;
158 ParLinearForm* precomputed_neutralizing_lf = nullptr;
159 bool precompute_neutralizing_const = false;
160 // Diffusion matrix
161 HypreParMatrix* diffusion_matrix;
162 // Gradient operator for computing E = -∇φ
163 ParDiscreteLinearOperator* grad_interpolator;
164 FindPointsGSLIB& E_finder;
166
167protected:
168 /** Compute neutralizing constant and initialize with the constant.
169 Returns a reference to the precomputed neutralizing ParLinearForm. */
170 const ParLinearForm& ComputeNeutralizingRHS(ParFiniteElementSpace* pfes,
171 const ParticleVector& Q,
172 MPI_Comm comm);
173
174 /** Deposit charge from particles into a ParLinearForm (RHS b).
175 b_i = sum_p q_p * φ_i(x_p) */
176 void DepositCharge(ParFiniteElementSpace* pfes, const ParticleVector& Q);
177
178public:
179 FieldSolver(ParFiniteElementSpace* phi_fes, ParFiniteElementSpace* E_fes,
180 FindPointsGSLIB& E_finder_,
181 bool precompute_neutralizing_const_ = false);
182
183 ~FieldSolver();
184
185 /** Update the phi_gf grid function from the particles.
186 Solve periodic Poisson: diffusion_matrix * phi = (rho - <rho>)
187 with zero-mean enforcement via OrthoSolver. */
188 void UpdatePhiGridFunction(ParticleSet& particles, ParGridFunction& phi_gf);
189
190 /** Update E_gf grid function from phi_gf grid function.
191 Compute the gradient: E = -∇φ. */
192 void UpdateEGridFunction(ParGridFunction& phi_gf, ParGridFunction& E_gf);
193
194 /// Compute (global) field energy: 0.5 * ∫ ||E||^2 dx
195 real_t ComputeFieldEnergy(const ParGridFunction& E_gf) const;
196};
197
198/// Prints the program's logo to the given output stream
199void display_banner(ostream& os);
200
201int main(int argc, char* argv[])
202{
203 Mpi::Init(argc, argv);
204 int num_ranks = Mpi::WorldSize();
205 int rank = Mpi::WorldRank();
206 Hypre::Init();
207
208 if (Mpi::Root()) { display_banner(cout); }
209
210 OptionsParser args(argc, argv);
211 args.AddOption(&ctx.dim, "-dim", "--dimension",
212 "Spatial dimension (2 or 3)");
213 args.AddOption(&ctx.order, "-O", "--order",
214 "Finite element polynomial degree");
215 args.AddOption(&ctx.nx, "-nx", "--num-x",
216 "Number of elements in the x direction.");
217 args.AddOption(&ctx.ny, "-ny", "--num-y",
218 "Number of elements in the y direction.");
219 args.AddOption(&ctx.nz, "-nz", "--num-z",
220 "Number of elements in the z direction.");
221 args.AddOption(&ctx.q, "-q", "--charge", "Particle charge.");
222 args.AddOption(&ctx.m, "-m", "--mass", "Particle mass.");
223 args.AddOption(&ctx.dt, "-dt", "--time-step", "Time Step.");
224 args.AddOption(&ctx.nt, "-nt", "--num-timesteps", "Number of timesteps.");
225 args.AddOption(&ctx.npt, "-npt", "--num-particles",
226 "Total number of particles.");
227 args.AddOption(&ctx.k, "-k", "--k", "Wave number for initial distribution.");
228 args.AddOption(&ctx.alpha, "-a", "--alpha",
229 "Perturbation amplitude for initial distribution.");
230 args.AddOption(&ctx.ordering, "-o", "--ordering",
231 "Ordering of particle data. 0 = byNODES, 1 = byVDIM.");
232 args.AddOption(&ctx.redist_interval, "-rdi", "--redist-interval",
233 "Redistribution and update E_gf interval. Disabled if <= 0.");
234 args.AddOption(&ctx.output_csv_interval, "-oci", "--output-csv-interval",
235 "Output CSV interval. Disabled if <= 0.");
236 args.AddOption(&ctx.visualization, "-vis", "--visualization", "-no-vis",
237 "--no-visualization",
238 "Enable or disable GLVis visualization.");
239 args.AddOption(&ctx.visport, "-p", "--send-port", "Socket for GLVis.");
240 args.AddOption(&ctx.reproduce, "-rep", "--reproduce", "-no-rep",
241 "--no-reproduce",
242 "Enable or disable reproducible random seed.");
243 args.Parse();
244 if (!args.Good())
245 {
246 if (Mpi::Root()) { args.PrintUsage(cout); }
247 return 1;
248 }
249 if (Mpi::Root()) { args.PrintOptions(cout); }
250
251 // Assert that dimension is 2 or 3
252 MFEM_VERIFY(ctx.dim == 2 || ctx.dim == 3,
253 "Dimension must be 2 or 3, got " << ctx.dim);
254 MFEM_VERIFY(ctx.alpha >= -1.0 && ctx.alpha < 1.0,
255 "Alpha should be in range [-1, 1).");
256 MFEM_VERIFY(ctx.k > 0.0,
257 "k must be nonzero for displacement initialization.");
258
259 ctx.L = 2.0 * M_PI / ctx.k;
260
261 // 1. make a Cartesian Mesh (2D or 3D)
262 Mesh serial_mesh;
263 std::vector<Vector> translations;
264
265 if (ctx.dim == 2)
266 {
267 serial_mesh = Mesh(Mesh::MakeCartesian2D(
268 ctx.nx, ctx.ny, Element::QUADRILATERAL, false, ctx.L, ctx.L));
269 translations = {Vector({ctx.L, 0.0}), Vector({0.0, ctx.L})};
270 }
271 else // ctx.dim == 3
272 {
273 serial_mesh = Mesh(Mesh::MakeCartesian3D(
274 ctx.nx, ctx.ny, ctx.nz, Element::HEXAHEDRON, ctx.L, ctx.L, ctx.L));
275 translations = {Vector({ctx.L, 0.0, 0.0}), Vector({0.0, ctx.L, 0.0}),
276 Vector({0.0, 0.0, ctx.L})
277 };
278 }
279
280 Mesh periodic_mesh(Mesh::MakePeriodic(
281 serial_mesh, serial_mesh.CreatePeriodicVertexMapping(translations)));
282 // 2. Partition and distribute the mesh
283 ParMesh mesh(MPI_COMM_WORLD, periodic_mesh);
284 serial_mesh.Clear(); // the serial mesh is no longer needed
285 periodic_mesh.Clear(); // the periodic mesh is no longer needed
286
287 // 3. Build the interpolator of E field
288 mesh.EnsureNodes();
289 FindPointsGSLIB E_finder(mesh);
290
291 // 4. Define finite element spaces on the parallel mesh
292 H1_FECollection phi_fec(ctx.order, ctx.dim);
293 ParFiniteElementSpace phi_fespace(&mesh, &phi_fec);
294 ND_FECollection E_fec(ctx.order, ctx.dim);
295 ParFiniteElementSpace E_fespace(&mesh, &E_fec);
296
297 // 5. Initialize the grid functions for the electric field and potential
298 ParGridFunction phi_gf(&phi_fespace);
299 ParGridFunction E_gf(&E_fespace);
300 phi_gf = 0.0; // Initialize phi_gf to zero
301 E_gf = 0.0; // Initialize E_gf to zero
302
303 // 6. Construct the field solver
304 FieldSolver field_solver(&phi_fespace, &E_fespace, E_finder, true);
305
306 // 7. Initialize ParticleMover
307 Ordering::Type ordering_type =
308 ctx.ordering == 0 ? Ordering::byNODES : Ordering::byVDIM;
309 int num_particles =
310 ctx.npt / num_ranks + (rank < (ctx.npt % num_ranks) ? 1 : 0);
311 ParticleMover particle_mover(MPI_COMM_WORLD, &E_gf, E_finder, num_particles,
312 ordering_type);
313 particle_mover.InitializeChargedParticles(ctx.k, ctx.alpha, ctx.m, ctx.q,
314 ctx.L, ctx.reproduce);
315
316 // 8. Start the main loop
317 real_t t = 0;
318 real_t dt = ctx.dt;
319
321 sw.Start();
322 for (int step = 1; step <= ctx.nt; step++)
323 {
324 // Step the FieldSolver
325 if (ctx.redist_interval > 0 &&
326 (step % ctx.redist_interval == 0 || step == 1) &&
327 particle_mover.GetParticles().GetGlobalNParticles() > 0)
328 {
329 // Redistribute
330 particle_mover.Redistribute();
331
332 // Update phi_gf from particles
333 field_solver.UpdatePhiGridFunction(particle_mover.GetParticles(),
334 phi_gf);
335 // Update E_gf from phi_gf
336 field_solver.UpdateEGridFunction(phi_gf, E_gf);
337
338 // Visualize fields if requested
339 if (ctx.visualization)
340 {
341 static socketstream vis_e, vis_phi;
342 common::VisualizeField(vis_e, "localhost", ctx.visport, E_gf,
343 "E_field", 0, 0, 500, 500);
344 common::VisualizeField(vis_phi, "localhost", ctx.visport, phi_gf,
345 "Potential", 500, 0, 500, 500);
346 }
347 }
348
349 // Step the ParticleMover
350 particle_mover.Step(t, dt, ctx.L, step == 1);
351 if (Mpi::Root())
352 {
353 mfem::out << "Step: " << step << " | Time: " << t;
354 mfem::out << " | Time per step: " << sw.RealTime() / step;
355 mfem::out << endl;
356 }
357 // Output particle data to CSV
358 if (ctx.output_csv_interval > 0 &&
359 (step % ctx.output_csv_interval == 0 || step == 1))
360 {
361 std::string csv_prefix = "PIC_Part_";
362 Array<int> field_idx{2}, tag_idx;
363 std::string file_name =
364 csv_prefix + mfem::to_padded_string(step, 6) + ".csv";
365 particle_mover.GetParticles().PrintCSV(file_name.c_str(), field_idx,
366 tag_idx);
367 }
368
369 if (ctx.redist_interval > 0 &&
370 (step % ctx.redist_interval == 0 || step == 1) &&
371 particle_mover.GetParticles().GetGlobalNParticles() > 0)
372 {
373 // Compute energies
374 // Note that particle momenta are a half time step ahead of the field
375 // after particle_mover.Step(). Therefore they are returned to the
376 // time level of the field for calculation of kinetic energy.
377 real_t kinetic_energy = particle_mover.ComputeKineticEnergy(-dt/2.);
378 real_t field_energy = field_solver.ComputeFieldEnergy(E_gf);
379
380 // Output energies
381 if (Mpi::Root())
382 {
383 cout << "Kinetic energy: " << kinetic_energy << "\t"
384 << "Field energy: " << field_energy << "\t"
385 << "Total energy: " << kinetic_energy + field_energy
386 << endl;
387 }
388 // Write energies to a CSV file
389 if (Mpi::Root())
390 {
391 std::ofstream energy_file("energy.csv", std::ios::app);
392 energy_file << setprecision(10) << kinetic_energy << ","
393 << field_energy << "," << kinetic_energy + field_energy
394 << "\n";
395 }
396 }
397 }
398}
399
400ParticleMover::ParticleMover(MPI_Comm comm, ParGridFunction* E_gf_,
401 FindPointsGSLIB& E_finder_, int num_particles,
402 Ordering::Type pdata_ordering)
403 : E_gf(E_gf_), E_finder(E_finder_)
404{
405 MFEM_ASSERT(E_gf, "Must pass an E field to ParticleMover.");
406
407 int dim = E_gf->ParFESpace()->GetMesh()->SpaceDimension();
408
409 pm_.SetSize(dim);
410 pp_.SetSize(dim);
411
412 // Create particle set: 2 scalars of mass and charge,
413 // 2 vectors of size space dim for momentum and e field
414 Array<int> field_vdims({1, 1, dim, dim});
415 charged_particles = std::make_unique<ParticleSet>(
416 comm, num_particles, dim, field_vdims, 1, pdata_ordering);
417}
418
419void ParticleMover::InitializeChargedParticles(const real_t& k,
420 const real_t& alpha, real_t m,
421 real_t q, real_t L,
422 bool reproduce)
423{
424 int rank;
425 MPI_Comm_rank(charged_particles->GetComm(), &rank);
426 // use time-based seed for randomness
427 std::mt19937 gen(
428 reproduce ? rank : (rank + static_cast<unsigned int>(time(nullptr))));
429 std::uniform_real_distribution<> real_dist(0.0, 1.0);
430 std::normal_distribution<> norm_dist(0.0, 1.0);
431
432 int dim = charged_particles->Coords().GetVDim();
433
434 ParticleVector& X = charged_particles->Coords();
435 ParticleVector& P = charged_particles->Field(ParticleMover::MOM);
436 ParticleVector& M = charged_particles->Field(ParticleMover::MASS);
437 ParticleVector& Q = charged_particles->Field(ParticleMover::CHARGE);
438
439 for (int i = 0; i < charged_particles->GetNParticles(); i++)
440 {
441 // Initialize momentum
442 for (int d = 0; d < dim; d++) { P(i, d) = m * norm_dist(gen); }
443
444 // Uniform positions (no accept-reject)
445 for (int d = 0; d < dim; d++) { X(i, d) = real_dist(gen) * L; }
446
447 // Displacement along x for perturbation ~ cos(k x)
448 for (int d = 0; d < dim; d++)
449 {
450 real_t x = X(i, d);
451 x -= (alpha / k) * std::sin(k * x);
452
453 // periodic wrap to [0, L)
454 x = std::fmod(x, L);
455 if (x < 0) { x += L; }
456
457 X(i, d) = x;
458 }
459
460 // Initialize mass + charge
461 M(i) = m;
462 Q(i) = q;
463 }
464 FindParticles();
465}
466
467void ParticleMover::FindParticles()
468{
469 E_finder.FindPoints(charged_particles->Coords());
470}
471
472void ParticleMover::Step(real_t& t, real_t dt, real_t L, bool first_step)
473{
474 // Update E field at particles
475 ParticleVector& E = charged_particles->Field(EFIELD);
476 E_finder.Interpolate(*E_gf, E, E.GetOrdering());
477
478 // Extract particle data
479 ParticleVector& X = charged_particles->Coords();
480 ParticleVector& P = charged_particles->Field(MOM);
481 ParticleVector& M = charged_particles->Field(MASS);
482 ParticleVector& Q = charged_particles->Field(CHARGE);
483
484 // Accelerate the particles by the electric field
485 const int npt = charged_particles->GetNParticles();
486 const int dim = X.GetVDim();
487
488 for (int particle = 0; particle < npt; ++particle)
489 {
490 for (int d = 0; d < dim; ++d)
491 {
492 P(particle, d) +=
493 (first_step ? dt / 2.0 : dt) * Q(particle) * E(particle, d);
494 }
495 }
496
497 // Periodic boundary: wrap coordinates to [0, L)
498 for (int particle = 0; particle < npt; ++particle)
499 {
500 for (int d = 0; d < dim; ++d)
501 {
502 X(particle, d) += dt / M(particle) * P(particle, d);
503 while (X(particle, d) >= L) { X(particle, d) -= L; }
504 while (X(particle, d) < 0.0) { X(particle, d) += L; }
505 }
506 }
507
508 FindParticles();
509
510 // Update time
511 t += dt;
512}
513
514void ParticleMover::Redistribute()
515{
516 charged_particles->Redistribute(E_finder.GetProc());
517 FindParticles();
518}
519
520real_t ParticleMover::ComputeKineticEnergy(real_t dt) const
521{
522 const ParticleVector& P = charged_particles->Field(MOM);
523 const ParticleVector& M = charged_particles->Field(MASS);
524 const ParticleVector& Q = charged_particles->Field(CHARGE);
525 const ParticleVector& E = charged_particles->Field(EFIELD);
526
527 // Note the electric field is not reinterpolated here and the last
528 // update from Step() is used directly.
529
530 real_t kinetic_energy = 0.0;
531 for (int p = 0; p < charged_particles->GetNParticles(); ++p)
532 {
533 real_t p_square_p = 0.0;
534 for (int d = 0; d < P.GetVDim(); ++d)
535 {
536 const real_t P_m = P(p, d) + dt * Q(p) * E(p, d);
537 p_square_p += P_m * P_m;
538 }
539 kinetic_energy += 0.5 * p_square_p / M(p);
540 }
541
542 real_t global_kinetic_energy = 0.0;
543 MPI_Allreduce(&kinetic_energy, &global_kinetic_energy, 1,
545 MPI_SUM, charged_particles->GetComm());
546 return global_kinetic_energy;
547}
548
549FieldSolver::FieldSolver(ParFiniteElementSpace* phi_fes,
551 FindPointsGSLIB& E_finder_,
552 bool precompute_neutralizing_const_)
553 : precompute_neutralizing_const(precompute_neutralizing_const_),
554 E_finder(E_finder_),
555 b(phi_fes)
556{
557 // compute domain volume
558 ParMesh* pmesh = phi_fes->GetParMesh();
559 real_t local_domain_volume = 0.0;
560 for (int i = 0; i < pmesh->GetNE(); i++)
561 {
562 local_domain_volume += pmesh->GetElementVolume(i);
563 }
564 MPI_Allreduce(&local_domain_volume, &domain_volume, 1,
566 phi_fes->GetParMesh()->GetComm());
567
568 {
569 // Par bilinear form for the gradgrad matrix
570 ParBilinearForm dm(phi_fes);
571 ConstantCoefficient epsilon(EPSILON); // ε_0
572 dm.AddDomainIntegrator(
573 new DiffusionIntegrator(epsilon)); // ∫ ∇φ_i · ∇φ_j
574
575 dm.Assemble();
576 dm.Finalize();
577
578 diffusion_matrix = dm.ParallelAssemble(); // global gradgrad matrix
579 }
580
581 {
582 // Compute E = -∇φ using DiscreteLinearOperator
583 grad_interpolator = new ParDiscreteLinearOperator(phi_fes, E_fes);
584 grad_interpolator->AddDomainInterpolator(new GradientInterpolator);
585 grad_interpolator->Assemble();
586 }
587}
588
589FieldSolver::~FieldSolver()
590{
591 delete diffusion_matrix;
592 delete precomputed_neutralizing_lf;
593 delete grad_interpolator;
594}
595
596const ParLinearForm& FieldSolver::ComputeNeutralizingRHS(
597 ParFiniteElementSpace* pfes, const ParticleVector& Q, MPI_Comm comm)
598{
599 int npt = Q.Size();
600 // Get E_finder references
601 const Array<unsigned int>& code = E_finder.GetCode();
602
603 if (!precompute_neutralizing_const || precomputed_neutralizing_lf == nullptr)
604 {
605 // compute neutralizing constant
606 real_t local_sum = 0.0;
607 for (int p = 0; p < npt; ++p)
608 {
609 // Skip particles not successfully found
610 MFEM_ASSERT(code[p] != 2, "Particle " << p << " not found.");
611 local_sum += Q(p);
612 }
613
614 real_t global_sum = 0.0;
615 MPI_Allreduce(&local_sum, &global_sum, 1, MPITypeMap<real_t>::mpi_type,
616 MPI_SUM, comm);
617
618 neutralizing_const = -global_sum / domain_volume;
619 if (Mpi::Root())
620 {
621 cout << "Total charge: " << global_sum
622 << ", Domain volume: " << domain_volume
623 << ", Neutralizing constant: " << neutralizing_const << endl;
624 if (precompute_neutralizing_const)
625 {
626 cout << "Further updates will use this precomputed neutralizing "
627 "constant."
628 << endl;
629 }
630 }
631 delete precomputed_neutralizing_lf;
632 precomputed_neutralizing_lf = new ParLinearForm(pfes);
633 *precomputed_neutralizing_lf = 0.0;
634 ConstantCoefficient neutralizing_coeff(neutralizing_const);
635 precomputed_neutralizing_lf->AddDomainIntegrator(
636 new DomainLFIntegrator(neutralizing_coeff));
637 precomputed_neutralizing_lf->Assemble();
638 }
639 return *precomputed_neutralizing_lf;
640}
641
642void FieldSolver::DepositCharge(ParFiniteElementSpace* pfes,
643 const ParticleVector& Q)
644{
645 int npt = Q.Size();
646 ParMesh* pmesh = pfes->GetParMesh();
647 int dim = pmesh->SpaceDimension();
648 int curr_rank;
649 MPI_Comm_rank(pmesh->GetComm(), &curr_rank);
650
651 // Get E_finder references
652 // 0: inside, 1: boundary, 2: not found
653 const Array<unsigned int>& code = E_finder.GetCode();
654 const Array<unsigned int>& proc = E_finder.GetProc(); // owning MPI rank
655 const Array<unsigned int>& elem = E_finder.GetElem(); // local element id
656 const Vector& rref = E_finder.GetReferencePosition(); // (r,s,t) byVDIM
657
658 Array<int> dofs;
659
660 for (int p = 0; p < npt; ++p)
661 {
662 // Skip particles not successfully found
663 MFEM_ASSERT(code[p] != 2, "Particle " << p << " not found.");
664
665 // Assert particle is on the current rank
666 MFEM_ASSERT((int)proc[p] == curr_rank,
667 "Particle " << p << " found in element owned by rank "
668 << proc[p] << " but current rank is " << curr_rank
669 << "." << endl
670 << "You must call redistribute every time before "
671 "updating the density grid function.");
672 const int e = elem[p];
673
674 // Reference coordinates for this particle (r,s[,t]) with byVDIM layout
676 ip.Set(rref.GetData() + dim * p, dim);
677
678 const FiniteElement& fe = *pfes->GetFE(e);
679 const int ldofs = fe.GetDof();
680
681 Vector shape(ldofs);
682 fe.CalcShape(ip, shape); // φ_i(x_p) in this element
683
684 pfes->GetElementDofs(e, dofs); // local dof indices
685
686 const real_t q_p = Q(p);
687
688 // Add q_p * φ_i(x_p) to b_i
689 b.AddElementVector(dofs, q_p, shape);
690 }
691}
692
693void FieldSolver::UpdatePhiGridFunction(ParticleSet& particles,
694 ParGridFunction& phi_gf)
695{
696 // FE space / mesh
697 ParFiniteElementSpace* pfes = phi_gf.ParFESpace();
698
699 // Particle data: Q - charges (npt x 1)
700 ParticleVector& Q = particles.Field(ParticleMover::CHARGE);
701
702 // --------------------------------------------------------
703 // 1) Make RHS and pre-subtract averaged charge density for zero-mean RHS
704 // --------------------------------------------------------
705 MPI_Comm comm = pfes->GetComm();
706 b = ComputeNeutralizingRHS(pfes, Q, comm);
707
708 // --------------------------------------------------------
709 // 2) Deposit q_p * phi_i(x_p) into a ParLinearForm (RHS b)
710 // b_i = sum_p q_p * φ_i(x_p)
711 // --------------------------------------------------------
712 DepositCharge(pfes, Q);
713
714 // Assemble to a global true-dof RHS vector compatible with MassMatrix
715 HypreParVector B(pfes);
716 b.ParallelAssemble(B);
717
718 // ------------------------------------------------------------------
719 // 3) Solve A * phi = B with zero-mean enforcement via OrthoSolver
720 // ------------------------------------------------------------------
721 phi_gf = 0.0;
722 HypreParVector Phi_true(pfes);
723 Phi_true = 0.0;
724
725 HyprePCG solver(diffusion_matrix->GetComm());
726 solver.SetOperator(*diffusion_matrix);
727 solver.SetTol(1e-12);
728 solver.SetMaxIter(200);
729 solver.SetPrintLevel(0);
730
731 HypreBoomerAMG prec(*diffusion_matrix);
732 prec.SetPrintLevel(0);
733 solver.SetPreconditioner(prec);
734
735 OrthoSolver ortho(comm);
736 ortho.SetSolver(solver);
737 ortho.Mult(B, Phi_true);
738
739 // Map true-dof solution back to the ParGridFunction
740 phi_gf.Distribute(Phi_true);
741}
742
743void FieldSolver::UpdateEGridFunction(ParGridFunction& phi_gf,
744 ParGridFunction& E_gf)
745{
746 // Compute ∇φ using precomputed gradient operator
747 grad_interpolator->Mult(phi_gf, E_gf);
748 // Scale by -1 to get E = -∇φ
749 E_gf.Neg();
750}
751
752real_t FieldSolver::ComputeFieldEnergy(const ParGridFunction& E_gf) const
753{
754 // ---- Field energy: 0.5 * ∫ ||E||^2 dx ----
755 const ParFiniteElementSpace* fes = E_gf.ParFESpace();
756 const ParMesh* pmesh = fes->GetParMesh();
757
758 const int order = fes->GetMaxElementOrder();
759 const int qorder = std::max(2, 2 * order + 1);
760
762 for (int g = 0; g < Geometry::NumGeom; g++)
763 {
764 irs[g] = &IntRules.Get(g, qorder);
765 }
766
767 real_t field_energy = 0.0;
768
769 Vector zero(pmesh->Dimension());
770 zero = 0.0;
771 VectorConstantCoefficient zero_vec(zero);
772
773 const real_t E_l2 = E_gf.ComputeL2Error(zero_vec, irs);
774 field_energy = 0.5 * EPSILON * E_l2 * E_l2;
775
776 return field_energy;
777}
778
779void display_banner(ostream& os)
780{
781 os << R"(
782 ██████╗░██╗░█████╗░
783 ██╔══██╗██║██╔══██╗
784 ██████╔╝██║██║░░╚═╝
785 ██╔═══╝░██║██║░░██╗
786 ██║░░░░░██║╚█████╔╝
787 ╚═╝░░░░░╚═╝░╚════╝░
788 )"
789 << endl
790 << flush;
791}
A coefficient that is constant across space and time.
Class for domain integration .
Definition lininteg.hpp:108
FindPointsGSLIB can robustly evaluate a GridFunction on an arbitrary collection of points....
Definition gslib.hpp:115
void FindPoints(const Vector &point_pos, int point_pos_ordering=Ordering::byNODES)
Searches positions given in physical space by point_pos.
Definition gslib.cpp:1372
virtual const Vector & GetReferencePosition() const
Return reference coordinates for each point found by FindPoints.
Definition gslib.hpp:612
virtual void Interpolate(const GridFunction &field_in, Vector &field_out)
Interpolation of field values at prescribed reference space positions.
Definition gslib.cpp:3679
virtual const Array< unsigned int > & GetCode() const
Return code for each point searched by FindPoints: inside element (0), element boundary (1),...
Definition gslib.hpp:606
virtual const Array< unsigned int > & GetElem() const
Return element number for each point found by FindPoints.
Definition gslib.hpp:608
virtual const Array< unsigned int > & GetProc() const
Return MPI rank on which each point was found by FindPoints.
Definition gslib.hpp:610
Abstract class for all finite elements.
Definition fe_base.hpp:294
virtual void CalcShape(const IntegrationPoint &ip, Vector &shape) const =0
Evaluate the values of all shape functions of a scalar finite element in reference space at the given...
int GetDof() const
Returns the number of degrees of freedom in the finite element.
Definition fe_base.hpp:410
static const int NumGeom
Definition geom.hpp:46
Arbitrary order H1-conforming (continuous) finite elements.
Definition fe_coll.hpp:291
The BoomerAMG solver in hypre.
Definition hypre.hpp:1829
Wrapper for hypre's ParCSR matrix class.
Definition hypre.hpp:419
MPI_Comm GetComm() const
MPI communicator.
Definition hypre.hpp:610
Wrapper for hypre's parallel vector class.
Definition hypre.hpp:230
static void Init()
Initialize hypre by calling HYPRE_Init() and set default options. After calling Hypre::Init(),...
Definition hypre.cpp:33
Class for integration point with weight.
Definition intrules.hpp:35
void Set(const real_t x1, const real_t x2, const real_t x3, const real_t w)
Definition intrules.hpp:68
Class for an integration rule - an Array of IntegrationPoint.
Definition intrules.hpp:96
const IntegrationRule & Get(int GeomType, int Order)
Returns an integration rule for given GeomType and Order.
void AddDomainIntegrator(LinearFormIntegrator *lfi)
Adds new Domain Integrator. Assumes ownership of lfi.
Mesh data type.
Definition mesh.hpp:67
void EnsureNodes()
Make sure that the mesh has valid nodes, i.e. its geometry is described by a vector finite element gr...
Definition mesh.cpp:7159
void Clear()
Clear the contents of the Mesh.
Definition mesh.hpp:835
int GetNE() const
Returns number of elements.
Definition mesh.hpp:1390
int Dimension() const
Dimension of the reference space used within the elements.
Definition mesh.hpp:1314
int SpaceDimension() const
Dimension of the physical space containing the mesh.
Definition mesh.hpp:1317
static Mesh MakeCartesian3D(int nx, int ny, int nz, Element::Type type, real_t sx=1.0, real_t sy=1.0, real_t sz=1.0, bool sfc_ordering=true)
Creates a mesh for the parallelepiped [0,sx]x[0,sy]x[0,sz], divided into nx*ny*nz hexahedra if type =...
Definition mesh.cpp:4786
static Mesh MakePeriodic(const Mesh &orig_mesh, const std::vector< int > &v2v)
Create a periodic mesh by identifying vertices of orig_mesh.
Definition mesh.cpp:6205
real_t GetElementVolume(int i)
Definition mesh.cpp:125
std::vector< int > CreatePeriodicVertexMapping(const std::vector< Vector > &translations, real_t tol=1e-8) const
Creates a mapping v2v from the vertex indices of the mesh such that coincident vertices under the giv...
Definition mesh.cpp:6239
static Mesh MakeCartesian2D(int nx, int ny, Element::Type type, bool generate_edges=false, real_t sx=1.0, real_t sy=1.0, bool sfc_ordering=true)
Creates mesh for the rectangle [0,sx]x[0,sy], divided into nx*ny quadrilaterals if type = QUADRILATER...
Definition mesh.cpp:4776
void Mult(const Vector &x, Vector &y) const override
Matrix multiplication: .
static bool Root()
Return true if the rank in MPI_COMM_WORLD is zero.
static int WorldRank()
Return the MPI rank in MPI_COMM_WORLD.
static int WorldSize()
Return the size of MPI_COMM_WORLD.
static void Init(int &argc, char **&argv, int required=default_thread_required, int *provided=nullptr)
Singleton creation with Mpi::Init(argc, argv).
Arbitrary order H(curl)-conforming Nedelec finite elements.
Definition fe_coll.hpp:526
void Parse()
Parse the command-line options. Note that this function expects all the options provided through the ...
void PrintUsage(std::ostream &out) const
Print the usage message.
void PrintOptions(std::ostream &out) const
Print the options.
void AddOption(bool *var, const char *enable_short_name, const char *enable_long_name, const char *disable_short_name, const char *disable_long_name, const char *description, bool required=false)
Add a boolean option and set 'var' to receive the value. Enable/disable tags are used to set the bool...
Definition optparser.hpp:82
bool Good() const
Return true if the command line options were parsed successfully.
Type
Ordering methods:
Definition ordering.hpp:17
Solver wrapper which orthogonalizes the input and output vector.
Definition solvers.hpp:1332
Class for parallel bilinear form.
Abstract parallel finite element space.
Definition pfespace.hpp:31
MPI_Comm GetComm() const
Definition pfespace.hpp:337
int GetMaxElementOrder() const override
Returns the maximum polynomial order over all elements globally.
void GetElementDofs(int i, Array< int > &dofs, DofTransformation &doftrans) const override
The same as GetElementDofs(), but with a user-provided DofTransformation object.
Definition pfespace.cpp:593
ParMesh * GetParMesh() const
Definition pfespace.hpp:341
const FiniteElement * GetFE(int i) const override
Definition pfespace.cpp:663
Class for parallel grid function.
Definition pgridfunc.hpp:50
real_t ComputeL2Error(Coefficient *exsol[], const IntegrationRule *irs[]=NULL, const Array< int > *elems=NULL) const override
Returns ||u_ex - u_h||_L2 in parallel for H1 or L2 elements.
ParFiniteElementSpace * ParFESpace() const
void Distribute(const Vector *tv)
Class for parallel linear form.
void ParallelAssemble(Vector &tv)
Assemble the vector on the true dofs, i.e. P^t v.
void Assemble()
Assembles the ParLinearForm i.e. sums over all domain/bdr integrators.
Class for parallel meshes.
Definition pmesh.hpp:35
MPI_Comm GetComm() const
Definition pmesh.hpp:403
ParticleSet initializes and manages data associated with particles.
ParticleVector & Field(int f)
Get a reference to field f 's ParticleVector.
ParticleVector carries vector data (of a given vector dimension) for an arbitrary number of particles...
int GetVDim() const
Get the Vector dimension of the ParticleVector.
Ordering::Type GetOrdering() const
Get the ordering of data in the ParticleVector.
Timing object.
Definition tic_toc.hpp:36
double RealTime()
Return the number of real seconds elapsed since the stopwatch was started.
Definition tic_toc.cpp:432
void Start()
Start the stopwatch. The elapsed time is not cleared.
Definition tic_toc.cpp:411
Vector coefficient that is constant in space and time.
Vector data type.
Definition vector.hpp:82
void Neg()
(*this) = -(*this)
Definition vector.cpp:376
void AddElementVector(const Array< int > &dofs, const Vector &elemvect)
Add elements of the elemvect Vector to the entries listed in dofs. Negative dof values cause the -dof...
Definition vector.cpp:785
int Size() const
Returns the size of the vector.
Definition vector.hpp:234
real_t * GetData() const
Return a pointer to the beginning of the Vector data.
Definition vector.hpp:243
struct PICContext ctx
void display_banner(ostream &os)
Prints the program's logo to the given output stream.
const real_t alpha
Definition ex15.cpp:369
int dim
Definition ex24.cpp:53
real_t epsilon
Definition ex25.cpp:141
int main()
real_t b
Definition lissajous.cpp:42
void VisualizeField(socketstream &sock, const char *vishost, int visport, GridFunction &gf, const char *title, int x, int y, int w, int h, const char *keys, bool vec)
OutStream out(std::cout)
Global stream used by the library for standard output. Initially it uses the same std::streambuf as s...
Definition globals.hpp:66
std::string to_padded_string(int i, int digits)
Convert an integer to a 0-padded string with the given number of digits.
Definition text.hpp:96
float real_t
Definition config.hpp:46
IntegrationRules IntRules(0, Quadrature1D::GaussLegendre)
A global object with all integration rules (defined in intrules.cpp)
Definition intrules.hpp:549
STL namespace.
real_t p(const Vector &x, real_t t)
Helper struct to convert a C++ type to an MPI type.