From a44dbbf152bf2c8a65fe6d16bb3644f63b3638d6 Mon Sep 17 00:00:00 2001 From: Gamelin Alexis <gamelin@synchrotron-soleil.fr> Date: Fri, 20 Nov 2020 15:58:08 +0100 Subject: [PATCH] Various improvements WakePotential -> add method to plot wake potential for a perfect gaussian bunch utilities -> new function gaussian_bunch which gives the gaussian bunch profile utilities -> read_IW2D now also reads wake function from IW2D utilities -> new function read_IW2D_folder which reads the full results folder of IW2D into a WakeField element. resistive_wall -> change the sign of the wake functions to agree with the code convention --- collective_effects/__init__.py | 3 +- collective_effects/resistive_wall.py | 4 +- collective_effects/utilities.py | 96 +++++++++++++++++--- collective_effects/wakefield.py | 1 + tracking/wakepotential.py | 125 +++++++++++++++++++++++++-- 5 files changed, 205 insertions(+), 24 deletions(-) diff --git a/collective_effects/__init__.py b/collective_effects/__init__.py index a2b820a..39ac402 100644 --- a/collective_effects/__init__.py +++ b/collective_effects/__init__.py @@ -13,5 +13,6 @@ from mbtrack2.collective_effects.wakefield import ComplexData, Impedance, WakeFu from mbtrack2.collective_effects.utilities import read_CST, read_IW2D, spectral_density from mbtrack2.collective_effects.utilities import gaussian_bunch_spectrum, beam_spectrum, effective_impedance from mbtrack2.collective_effects.utilities import yokoya_elliptic, beam_loss_factor -from mbtrack2.collective_effects.utilities import double_sided_impedance +from mbtrack2.collective_effects.utilities import double_sided_impedance, read_IW2D_folder +from mbtrack2.collective_effects.utilities import gaussian_bunch from mbtrack2.collective_effects.impedance_model import ImpedanceModel \ No newline at end of file diff --git a/collective_effects/resistive_wall.py b/collective_effects/resistive_wall.py index 922cc94..d3ae124 100644 --- a/collective_effects/resistive_wall.py +++ b/collective_effects/resistive_wall.py @@ -155,7 +155,7 @@ class CircularResistiveWall(WakeField): ind = np.isnan(wl) wl[ind] = 0 - return wl + return -1*wl def TransverseWakeFunction(self, time, exact=False): """ @@ -206,7 +206,7 @@ class CircularResistiveWall(WakeField): / time**(1/2) * self.length * -1) ind = np.isnan(wt) wt[ind] = 0 - return wt + return -1*wt def function(self, t, x): return ( (x**2 * np.exp(-1* (x**2) * t / self.t0) ) / (x**6 + 8) ) diff --git a/collective_effects/utilities.py b/collective_effects/utilities.py index 11803ea..f6c6eec 100644 --- a/collective_effects/utilities.py +++ b/collective_effects/utilities.py @@ -9,8 +9,10 @@ collective_effects module. import pandas as pd import numpy as np +from pathlib import Path from scipy.interpolate import interp1d -from mbtrack2.collective_effects.wakefield import Impedance +from scipy.constants import c +from mbtrack2.collective_effects.wakefield import Impedance, WakeFunction, WakeField def read_CST(file, impedance_type='long', divide_by=None): """ @@ -45,30 +47,82 @@ def read_CST(file, impedance_type='long', divide_by=None): impedance_type=impedance_type) return result -def read_IW2D(file, impedance_type='long'): +def read_IW2D(file, file_type='Zlong'): """ - Read IW2D file format into an Impedance object. + Read IW2D file format into an Impedance object or a WakeField object. Parameters ---------- file : str Path to the file to read. - impedance_type : str, optional - Type of the Impedance object. + file_type : str, optional + Type of the Impedance or WakeField object. Returns ------- - result : Impedance object + result : Impedance or WakeField object Data from file. """ - df = pd.read_csv(file, sep = ' ', header = None, - names = ["Frequency","Real","Imaginary"], skiprows=1) - df.set_index("Frequency", inplace = True) - result = Impedance(variable = df.index, - function = df["Real"] + 1j*df["Imaginary"], - impedance_type=impedance_type) + if file_type[0] == "Z": + df = pd.read_csv(file, sep = ' ', header = None, + names = ["Frequency","Real","Imaginary"], skiprows=1) + df.set_index("Frequency", inplace = True) + df = df[df["Real"].notna()] + df = df[df["Imaginary"].notna()] + result = Impedance(variable = df.index, + function = df["Real"] + 1j*df["Imaginary"], + impedance_type=file_type[1:]) + elif file_type[0] == "W": + df = pd.read_csv(file, sep = ' ', header = None, + names = ["Distance","Wake"], skiprows=1) + df["Time"] = df["Distance"] / c + df.set_index("Time", inplace = True) + if np.any(df.isna()): + index = df.isna().values + df = df.interpolate() + print("Nan values have been interpolated to:") + print(df[index]) + # if file_type == "Wlong": + # df["Wake"] = df["Wake"]*-1 + result = WakeFunction(variable = df.index, + function = df["Wake"], + wake_type=file_type[1:]) + else: + raise ValueError("file_type should begin by Z or W.") return result +def read_IW2D_folder(folder, suffix): + """ + Read IW2D results into a WakeField object. + + Parameters + ---------- + file : str + Path to the file to read. + + Returns + ------- + result : WakeField object + WakeField object with Impedance and WakeFunction objects from the + different files. + """ + types = {"W" : WakeFunction, + "Z" : Impedance} + components = ["long", "xdip", "ydip", "xquad", "yquad"] + + data_folder = Path(folder) + + list_for_wakefield = [] + for key, item in types.items(): + for component in components: + name = data_folder / (key + component + suffix) + res = read_IW2D(file=name, file_type=key + component) + list_for_wakefield.append(res) + + wake = WakeField(list_for_wakefield) + + return wake + def spectral_density(frequency, sigma, m = 1, mode="Hermite"): """ Compute the spectral density of different modes for various values of the @@ -123,6 +177,24 @@ def gaussian_bunch_spectrum(frequency, sigma): regime and ion cloud mitigation in ThomX. p86, Eq. 4.19 """ return np.exp(-1/2*(2*np.pi*frequency)**2*sigma**2) + +def gaussian_bunch(time, sigma): + """ + Compute a Gaussian bunch profile. + + Parameters + ---------- + time : array + sample points of the bunch profile in [s]. + sigma : float + RMS bunch length in [s]. + + Returns + ------- + bunch_profile : array + Bunch profile in [s**-1] sampled at points given in time. + """ + return np.exp(-1/2*(time**2/sigma**2))/(sigma*np.sqrt(2*np.pi)) def beam_spectrum(frequency, M, bunch_spacing, sigma=None, diff --git a/collective_effects/wakefield.py b/collective_effects/wakefield.py index ee372cb..36b37f2 100644 --- a/collective_effects/wakefield.py +++ b/collective_effects/wakefield.py @@ -361,6 +361,7 @@ class Impedance(ComplexData): component_coefficients = table[self.impedance_type].to_dict() except KeyError: print('Impedance type {} does not exist'.format(self.impedance_type)) + raise self.a = component_coefficients["a"] self.b = component_coefficients["b"] diff --git a/tracking/wakepotential.py b/tracking/wakepotential.py index 41e5644..444309b 100644 --- a/tracking/wakepotential.py +++ b/tracking/wakepotential.py @@ -13,6 +13,7 @@ import pandas as pd from scipy import signal from scipy.interpolate import interp1d from mbtrack2.tracking.element import Element +from mbtrack2.collective_effects.utilities import gaussian_bunch class WakePotential(Element): """ @@ -166,7 +167,7 @@ class WakePotential(Element): return dipole0 - def prepare_wakefunction(self, wake_type): + def prepare_wakefunction(self, wake_type, tau, save_data=True): """ Prepare the wake function of a given wake_type to be used for the wake potential computation. @@ -179,6 +180,10 @@ class WakePotential(Element): ---------- wake_type : str Type of the wake function to prepare: "Wlong", "Wxdip", ... + tau : array + Time domain array of the bunch profile in [s]. + save_data : bool, optional + If True, the results are saved as atributes. Returns ------- @@ -202,7 +207,7 @@ class WakePotential(Element): W0 = np.array(getattr(self.wakefield, wake_type).data["real"]) # Keep only the wake function on the rho window - ind = np.all([min(self.tau[0], 0) < tau0, max(self.tau[-1], 0) > tau0], + ind = np.all([min(tau[0], 0) < tau0, max(tau[-1], 0) > tau0], axis=0) tau0 = tau0[ind] W0 = W0[ind] @@ -229,9 +234,9 @@ class WakePotential(Element): W0 = np.insert(W0, 0, np.zeros(n_to_add)) # Check is the wf is shorter than rho then add zeros - if (tau0[0] > self.tau[0]) or (tau0[-1] < self.tau[-1]): - n = max(int(np.ceil((tau0[0] - self.tau[0])/dtau0)), - int(np.ceil((self.tau[-1] - tau0[-1])/dtau0))) + if (tau0[0] > tau[0]) or (tau0[-1] < tau[-1]): + n = max(int(np.ceil((tau0[0] - tau[0])/dtau0)), + int(np.ceil((tau[-1] - tau0[-1])/dtau0))) tau0 = np.arange(tau0[0] - dtau0*n, tau0[-1] + dtau0*(n+1), @@ -239,10 +244,12 @@ class WakePotential(Element): W0 = np.insert(W0, 0, np.zeros(n)) n_to_add = len(tau0) - len(W0) W0 = np.insert(W0, len(W0), np.zeros(n_to_add)) + + if save_data: + setattr(self, "tau0_" + wake_type, tau0) + setattr(self, "dtau0_" + wake_type, dtau0) + setattr(self, "W0_" + wake_type, W0) - setattr(self, "tau0_" + wake_type, tau0) - setattr(self, "dtau0_" + wake_type, dtau0) - setattr(self, "W0_" + wake_type, W0) return (tau0, dtau0, W0) def get_wakepotential(self, bunch, wake_type): @@ -263,7 +270,7 @@ class WakePotential(Element): """ - (tau0, dtau0, W0) = self.prepare_wakefunction(wake_type) + (tau0, dtau0, W0) = self.prepare_wakefunction(wake_type, self.tau) interp_profile = interp1d(self.tau, self.rho, fill_value=0, bounds_error=False) @@ -375,6 +382,106 @@ class WakePotential(Element): return fig + def get_gaussian_wakepotential(self, sigma, wake_type, dipole=1e-3): + """ + Return the wake potential computed using a perfect gaussian profile. + + Parameters + ---------- + sigma : float + RMS bunch length in [s]. + wake_type : str + Wake function type: "Wlong", "Wxdip", ... + dipole : float, optional + Dipole moment to consider in [m], (uniform dipole moment). + Returns + ------- + tau0 : array + Time base in [s]. + W0 : array + Wake function. + Wp : array + Wake potential. + profile0 : array + Gaussian bunch profile. + dipole0 : array + Dipole moment. + + """ + + tau = np.linspace(-10*sigma,10*sigma, int(1e3)) + (tau0, dtau0, W0) = self.prepare_wakefunction(wake_type, tau, False) + + profile0 = gaussian_bunch(tau0, sigma) + dipole0 = np.ones_like(profile0)*dipole + + if wake_type == "Wlong" or wake_type == "Wxquad" or wake_type == "Wyquad": + Wp = signal.convolve(profile0, W0*-1, mode='same')*dtau0 + elif wake_type == "Wxdip": + Wp = signal.convolve(profile0*dipole0, W0*-1, mode='same')*dtau0 + elif wake_type == "Wydip": + Wp = signal.convolve(profile0*dipole0, W0*-1, mode='same')*dtau0 + else: + raise ValueError("This type of wake is not taken into account.") + + return tau0, W0, Wp, profile0, dipole0 + + def plot_gaussian_wake(self, sigma, wake_type, dipole=1e-3, plot_rho=True, + plot_dipole=False, plot_wake_function=True): + """ + Plot the wake potential of a given type for a perfect gaussian bunch. + + Parameters + ---------- + sigma : float + RMS bunch length in [s]. + wake_type : str + Type of the wake to plot: "Wlong", "Wxdip", ... + dipole : float, optional + Dipole moment to consider in [m], (uniform dipole moment). + plot_rho : bool, optional + Plot the normalised bunch profile. The default is True. + plot_dipole : bool, optional + Plot the normalised dipole moment. The default is False. + plot_wake_function : bool, optional + Plot the normalised wake function. The default is True. + + Returns + ------- + fig : figure + + """ + + labels = {"Wlong" : r"$W_{p,long}$ (V/pC)", + "Wxdip" : r"$W_{p,x}^{D} (V/pC)$", + "Wydip" : r"$W_{p,y}^{D} (V/pC)$", + "Wxquad" : r"$W_{p,x}^{Q} (V/pC/m)$", + "Wyquad" : r"$W_{p,y}^{Q} (V/pC/m)$"} + + tau0, W0, Wp, profile0, dipole0 = self.get_gaussian_wakepotential(sigma, wake_type, dipole) + + fig, ax = plt.subplots() + ax.plot(tau0*1e12, Wp*1e-12, label=labels[wake_type]) + ax.set_xlabel("$\\tau$ (ps)") + ax.set_ylabel(labels[wake_type]) + + if plot_rho is True: + profile_rescaled = profile0/max(profile0)*max(np.abs(Wp)) + ax.plot(tau0*1e12, profile_rescaled*1e-12, label=r"$\rho$ (a.u.)") + plt.legend() + + if plot_wake_function is True: + W0_rescaled = W0/max(W0)*max(np.abs(Wp)) + ax.plot(tau0*1e12, W0_rescaled*1e-12, label=r"$W_{function}$ (a.u.)") + plt.legend() + + if plot_dipole is True: + dipole_rescaled = dipole0/max(dipole0)*max(np.abs(Wp)) + ax.plot(tau0*1e12, dipole_rescaled*1e-12, label=r"Dipole moment (a.u.)") + plt.legend() + + return fig + def reference_loss(self, bunch): """ Calculate the loss factor and kick factor from the wake potential and -- GitLab