{ "cells": [ { "cell_type": "markdown", "id": "6daf22a3", "metadata": {}, "source": [ "# Library tutorial\n", "\n", "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." ] }, { "cell_type": "markdown", "id": "fc716ff8", "metadata": {}, "source": [ "## Motors" ] }, { "cell_type": "markdown", "id": "44901bb4", "metadata": {}, "source": [ "### Pure Python library" ] }, { "cell_type": "markdown", "id": "fd405ce7", "metadata": {}, "source": [ "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.\n", "\n", "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.\n", "\n", "With that said, create a dummy motor to see what they can do!" ] }, { "cell_type": "code", "execution_count": 19, "id": "c0a94faf", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "from contrast.motors import DummyMotor\n", "motor = DummyMotor(name='motor')\n", "print(motor)" ] }, { "cell_type": "markdown", "id": "8c373813", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 20, "id": "9f440feb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2.0" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import time\n", "motor.move(2.)\n", "time.sleep(2.)\n", "motor.dial_position" ] }, { "cell_type": "markdown", "id": "a44edacf", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 21, "id": "a4cc9cd5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.7491042613983154\n", "1.4985182285308838\n", "1.2479336261749268\n", "0.9969308376312256\n", "0.7459800243377686\n", "0.49478936195373535\n", "0.2441420555114746\n", "-0.00650787353515625\n", "-0.25818800926208496\n", "-0.5107831954956055\n", "-0.7620322704315186\n", "-1.0130293369293213\n", "-1.2638154029846191\n", "-1.5145387649536133\n", "-1.7655553817749023\n", "-2.0\n" ] } ], "source": [ "motor.move(-2)\n", "while motor.busy():\n", " time.sleep(.25)\n", " print(motor.dial_position)" ] }, { "cell_type": "markdown", "id": "35cc5bf3", "metadata": {}, "source": [ "That's really all we need to know about motors. But there are some convenient properties which make motor objects easier to user experimentally.\n", "\n", "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\n", "\n", "$$\\mathrm{(user\\ position)} = \\mathrm{(dial\\ position)} * \\mathrm{scaling} + \\mathrm{offset}$$." ] }, { "cell_type": "code", "execution_count": 22, "id": "515a6379", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "-2.0 -2.0\n", "12.0 -2.0 14.0\n" ] } ], "source": [ "print(motor.user_position, motor.dial_position)\n", "motor.user_position = 12\n", "print(motor.user_position, motor.dial_position, motor._offset)" ] }, { "cell_type": "markdown", "id": "a152d3df", "metadata": {}, "source": [ "For full control, you can write directly to the `_offset` and `_scaling` variables." ] }, { "cell_type": "code", "execution_count": 23, "id": "384ce134", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "12.0\n", "2.0\n" ] } ], "source": [ "print(motor.user_position)\n", "motor._offset = 0\n", "motor._scaling = -1\n", "print(motor.user_position)" ] }, { "cell_type": "markdown", "id": "c8817b44", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 24, "id": "2ae3933c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(-15.0, 5.0)\n" ] } ], "source": [ "motor._offset = 105\n", "motor._scaling = 1\n", "motor.user_limits = (90, 110)\n", "print(motor.dial_limits)" ] }, { "cell_type": "markdown", "id": "d752896c", "metadata": {}, "source": [ "At a beamline, these parameters are usually known, and can be declared when the motor object is created." ] }, { "cell_type": "code", "execution_count": 25, "id": "488b3a04", "metadata": {}, "outputs": [], "source": [ "motor2 = DummyMotor(name='motor2', scaling=-1, offset=100, dial_limits=(-5, 5))" ] }, { "cell_type": "markdown", "id": "cb56ac86", "metadata": {}, "source": [ "### Motor macros" ] }, { "cell_type": "markdown", "id": "826af292", "metadata": {}, "source": [ "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`." ] }, { "cell_type": "code", "execution_count": 26, "id": "3003b8e2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "motor user pos. limits dial pos. limits \n", "------------------------------------------------------------------\n", "motor2 100.00 (95.00, 105.00) 0.00 (-5.00, 5.00)\n" ] } ], "source": [ "%wm motor2" ] }, { "cell_type": "markdown", "id": "f250f094", "metadata": {}, "source": [ "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`." ] }, { "cell_type": "markdown", "id": "1298645c", "metadata": {}, "source": [ "### Creating our own `Motor`" ] }, { "cell_type": "markdown", "id": "efa2e3df", "metadata": {}, "source": [ "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](https://contrast.readthedocs.io/en/latest/_modules/contrast/motors/Motor.html#Motor)) contains three methods which need writing. Two of these were introduced above, and the third is quite obvious.\n", "\n", "1. `dial_position`, which reads the current motor position or initiates a movement,\n", "2. `busy()`, which tells if the motor is doing something or whether it is at the commanded position, and\n", "3. `stop()`, which stops any motion.\n", "\n", "Here we'll create a dummy motor which simply goes directly to the commanded position." ] }, { "cell_type": "code", "execution_count": 27, "id": "bdb3e8a5", "metadata": {}, "outputs": [], "source": [ "from contrast.motors import Motor\n", "\n", "class PretendMotor(Motor):\n", " \n", " def __init__(self, **kwargs):\n", " super().__init__(**kwargs) # call the base class constructor, Motor.__init__()\n", " self._pos = 0 # A variable for the commanded position\n", " \n", " @property\n", " def dial_position(self):\n", " # here we'd normally ask the physical motor where it is\n", " return self._pos\n", "\n", " @dial_position.setter\n", " def dial_position(self, pos):\n", " # here we'd normally tell the motor where to go\n", " self._pos = pos\n", " \n", " def busy(self):\n", " # here we'd normally ask the motor if it's on target\n", " return False\n", "\n", " def stop(self):\n", " # here we'd normally stop any motor motion\n", " print('stopping %s' % self.name)\n", " " ] }, { "cell_type": "markdown", "id": "eb18228e", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "id": "59d2474b", "metadata": {}, "source": [ "Now let's see if it works! Note that inheritaning `Motor` means that all macros, user positions and limits, work. Neat!" ] }, { "cell_type": "code", "execution_count": 28, "id": "df1e2803", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "motor user pos. limits dial pos. limits \n", "-------------------------------------------------------------------\n", "pret 3.14 (-10.00, 10.00) 3.14 (-10.00, 10.00)\n" ] } ], "source": [ "pret = PretendMotor(name='pret', dial_limits=(-10, 10))\n", "%mv pret 3.14\n", "%wm pret" ] }, { "cell_type": "markdown", "id": "8b864a98", "metadata": {}, "source": [ "## Detectors" ] }, { "cell_type": "markdown", "id": "e9e8e4eb", "metadata": {}, "source": [ "### Library basics" ] }, { "cell_type": "markdown", "id": "537f19e6", "metadata": {}, "source": [ "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.\n", "\n", "Detectors are also `Gadget`s, and as such need names. Let's make a dummy detector to see how it works." ] }, { "cell_type": "code", "execution_count": 29, "id": "27b4ec71", "metadata": {}, "outputs": [], "source": [ "from contrast.detectors import DummyDetector\n", "det = DummyDetector(name='det')" ] }, { "cell_type": "markdown", "id": "63914955", "metadata": {}, "source": [ "Now let's see how to operate on a detector. `contrast` defines a sequence of method calls for data acquisition.\n", "\n", "1. `prepare()` sets up the detector for the coming measurements (scan),\n", "2. `arm()` is called before every measurement (scan point),\n", "3. `start()` is called to initiate every measurement (scan point),\n", "4. `read()` is used after each measurement (scan point) to get the data.\n", "5. `busy()` is used to see if a detector is busy measuring,\n", "6. `stop()` is used to abort an ongoing measurement, and\n", "\n", "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()`.\n", "\n", "Below is a simple data acquisition loop which reads 10 random values from our dummy motor." ] }, { "cell_type": "code", "execution_count": 30, "id": "8cf27832", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.26129083987807955\n", "0.06965264389998532\n", "0.35052816316208546\n", "0.19012523957704158\n", "0.2778835439312592\n", "0.29301784021295463\n", "0.43974042737317354\n", "0.07276904135025292\n", "0.2096405657172989\n", "0.3142917132665356\n" ] } ], "source": [ "det.prepare(acqtime=.5, dataid=None)\n", "for i in range(10):\n", " det.start()\n", " while det.busy():\n", " time.sleep(.1)\n", " print(det.read())" ] }, { "cell_type": "markdown", "id": "5f8187d6", "metadata": {}, "source": [ "### Detector macros" ] }, { "cell_type": "markdown", "id": "dee7f98e", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 31, "id": "65158b74", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " name class \n", "-------------------------------------------------------------\n", "* det \n", "* det2 \n", "* det3 \n", "* vector \n" ] } ], "source": [ "%lsdet" ] }, { "cell_type": "markdown", "id": "34f2afef", "metadata": {}, "source": [ "`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." ] }, { "cell_type": "code", "execution_count": 32, "id": "061d952a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " name class \n", "-------------------------------------------------------------\n", " det \n", " det2 \n", "* det3 \n", "* vector \n" ] } ], "source": [ "det2 = DummyDetector(name='det2')\n", "det3 = DummyDetector(name='det3')\n", "\n", "%deactivate det\n", "%deactivate det2\n", "%lsdet" ] }, { "cell_type": "code", "execution_count": 33, "id": "58f7bd4c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", " name class \n", "-------------------------------------------------------------\n", "* det \n", "* det2 \n", "* det3 \n", "* vector \n" ] } ], "source": [ "%activate det*\n", "%lsdet" ] }, { "cell_type": "markdown", "id": "5d8a43c0", "metadata": {}, "source": [ "### Writing our own detector" ] }, { "cell_type": "markdown", "id": "715e144d", "metadata": {}, "source": [ "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](file:///home/alex/sw/contrast/docs/build/html/contrast.detectors.html#contrast.detectors.Detector.Detector)." ] }, { "cell_type": "code", "execution_count": 34, "id": "2e2782dc", "metadata": {}, "outputs": [], "source": [ "from contrast.detectors import Detector\n", "import numpy as np\n", "\n", "class VectorDetector(Detector):\n", "\n", " def initialize(self):\n", " # normally, we'd set up connections to the detector here\n", " self._started = 0\n", " self._acqtime = 1.\n", " \n", " def prepare(self, acqtime, dataid=None, n_starts=None):\n", " self._acqtime = acqtime\n", "\n", " def arm(self):\n", " self.values = None # clear any old data\n", "\n", " def start(self):\n", " self._started = time.time()\n", " self.values = (np.random.rand(10) * 256).astype(int)\n", "\n", " def stop(self):\n", " self._acqtime = 0.\n", "\n", " def busy(self):\n", " return (time.time() < self._started + self._acqtime)\n", "\n", " def read(self):\n", " return self.values" ] }, { "cell_type": "markdown", "id": "86d2aedf", "metadata": {}, "source": [ "Now we can carry out the same data acquisition loop that we did above, using our brand new home-made dummy." ] }, { "cell_type": "code", "execution_count": 35, "id": "48de26f9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[202 126 123 37 49 150 30 244 207 67]\n", "[103 55 12 37 135 124 222 247 60 58]\n", "[215 57 203 202 101 159 196 55 43 12]\n", "[194 120 230 12 52 38 132 41 140 40]\n", "[132 52 93 227 255 54 211 107 219 43]\n", "[ 23 229 194 135 240 149 238 62 0 17]\n", "[ 81 208 27 64 121 209 205 232 146 158]\n", "[ 84 118 7 22 248 230 24 249 20 194]\n", "[213 5 35 235 81 77 171 213 106 226]\n", "[ 65 137 20 137 253 18 154 76 18 40]\n" ] } ], "source": [ "vector = VectorDetector(name='vector')\n", "\n", "vector.prepare(acqtime=.5, dataid=None)\n", "for i in range(10):\n", " vector.start()\n", " while vector.busy():\n", " time.sleep(.1)\n", " print(vector.read())" ] }, { "cell_type": "markdown", "id": "d2023a05", "metadata": {}, "source": [ "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`." ] }, { "cell_type": "code", "execution_count": 36, "id": "72acfc47", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Scan #1 starting at Thu Dec 8 09:56:11 2022\n", "\n", " # motor det3 det vector det2 dt\n", "----------------------------------------------------------------------------\n", " 0 1.010e+02 7.043e-02 9.184e-02 [ 92,...,100 5.740e-02 2.113\n", " 1 1.011e+02 5.179e-02 7.081e-02 [ 84,...,217 2.889e-03 2.303\n", " 2 1.012e+02 3.359e-02 2.644e-02 [234,..., 1 6.646e-02 2.497\n", " 3 1.012e+02 8.029e-02 9.869e-02 [179,..., 38 9.226e-02 2.690\n", " 4 1.013e+02 7.940e-02 9.623e-02 [183,...,133 2.631e-02 2.878\n", " 5 1.014e+02 4.095e-02 6.861e-02 [238,..., 17 8.679e-02 3.066\n", " 6 1.015e+02 9.608e-02 4.792e-02 [141,...,245 1.168e-02 3.255\n", " 7 1.016e+02 8.491e-02 2.274e-02 [142,..., 73 9.287e-02 3.441\n", " 8 1.016e+02 1.898e-02 6.750e-03 [92,...,43] 5.346e-02 3.632\n", " 9 1.017e+02 8.134e-02 2.959e-03 [ 73,...,187 5.409e-03 3.826\n", " 10 1.018e+02 4.732e-02 1.991e-02 [218,..., 0 9.138e-02 4.019\n", " 11 1.019e+02 2.515e-02 5.170e-02 [202,...,141 4.069e-02 4.214\n", " 12 1.020e+02 8.351e-05 2.309e-02 [ 9,...,149 1.587e-02 4.406\n", " 13 1.020e+02 6.595e-02 2.711e-02 [ 30,...,109 8.367e-02 4.598\n", " 14 1.021e+02 7.516e-02 1.624e-02 [ 24,...,244 6.576e-02 4.790\n", " 15 1.022e+02 6.779e-02 7.807e-02 [199,...,140 5.320e-03 4.985\n", " 16 1.023e+02 2.215e-02 8.204e-02 [226,..., 56 1.643e-02 5.176\n", " 17 1.024e+02 5.798e-02 4.272e-02 [189,...,215 6.113e-02 5.369\n", " 18 1.024e+02 5.853e-02 7.080e-02 [88,...,50] 5.996e-02 5.563\n", " 19 1.025e+02 9.869e-02 6.112e-02 [139,...,227 7.337e-02 5.757\n", " 20 1.026e+02 4.788e-02 2.682e-02 [169,..., 13 6.206e-02 5.948\n", " 21 1.027e+02 8.024e-02 3.149e-02 [ 25,...,130 9.472e-02 6.143\n", " 22 1.028e+02 3.192e-02 4.735e-02 [ 99,...,156 2.368e-02 6.336\n", " 23 1.028e+02 1.773e-02 9.739e-02 [52,...,29] 9.260e-02 6.528\n", " 24 1.029e+02 3.273e-02 7.612e-02 [255,...,139 6.463e-03 6.719\n", " 25 1.030e+02 9.233e-02 1.953e-02 [178,...,162 9.737e-03 6.911\n", " 26 1.031e+02 8.940e-02 6.475e-03 [153,...,183 1.955e-02 7.103\n", " 27 1.032e+02 6.706e-03 2.523e-02 [248,..., 76 7.141e-04 7.294\n", " 28 1.032e+02 2.588e-02 8.373e-02 [249,...,235 8.910e-02 7.487\n", " 29 1.033e+02 5.932e-02 6.538e-03 [158,...,211 1.903e-03 7.681\n", " 30 1.034e+02 2.122e-02 3.072e-02 [117,..., 95 1.718e-02 7.874\n", " 31 1.035e+02 3.825e-02 1.498e-02 [ 59,...,235 4.656e-02 8.068\n", " 32 1.036e+02 3.476e-02 8.698e-02 [190,...,196 2.860e-02 8.260\n", " 33 1.036e+02 8.633e-02 3.118e-02 [198,...,248 3.045e-02 8.452\n", " 34 1.037e+02 8.075e-02 6.402e-02 [112,..., 46 5.275e-02 8.647\n", " 35 1.038e+02 4.849e-02 2.042e-02 [164,...,172 8.798e-02 8.842\n", " 36 1.039e+02 1.390e-02 6.381e-02 [187,...,126 1.621e-02 9.035\n", " 37 1.040e+02 3.405e-02 3.932e-02 [105,..., 90 8.533e-02 9.226\n", " 38 1.040e+02 7.910e-02 8.698e-02 [150,...,237 1.293e-02 9.418\n", " 39 1.041e+02 2.075e-02 9.283e-02 [29,...,88] 9.944e-02 9.611\n", " 40 1.042e+02 3.445e-02 7.814e-02 [154,..., 1 6.920e-02 9.803\n", " 41 1.043e+02 4.378e-02 9.236e-02 [178,...,167 6.982e-02 9.995\n", " 42 1.044e+02 6.608e-02 2.770e-03 [207,..., 39 1.018e-02 10.187\n", " 43 1.044e+02 8.715e-02 9.969e-02 [132,...,220 5.115e-03 10.376\n", " 44 1.045e+02 8.079e-02 6.314e-02 [144,..., 57 3.212e-02 10.564\n", " 45 1.046e+02 7.789e-02 5.284e-02 [124,..., 63 5.885e-02 10.755\n", " 46 1.047e+02 4.902e-02 7.388e-02 [120,..., 14 4.804e-02 10.944\n", " 47 1.048e+02 2.884e-02 5.077e-02 [102,...,142 7.392e-02 11.134\n", " 48 1.048e+02 6.302e-02 7.332e-02 [152,...,115 8.870e-03 11.317\n", " 49 1.049e+02 2.911e-02 4.379e-02 [116,..., 25 6.956e-02 11.501\n", " 50 1.050e+02 3.362e-02 3.179e-02 [118,...,118 2.243e-02 11.685\n", "Time left: 0:00:00\n", "Scan #1 ending at Thu Dec 8 09:56:23 2022\n", "Returning motors to their starting positions...\n", "...done\n" ] } ], "source": [ "%dscan motor -2 2 50 .1" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.4" } }, "nbformat": 4, "nbformat_minor": 5 }