Aperture and losses¶
Where in the lattice particles are lost, and against what pipe boundary. Critical for high-power machines (PIP-II at 1.6 MW target) where a 0.1% loss in the wrong place causes unmaintainable activation.
Aperture profile (pipe boundary vs s)¶
aperture_profile() walks the lattice and returns the beam-pipe
half-widths as piecewise step arrays — made for overlaying on
envelope plots:
from linac_gen.analysis.aperture_profile import aperture_profile
from linac_gen.core.lattice import Lattice
from linac_gen.elements.aperture import Aperture
from linac_gen.elements.drift import Drift
# A toy beamline with a tight collimator in the middle:
lattice = Lattice()
lattice.add(Drift(name="D1", length=300.0, aperture=15.0))
lattice.add(Aperture(name="COLL", dx=2.0, dy=2.0, aperture_type=1))
lattice.add(Drift(name="D2", length=300.0, aperture=15.0))
s_mm, rx_mm, ry_mm = aperture_profile(lattice)
# Overlay the pipe radius on the envelope of a quick run:
from linac_gen.core.config import BeamConfig
from linac_gen.core.simulation import Simulation
from linac_gen.distributions.factory import create_beam
beam0 = create_beam(BeamConfig(n_particles=1000), seed=2)
results = Simulation(lattice, beam0).run()
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot(results.s, results.sigma_x, label="σ_x [mm]")
ax.step(s_mm, rx_mm, where="post", color="k", label="pipe rx [mm]")
ax.set_xlabel("s [mm]"); ax.set_ylabel("mm"); ax.legend()
plt.close(fig)
Two aperture sources are honoured: the element's constant
aperture (and optional aperture_y for rectangular pipes), and —
for FieldMap elements parsed with Ka=1 and a .ouv file — the
per-z pipe_radius_profile. Zero-length elements and elements with
aperture <= 0 are skipped so they don't pull the boundary to zero.
For a circular pipe rx == ry.
How loss is detected and recorded¶
During multi-particle tracking, any particle exceeding the local
aperture is marked dead via Beam.record_loss(particle_id, s,
element_name). Each loss is logged once (no duplicates) with the
particle's position and energy at the moment of loss. Inside a
field-free drift the moment of loss is exact when
StepConfig.drift_single_push is on (the default): the crossing of
the straight-line trajectory with the pipe is solved analytically, so
s is the crossing point and x, y lie on the wall; a particle that
is already outside the pipe when it enters the drift is recorded at
the entrance. With the option off, losses inside drifts are recorded
at the end of the sub-step bundle that first saw the particle outside
(up to one bundle, 20 mm at PARTRAN_STEP 100 50, past the true
crossing). Losses in every other element are recorded at the
element's sub-step boundaries as before.
The full log is available after the run as Beam.loss_table — a
structured numpy array with fields particle_id, s, x, y,
energy, element_name:
from linac_gen.core.config import BeamConfig
from linac_gen.core.simulation import Simulation
from linac_gen.distributions.factory import create_beam
# Track a beam that is wider than the 2 mm collimator above, so some
# particles actually get lost:
beam = create_beam(BeamConfig(n_particles=2000, beta_x=8.0, beta_y=8.0),
seed=9)
Simulation(lattice, beam).run()
table = beam.loss_table
for row in table[:5]:
print(f" particle {row['particle_id']} lost at s={row['s']:.0f} mm "
f"in {row['element_name']} (x={row['x']:.2f}, y={row['y']:.2f})")
The recorder's transmission array (per-step live-particle
percentage) gives the cumulative view of the same information —
plot 100 - transmission for cumulative loss vs s.
Loss-per-element table¶
from collections import Counter
losses_by_elem = Counter(beam.loss_table["element_name"])
for elem, count in losses_by_elem.most_common(10):
print(f" {elem:20s} {count} particle(s) lost")
GUI¶
In the Results tab, the envelope plots have a Show aperture
checkbox that overlays the aperture_profile() boundary on σ_x/σ_y.
The Loss tile plots cumulative loss vs s from the transmission
record, and the aperture-losses popup overlays each loss_table
hit as a scatter point on the pipe profile. (Envelope mode tracks
no losses — transmission is implicitly 100 % there.)
Engineering decisions¶
| Loss profile shape | Diagnosis |
|---|---|
| Cliff edge at one element | aperture too tight there → loosen, or filter beam upstream |
| Slow ramp throughout | accumulating halo growth → upstream matching issue |
| Localised spike at FREQ jump | bunch length convention issue |
| Cumulative > 1% | reconsider design |
Watt/m budget¶
Translating particle counts into power deposition:
For PIP-II, the budget is ~1 W/m everywhere except the dump. See H⁻ stripping for the analytic loss-rate analyzers (used alongside aperture loss).
Cross-references¶
- Halo analysis
- H⁻ stripping
- Aperture element
linac_gen/analysis/aperture_profile.py:43—aperture_profile().linac_gen/core/beam.py:105—record_loss/loss_table.linac_gen/tracking/tracker.py—Tracker._freeze_analytic_drift_losses(the straight-line crossing inside a drift).