diff --git a/docs/examples/foo_detector.py b/docs/examples/foo_detector.py index c9cb433d85..66948398b2 100644 --- a/docs/examples/foo_detector.py +++ b/docs/examples/foo_detector.py @@ -3,22 +3,20 @@ from bluesky.protocols import HasHints, Hints -from ophyd_async.core import DirectoryProvider -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.detector import DetectorControl, DetectorTrigger, StandardDetector -from ophyd_async.epics.areadetector.drivers.ad_base import ( - ADBase, - ADBaseShapeProvider, - start_acquiring_driver_and_ensure_status, +from ophyd_async.core import ( + AsyncStatus, + DetectorControl, + DetectorTrigger, + PathProvider, + StandardDetector, ) -from ophyd_async.epics.areadetector.utils import ImageMode, ad_rw, stop_busy_record -from ophyd_async.epics.areadetector.writers.hdf_writer import HDFWriter -from ophyd_async.epics.areadetector.writers.nd_file_hdf import NDFileHDF +from ophyd_async.epics import adcore +from ophyd_async.epics.signal import epics_signal_rw_rbv -class FooDriver(ADBase): +class FooDriver(adcore.ADBaseIO): def __init__(self, prefix: str, name: str = "") -> None: - self.trigger_mode = ad_rw(str, prefix + "TriggerMode") + self.trigger_mode = epics_signal_rw_rbv(str, prefix + "TriggerMode") super().__init__(prefix, name) @@ -38,40 +36,40 @@ async def arm( ) -> AsyncStatus: await asyncio.gather( self._drv.num_images.set(num), - self._drv.image_mode.set(ImageMode.multiple), + self._drv.image_mode.set(adcore.ImageMode.multiple), self._drv.trigger_mode.set(f"FOO{trigger}"), ) if exposure is not None: await self._drv.acquire_time.set(exposure) - return await start_acquiring_driver_and_ensure_status(self._drv) + return await adcore.start_acquiring_driver_and_ensure_status(self._drv) async def disarm(self): - await stop_busy_record(self._drv.acquire, False, timeout=1) + await adcore.stop_busy_record(self._drv.acquire, False, timeout=1) class FooDetector(StandardDetector, HasHints): _controller: FooController - _writer: HDFWriter + _writer: adcore.ADHDFWriter def __init__( self, prefix: str, - directory_provider: DirectoryProvider, + path_provider: PathProvider, drv_suffix="cam1:", hdf_suffix="HDF1:", name="", ): # Must be children to pick up connect self.drv = FooDriver(prefix + drv_suffix) - self.hdf = NDFileHDF(prefix + hdf_suffix) + self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix) super().__init__( FooController(self.drv), - HDFWriter( + adcore.ADHDFWriter( self.hdf, - directory_provider, + path_provider, lambda: self.name, - ADBaseShapeProvider(self.drv), + adcore.ADBaseShapeProvider(self.drv), ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/docs/how-to/compound-devices.rst b/docs/how-to/compound-devices.rst index e4a2d7d92a..ef4aaf818a 100644 --- a/docs/how-to/compound-devices.rst +++ b/docs/how-to/compound-devices.rst @@ -11,7 +11,7 @@ Assembly Compound assemblies can be used to group Devices into larger logical Devices: -.. literalinclude:: ../../src/ophyd_async/epics/demo/__init__.py +.. literalinclude:: ../../src/ophyd_async/epics/demo/_mover.py :pyobject: SampleStage This applies prefixes on construction: @@ -35,7 +35,7 @@ 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 +.. literalinclude:: ../../src/ophyd_async/epics/demo/_sensor.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. diff --git a/docs/how-to/make-a-simple-device.rst b/docs/how-to/make-a-simple-device.rst index 76137e63da..af8d96c68e 100644 --- a/docs/how-to/make-a-simple-device.rst +++ b/docs/how-to/make-a-simple-device.rst @@ -22,7 +22,7 @@ For a simple :external+bluesky:py:class:`bluesky.protocols.Readable` object like define some signals, then tell the superclass which signals should contribute to ``read()`` and ``read_configuration()``: -.. literalinclude:: ../../src/ophyd_async/epics/demo/__init__.py +.. literalinclude:: ../../src/ophyd_async/epics/demo/_sensor.py :pyobject: Sensor First some Signals are constructed and stored on the Device. Each one is passed @@ -54,7 +54,7 @@ Movable For a more complicated device like a `Mover`, you can still use `StandardReadable` and implement some addition protocols: -.. literalinclude:: ../../src/ophyd_async/epics/demo/__init__.py +.. literalinclude:: ../../src/ophyd_async/epics/demo/_mover.py :pyobject: Mover The ``set()`` method implements :external+bluesky:py:class:`bluesky.protocols.Movable`. This @@ -71,7 +71,7 @@ Assembly Compound assemblies can be used to group Devices into larger logical Devices: -.. literalinclude:: ../../src/ophyd_async/epics/demo/__init__.py +.. literalinclude:: ../../src/ophyd_async/epics/demo/_mover.py :pyobject: SampleStage This applies prefixes on construction: diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst index bcb68a6b71..7016f2a247 100644 --- a/docs/how-to/make-a-standard-detector.rst +++ b/docs/how-to/make-a-standard-detector.rst @@ -9,7 +9,7 @@ Make a StandardDetector `StandardDetector` is an abstract class to assist in creating Device classes for hardware that writes its own data e.g. an AreaDetector implementation, or a PandA writing motor encoder positions to file. The `StandardDetector` is a simple compound device, with 2 standard components: -- `DetectorWriter` to handle data persistence, i/o and pass information about data to the RunEngine (usually an instance of :py:class:`HDFWriter`) +- `DetectorWriter` to handle data persistence, i/o and pass information about data to the RunEngine (usually an instance of :py:class:`ADHDFWriter`) - `DetectorControl` with logic for arming and disarming the detector. This will be unique to the StandardDetector implementation. Writing an AreaDetector StandardDetector @@ -17,11 +17,11 @@ Writing an AreaDetector StandardDetector For an AreaDetector implementation of the StandardDetector, two entity objects which are subdevices of the `StandardDetector` are used to map to AreaDetector plugins: -- An NDPluginFile instance (for :py:class:`HDFWriter` an instance of :py:class:`NDFileHDF`) -- An :py:class:`ADBase` instance mapping to NDArray for the "driver" of the detector implementation +- An NDPluginFile instance (for :py:class:`ADHDFWriter` an instance of :py:class:`NDFileHDFIO`) +- An :py:class:`ADBaseIO` instance mapping to NDArray for the "driver" of the detector implementation -Define a :py:class:`FooDriver` if the NDArray requires fields in addition to those on :py:class:`ADBase` to be exposed. It should extend :py:class:`ADBase`. +Define a :py:class:`FooDriver` if the NDArray requires fields in addition to those on :py:class:`ADBaseIO` to be exposed. It should extend :py:class:`ADBaseIO`. Enumeration fields should be named to prevent namespace collision, i.e. for a Signal named "TriggerSource" use the enum "FooTriggerSource" .. literalinclude:: ../examples/foo_detector.py @@ -50,15 +50,15 @@ Writing a non-AreaDetector StandardDetector A non-AreaDetector `StandardDetector` should implement `DetectorControl` and `DetectorWriter` directly. Here we construct a `DetectorControl` that co-ordinates signals on a PandA PositionCapture block - a child device "pcap" of the `StandardDetector` implementation, analogous to the :py:class:`FooDriver`. -.. literalinclude:: ../../src/ophyd_async/panda/_panda_controller.py +.. literalinclude:: ../../src/ophyd_async/fastcs/panda/_panda_controller.py :pyobject: PandaPcapController The PandA may write a number of fields, and the :py:class:`PandaHDFWriter` co-ordinates those, configures the filewriter and describes the data for the RunEngine. -.. literalinclude:: ../../src/ophyd_async/panda/writers/_hdf_writer.py +.. literalinclude:: ../../src/ophyd_async/fastcs/panda/_hdf_writer.py :pyobject: PandaHDFWriter The PandA StandardDetector implementation simply ties the component parts and its child devices together. -.. literalinclude:: ../../src/ophyd_async/panda/_hdf_panda.py +.. literalinclude:: ../../src/ophyd_async/fastcs/panda/_hdf_panda.py :pyobject: HDFPanda diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 66747af774..74485ee1e8 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -26,4 +26,4 @@ This is the internal API reference for ophyd_async core epics - panda \ No newline at end of file + fastcs \ No newline at end of file diff --git a/src/ophyd_async/core/__init__.py b/src/ophyd_async/core/__init__.py index e12e2a90d4..e86422ee0c 100644 --- a/src/ophyd_async/core/__init__.py +++ b/src/ophyd_async/core/__init__.py @@ -1,26 +1,13 @@ -from ._providers import ( - AutoIncrementFilenameProvider, - AutoIncrementingPathProvider, - FilenameProvider, - NameProvider, - PathInfo, - PathProvider, - ShapeProvider, - StaticFilenameProvider, - StaticPathProvider, - UUIDFilenameProvider, - YMDPathProvider, -) -from .async_status import AsyncStatus, WatchableAsyncStatus -from .detector import ( +from ._detector import ( DetectorControl, DetectorTrigger, DetectorWriter, StandardDetector, TriggerInfo, ) -from .device import Device, DeviceCollector, DeviceVector -from .device_save_loader import ( +from ._device import Device, DeviceCollector, DeviceVector +from ._device_save_loader import ( + all_at_once, get_signal_values, load_device, load_from_yaml, @@ -29,9 +16,11 @@ set_signal_values, walk_rw_signals, ) -from .flyer import HardwareTriggeredFlyable, TriggerLogic -from .mock_signal_backend import MockSignalBackend -from .mock_signal_utils import ( +from ._flyer import StandardFlyer, TriggerLogic +from ._hdf_dataset import HDFDataset, HDFFile +from ._log import config_ophyd_async_logging +from ._mock_signal_backend import MockSignalBackend +from ._mock_signal_utils import ( callback_on_mock_put, get_mock_put, mock_puts_blocked, @@ -40,7 +29,22 @@ set_mock_value, set_mock_values, ) -from .signal import ( +from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable +from ._providers import ( + AutoIncrementFilenameProvider, + AutoIncrementingPathProvider, + FilenameProvider, + NameProvider, + PathInfo, + PathProvider, + ShapeProvider, + StaticFilenameProvider, + StaticPathProvider, + UUIDFilenameProvider, + YMDPathProvider, +) +from ._readable import ConfigSignal, HintedSignal, StandardReadable +from ._signal import ( Signal, SignalR, SignalRW, @@ -56,93 +60,100 @@ soft_signal_rw, wait_for_value, ) -from .signal_backend import RuntimeSubsetEnum, SignalBackend, SubsetEnum -from .soft_signal_backend import SoftSignalBackend -from .standard_readable import ConfigSignal, HintedSignal, StandardReadable -from .utils import ( +from ._signal_backend import RuntimeSubsetEnum, SignalBackend, SubsetEnum +from ._soft_signal_backend import SignalMetadata, SoftSignalBackend +from ._status import AsyncStatus, WatchableAsyncStatus +from ._utils import ( DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, - Callback, NotConnected, ReadingValueCallback, T, + WatcherUpdate, get_dtype, get_unique, - merge_gathered_dicts, + in_micros, wait_for_connection, ) __all__ = [ - "AsyncStatus", - "AutoIncrementFilenameProvider", - "AutoIncrementingPathProvider", - "CalculatableTimeout", - "CalculateTimeout", - "Callback", - "ConfigSignal", - "DEFAULT_TIMEOUT", "DetectorControl", "DetectorTrigger", "DetectorWriter", + "StandardDetector", + "TriggerInfo", "Device", "DeviceCollector", "DeviceVector", - "FilenameProvider", - "HardwareTriggeredFlyable", - "HintedSignal", + "all_at_once", + "get_signal_values", + "load_device", + "load_from_yaml", + "save_device", + "save_to_yaml", + "set_signal_values", + "walk_rw_signals", + "StandardFlyer", + "TriggerLogic", + "HDFDataset", + "HDFFile", + "config_ophyd_async_logging", "MockSignalBackend", + "callback_on_mock_put", + "get_mock_put", + "mock_puts_blocked", + "reset_mock_put_calls", + "set_mock_put_proceeds", + "set_mock_value", + "set_mock_values", + "AsyncConfigurable", + "AsyncReadable", + "AsyncStageable", + "AutoIncrementFilenameProvider", + "AutoIncrementingPathProvider", + "FilenameProvider", "NameProvider", - "NotConnected", "PathInfo", "PathProvider", - "ReadingValueCallback", - "RuntimeSubsetEnum", "ShapeProvider", + "StaticFilenameProvider", + "StaticPathProvider", + "UUIDFilenameProvider", + "YMDPathProvider", + "ConfigSignal", + "HintedSignal", + "StandardReadable", "Signal", - "SignalBackend", "SignalR", "SignalRW", "SignalW", "SignalX", - "SoftSignalBackend", - "StandardDetector", - "StandardReadable", - "StaticFilenameProvider", - "StaticPathProvider", - "SubsetEnum", - "T", - "TriggerInfo", - "TriggerLogic", - "UUIDFilenameProvider", - "WatchableAsyncStatus", - "YMDPathProvider", - # Lower-cased imports "assert_configuration", "assert_emitted", "assert_reading", "assert_value", - "callback_on_mock_put", - "get_dtype", - "get_mock_put", - "get_signal_values", - "get_unique", - "load_device", - "load_from_yaml", - "merge_gathered_dicts", - "mock_puts_blocked", "observe_value", - "reset_mock_put_calls", - "save_device", - "save_to_yaml", "set_and_wait_for_value", - "set_mock_put_proceeds", - "set_mock_value", - "set_mock_values", - "set_signal_values", "soft_signal_r_and_setter", "soft_signal_rw", - "wait_for_connection", "wait_for_value", - "walk_rw_signals", + "RuntimeSubsetEnum", + "SignalBackend", + "SubsetEnum", + "SignalMetadata", + "SoftSignalBackend", + "AsyncStatus", + "WatchableAsyncStatus", + "DEFAULT_TIMEOUT", + "CalculatableTimeout", + "CalculateTimeout", + "NotConnected", + "ReadingValueCallback", + "T", + "WatcherUpdate", + "get_dtype", + "get_unique", + "in_micros", + "wait_for_connection", ] diff --git a/src/ophyd_async/core/detector.py b/src/ophyd_async/core/_detector.py similarity index 97% rename from src/ophyd_async/core/detector.py rename to src/ophyd_async/core/_detector.py index 68c6b5b4b5..cdc9fb324e 100644 --- a/src/ophyd_async/core/detector.py +++ b/src/ophyd_async/core/_detector.py @@ -13,7 +13,6 @@ List, Optional, Sequence, - TypeVar, ) from bluesky.protocols import ( @@ -29,13 +28,10 @@ ) from pydantic import BaseModel, Field -from ophyd_async.protocols import AsyncConfigurable, AsyncReadable - -from .async_status import AsyncStatus, WatchableAsyncStatus -from .device import Device -from .utils import DEFAULT_TIMEOUT, WatcherUpdate, merge_gathered_dicts - -T = TypeVar("T") +from ._device import Device +from ._protocol import AsyncConfigurable, AsyncReadable +from ._status import AsyncStatus, WatchableAsyncStatus +from ._utils import DEFAULT_TIMEOUT, T, WatcherUpdate, merge_gathered_dicts class DetectorTrigger(str, Enum): diff --git a/src/ophyd_async/core/device.py b/src/ophyd_async/core/_device.py similarity index 99% rename from src/ophyd_async/core/device.py rename to src/ophyd_async/core/_device.py index a674072df3..33f8ddb729 100644 --- a/src/ophyd_async/core/device.py +++ b/src/ophyd_async/core/_device.py @@ -19,7 +19,7 @@ from bluesky.protocols import HasName from bluesky.run_engine import call_in_bluesky_event_loop -from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection +from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection class Device(HasName): diff --git a/src/ophyd_async/core/device_save_loader.py b/src/ophyd_async/core/_device_save_loader.py similarity index 98% rename from src/ophyd_async/core/device_save_loader.py rename to src/ophyd_async/core/_device_save_loader.py index 77db3d37d6..5b81228264 100644 --- a/src/ophyd_async/core/device_save_loader.py +++ b/src/ophyd_async/core/_device_save_loader.py @@ -8,8 +8,8 @@ from bluesky.protocols import Location from bluesky.utils import Msg -from .device import Device -from .signal import SignalRW +from ._device import Device +from ._signal import SignalRW def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.Node: @@ -241,7 +241,7 @@ def save_device( Therefore, users should consider the order of device loading and write their own sorter algorithms accordingly. - See :func:`ophyd_async.panda.phase_sorter` for a valid implementation of the + See :func:`ophyd_async.fastcs.panda.phase_sorter` for a valid implementation of the sorter. Parameters diff --git a/src/ophyd_async/core/flyer.py b/src/ophyd_async/core/_flyer.py similarity index 89% rename from src/ophyd_async/core/flyer.py rename to src/ophyd_async/core/_flyer.py index f43f1a880f..79fff4029c 100644 --- a/src/ophyd_async/core/flyer.py +++ b/src/ophyd_async/core/_flyer.py @@ -1,14 +1,12 @@ from abc import ABC, abstractmethod -from typing import Dict, Generic, Sequence, TypeVar +from typing import Dict, Generic, Sequence from bluesky.protocols import DataKey, Flyable, Preparable, Reading, Stageable -from .async_status import AsyncStatus -from .device import Device -from .signal import SignalR -from .utils import merge_gathered_dicts - -T = TypeVar("T") +from ._device import Device +from ._signal import SignalR +from ._status import AsyncStatus +from ._utils import T, merge_gathered_dicts class TriggerLogic(ABC, Generic[T]): @@ -29,7 +27,7 @@ async def stop(self): """Stop flying and wait everything to be stopped""" -class HardwareTriggeredFlyable( +class StandardFlyer( Device, Stageable, Preparable, diff --git a/src/ophyd_async/epics/areadetector/writers/general_hdffile.py b/src/ophyd_async/core/_hdf_dataset.py similarity index 95% rename from src/ophyd_async/epics/areadetector/writers/general_hdffile.py rename to src/ophyd_async/core/_hdf_dataset.py index b11ca1d32e..dc06237ca0 100644 --- a/src/ophyd_async/epics/areadetector/writers/general_hdffile.py +++ b/src/ophyd_async/core/_hdf_dataset.py @@ -10,11 +10,11 @@ StreamResource, ) -from ophyd_async.core import PathInfo +from ._providers import PathInfo @dataclass -class _HDFDataset: +class HDFDataset: data_key: str dataset: str shape: Sequence[int] = field(default_factory=tuple) @@ -26,7 +26,7 @@ class _HDFDataset: SLICE_NAME = "AD_HDF5_SWMR_SLICE" -class _HDFFile: +class HDFFile: """ :param directory_info: Contains information about how to construct a StreamResource :param full_file_name: Absolute path to the file to be written @@ -37,7 +37,7 @@ def __init__( self, path_info: PathInfo, full_file_name: Path, - datasets: List[_HDFDataset], + datasets: List[HDFDataset], hostname: str = "localhost", ) -> None: self._last_emitted = 0 diff --git a/src/ophyd_async/log.py b/src/ophyd_async/core/_log.py similarity index 98% rename from src/ophyd_async/log.py rename to src/ophyd_async/core/_log.py index 0fa9b0c9da..2f40f2172d 100644 --- a/src/ophyd_async/log.py +++ b/src/ophyd_async/core/_log.py @@ -34,9 +34,7 @@ def format(self, record): def _validate_level(level) -> int: - """ - Return an int for level comparison - """ + """Return an int for level comparison.""" if isinstance(level, int): levelno = level elif isinstance(level, str): @@ -80,21 +78,31 @@ def config_ophyd_async_logging( level : str or int Python logging level, given as string or corresponding integer. Default is 'WARNING'. + Returns ------- handler : logging.Handler The handler, which has already been added to the 'ophyd_async' logger. + Examples -------- Log to a file. + config_ophyd_async_logging(file='/tmp/what_is_happening.txt') + Include the date along with the time. (The log messages will always include microseconds, which are configured separately, not as part of 'datefmt'.) + config_ophyd_async_logging(datefmt="%Y-%m-%d %H:%M:%S") + Turn off ANSI color codes. + config_ophyd_async_logging(color=False) + Increase verbosity: show level DEBUG or higher. + config_ophyd_async_logging(level='DEBUG') + """ global current_handler diff --git a/src/ophyd_async/core/mock_signal_backend.py b/src/ophyd_async/core/_mock_signal_backend.py similarity index 92% rename from src/ophyd_async/core/mock_signal_backend.py rename to src/ophyd_async/core/_mock_signal_backend.py index 24cc28f3f6..221645fe0b 100644 --- a/src/ophyd_async/core/mock_signal_backend.py +++ b/src/ophyd_async/core/_mock_signal_backend.py @@ -5,9 +5,9 @@ from bluesky.protocols import Descriptor, Reading -from ophyd_async.core.signal_backend import SignalBackend -from ophyd_async.core.soft_signal_backend import SoftSignalBackend -from ophyd_async.core.utils import DEFAULT_TIMEOUT, ReadingValueCallback, T +from ._signal_backend import SignalBackend +from ._soft_signal_backend import SoftSignalBackend +from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T class MockSignalBackend(SignalBackend[T]): diff --git a/src/ophyd_async/core/mock_signal_utils.py b/src/ophyd_async/core/_mock_signal_utils.py similarity index 97% rename from src/ophyd_async/core/mock_signal_utils.py rename to src/ophyd_async/core/_mock_signal_utils.py index dab9b32519..7026b4e842 100644 --- a/src/ophyd_async/core/mock_signal_utils.py +++ b/src/ophyd_async/core/_mock_signal_utils.py @@ -2,10 +2,9 @@ from typing import Any, Callable, Iterable from unittest.mock import Mock -from ophyd_async.core.signal import Signal -from ophyd_async.core.utils import T - -from .mock_signal_backend import MockSignalBackend +from ._mock_signal_backend import MockSignalBackend +from ._signal import Signal +from ._utils import T def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend: diff --git a/src/ophyd_async/protocols.py b/src/ophyd_async/core/_protocol.py similarity index 98% rename from src/ophyd_async/protocols.py rename to src/ophyd_async/core/_protocol.py index 05256e46c8..79e11a78ae 100644 --- a/src/ophyd_async/protocols.py +++ b/src/ophyd_async/core/_protocol.py @@ -14,7 +14,7 @@ from bluesky.protocols import DataKey, HasName, Reading if TYPE_CHECKING: - from ophyd_async.core.async_status import AsyncStatus + from ._status import AsyncStatus @runtime_checkable diff --git a/src/ophyd_async/core/standard_readable.py b/src/ophyd_async/core/_readable.py similarity index 96% rename from src/ophyd_async/core/standard_readable.py rename to src/ophyd_async/core/_readable.py index 7d25d9cd2a..c63a0f5dcf 100644 --- a/src/ophyd_async/core/standard_readable.py +++ b/src/ophyd_async/core/_readable.py @@ -1,24 +1,14 @@ import warnings from contextlib import contextmanager -from typing import ( - Callable, - Dict, - Generator, - Optional, - Sequence, - Tuple, - Type, - Union, -) +from typing import Callable, Dict, Generator, Optional, Sequence, Tuple, Type, Union from bluesky.protocols import DataKey, HasHints, Hints, Reading -from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable - -from .async_status import AsyncStatus -from .device import Device, DeviceVector -from .signal import SignalR -from .utils import merge_gathered_dicts +from ._device import Device, DeviceVector +from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable +from ._signal import SignalR +from ._status import AsyncStatus +from ._utils import merge_gathered_dicts ReadableChild = Union[AsyncReadable, AsyncConfigurable, AsyncStageable, HasHints] ReadableChildWrapper = Union[ diff --git a/src/ophyd_async/core/signal.py b/src/ophyd_async/core/_signal.py similarity index 97% rename from src/ophyd_async/core/signal.py rename to src/ophyd_async/core/_signal.py index 9ebc86f84c..c7bf3bab17 100644 --- a/src/ophyd_async/core/signal.py +++ b/src/ophyd_async/core/_signal.py @@ -25,14 +25,13 @@ Subscribable, ) -from ophyd_async.core.mock_signal_backend import MockSignalBackend -from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable - -from .async_status import AsyncStatus -from .device import Device -from .signal_backend import SignalBackend -from .soft_signal_backend import SignalMetadata, SoftSignalBackend -from .utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T +from ._device import Device +from ._mock_signal_backend import MockSignalBackend +from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable +from ._signal_backend import SignalBackend +from ._soft_signal_backend import SignalMetadata, SoftSignalBackend +from ._status import AsyncStatus +from ._utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T def _add_timeout(func): diff --git a/src/ophyd_async/core/signal_backend.py b/src/ophyd_async/core/_signal_backend.py similarity index 91% rename from src/ophyd_async/core/signal_backend.py rename to src/ophyd_async/core/_signal_backend.py index 10498e83b0..41e9fbcbd3 100644 --- a/src/ophyd_async/core/signal_backend.py +++ b/src/ophyd_async/core/_signal_backend.py @@ -1,17 +1,8 @@ from abc import abstractmethod -from typing import ( - TYPE_CHECKING, - ClassVar, - Generic, - Literal, - Optional, - Tuple, - Type, -) - -from bluesky.protocols import DataKey, Reading - -from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T +from typing import TYPE_CHECKING, ClassVar, Generic, Literal, Optional, Tuple, Type + +from ._protocol import DataKey, Reading +from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T class SignalBackend(Generic[T]): diff --git a/src/ophyd_async/core/soft_signal_backend.py b/src/ophyd_async/core/_soft_signal_backend.py similarity index 96% rename from src/ophyd_async/core/soft_signal_backend.py rename to src/ophyd_async/core/_soft_signal_backend.py index 5a591f670f..62bafd5bb1 100644 --- a/src/ophyd_async/core/soft_signal_backend.py +++ b/src/ophyd_async/core/_soft_signal_backend.py @@ -4,23 +4,14 @@ import time from collections import abc from enum import Enum -from typing import ( - Dict, - Generic, - Optional, - Tuple, - Type, - Union, - cast, - get_origin, -) +from typing import Dict, Generic, Optional, Tuple, Type, Union, cast, get_origin import numpy as np from bluesky.protocols import DataKey, Dtype, Reading from typing_extensions import TypedDict -from .signal_backend import RuntimeSubsetEnum, SignalBackend -from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype +from ._signal_backend import RuntimeSubsetEnum, SignalBackend +from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype primitive_dtypes: Dict[type, Dtype] = { str: "string", diff --git a/src/ophyd_async/core/async_status.py b/src/ophyd_async/core/_status.py similarity index 95% rename from src/ophyd_async/core/async_status.py rename to src/ophyd_async/core/_status.py index 78d0a4ab93..d2d285e675 100644 --- a/src/ophyd_async/core/async_status.py +++ b/src/ophyd_async/core/_status.py @@ -4,20 +4,12 @@ import functools import time from dataclasses import asdict, replace -from typing import ( - AsyncIterator, - Awaitable, - Callable, - Generic, - Type, - TypeVar, - cast, -) +from typing import AsyncIterator, Awaitable, Callable, Generic, Type, TypeVar, cast from bluesky.protocols import Status -from ..protocols import Watcher -from .utils import Callback, P, T, WatcherUpdate +from ._protocol import Watcher +from ._utils import Callback, P, T, WatcherUpdate AS = TypeVar("AS", bound="AsyncStatus") WAS = TypeVar("WAS", bound="WatchableAsyncStatus") diff --git a/src/ophyd_async/core/utils.py b/src/ophyd_async/core/_utils.py similarity index 100% rename from src/ophyd_async/core/utils.py rename to src/ophyd_async/core/_utils.py diff --git a/src/ophyd_async/epics/adaravis/__init__.py b/src/ophyd_async/epics/adaravis/__init__.py new file mode 100644 index 0000000000..34a9195678 --- /dev/null +++ b/src/ophyd_async/epics/adaravis/__init__.py @@ -0,0 +1,9 @@ +from ._aravis import AravisDetector +from ._aravis_controller import AravisController +from ._aravis_io import AravisDriverIO + +__all__ = [ + "AravisDetector", + "AravisController", + "AravisDriverIO", +] diff --git a/src/ophyd_async/epics/areadetector/aravis.py b/src/ophyd_async/epics/adaravis/_aravis.py similarity index 75% rename from src/ophyd_async/epics/areadetector/aravis.py rename to src/ophyd_async/epics/adaravis/_aravis.py index 2c500396a5..db7255f2fd 100644 --- a/src/ophyd_async/epics/areadetector/aravis.py +++ b/src/ophyd_async/epics/adaravis/_aravis.py @@ -3,12 +3,10 @@ from bluesky.protocols import HasHints, Hints from ophyd_async.core import PathProvider, StandardDetector -from ophyd_async.epics.areadetector.controllers.aravis_controller import ( - AravisController, -) -from ophyd_async.epics.areadetector.drivers import ADBaseShapeProvider -from ophyd_async.epics.areadetector.drivers.aravis_driver import AravisDriver -from ophyd_async.epics.areadetector.writers import HDFWriter, NDFileHDF +from ophyd_async.epics import adcore + +from ._aravis_controller import AravisController +from ._aravis_io import AravisDriverIO class AravisDetector(StandardDetector, HasHints): @@ -19,7 +17,7 @@ class AravisDetector(StandardDetector, HasHints): """ _controller: AravisController - _writer: HDFWriter + _writer: adcore.ADHDFWriter def __init__( self, @@ -30,16 +28,16 @@ def __init__( name="", gpio_number: AravisController.GPIO_NUMBER = 1, ): - self.drv = AravisDriver(prefix + drv_suffix) - self.hdf = NDFileHDF(prefix + hdf_suffix) + self.drv = AravisDriverIO(prefix + drv_suffix) + self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix) super().__init__( AravisController(self.drv, gpio_number=gpio_number), - HDFWriter( + adcore.ADHDFWriter( self.hdf, path_provider, lambda: self.name, - ADBaseShapeProvider(self.drv), + adcore.ADBaseShapeProvider(self.drv), ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/src/ophyd_async/epics/areadetector/controllers/aravis_controller.py b/src/ophyd_async/epics/adaravis/_aravis_controller.py similarity index 83% rename from src/ophyd_async/epics/areadetector/controllers/aravis_controller.py rename to src/ophyd_async/epics/adaravis/_aravis_controller.py index d7a4e1f5ce..6349d111b1 100644 --- a/src/ophyd_async/epics/areadetector/controllers/aravis_controller.py +++ b/src/ophyd_async/epics/adaravis/_aravis_controller.py @@ -7,12 +7,9 @@ DetectorTrigger, set_and_wait_for_value, ) -from ophyd_async.epics.areadetector.drivers.aravis_driver import ( - AravisDriver, - AravisTriggerMode, - AravisTriggerSource, -) -from ophyd_async.epics.areadetector.utils import ImageMode, stop_busy_record +from ophyd_async.epics import adcore + +from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource # The deadtime of an ADaravis controller varies depending on the exact model of camera. # Ideally we would maximize performance by dynamically retrieving the deadtime at @@ -23,7 +20,7 @@ class AravisController(DetectorControl): GPIO_NUMBER = Literal[1, 2, 3, 4] - def __init__(self, driver: AravisDriver, gpio_number: GPIO_NUMBER) -> None: + def __init__(self, driver: AravisDriverIO, gpio_number: GPIO_NUMBER) -> None: self._drv = driver self.gpio_number = gpio_number @@ -37,9 +34,9 @@ async def arm( exposure: Optional[float] = None, ) -> AsyncStatus: if num == 0: - image_mode = ImageMode.continuous + image_mode = adcore.ImageMode.continuous else: - image_mode = ImageMode.multiple + image_mode = adcore.ImageMode.multiple if exposure is not None: await self._drv.acquire_time.set(exposure) @@ -76,4 +73,4 @@ def _get_trigger_info( return (AravisTriggerMode.on, f"Line{self.gpio_number}") async def disarm(self): - await stop_busy_record(self._drv.acquire, False, timeout=1) + await adcore.stop_busy_record(self._drv.acquire, False, timeout=1) diff --git a/src/ophyd_async/epics/areadetector/drivers/aravis_driver.py b/src/ophyd_async/epics/adaravis/_aravis_io.py similarity index 86% rename from src/ophyd_async/epics/areadetector/drivers/aravis_driver.py rename to src/ophyd_async/epics/adaravis/_aravis_io.py index 6c49c66fb1..83da702a4f 100644 --- a/src/ophyd_async/epics/areadetector/drivers/aravis_driver.py +++ b/src/ophyd_async/epics/adaravis/_aravis_io.py @@ -1,8 +1,8 @@ from enum import Enum from ophyd_async.core import SubsetEnum -from ophyd_async.epics.areadetector.drivers import ADBase -from ophyd_async.epics.signal.signal import epics_signal_rw_rbv +from ophyd_async.epics import adcore +from ophyd_async.epics.signal import epics_signal_rw_rbv class AravisTriggerMode(str, Enum): @@ -22,12 +22,15 @@ class AravisTriggerMode(str, Enum): AravisTriggerSource = SubsetEnum["Freerun", "Line1"] -class AravisDriver(ADBase): +class AravisDriverIO(adcore.ADBaseIO): # If instantiating a new instance, ensure it is supported in the _deadtimes dict """Generic Driver supporting the Manta and Mako drivers. Fetches deadtime prior to use in a Streaming scan. + Requires driver firmware up to date: - Model_RBV must be of the form "^(Mako|Manta) (model)$" + + This mirrors the interface provided by ADAravis/db/aravisCamera.template. """ def __init__(self, prefix: str, name: str = "") -> None: diff --git a/src/ophyd_async/epics/adcore/__init__.py b/src/ophyd_async/epics/adcore/__init__.py new file mode 100644 index 0000000000..7f45e87881 --- /dev/null +++ b/src/ophyd_async/epics/adcore/__init__.py @@ -0,0 +1,36 @@ +from ._core_io import ADBaseIO, DetectorState, NDFileHDFIO, NDPluginStatsIO +from ._core_logic import ( + DEFAULT_GOOD_STATES, + ADBaseShapeProvider, + set_exposure_time_and_acquire_period_if_supplied, + start_acquiring_driver_and_ensure_status, +) +from ._hdf_writer import ADHDFWriter +from ._single_trigger import SingleTriggerDetector +from ._utils import ( + ADBaseDataType, + FileWriteMode, + ImageMode, + NDAttributeDataType, + NDAttributesXML, + stop_busy_record, +) + +__all__ = [ + "ADBaseIO", + "DetectorState", + "NDFileHDFIO", + "NDPluginStatsIO", + "DEFAULT_GOOD_STATES", + "ADBaseShapeProvider", + "set_exposure_time_and_acquire_period_if_supplied", + "start_acquiring_driver_and_ensure_status", + "ADHDFWriter", + "SingleTriggerDetector", + "ADBaseDataType", + "FileWriteMode", + "ImageMode", + "NDAttributeDataType", + "NDAttributesXML", + "stop_busy_record", +] diff --git a/src/ophyd_async/epics/adcore/_core_io.py b/src/ophyd_async/epics/adcore/_core_io.py new file mode 100644 index 0000000000..eab482c8ec --- /dev/null +++ b/src/ophyd_async/epics/adcore/_core_io.py @@ -0,0 +1,114 @@ +from enum import Enum + +from ophyd_async.core import Device +from ophyd_async.epics.signal import ( + epics_signal_r, + epics_signal_rw, + epics_signal_rw_rbv, +) + +from ._utils import ADBaseDataType, FileWriteMode, ImageMode + + +class Callback(str, Enum): + Enable = "Enable" + Disable = "Disable" + + +class NDArrayBaseIO(Device): + def __init__(self, prefix: str, name: str = "") -> None: + self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV") + self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile") + self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire") + self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV") + self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV") + self.data_type = epics_signal_r(ADBaseDataType, prefix + "NDDataType_RBV") + self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter") + # There is no _RBV for this one + self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins") + + super().__init__(name=name) + + +class NDPluginBaseIO(NDArrayBaseIO): + def __init__(self, prefix: str, name: str = "") -> None: + self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort") + self.enable_callback = epics_signal_rw_rbv(Callback, prefix + "EnableCallbacks") + self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress") + self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV") + self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV") + super().__init__(prefix, name) + + +class NDPluginStatsIO(NDPluginBaseIO): + pass + + +class DetectorState(str, Enum): + """ + Default set of states of an AreaDetector driver. + See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore + """ + + Idle = "Idle" + Acquire = "Acquire" + Readout = "Readout" + Correct = "Correct" + Saving = "Saving" + Aborting = "Aborting" + Error = "Error" + Waiting = "Waiting" + Initializing = "Initializing" + Disconnected = "Disconnected" + Aborted = "Aborted" + + +class ADBaseIO(NDArrayBaseIO): + def __init__(self, prefix: str, name: str = "") -> None: + # Define some signals + self.acquire_time = epics_signal_rw_rbv(float, prefix + "AcquireTime") + self.acquire_period = epics_signal_rw_rbv(float, prefix + "AcquirePeriod") + self.num_images = epics_signal_rw_rbv(int, prefix + "NumImages") + self.image_mode = epics_signal_rw_rbv(ImageMode, prefix + "ImageMode") + self.detector_state = epics_signal_r( + DetectorState, prefix + "DetectorState_RBV" + ) + super().__init__(prefix, name=name) + + +class Compression(str, Enum): + none = "None" + nbit = "N-bit" + szip = "szip" + zlib = "zlib" + blosc = "Blosc" + bslz4 = "BSLZ4" + lz4 = "LZ4" + jpeg = "JPEG" + + +class NDFileHDFIO(NDPluginBaseIO): + def __init__(self, prefix: str, name="") -> None: + # Define some signals + self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode") + self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression") + self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims") + self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath") + self.file_name = epics_signal_rw_rbv(str, prefix + "FileName") + self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV") + self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate") + self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV") + self.file_write_mode = epics_signal_rw_rbv( + FileWriteMode, prefix + "FileWriteMode" + ) + self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture") + self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV") + self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode") + self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen") + self.capture = epics_signal_rw_rbv(bool, prefix + "Capture") + self.flush_now = epics_signal_rw(bool, prefix + "FlushNow") + self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName") + self.array_size0 = epics_signal_r(int, prefix + "ArraySize0") + self.array_size1 = epics_signal_r(int, prefix + "ArraySize1") + self.create_dir_depth = epics_signal_rw(int, prefix + "CreateDirectory") + super().__init__(prefix, name) diff --git a/src/ophyd_async/epics/areadetector/drivers/ad_base.py b/src/ophyd_async/epics/adcore/_core_logic.py similarity index 67% rename from src/ophyd_async/epics/areadetector/drivers/ad_base.py rename to src/ophyd_async/epics/adcore/_core_logic.py index 13e07e8176..5fa8974d29 100644 --- a/src/ophyd_async/epics/areadetector/drivers/ad_base.py +++ b/src/ophyd_async/epics/adcore/_core_logic.py @@ -1,5 +1,4 @@ import asyncio -from enum import Enum from typing import FrozenSet, Set from ophyd_async.core import ( @@ -10,53 +9,31 @@ set_and_wait_for_value, ) -from ...signal.signal import epics_signal_r, epics_signal_rw_rbv -from ..utils import ImageMode -from ..writers.nd_plugin import NDArrayBase +from ._core_io import ADBaseIO, DetectorState - -class DetectorState(str, Enum): - """ - Default set of states of an AreaDetector driver. - See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore - """ - - Idle = "Idle" - Acquire = "Acquire" - Readout = "Readout" - Correct = "Correct" - Saving = "Saving" - Aborting = "Aborting" - Error = "Error" - Waiting = "Waiting" - Initializing = "Initializing" - Disconnected = "Disconnected" - Aborted = "Aborted" - - -#: Default set of states that we should consider "good" i.e. the acquisition +# Default set of states that we should consider "good" i.e. the acquisition # is complete and went well DEFAULT_GOOD_STATES: FrozenSet[DetectorState] = frozenset( [DetectorState.Idle, DetectorState.Aborted] ) -class ADBase(NDArrayBase): - def __init__(self, prefix: str, name: str = "") -> None: - # Define some signals - self.acquire_time = epics_signal_rw_rbv(float, prefix + "AcquireTime") - self.acquire_period = epics_signal_rw_rbv(float, prefix + "AcquirePeriod") - self.num_images = epics_signal_rw_rbv(int, prefix + "NumImages") - self.image_mode = epics_signal_rw_rbv(ImageMode, prefix + "ImageMode") - self.detector_state = epics_signal_r( - DetectorState, prefix + "DetectorState_RBV" +class ADBaseShapeProvider(ShapeProvider): + def __init__(self, driver: ADBaseIO) -> None: + self._driver = driver + + async def __call__(self) -> tuple: + shape = await asyncio.gather( + self._driver.array_size_y.get_value(), + self._driver.array_size_x.get_value(), + self._driver.data_type.get_value(), ) - super().__init__(prefix, name=name) + return shape async def set_exposure_time_and_acquire_period_if_supplied( controller: DetectorControl, - driver: ADBase, + driver: ADBaseIO, exposure: float | None = None, timeout: float = DEFAULT_TIMEOUT, ) -> None: @@ -70,7 +47,7 @@ async def set_exposure_time_and_acquire_period_if_supplied( controller: Controller that can supply a deadtime. driver: - The driver to start acquiring. Must subclass ADBase. + The driver to start acquiring. Must subclass ADBaseIO. exposure: Desired exposure time, this is a noop if it is None. timeout: @@ -85,7 +62,7 @@ async def set_exposure_time_and_acquire_period_if_supplied( async def start_acquiring_driver_and_ensure_status( - driver: ADBase, + driver: ADBaseIO, good_states: Set[DetectorState] = set(DEFAULT_GOOD_STATES), timeout: float = DEFAULT_TIMEOUT, ) -> AsyncStatus: @@ -99,7 +76,7 @@ async def start_acquiring_driver_and_ensure_status( Parameters ---------- driver: - The driver to start acquiring. Must subclass ADBase. + The driver to start acquiring. Must subclass ADBaseIO. good_states: set of states defined in DetectorState enum which are considered good states. timeout: @@ -125,16 +102,3 @@ async def complete_acquisition() -> None: ) return AsyncStatus(complete_acquisition()) - - -class ADBaseShapeProvider(ShapeProvider): - def __init__(self, driver: ADBase) -> None: - self._driver = driver - - async def __call__(self) -> tuple: - shape = await asyncio.gather( - self._driver.array_size_y.get_value(), - self._driver.array_size_x.get_value(), - self._driver.data_type.get_value(), - ) - return shape diff --git a/src/ophyd_async/epics/areadetector/writers/hdf_writer.py b/src/ophyd_async/epics/adcore/_hdf_writer.py similarity index 92% rename from src/ophyd_async/epics/areadetector/writers/hdf_writer.py rename to src/ophyd_async/epics/adcore/_hdf_writer.py index 14f69b7102..249ce331ca 100644 --- a/src/ophyd_async/epics/areadetector/writers/hdf_writer.py +++ b/src/ophyd_async/epics/adcore/_hdf_writer.py @@ -8,23 +8,24 @@ DEFAULT_TIMEOUT, AsyncStatus, DetectorWriter, + HDFDataset, + HDFFile, NameProvider, PathProvider, ShapeProvider, + observe_value, set_and_wait_for_value, wait_for_value, ) -from ophyd_async.core.signal import observe_value -from .general_hdffile import _HDFDataset, _HDFFile -from .nd_file_hdf import FileWriteMode, NDFileHDF -from .nd_plugin import convert_ad_dtype_to_np +from ._core_io import NDFileHDFIO +from ._utils import FileWriteMode, convert_ad_dtype_to_np -class HDFWriter(DetectorWriter): +class ADHDFWriter(DetectorWriter): def __init__( self, - hdf: NDFileHDF, + hdf: NDFileHDFIO, path_provider: PathProvider, name_provider: NameProvider, shape_provider: ShapeProvider, @@ -36,8 +37,8 @@ def __init__( self._shape_provider = shape_provider self._scalar_datasets_paths = scalar_datasets_paths self._capture_status: Optional[AsyncStatus] = None - self._datasets: List[_HDFDataset] = [] - self._file: Optional[_HDFFile] = None + self._datasets: List[HDFDataset] = [] + self._file: Optional[HDFFile] = None self._multiplier = 1 async def open(self, multiplier: int = 1) -> Dict[str, DataKey]: @@ -80,7 +81,7 @@ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]: # Add the main data self._datasets = [ - _HDFDataset( + HDFDataset( data_key=name, dataset="/entry/data/data", shape=frame_shape, @@ -91,7 +92,7 @@ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]: # And all the scalar datasets for ds_name, ds_path in self._scalar_datasets_paths.items(): self._datasets.append( - _HDFDataset( + HDFDataset( f"{name}-{ds_name}", f"/entry/instrument/NDAttributes/{ds_path}", (), @@ -131,7 +132,7 @@ async def collect_stream_docs( if indices_written: if not self._file: path = Path(await self.hdf.full_file_name.get_value()) - self._file = _HDFFile( + self._file = HDFFile( self._path_provider(), # See https://github.com/bluesky/ophyd-async/issues/122 path, diff --git a/src/ophyd_async/epics/areadetector/single_trigger_det.py b/src/ophyd_async/epics/adcore/_single_trigger.py similarity index 81% rename from src/ophyd_async/epics/areadetector/single_trigger_det.py rename to src/ophyd_async/epics/adcore/_single_trigger.py index a657c5b966..8cad9420b5 100644 --- a/src/ophyd_async/epics/areadetector/single_trigger_det.py +++ b/src/ophyd_async/epics/adcore/_single_trigger.py @@ -11,18 +11,17 @@ StandardReadable, ) -from .drivers.ad_base import ADBase -from .utils import ImageMode -from .writers.nd_plugin import NDPluginBase +from ._core_io import ADBaseIO, NDPluginBaseIO +from ._utils import ImageMode -class SingleTriggerDet(StandardReadable, Triggerable): +class SingleTriggerDetector(StandardReadable, Triggerable): def __init__( self, - drv: ADBase, + drv: ADBaseIO, read_uncached: Sequence[SignalR] = (), name="", - **plugins: NDPluginBase, + **plugins: NDPluginBaseIO, ) -> None: self.drv = drv self.__dict__.update(plugins) diff --git a/src/ophyd_async/epics/areadetector/utils.py b/src/ophyd_async/epics/adcore/_utils.py similarity index 78% rename from src/ophyd_async/epics/areadetector/utils.py rename to src/ophyd_async/epics/adcore/_utils.py index 2aa1a4efde..0aa1303971 100644 --- a/src/ophyd_async/epics/areadetector/utils.py +++ b/src/ophyd_async/epics/adcore/_utils.py @@ -5,6 +5,35 @@ from ophyd_async.core import DEFAULT_TIMEOUT, SignalRW, T, wait_for_value +class ADBaseDataType(str, Enum): + Int8 = "Int8" + UInt8 = "UInt8" + Int16 = "Int16" + UInt16 = "UInt16" + Int32 = "Int32" + UInt32 = "UInt32" + Int64 = "Int64" + UInt64 = "UInt64" + Float32 = "Float32" + Float64 = "Float64" + + +def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str: + ad_dtype_to_np_dtype = { + ADBaseDataType.Int8: "|i1", + ADBaseDataType.UInt8: "|u1", + ADBaseDataType.Int16: " None: self._drv = driver @@ -36,14 +33,14 @@ async def arm( await asyncio.gather( self._drv.trigger_mode.set(KINETIX_TRIGGER_MODE_MAP[trigger]), self._drv.num_images.set(num), - self._drv.image_mode.set(ImageMode.multiple), + self._drv.image_mode.set(adcore.ImageMode.multiple), ) if exposure is not None and trigger not in [ DetectorTrigger.variable_gate, DetectorTrigger.constant_gate, ]: await self._drv.acquire_time.set(exposure) - return await start_acquiring_driver_and_ensure_status(self._drv) + return await adcore.start_acquiring_driver_and_ensure_status(self._drv) async def disarm(self): - await stop_busy_record(self._drv.acquire, False, timeout=1) + await adcore.stop_busy_record(self._drv.acquire, False, timeout=1) diff --git a/src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py b/src/ophyd_async/epics/adkinetix/_kinetix_io.py similarity index 74% rename from src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py rename to src/ophyd_async/epics/adkinetix/_kinetix_io.py index b3497bee0b..c5144f1545 100644 --- a/src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py +++ b/src/ophyd_async/epics/adkinetix/_kinetix_io.py @@ -1,8 +1,7 @@ from enum import Enum -from ophyd_async.epics.signal.signal import epics_signal_rw_rbv - -from .ad_base import ADBase +from ophyd_async.epics import adcore +from ophyd_async.epics.signal import epics_signal_rw_rbv class KinetixTriggerMode(str, Enum): @@ -17,7 +16,9 @@ class KinetixReadoutMode(str, Enum): dynamic_range = 3 -class KinetixDriver(ADBase): +class KinetixDriverIO(adcore.ADBaseIO): + """This mirrors the interface provided by ADKinetix/db/ADKinetix.template.""" + def __init__(self, prefix: str, name: str = "") -> None: # self.pixel_format = epics_signal_rw_rbv(PixelFormat, prefix + "PixelFormat") self.trigger_mode = epics_signal_rw_rbv( diff --git a/src/ophyd_async/epics/adpilatus/__init__.py b/src/ophyd_async/epics/adpilatus/__init__.py new file mode 100644 index 0000000000..66bcd2feff --- /dev/null +++ b/src/ophyd_async/epics/adpilatus/__init__.py @@ -0,0 +1,11 @@ +from ._pilatus import PilatusDetector, PilatusReadoutTime +from ._pilatus_controller import PilatusController +from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode + +__all__ = [ + "PilatusDetector", + "PilatusReadoutTime", + "PilatusController", + "PilatusDriverIO", + "PilatusTriggerMode", +] diff --git a/src/ophyd_async/epics/areadetector/pilatus.py b/src/ophyd_async/epics/adpilatus/_pilatus.py similarity index 65% rename from src/ophyd_async/epics/areadetector/pilatus.py rename to src/ophyd_async/epics/adpilatus/_pilatus.py index a40ce9cedb..bb42471e3a 100644 --- a/src/ophyd_async/epics/areadetector/pilatus.py +++ b/src/ophyd_async/epics/adpilatus/_pilatus.py @@ -2,15 +2,11 @@ from bluesky.protocols import Hints -from ophyd_async.core import PathProvider -from ophyd_async.core.detector import StandardDetector -from ophyd_async.epics.areadetector.controllers.pilatus_controller import ( - PilatusController, -) -from ophyd_async.epics.areadetector.drivers.ad_base import ADBaseShapeProvider -from ophyd_async.epics.areadetector.drivers.pilatus_driver import PilatusDriver -from ophyd_async.epics.areadetector.writers.hdf_writer import HDFWriter -from ophyd_async.epics.areadetector.writers.nd_file_hdf import NDFileHDF +from ophyd_async.core import PathProvider, StandardDetector +from ophyd_async.epics import adcore + +from ._pilatus_controller import PilatusController +from ._pilatus_io import PilatusDriverIO #: Cite: https://media.dectris.com/User_Manual-PILATUS2-V1_4.pdf @@ -31,7 +27,7 @@ class PilatusDetector(StandardDetector): """A Pilatus StandardDetector writing HDF files""" _controller: PilatusController - _writer: HDFWriter + _writer: adcore.ADHDFWriter def __init__( self, @@ -42,16 +38,16 @@ def __init__( hdf_suffix: str = "HDF1:", name: str = "", ): - self.drv = PilatusDriver(prefix + drv_suffix) - self.hdf = NDFileHDF(prefix + hdf_suffix) + self.drv = PilatusDriverIO(prefix + drv_suffix) + self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix) super().__init__( PilatusController(self.drv, readout_time=readout_time.value), - HDFWriter( + adcore.ADHDFWriter( self.hdf, path_provider, lambda: self.name, - ADBaseShapeProvider(self.drv), + adcore.ADBaseShapeProvider(self.drv), ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/src/ophyd_async/epics/areadetector/controllers/pilatus_controller.py b/src/ophyd_async/epics/adpilatus/_pilatus_controller.py similarity index 69% rename from src/ophyd_async/epics/areadetector/controllers/pilatus_controller.py rename to src/ophyd_async/epics/adpilatus/_pilatus_controller.py index 5b9c1ee8bc..c45f402dd0 100644 --- a/src/ophyd_async/epics/areadetector/controllers/pilatus_controller.py +++ b/src/ophyd_async/epics/adpilatus/_pilatus_controller.py @@ -1,18 +1,16 @@ import asyncio from typing import Optional -from ophyd_async.core import DEFAULT_TIMEOUT, wait_for_value -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.detector import DetectorControl, DetectorTrigger -from ophyd_async.epics.areadetector.drivers.ad_base import ( - set_exposure_time_and_acquire_period_if_supplied, - start_acquiring_driver_and_ensure_status, +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + DetectorControl, + DetectorTrigger, + wait_for_value, ) -from ophyd_async.epics.areadetector.drivers.pilatus_driver import ( - PilatusDriver, - PilatusTriggerMode, -) -from ophyd_async.epics.areadetector.utils import ImageMode, stop_busy_record +from ophyd_async.epics import adcore + +from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode class PilatusController(DetectorControl): @@ -24,7 +22,7 @@ class PilatusController(DetectorControl): def __init__( self, - driver: PilatusDriver, + driver: PilatusDriverIO, readout_time: float, ) -> None: self._drv = driver @@ -40,17 +38,17 @@ async def arm( exposure: Optional[float] = None, ) -> AsyncStatus: if exposure is not None: - await set_exposure_time_and_acquire_period_if_supplied( + await adcore.set_exposure_time_and_acquire_period_if_supplied( self, self._drv, exposure ) await asyncio.gather( self._drv.trigger_mode.set(self._get_trigger_mode(trigger)), self._drv.num_images.set(999_999 if num == 0 else num), - self._drv.image_mode.set(ImageMode.multiple), + self._drv.image_mode.set(adcore.ImageMode.multiple), ) # Standard arm the detector and wait for the acquire PV to be True - idle_status = await start_acquiring_driver_and_ensure_status(self._drv) + idle_status = await adcore.start_acquiring_driver_and_ensure_status(self._drv) # The pilatus has an additional PV that goes True when the camserver # is actually ready. Should wait for that too or we risk dropping @@ -74,4 +72,4 @@ def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode: return cls._supported_trigger_types[trigger] async def disarm(self): - await stop_busy_record(self._drv.acquire, False, timeout=1) + await adcore.stop_busy_record(self._drv.acquire, False, timeout=1) diff --git a/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py b/src/ophyd_async/epics/adpilatus/_pilatus_io.py similarity index 68% rename from src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py rename to src/ophyd_async/epics/adpilatus/_pilatus_io.py index 1d8868d52c..db27075875 100644 --- a/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py +++ b/src/ophyd_async/epics/adpilatus/_pilatus_io.py @@ -1,7 +1,7 @@ from enum import Enum -from ...signal import epics_signal_r, epics_signal_rw_rbv -from .ad_base import ADBase +from ophyd_async.epics import adcore +from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw_rbv class PilatusTriggerMode(str, Enum): @@ -12,7 +12,9 @@ class PilatusTriggerMode(str, Enum): alignment = "Alignment" -class PilatusDriver(ADBase): +class PilatusDriverIO(adcore.ADBaseIO): + """This mirrors the interface provided by ADPilatus/db/pilatus.template.""" + def __init__(self, prefix: str, name: str = "") -> None: self.trigger_mode = epics_signal_rw_rbv( PilatusTriggerMode, prefix + "TriggerMode" diff --git a/src/ophyd_async/epics/adsimdetector/__init__.py b/src/ophyd_async/epics/adsimdetector/__init__.py new file mode 100644 index 0000000000..9904acb2e6 --- /dev/null +++ b/src/ophyd_async/epics/adsimdetector/__init__.py @@ -0,0 +1,7 @@ +from ._sim import SimDetector +from ._sim_controller import SimController + +__all__ = [ + "SimDetector", + "SimController", +] diff --git a/src/ophyd_async/epics/demo/demo_ad_sim_detector.py b/src/ophyd_async/epics/adsimdetector/_sim.py similarity index 53% rename from src/ophyd_async/epics/demo/demo_ad_sim_detector.py rename to src/ophyd_async/epics/adsimdetector/_sim.py index 07d3a6d24a..609ff99227 100644 --- a/src/ophyd_async/epics/demo/demo_ad_sim_detector.py +++ b/src/ophyd_async/epics/adsimdetector/_sim.py @@ -1,20 +1,19 @@ from typing import Sequence from ophyd_async.core import PathProvider, SignalR, StandardDetector +from ophyd_async.epics import adcore -from ..areadetector.controllers import ADSimController -from ..areadetector.drivers import ADBase, ADBaseShapeProvider -from ..areadetector.writers import HDFWriter, NDFileHDF +from ._sim_controller import SimController -class DemoADSimDetector(StandardDetector): - _controller: ADSimController - _writer: HDFWriter +class SimDetector(StandardDetector): + _controller: SimController + _writer: adcore.ADHDFWriter def __init__( self, - drv: ADBase, - hdf: NDFileHDF, + drv: adcore.ADBaseIO, + hdf: adcore.NDFileHDFIO, path_provider: PathProvider, name: str = "", config_sigs: Sequence[SignalR] = (), @@ -23,12 +22,12 @@ def __init__( self.hdf = hdf super().__init__( - ADSimController(self.drv), - HDFWriter( + SimController(self.drv), + adcore.ADHDFWriter( self.hdf, path_provider, lambda: self.name, - ADBaseShapeProvider(self.drv), + adcore.ADBaseShapeProvider(self.drv), ), config_sigs=config_sigs, name=name, diff --git a/src/ophyd_async/epics/areadetector/controllers/ad_sim_controller.py b/src/ophyd_async/epics/adsimdetector/_sim_controller.py similarity index 67% rename from src/ophyd_async/epics/areadetector/controllers/ad_sim_controller.py rename to src/ophyd_async/epics/adsimdetector/_sim_controller.py index ea0d9684fb..789f89701c 100644 --- a/src/ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +++ b/src/ophyd_async/epics/adsimdetector/_sim_controller.py @@ -7,20 +7,14 @@ DetectorControl, DetectorTrigger, ) - -from ..drivers.ad_base import ( - DEFAULT_GOOD_STATES, - ADBase, - DetectorState, - ImageMode, - start_acquiring_driver_and_ensure_status, -) -from ..utils import stop_busy_record +from ophyd_async.epics import adcore -class ADSimController(DetectorControl): +class SimController(DetectorControl): def __init__( - self, driver: ADBase, good_states: Set[DetectorState] = set(DEFAULT_GOOD_STATES) + self, + driver: adcore.ADBaseIO, + good_states: Set[adcore.DetectorState] = set(adcore.DEFAULT_GOOD_STATES), ) -> None: self.driver = driver self.good_states = good_states @@ -40,13 +34,13 @@ async def arm( frame_timeout = DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value() await asyncio.gather( self.driver.num_images.set(num), - self.driver.image_mode.set(ImageMode.multiple), + self.driver.image_mode.set(adcore.ImageMode.multiple), ) - return await start_acquiring_driver_and_ensure_status( + return await adcore.start_acquiring_driver_and_ensure_status( self.driver, good_states=self.good_states, timeout=frame_timeout ) async def disarm(self): # We can't use caput callback as we already used it in arm() and we can't have # 2 or they will deadlock - await stop_busy_record(self.driver.acquire, False, timeout=1) + await adcore.stop_busy_record(self.driver.acquire, False, timeout=1) diff --git a/src/ophyd_async/epics/advimba/__init__.py b/src/ophyd_async/epics/advimba/__init__.py new file mode 100644 index 0000000000..0a02423c78 --- /dev/null +++ b/src/ophyd_async/epics/advimba/__init__.py @@ -0,0 +1,9 @@ +from ._vimba import VimbaDetector +from ._vimba_controller import VimbaController +from ._vimba_io import VimbaDriverIO + +__all__ = [ + "VimbaDetector", + "VimbaController", + "VimbaDriverIO", +] diff --git a/src/ophyd_async/epics/areadetector/vimba.py b/src/ophyd_async/epics/advimba/_vimba.py similarity index 60% rename from src/ophyd_async/epics/areadetector/vimba.py rename to src/ophyd_async/epics/advimba/_vimba.py index 08610922e9..40f3f40b66 100644 --- a/src/ophyd_async/epics/areadetector/vimba.py +++ b/src/ophyd_async/epics/advimba/_vimba.py @@ -1,10 +1,10 @@ from bluesky.protocols import HasHints, Hints from ophyd_async.core import PathProvider, StandardDetector -from ophyd_async.epics.areadetector.controllers.vimba_controller import VimbaController -from ophyd_async.epics.areadetector.drivers import ADBaseShapeProvider -from ophyd_async.epics.areadetector.drivers.vimba_driver import VimbaDriver -from ophyd_async.epics.areadetector.writers import HDFWriter, NDFileHDF +from ophyd_async.epics import adcore + +from ._vimba_controller import VimbaController +from ._vimba_io import VimbaDriverIO class VimbaDetector(StandardDetector, HasHints): @@ -13,7 +13,7 @@ class VimbaDetector(StandardDetector, HasHints): """ _controller: VimbaController - _writer: HDFWriter + _writer: adcore.ADHDFWriter def __init__( self, @@ -23,16 +23,16 @@ def __init__( hdf_suffix="HDF1:", name="", ): - self.drv = VimbaDriver(prefix + drv_suffix) - self.hdf = NDFileHDF(prefix + hdf_suffix) + self.drv = VimbaDriverIO(prefix + drv_suffix) + self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix) super().__init__( VimbaController(self.drv), - HDFWriter( + adcore.ADHDFWriter( self.hdf, path_provider, lambda: self.name, - ADBaseShapeProvider(self.drv), + adcore.ADBaseShapeProvider(self.drv), ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/src/ophyd_async/epics/areadetector/controllers/vimba_controller.py b/src/ophyd_async/epics/advimba/_vimba_controller.py similarity index 76% rename from src/ophyd_async/epics/areadetector/controllers/vimba_controller.py rename to src/ophyd_async/epics/advimba/_vimba_controller.py index 82fe420281..1b898a691c 100644 --- a/src/ophyd_async/epics/areadetector/controllers/vimba_controller.py +++ b/src/ophyd_async/epics/advimba/_vimba_controller.py @@ -2,17 +2,9 @@ from typing import Optional from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger -from ophyd_async.epics.areadetector.drivers.ad_base import ( - start_acquiring_driver_and_ensure_status, -) +from ophyd_async.epics import adcore -from ..drivers.vimba_driver import ( - VimbaDriver, - VimbaExposeOutMode, - VimbaOnOff, - VimbaTriggerSource, -) -from ..utils import ImageMode, stop_busy_record +from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource TRIGGER_MODE = { DetectorTrigger.internal: VimbaOnOff.off, @@ -32,7 +24,7 @@ class VimbaController(DetectorControl): def __init__( self, - driver: VimbaDriver, + driver: VimbaDriverIO, ) -> None: self._drv = driver @@ -49,7 +41,7 @@ async def arm( self._drv.trigger_mode.set(TRIGGER_MODE[trigger]), self._drv.expose_mode.set(EXPOSE_OUT_MODE[trigger]), self._drv.num_images.set(num), - self._drv.image_mode.set(ImageMode.multiple), + self._drv.image_mode.set(adcore.ImageMode.multiple), ) if exposure is not None and trigger not in [ DetectorTrigger.variable_gate, @@ -60,7 +52,7 @@ async def arm( self._drv.trig_source.set(VimbaTriggerSource.line1) else: self._drv.trig_source.set(VimbaTriggerSource.freerun) - return await start_acquiring_driver_and_ensure_status(self._drv) + return await adcore.start_acquiring_driver_and_ensure_status(self._drv) async def disarm(self): - await stop_busy_record(self._drv.acquire, False, timeout=1) + await adcore.stop_busy_record(self._drv.acquire, False, timeout=1) diff --git a/src/ophyd_async/epics/areadetector/drivers/vimba_driver.py b/src/ophyd_async/epics/advimba/_vimba_io.py similarity index 88% rename from src/ophyd_async/epics/areadetector/drivers/vimba_driver.py rename to src/ophyd_async/epics/advimba/_vimba_io.py index 0f2b69ebc2..9304597936 100644 --- a/src/ophyd_async/epics/areadetector/drivers/vimba_driver.py +++ b/src/ophyd_async/epics/advimba/_vimba_io.py @@ -1,8 +1,7 @@ from enum import Enum -from ophyd_async.epics.signal.signal import epics_signal_rw_rbv - -from .ad_base import ADBase +from ophyd_async.epics import adcore +from ophyd_async.epics.signal import epics_signal_rw_rbv class VimbaPixelFormat(str, Enum): @@ -46,7 +45,9 @@ class VimbaExposeOutMode(str, Enum): trigger_width = "TriggerWidth" # Expose for length of high signal -class VimbaDriver(ADBase): +class VimbaDriverIO(adcore.ADBaseIO): + """This mirrors the interface provided by ADVimba/db/vimba.template.""" + def __init__(self, prefix: str, name: str = "") -> None: # self.pixel_format = epics_signal_rw_rbv(PixelFormat, prefix + "PixelFormat") self.convert_format = epics_signal_rw_rbv( diff --git a/src/ophyd_async/epics/areadetector/__init__.py b/src/ophyd_async/epics/areadetector/__init__.py deleted file mode 100644 index 7678cd384c..0000000000 --- a/src/ophyd_async/epics/areadetector/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from .aravis import AravisDetector -from .kinetix import KinetixDetector -from .pilatus import PilatusDetector -from .single_trigger_det import SingleTriggerDet -from .utils import ( - FileWriteMode, - ImageMode, - NDAttributeDataType, - NDAttributesXML, -) -from .vimba import VimbaDetector - -__all__ = [ - "AravisDetector", - "KinetixDetector", - "VimbaDetector", - "SingleTriggerDet", - "FileWriteMode", - "ImageMode", - "NDAttributeDataType", - "NDAttributesXML", - "PilatusDetector", -] diff --git a/src/ophyd_async/epics/areadetector/controllers/__init__.py b/src/ophyd_async/epics/areadetector/controllers/__init__.py deleted file mode 100644 index 5049a5077f..0000000000 --- a/src/ophyd_async/epics/areadetector/controllers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .ad_sim_controller import ADSimController -from .aravis_controller import AravisController -from .pilatus_controller import PilatusController - -__all__ = ["PilatusController", "ADSimController", "AravisController"] diff --git a/src/ophyd_async/epics/areadetector/drivers/__init__.py b/src/ophyd_async/epics/areadetector/drivers/__init__.py deleted file mode 100644 index fae5533fbc..0000000000 --- a/src/ophyd_async/epics/areadetector/drivers/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from .ad_base import ( - ADBase, - ADBaseShapeProvider, - DetectorState, - set_exposure_time_and_acquire_period_if_supplied, - start_acquiring_driver_and_ensure_status, -) -from .aravis_driver import AravisDriver -from .kinetix_driver import KinetixDriver -from .pilatus_driver import PilatusDriver -from .vimba_driver import VimbaDriver - -__all__ = [ - "ADBase", - "ADBaseShapeProvider", - "PilatusDriver", - "AravisDriver", - "KinetixDriver", - "VimbaDriver", - "start_acquiring_driver_and_ensure_status", - "set_exposure_time_and_acquire_period_if_supplied", - "DetectorState", -] diff --git a/src/ophyd_async/epics/areadetector/writers/__init__.py b/src/ophyd_async/epics/areadetector/writers/__init__.py deleted file mode 100644 index 37ca55ccfd..0000000000 --- a/src/ophyd_async/epics/areadetector/writers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .hdf_writer import HDFWriter -from .nd_file_hdf import NDFileHDF -from .nd_plugin import ADBaseDataType, NDPluginBase, NDPluginStats - -__all__ = ["HDFWriter", "NDFileHDF", "NDPluginBase", "NDPluginStats", "ADBaseDataType"] diff --git a/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py b/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py deleted file mode 100644 index 9e84be5bf1..0000000000 --- a/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py +++ /dev/null @@ -1,43 +0,0 @@ -from enum import Enum - -from ...signal.signal import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv -from ..utils import FileWriteMode -from .nd_plugin import NDPluginBase - - -class Compression(str, Enum): - none = "None" - nbit = "N-bit" - szip = "szip" - zlib = "zlib" - blosc = "Blosc" - bslz4 = "BSLZ4" - lz4 = "LZ4" - jpeg = "JPEG" - - -class NDFileHDF(NDPluginBase): - def __init__(self, prefix: str, name="") -> None: - # Define some signals - self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode") - self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression") - self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims") - self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath") - self.file_name = epics_signal_rw_rbv(str, prefix + "FileName") - self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV") - self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate") - self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV") - self.file_write_mode = epics_signal_rw_rbv( - FileWriteMode, prefix + "FileWriteMode" - ) - self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture") - self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV") - self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode") - self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen") - self.capture = epics_signal_rw_rbv(bool, prefix + "Capture") - self.flush_now = epics_signal_rw(bool, prefix + "FlushNow") - self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName") - self.array_size0 = epics_signal_r(int, prefix + "ArraySize0") - self.array_size1 = epics_signal_r(int, prefix + "ArraySize1") - self.create_dir_depth = epics_signal_rw(int, prefix + "CreateDirectory") - super().__init__(prefix, name) diff --git a/src/ophyd_async/epics/areadetector/writers/nd_plugin.py b/src/ophyd_async/epics/areadetector/writers/nd_plugin.py deleted file mode 100644 index 89069b80f1..0000000000 --- a/src/ophyd_async/epics/areadetector/writers/nd_plugin.py +++ /dev/null @@ -1,68 +0,0 @@ -from enum import Enum - -from ophyd_async.core import Device -from ophyd_async.epics.signal import epics_signal_rw -from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv - - -class Callback(str, Enum): - Enable = "Enable" - Disable = "Disable" - - -class ADBaseDataType(str, Enum): - Int8 = "Int8" - UInt8 = "UInt8" - Int16 = "Int16" - UInt16 = "UInt16" - Int32 = "Int32" - UInt32 = "UInt32" - Int64 = "Int64" - UInt64 = "UInt64" - Float32 = "Float32" - Float64 = "Float64" - - -def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str: - ad_dtype_to_np_dtype = { - ADBaseDataType.Int8: "|i1", - ADBaseDataType.UInt8: "|u1", - ADBaseDataType.Int16: " None: - self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV") - self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile") - self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire") - self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV") - self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV") - self.data_type = epics_signal_r(ADBaseDataType, prefix + "NDDataType_RBV") - self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter") - # There is no _RBV for this one - self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins") - - super().__init__(name=name) - - -class NDPluginBase(NDArrayBase): - def __init__(self, prefix: str, name: str = "") -> None: - self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort") - self.enable_callback = epics_signal_rw_rbv(Callback, prefix + "EnableCallbacks") - self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress") - self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV") - self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV") - super().__init__(prefix, name) - - -class NDPluginStats(NDPluginBase): - pass diff --git a/src/ophyd_async/epics/demo/__init__.py b/src/ophyd_async/epics/demo/__init__.py index 13d73bb4b3..3e3d602f91 100644 --- a/src/ophyd_async/epics/demo/__init__.py +++ b/src/ophyd_async/epics/demo/__init__.py @@ -1,145 +1,22 @@ """Demo EPICS Devices for the tutorial""" -import asyncio import atexit import random import string import subprocess import sys -from enum import Enum from pathlib import Path -import numpy as np -from bluesky.protocols import Movable, Stoppable +from ._mover import Mover, SampleStage +from ._sensor import EnergyMode, Sensor, SensorGroup -from ophyd_async.core import ( - ConfigSignal, - Device, - DeviceVector, - HintedSignal, - StandardReadable, - WatchableAsyncStatus, - observe_value, -) -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.utils import ( - DEFAULT_TIMEOUT, - CalculatableTimeout, - CalculateTimeout, - WatcherUpdate, -) - -from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x - - -class EnergyMode(str, Enum): - """Energy mode for `Sensor`""" - - #: Low energy mode - low = "Low Energy" - #: High energy mode - high = "High Energy" - - -class Sensor(StandardReadable): - """A demo sensor that produces a scalar value based on X and Y Movers""" - - def __init__(self, prefix: str, name="") -> None: - # Define some signals - 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) - - -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) - - -class Mover(StandardReadable, Movable, Stoppable): - """A demo movable that moves based on velocity""" - - def __init__(self, prefix: str, name="") -> None: - # Define some signals - with self.add_children_as_readables(HintedSignal): - self.readback = epics_signal_r(float, prefix + "Readback") - with self.add_children_as_readables(ConfigSignal): - self.velocity = epics_signal_rw(float, prefix + "Velocity") - self.units = epics_signal_r(str, prefix + "Readback.EGU") - self.setpoint = epics_signal_rw(float, prefix + "Setpoint") - self.precision = epics_signal_r(int, prefix + "Readback.PREC") - # Signals that collide with standard methods should have a trailing underscore - self.stop_ = epics_signal_x(prefix + "Stop.PROC") - # Whether set() should complete successfully or not - self._set_success = True - - super().__init__(name=name) - - def set_name(self, name: str): - super().set_name(name) - # Readback should be named the same as its parent in read() - self.readback.set_name(name) - - @WatchableAsyncStatus.wrap - async def set( - self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout - ): - self._set_success = True - old_position, units, precision, velocity = await asyncio.gather( - self.setpoint.get_value(), - self.units.get_value(), - self.precision.get_value(), - self.velocity.get_value(), - ) - if timeout is CalculateTimeout: - assert velocity > 0, "Mover has zero velocity" - timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT - # Make an Event that will be set on completion, and a Status that will - # error if not done in time - done = asyncio.Event() - done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout)) - # Wait for the value to set, but don't wait for put completion callback - await self.setpoint.set(new_position, wait=False) - async for current_position in observe_value( - self.readback, done_status=done_status - ): - yield WatcherUpdate( - current=current_position, - initial=old_position, - target=new_position, - name=self.name, - unit=units, - precision=precision, - ) - if np.isclose(current_position, new_position): - done.set() - break - if not self._set_success: - raise RuntimeError("Motor was stopped") - - async def stop(self, success=True): - self._set_success = success - status = self.stop_.trigger() - await status - - -class SampleStage(Device): - """A demo sample stage with X and Y movables""" - - def __init__(self, prefix: str, name="") -> None: - # Define some child Devices - self.x = Mover(prefix + "X:") - self.y = Mover(prefix + "Y:") - # Set name of device and child devices - super().__init__(name=name) +__all__ = [ + "Mover", + "SampleStage", + "EnergyMode", + "Sensor", + "SensorGroup", +] def start_ioc_subprocess() -> str: diff --git a/src/ophyd_async/epics/demo/_mover.py b/src/ophyd_async/epics/demo/_mover.py new file mode 100644 index 0000000000..31aa198e3d --- /dev/null +++ b/src/ophyd_async/epics/demo/_mover.py @@ -0,0 +1,97 @@ +import asyncio + +import numpy as np +from bluesky.protocols import Movable, Stoppable + +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + CalculatableTimeout, + CalculateTimeout, + ConfigSignal, + Device, + HintedSignal, + StandardReadable, + WatchableAsyncStatus, + WatcherUpdate, + observe_value, +) +from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x + + +class Mover(StandardReadable, Movable, Stoppable): + """A demo movable that moves based on velocity""" + + def __init__(self, prefix: str, name="") -> None: + # Define some signals + with self.add_children_as_readables(HintedSignal): + self.readback = epics_signal_r(float, prefix + "Readback") + with self.add_children_as_readables(ConfigSignal): + self.velocity = epics_signal_rw(float, prefix + "Velocity") + self.units = epics_signal_r(str, prefix + "Readback.EGU") + self.setpoint = epics_signal_rw(float, prefix + "Setpoint") + self.precision = epics_signal_r(int, prefix + "Readback.PREC") + # Signals that collide with standard methods should have a trailing underscore + self.stop_ = epics_signal_x(prefix + "Stop.PROC") + # Whether set() should complete successfully or not + self._set_success = True + + super().__init__(name=name) + + def set_name(self, name: str): + super().set_name(name) + # Readback should be named the same as its parent in read() + self.readback.set_name(name) + + @WatchableAsyncStatus.wrap + async def set( + self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout + ): + self._set_success = True + old_position, units, precision, velocity = await asyncio.gather( + self.setpoint.get_value(), + self.units.get_value(), + self.precision.get_value(), + self.velocity.get_value(), + ) + if timeout is CalculateTimeout: + assert velocity > 0, "Mover has zero velocity" + timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT + # Make an Event that will be set on completion, and a Status that will + # error if not done in time + done = asyncio.Event() + done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout)) + # Wait for the value to set, but don't wait for put completion callback + await self.setpoint.set(new_position, wait=False) + async for current_position in observe_value( + self.readback, done_status=done_status + ): + yield WatcherUpdate( + current=current_position, + initial=old_position, + target=new_position, + name=self.name, + unit=units, + precision=precision, + ) + if np.isclose(current_position, new_position): + done.set() + break + if not self._set_success: + raise RuntimeError("Motor was stopped") + + async def stop(self, success=True): + self._set_success = success + status = self.stop_.trigger() + await status + + +class SampleStage(Device): + """A demo sample stage with X and Y movables""" + + def __init__(self, prefix: str, name="") -> None: + # Define some child Devices + self.x = Mover(prefix + "X:") + self.y = Mover(prefix + "Y:") + # Set name of device and child devices + super().__init__(name=name) diff --git a/src/ophyd_async/epics/demo/_sensor.py b/src/ophyd_async/epics/demo/_sensor.py new file mode 100644 index 0000000000..37d590d155 --- /dev/null +++ b/src/ophyd_async/epics/demo/_sensor.py @@ -0,0 +1,36 @@ +from enum import Enum + +from ophyd_async.core import ConfigSignal, DeviceVector, HintedSignal, StandardReadable +from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw + + +class EnergyMode(str, Enum): + """Energy mode for `Sensor`""" + + #: Low energy mode + low = "Low Energy" + #: High energy mode + high = "High Energy" + + +class Sensor(StandardReadable): + """A demo sensor that produces a scalar value based on X and Y Movers""" + + def __init__(self, prefix: str, name="") -> None: + # Define some signals + 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) + + +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) diff --git a/src/ophyd_async/epics/motion/__init__.py b/src/ophyd_async/epics/motion/__init__.py deleted file mode 100644 index 5effa7fb6e..0000000000 --- a/src/ophyd_async/epics/motion/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .motor import Motor - -__all__ = ["Motor"] diff --git a/src/ophyd_async/epics/motion/motor.py b/src/ophyd_async/epics/motor.py similarity index 97% rename from src/ophyd_async/epics/motion/motor.py rename to src/ophyd_async/epics/motor.py index 9eca0916bf..8c20d238fe 100644 --- a/src/ophyd_async/epics/motion/motor.py +++ b/src/ophyd_async/epics/motor.py @@ -5,21 +5,18 @@ from pydantic import BaseModel, Field from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + CalculatableTimeout, + CalculateTimeout, ConfigSignal, HintedSignal, StandardReadable, WatchableAsyncStatus, -) -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.signal import observe_value -from ophyd_async.core.utils import ( - DEFAULT_TIMEOUT, - CalculatableTimeout, - CalculateTimeout, WatcherUpdate, + observe_value, ) - -from ..signal.signal 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 MotorLimitsException(Exception): diff --git a/src/ophyd_async/epics/pvi/__init__.py b/src/ophyd_async/epics/pvi/__init__.py index ad638b740c..2bdc1ae643 100644 --- a/src/ophyd_async/epics/pvi/__init__.py +++ b/src/ophyd_async/epics/pvi/__init__.py @@ -1,3 +1,3 @@ -from .pvi import PVIEntry, create_children_from_annotations, fill_pvi_entries +from ._pvi import create_children_from_annotations, fill_pvi_entries -__all__ = ["PVIEntry", "fill_pvi_entries", "create_children_from_annotations"] +__all__ = ["fill_pvi_entries", "create_children_from_annotations"] diff --git a/src/ophyd_async/epics/pvi/pvi.py b/src/ophyd_async/epics/pvi/_pvi.py similarity index 94% rename from src/ophyd_async/epics/pvi/pvi.py rename to src/ophyd_async/epics/pvi/_pvi.py index d7707ceb17..a2d8cf5f24 100644 --- a/src/ophyd_async/epics/pvi/pvi.py +++ b/src/ophyd_async/epics/pvi/_pvi.py @@ -10,25 +10,28 @@ Optional, Tuple, Type, - TypeVar, Union, get_args, get_origin, get_type_hints, ) -from ophyd_async.core import Device, DeviceVector, SoftSignalBackend -from ophyd_async.core.signal import Signal -from ophyd_async.core.utils import DEFAULT_TIMEOUT -from ophyd_async.epics._backend._p4p import PvaSignalBackend -from ophyd_async.epics.signal.signal import ( +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + Device, + DeviceVector, + Signal, + SoftSignalBackend, + T, +) +from ophyd_async.epics.signal import ( + PvaSignalBackend, epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x, ) -T = TypeVar("T") Access = FrozenSet[ Union[Literal["r"], Literal["w"], Literal["rw"], Literal["x"], Literal["d"]] ] @@ -74,19 +77,19 @@ def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device] @dataclass -class PVIEntry: +class _PVIEntry: """ A dataclass to represent a single entry in the PVI table. This could either be a signal or a sub-table. """ - sub_entries: Dict[str, Union[Dict[int, "PVIEntry"], "PVIEntry"]] + sub_entries: Dict[str, Union[Dict[int, "_PVIEntry"], "_PVIEntry"]] pvi_pv: Optional[str] = None device: Optional[Device] = None common_device_type: Optional[Type[Device]] = None -def _verify_common_blocks(entry: PVIEntry, common_device: Type[Device]): +def _verify_common_blocks(entry: _PVIEntry, common_device: Type[Device]): if not entry.sub_entries: return common_sub_devices = get_type_hints(common_device) @@ -205,7 +208,7 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None): sub_device.parent = device -async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT): +async def _get_pvi_entries(entry: _PVIEntry, timeout=DEFAULT_TIMEOUT): if not entry.pvi_pv or not entry.pvi_pv.endswith(":PVI"): raise RuntimeError("Top level entry must be a pvi table") @@ -235,7 +238,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT): else: device = getattr(entry.device, sub_name, device_type()) - sub_entry = PVIEntry( + sub_entry = _PVIEntry( device=device, common_device_type=device_type, sub_entries={} ) @@ -257,7 +260,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT): _verify_common_blocks(entry, entry.common_device_type) -def _set_device_attributes(entry: PVIEntry): +def _set_device_attributes(entry: _PVIEntry): for sub_name, sub_entry in entry.sub_entries.items(): if isinstance(sub_entry, dict): sub_device = DeviceVector() # type: ignore @@ -289,7 +292,7 @@ async def fill_pvi_entries( _mock_common_blocks(device) else: # check the pvi table for devices and fill the device with them - root_entry = PVIEntry( + root_entry = _PVIEntry( pvi_pv=root_pv, device=device, common_device_type=type(device), diff --git a/src/ophyd_async/epics/signal/__init__.py b/src/ophyd_async/epics/signal/__init__.py index 05870d33e4..8d7628bf01 100644 --- a/src/ophyd_async/epics/signal/__init__.py +++ b/src/ophyd_async/epics/signal/__init__.py @@ -1,4 +1,6 @@ -from .signal import ( +from ._common import LimitPair, Limits, get_supported_values +from ._p4p import PvaSignalBackend +from ._signal import ( epics_signal_r, epics_signal_rw, epics_signal_rw_rbv, @@ -7,6 +9,10 @@ ) __all__ = [ + "get_supported_values", + "LimitPair", + "Limits", + "PvaSignalBackend", "epics_signal_r", "epics_signal_rw", "epics_signal_rw_rbv", diff --git a/src/ophyd_async/epics/_backend/_aioca.py b/src/ophyd_async/epics/signal/_aioca.py similarity index 98% rename from src/ophyd_async/epics/_backend/_aioca.py rename to src/ophyd_async/epics/signal/_aioca.py index fbd8fb1ea1..6c45ea4ab9 100644 --- a/src/ophyd_async/epics/_backend/_aioca.py +++ b/src/ophyd_async/epics/signal/_aioca.py @@ -21,6 +21,8 @@ from epicscorelibs.ca import dbr from ophyd_async.core import ( + DEFAULT_TIMEOUT, + NotConnected, ReadingValueCallback, SignalBackend, T, @@ -28,9 +30,8 @@ get_unique, wait_for_connection, ) -from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected -from .common import LimitPair, Limits, common_meta, get_supported_values +from ._common import LimitPair, Limits, common_meta, get_supported_values dbr_to_dtype: Dict[Dbr, Dtype] = { dbr.DBR_STRING: "string", diff --git a/src/ophyd_async/epics/_backend/common.py b/src/ophyd_async/epics/signal/_common.py similarity index 96% rename from src/ophyd_async/epics/_backend/common.py rename to src/ophyd_async/epics/signal/_common.py index a976efee57..82070ef38e 100644 --- a/src/ophyd_async/epics/_backend/common.py +++ b/src/ophyd_async/epics/signal/_common.py @@ -4,7 +4,7 @@ from typing_extensions import TypedDict -from ophyd_async.core.signal_backend import RuntimeSubsetEnum +from ophyd_async.core import RuntimeSubsetEnum common_meta = { "units", diff --git a/src/ophyd_async/epics/signal/_epics_transport.py b/src/ophyd_async/epics/signal/_epics_transport.py index 145af21622..b6954c9d1c 100644 --- a/src/ophyd_async/epics/signal/_epics_transport.py +++ b/src/ophyd_async/epics/signal/_epics_transport.py @@ -5,7 +5,7 @@ from enum import Enum try: - from .._backend._aioca import CaSignalBackend + from ._aioca import CaSignalBackend except ImportError as ca_error: class CaSignalBackend: # type: ignore @@ -14,7 +14,7 @@ def __init__(*args, ca_error=ca_error, **kwargs): try: - from .._backend._p4p import PvaSignalBackend + from ._p4p import PvaSignalBackend except ImportError as pva_error: class PvaSignalBackend: # type: ignore @@ -22,7 +22,7 @@ def __init__(*args, pva_error=pva_error, **kwargs): raise NotImplementedError("PVA support not available") from pva_error -class EpicsTransport(Enum): +class _EpicsTransport(Enum): """The sorts of transport EPICS support""" #: Use Channel Access (using aioca library) diff --git a/src/ophyd_async/epics/_backend/_p4p.py b/src/ophyd_async/epics/signal/_p4p.py similarity index 98% rename from src/ophyd_async/epics/_backend/_p4p.py rename to src/ophyd_async/epics/signal/_p4p.py index 686e4a303b..deda7b7159 100644 --- a/src/ophyd_async/epics/_backend/_p4p.py +++ b/src/ophyd_async/epics/signal/_p4p.py @@ -13,17 +13,18 @@ from p4p.client.asyncio import Context, Subscription from ophyd_async.core import ( + DEFAULT_TIMEOUT, + NotConnected, ReadingValueCallback, + RuntimeSubsetEnum, SignalBackend, T, get_dtype, get_unique, wait_for_connection, ) -from ophyd_async.core.signal_backend import RuntimeSubsetEnum -from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected -from .common import LimitPair, Limits, common_meta, get_supported_values +from ._common import LimitPair, Limits, common_meta, get_supported_values # https://mdavidsaver.github.io/p4p/values.html specifier_to_dtype: Dict[str, Dtype] = { diff --git a/src/ophyd_async/epics/signal/signal.py b/src/ophyd_async/epics/signal/_signal.py similarity index 82% rename from src/ophyd_async/epics/signal/signal.py rename to src/ophyd_async/epics/signal/_signal.py index 2070f6dabb..2e50c67b65 100644 --- a/src/ophyd_async/epics/signal/signal.py +++ b/src/ophyd_async/epics/signal/_signal.py @@ -14,26 +14,27 @@ get_unique, ) -from ._epics_transport import EpicsTransport +from ._epics_transport import _EpicsTransport -_default_epics_transport = EpicsTransport.ca +_default_epics_transport = _EpicsTransport.ca -def _transport_pv(pv: str) -> Tuple[EpicsTransport, str]: +def _transport_pv(pv: str) -> Tuple[_EpicsTransport, str]: split = pv.split("://", 1) if len(split) > 1: # We got something like pva://mydevice, so use specified comms mode transport_str, pv = split - transport = EpicsTransport[transport_str] + transport = _EpicsTransport[transport_str] else: # No comms mode specified, use the default transport = _default_epics_transport return transport, pv -def _make_backend( +def _epics_signal_backend( datatype: Optional[Type[T]], read_pv: str, write_pv: str ) -> SignalBackend[T]: + """Create an epics signal backend.""" r_transport, r_pv = _transport_pv(read_pv) w_transport, w_pv = _transport_pv(write_pv) transport = get_unique({read_pv: r_transport, write_pv: w_transport}, "transports") @@ -54,7 +55,7 @@ def epics_signal_rw( write_pv: If given, use this PV to write to, otherwise use read_pv """ - backend = _make_backend(datatype, read_pv, write_pv or read_pv) + backend = _epics_signal_backend(datatype, read_pv, write_pv or read_pv) return SignalRW(backend, name=name) @@ -85,7 +86,7 @@ def epics_signal_r(datatype: Type[T], read_pv: str, name: str = "") -> SignalR[T read_pv: The PV to read and monitor """ - backend = _make_backend(datatype, read_pv, read_pv) + backend = _epics_signal_backend(datatype, read_pv, read_pv) return SignalR(backend, name=name) @@ -99,7 +100,7 @@ def epics_signal_w(datatype: Type[T], write_pv: str, name: str = "") -> SignalW[ write_pv: The PV to write to """ - backend = _make_backend(datatype, write_pv, write_pv) + backend = _epics_signal_backend(datatype, write_pv, write_pv) return SignalW(backend, name=name) @@ -111,5 +112,5 @@ def epics_signal_x(write_pv: str, name: str = "") -> SignalX: write_pv: The PV to write its initial value to on trigger """ - backend: SignalBackend = _make_backend(None, write_pv, write_pv) + backend: SignalBackend = _epics_signal_backend(None, write_pv, write_pv) return SignalX(backend, name=name) diff --git a/src/ophyd_async/epics/_backend/__init__.py b/src/ophyd_async/fastcs/__init__.py similarity index 100% rename from src/ophyd_async/epics/_backend/__init__.py rename to src/ophyd_async/fastcs/__init__.py diff --git a/tests/epics/areadetector/__init__.py b/src/ophyd_async/fastcs/odin/__init__.py similarity index 100% rename from tests/epics/areadetector/__init__.py rename to src/ophyd_async/fastcs/odin/__init__.py diff --git a/src/ophyd_async/panda/__init__.py b/src/ophyd_async/fastcs/panda/__init__.py similarity index 87% rename from src/ophyd_async/panda/__init__.py rename to src/ophyd_async/fastcs/panda/__init__.py index c0f57d1534..99c3319d4a 100644 --- a/src/ophyd_async/panda/__init__.py +++ b/src/ophyd_async/fastcs/panda/__init__.py @@ -10,8 +10,11 @@ TimeUnits, ) from ._hdf_panda import HDFPanda +from ._hdf_writer import PandaHDFWriter from ._panda_controller import PandaPcapController from ._table import ( + DatasetTable, + PandaHdf5DatasetType, SeqTable, SeqTableRow, SeqTrigger, @@ -28,25 +31,27 @@ __all__ = [ "CommonPandaBlocks", - "HDFPanda", - "PcompBlock", - "PcompInfo", - "PcompDirectionOptions", + "DataBlock", "EnableDisableOptions", "PcapBlock", + "PcompBlock", + "PcompDirectionOptions", "PulseBlock", - "seq_table_from_arrays", - "seq_table_from_rows", "SeqBlock", - "SeqTableInfo", + "TimeUnits", + "HDFPanda", + "PandaHDFWriter", + "PandaPcapController", + "DatasetTable", + "PandaHdf5DatasetType", "SeqTable", "SeqTableRow", "SeqTrigger", - "phase_sorter", - "PandaPcapController", - "TimeUnits", - "DataBlock", - "CommonPandABlocks", - "StaticSeqTableTriggerLogic", + "seq_table_from_arrays", + "seq_table_from_rows", + "PcompInfo", + "SeqTableInfo", "StaticPcompTriggerLogic", + "StaticSeqTableTriggerLogic", + "phase_sorter", ] diff --git a/src/ophyd_async/panda/_common_blocks.py b/src/ophyd_async/fastcs/panda/_common_blocks.py similarity index 92% rename from src/ophyd_async/panda/_common_blocks.py rename to src/ophyd_async/fastcs/panda/_common_blocks.py index 512de41d69..56f15c4134 100644 --- a/src/ophyd_async/panda/_common_blocks.py +++ b/src/ophyd_async/fastcs/panda/_common_blocks.py @@ -2,9 +2,9 @@ from enum import Enum -from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW -from ophyd_async.core.signal_backend import SubsetEnum -from ophyd_async.panda._table import DatasetTable, SeqTable +from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW, SubsetEnum + +from ._table import DatasetTable, SeqTable class DataBlock(Device): diff --git a/src/ophyd_async/panda/_hdf_panda.py b/src/ophyd_async/fastcs/panda/_hdf_panda.py similarity index 88% rename from src/ophyd_async/panda/_hdf_panda.py rename to src/ophyd_async/fastcs/panda/_hdf_panda.py index 5c3b765bef..55d4f825ad 100644 --- a/src/ophyd_async/panda/_hdf_panda.py +++ b/src/ophyd_async/fastcs/panda/_hdf_panda.py @@ -2,17 +2,12 @@ from typing import Sequence -from ophyd_async.core import ( - DEFAULT_TIMEOUT, - PathProvider, - SignalR, - StandardDetector, -) +from ophyd_async.core import DEFAULT_TIMEOUT, PathProvider, SignalR, StandardDetector from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries from ._common_blocks import CommonPandaBlocks +from ._hdf_writer import PandaHDFWriter from ._panda_controller import PandaPcapController -from .writers._hdf_writer import PandaHDFWriter class HDFPanda(CommonPandaBlocks, StandardDetector): diff --git a/src/ophyd_async/panda/writers/_hdf_writer.py b/src/ophyd_async/fastcs/panda/_hdf_writer.py similarity index 92% rename from src/ophyd_async/panda/writers/_hdf_writer.py rename to src/ophyd_async/fastcs/panda/_hdf_writer.py index 995d758baf..b46ef97c66 100644 --- a/src/ophyd_async/panda/writers/_hdf_writer.py +++ b/src/ophyd_async/fastcs/panda/_hdf_writer.py @@ -8,14 +8,15 @@ from ophyd_async.core import ( DEFAULT_TIMEOUT, DetectorWriter, + HDFDataset, + HDFFile, NameProvider, PathProvider, + observe_value, wait_for_value, ) -from ophyd_async.core.signal import observe_value -from ophyd_async.epics.areadetector.writers.general_hdffile import _HDFDataset, _HDFFile -from .._common_blocks import CommonPandaBlocks +from ._common_blocks import CommonPandaBlocks class PandaHDFWriter(DetectorWriter): @@ -32,8 +33,8 @@ def __init__( self._prefix = prefix self._path_provider = path_provider self._name_provider = name_provider - self._datasets: List[_HDFDataset] = [] - self._file: Optional[_HDFFile] = None + self._datasets: List[HDFDataset] = [] + self._file: Optional[HDFFile] = None self._multiplier = 1 # Triggered on PCAP arm @@ -93,7 +94,7 @@ async def _update_datasets(self) -> None: capture_table = await self.panda_device.data.datasets.get_value() self._datasets = [ - _HDFDataset(dataset_name, "/" + dataset_name, [1], multiplier=1) + HDFDataset(dataset_name, "/" + dataset_name, [1], multiplier=1) for dataset_name in capture_table["name"] ] @@ -128,7 +129,7 @@ async def collect_stream_docs( # TODO: fail if we get dropped frames if indices_written: if not self._file: - self._file = _HDFFile( + self._file = HDFFile( self._path_provider(), Path(await self.panda_device.data.hdf_directory.get_value()) / Path(await self.panda_device.data.hdf_file_name.get_value()), diff --git a/src/ophyd_async/panda/_panda_controller.py b/src/ophyd_async/fastcs/panda/_panda_controller.py similarity index 96% rename from src/ophyd_async/panda/_panda_controller.py rename to src/ophyd_async/fastcs/panda/_panda_controller.py index 6000909576..04f953a5ef 100644 --- a/src/ophyd_async/panda/_panda_controller.py +++ b/src/ophyd_async/fastcs/panda/_panda_controller.py @@ -7,7 +7,8 @@ DetectorTrigger, wait_for_value, ) -from ophyd_async.panda import PcapBlock + +from ._common_blocks import PcapBlock class PandaPcapController(DetectorControl): diff --git a/src/ophyd_async/panda/_table.py b/src/ophyd_async/fastcs/panda/_table.py similarity index 100% rename from src/ophyd_async/panda/_table.py rename to src/ophyd_async/fastcs/panda/_table.py diff --git a/src/ophyd_async/panda/_trigger.py b/src/ophyd_async/fastcs/panda/_trigger.py similarity index 96% rename from src/ophyd_async/panda/_trigger.py rename to src/ophyd_async/fastcs/panda/_trigger.py index d68aac9e05..010e3e07ba 100644 --- a/src/ophyd_async/panda/_trigger.py +++ b/src/ophyd_async/fastcs/panda/_trigger.py @@ -4,13 +4,9 @@ from pydantic import BaseModel, Field from ophyd_async.core import TriggerLogic, wait_for_value -from ophyd_async.panda import ( - PcompBlock, - PcompDirectionOptions, - SeqBlock, - SeqTable, - TimeUnits, -) + +from ._common_blocks import PcompBlock, PcompDirectionOptions, SeqBlock, TimeUnits +from ._table import SeqTable class SeqTableInfo(BaseModel): diff --git a/src/ophyd_async/panda/_utils.py b/src/ophyd_async/fastcs/panda/_utils.py similarity index 100% rename from src/ophyd_async/panda/_utils.py rename to src/ophyd_async/fastcs/panda/_utils.py diff --git a/src/ophyd_async/panda/writers/__init__.py b/src/ophyd_async/panda/writers/__init__.py deleted file mode 100644 index 7cc7974ea7..0000000000 --- a/src/ophyd_async/panda/writers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._hdf_writer import PandaHDFWriter - -__all__ = ["PandaHDFWriter"] diff --git a/src/ophyd_async/plan_stubs/__init__.py b/src/ophyd_async/plan_stubs/__init__.py index 360ee38b54..e402ff6e4b 100644 --- a/src/ophyd_async/plan_stubs/__init__.py +++ b/src/ophyd_async/plan_stubs/__init__.py @@ -1,5 +1,5 @@ -from .ensure_connected import ensure_connected -from .fly import ( +from ._ensure_connected import ensure_connected +from ._fly import ( fly_and_collect, prepare_static_seq_table_flyer_and_detectors_with_same_trigger, time_resolved_fly_and_collect_with_static_seq_table, diff --git a/src/ophyd_async/plan_stubs/ensure_connected.py b/src/ophyd_async/plan_stubs/_ensure_connected.py similarity index 84% rename from src/ophyd_async/plan_stubs/ensure_connected.py rename to src/ophyd_async/plan_stubs/_ensure_connected.py index 6049595971..3a64619a5c 100644 --- a/src/ophyd_async/plan_stubs/ensure_connected.py +++ b/src/ophyd_async/plan_stubs/_ensure_connected.py @@ -1,7 +1,6 @@ import bluesky.plan_stubs as bps -from ophyd_async.core.device import Device -from ophyd_async.core.utils import DEFAULT_TIMEOUT, wait_for_connection +from ophyd_async.core import DEFAULT_TIMEOUT, Device, wait_for_connection def ensure_connected( diff --git a/src/ophyd_async/plan_stubs/fly.py b/src/ophyd_async/plan_stubs/_fly.py similarity index 92% rename from src/ophyd_async/plan_stubs/fly.py rename to src/ophyd_async/plan_stubs/_fly.py index b5e93a359a..087ec62dd1 100644 --- a/src/ophyd_async/plan_stubs/fly.py +++ b/src/ophyd_async/plan_stubs/_fly.py @@ -3,10 +3,14 @@ import bluesky.plan_stubs as bps from bluesky.utils import short_uid -from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo -from ophyd_async.core.flyer import HardwareTriggeredFlyable -from ophyd_async.core.utils import in_micros -from ophyd_async.panda import ( +from ophyd_async.core import ( + DetectorTrigger, + StandardDetector, + StandardFlyer, + TriggerInfo, + in_micros, +) +from ophyd_async.fastcs.panda import ( PcompDirectionOptions, PcompInfo, SeqTable, @@ -17,7 +21,7 @@ def prepare_static_pcomp_flyer_and_detectors( - flyer: HardwareTriggeredFlyable[PcompInfo], + flyer: StandardFlyer[PcompInfo], detectors: List[StandardDetector], pcomp_info: PcompInfo, trigger_info: TriggerInfo, @@ -36,7 +40,7 @@ def prepare_static_pcomp_flyer_and_detectors( def prepare_static_seq_table_flyer_and_detectors_with_same_trigger( - flyer: HardwareTriggeredFlyable[SeqTableInfo], + flyer: StandardFlyer[SeqTableInfo], detectors: List[StandardDetector], number_of_frames: int, exposure: float, @@ -100,7 +104,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger( def fly_and_collect( stream_name: str, - flyer: HardwareTriggeredFlyable[SeqTableInfo] | HardwareTriggeredFlyable[PcompInfo], + flyer: StandardFlyer[SeqTableInfo] | StandardFlyer[PcompInfo], detectors: List[StandardDetector], ): """Kickoff, complete and collect with a flyer and multiple detectors. @@ -140,7 +144,7 @@ def fly_and_collect( def fly_and_collect_with_static_pcomp( stream_name: str, - flyer: HardwareTriggeredFlyable[PcompInfo], + flyer: StandardFlyer[PcompInfo], detectors: List[StandardDetector], number_of_pulses: int, pulse_width: int, @@ -166,7 +170,7 @@ def fly_and_collect_with_static_pcomp( def time_resolved_fly_and_collect_with_static_seq_table( stream_name: str, - flyer: HardwareTriggeredFlyable[SeqTableInfo], + flyer: StandardFlyer[SeqTableInfo], detectors: List[StandardDetector], number_of_frames: int, exposure: float, diff --git a/src/ophyd_async/sim/__init__.py b/src/ophyd_async/sim/__init__.py index 9540698461..e69de29bb2 100644 --- a/src/ophyd_async/sim/__init__.py +++ b/src/ophyd_async/sim/__init__.py @@ -1,11 +0,0 @@ -from .pattern_generator import PatternGenerator -from .sim_pattern_detector_control import SimPatternDetectorControl -from .sim_pattern_detector_writer import SimPatternDetectorWriter -from .sim_pattern_generator import SimPatternDetector - -__all__ = [ - "PatternGenerator", - "SimPatternDetectorControl", - "SimPatternDetectorWriter", - "SimPatternDetector", -] diff --git a/src/ophyd_async/sim/demo/__init__.py b/src/ophyd_async/sim/demo/__init__.py index 662d42322c..fa19366c11 100644 --- a/src/ophyd_async/sim/demo/__init__.py +++ b/src/ophyd_async/sim/demo/__init__.py @@ -1,3 +1,19 @@ -from .sim_motor import SimMotor +from ._pattern_detector import ( + DATA_PATH, + SUM_PATH, + PatternDetector, + PatternDetectorController, + PatternDetectorWriter, + PatternGenerator, +) +from ._sim_motor import SimMotor -__all__ = ["SimMotor"] +__all__ = [ + "DATA_PATH", + "SUM_PATH", + "PatternGenerator", + "PatternDetector", + "PatternDetectorController", + "PatternDetectorWriter", + "SimMotor", +] diff --git a/src/ophyd_async/sim/demo/_pattern_detector/__init__.py b/src/ophyd_async/sim/demo/_pattern_detector/__init__.py new file mode 100644 index 0000000000..8bce03bd3f --- /dev/null +++ b/src/ophyd_async/sim/demo/_pattern_detector/__init__.py @@ -0,0 +1,13 @@ +from ._pattern_detector import PatternDetector +from ._pattern_detector_controller import PatternDetectorController +from ._pattern_detector_writer import PatternDetectorWriter +from ._pattern_generator import DATA_PATH, SUM_PATH, PatternGenerator + +__all__ = [ + "PatternDetector", + "PatternDetectorController", + "PatternDetectorWriter", + "DATA_PATH", + "SUM_PATH", + "PatternGenerator", +] diff --git a/src/ophyd_async/sim/sim_pattern_generator.py b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py similarity index 68% rename from src/ophyd_async/sim/sim_pattern_generator.py rename to src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py index cb98f9b0f6..ede7e03fee 100644 --- a/src/ophyd_async/sim/sim_pattern_generator.py +++ b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py @@ -2,20 +2,20 @@ from typing import Sequence from ophyd_async.core import ( + AsyncReadable, FilenameProvider, PathProvider, + StandardDetector, StaticFilenameProvider, StaticPathProvider, ) -from ophyd_async.core.detector import StandardDetector -from ophyd_async.protocols import AsyncReadable -from ophyd_async.sim.pattern_generator import PatternGenerator -from .sim_pattern_detector_control import SimPatternDetectorControl -from .sim_pattern_detector_writer import SimPatternDetectorWriter +from ._pattern_detector_controller import PatternDetectorController +from ._pattern_detector_writer import PatternDetectorWriter +from ._pattern_generator import PatternGenerator -class SimPatternDetector(StandardDetector): +class PatternDetector(StandardDetector): def __init__( self, path: Path, @@ -25,12 +25,12 @@ def __init__( fp: FilenameProvider = StaticFilenameProvider(name) self.path_provider: PathProvider = StaticPathProvider(fp, path) self.pattern_generator = PatternGenerator() - writer = SimPatternDetectorWriter( + writer = PatternDetectorWriter( pattern_generator=self.pattern_generator, path_provider=self.path_provider, name_provider=lambda: self.name, ) - controller = SimPatternDetectorControl( + controller = PatternDetectorController( pattern_generator=self.pattern_generator, path_provider=self.path_provider, ) diff --git a/src/ophyd_async/sim/sim_pattern_detector_control.py b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py similarity index 84% rename from src/ophyd_async/sim/sim_pattern_detector_control.py rename to src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py index 4689447388..f37c537b15 100644 --- a/src/ophyd_async/sim/sim_pattern_detector_control.py +++ b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py @@ -1,13 +1,12 @@ import asyncio from typing import Optional -from ophyd_async.core import PathProvider -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.detector import DetectorControl, DetectorTrigger -from ophyd_async.sim.pattern_generator import PatternGenerator +from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger, PathProvider +from ._pattern_generator import PatternGenerator -class SimPatternDetectorControl(DetectorControl): + +class PatternDetectorController(DetectorControl): def __init__( self, pattern_generator: PatternGenerator, diff --git a/src/ophyd_async/sim/sim_pattern_detector_writer.py b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py similarity index 83% rename from src/ophyd_async/sim/sim_pattern_detector_writer.py rename to src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py index 0dc6700c9f..83178eb768 100644 --- a/src/ophyd_async/sim/sim_pattern_detector_writer.py +++ b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py @@ -2,12 +2,12 @@ from bluesky.protocols import DataKey -from ophyd_async.core import NameProvider, PathProvider -from ophyd_async.core.detector import DetectorWriter -from ophyd_async.sim.pattern_generator import PatternGenerator +from ophyd_async.core import DetectorWriter, NameProvider, PathProvider +from ._pattern_generator import PatternGenerator -class SimPatternDetectorWriter(DetectorWriter): + +class PatternDetectorWriter(DetectorWriter): pattern_generator: PatternGenerator def __init__( diff --git a/src/ophyd_async/sim/pattern_generator.py b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py similarity index 94% rename from src/ophyd_async/sim/pattern_generator.py rename to src/ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py index eeb6501014..d192b50abd 100644 --- a/src/ophyd_async/sim/pattern_generator.py +++ b/src/ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py @@ -5,10 +5,14 @@ import numpy as np from bluesky.protocols import DataKey, StreamAsset -from ophyd_async.core import PathProvider -from ophyd_async.core.signal import observe_value, soft_signal_r_and_setter -from ophyd_async.core.utils import DEFAULT_TIMEOUT -from ophyd_async.epics.areadetector.writers.general_hdffile import _HDFDataset, _HDFFile +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + HDFDataset, + HDFFile, + PathProvider, + observe_value, + soft_signal_r_and_setter, +) # raw data path DATA_PATH = "/entry/data/data" @@ -56,7 +60,7 @@ def __init__( generate_gaussian_blob(width=detector_width, height=detector_height) * MAX_UINT8_VALUE ) - self._hdf_stream_provider: Optional[_HDFFile] = None + self._hdf_stream_provider: Optional[HDFFile] = None self._handle_for_h5_file: Optional[h5py.File] = None self.target_path: Optional[Path] = None @@ -135,13 +139,13 @@ async def open_file( # cache state to self # Add the main data self._datasets = [ - _HDFDataset( + HDFDataset( data_key=name, dataset=DATA_PATH, shape=(self.height, self.width), multiplier=multiplier, ), - _HDFDataset( + HDFDataset( f"{name}-sum", dataset=SUM_PATH, shape=(), @@ -183,7 +187,7 @@ async def collect_stream_docs( # until the first frame comes in if not self._hdf_stream_provider: assert self.target_path, "open file has not been called" - self._hdf_stream_provider = _HDFFile( + self._hdf_stream_provider = HDFFile( self._path_provider(), self.target_path, self._datasets, diff --git a/src/ophyd_async/sim/demo/sim_motor.py b/src/ophyd_async/sim/demo/_sim_motor.py similarity index 92% rename from src/ophyd_async/sim/demo/sim_motor.py rename to src/ophyd_async/sim/demo/_sim_motor.py index db2083ab02..e2a63b1657 100644 --- a/src/ophyd_async/sim/demo/sim_motor.py +++ b/src/ophyd_async/sim/demo/_sim_motor.py @@ -4,15 +4,17 @@ from bluesky.protocols import Movable, Stoppable -from ophyd_async.core import StandardReadable -from ophyd_async.core.async_status import AsyncStatus, WatchableAsyncStatus -from ophyd_async.core.signal import ( +from ophyd_async.core import ( + AsyncStatus, + ConfigSignal, + HintedSignal, + StandardReadable, + WatchableAsyncStatus, + WatcherUpdate, observe_value, soft_signal_r_and_setter, soft_signal_rw, ) -from ophyd_async.core.standard_readable import ConfigSignal, HintedSignal -from ophyd_async.core.utils import WatcherUpdate class SimMotor(StandardReadable, Movable, Stoppable): diff --git a/tests/epics/motion/__init__.py b/src/ophyd_async/sim/testing/__init__.py similarity index 100% rename from tests/epics/motion/__init__.py rename to src/ophyd_async/sim/testing/__init__.py diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/conftest.py b/tests/conftest.py index b4b8752fbd..788c032f32 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,13 +15,15 @@ StaticPathProvider, ) -PANDA_RECORD = str(Path(__file__).parent / "panda" / "db" / "panda.db") +PANDA_RECORD = str(Path(__file__).parent / "fastcs" / "panda" / "db" / "panda.db") INCOMPLETE_BLOCK_RECORD = str( - Path(__file__).parent / "panda" / "db" / "incomplete_block_panda.db" + Path(__file__).parent / "fastcs" / "panda" / "db" / "incomplete_block_panda.db" +) +INCOMPLETE_RECORD = str( + Path(__file__).parent / "fastcs" / "panda" / "db" / "incomplete_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" + Path(__file__).parent / "fastcs" / "panda" / "db" / "extra_blocks_panda.db" ) # Prevent pytest from catching exceptions when debugging in vscode so that break on diff --git a/tests/core/test_device.py b/tests/core/test_device.py index 1294e64511..85d2a0fc14 100644 --- a/tests/core/test_device.py +++ b/tests/core/test_device.py @@ -11,12 +11,12 @@ DeviceVector, MockSignalBackend, NotConnected, + SoftSignalBackend, wait_for_connection, ) -from ophyd_async.core.soft_signal_backend import SoftSignalBackend -from ophyd_async.epics.motion import motor -from ophyd_async.plan_stubs.ensure_connected import ensure_connected -from ophyd_async.sim.demo.sim_motor import SimMotor +from ophyd_async.epics import motor +from ophyd_async.plan_stubs import ensure_connected +from ophyd_async.sim.demo import SimMotor class DummyBaseDevice(Device): diff --git a/tests/core/test_device_collector.py b/tests/core/test_device_collector.py index f91f92c9df..75d8550ac2 100644 --- a/tests/core/test_device_collector.py +++ b/tests/core/test_device_collector.py @@ -4,9 +4,14 @@ from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine, TransitionError -from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceCollector, NotConnected -from ophyd_async.core.mock_signal_utils import set_mock_value -from ophyd_async.epics.motion import motor +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + Device, + DeviceCollector, + NotConnected, + set_mock_value, +) +from ophyd_async.epics import motor class FailingDevice(Device): diff --git a/tests/core/test_device_save_loader.py b/tests/core/test_device_save_loader.py index b0e1da0c4f..aa60be9802 100644 --- a/tests/core/test_device_save_loader.py +++ b/tests/core/test_device_save_loader.py @@ -13,6 +13,7 @@ Device, SignalR, SignalRW, + all_at_once, get_signal_values, load_device, load_from_yaml, @@ -21,7 +22,6 @@ set_signal_values, walk_rw_signals, ) -from ophyd_async.core.device_save_loader import all_at_once from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw @@ -276,9 +276,9 @@ async def test_set_signal_values_restores_value(RE: RunEngine, device, tmp_path) assert np.array_equal(array_value, np.array([1, 1, 1, 1, 1])) -@patch("ophyd_async.core.device_save_loader.load_from_yaml") -@patch("ophyd_async.core.device_save_loader.walk_rw_signals") -@patch("ophyd_async.core.device_save_loader.set_signal_values") +@patch("ophyd_async.core._device_save_loader.load_from_yaml") +@patch("ophyd_async.core._device_save_loader.walk_rw_signals") +@patch("ophyd_async.core._device_save_loader.set_signal_values") async def test_load_device( mock_set_signal_values, mock_walk_rw_signals, mock_load_from_yaml, device ): diff --git a/tests/core/test_flyer.py b/tests/core/test_flyer.py index aaf4c0a424..9d968526d9 100644 --- a/tests/core/test_flyer.py +++ b/tests/core/test_flyer.py @@ -15,13 +15,13 @@ DetectorControl, DetectorTrigger, DetectorWriter, - HardwareTriggeredFlyable, + StandardDetector, + StandardFlyer, TriggerInfo, TriggerLogic, + observe_value, ) -from ophyd_async.core.detector import StandardDetector -from ophyd_async.core.signal import observe_value -from ophyd_async.epics.signal.signal import epics_signal_rw +from ophyd_async.epics.signal import epics_signal_rw class TriggerState(str, Enum): @@ -150,7 +150,7 @@ def append_and_print(name, doc): RE.subscribe(append_and_print) trigger_logic = DummyTriggerLogic() - flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer") + flyer = StandardFlyer(trigger_logic, [], name="flyer") trigger_info = TriggerInfo( number=1, trigger=DetectorTrigger.constant_gate, deadtime=2, livetime=2 ) @@ -228,13 +228,13 @@ def flying_plan(): # To do: Populate configuration signals async def test_describe_configuration(): - flyer = HardwareTriggeredFlyable(DummyTriggerLogic(), [], name="flyer") + flyer = StandardFlyer(DummyTriggerLogic(), [], name="flyer") assert await flyer.describe_configuration() == {} # To do: Populate configuration signals async def test_read_configuration(): - flyer = HardwareTriggeredFlyable(DummyTriggerLogic(), [], name="flyer") + flyer = StandardFlyer(DummyTriggerLogic(), [], name="flyer") assert await flyer.read_configuration() == {} diff --git a/tests/core/test_log.py b/tests/core/test_log.py new file mode 100644 index 0000000000..a62b4ef762 --- /dev/null +++ b/tests/core/test_log.py @@ -0,0 +1,81 @@ +import io +import logging +import logging.handlers +from unittest.mock import MagicMock, patch + +import pytest + +from ophyd_async.core import Device, _log, config_ophyd_async_logging + +# Allow this importing of _log for now to test the internal interface +# But this needs resolving. + + +def test_validate_level(): + assert _log._validate_level("CRITICAL") == 50 + assert _log._validate_level("ERROR") == 40 + assert _log._validate_level("WARNING") == 30 + assert _log._validate_level("INFO") == 20 + assert _log._validate_level("DEBUG") == 10 + assert _log._validate_level("NOTSET") == 0 + assert _log._validate_level(123) == 123 + with pytest.raises(ValueError): + _log._validate_level("MYSTERY") + + +def test_default_config_ophyd_async_logging(): + config_ophyd_async_logging() + assert isinstance(_log.current_handler, logging.StreamHandler) + assert _log.logger.getEffectiveLevel() <= logging.WARNING + + +def test_config_ophyd_async_logging_with_file_handler(): + config_ophyd_async_logging(file="file") + assert isinstance(_log.current_handler, logging.StreamHandler) + assert _log.logger.getEffectiveLevel() <= logging.WARNING + + +def test_config_ophyd_async_logging_removes_extra_handlers(): + # Protect global variable in other pytests + class FakeLogger: + def __init__(self): + self.handlers = [] + self.removeHandler = MagicMock() + self.setLevel = MagicMock() + + def addHandler(self, handler): + self.handlers.append(handler) + + def getEffectiveLevel(self): + return 100000 + + fake_logger = FakeLogger() + with ( + patch("ophyd_async.core._log.logger", fake_logger), + ): + config_ophyd_async_logging() + fake_logger.removeHandler.assert_not_called() + config_ophyd_async_logging() + fake_logger.removeHandler.assert_called() + + +# Full format looks like: +#'[test_device][W 240501 13:28:08.937 test_log:35] here is a warning\n' +def test_logger_adapter_ophyd_async_device(): + log_buffer = io.StringIO() + log_stream = logging.StreamHandler(stream=log_buffer) + log_stream.setFormatter( + _log.ColoredFormatterWithDeviceName( + fmt=_log.DEFAULT_FORMAT, datefmt=_log.DEFAULT_DATE_FORMAT, no_color=True + ) + ) + _log.logger.addHandler(log_stream) + + device = Device(name="test_device") + device._log = logging.LoggerAdapter( + logging.getLogger("ophyd_async.devices"), + {"ophyd_async_device_name": device.name}, + ) + device._log.warning("here is a warning") + assert log_buffer.getvalue().startswith("[test_device]") + assert log_buffer.getvalue().endswith("here is a warning\n") diff --git a/tests/core/test_mock_signal_backend.py b/tests/core/test_mock_signal_backend.py index ddcd4a8c95..5aa1b03613 100644 --- a/tests/core/test_mock_signal_backend.py +++ b/tests/core/test_mock_signal_backend.py @@ -5,9 +5,13 @@ import pytest -from ophyd_async.core import MockSignalBackend, SignalRW -from ophyd_async.core.device import Device, DeviceCollector -from ophyd_async.core.mock_signal_utils import ( +from ophyd_async.core import ( + Device, + DeviceCollector, + MockSignalBackend, + SignalRW, + SignalW, + SoftSignalBackend, callback_on_mock_put, get_mock_put, mock_puts_blocked, @@ -15,10 +19,10 @@ set_mock_put_proceeds, set_mock_value, set_mock_values, + soft_signal_r_and_setter, + soft_signal_rw, ) -from ophyd_async.core.signal import SignalW, soft_signal_r_and_setter, soft_signal_rw -from ophyd_async.core.soft_signal_backend import SoftSignalBackend -from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw +from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw @pytest.mark.parametrize("connect_mock_mode", [True, False]) diff --git a/tests/protocols/test_protocols.py b/tests/core/test_protocol.py similarity index 52% rename from tests/protocols/test_protocols.py rename to tests/core/test_protocol.py index d400fb1922..d71c4cce09 100644 --- a/tests/protocols/test_protocols.py +++ b/tests/core/test_protocol.py @@ -2,16 +2,14 @@ from bluesky.utils import new_uid -from ophyd_async import protocols as bs_protocols from ophyd_async.core import ( + AsyncReadable, DeviceCollector, + StandardFlyer, StaticFilenameProvider, StaticPathProvider, ) -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.epics import adcore, adsimdetector from ophyd_async.sim.demo import SimMotor @@ -20,9 +18,9 @@ async def make_detector(prefix: str, name: str, tmp_path: Path): dp = StaticPathProvider(fp, tmp_path) async with DeviceCollector(mock=True): - drv = ADBase(f"{prefix}DRV:") - hdf = NDFileHDF(f"{prefix}HDF:") - det = DemoADSimDetector( + drv = adcore.ADBaseIO(f"{prefix}DRV:") + hdf = adcore.NDFileHDFIO(f"{prefix}HDF:") + det = adsimdetector.SimDetector( drv, hdf, dp, config_sigs=[drv.acquire_time, drv.acquire], name=name ) @@ -32,6 +30,6 @@ async def make_detector(prefix: str, name: str, tmp_path: Path): async def test_readable(): async with DeviceCollector(mock=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) + assert isinstance(SimMotor, AsyncReadable) + assert isinstance(det, AsyncReadable) + assert not isinstance(StandardFlyer, AsyncReadable) diff --git a/tests/core/test_standard_readable.py b/tests/core/test_readable.py similarity index 94% rename from tests/core/test_standard_readable.py rename to tests/core/test_readable.py index 182cadeabc..7d13f308c3 100644 --- a/tests/core/test_standard_readable.py +++ b/tests/core/test_readable.py @@ -5,11 +5,19 @@ import pytest from bluesky.protocols import HasHints -from ophyd_async.core import ConfigSignal, HintedSignal, StandardReadable -from ophyd_async.core.device import Device, DeviceVector -from ophyd_async.core.mock_signal_backend import MockSignalBackend -from ophyd_async.core.signal import SignalR, soft_signal_r_and_setter -from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable +from ophyd_async.core import ( + AsyncConfigurable, + AsyncReadable, + AsyncStageable, + ConfigSignal, + Device, + DeviceVector, + HintedSignal, + MockSignalBackend, + SignalR, + StandardReadable, + soft_signal_r_and_setter, +) def test_standard_readable_hints(): diff --git a/tests/core/test_signal.py b/tests/core/test_signal.py index a60bcd63f3..d8ce9994fd 100644 --- a/tests/core/test_signal.py +++ b/tests/core/test_signal.py @@ -9,10 +9,12 @@ from bluesky.protocols import Reading from ophyd_async.core import ( + DEFAULT_TIMEOUT, ConfigSignal, DeviceCollector, HintedSignal, MockSignalBackend, + NotConnected, Signal, SignalR, SignalRW, @@ -28,8 +30,6 @@ soft_signal_rw, wait_for_value, ) -from ophyd_async.core.signal import _SignalCache -from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw from ophyd_async.plan_stubs import ensure_connected @@ -372,7 +372,8 @@ async def test_subscription_logs(caplog): caplog.set_level(logging.DEBUG) mock_signal_rw = epics_signal_rw(int, "pva://mock_signal", name="mock_signal") await mock_signal_rw.connect(mock=True) - cache = _SignalCache(mock_signal_rw._backend, signal=mock_signal_rw) + cbs = [] + mock_signal_rw.subscribe(cbs.append) assert "Making subscription" in caplog.text - cache.close() + mock_signal_rw.clear_sub(cbs.append) assert "Closing subscription on source" in caplog.text diff --git a/tests/core/test_soft_signal_backend.py b/tests/core/test_soft_signal_backend.py index 8da4e750d5..5e55507626 100644 --- a/tests/core/test_soft_signal_backend.py +++ b/tests/core/test_soft_signal_backend.py @@ -8,8 +8,7 @@ import pytest from bluesky.protocols import Reading -from ophyd_async.core import Signal, SignalBackend, T -from ophyd_async.core.soft_signal_backend import SignalMetadata, SoftSignalBackend +from ophyd_async.core import Signal, SignalBackend, SignalMetadata, SoftSignalBackend, T class MyEnum(str, Enum): diff --git a/tests/core/test_async_status.py b/tests/core/test_status.py similarity index 100% rename from tests/core/test_async_status.py rename to tests/core/test_status.py diff --git a/tests/core/test_subset_enum.py b/tests/core/test_subset_enum.py index 4bcdcaf770..dfdc7416dd 100644 --- a/tests/core/test_subset_enum.py +++ b/tests/core/test_subset_enum.py @@ -4,9 +4,11 @@ from p4p.nt import NTEnum from ophyd_async.core import SubsetEnum -from ophyd_async.epics._backend._aioca import make_converter as aioca_make_converter -from ophyd_async.epics._backend._p4p import make_converter as p4p_make_converter -from ophyd_async.epics.signal.signal import epics_signal_rw +from ophyd_async.epics.signal import epics_signal_rw + +# I think we want to allow these as calling these is only needed for tests? +from ophyd_async.epics.signal._aioca import make_converter as aioca_make_converter +from ophyd_async.epics.signal._p4p import make_converter as p4p_make_converter async def test_runtime_enum_behaviour(): diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 8298eec7c4..37f386bb91 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -150,7 +150,7 @@ async def test_error_handling_value_errors(caplog): logs = [ log for log in logs - if "ophyd_async" in log.pathname and "signal" not in log.pathname + if "ophyd_async" in log.pathname and "_signal" not in log.pathname ] assert len(logs) == 4 @@ -193,7 +193,7 @@ async def test_error_handling_device_collector(caplog): logs = [ log for log in logs - if "ophyd_async" in log.pathname and "signal" not in log.pathname + if "ophyd_async" in log.pathname and "_signal" not in log.pathname ] assert len(logs) == 5 assert ( diff --git a/tests/core/test_watchable_async_status.py b/tests/core/test_watchable_async_status.py index f7c6c04aeb..eaf23bb61b 100644 --- a/tests/core/test_watchable_async_status.py +++ b/tests/core/test_watchable_async_status.py @@ -6,10 +6,13 @@ import pytest from bluesky.protocols import Movable -from ophyd_async.core.async_status import AsyncStatus, WatchableAsyncStatus -from ophyd_async.core.signal import soft_signal_r_and_setter -from ophyd_async.core.standard_readable import StandardReadable -from ophyd_async.core.utils import WatcherUpdate +from ophyd_async.core import ( + AsyncStatus, + StandardReadable, + WatchableAsyncStatus, + WatcherUpdate, + soft_signal_r_and_setter, +) class SetFailed(Exception): diff --git a/tests/epics/adaravis/test_aravis.py b/tests/epics/adaravis/test_aravis.py new file mode 100644 index 0000000000..373accd13f --- /dev/null +++ b/tests/epics/adaravis/test_aravis.py @@ -0,0 +1,158 @@ +import re + +import pytest +from bluesky.run_engine import RunEngine + +from ophyd_async.core import ( + DetectorTrigger, + DeviceCollector, + PathProvider, + TriggerInfo, + set_mock_value, +) +from ophyd_async.epics import adaravis + + +@pytest.fixture +async def test_adaravis( + RE: RunEngine, + static_path_provider: PathProvider, +) -> adaravis.AravisDetector: + async with DeviceCollector(mock=True): + test_adaravis = adaravis.AravisDetector("ADARAVIS:", static_path_provider) + + return test_adaravis + + +@pytest.mark.parametrize("exposure_time", [0.0, 0.1, 1.0, 10.0, 100.0]) +async def test_deadtime_invariant_with_exposure_time( + exposure_time: float, + test_adaravis: adaravis.AravisDetector, +): + assert test_adaravis.controller.get_deadtime(exposure_time) == 1961e-6 + + +async def test_trigger_source_set_to_gpio_line(test_adaravis: adaravis.AravisDetector): + set_mock_value(test_adaravis.drv.trigger_source, "Freerun") + + async def trigger_and_complete(): + await test_adaravis.controller.arm(num=1, trigger=DetectorTrigger.edge_trigger) + # Prevent timeouts + set_mock_value(test_adaravis.drv.acquire, True) + + # Default TriggerSource + assert (await test_adaravis.drv.trigger_source.get_value()) == "Freerun" + test_adaravis.set_external_trigger_gpio(1) + # TriggerSource not changed by setting gpio + assert (await test_adaravis.drv.trigger_source.get_value()) == "Freerun" + + await trigger_and_complete() + + # TriggerSource changes + assert (await test_adaravis.drv.trigger_source.get_value()) == "Line1" + + test_adaravis.set_external_trigger_gpio(3) + # TriggerSource not changed by setting gpio + await trigger_and_complete() + assert (await test_adaravis.drv.trigger_source.get_value()) == "Line3" + + +def test_gpio_pin_limited(test_adaravis: adaravis.AravisDetector): + assert test_adaravis.get_external_trigger_gpio() == 1 + test_adaravis.set_external_trigger_gpio(2) + assert test_adaravis.get_external_trigger_gpio() == 2 + with pytest.raises( + ValueError, + match=re.escape( + "AravisDetector only supports the following GPIO indices: " + "(1, 2, 3, 4) but was asked to use 55" + ), + ): + test_adaravis.set_external_trigger_gpio(55) # type: ignore + + +async def test_hints_from_hdf_writer(test_adaravis: adaravis.AravisDetector): + assert test_adaravis.hints == {"fields": ["test_adaravis"]} + + +async def test_can_read(test_adaravis: adaravis.AravisDetector): + # Standard detector can be used as Readable + assert (await test_adaravis.read()) == {} + + +async def test_decribe_describes_writer_dataset(test_adaravis: adaravis.AravisDetector): + set_mock_value(test_adaravis._writer.hdf.file_path_exists, True) + set_mock_value(test_adaravis._writer.hdf.capture, True) + + assert await test_adaravis.describe() == {} + await test_adaravis.stage() + assert await test_adaravis.describe() == { + "test_adaravis": { + "source": "mock+ca://ADARAVIS:HDF1:FullFileName_RBV", + "shape": (0, 0), + "dtype": "array", + "dtype_numpy": "|i1", + "external": "STREAM:", + } + } + + +async def test_can_collect( + test_adaravis: adaravis.AravisDetector, static_path_provider: PathProvider +): + path_info = static_path_provider() + full_file_name = path_info.root / path_info.resource_dir / "foo.h5" + set_mock_value(test_adaravis.hdf.full_file_name, str(full_file_name)) + set_mock_value(test_adaravis._writer.hdf.file_path_exists, True) + set_mock_value(test_adaravis._writer.hdf.capture, True) + await test_adaravis.stage() + docs = [(name, doc) async for name, doc in test_adaravis.collect_asset_docs(1)] + assert len(docs) == 2 + assert docs[0][0] == "stream_resource" + stream_resource = docs[0][1] + sr_uid = stream_resource["uid"] + assert stream_resource["data_key"] == "test_adaravis" + assert stream_resource["uri"] == "file://localhost" + str(full_file_name) + assert stream_resource["parameters"] == { + "dataset": "/entry/data/data", + "swmr": False, + "multiplier": 1, + } + assert docs[1][0] == "stream_datum" + stream_datum = docs[1][1] + assert stream_datum["stream_resource"] == sr_uid + assert stream_datum["seq_nums"] == {"start": 0, "stop": 0} + assert stream_datum["indices"] == {"start": 0, "stop": 1} + + +async def test_can_decribe_collect(test_adaravis: adaravis.AravisDetector): + set_mock_value(test_adaravis._writer.hdf.file_path_exists, True) + set_mock_value(test_adaravis._writer.hdf.capture, True) + assert (await test_adaravis.describe_collect()) == {} + await test_adaravis.stage() + assert (await test_adaravis.describe_collect()) == { + "test_adaravis": { + "source": "mock+ca://ADARAVIS:HDF1:FullFileName_RBV", + "shape": (0, 0), + "dtype": "array", + "dtype_numpy": "|i1", + "external": "STREAM:", + } + } + + +async def test_unsupported_trigger_excepts(test_adaravis: adaravis.AravisDetector): + with pytest.raises( + ValueError, + # str(EnumClass.value) handling changed in Python 3.11 + match=r"AravisController only supports the following trigger types: .* but", + ): + await test_adaravis.prepare( + TriggerInfo( + number=1, + trigger=DetectorTrigger.variable_gate, + deadtime=1, + livetime=1, + frame_timeout=3, + ) + ) diff --git a/tests/epics/areadetector/test_drivers.py b/tests/epics/adcore/test_drivers.py similarity index 67% rename from tests/epics/areadetector/test_drivers.py rename to tests/epics/adcore/test_drivers.py index 800d2d5b7b..379eac9a0f 100644 --- a/tests/epics/areadetector/test_drivers.py +++ b/tests/epics/adcore/test_drivers.py @@ -9,25 +9,20 @@ get_mock_put, set_mock_value, ) -from ophyd_async.epics.areadetector.drivers import ( - ADBase, - DetectorState, - set_exposure_time_and_acquire_period_if_supplied, - start_acquiring_driver_and_ensure_status, -) +from ophyd_async.epics import adcore TEST_DEADTIME = 0.1 @pytest.fixture -def driver(RE) -> ADBase: +def driver(RE) -> adcore.ADBaseIO: with DeviceCollector(mock=True): - driver = ADBase("DRV:", name="drv") + driver = adcore.ADBaseIO("DRV:", name="drv") return driver @pytest.fixture -async def controller(RE, driver: ADBase) -> Mock: +async def controller(RE, driver: adcore.ADBaseIO) -> Mock: controller = Mock(spec=DetectorControl) controller.get_deadtime.return_value = TEST_DEADTIME return controller @@ -35,11 +30,13 @@ async def controller(RE, driver: ADBase) -> Mock: async def test_set_exposure_time_and_acquire_period_if_supplied_is_a_noop_if_no_exposure_supplied( # noqa: E501 controller: DetectorControl, - driver: ADBase, + driver: adcore.ADBaseIO, ): put_exposure = get_mock_put(driver.acquire_time) put_acquire_period = get_mock_put(driver.acquire_period) - await set_exposure_time_and_acquire_period_if_supplied(controller, driver, None) + await adcore.set_exposure_time_and_acquire_period_if_supplied( + controller, driver, None + ) put_exposure.assert_not_called() put_acquire_period.assert_not_called() @@ -54,12 +51,14 @@ async def test_set_exposure_time_and_acquire_period_if_supplied_is_a_noop_if_no_ ) async def test_set_exposure_time_and_acquire_period_if_supplied_uses_deadtime( controller: DetectorControl, - driver: ADBase, + driver: adcore.ADBaseIO, exposure: float, expected_exposure: float, expected_acquire_period: float, ): - await set_exposure_time_and_acquire_period_if_supplied(controller, driver, exposure) + await adcore.set_exposure_time_and_acquire_period_if_supplied( + controller, driver, exposure + ) actual_exposure = await driver.acquire_time.get_value() actual_acquire_period = await driver.acquire_period.get_value() assert expected_exposure == actual_exposure @@ -67,29 +66,33 @@ async def test_set_exposure_time_and_acquire_period_if_supplied_uses_deadtime( async def test_start_acquiring_driver_and_ensure_status_flags_immediate_failure( - driver: ADBase, + driver: adcore.ADBaseIO, ): - set_mock_value(driver.detector_state, DetectorState.Error) - acquiring = await start_acquiring_driver_and_ensure_status(driver, timeout=0.01) + set_mock_value(driver.detector_state, adcore.DetectorState.Error) + acquiring = await adcore.start_acquiring_driver_and_ensure_status( + driver, timeout=0.01 + ) with pytest.raises(ValueError): await acquiring async def test_start_acquiring_driver_and_ensure_status_fails_after_some_time( - driver: ADBase, + driver: adcore.ADBaseIO, ): """This test ensures a failing status is captured halfway through acquisition. Real world application; it takes some time to start acquiring, and during that time the detector gets itself into a bad state. """ - set_mock_value(driver.detector_state, DetectorState.Idle) + set_mock_value(driver.detector_state, adcore.DetectorState.Idle) async def wait_then_fail(): await asyncio.sleep(0) - set_mock_value(driver.detector_state, DetectorState.Disconnected) + set_mock_value(driver.detector_state, adcore.DetectorState.Disconnected) - acquiring = await start_acquiring_driver_and_ensure_status(driver, timeout=0.1) + acquiring = await adcore.start_acquiring_driver_and_ensure_status( + driver, timeout=0.1 + ) await wait_then_fail() with pytest.raises(ValueError): diff --git a/tests/epics/areadetector/test_scans.py b/tests/epics/adcore/test_scans.py similarity index 79% rename from tests/epics/areadetector/test_scans.py rename to tests/epics/adcore/test_scans.py index b788ef5ed2..b531db8e5f 100644 --- a/tests/epics/areadetector/test_scans.py +++ b/tests/epics/adcore/test_scans.py @@ -13,15 +13,13 @@ DetectorControl, DetectorTrigger, DeviceCollector, - HardwareTriggeredFlyable, StandardDetector, + StandardFlyer, TriggerInfo, TriggerLogic, set_mock_value, ) -from ophyd_async.epics.areadetector.controllers import ADSimController -from ophyd_async.epics.areadetector.drivers import ADBase -from ophyd_async.epics.areadetector.writers import HDFWriter, NDFileHDF +from ophyd_async.epics import adcore, adsimdetector class DummyTriggerLogic(TriggerLogic[int]): @@ -55,19 +53,19 @@ def get_deadtime(self, exposure: float) -> float: @pytest.fixture -def controller(RE) -> ADSimController: +def controller(RE) -> adsimdetector.SimController: with DeviceCollector(mock=True): - drv = ADBase("DRV") + drv = adcore.ADBaseIO("DRV") - return ADSimController(drv) + return adsimdetector.SimController(drv) @pytest.fixture -def writer(RE, static_path_provider, tmp_path: Path) -> HDFWriter: +def writer(RE, static_path_provider, tmp_path: Path) -> adcore.ADHDFWriter: with DeviceCollector(mock=True): - hdf = NDFileHDF("HDF") + hdf = adcore.NDFileHDFIO("HDF") - return HDFWriter( + return adcore.ADHDFWriter( hdf, path_provider=static_path_provider, name_provider=lambda: "test", @@ -75,11 +73,11 @@ def writer(RE, static_path_provider, tmp_path: Path) -> HDFWriter: ) -@patch("ophyd_async.core.detector.DEFAULT_TIMEOUT", 0.1) +@patch("ophyd_async.core._detector.DEFAULT_TIMEOUT", 0.1) async def test_hdf_writer_fails_on_timeout_with_stepscan( RE: RunEngine, - writer: HDFWriter, - controller: ADSimController, + writer: adcore.ADHDFWriter, + controller: adsimdetector.SimController, ): set_mock_value(writer.hdf.file_path_exists, True) detector: StandardDetector[Any] = StandardDetector( @@ -92,8 +90,10 @@ async def test_hdf_writer_fails_on_timeout_with_stepscan( assert isinstance(exc.value.__cause__, asyncio.TimeoutError) -@patch("ophyd_async.core.detector.DEFAULT_TIMEOUT", 0.1) -def test_hdf_writer_fails_on_timeout_with_flyscan(RE: RunEngine, writer: HDFWriter): +@patch("ophyd_async.core._detector.DEFAULT_TIMEOUT", 0.1) +def test_hdf_writer_fails_on_timeout_with_flyscan( + RE: RunEngine, writer: adcore.ADHDFWriter +): controller = DummyController() set_mock_value(writer.hdf.file_path_exists, True) @@ -102,7 +102,7 @@ def test_hdf_writer_fails_on_timeout_with_flyscan(RE: RunEngine, writer: HDFWrit ) trigger_logic = DummyTriggerLogic() - flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer") + flyer = StandardFlyer(trigger_logic, [], name="flyer") trigger_info = TriggerInfo( number=1, trigger=DetectorTrigger.constant_gate, deadtime=2, livetime=2 ) diff --git a/tests/epics/areadetector/test_single_trigger_det.py b/tests/epics/adcore/test_single_trigger.py similarity index 68% rename from tests/epics/areadetector/test_single_trigger_det.py rename to tests/epics/adcore/test_single_trigger.py index 86d76afd27..93e42ed400 100644 --- a/tests/epics/areadetector/test_single_trigger_det.py +++ b/tests/epics/adcore/test_single_trigger.py @@ -3,17 +3,17 @@ from bluesky import RunEngine from ophyd_async.core import DeviceCollector, set_mock_value -from ophyd_async.epics.areadetector import ImageMode, SingleTriggerDet -from ophyd_async.epics.areadetector.drivers import ADBase -from ophyd_async.epics.areadetector.writers import NDPluginStats +from ophyd_async.epics import adcore @pytest.fixture async def single_trigger_det(): async with DeviceCollector(mock=True): - stats = NDPluginStats("PREFIX:STATS") - det = SingleTriggerDet( - drv=ADBase("PREFIX:DRV"), stats=stats, read_uncached=[stats.unique_id] + stats = adcore.NDPluginStatsIO("PREFIX:STATS") + det = adcore.SingleTriggerDetector( + drv=adcore.ADBaseIO("PREFIX:DRV"), + stats=stats, + read_uncached=[stats.unique_id], ) assert det.name == "det" @@ -23,12 +23,14 @@ async def single_trigger_det(): # in a particular way, rather than values being set by the Ophyd signals set_mock_value(det.drv.acquire_time, 0.5) set_mock_value(det.drv.array_counter, 1) - set_mock_value(det.drv.image_mode, ImageMode.continuous) + set_mock_value(det.drv.image_mode, adcore.ImageMode.continuous) set_mock_value(stats.unique_id, 3) yield det -async def test_single_trigger_det(single_trigger_det: SingleTriggerDet, RE: RunEngine): +async def test_single_trigger_det( + single_trigger_det: adcore.SingleTriggerDetector, RE: RunEngine +): names = [] docs = [] RE.subscribe(lambda name, _: names.append(name)) @@ -38,7 +40,7 @@ async def test_single_trigger_det(single_trigger_det: SingleTriggerDet, RE: RunE drv = single_trigger_det.drv assert 1 == await drv.acquire.get_value() - assert ImageMode.single == await drv.image_mode.get_value() + assert adcore.ImageMode.single == await drv.image_mode.get_value() assert True is await drv.wait_for_plugins.get_value() assert names == ["start", "descriptor", "event", "stop"] diff --git a/tests/epics/areadetector/test_utils.py b/tests/epics/adcore/test_utils_adcore.py similarity index 81% rename from tests/epics/areadetector/test_utils.py rename to tests/epics/adcore/test_utils_adcore.py index 3823cb9e42..38a55ff9f7 100644 --- a/tests/epics/areadetector/test_utils.py +++ b/tests/epics/adcore/test_utils_adcore.py @@ -1,13 +1,13 @@ -from ophyd_async.epics.areadetector import NDAttributeDataType, NDAttributesXML +from ophyd_async.epics import adcore def test_ndattribute_writing_xml(): - xml = NDAttributesXML() + xml = adcore.NDAttributesXML() xml.add_epics_pv("Temperature", "LINKAM:TEMP", description="The sample temperature") xml.add_param( "STATS_SUM", "SUM", - NDAttributeDataType.DOUBLE, + adcore.NDAttributeDataType.DOUBLE, description="Sum of pilatus frame", ) actual = str(xml) diff --git a/tests/epics/areadetector/test_writers.py b/tests/epics/adcore/test_writers.py similarity index 67% rename from tests/epics/areadetector/test_writers.py rename to tests/epics/adcore/test_writers.py index e144fbf4fc..76039db002 100644 --- a/tests/epics/areadetector/test_writers.py +++ b/tests/epics/adcore/test_writers.py @@ -8,7 +8,7 @@ StaticPathProvider, set_mock_value, ) -from ophyd_async.epics.areadetector.writers import ADBaseDataType, HDFWriter, NDFileHDF +from ophyd_async.epics import adcore class DummyShapeProvider(ShapeProvider): @@ -16,15 +16,17 @@ def __init__(self) -> None: pass async def __call__(self) -> tuple: - return (10, 10, ADBaseDataType.UInt16) + return (10, 10, adcore.ADBaseDataType.UInt16) @pytest.fixture -async def hdf_writer(RE, static_path_provider: StaticPathProvider) -> HDFWriter: +async def hdf_writer( + RE, static_path_provider: StaticPathProvider +) -> adcore.ADHDFWriter: async with DeviceCollector(mock=True): - hdf = NDFileHDF("HDF:") + hdf = adcore.NDFileHDFIO("HDF:") - return HDFWriter( + return adcore.ADHDFWriter( hdf, static_path_provider, name_provider=lambda: "test", @@ -32,9 +34,9 @@ async def hdf_writer(RE, static_path_provider: StaticPathProvider) -> HDFWriter: ) -async def test_correct_descriptor_doc_after_open(hdf_writer: HDFWriter): +async def test_correct_descriptor_doc_after_open(hdf_writer: adcore.ADHDFWriter): set_mock_value(hdf_writer.hdf.file_path_exists, True) - with patch("ophyd_async.core.signal.wait_for_value", return_value=None): + with patch("ophyd_async.core._signal.wait_for_value", return_value=None): descriptor = await hdf_writer.open() assert descriptor == { @@ -50,7 +52,7 @@ async def test_correct_descriptor_doc_after_open(hdf_writer: HDFWriter): await hdf_writer.close() -async def test_collect_stream_docs(hdf_writer: HDFWriter): +async def test_collect_stream_docs(hdf_writer: adcore.ADHDFWriter): assert hdf_writer._file is None [item async for item in hdf_writer.collect_stream_docs(1)] diff --git a/tests/epics/adkinetix/test_kinetix.py b/tests/epics/adkinetix/test_kinetix.py new file mode 100644 index 0000000000..257aa15878 --- /dev/null +++ b/tests/epics/adkinetix/test_kinetix.py @@ -0,0 +1,124 @@ +import pytest +from bluesky.run_engine import RunEngine + +from ophyd_async.core import ( + DetectorTrigger, + DeviceCollector, + StaticPathProvider, + set_mock_value, +) +from ophyd_async.epics import adkinetix + + +@pytest.fixture +async def test_adkinetix( + RE: RunEngine, + static_path_provider: StaticPathProvider, +) -> adkinetix.KinetixDetector: + async with DeviceCollector(mock=True): + test_adkinetix = adkinetix.KinetixDetector("KINETIX:", static_path_provider) + + return test_adkinetix + + +async def test_get_deadtime( + test_adkinetix: adkinetix.KinetixDetector, +): + # Currently Kinetix driver doesn't support getting deadtime. + assert test_adkinetix._controller.get_deadtime(0) == 0.001 + + +async def test_trigger_modes(test_adkinetix: adkinetix.KinetixDetector): + set_mock_value(test_adkinetix.drv.trigger_mode, "Internal") + + async def setup_trigger_mode(trig_mode: DetectorTrigger): + await test_adkinetix.controller.arm(num=1, trigger=trig_mode) + # Prevent timeouts + set_mock_value(test_adkinetix.drv.acquire, True) + + # Default TriggerSource + assert (await test_adkinetix.drv.trigger_mode.get_value()) == "Internal" + + await setup_trigger_mode(DetectorTrigger.edge_trigger) + assert (await test_adkinetix.drv.trigger_mode.get_value()) == "Rising Edge" + + await setup_trigger_mode(DetectorTrigger.constant_gate) + assert (await test_adkinetix.drv.trigger_mode.get_value()) == "Exp. Gate" + + await setup_trigger_mode(DetectorTrigger.internal) + assert (await test_adkinetix.drv.trigger_mode.get_value()) == "Internal" + + await setup_trigger_mode(DetectorTrigger.variable_gate) + assert (await test_adkinetix.drv.trigger_mode.get_value()) == "Exp. Gate" + + +async def test_hints_from_hdf_writer(test_adkinetix: adkinetix.KinetixDetector): + assert test_adkinetix.hints == {"fields": ["test_adkinetix"]} + + +async def test_can_read(test_adkinetix: adkinetix.KinetixDetector): + # Standard detector can be used as Readable + assert (await test_adkinetix.read()) == {} + + +async def test_decribe_describes_writer_dataset( + test_adkinetix: adkinetix.KinetixDetector, +): + set_mock_value(test_adkinetix._writer.hdf.file_path_exists, True) + set_mock_value(test_adkinetix._writer.hdf.capture, True) + + assert await test_adkinetix.describe() == {} + await test_adkinetix.stage() + assert await test_adkinetix.describe() == { + "test_adkinetix": { + "source": "mock+ca://KINETIX:HDF1:FullFileName_RBV", + "shape": (0, 0), + "dtype": "array", + "dtype_numpy": "|i1", + "external": "STREAM:", + } + } + + +async def test_can_collect( + test_adkinetix: adkinetix.KinetixDetector, static_path_provider: StaticPathProvider +): + path_info = static_path_provider() + full_file_name = path_info.root / path_info.resource_dir / "foo.h5" + set_mock_value(test_adkinetix.hdf.full_file_name, str(full_file_name)) + set_mock_value(test_adkinetix._writer.hdf.file_path_exists, True) + set_mock_value(test_adkinetix._writer.hdf.capture, True) + await test_adkinetix.stage() + docs = [(name, doc) async for name, doc in test_adkinetix.collect_asset_docs(1)] + assert len(docs) == 2 + assert docs[0][0] == "stream_resource" + stream_resource = docs[0][1] + sr_uid = stream_resource["uid"] + assert stream_resource["data_key"] == "test_adkinetix" + assert stream_resource["uri"] == "file://localhost" + str(full_file_name) + assert stream_resource["parameters"] == { + "dataset": "/entry/data/data", + "swmr": False, + "multiplier": 1, + } + assert docs[1][0] == "stream_datum" + stream_datum = docs[1][1] + assert stream_datum["stream_resource"] == sr_uid + assert stream_datum["seq_nums"] == {"start": 0, "stop": 0} + assert stream_datum["indices"] == {"start": 0, "stop": 1} + + +async def test_can_decribe_collect(test_adkinetix: adkinetix.KinetixDetector): + set_mock_value(test_adkinetix._writer.hdf.file_path_exists, True) + set_mock_value(test_adkinetix._writer.hdf.capture, True) + assert (await test_adkinetix.describe_collect()) == {} + await test_adkinetix.stage() + assert (await test_adkinetix.describe_collect()) == { + "test_adkinetix": { + "source": "mock+ca://KINETIX:HDF1:FullFileName_RBV", + "shape": (0, 0), + "dtype": "array", + "dtype_numpy": "|i1", + "external": "STREAM:", + } + } diff --git a/tests/epics/adpilatus/test_pilatus.py b/tests/epics/adpilatus/test_pilatus.py new file mode 100644 index 0000000000..8efd84dc9f --- /dev/null +++ b/tests/epics/adpilatus/test_pilatus.py @@ -0,0 +1,141 @@ +from typing import Awaitable, Callable +from unittest.mock import patch + +import pytest +from bluesky.run_engine import RunEngine + +from ophyd_async.core import ( + DetectorTrigger, + DeviceCollector, + PathProvider, + TriggerInfo, + set_mock_value, +) +from ophyd_async.epics import adpilatus + + +@pytest.fixture +async def test_adpilatus( + RE: RunEngine, + static_path_provider: PathProvider, +) -> adpilatus.PilatusDetector: + async with DeviceCollector(mock=True): + test_adpilatus = adpilatus.PilatusDetector("PILATUS:", static_path_provider) + + return test_adpilatus + + +async def test_deadtime_overridable(static_path_provider: PathProvider): + async with DeviceCollector(mock=True): + test_adpilatus = adpilatus.PilatusDetector( + "PILATUS:", + static_path_provider, + readout_time=adpilatus.PilatusReadoutTime.pilatus2, + ) + pilatus_controller = test_adpilatus.controller + # deadtime invariant with exposure time + assert pilatus_controller.get_deadtime(0) == 2.28e-3 + + +async def test_deadtime_invariant( + test_adpilatus: adpilatus.PilatusDetector, +): + pilatus_controller = test_adpilatus.controller + # deadtime invariant with exposure time + assert pilatus_controller.get_deadtime(0) == 0.95e-3 + assert pilatus_controller.get_deadtime(500) == 0.95e-3 + + +@pytest.mark.parametrize( + "detector_trigger,expected_trigger_mode", + [ + (DetectorTrigger.internal, adpilatus.PilatusTriggerMode.internal), + (DetectorTrigger.internal, adpilatus.PilatusTriggerMode.internal), + (DetectorTrigger.internal, adpilatus.PilatusTriggerMode.internal), + ], +) +async def test_trigger_mode_set( + test_adpilatus: adpilatus.PilatusDetector, + detector_trigger: DetectorTrigger, + expected_trigger_mode: adpilatus.PilatusTriggerMode, +): + async def trigger_and_complete(): + set_mock_value(test_adpilatus.drv.armed_for_triggers, True) + status = await test_adpilatus.controller.arm( + num=1, + trigger=detector_trigger, + ) + await status + + await _trigger(test_adpilatus, expected_trigger_mode, trigger_and_complete) + + +async def test_trigger_mode_set_without_armed_pv( + test_adpilatus: adpilatus.PilatusDetector, +): + async def trigger_and_complete(): + status = await test_adpilatus.controller.arm( + num=1, + trigger=DetectorTrigger.internal, + ) + await status + + with patch( + "ophyd_async.epics.adpilatus._pilatus_controller.DEFAULT_TIMEOUT", + 0.1, + ): + with pytest.raises(TimeoutError): + await _trigger( + test_adpilatus, + adpilatus.PilatusTriggerMode.internal, + trigger_and_complete, + ) + + +async def _trigger( + test_adpilatus: adpilatus.PilatusDetector, + expected_trigger_mode: adpilatus.PilatusTriggerMode, + trigger_and_complete: Callable[[], Awaitable], +): + # Default TriggerMode + assert ( + await test_adpilatus.drv.trigger_mode.get_value() + ) == adpilatus.PilatusTriggerMode.internal + + await trigger_and_complete() + + # TriggerSource changes + assert (await test_adpilatus.drv.trigger_mode.get_value()) == expected_trigger_mode + + +async def test_hints_from_hdf_writer(test_adpilatus: adpilatus.PilatusDetector): + assert test_adpilatus.hints == {"fields": ["test_adpilatus"]} + + +async def test_unsupported_trigger_excepts(test_adpilatus: adpilatus.PilatusDetector): + with pytest.raises( + ValueError, + # str(EnumClass.value) handling changed in Python 3.11 + match=r"PilatusController only supports the following trigger types: .* but", + ): + await test_adpilatus.prepare( + TriggerInfo( + number=1, + trigger=DetectorTrigger.edge_trigger, + deadtime=1.0, + livetime=1.0, + ) + ) + + +async def test_exposure_time_and_acquire_period_set( + test_adpilatus: adpilatus.PilatusDetector, +): + set_mock_value(test_adpilatus.drv.armed_for_triggers, True) + await test_adpilatus.prepare( + TriggerInfo( + number=1, trigger=DetectorTrigger.internal, deadtime=1.0, livetime=1.0 + ) + ) + assert (await test_adpilatus.drv.acquire_time.get_value()) == 1.0 + assert (await test_adpilatus.drv.acquire_period.get_value()) == 1.0 + 950e-6 diff --git a/tests/epics/adpilatus/test_pilatus_controller.py b/tests/epics/adpilatus/test_pilatus_controller.py new file mode 100644 index 0000000000..1145df355e --- /dev/null +++ b/tests/epics/adpilatus/test_pilatus_controller.py @@ -0,0 +1,44 @@ +import pytest + +from ophyd_async.core import DetectorTrigger, DeviceCollector, set_mock_value +from ophyd_async.epics import adcore, adpilatus + + +@pytest.fixture +async def pilatus_driver(RE) -> adpilatus.PilatusDriverIO: + async with DeviceCollector(mock=True): + drv = adpilatus.PilatusDriverIO("DRIVER:") + + return drv + + +@pytest.fixture +async def pilatus( + RE, pilatus_driver: adpilatus.PilatusDriverIO +) -> adpilatus.PilatusController: + async with DeviceCollector(mock=True): + controller = adpilatus.PilatusController(pilatus_driver, readout_time=2.28) + + return controller + + +async def test_pilatus_controller( + RE, + pilatus: adpilatus.PilatusController, + pilatus_driver: adpilatus.PilatusDriverIO, +): + set_mock_value(pilatus_driver.armed_for_triggers, True) + status = await pilatus.arm(num=1, trigger=DetectorTrigger.constant_gate) + await status + + assert await pilatus_driver.num_images.get_value() == 1 + assert await pilatus_driver.image_mode.get_value() == adcore.ImageMode.multiple + assert ( + await pilatus_driver.trigger_mode.get_value() + == adpilatus.PilatusTriggerMode.ext_enable + ) + assert await pilatus_driver.acquire.get_value() is True + + await pilatus.disarm() + + assert await pilatus_driver.acquire.get_value() is False diff --git a/tests/epics/adsimdetector/test_adsim_controller.py b/tests/epics/adsimdetector/test_adsim_controller.py new file mode 100644 index 0000000000..b2610fe31b --- /dev/null +++ b/tests/epics/adsimdetector/test_adsim_controller.py @@ -0,0 +1,30 @@ +from unittest.mock import patch + +import pytest + +from ophyd_async.core import DeviceCollector +from ophyd_async.epics import adcore, adsimdetector + + +@pytest.fixture +async def ad(RE) -> adsimdetector.SimController: + async with DeviceCollector(mock=True): + drv = adcore.ADBaseIO("DRIVER:") + controller = adsimdetector.SimController(drv) + + return controller + + +async def test_ad_controller(RE, ad: adsimdetector.SimController): + with patch("ophyd_async.core._signal.wait_for_value", return_value=None): + await ad.arm(num=1) + + driver = ad.driver + assert await driver.num_images.get_value() == 1 + assert await driver.image_mode.get_value() == adcore.ImageMode.multiple + assert await driver.acquire.get_value() is True + + with patch("ophyd_async.epics.adcore._utils.wait_for_value", return_value=None): + await ad.disarm() + + assert await driver.acquire.get_value() is False diff --git a/tests/epics/demo/test_demo_ad_sim_detector.py b/tests/epics/adsimdetector/test_sim.py similarity index 84% rename from tests/epics/demo/test_demo_ad_sim_detector.py rename to tests/epics/adsimdetector/test_sim.py index 207282c4a5..aa1eb9af8f 100644 --- a/tests/epics/demo/test_demo_ad_sim_detector.py +++ b/tests/epics/adsimdetector/test_sim.py @@ -1,4 +1,4 @@ -"""Integration tests for a StandardDetector using a HDFWriter and ADSimController.""" +"""Integration tests for a StandardDetector using a ADHDFWriter and SimController.""" import time from collections import defaultdict @@ -17,15 +17,11 @@ StandardDetector, StaticFilenameProvider, StaticPathProvider, + assert_emitted, callback_on_mock_put, set_mock_value, ) -from ophyd_async.core.signal import assert_emitted -from ophyd_async.epics.areadetector.controllers import ADSimController -from ophyd_async.epics.areadetector.drivers import ADBase -from ophyd_async.epics.areadetector.utils import FileWriteMode, ImageMode -from ophyd_async.epics.areadetector.writers import HDFWriter, NDFileHDF -from ophyd_async.epics.demo.demo_ad_sim_detector import DemoADSimDetector +from ophyd_async.epics import adcore, adsimdetector async def make_detector(prefix: str, name: str, tmp_path: Path): @@ -33,9 +29,9 @@ async def make_detector(prefix: str, name: str, tmp_path: Path): dp = StaticPathProvider(fp, tmp_path) async with DeviceCollector(mock=True): - drv = ADBase(f"{prefix}DRV:", name="drv") - hdf = NDFileHDF(f"{prefix}HDF:") - det = DemoADSimDetector( + drv = adcore.ADBaseIO(f"{prefix}DRV:", name="drv") + hdf = adcore.NDFileHDFIO(f"{prefix}HDF:") + det = adsimdetector.SimDetector( drv, hdf, dp, config_sigs=[drv.acquire_time, drv.acquire], name=name ) @@ -57,7 +53,7 @@ def count_sim(dets: List[StandardDetector], times: int = 1): read_values = {} for det in dets: read_values[det] = yield from bps.rd( - cast(HDFWriter, det.writer).hdf.num_captured + cast(adcore.ADHDFWriter, det.writer).hdf.num_captured ) for det in dets: @@ -66,7 +62,8 @@ def count_sim(dets: List[StandardDetector], times: int = 1): yield from bps.sleep(0.001) [ set_mock_value( - cast(HDFWriter, det.writer).hdf.num_captured, read_values[det] + 1 + cast(adcore.ADHDFWriter, det.writer).hdf.num_captured, + read_values[det] + 1, ) for det in dets ] @@ -104,7 +101,7 @@ async def two_detectors(tmp_path: Path): writer = det._writer set_mock_value(controller.driver.acquire_time, 0.8 + i) - set_mock_value(controller.driver.image_mode, ImageMode.continuous) + set_mock_value(controller.driver.image_mode, adcore.ImageMode.continuous) set_mock_value(writer.hdf.num_capture, 1000) set_mock_value(writer.hdf.num_captured, 0) set_mock_value(writer.hdf.file_path_exists, True) @@ -114,7 +111,7 @@ async def two_detectors(tmp_path: Path): async def test_two_detectors_fly_different_rate( - two_detectors: List[DemoADSimDetector], RE: RunEngine + two_detectors: List[adsimdetector.SimDetector], RE: RunEngine ): docs = defaultdict(list) @@ -146,25 +143,25 @@ async def test_two_detectors_step( RE.subscribe(lambda name, _: names.append(name)) RE.subscribe(lambda _, doc: docs.append(doc)) [ - set_mock_value(cast(HDFWriter, det._writer).hdf.file_path_exists, True) + set_mock_value(cast(adcore.ADHDFWriter, det._writer).hdf.file_path_exists, True) for det in two_detectors ] RE(count_sim(two_detectors, times=1)) - controller_a = cast(ADSimController, two_detectors[0].controller) - writer_a = cast(HDFWriter, two_detectors[0].writer) - writer_b = cast(HDFWriter, two_detectors[1].writer) + controller_a = cast(adsimdetector.SimController, two_detectors[0].controller) + writer_a = cast(adcore.ADHDFWriter, two_detectors[0].writer) + writer_b = cast(adcore.ADHDFWriter, two_detectors[1].writer) drv = controller_a.driver assert 1 == await drv.acquire.get_value() - assert ImageMode.multiple == await drv.image_mode.get_value() + assert adcore.ImageMode.multiple == await drv.image_mode.get_value() hdfb = writer_b.hdf assert True is await hdfb.lazy_open.get_value() assert True is await hdfb.swmr_mode.get_value() assert 0 == await hdfb.num_capture.get_value() - assert FileWriteMode.stream == await hdfb.file_write_mode.get_value() + assert adcore.FileWriteMode.stream == await hdfb.file_write_mode.get_value() assert names == [ "start", @@ -211,12 +208,14 @@ async def test_detector_writes_to_file( docs = [] RE.subscribe(lambda name, _: names.append(name)) RE.subscribe(lambda _, doc: docs.append(doc)) - set_mock_value(cast(HDFWriter, single_detector._writer).hdf.file_path_exists, True) + set_mock_value( + cast(adcore.ADHDFWriter, single_detector._writer).hdf.file_path_exists, True + ) RE(count_sim([single_detector], times=3)) assert await cast( - HDFWriter, single_detector.writer + adcore.ADHDFWriter, single_detector.writer ).hdf.file_path.get_value() == str(tmp_path) descriptor_index = names.index("descriptor") @@ -278,8 +277,7 @@ async def test_trigger_logic(): 2. The detector.writer.hdf.num_captured is 1 Probably the best thing to do here is mock the detector.controller.driver and - detector.writer.hdf. Then, mock out set_and_wait_for_value in - ophyd_async.epics.DemoADSimDetector.controllers.standard_controller.ADSimController + detector.writer.hdf. Then, mock out set_and_wait_for_value in the SimController so that, as well as setting detector.controller.driver.acquire to True, it sets detector.writer.hdf.num_captured to 1, using set_mock_value """ @@ -290,13 +288,13 @@ async def test_detector_with_unnamed_or_disconnected_config_sigs( RE, static_filename_provider: StaticFilenameProvider, tmp_path: Path ): dp = StaticPathProvider(static_filename_provider, tmp_path) - drv = ADBase("FOO:DRV:") + drv = adcore.ADBaseIO("FOO:DRV:") - some_other_driver = ADBase("TEST") + some_other_driver = adcore.ADBaseIO("TEST") async with DeviceCollector(mock=True): - hdf = NDFileHDF("FOO:HDF:") - det = DemoADSimDetector( + hdf = adcore.NDFileHDFIO("FOO:HDF:") + det = adsimdetector.SimDetector( drv, hdf, dp, diff --git a/tests/epics/advimba/test_vimba.py b/tests/epics/advimba/test_vimba.py new file mode 100644 index 0000000000..0b6c777d4c --- /dev/null +++ b/tests/epics/advimba/test_vimba.py @@ -0,0 +1,134 @@ +import pytest +from bluesky.run_engine import RunEngine + +from ophyd_async.core import ( + DetectorTrigger, + DeviceCollector, + PathProvider, + set_mock_value, +) +from ophyd_async.epics import advimba + + +@pytest.fixture +async def test_advimba( + RE: RunEngine, + static_path_provider: PathProvider, +) -> advimba.VimbaDetector: + async with DeviceCollector(mock=True): + test_advimba = advimba.VimbaDetector("VIMBA:", static_path_provider) + + return test_advimba + + +async def test_get_deadtime( + test_advimba: advimba.VimbaDetector, +): + # Currently Vimba controller doesn't support getting deadtime. + assert test_advimba._controller.get_deadtime(0) == 0.001 + + +async def test_arming_trig_modes(test_advimba: advimba.VimbaDetector): + set_mock_value(test_advimba.drv.trig_source, "Freerun") + set_mock_value(test_advimba.drv.trigger_mode, "Off") + set_mock_value(test_advimba.drv.expose_mode, "Timed") + + async def setup_trigger_mode(trig_mode: DetectorTrigger): + await test_advimba.controller.arm(num=1, trigger=trig_mode) + # Prevent timeouts + set_mock_value(test_advimba.drv.acquire, True) + + # Default TriggerSource + assert (await test_advimba.drv.trig_source.get_value()) == "Freerun" + assert (await test_advimba.drv.trigger_mode.get_value()) == "Off" + assert (await test_advimba.drv.expose_mode.get_value()) == "Timed" + + await setup_trigger_mode(DetectorTrigger.edge_trigger) + assert (await test_advimba.drv.trig_source.get_value()) == "Line1" + assert (await test_advimba.drv.trigger_mode.get_value()) == "On" + assert (await test_advimba.drv.expose_mode.get_value()) == "Timed" + + await setup_trigger_mode(DetectorTrigger.constant_gate) + assert (await test_advimba.drv.trig_source.get_value()) == "Line1" + assert (await test_advimba.drv.trigger_mode.get_value()) == "On" + assert (await test_advimba.drv.expose_mode.get_value()) == "TriggerWidth" + + await setup_trigger_mode(DetectorTrigger.internal) + assert (await test_advimba.drv.trig_source.get_value()) == "Freerun" + assert (await test_advimba.drv.trigger_mode.get_value()) == "Off" + assert (await test_advimba.drv.expose_mode.get_value()) == "Timed" + + await setup_trigger_mode(DetectorTrigger.variable_gate) + assert (await test_advimba.drv.trig_source.get_value()) == "Line1" + assert (await test_advimba.drv.trigger_mode.get_value()) == "On" + assert (await test_advimba.drv.expose_mode.get_value()) == "TriggerWidth" + + +async def test_hints_from_hdf_writer(test_advimba: advimba.VimbaDetector): + assert test_advimba.hints == {"fields": ["test_advimba"]} + + +async def test_can_read(test_advimba: advimba.VimbaDetector): + # Standard detector can be used as Readable + assert (await test_advimba.read()) == {} + + +async def test_decribe_describes_writer_dataset(test_advimba: advimba.VimbaDetector): + set_mock_value(test_advimba._writer.hdf.file_path_exists, True) + set_mock_value(test_advimba._writer.hdf.capture, True) + + assert await test_advimba.describe() == {} + await test_advimba.stage() + assert await test_advimba.describe() == { + "test_advimba": { + "source": "mock+ca://VIMBA:HDF1:FullFileName_RBV", + "shape": (0, 0), + "dtype": "array", + "dtype_numpy": "|i1", + "external": "STREAM:", + } + } + + +async def test_can_collect( + test_advimba: advimba.VimbaDetector, static_path_provider: PathProvider +): + path_info = static_path_provider() + full_file_name = path_info.root / path_info.resource_dir / "foo.h5" + set_mock_value(test_advimba.hdf.full_file_name, str(full_file_name)) + set_mock_value(test_advimba._writer.hdf.file_path_exists, True) + set_mock_value(test_advimba._writer.hdf.capture, True) + await test_advimba.stage() + docs = [(name, doc) async for name, doc in test_advimba.collect_asset_docs(1)] + assert len(docs) == 2 + assert docs[0][0] == "stream_resource" + stream_resource = docs[0][1] + sr_uid = stream_resource["uid"] + assert stream_resource["data_key"] == "test_advimba" + assert stream_resource["uri"] == "file://localhost" + str(full_file_name) + assert stream_resource["parameters"] == { + "dataset": "/entry/data/data", + "swmr": False, + "multiplier": 1, + } + assert docs[1][0] == "stream_datum" + stream_datum = docs[1][1] + assert stream_datum["stream_resource"] == sr_uid + assert stream_datum["seq_nums"] == {"start": 0, "stop": 0} + assert stream_datum["indices"] == {"start": 0, "stop": 1} + + +async def test_can_decribe_collect(test_advimba: advimba.VimbaDetector): + set_mock_value(test_advimba._writer.hdf.file_path_exists, True) + set_mock_value(test_advimba._writer.hdf.capture, True) + assert (await test_advimba.describe_collect()) == {} + await test_advimba.stage() + assert (await test_advimba.describe_collect()) == { + "test_advimba": { + "source": "mock+ca://VIMBA:HDF1:FullFileName_RBV", + "shape": (0, 0), + "dtype": "array", + "dtype_numpy": "|i1", + "external": "STREAM:", + } + } diff --git a/tests/epics/areadetector/test_aravis.py b/tests/epics/areadetector/test_aravis.py deleted file mode 100644 index b2cf16ae35..0000000000 --- a/tests/epics/areadetector/test_aravis.py +++ /dev/null @@ -1,158 +0,0 @@ -import re - -import pytest -from bluesky.run_engine import RunEngine - -from ophyd_async.core import ( - DetectorTrigger, - DeviceCollector, - PathProvider, - TriggerInfo, - set_mock_value, -) -from ophyd_async.epics.areadetector.aravis import AravisDetector - - -@pytest.fixture -async def adaravis( - RE: RunEngine, - static_path_provider: PathProvider, -) -> AravisDetector: - async with DeviceCollector(mock=True): - adaravis = AravisDetector("ADARAVIS:", static_path_provider) - - return adaravis - - -@pytest.mark.parametrize("exposure_time", [0.0, 0.1, 1.0, 10.0, 100.0]) -async def test_deadtime_invariant_with_exposure_time( - exposure_time: float, - adaravis: AravisDetector, -): - assert adaravis.controller.get_deadtime(exposure_time) == 1961e-6 - - -async def test_trigger_source_set_to_gpio_line(adaravis: AravisDetector): - set_mock_value(adaravis.drv.trigger_source, "Freerun") - - async def trigger_and_complete(): - await adaravis.controller.arm(num=1, trigger=DetectorTrigger.edge_trigger) - # Prevent timeouts - set_mock_value(adaravis.drv.acquire, True) - - # Default TriggerSource - assert (await adaravis.drv.trigger_source.get_value()) == "Freerun" - adaravis.set_external_trigger_gpio(1) - # TriggerSource not changed by setting gpio - assert (await adaravis.drv.trigger_source.get_value()) == "Freerun" - - await trigger_and_complete() - - # TriggerSource changes - assert (await adaravis.drv.trigger_source.get_value()) == "Line1" - - adaravis.set_external_trigger_gpio(3) - # TriggerSource not changed by setting gpio - await trigger_and_complete() - assert (await adaravis.drv.trigger_source.get_value()) == "Line3" - - -def test_gpio_pin_limited(adaravis: AravisDetector): - assert adaravis.get_external_trigger_gpio() == 1 - adaravis.set_external_trigger_gpio(2) - assert adaravis.get_external_trigger_gpio() == 2 - with pytest.raises( - ValueError, - match=re.escape( - "AravisDetector only supports the following GPIO indices: " - "(1, 2, 3, 4) but was asked to use 55" - ), - ): - adaravis.set_external_trigger_gpio(55) # type: ignore - - -async def test_hints_from_hdf_writer(adaravis: AravisDetector): - assert adaravis.hints == {"fields": ["adaravis"]} - - -async def test_can_read(adaravis: AravisDetector): - # Standard detector can be used as Readable - assert (await adaravis.read()) == {} - - -async def test_decribe_describes_writer_dataset(adaravis: AravisDetector): - set_mock_value(adaravis._writer.hdf.file_path_exists, True) - set_mock_value(adaravis._writer.hdf.capture, True) - - assert await adaravis.describe() == {} - await adaravis.stage() - assert await adaravis.describe() == { - "adaravis": { - "source": "mock+ca://ADARAVIS:HDF1:FullFileName_RBV", - "shape": (0, 0), - "dtype": "array", - "dtype_numpy": "|i1", - "external": "STREAM:", - } - } - - -async def test_can_collect( - adaravis: AravisDetector, static_path_provider: PathProvider -): - path_info = static_path_provider() - full_file_name = path_info.root / path_info.resource_dir / "foo.h5" - set_mock_value(adaravis.hdf.full_file_name, str(full_file_name)) - set_mock_value(adaravis._writer.hdf.file_path_exists, True) - set_mock_value(adaravis._writer.hdf.capture, True) - await adaravis.stage() - docs = [(name, doc) async for name, doc in adaravis.collect_asset_docs(1)] - assert len(docs) == 2 - assert docs[0][0] == "stream_resource" - stream_resource = docs[0][1] - sr_uid = stream_resource["uid"] - assert stream_resource["data_key"] == "adaravis" - assert stream_resource["uri"] == "file://localhost" + str(full_file_name) - assert stream_resource["parameters"] == { - "dataset": "/entry/data/data", - "swmr": False, - "multiplier": 1, - } - assert docs[1][0] == "stream_datum" - stream_datum = docs[1][1] - assert stream_datum["stream_resource"] == sr_uid - assert stream_datum["seq_nums"] == {"start": 0, "stop": 0} - assert stream_datum["indices"] == {"start": 0, "stop": 1} - - -async def test_can_decribe_collect(adaravis: AravisDetector): - set_mock_value(adaravis._writer.hdf.file_path_exists, True) - set_mock_value(adaravis._writer.hdf.capture, True) - assert (await adaravis.describe_collect()) == {} - await adaravis.stage() - assert (await adaravis.describe_collect()) == { - "adaravis": { - "source": "mock+ca://ADARAVIS:HDF1:FullFileName_RBV", - "shape": (0, 0), - "dtype": "array", - "dtype_numpy": "|i1", - "external": "STREAM:", - } - } - - -async def test_unsupported_trigger_excepts(adaravis: AravisDetector): - with pytest.raises( - ValueError, - # str(EnumClass.value) handling changed in Python 3.11 - match=r"AravisController only supports the following trigger types: .* but", - ): - await adaravis.prepare( - TriggerInfo( - number=1, - trigger=DetectorTrigger.variable_gate, - deadtime=1, - livetime=1, - frame_timeout=3, - ) - ) diff --git a/tests/epics/areadetector/test_controllers.py b/tests/epics/areadetector/test_controllers.py deleted file mode 100644 index 60b82bea5c..0000000000 --- a/tests/epics/areadetector/test_controllers.py +++ /dev/null @@ -1,75 +0,0 @@ -from unittest.mock import patch - -import pytest - -from ophyd_async.core import DetectorTrigger, DeviceCollector, set_mock_value -from ophyd_async.epics.areadetector.controllers import ( - ADSimController, - PilatusController, -) -from ophyd_async.epics.areadetector.drivers import ADBase, PilatusDriver -from ophyd_async.epics.areadetector.drivers.pilatus_driver import PilatusTriggerMode -from ophyd_async.epics.areadetector.utils import ImageMode - - -@pytest.fixture -async def pilatus_driver(RE) -> PilatusDriver: - async with DeviceCollector(mock=True): - drv = PilatusDriver("DRIVER:") - - return drv - - -@pytest.fixture -async def pilatus(RE, pilatus_driver: PilatusDriver) -> PilatusController: - async with DeviceCollector(mock=True): - controller = PilatusController(pilatus_driver, readout_time=2.28) - - return controller - - -@pytest.fixture -async def ad(RE) -> ADSimController: - async with DeviceCollector(mock=True): - drv = ADBase("DRIVER:") - controller = ADSimController(drv) - - return controller - - -async def test_ad_controller(RE, ad: ADSimController): - with patch("ophyd_async.core.signal.wait_for_value", return_value=None): - await ad.arm(num=1) - - driver = ad.driver - assert await driver.num_images.get_value() == 1 - assert await driver.image_mode.get_value() == ImageMode.multiple - assert await driver.acquire.get_value() is True - - with patch( - "ophyd_async.epics.areadetector.utils.wait_for_value", return_value=None - ): - await ad.disarm() - - assert await driver.acquire.get_value() is False - - -async def test_pilatus_controller( - RE, - pilatus: PilatusController, - pilatus_driver: PilatusDriver, -): - set_mock_value(pilatus_driver.armed_for_triggers, True) - status = await pilatus.arm(num=1, trigger=DetectorTrigger.constant_gate) - await status - - assert await pilatus_driver.num_images.get_value() == 1 - assert await pilatus_driver.image_mode.get_value() == ImageMode.multiple - assert ( - await pilatus_driver.trigger_mode.get_value() == PilatusTriggerMode.ext_enable - ) - assert await pilatus_driver.acquire.get_value() is True - - await pilatus.disarm() - - assert await pilatus_driver.acquire.get_value() is False diff --git a/tests/epics/areadetector/test_kinetix.py b/tests/epics/areadetector/test_kinetix.py deleted file mode 100644 index edd49f86b5..0000000000 --- a/tests/epics/areadetector/test_kinetix.py +++ /dev/null @@ -1,122 +0,0 @@ -import pytest -from bluesky.run_engine import RunEngine - -from ophyd_async.core import ( - DetectorTrigger, - DeviceCollector, - StaticPathProvider, - set_mock_value, -) -from ophyd_async.epics.areadetector.kinetix import KinetixDetector - - -@pytest.fixture -async def adkinetix( - RE: RunEngine, - static_path_provider: StaticPathProvider, -) -> KinetixDetector: - async with DeviceCollector(mock=True): - adkinetix = KinetixDetector("KINETIX:", static_path_provider) - - return adkinetix - - -async def test_get_deadtime( - adkinetix: KinetixDetector, -): - # Currently Kinetix driver doesn't support getting deadtime. - assert adkinetix._controller.get_deadtime(0) == 0.001 - - -async def test_trigger_modes(adkinetix: KinetixDetector): - set_mock_value(adkinetix.drv.trigger_mode, "Internal") - - async def setup_trigger_mode(trig_mode: DetectorTrigger): - await adkinetix.controller.arm(num=1, trigger=trig_mode) - # Prevent timeouts - set_mock_value(adkinetix.drv.acquire, True) - - # Default TriggerSource - assert (await adkinetix.drv.trigger_mode.get_value()) == "Internal" - - await setup_trigger_mode(DetectorTrigger.edge_trigger) - assert (await adkinetix.drv.trigger_mode.get_value()) == "Rising Edge" - - await setup_trigger_mode(DetectorTrigger.constant_gate) - assert (await adkinetix.drv.trigger_mode.get_value()) == "Exp. Gate" - - await setup_trigger_mode(DetectorTrigger.internal) - assert (await adkinetix.drv.trigger_mode.get_value()) == "Internal" - - await setup_trigger_mode(DetectorTrigger.variable_gate) - assert (await adkinetix.drv.trigger_mode.get_value()) == "Exp. Gate" - - -async def test_hints_from_hdf_writer(adkinetix: KinetixDetector): - assert adkinetix.hints == {"fields": ["adkinetix"]} - - -async def test_can_read(adkinetix: KinetixDetector): - # Standard detector can be used as Readable - assert (await adkinetix.read()) == {} - - -async def test_decribe_describes_writer_dataset(adkinetix: KinetixDetector): - set_mock_value(adkinetix._writer.hdf.file_path_exists, True) - set_mock_value(adkinetix._writer.hdf.capture, True) - - assert await adkinetix.describe() == {} - await adkinetix.stage() - assert await adkinetix.describe() == { - "adkinetix": { - "source": "mock+ca://KINETIX:HDF1:FullFileName_RBV", - "shape": (0, 0), - "dtype": "array", - "dtype_numpy": "|i1", - "external": "STREAM:", - } - } - - -async def test_can_collect( - adkinetix: KinetixDetector, static_path_provider: StaticPathProvider -): - path_info = static_path_provider() - full_file_name = path_info.root / path_info.resource_dir / "foo.h5" - set_mock_value(adkinetix.hdf.full_file_name, str(full_file_name)) - set_mock_value(adkinetix._writer.hdf.file_path_exists, True) - set_mock_value(adkinetix._writer.hdf.capture, True) - await adkinetix.stage() - docs = [(name, doc) async for name, doc in adkinetix.collect_asset_docs(1)] - assert len(docs) == 2 - assert docs[0][0] == "stream_resource" - stream_resource = docs[0][1] - sr_uid = stream_resource["uid"] - assert stream_resource["data_key"] == "adkinetix" - assert stream_resource["uri"] == "file://localhost" + str(full_file_name) - assert stream_resource["parameters"] == { - "dataset": "/entry/data/data", - "swmr": False, - "multiplier": 1, - } - assert docs[1][0] == "stream_datum" - stream_datum = docs[1][1] - assert stream_datum["stream_resource"] == sr_uid - assert stream_datum["seq_nums"] == {"start": 0, "stop": 0} - assert stream_datum["indices"] == {"start": 0, "stop": 1} - - -async def test_can_decribe_collect(adkinetix: KinetixDetector): - set_mock_value(adkinetix._writer.hdf.file_path_exists, True) - set_mock_value(adkinetix._writer.hdf.capture, True) - assert (await adkinetix.describe_collect()) == {} - await adkinetix.stage() - assert (await adkinetix.describe_collect()) == { - "adkinetix": { - "source": "mock+ca://KINETIX:HDF1:FullFileName_RBV", - "shape": (0, 0), - "dtype": "array", - "dtype_numpy": "|i1", - "external": "STREAM:", - } - } diff --git a/tests/epics/areadetector/test_pilatus.py b/tests/epics/areadetector/test_pilatus.py deleted file mode 100644 index 0601bf3957..0000000000 --- a/tests/epics/areadetector/test_pilatus.py +++ /dev/null @@ -1,132 +0,0 @@ -from typing import Awaitable, Callable -from unittest.mock import patch - -import pytest -from bluesky.run_engine import RunEngine - -from ophyd_async.core import ( - DetectorTrigger, - DeviceCollector, - PathProvider, - TriggerInfo, - set_mock_value, -) -from ophyd_async.epics.areadetector.drivers.pilatus_driver import PilatusTriggerMode -from ophyd_async.epics.areadetector.pilatus import PilatusDetector, PilatusReadoutTime - - -@pytest.fixture -async def pilatus( - RE: RunEngine, - static_path_provider: PathProvider, -) -> PilatusDetector: - async with DeviceCollector(mock=True): - adpilatus = PilatusDetector("PILATUS:", static_path_provider) - - return adpilatus - - -async def test_deadtime_overridable(static_path_provider: PathProvider): - async with DeviceCollector(mock=True): - pilatus = PilatusDetector( - "PILATUS:", - static_path_provider, - readout_time=PilatusReadoutTime.pilatus2, - ) - pilatus_controller = pilatus.controller - # deadtime invariant with exposure time - assert pilatus_controller.get_deadtime(0) == 2.28e-3 - - -async def test_deadtime_invariant( - pilatus: PilatusDetector, -): - pilatus_controller = pilatus.controller - # deadtime invariant with exposure time - assert pilatus_controller.get_deadtime(0) == 0.95e-3 - assert pilatus_controller.get_deadtime(500) == 0.95e-3 - - -@pytest.mark.parametrize( - "detector_trigger,expected_trigger_mode", - [ - (DetectorTrigger.internal, PilatusTriggerMode.internal), - (DetectorTrigger.internal, PilatusTriggerMode.internal), - (DetectorTrigger.internal, PilatusTriggerMode.internal), - ], -) -async def test_trigger_mode_set( - pilatus: PilatusDetector, - detector_trigger: DetectorTrigger, - expected_trigger_mode: PilatusTriggerMode, -): - async def trigger_and_complete(): - set_mock_value(pilatus.drv.armed_for_triggers, True) - status = await pilatus.controller.arm( - num=1, - trigger=detector_trigger, - ) - await status - - await _trigger(pilatus, expected_trigger_mode, trigger_and_complete) - - -async def test_trigger_mode_set_without_armed_pv(pilatus: PilatusDetector): - async def trigger_and_complete(): - status = await pilatus.controller.arm( - num=1, - trigger=DetectorTrigger.internal, - ) - await status - - with patch( - "ophyd_async.epics.areadetector.controllers.pilatus_controller.DEFAULT_TIMEOUT", - 0.1, - ): - with pytest.raises(TimeoutError): - await _trigger(pilatus, PilatusTriggerMode.internal, trigger_and_complete) - - -async def _trigger( - pilatus: PilatusDetector, - expected_trigger_mode: PilatusTriggerMode, - trigger_and_complete: Callable[[], Awaitable], -): - # Default TriggerMode - assert (await pilatus.drv.trigger_mode.get_value()) == PilatusTriggerMode.internal - - await trigger_and_complete() - - # TriggerSource changes - assert (await pilatus.drv.trigger_mode.get_value()) == expected_trigger_mode - - -async def test_hints_from_hdf_writer(pilatus: PilatusDetector): - assert pilatus.hints == {"fields": ["adpilatus"]} - - -async def test_unsupported_trigger_excepts(pilatus: PilatusDetector): - with pytest.raises( - ValueError, - # str(EnumClass.value) handling changed in Python 3.11 - match=r"PilatusController only supports the following trigger types: .* but", - ): - await pilatus.prepare( - TriggerInfo( - number=1, - trigger=DetectorTrigger.edge_trigger, - deadtime=1.0, - livetime=1.0, - ) - ) - - -async def test_exposure_time_and_acquire_period_set(pilatus: PilatusDetector): - set_mock_value(pilatus.drv.armed_for_triggers, True) - await pilatus.prepare( - TriggerInfo( - number=1, trigger=DetectorTrigger.internal, deadtime=1.0, livetime=1.0 - ) - ) - assert (await pilatus.drv.acquire_time.get_value()) == 1.0 - assert (await pilatus.drv.acquire_period.get_value()) == 1.0 + 950e-6 diff --git a/tests/epics/areadetector/test_vimba.py b/tests/epics/areadetector/test_vimba.py deleted file mode 100644 index 1edb645893..0000000000 --- a/tests/epics/areadetector/test_vimba.py +++ /dev/null @@ -1,132 +0,0 @@ -import pytest -from bluesky.run_engine import RunEngine - -from ophyd_async.core import ( - DetectorTrigger, - DeviceCollector, - PathProvider, - set_mock_value, -) -from ophyd_async.epics.areadetector.vimba import VimbaDetector - - -@pytest.fixture -async def advimba( - RE: RunEngine, - static_path_provider: PathProvider, -) -> VimbaDetector: - async with DeviceCollector(mock=True): - advimba = VimbaDetector("VIMBA:", static_path_provider) - - return advimba - - -async def test_get_deadtime( - advimba: VimbaDetector, -): - # Currently Vimba controller doesn't support getting deadtime. - assert advimba._controller.get_deadtime(0) == 0.001 - - -async def test_arming_trig_modes(advimba: VimbaDetector): - set_mock_value(advimba.drv.trig_source, "Freerun") - set_mock_value(advimba.drv.trigger_mode, "Off") - set_mock_value(advimba.drv.expose_mode, "Timed") - - async def setup_trigger_mode(trig_mode: DetectorTrigger): - await advimba.controller.arm(num=1, trigger=trig_mode) - # Prevent timeouts - set_mock_value(advimba.drv.acquire, True) - - # Default TriggerSource - assert (await advimba.drv.trig_source.get_value()) == "Freerun" - assert (await advimba.drv.trigger_mode.get_value()) == "Off" - assert (await advimba.drv.expose_mode.get_value()) == "Timed" - - await setup_trigger_mode(DetectorTrigger.edge_trigger) - assert (await advimba.drv.trig_source.get_value()) == "Line1" - assert (await advimba.drv.trigger_mode.get_value()) == "On" - assert (await advimba.drv.expose_mode.get_value()) == "Timed" - - await setup_trigger_mode(DetectorTrigger.constant_gate) - assert (await advimba.drv.trig_source.get_value()) == "Line1" - assert (await advimba.drv.trigger_mode.get_value()) == "On" - assert (await advimba.drv.expose_mode.get_value()) == "TriggerWidth" - - await setup_trigger_mode(DetectorTrigger.internal) - assert (await advimba.drv.trig_source.get_value()) == "Freerun" - assert (await advimba.drv.trigger_mode.get_value()) == "Off" - assert (await advimba.drv.expose_mode.get_value()) == "Timed" - - await setup_trigger_mode(DetectorTrigger.variable_gate) - assert (await advimba.drv.trig_source.get_value()) == "Line1" - assert (await advimba.drv.trigger_mode.get_value()) == "On" - assert (await advimba.drv.expose_mode.get_value()) == "TriggerWidth" - - -async def test_hints_from_hdf_writer(advimba: VimbaDetector): - assert advimba.hints == {"fields": ["advimba"]} - - -async def test_can_read(advimba: VimbaDetector): - # Standard detector can be used as Readable - assert (await advimba.read()) == {} - - -async def test_decribe_describes_writer_dataset(advimba: VimbaDetector): - set_mock_value(advimba._writer.hdf.file_path_exists, True) - set_mock_value(advimba._writer.hdf.capture, True) - - assert await advimba.describe() == {} - await advimba.stage() - assert await advimba.describe() == { - "advimba": { - "source": "mock+ca://VIMBA:HDF1:FullFileName_RBV", - "shape": (0, 0), - "dtype": "array", - "dtype_numpy": "|i1", - "external": "STREAM:", - } - } - - -async def test_can_collect(advimba: VimbaDetector, static_path_provider: PathProvider): - path_info = static_path_provider() - full_file_name = path_info.root / path_info.resource_dir / "foo.h5" - set_mock_value(advimba.hdf.full_file_name, str(full_file_name)) - set_mock_value(advimba._writer.hdf.file_path_exists, True) - set_mock_value(advimba._writer.hdf.capture, True) - await advimba.stage() - docs = [(name, doc) async for name, doc in advimba.collect_asset_docs(1)] - assert len(docs) == 2 - assert docs[0][0] == "stream_resource" - stream_resource = docs[0][1] - sr_uid = stream_resource["uid"] - assert stream_resource["data_key"] == "advimba" - assert stream_resource["uri"] == "file://localhost" + str(full_file_name) - assert stream_resource["parameters"] == { - "dataset": "/entry/data/data", - "swmr": False, - "multiplier": 1, - } - assert docs[1][0] == "stream_datum" - stream_datum = docs[1][1] - assert stream_datum["stream_resource"] == sr_uid - assert stream_datum["seq_nums"] == {"start": 0, "stop": 0} - assert stream_datum["indices"] == {"start": 0, "stop": 1} - - -async def test_can_decribe_collect(advimba: VimbaDetector): - set_mock_value(advimba._writer.hdf.file_path_exists, True) - set_mock_value(advimba._writer.hdf.capture, True) - assert (await advimba.describe_collect()) == {} - await advimba.stage() - assert (await advimba.describe_collect()) == { - "advimba": { - "source": "mock+ca://VIMBA:HDF1:FullFileName_RBV", - "shape": (0, 0), - "dtype": "array", - "dtype_numpy": "|i1", - "external": "STREAM:", - } - } diff --git a/tests/epics/demo/test_demo.py b/tests/epics/demo/test_demo.py index 011afc565d..5fe63215d0 100644 --- a/tests/epics/demo/test_demo.py +++ b/tests/epics/demo/test_demo.py @@ -242,7 +242,7 @@ async def test_sensor_disconnected(caplog): async with DeviceCollector(timeout=0.1): s = demo.Sensor("ca://PRE:", name="sensor") logs = caplog.get_records("call") - logs = [log for log in logs if "signal" not in log.pathname] + logs = [log for log in logs if "_signal" not in log.pathname] assert len(logs) == 2 assert logs[0].message == ("signal ca://PRE:Value timed out") diff --git a/tests/epics/test_pvi.py b/tests/epics/pvi/test_pvi.py similarity index 100% rename from tests/epics/test_pvi.py rename to tests/epics/pvi/test_pvi.py diff --git a/tests/epics/_backend/test_common.py b/tests/epics/signal/test_common.py similarity index 95% rename from tests/epics/_backend/test_common.py rename to tests/epics/signal/test_common.py index 2958179821..e45a78f031 100644 --- a/tests/epics/_backend/test_common.py +++ b/tests/epics/signal/test_common.py @@ -2,7 +2,7 @@ import pytest -from ophyd_async.epics._backend.common import get_supported_values +from ophyd_async.epics.signal import get_supported_values def test_given_a_non_enum_passed_to_get_supported_enum_then_raises(): diff --git a/tests/epics/test_records.db b/tests/epics/signal/test_records.db similarity index 100% rename from tests/epics/test_records.db rename to tests/epics/signal/test_records.db diff --git a/tests/epics/test_signals.py b/tests/epics/signal/test_signals.py similarity index 98% rename from tests/epics/test_signals.py rename to tests/epics/signal/test_signals.py index 075509816b..61c0ca5d68 100644 --- a/tests/epics/test_signals.py +++ b/tests/epics/signal/test_signals.py @@ -20,19 +20,24 @@ from bluesky.protocols import DataKey, Reading from typing_extensions import TypedDict -from ophyd_async.core import SignalBackend, T, load_from_yaml, save_to_yaml -from ophyd_async.core.signal_backend import SubsetEnum -from ophyd_async.core.utils import NotConnected -from ophyd_async.epics._backend.common import LimitPair, Limits -from ophyd_async.epics.signal._epics_transport import EpicsTransport -from ophyd_async.epics.signal.signal import ( - _make_backend, +from ophyd_async.core import ( + NotConnected, + SignalBackend, + SubsetEnum, + T, + load_from_yaml, + save_to_yaml, +) +from ophyd_async.epics.signal import ( + LimitPair, + Limits, epics_signal_r, epics_signal_rw, epics_signal_rw_rbv, epics_signal_w, epics_signal_x, ) +from ophyd_async.epics.signal._epics_transport import _EpicsTransport # noqa RECORDS = str(Path(__file__).parent / "test_records.db") PV_PREFIX = "".join(random.choice(string.ascii_lowercase) for _ in range(12)) @@ -49,7 +54,7 @@ async def make_backend( # Calculate the pv pv = f"{PV_PREFIX}:{self.protocol}:{suff}" # Make and connect the backend - cls = EpicsTransport[self.protocol].value + cls = _EpicsTransport[self.protocol].value backend = cls(typ, pv, pv) if connect: await asyncio.wait_for(backend.connect(), 10) @@ -720,7 +725,7 @@ def test_make_backend_fails_for_different_transports(): write_pv = "pva://test" with pytest.raises(TypeError) as err: - _make_backend(str, read_pv, write_pv) + epics_signal_rw(str, read_pv, write_pv) assert err.args[0] == f"Differing transports: {read_pv} has EpicsTransport.ca," +" {write_pv} has EpicsTransport.pva" diff --git a/tests/epics/motion/test_motor.py b/tests/epics/test_motor.py similarity index 92% rename from tests/epics/motion/test_motor.py rename to tests/epics/test_motor.py index 6875ffb774..d83a90b3f4 100644 --- a/tests/epics/motion/test_motor.py +++ b/tests/epics/test_motor.py @@ -7,20 +7,17 @@ from bluesky.protocols import Reading from ophyd_async.core import ( + AsyncStatus, + CalculateTimeout, DeviceCollector, + MockSignalBackend, + SignalRW, + callback_on_mock_put, + observe_value, set_mock_put_proceeds, set_mock_value, ) -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.mock_signal_backend import MockSignalBackend -from ophyd_async.core.mock_signal_utils import callback_on_mock_put -from ophyd_async.core.signal import SignalRW, observe_value -from ophyd_async.core.utils import CalculateTimeout -from ophyd_async.epics.motion import motor -from ophyd_async.epics.motion.motor import ( - FlyMotorInfo, - MotorLimitsException, -) +from ophyd_async.epics import motor # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run @@ -199,8 +196,10 @@ async def test_set_velocity(sim_motor: motor.Motor) -> None: async def test_prepare_velocity_errors(sim_motor: motor.Motor): set_mock_value(sim_motor.max_velocity, 10) - with pytest.raises(MotorLimitsException): - fly_info = FlyMotorInfo(start_position=-10, end_position=0, time_for_move=0.9) + with pytest.raises(motor.MotorLimitsException): + fly_info = motor.FlyMotorInfo( + start_position=-10, end_position=0, time_for_move=0.9 + ) await sim_motor._prepare_velocity( fly_info.start_position, fly_info.end_position, @@ -210,7 +209,7 @@ async def test_prepare_velocity_errors(sim_motor: motor.Motor): async def test_valid_prepare_velocity(sim_motor: motor.Motor): set_mock_value(sim_motor.max_velocity, 10) - fly_info = FlyMotorInfo(start_position=-10, end_position=0, time_for_move=1) + fly_info = motor.FlyMotorInfo(start_position=-10, end_position=0, time_for_move=1) await sim_motor._prepare_velocity( fly_info.start_position, fly_info.end_position, @@ -243,7 +242,7 @@ async def test_prepare_motor_path_errors( set_mock_value(sim_motor.acceleration_time, acceleration_time) set_mock_value(sim_motor.low_limit_travel, lower_limit) set_mock_value(sim_motor.high_limit_travel, upper_limit) - with pytest.raises(MotorLimitsException): + with pytest.raises(motor.MotorLimitsException): await sim_motor._prepare_motor_path(velocity, start_position, end_position) @@ -251,7 +250,7 @@ async def test_prepare_motor_path(sim_motor: motor.Motor): set_mock_value(sim_motor.acceleration_time, 1) set_mock_value(sim_motor.low_limit_travel, -10.01) set_mock_value(sim_motor.high_limit_travel, 20.01) - fly_info = FlyMotorInfo( + fly_info = motor.FlyMotorInfo( start_position=0, end_position=10, time_for_move=1, @@ -288,7 +287,7 @@ async def wait_for_status(status: AsyncStatus): await status status = sim_motor.prepare( - FlyMotorInfo( + motor.FlyMotorInfo( start_position=0, end_position=target_position, time_for_move=1, diff --git a/tests/panda/db/panda.db b/tests/fastcs/panda/db/panda.db similarity index 100% rename from tests/panda/db/panda.db rename to tests/fastcs/panda/db/panda.db diff --git a/tests/panda/test_hdf_panda.py b/tests/fastcs/panda/test_hdf_panda.py similarity index 91% rename from tests/panda/test_hdf_panda.py rename to tests/fastcs/panda/test_hdf_panda.py index 4f33f340e0..8163d71c5f 100644 --- a/tests/panda/test_hdf_panda.py +++ b/tests/fastcs/panda/test_hdf_panda.py @@ -6,16 +6,22 @@ from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine -from ophyd_async.core import StaticFilenameProvider, StaticPathProvider, set_mock_value -from ophyd_async.core.device import Device -from ophyd_async.core.flyer import HardwareTriggeredFlyable -from ophyd_async.core.mock_signal_utils import callback_on_mock_put -from ophyd_async.core.signal import SignalR, assert_emitted -from ophyd_async.panda import ( +from ophyd_async.core import ( + Device, + SignalR, + StandardFlyer, + StaticFilenameProvider, + StaticPathProvider, + assert_emitted, + callback_on_mock_put, + set_mock_value, +) +from ophyd_async.fastcs.panda import ( + DatasetTable, HDFPanda, + PandaHdf5DatasetType, StaticSeqTableTriggerLogic, ) -from ophyd_async.panda._table import DatasetTable, PandaHdf5DatasetType from ophyd_async.plan_stubs import ( prepare_static_seq_table_flyer_and_detectors_with_same_trigger, ) @@ -79,7 +85,7 @@ def append_and_print(name, doc): exposure = 1 trigger_logic = StaticSeqTableTriggerLogic(mock_hdf_panda.seq[1]) - flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer") + flyer = StandardFlyer(trigger_logic, [], name="flyer") def flying_plan(): yield from bps.stage_all(mock_hdf_panda, flyer) diff --git a/tests/panda/test_panda_connect.py b/tests/fastcs/panda/test_panda_connect.py similarity index 90% rename from tests/panda/test_panda_connect.py rename to tests/fastcs/panda/test_panda_connect.py index 60ac635e93..d5d0900b3b 100644 --- a/tests/panda/test_panda_connect.py +++ b/tests/fastcs/panda/test_panda_connect.py @@ -6,11 +6,22 @@ import numpy as np import pytest -from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceCollector, DeviceVector -from ophyd_async.core.utils import NotConnected -from ophyd_async.epics.pvi import PVIEntry, fill_pvi_entries -from ophyd_async.epics.pvi.pvi import create_children_from_annotations -from ophyd_async.panda import PcapBlock, PulseBlock, SeqBlock, SeqTable, SeqTrigger +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + Device, + DeviceCollector, + DeviceVector, + NotConnected, +) +from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries +from ophyd_async.epics.pvi._pvi import _PVIEntry # Allow as edge case for typing +from ophyd_async.fastcs.panda import ( + PcapBlock, + PulseBlock, + SeqBlock, + SeqTable, + SeqTrigger, +) class DummyDict: @@ -22,7 +33,7 @@ def todict(self): class MockPvi: - def __init__(self, pvi: Dict[str, PVIEntry]) -> None: + def __init__(self, pvi: Dict[str, _PVIEntry]) -> None: self.pvi = pvi def get(self, item: str): @@ -30,7 +41,7 @@ def get(self, item: str): class MockCtxt: - def __init__(self, pvi: Dict[str, PVIEntry]) -> None: + def __init__(self, pvi: Dict[str, _PVIEntry]) -> None: self.pvi = copy.copy(pvi) def get(self, pv: str, timeout: float = 0.0): diff --git a/tests/panda/test_panda_controller.py b/tests/fastcs/panda/test_panda_controller.py similarity index 85% rename from tests/panda/test_panda_controller.py rename to tests/fastcs/panda/test_panda_controller.py index 7cb64cdf8c..e691b737ee 100644 --- a/tests/panda/test_panda_controller.py +++ b/tests/fastcs/panda/test_panda_controller.py @@ -7,7 +7,7 @@ from ophyd_async.core import DEFAULT_TIMEOUT, DetectorTrigger, Device, DeviceCollector from ophyd_async.epics.pvi import fill_pvi_entries from ophyd_async.epics.signal import epics_signal_rw -from ophyd_async.panda import CommonPandaBlocks, PandaPcapController +from ophyd_async.fastcs.panda import CommonPandaBlocks, PandaPcapController @pytest.fixture @@ -34,7 +34,9 @@ class PcapBlock(Device): pass # Not filled pandaController = PandaPcapController(pcap=PcapBlock()) - with patch("ophyd_async.panda._panda_controller.wait_for_value", return_value=None): + with patch( + "ophyd_async.fastcs.panda._panda_controller.wait_for_value", return_value=None + ): with pytest.raises(AttributeError) as exc: await pandaController.arm(num=1, trigger=DetectorTrigger.constant_gate) assert ("'PcapBlock' object has no attribute 'arm'") in str(exc.value) @@ -42,7 +44,9 @@ class PcapBlock(Device): async def test_panda_controller_arm_disarm(mock_panda): pandaController = PandaPcapController(mock_panda.pcap) - with patch("ophyd_async.panda._panda_controller.wait_for_value", return_value=None): + with patch( + "ophyd_async.fastcs.panda._panda_controller.wait_for_value", return_value=None + ): await pandaController.arm(num=1, trigger=DetectorTrigger.constant_gate) await pandaController.disarm() diff --git a/tests/panda/test_panda_utils.py b/tests/fastcs/panda/test_panda_utils.py similarity index 88% rename from tests/panda/test_panda_utils.py rename to tests/fastcs/panda/test_panda_utils.py index 16b19843b4..28510d08fa 100644 --- a/tests/panda/test_panda_utils.py +++ b/tests/fastcs/panda/test_panda_utils.py @@ -3,18 +3,16 @@ import pytest from bluesky import RunEngine -from ophyd_async.core import save_device -from ophyd_async.core.device import DeviceCollector -from ophyd_async.core.utils import DEFAULT_TIMEOUT +from ophyd_async.core import DEFAULT_TIMEOUT, DeviceCollector, save_device from ophyd_async.epics.pvi import fill_pvi_entries from ophyd_async.epics.signal import epics_signal_rw -from ophyd_async.panda import ( +from ophyd_async.fastcs.panda import ( CommonPandaBlocks, + DataBlock, PcompDirectionOptions, TimeUnits, + phase_sorter, ) -from ophyd_async.panda._common_blocks import DataBlock -from ophyd_async.panda._utils import phase_sorter @pytest.fixture @@ -39,7 +37,7 @@ async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): yield mock_panda -@patch("ophyd_async.core.device_save_loader.save_to_yaml") +@patch("ophyd_async.core._device_save_loader.save_to_yaml") async def test_save_panda(mock_save_to_yaml, mock_panda, RE: RunEngine): RE(save_device(mock_panda, "path", sorter=phase_sorter)) mock_save_to_yaml.assert_called_once() diff --git a/tests/panda/test_table.py b/tests/fastcs/panda/test_table.py similarity index 94% rename from tests/panda/test_table.py rename to tests/fastcs/panda/test_table.py index 735fa32886..ad92683bbd 100644 --- a/tests/panda/test_table.py +++ b/tests/fastcs/panda/test_table.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from ophyd_async.panda._table import seq_table_from_arrays +from ophyd_async.fastcs.panda import seq_table_from_arrays def test_from_arrays_inconsistent_lengths(): diff --git a/tests/panda/test_trigger.py b/tests/fastcs/panda/test_trigger.py similarity index 93% rename from tests/panda/test_trigger.py rename to tests/fastcs/panda/test_trigger.py index f1bd103af5..1a76614afa 100644 --- a/tests/panda/test_trigger.py +++ b/tests/fastcs/panda/test_trigger.py @@ -4,10 +4,9 @@ import pytest from pydantic import ValidationError -from ophyd_async.core import set_mock_value -from ophyd_async.core.device import DEFAULT_TIMEOUT, DeviceCollector -from ophyd_async.epics.pvi.pvi import fill_pvi_entries -from ophyd_async.panda import ( +from ophyd_async.core import DEFAULT_TIMEOUT, DeviceCollector, set_mock_value +from ophyd_async.epics.pvi import fill_pvi_entries +from ophyd_async.fastcs.panda import ( CommonPandaBlocks, PcompInfo, SeqTable, diff --git a/tests/panda/test_writer.py b/tests/fastcs/panda/test_writer.py similarity index 94% rename from tests/panda/test_writer.py rename to tests/fastcs/panda/test_writer.py index 5231b7c5d7..6aaae7f0c9 100644 --- a/tests/panda/test_writer.py +++ b/tests/fastcs/panda/test_writer.py @@ -8,16 +8,19 @@ DEFAULT_TIMEOUT, Device, DeviceCollector, + HDFFile, SignalR, StaticFilenameProvider, StaticPathProvider, set_mock_value, ) -from ophyd_async.epics.areadetector.writers.general_hdffile import _HDFFile from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries -from ophyd_async.panda import CommonPandaBlocks -from ophyd_async.panda._table import DatasetTable, PandaHdf5DatasetType -from ophyd_async.panda.writers._hdf_writer import PandaHDFWriter +from ophyd_async.fastcs.panda import ( + CommonPandaBlocks, + DatasetTable, + PandaHdf5DatasetType, + PandaHDFWriter, +) TABLES = [ DatasetTable( @@ -191,7 +194,7 @@ def assert_resource_document(name, resource_doc): assert "mock_panda/data.h5" in resource_doc["uri"] [item async for item in mock_writer.collect_stream_docs(1)] - assert type(mock_writer._file) is _HDFFile + assert type(mock_writer._file) is HDFFile assert mock_writer._file._last_emitted == 1 for i in range(len(table["name"])): diff --git a/tests/plan_stubs/test_ensure_connected.py b/tests/plan_stubs/test_ensure_connected.py index f77ad50355..c876932f58 100644 --- a/tests/plan_stubs/test_ensure_connected.py +++ b/tests/plan_stubs/test_ensure_connected.py @@ -1,8 +1,6 @@ import pytest -from ophyd_async.core import Device, NotConnected -from ophyd_async.core.mock_signal_backend import MockSignalBackend -from ophyd_async.core.signal import SignalRW +from ophyd_async.core import Device, MockSignalBackend, NotConnected, SignalRW from ophyd_async.epics.signal import epics_signal_rw from ophyd_async.plan_stubs import ensure_connected diff --git a/tests/plan_stubs/test_fly.py b/tests/plan_stubs/test_fly.py index b634d39013..791935c111 100644 --- a/tests/plan_stubs/test_fly.py +++ b/tests/plan_stubs/test_fly.py @@ -10,21 +10,23 @@ from ophyd_async.core import ( DEFAULT_TIMEOUT, + AsyncReadable, + AsyncStatus, DetectorControl, DetectorWriter, - HardwareTriggeredFlyable, + DeviceCollector, + SignalR, + StandardDetector, + StandardFlyer, + TriggerLogic, + WatchableAsyncStatus, + WatcherUpdate, observe_value, set_mock_value, ) -from ophyd_async.core.async_status import AsyncStatus, WatchableAsyncStatus -from ophyd_async.core.detector import StandardDetector -from ophyd_async.core.device import DeviceCollector -from ophyd_async.core.flyer import TriggerLogic -from ophyd_async.core.signal import SignalR -from ophyd_async.core.utils import WatcherUpdate -from ophyd_async.epics.pvi.pvi import fill_pvi_entries -from ophyd_async.epics.signal.signal import epics_signal_rw -from ophyd_async.panda import ( +from ophyd_async.epics.pvi import fill_pvi_entries +from ophyd_async.epics.signal import epics_signal_rw +from ophyd_async.fastcs.panda import ( CommonPandaBlocks, StaticPcompTriggerLogic, StaticSeqTableTriggerLogic, @@ -33,7 +35,6 @@ prepare_static_seq_table_flyer_and_detectors_with_same_trigger, time_resolved_fly_and_collect_with_static_seq_table, ) -from ophyd_async.protocols import AsyncReadable class DummyWriter(DetectorWriter): @@ -183,7 +184,7 @@ async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT): yield mock_panda -class MockFlyer(HardwareTriggeredFlyable): +class MockFlyer(StandardFlyer): def __init__( self, trigger_logic: TriggerLogic, @@ -250,7 +251,7 @@ def append_and_print(name, doc): shutter_time = 0.004 trigger_logic = StaticSeqTableTriggerLogic(mock_panda.seq[1]) - flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer") + flyer = StandardFlyer(trigger_logic, [], name="flyer") def flying_plan(): yield from bps.stage_all(*detector_list, flyer) @@ -398,7 +399,7 @@ def fly(): @pytest.mark.parametrize("timeout_setting,expected_timeout", [(None, 12), (5.0, 5.0)]) async def test_trigger_sets_or_defaults_timeout( RE: RunEngine, - seq_flyer: HardwareTriggeredFlyable, + seq_flyer: StandardFlyer, detectors: tuple[StandardDetector, ...], timeout_setting: float | None, expected_timeout: float, diff --git a/tests/sim/conftest.py b/tests/sim/conftest.py index fae8a7de08..fe0871f5e0 100644 --- a/tests/sim/conftest.py +++ b/tests/sim/conftest.py @@ -2,14 +2,14 @@ import pytest -from ophyd_async.core.device import DeviceCollector -from ophyd_async.sim import SimPatternDetector +from ophyd_async.core import DeviceCollector +from ophyd_async.sim.demo import PatternDetector @pytest.fixture -async def sim_pattern_detector(tmp_path_factory) -> SimPatternDetector: +async def sim_pattern_detector(tmp_path_factory) -> PatternDetector: path: Path = tmp_path_factory.mktemp("tmp") async with DeviceCollector(mock=True): - sim_pattern_detector = SimPatternDetector(name="PATTERN1", path=path) + sim_pattern_detector = PatternDetector(name="PATTERN1", path=path) return sim_pattern_detector diff --git a/tests/sim/demo/test_sim_motor.py b/tests/sim/demo/test_sim_motor.py index b8953df8c8..7d4ed46f19 100644 --- a/tests/sim/demo/test_sim_motor.py +++ b/tests/sim/demo/test_sim_motor.py @@ -4,8 +4,8 @@ from bluesky.plans import spiral_square from bluesky.run_engine import RunEngine -from ophyd_async.core.device import DeviceCollector -from ophyd_async.sim.demo.sim_motor import SimMotor +from ophyd_async.core import DeviceCollector +from ophyd_async.sim.demo import SimMotor async def test_move_sim_in_plan(): diff --git a/tests/sim/test_pattern_generator.py b/tests/sim/test_pattern_generator.py index 27871dfc77..a8522c59c5 100644 --- a/tests/sim/test_pattern_generator.py +++ b/tests/sim/test_pattern_generator.py @@ -1,6 +1,6 @@ import pytest -from ophyd_async.sim.pattern_generator import PatternGenerator +from ophyd_async.sim.demo import PatternGenerator @pytest.fixture diff --git a/tests/sim/test_sim_detector.py b/tests/sim/test_sim_detector.py index 838be5a6d9..785afee3d9 100644 --- a/tests/sim/test_sim_detector.py +++ b/tests/sim/test_sim_detector.py @@ -6,10 +6,9 @@ import pytest from bluesky import RunEngine -from ophyd_async.core.device import DeviceCollector -from ophyd_async.core.signal import assert_emitted -from ophyd_async.epics.motion import motor -from ophyd_async.sim.sim_pattern_generator import SimPatternDetector +from ophyd_async.core import DeviceCollector, assert_emitted +from ophyd_async.epics import motor +from ophyd_async.sim.demo import PatternDetector @pytest.fixture @@ -20,7 +19,7 @@ async def sim_motor(): async def test_sim_pattern_detector_initialization( - sim_pattern_detector: SimPatternDetector, + sim_pattern_detector: PatternDetector, ): assert ( sim_pattern_detector.pattern_generator @@ -28,14 +27,14 @@ async def test_sim_pattern_detector_initialization( async def test_detector_creates_controller_and_writer( - sim_pattern_detector: SimPatternDetector, + sim_pattern_detector: PatternDetector, ): assert sim_pattern_detector.writer assert sim_pattern_detector.controller async def test_writes_pattern_to_file( - sim_pattern_detector: SimPatternDetector, + sim_pattern_detector: PatternDetector, RE: RunEngine, ): # assert that the file contains data in expected dimensions diff --git a/tests/sim/test_sim_writer.py b/tests/sim/test_sim_writer.py index 049b566fb2..89f2540e86 100644 --- a/tests/sim/test_sim_writer.py +++ b/tests/sim/test_sim_writer.py @@ -2,21 +2,20 @@ import pytest -from ophyd_async.core.device import DeviceCollector -from ophyd_async.sim import PatternGenerator -from ophyd_async.sim.sim_pattern_detector_writer import SimPatternDetectorWriter +from ophyd_async.core import DeviceCollector +from ophyd_async.sim.demo import PatternDetectorWriter, PatternGenerator @pytest.fixture -async def writer(static_path_provider) -> SimPatternDetectorWriter: +async def writer(static_path_provider) -> PatternDetectorWriter: async with DeviceCollector(mock=True): driver = PatternGenerator() - return SimPatternDetectorWriter(driver, static_path_provider, lambda: "NAME") + return PatternDetectorWriter(driver, static_path_provider, lambda: "NAME") -async def test_correct_descriptor_doc_after_open(writer: SimPatternDetectorWriter): - with patch("ophyd_async.core.signal.wait_for_value", return_value=None): +async def test_correct_descriptor_doc_after_open(writer: PatternDetectorWriter): + with patch("ophyd_async.core._signal.wait_for_value", return_value=None): descriptor = await writer.open() assert descriptor == { @@ -37,7 +36,7 @@ async def test_correct_descriptor_doc_after_open(writer: SimPatternDetectorWrite await writer.close() -async def test_collect_stream_docs(writer: SimPatternDetectorWriter): +async def test_collect_stream_docs(writer: PatternDetectorWriter): await writer.open() [item async for item in writer.collect_stream_docs(1)] assert writer.pattern_generator._handle_for_h5_file diff --git a/tests/sim/test_streaming_plan.py b/tests/sim/test_streaming_plan.py index e5369354a7..9a4b46a80e 100644 --- a/tests/sim/test_streaming_plan.py +++ b/tests/sim/test_streaming_plan.py @@ -3,14 +3,14 @@ from bluesky import plans as bp from bluesky.run_engine import RunEngine -from ophyd_async.core.signal import assert_emitted -from ophyd_async.sim.sim_pattern_generator import SimPatternDetector +from ophyd_async.core import assert_emitted +from ophyd_async.sim.demo import PatternDetector # NOTE the async operations with h5py are non-trival # because of lack of native support for async operations # see https://github.com/h5py/h5py/issues/837 -async def test_streaming_plan(RE: RunEngine, sim_pattern_detector: SimPatternDetector): +async def test_streaming_plan(RE: RunEngine, sim_pattern_detector: PatternDetector): names = [] docs = [] @@ -36,7 +36,7 @@ def append_and_print(name, doc): await sim_pattern_detector.writer.close() -async def test_plan(RE: RunEngine, sim_pattern_detector: SimPatternDetector): +async def test_plan(RE: RunEngine, sim_pattern_detector: PatternDetector): docs = defaultdict(list) RE(bp.count([sim_pattern_detector]), lambda name, doc: docs[name].append(doc)) assert_emitted( diff --git a/tests/test_log.py b/tests/test_log.py deleted file mode 100644 index c90b48c940..0000000000 --- a/tests/test_log.py +++ /dev/null @@ -1,88 +0,0 @@ -import io -import logging -import logging.handlers -from unittest.mock import MagicMock, patch - -import pytest - -from ophyd_async import log -from ophyd_async.core import Device -from ophyd_async.log import DEFAULT_DATE_FORMAT, DEFAULT_FORMAT - - -def test_validate_level(): - assert log._validate_level("CRITICAL") == 50 - assert log._validate_level("ERROR") == 40 - assert log._validate_level("WARNING") == 30 - assert log._validate_level("INFO") == 20 - assert log._validate_level("DEBUG") == 10 - assert log._validate_level("NOTSET") == 0 - assert log._validate_level(123) == 123 - with pytest.raises(ValueError): - log._validate_level("MYSTERY") - - -@patch("ophyd_async.log.current_handler") -@patch("ophyd_async.log.logging.Logger.addHandler") -def test_default_config_ophyd_async_logging(mock_add_handler, mock_current_handler): - log.config_ophyd_async_logging() - assert isinstance(log.current_handler, logging.StreamHandler) - assert log.logger.getEffectiveLevel() <= logging.WARNING - - -@patch("ophyd_async.log.current_handler") -@patch("ophyd_async.log.logging.FileHandler") -@patch("ophyd_async.log.logging.Logger.addHandler") -def test_config_ophyd_async_logging_with_file_handler( - mock_add_handler, mock_file_handler, mock_current_handler -): - log.config_ophyd_async_logging(file="file") - assert isinstance(log.current_handler, MagicMock) - assert log.logger.getEffectiveLevel() <= logging.WARNING - - -@patch("ophyd_async.log.current_handler") -def test_config_ophyd_async_logging_removes_extra_handlers(mock_current_handler): - # Protect global variable in other pytests - class FakeLogger: - def __init__(self): - self.handlers = [] - self.removeHandler = MagicMock() - self.setLevel = MagicMock() - - def addHandler(self, handler): - self.handlers.append(handler) - - def getEffectiveLevel(self): - return 100000 - - fake_logger = FakeLogger() - with ( - patch("ophyd_async.log.logger", fake_logger), - ): - log.config_ophyd_async_logging() - fake_logger.removeHandler.assert_not_called() - log.config_ophyd_async_logging() - fake_logger.removeHandler.assert_called() - - -# Full format looks like: -#'[test_device][W 240501 13:28:08.937 test_log:35] here is a warning\n' -def test_logger_adapter_ophyd_async_device(): - log_buffer = io.StringIO() - log_stream = logging.StreamHandler(stream=log_buffer) - log_stream.setFormatter( - log.ColoredFormatterWithDeviceName( - fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, no_color=True - ) - ) - log.logger.addHandler(log_stream) - - device = Device(name="test_device") - device.log = logging.LoggerAdapter( - logging.getLogger("ophyd_async.devices"), - {"ophyd_async_device_name": device.name}, - ) - device.log.warning("here is a warning") - assert log_buffer.getvalue().startswith("[test_device]") - assert log_buffer.getvalue().endswith("here is a warning\n")