Skip to content

Commit

Permalink
Merge pull request #276 from spc-group/async-ad
Browse files Browse the repository at this point in the history
Async ad
  • Loading branch information
canismarko authored Oct 22, 2024
2 parents a2224c1 + e772685 commit 6eedf4b
Show file tree
Hide file tree
Showing 18 changed files with 302 additions and 81 deletions.
86 changes: 86 additions & 0 deletions docs/how_to_guides/adding_devices.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
Adding Devices in Haven
=======================

This guide encompasses two concepts:

- adding a new instance of a device that already has Haven support
- adding a new category of device that does not have Haven support

Existing Haven Support
----------------------

If the device you are using already has a device class created for it,
then using the device only requires a suitable entry in the iconfig
file. By inspecting the environmental variable HAVEN_CONFIG_FILES we
can see that in this case the configuration file is
``~/bluesky/iconfig.toml``, though your beamline may be different.

.. code-block:: bash
$ echo $HAVEN_CONFIG_FILES
/home/beams/S255IDCUSER/bluesky/iconfig.toml
Next, add a new section for this device. The TOML section name should
be the device category, often a lower-case version of the device
class. Most devices also accept at least the parameter *name*, which
will be used to retrieve the device later from the instrument
registry.

In most cases the key "prefix" should list the IOC prefix, including
the trailing ":". For this example, we will add a simple motor.

.. code-block:: toml
[[ motor ]]
prefix = "255idc:m1"
name = "bpm"
Once this section has been added to ``iconfig.toml``, then the device
can be loaded from the instrument registry. No special configuration
is generally necessary.

This device will be loaded once the beamline's load method is called,
though this **usually done automatically** during startup.

.. code-block::
from haven.instrument import beamline
await beamline.load()
bpm_motor = beamline.registry['bpm']
New Haven Support
-----------------

If Haven does not already have support for the device, this will need
to be added. Details on how to create Ophyd and Ophyd-async devices is
beyond the scope of this guide. Assume for this example that there is
a file ``src/haven/devices/toaster.py`` which contains a device class
``Toaster()`` that accepts the parameters *max_power*, *num_slots*,
*prefix*, and *name*.

To let the Haven instrument loader know about this device, edit the
file ``src/haven/instrument.py`` and look for the line like ``beamline
= Instrument({``. Following this line is a mapping of device classes
to their TOML section names. We will now add our new device to this mapping:

.. code-block:: python
beamline = Instrument({
...
"toaster": Toaster,
...
})
The order does not usually matter, though device classes will be
created in the order they are retrieved from this dictionary.

Now the following section can be added to the ``iconfig.toml`` file.

.. code-block:: toml
[[ toaster ]]
name = "sunbeam"
prefix = "255idc:toast:"
num_slots = 2 # 2-slot toaster
max_power = 1200 # Watts
14 changes: 14 additions & 0 deletions docs/how_to_guides/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
##############
How-To Guides
##############

These guides provide a step-by-step guide for performing specific
tasks. In contrast with tutorials, these guides assume a basic level
of understanding of topics like Haven, Ophyd, Ophyd-async, and
Bluesky.

.. toctree::
:maxdepth: 2
:caption: Contents:

adding_devices.rst
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Welcome to haven's documentation!
:caption: Contents:

tutorials/index.rst
how_to_guides/index.rst
topic_guides/index.rst
haven/haven.rst

Expand Down
59 changes: 5 additions & 54 deletions docs/topic_guides/area_detectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ Area Detectors and Cameras
###########################

Area detectors are all largely the same but with small variations from
device-to-device. All the device definitions for area detectors are in
the :py:mod:`haven.devices.area_detector` module.
device-to-device. Old (threaded) device definitions for area detectors
are in the :py:mod:`haven.devices.area_detector` module. Newer
(awaitable) device definitions are in the
:pyd:mod:`haven.devices.detectors` package.

Currently supported detectors:

- Eiger 500K (:py:class:`~haven.devices.area_detector.Eiger500K`)
- Lambda (:py:class:`~haven.devices.area_detector.Lambda250K`)
- Simulated detector (:py:class:`~haven.devices.area_detector.SimDetector`)
- Simulated detector (:py:class:`~haven.devices.detectors.sim_detector.SimDetector`)

EPICS and Ophyd do not make a distinction between area detectors and
cameras. After all, a camera is just an area detector for visible
Expand All @@ -21,57 +23,6 @@ substantive difference is that cameras have the ophyd label "cameras",
whereas non-camera area detectors (e.g. Eiger 500K), have the ophyd
label "area_detectors". They can be used interchangeably in plans.

.. warning::

Currently, cameras are not properly implemented in Haven. This will
be fixed soon.

Using Devices in Haven
======================

If the device you are using already has a device class created for it,
then using the device only requires a suitable entry in the iconfig
file (``~/bluesky/instrument/iconfig.toml``). The iconfig section name
should begin with "area_detector", and end with the device name
(e.g. "area_detector.eiger"). The device name will be used to retrieve
the device later from the instrument registry.

The key "prefix" should list the IOC prefix, minus the trailing
":". The key "device_class" should point to a subclass of ophyd's
:py:class:`~ophyd.areadetector.detectors.DetectorBase` class that is
defined in :py:mod:`haven.devices.area_detector`.


.. code-block:: toml
[area_detector.eiger]
prefix = "dp_eiger_xrd91"
device_class = "Eiger500K"
Once this section has been added to ``iconfig.toml``, then the device
can be loaded from the instrument registry. No special configuration
is generally necessary.

.. code-block:: python
>>> import haven
>>> haven.load_instrument()
>>> det = haven.registry.find("eiger")
>>> plan = haven.xafs_scan(..., detectors=[det])
Usually, no special configuration is needed for area detectors. By
default it will save HDF5 and TIFF files for each frame. The filenames
for these TIFF and HDF5 files will be stored automatically to the
database. The outputs of the stats plugins will also be saved.

.. warning::

It is up you to make sure the file path settings are correct for
the HDF5 and TIFF NDplugins. Also, ensure that the routing is
correct for the ROI and STATS NDplugins.

.. warning::

The first time you stage the device after the IOC has been
Expand Down
2 changes: 2 additions & 0 deletions src/haven/devices/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .detectors.aravis import AravisDetector # noqa: F401
from .detectors.sim_detector import SimDetector # noqa: F401
from .ion_chamber import IonChamber # noqa: F401
from .monochromator import Monochromator # noqa: F401
from .motor import HavenMotor, Motor # noqa: F401
Expand Down
1 change: 1 addition & 0 deletions src/haven/devices/detectors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
...
20 changes: 20 additions & 0 deletions src/haven/devices/detectors/aravis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from ophyd_async.core import SubsetEnum
from ophyd_async.epics.adaravis import AravisDetector as DetectorBase
from ophyd_async.epics.signal import epics_signal_rw_rbv

from .area_detectors import HavenDetector

AravisTriggerSource = SubsetEnum["Software", "Line1"]


class AravisDetector(HavenDetector, DetectorBase):
_ophyd_labels_ = {"cameras", "detectors"}

def __init__(self, prefix, *args, **kwargs):
super().__init__(*args, prefix=prefix, **kwargs)
# Replace a signal that has different enum options
self.drv.trigger_source = epics_signal_rw_rbv(
AravisTriggerSource, # type: ignore
f"{prefix}cam1:TriggerSource",
)
self.set_name(self.name)
26 changes: 26 additions & 0 deletions src/haven/devices/detectors/area_detectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pathlib import Path

from ophyd_async.core import UUIDFilenameProvider, YMDPathProvider

from ..._iconfig import load_config


class HavenDetector:
def __init__(self, *args, writer_path=None, **kwargs):
# Create a path provider based on the path given
if writer_path is None:
writer_path = default_path()
path_provider = YMDPathProvider(
filename_provider=UUIDFilenameProvider(),
base_directory_path=writer_path,
create_dir_depth=-4,
)
super().__init__(*args, path_provider=path_provider, **kwargs)


def default_path(config=None):
if config is None:
config = load_config()
# Generate a default path provider
root_dir = Path(config.get("area_detector_root_path", "/tmp"))
return root_dir
7 changes: 7 additions & 0 deletions src/haven/devices/detectors/sim_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from ophyd_async.epics.adsimdetector import SimDetector as SimDetectorBase

from .area_detectors import HavenDetector


class SimDetector(HavenDetector, SimDetectorBase):
_ophyd_labels_ = {"area_detectors", "detectors"}
5 changes: 3 additions & 2 deletions src/haven/devices/fluorescence_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from typing import Optional, Sequence

import numpy as np
from apstools.utils import cleanupText

# from apstools.utils import cleanupText
from ophyd import Component as Cpt
from ophyd import Device, Kind
from ophyd.signal import DerivedSignal, InternalSignal
Expand Down Expand Up @@ -125,7 +126,7 @@ def stage(self):
self._original_name = self.name
# Append the ROI label to the signal name
label = str(self.label.get()).strip("~")
label = cleanupText(label)
label = label.encode("ascii", "replace").decode("ascii")
old_name_base = self.name
new_name_base = f"{self.name}_{label}"

Expand Down
3 changes: 2 additions & 1 deletion src/haven/devices/ion_chamber.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import warnings
from typing import Dict

from apstools.utils.misc import safe_ophyd_name
# from apstools.utils.misc import safe_ophyd_name
safe_ophyd_name = lambda n: n
from bluesky.protocols import Triggerable
from ophyd_async.core import (
DEFAULT_TIMEOUT,
Expand Down
3 changes: 2 additions & 1 deletion src/haven/devices/motor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
import warnings

from apstools.utils.misc import safe_ophyd_name
# from apstools.utils.misc import safe_ophyd_name
safe_ophyd_name = lambda n: n
from ophyd import Component as Cpt
from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO, Kind
from ophyd_async.core import DEFAULT_TIMEOUT, ConfigSignal, SubsetEnum
Expand Down
2 changes: 2 additions & 0 deletions src/haven/iconfig_testing.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
area_detector_root_path = "/tmp"

[beamline]
# General name for the beamline, used for metadata.
name = "SPC Beamline (sector unknown)"
Expand Down
38 changes: 34 additions & 4 deletions src/haven/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from .devices.aps import ApsMachine
from .devices.area_detector import make_area_detector
from .devices.beamline_manager import BeamlineManager
from .devices.detectors.aravis import AravisDetector
from .devices.detectors.sim_detector import SimDetector
from .devices.dxp import make_dxp_device
from .devices.energy_positioner import EnergyPositioner
from .devices.heater import CapillaryHeater
Expand Down Expand Up @@ -113,8 +115,33 @@ def parse_config(self, cfg):
self.devices.extend(devices)
return devices

def validate_params(self, params, Klass):
"""Check that parameters match a Device class's initializer."""
def validate_params(self, params: Mapping, Klass) -> bool:
"""Check that parameters match a Device class's initializer.
If the function returns successfully, it returns True. Invalid
parameters will cause and exception.
*args and **kwargs are ignored.
Parameters
==========
params
The loaded keys and value to be validated.
Klass
The class (or loading function) to check arguments for.
Returns
=======
True
Raises
======
InvalidConfiguration
A key for a required argument was not present in *params*,
or the value for a type-annotated arguments was of a
different type.
"""
sig = inspect.signature(Klass)
has_kwargs = any(
[param.kind == param.VAR_KEYWORD for param in sig.parameters.values()]
Expand All @@ -123,9 +150,9 @@ def validate_params(self, params, Klass):
for key, sig_param in sig.parameters.items():
# Check for missing parameters
param_missing = key not in params
var_kinds = [sig_param.VAR_KEYWORD, sig_param.VAR_POSITIONAL]
param_required = (
sig_param.default is sig_param.empty
and sig_param.kind != sig_param.VAR_KEYWORD
sig_param.default is sig_param.empty and sig_param.kind not in var_kinds
)
if param_missing and param_required:
raise InvalidConfiguration(
Expand All @@ -145,6 +172,7 @@ def validate_params(self, params, Klass):
f"expected `{sig_param.annotation}` but got "
f"`{type(params[key])}`."
)
return True

def make_device(self, params, Klass):
"""Create the devices from their parameters."""
Expand Down Expand Up @@ -379,6 +407,8 @@ async def load(
"aerotech_stage": AerotechStage,
"motor": Motor,
"energy": EnergyPositioner,
"sim_detector": SimDetector,
"camera": AravisDetector,
"pss_shutter": PssShutter,
# Threaded ophyd devices
"blade_slits": BladeSlits,
Expand Down
Loading

0 comments on commit 6eedf4b

Please sign in to comment.