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 ('\n Tracking 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 ('\n Tracking 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