diff --git a/tracking/element.py b/tracking/element.py index bee031ce02323679e53832f4745196ef33dc1666..d7f7e44bdca7c13dbfa521f7cb288dd365709cc3 100644 --- a/tracking/element.py +++ b/tracking/element.py @@ -9,9 +9,13 @@ included in the tracking. """ import numpy as np +import pandas as pd from abc import ABCMeta, abstractmethod from functools import wraps from tracking.particles import Beam +from scipy import signal +import matplotlib.pyplot as plt +from scipy.interpolate import interp1d class Element(metaclass=ABCMeta): """ @@ -95,6 +99,203 @@ class LongitudinalMap(Element): bunch["delta"] -= self.ring.U0 / self.ring.E0 bunch["tau"] -= self.ring.ac * self.ring.T0 * bunch["delta"] +class WakePotential(Element): + """ + Resonator model based wake potential calculation for one turn. + + Parameters + ---------- + ring : Synchrotron object. + Q_factor : float, optional + Resonator quality factor. The default value is 1. + f_res : float, optional + Resonator resonance frequency in [Hz]. The default value is 10e9 Hz. + R_shunt : float, optional + Resonator shunt impedance in [Ohm]. The default value is 100 Ohm. + n_bin : int, optional + Number of bins for constructing the longitudinal bunch profile. + The default is 65. + + Attributes + ---------- + rho : array of shape (n_bin, ) + Bunch charge density profile. + tau : array of shape (n_bin + time_extra, ) + Time array starting from the head of the bunch until the wake tail + called timestop. + + The length of time_extra is determined by the last position of the + bunch time_bunch[-1], timestop, and the mean bin width of the bunch + profile mean_bin_size as + len(time_extra) = (timestop - time_bunch[-1]) / mean_bin_size + W_long : array of shape (n_bin + time_extra, ) + Wakefunction profile. + W_p : array of shape (n_bin + time_extra, ) + Wake potential profile. + wp : array of shape (mp_number, ) + Wake potential exerted on each macro-particle. + + Methods + ------- + charge_density(bunch, n_bin=65) + Calculate bunch charge density. + plot(self, var, plot_rho=True) + Plotting wakefunction or wake potential. + track(bunch) + Tracking method for the element. + + """ + + def __init__(self, ring, Q_factor=1, f_res=10e9, R_shunt=100, n_bin=65): + self.ring = ring + self.n_bin = n_bin + + self.Q_factor = Q_factor + self.omega_res = 2*np.pi*f_res + self.R_shunt = R_shunt + + if Q_factor >= 0.5: + self.Q_factor_p = np.sqrt(self.Q_factor**2 - 0.25) + self.omega_res_p = (self.omega_res*self.Q_factor_p)/self.Q_factor + else: + self.Q_factor_pp = np.sqrt(0.25 - self.Q_factor**2) + self.omega_res_p = (self.omega_res*self.Q_factor_pp)/self.Q_factor + + def charge_density(self, bunch, n_bin): + self.bins = bunch.binning(n_bin=self.n_bin) + self.bin_data = self.bins[2] + self.bin_size = self.bins[0].length + + self.rho = bunch.charge_per_mp*self.bin_data/ \ + (self.bin_size*bunch.charge) + + def init_timestop(self): + self.timestop = round(np.log(1000)/self.omega_res*2*self.Q_factor, 12) + + def time_array(self): + time_bunch = self.bins[0].mid + mean_bin_size = np.mean(self.bin_size) + time_extra = np.arange(start = time_bunch[-1]+mean_bin_size, + stop = self.timestop, step = mean_bin_size) + + self.tau = np.concatenate((time_bunch,time_extra)) + + def long_wakefunction(self): + w_list = [] + if self.Q_factor >= 0.5: + for t in self.tau: + if t >= 0: + w_long = -(self.omega_res*self.R_shunt/self.Q_factor)*\ + np.exp(-self.omega_res*t/(2*self.Q_factor))*\ + (np.cos(self.omega_res_p*t)-\ + np.sin(self.omega_res_p*t)/(2*self.Q_factor_p)) + else: + w_long = 0 + + w_list.append(w_long) + + elif self.Q_factor < 0.5: + for t in self.tau: + if t >= 0: + w_long = -(self.omega_res*self.R_shunt/self.Q_factor)*\ + np.exp(-self.omega_res*t/(2*self.Q_factor))*\ + (np.cosh(self.omega_res_p*t)-\ + np.sinh(self.omega_res_p*t)/(2*self.Q_factor_pp)) + else: + w_long = 0 + + w_list.append(w_long) + + self.W_long = np.array(w_list) + + def wake_potential(self): + self.W_p = signal.convolve(self.W_long*1e-12, self.rho, mode="same") + + def plot(self, var, plot_rho=True): + """ + Plotting wakefunction or wake potential. + + Parameters + ---------- + var : {'W_p', 'W_long' } + If 'W_p', the wake potential is plotted. + If 'W_long', the wakefunction is plotted. + plot_rho : bool, optional + Overlay the bunch charge density profile on the plot. + The default is True. + + Returns + ------- + fig : Figure + Figure object with the plot on it. + + """ + + fig, ax = plt.subplots() + + if var == "W_p": + ax.plot(self.tau*1e12, self.W_p*1e-12) + + ax.set_xlabel("$\\tau$ (ps)") + ax.set_ylabel("W$_p$ (V/pC)") + + elif var == "W_long": + ax.plot(self.tau*1e12, self.W_long*1e-12) + ax.set_xlabel("$\\tau$ (ps)") + ax.set_ylabel("W$_{||}$ ($\\Omega$/ps)") + + if plot_rho is True: + rho_array = np.array(self.rho) + rho_rescaled = rho_array/max(rho_array)*max(self.W_p) + + ax.plot(self.bins[0].mid*1e12, rho_rescaled*1e-12) + + else: + pass + + return fig + + def check_wake_tail(self): + """ + Checking whether the full wakefunction is obtained by the calculated + initial timestop. + + """ + + ratio = np.abs(min(self.W_long) / self.W_long[-6:-1]) + while any(ratio < 1000): + # Initial timestop is too short. + # Extending timestop by 50 ps and recalculating." + self.timestop += 50e-12 + self.time_array() + self.long_wakefunction() + ratio = np.abs(min(self.W_long) / self.W_long[-6:-1]) + + @Element.parallel + def track(self, bunch): + """ + Tracking method for the element. + No bunch to bunch interaction, so written for Bunch objects and + @Element.parallel is used to handle Beam objects. + + Parameters + ---------- + bunch : Bunch or Beam object. + + """ + + self.charge_density(bunch, n_bin = self.n_bin) + self.init_timestop() + self.time_array() + self.long_wakefunction() + self.check_wake_tail() + self.wake_potential() + + f = interp1d(self.tau, self.W_p, fill_value = 0, bounds_error = False) + self.wp = f(bunch["tau"]) + + bunch["delta"] += self.wp * bunch.charge / self.ring.E0 + class SynchrotronRadiation(Element): """ Element to handle synchrotron radiation, radiation damping and quantum