Declarative vs Procedural Devices#
Ophyd async has two styles of creating Devices, Declarative and Procedural. This article describes why there are two mechanisms for building Devices, and looks at the pros and cons of each style.
Procedural style#
The procedural style mirrors how you would create a traditional python class, you define an __init__
method, add some class members, then call the superclass __init__
method. In the case of ophyd async those class members are likely to be Signals and other Devices. For example, in the ophyd_async.sim.SimMotor
we create its soft signal children in an __init__
method:
"""For usage when simulating a motor."""
def __init__(self, name="", instant=True) -> None:
"""Simulate a motor, with optional velocity.
:param name: name of device
:param instant: whether to move instantly or calculate move time using velocity
"""
# Define some signals
with self.add_children_as_readables(Format.HINTED_SIGNAL):
self.user_readback, self._user_readback_set = soft_signal_r_and_setter(
float, 0
)
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
self.velocity = soft_signal_rw(float, 0 if instant else 1.0)
self.acceleration_time = soft_signal_rw(float, 0.5)
self.units = soft_signal_rw(str, "mm")
self.user_setpoint = soft_signal_rw(float, 0)
# Whether set() should complete successfully or not
self._set_success = True
self._move_status: AsyncStatus | None = None
# Stored in prepare
self._fly_info: FlySimMotorInfo | None = None
# Set on kickoff(), complete when motor reaches end position
self._fly_status: WatchableAsyncStatus | None = None
super().__init__(name=name)
It is explicit and obvious, but verbose. It also allows you to embed arbitrary python logic in the creation of signals, so is required for making soft signals and DeviceVectors with contents based on an argument passed to __init__
. It also allows you to use the StandardReadable.add_children_as_readables
context manager which can save some typing.
Declarative style#
The declarative style mirrors how you would create a pydantic BaseModel
. You create type hints to tell the base class what type of object you create, add annotations to tell it some parameters on how to create it, then the base class __init__
will introspect and create them. For example, in the ophyd_async.fastcs.panda.PulseBlock
we define the members we expect, and the baseclass will introspect the selected FastCS transport (EPICS IOC or Tango Device Server) and connect them, adding any extras that are published:
class PulseBlock(Device):
"""Used for configuring pulses in the PandA."""
delay: SignalRW[float]
width: SignalRW[float]
For a traditional EPICS IOC there is no such introspection mechanism, so we require a PV Suffix to be supplied via an annotation. For example, in ophyd_async.epics.demo.DemoPointDetectorChannel
we describe the PV Suffix and whether the signal appears in read()
or read_configuration()
using typing.Annotated
:
class DemoPointDetectorChannel(StandardReadable, EpicsDevice):
"""A channel for `DemoPointDetector` with int value based on X and Y Motors."""
value: A[SignalR[int], PvSuffix("Value"), Format.HINTED_UNCACHED_SIGNAL]
mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
It is compact and has the minimum amount of boilerplate, but is limited in its scope to what sorts of Signals and Devices the base class can create. It also requires the usage of a StandardReadableFormat
for each Signal if using StandardReadable
which may be more verbose than the procedural approach. It is best suited for introspectable FastCS and Tango devices, and repetitive EPICS Devices that are wrapped into larger Devices like areaDetectors.
Grey area#
There is quite a large segment of Devices that could be written both ways, for instance ophyd_async.epics.demo.DemoMotor
. This could be written in either style with roughly the same legibility, so is a matter of taste:
"""A demo movable that moves based on velocity."""
# Whether set() should complete successfully or not
_set_success = True
# Define some signals
readback: A[SignalR[float], PvSuffix("Readback"), Format.HINTED_SIGNAL]
velocity: A[SignalRW[float], PvSuffix("Velocity"), Format.CONFIG_SIGNAL]
units: A[SignalR[str], PvSuffix("Readback.EGU"), Format.CONFIG_SIGNAL]
setpoint: A[SignalRW[float], PvSuffix("Setpoint")]
precision: A[SignalR[int], PvSuffix("Readback.PREC")]
# If a signal name clashes with a bluesky verb add _ to the attribute name
stop_: A[SignalX, PvSuffix("Stop.PROC")]
Conclusion#
Ophyd async supports both the declarative and procedural style, and is not prescriptive about which is used. In the end the decision is likely to come down to personal taste, and the style of the surrounding code.