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:

\[\begin{split}\Delta p_x &= \frac{q^2 e^2}{m_0 \gamma \beta_0^2 c^2} \sum_{i,j,k,l \ge 0} x^k y^l \int_{-\infty}^{\infty} \bar{x}^i(z')\,\bar{y}^j(z')\,\lambda(z')\,W^{i,j,k,l}_{x}(z - z')\,dz' \\ \Delta p_y &= \frac{q^2 e^2}{m_0 \gamma \beta_0^2 c^2} \sum_{i,j,k,l \ge 0} x^k y^l \int_{-\infty}^{\infty} \bar{x}^i(z')\,\bar{y}^j(z')\,\lambda(z')\,W^{i,j,k,l}_{y}(z - z')\,dz'\end{split}\]

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:

\[\Delta \delta = -\frac{q^2 e^2}{m_0 \gamma \beta_0^2 c^2} \int_{-\infty}^{\infty} \lambda(z')\, W_s(z - z')\,dz' ~,\]

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

longitudinal

z

(0, 0)

(0, 0)

longitudinal

constant_x

x

(0, 0)

(0, 0)

constant x

constant_y

y

(0, 0)

(0, 0)

constant y

dipolar_x

x

(1, 0)

(0, 0)

dipolar / driving x

dipolar_y

y

(0, 1)

(0, 0)

dipolar / driving y

dipolar_xy

x

(0, 1)

(0, 0)

dipolar / driving xy

dipolar_yx

y

(1, 0)

(0, 0)

dipolar / driving yx

quadrupolar_x

x

(0, 0)

(1, 0)

quadrupolar / detuning x

quadrupolar_y

y

(0, 0)

(0, 1)

quadrupolar / detuning y

quadrupolar_xy

x

(0, 0)

(0, 1)

quadrupolar / detuning xy

quadrupolar_yx

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 meters

  • num_turns and circumference: 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)