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:

  1. A Status object is created with an associated timeout. The timeout clock starts.

  2. The recipient of the Status object may add callbacks that will be notified when the Status object completes.

  3. 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.