Device Standards#

Ophyd vs Ophyd-async#

Some devices have been written in ophyd for historic reasons. However, all new devices should be written in ophyd-async and any old ophyd devices undergoing a large re-write should be considered for conversion to ophyd-async.

Where to put devices#

Dodal is written with the philosophy that Ophyd devices should be assumed to be as generic as possible. I.e. you should think about where to place them in the following order:

  1. A device that could be used at any facility, e.g. a generic EpicsMotor or a commercial product with a standard IOC, should go in bluesky/ophyd-epics-devices

  2. A device that may be on any beamline should go in the top level of the devices folder. If it is a quite complex device (e.g. multiple files) it should have a folder of its own e.g. oav

  3. A device that is very specific to a particular beamline should go in the devices/ixx folder

This is in an effort to avoid duplication across facilities/beamlines.

Device Best Practices#

Ophyd-async directory contains a flowchart for a simplified decision tree about what interfaces should a given device implement. In addition to this the following guidelines are strongly recommended:

  1. Devices should contain only the PV suffixes that are generic for any instance of the device. See PV Suffixes

  2. Anything in a device that is expected to be set externally should be a signal. See Use of signals

  3. Devices should not hold state, when they are read they should read the hardware. See Holding State

PV Suffixes#

In general devices should contain only the PV suffixes that are generic for any instance of the device e.g.

class MyDevice(Device):
    def __init__(self, name: str, prefix: str)
        self.bragg = Motor(prefix + "BRAGG")
        super().__init__(name)

device_instantiation(MyDevice, "dcm", "-MO-DCM-01:")

is preferred over

class MyDevice(Device):
    def __init__(self, name: str, prefix: str)
        self.bragg = Motor(prefix + "-MO-DCM-01:BRAGG")
        super().__init__(name)

device_instantiation(MyDevice, "dcm", "")

This is so that a new device on say -MO-DCM-02 can be easily created.

Beamline Prefix#

Most devices have a beamline-specific prefix such as BL03I however some devices do not. In such cases when calling device_instantiation() you can specify bl_prefix=False to ensure that the beamline prefix is not automatically prepended.

Use of signals#

Anything in a device that is expected to be set externally e.g. by a plan should be a signal, even if it does not connect to EPICS. If it does not connect to EPICS it should be a soft signal.

Whilst it would be possible to do:

class MyDevice(Device):
    def __init__(self):
         self.param = "blah"

my_device = MyDevice()
def my_plan():
    my_device.param = "new_value"

this has potential negative side effects:

  • When the plan is simulated it will still set the parameter on the device

  • There may be external things attached to the RE that are tracking messages e.g. metrics. A set like this would be lost

Instead you should make a soft signal:

class MyDevice(Device):
    def __init__(self):
         self.param = soft_signal_rw(str)

my_device = MyDevice()
def my_plan():
    yield from bps.mv(my_device.param, "new_value")

Holding State#

Devices should avoid holding state as much as possible. Ophyd devices are mostly trying to reflect the state of hardware and so when the device is read that hardware should be read.

If the device holds the state itself it is likely to not reflect the real hardware if: * The device has just been initialised * The hardware has changed independently e.g. via EPICS directly * The hardware has failed to do what the device expected

For example, if I have a device that I would like to treat as moving in/out based on an underlying axis then it would be incorrect to implement it like this:

class InOut(Enum):
    IN = 0
    OUT = 0

class MyDevice(Device):
    def __init__(self):
        self.underlying_motor = Motor("MOTOR")
        with self.add_children_as_readables():
            self.in_out, self._in_out_setter = soft_signal_r_and_setter(InOut)


    @AsyncStatus.wrap
    async def set(self, value: InOut):
        if value == InOut.IN:
            await self.underlying_motor.set(100)
        else:
            await self.underlying_motor.set(0)
        self._in_out_setter(value)

While this may appear to work fine during normal operation the state of in_out is only ever updated if the ophyd device is set. It is incorrect to assume that underlying_motor only changes based on this and so this has the issues listed above. Instead you should make sure to update in_out whenever the device is read e.g.

class InOut(Enum):
    IN = 0
    OUT = 0

class MyDevice(Device):
    def __init__(self):
        self.underlying_motor = Motor("MOTOR")
        with self.add_children_as_readables():
            self.in_out = create_hardware_backed_soft_signal(InOut, self._get_in_out_from_hardware)

    async def _get_in_out_from_hardware(self):
        current_position = await self.underlying_motor.get_value()
        if isclose(current_position, 0):
            return InOut.IN
        elif isclose(current_position, 100):
            return InOut.OUT
        else:
            raise ValueError()


    @AsyncStatus.wrap
    async def set(self, value: InOut):
        if value == InOut.IN:
            await self.underlying_motor.set(100)
        else:
            await self.underlying_motor.set(0)

This will be simplified by bluesky/ophyd-async#525