Skip to content

Commit

Permalink
Document How to Group Devices
Browse files Browse the repository at this point in the history
Move initial how-to on assembling devices into its own page and add a
section about using DeviceVector to make arbitrary-length groups. Add an
example to the epics demo with tests.
  • Loading branch information
callumforrester committed Apr 9, 2024
1 parent fbcc792 commit b0e593b
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 32 deletions.
44 changes: 44 additions & 0 deletions docs/user/how-to/compound-devices.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.. note::

Ophyd async is included on a provisional basis until the v1.0 release and
may change API on minor release numbers before then

Compound Devices Together
=========================

Assembly
--------

Compound assemblies can be used to group Devices into larger logical Devices:

.. literalinclude:: ../../../src/ophyd_async/epics/demo/__init__.py
:pyobject: SampleStage

This applies prefixes on construction:

- SampleStage is passed a prefix like ``DEVICE:``
- SampleStage.x will append its prefix ``X:`` to get ``DEVICE:X:``
- SampleStage.x.velocity will append its suffix ``Velocity`` to get
``DEVICE:X:Velocity``

If SampleStage is further nested in another Device another layer of prefix nesting would occur

.. note::

SampleStage does not pass any signals into its superclass init. This means
that its ``read()`` method will return an empty dictionary. This means you
can ``rd sample_stage.x``, but not ``rd sample_stage``.


Grouping by Index
-----------------

Sometimes, it makes sense to group devices by number, say an array of sensors:

.. literalinclude:: ../../../src/ophyd_async/epics/demo/__init__.py
:pyobject: SensorGroup

:class:`~ophyd-async.core.DeviceVector` allows writing maintainable, arbitrary-length device groups instead of fixed classes for each possible grouping. A :class:`~ophyd-async.core.DeviceVector` can be accessed via indices, for example: ``my_sensor_group.sensors[2]``. Here ``sensors`` is a dictionary with integer indices rather than a list so that the most semantically sensible indices may be used, the sensor group above may be 1-indexed, for example, because the sensors' datasheet calls them "sensor 1", "sensor 2" etc.

.. note::
The :class:`~ophyd-async.core.DeviceVector` adds an extra level of nesting to the device tree compared to static components like ``sensor_1``, ``sensor_2`` etc. so the behavior is not completely equivalent.
24 changes: 0 additions & 24 deletions docs/user/how-to/make-a-simple-device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,3 @@ completes. This co-routine is wrapped in a timeout handler, and passed to an
`AsyncStatus` which will start executing it as soon as the Run Engine adds a
callback to it. The ``stop()`` method then pokes a PV if the move needs to be
interrupted.

Assembly
--------

Compound assemblies can be used to group Devices into larger logical Devices:

.. literalinclude:: ../../../src/ophyd_async/epics/demo/__init__.py
:pyobject: SampleStage

This applies prefixes on construction:

- SampleStage is passed a prefix like ``DEVICE:``
- SampleStage.x will append its prefix ``X:`` to get ``DEVICE:X:``
- SampleStage.x.velocity will append its suffix ``Velocity`` to get
``DEVICE:X:Velocity``

If SampleStage is further nested in another Device another layer of prefix
nesting would occur

.. note::

SampleStage does not pass any signals into its superclass init. This means
that its ``read()`` method will return an empty dictionary. This means you
can ``rd sample_stage.x``, but not ``rd sample_stage``.
1 change: 1 addition & 0 deletions docs/user/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ side-bar.
:maxdepth: 1

how-to/make-a-simple-device
how-to/compound-devices
how-to/write-tests-for-devices
how-to/run-container

Expand Down
5 changes: 2 additions & 3 deletions src/ophyd_async/core/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,8 @@ class DeviceVector(Dict[int, VT], Device):
Defines device components with indices.
In the below example, foos becomes a dictionary on the parent device
at runtime, so parent.foos[2] returns a FooDevice.
Example Usage:
self.foos = DeviceVector({i: FooDevice("THING:" + i) for i in range(5)})
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
:class:`~ophyd_async.epics.demo.DynamicSensorGroup`
"""

def children(self) -> Generator[Tuple[str, Device], None, None]:
Expand Down
21 changes: 20 additions & 1 deletion src/ophyd_async/epics/demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
import numpy as np
from bluesky.protocols import Movable, Stoppable

from ophyd_async.core import AsyncStatus, Device, StandardReadable, observe_value
from ophyd_async.core import (
AsyncStatus,
Device,
DeviceVector,
StandardReadable,
observe_value,
)

from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x

Expand Down Expand Up @@ -43,6 +49,19 @@ def __init__(self, prefix: str, name="") -> None:
super().__init__(name=name)


class SensorGroup(StandardReadable):
def __init__(self, prefix: str, name: str = "", sensor_count: int = 3) -> None:
self.sensors = DeviceVector(
{i: Sensor(f"{prefix}{i}:") for i in range(1, sensor_count + 1)}
)

# Makes read() produce the values of all sensors
self.set_readable_signals(
read=[sensor.value for sensor in self.sensors.values()],
)
super().__init__(name)


class Mover(StandardReadable, Movable, Stoppable):
"""A demo movable that moves based on velocity"""

Expand Down
74 changes: 70 additions & 4 deletions tests/epics/demo/test_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@


@pytest.fixture
async def sim_mover():
async def sim_mover() -> demo.Mover:
async with DeviceCollector(sim=True):
sim_mover = demo.Mover("BLxxI-MO-TABLE-01:X:")
# Signals connected here
Expand All @@ -28,17 +28,27 @@ async def sim_mover():
set_sim_value(sim_mover.units, "mm")
set_sim_value(sim_mover.precision, 3)
set_sim_value(sim_mover.velocity, 1)
yield sim_mover
return sim_mover


@pytest.fixture
async def sim_sensor():
async def sim_sensor() -> demo.Sensor:
async with DeviceCollector(sim=True):
sim_sensor = demo.Sensor("SIM:SENSOR:")
# Signals connected here

assert sim_sensor.name == "sim_sensor"
yield sim_sensor
return sim_sensor


@pytest.fixture
async def sim_sensor_group() -> demo.SensorGroup:
async with DeviceCollector(sim=True):
sim_sensor_group = demo.SensorGroup("SIM:SENSOR:")
# Signals connected here

assert sim_sensor_group.name == "sim_sensor_group"
return sim_sensor_group


class Watcher:
Expand Down Expand Up @@ -224,3 +234,59 @@ def my_plan():

with pytest.raises(RuntimeError, match="Will deadlock run engine if run in a plan"):
RE(my_plan())


async def test_dynamic_sensor_group_disconnected():
with pytest.raises(NotConnected):
async with DeviceCollector(timeout=0.1):
sim_sensor_group_dynamic = demo.SensorGroup("SIM:SENSOR:")

assert sim_sensor_group_dynamic.name == "sim_sensor_group_dynamic"


async def test_dynamic_sensor_group_read_and_describe(
sim_sensor_group: demo.SensorGroup,
):
set_sim_value(sim_sensor_group.sensors[1].value, 0.0)
set_sim_value(sim_sensor_group.sensors[2].value, 0.5)
set_sim_value(sim_sensor_group.sensors[3].value, 1.0)

await sim_sensor_group.stage()
description = await sim_sensor_group.describe()
reading = await sim_sensor_group.read()
await sim_sensor_group.unstage()

assert description == {
"sim_sensor_group-sensors-1-value": {
"dtype": "number",
"shape": [],
"source": "sim://SIM:SENSOR:1:Value",
},
"sim_sensor_group-sensors-2-value": {
"dtype": "number",
"shape": [],
"source": "sim://SIM:SENSOR:2:Value",
},
"sim_sensor_group-sensors-3-value": {
"dtype": "number",
"shape": [],
"source": "sim://SIM:SENSOR:3:Value",
},
}
assert reading == {
"sim_sensor_group-sensors-1-value": {
"alarm_severity": 0,
"timestamp": ANY,
"value": 0.0,
},
"sim_sensor_group-sensors-2-value": {
"alarm_severity": 0,
"timestamp": ANY,
"value": 0.5,
},
"sim_sensor_group-sensors-3-value": {
"alarm_severity": 0,
"timestamp": ANY,
"value": 1.0,
},
}

0 comments on commit b0e593b

Please sign in to comment.