Implementing File Writing Detectors#

In Implementing Devices we learned how to create Devices that talk to a control system to implement a particular behavior in bluesky plans. This behavior was based around the verbs from the bluesky.protocols.Movable and bluesky.protocols.Readable protocols, allowing us to use these Devices in a typical scan: moving them to a position, then acquiring data via the control system. We will now explore the bluesky.protocols.WritesExternalAssets protocol, and how it would be implemented for a File Writing Detector.

Run the demo#

We will return to our ophyd_async.sim devices we saw in Using Devices for this tutorial, and dig a little deeper into what Event Model Documents they produce. Let’s run up our ipython shell again:

$ ipython --matplotlib=qt6 -i -m ophyd_async.sim
Python 3.11.11 (main, Dec  4 2024, 20:38:25) [GCC 12.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.30.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: 

Run a grid scan and investigate the documents#

Now let’s run a grid scan on the point detector, and pass a callback to the RunEngine so it prints the documents that are emitted:

In [1]: RE(bp.grid_scan([pdet], stage.x, 1, 2, 2, stage.y, 2, 3, 2), print)


Transient Scan ID: 1     Time: 2026-03-02 10:47:32
Persistent Unique Scan ID: '17d607c6-ab36-430e-94fa-6f50ba56ad73'
start {'uid': '17d607c6-ab36-430e-94fa-6f50ba56ad73', 'time': 1772448452.2422254, 'versions': {'ophyd': '1.11.0', 'ophyd_async': '0.17.dev6+g3095dfc8b', 'bluesky': '1.14.6'}, 'scan_id': 1, 'plan_type': 'generator', 'plan_name': 'grid_scan', 'detectors': ['pdet'], 'motors': ('stage-x', 'stage-y'), 'num_points': 4, 'num_intervals': 3, 'plan_args': {'detectors': ['<ophyd_async.sim._point_detector.SimPointDetector object at 0x7ff14fd5b590>'], 'args': ['<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47a90>', 1, 2, 2, '<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47ad0>', 2, 3, 2, False], 'per_step': 'None'}, 'hints': {'gridding': 'rectilinear', 'dimensions': [(['stage-x'], 'primary'), (['stage-y'], 'primary')]}, 'shape': (2, 2), 'extents': ([1, 2], [2, 3]), 'snaking': (False, False), 'plan_pattern': 'outer_product', 'plan_pattern_args': {'args': ['<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47a90>', 1, 2, 2, '<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47ad0>', 2, 3, 2, False]}, 'plan_pattern_module': 'bluesky.plan_patterns'}
New stream: 'primary'
+-----------+------------+------------+------------+----------------------+----------------------+----------------------+
|   seq_num |       time |    stage-x |    stage-y | pdet-channel-1-value | pdet-channel-2-value | pdet-channel-3-value |
+-----------+------------+------------+------------+----------------------+----------------------+----------------------+
descriptor {'configuration': {'pdet': {'data': {'pdet-channel-1-mode': <EnergyMode.LOW: 'Low Energy'>, 'pdet-channel-2-mode': <EnergyMode.LOW: 'Low Energy'>, 'pdet-channel-3-mode': <EnergyMode.LOW: 'Low Energy'>}, 'timestamps': {'pdet-channel-1-mode': 1772448452.2275295, 'pdet-channel-2-mode': 1772448452.2275922, 'pdet-channel-3-mode': 1772448452.227637}, 'data_keys': {'pdet-channel-1-mode': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://pdet-channel-1-mode', 'choices': ['Low Energy', 'High Energy']}, 'pdet-channel-2-mode': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://pdet-channel-2-mode', 'choices': ['Low Energy', 'High Energy']}, 'pdet-channel-3-mode': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://pdet-channel-3-mode', 'choices': ['Low Energy', 'High Energy']}}}, 'stage-x': {'data': {'stage-x-velocity': 1000.0, 'stage-x-acceleration_time': 0.5, 'stage-x-units': 'mm'}, 'timestamps': {'stage-x-velocity': 1772448452.2331119, 'stage-x-acceleration_time': 1772448452.2267365, 'stage-x-units': 1772448452.2267563}, 'data_keys': {'stage-x-velocity': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x-velocity'}, 'stage-x-acceleration_time': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x-acceleration_time'}, 'stage-x-units': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://stage-x-units'}}}, 'stage-y': {'data': {'stage-y-velocity': 1000.0, 'stage-y-acceleration_time': 0.5, 'stage-y-units': 'mm'}, 'timestamps': {'stage-y-velocity': 1772448452.2332294, 'stage-y-acceleration_time': 1772448452.2271004, 'stage-y-units': 1772448452.227107}, 'data_keys': {'stage-y-velocity': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y-velocity'}, 'stage-y-acceleration_time': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y-acceleration_time'}, 'stage-y-units': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://stage-y-units'}}}}, 'data_keys': {'pdet-channel-1-value': {'dtype': 'integer', 'shape': [], 'dtype_numpy': '<i8', 'source': 'soft://pdet-channel-1-value', 'object_name': 'pdet'}, 'pdet-channel-2-value': {'dtype': 'integer', 'shape': [], 'dtype_numpy': '<i8', 'source': 'soft://pdet-channel-2-value', 'object_name': 'pdet'}, 'pdet-channel-3-value': {'dtype': 'integer', 'shape': [], 'dtype_numpy': '<i8', 'source': 'soft://pdet-channel-3-value', 'object_name': 'pdet'}, 'stage-x': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x', 'object_name': 'stage-x'}, 'stage-y': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y', 'object_name': 'stage-y'}}, 'name': 'primary', 'object_keys': {'pdet': ['pdet-channel-1-value', 'pdet-channel-2-value', 'pdet-channel-3-value'], 'stage-x': ['stage-x'], 'stage-y': ['stage-y']}, 'run_start': '17d607c6-ab36-430e-94fa-6f50ba56ad73', 'time': 1772448452.414651, 'uid': '81504d64-a784-4014-8e26-1917f1fcad6f', 'hints': {'pdet': {'fields': ['pdet-channel-1-value', 'pdet-channel-2-value', 'pdet-channel-3-value']}, 'stage-x': {'fields': ['stage-x']}, 'stage-y': {'fields': ['stage-y']}}}
|         1 | 10:47:32.4 |      1.000 |      2.000 |                  921 |                  887 |                  859 |
event {'uid': 'fe09e6ae-c43f-43d7-9903-423a32d07048', 'time': 1772448452.4869502, 'data': {'pdet-channel-1-value': 921, 'pdet-channel-2-value': 887, 'pdet-channel-3-value': 859, 'stage-x': 1.0, 'stage-y': 2.0}, 'timestamps': {'pdet-channel-1-value': 1772448452.4115682, 'pdet-channel-2-value': 1772448452.41161, 'pdet-channel-3-value': 1772448452.4116244, 'stage-x': 1772448452.2904706, 'stage-y': 1772448452.3098276}, 'seq_num': 1, 'filled': {}, 'descriptor': '81504d64-a784-4014-8e26-1917f1fcad6f'}
|         2 | 10:47:33.4 |      1.000 |      3.000 |                  937 |                  903 |                  875 |
event {'uid': 'be6b155d-27a6-4923-bfe8-23c17c124b5d', 'time': 1772448453.4711554, 'data': {'pdet-channel-1-value': 937, 'pdet-channel-2-value': 903, 'pdet-channel-3-value': 875, 'stage-x': 1.0, 'stage-y': 3.0}, 'timestamps': {'pdet-channel-1-value': 1772448453.4694088, 'pdet-channel-2-value': 1772448453.4694445, 'pdet-channel-3-value': 1772448453.469458, 'stage-x': 1772448452.2904706, 'stage-y': 1772448453.367826}, 'seq_num': 2, 'filled': {}, 'descriptor': '81504d64-a784-4014-8e26-1917f1fcad6f'}
|         3 | 10:47:34.3 |      2.000 |      2.000 |                  761 |                  740 |                  722 |
event {'uid': '492dc1fe-c15c-44be-83a1-58599078051b', 'time': 1772448454.3918223, 'data': {'pdet-channel-1-value': 761, 'pdet-channel-2-value': 740, 'pdet-channel-3-value': 722, 'stage-x': 2.0, 'stage-y': 2.0}, 'timestamps': {'pdet-channel-1-value': 1772448454.3900635, 'pdet-channel-2-value': 1772448454.3901024, 'pdet-channel-3-value': 1772448454.3901157, 'stage-x': 1772448454.2882583, 'stage-y': 1772448454.2883208}, 'seq_num': 3, 'filled': {}, 'descriptor': '81504d64-a784-4014-8e26-1917f1fcad6f'}
|         4 | 10:47:35.2 |      2.000 |      3.000 |                  487 |                  467 |                  448 |
event {'uid': 'df425314-f178-49be-97a4-4fc22eca3328', 'time': 1772448455.2959337, 'data': {'pdet-channel-1-value': 487, 'pdet-channel-2-value': 467, 'pdet-channel-3-value': 448, 'stage-x': 2.0, 'stage-y': 3.0}, 'timestamps': {'pdet-channel-1-value': 1772448455.2940948, 'pdet-channel-2-value': 1772448455.2941284, 'pdet-channel-3-value': 1772448455.2941408, 'stage-x': 1772448454.2882583, 'stage-y': 1772448455.1924918}, 'seq_num': 4, 'filled': {}, 'descriptor': '81504d64-a784-4014-8e26-1917f1fcad6f'}
+-----------+------------+------------+------------+----------------------+----------------------+----------------------+
generator grid_scan ['17d607c6'] (scan num: 1)



stop {'uid': '7b0cb86f-b5b8-4591-acc8-a581aaf9986d', 'time': 1772448456.066601, 'run_start': '17d607c6-ab36-430e-94fa-6f50ba56ad73', 'exit_status': 'success', 'reason': '', 'num_events': {'primary': 4}}
Out[1]: RunEngineResult(run_start_uids=('17d607c6-ab36-430e-94fa-6f50ba56ad73',), plan_result='17d607c6-ab36-430e-94fa-6f50ba56ad73', exit_status='success', interrupted=False, reason='', exception=None)

We see a series of documents being emitted:

  • A event_model.RunStart document that tells us a scan is starting and what sort of scan it is, along with the names of the motors that will be moved.

  • An event_model.EventDescriptor document that tells us that the motor readbacks and detector channels will be all be read together in a single stream. It is used to make the column headings, but it contains more metadata about the Devices too, like their configuration.

  • For each point in the scan:

    • An event_model.Event document, containing the motor readbacks and detector channels with their timestamps. It is used to make each row of the table.

  • A event_model.RunStop document that tells us the scan has stopped, and gives us its status.

Now let’s try the same thing, but this time with the blob detector:

In [2]: RE(bp.grid_scan([bdet], stage.x, 1, 2, 2, stage.y, 2, 3, 2), print)


Transient Scan ID: 2     Time: 2026-03-02 10:47:36
Persistent Unique Scan ID: 'ab76ed97-63c7-46be-9a0b-fc6526be84ed'
start {'uid': 'ab76ed97-63c7-46be-9a0b-fc6526be84ed', 'time': 1772448456.1774876, 'versions': {'ophyd': '1.11.0', 'ophyd_async': '0.17.dev6+g3095dfc8b', 'bluesky': '1.14.6'}, 'scan_id': 2, 'plan_type': 'generator', 'plan_name': 'grid_scan', 'detectors': ['bdet'], 'motors': ('stage-x', 'stage-y'), 'num_points': 4, 'num_intervals': 3, 'plan_args': {'detectors': ['<ophyd_async.sim._blob_detector.SimBlobDetector object at 0x7ff14fd58d50>'], 'args': ['<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47a90>', 1, 2, 2, '<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47ad0>', 2, 3, 2, False], 'per_step': 'None'}, 'hints': {'gridding': 'rectilinear', 'dimensions': [(['stage-x'], 'primary'), (['stage-y'], 'primary')]}, 'shape': (2, 2), 'extents': ([1, 2], [2, 3]), 'snaking': (False, False), 'plan_pattern': 'outer_product', 'plan_pattern_args': {'args': ['<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47a90>', 1, 2, 2, '<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47ad0>', 2, 3, 2, False]}, 'plan_pattern_module': 'bluesky.plan_patterns'}
New stream: 'primary'
+-----------+------------+------------+------------+
|   seq_num |       time |    stage-x |    stage-y |
+-----------+------------+------------+------------+
descriptor {'configuration': {'bdet': {'data': {}, 'timestamps': {}, 'data_keys': {}}, 'stage-x': {'data': {'stage-x-velocity': 1000.0, 'stage-x-acceleration_time': 0.5, 'stage-x-units': 'mm'}, 'timestamps': {'stage-x-velocity': 1772448452.2331119, 'stage-x-acceleration_time': 1772448452.2267365, 'stage-x-units': 1772448452.2267563}, 'data_keys': {'stage-x-velocity': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x-velocity'}, 'stage-x-acceleration_time': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x-acceleration_time'}, 'stage-x-units': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://stage-x-units'}}}, 'stage-y': {'data': {'stage-y-velocity': 1000.0, 'stage-y-acceleration_time': 0.5, 'stage-y-units': 'mm'}, 'timestamps': {'stage-y-velocity': 1772448452.2332294, 'stage-y-acceleration_time': 1772448452.2271004, 'stage-y-units': 1772448452.227107}, 'data_keys': {'stage-y-velocity': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y-velocity'}, 'stage-y-acceleration_time': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y-acceleration_time'}, 'stage-y-units': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://stage-y-units'}}}}, 'data_keys': {'bdet': {'source': 'file://localhost/tmp/tmpwgdysp9j/eed1c84c-4135-4b6d-ba31-ab3aefda20ce.h5', 'shape': [1, 240, 320], 'dtype': 'array', 'dtype_numpy': '|u1', 'external': 'STREAM:', 'object_name': 'bdet'}, 'bdet-sum': {'source': 'file://localhost/tmp/tmpwgdysp9j/eed1c84c-4135-4b6d-ba31-ab3aefda20ce.h5', 'shape': [1], 'dtype': 'number', 'dtype_numpy': '<i8', 'external': 'STREAM:', 'object_name': 'bdet'}, 'stage-x': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x', 'object_name': 'stage-x'}, 'stage-y': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y', 'object_name': 'stage-y'}}, 'name': 'primary', 'object_keys': {'bdet': ['bdet', 'bdet-sum'], 'stage-x': ['stage-x'], 'stage-y': ['stage-y']}, 'run_start': 'ab76ed97-63c7-46be-9a0b-fc6526be84ed', 'time': 1772448456.4337516, 'uid': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5', 'hints': {'bdet': {'fields': ['bdet']}, 'stage-x': {'fields': ['stage-x']}, 'stage-y': {'fields': ['stage-y']}}}
stream_resource {'uid': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804', 'data_key': 'bdet', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/eed1c84c-4135-4b6d-ba31-ab3aefda20ce.h5', 'parameters': {'chunk_shape': (1, 240, 320), 'dataset': '/entry/data/data'}, 'run_start': 'ab76ed97-63c7-46be-9a0b-fc6526be84ed'}
stream_resource {'uid': '2a9abd50-670f-4487-a17f-3040fc58f211', 'data_key': 'bdet-sum', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/eed1c84c-4135-4b6d-ba31-ab3aefda20ce.h5', 'parameters': {'chunk_shape': (1024,), 'dataset': '/entry/sum'}, 'run_start': 'ab76ed97-63c7-46be-9a0b-fc6526be84ed'}
stream_datum {'stream_resource': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804', 'uid': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
stream_datum {'stream_resource': '2a9abd50-670f-4487-a17f-3040fc58f211', 'uid': '2a9abd50-670f-4487-a17f-3040fc58f211/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
|         1 | 10:47:36.4 |      1.000 |      2.000 |
event {'uid': '7ee70b7e-fdd5-4d52-b258-684cc6952568', 'time': 1772448456.4585319, 'data': {'stage-x': 1.0, 'stage-y': 2.0}, 'timestamps': {'stage-x': 1772448456.2248433, 'stage-y': 1772448456.2248979}, 'seq_num': 1, 'filled': {}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
stream_datum {'stream_resource': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804', 'uid': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
stream_datum {'stream_resource': '2a9abd50-670f-4487-a17f-3040fc58f211', 'uid': '2a9abd50-670f-4487-a17f-3040fc58f211/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
|         2 | 10:47:36.7 |      1.000 |      3.000 |
event {'uid': '0f16e929-831d-447b-9464-297bd80b1923', 'time': 1772448456.7098262, 'data': {'stage-x': 1.0, 'stage-y': 3.0}, 'timestamps': {'stage-x': 1772448456.2248433, 'stage-y': 1772448456.5050278}, 'seq_num': 2, 'filled': {}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
stream_datum {'stream_resource': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804', 'uid': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804/2', 'seq_nums': {'start': 3, 'stop': 4}, 'indices': {'start': 2, 'stop': 3}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
stream_datum {'stream_resource': '2a9abd50-670f-4487-a17f-3040fc58f211', 'uid': '2a9abd50-670f-4487-a17f-3040fc58f211/2', 'seq_nums': {'start': 3, 'stop': 4}, 'indices': {'start': 2, 'stop': 3}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
|         3 | 10:47:36.9 |      2.000 |      2.000 |
event {'uid': 'e66f8d62-9737-40f0-ad8a-dd97e98f87bb', 'time': 1772448456.961, 'data': {'stage-x': 2.0, 'stage-y': 2.0}, 'timestamps': {'stage-x': 1772448456.7564647, 'stage-y': 1772448456.75652}, 'seq_num': 3, 'filled': {}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
stream_datum {'stream_resource': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804', 'uid': 'adccfd0b-895b-4cab-a0cf-5cfd50ff3804/3', 'seq_nums': {'start': 4, 'stop': 5}, 'indices': {'start': 3, 'stop': 4}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
stream_datum {'stream_resource': '2a9abd50-670f-4487-a17f-3040fc58f211', 'uid': '2a9abd50-670f-4487-a17f-3040fc58f211/3', 'seq_nums': {'start': 4, 'stop': 5}, 'indices': {'start': 3, 'stop': 4}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
|         4 | 10:47:37.2 |      2.000 |      3.000 |
event {'uid': '0cb2c69f-c4be-4056-aec0-3651784c0644', 'time': 1772448457.2119665, 'data': {'stage-x': 2.0, 'stage-y': 3.0}, 'timestamps': {'stage-x': 1772448456.7564647, 'stage-y': 1772448457.0072923}, 'seq_num': 4, 'filled': {}, 'descriptor': 'dda567fa-a67d-4bcf-8e39-f2db2bd00eb5'}
+-----------+------------+------------+------------+
generator grid_scan ['ab76ed97'] (scan num: 2)



stop {'uid': '37595c94-ed0e-4cad-9577-4b1c32b67972', 'time': 1772448457.2124233, 'run_start': 'ab76ed97-63c7-46be-9a0b-fc6526be84ed', 'exit_status': 'success', 'reason': '', 'num_events': {'primary': 4}}
Out[2]: RunEngineResult(run_start_uids=('ab76ed97-63c7-46be-9a0b-fc6526be84ed',), plan_result='ab76ed97-63c7-46be-9a0b-fc6526be84ed', exit_status='success', interrupted=False, reason='', exception=None)

This time we see some different documents:

And we can run the plan with both detectors to see a document stream that combines both the previous example:

In [3]: RE(bp.grid_scan([bdet, pdet], stage.x, 1, 2, 2, stage.y, 2, 3, 2), print)


Transient Scan ID: 3     Time: 2026-03-02 10:47:37
Persistent Unique Scan ID: 'db67e592-ab16-45e9-bb8c-9327895538d6'
start {'uid': 'db67e592-ab16-45e9-bb8c-9327895538d6', 'time': 1772448457.3236752, 'versions': {'ophyd': '1.11.0', 'ophyd_async': '0.17.dev6+g3095dfc8b', 'bluesky': '1.14.6'}, 'scan_id': 3, 'plan_type': 'generator', 'plan_name': 'grid_scan', 'detectors': ['bdet', 'pdet'], 'motors': ('stage-x', 'stage-y'), 'num_points': 4, 'num_intervals': 3, 'plan_args': {'detectors': ['<ophyd_async.sim._blob_detector.SimBlobDetector object at 0x7ff14fd58d50>', '<ophyd_async.sim._point_detector.SimPointDetector object at 0x7ff14fd5b590>'], 'args': ['<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47a90>', 1, 2, 2, '<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47ad0>', 2, 3, 2, False], 'per_step': 'None'}, 'hints': {'gridding': 'rectilinear', 'dimensions': [(['stage-x'], 'primary'), (['stage-y'], 'primary')]}, 'shape': (2, 2), 'extents': ([1, 2], [2, 3]), 'snaking': (False, False), 'plan_pattern': 'outer_product', 'plan_pattern_args': {'args': ['<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47a90>', 1, 2, 2, '<ophyd_async.sim._motor.SimMotor object at 0x7ff14fd47ad0>', 2, 3, 2, False]}, 'plan_pattern_module': 'bluesky.plan_patterns'}
New stream: 'primary'
+-----------+------------+------------+------------+----------------------+----------------------+----------------------+
|   seq_num |       time |    stage-x |    stage-y | pdet-channel-1-value | pdet-channel-2-value | pdet-channel-3-value |
+-----------+------------+------------+------------+----------------------+----------------------+----------------------+
descriptor {'configuration': {'bdet': {'data': {}, 'timestamps': {}, 'data_keys': {}}, 'pdet': {'data': {'pdet-channel-1-mode': <EnergyMode.LOW: 'Low Energy'>, 'pdet-channel-2-mode': <EnergyMode.LOW: 'Low Energy'>, 'pdet-channel-3-mode': <EnergyMode.LOW: 'Low Energy'>}, 'timestamps': {'pdet-channel-1-mode': 1772448452.2275295, 'pdet-channel-2-mode': 1772448452.2275922, 'pdet-channel-3-mode': 1772448452.227637}, 'data_keys': {'pdet-channel-1-mode': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://pdet-channel-1-mode', 'choices': ['Low Energy', 'High Energy']}, 'pdet-channel-2-mode': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://pdet-channel-2-mode', 'choices': ['Low Energy', 'High Energy']}, 'pdet-channel-3-mode': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://pdet-channel-3-mode', 'choices': ['Low Energy', 'High Energy']}}}, 'stage-x': {'data': {'stage-x-velocity': 1000.0, 'stage-x-acceleration_time': 0.5, 'stage-x-units': 'mm'}, 'timestamps': {'stage-x-velocity': 1772448452.2331119, 'stage-x-acceleration_time': 1772448452.2267365, 'stage-x-units': 1772448452.2267563}, 'data_keys': {'stage-x-velocity': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x-velocity'}, 'stage-x-acceleration_time': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x-acceleration_time'}, 'stage-x-units': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://stage-x-units'}}}, 'stage-y': {'data': {'stage-y-velocity': 1000.0, 'stage-y-acceleration_time': 0.5, 'stage-y-units': 'mm'}, 'timestamps': {'stage-y-velocity': 1772448452.2332294, 'stage-y-acceleration_time': 1772448452.2271004, 'stage-y-units': 1772448452.227107}, 'data_keys': {'stage-y-velocity': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y-velocity'}, 'stage-y-acceleration_time': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y-acceleration_time'}, 'stage-y-units': {'dtype': 'string', 'shape': [], 'dtype_numpy': '|S40', 'source': 'soft://stage-y-units'}}}}, 'data_keys': {'bdet': {'source': 'file://localhost/tmp/tmpwgdysp9j/46e6b19e-328c-4c76-9799-e1a0141973eb.h5', 'shape': [1, 240, 320], 'dtype': 'array', 'dtype_numpy': '|u1', 'external': 'STREAM:', 'object_name': 'bdet'}, 'bdet-sum': {'source': 'file://localhost/tmp/tmpwgdysp9j/46e6b19e-328c-4c76-9799-e1a0141973eb.h5', 'shape': [1], 'dtype': 'number', 'dtype_numpy': '<i8', 'external': 'STREAM:', 'object_name': 'bdet'}, 'pdet-channel-1-value': {'dtype': 'integer', 'shape': [], 'dtype_numpy': '<i8', 'source': 'soft://pdet-channel-1-value', 'object_name': 'pdet'}, 'pdet-channel-2-value': {'dtype': 'integer', 'shape': [], 'dtype_numpy': '<i8', 'source': 'soft://pdet-channel-2-value', 'object_name': 'pdet'}, 'pdet-channel-3-value': {'dtype': 'integer', 'shape': [], 'dtype_numpy': '<i8', 'source': 'soft://pdet-channel-3-value', 'object_name': 'pdet'}, 'stage-x': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-x', 'object_name': 'stage-x'}, 'stage-y': {'dtype': 'number', 'shape': [], 'dtype_numpy': '<f8', 'source': 'soft://stage-y', 'object_name': 'stage-y'}}, 'name': 'primary', 'object_keys': {'bdet': ['bdet', 'bdet-sum'], 'pdet': ['pdet-channel-1-value', 'pdet-channel-2-value', 'pdet-channel-3-value'], 'stage-x': ['stage-x'], 'stage-y': ['stage-y']}, 'run_start': 'db67e592-ab16-45e9-bb8c-9327895538d6', 'time': 1772448457.5810208, 'uid': '7b47b3f0-f695-44da-bcd4-30d44811af63', 'hints': {'bdet': {'fields': ['bdet']}, 'pdet': {'fields': ['pdet-channel-1-value', 'pdet-channel-2-value', 'pdet-channel-3-value']}, 'stage-x': {'fields': ['stage-x']}, 'stage-y': {'fields': ['stage-y']}}}
stream_resource {'uid': 'c9778d72-0c4c-43f7-870b-033dabb10513', 'data_key': 'bdet', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/46e6b19e-328c-4c76-9799-e1a0141973eb.h5', 'parameters': {'chunk_shape': (1, 240, 320), 'dataset': '/entry/data/data'}, 'run_start': 'db67e592-ab16-45e9-bb8c-9327895538d6'}
stream_resource {'uid': 'b680967c-fafa-4392-a127-836267e2e2aa', 'data_key': 'bdet-sum', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/46e6b19e-328c-4c76-9799-e1a0141973eb.h5', 'parameters': {'chunk_shape': (1024,), 'dataset': '/entry/sum'}, 'run_start': 'db67e592-ab16-45e9-bb8c-9327895538d6'}
stream_datum {'stream_resource': 'c9778d72-0c4c-43f7-870b-033dabb10513', 'uid': 'c9778d72-0c4c-43f7-870b-033dabb10513/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
stream_datum {'stream_resource': 'b680967c-fafa-4392-a127-836267e2e2aa', 'uid': 'b680967c-fafa-4392-a127-836267e2e2aa/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
|         1 | 10:47:37.6 |      1.000 |      2.000 |                  921 |                  887 |                  859 |
event {'uid': '0efb1187-803d-4eb6-b026-1548acdc3f43', 'time': 1772448457.6716943, 'data': {'pdet-channel-1-value': 921, 'pdet-channel-2-value': 887, 'pdet-channel-3-value': 859, 'stage-x': 1.0, 'stage-y': 2.0}, 'timestamps': {'pdet-channel-1-value': 1772448457.4759786, 'pdet-channel-2-value': 1772448457.4760158, 'pdet-channel-3-value': 1772448457.4760294, 'stage-x': 1772448457.3710356, 'stage-y': 1772448457.3711014}, 'seq_num': 1, 'filled': {}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
stream_datum {'stream_resource': 'c9778d72-0c4c-43f7-870b-033dabb10513', 'uid': 'c9778d72-0c4c-43f7-870b-033dabb10513/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
stream_datum {'stream_resource': 'b680967c-fafa-4392-a127-836267e2e2aa', 'uid': 'b680967c-fafa-4392-a127-836267e2e2aa/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
|         2 | 10:47:38.9 |      1.000 |      3.000 |                  937 |                  903 |                  875 |
event {'uid': '82942edb-60c8-4019-aa6d-619f39cf3314', 'time': 1772448458.999021, 'data': {'pdet-channel-1-value': 937, 'pdet-channel-2-value': 903, 'pdet-channel-3-value': 875, 'stage-x': 1.0, 'stage-y': 3.0}, 'timestamps': {'pdet-channel-1-value': 1772448458.8952668, 'pdet-channel-2-value': 1772448458.8953016, 'pdet-channel-3-value': 1772448458.8953145, 'stage-x': 1772448457.3710356, 'stage-y': 1772448458.793343}, 'seq_num': 2, 'filled': {}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
stream_datum {'stream_resource': 'c9778d72-0c4c-43f7-870b-033dabb10513', 'uid': 'c9778d72-0c4c-43f7-870b-033dabb10513/2', 'seq_nums': {'start': 3, 'stop': 4}, 'indices': {'start': 2, 'stop': 3}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
stream_datum {'stream_resource': 'b680967c-fafa-4392-a127-836267e2e2aa', 'uid': 'b680967c-fafa-4392-a127-836267e2e2aa/2', 'seq_nums': {'start': 3, 'stop': 4}, 'indices': {'start': 2, 'stop': 3}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
|         3 | 10:47:40.2 |      2.000 |      2.000 |                  761 |                  740 |                  722 |
event {'uid': 'fc2a4b5e-7d78-4ed1-a347-6bd85673b359', 'time': 1772448460.257184, 'data': {'pdet-channel-1-value': 761, 'pdet-channel-2-value': 740, 'pdet-channel-3-value': 722, 'stage-x': 2.0, 'stage-y': 2.0}, 'timestamps': {'pdet-channel-1-value': 1772448460.1535048, 'pdet-channel-2-value': 1772448460.1535387, 'pdet-channel-3-value': 1772448460.1535509, 'stage-x': 1772448460.0516531, 'stage-y': 1772448460.051696}, 'seq_num': 3, 'filled': {}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
stream_datum {'stream_resource': 'c9778d72-0c4c-43f7-870b-033dabb10513', 'uid': 'c9778d72-0c4c-43f7-870b-033dabb10513/3', 'seq_nums': {'start': 4, 'stop': 5}, 'indices': {'start': 3, 'stop': 4}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
stream_datum {'stream_resource': 'b680967c-fafa-4392-a127-836267e2e2aa', 'uid': 'b680967c-fafa-4392-a127-836267e2e2aa/3', 'seq_nums': {'start': 4, 'stop': 5}, 'indices': {'start': 3, 'stop': 4}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
|         4 | 10:47:41.5 |      2.000 |      3.000 |                  487 |                  467 |                  448 |
event {'uid': 'c953be23-359c-45fa-8f3d-db1febf933ac', 'time': 1772448461.537857, 'data': {'pdet-channel-1-value': 487, 'pdet-channel-2-value': 467, 'pdet-channel-3-value': 448, 'stage-x': 2.0, 'stage-y': 3.0}, 'timestamps': {'pdet-channel-1-value': 1772448461.4340785, 'pdet-channel-2-value': 1772448461.4341104, 'pdet-channel-3-value': 1772448461.4341233, 'stage-x': 1772448460.0516531, 'stage-y': 1772448461.3322856}, 'seq_num': 4, 'filled': {}, 'descriptor': '7b47b3f0-f695-44da-bcd4-30d44811af63'}
+-----------+------------+------------+------------+----------------------+----------------------+----------------------+
generator grid_scan ['db67e592'] (scan num: 3)



stop {'uid': '0d89b0d5-fef8-4d9d-8ba9-1136a266d227', 'time': 1772448462.5411136, 'run_start': 'db67e592-ab16-45e9-bb8c-9327895538d6', 'exit_status': 'success', 'reason': '', 'num_events': {'primary': 4}}
Out[3]: RunEngineResult(run_start_uids=('db67e592-ab16-45e9-bb8c-9327895538d6',), plan_result='db67e592-ab16-45e9-bb8c-9327895538d6', exit_status='success', interrupted=False, reason='', exception=None)

Simplify the plan to just use the detector#

The above examples show what happens if you trigger() a detector at each point of a scan, in this case taking a single frame each time. Let’s write our own simple plan that only triggers and reads from the detector, using the utility bps (bluesky.plan_stubs) and bpp (bluesky.preprocessors):

In [4]: @bpp.stage_decorator([bdet])
   ...: @bpp.run_decorator()
   ...: def my_count_plan():
   ...:     for i in range(2):
   ...:         yield from bps.trigger_and_read([bdet])
   ...: 
In [5]: RE(my_count_plan(), print)


Transient Scan ID: 4     Time: 2026-03-02 10:47:42
Persistent Unique Scan ID: 'e320c88f-fc4d-4e20-be4f-d7cbbb160dc1'
start {'uid': 'e320c88f-fc4d-4e20-be4f-d7cbbb160dc1', 'time': 1772448462.6557374, 'versions': {'ophyd': '1.11.0', 'ophyd_async': '0.17.dev6+g3095dfc8b', 'bluesky': '1.14.6'}, 'scan_id': 4, 'plan_type': 'generator', 'plan_name': 'my_count_plan'}
New stream: 'primary'
+-----------+------------+
|   seq_num |       time |
+-----------+------------+
descriptor {'configuration': {'bdet': {'data': {}, 'timestamps': {}, 'data_keys': {}}}, 'data_keys': {'bdet': {'source': 'file://localhost/tmp/tmpwgdysp9j/89044546-7470-484d-bd43-8567c7e63d5d.h5', 'shape': [1, 240, 320], 'dtype': 'array', 'dtype_numpy': '|u1', 'external': 'STREAM:', 'object_name': 'bdet'}, 'bdet-sum': {'source': 'file://localhost/tmp/tmpwgdysp9j/89044546-7470-484d-bd43-8567c7e63d5d.h5', 'shape': [1], 'dtype': 'number', 'dtype_numpy': '<i8', 'external': 'STREAM:', 'object_name': 'bdet'}}, 'name': 'primary', 'object_keys': {'bdet': ['bdet', 'bdet-sum']}, 'run_start': 'e320c88f-fc4d-4e20-be4f-d7cbbb160dc1', 'time': 1772448462.8623035, 'uid': 'cb576516-bd3e-4c3c-b873-37f432848ae3', 'hints': {'bdet': {'fields': ['bdet']}}}
stream_resource {'uid': 'e8c98f48-d493-4242-89dc-cbe6720b5c2f', 'data_key': 'bdet', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/89044546-7470-484d-bd43-8567c7e63d5d.h5', 'parameters': {'chunk_shape': (1, 240, 320), 'dataset': '/entry/data/data'}, 'run_start': 'e320c88f-fc4d-4e20-be4f-d7cbbb160dc1'}
stream_resource {'uid': 'fc450881-1013-4261-aa07-e3cd20c42c3c', 'data_key': 'bdet-sum', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/89044546-7470-484d-bd43-8567c7e63d5d.h5', 'parameters': {'chunk_shape': (1024,), 'dataset': '/entry/sum'}, 'run_start': 'e320c88f-fc4d-4e20-be4f-d7cbbb160dc1'}
stream_datum {'stream_resource': 'e8c98f48-d493-4242-89dc-cbe6720b5c2f', 'uid': 'e8c98f48-d493-4242-89dc-cbe6720b5c2f/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': 'cb576516-bd3e-4c3c-b873-37f432848ae3'}
stream_datum {'stream_resource': 'fc450881-1013-4261-aa07-e3cd20c42c3c', 'uid': 'fc450881-1013-4261-aa07-e3cd20c42c3c/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': 'cb576516-bd3e-4c3c-b873-37f432848ae3'}
|         1 | 10:47:42.8 |
event {'uid': 'a92715f2-ee81-450e-acb3-580f17086a79', 'time': 1772448462.8691936, 'data': {}, 'timestamps': {}, 'seq_num': 1, 'filled': {}, 'descriptor': 'cb576516-bd3e-4c3c-b873-37f432848ae3'}
stream_datum {'stream_resource': 'e8c98f48-d493-4242-89dc-cbe6720b5c2f', 'uid': 'e8c98f48-d493-4242-89dc-cbe6720b5c2f/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': 'cb576516-bd3e-4c3c-b873-37f432848ae3'}
stream_datum {'stream_resource': 'fc450881-1013-4261-aa07-e3cd20c42c3c', 'uid': 'fc450881-1013-4261-aa07-e3cd20c42c3c/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': 'cb576516-bd3e-4c3c-b873-37f432848ae3'}
|         2 | 10:47:43.0 |
event {'uid': '6fdbd680-6ec6-4b19-8c2e-563c0b31f889', 'time': 1772448463.0728254, 'data': {}, 'timestamps': {}, 'seq_num': 2, 'filled': {}, 'descriptor': 'cb576516-bd3e-4c3c-b873-37f432848ae3'}
+-----------+------------+
generator my_count_plan ['e320c88f'] (scan num: 4)



stop {'uid': '9352d062-96a9-4ed0-ae34-1676dcff5998', 'time': 1772448463.0732408, 'run_start': 'e320c88f-fc4d-4e20-be4f-d7cbbb160dc1', 'exit_status': 'success', 'reason': '', 'num_events': {'primary': 2}}
Out[5]: RunEngineResult(run_start_uids=('e320c88f-fc4d-4e20-be4f-d7cbbb160dc1',), plan_result='e320c88f-fc4d-4e20-be4f-d7cbbb160dc1', exit_status='success', interrupted=False, reason='', exception=None)

Here we see the same sort of documents as above, but with only detector information in it.

Note that on each trigger, only a single image is taken, at the default exposure of 0.1s. If we would like a different exposure time, we can specify with a TriggerInfo:

In [6]: from ophyd_async.core import TriggerInfo

In [7]: @bpp.stage_decorator([bdet])
   ...: @bpp.run_decorator()
   ...: def my_count_plan_with_prepare():
   ...:     yield from bps.prepare(bdet, TriggerInfo(livetime=0.001), wait=True)
   ...:     for i in range(2):
   ...:         yield from bps.trigger_and_read([bdet])
   ...: 
In [8]: RE(my_count_plan_with_prepare(), print)


Transient Scan ID: 5     Time: 2026-03-02 10:47:43
Persistent Unique Scan ID: 'deaea30e-477d-4c08-8258-cb5667b86392'
start {'uid': 'deaea30e-477d-4c08-8258-cb5667b86392', 'time': 1772448463.1858566, 'versions': {'ophyd': '1.11.0', 'ophyd_async': '0.17.dev6+g3095dfc8b', 'bluesky': '1.14.6'}, 'scan_id': 5, 'plan_type': 'generator', 'plan_name': 'my_count_plan_with_prepare'}
New stream: 'primary'
+-----------+------------+
|   seq_num |       time |
+-----------+------------+
descriptor {'configuration': {'bdet': {'data': {}, 'timestamps': {}, 'data_keys': {}}}, 'data_keys': {'bdet': {'source': 'file://localhost/tmp/tmpwgdysp9j/dfa7d0f1-642b-456a-858b-db0abf6d9e90.h5', 'shape': [1, 240, 320], 'dtype': 'array', 'dtype_numpy': '|u1', 'external': 'STREAM:', 'object_name': 'bdet'}, 'bdet-sum': {'source': 'file://localhost/tmp/tmpwgdysp9j/dfa7d0f1-642b-456a-858b-db0abf6d9e90.h5', 'shape': [1], 'dtype': 'number', 'dtype_numpy': '<i8', 'external': 'STREAM:', 'object_name': 'bdet'}}, 'name': 'primary', 'object_keys': {'bdet': ['bdet', 'bdet-sum']}, 'run_start': 'deaea30e-477d-4c08-8258-cb5667b86392', 'time': 1772448463.192914, 'uid': '09270c2c-ec45-48af-b966-00931b7aa96f', 'hints': {'bdet': {'fields': ['bdet']}}}
stream_resource {'uid': '900b20db-56ab-452b-885e-6df795676c64', 'data_key': 'bdet', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/dfa7d0f1-642b-456a-858b-db0abf6d9e90.h5', 'parameters': {'chunk_shape': (1, 240, 320), 'dataset': '/entry/data/data'}, 'run_start': 'deaea30e-477d-4c08-8258-cb5667b86392'}
stream_resource {'uid': '08f61af4-5b8e-4f56-b581-ca37e0a44004', 'data_key': 'bdet-sum', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/dfa7d0f1-642b-456a-858b-db0abf6d9e90.h5', 'parameters': {'chunk_shape': (1024,), 'dataset': '/entry/sum'}, 'run_start': 'deaea30e-477d-4c08-8258-cb5667b86392'}
stream_datum {'stream_resource': '900b20db-56ab-452b-885e-6df795676c64', 'uid': '900b20db-56ab-452b-885e-6df795676c64/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': '09270c2c-ec45-48af-b966-00931b7aa96f'}
stream_datum {'stream_resource': '08f61af4-5b8e-4f56-b581-ca37e0a44004', 'uid': '08f61af4-5b8e-4f56-b581-ca37e0a44004/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': '09270c2c-ec45-48af-b966-00931b7aa96f'}
|         1 | 10:47:43.1 |
event {'uid': '145e4745-1d00-4584-87b5-01ae3705aa7c', 'time': 1772448463.194196, 'data': {}, 'timestamps': {}, 'seq_num': 1, 'filled': {}, 'descriptor': '09270c2c-ec45-48af-b966-00931b7aa96f'}
stream_datum {'stream_resource': '900b20db-56ab-452b-885e-6df795676c64', 'uid': '900b20db-56ab-452b-885e-6df795676c64/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': '09270c2c-ec45-48af-b966-00931b7aa96f'}
stream_datum {'stream_resource': '08f61af4-5b8e-4f56-b581-ca37e0a44004', 'uid': '08f61af4-5b8e-4f56-b581-ca37e0a44004/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': '09270c2c-ec45-48af-b966-00931b7aa96f'}
|         2 | 10:47:43.1 |
event {'uid': '66a24dc4-f339-40f9-81f0-3049da8fae1b', 'time': 1772448463.1978405, 'data': {}, 'timestamps': {}, 'seq_num': 2, 'filled': {}, 'descriptor': '09270c2c-ec45-48af-b966-00931b7aa96f'}
+-----------+------------+
generator my_count_plan_with_prepare ['deaea30e'] (scan num: 5)



stop {'uid': 'fe5f9611-6060-4a5e-a95c-eb63cd26e870', 'time': 1772448463.1982195, 'run_start': 'deaea30e-477d-4c08-8258-cb5667b86392', 'exit_status': 'success', 'reason': '', 'num_events': {'primary': 2}}
Out[8]: RunEngineResult(run_start_uids=('deaea30e-477d-4c08-8258-cb5667b86392',), plan_result='deaea30e-477d-4c08-8258-cb5667b86392', exit_status='success', interrupted=False, reason='', exception=None)

The TriggerInfo contains all the parameters needed to set up the detector:

  • trigger - The trigger type: INTERNAL (detector generates its own triggers), EXTERNAL_EDGE (rising edge starts an internally-timed exposure), or EXTERNAL_LEVEL (high level duration determines exposure time)

  • livetime - The exposure time per frame

  • deadtime - The minimum time between exposures (for internal triggering)

  • exposures_per_collection - Number of exposures averaged into a single collection that is passed to the data writer (default 1)

  • collections_per_event - Number of collections per bluesky event (default 1)

And if being used for flyscanning (kickoff/complete) then specify:

  • number_of_events - Number of bluesky events to emit (default 1)

This also moves the work of setting up the detector from the first call of trigger() to the prepare() call. We can also move the creation of the descriptor earlier, so there is no extra work to do on the first call to trigger():

In [9]: from ophyd_async.core import TriggerInfo

In [10]: @bpp.stage_decorator([bdet])
   ....: @bpp.run_decorator()
   ....: def my_count_plan_with_prepare():
   ....:     yield from bps.prepare(bdet, TriggerInfo(), wait=True)
   ....:     yield from bps.declare_stream(bdet, name="primary")
   ....:     for i in range(2):
   ....:         yield from bps.trigger_and_read([bdet])
   ....: 
In [11]: RE(my_count_plan_with_prepare(), print)


Transient Scan ID: 6     Time: 2026-03-02 10:47:43
Persistent Unique Scan ID: 'fc9173e2-fcec-478f-bece-654d69ab058e'
start {'uid': 'fc9173e2-fcec-478f-bece-654d69ab058e', 'time': 1772448463.3102741, 'versions': {'ophyd': '1.11.0', 'ophyd_async': '0.17.dev6+g3095dfc8b', 'bluesky': '1.14.6'}, 'scan_id': 6, 'plan_type': 'generator', 'plan_name': 'my_count_plan_with_prepare'}
New stream: 'primary'
+-----------+------------+
|   seq_num |       time |
+-----------+------------+
descriptor {'configuration': {'bdet': {'data': {}, 'timestamps': {}, 'data_keys': {}}}, 'data_keys': {'bdet': {'source': 'file://localhost/tmp/tmpwgdysp9j/f2abd1d2-174b-43e9-ab52-7a1cd81ed305.h5', 'shape': [1, 240, 320], 'dtype': 'array', 'dtype_numpy': '|u1', 'external': 'STREAM:', 'object_name': 'bdet'}, 'bdet-sum': {'source': 'file://localhost/tmp/tmpwgdysp9j/f2abd1d2-174b-43e9-ab52-7a1cd81ed305.h5', 'shape': [1], 'dtype': 'number', 'dtype_numpy': '<i8', 'external': 'STREAM:', 'object_name': 'bdet'}}, 'name': 'primary', 'object_keys': {'bdet': ['bdet', 'bdet-sum']}, 'run_start': 'fc9173e2-fcec-478f-bece-654d69ab058e', 'time': 1772448463.313677, 'uid': '37a4db4f-a8e3-4b99-b0b4-ba415a24ab0c', 'hints': {'bdet': {'fields': ['bdet']}}}
stream_resource {'uid': 'a69eeae1-4716-4e83-b398-cb6a15c55e94', 'data_key': 'bdet', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/f2abd1d2-174b-43e9-ab52-7a1cd81ed305.h5', 'parameters': {'chunk_shape': (1, 240, 320), 'dataset': '/entry/data/data'}, 'run_start': 'fc9173e2-fcec-478f-bece-654d69ab058e'}
stream_resource {'uid': 'f3b892bd-362d-4525-8ba2-f45b86a70a85', 'data_key': 'bdet-sum', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/f2abd1d2-174b-43e9-ab52-7a1cd81ed305.h5', 'parameters': {'chunk_shape': (1024,), 'dataset': '/entry/sum'}, 'run_start': 'fc9173e2-fcec-478f-bece-654d69ab058e'}
stream_datum {'stream_resource': 'a69eeae1-4716-4e83-b398-cb6a15c55e94', 'uid': 'a69eeae1-4716-4e83-b398-cb6a15c55e94/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': '37a4db4f-a8e3-4b99-b0b4-ba415a24ab0c'}
stream_datum {'stream_resource': 'f3b892bd-362d-4525-8ba2-f45b86a70a85', 'uid': 'f3b892bd-362d-4525-8ba2-f45b86a70a85/0', 'seq_nums': {'start': 1, 'stop': 2}, 'indices': {'start': 0, 'stop': 1}, 'descriptor': '37a4db4f-a8e3-4b99-b0b4-ba415a24ab0c'}
|         1 | 10:47:43.3 |
event {'uid': '1c023d01-1394-43a5-844c-8143a9c5d199', 'time': 1772448463.318458, 'data': {}, 'timestamps': {}, 'seq_num': 1, 'filled': {}, 'descriptor': '37a4db4f-a8e3-4b99-b0b4-ba415a24ab0c'}
stream_datum {'stream_resource': 'a69eeae1-4716-4e83-b398-cb6a15c55e94', 'uid': 'a69eeae1-4716-4e83-b398-cb6a15c55e94/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': '37a4db4f-a8e3-4b99-b0b4-ba415a24ab0c'}
stream_datum {'stream_resource': 'f3b892bd-362d-4525-8ba2-f45b86a70a85', 'uid': 'f3b892bd-362d-4525-8ba2-f45b86a70a85/1', 'seq_nums': {'start': 2, 'stop': 3}, 'indices': {'start': 1, 'stop': 2}, 'descriptor': '37a4db4f-a8e3-4b99-b0b4-ba415a24ab0c'}
|         2 | 10:47:43.3 |
event {'uid': '76f0cba0-c77f-412a-bd25-c9f8f1bd7a07', 'time': 1772448463.3220994, 'data': {}, 'timestamps': {}, 'seq_num': 2, 'filled': {}, 'descriptor': '37a4db4f-a8e3-4b99-b0b4-ba415a24ab0c'}
+-----------+------------+
generator my_count_plan_with_prepare ['fc9173e2'] (scan num: 6)



stop {'uid': '95ac9e5c-1dec-4b83-bb44-2fc9d2bf5ad1', 'time': 1772448463.3224783, 'run_start': 'fc9173e2-fcec-478f-bece-654d69ab058e', 'exit_status': 'success', 'reason': '', 'num_events': {'primary': 2}}
Out[11]: RunEngineResult(run_start_uids=('fc9173e2-fcec-478f-bece-654d69ab058e',), plan_result='fc9173e2-fcec-478f-bece-654d69ab058e', exit_status='success', interrupted=False, reason='', exception=None)

Run a fly scan and investigate the documents#

The above demonstrates the detector portion of a step scan, letting the things you want to scan settle before taking data from the detector, and doing this at every point of the scan. Our filewriting detector also supports the ability to fly scan it, taking data while you are scanning other things. To do this, it implements the bluesky.protocols.Flyable protocol, which allows us to kickoff() a series of images, then wait until it is complete():

In [12]: @bpp.stage_decorator([bdet])
   ....: @bpp.run_decorator()
   ....: def fly_plan():
   ....:     yield from bps.prepare(bdet, TriggerInfo(number_of_events=7), wait=True)
   ....:     yield from bps.declare_stream(bdet, name="primary")
   ....:     yield from bps.kickoff(bdet, wait=True)
   ....:     yield from bps.collect_while_completing(flyers=[bdet], dets=[bdet], flush_period=0.5)
   ....: 
In [13]: RE(fly_plan(), print)


Transient Scan ID: 7     Time: 2026-03-02 10:47:43
Persistent Unique Scan ID: 'cfd60e71-8106-4440-86a3-503fd978bfb6'
start {'uid': 'cfd60e71-8106-4440-86a3-503fd978bfb6', 'time': 1772448463.4341137, 'versions': {'ophyd': '1.11.0', 'ophyd_async': '0.17.dev6+g3095dfc8b', 'bluesky': '1.14.6'}, 'scan_id': 7, 'plan_type': 'generator', 'plan_name': 'fly_plan'}
New stream: 'primary'
+-----------+------------+
|   seq_num |       time |
+-----------+------------+
descriptor {'configuration': {'bdet': {'data': {}, 'timestamps': {}, 'data_keys': {}}}, 'data_keys': {'bdet': {'source': 'file://localhost/tmp/tmpwgdysp9j/2b9247f5-a321-418b-8921-c827fdf3c536.h5', 'shape': [1, 240, 320], 'dtype': 'array', 'dtype_numpy': '|u1', 'external': 'STREAM:', 'object_name': 'bdet'}, 'bdet-sum': {'source': 'file://localhost/tmp/tmpwgdysp9j/2b9247f5-a321-418b-8921-c827fdf3c536.h5', 'shape': [1], 'dtype': 'number', 'dtype_numpy': '<i8', 'external': 'STREAM:', 'object_name': 'bdet'}}, 'name': 'primary', 'object_keys': {'bdet': ['bdet', 'bdet-sum']}, 'run_start': 'cfd60e71-8106-4440-86a3-503fd978bfb6', 'time': 1772448463.437773, 'uid': '9966fa4a-32af-43cc-a6ad-5a73e9cfc432', 'hints': {'bdet': {'fields': ['bdet']}}}
stream_resource {'uid': 'bca65a05-5f36-4c7d-aae6-33b589f4e4b5', 'data_key': 'bdet', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/2b9247f5-a321-418b-8921-c827fdf3c536.h5', 'parameters': {'chunk_shape': (1, 240, 320), 'dataset': '/entry/data/data'}, 'run_start': 'cfd60e71-8106-4440-86a3-503fd978bfb6'}
stream_resource {'uid': '39d4e4c7-9809-48f9-a9e1-333e0f388296', 'data_key': 'bdet-sum', 'mimetype': 'application/x-hdf5', 'uri': 'file://localhost/tmp/tmpwgdysp9j/2b9247f5-a321-418b-8921-c827fdf3c536.h5', 'parameters': {'chunk_shape': (1024,), 'dataset': '/entry/sum'}, 'run_start': 'cfd60e71-8106-4440-86a3-503fd978bfb6'}
stream_datum {'stream_resource': 'bca65a05-5f36-4c7d-aae6-33b589f4e4b5', 'uid': 'bca65a05-5f36-4c7d-aae6-33b589f4e4b5/0', 'seq_nums': {'start': 1, 'stop': 8}, 'indices': {'start': 0, 'stop': 7}, 'descriptor': '9966fa4a-32af-43cc-a6ad-5a73e9cfc432'}
stream_datum {'stream_resource': '39d4e4c7-9809-48f9-a9e1-333e0f388296', 'uid': '39d4e4c7-9809-48f9-a9e1-333e0f388296/0', 'seq_nums': {'start': 1, 'stop': 8}, 'indices': {'start': 0, 'stop': 7}, 'descriptor': '9966fa4a-32af-43cc-a6ad-5a73e9cfc432'}
+-----------+------------+
generator fly_plan ['cfd60e71'] (scan num: 7)



stop {'uid': '2b6b0dfc-cf6c-4c50-941f-14fd3bc3a4a8', 'time': 1772448463.4490166, 'run_start': 'cfd60e71-8106-4440-86a3-503fd978bfb6', 'exit_status': 'success', 'reason': '', 'num_events': {'primary': 7}}
Out[13]: RunEngineResult(run_start_uids=('cfd60e71-8106-4440-86a3-503fd978bfb6',), plan_result='cfd60e71-8106-4440-86a3-503fd978bfb6', exit_status='success', interrupted=False, reason='', exception=None)

As before, we see the start, descriptor, and pair of stream resources, but this time we don’t see any event documents. Also, even though we asked for 7 frames from each of the 2 streams, we only got 2 stream datums for each stream.

What is happening is that instead of triggering, waiting, and publishing a single frame, we are setting up the detector to take 7 frames without stopping, then at the flush_period of 0.5s emitting a stream datum with the frames that have been captured. If we inspect the stream datum documents for each stream we see that:

  • The first has 'indices': {'start': 0, 'stop': 4}

  • The second has 'indices': {'start': 4, 'stop': 7}

This behavior allows us to scale up the framerate of the detector without scaling up the number of documents emitted: whether the detector goes at 10Hz or 10MHz it still only emits one stream datum per stream per flush period, just with different numbers in the indices field.

Look at the Device implementations#

Now we’ll have a look at the code to see how we implement one of these detectors:

SimBlobDetector#

from collections.abc import Sequence

from ophyd_async.core import PathProvider, SignalR, StandardDetector

from ._blob_arm_logic import BlobArmLogic
from ._blob_data_logic import BlobDataLogic
from ._blob_trigger_logic import BlobTriggerLogic
from ._pattern_generator import PatternGenerator


class SimBlobDetector(StandardDetector):
    """Simulates a detector and writes Blobs to file."""

    def __init__(
        self,
        path_provider: PathProvider,
        pattern_generator: PatternGenerator | None = None,
        config_sigs: Sequence[SignalR] = (),
        name: str = "",
    ) -> None:
        self.pattern_generator = pattern_generator or PatternGenerator()
        self.add_detector_logics(
            BlobTriggerLogic(pattern_generator=self.pattern_generator),
            BlobArmLogic(pattern_generator=self.pattern_generator),
            BlobDataLogic(
                path_provider=path_provider, pattern_generator=self.pattern_generator
            ),
        )
        self.add_config_signals(*config_sigs)
        super().__init__(name=name)

It derives from StandardDetector which is a utility baseclass that implements the protocols we have mentioned so far in this tutorial. It uses three types of logic classes to provide behavior for each protocol verb:

  • DetectorTriggerLogic to setup the exposure and trigger mode of the detector

  • DetectorArmLogic to arm it and wait for it to complete

  • DetectorDataLogic to tell the detector to open a file, describe the datasets it will write, and emit StreamAsset documents as frames are written

In this case, we have three logic classes written just for this simulation, all taking a reference to the pattern generator that provides methods for both detector control and file writing. In other cases the detector control and filewriting may be handled by different sub-devices that talk to different parts of the control system. The job of the top level detector class is to take the arguments that the logic classes need, create the logic instances, and pass them to StandardDetector.add_detector_logics.

Now let’s look at the underlying classes that define the detector behavior:

BlobTriggerLogic#

First we have BlobTriggerLogic, a DetectorTriggerLogic subclass:

from ophyd_async.core import DetectorTriggerLogic

from ._pattern_generator import PatternGenerator


class BlobTriggerLogic(DetectorTriggerLogic):
    def __init__(self, pattern_generator: PatternGenerator):
        self.pattern_generator = pattern_generator

    async def prepare_internal(self, num: int, livetime: float, deadtime: float):
        self.pattern_generator.setup_acquisition_parameters(
            exposure=livetime,
            period=livetime + deadtime,
            number_of_frames=num,
        )

Its job is to configure the detector for different trigger modes. In this case we only implement internal triggering:

  • prepare_internal() takes the number of frames, livetime (exposure), and deadtime, and sets up the pattern generator with those parameters.

If we wanted to support external triggering, we would also implement:

  • prepare_edge() for external edge triggering (rising edge starts internally-timed exposure)

  • prepare_level() for external level/gate triggering (high level duration determines exposure)

We could also implement:

  • get_deadtime() to calculate the minimum time between exposures based on configuration

  • config_sigs() to return signals that should appear in read_configuration()

BlobArmLogic#

Next we have BlobArmLogic, a DetectorArmLogic subclass:

import asyncio
from contextlib import suppress

from ophyd_async.core import DetectorArmLogic, TriggerInfo

from ._pattern_generator import PatternGenerator


class BlobArmLogic(DetectorArmLogic):
    def __init__(self, pattern_generator: PatternGenerator):
        self.pattern_generator = pattern_generator
        self.trigger_info: TriggerInfo | None = None
        self.task: asyncio.Task | None = None

    async def arm(self):
        # Start a background process off writing the images to file
        self.task = asyncio.create_task(self.pattern_generator.write_images_to_file())

    async def wait_for_idle(self):
        # Wait for the background task to complete
        if self.task:
            await self.task

    async def disarm(self):
        # Stop the background task and wait for it to finish
        if self.task:
            self.task.cancel()
            with suppress(asyncio.CancelledError):
                await self.task
            self.task = None

Its job is to control the acquisition process on the detector, starting and stopping the collection of data:

  • arm() starts the acquisition process that has been prepared. In this case we create a background task that will write our simulation images to file.

  • wait_for_idle() waits for that acquisition process to be complete.

  • disarm() interrupts the acquisition process, and then waits for it to complete.

BlobDataLogic#

Then we have BlobDataLogic, a DetectorDataLogic subclass:

from collections.abc import Sequence

import numpy as np

from ophyd_async.core import (
    DetectorDataLogic,
    PathProvider,
    StreamableDataProvider,
    StreamResourceDataProvider,
    StreamResourceInfo,
)

from ._pattern_generator import DATA_PATH, SUM_PATH, PatternGenerator

WIDTH = 320
HEIGHT = 240


class BlobDataLogic(DetectorDataLogic):
    def __init__(
        self,
        path_provider: PathProvider,
        pattern_generator: PatternGenerator,
    ):
        self.path_provider = path_provider
        self.pattern_generator = pattern_generator

    async def prepare_unbounded(self, datakey_name: str) -> StreamableDataProvider:
        # Work out where to write
        path_info = self.path_provider(datakey_name)
        # Open the file
        write_path = path_info.directory_path / f"{path_info.filename}.h5"
        self.pattern_generator.open_file(write_path, WIDTH, HEIGHT)
        # Return a provider that reflects what we have made
        data_resource = StreamResourceInfo(
            data_key=datakey_name,
            shape=(HEIGHT, WIDTH),
            # NDAttributes appear to always be configured with
            # this chunk size
            chunk_shape=(1, HEIGHT, WIDTH),
            dtype_numpy=np.dtype(np.uint8).str,
            parameters={"dataset": DATA_PATH},
        )
        sum_resource = StreamResourceInfo(
            data_key=f"{datakey_name}-sum",
            shape=(),
            # NDAttributes appear to always be configured with
            # this chunk size
            chunk_shape=(1024,),
            dtype_numpy=np.dtype(np.int64).str,
            parameters={"dataset": SUM_PATH},
        )
        return StreamResourceDataProvider(
            uri=f"{path_info.directory_uri}{path_info.filename}.h5",
            resources=[data_resource, sum_resource],
            mimetype="application/x-hdf5",
            collections_written_signal=self.pattern_generator.images_written,
        )

    async def stop(self) -> None:
        self.pattern_generator.close_file()

    def get_hinted_fields(self, datakey_name: str) -> Sequence[str]:
        # The main dataset is always hinted
        return [datakey_name]

Its job is to manage the file writing and data streaming:

  • prepare_unbounded() tells the detector to open a file, and returns a StreamableDataProvider that describes the datasets that will be written and tracks write progress

  • get_hinted_fields() returns the data keys that are interesting to plot

  • stop() tells the detector to close the file

The StreamableDataProvider returned from prepare_unbounded() contains:

  • collections_written_signal - a signal that tracks how many frames have been written

  • make_datakeys() - creates DataKey descriptions for each dataset

  • make_stream_docs() - emits StreamResource and StreamDatum documents as frames are written

Conclusion#

We have seen how to make a StandardDetector and how the DetectorTriggerLogic, DetectorArmLogic, and DetectorDataLogic classes allow us to customise its behavior through composition. This separation of concerns makes it easy to:

  • Mix and match different trigger modes, arming strategies, and data outputs

  • Add multiple data streams (e.g., multiple HDF writers for different ROIs)

  • Combine file writing with signal reading (e.g., add stats plugin outputs)

  • Test each component independently

See also

How to implement a Device for an EPICS areaDetector for writing an implementation of StandardDetector for an EPICS areaDetector