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_t is \(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