{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Anatomy of a Device\n", "\n", "In this notebook you will:\n", "\n", "* Understand the various methods of an ophyd Signal\n", "* Learn how to group Signals into Devices.\n", "* Learn how to specific \"pseudopositions\" that expose real axes (corresponding to physical hardware) and pseudoaxes, that move real axes via some mathematical transformation.\n", "\n", "Recommended Prerequisites:\n", "\n", "* [Hello Bluesky](./Hello%20Bluesky.ipynb)\n", "\n", "## Configuration\n", "Below, we will connect to EPICS IOC(s) controlling simulated hardware in lieu of actual motors, detectors. The IOCs should already be running in the background. Run this command to verify that they are running: it should produce output with RUNNING on each line. In the event of a problem, edit this command to replace `status` with `restart all` and run again." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!supervisorctl -c supervisor/supervisord.conf status" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%run scripts/beamline_configuration.py" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "from ophyd import Device, Signal, Component as Cpt, DeviceStatus\n", "from ophyd.sim import SynSignal, SynPeriodicSignal" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Interface to Signal" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig = Signal(name='sig', value=3)\n", "sig" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Methods that require no communication with the IOC" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.name" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.parent is None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Methods that ask the IOC to tell us something it already 'knows'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.connected" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.limits" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.read()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Monitoring (subscribing for updates asynchronously)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def cb(value, old_value, **a_whole_bunch_of_junk):\n", " print(f'changed from {old_value} to {value}')\n", " \n", "sig.subscribe(cb)\n", "# The act of subscribing always generates one reading immediately..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If this were an `EpicsSignal` instead of a `Signal`, `cb` would be called from a thread every time pyepics receives a new update about the value of `sig`. In this case, we have to update it manually." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.put(5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sig.put(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or we can connect to the `random_walk` IOC which publishes a new updates at a regular interval." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ophyd import EpicsSignal\n", "\n", "rand = EpicsSignal('random_walk:x', name='rand')\n", "token = rand.subscribe(cb)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rand.unsubscribe(token)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Methods that ask the IOC to take a (potentially lengthy) action" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def cb():\n", " print(\"finished at t =\", time.time())\n", "\n", "status = sig.set(5)\n", "status.add_callback(cb)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status.done" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status = sig.trigger()\n", "status.add_callback(cb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Interface of a Status object" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status = DeviceStatus(sig)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status.done" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status.success" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def cb():\n", " print(\"BOOM\")\n", "\n", "status.add_callback(cb)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status.callbacks" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status.device # the Device or Signal that the Status pertains to" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status._finished()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status.done" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "status.success" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Failure looks like this:\n", "status = DeviceStatus(sig)\n", "status.add_callback(cb)\n", "status._finished(success=False)\n", "status.success" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll see later how to actually use this in practice." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Interface to Device" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# This encodes the _structure_ of a kind of Device.\n", "# Real examples include EpicsMotor, EpicsScaler or user-defined\n", "# combinations of these, such as a platform that can move in X and Y.\n", "\n", "class Platform(Device):\n", " x = Cpt(Signal, value=3)\n", " y = Cpt(Signal, value=4)\n", " \n", "p1 = Platform(name='p1')\n", "p2 = Platform(name='p2')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Names and relationships" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.component_names" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.x" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.y" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.name" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.x.name" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.x.parent is p1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Reading the parent combines the readings of its children" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.read()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.x.read()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and `describe` works exactly the same way:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.describe()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.x.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Components are sorted into categories:\n", "\n", "* `OMITTED` -- not read (exposed for debugging only)\n", "* `NORMAL` / `read_attrs` -- things to read once per Event (i.e. row in the table)\n", "* `CONFIG` / `configuration_attrs` -- things to read once per Event Descriptor (which usually means one per run)\n", "* things ommitted from data collection entirely, but available for debugging etc.\n", "* `HINTED` -- subset of `NORMAL` flagged as interesting" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.read_attrs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.configuration_attrs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# dumb example...\n", "\n", "class Platform(Device):\n", " x = Cpt(Signal, value=3)\n", " y = Cpt(Signal, value=4)\n", " motion_compensation = Cpt(Signal, value=1, kind='CONFIG') # a boolean\n", " \n", "p1 = Platform(name='p1')\n", "p2 = Platform(name='p2')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.read_attrs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.configuration_attrs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.read_configuration()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.describe_configuration()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The data from `configuration_attrs` isn't displayed by the built-in callbacks..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "RE(count([p1]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "... but the data is saved, and it can accessed conveniently like so:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "h = db[-1]\n", "h.config_data('p1')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.summary()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Hints are meant to help downstream consumers of the data correctly infer user intent and automatically construct useful views on the data. They are only a suggestion. *They do not affect what is saved.*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# dumb example...\n", "\n", "class Platform(Device):\n", " x = Cpt(Signal, value=3, kind='hinted')\n", " y = Cpt(Signal, value=4, kind='hinted')\n", " motion_compensation = Cpt(Signal, value=1, kind='config') # a boolean\n", " \n", "p1 = Platform(name='p1')\n", "p1.hints" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.summary()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 'Staging' -- a hook for putting a device into a controlled state for data collection (and then putting it back)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Platform(Device):\n", " _default_configuration_attrs = ('motion_compensation',)\n", " _default_read_attrs = ('x', 'y')\n", " x = Cpt(Signal, value=3)\n", " y = Cpt(Signal, value=4)\n", " motion_compensation = Cpt(Signal, value=1) # a boolean\n", " \n", " def __init__(self, *args, **kwargs):\n", " super().__init__(*args, **kwargs)\n", " self.stage_sigs['motion_compensation'] = 1\n", " \n", "\n", "p1 = Platform(name='p1')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.motion_compensation.get()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.motion_compensation.put(0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Device.stage()` stashes the current state of the signals in `stage_sigs` and then puts the device into the desired state." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.stage()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.motion_compensation.get()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Device.unstage()` uses that stashed stage to put everything back." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.unstage()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.motion_compensation.get()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Staging twice is illegal:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.stage()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [], "source": [ "# THIS IS EXPECTED TO CREATE AN ERROR.\n", "\n", "p1.stage()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But unstaging is indempotent:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p1.unstage()\n", "p1.unstage()\n", "p1.unstage()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Pseudopositioners" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ophyd import (PseudoPositioner, PseudoSingle)\n", "from ophyd.pseudopos import (pseudo_position_argument,\n", " real_position_argument)\n", "from ophyd import SoftPositioner\n", "C = Cpt\n", "\n", "class SPseudo3x3(PseudoPositioner):\n", " pseudo1 = C(PseudoSingle, limits=(-10, 10), egu='a')\n", " pseudo2 = C(PseudoSingle, limits=(-10, 10), egu='b')\n", " pseudo3 = C(PseudoSingle, limits=None, egu='c')\n", " \n", " real1 = C(SoftPositioner, init_pos=0.)\n", " real2 = C(SoftPositioner, init_pos=0.)\n", " real3 = C(SoftPositioner, init_pos=0.)\n", "\n", " sig = C(Signal, value=0)\n", "\n", " @pseudo_position_argument\n", " def forward(self, pseudo_pos):\n", " # logger.debug('forward %s', pseudo_pos)\n", " return self.RealPosition(real1=-pseudo_pos.pseudo1,\n", " real2=-pseudo_pos.pseudo2,\n", " real3=-pseudo_pos.pseudo3)\n", "\n", " @real_position_argument\n", " def inverse(self, real_pos):\n", " # logger.debug('inverse %s', real_pos)\n", " return self.PseudoPosition(pseudo1=-real_pos.real1,\n", " pseudo2=-real_pos.real2,\n", " pseudo3=-real_pos.real3)\n", " \n", "\n", "p3 = SPseudo3x3(name='p3')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ophyd.sim import det\n", "\n", "RE(scan([det, p3], p3.pseudo2, -1, 1, 5))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.6.5" } }, "nbformat": 4, "nbformat_minor": 2 }