9. Procedural vs Declarative Devices#
Date: 01/10/24
Status#
Accepted
Context#
In 6. Procedural Device Definitions we decided we preferred the procedural approach to devices, because of the issue of applying structure like DeviceVector
. Since then we have FastCS
and Tango
support which use a declarative approach. We need to decide whether we are happy with this situation, or whether we should go all in one way or the other. A suitable test Device would be:
class EpicsProceduralDevice(StandardReadable):
def __init__(self, prefix: str, num_values: int, name="") -> None:
with self.add_children_as_readables():
self.value = DeviceVector(
{
i: epics_signal_r(float, f"{prefix}Value{i}")
for i in range(1, num_values + 1)
}
)
with self.add_children_as_readables(ConfigSignal):
self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
super().__init__(name=name)
and a Tango/FastCS procedural equivalent would be (if we add support to StandardReadable for Format.HINTED_SIGNAL and Format.CONFIG_SIGNAL annotations):
class TangoDeclarativeDevice(StandardReadable, TangoDevice):
value: Annotated[DeviceVector[SignalR[float]], Format.HINTED_SIGNAL]
mode: Annotated[SignalRW[EnergyMode], Format.CONFIG_SIGNAL]
But we could specify the Tango one procedurally (with some slight ugliness around the DeviceVector):
class TangoProceduralDevice(StandardReadable):
def __init__(self, prefix: str, name="") -> None:
with self.add_children_as_readables():
self.value = DeviceVector({0: tango_signal_r(float)})
with self.add_children_as_readables(ConfigSignal):
self.mode = tango_signal_rw(EnergyMode)
super().__init__(name=name, connector=TangoConnector(prefix))
or the EPICS one could be declarative:
class EpicsDeclarativeDevice(StandardReadable, EpicsDevice):
value: Annotated[
DeviceVector[SignalR[float]], Format.HINTED_SIGNAL, EpicsSuffix("Value%d", "num_values")
]
mode: Annotated[SignalRW[EnergyMode], Format.CONFIG_SIGNAL, EpicsSuffix("Mode")]
Which do we prefer?
Decision#
We decided that the declarative approach is to be preferred until we need to write formatted strings. At that point we should drop to an __init__
method and a for loop. This is not a step towards only supporting the declarative approach and there are no plans to drop the procedural approach.
The two approaches now look like:
class Sensor(StandardReadable, EpicsDevice):
"""A demo sensor that produces a scalar value based on X and Y Movers"""
value: A[SignalR[float], PvSuffix("Value"), Format.HINTED_SIGNAL]
mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
class SensorGroup(StandardReadable):
def __init__(self, prefix: str, name: str = "", sensor_count: int = 3) -> None:
with self.add_children_as_readables():
self.sensors = DeviceVector(
{i: Sensor(f"{prefix}{i}:") for i in range(1, sensor_count + 1)}
)
super().__init__(name)
Consequences#
We need to:
Add support for reading annotations and
PvSuffix
in anophyd_async.epics.core.EpicsDevice
baseclassDo the
Format.HINTED_SIGNAL
andFormat.CONFIG_SIGNAL
flags in annotations forStandardReadable
Ensure we can always drop to
__init__
pvi structure changes#
Structure read from .value
now includes DeviceVector
support. Requires at least PandABlocks-ioc 0.11.2
Epics signal
module moves#
ophyd_async.epics.signal
moves to ophyd_async.epics.core
with a backwards compat module that emits deprecation warning.
# old
from ophyd_async.epics.signal import epics_signal_rw
# new
from ophyd_async.epics.core import epics_signal_rw
StandardReadable
wrappers change to StandardReadableFormat
#
StandardReadable
wrappers change to enum members of StandardReadableFormat
(normally imported as Format
)
# old
from ophyd_async.core import ConfigSignal, HintedSignal
class MyDevice(StandardReadable):
def __init__(self):
self.add_readables([sig1], ConfigSignal)
self.add_readables([sig2], HintedSignal)
self.add_readables([sig3], HintedSignal.uncached)
# new
from ophyd_async.core import StandardReadableFormat as Format
class MyDevice(StandardReadable):
def __init__(self):
self.add_readables([sig1], Format.CONFIG_SIGNAL)
self.add_readables([sig2], Format.HINTED_SIGNAL)
self.add_readables([sig3], Format.HINTED_UNCACHED_SIGNAL
Declarative Devices are now available#
# old
from ophyd_async.core import ConfigSignal, HintedSignal
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
class Sensor(StandardReadable):
def __init__(self, prefix: str, name="") -> None:
with self.add_children_as_readables(HintedSignal):
self.value = epics_signal_r(float, prefix + "Value")
with self.add_children_as_readables(ConfigSignal):
self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
super().__init__(name=name)
# new
from typing import Annotated as A
from ophyd_async.core import StandardReadableFormat as Format
from ophyd_async.epics.core import EpicsDevice, PvSuffix, epics_signal_r, epics_signal_rw
class Sensor(StandardReadable, EpicsDevice):
value: A[SignalR[float], PvSuffix("Value"), Format.HINTED_SIGNAL]
mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]