BEC Signals for Custom Devices¶
Ophyd devices typically use Signal and Component objects to communicate with hardware. BECSignals are building their counterparts designed to communicate with BEC instead of hardware. This allows devices to send structured messages and inform BEC about e.g. progress updates, file events, and asynchronous data streams.
BEC Signals are internal components
BEC signals are not meant to be read out directly through the command-line interface. They are internal components that allow devices to send structured messages to BEC.
Signal classes¶
BEC provides several signal classes for different use cases. Some of them are designed for broadcasting runtime information such as progress updates and file events, while others are designed for streaming asynchronous data into the scan dataset and are therefore saved to HDF5.
| Signal class | Description | Saved to HDF5? |
|---|---|---|
ProgressSignal |
Used to emit a device-related progress using a ProgressMessage. |
No |
FileEventSignal |
Used to report external file-writing events using a FileMessage. |
Yes, as file references |
PreviewSignal |
Used to stream 1D or 2D preview data using a DevicePreviewMessage. |
No |
AsyncSignal |
Used to represent a single asynchronous data channel with async-update metadata. Every update must provide the specified signal. | Yes |
AsyncMultiSignal |
Used to represent multiple asynchronous sub-signals with strict signal-name validation. Every update must provide all specified signals. | Yes |
DynamicSignal |
Similar to AsyncMultiSignal but an update does not require all specified signals. |
Yes |
FileEventSignal does not store the detector payload itself in the scan dataset. Instead, BEC records file references in the master HDF5 file so externally written data can be linked there.
Usage and metadata¶
BEC signals are defined as normal ophyd components:
class MyDevice(Device):
progress = Cpt(ProgressSignal, name="progress")
file_event = Cpt(FileEventSignal, name="file_event")
preview = Cpt(PreviewSignal, name="preview", ndim=2, max_shape=[1024, 1024])
async_signal = Cpt(
AsyncSignal,
name="async_signal",
ndim=1,
max_size=1000,
async_update={"type": "add", "max_shape": [None, 1000]},
)
async_multi_signal = Cpt(
AsyncMultiSignal,
name="async_multi_signal",
signals=["temperature", "pressure"],
async_update={"type": "add", "max_shape": [None, None]},
)
and then used from within the device methods:
self.progress.put(value=50, max_value=100, done=False)
The exact signature depends on the signal class but in general, the put(...) can be used to pass in either the data values directly or a structured message object.
For more details on how to use each signal class, see the following how-to guides:
- Add a Progress Signal
- Add a File Event Signal
- Add a Preview Signal
- Add an Async Signal
- Add an Async Multi Signal
Async update metadata¶
AsyncSignal, AsyncMultiSignal, and DynamicSignal need async_update metadata so BEC knows how each async update should be aggregated into the scan dataset.
The async update metadata supports three modes:
| Mode | Description | Required metadata |
|---|---|---|
add |
Append each new update along the first axis of the dataset. | type, max_shape |
add_slice |
Write each new update into a specific slice of a larger dataset. | type, index, max_shape |
replace |
Replace the current dataset with each new async update instead of extending it. | type |
In normal usage, define async_update once on the signal declaration and then send only data values at runtime.
waveform = Cpt(
AsyncSignal,
name="waveform",
ndim=1,
max_size=1000,
async_update={"type": "add", "max_shape": [None, 1024]},
)
self.waveform.put(values)
add¶
Use add when every new update should be appended along the first axis.
Typical uses:
- one growing 1D stream
- a sequence of fixed-length waveforms
- a sequence of variable-length 1D datasets
- a sequence of images
Example definitions:
# growing 1D stream
async_update={"type": "add", "max_shape": [None]}
# stream of fixed-length waveforms
async_update={"type": "add", "max_shape": [None, 1024]}
# stream of variable-length 1D datasets
async_update={"type": "add", "max_shape": [None, None]}
# stream of fixed-size images
async_update={"type": "add", "max_shape": [None, 512, 512]}
What is required:
type="add"max_shape
How to read the max_shape argument:
[None]means one unlimited 1D stream[None, 1024]means an unlimited number of rows, each of length1024[None, None]means an unlimited number of 1D datasets with varying length[None, 512, 512]means an unlimited number of images, each512 x 512
When to use it:
- when each update is one new value block, row, waveform, or image
- when you do not need to target a specific slice index
Prefer fixed inner sizes when possible
[None, None] is supported for a dataset that is an array of 1D datasets with varying length. However, whenever possible, prefer a fixed inner size such as [None, 1024]. Variable-length inner data is the most inefficient way of writing the dataset.
add_slice¶
Use add_slice when async updates should be written into a specific slice of a larger dataset: Rather than appending an entire new row, each update fills a slice of the specified index along the first axis.
Example definition:
waveform = Cpt(
AsyncSignal,
name="waveform",
ndim=1,
max_size=1000,
async_update={"type": "add_slice", "index": 0, "max_shape": [None, 20]},
)
Example runtime updates for building up a 2D dataset, one slice at a time:
# fill the first slice (index 0)
self.waveform.put(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
async_update={"type": "add_slice", "index": 0, "max_shape": [None, 20]}
)
# append to the first slice (index 0)
self.waveform.put(
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
async_update={"type": "add_slice", "index": 0, "max_shape": [None, 20]}
)
# fill in the second slice (index 1)
self.waveform.put(
[21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
async_update={"type": "add_slice", "index": 1, "max_shape": [None, 20]}
)
What is required:
type="add_slice"max_shapeindex
When to use it:
- when updates belong to a specific slice index
- when one logical row or slice is filled over multiple updates
Specify the slice index on each update
In contrast to add and replace, add_slice updates require the async_update metadata to be passed on each put(...) call, because the slice index may change between updates.
replace¶
Use replace when each new async update should replace the current dataset instead of extending it.
Example definition:
result = Cpt(
AsyncSignal,
name="result",
ndim=1,
max_size=10,
async_update={"type": "replace"},
)
At runtime:
self.result.put(latest_result)
What is required:
type="replace"
When to use it:
- when the device publishes a refreshed full result
- when old async data should be superseded instead of extended
What to remember
BECSignalclasses are ophyd-style components for sending structured messages from devices into BEC instead of talking directly to hardware.- Choose the signal class based on the kind of information your device publishes: progress updates, file events, preview data, or asynchronous data streams.
- Declare BEC signals on the device class like normal ophyd components, then publish updates with
put(...)from the device code. AsyncSignal,AsyncMultiSignal, andDynamicSignalneedasync_updatemetadata so BEC knows how incoming async data should be written into the scan dataset.- For async streams, use
addby default, useadd_slicefor indexed slice updates, and usereplacewhen each update should overwrite the previous result.