diff --git a/tracking/particles.py b/tracking/particles.py index 7a97fa35b64ca960442bcb856103b0e4c3ab8722..4378745f0bb80048aa21b266f3c674bf8df3c144 100644 --- a/tracking/particles.py +++ b/tracking/particles.py @@ -8,15 +8,15 @@ Module where particles, bunches and beams are described as objects. """ import numpy as np -import pandas as pd import matplotlib.pyplot as plt -from mbtrack2.tracking.parallel import Mpi import seaborn as sns +import pandas as pd +from mbtrack2.tracking.parallel import Mpi from scipy.constants import c, m_e, m_p, e class Particle: """ - Define a particle object + Define a particle object. Attributes ---------- @@ -47,35 +47,41 @@ class Proton(Particle): class Bunch: """ - Define a bunch + Define a bunch object. Parameters ---------- ring : Synchrotron object mp_number : float, optional - Macro-particle number + Macro-particle number. current : float, optional - Bunch current in [A] + Bunch current in [A]. + track_alive : bool, optional + If False, the code no longer take into account alive/dead particles. + Should be set to True if element such as apertures are used. + Can be set to False to gain a speed increase. alive : bool, optional - If False, the bunch is defined as empty + If False, the bunch is defined as empty. Attributes ---------- mp_number : int - Macro-particle number + Macro-particle number. + alive : array of bool of shape (mp_number,) + Array used to monitor which particle is dead or alive. charge : float - Bunch charge in [C] + Bunch charge in [C]. charge_per_mp : float - Charge per macro-particle in [C] + Charge per macro-particle in [C]. particle_number : int - Number of particles in the bunch + Number of particles in the bunch. current : float - Bunch current in [A] + Bunch current in [A]. mean : array of shape (6,) - Mean position of alive particles for each coordinates + Mean position of alive particles for each coordinates. std : array of shape (6,) Standard deviation of the position of alive particles for each - coordinates + coordinates. emit : array of shape (3,) Bunch emittance for each plane [1]. !!! -> Correct for long ? energy_change : series of shape (alive,) @@ -97,7 +103,8 @@ class Bunch: Springer, Eq.(8.39) of p224. """ - def __init__(self, ring, mp_number=1e3, current=1e-3, alive=True): + def __init__(self, ring, mp_number=1e3, current=1e-3, track_alive=True, + alive=True): self.ring = ring if not alive: @@ -105,20 +112,22 @@ class Bunch: current = 0 self._mp_number = int(mp_number) - particles = {"x":np.zeros((self.mp_number,)), - "xp":np.zeros((self.mp_number,)), - "y":np.zeros((self.mp_number,)), - "yp":np.zeros((self.mp_number,)), - "tau":np.zeros((self.mp_number,)), - "delta":np.zeros((self.mp_number,)), - } - self.particles = pd.DataFrame(particles) - self.alive = pd.Series(np.ones((self.mp_number,),dtype=bool)) + self.dtype = np.dtype([('x',np.float), + ('xp',np.float), + ('y',np.float), + ('yp',np.float), + ('tau',np.float), + ('delta',np.float)]) + + self.particles = np.zeros(self.mp_number, self.dtype) + self.track_alive = track_alive + + self.alive = np.ones((self.mp_number,),dtype=bool) self.current = current if not alive: - self.alive = pd.Series(np.zeros((self.mp_number,),dtype=bool)) + self.alive = np.zeros((self.mp_number,),dtype=bool) - self._energy_change = pd.Series(np.zeros((self.mp_number,),dtype=float)) + self._energy_change = np.zeros((self.mp_number,),dtype=float) def __len__(self): """Return the number of alive particles""" @@ -126,19 +135,25 @@ class Bunch: def __getitem__(self, label): """Return the columns label for alive particles""" - return self.particles.loc[self.alive, label] + if self.track_alive is True: + return self.particles[label][self.alive] + else: + return self.particles[label] def __setitem__(self, label, value): """Set value to the columns label for alive particles""" - self.particles.loc[self.alive, label] = value + if self.track_alive is True: + self.particles[label][self.alive] = value + else: + self.particles[label] = value def __iter__(self): """Iterate over labels""" - return self[:].__iter__() + return self.dtype.names.__iter__() def __repr__(self): """Return representation of alive particles""" - return f'{self[:]!r}' + return f'{pd.DataFrame(self[:])!r}' @property def mp_number(self): @@ -222,11 +237,17 @@ class Bunch: the SynchrotronRadiation class to compute radiation damping. To be changed by Element objects which change the energy, for example RF cavities.""" - return self._energy_change.loc[self.alive] + if self.track_alive is True: + return self._energy_change[self.alive] + else: + return self._energy_change @energy_change.setter def energy_change(self, value): - self._energy_change.loc[self.alive] = value + if self.track_alive is True: + self._energy_change[self.alive] = value + else: + self._energy_change = value def init_gaussian(self, cov=None, mean=None, **kwargs): """ @@ -289,22 +310,28 @@ class Bunch: Returns ------- - bins : IntervalIndex of shape (n_bin,) + bins : array of shape (n_bin,) Bins where the particles are sorted. - sorted_index : Series of shape (self.mp_number,) + sorted_index : array of shape (self.mp_number,) Bin number of each macro-particles. - profile : Series of shape (n_bin,) + profile : array of shape (n_bin - 1,) Number of marco-particles in each bin. + center : array of shape (n_bin - 1,) + Center of each bin. """ - bins = pd.interval_range(start=min(self[dimension]), - end=max(self[dimension]), - periods=n_bin, - name=dimension) - sorted_index = pd.cut(self[dimension], bins) - sorted_index = sorted_index.reindex(self.alive.index) - profile = sorted_index.value_counts().sort_index() - return (bins, sorted_index, profile) + bin_min = self[dimension].min() + bin_min = min(bin_min*0.99, bin_min*1.01) + bin_max = self[dimension].max() + bin_max = max(bin_max*0.99, bin_max*1.01) + + bins = np.linspace(bin_min, bin_max, n_bin) + center = (bins[1:] + bins[:-1])/2 + sorted_index = np.searchsorted(bins, self[dimension], side='left') + sorted_index -= 1 + profile = np.bincount(sorted_index, minlength=n_bin-1) + + return (bins, sorted_index, profile, center) def plot_profile(self, dimension="tau", n_bin=75): """ @@ -318,10 +345,10 @@ class Bunch: Number of bins. The default is 75. """ - bins, sorted_index, profile = self.binning(dimension, n_bin) + bins, sorted_index, profile, center = self.binning(dimension, n_bin) fig = plt.figure() ax = fig.gca() - ax.plot(bins.mid, profile) + ax.plot(center, profile) def plot_phasespace(self, x_var="tau", y_var="delta", plot_type="j"): """ @@ -476,7 +503,7 @@ class Beam: return distance def init_beam(self, filling_pattern, current_per_bunch=1e-3, - mp_per_bunch=1e3): + mp_per_bunch=1e3, track_alive=True): """ Initialize beam with a given filling pattern and marco-particle number per bunch. Then initialize the different bunches with a 6D gaussian @@ -496,6 +523,10 @@ class Beam: Current per bunch in [A] mp_per_bunch : float, optional Macro-particle number per bunch + track_alive : bool, optional + If False, the code no longer take into account alive/dead particles. + Should be set to True if element such as apertures are used. + Can be set to False to gain a speed increase. """ if (len(filling_pattern) != self.ring.h): @@ -507,13 +538,15 @@ class Beam: if filling_pattern.dtype == np.dtype("bool"): for value in filling_pattern: if value == True: - bunch_list.append(Bunch(self.ring, mp_per_bunch, current_per_bunch)) + bunch_list.append(Bunch(self.ring, mp_per_bunch, + current_per_bunch, track_alive)) elif value == False: bunch_list.append(Bunch(self.ring, alive=False)) elif filling_pattern.dtype == np.dtype("float64"): for current in filling_pattern: if current != 0: - bunch_list.append(Bunch(self.ring, mp_per_bunch, current)) + bunch_list.append(Bunch(self.ring, mp_per_bunch, + current_per_bunch, track_alive)) elif current == 0: bunch_list.append(Bunch(self.ring, alive=False)) else: diff --git a/tracking/wakepotential.py b/tracking/wakepotential.py index 41bd2d019270b976e91883c1c8f3d8722b93fa8a..4a0c599d1cd9824fb566bbcf3eed8a068f01e324 100644 --- a/tracking/wakepotential.py +++ b/tracking/wakepotential.py @@ -85,18 +85,19 @@ class WakePotential(Element): """ # Get binning data - a, b, c = bunch.binning(n_bin=self.n_bin) + a, b, c, d = bunch.binning(n_bin=self.n_bin) self.bins = a self.sorted_index = b self.profile = c - self.bin_size = self.bins.length + self.center = d + self.bin_size = self.bins[1] - self.bins[0] # Compute charge density self.rho = bunch.charge_per_mp*self.profile/(self.bin_size*bunch.charge) self.rho = np.array(self.rho) # Compute time array - self.tau = np.array(self.bins.mid) + self.tau = np.array(self.center) self.dtau = self.tau[1] - self.tau[0] # Add N values before and after rho and tau