From c611185bfde52e81da9bcdfb87f50b715722146c Mon Sep 17 00:00:00 2001 From: Gamelin Alexis <gamelin@synchrotron-soleil.fr> Date: Fri, 7 Feb 2020 17:28:08 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + CollectiveEffect/__init__.py | 7 + CollectiveEffect/instabilities.py | 44 +++ CollectiveEffect/resistive_wall.py | 65 +++++ CollectiveEffect/tools.py | 185 +++++++++++++ CollectiveEffect/wakefield.py | 425 +++++++++++++++++++++++++++++ Machines/__init__.py | 7 + Machines/soleil.py | 55 ++++ Tracking/__init__.py | 7 + Tracking/beam.py | 341 +++++++++++++++++++++++ Tracking/synchrotron.py | 181 ++++++++++++ __init__.py | 7 + 12 files changed, 1325 insertions(+) create mode 100644 .gitignore create mode 100644 CollectiveEffect/__init__.py create mode 100644 CollectiveEffect/instabilities.py create mode 100644 CollectiveEffect/resistive_wall.py create mode 100644 CollectiveEffect/tools.py create mode 100644 CollectiveEffect/wakefield.py create mode 100644 Machines/__init__.py create mode 100644 Machines/soleil.py create mode 100644 Tracking/__init__.py create mode 100644 Tracking/beam.py create mode 100644 Tracking/synchrotron.py create mode 100644 __init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c86bbc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*__pycache__* diff --git a/CollectiveEffect/__init__.py b/CollectiveEffect/__init__.py new file mode 100644 index 0000000..27c993f --- /dev/null +++ b/CollectiveEffect/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jan 14 12:25:30 2020 + +@author: gamelina +""" + diff --git a/CollectiveEffect/instabilities.py b/CollectiveEffect/instabilities.py new file mode 100644 index 0000000..34bd523 --- /dev/null +++ b/CollectiveEffect/instabilities.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +General calculations about instabilities + +@author: Alexis Gamelin +@date: 15/01/2020 +""" + +from scipy.constants import c, m_e, e, pi, epsilon_0 + +def MBI_threshold(ring, sigma, R, b): + """ + Compute the microbunching instability (MBI) threshold for a bunched beam + considering the steady-state parallel plate model [1][2]. + + Parameters + ---------- + ring : Synchrotron object + sigma : float + RMS bunch length in [s] + R : float + dipole bending radius in [m] + b : float + vertical distance between the conducting parallel plates in [m] + + Returns + ------- + I : float + MBI current threshold + + [1] : Y. Cai, "Theory of microwave instability and coherent synchrotron + radiation in electron storage rings", SLAC-PUB-14561 + [2] : D. Zhou, "Coherent synchrotron radiation and microwave instability in + electron storage rings", PhD thesis, p112 + """ + + Ia = 4*pi*epsilon_0*m_e*c**3/e # Alfven current + chi = sigma*(R/b**3)**(1/2) # Shielding paramter + xi = 0.5 + 0.34*chi + N = (ring.L0 * Ia * ring.ac * ring.gamma * ring.sigma_delta**2 * xi * + sigma**(1/3) / ( c * e * R**(1/3) )) + I = N*e/ring.T0 + + return I \ No newline at end of file diff --git a/CollectiveEffect/resistive_wall.py b/CollectiveEffect/resistive_wall.py new file mode 100644 index 0000000..764a59e --- /dev/null +++ b/CollectiveEffect/resistive_wall.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Define resistive wall elements based on the Wakefield class. + +@author: Alexis Gamelin +@date: 14/01/2020 +""" + +import numpy as np +from scipy.constants import mu_0, epsilon_0, c +from CollectiveEffect.wakefield import Wakefield, Impedance + +def skin_depth(omega, rho, mu_r = 1, epsilon_r = 1): + """ General formula for the skin depth + + Parameters + ---------- + omega: angular frequency in [Hz] + rho: resistivity in [ohm.m] + mu_r : relative magnetic permeability, optional + epsilon_r : relative electric permittivity, optional + + Returns + ------- + delta : skin depth in [m] + + """ + + delta = np.sqrt(2*rho/(np.abs(omega)*mu_r*mu_0))*np.sqrt(np.sqrt(1 + + (rho*np.abs(omega)*epsilon_r*epsilon_0)**2 ) + + rho*np.abs(omega)*epsilon_r*epsilon_0) + return delta + + +class CircularResistiveWall(Wakefield): + """ + Resistive wall Wakefield element for a circular beam pipe, approximated + formulas from Eq. (2.77) of Chao book [1]. + + Parameters + ---------- + frequency: frequency points where the impedance will be evaluated in [Hz] + length: beam pipe length in [m] + rho: resistivity in [ohm.m] + radius: beam pipe radius in [m] + mu_r : relative magnetic permeability, optional + epsilon_r : relative electric permittivity, optional + """ + + def __init__(self, frequency, length, rho, radius, mu_r = 1, epsilon_r = 1): + super().__init__() + + omega = 2*np.pi*frequency + Z1 = length*(1 + np.sign(omega)*1j)*rho/( + 2*np.pi*radius*skin_depth(omega,rho)) + Z2 = c/omega*length*(1 + np.sign(omega)*1j)*rho/( + np.pi*radius**3*skin_depth(omega,rho)) + + Zlong = Impedance(variable = frequency, function = Z1, impedance_type='long') + Zxdip = Impedance(variable = frequency, function = Z2, impedance_type='xdip') + Zydip = Impedance(variable = frequency, function = Z2, impedance_type='ydip') + + super().append_to_model(Zlong) + super().append_to_model(Zxdip) + super().append_to_model(Zydip) \ No newline at end of file diff --git a/CollectiveEffect/tools.py b/CollectiveEffect/tools.py new file mode 100644 index 0000000..3f3f633 --- /dev/null +++ b/CollectiveEffect/tools.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +""" +Tools and utilities functions for the Wakefield and impedances classes. + +@author: Alexis Gamelin +@date: 14/01/2020 +""" + +import pandas as pd +import numpy as np +from scipy.integrate import trapz +from CollectiveEffect.wakefield import Wakefield, Impedance, WakeFunction +#from ..Tracking.synchrotron import Synchrotron + +def read_CST(file, impedance_type='long'): + """Read CST file format into an Impedance object.""" + df = pd.read_csv(file, comment="#", header = None, sep = "\t", + names = ["Frequency","Real","Imaginary"]) + df["Frequency"] = df["Frequency"]*1e9 + df.set_index("Frequency", inplace = True) + result = Impedance(variable = df.index, + function = df["Real"] + 1j*df["Imaginary"], + impedance_type=impedance_type) + return result + +def read_IW2D(file, impedance_type='long'): + """Read IW2D file format into an Impedance object.""" + 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) + return result + +def loss_factor(wake, sigma): + """ + Compute the loss factor or the kick factor of a Wakefield, Impedance or + WakeFunction for a Gaussian bunch. Formulas from Eq. (2) p239 and + Eq.(7) p240 of [1]. + + Parameters + ---------- + wake: Wakefield, Impedance or WakeFunction object. + sigma: RMS bunch length in [s] + + Returns + ------- + kloss: loss factor in [V/C] or kick factor in [V/C/m] depanding on the + impedance type. + + [1] Handbook of accelerator physics and engineering, 3rd printing. + """ + + if isinstance(wake, Impedance): + + positive_index = wake.data.index > 0 + omega = 2*np.pi*wake.data.index[positive_index] + gaussian_spectrum = np.exp(-1*omega**2*sigma**2) + + if(wake.impedance_type == "long"): + kloss = trapz(x = omega, + y = 1/np.pi*wake.data["real"][positive_index]*gaussian_spectrum) + if(wake.impedance_type == "xdip" or wake.impedance_type == "ydip"): + kloss = trapz(x = omega, + y = 1/np.pi*wake.data["imag"][positive_index]*gaussian_spectrum) + if isinstance(wake, WakeFunction): + # To be implemented. + pass + + if isinstance(wake, Wakefield): + # To be implemented. Create field in wakefield with values ? + pass + + return kloss + +def spectral_density(frequency, sigma, m = 1, mode="Hermite"): + """ + Compute the spectral density of different modes for various values of the + head-tail mode number, based on Table 1 p238 of [1]. + + Parameters + ---------- + frequency : list or numpy array + sample points of the spectral density in [Hz] + sigma : float + RMS bunch length in [s] + m : int, optional + head-tail (or azimutal/synchrotron) mode number + mode: str, optional + type of the mode taken into account for the computation: + -"Hermite" modes for Gaussian bunches + + Returns + ------- + numpy array + + [1] Handbook of accelerator physics and engineering, 3rd printing. + """ + + if mode == "Hermite": + return (2*np.pi*frequency*sigma)**(2*m)*np.exp( + -1*(2*np.pi*frequency*sigma)**2) + else: + raise NotImplementedError("Not implemanted yet.") + +def effective_impedance(ring, imp, m, mu, sigma, xi=None, mode="Hermite"): + """ + Compute the effective (longitudinal or transverse) impedance. + Formulas from Eq. (1) and (2) p238 of [1]. + + Parameters + ---------- + ring : Synchrotron object + imp : Impedance object + mu : int + coupled bunch mode number, goes from 0 to (M-1) where M is the + number of bunches + m : int + head-tail (or azimutal/synchrotron) mode number + sigma : float + RMS bunch length in [s] + xi : float, optional + (non-normalized) chromaticity + mode: str, optional + type of the mode taken into account for the computation: + -"Hermite" modes for Gaussian bunches + + Returns + ------- + Zeff : float + effective impedance in [ohm] or in [ohm/m] depanding on the impedance + type. + + [1] Handbook of accelerator physics and engineering, 3rd printing. + """ + + if not isinstance(imp, Impedance): + raise TypeError("{} should be an Impedance object.".format(imp)) + + fmin = imp.data.index.min() + fmax = imp.data.index.max() + if fmin > 0: + raise ValueError("A double sided impedance spectrum, with positive and" + " negative frequencies, is needed to compute the " + "effective impedance.") + + if mode == "Hermite": + def h(f): + return spectral_density(frequency=f, sigma=sigma, m=m, + mode="Hermite") + else: + raise NotImplementedError("Not implemanted yet.") + + M = 416 + + pmax = fmax/(ring.f0 * M) - 50 + pmin = fmin/(ring.f0 * M) + 50 + + p = np.arange(pmin,pmax+1) + + if imp.impedance_type == "long": + fp = ring.f0*(p*M + mu + m*ring.tune[2]) + fp = fp[np.nonzero(fp)] # Avoid division by 0 + num = np.sum( imp(fp) * h(fp) / (fp*2*np.pi) ) + den = np.sum( h(fp) ) + Zeff = num/den + + elif imp.impedance_type == "xdip" or imp.impedance_type == "ydip": + if imp.impedance_type == "xdip": + tuneXY = ring.tune[0] + xi = ring.chro[0] + elif imp.impedance_type == "ydip": + tuneXY = ring.tune[1] + xi = ring.chro[1] + fp = ring.f0*(p*M + mu + tuneXY + m*ring.tuneS) + f_xi = xi/ring.eta*ring.f0 + num = np.sum( imp(fp) * h(fp - f_xi) ) + den = np.sum( h(fp - f_xi) ) + Zeff = num/den + else: + raise TypeError("Effective impedance is only defined for long, xdip" + " and ydip impedance type.") + + return Zeff diff --git a/CollectiveEffect/wakefield.py b/CollectiveEffect/wakefield.py new file mode 100644 index 0000000..dea656d --- /dev/null +++ b/CollectiveEffect/wakefield.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +""" +This module defines general classes to describes wakefields, impedances and +wake functions. Based on impedance library by David Amorim. + +@author: David Amorin, Alexis Gamelin +@date: 14/01/2020 +""" + +import warnings +import re +import pandas as pd +import numpy as np +from scipy.interpolate import interp1d + +class ComplexData: + """ + Define a general data structure for a complex function based on a pandas + DataFrame. + + Parameters + ---------- + variable : list, numpy array + contains the function variable values + + function : list or numpy array of complex numbers + contains the values taken by the complex function + """ + + def __init__(self, variable=np.array([-1e15, 1e15]), + function=np.array([0, 0])): + self.data = pd.DataFrame({'real': np.real(function), + 'imag': np.imag(function)}, + index=variable) + self.data.index.name = 'variable' + + def add(self, structure_to_add, method='zero', interp_kind='cubic'): + """ + Method to add two structures. If the data don't have the same length, + different cases are handled. + + Parameters + ---------- + structure_to_add : ComplexData object, int, float or complex. + structure to be added. + method : str, optional + manage how the addition is done, possibilties are: + -common: the common range of the index (variable) from the two + structures is used. The input data are cross-interpolated + so that the result data contains the points at all initial + points in the common domain. + -extrapolate: extrapolate the value of both ComplexData. + -zero: outside of the common range, the missing values are zero. In + the common range, behave as "common" method. + interp_kind : str, optional + interpolation method which is passed to pandas and to + scipy.interpolate.interp1d. + + Returns + ------- + ComplexData + Contains the sum of the two inputs. + """ + + # Create first two new DataFrames merging the variable + # from the two input data + + if isinstance(structure_to_add, (int, float, complex)): + structure_to_add = ComplexData( + variable=self.data.index, + function=structure_to_add * np.ones(len(self.data.index))) + + initial_data = pd.concat( + [self.data, + structure_to_add.data.index.to_frame().set_index('variable')], + sort=True) + initial_data = initial_data[~initial_data.index.duplicated(keep='first')] + + data_to_add = pd.concat( + [structure_to_add.data, + self.data.index.to_frame().set_index('variable')], + sort=True) + data_to_add = data_to_add[~data_to_add.index.duplicated(keep='first')] + + if method == 'common': + max_variable = min(structure_to_add.data.index.max(), + self.data.index.max()) + + min_variable = max(structure_to_add.data.index.min(), + self.data.index.min()) + + initial_data = initial_data.interpolate(method=interp_kind) + mask = ((initial_data.index <= max_variable) + & (initial_data.index >= min_variable)) + initial_data = initial_data[mask] + + data_to_add = data_to_add.interpolate(method=interp_kind) + mask = ((data_to_add.index <= max_variable) + & (data_to_add.index >= min_variable)) + data_to_add = data_to_add[mask] + + result_structure = ComplexData() + result_structure.data = initial_data + data_to_add + return result_structure + + if method == 'extrapolate': + print('Not there yet') + return self + + if method == 'zero': + max_variable = min(structure_to_add.data.index.max(), + self.data.index.max()) + + min_variable = max(structure_to_add.data.index.min(), + self.data.index.min()) + + mask = ((initial_data.index <= max_variable) + & (initial_data.index >= min_variable)) + initial_data[mask] = initial_data[mask].interpolate(method=interp_kind) + + mask = ((data_to_add.index <= max_variable) + & (data_to_add.index >= min_variable)) + data_to_add[mask] = data_to_add[mask].interpolate(method=interp_kind) + + initial_data.replace(to_replace=np.nan, value=0, inplace=True) + data_to_add.replace(to_replace=np.nan, value=0, inplace=True) + + result_structure = ComplexData() + result_structure.data = initial_data + data_to_add + return result_structure + + def __radd__(self, structure_to_add): + return self.add(structure_to_add, method='zero') + + def __add__(self, structure_to_add): + return self.add(structure_to_add, method='zero') + + def multiply(self, factor): + """ + Multiply a data strucure with a float or an int. + If the multiplication is done with something else, throw a warning. + """ + if isinstance(factor, (int, float)): + result_structure = ComplexData() + result_structure.data = self.data * factor + return result_structure + else: + warnings.warn(('The multiplication factor is not a float ' + 'or an int.'), UserWarning) + return self + + def __mul__(self, factor): + return self.multiply(factor) + + def __rmul__(self, factor): + return self.multiply(factor) + + def __call__(self, values, interp_kind="cubic"): + """ + Interpolation of the data by calling the class to have a function-like + behaviour. + + Parameters + ---------- + values : list or numpy array of complex, int or float + values to be interpolated. + interp_kind : str, optional + interpolation method which is passed to scipy.interpolate.interp1d. + + Returns + ------- + numpy array + Contains the interpolated data. + """ + real_func = interp1d(x = self.data.index, + y = self.data["real"], kind=interp_kind) + imag_func = interp1d(x = self.data.index, + y = self.data["imag"], kind=interp_kind) + return real_func(values) + 1j*imag_func(values) + +class WakeFunction(ComplexData): + """ + Define a WakeFunction object based on a ComplexData object. + """ + + def __init__(self, + variable=np.array([-1e15, 1e15]), + function=np.array([0, 0]), wake_type='long'): + super().__init__(variable, function) + pass + +class Impedance(ComplexData): + """ + Define an Impedance object based on a ComplexData object. + """ + + def __init__(self, + variable=np.array([-1e15, 1e15]), + function=np.array([0, 0]), impedance_type='long'): + super().__init__(variable, function) + self._impedance_type = impedance_type + self.data.index.name = 'frequency [Hz]' + self.initialize_impedance_coefficient() + + + def initialize_impedance_coefficient(self): + """ + Define the impedance coefficients and the plane of the impedance that + are presents in attributes of the class. + """ + table = self.impedance_name_and_coefficients_table() + + try: + component_coefficients = table[self.impedance_type].to_dict() + except KeyError: + print('Impedance type {} does not exist'.format(self.impedance_type)) + + self.a = component_coefficients["a"] + self.b = component_coefficients["b"] + self.c = component_coefficients["c"] + self.d = component_coefficients["d"] + self.plane = component_coefficients["plane"] + + def impedance_name_and_coefficients_table(self): + """ + Return a table associating the human readbale names of an impedance + component and its associated coefficients and plane. + """ + + component_dict = { + 'long': {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'plane': 'z'}, + 'xcst': {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'plane': 'x'}, + 'ycst': {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'plane': 'y'}, + 'xdip': {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'plane': 'x'}, + 'ydip': {'a': 0, 'b': 1, 'c': 0, 'd': 0, 'plane': 'y'}, + 'xydip': {'a': 0, 'b': 1, 'c': 0, 'd': 0, 'plane': 'x'}, + 'yxdip': {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'plane': 'y'}, + 'xquad': {'a': 0, 'b': 0, 'c': 1, 'd': 0, 'plane': 'x'}, + 'yquad': {'a': 0, 'b': 0, 'c': 0, 'd': 1, 'plane': 'y'}, + 'xyquad': {'a': 0, 'b': 0, 'c': 0, 'd': 1, 'plane': 'x'}, + 'yxquad': {'a': 0, 'b': 0, 'c': 1, 'd': 0, 'plane': 'y'}, + } + + return pd.DataFrame(component_dict) + + def add(self, structure_to_add, beta_x=1, beta_y=1, method='zero'): + """ + Method to add two Impedance objects. The two structures are + compared so that the addition is self-consistent. + Beta functions can be precised as well. + """ + if isinstance(structure_to_add, (int, float, complex)): + result = super().add(structure_to_add, method=method) + elif isinstance(structure_to_add, Impedance): + if (self.impedance_type == structure_to_add.impedance_type): + weight = (beta_x ** self.power_x) * (beta_y ** self.power_y) + result = super().add(structure_to_add * weight, method=method) + else: + warnings.warn(('The two Impedance objects do not have the ' + 'same coordinates or plane or type. ' + 'Returning initial Impedance object.'), + UserWarning) + result = self + + impedance_to_return = Impedance( + result.data.index, + result.data.real.values + 1j*result.data.imag.values, + self.impedance_type) + return impedance_to_return + + def __radd__(self, structure_to_add): + return self.add(structure_to_add) + + def __add__(self, structure_to_add): + return self.add(structure_to_add) + + def multiply(self, factor): + """ + Multiply a Impedance object with a float or an int. + If the multiplication is done with something else, throw a warning. + """ + result = super().multiply(factor) + impedance_to_return = Impedance( + result.data.index, + result.data.real.values + 1j*result.data.imag.values, + self.impedance_type) + return impedance_to_return + + def __mul__(self, factor): + return self.multiply(factor) + + def __rmul__(self, factor): + return self.multiply(factor) + + @property + def impedance_type(self): + return self._impedance_type + + @impedance_type.setter + def impedance_type(self, value): + self._impedance_type = value + self.initialize_impedance_coefficient() + + @property + def power_x(self): + power_x = self.a/2 + self.c/2. + if self.plane == 'x': + power_x += 1./2. + return power_x + + @property + def power_y(self): + power_y = self.b/2. + self.d/2. + if self.plane == 'y': + power_y += 1./2. + return power_y + +class Wakefield: + """ + Define a general Wakefield machine element. + It contains Imepdance or WakeFunction objects which defines the wakefield + and other informations: beta functions. + """ + + def __init__(self, betax=1, betay=1, structure_list=[]): + self.betax = betax + self.betay = betay + self.list_to_attr(structure_list) + + def append_to_model(self, structure_to_add): + list_of_attributes = dir(self) + if isinstance(structure_to_add, Impedance): + attribute_name = "Z" + structure_to_add.impedance_type + if attribute_name in list_of_attributes: + raise ValueError("There is already a component of the type " + "{} in this element.".format(attribute_name)) + else: + self.__setattr__(attribute_name, structure_to_add) + elif isinstance(structure_to_add, WakeFunction): + attribute_name = "W" + structure_to_add.impedance_type + if attribute_name in list_of_attributes: + raise ValueError("There is already a component of the type " + "{} in this element.".format(attribute_name)) + else: + self.__setattr__(attribute_name, structure_to_add) + else: + raise ValueError("{} is not an Impedance nor a WakeFunction.".format(structure_to_add)) + + def list_to_attr(self, structure_list): + for component in structure_list: + self.append_to_model(component) + + @property + def list_impedance_components(self): + """ + Return the list of impedance components for the element, + based on the attributes names. + """ + return np.array([comp for comp in dir(self) if re.match(r'[Z]', comp)]) + + def add(self, element_to_add): + """ + This function adds two Wakefield objects. + The different impedance components are weighted and added. + The result is a Wakefield object with all the components but the + beta function equal to 1. + """ + + if not isinstance(element_to_add, Wakefield): + raise TypeError("You can only add Wakefield objects to other" + "Wakefields objects.") + + list_of_summed_impedances = [] + list_of_components_added = [] + + betax1 = self.betax + betay1 = self.betay + betax2 = element_to_add.betax + betay2 = element_to_add.betay + + # We first go through element1 impedance components and add them + # to the element2 components. If the component is missing in element2, + # we then simply weight the element1 component and add it to the model. + for component_name in self.list_impedance_components: + + element1_impedance = self.__getattribute__(component_name) + + element1_weight = ((betax1 ** element1_impedance.power_x) + * (betay1 ** element1_impedance.power_y)) + + try: + element2_impedance = element_to_add.__getattribute__(component_name) + list_of_components_added.append(component_name) + element12_impedance_sum = element1_weight * element1_impedance + element12_impedance_sum = element12_impedance_sum.add(element2_impedance, betax2, betay2) + except: + element12_impedance_sum = element1_weight * element1_impedance + + list_of_summed_impedances.append(element12_impedance_sum) + + # We now go through the components which are unique to element2 and + # add them to the impedance model, with the beta function weighting + missing_components_list = list(set(element_to_add.list_impedance_components) - + set(list_of_components_added)) + + for component_name in missing_components_list: + + element2_impedance = element_to_add.__getattribute__(component_name) + element2_weight = ((betax2 ** element2_impedance.power_x) + * (betay2 ** element2_impedance.power_y)) + element12_impedance_sum = element2_weight * element2_impedance + + list_of_summed_impedances.append(element12_impedance_sum) + + # Gather everything in an Wakefield object + sum_wakefield = Wakefield(betax=1., betay=1., + structure_list=list_of_summed_impedances) + + return sum_wakefield + + def __radd__(self, structure_to_add): + return self.add(structure_to_add) + + def __add__(self, structure_to_add): + return self.add(structure_to_add) diff --git a/Machines/__init__.py b/Machines/__init__.py new file mode 100644 index 0000000..c67145e --- /dev/null +++ b/Machines/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jan 14 18:11:33 2020 + +@author: gamelina +""" + diff --git a/Machines/soleil.py b/Machines/soleil.py new file mode 100644 index 0000000..b0ef691 --- /dev/null +++ b/Machines/soleil.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +SOLEIL synchrotron parameters script. + +@author: Alexis Gamelin +@date: 14/01/2020 +""" + +import numpy as np +from Tracking.synchrotron import Synchrotron, Electron, Optics +from Tracking.beam import Beam + +def soleil(mode = 'Uniform'): + """ + + """ + + h = 416 + L = 3.540969742590899e+02 + E0 = 2.75e9 + particle = Electron() + ac = 4.16e-4 + U0 = 1171e3 + tau = np.array([0, 0, 3.27e-3]) + tune = np.array([18.15687, 10.22824, 0.00502]) + emit = np.array([4.5e-9, 4.5e-9*0.01]) + sigma_0 = 8e-12 + sigma_delta = 9e-4 + + # mean values + beta = np.array([3, 1.3]) + alpha = np.array([0, 0]) + mean_val = Optics(beta, alpha) + + ring = Synchrotron(h, L, E0, particle, ac=ac, U0=U0, tau=tau, + mean_optics=mean_val, emit=emit, tune=tune, + sigma_delta=sigma_delta, sigma_0=sigma_0) + + if mode == 'Uniform': + beam = Beam(ring, ) + elif mode == 'Hybrid': + pass + elif mode == 'Low alpha/25': + pass + elif mode == 'Low alpha/25': + pass + elif mode == '8 bunches': + pass + elif mode == 'Single': + pass + else: + raise ValueError("{} is not a correct operation mode.".format(mode)) + + + return ring \ No newline at end of file diff --git a/Tracking/__init__.py b/Tracking/__init__.py new file mode 100644 index 0000000..c67145e --- /dev/null +++ b/Tracking/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jan 14 18:11:33 2020 + +@author: gamelina +""" + diff --git a/Tracking/beam.py b/Tracking/beam.py new file mode 100644 index 0000000..e3c4f1b --- /dev/null +++ b/Tracking/beam.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +""" +Beam and bunch elements + +@author: Alexis Gamelin +@date: 17/01/2020 +""" + +import numpy as np +import numpy.ma as ma +import warnings + +class Bunch: + """ + Define a bunch + + Parameters + ---------- + ring : Synchrotron object + mp_number : float, optional + Macro-particle number + current : float, optional + Bunch current in [A] + + Attributes + ---------- + mp_number : int + Macro-particle number + charge : float + Bunch charge in [C] + charge_per_mp : float + Charge per macro-particle in [C] + particle_number : int + Number of particles in the bunch + current : float + Bunch current in [A] + + Methods + ------ + stats() + Compute the mean and the standard deviation of alive particles for each + coordinates. + init_gaussian(cov=None, mean=None, **kwargs) + Initialize bunch particles with 6D gaussian phase space. + """ + + def __init__(self, ring, mp_number=1e3, current=1e-3): + dpart = np.dtype([("x",np.float64),("xp",np.float64), + ("y",np.float64),("yp",np.float64), + ("tau",np.float64),("delta",np.float64)]) + self.dtype = dpart + particles = np.zeros((int(mp_number),6), dtype=np.float64) + self.particles = ma.masked_array(particles) + + self.ring = ring + self._mp_number = int(mp_number) + self.current = current + + def __len__(self): + """Return the number of alive particles""" + #mask = self.particles["alive"] == True + return len(self.particles) + + def __getitem__(self, i): + """Return the alive particle number i""" + return self.particles[i] + + def __setitem__(self, i, value): + """Set value to the alive particle number i""" + self.particles[i] = value + + def __iter__(self): + """Iterate over alive particles""" + return self.particles.__iter__() + + @property + def mp_number(self): + """Macro-particle number""" + return self._mp_number + + @mp_number.setter + def mp_number(self, value): + self._mp_number = int(value) + self.__init__(self.ring, value, self.charge) + + @property + def charge_per_mp(self): + """Charge per macro-particle [C]""" + return self._charge_per_mp + + @charge_per_mp.setter + def charge_per_mp(self, value): + self._charge_per_mp = value + + @property + def charge(self): + """Bunch charge in [C]""" + return self.__len__()*self.charge_per_mp + + @charge.setter + def charge(self, value): + self.charge_per_mp = value / self.__len__() + + @property + def particle_number(self): + """Particle number""" + return int(self.charge / np.abs(self.ring.particle.charge)) + + @particle_number.setter + def particle_number(self, value): + self.charge_per_mp = value * self.ring.particle.charge / self.__len__() + + @property + def current(self): + """Bunch current [A]""" + return self.charge / self.ring.T0 + + @current.setter + def current(self, value): + self.charge_per_mp = value * self.ring.T0 / self.__len__() + + def mean(self): + """ + Compute the mean position of alive particles for each + coordinates. + + Returns + ------- + mean : numpy array + mean position of alive particles + """ + mean = [[self[name].mean()] for name in self.dtype.names] + return np.array(mean) + + def std(self): + """ + Compute the standard deviation of the position of alive + particles for each coordinates. + + Returns + ------- + std : numpy array + standard deviation of the position of alive particles + """ + std = [[self[name].std()] for name in self.dtype.names] + return np.array(std) + + def emit(self): + """ + Compute the bunch emittance for each plane [1]. + Correct definition of long emit ? + + Returns + ------- + emit : numpy array + bunch emittance + + References + ---------- + [1] Wiedemann, H. (2015). Particle accelerator physics. 4th + edition. Springer, Eq.(8.39) of p224. + """ + emitX = (np.mean(self['x']**2)*np.mean(self['xp']**2) - + np.mean(self['x']*self['xp'])**2)**(0.5) + emitY = (np.mean(self['y']**2)*np.mean(self['yp']**2) - + np.mean(self['y']*self['yp'])**2)**(0.5) + emitS = (np.mean(self['tau']**2)*np.mean(self['delta']**2) - + np.mean(self['tau']*self['delta'])**2)**(0.5) + return np.array([emitX, emitY, emitS]) + + def init_gaussian(self, cov=None, mean=None, **kwargs): + """ + Initialize bunch particles with 6D gaussian phase space. + Covariance matrix is taken from [1]. + + Parameters + ---------- + cov : (6,6) array, optional + Covariance matrix of the bunch distribution + mean : (6,) array, optional + Mean of the bunch distribution + + References + ---------- + [1] Wiedemann, H. (2015). Particle accelerator physics. 4th + edition. Springer, Eq.(8.38) of p223. + + """ + if mean is None: + mean = np.zeros((6,)) + + if cov is None: + sigma_0 = kwargs.get("sigma_0", self.ring.sigma_0) + sigma_delta = kwargs.get("sigma_delta", self.ring.sigma_delta) + optics = kwargs.get("optics", self.ring.mean_optics) + + cov = np.zeros((6,6)) + cov[0,0] = self.ring.emit[0]*optics.beta[0] + cov[1,1] = self.ring.emit[0]*optics.gamma[0] + cov[0,1] = -1*self.ring.emit[0]*optics.alpha[0] + cov[1,0] = -1*self.ring.emit[0]*optics.alpha[0] + cov[2,2] = self.ring.emit[1]*optics.beta[1] + cov[3,3] = self.ring.emit[1]*optics.gamma[1] + cov[2,3] = -1*self.ring.emit[1]*optics.alpha[1] + cov[3,2] = -1*self.ring.emit[1]*optics.alpha[1] + cov[4,4] = sigma_0**2 + cov[5,5] = sigma_delta**2 + + values = np.random.multivariate_normal(mean, cov, size=self.mp_number) + self.particles["x"] = values[:,0] + self.particles["xp"] = values[:,1] + self.particles["y"] = values[:,2] + self.particles["yp"] = values[:,3] + self.particles["tau"] = values[:,4] + self.particles["delta"] = values[:,5] + +class Beam: + """ + Define a Beam object composed of several Bunch objects. + + Parameters + ---------- + ring : Synchrotron object + bunch_list : list of Bunch object, optional + + Attributes + ---------- + current : float + Total beam current in [A] + charge : float + Total bunch charge in [C] + particle_number : int + Total number of particle in the beam + filling_pattern : list of bool + Filling pattern of the beam + + Methods + ------ + init_beam() + """ + + def __init__(self, ring, bunch_list=None): + self.ring = ring + + if bunch_list is None: + self.init_beam(np.zeros((self.ring.h,1),dtype=bool)) + else: + if (len(bunch_list) != self.ring.h): + raise ValueError(("The length of the bunch list is {} ".format(len(bunch_list)) + + "but should be {}".format(self.ring.h))) + self.bunch_list = bunch_list + + def __len__(self): + """Return the number of bunches""" + length = 0 + for bunch in self: + if bunch is not None: + length += 1 + return length + + def __getitem__(self, i): + """Return the bunch number i""" + return self.bunch_list.__getitem__(i) + + def __setitem__(self, i, value): + """Set value to the bunch number i""" + self.bunch_list.__setitem__(i, value) + + def __iter__(self): + """Iterate over the bunches""" + return self.bunch_list.__iter__() + + def init_beam(self, filling_pattern, current_per_bunch=1e-3, + mp_per_bunch=1e3): + """ + Initialize beam with a given filling pattern but with uniform current + per bunch and marco-particle number per bunch. + + Parameters + ---------- + filling_pattern : numpy array or list of length ring.h + Filling pattern of the beam, + charge_per_bunch : float, optional + Charge per bunch in [C] + mp_per_bunch : float, optional + Macro-particle number per bunch + """ + + if (len(filling_pattern) != self.ring.h): + raise ValueError(("The length of filling pattern is {} ".format(len(filling_pattern)) + + "but should be {}".format(self.ring.h))) + + bunch_list = [] + for value in filling_pattern: + if value == True: + bunch_list.append(Bunch(self.ring, mp_per_bunch, current_per_bunch)) + elif value == False: + bunch_list.append(None) + else: + raise ValueError("{} should be True or False".format(value)) + self.bunch_list = bunch_list + # init bunches => gauss + + @property + def current(self): + """Total beam current in [A]""" + current = 0 + for bunch in self: + if bunch is not None: + current += bunch.current + return current + + @property + def charge(self): + """Total beam charge in [C]""" + charge = 0 + for bunch in self: + if bunch is not None: + charge += bunch.charge + return charge + + @property + def particle_number(self): + """Total number of particle in the beam""" + particle_number = 0 + for bunch in self: + if bunch is not None: + particle_number += bunch.particle_number + return particle_number + + @property + def filling_pattern(self): + """Filling pattern of the beam""" + filling_pattern = [] + for bunch in self: + if isinstance(bunch, Bunch): + filling_pattern.append(True) + else: + filling_pattern.append(False) + return filling_pattern + \ No newline at end of file diff --git a/Tracking/synchrotron.py b/Tracking/synchrotron.py new file mode 100644 index 0000000..a7eb44b --- /dev/null +++ b/Tracking/synchrotron.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +""" +General elements + +@author: Alexis Gamelin +@date: 15/01/2020 +""" + +import numpy as np +from scipy.constants import c, m_e, m_p, e + +class Particle: + """ Define a particle + + Attributes + ---------- + mass : float + total particle mass in [kg] + charge : float + electrical charge in [C] + E_rest : float + particle rest energy in [eV] + """ + def __init__(self, mass, charge): + self.mass = mass + self.charge = charge + + @property + def E_rest(self): + return self.mass * c ** 2 / e + +class Electron(Particle): + """ Define an electron""" + def __init__(self): + super().__init__(m_e, -1*e) + +class Proton(Particle): + """ Define a proton""" + def __init__(self): + super().__init__(m_p, e) + +class Optics: + """Handle optic functions""" + def __init__(self, beta, alpha): + self.beta = beta + self.alpha = alpha + + @property + def gamma(self): + return (1 + self.alpha**2)/self.beta + +class Synchrotron: + """ Synchrotron class to store main properties. """ + def __init__(self, h, L, E0, particle, **kwargs): + self.particle = particle + self._h = h + self.L = L + self.E0 = E0 # Nominal (total) energy of the ring [eV] + + self.ac = kwargs.get('ac') # Momentum compaction factor + self.U0 = kwargs.get('U0') # Energy loss per turn [eV] + self.tau = kwargs.get('tau') # X/Y/S damping times [s] + self.sigma_delta = kwargs.get('sigma_delta') # Equilibrium energy spread + self.sigma_0 = kwargs.get('sigma_0') # Natural bunch length [s] + self.tune = kwargs.get('tune') # X/Y/S tunes + self.emit = kwargs.get('emit') # X/Y emittances in [m.rad] + self.chro = kwargs.get('chro') # X/Y (non-normalized) chromaticities + self.mean_optics = kwargs.get('mean_optics') # Optics object with mean values + + @property + def h(self): + """Harmonic number""" + return self._h + + @h.setter + def h(self,value): + self._h = value + self.L = self.L # call setter + + @property + def L(self): + """Ring circumference [m]""" + return self._L + + @L.setter + def L(self,value): + self._L = value + self._T0 = self.L/c + self._f0 = 1/self.T0 + self._omega0 = 2*np.pi*self.f0 + self._f1 = self.h*self.f0 + self._omega1 = 2*np.pi*self.f1 + self._k1 = self.omega1/c + + @property + def T0(self): + """Revolution time [s]""" + return self._T0 + + @T0.setter + def T0(self, value): + self.L = c*value + + @property + def f0(self): + """Revolution frequency [Hz]""" + return self._f0 + + @f0.setter + def f0(self,value): + self.L = c/value + + @property + def omega0(self): + """Angular revolution frequency [Hz rad]""" + return self._omega0 + + @omega0.setter + def omega0(self,value): + self.L = 2*np.pi*c/value + + @property + def f1(self): + """Fundamental RF frequency [Hz]""" + return self._f1 + + @f1.setter + def f1(self,value): + self.L = self.h*c/value + + @property + def omega1(self): + """Fundamental RF angular frequency[Hz rad]""" + return self._omega1 + + @omega1.setter + def omega1(self,value): + self.L = 2*np.pi*self.h*c/value + + @property + def k1(self): + """Fundamental RF wave number [m**-1]""" + return self._k1 + + @k1.setter + def k1(self,value): + self.L = 2*np.pi*self.h/value + + @property + def gamma(self): + """Relativistic gamma""" + return self._gamma + + @gamma.setter + def gamma(self, value): + self._gamma = value + self._beta = np.sqrt(1 - self.gamma**-2) + self._E0 = self.gamma*self.particle.mass*c**2/e + + @property + def beta(self): + """Relativistic beta""" + return self._beta + + @beta.setter + def beta(self, value): + self.gamma = 1/np.sqrt(1-value**2) + + @property + def E0(self): + """Nominal (total) energy of the ring [eV]""" + return self._E0 + + @E0.setter + def E0(self, value): + self.gamma = value/(self.particle.mass*c**2/e) + + @property + def eta(self): + """Momentum compaction""" + return self.ac - 1/(self.gamma**2) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c67145e --- /dev/null +++ b/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jan 14 18:11:33 2020 + +@author: gamelina +""" + -- GitLab