Skip to content

Wakefields

Resistive Wall Wakefield Classes

ResistiveWallWakefield dataclass

ResistiveWallWakefield(radius: float, conductivity: float, relaxation_time: float, geometry: Geometry = ROUND)

Bases: ResistiveWallWakefieldBase, ImpedanceWakefield


              flowchart TD
              beamphysics.wakefields.ResistiveWallWakefield[ResistiveWallWakefield]
              beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase[ResistiveWallWakefieldBase]
              beamphysics.wakefields.impedance.ImpedanceWakefield[ImpedanceWakefield]
              beamphysics.wakefields.base.WakefieldBase[WakefieldBase]

                              beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase --> beamphysics.wakefields.ResistiveWallWakefield
                                beamphysics.wakefields.base.WakefieldBase --> beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase
                

                beamphysics.wakefields.impedance.ImpedanceWakefield --> beamphysics.wakefields.ResistiveWallWakefield
                                beamphysics.wakefields.base.WakefieldBase --> beamphysics.wakefields.impedance.ImpedanceWakefield
                



              click beamphysics.wakefields.ResistiveWallWakefield href "" "beamphysics.wakefields.ResistiveWallWakefield"
              click beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase href "" "beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase"
              click beamphysics.wakefields.impedance.ImpedanceWakefield href "" "beamphysics.wakefields.impedance.ImpedanceWakefield"
              click beamphysics.wakefields.base.WakefieldBase href "" "beamphysics.wakefields.base.WakefieldBase"
            

Accurate impedance-based resistive wall wakefield model.

This class computes the short-range resistive wall wakefield by numerically evaluating the full longitudinal impedance Z(k) and transforming to the wakefield W(z) via FFT. This approach captures all physical effects including the anomalous skin effect at high frequencies.

Physics Background

When a relativistic charged particle travels through a conducting beam pipe, electromagnetic fields penetrate into the conductor due to the finite conductivity. The skin depth δ decreases with frequency:

\[\delta = \sqrt{\frac{2}{\omega \mu_0 \sigma}}\]

At very high frequencies (relevant for short-range wakes), the electron mean free path becomes comparable to the skin depth, and the simple Ohmic model breaks down. The Drude model accounts for this through a frequency- dependent conductivity with relaxation time τ:

\[\sigma(k) = \frac{\sigma_0}{1 - ikc\tau}\]

The longitudinal impedance per unit length for a round pipe is:

\[Z(k) = \frac{Z_0}{2\pi a} \cdot \frac{\zeta(k)}{1 - i k a \zeta(k) / 2}\]

where ζ(k) is the surface impedance and a is the pipe radius.

The wakefield is obtained via cosine transform (for z ≤ 0):

\[W(z) = \frac{2c}{\pi} \int_0^{\infty} \text{Re}[Z(k)] \cos(kz) \, dk\]
Characteristic Scales
  • s₀: Characteristic length scale:

$\(s_0 = \left( \frac{2a^2}{Z_0 \sigma_0} \right)^{1/3}\)$

For copper with a = 2.5 mm, s₀ ≈ 8 µm.

  • W₀: Characteristic wake amplitude at z = 0:
  • Round: \(W_0 = c Z_0 / (\pi a^2)\)
  • Flat: \(W_0 = c Z_0 \pi / (16 a^2)\)
Geometry
  • Round (circular pipe): Closed-form impedance expression.
  • Flat (parallel plates): Requires numerical integration over transverse modes; more computationally expensive.

Parameters:

  • radius (float) –

    Radius of the beam pipe (m). For flat geometry, this is the half-gap.

  • conductivity (float) –

    Electrical DC conductivity of the wall material (S/m).

  • relaxation_time (float) –

    Drude-model relaxation time τ of the conductor (s). Typical values: Cu ≈ 27 fs, Al ≈ 8 fs.

  • geometry (str, default: ROUND ) –

    Geometry of the beam pipe: 'round' or 'flat'. Default is 'round'.

Attributes:

  • s0 (float) –

    Characteristic length scale (m)

  • W0 (float) –

    Characteristic wake amplitude (V/C/m)

Notes
  • This model is ~10-20× slower than ResistiveWallPseudomode but more accurate, especially for the first oscillation peak.
  • The FFT-based convolution uses O(N log N) complexity for density convolution, compared to O(N) for the pseudomode model.
  • For flat geometry, impedance calculation requires numerical integration and is significantly slower than round geometry.
See Also

ResistiveWallPseudomode : Fast approximate model using damped sinusoid

References

.. [1] K. Bane and G. Stupakov, "Resistive wall wakefield in the LCLS undulator beam pipe," SLAC-PUB-10707 (2004). https://www.slac.stanford.edu/cgi-wrap/getdoc/slac-pub-10707.pdf

.. [2] A. Chao, "Physics of Collective Beam Instabilities in High Energy Accelerators," Wiley, 1993, Chapter 2.

Examples:

>>> wake = ResistiveWallWakefield.from_material(
...     "copper-slac-pub-10707", radius=2.5e-3, geometry="round"
... )
>>> wake.wake(-10e-6)  # Wake at 10 µm behind source
>>> wake.impedance(1e5)  # Impedance at k = 100/mm

Methods:

  • __call__ –

    Evaluate the wakefield at position z (convenience method).

  • convolve_density –

    Compute integrated wakefield from a charge density distribution.

  • default_n_samples –

    Default number of uniform samples out to zmax.

  • from_material –

    Create a wakefield from a known material preset.

  • impedance –

    Evaluate the impedance at wavenumber k.

  • material_from_properties –

    Attempt to identify the material based on conductivity and relaxation time.

  • particle_kicks –

    Compute wakefield-induced energy kicks per unit length.

  • plot –

    Plot the resistive wall wakefield W(z).

  • plot_impedance –

    Plot the resistive wall impedance Z(k).

  • to_tabular –

    Resample this model onto a uniform TabularWakefield.

  • wake –

    Evaluate the wakefield at position z.

W0 property

W0

Characteristic wake amplitude W₀ at z=0 (V/C/m).

From SLAC-PUB-10707: - Round: W₀ = c·Z₀ / (π·a²) - Flat: W₀ = c·Z₀·π / (16·a²) = W₀_round · (π²/16)

The dimensionless scaled wake is Ŵ(z/s₀) = W(z) / W₀.

default_zmax property

default_zmax: float

Distance behind the source particle beyond which the wake is negligible (m).

\[z_{\max} = 100 \, s_0\]

The tail falls as |z|^(-3/2); at 100 s0 it is about 1e-04 of W0 for a round pipe.

k_max property writable

k_max: float

Maximum wavenumber for integration.

min_wavelength property

min_wavelength: float

Shortest length scale of the wake (m).

\[\lambda_{\min} = \frac{2 \pi s_0}{\sqrt{3}}\]

This is the period of the DC-conductivity wake of a round pipe.

s0 property

s0

Characteristic length scale s₀ of the resistive wall wakefield (m).

From SLAC-PUB-10707 Eq. (5):

\[s_0 = \left( \frac{2 a^2}{Z_0 \sigma_0} \right)^{1/3}\]

where a is the pipe radius (round) or half-gap (flat), Z₀ is the impedance of free space, and σ₀ is the DC conductivity.

__call__

__call__(z: float | ndarray) -> float | ndarray

Evaluate the wakefield at position z (convenience method).

Source code in beamphysics/wakefields/resistive_wall/base.py
721
722
723
def __call__(self, z: float | np.ndarray) -> float | np.ndarray:
    """Evaluate the wakefield at position z (convenience method)."""
    return self.wake(z)

convolve_density

convolve_density(density: ndarray, dz: float, offset: float = 0, include_self_kick: bool = True, plot: bool = False, ax=None) -> ndarray

Compute integrated wakefield from a charge density distribution.

This computes the causal wake potential:

\[V(z_i) = \sum_{j>i} Q_j \cdot W(z_j - z_i) + \frac{1}{2} Q_i \cdot W(0)\]

where only particles ahead (larger z index) contribute to the wake felt by each particle. This is mathematically a correlation, not a convolution, and is computed efficiently using FFT.

Parameters:

  • density (ndarray) –

    Charge density array (C/m). Index 0 is the tail (back), index n-1 is the head (front).

  • dz (float) –

    Grid spacing (m).

  • offset (float, default: 0 ) –

    Offset for the z coordinate (m). Default is 0.

  • include_self_kick (bool, default: True ) –

    Whether to include the half self-kick term. Default is True.

  • plot (bool, default: False ) –

    If True, plot the density profile and wake potential. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None and plot=True, creates a new figure.

Returns:

  • integrated_wake ( ndarray ) –

    Integrated longitudinal wakefield (V/m).

Source code in beamphysics/wakefields/base.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def convolve_density(
    self,
    density: np.ndarray,
    dz: float,
    offset: float = 0,
    include_self_kick: bool = True,
    plot: bool = False,
    ax=None,
) -> np.ndarray:
    """
    Compute integrated wakefield from a charge density distribution.

    This computes the causal wake potential:

    $$V(z_i) = \\sum_{j>i} Q_j \\cdot W(z_j - z_i) + \\frac{1}{2} Q_i \\cdot W(0)$$

    where only particles ahead (larger z index) contribute to the wake
    felt by each particle. This is mathematically a correlation, not
    a convolution, and is computed efficiently using FFT.

    Parameters
    ----------
    density : np.ndarray
        Charge density array (C/m). Index 0 is the tail (back),
        index n-1 is the head (front).
    dz : float
        Grid spacing (m).
    offset : float, optional
        Offset for the z coordinate (m). Default is 0.
    include_self_kick : bool, optional
        Whether to include the half self-kick term. Default is True.
    plot : bool, optional
        If True, plot the density profile and wake potential. Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None and plot=True, creates a new figure.

    Returns
    -------
    integrated_wake : np.ndarray
        Integrated longitudinal wakefield (V/m).
    """
    from scipy.signal import correlate

    n = len(density)
    charge = density * dz  # Convert to charge per bin (C)

    # Build causal wake array: W[k] = wake at lag k*dz
    # W[0] = W(0), W[1] = W(-dz), W[2] = W(-2*dz), ...
    z_wake = -np.arange(n) * dz + offset
    W_causal = self.wake(z_wake)

    # Correlation: result[i] = sum_k charge[i+k] * W[k]
    # This gives the wake from particles ahead (larger indices)
    corr_result = correlate(charge, W_causal, mode="full")

    # Extract the portion corresponding to non-negative lags
    # correlate output has length 2*n-1, with zero lag at index n-1
    integrated_wake = corr_result[n - 1 : 2 * n - 1]

    # Correlation includes full W[0] contribution from self
    # Remove it, then add half if self-kick is requested
    W0 = W_causal[0]
    integrated_wake = integrated_wake - charge * W0
    if include_self_kick:
        integrated_wake = integrated_wake + 0.5 * W0 * charge

    if plot:
        self._plot_convolve_density(density, dz, integrated_wake, ax=ax)

    return integrated_wake

default_n_samples

default_n_samples(zmax: float) -> int

Default number of uniform samples out to zmax.

\[n = \max\left( \left\lceil \frac{128 \, z_{\max}}{\lambda_{\min}} \right\rceil + 1, \; 4 \right)\]

Here lambda_min is min_wavelength, and 4 is the minimum for cubic interpolation.

Parameters:

  • zmax (float) –

    Largest distance behind the source particle (m).

Returns:

  • int –
Source code in beamphysics/wakefields/base.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def default_n_samples(self, zmax: float) -> int:
    """
    Default number of uniform samples out to zmax.

    $$n = \\max\\left( \\left\\lceil \\frac{128 \\, z_{\\max}}{\\lambda_{\\min}}
    \\right\\rceil + 1, \\; 4 \\right)$$

    Here lambda_min is `min_wavelength`, and 4 is the minimum for cubic
    interpolation.

    Parameters
    ----------
    zmax : float
        Largest distance behind the source particle (m).

    Returns
    -------
    int
    """
    n = int(np.ceil(SAMPLES_PER_WAVELENGTH * zmax / self.min_wavelength)) + 1

    return max(n, MIN_SAMPLES)

from_material classmethod

from_material(material: str, radius: float, geometry: Geometry | str = ROUND)

Create a wakefield from a known material preset.

Parameters:

  • material (str) –

    Material name. Must be in list(cls.MATERIALS)

  • radius (float) –

    Pipe radius (m)

  • geometry (str, default: ROUND ) –

    Geometry type: 'round' or 'flat'

Source code in beamphysics/wakefields/resistive_wall/base.py
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
@classmethod
def from_material(
    cls,
    material: str,
    radius: float,
    geometry: Geometry | str = Geometry.ROUND,
):
    """
    Create a wakefield from a known material preset.

    Parameters
    ----------
    material : str
        Material name. Must be in list(cls.MATERIALS)
    radius : float
        Pipe radius (m)
    geometry : str
        Geometry type: 'round' or 'flat'
    """
    if material not in cls.MATERIALS:
        raise ValueError(
            f"Unknown material {material!r}. Available: {list(cls.MATERIALS)}"
        )

    props = cls.MATERIALS[material]
    return cls(
        radius=radius,
        conductivity=props["conductivity"],
        relaxation_time=props["relaxation_time"],
        geometry=geometry,
    )

impedance

impedance(k)

Evaluate the impedance at wavenumber k.

Parameters:

  • k (float or ndarray) –

    Wavenumber (1/m).

Returns:

  • Z ( complex or ndarray ) –

    Impedance (Ohm/m).

Source code in beamphysics/wakefields/resistive_wall/impedance.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def impedance(self, k):
    """
    Evaluate the impedance at wavenumber k.

    Parameters
    ----------
    k : float or np.ndarray
        Wavenumber (1/m).

    Returns
    -------
    Z : complex or np.ndarray
        Impedance (Ohm/m).
    """
    if self.geometry == Geometry.ROUND:
        return longitudinal_impedance_round(
            k, self.radius, self.conductivity, self._ctau
        )
    else:  # FLAT
        return longitudinal_impedance_flat(
            k, self.radius, self.conductivity, self._ctau
        )

material_from_properties

material_from_properties(tol=0.01)

Attempt to identify the material based on conductivity and relaxation time.

Parameters:

  • tol (float, default: 0.01 ) –

    Relative tolerance for matching, default is 1% (0.01)

Returns:

  • material ( str or None ) –

    Name of the matched material, or None if no match is found.

Source code in beamphysics/wakefields/resistive_wall/base.py
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
def material_from_properties(self, tol=0.01):
    """
    Attempt to identify the material based on conductivity and relaxation time.

    Parameters
    ----------
    tol : float, optional
        Relative tolerance for matching, default is 1% (0.01)

    Returns
    -------
    material : str or None
        Name of the matched material, or None if no match is found.
    """
    from math import isclose

    for name, props in self.MATERIALS.items():
        cond_match = isclose(self.conductivity, props["conductivity"], rel_tol=tol)
        tau_match = isclose(
            self.relaxation_time, props["relaxation_time"], rel_tol=tol
        )
        if cond_match and tau_match:
            return name
    return None

particle_kicks

particle_kicks(z: ndarray, weight: ndarray, include_self_kick: bool = True, n_bins: int = None) -> ndarray

Compute wakefield-induced energy kicks per unit length.

Uses FFT-based density convolution with interpolation back to particle positions. This is O(N + M log M) where N is the number of particles and M is the number of grid points.

Parameters:

  • z (ndarray) –

    Particle positions (m).

  • weight (ndarray) –

    Particle charges (C).

  • include_self_kick (bool, default: True ) –

    Whether to include the self-kick term. Default is True.

  • n_bins (int, default: None ) –

    Number of bins for the density grid. Default is max(100, N//10).

Returns:

  • kicks ( ndarray ) –

    Energy kick per unit length at each particle (eV/m).

Source code in beamphysics/wakefields/impedance.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def particle_kicks(
    self,
    z: np.ndarray,
    weight: np.ndarray,
    include_self_kick: bool = True,
    n_bins: int = None,
) -> np.ndarray:
    """
    Compute wakefield-induced energy kicks per unit length.

    Uses FFT-based density convolution with interpolation back to
    particle positions. This is O(N + M log M) where N is the number
    of particles and M is the number of grid points.

    Parameters
    ----------
    z : np.ndarray
        Particle positions (m).
    weight : np.ndarray
        Particle charges (C).
    include_self_kick : bool, optional
        Whether to include the self-kick term. Default is True.
    n_bins : int, optional
        Number of bins for the density grid. Default is max(100, N//10).

    Returns
    -------
    kicks : np.ndarray
        Energy kick per unit length at each particle (eV/m).
    """
    from scipy.interpolate import interp1d

    z = np.asarray(z)
    weight = np.asarray(weight)
    n = len(z)

    if n_bins is None:
        n_bins = max(100, n // 10)

    # Bin particles into density distribution
    z_min, z_max = z.min(), z.max()
    z_range = z_max - z_min
    if z_range == 0:
        z_range = 1e-6  # Avoid division by zero for single particle

    # Add padding to avoid edge effects
    padding = 0.1 * z_range
    z_min_padded = z_min - padding
    z_max_padded = z_max + padding

    dz = (z_max_padded - z_min_padded) / n_bins
    z_grid = np.linspace(z_min_padded + dz / 2, z_max_padded - dz / 2, n_bins)

    # Create density histogram (charge per bin)
    density, bin_edges = np.histogram(
        z, bins=n_bins, range=(z_min_padded, z_max_padded), weights=weight
    )
    # Convert to charge density (C/m)
    density = density / dz

    # Compute wake potential via FFT convolution
    wake_potential = self.convolve_density(
        density, dz, include_self_kick=include_self_kick
    )

    # Interpolate wake potential to particle positions
    interp = interp1d(
        z_grid, wake_potential, kind="linear", bounds_error=False, fill_value=0.0
    )
    kicks = -interp(z)  # Negative sign: energy loss for trailing particles

    return kicks

plot

plot(zmax=None, zmin=0, n=200, normalized=False, ax=None)

Plot the resistive wall wakefield W(z).

Parameters:

  • zmax (float, default: None ) –

    Maximum trailing distance (m). Defaults to 100 * s0.

  • zmin (float, default: 0 ) –

    Minimum trailing distance (m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • normalized (bool, default: False ) –

    If True, plot dimensionless Ŵ(z/s₀) = W(z)/W₀ vs z/s₀. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/resistive_wall/impedance.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def plot(self, zmax=None, zmin=0, n=200, normalized=False, ax=None):
    """
    Plot the resistive wall wakefield W(z).

    Parameters
    ----------
    zmax : float, optional
        Maximum trailing distance (m). Defaults to 100 * s0.
    zmin : float, optional
        Minimum trailing distance (m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    normalized : bool, optional
        If True, plot dimensionless Ŵ(z/s₀) = W(z)/W₀ vs z/s₀.
        Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if zmax is None:
        zmax = 20 * self.s0

    zlist = np.linspace(zmin, zmax, n)
    Wz = self.wake(-zlist)

    if ax is None:
        _, ax = plt.subplots()

    if normalized:
        ax.plot(zlist / self.s0, Wz / self.W0)
        ax.set_xlabel(r"$|z|/s_0$")
        ax.set_ylabel(r"$W(z)/W_0$")
    else:
        ax.plot(zlist * 1e6, Wz * 1e-12)
        ax.set_xlabel(r"Distance behind source $|z|$ (µm)")
        ax.set_ylabel(r"$W(z)$ (V/pC/m)")

plot_impedance

plot_impedance(kmax: float = None, kmin: float = 0, n: int = 500, ax=None)

Plot the resistive wall impedance Z(k).

Parameters:

  • kmax (float, default: None ) –

    Maximum wavenumber (1/m). Defaults to 10/s0.

  • kmin (float, default: 0 ) –

    Minimum wavenumber (1/m). Default is 0.

  • n (int, default: 500 ) –

    Number of points. Default is 500.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/resistive_wall/impedance.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def plot_impedance(
    self, kmax: float = None, kmin: float = 0, n: int = 500, ax=None
):
    """
    Plot the resistive wall impedance Z(k).

    Parameters
    ----------
    kmax : float, optional
        Maximum wavenumber (1/m). Defaults to 10/s0.
    kmin : float, optional
        Minimum wavenumber (1/m). Default is 0.
    n : int, optional
        Number of points. Default is 500.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if kmax is None:
        kmax = 10 / self.s0
    super().plot_impedance(kmax=kmax, kmin=kmin, n=n, ax=ax)

to_tabular

to_tabular(zmax: float | None = None, n: int | None = None) -> TabularWakefield

Resample this model onto a uniform TabularWakefield.

See TabularWakefield.from_wakefield.

Parameters:

  • zmax (float, default: None ) –

    Largest distance behind the source particle (m). Defaults to default_zmax.

  • n (int, default: None ) –

    Number of samples. Defaults to default_n_samples(zmax).

Returns:

Examples:

>>> wake = ResistiveWallPseudomode.from_material(
...     "copper-slac-pub-10707", radius=2.5e-3
... )
>>> table = wake.to_tabular()
Source code in beamphysics/wakefields/resistive_wall/base.py
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def to_tabular(
    self, zmax: float | None = None, n: int | None = None
) -> TabularWakefield:
    """
    Resample this model onto a uniform `TabularWakefield`.

    See `TabularWakefield.from_wakefield`.

    Parameters
    ----------
    zmax : float, optional
        Largest distance behind the source particle (m). Defaults to
        `default_zmax`.
    n : int, optional
        Number of samples. Defaults to `default_n_samples(zmax)`.

    Returns
    -------
    TabularWakefield

    Examples
    --------
    >>> wake = ResistiveWallPseudomode.from_material(
    ...     "copper-slac-pub-10707", radius=2.5e-3
    ... )
    >>> table = wake.to_tabular()
    """
    return TabularWakefield.from_wakefield(self, zmax=zmax, n=n)

wake

wake(z: ndarray | float, n_fft: int = 4096) -> ndarray | float

Evaluate the wakefield at position z.

If a wakefield function was provided, uses it directly. Otherwise, computes via cosine transform of Re[Z(k)]: - Scalars: numerical quadrature (accurate) - Arrays: FFT with interpolation (fast)

Parameters:

  • z (float or ndarray) –

    Longitudinal position (m).

  • n_fft (int, default: 4096 ) –

    Number of FFT points for array evaluation. Default is 4096.

Returns:

  • W ( float or ndarray ) –

    Wakefield value (V/C/m). Returns 0 for z > 0.

Source code in beamphysics/wakefields/impedance.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def wake(self, z: np.ndarray | float, n_fft: int = 4096) -> np.ndarray | float:
    """
    Evaluate the wakefield at position z.

    If a wakefield function was provided, uses it directly.
    Otherwise, computes via cosine transform of Re[Z(k)]:
    - Scalars: numerical quadrature (accurate)
    - Arrays: FFT with interpolation (fast)

    Parameters
    ----------
    z : float or np.ndarray
        Longitudinal position (m).
    n_fft : int, optional
        Number of FFT points for array evaluation. Default is 4096.

    Returns
    -------
    W : float or np.ndarray
        Wakefield value (V/C/m). Returns 0 for z > 0.
    """
    if self._wakefield_func is not None:
        return self._wakefield_func(z)

    z = np.asarray(z)
    scalar_input = z.ndim == 0
    z = np.atleast_1d(z)

    if scalar_input:
        # Single point: use quadrature for accuracy
        from scipy.integrate import quad

        z_val = float(z[0])
        if z_val > 0:
            return 0.0

        def integrand(k):
            return np.real(self._impedance_func(k)) * np.cos(k * z_val)

        result, _ = quad(integrand, 0, self._k_max, limit=200)
        return (2 * c_light / np.pi) * result
    else:
        # Array: use FFT for speed
        from scipy.fft import irfft
        from scipy.interpolate import interp1d

        k_max = self._k_max
        dk = k_max / (n_fft - 1)
        k_grid = np.linspace(0, k_max, n_fft)

        Zk_grid = self._impedance_func(k_grid)
        ReZ = np.real(Zk_grid)

        n_full = 2 * (n_fft - 1)
        dz = 2 * np.pi / (n_full * dk)
        z_grid = np.arange(n_full) * dz

        W_grid = irfft(ReZ, n=n_full) * n_full * dk * (c_light / np.pi)

        interp = interp1d(
            z_grid, W_grid, kind="cubic", bounds_error=False, fill_value=0.0
        )

        result = interp(-z)
        result = np.where(z > 0, 0.0, result)

        return result

ResistiveWallPseudomode dataclass

ResistiveWallPseudomode(radius: float, conductivity: float, relaxation_time: float, geometry: Geometry = ROUND)

Bases: ResistiveWallWakefieldBase, PseudomodeWakefield


              flowchart TD
              beamphysics.wakefields.ResistiveWallPseudomode[ResistiveWallPseudomode]
              beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase[ResistiveWallWakefieldBase]
              beamphysics.wakefields.pseudomode.PseudomodeWakefield[PseudomodeWakefield]
              beamphysics.wakefields.base.WakefieldBase[WakefieldBase]

                              beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase --> beamphysics.wakefields.ResistiveWallPseudomode
                                beamphysics.wakefields.base.WakefieldBase --> beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase
                

                beamphysics.wakefields.pseudomode.PseudomodeWakefield --> beamphysics.wakefields.ResistiveWallPseudomode
                                beamphysics.wakefields.base.WakefieldBase --> beamphysics.wakefields.pseudomode.PseudomodeWakefield
                



              click beamphysics.wakefields.ResistiveWallPseudomode href "" "beamphysics.wakefields.ResistiveWallPseudomode"
              click beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase href "" "beamphysics.wakefields.resistive_wall.base.ResistiveWallWakefieldBase"
              click beamphysics.wakefields.pseudomode.PseudomodeWakefield href "" "beamphysics.wakefields.pseudomode.PseudomodeWakefield"
              click beamphysics.wakefields.base.WakefieldBase href "" "beamphysics.wakefields.base.WakefieldBase"
            

Fast pseudomode-based resistive wall wakefield model.

This class models the short-range resistive wall wakefield using a damped sinusoidal "pseudomode" approximation. When a relativistic charged particle travels through a conducting beam pipe, it induces image currents in the pipe walls. Due to the finite conductivity of the wall material, these currents penetrate into the conductor and dissipate energy, creating a wakefield that acts on trailing particles.

Physics Background

The resistive wall impedance arises from the skin effect in the conducting walls. At high frequencies (short distances), the AC conductivity of metals deviates from DC behavior due to the Drude relaxation time τ:

\[\sigma(\omega) = \frac{\sigma_0}{1 - i\omega\tau}\]

This frequency dependence causes the wakefield to oscillate and decay exponentially behind the source particle. The wakefield can be well approximated by a single damped sinusoid (pseudomode):

\[W(z) = A \cdot e^{d \cdot z} \cdot \sin(k_r z + \phi) \quad (z \le 0)\]

where the parameters \(k_r\) (oscillation wavenumber) and \(Q_r\) (quality factor, related to decay rate \(d = k_r / 2Q_r\)) depend on the dimensionless relaxation parameter \(\Gamma = c\tau / s_0\).

Characteristic Scales
  • s₀: Characteristic length scale where the wake transitions from the \(1/\sqrt{z}\) DC behavior to oscillatory AC behavior:

$\(s_0 = \left( \frac{2a^2}{Z_0 \sigma_0} \right)^{1/3}\)$

  • Γ: Dimensionless relaxation parameter \(\Gamma = c\tau / s_0\). For copper, Γ ≈ 0.8; for aluminum, Γ ≈ 0.2.

  • W₀: Characteristic wake amplitude at z = 0:

  • Round: \(W_0 = c Z_0 / (\pi a^2)\)
  • Flat: \(W_0 = c Z_0 \pi / (16 a^2)\)
Algorithm

The pseudomode form enables an O(N) algorithm for computing particle kicks, compared to O(N²) or O(N log N) for general wakefields. This makes it suitable for multi-pass tracking simulations.

Parameters:

  • radius (float) –

    Radius of the beam pipe (m). For flat geometry, this is half the gap.

  • conductivity (float) –

    Electrical DC conductivity of the wall material (S/m).

  • relaxation_time (float) –

    Drude-model relaxation time τ of the conductor (s). Typical values: Cu ≈ 27 fs, Al ≈ 8 fs.

  • geometry (str, default: ROUND ) –

    Geometry of the beam pipe: 'round' or 'flat'. Default is 'round'.

Attributes:

  • s0 (float) –

    Characteristic length scale (m)

  • Gamma (float) –

    Dimensionless relaxation parameter Γ = cτ/s₀

  • kr (float) –

    Resonant wavenumber (1/m)

  • Qr (float) –

    Quality factor of the pseudomode

Notes
  • The pseudomode approximation has ~10-20% error compared to the full impedance model, primarily in the first oscillation peak.
  • The polynomial fits for k_r and Q_r are valid for Γ ≲ 3. A warning is issued for larger values.
  • Materials with known conductivity and τ values are available via from_material().
See Also

ResistiveWallWakefield : Accurate impedance-based model (slower)

References

.. [1] K. Bane and G. Stupakov, "Resistive wall wakefield in the LCLS undulator beam pipe," SLAC-PUB-10707 (2004). https://www.slac.stanford.edu/cgi-wrap/getdoc/slac-pub-10707.pdf

.. [2] D. Sagan, "Bmad Manual," Section 24.6: Short-Range Wakefields. https://www.classe.cornell.edu/bmad/manual.html

.. [3] A. Chao, "Physics of Collective Beam Instabilities in High Energy Accelerators," Wiley, 1993, Chapter 2.

Examples:

>>> wake = ResistiveWallPseudomode.from_material(
...     "copper-slac-pub-10707", radius=2.5e-3, geometry="round"
... )
>>> wake.wake(-10e-6)  # Wake at 10 µm behind source

Methods:

  • __call__ –

    Evaluate the wakefield at position z (convenience method).

  • convolve_density –

    Compute integrated wakefield from a charge density distribution.

  • default_n_samples –

    Default number of uniform samples out to zmax.

  • from_material –

    Create a wakefield from a known material preset.

  • impedance –

    Compute the impedance Z(k) analytically.

  • material_from_properties –

    Attempt to identify the material based on conductivity and relaxation time.

  • particle_kicks –

    Compute short-range wakefield energy kicks per unit length.

  • plot –

    Plot the resistive wall wakefield W(z).

  • plot_impedance –

    Plot the resistive wall impedance Z(k).

  • to_bmad –

    Export wakefield in Bmad format.

  • to_tabular –

    Resample this model onto a uniform TabularWakefield.

  • wake –

    Evaluate the pseudomode wakefield at position z.

Gamma property

Gamma

Dimensionless relaxation time Γ = c * τ / s₀.

Qr property

Qr

Dimensionless quality factor Q_r of the wakefield pseudomode.

W0 property

W0

Characteristic wake amplitude W₀ at z=0 (V/C/m).

From SLAC-PUB-10707: - Round: W₀ = c·Z₀ / (π·a²) - Flat: W₀ = c·Z₀·π / (16·a²) = W₀_round · (π²/16)

The dimensionless scaled wake is Ŵ(z/s₀) = W(z) / W₀.

decay_length property

decay_length: float

Envelope decay length of the most slowly damped mode (m).

\[L = \frac{1}{\min_i d_i}\]

Raises:

  • ValueError –

    If any mode has d <= 0, so that the wake does not decay.

default_zmax property

default_zmax: float

Distance behind the source particle beyond which the wake is negligible (m).

\[z_{\max} = 10 \, L\]

Here L is decay_length, so the envelope has fallen to exp(-10).

kr property

kr

Real-valued wave number k_r of the wakefield pseudomode (1/m).

min_wavelength property

min_wavelength: float

Shortest length scale of the wake (m).

\[\lambda_{\min} = \min\left( \frac{2 \pi}{\max_i |k_i|}, \frac{1}{\max_i d_i} \right)\]

The period of the fastest mode or the decay length of the most strongly damped mode, whichever is shorter.

Raises:

  • ValueError –

    If any mode has d <= 0.

modes property

modes: list

List of Pseudomode objects.

n_modes property

n_modes: int

Number of pseudomodes.

s0 property

s0

Characteristic length scale s₀ of the resistive wall wakefield (m).

From SLAC-PUB-10707 Eq. (5):

\[s_0 = \left( \frac{2 a^2}{Z_0 \sigma_0} \right)^{1/3}\]

where a is the pipe radius (round) or half-gap (flat), Z₀ is the impedance of free space, and σ₀ is the DC conductivity.

__call__

__call__(z: float | ndarray) -> float | ndarray

Evaluate the wakefield at position z (convenience method).

Source code in beamphysics/wakefields/resistive_wall/base.py
721
722
723
def __call__(self, z: float | np.ndarray) -> float | np.ndarray:
    """Evaluate the wakefield at position z (convenience method)."""
    return self.wake(z)

convolve_density

convolve_density(density: ndarray, dz: float, offset: float = 0, include_self_kick: bool = True, plot: bool = False, ax=None) -> ndarray

Compute integrated wakefield from a charge density distribution.

This computes the causal wake potential:

\[V(z_i) = \sum_{j>i} Q_j \cdot W(z_j - z_i) + \frac{1}{2} Q_i \cdot W(0)\]

where only particles ahead (larger z index) contribute to the wake felt by each particle. This is mathematically a correlation, not a convolution, and is computed efficiently using FFT.

Parameters:

  • density (ndarray) –

    Charge density array (C/m). Index 0 is the tail (back), index n-1 is the head (front).

  • dz (float) –

    Grid spacing (m).

  • offset (float, default: 0 ) –

    Offset for the z coordinate (m). Default is 0.

  • include_self_kick (bool, default: True ) –

    Whether to include the half self-kick term. Default is True.

  • plot (bool, default: False ) –

    If True, plot the density profile and wake potential. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None and plot=True, creates a new figure.

Returns:

  • integrated_wake ( ndarray ) –

    Integrated longitudinal wakefield (V/m).

Source code in beamphysics/wakefields/base.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def convolve_density(
    self,
    density: np.ndarray,
    dz: float,
    offset: float = 0,
    include_self_kick: bool = True,
    plot: bool = False,
    ax=None,
) -> np.ndarray:
    """
    Compute integrated wakefield from a charge density distribution.

    This computes the causal wake potential:

    $$V(z_i) = \\sum_{j>i} Q_j \\cdot W(z_j - z_i) + \\frac{1}{2} Q_i \\cdot W(0)$$

    where only particles ahead (larger z index) contribute to the wake
    felt by each particle. This is mathematically a correlation, not
    a convolution, and is computed efficiently using FFT.

    Parameters
    ----------
    density : np.ndarray
        Charge density array (C/m). Index 0 is the tail (back),
        index n-1 is the head (front).
    dz : float
        Grid spacing (m).
    offset : float, optional
        Offset for the z coordinate (m). Default is 0.
    include_self_kick : bool, optional
        Whether to include the half self-kick term. Default is True.
    plot : bool, optional
        If True, plot the density profile and wake potential. Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None and plot=True, creates a new figure.

    Returns
    -------
    integrated_wake : np.ndarray
        Integrated longitudinal wakefield (V/m).
    """
    from scipy.signal import correlate

    n = len(density)
    charge = density * dz  # Convert to charge per bin (C)

    # Build causal wake array: W[k] = wake at lag k*dz
    # W[0] = W(0), W[1] = W(-dz), W[2] = W(-2*dz), ...
    z_wake = -np.arange(n) * dz + offset
    W_causal = self.wake(z_wake)

    # Correlation: result[i] = sum_k charge[i+k] * W[k]
    # This gives the wake from particles ahead (larger indices)
    corr_result = correlate(charge, W_causal, mode="full")

    # Extract the portion corresponding to non-negative lags
    # correlate output has length 2*n-1, with zero lag at index n-1
    integrated_wake = corr_result[n - 1 : 2 * n - 1]

    # Correlation includes full W[0] contribution from self
    # Remove it, then add half if self-kick is requested
    W0 = W_causal[0]
    integrated_wake = integrated_wake - charge * W0
    if include_self_kick:
        integrated_wake = integrated_wake + 0.5 * W0 * charge

    if plot:
        self._plot_convolve_density(density, dz, integrated_wake, ax=ax)

    return integrated_wake

default_n_samples

default_n_samples(zmax: float) -> int

Default number of uniform samples out to zmax.

\[n = \max\left( \left\lceil \frac{128 \, z_{\max}}{\lambda_{\min}} \right\rceil + 1, \; 4 \right)\]

Here lambda_min is min_wavelength, and 4 is the minimum for cubic interpolation.

Parameters:

  • zmax (float) –

    Largest distance behind the source particle (m).

Returns:

  • int –
Source code in beamphysics/wakefields/base.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def default_n_samples(self, zmax: float) -> int:
    """
    Default number of uniform samples out to zmax.

    $$n = \\max\\left( \\left\\lceil \\frac{128 \\, z_{\\max}}{\\lambda_{\\min}}
    \\right\\rceil + 1, \\; 4 \\right)$$

    Here lambda_min is `min_wavelength`, and 4 is the minimum for cubic
    interpolation.

    Parameters
    ----------
    zmax : float
        Largest distance behind the source particle (m).

    Returns
    -------
    int
    """
    n = int(np.ceil(SAMPLES_PER_WAVELENGTH * zmax / self.min_wavelength)) + 1

    return max(n, MIN_SAMPLES)

from_material classmethod

from_material(material: str, radius: float, geometry: Geometry | str = ROUND)

Create a wakefield from a known material preset.

Parameters:

  • material (str) –

    Material name. Must be in list(cls.MATERIALS)

  • radius (float) –

    Pipe radius (m)

  • geometry (str, default: ROUND ) –

    Geometry type: 'round' or 'flat'

Source code in beamphysics/wakefields/resistive_wall/base.py
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
@classmethod
def from_material(
    cls,
    material: str,
    radius: float,
    geometry: Geometry | str = Geometry.ROUND,
):
    """
    Create a wakefield from a known material preset.

    Parameters
    ----------
    material : str
        Material name. Must be in list(cls.MATERIALS)
    radius : float
        Pipe radius (m)
    geometry : str
        Geometry type: 'round' or 'flat'
    """
    if material not in cls.MATERIALS:
        raise ValueError(
            f"Unknown material {material!r}. Available: {list(cls.MATERIALS)}"
        )

    props = cls.MATERIALS[material]
    return cls(
        radius=radius,
        conductivity=props["conductivity"],
        relaxation_time=props["relaxation_time"],
        geometry=geometry,
    )

impedance

impedance(k: ndarray | float) -> ndarray | complex

Compute the impedance Z(k) analytically.

Sums the contribution from each pseudomode using the analytic Fourier transform.

Parameters:

  • k (float or ndarray) –

    Wavenumber (1/m).

Returns:

  • Z ( complex or ndarray ) –

    Impedance (Ohm/m).

Source code in beamphysics/wakefields/pseudomode.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def impedance(self, k: np.ndarray | float) -> np.ndarray | complex:
    """
    Compute the impedance Z(k) analytically.

    Sums the contribution from each pseudomode using the analytic
    Fourier transform.

    Parameters
    ----------
    k : float or np.ndarray
        Wavenumber (1/m).

    Returns
    -------
    Z : complex or np.ndarray
        Impedance (Ohm/m).
    """
    k = np.asarray(k)
    scalar_input = k.ndim == 0
    k = np.atleast_1d(k)

    Z = np.zeros(len(k), dtype=complex)
    for mode in self._modes:
        Z += mode.impedance(k)

    if scalar_input:
        return complex(Z[0])
    return Z

material_from_properties

material_from_properties(tol=0.01)

Attempt to identify the material based on conductivity and relaxation time.

Parameters:

  • tol (float, default: 0.01 ) –

    Relative tolerance for matching, default is 1% (0.01)

Returns:

  • material ( str or None ) –

    Name of the matched material, or None if no match is found.

Source code in beamphysics/wakefields/resistive_wall/base.py
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
def material_from_properties(self, tol=0.01):
    """
    Attempt to identify the material based on conductivity and relaxation time.

    Parameters
    ----------
    tol : float, optional
        Relative tolerance for matching, default is 1% (0.01)

    Returns
    -------
    material : str or None
        Name of the matched material, or None if no match is found.
    """
    from math import isclose

    for name, props in self.MATERIALS.items():
        cond_match = isclose(self.conductivity, props["conductivity"], rel_tol=tol)
        tau_match = isclose(
            self.relaxation_time, props["relaxation_time"], rel_tol=tol
        )
        if cond_match and tau_match:
            return name
    return None

particle_kicks

particle_kicks(z: ndarray, weight: ndarray, include_self_kick: bool = True) -> ndarray

Compute short-range wakefield energy kicks per unit length.

Uses an O(N) single-pass algorithm for each mode by exploiting the exponential form of the pseudomode wakefield.

Parameters:

  • z (ndarray) –

    Particle positions (m).

  • weight (ndarray) –

    Particle charges (C).

  • include_self_kick (bool, default: True ) –

    If True, applies the self-kick. Default is True.

Returns:

  • kicks ( ndarray ) –

    Wake-induced energy kick per unit length (eV/m).

Source code in beamphysics/wakefields/pseudomode.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
def particle_kicks(
    self,
    z: np.ndarray,
    weight: np.ndarray,
    include_self_kick: bool = True,
) -> np.ndarray:
    """
    Compute short-range wakefield energy kicks per unit length.

    Uses an O(N) single-pass algorithm for each mode by exploiting
    the exponential form of the pseudomode wakefield.

    Parameters
    ----------
    z : np.ndarray
        Particle positions (m).
    weight : np.ndarray
        Particle charges (C).
    include_self_kick : bool, optional
        If True, applies the self-kick. Default is True.

    Returns
    -------
    kicks : np.ndarray
        Wake-induced energy kick per unit length (eV/m).
    """
    z = np.asarray(z)
    weight = np.asarray(weight)

    if z.shape != weight.shape:
        raise ValueError(
            f"Mismatched shapes: z.shape={z.shape}, weight.shape={weight.shape}"
        )
    if z.ndim != 1:
        raise ValueError("z and weight must be 1D arrays")

    # Sort particles from tail to head
    ix = z.argsort()
    z_sorted = z[ix].copy()
    z_sorted -= z_sorted.max()  # Offset for numerical stability
    weight_sorted = weight[ix]

    n = len(z)
    delta_E = np.zeros(n)

    # Process each mode with O(N) algorithm
    for mode in self._modes:
        s = mode.d + 1j * mode.k
        c = mode.A * np.exp(1j * mode.phi)

        # O(N) accumulator for this mode
        b = 0.0 + 0.0j

        # Iterate from tail to head
        for i in range(n - 1, -1, -1):
            zi = z_sorted[i]
            qi = weight_sorted[i]

            # Kick from the particles already accumulated in b (those ahead)
            delta_E[i] -= np.imag(c * np.exp(s * zi) * b)

            # Add this particle to accumulator
            b += qi * np.exp(-s * zi)

        if include_self_kick:
            delta_E -= 0.5 * mode.A * weight_sorted * np.sin(mode.phi)

    # Restore original order
    kicks = np.empty_like(delta_E)
    kicks[ix] = delta_E

    return kicks

plot

plot(zmax=None, zmin=0, n=200, normalized=False, ax=None)

Plot the resistive wall wakefield W(z).

Parameters:

  • zmax (float, default: None ) –

    Maximum trailing distance (m). Defaults to 10 decay lengths.

  • zmin (float, default: 0 ) –

    Minimum trailing distance (m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • normalized (bool, default: False ) –

    If True, plot dimensionless Ŵ(z/s₀) = W(z)/W₀ vs z/s₀. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/resistive_wall/pseudomode.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def plot(self, zmax=None, zmin=0, n=200, normalized=False, ax=None):
    """
    Plot the resistive wall wakefield W(z).

    Parameters
    ----------
    zmax : float, optional
        Maximum trailing distance (m). Defaults to 10 decay lengths.
    zmin : float, optional
        Minimum trailing distance (m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    normalized : bool, optional
        If True, plot dimensionless Ŵ(z/s₀) = W(z)/W₀ vs z/s₀.
        Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if zmax is None:
        zmax = 1 / (self.kr / (2 * self.Qr)) * 10

    zlist = np.linspace(zmin, zmax, n)
    Wz = self.wake(-zlist)

    if ax is None:
        _, ax = plt.subplots()

    if normalized:
        ax.plot(zlist / self.s0, Wz / self.W0)
        ax.set_xlabel(r"$|z|/s_0$")
        ax.set_ylabel(r"$W(z)/W_0$")
    else:
        ax.plot(zlist * 1e6, Wz * 1e-12)
        ax.set_xlabel(r"Distance behind source $|z|$ (µm)")
        ax.set_ylabel(r"$W(z)$ (V/pC/m)")

plot_impedance

plot_impedance(kmax: float = None, kmin: float = 0, n: int = 500, ax=None)

Plot the resistive wall impedance Z(k).

Parameters:

  • kmax (float, default: None ) –

    Maximum wavenumber (1/m). Defaults to 10/s0.

  • kmin (float, default: 0 ) –

    Minimum wavenumber (1/m). Default is 0.

  • n (int, default: 500 ) –

    Number of points. Default is 500.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/resistive_wall/pseudomode.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def plot_impedance(
    self, kmax: float = None, kmin: float = 0, n: int = 500, ax=None
):
    """
    Plot the resistive wall impedance Z(k).

    Parameters
    ----------
    kmax : float, optional
        Maximum wavenumber (1/m). Defaults to 10/s0.
    kmin : float, optional
        Minimum wavenumber (1/m). Default is 0.
    n : int, optional
        Number of points. Default is 500.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if kmax is None:
        kmax = 10 / self.s0
    super().plot_impedance(kmax=kmax, kmin=kmin, n=n, ax=ax)

to_bmad

to_bmad(file=None, z_max=100, amp_scale=1, scale_with_length=True, z_scale=1)

Export wakefield in Bmad format.

Parameters:

  • file (str, default: None ) –

    Output file path. If None, returns string only.

  • z_max (float, default: 100 ) –

    Trailing z distance for Bmad.

  • amp_scale (float, default: 1 ) –

    Amplitude scaling factor.

  • scale_with_length (bool, default: True ) –

    Whether to scale with length.

  • z_scale (float, default: 1 ) –

    Z scaling factor.

Returns:

  • str –

    Bmad-formatted wakefield string.

Source code in beamphysics/wakefields/resistive_wall/pseudomode.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
    def to_bmad(
        self, file=None, z_max=100, amp_scale=1, scale_with_length=True, z_scale=1
    ):
        """
        Export wakefield in Bmad format.

        Parameters
        ----------
        file : str, optional
            Output file path. If None, returns string only.
        z_max : float
            Trailing z distance for Bmad.
        amp_scale : float
            Amplitude scaling factor.
        scale_with_length : bool
            Whether to scale with length.
        z_scale : float
            Z scaling factor.

        Returns
        -------
        str
            Bmad-formatted wakefield string.
        """
        s = f"""! AC Resistive wall wakefield
! Adapted from SLAC-PUB-10707
!    Material        : {self.material_from_properties()}
!    Conductivity    : {self.conductivity} S/m
!    Relaxation time : {self.relaxation_time} s
!    Geometry        : {self.geometry.value}
"""

        if self.geometry == Geometry.ROUND:
            s += f"!    Radius          : {self.radius} m\n"
        elif self.geometry == Geometry.FLAT:
            s += f"!    full gap        : {2 * self.radius} m\n"

        s += f"!    s₀              : {self.s0}  m\n"
        s += f"!    Γ               : {self.Gamma} \n"
        s += "! sr_wake =  \n"

        s += f"{{{z_scale=}, {amp_scale=}, {scale_with_length=}, {z_max=},\n"
        s += PseudomodeWakefield.to_bmad(self) + "}\n"

        if file is not None:
            with open(file, "w") as f:
                f.write(s)

        return s

to_tabular

to_tabular(zmax: float | None = None, n: int | None = None) -> TabularWakefield

Resample this model onto a uniform TabularWakefield.

See TabularWakefield.from_wakefield.

Parameters:

  • zmax (float, default: None ) –

    Largest distance behind the source particle (m). Defaults to default_zmax.

  • n (int, default: None ) –

    Number of samples. Defaults to default_n_samples(zmax).

Returns:

Examples:

>>> wake = ResistiveWallPseudomode.from_material(
...     "copper-slac-pub-10707", radius=2.5e-3
... )
>>> table = wake.to_tabular()
Source code in beamphysics/wakefields/resistive_wall/base.py
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def to_tabular(
    self, zmax: float | None = None, n: int | None = None
) -> TabularWakefield:
    """
    Resample this model onto a uniform `TabularWakefield`.

    See `TabularWakefield.from_wakefield`.

    Parameters
    ----------
    zmax : float, optional
        Largest distance behind the source particle (m). Defaults to
        `default_zmax`.
    n : int, optional
        Number of samples. Defaults to `default_n_samples(zmax)`.

    Returns
    -------
    TabularWakefield

    Examples
    --------
    >>> wake = ResistiveWallPseudomode.from_material(
    ...     "copper-slac-pub-10707", radius=2.5e-3
    ... )
    >>> table = wake.to_tabular()
    """
    return TabularWakefield.from_wakefield(self, zmax=zmax, n=n)

wake

wake(z: ndarray | float) -> ndarray | float

Evaluate the pseudomode wakefield at position z.

Parameters:

  • z (float or ndarray) –

    Longitudinal position (m). Negative z is behind the source.

Returns:

  • W ( float or ndarray ) –

    Wakefield value (V/C/m). Returns 0 for z > 0.

Source code in beamphysics/wakefields/pseudomode.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def wake(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the pseudomode wakefield at position z.

    Parameters
    ----------
    z : float or np.ndarray
        Longitudinal position (m). Negative z is behind the source.

    Returns
    -------
    W : float or np.ndarray
        Wakefield value (V/C/m). Returns 0 for z > 0.
    """
    z = np.asarray(z)
    scalar_input = z.ndim == 0
    z = np.atleast_1d(z)

    out = np.zeros_like(z, dtype=float)
    mask = z <= 0
    z_masked = z[mask]

    # Sum contributions from all modes
    for mode in self._modes:
        out[mask] += mode(z_masked)

    if scalar_input:
        return float(out[0])
    return out

Base Classes

WakefieldBase

Bases: ABC


              flowchart TD
              beamphysics.wakefields.WakefieldBase[WakefieldBase]

              

              click beamphysics.wakefields.WakefieldBase href "" "beamphysics.wakefields.WakefieldBase"
            

Abstract base class for longitudinal wakefield models.

All wakefield implementations should inherit from this class and implement the required abstract methods.

Sign Convention

The wakefield W(z) is defined for a trailing particle at position z relative to the source particle at z=0:

  • z < 0: Behind the source (trailing particle feels the wake)
  • z > 0: Ahead of the source (causality requires W=0)

The wake is positive for an energy-losing interaction (the trailing particle loses energy).

Methods:

  • wake –

    Evaluate the wakefield at position z (V/C/m)

  • impedance –

    Evaluate the impedance at wavenumber k (Ohm/m)

  • convolve_density –

    Compute integrated wake from a charge density distribution (V/m)

  • particle_kicks –

    Compute momentum kicks for a distribution of particles (eV/m)

  • plot –

    Plot the wakefield over a range of z values

  • plot_impedance –

    Plot the impedance over a range of wavenumbers

See Also

ParticleGroup.apply_wakefield : Apply wakefield kicks to a ParticleGroup

Attributes:

  • default_zmax (float) –

    Distance behind the source particle beyond which the wake is negligible (m).

  • min_wavelength (float) –

    Shortest length scale of the wake (m), which sets the default sampling.

default_zmax property

default_zmax: float

Distance behind the source particle beyond which the wake is negligible (m).

Used as the default range when tabulating.

Raises:

  • NotImplementedError –

    If the model defines no default; pass zmax explicitly.

min_wavelength property

min_wavelength: float

Shortest length scale of the wake (m), which sets the default sampling.

Raises:

  • NotImplementedError –

    If the model defines no length scale; pass n explicitly.

__call__

__call__(z: ndarray | float) -> ndarray | float

Evaluate the wakefield at position z (convenience method).

This is equivalent to calling wake(z).

Source code in beamphysics/wakefields/base.py
104
105
106
107
108
109
110
def __call__(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the wakefield at position z (convenience method).

    This is equivalent to calling wake(z).
    """
    return self.wake(z)

convolve_density

convolve_density(density: ndarray, dz: float, offset: float = 0, include_self_kick: bool = True, plot: bool = False, ax=None) -> ndarray

Compute integrated wakefield from a charge density distribution.

This computes the causal wake potential:

\[V(z_i) = \sum_{j>i} Q_j \cdot W(z_j - z_i) + \frac{1}{2} Q_i \cdot W(0)\]

where only particles ahead (larger z index) contribute to the wake felt by each particle. This is mathematically a correlation, not a convolution, and is computed efficiently using FFT.

Parameters:

  • density (ndarray) –

    Charge density array (C/m). Index 0 is the tail (back), index n-1 is the head (front).

  • dz (float) –

    Grid spacing (m).

  • offset (float, default: 0 ) –

    Offset for the z coordinate (m). Default is 0.

  • include_self_kick (bool, default: True ) –

    Whether to include the half self-kick term. Default is True.

  • plot (bool, default: False ) –

    If True, plot the density profile and wake potential. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None and plot=True, creates a new figure.

Returns:

  • integrated_wake ( ndarray ) –

    Integrated longitudinal wakefield (V/m).

Source code in beamphysics/wakefields/base.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def convolve_density(
    self,
    density: np.ndarray,
    dz: float,
    offset: float = 0,
    include_self_kick: bool = True,
    plot: bool = False,
    ax=None,
) -> np.ndarray:
    """
    Compute integrated wakefield from a charge density distribution.

    This computes the causal wake potential:

    $$V(z_i) = \\sum_{j>i} Q_j \\cdot W(z_j - z_i) + \\frac{1}{2} Q_i \\cdot W(0)$$

    where only particles ahead (larger z index) contribute to the wake
    felt by each particle. This is mathematically a correlation, not
    a convolution, and is computed efficiently using FFT.

    Parameters
    ----------
    density : np.ndarray
        Charge density array (C/m). Index 0 is the tail (back),
        index n-1 is the head (front).
    dz : float
        Grid spacing (m).
    offset : float, optional
        Offset for the z coordinate (m). Default is 0.
    include_self_kick : bool, optional
        Whether to include the half self-kick term. Default is True.
    plot : bool, optional
        If True, plot the density profile and wake potential. Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None and plot=True, creates a new figure.

    Returns
    -------
    integrated_wake : np.ndarray
        Integrated longitudinal wakefield (V/m).
    """
    from scipy.signal import correlate

    n = len(density)
    charge = density * dz  # Convert to charge per bin (C)

    # Build causal wake array: W[k] = wake at lag k*dz
    # W[0] = W(0), W[1] = W(-dz), W[2] = W(-2*dz), ...
    z_wake = -np.arange(n) * dz + offset
    W_causal = self.wake(z_wake)

    # Correlation: result[i] = sum_k charge[i+k] * W[k]
    # This gives the wake from particles ahead (larger indices)
    corr_result = correlate(charge, W_causal, mode="full")

    # Extract the portion corresponding to non-negative lags
    # correlate output has length 2*n-1, with zero lag at index n-1
    integrated_wake = corr_result[n - 1 : 2 * n - 1]

    # Correlation includes full W[0] contribution from self
    # Remove it, then add half if self-kick is requested
    W0 = W_causal[0]
    integrated_wake = integrated_wake - charge * W0
    if include_self_kick:
        integrated_wake = integrated_wake + 0.5 * W0 * charge

    if plot:
        self._plot_convolve_density(density, dz, integrated_wake, ax=ax)

    return integrated_wake

default_n_samples

default_n_samples(zmax: float) -> int

Default number of uniform samples out to zmax.

\[n = \max\left( \left\lceil \frac{128 \, z_{\max}}{\lambda_{\min}} \right\rceil + 1, \; 4 \right)\]

Here lambda_min is min_wavelength, and 4 is the minimum for cubic interpolation.

Parameters:

  • zmax (float) –

    Largest distance behind the source particle (m).

Returns:

  • int –
Source code in beamphysics/wakefields/base.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def default_n_samples(self, zmax: float) -> int:
    """
    Default number of uniform samples out to zmax.

    $$n = \\max\\left( \\left\\lceil \\frac{128 \\, z_{\\max}}{\\lambda_{\\min}}
    \\right\\rceil + 1, \\; 4 \\right)$$

    Here lambda_min is `min_wavelength`, and 4 is the minimum for cubic
    interpolation.

    Parameters
    ----------
    zmax : float
        Largest distance behind the source particle (m).

    Returns
    -------
    int
    """
    n = int(np.ceil(SAMPLES_PER_WAVELENGTH * zmax / self.min_wavelength)) + 1

    return max(n, MIN_SAMPLES)

impedance abstractmethod

impedance(k: ndarray | float) -> ndarray | complex

Evaluate the impedance at wavenumber k.

Parameters:

  • k (float or ndarray) –

    Wavenumber (1/m).

Returns:

  • Z ( complex or ndarray ) –

    Impedance (Ohm/m).

Source code in beamphysics/wakefields/base.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@abstractmethod
def impedance(self, k: np.ndarray | float) -> np.ndarray | complex:
    """
    Evaluate the impedance at wavenumber k.

    Parameters
    ----------
    k : float or np.ndarray
        Wavenumber (1/m).

    Returns
    -------
    Z : complex or np.ndarray
        Impedance (Ohm/m).
    """
    pass

particle_kicks

particle_kicks(z: ndarray, weight: ndarray, include_self_kick: bool = True) -> ndarray

Compute wakefield-induced energy kicks per unit length.

This is a default O(N²) implementation. Subclasses may override with more efficient algorithms.

Parameters:

  • z (ndarray) –

    Particle positions (m).

  • weight (ndarray) –

    Particle charges (C).

  • include_self_kick (bool, default: True ) –

    Whether to include the self-kick term. Default is True.

Returns:

  • kicks ( ndarray ) –

    Energy kick per unit length at each particle (eV/m).

Source code in beamphysics/wakefields/base.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
def particle_kicks(
    self,
    z: np.ndarray,
    weight: np.ndarray,
    include_self_kick: bool = True,
) -> np.ndarray:
    """
    Compute wakefield-induced energy kicks per unit length.

    This is a default O(N²) implementation. Subclasses may override
    with more efficient algorithms.

    Parameters
    ----------
    z : np.ndarray
        Particle positions (m).
    weight : np.ndarray
        Particle charges (C).
    include_self_kick : bool, optional
        Whether to include the self-kick term. Default is True.

    Returns
    -------
    kicks : np.ndarray
        Energy kick per unit length at each particle (eV/m).
    """
    z = np.asarray(z)
    weight = np.asarray(weight)
    n = len(z)
    kicks = np.zeros(n)

    # O(N²) algorithm: sum contribution from all particles
    for i in range(n):
        for j in range(n):
            if i == j:
                if include_self_kick:
                    # Self-kick uses W(0⁻) / 2
                    kicks[i] -= 0.5 * weight[j] * self._self_kick_value()
            else:
                dz = z[i] - z[j]
                if dz < 0:  # j is ahead of i, so i feels wake from j
                    kicks[i] -= weight[j] * self.wake(dz)

    return kicks

plot

plot(zmax: float = None, zmin: float = 0, n: int = 200, ax=None)

Plot the wakefield over a range of z values.

Parameters:

  • zmax (float, default: None ) –

    Maximum trailing distance (m). If None, uses a sensible default.

  • zmin (float, default: 0 ) –

    Minimum trailing distance (m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/base.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def plot(self, zmax: float = None, zmin: float = 0, n: int = 200, ax=None):
    """
    Plot the wakefield over a range of z values.

    Parameters
    ----------
    zmax : float, optional
        Maximum trailing distance (m). If None, uses a sensible default.
    zmin : float, optional
        Minimum trailing distance (m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if zmax is None:
        zmax = 1e-3  # 1 mm default

    zlist = np.linspace(zmin, zmax, n)
    Wz = self.wake(-zlist)  # Negative z for trailing particles

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(zlist * 1e6, Wz * 1e-12)
    ax.set_xlabel(r"Distance behind source $|z|$ (µm)")
    ax.set_ylabel(r"$W(z)$ (V/pC/m)")

plot_impedance

plot_impedance(kmax: float = None, kmin: float = 0, n: int = 200, ax=None)

Plot the impedance over a range of wavenumbers.

Parameters:

  • kmax (float, default: None ) –

    Maximum wavenumber (1/m). If None, uses a sensible default.

  • kmin (float, default: 0 ) –

    Minimum wavenumber (1/m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/base.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def plot_impedance(
    self, kmax: float = None, kmin: float = 0, n: int = 200, ax=None
):
    """
    Plot the impedance over a range of wavenumbers.

    Parameters
    ----------
    kmax : float, optional
        Maximum wavenumber (1/m). If None, uses a sensible default.
    kmin : float, optional
        Minimum wavenumber (1/m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if kmax is None:
        kmax = 1e6  # 1/µm default

    k = np.linspace(kmin, kmax, n)
    Z = self.impedance(k)

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(k * 1e-3, np.real(Z), label=r"Re[$Z$]")
    ax.plot(k * 1e-3, np.imag(Z), label=r"Im[$Z$]")
    ax.set_xlabel(r"$k$ (1/mm)")
    ax.set_ylabel(r"$Z(k)$ (Ω/m)")
    ax.legend()

wake abstractmethod

wake(z: ndarray | float) -> ndarray | float

Evaluate the wakefield at longitudinal position z.

Parameters:

  • z (float or ndarray) –

    Longitudinal position (m). Negative z is behind the source.

Returns:

  • W ( float or ndarray ) –

    Wakefield value (V/C/m). Returns 0 for z > 0 (causality).

Source code in beamphysics/wakefields/base.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@abstractmethod
def wake(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the wakefield at longitudinal position z.

    Parameters
    ----------
    z : float or np.ndarray
        Longitudinal position (m). Negative z is behind the source.

    Returns
    -------
    W : float or np.ndarray
        Wakefield value (V/C/m). Returns 0 for z > 0 (causality).
    """
    pass

PseudomodeWakefield

PseudomodeWakefield(modes: list)

Bases: WakefieldBase


              flowchart TD
              beamphysics.wakefields.PseudomodeWakefield[PseudomodeWakefield]
              beamphysics.wakefields.base.WakefieldBase[WakefieldBase]

                              beamphysics.wakefields.base.WakefieldBase --> beamphysics.wakefields.PseudomodeWakefield
                


              click beamphysics.wakefields.PseudomodeWakefield href "" "beamphysics.wakefields.PseudomodeWakefield"
              click beamphysics.wakefields.base.WakefieldBase href "" "beamphysics.wakefields.base.WakefieldBase"
            

Wakefield represented as a sum of damped sinusoidal pseudomodes.

Models the longitudinal wakefield as:

\[W(z) = \sum_i A_i \cdot e^{d_i \cdot z} \cdot \sin(k_i \cdot z + \phi_i)\]

This form is used to approximate short-range wakefields such as the resistive wall wake.

Parameters:

  • modes (list of Pseudomode or list of dict) –

    List of pseudomodes. Each can be a Pseudomode object or a dict with keys 'A', 'd', 'k', 'phi'.

Notes
  • Wakefields are defined for z ≤ 0 (trailing the source particle).
  • For z > 0, returns 0 (causality).

Examples:

Single mode::

pm = PseudomodeWakefield([Pseudomode(A=1e15, d=1e4, k=1e5, phi=np.pi/2)])
pm.wake(-10e-6)  # Wake at 10 µm behind source

Multiple modes::

modes = [
    Pseudomode(A=1e15, d=1e4, k=1e5, phi=np.pi/2),
    Pseudomode(A=5e14, d=2e4, k=2e5, phi=np.pi/4),
]
pm = PseudomodeWakefield(modes)

Methods:

  • __call__ –

    Evaluate the wakefield at position z (convenience method).

  • convolve_density –

    Compute integrated wakefield from a charge density distribution.

  • default_n_samples –

    Default number of uniform samples out to zmax.

  • impedance –

    Compute the impedance Z(k) analytically.

  • particle_kicks –

    Compute short-range wakefield energy kicks per unit length.

  • plot –

    Plot the pseudomode wakefield.

  • plot_impedance –

    Plot the impedance over a range of wavenumbers.

  • to_bmad –

    Format pseudomode parameters as Bmad-compatible strings.

  • wake –

    Evaluate the pseudomode wakefield at position z.

Attributes:

  • decay_length (float) –

    Envelope decay length of the most slowly damped mode (m).

  • default_zmax (float) –

    Distance behind the source particle beyond which the wake is negligible (m).

  • min_wavelength (float) –

    Shortest length scale of the wake (m).

  • modes (list) –

    List of Pseudomode objects.

  • n_modes (int) –

    Number of pseudomodes.

Source code in beamphysics/wakefields/pseudomode.py
213
214
215
216
217
218
219
220
221
def __init__(self, modes: list) -> None:
    self._modes = []
    for mode in modes:
        if isinstance(mode, Pseudomode):
            self._modes.append(mode)
        elif isinstance(mode, dict):
            self._modes.append(Pseudomode(**mode))
        else:
            raise TypeError(f"Mode must be Pseudomode or dict, got {type(mode)}")

decay_length property

decay_length: float

Envelope decay length of the most slowly damped mode (m).

\[L = \frac{1}{\min_i d_i}\]

Raises:

  • ValueError –

    If any mode has d <= 0, so that the wake does not decay.

default_zmax property

default_zmax: float

Distance behind the source particle beyond which the wake is negligible (m).

\[z_{\max} = 10 \, L\]

Here L is decay_length, so the envelope has fallen to exp(-10).

min_wavelength property

min_wavelength: float

Shortest length scale of the wake (m).

\[\lambda_{\min} = \min\left( \frac{2 \pi}{\max_i |k_i|}, \frac{1}{\max_i d_i} \right)\]

The period of the fastest mode or the decay length of the most strongly damped mode, whichever is shorter.

Raises:

  • ValueError –

    If any mode has d <= 0.

modes property

modes: list

List of Pseudomode objects.

n_modes property

n_modes: int

Number of pseudomodes.

__call__

__call__(z: ndarray | float) -> ndarray | float

Evaluate the wakefield at position z (convenience method).

This is equivalent to calling wake(z).

Source code in beamphysics/wakefields/base.py
104
105
106
107
108
109
110
def __call__(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the wakefield at position z (convenience method).

    This is equivalent to calling wake(z).
    """
    return self.wake(z)

convolve_density

convolve_density(density: ndarray, dz: float, offset: float = 0, include_self_kick: bool = True, plot: bool = False, ax=None) -> ndarray

Compute integrated wakefield from a charge density distribution.

This computes the causal wake potential:

\[V(z_i) = \sum_{j>i} Q_j \cdot W(z_j - z_i) + \frac{1}{2} Q_i \cdot W(0)\]

where only particles ahead (larger z index) contribute to the wake felt by each particle. This is mathematically a correlation, not a convolution, and is computed efficiently using FFT.

Parameters:

  • density (ndarray) –

    Charge density array (C/m). Index 0 is the tail (back), index n-1 is the head (front).

  • dz (float) –

    Grid spacing (m).

  • offset (float, default: 0 ) –

    Offset for the z coordinate (m). Default is 0.

  • include_self_kick (bool, default: True ) –

    Whether to include the half self-kick term. Default is True.

  • plot (bool, default: False ) –

    If True, plot the density profile and wake potential. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None and plot=True, creates a new figure.

Returns:

  • integrated_wake ( ndarray ) –

    Integrated longitudinal wakefield (V/m).

Source code in beamphysics/wakefields/base.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def convolve_density(
    self,
    density: np.ndarray,
    dz: float,
    offset: float = 0,
    include_self_kick: bool = True,
    plot: bool = False,
    ax=None,
) -> np.ndarray:
    """
    Compute integrated wakefield from a charge density distribution.

    This computes the causal wake potential:

    $$V(z_i) = \\sum_{j>i} Q_j \\cdot W(z_j - z_i) + \\frac{1}{2} Q_i \\cdot W(0)$$

    where only particles ahead (larger z index) contribute to the wake
    felt by each particle. This is mathematically a correlation, not
    a convolution, and is computed efficiently using FFT.

    Parameters
    ----------
    density : np.ndarray
        Charge density array (C/m). Index 0 is the tail (back),
        index n-1 is the head (front).
    dz : float
        Grid spacing (m).
    offset : float, optional
        Offset for the z coordinate (m). Default is 0.
    include_self_kick : bool, optional
        Whether to include the half self-kick term. Default is True.
    plot : bool, optional
        If True, plot the density profile and wake potential. Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None and plot=True, creates a new figure.

    Returns
    -------
    integrated_wake : np.ndarray
        Integrated longitudinal wakefield (V/m).
    """
    from scipy.signal import correlate

    n = len(density)
    charge = density * dz  # Convert to charge per bin (C)

    # Build causal wake array: W[k] = wake at lag k*dz
    # W[0] = W(0), W[1] = W(-dz), W[2] = W(-2*dz), ...
    z_wake = -np.arange(n) * dz + offset
    W_causal = self.wake(z_wake)

    # Correlation: result[i] = sum_k charge[i+k] * W[k]
    # This gives the wake from particles ahead (larger indices)
    corr_result = correlate(charge, W_causal, mode="full")

    # Extract the portion corresponding to non-negative lags
    # correlate output has length 2*n-1, with zero lag at index n-1
    integrated_wake = corr_result[n - 1 : 2 * n - 1]

    # Correlation includes full W[0] contribution from self
    # Remove it, then add half if self-kick is requested
    W0 = W_causal[0]
    integrated_wake = integrated_wake - charge * W0
    if include_self_kick:
        integrated_wake = integrated_wake + 0.5 * W0 * charge

    if plot:
        self._plot_convolve_density(density, dz, integrated_wake, ax=ax)

    return integrated_wake

default_n_samples

default_n_samples(zmax: float) -> int

Default number of uniform samples out to zmax.

\[n = \max\left( \left\lceil \frac{128 \, z_{\max}}{\lambda_{\min}} \right\rceil + 1, \; 4 \right)\]

Here lambda_min is min_wavelength, and 4 is the minimum for cubic interpolation.

Parameters:

  • zmax (float) –

    Largest distance behind the source particle (m).

Returns:

  • int –
Source code in beamphysics/wakefields/base.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def default_n_samples(self, zmax: float) -> int:
    """
    Default number of uniform samples out to zmax.

    $$n = \\max\\left( \\left\\lceil \\frac{128 \\, z_{\\max}}{\\lambda_{\\min}}
    \\right\\rceil + 1, \\; 4 \\right)$$

    Here lambda_min is `min_wavelength`, and 4 is the minimum for cubic
    interpolation.

    Parameters
    ----------
    zmax : float
        Largest distance behind the source particle (m).

    Returns
    -------
    int
    """
    n = int(np.ceil(SAMPLES_PER_WAVELENGTH * zmax / self.min_wavelength)) + 1

    return max(n, MIN_SAMPLES)

impedance

impedance(k: ndarray | float) -> ndarray | complex

Compute the impedance Z(k) analytically.

Sums the contribution from each pseudomode using the analytic Fourier transform.

Parameters:

  • k (float or ndarray) –

    Wavenumber (1/m).

Returns:

  • Z ( complex or ndarray ) –

    Impedance (Ohm/m).

Source code in beamphysics/wakefields/pseudomode.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def impedance(self, k: np.ndarray | float) -> np.ndarray | complex:
    """
    Compute the impedance Z(k) analytically.

    Sums the contribution from each pseudomode using the analytic
    Fourier transform.

    Parameters
    ----------
    k : float or np.ndarray
        Wavenumber (1/m).

    Returns
    -------
    Z : complex or np.ndarray
        Impedance (Ohm/m).
    """
    k = np.asarray(k)
    scalar_input = k.ndim == 0
    k = np.atleast_1d(k)

    Z = np.zeros(len(k), dtype=complex)
    for mode in self._modes:
        Z += mode.impedance(k)

    if scalar_input:
        return complex(Z[0])
    return Z

particle_kicks

particle_kicks(z: ndarray, weight: ndarray, include_self_kick: bool = True) -> ndarray

Compute short-range wakefield energy kicks per unit length.

Uses an O(N) single-pass algorithm for each mode by exploiting the exponential form of the pseudomode wakefield.

Parameters:

  • z (ndarray) –

    Particle positions (m).

  • weight (ndarray) –

    Particle charges (C).

  • include_self_kick (bool, default: True ) –

    If True, applies the self-kick. Default is True.

Returns:

  • kicks ( ndarray ) –

    Wake-induced energy kick per unit length (eV/m).

Source code in beamphysics/wakefields/pseudomode.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
def particle_kicks(
    self,
    z: np.ndarray,
    weight: np.ndarray,
    include_self_kick: bool = True,
) -> np.ndarray:
    """
    Compute short-range wakefield energy kicks per unit length.

    Uses an O(N) single-pass algorithm for each mode by exploiting
    the exponential form of the pseudomode wakefield.

    Parameters
    ----------
    z : np.ndarray
        Particle positions (m).
    weight : np.ndarray
        Particle charges (C).
    include_self_kick : bool, optional
        If True, applies the self-kick. Default is True.

    Returns
    -------
    kicks : np.ndarray
        Wake-induced energy kick per unit length (eV/m).
    """
    z = np.asarray(z)
    weight = np.asarray(weight)

    if z.shape != weight.shape:
        raise ValueError(
            f"Mismatched shapes: z.shape={z.shape}, weight.shape={weight.shape}"
        )
    if z.ndim != 1:
        raise ValueError("z and weight must be 1D arrays")

    # Sort particles from tail to head
    ix = z.argsort()
    z_sorted = z[ix].copy()
    z_sorted -= z_sorted.max()  # Offset for numerical stability
    weight_sorted = weight[ix]

    n = len(z)
    delta_E = np.zeros(n)

    # Process each mode with O(N) algorithm
    for mode in self._modes:
        s = mode.d + 1j * mode.k
        c = mode.A * np.exp(1j * mode.phi)

        # O(N) accumulator for this mode
        b = 0.0 + 0.0j

        # Iterate from tail to head
        for i in range(n - 1, -1, -1):
            zi = z_sorted[i]
            qi = weight_sorted[i]

            # Kick from the particles already accumulated in b (those ahead)
            delta_E[i] -= np.imag(c * np.exp(s * zi) * b)

            # Add this particle to accumulator
            b += qi * np.exp(-s * zi)

        if include_self_kick:
            delta_E -= 0.5 * mode.A * weight_sorted * np.sin(mode.phi)

    # Restore original order
    kicks = np.empty_like(delta_E)
    kicks[ix] = delta_E

    return kicks

plot

plot(zmax: float = 0.001, zmin: float = 0, n: int = 200, ax=None)

Plot the pseudomode wakefield.

Source code in beamphysics/wakefields/pseudomode.py
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
def plot(self, zmax: float = 0.001, zmin: float = 0, n: int = 200, ax=None):
    """Plot the pseudomode wakefield."""
    zlist = np.linspace(zmin, zmax, n)
    Wz = self.wake(-zlist)

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(zlist * 1e6, Wz * 1e-12)
    ax.set_xlabel(r"Distance behind source $|z|$ (µm)")
    ax.set_ylabel(r"$W(z)$ (V/pC/m)")
    title = f"Pseudomode Wakefield ({self.n_modes} mode"
    if self.n_modes > 1:
        title += "s"
    title += ")"
    ax.set_title(title)

plot_impedance

plot_impedance(kmax: float = None, kmin: float = 0, n: int = 200, ax=None)

Plot the impedance over a range of wavenumbers.

Parameters:

  • kmax (float, default: None ) –

    Maximum wavenumber (1/m). If None, uses a sensible default.

  • kmin (float, default: 0 ) –

    Minimum wavenumber (1/m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/base.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def plot_impedance(
    self, kmax: float = None, kmin: float = 0, n: int = 200, ax=None
):
    """
    Plot the impedance over a range of wavenumbers.

    Parameters
    ----------
    kmax : float, optional
        Maximum wavenumber (1/m). If None, uses a sensible default.
    kmin : float, optional
        Minimum wavenumber (1/m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if kmax is None:
        kmax = 1e6  # 1/µm default

    k = np.linspace(kmin, kmax, n)
    Z = self.impedance(k)

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(k * 1e-3, np.real(Z), label=r"Re[$Z$]")
    ax.plot(k * 1e-3, np.imag(Z), label=r"Im[$Z$]")
    ax.set_xlabel(r"$k$ (1/mm)")
    ax.set_ylabel(r"$Z(k)$ (Ω/m)")
    ax.legend()

to_bmad

to_bmad(type: str = 'longitudinal', transverse_dependence: str = 'none') -> str

Format pseudomode parameters as Bmad-compatible strings.

Parameters:

  • type (str, default: 'longitudinal' ) –

    Wake type. Default is "longitudinal".

  • transverse_dependence (str, default: 'none' ) –

    Transverse dependence. Default is "none".

Returns:

  • str –

    Bmad-formatted string (one line per mode).

Source code in beamphysics/wakefields/pseudomode.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
def to_bmad(
    self,
    type: str = "longitudinal",
    transverse_dependence: str = "none",
) -> str:
    """
    Format pseudomode parameters as Bmad-compatible strings.

    Parameters
    ----------
    type : str, optional
        Wake type. Default is "longitudinal".
    transverse_dependence : str, optional
        Transverse dependence. Default is "none".

    Returns
    -------
    str
        Bmad-formatted string (one line per mode).
    """
    lines = [mode.to_bmad(type, transverse_dependence) for mode in self._modes]
    return "\n".join(lines)

wake

wake(z: ndarray | float) -> ndarray | float

Evaluate the pseudomode wakefield at position z.

Parameters:

  • z (float or ndarray) –

    Longitudinal position (m). Negative z is behind the source.

Returns:

  • W ( float or ndarray ) –

    Wakefield value (V/C/m). Returns 0 for z > 0.

Source code in beamphysics/wakefields/pseudomode.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def wake(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the pseudomode wakefield at position z.

    Parameters
    ----------
    z : float or np.ndarray
        Longitudinal position (m). Negative z is behind the source.

    Returns
    -------
    W : float or np.ndarray
        Wakefield value (V/C/m). Returns 0 for z > 0.
    """
    z = np.asarray(z)
    scalar_input = z.ndim == 0
    z = np.atleast_1d(z)

    out = np.zeros_like(z, dtype=float)
    mask = z <= 0
    z_masked = z[mask]

    # Sum contributions from all modes
    for mode in self._modes:
        out[mask] += mode(z_masked)

    if scalar_input:
        return float(out[0])
    return out

ImpedanceWakefield

ImpedanceWakefield(impedance_func: Callable[[ndarray], ndarray], wakefield_func: Callable[[ndarray], ndarray] = None, k_max: float = 10000000.0)

Bases: WakefieldBase


              flowchart TD
              beamphysics.wakefields.ImpedanceWakefield[ImpedanceWakefield]
              beamphysics.wakefields.base.WakefieldBase[WakefieldBase]

                              beamphysics.wakefields.base.WakefieldBase --> beamphysics.wakefields.ImpedanceWakefield
                


              click beamphysics.wakefields.ImpedanceWakefield href "" "beamphysics.wakefields.ImpedanceWakefield"
              click beamphysics.wakefields.base.WakefieldBase href "" "beamphysics.wakefields.base.WakefieldBase"
            

Wakefield defined through its longitudinal impedance Z(k).

Uses FFT-based convolution in frequency domain for efficient wake potential calculations.

Parameters:

  • impedance_func (callable) –

    Function that returns complex impedance Z(k) (Ohm/m) for wave number k (1/m). Should handle arrays.

  • wakefield_func (callable, default: None ) –

    Function that returns wakefield W(z) (V/C/m) for position z (m). If not provided, uses numerical cosine transform of impedance.

  • k_max (float, default: 10000000.0 ) –

    Maximum wavenumber for integration (1/m). Default is 1e7.

Notes

The wakefield W(z) is defined for z ≤ 0 (trailing particles), with W(z) = 0 for z > 0 (causality). The impedance Z(k) is related to the wakefield by:

\[Z(k) = \frac{1}{c} \int_{-\infty}^{0} W(z) e^{-ikz} dz\]

And the inverse:

\[W(z) = \frac{2c}{\pi} \int_0^{\infty} \text{Re}[Z(k)] \cos(kz) dk \quad (z \le 0)\]

Examples:

>>> def my_impedance(k):
...     return 100 / (1 + 1j * k * 1e-6)  # Simple resonator
>>> wake = ImpedanceWakefield(my_impedance)

Methods:

  • __call__ –

    Evaluate the wakefield at position z (convenience method).

  • convolve_density –

    Compute integrated wakefield from a charge density distribution.

  • default_n_samples –

    Default number of uniform samples out to zmax.

  • impedance –

    Evaluate the impedance at wavenumber k.

  • particle_kicks –

    Compute wakefield-induced energy kicks per unit length.

  • plot –

    Plot the wakefield over a range of z values.

  • plot_impedance –

    Plot the impedance over a range of wavenumbers.

  • wake –

    Evaluate the wakefield at position z.

Attributes:

  • default_zmax (float) –

    Distance behind the source particle beyond which the wake is negligible (m).

  • k_max (float) –

    Maximum wavenumber for integration.

  • min_wavelength (float) –

    Shortest length scale of the wake (m), which sets the default sampling.

Source code in beamphysics/wakefields/impedance.py
61
62
63
64
65
66
67
68
69
def __init__(
    self,
    impedance_func: Callable[[np.ndarray], np.ndarray],
    wakefield_func: Callable[[np.ndarray], np.ndarray] = None,
    k_max: float = 1e7,
) -> None:
    self._impedance_func = impedance_func
    self._wakefield_func = wakefield_func
    self._k_max = k_max

default_zmax property

default_zmax: float

Distance behind the source particle beyond which the wake is negligible (m).

Used as the default range when tabulating.

Raises:

  • NotImplementedError –

    If the model defines no default; pass zmax explicitly.

k_max property writable

k_max: float

Maximum wavenumber for integration.

min_wavelength property

min_wavelength: float

Shortest length scale of the wake (m), which sets the default sampling.

Raises:

  • NotImplementedError –

    If the model defines no length scale; pass n explicitly.

__call__

__call__(z: ndarray | float) -> ndarray | float

Evaluate the wakefield at position z (convenience method).

This is equivalent to calling wake(z).

Source code in beamphysics/wakefields/base.py
104
105
106
107
108
109
110
def __call__(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the wakefield at position z (convenience method).

    This is equivalent to calling wake(z).
    """
    return self.wake(z)

convolve_density

convolve_density(density: ndarray, dz: float, offset: float = 0, include_self_kick: bool = True, plot: bool = False, ax=None) -> ndarray

Compute integrated wakefield from a charge density distribution.

This computes the causal wake potential:

\[V(z_i) = \sum_{j>i} Q_j \cdot W(z_j - z_i) + \frac{1}{2} Q_i \cdot W(0)\]

where only particles ahead (larger z index) contribute to the wake felt by each particle. This is mathematically a correlation, not a convolution, and is computed efficiently using FFT.

Parameters:

  • density (ndarray) –

    Charge density array (C/m). Index 0 is the tail (back), index n-1 is the head (front).

  • dz (float) –

    Grid spacing (m).

  • offset (float, default: 0 ) –

    Offset for the z coordinate (m). Default is 0.

  • include_self_kick (bool, default: True ) –

    Whether to include the half self-kick term. Default is True.

  • plot (bool, default: False ) –

    If True, plot the density profile and wake potential. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None and plot=True, creates a new figure.

Returns:

  • integrated_wake ( ndarray ) –

    Integrated longitudinal wakefield (V/m).

Source code in beamphysics/wakefields/base.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def convolve_density(
    self,
    density: np.ndarray,
    dz: float,
    offset: float = 0,
    include_self_kick: bool = True,
    plot: bool = False,
    ax=None,
) -> np.ndarray:
    """
    Compute integrated wakefield from a charge density distribution.

    This computes the causal wake potential:

    $$V(z_i) = \\sum_{j>i} Q_j \\cdot W(z_j - z_i) + \\frac{1}{2} Q_i \\cdot W(0)$$

    where only particles ahead (larger z index) contribute to the wake
    felt by each particle. This is mathematically a correlation, not
    a convolution, and is computed efficiently using FFT.

    Parameters
    ----------
    density : np.ndarray
        Charge density array (C/m). Index 0 is the tail (back),
        index n-1 is the head (front).
    dz : float
        Grid spacing (m).
    offset : float, optional
        Offset for the z coordinate (m). Default is 0.
    include_self_kick : bool, optional
        Whether to include the half self-kick term. Default is True.
    plot : bool, optional
        If True, plot the density profile and wake potential. Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None and plot=True, creates a new figure.

    Returns
    -------
    integrated_wake : np.ndarray
        Integrated longitudinal wakefield (V/m).
    """
    from scipy.signal import correlate

    n = len(density)
    charge = density * dz  # Convert to charge per bin (C)

    # Build causal wake array: W[k] = wake at lag k*dz
    # W[0] = W(0), W[1] = W(-dz), W[2] = W(-2*dz), ...
    z_wake = -np.arange(n) * dz + offset
    W_causal = self.wake(z_wake)

    # Correlation: result[i] = sum_k charge[i+k] * W[k]
    # This gives the wake from particles ahead (larger indices)
    corr_result = correlate(charge, W_causal, mode="full")

    # Extract the portion corresponding to non-negative lags
    # correlate output has length 2*n-1, with zero lag at index n-1
    integrated_wake = corr_result[n - 1 : 2 * n - 1]

    # Correlation includes full W[0] contribution from self
    # Remove it, then add half if self-kick is requested
    W0 = W_causal[0]
    integrated_wake = integrated_wake - charge * W0
    if include_self_kick:
        integrated_wake = integrated_wake + 0.5 * W0 * charge

    if plot:
        self._plot_convolve_density(density, dz, integrated_wake, ax=ax)

    return integrated_wake

default_n_samples

default_n_samples(zmax: float) -> int

Default number of uniform samples out to zmax.

\[n = \max\left( \left\lceil \frac{128 \, z_{\max}}{\lambda_{\min}} \right\rceil + 1, \; 4 \right)\]

Here lambda_min is min_wavelength, and 4 is the minimum for cubic interpolation.

Parameters:

  • zmax (float) –

    Largest distance behind the source particle (m).

Returns:

  • int –
Source code in beamphysics/wakefields/base.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def default_n_samples(self, zmax: float) -> int:
    """
    Default number of uniform samples out to zmax.

    $$n = \\max\\left( \\left\\lceil \\frac{128 \\, z_{\\max}}{\\lambda_{\\min}}
    \\right\\rceil + 1, \\; 4 \\right)$$

    Here lambda_min is `min_wavelength`, and 4 is the minimum for cubic
    interpolation.

    Parameters
    ----------
    zmax : float
        Largest distance behind the source particle (m).

    Returns
    -------
    int
    """
    n = int(np.ceil(SAMPLES_PER_WAVELENGTH * zmax / self.min_wavelength)) + 1

    return max(n, MIN_SAMPLES)

impedance

impedance(k: ndarray | float) -> ndarray | complex

Evaluate the impedance at wavenumber k.

Parameters:

  • k (float or ndarray) –

    Wavenumber (1/m).

Returns:

  • Z ( complex or ndarray ) –

    Impedance (Ohm/m).

Source code in beamphysics/wakefields/impedance.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def impedance(self, k: np.ndarray | float) -> np.ndarray | complex:
    """
    Evaluate the impedance at wavenumber k.

    Parameters
    ----------
    k : float or np.ndarray
        Wavenumber (1/m).

    Returns
    -------
    Z : complex or np.ndarray
        Impedance (Ohm/m).
    """
    return self._impedance_func(k)

particle_kicks

particle_kicks(z: ndarray, weight: ndarray, include_self_kick: bool = True, n_bins: int = None) -> ndarray

Compute wakefield-induced energy kicks per unit length.

Uses FFT-based density convolution with interpolation back to particle positions. This is O(N + M log M) where N is the number of particles and M is the number of grid points.

Parameters:

  • z (ndarray) –

    Particle positions (m).

  • weight (ndarray) –

    Particle charges (C).

  • include_self_kick (bool, default: True ) –

    Whether to include the self-kick term. Default is True.

  • n_bins (int, default: None ) –

    Number of bins for the density grid. Default is max(100, N//10).

Returns:

  • kicks ( ndarray ) –

    Energy kick per unit length at each particle (eV/m).

Source code in beamphysics/wakefields/impedance.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def particle_kicks(
    self,
    z: np.ndarray,
    weight: np.ndarray,
    include_self_kick: bool = True,
    n_bins: int = None,
) -> np.ndarray:
    """
    Compute wakefield-induced energy kicks per unit length.

    Uses FFT-based density convolution with interpolation back to
    particle positions. This is O(N + M log M) where N is the number
    of particles and M is the number of grid points.

    Parameters
    ----------
    z : np.ndarray
        Particle positions (m).
    weight : np.ndarray
        Particle charges (C).
    include_self_kick : bool, optional
        Whether to include the self-kick term. Default is True.
    n_bins : int, optional
        Number of bins for the density grid. Default is max(100, N//10).

    Returns
    -------
    kicks : np.ndarray
        Energy kick per unit length at each particle (eV/m).
    """
    from scipy.interpolate import interp1d

    z = np.asarray(z)
    weight = np.asarray(weight)
    n = len(z)

    if n_bins is None:
        n_bins = max(100, n // 10)

    # Bin particles into density distribution
    z_min, z_max = z.min(), z.max()
    z_range = z_max - z_min
    if z_range == 0:
        z_range = 1e-6  # Avoid division by zero for single particle

    # Add padding to avoid edge effects
    padding = 0.1 * z_range
    z_min_padded = z_min - padding
    z_max_padded = z_max + padding

    dz = (z_max_padded - z_min_padded) / n_bins
    z_grid = np.linspace(z_min_padded + dz / 2, z_max_padded - dz / 2, n_bins)

    # Create density histogram (charge per bin)
    density, bin_edges = np.histogram(
        z, bins=n_bins, range=(z_min_padded, z_max_padded), weights=weight
    )
    # Convert to charge density (C/m)
    density = density / dz

    # Compute wake potential via FFT convolution
    wake_potential = self.convolve_density(
        density, dz, include_self_kick=include_self_kick
    )

    # Interpolate wake potential to particle positions
    interp = interp1d(
        z_grid, wake_potential, kind="linear", bounds_error=False, fill_value=0.0
    )
    kicks = -interp(z)  # Negative sign: energy loss for trailing particles

    return kicks

plot

plot(zmax: float = None, zmin: float = 0, n: int = 200, ax=None)

Plot the wakefield over a range of z values.

Parameters:

  • zmax (float, default: None ) –

    Maximum trailing distance (m). If None, uses a sensible default.

  • zmin (float, default: 0 ) –

    Minimum trailing distance (m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/base.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def plot(self, zmax: float = None, zmin: float = 0, n: int = 200, ax=None):
    """
    Plot the wakefield over a range of z values.

    Parameters
    ----------
    zmax : float, optional
        Maximum trailing distance (m). If None, uses a sensible default.
    zmin : float, optional
        Minimum trailing distance (m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if zmax is None:
        zmax = 1e-3  # 1 mm default

    zlist = np.linspace(zmin, zmax, n)
    Wz = self.wake(-zlist)  # Negative z for trailing particles

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(zlist * 1e6, Wz * 1e-12)
    ax.set_xlabel(r"Distance behind source $|z|$ (µm)")
    ax.set_ylabel(r"$W(z)$ (V/pC/m)")

plot_impedance

plot_impedance(kmax: float = None, kmin: float = 0, n: int = 200, ax=None)

Plot the impedance over a range of wavenumbers.

Parameters:

  • kmax (float, default: None ) –

    Maximum wavenumber (1/m). If None, uses a sensible default.

  • kmin (float, default: 0 ) –

    Minimum wavenumber (1/m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/base.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def plot_impedance(
    self, kmax: float = None, kmin: float = 0, n: int = 200, ax=None
):
    """
    Plot the impedance over a range of wavenumbers.

    Parameters
    ----------
    kmax : float, optional
        Maximum wavenumber (1/m). If None, uses a sensible default.
    kmin : float, optional
        Minimum wavenumber (1/m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if kmax is None:
        kmax = 1e6  # 1/µm default

    k = np.linspace(kmin, kmax, n)
    Z = self.impedance(k)

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(k * 1e-3, np.real(Z), label=r"Re[$Z$]")
    ax.plot(k * 1e-3, np.imag(Z), label=r"Im[$Z$]")
    ax.set_xlabel(r"$k$ (1/mm)")
    ax.set_ylabel(r"$Z(k)$ (Ω/m)")
    ax.legend()

wake

wake(z: ndarray | float, n_fft: int = 4096) -> ndarray | float

Evaluate the wakefield at position z.

If a wakefield function was provided, uses it directly. Otherwise, computes via cosine transform of Re[Z(k)]: - Scalars: numerical quadrature (accurate) - Arrays: FFT with interpolation (fast)

Parameters:

  • z (float or ndarray) –

    Longitudinal position (m).

  • n_fft (int, default: 4096 ) –

    Number of FFT points for array evaluation. Default is 4096.

Returns:

  • W ( float or ndarray ) –

    Wakefield value (V/C/m). Returns 0 for z > 0.

Source code in beamphysics/wakefields/impedance.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def wake(self, z: np.ndarray | float, n_fft: int = 4096) -> np.ndarray | float:
    """
    Evaluate the wakefield at position z.

    If a wakefield function was provided, uses it directly.
    Otherwise, computes via cosine transform of Re[Z(k)]:
    - Scalars: numerical quadrature (accurate)
    - Arrays: FFT with interpolation (fast)

    Parameters
    ----------
    z : float or np.ndarray
        Longitudinal position (m).
    n_fft : int, optional
        Number of FFT points for array evaluation. Default is 4096.

    Returns
    -------
    W : float or np.ndarray
        Wakefield value (V/C/m). Returns 0 for z > 0.
    """
    if self._wakefield_func is not None:
        return self._wakefield_func(z)

    z = np.asarray(z)
    scalar_input = z.ndim == 0
    z = np.atleast_1d(z)

    if scalar_input:
        # Single point: use quadrature for accuracy
        from scipy.integrate import quad

        z_val = float(z[0])
        if z_val > 0:
            return 0.0

        def integrand(k):
            return np.real(self._impedance_func(k)) * np.cos(k * z_val)

        result, _ = quad(integrand, 0, self._k_max, limit=200)
        return (2 * c_light / np.pi) * result
    else:
        # Array: use FFT for speed
        from scipy.fft import irfft
        from scipy.interpolate import interp1d

        k_max = self._k_max
        dk = k_max / (n_fft - 1)
        k_grid = np.linspace(0, k_max, n_fft)

        Zk_grid = self._impedance_func(k_grid)
        ReZ = np.real(Zk_grid)

        n_full = 2 * (n_fft - 1)
        dz = 2 * np.pi / (n_full * dk)
        z_grid = np.arange(n_full) * dz

        W_grid = irfft(ReZ, n=n_full) * n_full * dk * (c_light / np.pi)

        interp = interp1d(
            z_grid, W_grid, kind="cubic", bounds_error=False, fill_value=0.0
        )

        result = interp(-z)
        result = np.where(z > 0, 0.0, result)

        return result

TabularWakefield

TabularWakefield(z: ndarray, W: ndarray, fill_value: float = 0.0, kind: str = 'cubic')

Bases: WakefieldBase


              flowchart TD
              beamphysics.wakefields.TabularWakefield[TabularWakefield]
              beamphysics.wakefields.base.WakefieldBase[WakefieldBase]

                              beamphysics.wakefields.base.WakefieldBase --> beamphysics.wakefields.TabularWakefield
                


              click beamphysics.wakefields.TabularWakefield href "" "beamphysics.wakefields.TabularWakefield"
              click beamphysics.wakefields.base.WakefieldBase href "" "beamphysics.wakefields.base.WakefieldBase"
            

Wakefield defined by user-supplied tabular data with interpolation.

Uses cubic spline interpolation to evaluate the wakefield at arbitrary positions between the supplied data points.

Parameters:

  • z (ndarray) –

    Longitudinal positions (m). Should be negative (behind source) and must be strictly increasing.

  • W (ndarray) –

    Wakefield values (V/C/m) at each z position. At z = 0 this is W(0-), the wake immediately behind the source particle.

  • fill_value (float, default: 0.0 ) –

    Value to return outside the interpolation range. Default is 0.

  • kind (str, default: 'cubic' ) –

    Interpolation method. Default is 'cubic'.

Examples:

>>> z = -np.linspace(1e-3, 0, 100)
>>> wake = TabularWakefield(z, 1e15 * np.exp(z / 100e-6) * np.sin(1e5 * z))
>>> wake.wake(-50e-6)  # 50 µm behind the source

Methods:

  • __call__ –

    Evaluate the wakefield at position z (convenience method).

  • convolve_density –

    Compute integrated wakefield from a charge density distribution.

  • default_n_samples –

    Number of uniform samples that keeps the finest spacing of the table.

  • from_bmad –

    Read the longitudinal wake from a Bmad z_long wake table.

  • from_impact_t –

    Read the longitudinal wake from an IMPACT-T wake table.

  • from_impact_z –

    Read the longitudinal wake from an IMPACT-Z wake table.

  • from_wakefield –

    Resample a wakefield model onto a uniform table.

  • impedance –

    Compute the impedance Z(k) via numerical FFT.

  • particle_kicks –

    Compute wakefield-induced energy kicks per unit length.

  • plot –

    Plot the wakefield over a range of z values.

  • plot_impedance –

    Plot the impedance over a range of wavenumbers.

  • to_bmad –

    Format this table as the value of a Bmad sr_wake with a z_long table.

  • to_impact_t –

    Build, and optionally write, the IMPACT-T wakefield table for this wake.

  • to_impact_z –

    Build, and optionally write, the IMPACT-Z wakefield table for this wake.

  • wake –

    Evaluate the wakefield at position z using interpolation.

Attributes:

  • W_data (ndarray) –

    Return the W data points.

  • default_zmax (float) –

    Range covered by the table (m).

  • min_wavelength (float) –

    Shortest length scale of the wake (m), which sets the default sampling.

  • z_data (ndarray) –

    Return the z data points.

Source code in beamphysics/wakefields/tabular.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def __init__(
    self,
    z: np.ndarray,
    W: np.ndarray,
    fill_value: float = 0.0,
    kind: str = "cubic",
) -> None:
    z = np.asarray(z)
    W = np.asarray(W)

    if z.shape != W.shape:
        raise ValueError(f"Shape mismatch: z.shape={z.shape}, W.shape={W.shape}")
    if len(z) < 4:
        raise ValueError("Need at least 4 points for cubic interpolation")
    if np.any(np.diff(z) <= 0):
        raise ValueError("z must be strictly increasing")

    # Store data
    self._z = z
    self._W = W
    self._fill_value = fill_value

    # Create interpolator
    self._interp = interp1d(
        z,
        W,
        kind=kind,
        bounds_error=False,
        fill_value=fill_value,
    )

W_data property

W_data: ndarray

Return the W data points.

default_zmax property

default_zmax: float

Range covered by the table (m).

\[z_{\max} = -\min_i z_i\]

min_wavelength property

min_wavelength: float

Shortest length scale of the wake (m), which sets the default sampling.

Raises:

  • NotImplementedError –

    If the model defines no length scale; pass n explicitly.

z_data property

z_data: ndarray

Return the z data points.

__call__

__call__(z: ndarray | float) -> ndarray | float

Evaluate the wakefield at position z (convenience method).

This is equivalent to calling wake(z).

Source code in beamphysics/wakefields/base.py
104
105
106
107
108
109
110
def __call__(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the wakefield at position z (convenience method).

    This is equivalent to calling wake(z).
    """
    return self.wake(z)

convolve_density

convolve_density(density: ndarray, dz: float, offset: float = 0, include_self_kick: bool = True, plot: bool = False, ax=None) -> ndarray

Compute integrated wakefield from a charge density distribution.

This computes the causal wake potential:

\[V(z_i) = \sum_{j>i} Q_j \cdot W(z_j - z_i) + \frac{1}{2} Q_i \cdot W(0)\]

where only particles ahead (larger z index) contribute to the wake felt by each particle. This is mathematically a correlation, not a convolution, and is computed efficiently using FFT.

Parameters:

  • density (ndarray) –

    Charge density array (C/m). Index 0 is the tail (back), index n-1 is the head (front).

  • dz (float) –

    Grid spacing (m).

  • offset (float, default: 0 ) –

    Offset for the z coordinate (m). Default is 0.

  • include_self_kick (bool, default: True ) –

    Whether to include the half self-kick term. Default is True.

  • plot (bool, default: False ) –

    If True, plot the density profile and wake potential. Default is False.

  • ax (Axes, default: None ) –

    Axes to plot on. If None and plot=True, creates a new figure.

Returns:

  • integrated_wake ( ndarray ) –

    Integrated longitudinal wakefield (V/m).

Source code in beamphysics/wakefields/base.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def convolve_density(
    self,
    density: np.ndarray,
    dz: float,
    offset: float = 0,
    include_self_kick: bool = True,
    plot: bool = False,
    ax=None,
) -> np.ndarray:
    """
    Compute integrated wakefield from a charge density distribution.

    This computes the causal wake potential:

    $$V(z_i) = \\sum_{j>i} Q_j \\cdot W(z_j - z_i) + \\frac{1}{2} Q_i \\cdot W(0)$$

    where only particles ahead (larger z index) contribute to the wake
    felt by each particle. This is mathematically a correlation, not
    a convolution, and is computed efficiently using FFT.

    Parameters
    ----------
    density : np.ndarray
        Charge density array (C/m). Index 0 is the tail (back),
        index n-1 is the head (front).
    dz : float
        Grid spacing (m).
    offset : float, optional
        Offset for the z coordinate (m). Default is 0.
    include_self_kick : bool, optional
        Whether to include the half self-kick term. Default is True.
    plot : bool, optional
        If True, plot the density profile and wake potential. Default is False.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None and plot=True, creates a new figure.

    Returns
    -------
    integrated_wake : np.ndarray
        Integrated longitudinal wakefield (V/m).
    """
    from scipy.signal import correlate

    n = len(density)
    charge = density * dz  # Convert to charge per bin (C)

    # Build causal wake array: W[k] = wake at lag k*dz
    # W[0] = W(0), W[1] = W(-dz), W[2] = W(-2*dz), ...
    z_wake = -np.arange(n) * dz + offset
    W_causal = self.wake(z_wake)

    # Correlation: result[i] = sum_k charge[i+k] * W[k]
    # This gives the wake from particles ahead (larger indices)
    corr_result = correlate(charge, W_causal, mode="full")

    # Extract the portion corresponding to non-negative lags
    # correlate output has length 2*n-1, with zero lag at index n-1
    integrated_wake = corr_result[n - 1 : 2 * n - 1]

    # Correlation includes full W[0] contribution from self
    # Remove it, then add half if self-kick is requested
    W0 = W_causal[0]
    integrated_wake = integrated_wake - charge * W0
    if include_self_kick:
        integrated_wake = integrated_wake + 0.5 * W0 * charge

    if plot:
        self._plot_convolve_density(density, dz, integrated_wake, ax=ax)

    return integrated_wake

default_n_samples

default_n_samples(zmax: float) -> int

Number of uniform samples that keeps the finest spacing of the table.

\[n = \max\left( \left\lceil \frac{z_{\max}}{\Delta z_{\min}} \right\rceil + 1, \; 4 \right)\]

Here Delta z_min is the finest spacing within -zmax <= z <= 0.

Parameters:

  • zmax (float) –

    Largest distance behind the source particle (m).

Returns:

  • int –
Source code in beamphysics/wakefields/tabular.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def default_n_samples(self, zmax: float) -> int:
    """
    Number of uniform samples that keeps the finest spacing of the table.

    $$n = \\max\\left( \\left\\lceil \\frac{z_{\\max}}{\\Delta z_{\\min}}
    \\right\\rceil + 1, \\; 4 \\right)$$

    Here Delta z_min is the finest spacing within -zmax <= z <= 0.

    Parameters
    ----------
    zmax : float
        Largest distance behind the source particle (m).

    Returns
    -------
    int
    """
    dz = np.diff(self._z)
    dz = dz[self._z[1:] > -zmax]
    # Tolerate rounding in the spacing of a nominally uniform grid.
    intervals = np.ceil(zmax / dz.min() * (1 - 1e-9))

    return max(int(intervals) + 1, MIN_SAMPLES)

from_bmad classmethod

from_bmad(source: ndarray, amp_scale: float = 1, z_scale: float = 1, kind: str = 'cubic') -> TabularWakefield

Read the longitudinal wake from a Bmad z_long wake table.

This is the inverse of to_bmad, applied to the array that pytao returns for tao.ele_wake(ele_id, who="sr_z_long_table"). Bmad extends the table symmetrically about z = 0; the rows ahead of the source particle are dropped, and the grid is rebuilt to end exactly at z = 0.

\[W(z) = \text{amp\_scale} \cdot W_\text{table}(\text{z\_scale} \cdot z)\]

Parameters:

  • source (ndarray) –

    Table of shape (n, 2) holding z (m) and W (V/C/m) on a uniform grid.

  • amp_scale (float, default: 1 ) –

    Bmad sr%amp_scale. Default is 1.

  • z_scale (float, default: 1 ) –

    Bmad sr%z_scale. Default is 1.

  • kind (str, default: 'cubic' ) –

    Interpolation method. Default is 'cubic'.

Returns:

Raises:

  • ValueError –

    If the table is empty (the element has no z_long wake), if z_scale is not positive, if the grid is not uniform or has no row at z = 0, or if the wake is nonzero ahead of the source particle.

Notes

With scale_with_length = F, Bmad reads the table in V/C; pass amp_scale divided by the structure length to obtain V/C/m. The smoothing_sigma and position_dependence settings are ignored.

Examples:

>>> rows = tao.ele_wake("pipe1", who="sr_z_long_table")
>>> table = TabularWakefield.from_bmad(rows)
Source code in beamphysics/wakefields/tabular.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
@classmethod
def from_bmad(
    cls,
    source: np.ndarray,
    amp_scale: float = 1,
    z_scale: float = 1,
    kind: str = "cubic",
) -> TabularWakefield:
    """
    Read the longitudinal wake from a Bmad z_long wake table.

    This is the inverse of `to_bmad`, applied to the array that pytao returns for
    `tao.ele_wake(ele_id, who="sr_z_long_table")`. Bmad extends the table
    symmetrically about z = 0; the rows ahead of the source particle are dropped,
    and the grid is rebuilt to end exactly at z = 0.

    $$W(z) = \\text{amp\\_scale} \\cdot W_\\text{table}(\\text{z\\_scale} \\cdot z)$$

    Parameters
    ----------
    source : np.ndarray
        Table of shape (n, 2) holding z (m) and W (V/C/m) on a uniform grid.
    amp_scale : float, optional
        Bmad `sr%amp_scale`. Default is 1.
    z_scale : float, optional
        Bmad `sr%z_scale`. Default is 1.
    kind : str, optional
        Interpolation method. Default is 'cubic'.

    Returns
    -------
    TabularWakefield

    Raises
    ------
    ValueError
        If the table is empty (the element has no z_long wake), if z_scale is not
        positive, if the grid is not uniform or has no row at z = 0, or if the
        wake is nonzero ahead of the source particle.

    Notes
    -----
    With `scale_with_length = F`, Bmad reads the table in V/C; pass amp_scale
    divided by the structure length to obtain V/C/m. The smoothing_sigma and
    position_dependence settings are ignored.

    Examples
    --------
    >>> rows = tao.ele_wake("pipe1", who="sr_z_long_table")
    >>> table = TabularWakefield.from_bmad(rows)
    """
    dat = np.asarray(source, dtype=float)
    if dat.size == 0:
        raise ValueError(
            "The table is empty. The element has no z_long wake; a wake given only "
            "by pseudomodes has no table."
        )
    if dat.ndim != 2 or dat.shape[1] != 2 or dat.shape[0] < 2:
        raise ValueError(
            f"Expected an array of shape (n, 2) holding z and W, got {dat.shape}"
        )
    if z_scale <= 0:
        raise ValueError(f"z_scale must be positive, got {z_scale}")
    z, W = dat[:, 0], dat[:, 1]

    dz = (z[-1] - z[0]) / (len(z) - 1)
    # A fraction of the spacing, so the check does not depend on Tao's print format.
    atol = 1e-2 * abs(dz)
    if dz <= 0 or not np.allclose(np.diff(z), dz, rtol=0, atol=atol):
        raise ValueError(
            "The z column must be uniformly spaced and increasing, as Bmad stores "
            "z_long tables."
        )

    ahead = z > dz / 2
    if np.any(W[ahead] != 0):
        raise ValueError(
            "The table is nonzero ahead of the source particle, up to "
            f"{np.abs(W[ahead]).max():.6e}. TabularWakefield is causal, and "
            "dropping these values would change the wake."
        )
    m = np.count_nonzero(~ahead)
    if m == 0 or abs(z[m - 1]) > atol:
        raise ValueError("The table has no row at z = 0.")

    z = -dz * np.arange(m - 1, -1, -1)

    return cls(z / z_scale, amp_scale * W[:m], kind=kind)

from_impact_t classmethod

from_impact_t(source: str | PathLike | ndarray, length: float, kind: str = 'cubic') -> TabularWakefield

Read the longitudinal wake from an IMPACT-T wake table.

The table has no position column, so its length must be supplied. The transverse columns are ignored. See beamphysics.interfaces.impact.parse_impact_t_wakefield.

Parameters:

  • source ((str, Path or ndarray)) –

    Path to the table, or the table itself as an array of shape (n, 3) or (n, 2).

  • length (float) –

    Distance covered by the table (m), given as V7 on the -6 element.

  • kind (str, default: 'cubic' ) –

    Interpolation method. Default is 'cubic'.

Returns:

Examples:

>>> table = TabularWakefield.from_impact_t("fort.41", length=3.55e-4)
Source code in beamphysics/wakefields/tabular.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
@classmethod
def from_impact_t(
    cls, source: str | os.PathLike | np.ndarray, length: float, kind: str = "cubic"
) -> TabularWakefield:
    """
    Read the longitudinal wake from an IMPACT-T wake table.

    The table has no position column, so its length must be supplied. The
    transverse columns are ignored. See
    `beamphysics.interfaces.impact.parse_impact_t_wakefield`.

    Parameters
    ----------
    source : str, pathlib.Path or np.ndarray
        Path to the table, or the table itself as an array of shape (n, 3) or
        (n, 2).
    length : float
        Distance covered by the table (m), given as V7 on the -6 element.
    kind : str, optional
        Interpolation method. Default is 'cubic'.

    Returns
    -------
    TabularWakefield

    Examples
    --------
    >>> table = TabularWakefield.from_impact_t("fort.41", length=3.55e-4)
    """
    from ..interfaces.impact import parse_impact_t_wakefield

    data = parse_impact_t_wakefield(source, length)

    return cls(-data["s"][::-1], data["Wz"][::-1], kind=kind)

from_impact_z classmethod

from_impact_z(source: str | PathLike | ndarray, kind: str = 'cubic') -> TabularWakefield

Read the longitudinal wake from an IMPACT-Z wake table.

The abscissa s = -z is mirrored into z <= 0, and the transverse columns are ignored. See beamphysics.interfaces.impact.parse_impact_z_wakefield.

Parameters:

  • source ((str, Path or ndarray)) –

    Path to the table, or the table itself as an array of shape (n, 4), such as an entry of the file_data of a lume-impact ImpactZInput.

  • kind (str, default: 'cubic' ) –

    Interpolation method. Default is 'cubic'.

Returns:

Examples:

>>> table = TabularWakefield.from_impact_z("rfdata41.in")
Source code in beamphysics/wakefields/tabular.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
@classmethod
def from_impact_z(
    cls, source: str | os.PathLike | np.ndarray, kind: str = "cubic"
) -> TabularWakefield:
    """
    Read the longitudinal wake from an IMPACT-Z wake table.

    The abscissa s = -z is mirrored into z <= 0, and the transverse columns are
    ignored. See `beamphysics.interfaces.impact.parse_impact_z_wakefield`.

    Parameters
    ----------
    source : str, pathlib.Path or np.ndarray
        Path to the table, or the table itself as an array of shape (n, 4), such
        as an entry of the `file_data` of a lume-impact `ImpactZInput`.
    kind : str, optional
        Interpolation method. Default is 'cubic'.

    Returns
    -------
    TabularWakefield

    Examples
    --------
    >>> table = TabularWakefield.from_impact_z("rfdata41.in")
    """
    from ..interfaces.impact import parse_impact_z_wakefield

    data = parse_impact_z_wakefield(source)

    # s ascends from zero, so z = -s descends. Reverse to ascend in z.
    z = -data["s"][::-1]
    W = data["Wz"][::-1]

    return cls(z, W, kind=kind)

from_wakefield classmethod

from_wakefield(wakefield: WakefieldBase, zmax: float | None = None, n: int | None = None, kind: str = 'cubic') -> TabularWakefield

Resample a wakefield model onto a uniform table.

The wake is evaluated at n equally spaced points from z = -zmax to z = 0. This makes an expensive model, such as ResistiveWallWakefield, cheap to evaluate, and gives the uniform table that IMPACT-Z and Bmad read.

Parameters:

  • wakefield (WakefieldBase) –

    Model to resample.

  • zmax (float, default: None ) –

    Largest distance behind the source particle (m). Defaults to wakefield.default_zmax.

  • n (int, default: None ) –

    Number of samples. Defaults to wakefield.default_n_samples(zmax).

  • kind (str, default: 'cubic' ) –

    Interpolation method. Default is 'cubic'.

Returns:

Raises:

  • ValueError –

    If zmax is not positive, if n < 4, or if a TabularWakefield source does not cover -zmax <= z <= 0, which would silently pad the result with zeros.

  • NotImplementedError –

    If zmax or n is omitted and the model defines no default for it.

Examples:

>>> wake = ResistiveWallPseudomode.from_material(
...     "copper-slac-pub-10707", radius=2.5e-3
... )
>>> table = TabularWakefield.from_wakefield(wake)
Source code in beamphysics/wakefields/tabular.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@classmethod
def from_wakefield(
    cls,
    wakefield: WakefieldBase,
    zmax: float | None = None,
    n: int | None = None,
    kind: str = "cubic",
) -> TabularWakefield:
    """
    Resample a wakefield model onto a uniform table.

    The wake is evaluated at n equally spaced points from z = -zmax to z = 0.
    This makes an expensive model, such as `ResistiveWallWakefield`, cheap to
    evaluate, and gives the uniform table that IMPACT-Z and Bmad read.

    Parameters
    ----------
    wakefield : WakefieldBase
        Model to resample.
    zmax : float, optional
        Largest distance behind the source particle (m). Defaults to
        `wakefield.default_zmax`.
    n : int, optional
        Number of samples. Defaults to `wakefield.default_n_samples(zmax)`.
    kind : str, optional
        Interpolation method. Default is 'cubic'.

    Returns
    -------
    TabularWakefield
        Table covering -zmax <= z <= 0.

    Raises
    ------
    ValueError
        If zmax is not positive, if n < 4, or if a `TabularWakefield` source does
        not cover -zmax <= z <= 0, which would silently pad the result with zeros.
    NotImplementedError
        If zmax or n is omitted and the model defines no default for it.

    Examples
    --------
    >>> wake = ResistiveWallPseudomode.from_material(
    ...     "copper-slac-pub-10707", radius=2.5e-3
    ... )
    >>> table = TabularWakefield.from_wakefield(wake)
    """
    if zmax is None:
        zmax = wakefield.default_zmax

    if zmax <= 0:
        raise ValueError(f"zmax must be a positive trailing distance, got {zmax}")

    if isinstance(wakefield, TabularWakefield):
        zmax_source = wakefield.default_zmax
        if zmax > zmax_source:
            raise ValueError(
                f"Requested zmax={zmax:.6e} m exceeds the {zmax_source:.6e} m "
                "covered by the source table. Resampling beyond the tabulated "
                "range would return the fill value, giving a silently zero "
                "padded result."
            )
        z_end = float(np.max(wakefield.z_data))
        if z_end < 0:
            raise ValueError(
                f"The source table ends at z={z_end:.6e} m, short of the source "
                "particle at z = 0. Resampling there would return the fill "
                "value, giving a silently zero padded result."
            )

    if n is None:
        n = wakefield.default_n_samples(zmax)

    if n < 4:
        raise ValueError(f"Need at least 4 points for interpolation, got n={n}")

    z = -np.linspace(zmax, 0.0, n)

    if isinstance(wakefield, ImpedanceWakefield):
        # The cosine transform has period L = 2*pi*(n_fft - 1)/k_max and is even,
        # so W(s) is aliased by W(L - s). L >= 2*zmax keeps the mirror beyond zmax.
        n_required = int(np.ceil(zmax * wakefield._k_max / np.pi)) + 2
        n_fft = max(4096, 1 << int(n_required - 1).bit_length())
        W = wakefield.wake(z, n_fft=n_fft)
    else:
        # The scalar branch of some models uses quadrature, which is
        # prohibitively slow point by point.
        W = wakefield.wake(z)

    return cls(z, np.asarray(W, dtype=float), kind=kind)

impedance

impedance(k: ndarray | float) -> ndarray | complex

Compute the impedance Z(k) via numerical FFT.

Uses FFT to compute the Fourier transform of the tabular wake data.

Parameters:

  • k (float or ndarray) –

    Wavenumber (1/m).

Returns:

  • Z ( complex or ndarray ) –

    Impedance (Ohm/m).

Source code in beamphysics/wakefields/tabular.py
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
def impedance(self, k: np.ndarray | float) -> np.ndarray | complex:
    """
    Compute the impedance Z(k) via numerical FFT.

    Uses FFT to compute the Fourier transform of the tabular wake data.

    Parameters
    ----------
    k : float or np.ndarray
        Wavenumber (1/m).

    Returns
    -------
    Z : complex or np.ndarray
        Impedance (Ohm/m).
    """
    k = np.asarray(k)
    scalar_input = k.ndim == 0
    k = np.atleast_1d(k)

    # Use the stored data for FFT
    z_data = self._z
    W_data = self._W

    # Sort by z (ascending)
    sort_idx = np.argsort(z_data)
    z_sorted = z_data[sort_idx]
    W_sorted = W_data[sort_idx]

    # Z(k) = (1/c) * integral of W(z) * exp(-ikz) dz
    # For each k, compute numerical integral
    Z = np.zeros(len(k), dtype=complex)
    for i, ki in enumerate(k):
        integrand = W_sorted * np.exp(-1j * ki * z_sorted)
        Z[i] = np.trapezoid(integrand, z_sorted) / c_light

    if scalar_input:
        return complex(Z[0])
    return Z

particle_kicks

particle_kicks(z: ndarray, weight: ndarray, include_self_kick: bool = True) -> ndarray

Compute wakefield-induced energy kicks per unit length.

This is a default O(N²) implementation. Subclasses may override with more efficient algorithms.

Parameters:

  • z (ndarray) –

    Particle positions (m).

  • weight (ndarray) –

    Particle charges (C).

  • include_self_kick (bool, default: True ) –

    Whether to include the self-kick term. Default is True.

Returns:

  • kicks ( ndarray ) –

    Energy kick per unit length at each particle (eV/m).

Source code in beamphysics/wakefields/base.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
def particle_kicks(
    self,
    z: np.ndarray,
    weight: np.ndarray,
    include_self_kick: bool = True,
) -> np.ndarray:
    """
    Compute wakefield-induced energy kicks per unit length.

    This is a default O(N²) implementation. Subclasses may override
    with more efficient algorithms.

    Parameters
    ----------
    z : np.ndarray
        Particle positions (m).
    weight : np.ndarray
        Particle charges (C).
    include_self_kick : bool, optional
        Whether to include the self-kick term. Default is True.

    Returns
    -------
    kicks : np.ndarray
        Energy kick per unit length at each particle (eV/m).
    """
    z = np.asarray(z)
    weight = np.asarray(weight)
    n = len(z)
    kicks = np.zeros(n)

    # O(N²) algorithm: sum contribution from all particles
    for i in range(n):
        for j in range(n):
            if i == j:
                if include_self_kick:
                    # Self-kick uses W(0⁻) / 2
                    kicks[i] -= 0.5 * weight[j] * self._self_kick_value()
            else:
                dz = z[i] - z[j]
                if dz < 0:  # j is ahead of i, so i feels wake from j
                    kicks[i] -= weight[j] * self.wake(dz)

    return kicks

plot

plot(zmax: float = None, zmin: float = 0, n: int = 200, ax=None)

Plot the wakefield over a range of z values.

Parameters:

  • zmax (float, default: None ) –

    Maximum trailing distance (m). If None, uses a sensible default.

  • zmin (float, default: 0 ) –

    Minimum trailing distance (m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/base.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def plot(self, zmax: float = None, zmin: float = 0, n: int = 200, ax=None):
    """
    Plot the wakefield over a range of z values.

    Parameters
    ----------
    zmax : float, optional
        Maximum trailing distance (m). If None, uses a sensible default.
    zmin : float, optional
        Minimum trailing distance (m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if zmax is None:
        zmax = 1e-3  # 1 mm default

    zlist = np.linspace(zmin, zmax, n)
    Wz = self.wake(-zlist)  # Negative z for trailing particles

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(zlist * 1e6, Wz * 1e-12)
    ax.set_xlabel(r"Distance behind source $|z|$ (µm)")
    ax.set_ylabel(r"$W(z)$ (V/pC/m)")

plot_impedance

plot_impedance(kmax: float = None, kmin: float = 0, n: int = 200, ax=None)

Plot the impedance over a range of wavenumbers.

Parameters:

  • kmax (float, default: None ) –

    Maximum wavenumber (1/m). If None, uses a sensible default.

  • kmin (float, default: 0 ) –

    Minimum wavenumber (1/m). Default is 0.

  • n (int, default: 200 ) –

    Number of points. Default is 200.

  • ax (Axes, default: None ) –

    Axes to plot on. If None, creates a new figure.

Source code in beamphysics/wakefields/base.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def plot_impedance(
    self, kmax: float = None, kmin: float = 0, n: int = 200, ax=None
):
    """
    Plot the impedance over a range of wavenumbers.

    Parameters
    ----------
    kmax : float, optional
        Maximum wavenumber (1/m). If None, uses a sensible default.
    kmin : float, optional
        Minimum wavenumber (1/m). Default is 0.
    n : int, optional
        Number of points. Default is 200.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates a new figure.
    """
    if kmax is None:
        kmax = 1e6  # 1/µm default

    k = np.linspace(kmin, kmax, n)
    Z = self.impedance(k)

    if ax is None:
        _, ax = plt.subplots()
    ax.plot(k * 1e-3, np.real(Z), label=r"Re[$Z$]")
    ax.plot(k * 1e-3, np.imag(Z), label=r"Im[$Z$]")
    ax.set_xlabel(r"$k$ (1/mm)")
    ax.set_ylabel(r"$Z(k)$ (Ω/m)")
    ax.legend()

to_bmad

to_bmad(file: str | PathLike | None = None, amp_scale: float = 1, z_scale: float = 1, scale_with_length: bool = True) -> str

Format this table as the value of a Bmad sr_wake with a z_long table.

Bmad needs a uniform table that ends at z = 0. Any other table is first resampled at its finest spacing, limited to 10000 rows. Bmad uses the same sign conventions and halves the z = 0 entry itself, so W(0-) is written.

Parameters:

  • file (str or Path, default: None ) –

    If given, also write the value there, for use as sr_wake = call::<file>.

  • amp_scale (float, default: 1 ) –

    Bmad amp_scale. Default is 1.

  • z_scale (float, default: 1 ) –

    Bmad z_scale. Default is 1.

  • scale_with_length (bool, default: True ) –

    If True (default), Bmad reads the table in V/C/m and multiplies it by the element length. If False, Bmad reads it in V/C, so multiply a table in V/C/m by the structure length, for example through amp_scale.

Returns:

  • str –

Warns:

  • UserWarning –

    If the resample would need more than 10000 rows.

Notes

Bmad applies the table by a circular convolution over the table extended symmetrically about z = 0, so the table must be longer than the bunch.

Examples:

>>> table.to_bmad("wake.bmad")  # sr_wake = call::wake.bmad
Source code in beamphysics/wakefields/tabular.py
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def to_bmad(
    self,
    file: str | os.PathLike | None = None,
    amp_scale: float = 1,
    z_scale: float = 1,
    scale_with_length: bool = True,
) -> str:
    """
    Format this table as the value of a Bmad `sr_wake` with a z_long table.

    Bmad needs a uniform table that ends at z = 0. Any other table is first
    resampled at its finest spacing, limited to 10000 rows. Bmad uses the same
    sign conventions and halves the z = 0 entry itself, so W(0-) is written.

    Parameters
    ----------
    file : str or pathlib.Path, optional
        If given, also write the value there, for use as `sr_wake = call::<file>`.
    amp_scale : float, optional
        Bmad `amp_scale`. Default is 1.
    z_scale : float, optional
        Bmad `z_scale`. Default is 1.
    scale_with_length : bool, optional
        If True (default), Bmad reads the table in V/C/m and multiplies it by the
        element length. If False, Bmad reads it in V/C, so multiply a table in
        V/C/m by the structure length, for example through amp_scale.

    Returns
    -------
    str

    Warns
    -----
    UserWarning
        If the resample would need more than 10000 rows.

    Notes
    -----
    Bmad applies the table by a circular convolution over the table extended
    symmetrically about z = 0, so the table must be longer than the bunch.

    Examples
    --------
    >>> table.to_bmad("wake.bmad")  # sr_wake = call::wake.bmad
    """
    dz = np.diff(self._z)
    uniform = np.allclose(dz, dz[0], rtol=1e-9, atol=0) and self._z[-1] == 0
    if uniform:
        table = self
    else:
        n = self.default_n_samples(self.default_zmax)
        if n > BMAD_MAX_AUTOMATIC_ROWS:
            warnings.warn(
                f"Preserving the finest spacing of this table needs {n} rows. The "
                f"Bmad table is resampled at {BMAD_MAX_AUTOMATIC_ROWS} rows instead, "
                "which is coarser than that spacing. Use "
                "TabularWakefield.from_wakefield with an explicit n to choose the "
                "size.",
                UserWarning,
                stacklevel=2,
            )
            n = BMAD_MAX_AUTOMATIC_ROWS
        table = TabularWakefield.from_wakefield(self, n=n)

    logical = "T" if scale_with_length else "F"
    lines = [
        f"{{amp_scale = {amp_scale}, z_scale = {z_scale}, "
        f"scale_with_length = {logical},",
        "z_long = {w = {",
    ]
    # Adding 0.0 turns -0.0 into 0.0.
    for z, W in zip(table._z + 0.0, table._W):
        lines.append(f"{z:.17e} {W:.17e},")
    lines.append("}}}")
    s = "\n".join(lines) + "\n"

    if file is not None:
        with open(file, "w") as f:
            f.write(s)

    return s

to_impact_t

to_impact_t(file: str | PathLike | None = None, zmax: float | None = None, n: int | None = None, wake_x: Callable[[ndarray], ndarray] | None = None, wake_y: Callable[[ndarray], ndarray] | None = None) -> tuple[ndarray, float]

Build, and optionally write, the IMPACT-T wakefield table for this wake.

See beamphysics.interfaces.impact.create_impact_t_wakefield_data.

Parameters:

  • file (str or Path, default: None ) –

    If given, also write the table there, conventionally as fort.{Bmpstp}.

  • zmax (float | None, default: None ) –

    As for create_impact_t_wakefield_data.

  • n (float | None, default: None ) –

    As for create_impact_t_wakefield_data.

  • wake_x (float | None, default: None ) –

    As for create_impact_t_wakefield_data.

  • wake_y (float | None, default: None ) –

    As for create_impact_t_wakefield_data.

Returns:

  • data ( ndarray ) –

    Array of shape (n, 3) with columns Wz (V/C/m), Wx and Wy (V/C/m^2).

  • length ( float ) –

    Distance covered by the table (m), to be given as V7.

Examples:

>>> data, length = table.to_impact_t("fort.41", n=1000)
Source code in beamphysics/wakefields/tabular.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
def to_impact_t(
    self,
    file: str | os.PathLike | None = None,
    zmax: float | None = None,
    n: int | None = None,
    wake_x: Callable[[np.ndarray], np.ndarray] | None = None,
    wake_y: Callable[[np.ndarray], np.ndarray] | None = None,
) -> tuple[np.ndarray, float]:
    """
    Build, and optionally write, the IMPACT-T wakefield table for this wake.

    See `beamphysics.interfaces.impact.create_impact_t_wakefield_data`.

    Parameters
    ----------
    file : str or pathlib.Path, optional
        If given, also write the table there, conventionally as `fort.{Bmpstp}`.
    zmax, n, wake_x, wake_y
        As for `create_impact_t_wakefield_data`.

    Returns
    -------
    data : np.ndarray
        Array of shape (n, 3) with columns Wz (V/C/m), Wx and Wy (V/C/m^2).
    length : float
        Distance covered by the table (m), to be given as V7.

    Examples
    --------
    >>> data, length = table.to_impact_t("fort.41", n=1000)
    """
    from ..interfaces.impact import (
        _WAKEFIELD_FMT,
        create_impact_t_wakefield_data,
    )

    data, length = create_impact_t_wakefield_data(self, zmax, n, wake_x, wake_y)

    if file is not None:
        np.savetxt(file, data, fmt=_WAKEFIELD_FMT)

    return data, length

to_impact_z

to_impact_z(file: str | PathLike | None = None, zmax: float | None = None, n: int | None = None, wake_x: Callable[[ndarray], ndarray] | None = None, wake_y: Callable[[ndarray], ndarray] | None = None) -> ndarray

Build, and optionally write, the IMPACT-Z wakefield table for this wake.

See beamphysics.interfaces.impact.create_impact_z_wakefield_rfdata.

Parameters:

  • file (str or Path, default: None ) –

    If given, also write the table there, conventionally as rfdata{file_id}.in.

  • zmax (float | None, default: None ) –

    As for create_impact_z_wakefield_rfdata.

  • n (float | None, default: None ) –

    As for create_impact_z_wakefield_rfdata.

  • wake_x (float | None, default: None ) –

    As for create_impact_z_wakefield_rfdata.

  • wake_y (float | None, default: None ) –

    As for create_impact_z_wakefield_rfdata.

Returns:

  • ndarray –

    Array of shape (n, 4) with columns s (m), Wz (V/C/m), Wx and Wy (V/C/m^2).

Examples:

>>> table.to_impact_z("rfdata41.in")
Source code in beamphysics/wakefields/tabular.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
def to_impact_z(
    self,
    file: str | os.PathLike | None = None,
    zmax: float | None = None,
    n: int | None = None,
    wake_x: Callable[[np.ndarray], np.ndarray] | None = None,
    wake_y: Callable[[np.ndarray], np.ndarray] | None = None,
) -> np.ndarray:
    """
    Build, and optionally write, the IMPACT-Z wakefield table for this wake.

    See `beamphysics.interfaces.impact.create_impact_z_wakefield_rfdata`.

    Parameters
    ----------
    file : str or pathlib.Path, optional
        If given, also write the table there, conventionally as
        `rfdata{file_id}.in`.
    zmax, n, wake_x, wake_y
        As for `create_impact_z_wakefield_rfdata`.

    Returns
    -------
    np.ndarray
        Array of shape (n, 4) with columns s (m), Wz (V/C/m), Wx and Wy (V/C/m^2).

    Examples
    --------
    >>> table.to_impact_z("rfdata41.in")
    """
    from ..interfaces.impact import (
        _WAKEFIELD_FMT,
        create_impact_z_wakefield_rfdata,
    )

    rfdata = create_impact_z_wakefield_rfdata(self, zmax, n, wake_x, wake_y)

    if file is not None:
        np.savetxt(file, rfdata, fmt=_WAKEFIELD_FMT)

    return rfdata

wake

wake(z: ndarray | float) -> ndarray | float

Evaluate the wakefield at position z using interpolation.

Parameters:

  • z (float or ndarray) –

    Longitudinal position (m).

Returns:

  • W ( float or ndarray ) –

    Interpolated wakefield value (V/C/m). Returns fill_value outside the data range, and 0 for z > 0 (causality).

Source code in beamphysics/wakefields/tabular.py
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
def wake(self, z: np.ndarray | float) -> np.ndarray | float:
    """
    Evaluate the wakefield at position z using interpolation.

    Parameters
    ----------
    z : float or np.ndarray
        Longitudinal position (m).

    Returns
    -------
    W : float or np.ndarray
        Interpolated wakefield value (V/C/m). Returns fill_value
        outside the data range, and 0 for z > 0 (causality).
    """
    z = np.asarray(z)
    scalar_input = z.ndim == 0
    z = np.atleast_1d(z)

    # Apply causality
    result = np.where(z > 0, 0.0, self._interp(z))

    if scalar_input:
        return float(result[0])
    return result

Pseudomode dataclass

Pseudomode(A: float, d: float, k: float, phi: float)

Single pseudomode parameters for a damped sinusoidal wakefield component.

Represents one term in the sum: W(z) = A·exp(d·z)·sin(k·z + φ)

Parameters:

  • A (float) –

    Amplitude coefficient (V/C/m).

  • d (float) –

    Exponential decay rate (1/m).

  • k (float) –

    Oscillation wavenumber (1/m).

  • phi (float) –

    Phase offset (rad).

Methods:

  • __call__ –

    Evaluate this mode at position z (array input assumed).

  • impedance –

    Compute the impedance Z(k) for this pseudomode analytically.

  • to_bmad –

    Format this mode as a Bmad short-range wake specification.

__call__

__call__(z: ndarray) -> ndarray

Evaluate this mode at position z (array input assumed).

Source code in beamphysics/wakefields/pseudomode.py
52
53
54
def __call__(self, z: np.ndarray) -> np.ndarray:
    """Evaluate this mode at position z (array input assumed)."""
    return self.A * np.exp(self.d * z) * np.sin(self.k * z + self.phi)

impedance

impedance(k: ndarray | float) -> ndarray | complex

Compute the impedance Z(k) for this pseudomode analytically.

For a pseudomode wakefield defined for z ≤ 0:

\[W(z) = A \cdot e^{dz} \cdot \sin(k_0 z + \phi)\]

The impedance is defined as:

\[Z(k) = \frac{1}{c} \int_{-\infty}^{0} W(z) \cdot e^{-ikz} \, dz\]

Derivation:

Substituting the wakefield:

\[Z(k) = \frac{A}{c} \int_{-\infty}^{0} e^{dz} \sin(k_0 z + \phi) e^{-ikz} \, dz\]

Using \(\sin(\theta) = \frac{e^{i\theta} - e^{-i\theta}}{2i}\):

\[Z(k) = \frac{A}{2ic} \left[ e^{i\phi} \int_{-\infty}^{0} e^{(d + i(k_0 - k))z} dz - e^{-i\phi} \int_{-\infty}^{0} e^{(d - i(k_0 + k))z} dz \right]\]

Since \(d > 0\), both integrals converge:

\[\int_{-\infty}^{0} e^{az} dz = \frac{1}{a} \quad \text{for } \text{Re}(a) > 0\]

Evaluating:

\[Z(k) = \frac{A}{2ic} \left[ \frac{e^{i\phi}}{d + i(k_0 - k)} - \frac{e^{-i\phi}}{d - i(k_0 + k)} \right]\]

Mathematica verification:

(* Define the wakefield and compute impedance numerically *)
W[z_, A_, d_, k0_, phi_] := A Exp[d z] Sin[k0 z + phi]

(* Analytical result *)
Zanalytic[k_, A_, d_, k0_, phi_] :=
    A/(2 I c) (Exp[I phi]/(d + I (k0 - k)) - Exp[-I phi]/(d - I (k0 + k)))

(* Numerical integration (should match) *)
Znumeric[k_, A_, d_, k0_, phi_] :=
    1/c NIntegrate[W[z, A, d, k0, phi] Exp[-I k z], {z, -Infinity, 0}]

(* Test with sample values *)
c = 299792458;
{A, d, k0, phi} = {1*^15, 1*^4, 1*^5, Pi/4};
ktest = 5*^4;
{Zanalytic[ktest, A, d, k0, phi], Znumeric[ktest, A, d, k0, phi]}
(* Both should give the same complex number *)

Parameters:

  • k (float or ndarray) –

    Wavenumber (1/m).

Returns:

  • Z ( complex or ndarray ) –

    Impedance (Ohm/m).

Source code in beamphysics/wakefields/pseudomode.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def impedance(self, k: np.ndarray | float) -> np.ndarray | complex:
    """
    Compute the impedance Z(k) for this pseudomode analytically.

    For a pseudomode wakefield defined for z ≤ 0:

    $$W(z) = A \\cdot e^{dz} \\cdot \\sin(k_0 z + \\phi)$$

    The impedance is defined as:

    $$Z(k) = \\frac{1}{c} \\int_{-\\infty}^{0} W(z) \\cdot e^{-ikz} \\, dz$$

    **Derivation:**

    Substituting the wakefield:

    $$Z(k) = \\frac{A}{c} \\int_{-\\infty}^{0} e^{dz} \\sin(k_0 z + \\phi) e^{-ikz} \\, dz$$

    Using $\\sin(\\theta) = \\frac{e^{i\\theta} - e^{-i\\theta}}{2i}$:

    $$Z(k) = \\frac{A}{2ic} \\left[
        e^{i\\phi} \\int_{-\\infty}^{0} e^{(d + i(k_0 - k))z} dz
        - e^{-i\\phi} \\int_{-\\infty}^{0} e^{(d - i(k_0 + k))z} dz
    \\right]$$

    Since $d > 0$, both integrals converge:

    $$\\int_{-\\infty}^{0} e^{az} dz = \\frac{1}{a} \\quad \\text{for } \\text{Re}(a) > 0$$

    Evaluating:

    $$Z(k) = \\frac{A}{2ic} \\left[
        \\frac{e^{i\\phi}}{d + i(k_0 - k)}
        - \\frac{e^{-i\\phi}}{d - i(k_0 + k)}
    \\right]$$

    **Mathematica verification:**

    ```mathematica
    (* Define the wakefield and compute impedance numerically *)
    W[z_, A_, d_, k0_, phi_] := A Exp[d z] Sin[k0 z + phi]

    (* Analytical result *)
    Zanalytic[k_, A_, d_, k0_, phi_] :=
        A/(2 I c) (Exp[I phi]/(d + I (k0 - k)) - Exp[-I phi]/(d - I (k0 + k)))

    (* Numerical integration (should match) *)
    Znumeric[k_, A_, d_, k0_, phi_] :=
        1/c NIntegrate[W[z, A, d, k0, phi] Exp[-I k z], {z, -Infinity, 0}]

    (* Test with sample values *)
    c = 299792458;
    {A, d, k0, phi} = {1*^15, 1*^4, 1*^5, Pi/4};
    ktest = 5*^4;
    {Zanalytic[ktest, A, d, k0, phi], Znumeric[ktest, A, d, k0, phi]}
    (* Both should give the same complex number *)
    ```

    Parameters
    ----------
    k : float or np.ndarray
        Wavenumber (1/m).

    Returns
    -------
    Z : complex or np.ndarray
        Impedance (Ohm/m).
    """
    k = np.asarray(k)
    scalar_input = k.ndim == 0
    k = np.atleast_1d(k)

    d = self.d
    k0 = self.k
    A = self.A
    phi = self.phi

    # Z(k) = A/(2ic) * [e^{iφ}/(d + i(k₀-k)) - e^{-iφ}/(d - i(k₀+k))]
    denom1 = d + 1j * (k0 - k)
    denom2 = d - 1j * (k0 + k)

    Z = (A / (2j * c_light)) * (
        np.exp(1j * phi) / denom1 - np.exp(-1j * phi) / denom2
    )

    if scalar_input:
        return complex(Z[0])
    return Z

to_bmad

to_bmad(type: str = 'longitudinal', transverse_dependence: str = 'none') -> str

Format this mode as a Bmad short-range wake specification.

Parameters:

  • type (str, default: 'longitudinal' ) –

    Wake type. Default is "longitudinal".

  • transverse_dependence (str, default: 'none' ) –

    Transverse dependence. Default is "none".

Returns:

  • str –

    One Bmad mode entry of the form type = {A, d, k, phi, dependence}.

Notes

Bmad gives the phase in units of 2 pi, so the fourth field is phi / (2 * pi).

Source code in beamphysics/wakefields/pseudomode.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def to_bmad(
    self,
    type: str = "longitudinal",
    transverse_dependence: str = "none",
) -> str:
    """
    Format this mode as a Bmad short-range wake specification.

    Parameters
    ----------
    type : str, optional
        Wake type. Default is "longitudinal".
    transverse_dependence : str, optional
        Transverse dependence. Default is "none".

    Returns
    -------
    str
        One Bmad mode entry of the form `type = {A, d, k, phi, dependence}`.

    Notes
    -----
    Bmad gives the phase in units of 2 pi, so the fourth field is `phi / (2 * pi)`.
    """
    return (
        f"{type} = {{{self.A}, {self.d}, {self.k}, "
        f"{self.phi / (2 * np.pi)}, {transverse_dependence}}}"
    )

Tabulation and Interchange

TabularWakefield is the common format for exchanging wakes with tracking codes. TabularWakefield.from_wakefield, or to_tabular on the resistive wall classes, resamples any model onto a uniform table. When zmax or n is omitted, each model supplies default_zmax and default_n_samples.

IMPACT-Z

IMPACT-Z reads a four-column table, rfdata{file_id}.in, through its -41 element. The builder returns an array, so that a driver such as lume-impact can decide where to write it, and the readers accept either a path or an array.

create_impact_z_wakefield_rfdata

create_impact_z_wakefield_rfdata(wakefield: WakefieldBase, zmax: float | None = None, n: int | None = None, wake_x: Callable[[ndarray], ndarray] | None = None, wake_y: Callable[[ndarray], ndarray] | None = None) -> ndarray

Build an IMPACT-Z wakefield table for a wakefield model.

The wake is sampled at n uniform distances s = -z from 0 to zmax. IMPACT-Z uses the same sign convention, so only the abscissa is mirrored.

Parameters:

  • wakefield (WakefieldBase) –

    Longitudinal wakefield model.

  • zmax (float, default: None ) –

    Largest distance behind the source particle (m). Defaults to wakefield.default_zmax.

  • n (int, default: None ) –

    Number of rows. Defaults to wakefield.default_n_samples(zmax), limited to 5000.

  • wake_x (callable, default: None ) –

    Dipole wakes (V/C/m^2) as functions of z <= 0 (m). Their columns are zero if omitted.

  • wake_y (callable, default: None ) –

    Dipole wakes (V/C/m^2) as functions of z <= 0 (m). Their columns are zero if omitted.

Returns:

  • ndarray –

    Array of shape (n, 4) with columns s (m), Wz (V/C/m), Wx and Wy (V/C/m^2).

Raises:

  • ValueError –

    If n exceeds the 5000 rows IMPACT-Z allocates.

  • NotImplementedError –

    If zmax or n is omitted and the model defines no default for it.

Warns:

  • UserWarning –

    If the default n exceeds 5000, in which case the wake is under-resolved.

Notes

IMPACT-Z extrapolates linearly beyond the last row, so the table should cover the full bunch length.

Examples:

>>> wake = ResistiveWallPseudomode.from_material(
...     "copper-slac-pub-10707", radius=2.5e-3
... )
>>> rfdata = create_impact_z_wakefield_rfdata(wake)
Source code in beamphysics/interfaces/impact.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def create_impact_z_wakefield_rfdata(
    wakefield: WakefieldBase,
    zmax: float | None = None,
    n: int | None = None,
    wake_x: Callable[[np.ndarray], np.ndarray] | None = None,
    wake_y: Callable[[np.ndarray], np.ndarray] | None = None,
) -> np.ndarray:
    """
    Build an IMPACT-Z wakefield table for a wakefield model.

    The wake is sampled at n uniform distances s = -z from 0 to zmax. IMPACT-Z uses
    the same sign convention, so only the abscissa is mirrored.

    Parameters
    ----------
    wakefield : WakefieldBase
        Longitudinal wakefield model.
    zmax : float, optional
        Largest distance behind the source particle (m). Defaults to
        `wakefield.default_zmax`.
    n : int, optional
        Number of rows. Defaults to `wakefield.default_n_samples(zmax)`, limited to
        5000.
    wake_x, wake_y : callable, optional
        Dipole wakes (V/C/m^2) as functions of z <= 0 (m). Their columns are zero if
        omitted.

    Returns
    -------
    np.ndarray
        Array of shape (n, 4) with columns s (m), Wz (V/C/m), Wx and Wy (V/C/m^2).

    Raises
    ------
    ValueError
        If n exceeds the 5000 rows IMPACT-Z allocates.
    NotImplementedError
        If zmax or n is omitted and the model defines no default for it.

    Warns
    -----
    UserWarning
        If the default n exceeds 5000, in which case the wake is under-resolved.

    Notes
    -----
    IMPACT-Z extrapolates linearly beyond the last row, so the table should cover
    the full bunch length.

    Examples
    --------
    >>> wake = ResistiveWallPseudomode.from_material(
    ...     "copper-slac-pub-10707", radius=2.5e-3
    ... )
    >>> rfdata = create_impact_z_wakefield_rfdata(wake)
    """
    s, Wz, Wx, Wy = _tabulate_impact_wakefield(
        wakefield,
        zmax,
        n,
        wake_x,
        wake_y,
        code="IMPACT-Z",
        max_rows=IMPACT_Z_MAX_WAKEFIELD_ROWS,
        limit_reason=(
            "A longer table overruns the IMPACT-Z arrays without any diagnostic."
        ),
    )

    return np.column_stack([s, Wz, Wx, Wy])

parse_impact_z_wakefield

parse_impact_z_wakefield(source: str | PathLike | ndarray) -> dict[str, ndarray]

Parse an IMPACT-Z wakefield table, as read by the -41 element.

The file rfdata{file_id}.in holds the distance s = -z >= 0 behind the source particle, Wz, Wx and Wy on a uniform grid. The abscissa is shifted to start at zero, as IMPACT-Z does.

Parameters:

  • source ((str, Path or ndarray)) –

    Path to the table, or the table itself as an array of shape (n, 4).

Returns:

  • dict –

    Keys 's' (m), 'Wz' (V/C/m), 'Wx' and 'Wy' (V/C/m^2).

Raises:

  • ValueError –

    If the table does not have four columns and at least two rows on a uniform, increasing grid. IMPACT-Z would silently misread a non-uniform grid.

Source code in beamphysics/interfaces/impact.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def parse_impact_z_wakefield(
    source: str | os.PathLike | np.ndarray,
) -> dict[str, np.ndarray]:
    """
    Parse an IMPACT-Z wakefield table, as read by the -41 element.

    The file `rfdata{file_id}.in` holds the distance s = -z >= 0 behind the source
    particle, Wz, Wx and Wy on a uniform grid. The abscissa is shifted to start at
    zero, as IMPACT-Z does.

    Parameters
    ----------
    source : str, pathlib.Path or np.ndarray
        Path to the table, or the table itself as an array of shape (n, 4).

    Returns
    -------
    dict
        Keys 's' (m), 'Wz' (V/C/m), 'Wx' and 'Wy' (V/C/m^2).

    Raises
    ------
    ValueError
        If the table does not have four columns and at least two rows on a uniform,
        increasing grid. IMPACT-Z would silently misread a non-uniform grid.
    """
    if isinstance(source, (str, os.PathLike)):
        dat = np.loadtxt(source, ndmin=2)
        label = str(source)
    else:
        dat = np.atleast_2d(np.asarray(source, dtype=float))
        label = f"the array of shape {dat.shape}"

    if dat.shape[1] != 4:
        raise ValueError(
            f"Expected 4 columns (s, Wz, Wx, Wy) in {label}, got {dat.shape[1]}"
        )
    if dat.shape[0] < 2:
        raise ValueError(
            f"Expected at least 2 rows in {label}, got {dat.shape[0]}. "
            "IMPACT-Z takes the step size from the first two rows."
        )

    s = dat[:, 0]
    ds = np.diff(s)

    if np.any(ds <= 0):
        raise ValueError(
            f"The abscissa in {label} must be strictly increasing. "
            "IMPACT-Z indexes the table arithmetically."
        )
    if not np.allclose(ds, ds[0], rtol=1e-6, atol=0):
        raise ValueError(
            f"The abscissa in {label} is not uniformly spaced: the step ranges "
            f"from {ds.min():.6e} to {ds.max():.6e} m. IMPACT-Z takes the step from "
            "the first two rows and would silently misinterpret this table."
        )

    return {
        "s": s - s[0],
        "Wz": dat[:, 1],
        "Wx": dat[:, 2],
        "Wy": dat[:, 3],
    }

write_impact_z_wakefield

write_impact_z_wakefield(filename: str | PathLike, wakefield: WakefieldBase, zmax: float | None = None, n: int | None = None, wake_x: Callable[[ndarray], ndarray] | None = None, wake_y: Callable[[ndarray], ndarray] | None = None) -> None

Write an IMPACT-Z wakefield table for a wakefield model.

The table is built by create_impact_z_wakefield_rfdata and written with 17 significant digits, so that it reads back exactly.

Parameters:

  • filename (str or Path) –

    Output path, conventionally rfdata{file_id}.in.

  • wakefield (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • zmax (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • n (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • wake_x (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • wake_y (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

Source code in beamphysics/interfaces/impact.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
def write_impact_z_wakefield(
    filename: str | os.PathLike,
    wakefield: WakefieldBase,
    zmax: float | None = None,
    n: int | None = None,
    wake_x: Callable[[np.ndarray], np.ndarray] | None = None,
    wake_y: Callable[[np.ndarray], np.ndarray] | None = None,
) -> None:
    """
    Write an IMPACT-Z wakefield table for a wakefield model.

    The table is built by `create_impact_z_wakefield_rfdata` and written with 17
    significant digits, so that it reads back exactly.

    Parameters
    ----------
    filename : str or pathlib.Path
        Output path, conventionally `rfdata{file_id}.in`.
    wakefield, zmax, n, wake_x, wake_y
        As for `create_impact_z_wakefield_rfdata`.
    """
    np.savetxt(
        filename,
        create_impact_z_wakefield_rfdata(wakefield, zmax, n, wake_x, wake_y),
        fmt=_WAKEFIELD_FMT,
    )

IMPACT-T

IMPACT-T reads a three-column table, fort.{Bmpstp}, through its -6 element. The file has no position column, so the builder also returns the length of the table, which is entered as V7.

create_impact_t_wakefield_data

create_impact_t_wakefield_data(wakefield: WakefieldBase, zmax: float | None = None, n: int | None = None, wake_x: Callable[[ndarray], ndarray] | None = None, wake_y: Callable[[ndarray], ndarray] | None = None) -> tuple[ndarray, float]

Build an IMPACT-T wakefield table for a wakefield model.

The wake is sampled as for create_impact_z_wakefield_rfdata. IMPACT-T has no position column and takes the row spacing as V7 / (n - 1), so the covered length is returned for use as V7.

Parameters:

  • wakefield (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • zmax (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • wake_x (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • wake_y (WakefieldBase) –

    As for create_impact_z_wakefield_rfdata.

  • n (int, default: None ) –

    Number of rows. Defaults to wakefield.default_n_samples(zmax), limited to 1000.

Returns:

  • data ( ndarray ) –

    Array of shape (n, 3) with columns Wz (V/C/m), Wx and Wy (V/C/m^2).

  • length ( float ) –

    Distance covered by the table (m), to be given as V7.

Raises:

  • ValueError –

    If n exceeds the 1000 rows IMPACT-T reads.

  • NotImplementedError –

    If zmax or n is omitted and the model defines no default for it.

Warns:

  • UserWarning –

    If the default n exceeds 1000, in which case the wake is under-resolved.

Notes

The -6 element reads the table when Bnseg > 0 and V6 <= 0. The three-column format requires IMPACT-T 3.0 or later.

Examples:

>>> data, length = create_impact_t_wakefield_data(wake, n=1000)
Source code in beamphysics/interfaces/impact.py
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
def create_impact_t_wakefield_data(
    wakefield: WakefieldBase,
    zmax: float | None = None,
    n: int | None = None,
    wake_x: Callable[[np.ndarray], np.ndarray] | None = None,
    wake_y: Callable[[np.ndarray], np.ndarray] | None = None,
) -> tuple[np.ndarray, float]:
    """
    Build an IMPACT-T wakefield table for a wakefield model.

    The wake is sampled as for `create_impact_z_wakefield_rfdata`. IMPACT-T has no
    position column and takes the row spacing as V7 / (n - 1), so the covered length
    is returned for use as V7.

    Parameters
    ----------
    wakefield, zmax, wake_x, wake_y
        As for `create_impact_z_wakefield_rfdata`.
    n : int, optional
        Number of rows. Defaults to `wakefield.default_n_samples(zmax)`, limited to
        1000.

    Returns
    -------
    data : np.ndarray
        Array of shape (n, 3) with columns Wz (V/C/m), Wx and Wy (V/C/m^2).
    length : float
        Distance covered by the table (m), to be given as V7.

    Raises
    ------
    ValueError
        If n exceeds the 1000 rows IMPACT-T reads.
    NotImplementedError
        If zmax or n is omitted and the model defines no default for it.

    Warns
    -----
    UserWarning
        If the default n exceeds 1000, in which case the wake is under-resolved.

    Notes
    -----
    The -6 element reads the table when Bnseg > 0 and V6 <= 0. The three-column
    format requires IMPACT-T 3.0 or later.

    Examples
    --------
    >>> data, length = create_impact_t_wakefield_data(wake, n=1000)
    """
    s, Wz, Wx, Wy = _tabulate_impact_wakefield(
        wakefield,
        zmax,
        n,
        wake_x,
        wake_y,
        code="IMPACT-T",
        max_rows=IMPACT_T_MAX_WAKEFIELD_ROWS,
        limit_reason=(
            "IMPACT-T silently ignores further rows while still taking the row "
            "spacing from V7."
        ),
    )

    return np.column_stack([Wz, Wx, Wy]), float(s[-1])

parse_impact_t_wakefield

parse_impact_t_wakefield(source: str | PathLike | ndarray, length: float) -> dict[str, ndarray]

Parse an IMPACT-T wakefield table, as read by the -6 element.

The file fort.{Bmpstp} holds Wz, Wx and Wy with no position column. Its rows are equally spaced in s = -z from 0 to length.

Parameters:

  • source ((str, Path or ndarray)) –

    Path to the table, or the table itself as an array of shape (n, 3). The two-column tables of IMPACT-T before 3.0, with one transverse column for both planes, are also accepted.

  • length (float) –

    Distance covered by the table (m), given as V7 on the -6 element.

Returns:

  • dict –

    Keys 's' (m), 'Wz' (V/C/m), 'Wx' and 'Wy' (V/C/m^2).

Raises:

  • ValueError –

    If length is not positive, or if the table does not have two or three columns and between 2 and 1000 rows.

Source code in beamphysics/interfaces/impact.py
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
def parse_impact_t_wakefield(
    source: str | os.PathLike | np.ndarray, length: float
) -> dict[str, np.ndarray]:
    """
    Parse an IMPACT-T wakefield table, as read by the -6 element.

    The file `fort.{Bmpstp}` holds Wz, Wx and Wy with no position column. Its rows
    are equally spaced in s = -z from 0 to `length`.

    Parameters
    ----------
    source : str, pathlib.Path or np.ndarray
        Path to the table, or the table itself as an array of shape (n, 3). The
        two-column tables of IMPACT-T before 3.0, with one transverse column for both
        planes, are also accepted.
    length : float
        Distance covered by the table (m), given as V7 on the -6 element.

    Returns
    -------
    dict
        Keys 's' (m), 'Wz' (V/C/m), 'Wx' and 'Wy' (V/C/m^2).

    Raises
    ------
    ValueError
        If length is not positive, or if the table does not have two or three
        columns and between 2 and 1000 rows.
    """
    if length <= 0:
        raise ValueError(f"length must be positive, got {length}")

    if isinstance(source, (str, os.PathLike)):
        dat = np.loadtxt(source, ndmin=2)
        label = str(source)
    else:
        dat = np.atleast_2d(np.asarray(source, dtype=float))
        label = f"the array of shape {dat.shape}"

    if dat.shape[1] not in (2, 3):
        raise ValueError(
            f"Expected 3 columns (Wz, Wx, Wy) or 2 columns (Wz, Wt) in {label}, "
            f"got {dat.shape[1]}"
        )
    n = dat.shape[0]
    if n < 2:
        raise ValueError(f"Expected at least 2 rows in {label}, got {n}")
    if n > IMPACT_T_MAX_WAKEFIELD_ROWS:
        raise ValueError(
            f"{label} has {n} rows, above the IMPACT-T limit of "
            f"{IMPACT_T_MAX_WAKEFIELD_ROWS}. IMPACT-T would ignore the excess rows "
            "and misplace the rest."
        )

    return {
        "s": np.linspace(0.0, length, n),
        "Wz": dat[:, 0],
        "Wx": dat[:, 1],
        "Wy": dat[:, -1],
    }

write_impact_t_wakefield

write_impact_t_wakefield(filename: str | PathLike, wakefield: WakefieldBase, zmax: float | None = None, n: int | None = None, wake_x: Callable[[ndarray], ndarray] | None = None, wake_y: Callable[[ndarray], ndarray] | None = None) -> float

Write an IMPACT-T wakefield table for a wakefield model.

The table is built by create_impact_t_wakefield_data and written with 17 significant digits.

Parameters:

  • filename (str or Path) –

    Output path, conventionally fort.{Bmpstp}.

  • wakefield (WakefieldBase) –

    As for create_impact_t_wakefield_data.

  • zmax (WakefieldBase) –

    As for create_impact_t_wakefield_data.

  • n (WakefieldBase) –

    As for create_impact_t_wakefield_data.

  • wake_x (WakefieldBase) –

    As for create_impact_t_wakefield_data.

  • wake_y (WakefieldBase) –

    As for create_impact_t_wakefield_data.

Returns:

  • float –

    Distance covered by the table (m), to be given as V7.

Source code in beamphysics/interfaces/impact.py
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
def write_impact_t_wakefield(
    filename: str | os.PathLike,
    wakefield: WakefieldBase,
    zmax: float | None = None,
    n: int | None = None,
    wake_x: Callable[[np.ndarray], np.ndarray] | None = None,
    wake_y: Callable[[np.ndarray], np.ndarray] | None = None,
) -> float:
    """
    Write an IMPACT-T wakefield table for a wakefield model.

    The table is built by `create_impact_t_wakefield_data` and written with 17
    significant digits.

    Parameters
    ----------
    filename : str or pathlib.Path
        Output path, conventionally `fort.{Bmpstp}`.
    wakefield, zmax, n, wake_x, wake_y
        As for `create_impact_t_wakefield_data`.

    Returns
    -------
    float
        Distance covered by the table (m), to be given as V7.
    """
    data, length = create_impact_t_wakefield_data(wakefield, zmax, n, wake_x, wake_y)
    np.savetxt(filename, data, fmt=_WAKEFIELD_FMT)

    return length

Bmad

TabularWakefield.to_bmad formats a table as the value of a Bmad sr_wake, and TabularWakefield.from_bmad reads back the table that pytao returns for tao.ele_wake(ele_id, who="sr_z_long_table"). The table must be longer than the bunch, because Bmad applies it by a circular convolution.

Low-level Functions

longitudinal_impedance_round

longitudinal_impedance_round(k: float | ndarray, a: float, sigma0: float, ctau: float) -> complex | ndarray

Compute longitudinal impedance Z(k) for round (circular pipe) geometry.

Uses closed-form expression (SLAC-PUB-10707 Eq. 2).

Parameters:

  • k (float or ndarray) –

    Longitudinal wave number (1/m)

  • a (float) –

    Pipe radius (m)

  • sigma0 (float) –

    DC conductivity (S/m)

  • ctau (float) –

    Relaxation distance c·τ (m)

Returns:

  • Zk ( complex or np.ndarray of complex ) –

    Longitudinal impedance (Ohm/m)

Source code in beamphysics/wakefields/resistive_wall/base.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def longitudinal_impedance_round(
    k: float | np.ndarray,
    a: float,
    sigma0: float,
    ctau: float,
) -> complex | np.ndarray:
    """
    Compute longitudinal impedance Z(k) for round (circular pipe) geometry.

    Uses closed-form expression (SLAC-PUB-10707 Eq. 2).

    Parameters
    ----------
    k : float or np.ndarray
        Longitudinal wave number (1/m)
    a : float
        Pipe radius (m)
    sigma0 : float
        DC conductivity (S/m)
    ctau : float
        Relaxation distance c·τ (m)

    Returns
    -------
    Zk : complex or np.ndarray of complex
        Longitudinal impedance (Ohm/m)
    """
    k = np.asarray(k)
    scalar_input = k.ndim == 0
    k = np.atleast_1d(k)

    prefactor = Z0 / (2 * np.pi * a)
    zeta = surface_impedance(k, sigma0, ctau)

    with np.errstate(divide="ignore", invalid="ignore"):
        Zk = prefactor / (1.0 / zeta - 1j * k * a / 2)
        Zk = np.where(k == 0, 0.0 + 0.0j, Zk)

    if scalar_input:
        return complex(Zk[0])
    return Zk

longitudinal_impedance_flat

longitudinal_impedance_flat(k: float | ndarray, a: float, sigma0: float, ctau: float) -> complex | ndarray

Compute longitudinal impedance Z(k) for flat (parallel plate) geometry.

Uses numerical integration over the transverse wavenumber (SLAC-PUB-10707 Eq. 13, after Henke and Napoly).

Parameters:

  • k (float or ndarray) –

    Longitudinal wave number (1/m)

  • a (float) –

    Half-gap height between parallel plates (m)

  • sigma0 (float) –

    DC conductivity (S/m)

  • ctau (float) –

    Relaxation distance c·τ (m)

Returns:

  • Zk ( complex or np.ndarray of complex ) –

    Longitudinal impedance (Ohm/m)

Source code in beamphysics/wakefields/resistive_wall/base.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def longitudinal_impedance_flat(
    k: float | np.ndarray,
    a: float,
    sigma0: float,
    ctau: float,
) -> complex | np.ndarray:
    """
    Compute longitudinal impedance Z(k) for flat (parallel plate) geometry.

    Uses numerical integration over the transverse wavenumber (SLAC-PUB-10707
    Eq. 13, after Henke and Napoly).

    Parameters
    ----------
    k : float or np.ndarray
        Longitudinal wave number (1/m)
    a : float
        Half-gap height between parallel plates (m)
    sigma0 : float
        DC conductivity (S/m)
    ctau : float
        Relaxation distance c·τ (m)

    Returns
    -------
    Zk : complex or np.ndarray of complex
        Longitudinal impedance (Ohm/m)
    """

    @np.vectorize
    def _Zk_scalar(k_val):
        if k_val == 0:
            return 0.0 + 0.0j

        def integrand(x):
            return _impedance_integrand_flat(k_val, x, a, sigma0, ctau)

        return quad_vec(integrand, 0, np.inf)[0]

    return _Zk_scalar(k)

wakefield_from_impedance

wakefield_from_impedance(z: float | ndarray, Zk_func: callable, k_max: float = 10000000.0, epsabs: float = 1e-09, epsrel: float = 1e-06) -> float | ndarray

Compute wakefield W(z) from Re[Z(k)] using a cosine transform (quadrature).

For z ≤ 0 (trailing particles):

\[W(z) = \frac{2c}{\pi} \int_0^{k_{\max}} \text{Re}[Z(k)] \cos(kz) \, dk\]

Returns 0 for z > 0 (causality).

Parameters:

  • z (float or ndarray) –

    Longitudinal position (m). Negative z is behind the source.

  • Zk_func (callable) –

    Function returning complex impedance Z(k) (Ohm/m)

  • k_max (float, default: 10000000.0 ) –

    Upper limit of k integration (1/m). Default is 1e7.

  • epsabs (float, default: 1e-09 ) –

    Absolute tolerance. Default is 1e-9.

  • epsrel (float, default: 1e-06 ) –

    Relative tolerance. Default is 1e-6.

Returns:

  • Wz ( float or ndarray ) –

    Wakefield (V/C/m)

Source code in beamphysics/wakefields/resistive_wall/base.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
def wakefield_from_impedance(
    z: float | np.ndarray,
    Zk_func: callable,
    k_max: float = 1e7,
    epsabs: float = 1e-9,
    epsrel: float = 1e-6,
) -> float | np.ndarray:
    """
    Compute wakefield W(z) from Re[Z(k)] using a cosine transform (quadrature).

    For z ≤ 0 (trailing particles):

    $$W(z) = \\frac{2c}{\\pi} \\int_0^{k_{\\max}} \\text{Re}[Z(k)] \\cos(kz) \\, dk$$

    Returns 0 for z > 0 (causality).

    Parameters
    ----------
    z : float or np.ndarray
        Longitudinal position (m). Negative z is behind the source.
    Zk_func : callable
        Function returning complex impedance Z(k) (Ohm/m)
    k_max : float, optional
        Upper limit of k integration (1/m). Default is 1e7.
    epsabs : float, optional
        Absolute tolerance. Default is 1e-9.
    epsrel : float, optional
        Relative tolerance. Default is 1e-6.

    Returns
    -------
    Wz : float or np.ndarray
        Wakefield (V/C/m)
    """

    def _wakefield_scalar(z_val):
        if z_val > 0:
            return 0.0

        def integrand(k):
            return np.real(Zk_func(k)) * np.cos(k * z_val)

        result, _ = quad(integrand, 0, k_max, epsabs=epsabs, epsrel=epsrel, limit=200)
        return (2 * c_light / np.pi) * result

    z = np.asarray(z)
    if z.ndim == 0:
        return _wakefield_scalar(float(z))

    return np.array([_wakefield_scalar(zi) for zi in z])

wakefield_from_impedance_fft

wakefield_from_impedance_fft(z: ndarray, Zk_func: callable, k_max: float = 10000000.0, n_fft: int = 8192) -> ndarray

Compute wakefield W(z) from Z(k) using FFT and interpolation.

Much faster than wakefield_from_impedance for arrays.

Parameters:

  • z (ndarray) –

    Longitudinal positions (m). Negative z is behind the source.

  • Zk_func (callable) –

    Function returning complex impedance Z(k) (Ohm/m)

  • k_max (float, default: 10000000.0 ) –

    Upper limit of k integration (1/m). Default is 1e7.

  • n_fft (int, default: 8192 ) –

    Number of FFT points. Default is 8192.

Returns:

  • Wz ( ndarray ) –

    Wakefield (V/C/m)

Source code in beamphysics/wakefields/resistive_wall/base.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def wakefield_from_impedance_fft(
    z: np.ndarray,
    Zk_func: callable,
    k_max: float = 1e7,
    n_fft: int = 8192,
) -> np.ndarray:
    """
    Compute wakefield W(z) from Z(k) using FFT and interpolation.

    Much faster than `wakefield_from_impedance` for arrays.

    Parameters
    ----------
    z : np.ndarray
        Longitudinal positions (m). Negative z is behind the source.
    Zk_func : callable
        Function returning complex impedance Z(k) (Ohm/m)
    k_max : float, optional
        Upper limit of k integration (1/m). Default is 1e7.
    n_fft : int, optional
        Number of FFT points. Default is 8192.

    Returns
    -------
    Wz : np.ndarray
        Wakefield (V/C/m)
    """
    from scipy.fft import irfft
    from scipy.interpolate import interp1d

    z = np.asarray(z)

    dk = k_max / (n_fft - 1)
    k_grid = np.linspace(0, k_max, n_fft)

    Zk_grid = Zk_func(k_grid)
    ReZ = np.real(Zk_grid)

    n_full = 2 * (n_fft - 1)
    dz = 2 * np.pi / (n_full * dk)
    z_grid = np.arange(n_full) * dz

    W_grid = irfft(ReZ, n=n_full) * n_full * dk * (c_light / np.pi)

    interp = interp1d(z_grid, W_grid, kind="cubic", bounds_error=False, fill_value=0.0)

    result = interp(-z)
    result = np.where(z > 0, 0.0, result)

    return result

ac_conductivity

ac_conductivity(k: float | ndarray, sigma0: float, ctau: float) -> complex | ndarray

Frequency-dependent AC conductivity with relaxation time (Drude model).

\[\sigma(k) = \frac{\sigma_0}{1 - i k c \tau}\]

Parameters:

  • k (float or ndarray) –

    Longitudinal wave number (1/m)

  • sigma0 (float) –

    DC conductivity (S/m)

  • ctau (float) –

    Relaxation distance c·τ (m)

Returns:

  • sigma ( complex or np.ndarray of complex ) –

    AC conductivity (S/m)

Source code in beamphysics/wakefields/resistive_wall/base.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def ac_conductivity(
    k: float | np.ndarray,
    sigma0: float,
    ctau: float,
) -> complex | np.ndarray:
    """
    Frequency-dependent AC conductivity with relaxation time (Drude model).

    $$\\sigma(k) = \\frac{\\sigma_0}{1 - i k c \\tau}$$

    Parameters
    ----------
    k : float or np.ndarray
        Longitudinal wave number (1/m)
    sigma0 : float
        DC conductivity (S/m)
    ctau : float
        Relaxation distance c·τ (m)

    Returns
    -------
    sigma : complex or np.ndarray of complex
        AC conductivity (S/m)
    """
    return sigma0 / (1 - 1j * k * ctau)

surface_impedance

surface_impedance(k: float | ndarray, sigma0: float, ctau: float) -> complex | ndarray

Surface impedance for a conducting wall with AC conductivity.

\[\zeta(k) = (1 - i) \sqrt{\frac{k c}{2 \sigma(k) Z_0 c}}\]

Parameters:

  • k (float or ndarray) –

    Longitudinal wave number (1/m)

  • sigma0 (float) –

    DC conductivity (S/m)

  • ctau (float) –

    Relaxation distance c·τ (m)

Returns:

  • zeta ( complex or np.ndarray of complex ) –

    Surface impedance (dimensionless)

Source code in beamphysics/wakefields/resistive_wall/base.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def surface_impedance(
    k: float | np.ndarray,
    sigma0: float,
    ctau: float,
) -> complex | np.ndarray:
    """
    Surface impedance for a conducting wall with AC conductivity.

    $$\\zeta(k) = (1 - i) \\sqrt{\\frac{k c}{2 \\sigma(k) Z_0 c}}$$

    Parameters
    ----------
    k : float or np.ndarray
        Longitudinal wave number (1/m)
    sigma0 : float
        DC conductivity (S/m)
    ctau : float
        Relaxation distance c·τ (m)

    Returns
    -------
    zeta : complex or np.ndarray of complex
        Surface impedance (dimensionless)
    """
    sigma = ac_conductivity(k, sigma0, ctau)
    return (1 - 1j) * np.sqrt(k * c_light / (2 * sigma * Z0 * c_light))

characteristic_length

characteristic_length(a: float, sigma0: float) -> float

Characteristic length scale s₀ for resistive wall wakefield.

From SLAC-PUB-10707 Eq. (5):

\[s_0 = \left( \frac{2 a^2}{Z_0 \sigma_0} \right)^{1/3}\]

Parameters:

  • a (float) –

    Half-gap height (flat) or radius (round) (m)

  • sigma0 (float) –

    DC conductivity (S/m)

Returns:

  • s0 ( float ) –

    Characteristic length (m)

Source code in beamphysics/wakefields/resistive_wall/base.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def characteristic_length(a: float, sigma0: float) -> float:
    """
    Characteristic length scale s₀ for resistive wall wakefield.

    From SLAC-PUB-10707 Eq. (5):

    $$s_0 = \\left( \\frac{2 a^2}{Z_0 \\sigma_0} \\right)^{1/3}$$

    Parameters
    ----------
    a : float
        Half-gap height (flat) or radius (round) (m)
    sigma0 : float
        DC conductivity (S/m)

    Returns
    -------
    s0 : float
        Characteristic length (m)
    """
    return (2 * a**2 / (Z0 * sigma0)) ** (1 / 3)