Device and Component

Usage

The core class of ophyd is Device which encodes the nodes of the hierarchical structure of the device and provides much of core API.

Device([prefix, kind, read_attrs, …]) Base class for device objects

The base Device is not particularly useful on it’s own, it must be sub-classed to provide it with components to do something with.

Creating a custom device is as simple as

from ophyd import Device, EpicsMotor
from ophyd import Component as Cpt

class StageXY(Device):
    x = Cpt(EpicsMotor, ':X')
    y = Cpt(EpicsMotor, ':Y')

stage = StageXY('STAGE_PV', name='stage')

You can then use stage as an input to any plan as a detector and stage.x and stage.y as independent motors.

A Robot

A slightly more complex example is to control a simple sample loading robot.

from ophyd import Device, EpicsSignal, EpicsSignalRO
from ophyd import Component as Cpt
from ophyd.utils import set_and_wait

class Robot(Device):
    sample_number = Cpt(EpicsSignal, 'ID:Tgt-SP')
    load_cmd = Cpt(EpicsSignal, 'Cmd:Load-Cmd.PROC')
    unload_cmd = Cpt(EpicsSignal, 'Cmd:Unload-Cmd.PROC')
    execute_cmd = Cpt(EpicsSignal, 'Cmd:Exec-Cmd')

    status = Cpt(EpicsSignalRO, 'Sts-Sts')

my_robot = Robot('PV_PREFIX:', name='my_robot',
                 read_attrs=['sample_number', 'status'])

Which creates an instance my_robot with 5 children

python attribute PV name in read()
my_robot.sample_number ‘PV_PREFIX:ID:Tgt-SP’ Y
my_robot.load_cmd ‘PV_PREFIX:CMD:Load-Cmd.PROC’ N
my_robot.unload_cmd ‘PV_PREFIX:CMD:Unload-Cmd.PROC’ N
my_robot.execute_cmd ‘PV_PREFIX:CMD:Exec-Cmd’ N
my_robot.status ‘PV_PREFIX:Sts-Sts’ Y

only 2 of which will be included when reading from the robot.

You could now use this device in a scan like

import bluesky.plans as bp

def load_sample(robot, sample):
    yield from bp.mv(robot.sample_number, sample)
    yield from bp.mv(robot.load_cmd, 1)
    yield from bp.mv(robot.execute_cmd, 1)

def unload_sample(robot):
    yield from bp.mv(robot.unload_cmd, 1)
    yield from bp.mv(robot.execute_cmd, 1)

def robot_plan(list_of_samples):
    for sample in list_of_samples:
        # load the sample
        yield from load_sample(my_robot, sample)
        # take a measurement
        yield from bp.count([det], md={'sample': sample})
        # unload the sample
        yield from unload_sample(my_robot)

and from the command line

RE(robot_plan([1, 2. 6]))

These classes were co-developed with bluesky and are the reference implementation of a hardware abstraction layer for bluesky. However, these are closely tied to EPICS and make some assumptions about the PV naming based on NSLS-II’s naming scheme. Despite attempting generality, it is likely that as ophyd and bluesky are used at other facilities (and when ophyd is adapted for a different control system) we will discover some latent NSLS-II-isms that should be corrected (or at least acknowledged and documented).

Device

Device adds a number of additional attributes beyond the required bluesky API and what is inherited from OphydObj for run-time configuration

Attribute Description
read_attrs Names of components for read() See Trigger, Read and Describe
configuration_attrs Names of components for read_configuration(). See Configuration and Friends
stage_sigs Signals to be set during Stage and Unstage
hints Names of components as suggestions for handling in bluesky callbacks.

and static information about the object

Attribute Description
prefix ‘base’ of PV name, used when building components
component_names List of the names components on this device. Direct children only
trigger_signals Signals for use in Implicit Triggering (provisional)

Device also has two class-level attributes to control the default contents of read_attrs and configuration_attrs.

Attribute Description
_default_read_attrs

The default contents of read_attrs if a subset of all available children.

An iterable or None. If None defaults to all children

A tuple is recommended.

_default_configuration_attrs

The default contents of configuration_attrs

An iterable or None. If None defaults to []

A tuple is recommended.

hints

The hints attribute is a dictionary that provides information about an ophyd device to bluesky callbacks that advise how that device should be handled by the callback. While it could be used for many purposes, its first use has been to direct the selection of the relevant axes and signals to use when plotting data from an event stream.

There are two different locations where the hints dictionary is created.

1. During the specification of an ophyd Device 1. Configuration of the start document by a bluesky plan

The hints dictionary has well-known keys.

Key Description of value
fields signal names to be used for a plot as dependent axes
dimensions signal names to be used for a plot as independent axes
gridding advises when to prefer LiveGrid instead of LivePlot

The hints dictionary may also have custom keys used by the custom support.

  • example using the ad hoc vis key in the creation of an ophyd detector Device:

    self.hints = {'vis': 'placeholder'}
    
  • then look for this key in the custom bluesky callback:

    assert doc['hints']['det'] == {'vis': 'placeholder'}
    

hints["fields"]

fields is a list of ophyd object name(s) to be used as dependent axes for visualization callbacks. The object name(s) must appear in the dictionary returned by the device’s read() method.

Examples:

quadem.hints == {'fields': ['quadem_current1_mean_value']}
sca.hints == {'fields': [sca.channels.name]}

To ensure internal consistency, the hints attribute of any Signal or Device cannot be set directly. [1] Instead of:

camera.hints = {'fields': [camera.stats1.total.name,
                                                       camera.stats2.total.name]}

use the kind kind attribute.

from ophyd import Kind

camera.stats1.total.kind = Kind.hinted
camera.stats2.total.kind = Kind.hinted

or, as a convenient shortcut

camera.stats1.total.kind = 'hinted'
camera.stats2.total.kind = 'hinted'
[1]starting with ophyd v1.2.0

hints["dimensions"]

Defines the ophyd signal names to be used as independent axes for visualization. The syntax is (list of field names, stream name) where the list of field names is as above and the stream name is usually primary. All the signals must be available in the named stream.

hints["dimensions"] is used by a bluesky plan to prepare a dimensions attribute that is placed in the start document. It is this dimensions attribute that identifies the independent axes for visualization callbacks. The plan can use or override what it finds in hints["dimensions"].

Examples:

dimensions = [(motor.hints['fields'], 'primary')]
dimensions = [(('time'), 'primary')]

For now, bluesky can only handle when all the dimensions belong to the same stream. To generalize, we would need to resample and we are not going to handle that yet.

hints["gridding"]

This key is used for mesh and grid scans. When present, it can take these values: rectilinear or rectilinear_nonsequential.

In the Best Effort Callback from bluesky, if hints["gridding"] exists and is "rectilinear", then use LiveGrid, otherwise use LivePlot.

Component

The Component class is a python descriptor which overrides the behavior on attribute access. This allows us to use a declarative style to define the software representation of the hardware. The best way to understand is through an example:

class Foo(Device):
    bar = Component(EpicsSignal, ':bar', string=True)

which means “When a Foo instance is created give it a bar attribute which is an instance of EpicsSignal and use the extra args and kwargs when creating it”. It is a declaration of what you want and it is the responsibility of ophyd to make it happen.

There are three classes

Component(cls[, suffix, lazy, …]) A descriptor representing a device component (or signal)
FormattedComponent(cls[, suffix, lazy, …]) A Component which takes a dynamic format string
DynamicDeviceComponent(defn, *[, clsname, …]) An Device component that dynamically creates an ophyd Device

Trigger, Read and Describe

The trigger() method is responsible for implementing ‘trigger’ or ‘acquire’ functionality of the Device.

The read() method is responsible for for returning the data from the Device.

The describe() method is responsible for providing schema and meta-data for the read() method.

Configuration and Friends

Stage and Unstage

When a Device d is used in scan, it is “staged” and “unstaged.” Think of this as “setup” and “cleanup”. That is, before a device is triggered, read, or moved, the scan is expected to call d.stage(). And, at the end of scan, d.unstage() is called. (Whenever possible, unstaging is performed even if the scan is aborted or fails due to an error.)

The staging process is a “hook” for preparing a device for use. To add custom staging logic to a Device, subclass it and override stage and/or unstage like so.

class MyMotor(EpicsMotor):

    def stage(self):
        print('I am staging.')
        super().stage()

    def unstage(self):
        print('I am unstaging.')
        super().unstage()

It is crucial to call super(), as above, so that any built-in staging behavior is not overridden.

A common use for staging is to set certain signals to certain values for a scan and then set them back at the end. For example, a detector device might turn on “capture mode” at the beginning of the scan and then flip it back off (or back to its original setting, whatever that was) at the end. For this, ophyd provides a convenience, stage_sigs — a dictionary mapping signals to desired values. The device reads the initial values of these signals, stashes them, changes them to the desired value, and then restore the initial value when the device is unstaged. It is best to customize stage_sigs in the device’s __init__ method, like so:

class MyMotor(EpicsMotor):
    def __init__(*args, **kwargs):
        super().__init__(*args, **kwargs)
        self.stage_sigs[self.user_offset] = 5

When a MyMotor device is staged, its user_offset value will be set to 5. When it is unstaged, it will be set back to whatever value it had right before it was staged.

Implicit Triggering

Count Time

Low level API

Device.connected
Device.wait_for_connection Wait for signals to connect
Device.get_instantiated_signals Yields all of the instantiated signals in a device hierarchy
Device.get Get the value of all components in the device
Device.put Put a value to all components of the device
Device.get_device_tuple The device tuple type associated with an Device class