Library tutorial¶
This is a walkthrough of the main contrast
classes corresponding to motors and detectors. These are the classes that are most often written to keep up with beamline developments and experimental needs. It shows in notebook format how to create, operate on, and write your own classes representing beamline components.
Motors¶
Pure Python library¶
Motors represent things that the user can directly affect. Obviously actual stepper, piezo, and servo motors, but also other things that are typically set from software, like temperatures or output voltages.
The first thing to note is that contrast objects need names. Contrast (actually, the Gadget
base class) uses these names to refer to objects in the inheritance tree. We need to supply the name explicitly, because Python objects don’t have names, any number of references (variable names) can be made to point at the same object. We’ll use the name (motor
) for the Gadget
and for the variable which refers to it.
With that said, create a dummy motor to see what they can do!
[19]:
from contrast.motors import DummyMotor
motor = DummyMotor(name='motor')
print(motor)
<contrast.motors.Motor.DummyMotor object at 0x7f09c6c33280>
Now, we have a dummy motor with the default settings. All Motor
objects have a bunch of methods and properties. The most important one is the methods to move the motor and to read its current position.
[20]:
import time
motor.move(2.)
time.sleep(2.)
motor.dial_position
[20]:
2.0
As you can see, move()
starts the motion, but it does not block the execution of the program. This is useful when moving many independent motors together. Of course, often you do want to wait for the motor to finish by blocking the calling code. This can be done by checking the Motor
’s busy()
method.
[21]:
motor.move(-2)
while motor.busy():
time.sleep(.25)
print(motor.dial_position)
1.7491042613983154
1.4985182285308838
1.2479336261749268
0.9969308376312256
0.7459800243377686
0.49478936195373535
0.2441420555114746
-0.00650787353515625
-0.25818800926208496
-0.5107831954956055
-0.7620322704315186
-1.0130293369293213
-1.2638154029846191
-1.5145387649536133
-1.7655553817749023
-2.0
That’s really all we need to know about motors. But there are some convenient properties which make motor objects easier to user experimentally.
The first is that we can defined user positions while still keeping track of the positions reported by the hardware. This is useful for setting some motor to zero in a reference position, to change units, or to invert an axis. The transformation can set by changing the user_position
attribute, which internally updates the _offset
and _scaling
variables. The positions are calculated as
.
[22]:
print(motor.user_position, motor.dial_position)
motor.user_position = 12
print(motor.user_position, motor.dial_position, motor._offset)
-2.0 -2.0
12.0 -2.0 14.0
For full control, you can write directly to the _offset
and _scaling
variables.
[23]:
print(motor.user_position)
motor._offset = 0
motor._scaling = -1
print(motor.user_position)
12.0
2.0
The second helpful property is that you can set software limits, which is a simple way to avoid making mistakes and crashing things at the beamline. We can set the limits using the library, either in dial or user coordinates. The Motor
class automatically keeps track of dial and user limits by respecting the dial limits, re-calculating user limits as needed.
[24]:
motor._offset = 105
motor._scaling = 1
motor.user_limits = (90, 110)
print(motor.dial_limits)
(-15.0, 5.0)
At a beamline, these parameters are usually known, and can be declared when the motor object is created.
[25]:
motor2 = DummyMotor(name='motor2', scaling=-1, offset=100, dial_limits=(-5, 5))
Motor macros¶
In practical beamline use, we often interact with motors using argument syntax macros instead of python syntax, just because macro syntax is more familiar to users and easier to type. The library module contrast.motors
defines a bunch of macros to make motor handling more convenient. For example, we can print the current position of a motor using %wm
.
[26]:
%wm motor2
motor user pos. limits dial pos. limits
------------------------------------------------------------------
motor2 100.00 (95.00, 105.00) 0.00 (-5.00, 5.00)
As with the rest of contrast, macros are stored in the library together with the classes they are meant to operate on. So in the contrast.motors.Motor
module, we can find familiar macros like %wa
, %umv
, %umvr
, etc. After reading the above description of the Motor
API, it should be quite clear to see how these macros operate on Motor
objects, simply by calling move()
, setting _offset
, _scaling
, and dial_limits
.
Creating our own Motor
¶
To create your own Motor
class, a subclass is created that handles peculiarities of whatever motor we want to interface. The Motor
base class (source code) contains three methods which need writing. Two of these were introduced above, and the third is quite obvious.
dial_position
, which reads the current motor position or initiates a movement,busy()
, which tells if the motor is doing something or whether it is at the commanded position, andstop()
, which stops any motion.
Here we’ll create a dummy motor which simply goes directly to the commanded position.
[27]:
from contrast.motors import Motor
class PretendMotor(Motor):
def __init__(self, **kwargs):
super().__init__(**kwargs) # call the base class constructor, Motor.__init__()
self._pos = 0 # A variable for the commanded position
@property
def dial_position(self):
# here we'd normally ask the physical motor where it is
return self._pos
@dial_position.setter
def dial_position(self, pos):
# here we'd normally tell the motor where to go
self._pos = pos
def busy(self):
# here we'd normally ask the motor if it's on target
return False
def stop(self):
# here we'd normally stop any motor motion
print('stopping %s' % self.name)
First we set up the object and do any initialization. This is Python boilerplate and half the internet describes how to do it. Next we write the code that moves the motor and reads its position via dial_position
. This is supplied as a python property, with a getter and a setter, and the second half of the internet explains how these work. Lastly, busy
and stop
are written, but in this case they won’t do much since the motor goes directly to the target.
Now let’s see if it works! Note that inheritaning Motor
means that all macros, user positions and limits, work. Neat!
[28]:
pret = PretendMotor(name='pret', dial_limits=(-10, 10))
%mv pret 3.14
%wm pret
motor user pos. limits dial pos. limits
-------------------------------------------------------------------
pret 3.14 (-10.00, 10.00) 3.14 (-10.00, 10.00)
Detectors¶
Library basics¶
Detectors include actual X-ray detectors, but also any number or other piece of information that can be acquired from the beamline. It’s often vert useful to be able to throw together a Detector
subclass to interface with user equipment.
Detectors are also Gadget
s, and as such need names. Let’s make a dummy detector to see how it works.
[29]:
from contrast.detectors import DummyDetector
det = DummyDetector(name='det')
Now let’s see how to operate on a detector. contrast
defines a sequence of method calls for data acquisition.
prepare()
sets up the detector for the coming measurements (scan),arm()
is called before every measurement (scan point),start()
is called to initiate every measurement (scan point),read()
is used after each measurement (scan point) to get the data.busy()
is used to see if a detector is busy measuring,stop()
is used to abort an ongoing measurement, and
A data acquisition routine is essentially just a loop calling these detector methods and perhaps moving motors and shuffling data in between. Each detector works differently, so what is acually done in prepare()
, arm()
, and start()
differs. Some detectors (Eigers and Merlins) arm for hardware triggers already in prepare()
and then just wait, some detectors need to be armed on every step, and some detectors simply grab a Tango value when you call start()
.
Below is a simple data acquisition loop which reads 10 random values from our dummy motor.
[30]:
det.prepare(acqtime=.5, dataid=None)
for i in range(10):
det.start()
while det.busy():
time.sleep(.1)
print(det.read())
0.26129083987807955
0.06965264389998532
0.35052816316208546
0.19012523957704158
0.2778835439312592
0.29301784021295463
0.43974042737317354
0.07276904135025292
0.2096405657172989
0.3142917132665356
Detector macros¶
Just like motors, a number of convenience macros are defined in the contrast.detectors.Detector
module. For example, the %lsdet
macro lists all current detectors.
[31]:
%lsdet
name class
-------------------------------------------------------------
* det <class 'contrast.detectors.Dummies.DummyDetector'>
* det2 <class 'contrast.detectors.Dummies.DummyDetector'>
* det3 <class 'contrast.detectors.Dummies.DummyDetector'>
* vector <class '__main__.VectorDetector'>
Detector
objects have an active
attribute which indicate whether that particular detector should be used data acquisition. The macros %activate
and %deactivate
toggle this flag in a helpful way. The %lsdet
output also indicates whether each detector is active.
[32]:
det2 = DummyDetector(name='det2')
det3 = DummyDetector(name='det3')
%deactivate det
%deactivate det2
%lsdet
name class
-------------------------------------------------------------
det <class 'contrast.detectors.Dummies.DummyDetector'>
det2 <class 'contrast.detectors.Dummies.DummyDetector'>
* det3 <class 'contrast.detectors.Dummies.DummyDetector'>
* vector <class '__main__.VectorDetector'>
[33]:
%activate det*
%lsdet
name class
-------------------------------------------------------------
* det <class 'contrast.detectors.Dummies.DummyDetector'>
* det2 <class 'contrast.detectors.Dummies.DummyDetector'>
* det3 <class 'contrast.detectors.Dummies.DummyDetector'>
* vector <class '__main__.VectorDetector'>
Writing our own detector¶
Writing Detector
subclasses involves a few more methods than for ‘Motor’s. Depending on the specifics of the physical detector, many of these methods are essentially empty. Here, we’ll write a detector which records a one-dimensional array of numbers at each measurement. For details on the arguments to prepare()
, see the base class documentation.
[34]:
from contrast.detectors import Detector
import numpy as np
class VectorDetector(Detector):
def initialize(self):
# normally, we'd set up connections to the detector here
self._started = 0
self._acqtime = 1.
def prepare(self, acqtime, dataid=None, n_starts=None):
self._acqtime = acqtime
def arm(self):
self.values = None # clear any old data
def start(self):
self._started = time.time()
self.values = (np.random.rand(10) * 256).astype(int)
def stop(self):
self._acqtime = 0.
def busy(self):
return (time.time() < self._started + self._acqtime)
def read(self):
return self.values
Now we can carry out the same data acquisition loop that we did above, using our brand new home-made dummy.
[35]:
vector = VectorDetector(name='vector')
vector.prepare(acqtime=.5, dataid=None)
for i in range(10):
vector.start()
while vector.busy():
time.sleep(.1)
print(vector.read())
[202 126 123 37 49 150 30 244 207 67]
[103 55 12 37 135 124 222 247 60 58]
[215 57 203 202 101 159 196 55 43 12]
[194 120 230 12 52 38 132 41 140 40]
[132 52 93 227 255 54 211 107 219 43]
[ 23 229 194 135 240 149 238 62 0 17]
[ 81 208 27 64 121 209 205 232 146 158]
[ 84 118 7 22 248 230 24 249 20 194]
[213 5 35 235 81 77 171 213 106 226]
[ 65 137 20 137 253 18 154 76 18 40]
In fact, this little loop represents most of what data acquisition is. We already have enough motors and detectors to use the full scanning machinery of contrast
, which is basically just loops like this with prettier printing and passing of data to Recorder
objects. We’ll finish with running a simple contrast dscan
.
[36]:
%dscan motor -2 2 50 .1
Scan #1 starting at Thu Dec 8 09:56:11 2022
# motor det3 det vector det2 dt
----------------------------------------------------------------------------
0 1.010e+02 7.043e-02 9.184e-02 [ 92,...,100 5.740e-02 2.113
1 1.011e+02 5.179e-02 7.081e-02 [ 84,...,217 2.889e-03 2.303
2 1.012e+02 3.359e-02 2.644e-02 [234,..., 1 6.646e-02 2.497
3 1.012e+02 8.029e-02 9.869e-02 [179,..., 38 9.226e-02 2.690
4 1.013e+02 7.940e-02 9.623e-02 [183,...,133 2.631e-02 2.878
5 1.014e+02 4.095e-02 6.861e-02 [238,..., 17 8.679e-02 3.066
6 1.015e+02 9.608e-02 4.792e-02 [141,...,245 1.168e-02 3.255
7 1.016e+02 8.491e-02 2.274e-02 [142,..., 73 9.287e-02 3.441
8 1.016e+02 1.898e-02 6.750e-03 [92,...,43] 5.346e-02 3.632
9 1.017e+02 8.134e-02 2.959e-03 [ 73,...,187 5.409e-03 3.826
10 1.018e+02 4.732e-02 1.991e-02 [218,..., 0 9.138e-02 4.019
11 1.019e+02 2.515e-02 5.170e-02 [202,...,141 4.069e-02 4.214
12 1.020e+02 8.351e-05 2.309e-02 [ 9,...,149 1.587e-02 4.406
13 1.020e+02 6.595e-02 2.711e-02 [ 30,...,109 8.367e-02 4.598
14 1.021e+02 7.516e-02 1.624e-02 [ 24,...,244 6.576e-02 4.790
15 1.022e+02 6.779e-02 7.807e-02 [199,...,140 5.320e-03 4.985
16 1.023e+02 2.215e-02 8.204e-02 [226,..., 56 1.643e-02 5.176
17 1.024e+02 5.798e-02 4.272e-02 [189,...,215 6.113e-02 5.369
18 1.024e+02 5.853e-02 7.080e-02 [88,...,50] 5.996e-02 5.563
19 1.025e+02 9.869e-02 6.112e-02 [139,...,227 7.337e-02 5.757
20 1.026e+02 4.788e-02 2.682e-02 [169,..., 13 6.206e-02 5.948
21 1.027e+02 8.024e-02 3.149e-02 [ 25,...,130 9.472e-02 6.143
22 1.028e+02 3.192e-02 4.735e-02 [ 99,...,156 2.368e-02 6.336
23 1.028e+02 1.773e-02 9.739e-02 [52,...,29] 9.260e-02 6.528
24 1.029e+02 3.273e-02 7.612e-02 [255,...,139 6.463e-03 6.719
25 1.030e+02 9.233e-02 1.953e-02 [178,...,162 9.737e-03 6.911
26 1.031e+02 8.940e-02 6.475e-03 [153,...,183 1.955e-02 7.103
27 1.032e+02 6.706e-03 2.523e-02 [248,..., 76 7.141e-04 7.294
28 1.032e+02 2.588e-02 8.373e-02 [249,...,235 8.910e-02 7.487
29 1.033e+02 5.932e-02 6.538e-03 [158,...,211 1.903e-03 7.681
30 1.034e+02 2.122e-02 3.072e-02 [117,..., 95 1.718e-02 7.874
31 1.035e+02 3.825e-02 1.498e-02 [ 59,...,235 4.656e-02 8.068
32 1.036e+02 3.476e-02 8.698e-02 [190,...,196 2.860e-02 8.260
33 1.036e+02 8.633e-02 3.118e-02 [198,...,248 3.045e-02 8.452
34 1.037e+02 8.075e-02 6.402e-02 [112,..., 46 5.275e-02 8.647
35 1.038e+02 4.849e-02 2.042e-02 [164,...,172 8.798e-02 8.842
36 1.039e+02 1.390e-02 6.381e-02 [187,...,126 1.621e-02 9.035
37 1.040e+02 3.405e-02 3.932e-02 [105,..., 90 8.533e-02 9.226
38 1.040e+02 7.910e-02 8.698e-02 [150,...,237 1.293e-02 9.418
39 1.041e+02 2.075e-02 9.283e-02 [29,...,88] 9.944e-02 9.611
40 1.042e+02 3.445e-02 7.814e-02 [154,..., 1 6.920e-02 9.803
41 1.043e+02 4.378e-02 9.236e-02 [178,...,167 6.982e-02 9.995
42 1.044e+02 6.608e-02 2.770e-03 [207,..., 39 1.018e-02 10.187
43 1.044e+02 8.715e-02 9.969e-02 [132,...,220 5.115e-03 10.376
44 1.045e+02 8.079e-02 6.314e-02 [144,..., 57 3.212e-02 10.564
45 1.046e+02 7.789e-02 5.284e-02 [124,..., 63 5.885e-02 10.755
46 1.047e+02 4.902e-02 7.388e-02 [120,..., 14 4.804e-02 10.944
47 1.048e+02 2.884e-02 5.077e-02 [102,...,142 7.392e-02 11.134
48 1.048e+02 6.302e-02 7.332e-02 [152,...,115 8.870e-03 11.317
49 1.049e+02 2.911e-02 4.379e-02 [116,..., 25 6.956e-02 11.501
50 1.050e+02 3.362e-02 3.179e-02 [118,...,118 2.243e-02 11.685
Time left: 0:00:00
Scan #1 ending at Thu Dec 8 09:56:23 2022
Returning motors to their starting positions...
...done