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 sim 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
PvSuffixin anophyd_async.epics.core.EpicsDevicebaseclassDo the
Format.HINTED_SIGNALandFormat.CONFIG_SIGNALflags in annotations forStandardReadableEnsure 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]
Note that to use Annotated as A with ruff name checking we recommend adding something like:
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
# We often shorten "Annotated" to "A" for brevity
"typing.Annotated" = "A"
to your pyproject.toml.