From 49a51befac1e2b6bb00b682dc8413abd306094c6 Mon Sep 17 00:00:00 2001 From: Rose Yemelyanova Date: Tue, 5 Sep 2023 15:27:29 +0100 Subject: [PATCH 1/2] Restructure code base and ensure tests still pass --- .../decisions/0004-repository-structure.rst | 34 +++-- docs/examples/ad_demo.py | 2 +- docs/user/examples/epics_demo.py | 2 +- docs/user/reference/api.rst | 3 +- other_licenses/pyepics | 84 ----------- src/ophyd_async/core/__init__.py | 14 +- src/ophyd_async/core/devices/__init__.py | 2 + .../core/{ => devices}/device_collector.py | 4 +- .../core/devices/standard_readable.py | 2 +- src/ophyd_async/core/epicsdemo/__init__.py | 11 +- src/ophyd_async/core/{signals => }/signal.py | 10 +- src/ophyd_async/core/signals/__init__.py | 39 ----- .../{devices => epics}/__init__.py | 0 .../epics/areadetector/__init__.py | 22 +++ .../epics/areadetector/ad_driver.py | 18 +++ .../epics/areadetector/directory_provider.py | 18 +++ .../areadetector/hdf_streamer_det.py} | 142 ++---------------- .../epics/areadetector/nd_file_hdf.py | 22 +++ .../epics/areadetector/nd_plugin.py | 13 ++ .../epics/areadetector/single_trigger_det.py | 43 ++++++ src/ophyd_async/epics/areadetector/utils.py | 26 ++++ src/ophyd_async/epics/backends/__init__.py | 0 .../{core => epics}/backends/_aioca.py | 4 +- .../{core => epics}/backends/_p4p.py | 4 +- src/ophyd_async/epics/motion/__init__.py | 0 .../{devices => epics/motion}/motor.py | 6 +- src/ophyd_async/epics/signal/__init__.py | 10 ++ .../epics/signal/epics_transport.py | 31 ++++ src/ophyd_async/epics/signal/pvi_get.py | 22 +++ .../epics.py => epics/signal/signal.py} | 32 +--- src/ophyd_async/panda/__init__.py | 0 src/ophyd_async/{devices => panda}/panda.py | 46 +++--- tests/conftest.py | 25 ++- tests/core/backends/test_sim.py | 2 +- tests/core/devices/test_device.py | 2 +- tests/core/signals/test_epics.py | 3 +- tests/core/signals/test_signal.py | 2 +- tests/core/test_epicsdemo.py | 4 +- tests/epics/areadetector/__init__.py | 0 .../areadetector/test_hdf_streamer_det.py} | 80 ++-------- .../areadetector/test_single_trigger_det.py | 50 ++++++ tests/epics/motion/__init__.py | 0 tests/{devices => epics/motion}/test_motor.py | 6 +- tests/{devices => panda}/db/panda.db | 0 tests/{devices => panda}/test_panda.py | 6 +- 45 files changed, 409 insertions(+), 437 deletions(-) delete mode 100644 other_licenses/pyepics rename src/ophyd_async/core/{ => devices}/device_collector.py (98%) rename src/ophyd_async/core/{signals => }/signal.py (97%) delete mode 100644 src/ophyd_async/core/signals/__init__.py rename src/ophyd_async/{devices => epics}/__init__.py (100%) create mode 100644 src/ophyd_async/epics/areadetector/__init__.py create mode 100644 src/ophyd_async/epics/areadetector/ad_driver.py create mode 100644 src/ophyd_async/epics/areadetector/directory_provider.py rename src/ophyd_async/{devices/areadetector.py => epics/areadetector/hdf_streamer_det.py} (59%) create mode 100644 src/ophyd_async/epics/areadetector/nd_file_hdf.py create mode 100644 src/ophyd_async/epics/areadetector/nd_plugin.py create mode 100644 src/ophyd_async/epics/areadetector/single_trigger_det.py create mode 100644 src/ophyd_async/epics/areadetector/utils.py create mode 100644 src/ophyd_async/epics/backends/__init__.py rename src/ophyd_async/{core => epics}/backends/_aioca.py (98%) rename src/ophyd_async/{core => epics}/backends/_p4p.py (99%) create mode 100644 src/ophyd_async/epics/motion/__init__.py rename src/ophyd_async/{devices => epics/motion}/motor.py (96%) create mode 100644 src/ophyd_async/epics/signal/__init__.py create mode 100644 src/ophyd_async/epics/signal/epics_transport.py create mode 100644 src/ophyd_async/epics/signal/pvi_get.py rename src/ophyd_async/{core/signals/epics.py => epics/signal/signal.py} (72%) create mode 100644 src/ophyd_async/panda/__init__.py rename src/ophyd_async/{devices => panda}/panda.py (89%) create mode 100644 tests/epics/areadetector/__init__.py rename tests/{devices/test_area_detector.py => epics/areadetector/test_hdf_streamer_det.py} (70%) create mode 100644 tests/epics/areadetector/test_single_trigger_det.py create mode 100644 tests/epics/motion/__init__.py rename tests/{devices => epics/motion}/test_motor.py (95%) rename tests/{devices => panda}/db/panda.db (100%) rename tests/{devices => panda}/test_panda.py (94%) diff --git a/docs/developer/explanations/decisions/0004-repository-structure.rst b/docs/developer/explanations/decisions/0004-repository-structure.rst index 31d5c572d0..69ac640400 100644 --- a/docs/developer/explanations/decisions/0004-repository-structure.rst +++ b/docs/developer/explanations/decisions/0004-repository-structure.rst @@ -55,24 +55,40 @@ During this process, the folder structure should incrementally be changed to │ │ ├── __init__.py │ │ ├── backends │ │ │ ├── __init__.py - │ │ │ ├── _aioca.py - │ │ │ └── _p4p.py + │ │ │ ├── signal_backend.py + │ │ │ └── sim.py │ │ ├── devices - │ │ ├── signals + │ │ │ ├── __init__.py + │ │ │ ├── device_collector.py + │ │ │ └── ... │ │ ├── epicsdemo + │ │ │ └── ... + │ │ ├── signal.py │ │ ├── async_status.py - │ │ ├── device_collector.py │ │ └── utils.py - │ └── devices - │ ├── epics - │ └── tango + │ ├── epics + │ │ ├── backends + │ │ │ ├── __init__.py + │ │ │ ├── _p4p.py + │ │ │ └── _aioca.py + │ │ ├── areadetector + │ │ │ ├── __init__.py + │ │ │ ├── ad_driver.py + │ │ │ └── ... + │ │ ├── signal + │ │ │ └── ... + │ │ └── motion + │ │ ├── __init__.py + │ │ └── motor.py + │ └── panda + │ └── ... ├── tests │ ├── core │ │ └── ... - │ └── devices + │ └── epics └── ... -The `__init__.py` files of each submodule (core, devices.epics and devices.tango) will +The `__init__.py` files of each submodule (core, epics, panda and eventually tango) will be modified such that end users experience little disruption to how they use Ophyd Async. For such users, `from ophyd.v2.core import ...` can be replaced with `from ophyd_async.core import ...`. diff --git a/docs/examples/ad_demo.py b/docs/examples/ad_demo.py index f11790c495..6e75da96e6 100644 --- a/docs/examples/ad_demo.py +++ b/docs/examples/ad_demo.py @@ -9,7 +9,7 @@ from bluesky.utils import ProgressBarManager, register_transform from ophyd.v2.core import DeviceCollector -from ophyd_async.devices import areadetector +from ophyd_async.epics.areadetector import areadetector # Create a run engine, with plotting, progressbar and transform RE = RunEngine({}, call_returns_result=True) diff --git a/docs/user/examples/epics_demo.py b/docs/user/examples/epics_demo.py index ebc1b4b4ee..c96c2cb100 100644 --- a/docs/user/examples/epics_demo.py +++ b/docs/user/examples/epics_demo.py @@ -8,7 +8,7 @@ from ophyd import Component, Device, EpicsSignal, EpicsSignalRO from ophyd_async.core import epicsdemo -from ophyd_async.core.device_collector import DeviceCollector +from ophyd_async.core.devices.device_collector import DeviceCollector # Create a run engine, with plotting, progressbar and transform RE = RunEngine({}, call_returns_result=True) diff --git a/docs/user/reference/api.rst b/docs/user/reference/api.rst index 60a3dfaf4a..d5a1bb90cf 100644 --- a/docs/user/reference/api.rst +++ b/docs/user/reference/api.rst @@ -27,7 +27,6 @@ This is the internal API reference for ophyd_async ophyd_async.core.backends ophyd_async.core.devices ophyd_async.core.epicsdemo - ophyd_async.core.signals + ophyd_async.core.signal ophyd_async.core.async_status - ophyd_async.core.device_collector ophyd_async.core.utils diff --git a/other_licenses/pyepics b/other_licenses/pyepics deleted file mode 100644 index 2c054a9c93..0000000000 --- a/other_licenses/pyepics +++ /dev/null @@ -1,84 +0,0 @@ -The epics python module was orignally written by - - Matthew Newville - CARS, University of Chicago - -There have been several contributions from many others, notably Angus -Gratton . See the Acknowledgements section of -the documentation for a list of more contributors. - -Except where explicitly noted, all files in this distribution are licensed -under the Epics Open License.: - ------------------------------------------------- - -Copyright 2010 Matthew Newville, The University of Chicago. All rights reserved. - -The epics python module is distributed subject to the following license conditions: -SOFTWARE LICENSE AGREEMENT -Software: epics python module - - 1. The "Software", below, refers to the epics python module (in either - source code, or binary form and accompanying documentation). Each - licensee is addressed as "you" or "Licensee." - - 2. The copyright holders shown above and their third-party licensors - hereby grant Licensee a royalty-free nonexclusive license, subject to - the limitations stated herein and U.S. Government license rights. - - 3. You may modify and make a copy or copies of the Software for use - within your organization, if you meet the following conditions: - - 1. Copies in source code must include the copyright notice and this - Software License Agreement. - - 2. Copies in binary form must include the copyright notice and this - Software License Agreement in the documentation and/or other - materials provided with the copy. - - 4. You may modify a copy or copies of the Software or any portion of - it, thus forming a work based on the Software, and distribute copies of - such work outside your organization, if you meet all of the following - conditions: - - 1. Copies in source code must include the copyright notice and this - Software License Agreement; - - 2. Copies in binary form must include the copyright notice and this - Software License Agreement in the documentation and/or other - materials provided with the copy; - - 3. Modified copies and works based on the Software must carry - prominent notices stating that you changed specified portions of - the Software. - - 5. Portions of the Software resulted from work developed under a - U.S. Government contract and are subject to the following license: the - Government is granted for itself and others acting on its behalf a - paid-up, nonexclusive, irrevocable worldwide license in this computer - software to reproduce, prepare derivative works, and perform publicly - and display publicly. - - 6. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT - WARRANTY OF ANY KIND. THE COPYRIGHT HOLDERS, THEIR THIRD PARTY - LICENSORS, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, - AND THEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, (2) DO NOT - ASSUME ANY LEGAL LIABILITY OR RESPONSIBILITY FOR THE ACCURACY, - COMPLETENESS, OR USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT - USE OF THE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4) DO - NOT WARRANT THAT THE SOFTWARE WILL FUNCTION UNINTERRUPTED, THAT IT IS - ERROR-FREE OR THAT ANY ERRORS WILL BE CORRECTED. - - 7. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT HOLDERS, - THEIR THIRD PARTY LICENSORS, THE UNITED STATES, THE UNITED STATES - DEPARTMENT OF ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT, - INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF ANY KIND OR - NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS OR LOSS OF DATA, - FOR ANY REASON WHATSOEVER, WHETHER SUCH LIABILITY IS ASSERTED ON THE - BASIS OF CONTRACT, TORT (INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR - OTHERWISE, EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE - POSSIBILITY OF SUCH LOSS OR DAMAGES. - ------------------------------------------------- diff --git a/src/ophyd_async/core/__init__.py b/src/ophyd_async/core/__init__.py index a6b6c95615..29c051d9f0 100644 --- a/src/ophyd_async/core/__init__.py +++ b/src/ophyd_async/core/__init__.py @@ -1,25 +1,20 @@ from .async_status import AsyncStatus from .backends import SignalBackend, SimSignalBackend -from .device_collector import DeviceCollector from .devices import ( Device, + DeviceCollector, DeviceVector, StandardReadable, connect_children, get_device_children, name_children, ) -from .signals import ( - EpicsTransport, +from .signal import ( Signal, SignalR, SignalRW, SignalW, SignalX, - epics_signal_r, - epics_signal_rw, - epics_signal_w, - epics_signal_x, observe_value, set_and_wait_for_value, set_sim_callback, @@ -50,16 +45,11 @@ "connect_children", "get_device_children", "name_children", - "EpicsTransport", "Signal", "SignalR", "SignalW", "SignalRW", "SignalX", - "epics_signal_r", - "epics_signal_w", - "epics_signal_rw", - "epics_signal_x", "observe_value", "set_and_wait_for_value", "set_sim_callback", diff --git a/src/ophyd_async/core/devices/__init__.py b/src/ophyd_async/core/devices/__init__.py index 43fbc4802d..6f88fe2d29 100644 --- a/src/ophyd_async/core/devices/__init__.py +++ b/src/ophyd_async/core/devices/__init__.py @@ -1,9 +1,11 @@ from .device import Device, connect_children, get_device_children, name_children +from .device_collector import DeviceCollector from .device_vector import DeviceVector from .standard_readable import StandardReadable __all__ = [ "Device", + "DeviceCollector", "connect_children", "get_device_children", "name_children", diff --git a/src/ophyd_async/core/device_collector.py b/src/ophyd_async/core/devices/device_collector.py similarity index 98% rename from src/ophyd_async/core/device_collector.py rename to src/ophyd_async/core/devices/device_collector.py index 2cf1a319aa..ffe286da48 100644 --- a/src/ophyd_async/core/device_collector.py +++ b/src/ophyd_async/core/devices/device_collector.py @@ -6,8 +6,8 @@ from bluesky.run_engine import call_in_bluesky_event_loop -from .devices import Device -from .utils import NotConnected +from ..utils import NotConnected +from .device import Device class DeviceCollector: diff --git a/src/ophyd_async/core/devices/standard_readable.py b/src/ophyd_async/core/devices/standard_readable.py index 1b60172d03..2e64c4f71f 100644 --- a/src/ophyd_async/core/devices/standard_readable.py +++ b/src/ophyd_async/core/devices/standard_readable.py @@ -3,7 +3,7 @@ from bluesky.protocols import Configurable, Descriptor, Readable, Reading, Stageable from ..async_status import AsyncStatus -from ..signals import SignalR +from ..signal import SignalR from ..utils import merge_gathered_dicts from .device import Device diff --git a/src/ophyd_async/core/epicsdemo/__init__.py b/src/ophyd_async/core/epicsdemo/__init__.py index 5d01f8ec2b..72bea9de4d 100644 --- a/src/ophyd_async/core/epicsdemo/__init__.py +++ b/src/ophyd_async/core/epicsdemo/__init__.py @@ -8,15 +8,8 @@ import numpy as np from bluesky.protocols import Movable, Stoppable -from ophyd_async.core import ( - AsyncStatus, - Device, - StandardReadable, - epics_signal_r, - epics_signal_rw, - epics_signal_x, - observe_value, -) +from ophyd_async.core import AsyncStatus, Device, StandardReadable, observe_value +from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x class EnergyMode(Enum): diff --git a/src/ophyd_async/core/signals/signal.py b/src/ophyd_async/core/signal.py similarity index 97% rename from src/ophyd_async/core/signals/signal.py rename to src/ophyd_async/core/signal.py index d97f871047..1e0ff0d23c 100644 --- a/src/ophyd_async/core/signals/signal.py +++ b/src/ophyd_async/core/signal.py @@ -13,11 +13,11 @@ Subscribable, ) -from ..async_status import AsyncStatus -from ..backends.signal_backend import SignalBackend -from ..backends.sim import SimSignalBackend -from ..devices import Device -from ..utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T +from .async_status import AsyncStatus +from .backends.signal_backend import SignalBackend +from .backends.sim import SimSignalBackend +from .devices import Device +from .utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T _sim_backends: Dict[Signal, SimSignalBackend] = {} diff --git a/src/ophyd_async/core/signals/__init__.py b/src/ophyd_async/core/signals/__init__.py deleted file mode 100644 index 33f7549ada..0000000000 --- a/src/ophyd_async/core/signals/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from .epics import ( - EpicsTransport, - epics_signal_r, - epics_signal_rw, - epics_signal_w, - epics_signal_x, -) -from .signal import ( - Signal, - SignalR, - SignalRW, - SignalW, - SignalX, - observe_value, - set_and_wait_for_value, - set_sim_callback, - set_sim_put_proceeds, - set_sim_value, - wait_for_value, -) - -__all__ = [ - "EpicsTransport", - "epics_signal_r", - "epics_signal_rw", - "epics_signal_w", - "epics_signal_x", - "Signal", - "SignalR", - "SignalRW", - "SignalW", - "SignalX", - "observe_value", - "set_and_wait_for_value", - "set_sim_callback", - "set_sim_put_proceeds", - "set_sim_value", - "wait_for_value", -] diff --git a/src/ophyd_async/devices/__init__.py b/src/ophyd_async/epics/__init__.py similarity index 100% rename from src/ophyd_async/devices/__init__.py rename to src/ophyd_async/epics/__init__.py diff --git a/src/ophyd_async/epics/areadetector/__init__.py b/src/ophyd_async/epics/areadetector/__init__.py new file mode 100644 index 0000000000..ef910a4170 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/__init__.py @@ -0,0 +1,22 @@ +from .ad_driver import ADDriver +from .directory_provider import DirectoryProvider, TmpDirectoryProvider +from .hdf_streamer_det import HDFStreamerDet +from .nd_file_hdf import NDFileHDF +from .nd_plugin import NDPlugin, NDPluginStats +from .single_trigger_det import SingleTriggerDet +from .utils import FileWriteMode, ImageMode, ad_r, ad_rw + +__all__ = [ + "ADDriver", + "DirectoryProvider", + "TmpDirectoryProvider", + "HDFStreamerDet", + "NDFileHDF", + "NDPlugin", + "NDPluginStats", + "SingleTriggerDet", + "FileWriteMode", + "ImageMode", + "ad_r", + "ad_rw", +] diff --git a/src/ophyd_async/epics/areadetector/ad_driver.py b/src/ophyd_async/epics/areadetector/ad_driver.py new file mode 100644 index 0000000000..5ec78b740f --- /dev/null +++ b/src/ophyd_async/epics/areadetector/ad_driver.py @@ -0,0 +1,18 @@ +from ophyd_async.core.devices import Device +from ophyd_async.epics.signal import epics_signal_rw + +from .utils import ImageMode, ad_r, ad_rw + + +class ADDriver(Device): + def __init__(self, prefix: str) -> None: + # Define some signals + self.acquire = ad_rw(bool, prefix + "Acquire") + self.acquire_time = ad_rw(float, prefix + "AcquireTime") + self.num_images = ad_rw(int, prefix + "NumImages") + self.image_mode = ad_rw(ImageMode, prefix + "ImageMode") + self.array_counter = ad_rw(int, prefix + "ArrayCounter") + self.array_size_x = ad_r(int, prefix + "ArraySizeX") + self.array_size_y = ad_r(int, prefix + "ArraySizeY") + # There is no _RBV for this one + self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins") diff --git a/src/ophyd_async/epics/areadetector/directory_provider.py b/src/ophyd_async/epics/areadetector/directory_provider.py new file mode 100644 index 0000000000..8c05d1f2ef --- /dev/null +++ b/src/ophyd_async/epics/areadetector/directory_provider.py @@ -0,0 +1,18 @@ +import tempfile +from abc import abstractmethod +from pathlib import Path +from typing import Protocol + + +class DirectoryProvider(Protocol): + @abstractmethod + async def get_directory(self) -> Path: + ... + + +class TmpDirectoryProvider(DirectoryProvider): + def __init__(self) -> None: + self._directory = Path(tempfile.mkdtemp()) + + async def get_directory(self) -> Path: + return self._directory diff --git a/src/ophyd_async/devices/areadetector.py b/src/ophyd_async/epics/areadetector/hdf_streamer_det.py similarity index 59% rename from src/ophyd_async/devices/areadetector.py rename to src/ophyd_async/epics/areadetector/hdf_streamer_det.py index 84a2348a93..070103a1b5 100644 --- a/src/ophyd_async/devices/areadetector.py +++ b/src/ophyd_async/epics/areadetector/hdf_streamer_det.py @@ -1,126 +1,33 @@ import asyncio import collections -import tempfile import time -from abc import abstractmethod -from enum import Enum -from pathlib import Path -from typing import Callable, Dict, Iterator, Optional, Protocol, Sequence, Sized, Type +from typing import Callable, Dict, Iterator, Optional, Sized from bluesky.protocols import ( Asset, Descriptor, Flyable, PartialEvent, - Triggerable, WritesExternalAssets, ) from bluesky.utils import new_uid from event_model import compose_stream_resource from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.devices import Device, StandardReadable -from ophyd_async.core.signals import ( - SignalR, - SignalRW, - epics_signal_r, - epics_signal_rw, - set_and_wait_for_value, -) -from ophyd_async.core.utils import DEFAULT_TIMEOUT, T - - -def ad_rw(datatype: Type[T], prefix: str) -> SignalRW[T]: - return epics_signal_rw(datatype, prefix + "_RBV", prefix) - - -def ad_r(datatype: Type[T], prefix: str) -> SignalR[T]: - return epics_signal_r(datatype, prefix + "_RBV") - - -class ImageMode(Enum): - single = "Single" - multiple = "Multiple" - continuous = "Continuous" - - -class ADDriver(Device): - def __init__(self, prefix: str) -> None: - # Define some signals - self.acquire = ad_rw(bool, prefix + "Acquire") - self.acquire_time = ad_rw(float, prefix + "AcquireTime") - self.num_images = ad_rw(int, prefix + "NumImages") - self.image_mode = ad_rw(ImageMode, prefix + "ImageMode") - self.array_counter = ad_rw(int, prefix + "ArrayCounter") - self.array_size_x = ad_r(int, prefix + "ArraySizeX") - self.array_size_y = ad_r(int, prefix + "ArraySizeY") - # There is no _RBV for this one - self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins") - - -class NDPlugin(Device): - pass - - -class NDPluginStats(NDPlugin): - def __init__(self, prefix: str) -> None: - # Define some signals - self.unique_id = ad_r(int, prefix + "UniqueId") - - -class SingleTriggerDet(StandardReadable, Triggerable): - def __init__( - self, - drv: ADDriver, - read_uncached: Sequence[SignalR] = (), - name="", - **plugins: NDPlugin, - ) -> None: - self.drv = drv - self.__dict__.update(plugins) - self.set_readable_signals( - # Can't subscribe to read signals as race between monitor coming back and - # caput callback on acquire - read_uncached=[self.drv.array_counter] + list(read_uncached), - config=[self.drv.acquire_time], - ) - super().__init__(name=name) - - @AsyncStatus.wrap - async def stage(self) -> None: - await asyncio.gather( - self.drv.image_mode.set(ImageMode.single), - self.drv.wait_for_plugins.set(True), - ) - await super().stage() - - @AsyncStatus.wrap - async def trigger(self) -> None: - await self.drv.acquire.set(1) - +from ophyd_async.core.devices import StandardReadable +from ophyd_async.core.signal import set_and_wait_for_value +from ophyd_async.core.utils import DEFAULT_TIMEOUT -class FileWriteMode(str, Enum): - single = "Single" - capture = "Capture" - stream = "Stream" +from .ad_driver import ADDriver +from .directory_provider import DirectoryProvider +from .nd_file_hdf import NDFileHDF +from .utils import FileWriteMode, ImageMode +# How long in seconds to wait between flushes of HDF datasets +FLUSH_PERIOD = 0.5 -class NDFileHDF(Device): - def __init__(self, prefix: str) -> None: - # Define some signals - self.file_path = ad_rw(str, prefix + "FilePath") - self.file_name = ad_rw(str, prefix + "FileName") - self.file_template = ad_rw(str, prefix + "FileTemplate") - self.full_file_name = ad_r(str, prefix + "FullFileName") - self.file_write_mode = ad_rw(FileWriteMode, prefix + "FileWriteMode") - self.num_capture = ad_rw(int, prefix + "NumCapture") - self.num_captured = ad_r(int, prefix + "NumCaptured") - self.swmr_mode = ad_rw(bool, prefix + "SWMRMode") - self.lazy_open = ad_rw(bool, prefix + "LazyOpen") - self.capture = ad_rw(bool, prefix + "Capture") - self.flush_now = epics_signal_rw(bool, prefix + "FlushNow") - self.array_size0 = ad_r(int, prefix + "ArraySize0") - self.array_size1 = ad_r(int, prefix + "ArraySize1") +# How long to wait for new frames before timing out +FRAME_TIMEOUT = 120 class _HDFResource: @@ -160,33 +67,12 @@ async def flush_and_publish(self, hdf: NDFileHDF): event_count = num_captured - self._last_emitted if event_count: self._append_datum(event_count) - await hdf.flush_now.set(1) + await hdf.flush_now.set(True) self._last_flush = time.monotonic() if time.monotonic() - self._last_flush > FRAME_TIMEOUT: raise TimeoutError(f"{hdf.name}: writing stalled on frame {num_captured}") -class DirectoryProvider(Protocol): - @abstractmethod - async def get_directory(self) -> Path: - ... - - -class TmpDirectoryProvider(DirectoryProvider): - def __init__(self) -> None: - self._directory = Path(tempfile.mkdtemp()) - - async def get_directory(self) -> Path: - return self._directory - - -# How long in seconds to wait between flushes of HDF datasets -FLUSH_PERIOD = 0.5 - -# How long to wait for new frames before timing out -FRAME_TIMEOUT = 120 - - class HDFStreamerDet(StandardReadable, Flyable, WritesExternalAssets): def __init__( self, drv: ADDriver, hdf: NDFileHDF, dp: DirectoryProvider, name="" @@ -273,7 +159,7 @@ async def complete(self) -> None: @AsyncStatus.wrap async def unstage(self) -> None: # Already done a caput callback in _capture_status, so can't do one here - await self.hdf.capture.set(0, wait=False) + await self.hdf.capture.set(False, wait=False) assert self._capture_status, "Stage not run" await self._capture_status await super().unstage() diff --git a/src/ophyd_async/epics/areadetector/nd_file_hdf.py b/src/ophyd_async/epics/areadetector/nd_file_hdf.py new file mode 100644 index 0000000000..851538d52a --- /dev/null +++ b/src/ophyd_async/epics/areadetector/nd_file_hdf.py @@ -0,0 +1,22 @@ +from ophyd_async.core.devices import Device +from ophyd_async.epics.signal import epics_signal_rw + +from .utils import FileWriteMode, ad_r, ad_rw + + +class NDFileHDF(Device): + def __init__(self, prefix: str) -> None: + # Define some signals + self.file_path = ad_rw(str, prefix + "FilePath") + self.file_name = ad_rw(str, prefix + "FileName") + self.file_template = ad_rw(str, prefix + "FileTemplate") + self.full_file_name = ad_r(str, prefix + "FullFileName") + self.file_write_mode = ad_rw(FileWriteMode, prefix + "FileWriteMode") + self.num_capture = ad_rw(int, prefix + "NumCapture") + self.num_captured = ad_r(int, prefix + "NumCaptured") + self.swmr_mode = ad_rw(bool, prefix + "SWMRMode") + self.lazy_open = ad_rw(bool, prefix + "LazyOpen") + self.capture = ad_rw(bool, prefix + "Capture") + self.flush_now = epics_signal_rw(bool, prefix + "FlushNow") + self.array_size0 = ad_r(int, prefix + "ArraySize0") + self.array_size1 = ad_r(int, prefix + "ArraySize1") diff --git a/src/ophyd_async/epics/areadetector/nd_plugin.py b/src/ophyd_async/epics/areadetector/nd_plugin.py new file mode 100644 index 0000000000..89cdab5bd6 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/nd_plugin.py @@ -0,0 +1,13 @@ +from ophyd_async.core.devices import Device + +from .utils import ad_r + + +class NDPlugin(Device): + pass + + +class NDPluginStats(NDPlugin): + def __init__(self, prefix: str) -> None: + # Define some signals + self.unique_id = ad_r(int, prefix + "UniqueId") diff --git a/src/ophyd_async/epics/areadetector/single_trigger_det.py b/src/ophyd_async/epics/areadetector/single_trigger_det.py new file mode 100644 index 0000000000..b402983cf5 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/single_trigger_det.py @@ -0,0 +1,43 @@ +import asyncio +from typing import Sequence + +from bluesky.protocols import Triggerable + +from ophyd_async.core.async_status import AsyncStatus +from ophyd_async.core.devices import StandardReadable +from ophyd_async.core.signal import SignalR + +from .ad_driver import ADDriver +from .nd_plugin import NDPlugin +from .utils import ImageMode + + +class SingleTriggerDet(StandardReadable, Triggerable): + def __init__( + self, + drv: ADDriver, + read_uncached: Sequence[SignalR] = (), + name="", + **plugins: NDPlugin, + ) -> None: + self.drv = drv + self.__dict__.update(plugins) + self.set_readable_signals( + # Can't subscribe to read signals as race between monitor coming back and + # caput callback on acquire + read_uncached=[self.drv.array_counter] + list(read_uncached), + config=[self.drv.acquire_time], + ) + super().__init__(name=name) + + @AsyncStatus.wrap + async def stage(self) -> None: + await asyncio.gather( + self.drv.image_mode.set(ImageMode.single), + self.drv.wait_for_plugins.set(True), + ) + await super().stage() + + @AsyncStatus.wrap + async def trigger(self) -> None: + await self.drv.acquire.set(True) diff --git a/src/ophyd_async/epics/areadetector/utils.py b/src/ophyd_async/epics/areadetector/utils.py new file mode 100644 index 0000000000..099f295ba7 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/utils.py @@ -0,0 +1,26 @@ +from enum import Enum +from typing import Type + +from ophyd_async.core.signal import SignalR, SignalRW +from ophyd_async.core.utils import T +from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw + + +def ad_rw(datatype: Type[T], prefix: str) -> SignalRW[T]: + return epics_signal_rw(datatype, prefix + "_RBV", prefix) + + +def ad_r(datatype: Type[T], prefix: str) -> SignalR[T]: + return epics_signal_r(datatype, prefix + "_RBV") + + +class FileWriteMode(str, Enum): + single = "Single" + capture = "Capture" + stream = "Stream" + + +class ImageMode(Enum): + single = "Single" + multiple = "Multiple" + continuous = "Continuous" diff --git a/src/ophyd_async/epics/backends/__init__.py b/src/ophyd_async/epics/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ophyd_async/core/backends/_aioca.py b/src/ophyd_async/epics/backends/_aioca.py similarity index 98% rename from src/ophyd_async/core/backends/_aioca.py rename to src/ophyd_async/epics/backends/_aioca.py index 7cb60b10d1..cc38db36e9 100644 --- a/src/ophyd_async/core/backends/_aioca.py +++ b/src/ophyd_async/epics/backends/_aioca.py @@ -17,8 +17,8 @@ from bluesky.protocols import Descriptor, Dtype, Reading from epicscorelibs.ca import dbr -from ..signals.signal import SignalBackend -from ..utils import ( +from ophyd_async.core.signal import SignalBackend +from ophyd_async.core.utils import ( NotConnected, ReadingValueCallback, T, diff --git a/src/ophyd_async/core/backends/_p4p.py b/src/ophyd_async/epics/backends/_p4p.py similarity index 99% rename from src/ophyd_async/core/backends/_p4p.py rename to src/ophyd_async/epics/backends/_p4p.py index f9b855e568..8e1ac645c5 100644 --- a/src/ophyd_async/core/backends/_p4p.py +++ b/src/ophyd_async/epics/backends/_p4p.py @@ -8,8 +8,8 @@ from bluesky.protocols import Descriptor, Dtype, Reading from p4p.client.asyncio import Context, Subscription -from ..signals.signal import SignalBackend -from ..utils import ( +from ophyd_async.core.signal import SignalBackend +from ophyd_async.core.utils import ( NotConnected, ReadingValueCallback, T, diff --git a/src/ophyd_async/epics/motion/__init__.py b/src/ophyd_async/epics/motion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ophyd_async/devices/motor.py b/src/ophyd_async/epics/motion/motor.py similarity index 96% rename from src/ophyd_async/devices/motor.py rename to src/ophyd_async/epics/motion/motor.py index 846d24320c..460515db79 100644 --- a/src/ophyd_async/devices/motor.py +++ b/src/ophyd_async/epics/motion/motor.py @@ -6,11 +6,7 @@ from ophyd_async.core.async_status import AsyncStatus from ophyd_async.core.devices import StandardReadable -from ophyd_async.core.signals.epics import ( - epics_signal_r, - epics_signal_rw, - epics_signal_x, -) +from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x class Motor(StandardReadable, Movable, Stoppable): diff --git a/src/ophyd_async/epics/signal/__init__.py b/src/ophyd_async/epics/signal/__init__.py new file mode 100644 index 0000000000..b9b3d1a811 --- /dev/null +++ b/src/ophyd_async/epics/signal/__init__.py @@ -0,0 +1,10 @@ +from .epics_transport import EpicsTransport +from .signal import epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x + +__all__ = [ + "EpicsTransport", + "epics_signal_r", + "epics_signal_rw", + "epics_signal_w", + "epics_signal_x", +] diff --git a/src/ophyd_async/epics/signal/epics_transport.py b/src/ophyd_async/epics/signal/epics_transport.py new file mode 100644 index 0000000000..393960c976 --- /dev/null +++ b/src/ophyd_async/epics/signal/epics_transport.py @@ -0,0 +1,31 @@ +"""EPICS Signals over CA or PVA""" + +from __future__ import annotations + +from enum import Enum + +try: + from ..backends._aioca import CaSignalBackend +except ImportError as ca_error: + + class CaSignalBackend: # type: ignore + def __init__(*args, ca_error=ca_error, **kwargs): + raise NotImplementedError("CA support not available") from ca_error + + +try: + from ..backends._p4p import PvaSignalBackend +except ImportError as pva_error: + + class PvaSignalBackend: # type: ignore + def __init__(*args, pva_error=pva_error, **kwargs): + raise NotImplementedError("PVA support not available") from pva_error + + +class EpicsTransport(Enum): + """The sorts of transport EPICS support""" + + #: Use Channel Access (using aioca library) + ca = CaSignalBackend + #: Use PVAccess (using p4p library) + pva = PvaSignalBackend diff --git a/src/ophyd_async/epics/signal/pvi_get.py b/src/ophyd_async/epics/signal/pvi_get.py new file mode 100644 index 0000000000..c7d814178d --- /dev/null +++ b/src/ophyd_async/epics/signal/pvi_get.py @@ -0,0 +1,22 @@ +from typing import Dict, TypedDict + +from p4p.client.thread import Context + + +class PVIEntry(TypedDict, total=False): + d: str + r: str + rw: str + w: str + x: str + + +async def pvi_get(pv: str, ctxt: Context, timeout: float = 5.0) -> Dict[str, PVIEntry]: + pv_info = ctxt.get(pv, timeout=timeout).get("pvi").todict() + + result = {} + + for attr_name, attr_info in pv_info.items(): + result[attr_name] = PVIEntry(**attr_info) # type: ignore + + return result diff --git a/src/ophyd_async/core/signals/epics.py b/src/ophyd_async/epics/signal/signal.py similarity index 72% rename from src/ophyd_async/core/signals/epics.py rename to src/ophyd_async/epics/signal/signal.py index 0ab3123a47..7c22da3754 100644 --- a/src/ophyd_async/core/signals/epics.py +++ b/src/ophyd_async/epics/signal/signal.py @@ -2,38 +2,12 @@ from __future__ import annotations -from enum import Enum from typing import Optional, Tuple, Type -from ..utils import T, get_unique -from .signal import SignalBackend, SignalR, SignalRW, SignalW, SignalX - -try: - from ..backends._aioca import CaSignalBackend -except ImportError as ca_error: - - class CaSignalBackend: # type: ignore - def __init__(*args, ca_error=ca_error, **kwargs): - raise NotImplementedError("CA support not available") from ca_error - - -try: - from ..backends._p4p import PvaSignalBackend -except ImportError as pva_error: - - class PvaSignalBackend: # type: ignore - def __init__(*args, pva_error=pva_error, **kwargs): - raise NotImplementedError("PVA support not available") from pva_error - - -class EpicsTransport(Enum): - """The sorts of transport EPICS support""" - - #: Use Channel Access (using aioca library) - ca = CaSignalBackend - #: Use PVAccess (using p4p library) - pva = PvaSignalBackend +from ophyd_async.core.signal import SignalBackend, SignalR, SignalRW, SignalW, SignalX +from ophyd_async.core.utils import T, get_unique +from .epics_transport import EpicsTransport _default_epics_transport = EpicsTransport.ca diff --git a/src/ophyd_async/panda/__init__.py b/src/ophyd_async/panda/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ophyd_async/devices/panda.py b/src/ophyd_async/panda/panda.py similarity index 89% rename from src/ophyd_async/devices/panda.py rename to src/ophyd_async/panda/panda.py index 01bb010a20..05230ef039 100644 --- a/src/ophyd_async/devices/panda.py +++ b/src/ophyd_async/panda/panda.py @@ -12,6 +12,7 @@ Tuple, Type, TypedDict, + cast, get_args, get_origin, get_type_hints, @@ -21,18 +22,16 @@ import numpy.typing as npt from p4p.client.thread import Context -from ophyd_async.core.backends import SimSignalBackend +from ophyd_async.core.backends import SignalBackend, SimSignalBackend from ophyd_async.core.devices import Device, DeviceVector -from ophyd_async.core.signals import ( - Signal, - SignalR, - SignalRW, - SignalX, +from ophyd_async.core.signal import Signal, SignalR, SignalRW, SignalX +from ophyd_async.epics.signal import ( epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x, ) +from ophyd_async.epics.signal.pvi_get import pvi_get class PulseBlock(Device): @@ -110,7 +109,7 @@ def block_name_number(block_name: str) -> Tuple[str, Optional[int]]: return block_name, None -def _remove_inconsistent_blocks(pvi: Dict[str, PVIEntry]) -> None: +def _remove_inconsistent_blocks(pvi_info: Dict[str, PVIEntry]) -> None: """Remove blocks from pvi information. This is needed because some pandas have 'pcap' and 'pcap1' blocks, which are @@ -118,20 +117,15 @@ def _remove_inconsistent_blocks(pvi: Dict[str, PVIEntry]) -> None: for example. """ - pvi_keys = set(pvi.keys()) + pvi_keys = set(pvi_info.keys()) for k in pvi_keys: kn = re.sub(r"\d*$", "", k) if kn and k != kn and kn in pvi_keys: - del pvi[k] + del pvi_info[k] -async def pvi_get(pv: str, ctxt: Context, timeout: float = 5.0) -> Dict[str, PVIEntry]: - pv_info = ctxt.get(pv, timeout=timeout).get("pvi").todict() - - result = {} - - for attr_name, attr_info in pv_info.items(): - result[attr_name] = PVIEntry(**attr_info) # type: ignore +async def pvi(pv: str, ctxt: Context, timeout: float = 5.0) -> Dict[str, PVIEntry]: + result = await pvi_get(pv, ctxt, timeout=timeout) _remove_inconsistent_blocks(result) return result @@ -195,7 +189,7 @@ async def _make_block( block = self.verify_block(name, num) field_annos = get_type_hints(block, globalns=globals()) - block_pvi = await pvi_get(block_pv, self.ctxt) if not sim else None + block_pvi = await pvi(block_pv, self.ctxt) if not sim else None # finds which fields this class actually has, e.g. delay, width... for sig_name, sig_type in field_annos.items(): @@ -204,6 +198,7 @@ async def _make_block( # if not in sim mode, if block_pvi: + block_pvi = cast(Dict[str, PVIEntry], block_pvi) # try to get this block in the pvi. entry: Optional[PVIEntry] = block_pvi.get(sig_name) if entry is None: @@ -215,7 +210,9 @@ async def _make_block( signal = self._make_signal(entry, args[0] if len(args) > 0 else None) else: - backend = SimSignalBackend(args[0] if len(args) > 0 else None, block_pv) + backend: SignalBackend = SimSignalBackend( + args[0] if len(args) > 0 else None, block_pv + ) signal = SignalX(backend) if not origin else origin(backend) setattr(block, sig_name, signal) @@ -237,7 +234,7 @@ async def _make_untyped_block(self, block_pv: str): included dynamically anyway. """ block = Device() - block_pvi = await pvi_get(block_pv, self.ctxt) + block_pvi: Dict[str, PVIEntry] = await pvi(block_pv, self.ctxt) for signal_name, signal_pvi in block_pvi.items(): signal = self._make_signal(signal_pvi) @@ -284,7 +281,7 @@ async def connect(self, sim=False) -> None: If there's no pvi information, that's because we're in sim mode. In that case, makes all required blocks. """ - pvi = await pvi_get(self._init_prefix + ":PVI", self.ctxt) if not sim else None + pvi_info = await pvi(self._init_prefix + ":PVI", self.ctxt) if not sim else None hints = { attr_name: attr_type for attr_name, attr_type in get_type_hints(self, globalns=globals()).items() @@ -292,8 +289,9 @@ async def connect(self, sim=False) -> None: } # create all the blocks pvi says it should have, - if pvi: - for block_name, block_pvi in pvi.items(): + if pvi_info: + pvi_info = cast(Dict[str, PVIEntry], pvi_info) + for block_name, block_pvi in pvi_info.items(): name, num = block_name_number(block_name) if name in hints: @@ -306,13 +304,13 @@ async def connect(self, sim=False) -> None: # then check if the ones defined in this class are in the pvi info # make them if there is no pvi info, i.e. sim mode. for block_name in hints.keys(): - if pvi is not None: + if pvi_info is not None: pvi_name = block_name if get_origin(hints[block_name]) == DeviceVector: pvi_name += "1" - entry: Optional[PVIEntry] = pvi.get(pvi_name) + entry: Optional[PVIEntry] = pvi_info.get(pvi_name) assert entry, f"Expected PandA to contain {block_name} block." assert list(entry) == [ diff --git a/tests/conftest.py b/tests/conftest.py index 94e2da660b..68c7a47484 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,12 +8,14 @@ import pytest from bluesky.run_engine import RunEngine, TransitionError -RECORD = str(Path(__file__).parent / "devices" / "db" / "panda.db") +RECORD = str(Path(__file__).parent / "panda" / "db" / "panda.db") INCOMPLETE_BLOCK_RECORD = str( - Path(__file__).parent / "devices" / "db" / "incomplete_block_panda.db" + Path(__file__).parent / "panda" / "db" / "incomplete_block_panda.db" +) +INCOMPLETE_RECORD = str(Path(__file__).parent / "panda" / "db" / "incomplete_panda.db") +EXTRA_BLOCKS_RECORD = str( + Path(__file__).parent / "panda" / "db" / "extra_blocks_panda.db" ) -INCOMPLETE_RECORD = str(Path(__file__).parent / "devices" / "db" / "incomplete_panda.db") -EXTRA_BLOCKS_RECORD = str(Path(__file__).parent / "devices" / "db" / "extra_blocks_panda.db") @pytest.fixture(scope="function") @@ -84,3 +86,18 @@ async def inner_coroutine(): raise ValueError() return inner_coroutine + + +class DocHolder: + def __init__(self): + self.names = [] + self.docs = [] + + def append(self, name, doc): + self.names.append(name) + self.docs.append(doc) + + +@pytest.fixture +def doc_holder() -> DocHolder: + return DocHolder() diff --git a/tests/core/backends/test_sim.py b/tests/core/backends/test_sim.py index 2285bdcf08..708792f260 100644 --- a/tests/core/backends/test_sim.py +++ b/tests/core/backends/test_sim.py @@ -9,7 +9,7 @@ from bluesky.protocols import Reading from ophyd_async.core.backends import SignalBackend, SimSignalBackend -from ophyd_async.core.signals import Signal +from ophyd_async.core.signal import Signal from ophyd_async.core.utils import T diff --git a/tests/core/devices/test_device.py b/tests/core/devices/test_device.py index b8c62b886c..2c3058bdc1 100644 --- a/tests/core/devices/test_device.py +++ b/tests/core/devices/test_device.py @@ -3,8 +3,8 @@ import pytest -from ophyd_async.core.device_collector import DeviceCollector from ophyd_async.core.devices import Device, DeviceVector, get_device_children +from ophyd_async.core.devices.device_collector import DeviceCollector from ophyd_async.core.utils import wait_for_connection diff --git a/tests/core/signals/test_epics.py b/tests/core/signals/test_epics.py index 9fb821eede..add157b69c 100644 --- a/tests/core/signals/test_epics.py +++ b/tests/core/signals/test_epics.py @@ -17,7 +17,8 @@ from bluesky.protocols import Reading from ophyd_async.core import NotConnected, SignalBackend, T, get_dtype -from ophyd_async.core.signals.epics import EpicsTransport, _make_backend +from ophyd_async.epics.signal.epics_transport import EpicsTransport +from ophyd_async.epics.signal.signal import _make_backend RECORDS = str(Path(__file__).parent / "test_records.db") PV_PREFIX = "".join(random.choice(string.ascii_lowercase) for _ in range(12)) diff --git a/tests/core/signals/test_signal.py b/tests/core/signals/test_signal.py index da59937546..21ded8fb2b 100644 --- a/tests/core/signals/test_signal.py +++ b/tests/core/signals/test_signal.py @@ -5,7 +5,7 @@ import pytest from ophyd_async.core.backends import SimSignalBackend -from ophyd_async.core.signals import ( +from ophyd_async.core.signal import ( Signal, SignalRW, set_and_wait_for_value, diff --git a/tests/core/test_epicsdemo.py b/tests/core/test_epicsdemo.py index b6d6504ffe..740d4c27f9 100644 --- a/tests/core/test_epicsdemo.py +++ b/tests/core/test_epicsdemo.py @@ -134,8 +134,8 @@ async def test_mover_disconncted(): assert m.name == "mover" -async def test_sensor_disconncted(): - with patch("ophyd_async.core.device_collector.logging") as mock_logging: +async def test_sensor_disconnected(): + with patch("ophyd_async.core.devices.device_collector.logging") as mock_logging: with pytest.raises(NotConnected, match="Not all Devices connected"): async with DeviceCollector(timeout=0.1): s = epicsdemo.Sensor("ca://PRE:", name="sensor") diff --git a/tests/epics/areadetector/__init__.py b/tests/epics/areadetector/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/test_area_detector.py b/tests/epics/areadetector/test_hdf_streamer_det.py similarity index 70% rename from tests/devices/test_area_detector.py rename to tests/epics/areadetector/test_hdf_streamer_det.py index 891e17483b..461754f9cc 100644 --- a/tests/devices/test_area_detector.py +++ b/tests/epics/areadetector/test_hdf_streamer_det.py @@ -6,71 +6,19 @@ import bluesky.plans as bp import bluesky.preprocessors as bpp import pytest -from ophyd_async.core.device_collector import DeviceCollector -from ophyd_async.core.signals import set_sim_put_proceeds, set_sim_value -from ophyd_async.devices.areadetector import ( +from ophyd_async.core.devices.device_collector import DeviceCollector +from ophyd_async.core.signal import set_sim_put_proceeds, set_sim_value +from ophyd_async.epics.areadetector import ( ADDriver, FileWriteMode, HDFStreamerDet, ImageMode, NDFileHDF, - NDPluginStats, - SingleTriggerDet, TmpDirectoryProvider, ) -@pytest.fixture -async def single_trigger_det(): - async with DeviceCollector(sim=True): - stats = NDPluginStats("PREFIX:STATS") - det = SingleTriggerDet( - drv=ADDriver("PREFIX:DRV"), stats=stats, read_uncached=[stats.unique_id] - ) - - assert det.name == "det" - assert stats.name == "det-stats" - # Set non-default values to check they are set back - # These are using set_sim_value to simulate the backend IOC being setup - # in a particular way, rather than values being set by the Ophyd signals - set_sim_value(det.drv.acquire_time, 0.5) - set_sim_value(det.drv.array_counter, 1) - set_sim_value(det.drv.image_mode, ImageMode.continuous) - set_sim_value(stats.unique_id, 3) - yield det - - -class DocHolder: - def __init__(self): - self.names = [] - self.docs = [] - - def append(self, name, doc): - self.names.append(name) - self.docs.append(doc) - - -async def test_single_trigger_det(single_trigger_det: SingleTriggerDet, RE): - d = DocHolder() - RE(bp.count([single_trigger_det]), d.append) - - drv = single_trigger_det.drv - assert 1 == await drv.acquire.get_value() - assert ImageMode.single == await drv.image_mode.get_value() - assert True is await drv.wait_for_plugins.get_value() - - assert d.names == ["start", "descriptor", "event", "stop"] - _, descriptor, event, _ = d.docs - assert descriptor["configuration"]["det"]["data"]["det-drv-acquire_time"] == 0.5 - assert ( - descriptor["data_keys"]["det-stats-unique_id"]["source"] - == "sim://PREFIX:STATSUniqueId_RBV" - ) - assert event["data"]["det-drv-array_counter"] == 1 - assert event["data"]["det-stats-unique_id"] == 3 - - @pytest.fixture async def hdf_streamer_dets(): dp = TmpDirectoryProvider() @@ -103,9 +51,10 @@ async def hdf_streamer_dets(): yield deta, detb -async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], RE): - d = DocHolder() - RE(bp.count(hdf_streamer_dets), d.append) +async def test_hdf_streamer_dets_step( + hdf_streamer_dets: List[HDFStreamerDet], RE, doc_holder +): + RE(bp.count(hdf_streamer_dets), doc_holder.append) # type: ignore drv = hdf_streamer_dets[0].drv assert 1 == await drv.acquire.get_value() @@ -118,7 +67,7 @@ async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], R assert 0 == await hdf.num_capture.get_value() assert FileWriteMode.stream == await hdf.file_write_mode.get_value() - assert d.names == [ + assert doc_holder.names == [ # type: ignore "start", "descriptor", "stream_resource", @@ -128,7 +77,7 @@ async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], R "event", "stop", ] - _, descriptor, sra, sda, srb, sdb, event, _ = d.docs + _, descriptor, sra, sda, srb, sdb, event, _ = doc_holder.docs # type: ignore assert descriptor["configuration"]["deta"]["data"]["deta-drv-acquire_time"] == 0.8 assert descriptor["configuration"]["detb"]["data"]["detb-drv-acquire_time"] == 1.8 assert descriptor["data_keys"]["deta"]["shape"] == [768, 1024] @@ -146,9 +95,8 @@ async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], R # TODO: write test where they are in the same stream after # https://github.com/bluesky/bluesky/issues/1558 async def test_hdf_streamer_dets_fly_different_streams( - hdf_streamer_dets: List[HDFStreamerDet], RE + hdf_streamer_dets: List[HDFStreamerDet], RE, doc_holder ): - d = DocHolder() deta, detb = hdf_streamer_dets for det in hdf_streamer_dets: @@ -174,10 +122,10 @@ def fly_det(num: int): yield from bps.collect(det, stream=True, return_payload=False) yield from bps.wait(group="complete") - RE(fly_det(5), d.append) + RE(fly_det(5), doc_holder.append) # type: ignore # TODO: stream_* will come after descriptor soon - assert d.names == [ + assert doc_holder.names == [ # type: ignore "start", "stream_resource", "stream_datum", @@ -199,7 +147,7 @@ def fly_det(num: int): assert 0 == await hdf.num_capture.get_value() assert FileWriteMode.stream == await hdf.file_write_mode.get_value() - _, sra, sda, descriptora, srb, sdb, descriptorb, _ = d.docs + _, sra, sda, descriptora, srb, sdb, descriptorb, _ = doc_holder.docs # type: ignore assert descriptora["configuration"]["deta"]["data"]["deta-drv-acquire_time"] == 0.8 assert descriptorb["configuration"]["detb"]["data"]["detb-drv-acquire_time"] == 1.8 @@ -220,7 +168,7 @@ async def test_hdf_streamer_dets_timeout(hdf_streamer_dets: List[HDFStreamerDet] set_sim_put_proceeds(det.drv.acquire, False) await det.kickoff() t = time.monotonic() - with patch("ophyd_async.devices.areadetector.FRAME_TIMEOUT", 0.1): + with patch("ophyd_async.epics.areadetector.hdf_streamer_det.FRAME_TIMEOUT", 0.1): with pytest.raises(TimeoutError, match="deta-hdf: writing stalled on frame 1"): await det.complete() assert 1.0 < time.monotonic() - t < 1.1 diff --git a/tests/epics/areadetector/test_single_trigger_det.py b/tests/epics/areadetector/test_single_trigger_det.py new file mode 100644 index 0000000000..85c8252042 --- /dev/null +++ b/tests/epics/areadetector/test_single_trigger_det.py @@ -0,0 +1,50 @@ +import bluesky.plans as bp +import pytest + +from ophyd_async.core.devices.device_collector import DeviceCollector +from ophyd_async.core.signal import set_sim_value +from ophyd_async.epics.areadetector import ( + ADDriver, + ImageMode, + NDPluginStats, + SingleTriggerDet, +) + + +@pytest.fixture +async def single_trigger_det(): + async with DeviceCollector(sim=True): + stats = NDPluginStats("PREFIX:STATS") + det = SingleTriggerDet( + drv=ADDriver("PREFIX:DRV"), stats=stats, read_uncached=[stats.unique_id] + ) + + assert det.name == "det" + assert stats.name == "det-stats" + # Set non-default values to check they are set back + # These are using set_sim_value to simulate the backend IOC being setup + # in a particular way, rather than values being set by the Ophyd signals + set_sim_value(det.drv.acquire_time, 0.5) + set_sim_value(det.drv.array_counter, 1) + set_sim_value(det.drv.image_mode, ImageMode.continuous) + set_sim_value(stats.unique_id, 3) + yield det + + +async def test_single_trigger_det(single_trigger_det: SingleTriggerDet, RE, doc_holder): + RE(bp.count([single_trigger_det]), doc_holder.append) # type: ignore + + drv = single_trigger_det.drv + assert 1 == await drv.acquire.get_value() + assert ImageMode.single == await drv.image_mode.get_value() + assert True is await drv.wait_for_plugins.get_value() + + assert doc_holder.names == ["start", "descriptor", "event", "stop"] # type: ignore + _, descriptor, event, _ = doc_holder.docs # type: ignore + assert descriptor["configuration"]["det"]["data"]["det-drv-acquire_time"] == 0.5 + assert ( + descriptor["data_keys"]["det-stats-unique_id"]["source"] + == "sim://PREFIX:STATSUniqueId_RBV" + ) + assert event["data"]["det-drv-array_counter"] == 1 + assert event["data"]["det-stats-unique_id"] == 3 diff --git a/tests/epics/motion/__init__.py b/tests/epics/motion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/test_motor.py b/tests/epics/motion/test_motor.py similarity index 95% rename from tests/devices/test_motor.py rename to tests/epics/motion/test_motor.py index fcfeebcc80..a01c7a1a17 100644 --- a/tests/devices/test_motor.py +++ b/tests/epics/motion/test_motor.py @@ -4,10 +4,10 @@ import pytest from bluesky.protocols import Reading -from ophyd_async.core.device_collector import DeviceCollector -from ophyd_async.core.signals import set_sim_put_proceeds, set_sim_value -from ophyd_async.devices import motor +from ophyd_async.core.devices.device_collector import DeviceCollector +from ophyd_async.core.signal import set_sim_put_proceeds, set_sim_value +from ophyd_async.epics.motion import motor # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run diff --git a/tests/devices/db/panda.db b/tests/panda/db/panda.db similarity index 100% rename from tests/devices/db/panda.db rename to tests/panda/db/panda.db diff --git a/tests/devices/test_panda.py b/tests/panda/test_panda.py similarity index 94% rename from tests/devices/test_panda.py rename to tests/panda/test_panda.py index 71d4eeb650..20b022dda5 100644 --- a/tests/devices/test_panda.py +++ b/tests/panda/test_panda.py @@ -4,9 +4,9 @@ import numpy as np import pytest -from ophyd_async.core.device_collector import DeviceCollector -from ophyd_async.devices.panda import PandA, PVIEntry, SeqTable, SeqTrigger, pvi_get +from ophyd_async.core.devices.device_collector import DeviceCollector +from ophyd_async.panda.panda import PandA, PVIEntry, SeqTable, SeqTrigger, pvi class DummyDict: @@ -57,7 +57,7 @@ async def test_pvi_get_for_inconsistent_blocks(): "sfp3_sync_out": {}, } - resulting_pvi = await pvi_get("", MockCtxt(dummy_pvi)) + resulting_pvi = await pvi("", MockCtxt(dummy_pvi)) assert "sfp3_sync_out1" not in resulting_pvi assert "pcap1" not in resulting_pvi From 1781e5f6d7b5f61a57646cb232d355c34f034385 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 11 Sep 2023 14:27:08 +0000 Subject: [PATCH 2/2] Add ADR for procedural device definition --- .../0006-procedural-device-definitions.rst | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/developer/explanations/decisions/0006-procedural-device-definitions.rst diff --git a/docs/developer/explanations/decisions/0006-procedural-device-definitions.rst b/docs/developer/explanations/decisions/0006-procedural-device-definitions.rst new file mode 100644 index 0000000000..5b9bb69dea --- /dev/null +++ b/docs/developer/explanations/decisions/0006-procedural-device-definitions.rst @@ -0,0 +1,88 @@ +6. Procedural Device Definitions +================================ + +Date: 2023-09-11 + +Status +------ + +Accepted + +Context +------- + +Ophyd creates devices in a declarative way: + +.. code-block:: python + + class Sensor(Device): + mode = Component(EpicsSignal, "Mode", kind="config") + value = Component(EpicsSignalRO, "Value", kind="hinted") + +This means when you make ``device = OldSensor(pv_prefix)`` then some metaclass +magic will call ``EpicsSignal(pv_prefix + "Mode", kind="config")`` and make it +available as ``device.mode``. + +ophyd-async could convert this approach to use type hints instead of metaclasses: + +.. code-block:: python + + from typing import Annotated as A + + class Sensor(EpicsDevice): + mode: A[SignalRW, CONFIG, pv_suffix("Mode")] + value: A[SignalR, READ, pv_suffix("Value")] + +The superclass init could then read all the type hints and instantiate them with +the correct SignalBackends. + +Alternatively it could use a procedural approach and be explicit about where the +arguments are passed at the cost of greater verbosity: + +.. code-block:: python + + class Sensor(StandardReadable): + def __init__(self, prefix: str, name="") -> None: + self.value = epics_signal_r(float, prefix + "Value") + self.mode = epics_signal_rw(EnergyMode, prefix + "Mode") + # Set name and signals for read() and read_configuration() + self.set_readable_signals(read=[self.value], config=[self.mode]) + super().__init__(name=name) + +The procedural approach to creating child Devices is: + +.. code-block:: python + + class SensorGroup(Device): + def __init__(self, prefix: str, num: int, name: Optional[str]=None): + self.sensors = DeviceVector( + {i: Sensor(f"{prefix}:CHAN{i}" for i in range(1, num+1))} + ) + super().__init__(name=name) + +We have not been able to come up with a declarative approach that can describe +the ``SensorGroup`` example in a succinct way. + +Decision +-------- + +Type safety and readability are regarded above velocity, and magic should be +minimized. With this in mind we will stick with the procedural approach for now. +We may find a less verbose way of doing ``set_readable_signals`` by using a +context manager and overriding setattr in the future: + +.. code-block:: python + + with self.signals_added_to(READ): + self.value = epics_signal_r(float, prefix + "Value") + with self.signals_added_to(CONFIG): + self.mode = epics_signal_rw(EnergyMode, prefix + "Mode") + +If someone comes up with a way to write ``SensorGroup`` in a declarative +and readable way then we may revisit this. + +Consequences +------------ + +Ophyd and ophyd-async Devices will look less alike, but ophyd-async should be +learnable for beginners.