From 2470c02f66ed759834f481586ecbbe5d3ccbfe09 Mon Sep 17 00:00:00 2001 From: Watanyu Foosang <watanyu.f@gmail.com> Date: Fri, 10 Dec 2021 17:59:13 +0100 Subject: [PATCH] Add WakePotential_LR class The long-range wake potential tracking class has been added. --- tracking/particles.py | 8 ++ tracking/wakepotential.py | 287 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) diff --git a/tracking/particles.py b/tracking/particles.py index 068fba4..107d374 100644 --- a/tracking/particles.py +++ b/tracking/particles.py @@ -362,6 +362,14 @@ class Bunch: Dimension to plot. The default is "tau". n_bin : int, optional Number of bins. The default is 75. + bin_min, bin_max : float, optional + The lowest and the hightest bound of the bins. The range between + this two bounds must cover every particle in the bunch, i.e. + they must satisfy bin_min <= Bunch[dimension].min() and + Bunch[dimension].max() <= bin_max. + bin_size : float, optional + Define the bin size. If both n_bin and bin_size are provided, + n_bin value will be ignored. """ bins, sorted_index, profile, center = self.binning(dimension, n_bin, diff --git a/tracking/wakepotential.py b/tracking/wakepotential.py index e85ff90..be05538 100644 --- a/tracking/wakepotential.py +++ b/tracking/wakepotential.py @@ -12,8 +12,12 @@ import matplotlib.pyplot as plt import pandas as pd from scipy import signal from scipy.interpolate import interp1d +from scipy.signal import convolve from mbtrack2.tracking.element import Element from mbtrack2.collective_effects.utilities import gaussian_bunch +from mbtrack2.collective_effects import WakeField +from mpi4py import MPI + class WakePotential(Element): """ @@ -526,6 +530,289 @@ class WakePotential(Element): columns=column, index=index) return loss_data + +class WakePotential_LR(Element): + """ + Long-range wake potential tracking class for multibunch and multiturn + effects tracking. + + Wake potentials are first calculated individually from each bunch, then + they are shifted longitudinally depending on the location of the bunch + before being summed up. Each bunch is then exerted by a apart of the + total wake that corresponds to its location. + + Parameters + ---------- + ring : Synchrotron object + beam : Beam object + wakefield : Wakefield object + Wakefield object which contains the wake functions to be used. The wake + functions must be uniformly sampled! + m : int, optional + Unitless parameter defining the bin size relative to the revolution + period T1 as : + T1 = m * bin_size. + window_length : float, optional + Defines the length of a fixed size window for bunch binning in [s]. + The window must cover the whole bunch. + wake_length : int, optional + Length of the wake potential to be stored in the unit of number of + revolutions. + + """ + + def __init__(self, ring, beam, wakefield, m=3000, window_length=1e-10, + wake_length=10): + + self.ring = ring + self.wakefield = wakefield + self.type = self.wakefield.wake_components + + self.m = int(m) + self.slice_size = self.ring.T1/self.m + self.bunch_head = window_length/2 + self.bunch_tail = -1*window_length/2 + self.n_bins = int(window_length/self.slice_size)+1 + + self.wake_length = int(wake_length) + + def charge_density(self, bunch, wake_length): + """ + Computes a time array and a charge density array of an individual bunch + with the length specified in wake_length. + + Parameters + ---------- + bunch : Bunch object + wake_lengths : int + Length of the multiturn effect in the unit of number of turns. + + """ + + _ , b, c, d = bunch.binning(n_bin=self.n_bins, bin_min=self.bunch_tail, + bin_max=self.bunch_head, bin_size=self.slice_size) + + self.sorted_index = b + self.profile = c + self.center = d + self.bin_size = self.center[1]-self.center[0] + + self.rho = bunch.charge_per_mp*self.profile/(self.bin_size*bunch.charge) # [mp_number/tau] + self.rho = np.array(self.rho) + + # Compute time array relative to t0 + self.tau = np.array(self.center) + self.dtau = self.tau[1] - self.tau[0] + + if self.n_bins % 2 == 0: + self.n_bins_total = int(wake_length*self.ring.T0/self.bin_size) + self.N_front = int(self.n_bins_total/2) + self.N_back = int(self.n_bins_total/2) + self.tau = np.arange(self.tau[0] - self.dtau*self.N_front, + self.tau[-1] + self.dtau*self.N_back, + self.dtau) + self.rho = np.append(self.rho, np.zeros(self.N_back)) + self.rho = np.insert(self.rho, 0, np.zeros(self.N_front)) + else: + self.n_bins_total = int(np.floor(wake_length*self.ring.T0/self.bin_size)) + self.N_front = int(np.floor(self.n_bins_total/2)) + self.N_back = int(np.floor(self.n_bins_total/2)) + self.tau = np.arange(self.tau[0] - self.dtau*self.N_front, + self.tau[-1] + self.dtau*(self.N_back+1), + self.dtau) + self.rho = np.append(self.rho, np.zeros(self.N_back)) + self.rho = np.insert(self.rho, 0, np.zeros(self.N_front+1)) + + if len(self.tau) != len(self.rho): + self.tau = np.append(self.tau, self.tau[-1]+self.dtau) + + def prepare_wakefunction(self, tau, wake_type): + """ + Interpolates the wake function from the wakefield object on the time + array given by charge_density method. The parts outside the orginal + wake function are filled with zeros. + + Parameters + ---------- + tau : array + Time array on which the wake fucntion will be interpolated, in [s]. + wake_type : str + Type of the wake function to prepare: "Wlong", "Wxdip", ... + + Returns + ------- + wf : array + Interpolated wake function array. + + """ + + wf0 = getattr(self.wakefield, wake_type) + wf_interp = interp1d(wf0.data.index, wf0.data['real'], + bounds_error=False, fill_value=0) + wf = wf_interp(tau) + + return wf + + def dipole_moment(self, bunch, plane, tau): + """ + Return the dipole moment of an individual bunch computed on the same + time array as the wake function. + + Parameters + ---------- + bunch : Bunch object + plane : str + Plane on which the dipole moment is computed, "x" or "y". + tau : array + Time array on which the dipole moment will be interpolated, in [s]. + + Returns + ------- + dipole : array + Dipole moment of the bunch. + + """ + + dipole0 = np.zeros((self.n_bins - 1,)) + + for i in range(self.n_bins - 1): + dipole0[i] = bunch[plane][self.sorted_index == i].sum() + dipole0 = dipole0/self.profile + dipole0[np.isnan(dipole0)] = 0 + + dipole0 = np.append(dipole0, np.zeros(self.N_back)) + dipole0 = np.insert(dipole0, 0, np.zeros(self.N_front)) + + if len(tau) != len(dipole0): + if len(tau)-len(dipole0) == 1: + dipole0 = np.append(dipole0, 0) + elif len(tau)-len(dipole0) == 2: + dipole0 = np.append(dipole0, 0) + dipole0 = np.insert(dipole0, 0, 0) + elif len(tau)-len(dipole0) == 3: + dipole0 = np.append(dipole0, np.array([0,0])) + dipole0 = np.insert(dipole0, 0, 0) + + # Interpole on tau to get the same size as wf + dipole_func = interp1d(tau, dipole0, fill_value=0, bounds_error=False) + dipole = dipole_func(tau) + + return dipole + + def get_wakepotential(self, bunch, wake_type): + """ + Computes the wake potential of an individual bunch. The zero part of + the wake before the bunch is cut off to reduce memory usage. The time + array is also cut accordingly to the wake potential. + + Parameters + ---------- + bunch : Bunch object + wake_type : str + Wake function type: "Wlong", "Wxdip", ... + + Returns + ------- + tau0 : array + Reduced time array. + wp0 : array + Reduced wake potential. + + """ + + wf = self.prepare_wakefunction(self.tau, wake_type) + + if wake_type == "Wxdip" : + dipole = self.dipole_moment(bunch, plane="x", tau=self.tau) + wp0 = convolve(self.rho*dipole, wf, mode='same')*self.dtau + elif wake_type == "Wydip" : + dipole = self.dipole_moment(bunch, plane="y", tau=self.tau) + wp0 = convolve(self.rho*dipole, wf, mode='same')*self.dtau + elif wake_type == "Wlong" or wake_type == "Wxquad" or wake_type == "Wyquad": + wp0 = convolve(self.rho, wf*-1, mode='same')*self.dtau + else: + raise ValueError("Incorrect wake field type.") + + # Cut off the zero part before the bunch + cut_off = (self.n_bins_total-self.n_bins)//2 + wp0 = wp0[cut_off:] + tau0 = self.tau[cut_off:] + + return tau0, wp0 + + def track(self, beam, k, save_data=False): + """ + Tracking method for the element. + + Parameters + ---------- + beam : Beam object + k : int + Temporary argument used for naming output files indicating the turn + number of the data. + save_data : bool, optional + Temporary argument used for saving wake potentials. This function + should be disabled when track for large number of turns or large + number of bunches to prevent output file flooding. + + """ + + bunch_num = beam.mpi.bunch_num + bunch = beam[bunch_num] + + beam.mpi.share_distributions(beam, n_bin=self.n_bins, + bin_min=self.bunch_tail, bin_max=self.bunch_head, + bin_size=self.slice_size) + + + self.charge_density(bunch, self.wake_length) + for wake_type in self.type: + tau, wp = self.get_wakepotential(bunch, wake_type) + + shift = bunch_num * self.m # shift in number of bins unit + wp = np.roll(wp, shift) + wp[:shift] = 0 + + if save_data is True: + np.savez('testclass_turn{0}_bunch{1}'.format(k, bunch_num), tau=tau, wp=wp) + + beam.mpi.__setattr__("wp_all", np.empty((len(tau),), dtype=np.float64)) + beam.mpi.comm.Allreduce([wp, MPI.DOUBLE], [beam.mpi.__getattribute__("wp_all"), MPI.DOUBLE], op=MPI.SUM) + + del wp + + if save_data is True and bunch_num == 0: + np.savez('testclass_turn{0}_sum'.format(k), tau=tau, wp_all=beam.mpi.wp_all) + + del tau + + if wake_type == "Wxdip": + bunch["xp"] += beam.mpi.wp_all[self.sorted_index+shift] * \ + bunch.charge / self.ring.E0 + elif wake_type == "Wydip": + bunch["yp"] += beam.mpi.wp_all[self.sorted_index+shift] * \ + bunch.charge / self.ring.E0 + elif wake_type == "Wxquad": + bunch["xp"] += (bunch["x"] * beam.mpi.wp_all[self.sorted_index+shift] * \ + bunch.charge / self.ring.E0) + elif wake_type == "Wyquad": + bunch["yp"] += (bunch["y"] * beam.mpi.wp_all[self.sorted_index+shift] * \ + bunch.charge / self.ring.E0) + elif wake_type == "Wlong": + bunch["delta"] += beam.mpi.wp_all[self.sorted_index+shift] * \ + bunch.charge / self.ring.E0 + + + + + + + + + + + + -- GitLab