-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Declarative EPICS and StandardReadable Devices (#598)
Add optional Declarative Device support Includes: - An ADR for optional Declarative Devices - Support for `StandardReadable` Declarative Devices via `StandardReadableFormat` annotations - Support for EPICS Declarative Devices via an `EpicsDevice` baseclass and `PvSuffic` annotations - Updates to the EPICS and Tango demo devices to use them # Changes ## 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. ```python # 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`) ```python # 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 ```python # 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] ```
- Loading branch information
Showing
56 changed files
with
968 additions
and
579 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
docs/explanations/decisions/0009-procedural-vs-declarative-devices.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
# 9. Procedural vs Declarative Devices | ||
|
||
Date: 01/10/24 | ||
|
||
## Status | ||
|
||
Accepted | ||
|
||
## Context | ||
|
||
In [](./0006-procedural-device-definitions.rst) 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: | ||
|
||
```python | ||
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): | ||
```python | ||
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): | ||
```python | ||
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: | ||
```python | ||
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: | ||
|
||
```python | ||
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 an `ophyd_async.epics.core.EpicsDevice` baseclass | ||
- Do the `Format.HINTED_SIGNAL` and `Format.CONFIG_SIGNAL` flags in annotations for `StandardReadable` | ||
- 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. | ||
```python | ||
# 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`) | ||
```python | ||
# 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 | ||
```python | ||
# 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] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.