Skip to content

[WIP] Add an ImpactX interface - #158

Open
ax3l wants to merge 5 commits into
ChristopherMayes:masterfrom
ax3l:add-interface-impactx
Open

[WIP] Add an ImpactX interface#158
ax3l wants to merge 5 commits into
ChristopherMayes:masterfrom
ax3l:add-interface-impactx

Conversation

@ax3l

@ax3l ax3l commented Sep 2, 2026

Copy link
Copy Markdown

ImpactX is an s-based beam dynamics code, the successor of IMPACT-Z, and there is currently no interface for it here. This adds one alongside the existing astra/bmad/elegant/gpt/impact interfaces.

Marked WIP: I would like feedback on the API choices below before this is considered for merging.

What's in it

  • beamphysics/interfaces/impactx.py — converters in both directions, plus a reader for ImpactX' openPMD BeamMonitor output. openpmd-api is imported lazily, so the module costs nothing unless the reader is used, and nothing here needs ImpactX installed.
  • ParticleGroup.to_impactx() / .from_impactx() — mirroring to_bmad/from_bmad.
  • Tests, plus a small BeamMonitor fixture (177 KB) written by ImpactX 26.08, with the generate.py that produced it beside the data — the same arrangement as docs/examples/data/genesis4/run.sh. This lets the reader be covered in CI without ImpactX.
  • openpmd-api added to environment.yml and the dev extra, without which the reader tests would silently skip.

Coordinates

ImpactX holds particles at fixed s, so the result is in z-coordinates — all z equal, the bunch length a spread in t — the same convention bmad.py uses ("Zero by definition in z-coordinates"). Bmad is the closest existing analogue, and the signatures follow it.

The transverse coordinates stay in the local frame relative to the reference particle: adding x_ref would be wrong wherever the reference orbit bends, since local x is then not lab x. Time is absolute (t = t_ref + position_t/c), which is unambiguous and is what openPMD's position/t + positionOffset/t means in ImpactX output. ImpactXRefPart carries the lab-frame reference particle for anyone who needs to place the bunch.

ImpactX specifics handled rather than papered over

  • t is c·t in metres in ImpactX; qm is reported in different units depending on how the particles were inserted, so it is never trusted on read — the species comes from the reference particle.
  • The openPMD id record holds AMReX' packed idcpu. Its id field comes from a per-rank counter, so it repeats across ranks and is not unique on its own; the whole value becomes the ParticleGroup id, with only the validity bit moved into status (which also makes it fit in the signed 64-bit integer ids are stored in — the raw value overflows silently).
  • Every particle in a particles_lost file is lost by construction even though AMReX marks them valid, so they are not reported as alive.
  • Spin and runtime per-particle components have no ParticleGroup representation. The reader refuses rather than returning a silently zeroed bunch; strict=False opts out with a warning.

Verification

Against a live ImpactX 26.08 run: a bunch injected via add_n_particles and read back out of the monitor file matches to 2e-16 of the spread in x, y and t and 2.4e-13 in pz, with identical emittances. The reader reproduces ImpactX' own reduced beam characteristics (sigma_x/y/t, mean_*, charge_C) to 1e-12, and the emittance to 1e-9 once the n/(n-1) difference between this package's bias-corrected covariance and ImpactX' population moments is accounted for.

Questions for review

  1. ParticleGroup cannot carry extra per-particle arrays. ImpactX has spin_x/y/z and arbitrary runtime components; bmad.py's write_bmad carries a TODO: Spin for the same reason. A general mechanism on ParticleGroup would close this for every s-based code at once. Worth a conversation — this interface could then carry them instead of refusing.
  2. UnrepresentableParticleData subclasses NotImplementedError. A ValueError subclass may read better for what is really a data-validation failure. Happy to change.
  3. Status code for lost particles. ParticleStatus has only CATHODE = 0 and ALIVE = 1, and each interface passes its own source code's value through, so there is no universal "lost" value. This uses 2; 0 is ruled out because astra.py writes status == 0 back out as Astra's -1, "at the cathode". Let me know if you would rather see a member added to the enum.
  4. Docs. No interface currently has a docs/api/*.md page, so I did not add one — but from_impactx's docstring points at beamphysics.interfaces.impactx, which is not rendered anywhere in the built site. A docs/api/interfaces.md page, or a cell in read_examples.ipynb, would fix that if you want it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DhbdaEDSBqFN974t9QcXem

ax3l and others added 5 commits September 1, 2026 22:55
ImpactX is an s-based beam dynamics code, the successor of IMPACT-Z, and
openPMD-beamphysics had no interface for it. This adds one alongside the
existing astra/bmad/elegant/gpt/impact interfaces:

- beamphysics/interfaces/impactx.py: converters in both directions, plus a
  reader for ImpactX' openPMD BeamMonitor output that needs no ImpactX
  installation (openpmd-api is imported lazily).
- ParticleGroup.to_impactx() / .from_impactx(), mirroring to_bmad/from_bmad.
- Tests, and a small BeamMonitor fixture written by ImpactX 26.08 with the
  generate.py script beside it, so the reader is covered in CI without ImpactX.

Coordinates: ImpactX holds particles at fixed s, so the result is in
z-coordinates -- all z equal, the bunch length a spread in t -- as bmad.py
does. The transverse coordinates stay in the local frame relative to the
reference particle, since adding x_ref would be wrong wherever the reference
orbit bends, while t is absolute lab time, which is unambiguous.

A few ImpactX specifics are handled rather than papered over: t is c*t in
metres there; qm is reported in different units depending on how the particles
were inserted, so it is never trusted on read; the openPMD id record holds
AMReX' packed idcpu, whose per-rank id field alone is not unique, so the whole
value becomes the ParticleGroup id with only the validity bit moved to status;
and every particle in a particles_lost file is lost by construction even
though AMReX marks them valid, so they are not reported as alive. Spin and
runtime per-particle components have no ParticleGroup representation and are
refused rather than silently dropped, with strict=False to opt out.

Verified against a live ImpactX 26.08 run: a bunch injected through
add_n_particles and read back out of the monitor file matches to 2e-16 of the
spread in x, y and t and 2.4e-13 in pz, with identical emittances; and the
reader reproduces ImpactX' own reduced beam characteristics to 1e-12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhbdaEDSBqFN974t9QcXem
The module docstring had no link for lume-impactx (I had removed a guessed
one) and named only the ImpactX documentation site. Add both source
repositories: ImpactX is BLAST-ImpactX/impactx and lume-impactx is
ax3l/lume-impactx. impactx.readthedocs.io stays as the documentation link,
confirmed canonical by ImpactX' own README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhbdaEDSBqFN974t9QcXem
BLAST-ImpactX/impactx#1647 hands the beam's live reference particle to the
lost-particle container, so particles_lost.* will carry a usable one instead
of a default-constructed RefPart. Three consequences here:

- The "zeroed reference particle" error now says which ImpactX versions it
  applies to, rather than presenting it as how ImpactX behaves.
- The ref= caveat generalizes. It was written as a cost of *supplying* a
  reference particle from a monitor; it is really intrinsic. The momenta are
  normalized by beta_gamma at the reference particle's own s, so wherever the
  reference energy changes between the loss point and the reference particle
  in hand, the conversion is approximate. That is the same limitation ImpactX
  documents for the reference particle it now stores: the invariants (mass,
  charge) describe lost particles exactly, the kinematic attributes are
  end-of-tracking values.
- The test asserted the fixture has a zeroed reference particle, which would
  have failed the day someone regenerated it against a newer ImpactX. It now
  branches on what the file actually holds and asserts the right behaviour for
  either.

The lost-file detection needs no change: it keys on the s_lost record, which
CollectLost always adds, and only falls back to the zeroed reference particle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhbdaEDSBqFN974t9QcXem
The ref= docstring claimed that ImpactX stores mass and charge as invariants
and the kinematic attributes as end-of-tracking values for lost particles.
That is the current shape of BLAST-ImpactX/impactx#1647, which is still in
flux -- what to key on, and whether final state is even the right thing to
store, is undecided.

Keep only the part that does not depend on how that lands: a lost particle
converts exactly when the reference energy did not change between the loss
point and the reference particle in hand, and this reader applies no
correction of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhbdaEDSBqFN974t9QcXem
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant