6. Procedural Device Definitions#

Date: 2023-09-11




Ophyd creates devices in a declarative way:

class Sensor(Device):
    mode = Component(EpicsSignal, "Mode", kind="config")
    value = Component(EpicsSignalRO, "Value", kind="hinted")

This means when you make device = OldSensor(pv_prefix) then some metaclass magic will call EpicsSignal(pv_prefix + "Mode", kind="config") and make it available as device.mode.

ophyd-async could convert this approach to use type hints instead of metaclasses:

from typing import Annotated as A

class Sensor(EpicsDevice):
    mode: A[SignalRW, CONFIG, pv_suffix("Mode")]
    value: A[SignalR, READ, pv_suffix("Value")]

The superclass init could then read all the type hints and instantiate them with the correct SignalBackends.

Alternatively it could use a procedural approach and be explicit about where the arguments are passed at the cost of greater verbosity:

class Sensor(StandardReadable):
    def __init__(self, prefix: str, name="") -> None:
        self.value = epics_signal_r(float, prefix + "Value")
        self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
        # Set name and signals for read() and read_configuration()
        self.set_readable_signals(read=[self.value], config=[self.mode])

The procedural approach to creating child Devices is:

class SensorGroup(Device):
    def __init__(self, prefix: str, num: int, name: Optional[str]=None):
        self.sensors = DeviceVector(
            {i: Sensor(f"{prefix}:CHAN{i}" for i in range(1, num+1))}

We have not been able to come up with a declarative approach that can describe the SensorGroup example in a succinct way.


Type safety and readability are regarded above velocity, and magic should be minimized. With this in mind we will stick with the procedural approach for now. We may find a less verbose way of doing set_readable_signals by using a context manager and overriding setattr in the future:

with self.signals_added_to(READ):
    self.value = epics_signal_r(float, prefix + "Value")
with self.signals_added_to(CONFIG):
    self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")

If someone comes up with a way to write SensorGroup in a declarative and readable way then we may revisit this.


Ophyd and ophyd-async Devices will look less alike, but ophyd-async should be learnable for beginners.