Tracking with collective elements

A collective beam element is an element that needs access to the entire particle set (in read and/or write mode). The following example shows how to handle such elements in Xsuite.

Example

A typical example of collective element is a space-charge interaction. We can create a space-charge beam element as follows:

import xobjects as xo
import xfields as xf

context = xo.ContextCpu()

spcharge = xf.SpaceChargeBiGaussian(_context=context,
    update_on_track = ['sigma_x', 'sigma_y'], length=2,
    longitudinal_profile=xf.LongitudinalProfileQGaussian(
        _context=context, number_of_particles=1e11, sigma_z=0.2))

This creates a space-charge element where the transverse beam sizes are updated based on the particle set at each interaction. Such an element can be included in an xtrack tracker similarly to single-particle elements.

import xtrack as xt
import xpart as xp

## Generate a simple beam line including the spacecharge element
myqf = xt.Multipole(knl=[0, 1.])
myqd = xt.Multipole(knl=[0, -1.])
mydrift = xt.Drift(length=1.)
line = xt.Line(
    elements = [myqf, mydrift, myqd, mydrift,
                spcharge,
                myqf, mydrift, myqd, mydrift,],
    element_names = ['qf1', 'drift1', 'qd1', 'drift2',
                        'spcharge'
                        'qf2', 'drift3', 'qd2', 'drift4'])

## Transfer lattice on context and compile tracking code
tracker = line.build_tracker(_context=context)

## Build particle object on context
n_part = 200
particles = xp.Particles(_context=context,
                        p0c=6500e9,
                        x=np.random.uniform(-1e-3, 1e-3, n_part),
                        px=np.random.uniform(-1e-5, 1e-5, n_part),
                        y=np.random.uniform(-2e-3, 2e-3, n_part),
                        py=np.random.uniform(-3e-5, 3e-5, n_part),
                        zeta=np.random.uniform(-1e-2, 1e-2, n_part),
                        delta=np.random.uniform(-1e-4, 1e-4, n_part),
                        )

## Track (saving turn-by-turn data)
n_turns = 100
tracker.track(particles, num_turns=n_turns,
            turn_by_turn_monitor=True)

How does it work?

To decide whether or not an element needs to be treated as collective, the tracker inspects its iscollective attribute. In our example:

print(qf.iscollective)
# Gives "False"

print(spcharge.iscollective)
# Gives "True"

Based in this information the line is divided in parts that are either collective elements or xtrack trackers simulating groups of consecutive non-collective elements.

We can visualize this in our example:

print(tracker._parts)
# Gives:
# [<xtrack.tracker.Tracker object at 0x7f5ba8ce7760>,
#  <xfields.beam_elements.spacecharge.SpaceChargeBiGaussian object at 0x7f5ba8e1bd30>,
#  <xtrack.tracker.Tracker object at 0x7f5ba8ce7610>]

where the first part tracks the particles through to the first potion of the machine up to the space-charge element, the second part simulates the space-charge interaction, the third part tracks the particles from the space-charge element to the end of the line.

As all xsuite and xsuite-compatible beam elements need to expose a .track method the instruction:

tracker.track(particles)

is equivalent to the loop:

for pp in tracker._parts:
    pp.track(particles)

Any python object exposing a ‘.track’ method can be used as beam_element. If the attribute iscollective is not present the element is handled as collective.