From 610d2d1fe7c78fe409c6ba8f95c1ae6d6c8d2633 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Fri, 19 Apr 2024 11:45:18 +0100 Subject: [PATCH] Add Async Protocols (#219) * Added AsyncReadable, AsyncConfigurable and AsyncPausable Also replaced imports of bluesky.protocols for each with their new async counterparts. * Fixed describe() in core/detector.py to be async * Sorted imports * Add tests * Removed test_mypy * Removed ophyd sim imports, replace with ophyd_async equivalents (requires make_detector) * Move protocols.py into src/ophyd_async --------- Co-authored-by: Oliver Copping Co-authored-by: Tom C (DLS) <101418278+coretl@users.noreply.github.com> --- src/ophyd_async/core/detector.py | 10 ++-- src/ophyd_async/core/signal.py | 5 +- src/ophyd_async/core/standard_readable.py | 6 +- src/ophyd_async/protocols.py | 73 +++++++++++++++++++++++ tests/protocols/test_protocols.py | 42 +++++++++++++ 5 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 src/ophyd_async/protocols.py create mode 100644 tests/protocols/test_protocols.py diff --git a/src/ophyd_async/core/detector.py b/src/ophyd_async/core/detector.py index 39bd2ac98d..b2b2ec890f 100644 --- a/src/ophyd_async/core/detector.py +++ b/src/ophyd_async/core/detector.py @@ -19,11 +19,9 @@ from bluesky.protocols import ( Collectable, - Configurable, Descriptor, Flyable, Preparable, - Readable, Reading, Stageable, StreamAsset, @@ -31,6 +29,8 @@ WritesStreamAssets, ) +from ophyd_async.protocols import AsyncConfigurable, AsyncReadable + from .async_status import AsyncStatus from .device import Device from .signal import SignalR @@ -143,8 +143,8 @@ async def close(self) -> None: class StandardDetector( Device, Stageable, - Configurable, - Readable, + AsyncConfigurable, + AsyncReadable, Triggerable, Preparable, Flyable, @@ -241,7 +241,7 @@ async def read(self) -> Dict[str, Reading]: # All data is in StreamResources, not Events, so nothing to output here return {} - def describe(self) -> Dict[str, Descriptor]: + async def describe(self) -> Dict[str, Descriptor]: return self._describe @AsyncStatus.wrap diff --git a/src/ophyd_async/core/signal.py b/src/ophyd_async/core/signal.py index dac3b10264..9d3785a3e2 100644 --- a/src/ophyd_async/core/signal.py +++ b/src/ophyd_async/core/signal.py @@ -9,12 +9,13 @@ Locatable, Location, Movable, - Readable, Reading, Stageable, Subscribable, ) +from ophyd_async.protocols import AsyncReadable + from .async_status import AsyncStatus from .device import Device from .signal_backend import SignalBackend @@ -127,7 +128,7 @@ def set_staged(self, staged: bool): return self._staged or bool(self._listeners) -class SignalR(Signal[T], Readable, Stageable, Subscribable): +class SignalR(Signal[T], AsyncReadable, Stageable, Subscribable): """Signal that can be read from and monitored""" _cache: Optional[_SignalCache] = None diff --git a/src/ophyd_async/core/standard_readable.py b/src/ophyd_async/core/standard_readable.py index dc9a11ef3e..3c37d5a332 100644 --- a/src/ophyd_async/core/standard_readable.py +++ b/src/ophyd_async/core/standard_readable.py @@ -1,6 +1,8 @@ from typing import Dict, Sequence, Tuple -from bluesky.protocols import Configurable, Descriptor, Readable, Reading, Stageable +from bluesky.protocols import Descriptor, Reading, Stageable + +from ophyd_async.protocols import AsyncConfigurable, AsyncReadable from .async_status import AsyncStatus from .device import Device @@ -8,7 +10,7 @@ from .utils import merge_gathered_dicts -class StandardReadable(Device, Readable, Configurable, Stageable): +class StandardReadable(Device, AsyncReadable, AsyncConfigurable, Stageable): """Device that owns its children and provides useful default behavior. - When its name is set it renames child Devices diff --git a/src/ophyd_async/protocols.py b/src/ophyd_async/protocols.py new file mode 100644 index 0000000000..51169d5418 --- /dev/null +++ b/src/ophyd_async/protocols.py @@ -0,0 +1,73 @@ +from abc import abstractmethod +from typing import Dict, Protocol, runtime_checkable + +from bluesky.protocols import Descriptor, HasName, Reading + + +@runtime_checkable +class AsyncReadable(HasName, Protocol): + @abstractmethod + async def read(self) -> Dict[str, Reading]: + """Return an OrderedDict mapping string field name(s) to dictionaries + of values and timestamps and optional per-point metadata. + + Example return value: + + .. code-block:: python + + OrderedDict(('channel1', + {'value': 5, 'timestamp': 1472493713.271991}), + ('channel2', + {'value': 16, 'timestamp': 1472493713.539238})) + """ + ... + + @abstractmethod + async def describe(self) -> Dict[str, Descriptor]: + """Return an OrderedDict with exactly the same keys as the ``read`` + method, here mapped to per-scan metadata about each field. + + Example return value: + + .. code-block:: python + + OrderedDict(('channel1', + {'source': 'XF23-ID:SOME_PV_NAME', + 'dtype': 'number', + 'shape': []}), + ('channel2', + {'source': 'XF23-ID:SOME_PV_NAME', + 'dtype': 'number', + 'shape': []})) + """ + ... + + +@runtime_checkable +class AsyncConfigurable(Protocol): + @abstractmethod + async def read_configuration(self) -> Dict[str, Reading]: + """Same API as ``read`` but for slow-changing fields related to configuration. + e.g., exposure time. These will typically be read only once per run. + """ + ... + + @abstractmethod + async def describe_configuration(self) -> Dict[str, Descriptor]: + """Same API as ``describe``, but corresponding to the keys in + ``read_configuration``. + """ + ... + + +@runtime_checkable +class AsyncPausable(Protocol): + @abstractmethod + async def pause(self) -> None: + """Perform device-specific work when the RunEngine pauses.""" + ... + + @abstractmethod + async def resume(self) -> None: + """Perform device-specific work when the RunEngine resumes after a pause.""" + ... diff --git a/tests/protocols/test_protocols.py b/tests/protocols/test_protocols.py new file mode 100644 index 0000000000..d75940fb55 --- /dev/null +++ b/tests/protocols/test_protocols.py @@ -0,0 +1,42 @@ +from pathlib import Path + +from bluesky.utils import new_uid + +from ophyd_async import protocols as bs_protocols +from ophyd_async.core import ( + DeviceCollector, + StaticDirectoryProvider, + set_sim_callback, + set_sim_value, +) +from ophyd_async.core.flyer import HardwareTriggeredFlyable +from ophyd_async.epics.areadetector.drivers import ADBase +from ophyd_async.epics.areadetector.writers import NDFileHDF +from ophyd_async.epics.demo.demo_ad_sim_detector import DemoADSimDetector +from ophyd_async.sim.demo import SimMotor + + +async def make_detector(prefix: str, name: str, tmp_path: Path): + dp = StaticDirectoryProvider(tmp_path, f"test-{new_uid()}") + + async with DeviceCollector(sim=True): + drv = ADBase(f"{prefix}DRV:") + hdf = NDFileHDF(f"{prefix}HDF:") + det = DemoADSimDetector( + drv, hdf, dp, config_sigs=[drv.acquire_time, drv.acquire], name=name + ) + + def _set_full_file_name(_, val): + set_sim_value(hdf.full_file_name, str(tmp_path / val)) + + set_sim_callback(hdf.file_name, _set_full_file_name) + + return det + + +async def test_readable(): + async with DeviceCollector(sim=True): + det = await make_detector("test", "test det", Path("/tmp")) + assert isinstance(SimMotor, bs_protocols.AsyncReadable) + assert isinstance(det, bs_protocols.AsyncReadable) + assert not isinstance(HardwareTriggeredFlyable, bs_protocols.AsyncReadable)