Status objects (Futures)#
Ophyd Status objects signal when some potentially-lengthy action is complete.
The action may be moving a motor, acquiring an image, or waiting for a
temperature controller to reach a setpoint. From a general software engineering
point of view, they are like concurrent.futures.Future
objects in the
Python standard library but with some semantics specific to controlling
physical hardware.
The lifecycle of a Status object is:
A Status object is created with an associated timeout. The timeout clock starts.
The recipient of the Status object may add callbacks that will be notified when the Status object completes.
The Status object is marked as completed successfully, or marked as completed with an error, or the timeout is reached, whichever happens first. The callbacks are called in any case.
Creation and Marking Completion#
A timeout, given in seconds, is optional but strongly recommended. (The
default, None
means it will wait forever to be marked completed.)
from ophyd import Status
status = Status(timeout=60)
Additionally, it accepts a settle_time, an extra delay which will be added
between the control system reporting successful completion and the Status being
marked as finished. This is also given in seconds. It is 0
by default.
status = Status(timeout=60, settle_time=10)
The status should be notified by the control system, typically from another
thread or task, when some action is complete. To mark success, call
set_finished
. To mark failure, call
set_exception
, passing it an Exception giving
information about the cause of failure.
As a toy example, we could hook it up to a threading.Timer
that marks it
as succeeded or failed based on a coin flip.
import random
import threading
def mark_done():
if random.random() > 0.5: # coin flip
status.set_finished() # success
else:
error = Exception("Bad luck")
status.set_exception(error) # failure
# Run mark_done 5 seconds from now in a thread.
threading.Timer(5, mark_done).start()
See the tutorials for more realistic examples involving integration with an actual control system.
Changed in version v1.5.0: In previous versions of ophyd, the Status objects were marked as completed
by calling status._finished(success=True)
or
status._finished(success=False)
. This is still supported but the new
methods status.set_finished()
and status.set_exception(...)
are
recommended because they can provide more information about the cause of
failure, and they match the Python standard library’s
concurrent.futures.Future
interface.
Notification of Completion#
The recipient of the Status object can request synchronous or asynchronous
notification of completion. To wait synchronously, the wait
will block until the Status is marked as complete or a timeout has expired.
status.wait() # Wait forever for the Status to finish or time out.
status.wait(10) # Wait for at most 10 seconds.
If and when the Status completes successfully, this will return None
. If
the Status is marked as failed, the exception (e.g. Exception("Bad luck")
in our example above) will be raised. If the Status’ own timeout has expired,
StatusTimeoutError
will be raised. If a timeout given to
wait
expires before any of these things happen,
WaitTimeoutError
will be raised.
The method exception
behaves similarly to
wait
; the only difference is that if the Status is marked as
failed or the Status’ own timeout expires it returns the exception rather
than raising it. Both return None
if the Status finishes successfully,
and both raise WaitTimeoutError
if the given timeout expires
before the Status completes or times out.
Alternatively, the recipient of the Status object can ask to be notified of completion asynchronously by adding a callback. The callback will be called when the Status is marked as complete or its timeout has expired. (If no timeout was given, the callback might never be called. This is why providing a timeout is strongly recommended.)
def callback(status):
print(f"{status} is done")
status.add_callback(callback)
Callbacks may be added at any time. Until the Status completes, it holds a hard
reference to each callback in a list, status.callbacks
. The list is cleared
when the callback completes. Any callbacks added to a Status object after
completion will be called immediately, and no reference will be held.
Each callback is passed to the Status object as an argument, and it can use this to distinguish success from failure.
def callback(status):
error = status.exception()
if error is None:
print(f"{status} has completed successfully.")
else:
print(f"{status} has failed with error {error}.")
SubscriptionStatus#
The SubscriptionStatus
is a special Status object that
correctly and succinctly handles a common use case, wherein the Status object
is marked finished based on some ophyd event. It reduces this:
from ophyd import Device, Component, DeviceStatus
class MyToyDetector(Device):
...
# When set to 1, acquires, and then goes back to 0.
acquire = Component(...)
def trigger(self):
def check_value(*, old_value, value, **kwargs):
"Mark status as finished when the acquisition is complete."
if old_value == 1 and value == 0:
status.set_finished()
# Clear the subscription.
self.acquire.clear_sub(check_value)
status = DeviceStatus(self.acquire)
self.acquire.subscribe(check_value)
self.acquire.set(1)
return status
to this:
from ophyd import Device, Component, SubscriptionStatus
class MyToyDetector(Device):
...
# When set to 1, acquires, and then goes back to 0.
acquire = Component(...)
def trigger(self):
def check_value(*, old_value, value, **kwargs):
"Return True when the acquisition is complete, False otherwise."
return (old_value == 1 and value == 0)
status = SubscriptionStatus(self.acquire, check_value)
self.acquire.set(1)
return status
Note that set_finished
, subscribe
and clear_sub
are gone; they are
handled automatically, internally. See
SubscriptionStatus
for additional options.
StableSubscriptionStatus#
The StableSubscriptionStatus
is a Status object that is
similar to the SubscriptionStatus
but is only marked
finished based on an ophyd event remaining stable for some given time. For
example, this could be used to ensure a temperature remains in a given range
for a set amount of time:
from ophyd import Device, Component, StableSubscriptionStatus
class MyTempSensor(Device):
...
# The set point and readback of a temperature that
# may fluctuate for a second before it can be considered set
temp_sp = Component(...)
temp_rbv = Component(...)
def set(self, set_value):
def check_value(*, old_value, value, **kwargs):
"Return True when the temperature is in a valid range."
return set_value - 0.01 < value < set_value + 0.01
status = StableSubscriptionStatus(self.temp_rbv, check_value, stability_time=1)
self.temp_sp.set(set_value)
return status
The timer for stability_time
is started when the callback condition first
becomes true and stopped if it becomes false. It is then restarted if the
condition becomes true again. This will continue until either the condition
stays true for the full stability_time
, in which case the Status will succeed,
or a timeout/exception is reached, in which it will fail.
Note: Before using this status it’s recommended you think about implementing this check in the server side i.e. as the put callback in the associated IOC. This will allow multiple clients to easily share the same logic. However, this client-side status can be useful in cases where logic may need to be modified often or where different clients may have varying opinions on what stability means.
Partial Progress Updates#
Some Status objects provide an additional method named watch
, as in
watch()
, which can be used to subscribe to
incremental progress updates suitable for building progress bars. See
Progress Bar for one application of this feature.
The watch
method accepts a callback which must accept the following
parameters as optional keyword arguments:
name
current
initial
target
unit
precision
fraction
time_elapsed
time_remaining
The callback may receive a subset of these depending on how much we can know
about the progress of a particular action. In the case of
ophyd.status.MoveStatus
and
ophyd.areadetector.trigger_mixins.ADTriggerStatus
, we know a lot, from
which one can build a frequently-updating progress bar with a realistic
estimated time of completion. In the case of a generic
ophyd.status.DeviceStatus
, we only know the name of the associated
Device, when the action starts, and when the action ends.