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