Accessing Beam Particle Data
Selecting the beam
The beam is accessed through sim.beam, where sim is the
ImpactX instance set up as described in How to run a simulation with Python extensions.
This returns an impactx.ParticleContainer, distributed over MPI ranks.
pc = sim.beam
The available per-particle attributes are:
Positions relative to the reference particle:
position_x,position_y,position_t(all in meters;position_tis \(c \Delta t\)).Momenta relative to and normalized by the reference momentum:
momentum_x,momentum_y,momentum_t.Spin components (only if
sim.spin = True):spin_x,spin_y,spin_z.Charge over mass
qm(in 1/eV), which is currently inconsistently used in ImpactX.Macroparticle weight
w(unitless), how many physical particles a simulation particle represents.Unique ID
idcpu, a 64bit integer that is unique to a particle over the lifetime of a simulation.
The ref attribute provides the reference particle (a
impactx.RefPart). All phase-space coordinates above are relative to the reference particle.
Note
The independent variable in ImpactX is the reference-trajectory path length s (not time).
See Coordinates and Units for the full coordinate and unit conventions.
Accessing/modifying the underlying particle data
There are two ways to access particle data, with different trade-offs between convenience and performance. (For an in-depth discussion of the underlying pyAMReX API, see the pyAMReX particles guide.)
The to_df() method returns a
pandas DataFrame
containing the particle data. The columns of the DataFrame are the particle attributes
(e.g. position_x, momentum_x, w, id), and each row corresponds to one
macroparticle across all tiles on the current MPI rank.
Warning
The DataFrame is a copy of the particle data. Modifying it does not write back to the simulation.
Note
to_df is convenient because it concatenates all particles across tiles
into a single table. However, it incurs copies and, on GPU runs, CPU↔GPU data transfers.
It is well-suited to debugging and visualization, and not to performance-critical paths.
pc = sim.beam
# local particles only (default): returns particles on the current MPI rank
df = pc.to_df(local=True)
if df is not None:
print("Available attributes:", list(df.columns))
print("Number of particles:", len(df))
print("position_x:", df["position_x"])
# positions in the lab frame, by adding the reference particle position
ref = pc.ref
x_lab = ref.x + df["position_x"]
To gather particles from all MPI ranks onto the root rank, pass local=False:
df_global = pc.to_df(local=False)
# df_global is non-None only on the root rank
The impactx.ImpactXParIter iterator gives direct, zero-copy access to the
Struct-of-Arrays storage of each tile on each mesh-refinement level. This avoids the copies
performed by to_df and is the right choice for performance-critical analysis or in-place
modification of the beam.
from impactx import ImpactXParIter
pc = sim.beam
for lvl in range(pc.finest_level + 1):
for pti in ImpactXParIter(pc, level=lvl):
soa = pti.soa().to_xp() # NumPy (CPU) or CuPy (GPU)
x = soa.real["position_x"]
px = soa.real["momentum_x"]
# in-place modification works:
x[:] += 1.0e-6 # shift every particle by 1 µm
Inside a impactx.elements.Programmable beam_particles callback, the
framework hands you a single pti directly — no need to construct the iterator yourself.
See Callback Functions for a full example.
Reference particle
The reference particle is accessed via pc.ref and
returns a impactx.RefPart. It exposes the integrated path length s, the lab-frame
position/momentum (x, y, z, px, py, pz), the time-of-flight t, the
energy-related quantities (pt, beta, gamma, beta_gamma), and the species mass/charge.
ref = sim.beam.ref
print(f"s = {ref.s:.3f} m, gamma = {ref.gamma:.3f}")
When using Programmable with separate ref_particle and
beam_particles callbacks, the reference particle is pushed before the beam particles,
so inside the beam_particles callback refpart already reflects the updated reference
state for the current slice.
Adding new particles
New particles can be appended to the beam at any time after init_grids()
using add_n_particles(). See its API reference for parameters.
See also
Extend a Simulation with Python: where and when to install callbacks.
Accessing Field Data: how to access the space-charge fields.
Data Analysis: offline analysis from the openPMD output of the
BeamMonitor.