Skip to content

Commit 043f1cf

Browse files
committed
updated sps proton example with tune ripple
1 parent bba2048 commit 043f1cf

File tree

5 files changed

+500
-0
lines changed

5 files changed

+500
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Initial script to generate a SPS Q20 proton line
3+
"""
4+
import fma_ions
5+
import xobjects as xo
6+
import json
7+
8+
# Generate line without magnet errors
9+
sps = fma_ions.SPS_sequence_maker(qx0=20.13, qy0=20.18, ion_type='proton', proton_optics='q20')
10+
line = sps.generate_xsuite_seq()
11+
12+
with open('sps_q20_proton_line.json', 'w') as fid:
13+
json.dump(line.to_dict(), fid, cls=xo.JEncoder)
14+
15+
# Generate line with magnet errors
16+
sps2 = fma_ions.SPS_sequence_maker(qx0=20.13, qy0=20.18, ion_type='proton', proton_optics='q20')
17+
line2 = sps2.generate_xsuite_seq(add_non_linear_magnet_errors=True)
18+
19+
with open('sps_q20_proton_line_with_errors.json', 'w') as fid:
20+
json.dump(line2.to_dict(), fid, cls=xo.JEncoder)
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""
2+
Minimalistic example to load an xsuite line, generate a particle distribution,
3+
install space charge, ripple and track the particles.
4+
5+
For SPS Q20 protons
6+
"""
7+
import numpy as np
8+
import xtrack as xt
9+
import xpart as xp
10+
import xfields as xf
11+
import xobjects as xo
12+
from records import Records, get_k_ripple_summed_signal
13+
import time
14+
15+
# Boolean parameters
16+
add_tune_ripple = True
17+
add_non_linear_magnet_errors = True
18+
fname_line = 'sps_q20_proton_line_with_errors.json' if add_non_linear_magnet_errors else 'sps_q20_proton_line.json'
19+
20+
# Tracking parameters and context
21+
n_turns = 20
22+
n_particles = 20
23+
context = xo.ContextCpu(omp_num_threads='auto')
24+
nturns_profile_accumulation_interval = 100 # turn interval between which to aggregate transverse particles for histogram
25+
nbins = 140 # number of bins for histograms of transverse monitors
26+
27+
# Tune ripple parameters
28+
I_QF_QD = 70. # quadrupolar circuit current in ampere used (approx). 70 A used for SPS Pb ions
29+
amp_adjustment_factor_from_current = 70./I_QF_QD
30+
31+
# Transfer function factors, by which we amplitude-adjust the ripple
32+
# From 2018 TBT vs current tune data analysis, by Elias using data from Hannes
33+
# See Elias Waagaard's PhD thesis for details
34+
# For SPS Pb ions, 70 A was used. If different current, adjust amplitudes accordinly
35+
a_50 = 1.0 * amp_adjustment_factor_from_current
36+
a_150 = 0.5098 * amp_adjustment_factor_from_current
37+
a_300 = 0.2360 * amp_adjustment_factor_from_current
38+
a_600 = 0.1095 * amp_adjustment_factor_from_current
39+
40+
# Desired ripple frequencies and amplitudes - typical values without 50 Hz compensation
41+
ripple_freqs = np.array([50.0, 150.0, 300.0, 600.0])
42+
kqf_amplitudes = np.array([1.0141062492337905e-06*a_50, 1.9665396648867768e-07*a_150, 3.1027971430227987e-07*a_300, 4.5102937494506313e-07*a_600])
43+
kqd_amplitudes = np.array([1.0344583265981035e-06*a_50, 4.5225494700433166e-07*a_150, 5.492718035100028e-07*a_300, 4.243698659233664e-07*a_600])
44+
kqf_phases = np.array([0.7646995873548973, 2.3435670020522825, -1.1888958255027886, 2.849205512655574])
45+
kqd_phases = np.array([0.6225130389353318, -1.044380492147742, -1.125401419249802, -0.30971750008702853])
46+
47+
# Typical values with 50 Hz compensation
48+
"""
49+
kqf_amplitudes = np.array([1.6384433351717334e-08*a_50, 2.1158318710898557e-07*a_150, 3.2779826135772383e-07*a_300, 4.7273849059164697e-07*a_600])
50+
kqd_amplitudes = np.array([2.753093584240069e-07*a_50, 4.511100472630622e-07*a_150, 5.796354631307802e-07*a_300, 4.5487568431405856e-07*a_600])
51+
kqf_phases = np.array([0.9192671763874849, 0.030176158557178895, 0.5596488397663701, 0.050511945653341016])
52+
kqd_phases = np.array([0.9985112397758237, 3.003827454851132, 0.6369886405485959, -3.1126209931146547])
53+
"""
54+
55+
# Beam parameters
56+
exn = 1.0e-6 # horizontal emittance in m
57+
eyn = 1.0e-6 # vertical emittance in m
58+
sigma_z = 0.2 # bunch length in m
59+
Nb = 1e11 # number of particles in the bunch
60+
61+
# Space charge parameters
62+
q_val = 1.0 # q-value of longitudinal profile, for space charge
63+
num_spacecharge_interactions = 1080
64+
65+
# Load the line from a file
66+
line = xt.Line.from_json(fname_line)
67+
68+
# Set RF voltage to different value, if desired
69+
#line['actcse.31632' ].voltage = 3.0e6
70+
#print('RF voltage set to {:.3e} V\n'.format(line['actcse.31632'].voltage))
71+
72+
# Twiss command, inspect tunes and reference particle
73+
tw = line.twiss()
74+
print('Tunes: Qx = {:.6f}, Qy = {:.6f}'.format(tw.qx, tw.qy))
75+
print('Reference particle: {}'.format(line.particle_ref.show()))
76+
77+
# Create horizontal beam monitor
78+
monitorH = xt.BeamProfileMonitor(
79+
start_at_turn=nturns_profile_accumulation_interval/2, stop_at_turn=n_turns,
80+
frev=1,
81+
sampling_frequency=1/nturns_profile_accumulation_interval,
82+
n=nbins,
83+
x_range=0.07,
84+
y_range=0.07)
85+
line.insert_element(index='bwsrc.51637', element=monitorH, name='monitorH')
86+
87+
# Create vertical beam monitor
88+
monitorV = xt.BeamProfileMonitor(
89+
start_at_turn=nturns_profile_accumulation_interval/2, stop_at_turn=n_turns,
90+
frev=1,
91+
sampling_frequency=1/nturns_profile_accumulation_interval,
92+
n=nbins,
93+
x_range=0.07,
94+
y_range=0.07)
95+
line.insert_element(index='bwsrc.41677', element=monitorV, name='monitorV')
96+
97+
98+
99+
# Build tracker
100+
line.build_tracker(_context=context)
101+
102+
# Generate a particle distribution
103+
particles = xp.generate_matched_gaussian_bunch(_context=context,
104+
num_particles=n_particles,
105+
total_intensity_particles=Nb,
106+
nemitt_x=exn,
107+
nemitt_y=eyn,
108+
sigma_z=sigma_z,
109+
particle_ref=line.particle_ref,
110+
line=line)
111+
112+
######### Frozen space charge #########
113+
114+
# Store the initial buffer of the line
115+
_buffer = line._buffer
116+
line.discard_tracker()
117+
118+
# Install space charge
119+
lprofile = xf.LongitudinalProfileQGaussian(
120+
number_of_particles = Nb,
121+
sigma_z = sigma_z,
122+
z0=0.,
123+
q_parameter=q_val)
124+
125+
# Install frozen space charge as base
126+
xf.install_spacecharge_frozen(line = line,
127+
particle_ref = line.particle_ref,
128+
longitudinal_profile = lprofile,
129+
nemitt_x = exn, nemitt_y = eyn,
130+
sigma_z = sigma_z,
131+
num_spacecharge_interactions = num_spacecharge_interactions)
132+
133+
# Add tune ripple if desired
134+
if add_tune_ripple:
135+
136+
# Create ripple in quadrupolar knobs, convert phases to turns
137+
turns_per_sec = 1/tw['T_rev0']
138+
ripple_periods = turns_per_sec/ripple_freqs #).astype(int) # number of turns particle makes during one ripple oscillation
139+
kqf_phases_turns = kqf_phases * turns_per_sec # convert time domain to turn domain, i.e. multiply with turns/sec
140+
kqd_phases_turns = kqd_phases * turns_per_sec # convert time domain to turn domain, i.e. multiply with turns/sec
141+
142+
# Generate custom tune ripple signal
143+
kqf_ripple, kqd_ripple = get_k_ripple_summed_signal(n_turns, ripple_periods, kqf_amplitudes, kqd_amplitudes,
144+
kqf_phases_turns, kqd_phases_turns)
145+
146+
# Save initial values
147+
kqf0 = line.vars['kqf']._value
148+
kqd0 = line.vars['kqd']._value
149+
150+
print('Norm. quadrupolar gradients will oscillate with')
151+
print('kqf = {:.4e} +/- {:.3e}'.format(kqf0, max(kqf_ripple)))
152+
print('kqd = {:.4e} +/- {:.3e}'.format(kqd0, max(kqd_ripple)))
153+
154+
155+
# Build tracker
156+
line.build_tracker(_buffer=_buffer)
157+
158+
159+
# Initialize the dataclasses to store particle values
160+
tbt = Records.init_zeroes(n_turns) # only emittances and bunch intensity
161+
tbt.update_at_turn(0, particles, tw)
162+
tbt.store_initial_particles(particles)
163+
tbt.store_twiss(tw.to_pandas())
164+
165+
# Empty arrays to store data
166+
X_data = np.zeros(n_turns)
167+
Y_data = np.zeros(n_turns)
168+
kqf_data = np.zeros(n_turns)
169+
kqd_data = np.zeros(n_turns)
170+
X_data[0] = np.mean(particles.x)
171+
Y_data[0] = np.mean(particles.y)
172+
kqf_data[0] = line.vars['kqf']._value
173+
kqd_data[0] = line.vars['kqd']._value
174+
175+
#### Track the particles ####
176+
time00 = time.time()
177+
178+
for turn in range(1, n_turns):
179+
180+
# Print out info at specified intervals
181+
if turn % 5 == 0:
182+
print('\nTracking turn {}'.format(turn))
183+
184+
########## ----- Exert TUNE RIPPLE if desired ----- ##########
185+
if add_tune_ripple:
186+
line.vars['kqf'] = kqf0 + kqf_ripple[turn-1]
187+
line.vars['kqd'] = kqd0 + kqd_ripple[turn-1]
188+
189+
# ----- Track and update records for tracked particles ----- #
190+
line.track(particles, num_turns=1)
191+
192+
# Store centroid and normalized quadrupolar gradient data
193+
X_data[turn] = np.mean(particles.x)
194+
Y_data[turn] = np.mean(particles.y)
195+
kqf_data[turn] = line.vars['kqf']._value
196+
kqd_data[turn] = line.vars['kqd']._value
197+
198+
tbt.update_at_turn(turn, particles, tw)
199+
200+
time01 = time.time()
201+
dt0 = time01-time00
202+
print('\nTracking time: {:.1f} s = {:.1f} h'.format(dt0, dt0/3600))
203+
204+
# Convert turns to seconds
205+
turns_per_sec = 1 / tw.T_rev0
206+
num_seconds = n_turns / turns_per_sec # number of seconds we are running for
207+
seconds_array = np.linspace(0.0, num_seconds, num=int(n_turns))
208+
209+
# Final turn-by-turn records
210+
tbt.store_final_particles(particles)
211+
tbt.append_profile_monitor_data(monitorH, monitorV, seconds_array)
212+
tbt.append_centroid_data(X_data, Y_data, kqf_data, kqd_data)
213+
tbt.to_dict(convert_to_numpy=True)
214+
215+
# then save tbt dict if desired

0 commit comments

Comments
 (0)