Skip to content

Commit f835bf5

Browse files
Add algorithm notes
1 parent dcb85f7 commit f835bf5

File tree

1 file changed

+33
-11
lines changed

1 file changed

+33
-11
lines changed

pmd_beamphysics/wakefields/resistive_wall.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,22 @@ def particle_kicks(
260260
include_self_kick: bool = True,
261261
) -> np.ndarray:
262262
"""
263-
Compute short-range wakefield energy kicks per unit length
263+
Compute short-range wakefield energy kicks per unit length.
264264
265-
Internally the particles will be sorted.
266-
This should take negligible time compared with the algorithm.
265+
Uses an O(N) single-pass algorithm by exploiting the exponential form
266+
of the pseudomode wakefield. The key insight is that the total wake
267+
at particle i from all trailing particles j > i can be written as:
268+
269+
ΔE_i = -Im[ c·e^(s·z_i) · Σ_{j>i} q_j·e^(-s·z_j) ]
270+
271+
where s = d + ik and c = A·e^(iφ). By iterating from tail to head
272+
and maintaining a running sum b = Σ q_j·e^(-s·z_j), each particle's
273+
kick is computed in O(1) time, giving O(N) total complexity.
267274
268275
Parameters
269276
----------
270277
z : ndarray of shape (N,)
271-
Particle positions [m], sorted from tail to head (increasing).
278+
Particle positions [m], need not be sorted.
272279
weight : ndarray of shape (N,)
273280
Particle charges [C].
274281
include_self_kick : bool, optional
@@ -278,6 +285,18 @@ def particle_kicks(
278285
-------
279286
delta_E : ndarray of shape (N,)
280287
Wake-induced energy kick per unit length at each particle [eV/m].
288+
289+
Notes
290+
-----
291+
The algorithm proceeds as follows:
292+
293+
1. Sort particles by z (tail to head)
294+
2. Initialize complex accumulator b = 0
295+
3. Loop from tail (i = N-1) to head (i = 0):
296+
a. Compute kick: ΔE_i = -Im[c·e^(s·z_i)·b]
297+
b. Update accumulator: b += q_i·e^(-s·z_i)
298+
4. Add self-kick if requested: ΔE_i -= ½·A·q_i·sin(φ)
299+
5. Restore original particle ordering
281300
"""
282301

283302
z = np.asarray(z)
@@ -290,28 +309,31 @@ def particle_kicks(
290309
if z.ndim != 1:
291310
raise ValueError("z and weight must be 1D arrays")
292311

293-
# Sort
312+
# Sort particles from tail to head
294313
ix = z.argsort()
295314
z = z[ix]
296-
z -= z.max() # Offset to avoid numerical problems
315+
z -= z.max() # Offset to keep exponents small for numerical stability
297316
weight = weight[ix]
298317

299318
N = len(z)
300319
delta_E = np.zeros(N)
301320

302-
s = self.d + 1j * self.k
303-
c = self.A * np.exp(1j * self.phi)
321+
# Precompute complex coefficients for the pseudomode W(z) = A·e^(dz)·sin(kz+φ)
322+
s = self.d + 1j * self.k # Complex decay+oscillation rate
323+
c = self.A * np.exp(1j * self.phi) # Amplitude with phase
304324

305-
b = 0.0 + 0.0j # complex accumulator
325+
# O(N) accumulator: b = Σ_{j>i} q_j·e^(-s·z_j) for particles behind current
326+
b = 0.0 + 0.0j
306327

328+
# Iterate from tail (large z index) to head (small z index)
307329
for i in range(N - 1, -1, -1):
308330
zi = z[i]
309331
qi = weight[i]
310332

311-
# Wake from trailing particles
333+
# Kick from all trailing particles: ΔE_i = -Im[c·e^(s·z_i)·b]
312334
delta_E[i] -= np.imag(c * np.exp(s * zi) * b)
313335

314-
# Accumulate this particle's contribution
336+
# Add this particle to the accumulator for the next iteration
315337
b += qi * np.exp(-s * zi)
316338

317339
if include_self_kick:

0 commit comments

Comments
 (0)