{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# synApps *busy* record\n", "\n", "**Objective**\n", "\n", "In this notebook, we show how to use the EPICS *busy* record.\n", "\n", "The synApps [*busy* record](https://epics.anl.gov/bcda/synApps/busy/busyRecord.html)\n", "is used to signal the completion of an operation. Generally, the *busy* record is used\n", "for operations that have no inherent means of reporting that a long or complex operation\n", "has completed. Two cases come to mind immediately, both involving waiting for\n", "completion of some operation):\n", "\n", "- arbitrary operation\n", "- movement of a positioner (or set of positioners such as a diffractometer or hexapod)\n", "\n", "One type of positioner, the EPICS *motor* record, already has such a means\n", "to report done moving, via the `.DMOV` field, so the *busy* record provides\n", "no additional benefit. But a set of simpler PVs (using *ao*, *ai*, *bo*, & *bi*\n", "records), which together implement the main features of a positioner, would benefit\n", "from having a _done moving_ signal. This is a case for use of a *busy* record.\n", "\n", "This notebook expects an EPICS IOC with prefix `gp:` that provides several PVs:\n", "\n", "PV | record type | description\n", "--- | --- | ---\n", "`{IOC}gp:bit1` | bo | general purpose binary output (bit) variable\n", "`{IOC}gp:float1` | ao | general purpose analog output (floating-point) variable\n", "`{IOC}gp:float2` | ao | general purpose floating-point (floating-point) variable\n", "`{IOC}mybusy1` | busy | general purpose busy record\n", "`{IOC}mybusy2` | busy | general purpose busy record\n", "\n", "\n", "The `instrument` package is not necessary. This notebook will use a [temporary databroker catalog](https://blueskyproject.io/databroker/generated/databroker.temp.html?highlight=temp#databroker.temp)." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Exception reporting mode: Minimal\n" ] }, { "data": { "text/plain": [ "1" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%xmode Minimal\n", "from apstools.synApps import BusyRecord\n", "from apstools.utils import run_in_thread\n", "from bluesky import plans as bp\n", "from bluesky import plan_stubs as bps\n", "from bluesky.callbacks.best_effort import BestEffortCallback\n", "from enum import Enum\n", "from ophyd import Component\n", "from ophyd import Device\n", "from ophyd import EpicsSignal\n", "from ophyd import PVPositioner\n", "from ophyd import Signal\n", "from ophyd.status import DeviceStatus\n", "\n", "import bluesky\n", "import databroker\n", "import logging\n", "import time\n", "\n", "IOC = \"gp:\"\n", "\n", "cat = databroker.temp()\n", "logging.basicConfig()\n", "logger = logging.getLogger(__name__)\n", "logger.setLevel(logging.INFO)\n", "RE = bluesky.RunEngine({})\n", "RE.subscribe(cat.v1.insert)\n", "RE.subscribe(BestEffortCallback())" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## arbitrary operation\n", "\n", "Use a *busy* record to indicate the state of some arbitrary operation. A *fly* scan is one example. In this example, a user-adjustable time delay is sued to simulate the arbitrary operation.\n", "\n", "The *apstools* package provides support for the *busy* record, `apstools.synApps.BusyRecord`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### _operation_ uses `trigger()` method\n", "\n", "Let's start with a Device with a *busy* PV and a user-settable delay time. The _operation_ is run (in an external thread) from the device's `.trigger()` method.\n", "\n", "Follow example from https://blueskyproject.io/ophyd/explanations/status.html?highlight=devicestatus" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class OperatorBase(Device):\n", " busy = Component(BusyRecord, \"mybusy1\", kind=\"omitted\")\n", " delay_time_s = Component(Signal, value=2, kind=\"hinted\")\n", "\n", " def trigger(self):\n", " def check_busy(*, old_value, value, **kwargs):\n", " \"Mark as finished when *busy* changes from Busy to Done.\"\n", " if old_value in (1, \"Busy\") and value in (0, \"Done\"):\n", " self.busy.state.clear_sub(check_busy)\n", " status.set_finished()\n", " \n", " @run_in_thread\n", " def simulated_operation():\n", " # simulate how the external process works\n", " self.busy.state.set(\"Busy\")\n", " time.sleep(self.delay_time_s.get())\n", " self.busy.state.set(\"Done\")\n", "\n", " status = DeviceStatus(self.busy.state)\n", " self.busy.state.subscribe(check_busy)\n", " simulated_operation()\n", " return status" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('operator_delay_time_s',\n", " {'value': 2, 'timestamp': 1681482822.6680264})])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "operator = OperatorBase(IOC, name=\"operator\")\n", "operator.wait_for_connection()\n", "operator.stage_sigs[\"delay_time_s\"] = 1.0\n", "operator.read()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Run the operation by calling the `operation.trigger()` method. Since that method returns a status object (used by the RunEngine to wait for the trigger method to complete), grab that status object. Use that to wait for the trigger method to complete. Report elapsed time, as well." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.013 st = DeviceStatus(device=operator_busy_state, done=False, success=False)\n", "2.020 st = DeviceStatus(device=operator_busy_state, done=True, success=True)\n" ] } ], "source": [ "t0 = time.time() # time the trigger()\n", "st = operator.trigger() # trigger() returns a status object\n", "print(f\"{time.time()-t0:.3f} {st = }\")\n", "st.wait()\n", "print(f\"{time.time()-t0:.3f} {st = }\") # default time, since device was not staged" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, run `count()` (one of the bluesky.plans) with the `operator` device as a \"detector\". The standard plans take care of staging, triggering, reading, and unstaging the device. A RunEngine subscription by the `BestEffortCallback` is responsible for generating the `LiveTable` view." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "Transient Scan ID: 1 Time: 2023-04-14 09:33:44\n", "Persistent Unique Scan ID: 'fb42dc41-560a-4ef1-8830-c06d44103fad'\n", "New stream: 'primary'\n", "+-----------+------------+-----------------------+\n", "| seq_num | time | operator_delay_time_s |\n", "+-----------+------------+-----------------------+\n", "| 1 | 09:33:45.9 | 1.000 |\n", "+-----------+------------+-----------------------+\n", "generator count ['fb42dc41'] (scan num: 1)\n", "\n", "\n", "\n" ] }, { "data": { "text/plain": [ "('fb42dc41-560a-4ef1-8830-c06d44103fad',)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(bp.count([operator]))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## positioner movement\n", "\n", "Use a *busy* record to signal _done moving_ for a positioner built\n", "from separate PVs (using `ophyd.PVPositioner`).\n", "\n", "PV | PVPositioner attribute\n", "--- | ---\n", "`{IOC}gp:bit1` | `stop_signal`\n", "`{IOC}gp:float1` | `setpoint`\n", "`{IOC}gp:float2` | `readback`\n", "`{IOC}gp:float2.PREC` | `precision`\n", "`{IOC}mybusy2` | `done`\n", "\n", "TODO: Discuss the implementation (Here, we connect to mybusy2 as EpicsSignal)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "import math\n", "\n", "class Mover(PVPositioner):\n", " setpoint = Component(EpicsSignal, \"gp:float1\")\n", " readback = Component(EpicsSignal, \"gp:float2\")\n", " done = Component(EpicsSignal, \"mybusy2\")\n", " done_value = 0\n", " stop_signal = Component(EpicsSignal, \"gp:bit1\")\n", " stop_value = 1\n", " precision = Component(EpicsSignal, \"gp:float2.PREC\")\n", "\n", " simulator_sleep_s = 0.1\n", " tolerance = 0.001\n", "\n", " @property\n", " def in_position(self):\n", " return math.isclose(self.setpoint.get(), self.readback.get(), abs_tol=self.tolerance)\n", "\n", " @property\n", " def is_done(self):\n", " return self.done.get() == self.done_value\n", " \n", " @run_in_thread\n", " def setpoint_watch(self, *args, **kwargs):\n", " if self.is_done:\n", " self.done.put(1 - self.done_value)\n", "\n", " @run_in_thread\n", " def motion_simulator(self):\n", " \"\"\"Simulate the motion using Python code.\"\"\"\n", " reset_stop_value = 1 - self.stop_value\n", "\n", " while True:\n", " if self.in_position and not self.is_done:\n", " # finish the move to the exact setpoint value\n", " self.readback.put(self.setpoint.get())\n", " self.done.put(self.done_value)\n", " logger.info(f\"simulator: {self.readback.get() = } {self.is_done = } end\")\n", "\n", " if not self.in_position:\n", " if self.stop_signal.get() == self.stop_value:\n", " # must STOP the move now, stay at current position\n", " self.setpoint.put(self.readback.get())\n", " self.stop_signal.put(reset_stop_value)\n", " self.done.put(self.done_value)\n", " logger.info(f\"simulator: {self.readback.get() = } stopped\")\n", "\n", " diff = self.setpoint.get() - self.readback.get()\n", "\n", " if abs(diff) > self.tolerance:\n", " # move closer to the setpoint\n", " step = diff * 0.5 # novel step size\n", " value = step + self.readback.get()\n", " self.readback.put(value)\n", " logger.info(f\"simulator: {self.tolerance=} {self.is_done=} {diff=} {value=}\")\n", "\n", " time.sleep(self.simulator_sleep_s)\n", "\n", " def __init__(self, *args, **kwargs):\n", " super().__init__(*args, **kwargs)\n", "\n", " # update tolerance based on display precision\n", " self.tolerance = 10**(-self.precision.get())\n", " self.setpoint.subscribe(self.setpoint_watch)\n", " self.motion_simulator()" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:__main__:simulator: self.readback.get() = 1.0 self.is_done = True end\n" ] } ], "source": [ "mover = Mover(IOC, name=\"mover\")\n", "mover.wait_for_connection()" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-2.0 value=0.0\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-1.0 value=-0.5\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.5 value=-0.75\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.25 value=-0.875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.125 value=-0.9375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0625 value=-0.96875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.03125 value=-0.984375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.015625 value=-0.9921875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0078125 value=-0.99609375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.00390625 value=-0.998046875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.001953125 value=-0.9990234375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0009765625 value=-0.99951171875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.00048828125 value=-0.999755859375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.000244140625 value=-0.9998779296875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0001220703125 value=-0.99993896484375\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "0 mover.position = -1.0 st.elapsed = 1.6434977054595947\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "INFO:__main__:simulator: self.readback.get() = -1.0 self.is_done = True end\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=2.0 value=0.0\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=1.0 value=0.5\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.5 value=0.75\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.25 value=0.875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.125 value=0.9375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0625 value=0.96875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.03125 value=0.984375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.015625 value=0.9921875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0078125 value=0.99609375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.00390625 value=0.998046875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.001953125 value=0.9990234375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0009765625 value=0.99951171875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.00048828125 value=0.999755859375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.000244140625 value=0.9998779296875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0001220703125 value=0.99993896484375\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "1 mover.position = 1.0 st.elapsed = 1.5773115158081055\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "INFO:__main__:simulator: self.readback.get() = 1.0 self.is_done = True end\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-2.0 value=0.0\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-1.0 value=-0.5\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.5 value=-0.75\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.25 value=-0.875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.125 value=-0.9375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0625 value=-0.96875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.03125 value=-0.984375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.015625 value=-0.9921875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0078125 value=-0.99609375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.00390625 value=-0.998046875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.001953125 value=-0.9990234375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0009765625 value=-0.99951171875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.00048828125 value=-0.999755859375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.000244140625 value=-0.9998779296875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0001220703125 value=-0.99993896484375\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "2 mover.position = -1.0 st.elapsed = 1.5689060688018799\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "INFO:__main__:simulator: self.readback.get() = -1.0 self.is_done = True end\n" ] } ], "source": [ "if mover.position == 0:\n", " mover.move(1)\n", "for i in range(3):\n", " st = mover.move(-mover.position)\n", " print(f\"{i} {mover.position = } {st.elapsed = }\")" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=2.0 value=0.0\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=1.0 value=0.5\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.5 value=0.75\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.25 value=0.875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.125 value=0.9375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0625 value=0.96875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.03125 value=0.984375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.015625 value=0.9921875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0078125 value=0.99609375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.00390625 value=0.998046875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.001953125 value=0.9990234375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0009765625 value=0.99951171875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.00048828125 value=0.999755859375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.000244140625 value=0.9998779296875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=0.0001220703125 value=0.99993896484375\n", "INFO:__main__:simulator: self.readback.get() = 1.0 self.is_done = True end\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-2.0 value=0.0\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-1.0 value=-0.5\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.5 value=-0.75\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.25 value=-0.875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.125 value=-0.9375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0625 value=-0.96875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.03125 value=-0.984375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.015625 value=-0.9921875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0078125 value=-0.99609375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.00390625 value=-0.998046875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.001953125 value=-0.9990234375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0009765625 value=-0.99951171875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.00048828125 value=-0.999755859375\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.000244140625 value=-0.9998779296875\n", "INFO:__main__:simulator: self.tolerance=0.0001 self.is_done=False diff=-0.0001220703125 value=-0.99993896484375\n" ] }, { "data": { "text/plain": [ "()" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" }, { "name": "stderr", "output_type": "stream", "text": [ "INFO:__main__:simulator: self.readback.get() = -1.0 self.is_done = True end\n" ] } ], "source": [ "def n_moves(n=2):\n", " for _ in range(n):\n", " yield from bps.mv(mover, -mover.position)\n", "\n", "RE(n_moves())" ] } ], "metadata": { "interpreter": { "hash": "0f620dbb5c23371d20cb5d4d85edb0a561f65de2f83c1c1c0d7383193492354a" }, "kernelspec": { "display_name": "Python 3.9.7 64-bit ('base': conda)", "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.10" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }