Source code for at.collective.haissinski

import numpy
from at import radiation_parameters
from at.constants import clight, qe
from scipy.interpolate import interp1d
import time
from at.collective import Wake
from at.lattice import Lattice


[docs] class Haissinski(object): r""" Class to find the longitudinal distribution in the presence of a short range wakefield. Parameters: wake_object: class initialized with Wake that contains a longitudinal wake Z and equivalent srange. ring: a lattice object that is used to extract machine parameters Keyword Args: m (int): the number of points in the full distribution kmax: the min and max of the range of the distribution. See equation 27. In units of [:math:`\sigma_z0`] current: bunch current. numIters (int): the number of iterations eps: the convergence criteria. This class is a direct implementation of the following paper: "Numerical solution of the Haïssinski equation for the equilibrium state of a stored electron beam", R. Warnock, K.Bane, Phys. Rev. Acc. and Beams 21, 124401 (2018) The reader is referred to this paper for a description of the methods. The equation number of key formula are written next to the relevant function. The functions solve or solve_steps can be used after initialisation An example usage can be found in: at/pyat/examples/Collective/LongDistribution.py Future developments of this class: Adding LR wake or harmonic cavity as done at SOLEIL. Needs to be added WITH this class which is just for short range wake. """ def __init__(self, wake_object: Wake, ring: Lattice, m: int = 12, kmax: float = 1, current: float = 1e-4, numIters: int = 10, eps: float = 1e-10): self.circumference = ring.circumference self.energy = ring.energy radpars = radiation_parameters(ring) self.f_s = radpars.f_s self.nu_s = self.f_s / (clight/self.circumference) self.sigma_e = radpars.sigma_e * self.energy self.sigma_l = radpars.sigma_l # The paper uses alpha and eta with same sign. # AT uses opposite signs. So negative sign is needed for eta. self.eta = -ring.radiation_off(copy=True).slip_factor self.ga = ring.gamma self.betrel = ring.beta self.numIters = numIters self.eps = eps # negative s to be consistent with paper and negative Wz s = wake_object.srange/self.sigma_l self.ds = numpy.diff(s)[-1] self.s = -s self.wtot_fun = interp1d(self.s, -wake_object.Z, bounds_error=False, fill_value = 0) if m % 2 != 0: raise AttributeError('m must be even and int') self.m = m self.npoints = 2*self.m + 1 self.kmax = kmax self.dq = self.kmax/self.m self.q_array = -self.kmax + numpy.arange(self.npoints)*self.dq self.set_weights() self.dtFlag = False self.set_I(current) self.initial_phi() try: self.Sfun except AttributeError: self.precompute_S() self.compute_Smat()
[docs] def set_weights(self): ''' Page 7 second paragraph, in the text ''' self.weights = numpy.ones(self.npoints)*2 self.weights[1::2] = self.weights[1::2]**2 self.weights[0] = 1 self.weights[-1] = 1 self.weights *= self.dq/3
[docs] def precompute_S(self): ''' Equation 16 ''' print('Computing integrated wake potential') sr = numpy.arange(2*numpy.amin(self.q_array), numpy.abs(2*numpy.amin(self.q_array)) + self.ds, self.ds) res = numpy.zeros(len(sr)) topend = numpy.trapz(self.wtot_fun(numpy.arange(numpy.amax(sr), numpy.amax(self.s), self.ds)), x=numpy.arange(numpy.amax(sr), numpy.amax(self.s), self.ds)) for p, low in enumerate(sr): if p % 10000 == 0: print(p, ' out of ', len(sr)) rr = numpy.arange(low, numpy.amax(sr), self.ds) val = numpy.trapz(self.wtot_fun(rr), x=rr) + topend res[p] = val # Not used except for plotting self.Sfun_range = sr self.Sfun = interp1d(sr, res)
[docs] def compute_Smat(self): ''' The sampling of the integrated wake potential S is only made at certain places. So all possibilities are loaded into a matrix for speed. ''' self.Smat = numpy.zeros((self.npoints, self.npoints)) for iq1, q1 in enumerate(self.q_array): for iq2, q2 in enumerate(self.q_array): self.Smat[iq1, iq2] = self.Sfun(q1-q2)
[docs] def set_I(self, current): ''' Equation 11 ''' self.N = current*self.circumference/(clight*self.betrel*qe) self.Ic = numpy.sign(self.eta)*qe*self.N/(2*numpy.pi * self.nu_s*self.sigma_e) # removed one qe for eV to joule print('Normalised current: ', self.Ic*1e12, ' pC/V')
[docs] def initial_phi(self): ''' Simply a gaussian but using the normalised units. Page 5 top right, in the text. ''' self.phi = numpy.exp(-self.q_array**2/2)*self.Ic/numpy.sqrt(2*numpy.pi) self.phi_0 = self.phi.copy()
[docs] def Fi(self): ''' Equation 28 ''' self.allFi = numpy.zeros(self.npoints) for i in numpy.arange(self.npoints): sum1 = 0 for j in numpy.arange(self.npoints): sum1b = 0 for k in numpy.arange(self.npoints): sum1b += self.weights[k] * self.Smat[j, k] * self.phi[k] sum1 += self.weights[j] * \ numpy.exp(-self.q_array[j]**2/2 + sum1b) sum2 = 0 for j in numpy.arange(self.npoints): sum2 += self.weights[j] * self.Smat[i, j] * self.phi[j] self.allFi[i] = self.phi[i] * sum1 - self.Ic * \ numpy.exp(-self.q_array[i]**2/2 + sum2)
[docs] def dFi_ij(self, i, j): sum1 = 0 for k in numpy.arange(self.npoints): sum1b = 0 for li in numpy.arange(self.npoints): sum1b += self.weights[li] * self.Smat[k, li] * self.phi[li] # in paper i starts from 1, but kron(0,0) is 0 in python. # No good for python, so i need this to be able to loop from 0 kron = 0 if i != j else 1 sum1 += self.weights[k] * \ (kron + self.phi[i] * self.weights[j] * self.Smat[k, j]) * \ numpy.exp(-self.q_array[k]**2/2 + sum1b) sum2 = 0 for k in numpy.arange(self.npoints): sum2 += self.weights[k] * self.Smat[i, k] * self.phi[k] val = sum1 - self.Ic*self.weights[j] * self.Smat[i, j] \ * numpy.exp(-self.q_array[i]**2/2 + sum2) return val
[docs] def dFi_dphij(self): ''' Equation 30 ''' if not self.dtFlag: t0 = time.time() self.dFi_ij(0, 0) dt = time.time() - t0 print('Computing dFi/dphij, will take approximately ', numpy.round(dt*self.npoints**2/60, 3), ' minutes per iteration') self.dtFlag = True allDat = [self.dFi_ij(i, j) for i in numpy.arange(self.npoints) for j in numpy.arange(self.npoints)] self.alldFi_dphij = numpy.reshape(allDat, (self.npoints, self.npoints))
[docs] def compute_new_phi(self): ainv = numpy.linalg.inv(self.alldFi_dphij) self.pseudo_inv = numpy.dot(ainv, -self.allFi) self.phi_1 = self.pseudo_inv + self.phi
[docs] def update(self): self.phi = self.phi_1.copy()
[docs] def convergence(self): ''' Equation 32 ''' self.conv = (numpy.sum(numpy.abs(self.phi_1 - self.phi))) \ / numpy.sum(numpy.abs(self.phi))
[docs] def set_output(self): self.res = (self.phi_1.copy())[::-1]
[docs] def solve(self): for it in numpy.arange(self.numIters): self.Fi() self.dFi_dphij() self.compute_new_phi() self.convergence() self.set_output() print('Iteration: ', it, ', Delta: ', self.conv) if self.conv < self.eps: print('Converged') break if it != self.numIters-1: self.update() if self.conv > self.eps: print('Did not converge, maybe solve for an intermediate ' 'current then use the solution as a starting point')
[docs] def solve_steps(self, currents): ''' INPUT: currents an array of currents to solve. If 0 is given, a current of 10uA is used to prevent failure. ''' self.I_steps = numpy.zeros(len(currents)) self.res_steps = numpy.zeros((len(currents), len(self.q_array))) for ii, Ib in enumerate(currents): print('Running step ', ii+1, ' out of ', len(currents)) # If Ib = 0, the gaussian is zero. Small epsilon is given if Ib == 0: self.set_I(1e-5) else: self.set_I(Ib) self.I_steps[ii] = self.Ic self.solve() self.res_steps[ii, :] = self.res