Skip to content

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:

\[ P/L\,[\text{W/m}] = \text{(loss fraction per metre)} \cdot \text{(beam power)} \]

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:43aperture_profile().
  • linac_gen/core/beam.py:105record_loss / loss_table.
  • linac_gen/tracking/tracker.pyTracker._freeze_analytic_drift_losses (the straight-line crossing inside a drift).

Halo · Continue to H⁻ stripping →