6. Procedural Device Definitions#

Date: 2023-09-11

Status#

Accepted

Context#

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])
        super().__init__(name=name)

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))}
        )
        super().__init__(name=name)

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

Decision#

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.

Consequences#

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