Area Detectors#
Area Detector devices require some customization to use. Here is the simplest possible configuration.
from ophyd import AreaDetector, SingleTrigger
class MyDetector(SingleTrigger, AreaDetector):
pass
prefix = 'XF:23ID1-ES{Tst-Cam:1}'
det = MyDetector(prefix, name="det")
The above should work correctly with any EPICS Area Detector. We test
on versions 1.9.1 and 2.2. For preliminary support for AD33 see the
nslsii
package.
Callbacks#
Internally, Area Detector provides a flexible array processing
pipeline.
The pipeline is a chain of ‘plugins’ which can be re-configured at
runtime by setting the .nd_array_port
on a downstream plugin to
the .port_name
of the upstream plugin. Internally the plugins
pass data between each other by passing a pointer to an NDArray
C++
object (which is an array plus some meta-data). The arrays are
allocated out of a shared pool when they are created (typically by the
‘cam’ plugin which wraps the detector driver) and freed when the last
plugin is done with them. Each plugin can trigger its children in
two ways:
blocking : The next plugin is called synchronously, blocking the parent plugin until all of the (blocking) children are finished. This is single-threaded.
non-blocking : The pointer is put on a queue that the child consumes from. This allows multi-threaded processing with each plugin running on its own thread.
This behavior is controlled by the .blocking_callbacks
signal on
the plugin.
The SingleTrigger
sets the
acquire bit ‘high’ and then watches for it to go low (indicating that
acquisition is complete). If any of the down-stream plugins are in
non-blocking mode are likely to have the following sequence of events
when using, for example, the Stats
plugin and taking one frame
detector produces the frame
puts the frame on the queue for the stats plugin to consume
flips the acquire bit to ‘low’
ophyd sees the acquire bit go low and marks the status object as done
bluesky continues with the plan and reads the Stats plugin (which still contains old data)
the Stats plugin processes the frame (updating the values for the just-collected frame)
Because (6) happens after (5) bluesky reads ‘stale’ data from the
stats plugin and produces an event which associates other measurements
with the incorrect reading from the camera. This issue has resulted
in alignment scans systematically returning the values from the
previous point. To avoid this, we ensure in stage()
that all
plugins are in ‘blocking’ mode. This has the downside of slowing the
detector down as we are only using a single thread but has the
advantage of giving correct measurements.
Prior to AD3-3, AD did not track if a given frame had fully propagated
through the pipeline. We looked into tracking this from the outside
and using this to determine when the data acquisition was done. In
principle this could be done by watching a combination of queue size
and the .uniqueID
signal, however this work was abandoned due to
the complexity of supporting this for all of the version of AD on the
floor.
In AD3-3,
the camera now tracks if all of the frames it produces have been
processed (added to support ophyd [1] ). There is now a
.wait_for_plugins
signal that controls the behavior of
put-complete on the .acquire
signal. If .wait_for_plugins
is
True
, then the put-complete callback on the .acquire
signal
will not process until all of the frames have been processed by all of
the plugins.
This allows us to run with all of the plugins in non-blocking mode and to simplify the trigger logic. Instead of waiting for the acquire bit to change value, we use the a put-completion callback.
To convert an existing area detector sub-class to support the new scheme you must:
Change the type of the came to sub-class
nslsii.ad33.CamV33Mixin
Change the trigger mixin to be
nslsii.ad33.SingleTriggerV33
Arrange for
det.cam.ensure_nonblocking
to be called after initializing the ophyd object.
Ports#
Each plugin has a read-only out-put port name (.port_name
) and a
settable in-put port name (.nd_array_port
). To connect plugin
downstream
to plugin upstream
set downstream.nd_array_port
to upstream.port_name
.
The top-level ADBase
class has several helper methods for
walking and validating the plugin network.
|
The AreaDetector base class |
|
This generates a figure showing the current asyn port layout. |
|
Get the plugin which has the given asyn port name |
Return port name : component map |
|
Get the directed graph of the ASYN ports |
|
Validate that all components of pipeline are known |
|
Find missing ports |
Filestore Plugins#
Note
The mixins in this section are to be mixed with the file plugin classes and used as components on a larger device. The siblings of the resulting classes are components representing the various plugins that make up an Area Detector.
Integration of the file writing with filestore is done by mixing
sub-classes of FileStorePluginBase
into one of the file
plugin classes and using the resulting class as a component in your
detector.
The base classes (which may be merged in the future)
|
Base class for FileStore mixin classes |
|
provide the basic methods required for integrating AreaDetector file plugins with
filestore
|
Generate a uid and cache it with its key for later insertion. |
The 'root' put into the Asset Registry |
|
DEPRECATED: The 'root' put into the Asset registry, use reg_root |
|
Returns write_path_template if read_path_template is not set |
|
Make a filename. |
FileStorePluginBase
must be sub-classed to match
each file plugin and take care of inserting the correct meta-data into
FileStore and configuring the file plugin.
|
|
|
|
|
Write out 'squashed' tiffs |
The FileStoreTIFFSquashing
also makes use of the
processing plugin to ‘squash’ multiple frames together into a single
saved image.
To create a functioning class you must also mixin
|
This adds 'point_number' to datum_kwargs. |
which extends generate_datum()
to
insert into the FileStore
instance as data is taken.
For convenience we provide
|
|
|
Area Detector Trigger dispatching#
Note
The mixins in this section are to be mixed with Device
to
represent the ‘top level’ area detector. The components of the resulting class are
the various plugins that make up a full Area Detector.
|
The base class for the hardware-specific classes that follow. |
|
Notify plugins of acquisition being complete. |
|
Notify plugins of acquisition being complete. |
The translation between the trigger()
and triggering
the underlying camera is mediated by the trigger mix-ins.
|
Base class for trigger mixin classes |
|
This trigger mixin class takes one acquisition per trigger. |
|
This trigger mixin class can take multiple acquisitions per trigger. |
Plugins#
|
AreaDetector plugin base class |
|
|
|
|
|
Plugin which adds graphics overlays to an NDArray image |
|
|
|
|
|
|
|
File Plugins#
|
|
|
|
|
|
|
|
|
|
|
|
|
Specific Hardware#
While the above example should work with any Area Detector, ophyd provides specialized classes for specific detectors supported by EPICS Area Detector. These specialized classes generally add components representing fields particular to a given detector, along with device-specific documentation for components.
To use these model-specific classes, swap out AreaDetector
like so:
# before
class MyDetector(SingleTrigger, AreaDetector):
pass
# after
class MyDetector(SingleTrigger, AndorDetector):
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Cams#
The vendor specific details are embedded in the cams
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Custom Devices#
For custom hardware based on area-detector it may be necesary to add a custom
device class (for custom plugins see section below). The new class should
inherit from ophyd.areadetector.base.ADbase
and should have the following
PV structure:
PV = 'Areadetector_device_PV_prefix:(Plugin_suffix or attribute_suffix)'
As an example, for the builtin areadetector ‘stats’ class this looks like:
PV = 'Areadetector_device_PV_prefix:Stats'
And for the builtin areadetector ‘color mode’ attribute it looks like:
PV = 'Areadetector_device_PV_prefix:cam1:ColorMode_RBV'
where 'Areadetector_device_PV_prefix'
is the base PV name for the
Area detector device, plugin_suffix = 'Stats'
is the ‘stats’ Plugin
suffix and attribute_suffix = 'ColorMode_RBV'
is the ‘color mode’
attribute suffix of the 'cam1'
plugin.
In order to create the class then the following code is required
(where XXX
is the name of the device):
from ophyd.areadetector.base import ad_group, EpicsSignalWithRBV
from ophyd.signal import EpicsSignal, EpicsSignalRO
from ophyd.device import DynamicDeviceComponent as DDCpt, Component as Cpt
from ophyd.detectors import DetectorBase
from ophyd.areadetector.trigger_mixins import SingleTrigger
class XXX(SingleTrigger, DetectorBase):
'''An areadetector device class for ...'''
# ADD ATTRIBUTES AS COMPONENTS HERE USING THE SYNTAX
# where 'Type' is EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV,..
attribute_name = Cpt(Type, attribute_suffix)
# ADD ATTRIBUTE GROUPS AS COMPONENTS USING THE SYNTAX
group_name = DDCpt(ad_group(Type,
(attribute_1_name, attribute_1_suffix),
(attribute_2_name, attribute_2_suffix),
...,
(attribute_n_name, attribute_n_suffix))
# ADD ATTRIBUTE PLUGINS AS COMPONENTS USING THE SYNTAX
plugin_name = Cpt(PluginClass, suffix=Plugin_suffix+':')
Note
ophyd.areadetector.detectors.DetectorBase
can be swapped out for any other Areadetector Device class that inherits fromophyd.areadetector.detectors.DetectorBase
.ophyd.areadetector.triggermixins.SingleTrigger
is an optional trigger_mixin class and can be swapped out for any other class that inherits fromophyd.areadetector.trigger_mixins.TriggerBase
. These classes provide the logic to ‘trigger’ the detector and actually acquire the images.PluginClass can be
ophyd.areadetector.plugins.PluginBase
,ophyd.areadetector.cam.CamBase
or any plugin/cam class that inherits from either of these.In the ophyd source code, you may see
ophyd.areadetector.base.ADComponent
used. Functionally, this is interchangeable with an ordinaryophyd.device.Component
(imported asCpt
above); it just adds extra machinery for generating a docstring based on a scrape of the HTML of the official AreaDetector documentation. For custom extensions such as we are addressing here, it is not generally applicable.
The Areadetector device should then be instantiated using:
ADdevice_name = Some_Areadetector_Device_Class(Areadetector_device_PV_suffix,
name = 'ADdevice_name')
Custom Plugins or Cameras#
For custom hardware based on area-detector it may be necessary to add a
custom plugin or camera class, this section will cover what is
required. Both ‘plugins’ and ‘cameras’ act in the same way, but have
slightly different ‘base’ attributes, hence they have different ‘base
classes’. New Plugin classes should inherit from
ophyd.areadetector.base.PluginBase
while new Camera classes
should inherit from ophyd.areadetector.cam.CamBase
. Both
should have the following PV structure (replace ‘plugin’ with ‘cam’
for cameras):
PV = 'Areadetector_device_PV_prefix:Plugin_suffix:attribute_suffix'
As an example, for the ‘max value’ component of the built-in areadetector ‘stats’ class this looks like:
PV = 'Areadetector_device_PV_prefix:Stats:max_value'
where Areadetector_device_PV_prefix
is the PV name for the Area
detector device, plugin_suffix = Stats
is the ‘stats’ Plugin
suffix and attribute_suffix = max_value
is the ‘max value’
attribute suffix.
In order to create the class then the following code is required
(where XXX
is the name of the plugin):
from ophyd.areadetector.base import ad_group, EpicsSignalWithRBV
from ophyd.signal import EpicsSignal, EpicsSignalRO
from ophyd.device import DynamicDeviceComponent as DDCpt, Component as Cpt
from ophyd.areadetector.plugins import PluginBase, register_plugin
from ophyd.areadetector.filestore_mixins import FileStoreHDF5
class XXXplugin(PluginBase, FileStoreHDF5):
'''An areadetector plugin class that does ......'''
_suffix_re = 'Plugin_suffix\d:'
# ADD ATTRIBUTES AS COMPONENTS HERE USING THE SYNTAX
attribute_name = Cpt(Type, attribute_suffix)
# where 'Type' is EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV,..
# ADD ATTRIBUTE GROUPS AS COMPONENTS USING THE SYNTAX
group_name = DDCpt(ad_group(Type,
(attribute_1_name, attribute_1_suffix),
(attribute_2_name, attribute_2_suffix),
...,
(attribute_n_name, attribute_n_suffix))
# this allows searching for a plugin class via matching _suffix_re for
# classes in the registry against the a PV name and is optional.
register_plugin(XXXplugin)
Note
ophyd.areadetector.plugins.PluginBase
can be swapped out forophyd.areadetector.cam.CamBase
,ophyd.areadetector.plugins.FilePlugin
or any other Areadetector Plugin, cam or FilePlugin class that inherits from these.For FilePlugin plugins the optional filestore_mixin
ophyd.areadetector.filestore_mixins.FileStoreHDF5
should also be defined. This can be replaced with any class that inherits fromophyd.areadetector.filestore_mixins.FileStorePluginBase
. These mix-in classes provide the logic for generating Asset Registry documents.
Once the class is defined above then it should be added to the Area detector device class as a component using the code:
class Some_Areadetector_Device_Class(Some_Area_Detector_Base_Class):
'The ophyd class for the device that has the custom plugin'
...
xxx = Cpt(XXXplugin, suffix=Plugin_suffix+':')
...
The Areadetector device should then be instantiated using:
ADdevice_name = Some_Areadetector_Device_Class(Areadetector_device_PV_suffix,
name = 'ADdevice_name')
Helpers#
|
|
|
|
|
Definition creation for groups of signals in areadetectors |