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

\[\mathrm{(user\ position)} = \mathrm{(dial\ position)} * \mathrm{scaling} + \mathrm{offset}\]

.

[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.

  1. dial_position, which reads the current motor position or initiates a movement,
  2. busy(), which tells if the motor is doing something or whether it is at the commanded position, and
  3. stop(), 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 Gadgets, 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.

  1. prepare() sets up the detector for the coming measurements (scan),
  2. arm() is called before every measurement (scan point),
  3. start() is called to initiate every measurement (scan point),
  4. read() is used after each measurement (scan point) to get the data.
  5. busy() is used to see if a detector is busy measuring,
  6. 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