@@ -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