Skip to content

Commit fccbb9e

Browse files
committed
Minimal working example
1 parent eb22eb7 commit fccbb9e

File tree

5 files changed

+323
-1
lines changed

5 files changed

+323
-1
lines changed

data/acc-models-sps

Submodule acc-models-sps updated from 83418f1 to beb2353
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Minimalistic example to load an xsuite line, generate a particle distribution,
3+
install space charge, IBS, and track the particles.
4+
"""
5+
import xtrack as xt
6+
import xpart as xp
7+
import xfields as xf
8+
import xobjects as xo
9+
from records import Records
10+
import time
11+
12+
# Tracking parameters and context
13+
n_turns = 20
14+
n_particles = 20
15+
context = xo.ContextCpu(omp_num_threads='auto')
16+
17+
# Beam parameters
18+
exn = 1.0e-6 # horizontal emittance in m
19+
eyn = 1.0e-6 # vertical emittance in m
20+
sigma_z = 0.2 # bunch length in m
21+
Nb = 1e8 # number of particles in the bunch
22+
23+
# Space charge and IBS parameters
24+
q_val = 1.0 # q-value of longitudinal profile, for space charge
25+
num_spacecharge_interactions = 1080
26+
ibs_step = 100 # number of turns between recomputing the IBS coefficients
27+
28+
# Load the line from a file
29+
line = xt.Line.from_json('SPS_2021_Pb_nominal_deferred_exp.json')
30+
harmonic_nb = 4653
31+
32+
# Set RF voltage to correct value
33+
line['actcse.31632' ].voltage = 3.0e6
34+
print('RF voltage set to {:.3e} V\n'.format(line['actcse.31632'].voltage))
35+
36+
# Twiss command, inspect tunes and reference particle
37+
tw = line.twiss()
38+
print('Tunes: Qx = {:.6f}, Qy = {:.6f}'.format(tw.qx, tw.qy))
39+
print('Reference particle: {}'.format(line.particle_ref.show()))
40+
41+
# Add longitudinal limit rectangle - to kill particles that fall out of bucket
42+
bucket_length = line.get_length()/harmonic_nb
43+
print('\nBucket length is {:.4f} m'.format(bucket_length))
44+
line.unfreeze() # if you had already build the tracker
45+
line.append_element(element=xt.LongitudinalLimitRect(min_zeta=-bucket_length/2, max_zeta=bucket_length/2), name='long_limit')
46+
line.build_tracker(_context=context)
47+
48+
# Generate a particle distribution
49+
particles = xp.generate_matched_gaussian_bunch(_context=context,
50+
num_particles=n_particles,
51+
total_intensity_particles=Nb,
52+
nemitt_x=exn,
53+
nemitt_y=eyn,
54+
sigma_z=sigma_z,
55+
particle_ref=line.particle_ref,
56+
line=line)
57+
58+
# Initialize the dataclasses to store particle values
59+
tbt = Records.init_zeroes(n_turns) # only emittances and bunch intensity
60+
tbt.update_at_turn(0, particles, tw)
61+
tbt.store_initial_particles(particles)
62+
tbt.store_twiss(tw.to_pandas())
63+
64+
######### Frozen space charge #########
65+
66+
# Store the initial buffer of the line
67+
_buffer = line._buffer
68+
line.discard_tracker()
69+
70+
# Install space charge
71+
lprofile = xf.LongitudinalProfileQGaussian(
72+
number_of_particles = Nb,
73+
sigma_z = sigma_z,
74+
z0=0.,
75+
q_parameter=q_val)
76+
77+
# Install frozen space charge as base
78+
xf.install_spacecharge_frozen(line = line,
79+
particle_ref = line.particle_ref,
80+
longitudinal_profile = lprofile,
81+
nemitt_x = exn, nemitt_y = eyn,
82+
sigma_z = sigma_z,
83+
num_spacecharge_interactions = num_spacecharge_interactions)
84+
line.build_tracker(_buffer=_buffer)
85+
86+
######### IBS kinetic kicks #########
87+
88+
# friction and diffusion terms of the kinetic theory of gases
89+
ibs_kick = xf.IBSKineticKick(num_slices=50)
90+
91+
### Install the IBS kinetic kick element ###
92+
#line.configure_intrabeam_scattering(
93+
# element=ibs_kick, name="ibskick", index=-1, update_every=ibs_step
94+
#)
95+
96+
# THESE LINES ABOVE WILL NOT WORK if space charge is already installed
97+
# Instead, follow manual steps Felix Soubelet's tips
98+
# Directly copy steps from https://github.com/xsuite/xfields/blob/6882e0d03bb6772f873ce57ef6cf2592e5779359/xfields/ibs/_api.py
99+
_buffer = line._buffer
100+
line.discard_tracker()
101+
line.insert_element(element=ibs_kick, name="ibskick", index=-1)
102+
line.build_tracker(_buffer=_buffer)
103+
104+
line_sc_off = line.filter_elements(exclude_types_starting_with='SpaceCh')
105+
twiss_no_sc = line_sc_off.twiss(method="4d")
106+
107+
# Figure out the IBS kick element and its name in the line
108+
only_ibs_kicks = {name: element for name, element in line.element_dict.items() if isinstance(element, xf.ibs._kicks.IBSKick)}
109+
assert len(only_ibs_kicks) == 1, "Only one 'IBSKick' element should be present in the line"
110+
name, element = only_ibs_kicks.popitem()
111+
112+
# Set necessary (private) attributes for the kick to function
113+
element.update_every = ibs_step
114+
element._name = name
115+
element._twiss = twiss_no_sc
116+
element._scale_strength = 1 # element is now ON, will track
117+
118+
print('\nFixed IBS coefficient recomputation at interval = {} steps\n'.format(ibs_step))
119+
120+
#### Track the particles ####
121+
time00 = time.time()
122+
123+
for turn in range(1, n_turns):
124+
125+
# Print out info at specified intervals
126+
if turn % 5 == 0:
127+
print('\nTracking turn {}'.format(turn))
128+
129+
# ----- Track and update records for tracked particles ----- #
130+
line.track(particles, num_turns=1)
131+
132+
tbt.update_at_turn(turn, particles, tw)
133+
134+
time01 = time.time()
135+
dt0 = time01-time00
136+
print('\nTracking time: {:.1f} s = {:.1f} h'.format(dt0, dt0/3600))
137+
138+
# Final turn-by-turn records
139+
tbt.store_final_particles(particles)
140+
tbt.to_dict(convert_to_numpy=True)
141+
142+
# then save tbt dict if desired

examples/minimal_working_examples/SPS_2021_Pb_nominal_deferred_exp.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
asttokens @ file:///home/conda/feedstock_root/build_artifacts/asttokens_1733250440834/work
2+
certifi==2025.8.3
3+
cffi==1.17.1
4+
charset-normalizer==3.4.3
5+
Cython==3.1.2
6+
decorator @ file:///home/conda/feedstock_root/build_artifacts/decorator_1740384970518/work
7+
exceptiongroup @ file:///home/conda/feedstock_root/build_artifacts/exceptiongroup_1746947292760/work
8+
executing @ file:///home/conda/feedstock_root/build_artifacts/executing_1745502089858/work
9+
idna==3.10
10+
ipython @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_ipython_1751465044/work
11+
ipython_pygments_lexers @ file:///home/conda/feedstock_root/build_artifacts/ipython_pygments_lexers_1737123620466/work
12+
jedi @ file:///home/conda/feedstock_root/build_artifacts/jedi_1733300866624/work
13+
lark==1.2.2
14+
matplotlib-inline @ file:///home/conda/feedstock_root/build_artifacts/matplotlib-inline_1733416936468/work
15+
numpy @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_numpy_1753401560/work/dist/numpy-2.3.2-cp311-cp311-linux_x86_64.whl#sha256=469440884ff65cdd5601d2ff9e1c5b9bcc2d8176c37e37ec78a0666703a54157
16+
pandas==2.3.1
17+
parso @ file:///home/conda/feedstock_root/build_artifacts/parso_1733271261340/work
18+
pexpect @ file:///home/conda/feedstock_root/build_artifacts/pexpect_1733301927746/work
19+
pickleshare @ file:///home/conda/feedstock_root/build_artifacts/pickleshare_1733327343728/work
20+
prompt_toolkit @ file:///home/conda/feedstock_root/build_artifacts/prompt-toolkit_1744724089886/work
21+
ptyprocess @ file:///home/conda/feedstock_root/build_artifacts/ptyprocess_1733302279685/work/dist/ptyprocess-0.7.0-py2.py3-none-any.whl#sha256=92c32ff62b5fd8cf325bec5ab90d7be3d2a8ca8c8a3813ff487a8d2002630d1f
22+
pure_eval @ file:///home/conda/feedstock_root/build_artifacts/pure_eval_1733569405015/work
23+
pycparser==2.22
24+
Pygments @ file:///home/conda/feedstock_root/build_artifacts/pygments_1750615794071/work
25+
python-dateutil==2.9.0.post0
26+
pytz==2025.2
27+
requests==2.32.4
28+
scipy @ file:///home/conda/feedstock_root/build_artifacts/scipy-split_1751147407503/work/dist/scipy-1.16.0-cp311-cp311-linux_x86_64.whl#sha256=af2237dd5dad564373927317264a4269e1a7afc6dc576b23ef2fd3e6f93926c5
29+
six==1.17.0
30+
stack_data @ file:///home/conda/feedstock_root/build_artifacts/stack_data_1733569443808/work
31+
tqdm==4.67.1
32+
traitlets @ file:///home/conda/feedstock_root/build_artifacts/traitlets_1733367359838/work
33+
typing_extensions @ file:///home/conda/feedstock_root/build_artifacts/bld/rattler-build_typing_extensions_1751643513/work
34+
tzdata==2025.2
35+
urllib3==2.5.0
36+
wcwidth @ file:///home/conda/feedstock_root/build_artifacts/wcwidth_1733231326287/work
37+
xcoll==0.6.2
38+
xdeps==0.10.5
39+
xfields==0.25.1
40+
xobjects==0.5.2
41+
xpart==0.23.1
42+
xsuite==0.36.2
43+
xtrack==0.88.3
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""
2+
Container of simplistic record class to store values during tracking
3+
"""
4+
import numpy as np
5+
import xpart as xp
6+
import xtrack as xt
7+
import xobjects as xo
8+
from dataclasses import dataclass
9+
from typing import Self
10+
import pandas as pd
11+
import json
12+
13+
####### Helper functions for bunch length, momentum spread and geometric emittances #######
14+
def _bunch_length(parts: xp.Particles) -> float:
15+
return np.std(parts.zeta[parts.state > 0])
16+
17+
18+
def _sigma_delta(parts: xp.Particles) -> float:
19+
return np.std(parts.delta[parts.state > 0])
20+
21+
22+
def _geom_epsx(parts: xp.Particles, twiss: xt.TwissTable) -> float:
23+
"""
24+
We index dx and betx at 0 which corresponds to the beginning / end of
25+
the line, since this is where / when we will be applying the kicks.
26+
"""
27+
delta = parts.delta[parts.state > 0]
28+
sigma_x = np.std(parts.x[parts.state > 0])
29+
sig_delta = _sigma_delta(parts)
30+
return (sigma_x**2 - (twiss["dx"][0] * sig_delta) ** 2) / twiss["betx"][0]
31+
32+
33+
def _geom_epsy(parts: xp.Particles, twiss: xt.TwissTable) -> float:
34+
"""
35+
We index dy and bety at 0 which corresponds to the beginning / end of
36+
the line, since this is where / when we will be applying the kicks.
37+
"""
38+
delta = parts.delta[parts.state > 0]
39+
sigma_y = np.std(parts.y[parts.state > 0])
40+
sig_delta = _sigma_delta(parts)
41+
return (sigma_y**2 - (twiss["dy"][0] * sig_delta) ** 2) / twiss["bety"][0]
42+
43+
#############################################################################
44+
45+
@dataclass
46+
class Records:
47+
"""
48+
Data class to store numpy.ndarray of results during flat-bottom tracking
49+
"""
50+
exn: np.ndarray
51+
eyn: np.ndarray
52+
sigma_delta: np.ndarray
53+
bunch_length: np.ndarray
54+
Nb: np.ndarray
55+
turns: np.ndarray
56+
twiss: dict
57+
particles_i: dict
58+
particles_f: dict
59+
60+
def update_at_turn(self, turn: int, parts: xp.Particles, twiss: xt.TwissTable):
61+
"""Automatically update the records at given turn from the xpart.Particles."""
62+
63+
# Store the particle ensemble quantities
64+
self.exn[turn] = _geom_epsx(parts, twiss) * parts.beta0[0] * parts.gamma0[0]
65+
self.eyn[turn] = _geom_epsy(parts, twiss) * parts.beta0[0] * parts.gamma0[0]
66+
self.Nb[turn] = parts.weight[parts.state > 0][0]*len(parts.x[parts.state > 0])
67+
self.sigma_delta[turn] = _sigma_delta(parts)
68+
self.bunch_length[turn] = _bunch_length(parts)
69+
70+
@classmethod
71+
def init_zeroes(cls, n_turns: int) -> Self: # noqa: F821
72+
"""Initialize the dataclass with arrays of zeroes."""
73+
return cls(
74+
exn=np.zeros(n_turns, dtype=float),
75+
eyn=np.zeros(n_turns, dtype=float),
76+
Nb=np.zeros(n_turns, dtype=float),
77+
sigma_delta=np.zeros(n_turns, dtype=float),
78+
bunch_length=np.zeros(n_turns, dtype=float),
79+
turns=np.arange(n_turns, dtype=int),
80+
twiss={},
81+
particles_i={},
82+
particles_f={}
83+
)
84+
85+
def store_twiss(self, df_twiss: pd.DataFrame):
86+
"""Store twiss table, before collective elements"""
87+
self.twiss = df_twiss.to_dict()
88+
self.includes_particle_and_twiss_data = True
89+
90+
def store_initial_particles(self, parts: xp.Particles):
91+
"""Store initial particle object"""
92+
self.particles_i = parts.to_dict()
93+
self.includes_particle_and_twiss_data = True
94+
95+
def store_final_particles(self, parts: xp.Particles):
96+
"""Store final particle object"""
97+
self.particles_f = parts.to_dict()
98+
self.includes_particle_and_twiss_data = True
99+
100+
def to_dict(self, convert_to_numpy=True):
101+
"""
102+
Convert data arrays to dictionary, possible also beam profile monitor data
103+
Convert lists to numpy format if desired, but typically not if data is saved to json
104+
"""
105+
data = {
106+
'exn': self.exn.tolist(),
107+
'eyn': self.eyn.tolist(),
108+
'sigma_delta': self.sigma_delta.tolist(),
109+
'bunch_length': self.bunch_length.tolist(),
110+
'Nb' : self.Nb.tolist(),
111+
'Turns': self.turns.tolist()
112+
}
113+
data['twiss'] = self.twiss
114+
data['particles_i'] = self.particles_i
115+
data['particles_f'] = self.particles_f
116+
117+
# Convert lists to numpy arrays if desired
118+
if convert_to_numpy:
119+
for key, value in data.items():
120+
if isinstance(data[key], list):
121+
data[key] = np.array(data[key])
122+
123+
return data
124+
125+
126+
def to_json(self, file_path=None):
127+
"""
128+
Save the data to a JSON file.
129+
"""
130+
if file_path is None:
131+
file_path = './'
132+
133+
data = self.to_dict()
134+
135+
with open('{}tbt.json'.format(file_path), 'w') as f:
136+
json.dump(data, f, cls=xo.JEncoder)

0 commit comments

Comments
 (0)