Skip to content

Commit

Permalink
Standardise and add documentation for creating new StandardDetector i…
Browse files Browse the repository at this point in the history
…mplementations (#281)

* Remove ScalarSigs (see issue #282)
  • Loading branch information
DiamondJoseph authored May 3, 2024
1 parent 995c126 commit 4ee3f29
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 162 deletions.
82 changes: 82 additions & 0 deletions docs/examples/foo_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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="",
):
# Must be children to pick up connect
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),
),
config_sigs=(self.drv.acquire_time,),
name=name,
)

@property
def hints(self) -> Hints:
return self._writer.hints
64 changes: 64 additions & 0 deletions docs/how-to/make-a-standard-detector.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.. 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
=======================

`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`)
- `DetectorControl` with logic for arming and disarming the detector. This will be unique to the StandardDetector implementation.

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


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 :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. **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` 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


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
: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
:pyobject: PandaHDFWriter

The PandA StandardDetector implementation simply ties the component parts and its child devices together.

.. literalinclude:: ../../src/ophyd_async/panda/_hdf_panda.py
:pyobject: HDFPanda
16 changes: 7 additions & 9 deletions src/ophyd_async/epics/areadetector/aravis.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ 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),
Expand All @@ -41,9 +40,8 @@ def __init__(
directory_provider,
lambda: self.name,
ADBaseShapeProvider(self.drv),
**scalar_sigs,
),
config_sigs=(self.drv.acquire_time, self.drv.acquire),
config_sigs=(self.drv.acquire_time,),
name=name,
)

Expand Down
16 changes: 7 additions & 9 deletions src/ophyd_async/epics/areadetector/kinetix.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ class KinetixDetector(StandardDetector, HasHints):

def __init__(
self,
name: str,
prefix: str,
directory_provider: DirectoryProvider,
driver: KinetixDriver,
hdf: NDFileHDF,
**scalar_sigs: str,
drv_suffix="cam1:",
hdf_suffix="HDF1:",
name="",
):
# 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),
Expand All @@ -37,9 +36,8 @@ def __init__(
directory_provider,
lambda: self.name,
ADBaseShapeProvider(self.drv),
**scalar_sigs,
),
config_sigs=(self.drv.acquire_time, self.drv.acquire),
config_sigs=(self.drv.acquire_time,),
name=name,
)

Expand Down
19 changes: 7 additions & 12 deletions src/ophyd_async/epics/areadetector/pilatus.py
Original file line number Diff line number Diff line change
@@ -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,
)
Expand All @@ -22,15 +19,14 @@ class PilatusDetector(StandardDetector):

def __init__(
self,
name: str,
prefix: str,
directory_provider: DirectoryProvider,
driver: PilatusDriver,
hdf: NDFileHDF,
config_sigs: Optional[Sequence[SignalR]] = None,
**scalar_sigs: str,
drv_suffix="cam1:",
hdf_suffix="HDF1:",
name="",
):
self.drv = driver
self.hdf = hdf
self.drv = PilatusDriver(prefix + drv_suffix)
self.hdf = NDFileHDF(prefix + hdf_suffix)

super().__init__(
PilatusController(self.drv),
Expand All @@ -39,9 +35,8 @@ def __init__(
directory_provider,
lambda: self.name,
ADBaseShapeProvider(self.drv),
**scalar_sigs,
),
config_sigs=config_sigs or (self.drv.acquire_time,),
config_sigs=(self.drv.acquire_time,),
name=name,
)

Expand Down
16 changes: 7 additions & 9 deletions src/ophyd_async/epics/areadetector/vimba.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ class VimbaDetector(StandardDetector, HasHints):

def __init__(
self,
name: str,
prefix: str,
directory_provider: DirectoryProvider,
driver: VimbaDriver,
hdf: NDFileHDF,
**scalar_sigs: str,
drv_suffix="cam1:",
hdf_suffix="HDF1:",
name="",
):
# 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),
Expand All @@ -34,9 +33,8 @@ def __init__(
directory_provider,
lambda: self.name,
ADBaseShapeProvider(self.drv),
**scalar_sigs,
),
config_sigs=(self.drv.acquire_time, self.drv.acquire),
config_sigs=(self.drv.acquire_time,),
name=name,
)

Expand Down
27 changes: 1 addition & 26 deletions tests/epics/areadetector/test_aravis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,15 @@
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 = AravisDetector("ADARAVIS:", static_directory_provider)

return adaravis

Expand Down
27 changes: 1 addition & 26 deletions tests/epics/areadetector/test_kinetix.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,16 @@
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,
)
adkinetix = KinetixDetector("KINETIX:", static_directory_provider)

return adkinetix

Expand Down
Loading

0 comments on commit 4ee3f29

Please sign in to comment.