Skip to content

Commit

Permalink
Declarative EPICS and StandardReadable Devices (#598)
Browse files Browse the repository at this point in the history
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
coretl authored Nov 4, 2024
1 parent ca56f74 commit 32afe3d
Show file tree
Hide file tree
Showing 56 changed files with 968 additions and 579 deletions.
2 changes: 1 addition & 1 deletion docs/examples/foo_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
StandardDetector,
)
from ophyd_async.epics import adcore
from ophyd_async.epics.signal import epics_signal_rw_rbv
from ophyd_async.epics.core import epics_signal_rw_rbv


class FooDriver(adcore.ADBaseIO):
Expand Down
140 changes: 140 additions & 0 deletions docs/explanations/decisions/0009-procedural-vs-declarative-devices.md
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]
```
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ reportMissingImports = false # Ignore missing stubs in imported modules
# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
addopts = """
--tb=native -vv --strict-markers --doctest-modules
--doctest-glob="*.rst" --doctest-glob="*.md" --ignore=docs/examples
--doctest-glob="*.rst" --doctest-glob="*.md"
--ignore=docs/examples --ignore=src/ophyd_async/epics/signal.py
"""
# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings
filterwarnings = "error"
Expand Down
8 changes: 7 additions & 1 deletion src/ophyd_async/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@
UUIDFilenameProvider,
YMDPathProvider,
)
from ._readable import ConfigSignal, HintedSignal, StandardReadable
from ._readable import (
ConfigSignal,
HintedSignal,
StandardReadable,
StandardReadableFormat,
)
from ._signal import (
Signal,
SignalR,
Expand Down Expand Up @@ -141,6 +146,7 @@
"ConfigSignal",
"HintedSignal",
"StandardReadable",
"StandardReadableFormat",
"Signal",
"SignalR",
"SignalRW",
Expand Down
1 change: 1 addition & 0 deletions src/ophyd_async/core/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def __init__(
self, name: str = "", connector: DeviceConnector | None = None
) -> None:
self._connector = connector or DeviceConnector()
self._connector.create_children_from_annotations(self)
self.set_name(name)

@property
Expand Down
Loading

0 comments on commit 32afe3d

Please sign in to comment.