Introduction
In this page we illustrate how to introduce a new beam element compatible with Xtrack. We will use for illustration the “SRotation” element which performs the following transformation of the particle coordinates:
x = cos(theta) * x + sin(theta) * y
y = -sin(theta) * x + cos(theta) * y
px = cos(theta) * px + sin(theta) * py
py = -sin(theta) * px + cos(theta) * py
The element is fully described by the rotation angle theta.
Definition and management of the data structure
New beam elements are defined as python classes inheriting from the class BeamElement
of xtrack.
In each element class we define a dictionary called _xofields
, which specifies names and types of the data to be made accessible to the C tracking code.
Although our beam element is defined by the single parameter (theta), it is convenient to store the quantities sin(theta) and cos(theta) to avoid recalculating them multiple times:
import xobjects as xo
import xtrack as xt
class SRotation(xt.BeamElement):
_xofields={
'cos_z': xo.Float64,
'sin_z': xo.Float64,
}
Allocation of beam elements on CPU or GPU
Objects of the defined class can be allocated as follows:
srot = SRotation(sin_z=1., cos_z=0)
By default the objects are allocated in the CPU memory. They can be allocated in the memory of a GPU by providing an xobject context or buffer. For example:
ctx = xo.ContextCupy()
# Object allocated on the GPU
srot = SRotation(sin_z=1., cos_z=0, _context=ctx)
Python access to beam-element data
The fields specified in _xofields
are automatically exposed as attributes of the objects that can be read and set with the standard python syntax, also if the object is allocated on the GPU:
print(srot.sin_z)
# returns 1.0
srot.sin_z = 0.9
print(srot.sin_z)
# returns 0.9
Underlying xobject
The data specified by the _xofields
dictionary is stored in a contiguous memory
block (an xobjects.Struct
instance) which is managed by the Xobjects library
and is made accessible to the C tracking code.
The xobject can be accessed with the _xobject
attribute of the beam element.
For example, in the case of our SRotation element:
srot._xobject
All attributes of the xobject are automatically exposed as attributes of the beam element.
For example, in the case of our SRotation element srot._xobject.sin_z
is the
same as srot.sin_z
.
Arrays are exposed as native Xobjects arrays in the _xobject
attribute, and
as numpy or numpy-like arrays as attributes of the beam element. For example, in
the case of a xtrack.Multipole
element we find:
mp = xtrack.Multipole(knl=[1,2,3])
mp._xobject.knl
# is an xobjects array
mp.knl
# is a numpy array
It should be noted that the the two are different views of the same memory area, hence any modification can be made indifferently on any of them.
The numpy view (or np-like on GPU contexts) gives the possibility of using
numpy features on the array (e.g. np.sum
, np.mean
, slicing, masking, etc.).
Custom __init__
method
Additional attributes and methods can be added to the class. If the __init__
method is defined, the __init__
of the parent class needs to be called to
initialize the xobject
, i.e. the data structure accessible from the C code.
In our example we want to initialize the object providing the rotation angle and
not its sine and cosine and we introduce a property called angle
that allows
setting or getting the angle from the stored sine and cosine. This can be done
as follows:
import numpy as np
import xobjects as xo
import xtrack as xt
class SRotation(BeamElement):
def __init__(self, angle=0, **kwargs):
anglerad = angle / 180 * np.pi
kwargs['cos_z']=np.cos(anglerad)
kwargs['sin_z']=np.sin(anglerad)
super().__init__(**kwargs)
@property
def angle(self):
return np.arctan2(self.sin_z, self.cos_z) * (180.0 / np.pi)
@angle.setter
def angle(self, value):
anglerad = value / 180 * np.pi
self.cos_z = np.cos(anglerad)
self.sin_z = np.sin(anglerad)