Architecture

Hardware abstraction

Ophyd is the hardware abstraction layer that provides a consistent interface between the underlying control communication protocol and bluesky. This is done by bundling sets of the underlying process variables into hierarchical devices and exposing a semantic API in terms of control system primitives. Two terms that will be used throughout are

Signal

Represents an atomic ‘process variable’. This is nominally a ‘scalar’ value and cannot be decomposed any further by layers above ophyd. In this context an array (waveform) or string would be a scalar because there is no ophyd API to read only part of it.

Device

Hierarchy composed of Signals and other Devices. The components of a Device can be introspected by layers above ophyd and may be decomposed to, ultimately, the underlying Signals.

Put another way, if a hierarchical device is a tree, Signals are the leaves and Devices are the nodes.

Names

In ophyd, we can think of a Device as a tree of sub-devices and eventually the ‘leaf’ nodes which are Signals (and map to 1 or 2 PVs). At the bottom of the tree, each Signal (leaf-node) has 3 names associated with it:

  1. The PV name it is going to talk to. Typically, this name must be globally unique within the control system you are using. This can lead to them being both verbose and cryptic. From ophyd's point of view these strings are taken as given and does not require any particular pattern, scheme, rhyme, or reason in the names.

  2. The Python attribute name. These are the names of the components of a device and allow attribute-style access to the sub components as dev.cpt_name. These names are set in the ophyd.Device sub-class definitions. They need to be a valid Python identifiers (which Python enforces) and should be chosen to makes sense to the people directly working with the ophyd instances. They need be unique within a ~ophyd.Device and hence Python ensures that the fully qualified name will be unique within a namespace.

  3. The ``obj.name`` attribute. This name is the one that will be used in the data returned by ~ophyd.Device.read and will eventually end up in the flowing through bluesky and into databroker to be eventually exposed to the users at analysis times. By default, these names are derived from the Python attribute name of the sub-device and the name of it’s parent, but can be set at runtime. These names should be picked to make scientific sense at analysis time and must be unique among devices that will be used simultaneously.

Uniform High-level Interface

All ophyd objects implemented a small set of methods which are used by bluesky plans. It is the responsibility of the ophyd objects to correctly implement these methods in terms of the underlying control system.

Read-able Interface

The minimum set of methods an object must implement is

trigger()

Trigger the device and return status object.

read()

Read data from the device.

describe()

Provide schema and meta-data for read().

along with three properties:

name

name of the device

parent

The parent of the ophyd object.

root

Walk parents to find ultimate ancestor (parent’s parent…).

There are two optional methods which plans may use to ‘enable’ or ‘disable’ a device for data collection. For example, a beam position monitor maybe in continuous mode when not collecting data but be stitched to a triggered mode for scanning. By convention unstage ‘undoes’ whatever stage did to the state of the underlying hardware and should return it to the state it was before stage was called.

stage()

Stage the device for data collection.

unstage()

Unstage the device.

Two additional optional methods are used to notify devices if, during a scan, the run is suspended. The semantics of these methods is coupled to RunEngine.

pause()

Attempt to ‘pause’ the device.

resume()

Resume a device from a ‘paused’ state.

Set-able Interface

Of course, most interesting uses of hardware requires telling it to do rather than just reading from it! To do that the high-level API has the set method and a corresponding stop method to halt motion before it is complete.

The set method which returns Status that can be used to tell when motion is done. It is the responsibility of the ophyd objects to implement this functionality in terms of the underlying control system. Thus, from the perspective of the bluesky, a motor, a temperature controller, a gate valve, and software pseudo-positioner can all be treated the same.

set(new_position, *[, timeout, moved_cb, wait])

Set a value and return a Status object

stop(*[, success])

Stops motion.

Configuration

In addition to values we will want to read, as ‘data’, or set, as a ‘position’, there tend to be many values associated with the configuration of hardware. This is things like the velocity of a motor, the PID loop parameters of a feedback loop, or the chip temperature of a detector. In general these are measurements that are not directly related to the measurement of interest, but maybe needed for understanding the measured data.

configure(d)

Configure the device for something during a run

read_configuration()

Dictionary mapping names to value dicts with keys: value, timestamp

describe_configuration()

Provide schema & meta-data for read_configuration()

Fly-able Interface

There is some hardware where instead of the fine-grained control provided by set, trigger, and read we just want to tell it “Go!” and check back later when it is done. This is typically done when there needs to coordinated motion or triggering at rates beyond what can reasonably done in via EPICS/Python and tend to be called ‘fly scans’.

The flyable interface provides four methods

kickoff()

Start a flyer

complete()

Wait for flying to be complete.

describe_collect()

Provide schema & meta-data from collect()

collect()

Retrieve data from the flyer as proto-events

The expected sequencing of the commands is

  • kickoff and wait

  • complete and wait

  • collect

Optionally, devices my implement “partial collection” so that they can be incrementally collected during acquisition. While this may not be technically possible in every situation, it can be used to get partial results and allow the fly scan to look more like a step scan from the point of view of the data consumers.

  • kickoff and wait

  • 0 or more calls to collect

  • complete and wait

  • collect

import bluesky.preprocessors as bpp
import bluesky.plan_stubs as bps
from bluesky import RunEngine
from bluesky.callbacks.best_effort import BestEffortCallback
from ophyd.sim import SynAxis, SynGauss, MockFlyer


motor = SynAxis(name="motor", labels={"motors"}, value=0.0)
det = SynGauss("det", motor, "motor", center=0, Imax=1, sigma=1, labels={"detectors"})
flyer1 = MockFlyer("primary", det, motor, -3, 5, 200)

motor.delay = .1


def single_collect(flyer, *, md=None):
    _md = {}
    _md.update(md or {})
    @bpp.run_decorator(md=_md)
    def single_collect(flyer):
        yield from bps.kickoff(flyer, wait=True)
        yield from bps.complete(flyer, wait=True)
        yield from bps.collect(flyer)

    return (yield from single_collect(flyer))


def multi_collect(flyer, *, md=None):
    _md = {}
    _md.update(md or {})
    @bpp.run_decorator(md=_md)
    def multi_collect(flyer):

        yield from bps.kickoff(flyer, wait=True)
        st = yield from bps.complete(flyer)
        while st is not None and not st.done:
            yield from bps.collect(flyer, stream=True)
            yield from bps.sleep(1)

        yield from bps.collect(flyer, stream=True)

    return (yield from multi_collect(flyer))


RE = RunEngine()
bec = BestEffortCallback()

# RE(multi_collect(flyer1), bec)
# RE(single_collect(flyer1), bec)

Asynchronous status

Hardware control and data collection is an inherently asynchronous activity. The many devices on a beamline are (in general) uncoupled and can move / read independently. This is reflected in the API as most of the methods in BlueskyInterface returning Status objects and in the callback registry at the core of OphydObject. The StatusBase objects are the bridge between the asynchronous behavior of the underlying control system and the asynchronous behavior of RunEngine.

The core API of the status objects is a property and a private method:

status.StatusBase.finished_cb

status.StatusBase._finished

Inform the status object that it is done and if it succeeded.

The bluesky side assigns a callback to status.StatusBase.finished_cb which is triggered when the status.StatusBase._finished() method is called. The status object conveys both that the action it ‘done’ and if the action was successful or not.

Callbacks

The base class of almost all objects in ophyd is OphydObject

a callback registry

OphydObject

The base class for all objects in Ophyd

OphydObject.event_types

Events that can be subscribed to via obj.subscribe

OphydObject.subscribe

Subscribe to events this event_type generates.

OphydObject.unsubscribe

Remove a subscription

OphydObject.clear_sub

Remove a subscription, given the original callback function

OphydObject._run_subs

Run a set of subscription callbacks

OphydObject._reset_sub

Remove all subscriptions in an event type

This registry is used to connect to the underlying events from the control system and propagate them up to bluesky, either via ~status.StatusBase objects or via direct subscription from the RunEngine.