Interact and run this jupyter notebook online:
Also find this lecture rendered as HTML slides on github $\nearrow$ along with the source repository $\nearrow$.
Imports and modules:
from config import (np, plt, plot_rfwave, tqdm, trange,
beta, gamma, Machine, track_one_turn,
charge, mass, emittance, hamiltonian,
plot_hamiltonian, plot_rf_overview,
plot_dist, plot_mp)
from scipy.constants import m_p, e, c
%matplotlib inline
If the progress bar by tqdm
(trange
) in this document does not work, run this:
!jupyter nbextension enable --py widgetsnbextension
Enabling notebook extension jupyter-js-widgets/extension...
- Validating: OK
For reference, the CERN Proton Synchrotron (PS) parameters:
You as operator in the (simulation) control centre shall pay attention to preserve the longitudinal emittance throughout the acceleration!
$\implies$ Investigate the tracking results. Can you explain what happens? Think about what you need to change in the machine settings to achieve emittance preservation.
We start by instantiating the PS, Machine(...)
:
(hint: hit both Shift+Tab
keys inside the parentheses ()
below to get info about the possible arguments to Machine
)
m = Machine()
assert m.phi_s > 0, "machine is not accelerating...?"
If you want to check the rf systems setup, uncomment (remove #
) the following plot command from the previous lecture:
# plot_rf_overview(m);
We initialise a Gaussian bunch distribution with very small rms bunch length $\sigma_z=1\,$m (so that the small-amplitude approximation holds):
sigma_z = 1
The "matched" rms momentum spread $\sigma_{\Delta p}$ (remember, $\sigma_{\Delta p}$ and $\sigma_z$ are linked via equal Hamiltonian values $\mathcal{H}_0$, the equilibrium condition):
sigma_deltap = np.sqrt(
2 * m.p0() / -m.eta(0) *
charge * m.voltage * np.pi * m.harmonic / (beta(gamma(m.p0())) * c * m.circumference**2)
) * sigma_z
(NB: with this small emittance / rms size in phase space, the Gaussian tails cannot reach outside the separatrix due to numerical reasons, so no rejection sampling is needed in this particular case.
Finite machine precision at FP64 is $\varepsilon\approx 2^{-53}$, therefore one can only generate pseudo-random numbers from the Gaussian distribution up to an amplitude of $x$ where $\exp(-x^2/2)=2^{-53}$, i.e.
np.sqrt(2 * -np.log(2**-53))
8.571674348652905
$\implies$ given $\sigma_z=1\,$m, no particles can be generated outside of $z=8.6\,$m and the equivalent Hamiltonian contour in phase space)
N = 1000
np.random.seed(12345)
z = np.random.normal(loc=0, scale=sigma_z, size=N)
deltap = np.random.normal(loc=0, scale=sigma_deltap, size=N)
plot_hamiltonian(m)
plt.scatter(z, deltap / m.p0(), marker='.', s=1);
$(\Delta\gamma)_\mathrm{turn} = \cfrac{\Delta E_\mathrm{tot}}{m_0c^2} = \cfrac{qV\sin(\varphi_s)}{m_0c^2}$
such that accelerating from $\gamma_\mathrm{ref}=3.1$ to $\gamma_\mathrm{ref}=27.7$ takes as many turns as
$n_\mathrm{turns}=\cfrac{27.7-3.1}{(\Delta\gamma)_\mathrm{turn}}$
dgamma_per_turn = charge * m.voltage * np.sin(m.phi_s) / (mass * c**2)
n_turns = int(np.ceil((27.7 - 3.13) / dgamma_per_turn))
n_turns
261756
Record longitudinal emittance during tracking:
epsn_z = np.zeros(n_turns, dtype=np.float64)
epsn_z[0] = emittance(z, deltap)
Let's go tracking! ($\approx 1\,$min total time)
for i_turn in trange(1, n_turns):
z, deltap = track_one_turn(z, deltap, m)
epsn_z[i_turn] = emittance(z, deltap)
0%| | 0/261755 [00:00<?, ?it/s]
We have reached the extraction energy of $\gamma=27.7$:
m.gamma_ref
27.69995958340248
Check that we properly conserved the rms emittance:
plt.plot(np.arange(n_turns) / 100000, epsn_z / e)
plt.xlabel('Turns [100k]')
plt.ylabel('$\epsilon_z$ [eV.s]');
$\implies$ use the following phase-space plot besides the plotting functions plot_hamiltonian
and plot_rf_overview
above (from last lecture) as diagnostics. You can stop the tracking after a certain turn to investigate, e.g. by changing the value of n_turns
inside the trange
counter in the for
loop.
plot_hamiltonian(m);
plt.scatter(z % 600, deltap / m.p0(), marker='.', s=1);
Last lecture we looked at the thermal equilibrium distribution in small-amplitude approximation ($=$ Gaussian distribution) to generate a bunch of macro-particles in longitudinal phase space.
For the Gaussian distribution, the target rms bunch length $\hat{\sigma}_z$ directly enters the generator as a multiplication factor: a random variable $x$ following a Gaussian normal distribution has $\sigma_x=1$, then $z=\hat{\sigma}_z\cdot x$ will provide the Gaussian normal distributed random variable $z$ with $\sigma_z=\hat{\sigma}_z$.
For a general thermal equilibrium distribution in a nonlinear Hamiltonian, the PDF becomes
$$\psi=\psi(\mathcal{H})\propto \exp\left(\frac{\mathcal{H}}{\mathcal{H}_0}\right)$$for a constant parameter $\mathcal{H}_0$. The value of $\mathcal{H}_0$ is determined through $\hat{\sigma}_z$ such that $\psi$ yields a distribution with corresponding rms bunch length. For nonlinear $\mathcal{H}$, the parameter $\mathcal{H}_0$ will generally have to be computed numerically. This process is referred to as "matching"!
In the following we will investigate the full nonlinear Hamiltonian case, $\psi=\psi(\mathcal{H})$.
Let's work with the python library PyHEADTAIL
(simulation suite for beam dynamics). If PyHEADTAIL
is not yet installed, please pip-install it:
$ pip install PyHEADTAIL==1.16.1
PyHEADTAIL
provides a class to represent rf buckets (for plotting as well as for matching):
from PyHEADTAIL.trackers.rf_bucket import RFBucket
PyHEADTAIL v1.16.1
We define a convenience function to provide RFBucket
instances given a Machine
instance:
def get_pyht_rfbucket(machine):
m = machine
deltap_per_turn = charge * m.voltage / (beta(gamma(m.p0())) * c) * np.sin(m.phi_s)
rfb = RFBucket(m.circumference, m.gamma_ref, mass, charge, [m.alpha_c], deltap_per_turn,
[m.harmonic], [m.voltage], [np.pi + m.phi_s])
# PyHEADTAIL has a different convention for the phase and is offset by π compared to our lecture
return rfb
m = Machine(gamma_ref=3.13)
rfb = get_pyht_rfbucket(m)
from PyHEADTAIL.particles.rfbucket_matching import (
ThermalDistribution, WaterbagDistribution, ParabolicDistribution)
sigma_z = 8
Computing the (initial) guess for $\mathcal{H}_0$ based on the small-amplitude approximation:
H0 = rfb.guess_H0(sigma_z, from_variable='sigma')
H0
153.62263838682642
Small-amplitude approximation and a stationary rf bucket below transition, $\varphi_s=0$: particles follow harmonic oscillation with
$$\mathcal{H}_\mathrm{stat,small}(z,\Delta p) = \frac{1}{2}\frac{-\eta}{p_0} \Delta p{}^2 + \frac{qV}{\beta c}\cdot \cfrac{\pi h}{C^2}\cdot z^2 $$The PDF of the thermal distribution becomes
$$\psi(\mathcal{H})\propto\exp\left(\frac{\mathcal{H}_{stat,small}}{\mathcal{H}_0}\right)=\exp\left(\frac{1}{2}\frac{\frac{qV\,2\pi h}{\beta c C^2}}{\mathcal{H}_0}\cdot z^2\right) \cdot \exp\left(\frac{1}{2}\frac{-\eta/p_0}{\mathcal{H}_0}\cdot \Delta p^2\right)$$which is simply a bi-Gaussian distribution in $\Delta p$ and $z$!
The constant $\mathcal{H}_0$ is determined by a choice of the rms bunch length $\sigma_z$ (or equivalently the rms momentum deviation $\sigma_\delta=\sigma_{\Delta p}/p_0$):
$$\mathcal{H}_0 = \mathcal{H}_\mathrm{stat,small}(\sigma_z,\Delta p=0) = \frac{qV}{\beta c}\cdot \cfrac{\pi h}{C^2}\cdot \sigma_z{}^2 \mathrel{\color{red}{\stackrel{!}{=}}} \mathcal{H}_\mathrm{stat,small}(z=0,\sigma_{\Delta p}) = \frac{1}{2}\frac{-\eta}{p_0} \sigma_{\Delta p}{}^2$$$\implies$ as equilibrium or matching condition, $\sigma_z$ and $\sigma_\delta$ are linked to each other via equal Hamiltonian values!
plot_dist(ThermalDistribution, rfb, H0=0.05*H0);
$\implies$ adjust the $\sigma_z$ and observe the impact of the non-linearities on the matched $\psi$ (small $\sigma_z$ will make the distribution more Gaussian-like)!
Change the distribution type from ThermalDistribution
to WaterbagDistribution
(or e.g. ParabolicDistribution
):
plot_dist(WaterbagDistribution, rfb, H0=H0*2);
Given a target rms bunch length $\hat{\sigma}_z$, define the following "matching" algorithm:
This matching algorithm is implemented in the RFBucketMatcher
class in PyHEADTAIL
:
from PyHEADTAIL.particles.generators import RFBucketMatcher
rfb_matcher = RFBucketMatcher(rfb, ThermalDistribution, sigma_z=sigma_z)
rfb_matcher.integrationmethod = 'cumtrapz' # better behaved numerical integration method
Calling the RFBucketMatcher.generate
method will
(1.) iterate on $\mathcal{H}_0$ until the numerical integration of $\psi(\mathcal{H})$ for the bunch length converges to $\hat{\sigma}_z$, and then
(2.) sample this $\psi(\mathcal{H})$ by rejection sampling (see previous lecture) to generate the macro-particle phase-space coordinates $(z,\delta)$
z, delta, _, _ = rfb_matcher.generate(int(1e5))
*** Maximum RMS bunch length 8.888845463360669m. ... distance to target bunch length: -7.9156e+00 ... distance to target bunch length: 6.5599e-01 ... distance to target bunch length: 6.1413e-01 ... distance to target bunch length: -3.2377e-01 ... distance to target bunch length: 2.5291e-01 ... distance to target bunch length: 6.5279e-02 ... distance to target bunch length: -4.6398e-03 ... distance to target bunch length: 2.6859e-04 ... distance to target bunch length: 1.0364e-06 ... distance to target bunch length: -8.7361e-06 --> Bunch length: 8.000001036447514 --> Emittance: 2.108543434891298
The relevant code in the RFBucketMatcher
implementing the rejection sampling:
xmin, xmax = self.rfbucket.z_left, self.rfbucket.z_right
ymin = -self.rfbucket.dp_max(self.rfbucket.z_right)
ymax = -ymin
uniform = np.random.uniform
n_gen = macroparticlenumber
u = uniform(low=xmin, high=xmax, size=n_gen)
v = uniform(low=ymin, high=ymax, size=n_gen)
s = uniform(size=n_gen)
def mask_out(s, u, v):
return s >= self.psi(u, v)
masked_out = mask_out(s, u, v)
while np.any(masked_out):
masked_ids = np.where(masked_out)[0]
n_gen = len(masked_ids)
u[masked_out] = uniform(low=xmin, high=xmax, size=n_gen)
v[masked_out] = uniform(low=ymin, high=ymax, size=n_gen)
s[masked_out] = uniform(size=n_gen)
masked_out[masked_ids] = mask_out(
s[masked_out], u[masked_out], v[masked_out]
)
Compare the converged value of $\mathcal{H}_0$...
rfb_matcher.psi_object.H0
216.61619727806732
... to the small-amplitude approximation value:
rfb.guess_H0(0.1 * sigma_z, from_variable='sigma')
1.5362263838682642
$\implies$ are the matched $\mathcal{H}_0$ and the small-amplitude approximation value closer for smaller target $\hat{\sigma}_z$?
Let's have a look at the generated macro-particle distribution:
plot_mp(z, delta, rfb);
Does the rms bunch length of the macro-particle distribution match the chosen target $\hat{\sigma}_z$?
np.std(z)
8.00462076948197
sigma_z
8
Equilibrium distributions for nonlinear Hamiltonians
$\implies$ thermal distribution $\psi\propto \exp(\mathcal{H}/\mathcal{H}_0)$ where constant $\mathcal{H}_0$ is determined by rms bunch length (or momentum spread)
We discussed:
(1.) iterate on $\mathcal{H}_0$ until the numerical integration of $\psi(\mathcal{H})$ for the bunch length converges to $\hat{\sigma}_z$
(...)
$\implies$ this poses a root-finding problem!
The relevant moments of a generic distribution function $\psi$ are
where $z_\text{left}$ and $z_\text{right}$ denote the left and right boundary of the rf bucket, and the second integral integrates over $\Delta p$ from the lower to the upper separatrix at position $z$.
The equation for the rms bunch length expresses the root-finding problem.
Given a (non-normalised) thermal distribution $\psi(\mathcal{H}) = \exp(\mathcal{H}/\mathcal{H}_0)$ and a target rms bunch length $\hat{\sigma}_z$, then $\mathcal{H}_0$ is determined by the integral equation
$$0 = \hat{\sigma}_z{}^2 - \cfrac{1}{Q_z}\int\limits_{z_\text{left}}^{z_\text{right}} dz \int_\text{separatrix} d(\Delta p)\cdot\exp\left(\cfrac{\mathcal{H}(z,\Delta p)}{\color{red}{\mathcal{H}_0}}\right) \cdot \left[z - \langle z \rangle \right]^2$$In the case of a linear harmonic oscillator Hamiltonian $\mathcal{H}_\text{lin}$ (typical result of small-amplitude approximation), the solution is given by
$$\mathcal{H}_{0,\text{lin}}(\hat{\sigma}_z)=\frac{qV}{\beta c}\cdot \frac{\pi h}{C^2}\cdot \hat{\sigma}_z{}^2$$For nonlinear $\mathcal{H}(z,\Delta p)$, numerical root-finding algorithms can be employed (the integrals are solved numerically, too). Using as input the expression $\mathcal{H}_{0,\text{lin}}(\sigma_z)$, one varies $\sigma_z$ and obtains a converged value via
scipy.optimize
!¶from scipy.optimize import brentq, newton
At what $x$ does $\cfrac{1}{\sqrt{x}+1}$ take the value 0.4
?
def f(x):
return 1 / (np.sqrt(x) + 1) - 0.4
Brent-Dekker algorithm with interval $x\in[0, 4]$:
brentq(f, 0, 4)
2.250000000000207
Secant method with initial values $x_0=0$, $x_1=10^{-4}$:
newton(f, 0)
2.2499999999999956
x = np.linspace(0, 4, 1000)
plt.plot(x, f(x))
plt.axhline(0, c='k', lw=2)
plt.xlabel('$x$')
plt.ylabel('$f(x)$');
def secant_method(f, x0, x1, iterations, rtol=2e-12):
"""Return the root calculated using the secant method."""
for i in range(iterations):
x2 = x1 - f(x1) * (x1 - x0) / float(f(x1) - f(x0))
if np.abs(x2 - x1) / x1 < rtol:
break
x0, x1 = x1, x2
return x2
secant_method(f, 0, 1, 100)
2.249999999999999
(The scipy
implementation of the secant method in newton()
computes the second guess $x_1$ as $x_1=x_0\cdot 1.0001\pm 0.0001$.)
Numerical:
Physical:
Based on the stationary rf bucket (no acceleration) case, we investigate the difference between initialising a bi-Gaussian and a matched thermal distribution based on the nonlinear Hamiltonian $\mathcal{H}$.
Start with the bi-Gaussian (simulation as previous lecture):
m = Machine(phi_s=0)
sigma_z = 13.5
def generate_gaussian_in_rfbucket(N, sigma_z, machine, seed=12345, margin=0.05):
'''Generate a bi-Gaussian distribution with N macro-particles,
rms bunch length sigma_z and a matched sigma_deltap via the
machine settings.
'''
np.random.seed(seed)
m = machine
sigma_deltap = np.sqrt(
2 * m.p0() / -m.eta(0) *
charge * m.voltage * np.pi * m.harmonic / (beta(gamma(m.p0())) * c * m.circumference**2)
) * sigma_z
z_ini = np.random.normal(loc=0, scale=sigma_z, size=N)
deltap_ini = np.random.normal(loc=0, scale=sigma_deltap, size=N)
H_safetymargin = margin * hamiltonian(0, 0, m)
H_values = hamiltonian(z_ini, deltap_ini, m) - H_safetymargin
while any(H_values >= 0):
mask_bad = H_values >= 0
N_bad = np.sum(mask_bad)
print (N_bad)
# re-initialise bad particles:
z_ini[mask_bad] = np.random.normal(loc=0, scale=sigma_z, size=N_bad)
deltap_ini[mask_bad] = np.random.normal(loc=0, scale=sigma_deltap, size=N_bad)
# re-evaluate rejection condition
H_values = hamiltonian(z_ini, deltap_ini, m) - H_safetymargin
return z_ini, deltap_ini
N = 10000
n_turns = 5000
z_ini, deltap_ini = generate_gaussian_in_rfbucket(N, sigma_z, m)
860 83 4 1
plot_mp(z_ini, deltap_ini / m.p0(), rfb=get_pyht_rfbucket(m), n_bins=20);
Track the bi-Gaussian distribution...
z = np.zeros((n_turns, N), dtype=np.float64)
deltap = np.zeros_like(z)
z[0] = z_ini
deltap[0] = deltap_ini
for i_turn in trange(1, n_turns):
z[i_turn], deltap[i_turn] = track_one_turn(z[i_turn - 1], deltap[i_turn - 1], m)
0%| | 0/4999 [00:00<?, ?it/s]
The rms emittance evolution:
epsn_z = np.array([emittance(z_i, deltap_i) for z_i, deltap_i in zip(z, deltap)])
ylim_m = np.median(4 * np.pi * epsn_z / e)
ylim_d = 1.1 * np.max(np.abs(ylim_m - 4 * np.pi * epsn_z / e))
plt.plot(4 * np.pi * epsn_z / e)
plt.ylim(ylim_m - ylim_d, ylim_m + ylim_d)
plt.xlabel('Turns')
plt.ylabel('$4\pi\epsilon_z$ [eV.s]');
$\leadsto$ the Gaussian particle distribution is not exactly in equilibrium for sufficiently large rms values in the nonlinear potential, the particles filament and the rms emittance grows (a little)!
$\implies$ compare to using full nonlinear Hamiltonian to construct PDF $\psi(\mathcal{H})\propto\exp\left(\cfrac{\mathcal{H}}{\mathcal{H}_0}\right)$
rfb = get_pyht_rfbucket(m)
rfb_matcher = RFBucketMatcher(rfb, ThermalDistribution, sigma_z=sigma_z)
rfb_matcher.integrationmethod = 'cumtrapz'
z_ini, delta_ini, _, _ = rfb_matcher.generate(N)
*** Maximum RMS bunch length 14.146533693849044m. ... distance to target bunch length: -1.3365e+01 ... distance to target bunch length: 2.5641e-01 ... distance to target bunch length: 2.4078e-01 ... distance to target bunch length: -1.8484e-01 ... distance to target bunch length: 5.7289e-02 ... distance to target bunch length: 1.0673e-02 ... distance to target bunch length: -1.5085e-04 ... distance to target bunch length: 1.9092e-06 ... distance to target bunch length: -4.9149e-06 --> Bunch length: 13.500001909209015 --> Emittance: 5.835458508892962
deltap_ini = delta_ini * m.p0()
plot_mp(z_ini, delta_ini, rfb, n_bins=20);
Track the matched thermal distribution...
z = np.zeros((n_turns, N), dtype=np.float64)
deltap = np.zeros_like(z)
z[0] = z_ini
deltap[0] = deltap_ini
for i_turn in trange(1, n_turns):
z[i_turn], deltap[i_turn] = track_one_turn(z[i_turn - 1], deltap[i_turn - 1], m)
0%| | 0/4999 [00:00<?, ?it/s]
The rms emittance evolution:
epsn_z = np.array([emittance(z_i, deltap_i) for z_i, deltap_i in zip(z, deltap)])
ylim_m = np.median(4 * np.pi * epsn_z / e)
plt.plot(4 * np.pi * epsn_z / e)
plt.ylim(ylim_m - ylim_d, ylim_m + ylim_d)
plt.xlabel('Turns')
plt.ylabel('$4\pi\epsilon_z$ [eV.s]');
$\implies$ this result shows that the nonlinearly matched thermal distribution is in equilibrium from the start (up to macro-particle noise, the fluctuations reduce with $1/\sqrt{N}$)!
m = Machine(phi_s=0)
sigma_z = 8
z_ini, deltap_ini = generate_gaussian_in_rfbucket(N, sigma_z, m, margin=0.15)
24
Simulate a dipole injection mismatch (e.g. when the rf phase is not well synchronised between the injector and the synchrotron):
z_ini -= 0.5 * sigma_z
4 meter mismatch in $z$ correspond to a phase mismatch of 16 degree:
4 / (m.circumference / m.harmonic) * 360
16.04281826366305
plot_mp(z_ini, deltap_ini / m.p0(), rfb=get_pyht_rfbucket(m), n_bins=20);
$\implies$ note the offset towards negative $z$, the contours of the macro-particle density are no longer matched to the Hamiltonian contours.
The safety margin
inside the separatrix (where no particles are generated in generate_gaussian_in_rfbucket
) should be chosen large enough such that no particles are located outside the rf bucket after the mismatch:
assert all(hamiltonian(z_ini, deltap_ini, m) < 0), 'particles have been generated outside the rf bucket!'
Tracking the mismatched distribution of macro-particles:
z = np.zeros((n_turns, N), dtype=np.float64)
deltap = np.zeros_like(z)
z[0] = z_ini
deltap[0] = deltap_ini
for i_turn in trange(1, n_turns):
z[i_turn], deltap[i_turn] = track_one_turn(z[i_turn - 1], deltap[i_turn - 1], m)
0%| | 0/4999 [00:00<?, ?it/s]
plt.plot(np.mean(z, axis=1))
plt.xlabel('Turns')
plt.ylabel(r'$\langle z \rangle$ [m]');
$\implies$ exponential decay of the initial offset (due to the non-linearity of the rf bucket)
plt.plot(np.std(z, axis=1))
plt.xlabel('Turns')
plt.ylabel(r'$\sigma_z$ [m]');
$\implies$ saturation of the rms bunch length growth
epsn_z = np.array([emittance(z_i, deltap_i) for z_i, deltap_i in zip(z, deltap)])
plt.plot(epsn_z / e)
plt.xlabel('Turns')
plt.ylabel('$\epsilon_z$ [eV.s]');
$\implies$ in this example, 10% emittance growth as a result of the 4 meter injection offset.
plot_mp(z[-1], deltap[-1] / m.p0(), rfb=get_pyht_rfbucket(m), n_bins=40);
$\implies$ the filamentation of the macro-particle distribution is clearly visible!
m = Machine(phi_s=0)
sigma_z = 8
z_ini, deltap_ini = generate_gaussian_in_rfbucket(N, sigma_z, m, margin=0.15)
24
Simulate a quadrupole injection mismatch (e.g. when the rf voltage (rf bucket height) is not matched between the injector and the synchrotron):
deltap_ini *= 0.5
plot_mp(z_ini, deltap_ini / m.p0(), rfb=get_pyht_rfbucket(m), n_bins=20);
$\implies$ note the squeezed rms momentum spread, the contours of the macro-particle density are no longer matched to the Hamiltonian contours.
Tracking the mismatched distribution of macro-particles:
z = np.zeros((n_turns, N), dtype=np.float64)
deltap = np.zeros_like(z)
z[0] = z_ini
deltap[0] = deltap_ini
for i_turn in trange(1, n_turns):
z[i_turn], deltap[i_turn] = track_one_turn(z[i_turn - 1], deltap[i_turn - 1], m)
0%| | 0/4999 [00:00<?, ?it/s]
plt.plot(np.mean(z, axis=1))
plt.xlabel('Turns')
plt.ylabel(r'$\langle z \rangle$ [m]');
$\implies$ only residual centroid fluctuations (due to macro-particle noise), note the amplitude of the oscillation in comparison to the rf bucket length!
plt.plot(np.std(z, axis=1))
plt.xlabel('Turns')
plt.ylabel(r'$\sigma_z$ [m]');
$\implies$ exponential decay of the initial momentum mismatch (due to the non-linearity of the rf bucket)
epsn_z = np.array([emittance(z_i, deltap_i) for z_i, deltap_i in zip(z, deltap)])
plt.plot(epsn_z / e)
plt.xlabel('Turns')
plt.ylabel('$\epsilon_z$ [eV.s]');
$\implies$ in this example, 20% emittance growth as a result of the 50% momentum spread mismatch.
plot_mp(z[-1], deltap[-1] / m.p0(), rfb=get_pyht_rfbucket(m), n_bins=40);
$\implies$ again, the filamentation of the macro-particle distribution is clearly visible!
To avoid the emittance growth in the simulation of the acceleration ramp in the CERN Proton Synchrotron, the operators should switch the synchronous phase at transition energy in order to restore phase focusing. (The previous lectures provide the conceptual background to understand what is going on.)
The switch in the synchronous phase can be implemented in the tracking loop as follows:
m.phi_s
(which determines the energy gain) and store the value in a variable, e.g. phi_s_1
, before the tracking startsm.phi_s
to the correct new value phi_s_1
phi_s_1 = # compute here the correct new synchronous phase above transition
# by using the one from below transition which is m.phi_s
for i_turn in trange(1, n_turns):
z, deltap = track_one_turn(z, deltap, m)
epsn_z[i_turn] = emittance(z, deltap)
condition = # write here X > Y after the "="
if condition:
m.phi_s = phi_s_1