{ "cells": [ { "cell_type": "markdown", "id": "great-wiring", "metadata": {}, "source": [ "# Single EPICS PVs\n", "\n", "In this tutorial we will read, write, and monitor an EPICS PV in ophyd.\n", "\n", "Set up for tutorial\n", "-------------------\n", "\n", "We'll start our IOCs connected to simulated hardware.\n", "One implements a [random walk](https://en.wikipedia.org/wiki/Random_walk). It has\n", "just two PVs. One is a tunable parameter, ``random_walk:dt``, the time between\n", "steps. The other is ``random_walk:x``, the current position of the random\n", "walker.\n", "\n", "The IOCs may already be running in the background. Run this command to verify that they are running: it should produce output with STARTING or 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, "id": "vietnamese-blocking", "metadata": {}, "outputs": [], "source": [ "!supervisor/start_supervisor.sh status" ] }, { "cell_type": "markdown", "id": "august-mediterranean", "metadata": {}, "source": [ "Connect to a PV from Ophyd\n", "--------------------------\n", "\n", "Let's connect to the PV ``random_walk:dt`` from Ophyd. We need two pieces of\n", "information:\n", "\n", "* The PV name, ``random_walk:dt``.\n", "* A human-friendly name. This name is used to label the readings and will be\n", " used in any downstream data analysis or file-writing code. We might choose,\n", " for example, ``time_delta``." ] }, { "cell_type": "code", "execution_count": null, "id": "photographic-right", "metadata": {}, "outputs": [], "source": [ "from ophyd.signal import EpicsSignal\n", "\n", "time_delta = EpicsSignal(\"random_walk:dt\", name=\"time_delta\")" ] }, { "cell_type": "markdown", "id": "tested-detection", "metadata": {}, "source": [ "It is *conventional* to name the Python variable on the left the same as the\n", "value of ``name``, but not required. That is, this is conventional...\n", "\n", "\n", " a = EpicsSignal(\"...\", name=\"a\")\n", "\n", "...but all of these are also allowed.\n", "\n", "\n", " a = EpicsSignal(\"...\", name=\"b\") # local variable different from name\n", " a = EpicsSignal(\"...\", name=\"some name with spaces in it\")\n", " a = b = EpicsSignal(\"...\", name=\"b\") # two local variables\n", "\n", "Next let's connect to ``random_walk:x``. It happens that this PV is not\n", "writable---any writes would be rejected by EPICS---so we should use a read-only\n", "EpicsSignal, `ophyd.signal.EPICSSignalRO`, to represent it in in ophyd. In\n", "EPICS, you just have to \"know\" this about your hardware. Fortunately if, in our\n", "ignorance, we used writable `ophyd.signal.EpicsSignal` instead, we could\n", "still use it to read the PV. It would just have a vestigial ``set()`` method\n", "that wouldn't work." ] }, { "cell_type": "code", "execution_count": null, "id": "fiscal-intersection", "metadata": {}, "outputs": [], "source": [ "from ophyd.signal import EpicsSignalRO\n", "\n", "x = EpicsSignalRO(\"random_walk:x\", name=\"x\")" ] }, { "cell_type": "code", "execution_count": null, "id": "spectacular-qualification", "metadata": {}, "outputs": [], "source": [ "time_delta.wait_for_connection()\n", "x.wait_for_connection()" ] }, { "cell_type": "markdown", "id": "checked-space", "metadata": {}, "source": [ "## Use it with the Bluesky RunEngine\n", "\n", "The signals can be used by the Bluesky RunEngine. Let's configure a RunEngine\n", "to print a table." ] }, { "cell_type": "code", "execution_count": null, "id": "affected-three", "metadata": {}, "outputs": [], "source": [ "from bluesky import RunEngine\n", "from bluesky.callbacks import LiveTable\n", "RE = RunEngine()\n", "token = RE.subscribe(LiveTable([\"time_delta\", \"x\"]))" ] }, { "cell_type": "markdown", "id": "blond-celebration", "metadata": {}, "source": [ "Because ``time_delta`` is writable, it can be scanned like a \"motor\". It can\n", "also be read like a \"detector\". (In Bluesky, all things that are \"motors\" are\n", "also \"detectors\".)" ] }, { "cell_type": "code", "execution_count": null, "id": "precious-corruption", "metadata": {}, "outputs": [], "source": [ "from bluesky.plans import count, list_scan\n", "\n", "RE(count([time_delta])) # Use as a \"detector\".\n", "RE(list_scan([], time_delta, [0.1, 0.3, 1, 3])) # Use as \"motor\"." ] }, { "cell_type": "markdown", "id": "failing-tribute", "metadata": {}, "source": [ "For the following example, set ``time_delta`` to ``1``." ] }, { "cell_type": "code", "execution_count": null, "id": "induced-undergraduate", "metadata": {}, "outputs": [], "source": [ "from bluesky.plan_stubs import mv\n", "\n", "RE(mv(time_delta, 1))" ] }, { "cell_type": "markdown", "id": "statistical-wagner", "metadata": {}, "source": [ "We know that ``x`` represents a time-dependent variable. We can \"poll\" it at\n", "regular intervals" ] }, { "cell_type": "code", "execution_count": null, "id": "treated-explosion", "metadata": {}, "outputs": [], "source": [ "RE(count([x], num=5, delay=0.5)) # Read every 0.5 seconds." ] }, { "cell_type": "markdown", "id": "administrative-moment", "metadata": {}, "source": [ "but this required us to choose an update frequency (``0.5``). It's often better\n", "to rely on the control system to *tell* us when a new value is available. In\n", "this example, we accumulate updates for ``x`` whenever it changes." ] }, { "cell_type": "code", "execution_count": null, "id": "emerging-active", "metadata": {}, "outputs": [], "source": [ "from bluesky.plan_stubs import monitor, unmonitor, open_run, close_run, sleep\n", "\n", "def monitor_x_for(duration, md=None):\n", " yield from open_run(md) # optional metadata\n", " yield from monitor(x, name=\"x_monitor\")\n", " yield from sleep(duration) # Wait for readings to accumulate.\n", " yield from unmonitor(x)\n", " yield from close_run()" ] }, { "cell_type": "code", "execution_count": null, "id": "material-navigator", "metadata": {}, "outputs": [], "source": [ "RE.unsubscribe(token) # Remove the old table.\n", "RE(monitor_x_for(3), LiveTable([\"x\"], stream_name=\"x_monitor\"))" ] }, { "cell_type": "markdown", "id": "lesser-mayor", "metadata": {}, "source": [ "If you are a scientist aiming to use Ophyd with the Bluesky Run Engine, you may\n", "stop at this point or read on to learn more about how the Run Engine interacts\n", "with these signals. If you are a controls engineer, the details that follow are\n", "likely important to you.\n", "\n", "## Use it directly\n", "\n", "Note: These methods should *not* be called inside a Bluesky plan.\n", "\n", "### Read\n", "\n", "The signal can be read. It return a dictionary with one item. The key is the\n", "human-friendly ``name`` we specified. The value is another dictionary,\n", "containing the ``value`` and the ``timestamp`` of the reading from the control\n", "system (in this case, EPICS)." ] }, { "cell_type": "code", "execution_count": null, "id": "derived-funeral", "metadata": {}, "outputs": [], "source": [ "time_delta.read()" ] }, { "cell_type": "markdown", "id": "frank-activity", "metadata": {}, "source": [ "### Describe\n", "\n", "Additional metadata is available. This always includes the data type, shape,\n", "and source (e.g. PV). It may also include units and other metadata." ] }, { "cell_type": "code", "execution_count": null, "id": "sharing-behavior", "metadata": {}, "outputs": [], "source": [ "time_delta.describe()" ] }, { "cell_type": "markdown", "id": "powered-planning", "metadata": {}, "source": [ "### Set\n", "\n", "This signal is writable, so it can also be set." ] }, { "cell_type": "code", "execution_count": null, "id": "connected-grounds", "metadata": {}, "outputs": [], "source": [ "time_delta.set(10).wait() # Set it to 10 and wait for it to get there." ] }, { "cell_type": "markdown", "id": "annual-sponsorship", "metadata": {}, "source": [ "Sometimes hardware gets stuck or does not do what it is told, and so it is good\n", "practice to put a timeout on how long you are willing to wait until deciding\n", "that there is an error that needs to be handled somehow." ] }, { "cell_type": "code", "execution_count": null, "id": "beautiful-bottle", "metadata": {}, "outputs": [], "source": [ "time_delta.set(10).wait(timeout=1) # Set it to 10 and wait up to 1 second." ] }, { "cell_type": "markdown", "id": "growing-criminal", "metadata": {}, "source": [ "If the signal fails to arrive, a ``TimeoutError`` will be raised.\n", "\n", "Note that ``set(...)`` starts the motion but does *not* wait for it to\n", "complete. It is a fast, \"non-blocking\" operation. This enables you to run\n", "code between starting a motion and completing it." ] }, { "cell_type": "code", "execution_count": null, "id": "balanced-fundamental", "metadata": {}, "outputs": [], "source": [ "status = time_delta.set(5)\n", "print(\"Moving to 5...\")\n", "status.wait(timeout=1)\n", "print(\"Moved to 5.\")" ] }, { "cell_type": "markdown", "id": "vertical-modification", "metadata": {}, "source": [ "To move more than one signal in parallel, use the `ophyd.status.wait`\n", "*function*.\n", "\n", " from ophyd.status import wait\n", "\n", " # Given signals a and b, set both in motion.\n", " status1 = a.set(1)\n", " status2 = b.set(1)\n", " # Wait for both to complete.\n", " wait(status1, status2, timeout=1)\n", "\n", "### Subscribe\n", "\n", "What's the best way to read a signal that changes over time, like our ``x``\n", "signal?\n", "\n", "First, set ``time_delta`` to a reasonable value like ``1``. This controls the\n", "update rate of ``x`` in our random walk simulation." ] }, { "cell_type": "markdown", "id": "sudden-australia", "metadata": {}, "source": [ "time_delta.set(1).wait()" ] }, { "cell_type": "markdown", "id": "polar-provider", "metadata": {}, "source": [ "We could poll the signal in a loop and collect N readings spaced T seconds\n", "apart." ] }, { "cell_type": "code", "execution_count": null, "id": "later-tonight", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "# Don't do this.\n", "N = 5\n", "T = 0.5\n", "readings = []\n", "for _ in range(N):\n", " time.sleep(T)\n", " reading = x.read()\n", " readings.append(reading)" ] }, { "cell_type": "markdown", "id": "inner-american", "metadata": {}, "source": [ "There are two problems with this counterexample.\n", "\n", "1. We might not know how often we need to check for updates.\n", "2. We often want to watch *multiple* signals with different update rates, and\n", " this pattern would quickly become messy.\n", "\n", "Alternatively, we can use *subscription*." ] }, { "cell_type": "code", "execution_count": null, "id": "aware-middle", "metadata": {}, "outputs": [], "source": [ "from collections import deque\n", "\n", "def accumulate(value, old_value, timestamp, **kwargs):\n", " readings.append({\"x\": {\"value\": value, \"timestamp\": timestamp}})\n", "\n", "readings = deque(maxlen=5)\n", "x.subscribe(accumulate)" ] }, { "cell_type": "markdown", "id": "invisible-subject", "metadata": {}, "source": [ "When the control system has a new ``reading`` for us, it calls\n", "``readings.append(reading)`` from a background thread. If we do other work or\n", "sleep for awhile and then check back on ``readings`` we'll see that it has some\n", "items in it." ] }, { "cell_type": "code", "execution_count": null, "id": "lyric-mount", "metadata": {}, "outputs": [], "source": [ "time.sleep(3)\n", "readings" ] }, { "cell_type": "markdown", "id": "minute-michigan", "metadata": {}, "source": [ "It will keep the last ``5``. We used a `collections.deque` instead of a\n", "plain `list` here because a `list` would grow without bound and, if left to\n", "run long enough, consume all available memory, crashing the program." ] } ], "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.7.10" } }, "nbformat": 4, "nbformat_minor": 5 }