From 9dc909e026b629de9a9912015fc6ebba39908e5b Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 2 May 2024 11:08:54 +0100 Subject: [PATCH 01/11] Standardise StandardDetector implementations --- src/ophyd_async/epics/areadetector/aravis.py | 14 +++++++------- src/ophyd_async/epics/areadetector/kinetix.py | 14 +++++++------- src/ophyd_async/epics/areadetector/pilatus.py | 17 +++++++---------- src/ophyd_async/epics/areadetector/vimba.py | 14 +++++++------- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/ophyd_async/epics/areadetector/aravis.py b/src/ophyd_async/epics/areadetector/aravis.py index 77fdc2a487..ac14decd4b 100644 --- a/src/ophyd_async/epics/areadetector/aravis.py +++ b/src/ophyd_async/epics/areadetector/aravis.py @@ -23,16 +23,16 @@ class AravisDetector(StandardDetector, HasHints): def __init__( self, - name: str, + prefix: str, directory_provider: DirectoryProvider, - driver: AravisDriver, - hdf: NDFileHDF, + drv_suffix="cam1:", + hdf_suffix="HDF1:", + name="", gpio_number: AravisController.GPIO_NUMBER = 1, **scalar_sigs: str, ): - # Must be child of Detector to pick up connect() - self.drv = driver - self.hdf = hdf + self.drv = AravisDriver(prefix + drv_suffix) + self.hdf = NDFileHDF(prefix + hdf_suffix) super().__init__( AravisController(self.drv, gpio_number=gpio_number), @@ -43,7 +43,7 @@ def __init__( ADBaseShapeProvider(self.drv), **scalar_sigs, ), - config_sigs=(self.drv.acquire_time, self.drv.acquire), + config_sigs=(self.drv.acquire_time,), name=name, ) diff --git a/src/ophyd_async/epics/areadetector/kinetix.py b/src/ophyd_async/epics/areadetector/kinetix.py index 36ec479a49..232878cc55 100644 --- a/src/ophyd_async/epics/areadetector/kinetix.py +++ b/src/ophyd_async/epics/areadetector/kinetix.py @@ -20,15 +20,15 @@ class KinetixDetector(StandardDetector, HasHints): def __init__( self, - name: str, + prefix: str, directory_provider: DirectoryProvider, - driver: KinetixDriver, - hdf: NDFileHDF, + drv_suffix="cam1:", + hdf_suffix="HDF1:", + name="", **scalar_sigs: str, ): - # Must be child of Detector to pick up connect() - self.drv = driver - self.hdf = hdf + self.drv = KinetixDriver(prefix + drv_suffix) + self.hdf = NDFileHDF(prefix + hdf_suffix) super().__init__( KinetixController(self.drv), @@ -39,7 +39,7 @@ def __init__( ADBaseShapeProvider(self.drv), **scalar_sigs, ), - config_sigs=(self.drv.acquire_time, self.drv.acquire), + config_sigs=(self.drv.acquire_time,), name=name, ) diff --git a/src/ophyd_async/epics/areadetector/pilatus.py b/src/ophyd_async/epics/areadetector/pilatus.py index fc3dd7f158..b6443fa189 100644 --- a/src/ophyd_async/epics/areadetector/pilatus.py +++ b/src/ophyd_async/epics/areadetector/pilatus.py @@ -1,10 +1,7 @@ -from typing import Optional, Sequence - from bluesky.protocols import Hints from ophyd_async.core import DirectoryProvider from ophyd_async.core.detector import StandardDetector -from ophyd_async.core.signal import SignalR from ophyd_async.epics.areadetector.controllers.pilatus_controller import ( PilatusController, ) @@ -22,15 +19,15 @@ class PilatusDetector(StandardDetector): def __init__( self, - name: str, + prefix: str, directory_provider: DirectoryProvider, - driver: PilatusDriver, - hdf: NDFileHDF, - config_sigs: Optional[Sequence[SignalR]] = None, + drv_suffix="cam1:", + hdf_suffix="HDF1:", + name="", **scalar_sigs: str, ): - self.drv = driver - self.hdf = hdf + self.drv = PilatusDriver(prefix + drv_suffix) + self.hdf = NDFileHDF(prefix + hdf_suffix) super().__init__( PilatusController(self.drv), @@ -41,7 +38,7 @@ def __init__( ADBaseShapeProvider(self.drv), **scalar_sigs, ), - config_sigs=config_sigs or (self.drv.acquire_time,), + config_sigs=(self.drv.acquire_time,), name=name, ) diff --git a/src/ophyd_async/epics/areadetector/vimba.py b/src/ophyd_async/epics/areadetector/vimba.py index 5e764b5b20..83f3da1974 100644 --- a/src/ophyd_async/epics/areadetector/vimba.py +++ b/src/ophyd_async/epics/areadetector/vimba.py @@ -17,15 +17,15 @@ class VimbaDetector(StandardDetector, HasHints): def __init__( self, - name: str, + prefix: str, directory_provider: DirectoryProvider, - driver: VimbaDriver, - hdf: NDFileHDF, + drv_suffix="cam1:", + hdf_suffix="HDF1:", + name="", **scalar_sigs: str, ): - # Must be child of Detector to pick up connect() - self.drv = driver - self.hdf = hdf + self.drv = VimbaDriver(prefix + drv_suffix) + self.hdf = NDFileHDF(prefix + hdf_suffix) super().__init__( VimbaController(self.drv), @@ -36,7 +36,7 @@ def __init__( ADBaseShapeProvider(self.drv), **scalar_sigs, ), - config_sigs=(self.drv.acquire_time, self.drv.acquire), + config_sigs=(self.drv.acquire_time,), name=name, ) From e4b78e6cdea974e54a79e5b985ed4b0445b644f5 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 2 May 2024 13:10:20 +0100 Subject: [PATCH 02/11] Add docs on writing a StandardDetector implementation --- docs/examples/foo_detector.py | 84 ++++++++++++++++++++++++ docs/how-to/make-a-standard-detector.rst | 45 +++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 docs/examples/foo_detector.py create mode 100644 docs/how-to/make-a-standard-detector.rst diff --git a/docs/examples/foo_detector.py b/docs/examples/foo_detector.py new file mode 100644 index 0000000000..15b86bb385 --- /dev/null +++ b/docs/examples/foo_detector.py @@ -0,0 +1,84 @@ +import asyncio +from typing import Optional + +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.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 + + +class FooDriver(ADBase): + def __init__(self, prefix: str, name: str = "") -> None: + self.trigger_mode = ad_rw(str, prefix + "TriggerMode") + super().__init__(prefix, name) + + +class FooController(DetectorControl): + def __init__(self, driver: FooDriver) -> None: + self._drv = driver + + def get_deadtime(self, exposure: float) -> float: + # FooDetector deadtime handling + return 0.001 + + async def arm( + self, + num: int, + trigger: DetectorTrigger = DetectorTrigger.internal, + exposure: Optional[float] = None, + ) -> AsyncStatus: + await asyncio.gather( + self._drv.num_images.set(num), + self._drv.image_mode.set(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) + + async def disarm(self): + await stop_busy_record(self._drv.acquire, False, timeout=1) + + +class FooDetector(StandardDetector, HasHints): + + _controller: FooController + _writer: HDFWriter + + def __init__( + self, + prefix: str, + directory_provider: DirectoryProvider, + drv_suffix="cam1:", + hdf_suffix="HDF1:", + name="", + **scalar_sigs: str, + ): + self.drv = FooDriver(prefix + drv_suffix) + self.hdf = NDFileHDF(prefix + hdf_suffix) + + super().__init__( + FooController(self.drv), + HDFWriter( + self.hdf, + directory_provider, + lambda: self.name, + ADBaseShapeProvider(self.drv), + **scalar_sigs, + ), + config_sigs=(self.drv.acquire_time,), + name=name, + ) + + @property + def hints(self) -> Hints: + return self._writer.hints diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst new file mode 100644 index 0000000000..943d281daa --- /dev/null +++ b/docs/how-to/make-a-standard-detector.rst @@ -0,0 +1,45 @@ +.. note:: + + Ophyd async is included on a provisional basis until the v1.0 release and + may change API on minor release numbers before then + +Make a StandardDetector +======================= + +.. currentmodule:: ophyd_async.core + +`StandardDetector` is an abstract class to assist in creating devices to control EPICS AreaDetector implementations. +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 `HDFWriter`) +- `DetectorControl` with logic for arming and disarming the detector. This will be unique to the StandardDetector implementation. + +These standard components are not devices, and therefore not subdevices of the `StandardDetector`, typically they are enabled by the use of two other components which are: + +- An implementation of `NDPluginBase`, an entity object mapping to an AreaDetector NDPluginFile instance (for `HDFWriter` an instance of `NDFileHDF`) +- `ADBase`, or an class which extends it, an entity object mapping to an AreaDetector "NDArray" for the "driver" of the detector implementation + +Writing a StandardDetector implementation +----------------------------------------- + +Define a `FooDriver` if the NDArray requires fields in addition to those on `ADBase` to be exposed. It should extend `ADBase`. +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 + :pyobject: FooDriver + +Define a `FooController` with handling for converting the standard pattern of `arm` and `disarm` to required state of `FooDriver` e.g. setting a compatible `FooTriggerSource` for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. +The `get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **In the case that it requires fetching values from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** + +.. literalinclude:: ../examples/foo_detector.py + :pyobject: FooController + +Assembly +-------- + +Define a `FooDetector` implementation to tie the Driver, Controller and data persistence layer together. The example `FooDetector` writes h5 files using the standard NDPlugin. It additionally supports the `HasHints` protocol which is optional but recommended. +Its initialiser assumes the NSLS-II AreaDetector plugin EPICS address suffixes as defaults but allows overriding: **this pattern is recommended for consistency**. +If the `FooDriver` exposed any `Signal`s that should be read as configuration, they should be added to the `config_sigs`. + +.. literalinclude:: ../examples/foo_detector.py + :pyobject: FooDetector From 2c2bf1b54d4308de7da61cf074fc6b93ac61a49e Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 2 May 2024 13:31:27 +0100 Subject: [PATCH 03/11] Linting, adjust test fixtures --- docs/examples/foo_detector.py | 3 +- tests/epics/areadetector/test_aravis.py | 25 +------------ tests/epics/areadetector/test_kinetix.py | 25 +------------ tests/epics/areadetector/test_pilatus.py | 47 ++---------------------- tests/epics/areadetector/test_vimba.py | 27 +------------- 5 files changed, 8 insertions(+), 119 deletions(-) diff --git a/docs/examples/foo_detector.py b/docs/examples/foo_detector.py index 15b86bb385..416433bdbd 100644 --- a/docs/examples/foo_detector.py +++ b/docs/examples/foo_detector.py @@ -39,7 +39,7 @@ async def arm( await asyncio.gather( self._drv.num_images.set(num), self._drv.image_mode.set(ImageMode.multiple), - self._drv.trigger_mode.set(f"FOO{trigger}") + self._drv.trigger_mode.set(f"FOO{trigger}"), ) if exposure is not None: await self._drv.acquire_time.set(exposure) @@ -50,7 +50,6 @@ async def disarm(self): class FooDetector(StandardDetector, HasHints): - _controller: FooController _writer: HDFWriter diff --git a/tests/epics/areadetector/test_aravis.py b/tests/epics/areadetector/test_aravis.py index 71cf62bb7f..2c0a71bb92 100644 --- a/tests/epics/areadetector/test_aravis.py +++ b/tests/epics/areadetector/test_aravis.py @@ -11,39 +11,16 @@ set_sim_value, ) from ophyd_async.epics.areadetector.aravis import AravisDetector -from ophyd_async.epics.areadetector.drivers.aravis_driver import AravisDriver -from ophyd_async.epics.areadetector.writers.nd_file_hdf import NDFileHDF - - -@pytest.fixture -async def adaravis_driver(RE: RunEngine) -> AravisDriver: - async with DeviceCollector(sim=True): - driver = AravisDriver("DRV:") - - return driver - - -@pytest.fixture -async def hdf(RE: RunEngine) -> NDFileHDF: - async with DeviceCollector(sim=True): - hdf = NDFileHDF("HDF:") - - return hdf @pytest.fixture async def adaravis( RE: RunEngine, static_directory_provider: DirectoryProvider, - adaravis_driver: AravisDriver, - hdf: NDFileHDF, ) -> AravisDetector: async with DeviceCollector(sim=True): adaravis = AravisDetector( - "adaravis", - static_directory_provider, - driver=adaravis_driver, - hdf=hdf, + "ADARAVIS:", static_directory_provider, name="adaravis" ) return adaravis diff --git a/tests/epics/areadetector/test_kinetix.py b/tests/epics/areadetector/test_kinetix.py index bb79a0b5f8..0ebd97fee2 100644 --- a/tests/epics/areadetector/test_kinetix.py +++ b/tests/epics/areadetector/test_kinetix.py @@ -7,40 +7,17 @@ DirectoryProvider, set_sim_value, ) -from ophyd_async.epics.areadetector.drivers.kinetix_driver import KinetixDriver from ophyd_async.epics.areadetector.kinetix import KinetixDetector -from ophyd_async.epics.areadetector.writers.nd_file_hdf import NDFileHDF - - -@pytest.fixture -async def adkinetix_driver(RE: RunEngine) -> KinetixDriver: - async with DeviceCollector(sim=True): - driver = KinetixDriver("DRV:") - - return driver - - -@pytest.fixture -async def hdf(RE: RunEngine) -> NDFileHDF: - async with DeviceCollector(sim=True): - hdf = NDFileHDF("HDF:") - - return hdf @pytest.fixture async def adkinetix( RE: RunEngine, static_directory_provider: DirectoryProvider, - adkinetix_driver: KinetixDriver, - hdf: NDFileHDF, ) -> KinetixDetector: async with DeviceCollector(sim=True): adkinetix = KinetixDetector( - "adkinetix", - static_directory_provider, - driver=adkinetix_driver, - hdf=hdf, + "KINETIX:", static_directory_provider, name="adkinetix" ) return adkinetix diff --git a/tests/epics/areadetector/test_pilatus.py b/tests/epics/areadetector/test_pilatus.py index e673a3e189..8ff47b1763 100644 --- a/tests/epics/areadetector/test_pilatus.py +++ b/tests/epics/areadetector/test_pilatus.py @@ -8,64 +8,25 @@ TriggerInfo, set_sim_value, ) -from ophyd_async.epics.areadetector.controllers.pilatus_controller import ( - PilatusController, -) -from ophyd_async.epics.areadetector.drivers.pilatus_driver import ( - PilatusDriver, - PilatusTriggerMode, -) +from ophyd_async.epics.areadetector.drivers.pilatus_driver import PilatusTriggerMode from ophyd_async.epics.areadetector.pilatus import PilatusDetector -from ophyd_async.epics.areadetector.writers.nd_file_hdf import NDFileHDF - - -@pytest.fixture -async def pilatus_driver(RE: RunEngine) -> PilatusDriver: - async with DeviceCollector(sim=True): - driver = PilatusDriver("DRV:") - - return driver - - -@pytest.fixture -async def pilatus_controller( - RE: RunEngine, pilatus_driver: PilatusDriver -) -> PilatusController: - async with DeviceCollector(sim=True): - controller = PilatusController(pilatus_driver) - - return controller - - -@pytest.fixture -async def hdf(RE: RunEngine) -> NDFileHDF: - async with DeviceCollector(sim=True): - hdf = NDFileHDF("HDF:") - - return hdf @pytest.fixture async def pilatus( RE: RunEngine, static_directory_provider: DirectoryProvider, - pilatus_driver: PilatusDriver, - hdf: NDFileHDF, ) -> PilatusDetector: async with DeviceCollector(sim=True): - pilatus = PilatusDetector( - "pilatus", - static_directory_provider, - driver=pilatus_driver, - hdf=hdf, - ) + pilatus = PilatusDetector("PILATUS:", static_directory_provider, name="pilatus") return pilatus async def test_deadtime_invariant( - pilatus_controller: PilatusController, + pilatus: PilatusDetector, ): + pilatus_controller = pilatus.controller # deadtime invariant with exposure time assert pilatus_controller.get_deadtime(0) == 2.28e-3 assert pilatus_controller.get_deadtime(500) == 2.28e-3 diff --git a/tests/epics/areadetector/test_vimba.py b/tests/epics/areadetector/test_vimba.py index 002ddb1b7e..6805696f9d 100644 --- a/tests/epics/areadetector/test_vimba.py +++ b/tests/epics/areadetector/test_vimba.py @@ -7,41 +7,16 @@ DirectoryProvider, set_sim_value, ) -from ophyd_async.epics.areadetector.drivers.vimba_driver import VimbaDriver from ophyd_async.epics.areadetector.vimba import VimbaDetector -from ophyd_async.epics.areadetector.writers.nd_file_hdf import NDFileHDF - - -@pytest.fixture -async def advimba_driver(RE: RunEngine) -> VimbaDriver: - async with DeviceCollector(sim=True): - driver = VimbaDriver("DRV:") - - return driver - - -@pytest.fixture -async def hdf(RE: RunEngine) -> NDFileHDF: - async with DeviceCollector(sim=True): - hdf = NDFileHDF("HDF:") - - return hdf @pytest.fixture async def advimba( RE: RunEngine, static_directory_provider: DirectoryProvider, - advimba_driver: VimbaDriver, - hdf: NDFileHDF, ) -> VimbaDetector: async with DeviceCollector(sim=True): - advimba = VimbaDetector( - "advimba", - static_directory_provider, - driver=advimba_driver, - hdf=hdf, - ) + advimba = VimbaDetector("VIMBA:", static_directory_provider, name="advimba") return advimba From 8aa41fabd58128ebdca4fd25f5e93670f7523ad3 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 2 May 2024 15:07:52 +0100 Subject: [PATCH 04/11] Fix doc build --- docs/how-to/make-a-standard-detector.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst index 943d281daa..e22e510ed7 100644 --- a/docs/how-to/make-a-standard-detector.rst +++ b/docs/how-to/make-a-standard-detector.rst @@ -6,29 +6,28 @@ Make a StandardDetector ======================= -.. currentmodule:: ophyd_async.core - `StandardDetector` is an abstract class to assist in creating devices to control EPICS AreaDetector implementations. 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 `HDFWriter`) +- `DetectorWriter` to handle data persistence, i/o and pass information about data to the RunEngine (usually an instance of :py:class:`HDFWriter`) - `DetectorControl` with logic for arming and disarming the detector. This will be unique to the StandardDetector implementation. These standard components are not devices, and therefore not subdevices of the `StandardDetector`, typically they are enabled by the use of two other components which are: -- An implementation of `NDPluginBase`, an entity object mapping to an AreaDetector NDPluginFile instance (for `HDFWriter` an instance of `NDFileHDF`) -- `ADBase`, or an class which extends it, an entity object mapping to an AreaDetector "NDArray" for the "driver" of the detector implementation +- An implementation of :py:class:`NDPluginBase`, an entity object mapping to an AreaDetector NDPluginFile instance (for :py:class:`HDFWriter` an instance of :py:class:`NDFileHDF`) +- :py:class:`ADBase`, or an class which extends it, an entity object mapping to an AreaDetector "NDArray" for the "driver" of the detector implementation Writing a StandardDetector implementation ----------------------------------------- -Define a `FooDriver` if the NDArray requires fields in addition to those on `ADBase` to be exposed. It should extend `ADBase`. +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`. 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 + :language: python :pyobject: FooDriver -Define a `FooController` with handling for converting the standard pattern of `arm` and `disarm` to required state of `FooDriver` e.g. setting a compatible `FooTriggerSource` for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. +Define a :py:class:`FooController` with handling for converting the standard pattern of `arm` and `disarm` to required state of :py:class:`FooDriver` e.g. setting a compatible "FooTriggerSource" for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. The `get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **In the case that it requires fetching values from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** .. literalinclude:: ../examples/foo_detector.py @@ -37,9 +36,9 @@ The `get_deadtime` method is used when constructing sequence tables for hardware Assembly -------- -Define a `FooDetector` implementation to tie the Driver, Controller and data persistence layer together. The example `FooDetector` writes h5 files using the standard NDPlugin. It additionally supports the `HasHints` protocol which is optional but recommended. +Define a :py:class:`FooDetector` implementation to tie the Driver, Controller and data persistence layer together. The example :py:class:`FooDetector` writes h5 files using the standard NDPlugin. It additionally supports the :py:class:`HasHints` protocol which is optional but recommended. Its initialiser assumes the NSLS-II AreaDetector plugin EPICS address suffixes as defaults but allows overriding: **this pattern is recommended for consistency**. -If the `FooDriver` exposed any `Signal`s that should be read as configuration, they should be added to the `config_sigs`. +If the :py:class:`FooDriver` exposes :py:class:`Signal` that should be read as configuration, they should be added to the `config_sigs`. .. literalinclude:: ../examples/foo_detector.py :pyobject: FooDetector From b64e3cdf300ccd337d174eb24ea17b2bf329cb33 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 2 May 2024 15:25:12 +0100 Subject: [PATCH 05/11] lint docs --- docs/how-to/make-a-standard-detector.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst index e22e510ed7..5662ceda47 100644 --- a/docs/how-to/make-a-standard-detector.rst +++ b/docs/how-to/make-a-standard-detector.rst @@ -27,8 +27,8 @@ Enumeration fields should be named to prevent namespace collision, i.e. for a Si :language: python :pyobject: FooDriver -Define a :py:class:`FooController` with handling for converting the standard pattern of `arm` and `disarm` to required state of :py:class:`FooDriver` e.g. setting a compatible "FooTriggerSource" for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. -The `get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **In the case that it requires fetching values from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** +Define a :py:class:`FooController` with handling for converting the standard pattern of :py:method:`ophyd_async.core.DetectorControl.arm` and :py:method:`ophyd_async.core.DetectorControl.disarm` to required state of :py:class:`FooDriver` e.g. setting a compatible "FooTriggerSource" for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. +The :py:method:`ophyd_async.core.DetectorControl.get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **In the case that it requires fetching values from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** .. literalinclude:: ../examples/foo_detector.py :pyobject: FooController @@ -38,7 +38,7 @@ Assembly Define a :py:class:`FooDetector` implementation to tie the Driver, Controller and data persistence layer together. The example :py:class:`FooDetector` writes h5 files using the standard NDPlugin. It additionally supports the :py:class:`HasHints` protocol which is optional but recommended. Its initialiser assumes the NSLS-II AreaDetector plugin EPICS address suffixes as defaults but allows overriding: **this pattern is recommended for consistency**. -If the :py:class:`FooDriver` exposes :py:class:`Signal` that should be read as configuration, they should be added to the `config_sigs`. +If the :py:class:`FooDriver` exposes :py:class:`Signal` that should be read as configuration, they should be added to the "config_sigs" passed to the super. .. literalinclude:: ../examples/foo_detector.py :pyobject: FooDetector From 116cce8409b2605f4ac1a7cdbfdacf7b8f89f1ab Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 2 May 2024 15:54:43 +0100 Subject: [PATCH 06/11] method -> meth --- docs/how-to/make-a-standard-detector.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst index 5662ceda47..bd9fd85714 100644 --- a/docs/how-to/make-a-standard-detector.rst +++ b/docs/how-to/make-a-standard-detector.rst @@ -27,8 +27,8 @@ Enumeration fields should be named to prevent namespace collision, i.e. for a Si :language: python :pyobject: FooDriver -Define a :py:class:`FooController` with handling for converting the standard pattern of :py:method:`ophyd_async.core.DetectorControl.arm` and :py:method:`ophyd_async.core.DetectorControl.disarm` to required state of :py:class:`FooDriver` e.g. setting a compatible "FooTriggerSource" for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. -The :py:method:`ophyd_async.core.DetectorControl.get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **In the case that it requires fetching values from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** +Define a :py:class:`FooController` with handling for converting the standard pattern of :py:meth:`ophyd_async.core.DetectorControl.arm` and :py:meth:`ophyd_async.core.DetectorControl.disarm` to required state of :py:class:`FooDriver` e.g. setting a compatible "FooTriggerSource" for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. +The :py:meth:`ophyd_async.core.DetectorControl.get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **In the case that it requires fetching values from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** .. literalinclude:: ../examples/foo_detector.py :pyobject: FooController From f0c96625ca6d9a5347e4d9ce81ecb203ee6d654e Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 3 May 2024 10:52:20 +0100 Subject: [PATCH 07/11] Remove ScalarSigs (see 282) --- src/ophyd_async/epics/areadetector/aravis.py | 2 -- src/ophyd_async/epics/areadetector/kinetix.py | 2 -- src/ophyd_async/epics/areadetector/pilatus.py | 2 -- src/ophyd_async/epics/areadetector/vimba.py | 2 -- 4 files changed, 8 deletions(-) diff --git a/src/ophyd_async/epics/areadetector/aravis.py b/src/ophyd_async/epics/areadetector/aravis.py index ac14decd4b..6e34174dc0 100644 --- a/src/ophyd_async/epics/areadetector/aravis.py +++ b/src/ophyd_async/epics/areadetector/aravis.py @@ -29,7 +29,6 @@ def __init__( hdf_suffix="HDF1:", name="", gpio_number: AravisController.GPIO_NUMBER = 1, - **scalar_sigs: str, ): self.drv = AravisDriver(prefix + drv_suffix) self.hdf = NDFileHDF(prefix + hdf_suffix) @@ -41,7 +40,6 @@ def __init__( directory_provider, lambda: self.name, ADBaseShapeProvider(self.drv), - **scalar_sigs, ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/src/ophyd_async/epics/areadetector/kinetix.py b/src/ophyd_async/epics/areadetector/kinetix.py index 232878cc55..a52914580a 100644 --- a/src/ophyd_async/epics/areadetector/kinetix.py +++ b/src/ophyd_async/epics/areadetector/kinetix.py @@ -25,7 +25,6 @@ def __init__( drv_suffix="cam1:", hdf_suffix="HDF1:", name="", - **scalar_sigs: str, ): self.drv = KinetixDriver(prefix + drv_suffix) self.hdf = NDFileHDF(prefix + hdf_suffix) @@ -37,7 +36,6 @@ def __init__( directory_provider, lambda: self.name, ADBaseShapeProvider(self.drv), - **scalar_sigs, ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/src/ophyd_async/epics/areadetector/pilatus.py b/src/ophyd_async/epics/areadetector/pilatus.py index b6443fa189..1a76a20671 100644 --- a/src/ophyd_async/epics/areadetector/pilatus.py +++ b/src/ophyd_async/epics/areadetector/pilatus.py @@ -24,7 +24,6 @@ def __init__( drv_suffix="cam1:", hdf_suffix="HDF1:", name="", - **scalar_sigs: str, ): self.drv = PilatusDriver(prefix + drv_suffix) self.hdf = NDFileHDF(prefix + hdf_suffix) @@ -36,7 +35,6 @@ def __init__( directory_provider, lambda: self.name, ADBaseShapeProvider(self.drv), - **scalar_sigs, ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/src/ophyd_async/epics/areadetector/vimba.py b/src/ophyd_async/epics/areadetector/vimba.py index 83f3da1974..5a4859c186 100644 --- a/src/ophyd_async/epics/areadetector/vimba.py +++ b/src/ophyd_async/epics/areadetector/vimba.py @@ -22,7 +22,6 @@ def __init__( drv_suffix="cam1:", hdf_suffix="HDF1:", name="", - **scalar_sigs: str, ): self.drv = VimbaDriver(prefix + drv_suffix) self.hdf = NDFileHDF(prefix + hdf_suffix) @@ -34,7 +33,6 @@ def __init__( directory_provider, lambda: self.name, ADBaseShapeProvider(self.drv), - **scalar_sigs, ), config_sigs=(self.drv.acquire_time,), name=name, From 735f4fac301b3d459b49164e07fc2aa32b9a52a0 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 3 May 2024 10:52:33 +0100 Subject: [PATCH 08/11] Do not pass name with DeviceCollector --- tests/epics/areadetector/test_aravis.py | 4 +--- tests/epics/areadetector/test_kinetix.py | 4 +--- tests/epics/areadetector/test_pilatus.py | 6 +++--- tests/epics/areadetector/test_vimba.py | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/epics/areadetector/test_aravis.py b/tests/epics/areadetector/test_aravis.py index 2c0a71bb92..9b61f50ff8 100644 --- a/tests/epics/areadetector/test_aravis.py +++ b/tests/epics/areadetector/test_aravis.py @@ -19,9 +19,7 @@ async def adaravis( static_directory_provider: DirectoryProvider, ) -> AravisDetector: async with DeviceCollector(sim=True): - adaravis = AravisDetector( - "ADARAVIS:", static_directory_provider, name="adaravis" - ) + adaravis = AravisDetector("ADARAVIS:", static_directory_provider) return adaravis diff --git a/tests/epics/areadetector/test_kinetix.py b/tests/epics/areadetector/test_kinetix.py index 0ebd97fee2..a5513a9ab7 100644 --- a/tests/epics/areadetector/test_kinetix.py +++ b/tests/epics/areadetector/test_kinetix.py @@ -16,9 +16,7 @@ async def adkinetix( static_directory_provider: DirectoryProvider, ) -> KinetixDetector: async with DeviceCollector(sim=True): - adkinetix = KinetixDetector( - "KINETIX:", static_directory_provider, name="adkinetix" - ) + adkinetix = KinetixDetector("KINETIX:", static_directory_provider) return adkinetix diff --git a/tests/epics/areadetector/test_pilatus.py b/tests/epics/areadetector/test_pilatus.py index 8ff47b1763..ac34a9e4e8 100644 --- a/tests/epics/areadetector/test_pilatus.py +++ b/tests/epics/areadetector/test_pilatus.py @@ -18,9 +18,9 @@ async def pilatus( static_directory_provider: DirectoryProvider, ) -> PilatusDetector: async with DeviceCollector(sim=True): - pilatus = PilatusDetector("PILATUS:", static_directory_provider, name="pilatus") + adpilatus = PilatusDetector("PILATUS:", static_directory_provider) - return pilatus + return adpilatus async def test_deadtime_invariant( @@ -60,7 +60,7 @@ async def trigger_and_complete(): async def test_hints_from_hdf_writer(pilatus: PilatusDetector): - assert pilatus.hints == {"fields": ["pilatus"]} + assert pilatus.hints == {"fields": ["adpilatus"]} async def test_unsupported_trigger_excepts(pilatus: PilatusDetector): diff --git a/tests/epics/areadetector/test_vimba.py b/tests/epics/areadetector/test_vimba.py index 6805696f9d..fbb1f88bf0 100644 --- a/tests/epics/areadetector/test_vimba.py +++ b/tests/epics/areadetector/test_vimba.py @@ -16,7 +16,7 @@ async def advimba( static_directory_provider: DirectoryProvider, ) -> VimbaDetector: async with DeviceCollector(sim=True): - advimba = VimbaDetector("VIMBA:", static_directory_provider, name="advimba") + advimba = VimbaDetector("VIMBA:", static_directory_provider) return advimba From ddfdcefe730f2253d939f2109b45e2223e056a1b Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 3 May 2024 11:59:12 +0100 Subject: [PATCH 09/11] Add docs on non-AreaDetector StandardDetector --- docs/how-to/make-a-standard-detector.rst | 38 +++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst index bd9fd85714..34947b8a57 100644 --- a/docs/how-to/make-a-standard-detector.rst +++ b/docs/how-to/make-a-standard-detector.rst @@ -6,19 +6,20 @@ Make a StandardDetector ======================= -`StandardDetector` is an abstract class to assist in creating devices to control EPICS AreaDetector implementations. +`StandardDetector` is an abstract class to assist in creating Device classes for hardware that write their 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`) - `DetectorControl` with logic for arming and disarming the detector. This will be unique to the StandardDetector implementation. -These standard components are not devices, and therefore not subdevices of the `StandardDetector`, typically they are enabled by the use of two other components which are: +Writing an AreaDetector StandardDetector +---------------------------------------- -- An implementation of :py:class:`NDPluginBase`, an entity object mapping to an AreaDetector NDPluginFile instance (for :py:class:`HDFWriter` an instance of :py:class:`NDFileHDF`) -- :py:class:`ADBase`, or an class which extends it, an entity object mapping to an AreaDetector "NDArray" for the "driver" of the detector implementation +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 -Writing a StandardDetector 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`. Enumeration fields should be named to prevent namespace collision, i.e. for a Signal named "TriggerSource" use the enum "FooTriggerSource" @@ -33,12 +34,29 @@ The :py:meth:`ophyd_async.core.DetectorControl.get_deadtime` method is used when .. literalinclude:: ../examples/foo_detector.py :pyobject: FooController -Assembly --------- - -Define a :py:class:`FooDetector` implementation to tie the Driver, Controller and data persistence layer together. The example :py:class:`FooDetector` writes h5 files using the standard NDPlugin. It additionally supports the :py:class:`HasHints` protocol which is optional but recommended. +:py:class:`FooDetector` ties the Driver, Controller and data persistence layer together. The example :py:class:`FooDetector` writes h5 files using the standard NDPlugin. It additionally supports the :py:class:`HasHints` protocol which is optional but recommended. Its initialiser assumes the NSLS-II AreaDetector plugin EPICS address suffixes as defaults but allows overriding: **this pattern is recommended for consistency**. If the :py:class:`FooDriver` exposes :py:class:`Signal` that should be read as configuration, they should be added to the "config_sigs" passed to the super. .. literalinclude:: ../examples/foo_detector.py :pyobject: FooDetector + + +Writing a non-AreaDetector StandardDetector +------------------------------------------- + +A non-AreaDetector `StandardDetector` should implement the `DetectorControl` and `DetectorWriter` protocol directly. +Here we construct a `DetectorControl` that co-ordinates signals on a PandA PositionCapture block which (analogously to the AreaDetector "Driver") is a child device of the `StandardDetector` implementation, while the `DetectorControl` is not. + +.. literalinclude:: ../../src/ophyd_async/panda/_panda_controller.py + :pyobject: PandaPcapController + +The PandA may capture a number of signals to be written into its persisted data, 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 + :pyobject: PandaHDFWriter + +The PandA StandardDetector implementation again ties the component parts together. + +.. literalinclude:: ../../src/ophyd_async/panda/_hdf_panda.py + :pyobject: HDFPanda From 966eb494d1fcf88a1fcb3f05df56e6a0a9b54a18 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 3 May 2024 12:13:43 +0100 Subject: [PATCH 10/11] Remove ScalarSigs from FooDetector --- docs/examples/foo_detector.py | 3 +-- docs/how-to/make-a-standard-detector.rst | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/examples/foo_detector.py b/docs/examples/foo_detector.py index 416433bdbd..c9cb433d85 100644 --- a/docs/examples/foo_detector.py +++ b/docs/examples/foo_detector.py @@ -60,8 +60,8 @@ def __init__( drv_suffix="cam1:", hdf_suffix="HDF1:", name="", - **scalar_sigs: str, ): + # Must be children to pick up connect self.drv = FooDriver(prefix + drv_suffix) self.hdf = NDFileHDF(prefix + hdf_suffix) @@ -72,7 +72,6 @@ def __init__( directory_provider, lambda: self.name, ADBaseShapeProvider(self.drv), - **scalar_sigs, ), config_sigs=(self.drv.acquire_time,), name=name, diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst index 34947b8a57..df0051b334 100644 --- a/docs/how-to/make-a-standard-detector.rst +++ b/docs/how-to/make-a-standard-detector.rst @@ -45,8 +45,8 @@ If the :py:class:`FooDriver` exposes :py:class:`Signal` that should be read as c Writing a non-AreaDetector StandardDetector ------------------------------------------- -A non-AreaDetector `StandardDetector` should implement the `DetectorControl` and `DetectorWriter` protocol directly. -Here we construct a `DetectorControl` that co-ordinates signals on a PandA PositionCapture block which (analogously to the AreaDetector "Driver") is a child device of the `StandardDetector` implementation, while the `DetectorControl` is not. +A non-AreaDetector `StandardDetector` should implement `DetectorControl` and `DetectorWriter` directly. +Here we construct a `DetectorControl` that co-ordinates signals on a PandA PositionCapture block which is a child device of the `StandardDetector` implementation, (the `DetectorControl` is not). .. literalinclude:: ../../src/ophyd_async/panda/_panda_controller.py :pyobject: PandaPcapController From d4e6f5cc0f6c78174ea570d85a9ad77ee907321c Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 3 May 2024 13:24:37 +0100 Subject: [PATCH 11/11] Rewording --- docs/how-to/make-a-standard-detector.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/how-to/make-a-standard-detector.rst b/docs/how-to/make-a-standard-detector.rst index df0051b334..bcb68a6b71 100644 --- a/docs/how-to/make-a-standard-detector.rst +++ b/docs/how-to/make-a-standard-detector.rst @@ -6,7 +6,7 @@ Make a StandardDetector ======================= -`StandardDetector` is an abstract class to assist in creating Device classes for hardware that write their own data e.g. an AreaDetector implementation, or a PandA writing motor encoder positions to file. +`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`) @@ -29,14 +29,16 @@ Enumeration fields should be named to prevent namespace collision, i.e. for a Si :pyobject: FooDriver Define a :py:class:`FooController` with handling for converting the standard pattern of :py:meth:`ophyd_async.core.DetectorControl.arm` and :py:meth:`ophyd_async.core.DetectorControl.disarm` to required state of :py:class:`FooDriver` e.g. setting a compatible "FooTriggerSource" for a given `DetectorTrigger`, or raising an exception if incompatible with the `DetectorTrigger`. -The :py:meth:`ophyd_async.core.DetectorControl.get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **In the case that it requires fetching values from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** + +The :py:meth:`ophyd_async.core.DetectorControl.get_deadtime` method is used when constructing sequence tables for hardware controlled scanning. Details on how to calculate the deadtime may be only available from technical manuals or otherwise complex. **If it requires fetching from signals, it is recommended to cache the value during the StandardDetector `prepare` method.** .. literalinclude:: ../examples/foo_detector.py :pyobject: FooController :py:class:`FooDetector` ties the Driver, Controller and data persistence layer together. The example :py:class:`FooDetector` writes h5 files using the standard NDPlugin. It additionally supports the :py:class:`HasHints` protocol which is optional but recommended. + Its initialiser assumes the NSLS-II AreaDetector plugin EPICS address suffixes as defaults but allows overriding: **this pattern is recommended for consistency**. -If the :py:class:`FooDriver` exposes :py:class:`Signal` that should be read as configuration, they should be added to the "config_sigs" passed to the super. +If the :py:class:`FooDriver` signals that should be read as configuration, they should be added to the "config_sigs" passed to the super. .. literalinclude:: ../examples/foo_detector.py :pyobject: FooDetector @@ -46,17 +48,17 @@ 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 which is a child device of the `StandardDetector` implementation, (the `DetectorControl` is not). +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 :pyobject: PandaPcapController -The PandA may capture a number of signals to be written into its persisted data, and the :py:class:`PandaHDFWriter` co-ordinates those, configures the filewriter and describes the data for the RunEngine. +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 :pyobject: PandaHDFWriter -The PandA StandardDetector implementation again ties the component parts together. +The PandA StandardDetector implementation simply ties the component parts and its child devices together. .. literalinclude:: ../../src/ophyd_async/panda/_hdf_panda.py :pyobject: HDFPanda