Xwakes: wakefields and impedances
xwakes provides wakefield and impedance elements that plug into the
Xsuite tracking environment. It offers analytic resonator wakes, wakes read from
tables, multi-bunch/multi-turn support, transverse damping, monitoring, and
MPI pipeline wiring. The list of available wakefield elements is available in
the API reference.
Quick start: resonator wake on a single bunch
import numpy as np
import xtrack as xt
import xwakes as xw
# Build a wake as the sum of dipolar and quadrupolar resonators
wf_dip_1 = xw.WakeResonator(kind='dipolar_x', r=1e8, q=1e5, f_r=1e3)
wf_dip_2 = xw.WakeResonator(kind='dipolar_x', r=5e7, q=5e4, f_r=5e2)
wf_quad_1 = xw.WakeResonator(kind='quadrupolar_x', r=2e7, q=8e4, f_r=2e3)
wf_quad_2 = xw.WakeResonator(kind='quadrupolar_y', r=3e7, q=6e4, f_r=1.5e3)
wf = wf_dip_1 + wf_dip_2 + wf_quad_1 + wf_quad_2
# Configure for tracking: zeta range and number of slices
wf.configure_for_tracking(
zeta_range=(-0.1, 0.1), # meters
num_slices=100
)
# Simple accelerator lattice: one turn map plus wake
one_turn = xt.LineSegmentMap(length=26000, betx=50., bety=40., qx=62.28, qy=62.31,
longitudinal_mode='linear_fixed_qs', qs=1e-3, bets=100)
line = xt.Line(elements=[one_turn, wf], element_names=['one_turn', 'wake'])
line.set_particle_ref('proton', p0c=7e12)
# One particle per slice, give it an offset, track once and inspect the kick
particles = line.build_particles(
x=0, px=0, y=0, py=0,
zeta=wf.slicer.zeta_centers.flatten(),
)
particles.x += 1e-3 # mm-level offset
# Track one turn
line.track(particles, num_turns=1)
configure_for_tracking prepares the internal slicer and wake tracker
(zeta_range, num_slices) and must be called before tracking. Summing
wakes works naturally: wf_total = wf1 + wf2 + wf3; the combined wake is
configured once and used like a single element.
Wakefield definitions
Transverse wakefields are defined such that the transverse kicks are given by:
where \(\bar{x}(z)\) and \(\bar{y}(z)\) are the transverse centroids, and \(\lambda(z)\) is the line density. The exponents \((i,j)\) belong to the source moments, while \((k,l)\) apply to the test particle offsets.
Longitudinal kicks are defined so that the energy momentum deviation change is:
with the sign convention that a positive wake causes energy loss.
Each predefined kind maps to a plane and to a set of source exponents
\((i,j)\) and test exponents \((k,l)\), following \(W^{i,j,k,l}\) in
the formulas above. The available kinds are listed below:
kind |
plane |
source_exponents |
test_exponents |
meaning |
|---|---|---|---|---|
|
z |
(0, 0) |
(0, 0) |
longitudinal |
|
x |
(0, 0) |
(0, 0) |
constant x |
|
y |
(0, 0) |
(0, 0) |
constant y |
|
x |
(1, 0) |
(0, 0) |
dipolar / driving x |
|
y |
(0, 1) |
(0, 0) |
dipolar / driving y |
|
x |
(0, 1) |
(0, 0) |
dipolar / driving xy |
|
y |
(1, 0) |
(0, 0) |
dipolar / driving yx |
|
x |
(0, 0) |
(1, 0) |
quadrupolar / detuning x |
|
y |
(0, 0) |
(0, 1) |
quadrupolar / detuning y |
|
x |
(0, 0) |
(0, 1) |
quadrupolar / detuning xy |
|
y |
(0, 0) |
(1, 0) |
quadrupolar / detuning yx |
Wakefield objects can be initialized in different ways, as illustrated by the following examples.
- Single component
# Horizontal dipolar resonator w1 = xw.WakeResonator(kind='dipolar_x', r=1e8, q=1e5, f_r=1e9)
- Multiple components (list/tuple)
# Horizontal + vertical dipolar with same r/q/f_r w2 = xw.WakeResonator(kind=['dipolar_x', 'dipolar_y'], r=1e8, q=1e5, f_r=1e9)
- Weighted components (dict)
# Scale horizontal twice as strong as vertical w3 = xw.WakeResonator(kind={'dipolar_x': 2.0, 'dipolar_y': 1.0}, r=1e8, q=1e5, f_r=1e9)
- Using Yokoya factors
# Flat chamber (horizontal) yokoya factors expanded into the right components w4 = xw.WakeResonator(kind=xw.Yokoya('flat_horizontal'), r=1e8, q=1e5, f_r=1e9)
- Custom polynomial term
# Custom plane/exponents without a predefined kind entry w5 = xw.WakeResonator( plane='y', source_exponents=(1, 0), # x_source^1 y_source^0 test_exponents=(0, 2), # x_test^0 y_test^2 r=1e8, q=1e5, f_r=1e9)
- Combine and configure
w = w1 + w2 + w3.components[0] # mix whole wakes and single components w.configure_for_tracking(zeta_range=(-0.1, 0.1), num_slices=200)
Building wakes from tables
The class WakeFromTable builds wakefields from tabulated data in the time domain.
Columns must correspond to the wakefield kinds defined above. The function
xw.read_headtail_file can be used to read HEADTAIL-format files, as illustrated
in the following examples.
import pathlib
import xwakes as xw
test_data = pathlib.Path(__file__).parent / 'test_data' / 'HLLHC_wake.dat'
columns = ['time', 'longitudinal', 'dipolar_x', 'dipolar_y',
'quadrupolar_x', 'quadrupolar_y']
table = xw.read_headtail_file(test_data, columns)
# use only dipolar terms
wf = xw.WakeFromTable(table, columns=['dipolar_x', 'dipolar_y'])
# Configure for tracking
wf.configure_for_tracking(zeta_range=(-0.4, 0.4), num_slices=100)
# Track as usual in a line
# ...
Defining custom wakes
For forms not covered by the built-in components, you can build a Component directly
from a wake callable in the time domain. Provide the plane of the kick, and the polynomial
exponents applied to the source/test offsets (source_exponents for the source particle,
test_exponents for the particle being kicked). The wake callable receives time t in
seconds; enforce causality by zeroing t <= 0. Wrap one or more components in xw.Wake
and configure it like any other wake.
import numpy as np
import xwakes as xw
a, b, c = 1.0e9, 0.1e9, 2.0 # frequency, damping rate, amplitude
def wake_vs_t(t):
t = np.atleast_1d(t)
out = c * np.sin(a * t) * np.exp(-b * t)
out[t <= 0] = 0.0 # causal wake
return out
custom_component = xw.Component(
wake=wake_vs_t,
plane='y',
source_exponents=(2, 0),
test_exponents=(1, 1),
name="Example damped sine wake",
)
custom_wake = xw.Wake(components=[custom_component])
# Inspect the zeta-domain wake or combine with other components
zeta = np.linspace(-10, 10, 500)
values = custom_component.function_vs_zeta(zeta, beta0=0.7)
custom_wake.configure_for_tracking(zeta_range=(-0.1, 0.1), num_slices=200)
The snippet mirrors xwakes/examples/003_custom_wake.py; you can mix these components with
resonators or tables via custom_wake + other_wake.
Multi-bunch, multi-turn wakes
configure_for_tracking accepts multi-bunch/multi-turn options:
filling_scheme: array of 0/1 slots (length = number of RF buckets considered)bunch_spacing_zeta: spacing between buckets in metersnum_turnsandcircumference: enable multi-turn wake memory
Example (two bunches, one-turn memory):
import numpy as np
import xwakes as xw
import xpart as xp
import xtrack as xt
filling_scheme = np.zeros(3564, dtype=int)
filling_scheme[0] = filling_scheme[1] = 1
wf = xw.WakeResonator(kind='dipolar_x', r=1e8, q=1e5, f_r=600e6)
wf.configure_for_tracking(
zeta_range=(-0.2, 0.2),
num_slices=200,
filling_scheme=filling_scheme,
bunch_spacing_zeta=26658.8832 / 3564,
num_turns=1,
circumference=26658.8832,
)
# Simple one-turn map and line
one_turn = xt.LineSegmentMap(
length=26658.8832, betx=70., bety=80., qx=62.31, qy=60.32,
longitudinal_mode='nonlinear', qs=2e-3, bets=731.27)
line = xt.Line([one_turn, wf], element_names=['one_turn', 'wake'])
line.particle_ref = xt.Particles(p0c=7e12)
# Generate a matched two-bunch beam and track
particles = xp.generate_matched_gaussian_multibunch_beam(
line=line, filling_scheme=filling_scheme,
bunch_num_particles=100_000, bunch_intensity_particles=2.3e11,
nemitt_x=2e-6, nemitt_y=2e-6, sigma_z=0.08,
bucket_length=26658.8832 / 35640, bunch_spacing_buckets=10,
)
line.track(particles, num_turns=10)