How Bluesky Interfaces with Hardware

Overview

Bluesky interacts with hardware through a high-level abstraction, leaving the low-level details of communication as a separate concern. In bluesky’s view, all devices are in a sense “detectors,” in that they can be read. A subset of these devices are “positioners” that can also be set (i.e., written to or moved).

In short, each device is represented by a Python object that has attributes and methods with certain established names. We have taken pains to make this interface as slim as possible, while still being general enough to address every kind of hardware we have encountered.

Specification

Status object

The interface of a “status” object, which the RunEngine uses to asynchronously monitor the compeletion of having triggered or set a device.

class bluesky.protocols.Status(*args, **kwargs)[source]
abstract add_callback(callback: Callable[[Status], None]) None[source]

Add a callback function to be called upon completion.

The function must take the status as an argument.

If the Status object is done when the function is added, it should be called immediately.

abstract property done: bool

If done return True, otherwise return False.

abstract exception(timeout: float | None = 0.0) BaseException | None[source]
abstract property success: bool

If done return whether the operation was successful.

If success is False when the Status is marked done, this is taken to mean, “We have given up.” For example, “The motor is stuck and will never get where it is going.” A FailedStatus exception will be raised inside the RunEngine.

Additionally, Status objects may (optionally) add a watch function that conforms to the following definition

watch(func)

Subscribe to notifications about progress. Useful for progress bars.

Parameters

funccallable

Expected to accept the keyword arguments:

  • name

  • current

  • initial

  • target

  • unit

  • precision

  • fraction

  • time_elapsed

  • time_remaining

Any given call to func may only include a subset of these parameters, depending on what the status object knows about its own progress.

The fraction argument accepts a single float representing fraction remaining. A fraction of zero indicates completion. A fraction of one indicates progress has not started.

Named Device

Some of the interfaces below require a name attribute, they implement this interface:

class bluesky.protocols.HasName(*args, **kwargs)[source]

Bases: Protocol

abstract property name: str

Used to populate object_keys in the Event DataKey

https://blueskyproject.io/event-model/event-descriptors.html#object-keys

Some of also require a parent attribute, they implement this interface:

class bluesky.protocols.HasParent(*args, **kwargs)[source]

Bases: Protocol

abstract property parent: Any | None

None, or a reference to a parent device.

Used by the RE to stop duplicate stages.

Readable Device

To produce data in a step scan, a device must be Readable:

class bluesky.protocols.Readable(*args, **kwargs)[source]

Bases: HasName, Protocol

abstract describe() Dict[str, DataKey] | Awaitable[Dict[str, DataKey]][source]

Return an OrderedDict with exactly the same keys as the read method, here mapped to per-scan metadata about each field.

This can be a standard function or an async function.

Example return value:

OrderedDict(('channel1',
             {'source': 'XF23-ID:SOME_PV_NAME',
              'dtype': 'number',
              'shape': []}),
            ('channel2',
             {'source': 'XF23-ID:SOME_PV_NAME',
              'dtype': 'number',
              'shape': []}))
abstract read() Dict[str, Reading] | Awaitable[Dict[str, Reading]][source]

Return an OrderedDict mapping string field name(s) to dictionaries of values and timestamps and optional per-point metadata.

This can be a standard function or an async function.

Example return value:

OrderedDict(('channel1',
             {'value': 5, 'timestamp': 1472493713.271991}),
             ('channel2',
             {'value': 16, 'timestamp': 1472493713.539238}))

A dict of stream name to Descriptors is returned from describe(), where a Descriptor is a dictionary with the following keys:

class bluesky.protocols.DataKey[source]

Describes the objects in the data property of Event documents

A dict of stream name to Reading is returned from read(), where a Reading is a dictionary with the following keys:

class bluesky.protocols.Reading[source]

A dictionary containing the value and timestamp of a piece of scan data

timestamp: float

Timestamp in seconds since the UNIX epoch

value: Any

The current value, as a JSON encodable type or numpy array

The following keys can optionally be present in a Reading:

class bluesky.protocols.ReadingOptional[source]

A dictionary containing the optional per-reading metadata of a piece of scan data

alarm_severity: int
  • -ve: alarm unknown, e.g. device disconnected

  • 0: ok, no alarm

  • +ve: there is an alarm

The exact numbers are transport specific

message: str

A descriptive message if there is an alarm

If the device has configuration that only needs to be read once at the start of scan, the following interface can be implemented:

class bluesky.protocols.Configurable(*args, **kwargs)[source]

Bases: Protocol

abstract describe_configuration() Dict[str, DataKey] | Awaitable[Dict[str, DataKey]][source]

Same API as describe, but corresponding to the keys in read_configuration.

This can be a standard function or an async function.

abstract read_configuration() Dict[str, Reading] | Awaitable[Dict[str, Reading]][source]

Same API as read but for slow-changing fields related to configuration. e.g., exposure time. These will typically be read only once per run.

This can be a standard function or an async function.

If a device needs to do something before it can be read, the following interface can be implemented:

class bluesky.protocols.Triggerable(*args, **kwargs)[source]

Bases: Protocol

abstract trigger() Status[source]

Return a Status that is marked done when the device is done triggering.

External Asset Writing Interface

Devices that write their data in external files, rather than returning directly from read() should implement the following interface:

class bluesky.protocols.WritesExternalAssets(*args, **kwargs)[source]

Bases: Protocol

abstract collect_asset_docs() Iterator[Tuple[Literal['resource'], PartialResource] | Tuple[Literal['datum'], Datum] | Tuple[Literal['stream_resource'], StreamResource] | Tuple[Literal['stream_datum'], StreamDatum]] | AsyncIterator[Tuple[Literal['resource'], PartialResource] | Tuple[Literal['datum'], Datum] | Tuple[Literal['stream_resource'], StreamResource] | Tuple[Literal['stream_datum'], StreamDatum]][source]

Create the resource, datum, stream_resource, and stream_datum documents describing data in external source.

Example yielded values:

('resource', {
    'path_semantics': 'posix',
    'resource_kwargs': {'frame_per_point': 1},
    'resource_path': 'det.h5',
    'root': '/tmp/tmpcvxbqctr/',
    'spec': 'AD_HDF5',
    'uid': '9123df61-a09f-49ae-9d23-41d4d6c6d788'
})
# or
('datum', {
    'datum_id': '9123df61-a09f-49ae-9d23-41d4d6c6d788/0',
    'datum_kwargs': {'point_number': 0},
    'resource': '9123df61-a09f-49ae-9d23-41d4d6c6d788'}
})

The yielded values are a tuple of the document type and the document as a dictionary.

A Resource will be yielded to show that data will be written to an external resource like a file on disk:

class bluesky.protocols.PartialResource() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list.  For example:  dict(one=1, two=2)[source]

While a Datum will be yielded to specify a single frame of data in a Resource:

class bluesky.protocols.Datum[source]

Document to reference a quanta of externally-stored data

Movable (or “Settable”) Device

The interface of a movable device extends the interface of a readable device with the following additional methods and attributes.

class bluesky.protocols.Movable(*args, **kwargs)[source]

Bases: Protocol

position

A optional heuristic that describes the current position of a device as a single scalar, as opposed to the potentially multi-valued description provided by read().

Note

The position attribute has been deprecated in favour of the Locatable protocol below

abstract set(value) Status[source]

Return a Status that is marked done when the device is done moving.

Certain plans like mvr() would like to know where a Device was last requested to move to, and other plans like rd() would like to know where a Device is currently located. Devices may implement locate() to provide this information.

class bluesky.protocols.Locatable(*args, **kwargs)[source]

Bases: Movable, Protocol

abstract locate() Location | Awaitable[Location][source]

Return the current location of a Device.

While a Readable reports many values, a Movable will have the concept of location. This is where the Device currently is, and where it was last requested to move to. This protocol formalizes how to get the location from a Movable.

Location objects are dictionaries with the following entries:

class bluesky.protocols.Location[source]

A dictionary containing the location of a Device

readback: T

Where the Device actually is at the moment

setpoint: T

Where the Device was requested to move to

“Flyer” Interface

For context on what we mean by “flyer”, refer to the section on Asynchronous Acquisition.

The interface of a “flyable” device is separate from the interface of a readable or settable device, though there is some overlap.

class bluesky.protocols.Flyable(*args, **kwargs)[source]

Bases: HasName, Protocol

abstract complete() Status[source]

Return a Status and mark it done when acquisition has completed.

abstract kickoff() Status[source]

Begin acculumating data.

Return a Status and mark it done when acqusition has begun.

The yielded values from collect() are partial Event dictionaries:

class bluesky.protocols.PartialEvent() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list.  For example:  dict(one=1, two=2)[source]

If any of the data keys are in external assets rather than including the data, a filled key should be present:

Flyable devices can also implement Configurable if they have configuration that only needs to be read once at the start of scan

Optional Interfaces

These are additional interfaces for providing optional behavior to Readable, Movable, and Flyable devices.

The methods described here are either hooks for various plans/RunEngine messages which are ignored if not present or required by only a subset of RunEngine messages. In the latter case, the RunEngine may error if it tries to use a device which does not define the required method.

class bluesky.protocols.Stageable(*args, **kwargs)[source]

Bases: Protocol

abstract stage() Status | List[Any][source]

An optional hook for “setting up” the device for acquisition.

It should return a Status that is marked done when the device is done staging.

abstract unstage() Status | List[Any][source]

A hook for “cleaning up” the device after acquisition.

It should return a Status that is marked done when the device is finished unstaging.

class bluesky.protocols.Subscribable(*args, **kwargs)[source]

Bases: HasName, Protocol

abstract clear_sub(function: Callable[[Dict[str, Reading]], None]) None[source]

Remove a subscription.

abstract subscribe(function: Callable[[Dict[str, Reading]], None]) None[source]

Subscribe to updates in value of a device.

When the device has a new value ready, it should call function with something that looks like the output of read().

Needed for monitored.

class bluesky.protocols.Pausable(*args, **kwargs)[source]

Bases: Protocol

abstract pause() None | Awaitable[None][source]

Perform device-specific work when the RunEngine pauses.

This can be a standard function or an async function.

abstract resume() None | Awaitable[None][source]

Perform device-specific work when the RunEngine resumes after a pause.

This can be a standard function or an async function.

class bluesky.protocols.Stoppable(*args, **kwargs)[source]

Bases: Protocol

abstract stop(success=True) None | Awaitable[None][source]

Safely stop a device that may or may not be in motion.

The argument success is a boolean. When success is true, bluesky is stopping the device as planned and the device should stop “normally”. When success is false, something has gone wrong and the device may wish to take defensive action to make itself safe.

This can be a standard function or an async function.

class bluesky.protocols.Checkable(*args, **kwargs)[source]

Bases: Protocol

abstract check_value(value: Any) None | Awaitable[None][source]

Test for a valid setpoint without actually moving.

This should accept the same arguments as set. It should raise an Exception if the argument represent an illegal setting — e.g. a position that would move a motor outside its limits or a temperature controller outside of its settable range.

This method is used by simulators that check limits. If not implemented those simulators should assume all values are valid, but may warn.

This method may be used during a scan, so should not write to any Signals

This can be a standard function or an async function.

class bluesky.protocols.HasHints(*args, **kwargs)[source]

Bases: HasName, Protocol

abstract property hints: Hints

A dictionary of suggestions for best-effort visualization and processing.

This does not affect what data is read or saved; it is only a suggestion to enable automated tools to provide helpful information with minimal guidance from the user. See Hints.

class bluesky.protocols.Hints[source]

A dictionary of optional hints for visualization

dimensions: List[Tuple[List[str], str]]

Partition fields (and their stream name) into dimensions for plotting

'dimensions': [(fields, stream_name), (fields, stream_name), ...]

fields: List[str]

A list of the interesting fields to plot

gridding: Literal['rectilinear', 'rectilinear_nonsequential']

Include this if scan data is sampled on a regular rectangular grid

class bluesky.protocols.Preparable(*args, **kwargs)[source]

Bases: Protocol

abstract prepare(value) Status[source]

Prepare a device for scanning.

This method provides similar functionality to Stageable.stage and Movable.set, with key differences:

Stageable.stage

Staging a device translates to, “I’m going to use this in a scan, but I’m not sure how”. Preparing it translates to, “I’m about to do a step or a fly scan with these parameters”. Staging should be universal across many different types of scans, however prepare is specific to an input value passed in.

Movable.set

For some devices, preparation for a scan could involve multiple soft or hardware signals being configured and/or set. prepare therefore allows these to be bundled together, along with other logic.

For example, a Flyable device should have the following methods called on it to perform a fly-scan:

prepare(flyscan_params) kickoff() complete()

If the device is a detector, collect_asset_docs can be called repeatedly while complete is not done to publish frames. Alternatively, to step-scan a detector,

prepare(frame_params) to setup N software triggered frames trigger() to take N frames collect_asset_docs() to publish N frames

Returns a Status that is marked done when the device is ready for a scan.

Checking if an object supports an interface

You can check at runtime if an object supports an interface with isinstance:

from bluesky.protocols import Readable

assert isinstance(obj, Readable)
obj.read()

This will check that the correct methods exist on the object, and are callable, but will not check any types.

There is also a helper function for this:

bluesky.protocols.check_supports(obj, protocol: Type[T]) T[source]

Check that an object supports a protocol

This exists so that multiple protocol checks can be run in a mypy compatible way, e.g.:

triggerable = check_supports(obj, Triggerable)
triggerable.trigger()
readable = check_supports(obj, Readable)
readable.read()