Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardise and add documentation for creating new StandardDetector implementations #281

Merged
merged 13 commits into from
May 3, 2024
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")
DiamondJoseph marked this conversation as resolved.
Show resolved Hide resolved
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
DiamondJoseph marked this conversation as resolved.
Show resolved Hide resolved
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`.
DiamondJoseph marked this conversation as resolved.
Show resolved Hide resolved
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
Loading