ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +97 -62
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +106 -125
- ophyd_async/core/_device.py +69 -63
- ophyd_async/core/_device_filler.py +65 -1
- ophyd_async/core/_flyer.py +14 -5
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +44 -35
- ophyd_async/core/_settings.py +36 -27
- ophyd_async/core/_signal.py +262 -170
- ophyd_async/core/_signal_backend.py +56 -13
- ophyd_async/core/_soft_signal_backend.py +16 -11
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +41 -11
- ophyd_async/core/_utils.py +96 -49
- ophyd_async/core/_yaml_settings.py +2 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/_andor.py +2 -2
- ophyd_async/epics/adandor/_andor_controller.py +4 -2
- ophyd_async/epics/adandor/_andor_io.py +2 -4
- ophyd_async/epics/adaravis/__init__.py +5 -0
- ophyd_async/epics/adaravis/_aravis.py +4 -8
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +23 -8
- ophyd_async/epics/adcore/_core_detector.py +42 -2
- ophyd_async/epics/adcore/_core_io.py +124 -99
- ophyd_async/epics/adcore/_core_logic.py +106 -27
- ophyd_async/epics/adcore/_core_writer.py +12 -8
- ophyd_async/epics/adcore/_hdf_writer.py +21 -38
- ophyd_async/epics/adcore/_single_trigger.py +2 -2
- ophyd_async/epics/adcore/_utils.py +2 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +3 -3
- ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +5 -0
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +4 -14
- ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +3 -2
- ophyd_async/epics/advimba/_vimba_controller.py +4 -2
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +35 -16
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +10 -2
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +4 -4
- ophyd_async/epics/demo/__init__.py +16 -0
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +65 -28
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/_example_ioc.py +21 -9
- ophyd_async/epics/testing/_utils.py +3 -0
- ophyd_async/epics/testing/test_records.db +8 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -6
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +5 -0
- ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
- ophyd_async/fastcs/panda/_table.py +9 -6
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +2 -0
- ophyd_async/plan_stubs/_ensure_connected.py +1 -0
- ophyd_async/plan_stubs/_fly.py +2 -4
- ophyd_async/plan_stubs/_nd_attributes.py +2 -0
- ophyd_async/plan_stubs/_panda.py +1 -0
- ophyd_async/plan_stubs/_settings.py +43 -16
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
- ophyd_async/sim/__init__.py +24 -14
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +18 -32
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +136 -60
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/{sim → demo}/_counter.py +2 -0
- ophyd_async/tango/{sim → demo}/_detector.py +2 -0
- ophyd_async/tango/{sim → demo}/_mover.py +5 -4
- ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +29 -7
- ophyd_async/testing/_assert.py +145 -83
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +89 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/sim/__init__.py +0 -54
- ophyd_async/epics/sim/_ioc.py +0 -29
- ophyd_async/epics/sim/_mover.py +0 -101
- ophyd_async/epics/sim/_sensor.py +0 -37
- ophyd_async/epics/sim/sensor.db +0 -19
- ophyd_async/sim/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
- ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
- ophyd_async/sim/_sim_motor.py +0 -107
- ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
- /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
- /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
ophyd_async/core/_detector.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
"""Module which defines abstract classes to work with detectors"""
|
|
1
|
+
"""Module which defines abstract classes to work with detectors."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import time
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
|
|
7
|
+
from enum import Enum
|
|
7
8
|
from functools import cached_property
|
|
8
9
|
from typing import (
|
|
9
10
|
Generic,
|
|
@@ -28,48 +29,62 @@ from ._device import Device, DeviceConnector
|
|
|
28
29
|
from ._protocol import AsyncConfigurable, AsyncReadable
|
|
29
30
|
from ._signal import SignalR
|
|
30
31
|
from ._status import AsyncStatus, WatchableAsyncStatus
|
|
31
|
-
from ._utils import DEFAULT_TIMEOUT,
|
|
32
|
+
from ._utils import DEFAULT_TIMEOUT, WatcherUpdate, merge_gathered_dicts
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
class DetectorTrigger(
|
|
35
|
-
"""Type of mechanism for triggering a detector to take frames"""
|
|
35
|
+
class DetectorTrigger(Enum):
|
|
36
|
+
"""Type of mechanism for triggering a detector to take frames."""
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
EDGE_TRIGGER = "
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
INTERNAL = "INTERNAL"
|
|
39
|
+
"""Detector generates internal trigger for given rate"""
|
|
40
|
+
|
|
41
|
+
EDGE_TRIGGER = "EDGE_TRIGGER"
|
|
42
|
+
"""Expect a series of arbitrary length trigger signals"""
|
|
43
|
+
|
|
44
|
+
CONSTANT_GATE = "CONSTANT_GATE"
|
|
45
|
+
"""Expect a series of constant width external gate signals"""
|
|
46
|
+
|
|
47
|
+
VARIABLE_GATE = "VARIABLE_GATE"
|
|
48
|
+
"""Expect a series of variable width external gate signals"""
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
class TriggerInfo(BaseModel):
|
|
48
|
-
"""Minimal set of information required to setup triggering on a detector"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
"""Minimal set of information required to setup triggering on a detector."""
|
|
53
|
+
|
|
54
|
+
number_of_triggers: NonNegativeInt | list[NonNegativeInt] = Field(default=1)
|
|
55
|
+
"""Number of triggers that will be sent, (0 means infinite).
|
|
56
|
+
|
|
57
|
+
Can be:
|
|
58
|
+
- A single integer or
|
|
59
|
+
- A list of integers for multiple triggers
|
|
60
|
+
|
|
61
|
+
Example for tomography: ``TriggerInfo(number=[2,3,100,3])``.
|
|
62
|
+
This would trigger:
|
|
63
|
+
|
|
64
|
+
- 2 times for dark field images
|
|
65
|
+
- 3 times for initial flat field images
|
|
66
|
+
- 100 times for projections
|
|
67
|
+
- 3 times for final flat field images
|
|
68
|
+
"""
|
|
69
|
+
|
|
61
70
|
trigger: DetectorTrigger = Field(default=DetectorTrigger.INTERNAL)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
"""Sort of triggers that will be sent"""
|
|
72
|
+
|
|
73
|
+
deadtime: float = Field(default=0.0, ge=0)
|
|
74
|
+
"""What is the minimum deadtime between triggers"""
|
|
75
|
+
|
|
65
76
|
livetime: float | None = Field(default=None, ge=0)
|
|
66
|
-
|
|
77
|
+
"""What is the maximum high time of the triggers"""
|
|
78
|
+
|
|
67
79
|
frame_timeout: float | None = Field(default=None, gt=0)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
#: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
|
|
71
|
-
#: but publish 2 indices, and describe() will show a shape of (5, h, w)
|
|
80
|
+
"""What is the maximum timeout on waiting for a frame"""
|
|
81
|
+
|
|
72
82
|
multiplier: int = 1
|
|
83
|
+
"""How many triggers make up a single StreamDatum index, to allow multiple frames
|
|
84
|
+
from a faster detector to be zipped with a single frame from a slow detector
|
|
85
|
+
e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
|
|
86
|
+
but publish 2 indices, and describe() will show a shape of (5, h, w)
|
|
87
|
+
"""
|
|
73
88
|
|
|
74
89
|
@computed_field
|
|
75
90
|
@cached_property
|
|
@@ -82,89 +97,68 @@ class TriggerInfo(BaseModel):
|
|
|
82
97
|
|
|
83
98
|
|
|
84
99
|
class DetectorController(ABC):
|
|
85
|
-
"""
|
|
86
|
-
Classes implementing this interface should hold the logic for
|
|
87
|
-
arming and disarming a detector
|
|
88
|
-
"""
|
|
100
|
+
"""Detector logic for arming and disarming the detector."""
|
|
89
101
|
|
|
90
102
|
@abstractmethod
|
|
91
103
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
92
|
-
"""For a given exposure, how long should the time between exposures be"""
|
|
104
|
+
"""For a given exposure, how long should the time between exposures be."""
|
|
93
105
|
|
|
94
106
|
@abstractmethod
|
|
95
107
|
async def prepare(self, trigger_info: TriggerInfo) -> None:
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
trigger_info: This is a Pydantic model which contains
|
|
101
|
-
number Expected number of frames.
|
|
102
|
-
trigger Type of trigger for which to prepare the detector. Defaults
|
|
103
|
-
to DetectorTrigger.internal.
|
|
104
|
-
livetime Livetime / Exposure time with which to set up the detector.
|
|
105
|
-
Defaults to None
|
|
106
|
-
if not applicable or the detector is expected to use its previously-set
|
|
107
|
-
exposure time.
|
|
108
|
-
deadtime Defaults to None. This is the minimum deadtime between
|
|
109
|
-
triggers.
|
|
110
|
-
multiplier The number of triggers grouped into a single StreamDatum
|
|
111
|
-
index.
|
|
108
|
+
"""Do all necessary steps to prepare the detector for triggers.
|
|
109
|
+
|
|
110
|
+
:param trigger_info: The sort of triggers to expect.
|
|
112
111
|
"""
|
|
113
112
|
|
|
114
113
|
@abstractmethod
|
|
115
114
|
async def arm(self) -> None:
|
|
116
|
-
"""
|
|
117
|
-
Arm the detector
|
|
118
|
-
"""
|
|
115
|
+
"""Arm the detector."""
|
|
119
116
|
|
|
120
117
|
@abstractmethod
|
|
121
118
|
async def wait_for_idle(self):
|
|
122
|
-
"""
|
|
123
|
-
This will wait on the internal _arm_status and wait for it to get disarmed/idle
|
|
124
|
-
"""
|
|
119
|
+
"""Wait on the internal _arm_status and wait for it to get disarmed/idle."""
|
|
125
120
|
|
|
126
121
|
@abstractmethod
|
|
127
122
|
async def disarm(self):
|
|
128
|
-
"""Disarm the detector, return detector to an idle state"""
|
|
123
|
+
"""Disarm the detector, return detector to an idle state."""
|
|
129
124
|
|
|
130
125
|
|
|
131
126
|
class DetectorWriter(ABC):
|
|
132
|
-
"""Logic for making
|
|
133
|
-
(e.g. an HDF5 file)"""
|
|
127
|
+
"""Logic for making detector write data to somewhere persistent (e.g. HDF5 file)."""
|
|
134
128
|
|
|
135
129
|
@abstractmethod
|
|
136
130
|
async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
|
|
137
131
|
"""Open writer and wait for it to be ready for data.
|
|
138
132
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
Output for ``describe()``
|
|
133
|
+
:param multiplier:
|
|
134
|
+
Each StreamDatum index corresponds to this many written exposures
|
|
135
|
+
:return: Output for ``describe()``
|
|
145
136
|
"""
|
|
146
137
|
|
|
147
|
-
@
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"""Yield the index of each frame (or equivalent data point) as it is written"""
|
|
138
|
+
@property
|
|
139
|
+
def hints(self) -> Hints:
|
|
140
|
+
"""The hints to be used for the detector."""
|
|
141
|
+
return {}
|
|
152
142
|
|
|
153
143
|
@abstractmethod
|
|
154
144
|
async def get_indices_written(self) -> int:
|
|
155
|
-
"""Get the number of indices written"""
|
|
145
|
+
"""Get the number of indices written."""
|
|
146
|
+
|
|
147
|
+
# Note: this method is really async, but if we make it async here then we
|
|
148
|
+
# need to give it a body with a redundant yield statement, which is a bit
|
|
149
|
+
# awkward. So we just leave it as a regular method and let the user
|
|
150
|
+
# implement it as async.
|
|
151
|
+
@abstractmethod
|
|
152
|
+
def observe_indices_written(self, timeout: float) -> AsyncGenerator[int, None]:
|
|
153
|
+
"""Yield the index of each frame (or equivalent data point) as it is written."""
|
|
156
154
|
|
|
157
155
|
@abstractmethod
|
|
158
156
|
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
|
|
159
|
-
"""Create Stream docs up to given number written"""
|
|
157
|
+
"""Create Stream docs up to given number written."""
|
|
160
158
|
|
|
161
159
|
@abstractmethod
|
|
162
160
|
async def close(self) -> None:
|
|
163
|
-
"""Close writer, blocks until I/O is complete"""
|
|
164
|
-
|
|
165
|
-
@property
|
|
166
|
-
def hints(self) -> Hints:
|
|
167
|
-
return {}
|
|
161
|
+
"""Close writer, blocks until I/O is complete."""
|
|
168
162
|
|
|
169
163
|
|
|
170
164
|
# Add type var for controller so we can define
|
|
@@ -192,9 +186,14 @@ class StandardDetector(
|
|
|
192
186
|
WritesStreamAssets,
|
|
193
187
|
Generic[DetectorControllerT, DetectorWriterT],
|
|
194
188
|
):
|
|
195
|
-
"""
|
|
196
|
-
|
|
189
|
+
"""Detector base class for step and fly scanning detectors.
|
|
190
|
+
|
|
197
191
|
Aggregates controller and writer logic together.
|
|
192
|
+
|
|
193
|
+
:param controller: Logic for arming and disarming the detector
|
|
194
|
+
:param writer: Logic for making the detector write persistent data
|
|
195
|
+
:param config_sigs: Signals to read when describe and read configuration are called
|
|
196
|
+
:param name: Device name
|
|
198
197
|
"""
|
|
199
198
|
|
|
200
199
|
def __init__(
|
|
@@ -205,16 +204,6 @@ class StandardDetector(
|
|
|
205
204
|
name: str = "",
|
|
206
205
|
connector: DeviceConnector | None = None,
|
|
207
206
|
) -> None:
|
|
208
|
-
"""
|
|
209
|
-
Constructor
|
|
210
|
-
|
|
211
|
-
Args:
|
|
212
|
-
controller: Logic for arming and disarming the detector
|
|
213
|
-
writer: Logic for making the detector write persistent data
|
|
214
|
-
config_sigs: Signals to read when describe and read
|
|
215
|
-
configuration are called. Defaults to ().
|
|
216
|
-
name: Device name. Defaults to "".
|
|
217
|
-
"""
|
|
218
207
|
self._controller = controller
|
|
219
208
|
self._writer = writer
|
|
220
209
|
self._describe: dict[str, DataKey] = {}
|
|
@@ -236,13 +225,13 @@ class StandardDetector(
|
|
|
236
225
|
|
|
237
226
|
@AsyncStatus.wrap
|
|
238
227
|
async def stage(self) -> None:
|
|
239
|
-
|
|
228
|
+
"""Make sure the detector is idle and ready to be used."""
|
|
240
229
|
await self._check_config_sigs()
|
|
241
230
|
await asyncio.gather(self._writer.close(), self._controller.disarm())
|
|
242
231
|
self._trigger_info = None
|
|
243
232
|
|
|
244
233
|
async def _check_config_sigs(self):
|
|
245
|
-
"""
|
|
234
|
+
"""Check configuration signals are named and connected."""
|
|
246
235
|
for signal in self._config_sigs:
|
|
247
236
|
if signal.name == "":
|
|
248
237
|
raise Exception(
|
|
@@ -258,7 +247,7 @@ class StandardDetector(
|
|
|
258
247
|
|
|
259
248
|
@AsyncStatus.wrap
|
|
260
249
|
async def unstage(self) -> None:
|
|
261
|
-
|
|
250
|
+
"""Disarm the detector and stop file writing."""
|
|
262
251
|
await asyncio.gather(self._writer.close(), self._controller.disarm())
|
|
263
252
|
|
|
264
253
|
async def read_configuration(self) -> dict[str, Reading]:
|
|
@@ -268,6 +257,7 @@ class StandardDetector(
|
|
|
268
257
|
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
269
258
|
|
|
270
259
|
async def read(self) -> dict[str, Reading]:
|
|
260
|
+
"""There is no data to be placed in events, so this is empty."""
|
|
271
261
|
# All data is in StreamResources, not Events, so nothing to output here
|
|
272
262
|
return {}
|
|
273
263
|
|
|
@@ -281,14 +271,11 @@ class StandardDetector(
|
|
|
281
271
|
TriggerInfo(
|
|
282
272
|
number_of_triggers=1,
|
|
283
273
|
trigger=DetectorTrigger.INTERNAL,
|
|
284
|
-
deadtime=None,
|
|
285
|
-
livetime=None,
|
|
286
|
-
frame_timeout=None,
|
|
287
274
|
)
|
|
288
275
|
)
|
|
289
276
|
|
|
290
|
-
|
|
291
|
-
if
|
|
277
|
+
trigger_info = _ensure_trigger_info_exists(self._trigger_info)
|
|
278
|
+
if trigger_info.trigger is not DetectorTrigger.INTERNAL:
|
|
292
279
|
msg = "The trigger method can only be called with INTERNAL triggering"
|
|
293
280
|
raise ValueError(msg)
|
|
294
281
|
|
|
@@ -299,28 +286,19 @@ class StandardDetector(
|
|
|
299
286
|
end_observation = indices_written + 1
|
|
300
287
|
|
|
301
288
|
async for index in self._writer.observe_indices_written(
|
|
302
|
-
DEFAULT_TIMEOUT
|
|
303
|
-
+ (self._trigger_info.livetime or 0)
|
|
304
|
-
+ (self._trigger_info.deadtime or 0)
|
|
289
|
+
DEFAULT_TIMEOUT + (trigger_info.livetime or 0) + trigger_info.deadtime
|
|
305
290
|
):
|
|
306
291
|
if index >= end_observation:
|
|
307
292
|
break
|
|
308
293
|
|
|
309
294
|
@AsyncStatus.wrap
|
|
310
295
|
async def prepare(self, value: TriggerInfo) -> None:
|
|
311
|
-
"""
|
|
312
|
-
Arm detector.
|
|
296
|
+
"""Arm detector.
|
|
313
297
|
|
|
314
298
|
Prepare the detector with trigger information. This is determined at and passed
|
|
315
299
|
in from the plan level.
|
|
316
300
|
|
|
317
|
-
|
|
318
|
-
trigger information determined in trigger.
|
|
319
|
-
|
|
320
|
-
To do: Unify prepare to be use for both fly and step scans.
|
|
321
|
-
|
|
322
|
-
Args:
|
|
323
|
-
value: TriggerInfo describing how to trigger the detector
|
|
301
|
+
:param value: TriggerInfo describing how to trigger the detector
|
|
324
302
|
"""
|
|
325
303
|
if value.trigger != DetectorTrigger.INTERNAL and not value.deadtime:
|
|
326
304
|
msg = "Deadtime must be supplied when in externally triggered mode"
|
|
@@ -332,25 +310,28 @@ class StandardDetector(
|
|
|
332
310
|
f"deadtime, but trigger logic provides only {value.deadtime}s"
|
|
333
311
|
)
|
|
334
312
|
raise ValueError(msg)
|
|
335
|
-
|
|
336
|
-
|
|
313
|
+
elif not value.deadtime:
|
|
314
|
+
value.deadtime = self._controller.get_deadtime(value.livetime)
|
|
337
315
|
self._number_of_triggers_iter = iter(
|
|
338
|
-
|
|
339
|
-
if isinstance(
|
|
340
|
-
else [
|
|
316
|
+
value.number_of_triggers
|
|
317
|
+
if isinstance(value.number_of_triggers, list)
|
|
318
|
+
else [value.number_of_triggers]
|
|
341
319
|
)
|
|
342
|
-
self._initial_frame = await self._writer.get_indices_written()
|
|
343
320
|
self._describe, _ = await asyncio.gather(
|
|
344
321
|
self._writer.open(value.multiplier), self._controller.prepare(value)
|
|
345
322
|
)
|
|
323
|
+
self._initial_frame = await self._writer.get_indices_written()
|
|
346
324
|
if value.trigger != DetectorTrigger.INTERNAL:
|
|
347
325
|
await self._controller.arm()
|
|
348
|
-
|
|
326
|
+
self._trigger_info = value
|
|
349
327
|
|
|
350
328
|
@AsyncStatus.wrap
|
|
351
329
|
async def kickoff(self):
|
|
352
330
|
if self._trigger_info is None or self._number_of_triggers_iter is None:
|
|
353
331
|
raise RuntimeError("Prepare must be called before kickoff!")
|
|
332
|
+
if self._trigger_info.trigger == DetectorTrigger.INTERNAL:
|
|
333
|
+
await self._controller.arm()
|
|
334
|
+
self._fly_start = time.monotonic()
|
|
354
335
|
try:
|
|
355
336
|
self._frames_to_complete = next(self._number_of_triggers_iter)
|
|
356
337
|
self._completable_frames += self._frames_to_complete
|
|
@@ -362,13 +343,13 @@ class StandardDetector(
|
|
|
362
343
|
|
|
363
344
|
@WatchableAsyncStatus.wrap
|
|
364
345
|
async def complete(self):
|
|
365
|
-
|
|
346
|
+
trigger_info = _ensure_trigger_info_exists(self._trigger_info)
|
|
366
347
|
indices_written = self._writer.observe_indices_written(
|
|
367
|
-
|
|
348
|
+
trigger_info.frame_timeout
|
|
368
349
|
or (
|
|
369
350
|
DEFAULT_TIMEOUT
|
|
370
|
-
+ (
|
|
371
|
-
+ (
|
|
351
|
+
+ (trigger_info.livetime or 0)
|
|
352
|
+
+ (trigger_info.deadtime or 0)
|
|
372
353
|
)
|
|
373
354
|
)
|
|
374
355
|
try:
|
|
@@ -388,7 +369,7 @@ class StandardDetector(
|
|
|
388
369
|
break
|
|
389
370
|
finally:
|
|
390
371
|
await indices_written.aclose()
|
|
391
|
-
if self._completable_frames >=
|
|
372
|
+
if self._completable_frames >= trigger_info.total_number_of_triggers:
|
|
392
373
|
self._completable_frames = 0
|
|
393
374
|
self._frames_to_complete = 0
|
|
394
375
|
self._number_of_triggers_iter = None
|
ophyd_async/core/_device.py
CHANGED
|
@@ -17,7 +17,7 @@ class DeviceConnector:
|
|
|
17
17
|
"""Defines how a `Device` should be connected and type hints processed."""
|
|
18
18
|
|
|
19
19
|
def create_children_from_annotations(self, device: Device):
|
|
20
|
-
"""
|
|
20
|
+
"""Use when children can be created from introspecting the hardware.
|
|
21
21
|
|
|
22
22
|
Some control systems allow introspection of a device to determine what
|
|
23
23
|
children it has. To allow this to work nicely with typing we add these
|
|
@@ -26,15 +26,20 @@ class DeviceConnector:
|
|
|
26
26
|
my_signal: SignalRW[int]
|
|
27
27
|
my_device: MyDevice
|
|
28
28
|
|
|
29
|
-
This method will be run during
|
|
29
|
+
This method will be run during `Device.__init__`, and is responsible
|
|
30
30
|
for turning all of those type hints into real Signal and Device instances.
|
|
31
31
|
|
|
32
32
|
Subsequent runs of this function should do nothing, to allow it to be
|
|
33
33
|
called early in Devices that need to pass references to their children
|
|
34
|
-
during
|
|
34
|
+
during `__init__`.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
37
|
async def connect_mock(self, device: Device, mock: LazyMock):
|
|
38
|
+
"""Use during [](#Device.connect) with `mock=True`.
|
|
39
|
+
|
|
40
|
+
This is called when there is no cached connect done in `mock=True`
|
|
41
|
+
mode. It connects the Device and all its children in mock mode.
|
|
42
|
+
"""
|
|
38
43
|
# Connect serially, no errors to gather up as in mock mode
|
|
39
44
|
exceptions: dict[str, Exception] = {}
|
|
40
45
|
for name, child_device in device.children():
|
|
@@ -46,11 +51,10 @@ class DeviceConnector:
|
|
|
46
51
|
raise NotConnected.with_other_exceptions_logged(exceptions)
|
|
47
52
|
|
|
48
53
|
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
|
|
49
|
-
"""
|
|
54
|
+
"""Use during [](#Device.connect) with `mock=False`.
|
|
50
55
|
|
|
51
|
-
This is called when
|
|
52
|
-
|
|
53
|
-
children.
|
|
56
|
+
This is called when there is no cached connect done in `mock=False`
|
|
57
|
+
mode. It connects the Device and all its children in real mode in parallel.
|
|
54
58
|
"""
|
|
55
59
|
# Connect in parallel, gathering up NotConnected errors
|
|
56
60
|
coros = {
|
|
@@ -61,11 +65,15 @@ class DeviceConnector:
|
|
|
61
65
|
|
|
62
66
|
|
|
63
67
|
class Device(HasName):
|
|
64
|
-
"""Common base class for all Ophyd Async Devices.
|
|
68
|
+
"""Common base class for all Ophyd Async Devices.
|
|
69
|
+
|
|
70
|
+
:param name: Optional name of the Device
|
|
71
|
+
:param connector: Optional DeviceConnector instance to use at connect()
|
|
72
|
+
"""
|
|
65
73
|
|
|
66
|
-
_name: str = ""
|
|
67
|
-
#: The parent Device if it exists
|
|
68
74
|
parent: Device | None = None
|
|
75
|
+
"""The parent Device if it exists"""
|
|
76
|
+
_name: str = ""
|
|
69
77
|
# None if connect hasn't started, a Task if it has
|
|
70
78
|
_connect_task: asyncio.Task | None = None
|
|
71
79
|
# The mock if we have connected in mock mode
|
|
@@ -83,7 +91,7 @@ class Device(HasName):
|
|
|
83
91
|
|
|
84
92
|
@property
|
|
85
93
|
def name(self) -> str:
|
|
86
|
-
"""Return the name of the Device"""
|
|
94
|
+
"""Return the name of the Device."""
|
|
87
95
|
return self._name
|
|
88
96
|
|
|
89
97
|
@cached_property
|
|
@@ -91,24 +99,26 @@ class Device(HasName):
|
|
|
91
99
|
return {}
|
|
92
100
|
|
|
93
101
|
def children(self) -> Iterator[tuple[str, Device]]:
|
|
102
|
+
"""For each attribute that is a Device, yield the name and Device.
|
|
103
|
+
|
|
104
|
+
:yields: `(attr_name, attr)` for each child attribute that is a Device.
|
|
105
|
+
"""
|
|
94
106
|
yield from self._child_devices.items()
|
|
95
107
|
|
|
96
108
|
@cached_property
|
|
97
109
|
def log(self) -> LoggerAdapter:
|
|
110
|
+
"""Return a logger configured with the device name."""
|
|
98
111
|
return LoggerAdapter(
|
|
99
112
|
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
100
113
|
)
|
|
101
114
|
|
|
102
115
|
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
103
|
-
"""Set
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
child_name_separator:
|
|
110
|
-
Use this as a separator instead of "-". Use "_" instead to make the same
|
|
111
|
-
names as the equivalent ophyd sync device.
|
|
116
|
+
"""Set `self.name=name` and each `self.child.name=name+"-child"`.
|
|
117
|
+
|
|
118
|
+
:param name: New name to set.
|
|
119
|
+
:param child_name_separator:
|
|
120
|
+
Use this as a separator instead of "-". Use "_" instead to make the
|
|
121
|
+
same names as the equivalent ophyd sync device.
|
|
112
122
|
"""
|
|
113
123
|
self._name = name
|
|
114
124
|
if child_name_separator:
|
|
@@ -147,16 +157,19 @@ class Device(HasName):
|
|
|
147
157
|
timeout: float = DEFAULT_TIMEOUT,
|
|
148
158
|
force_reconnect: bool = False,
|
|
149
159
|
) -> None:
|
|
150
|
-
"""Connect
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
mock:
|
|
157
|
-
If True then use
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
"""Connect the device and all child devices.
|
|
161
|
+
|
|
162
|
+
Successful connects will be cached so subsequent calls will return
|
|
163
|
+
immediately. Contains a timeout that gets propagated to child.connect
|
|
164
|
+
methods.
|
|
165
|
+
|
|
166
|
+
:param mock:
|
|
167
|
+
If True then use [](#MockSignalBackend) for all Signals. If passed a
|
|
168
|
+
[](#LazyMock) then pass this down for use within the Signals,
|
|
169
|
+
otherwise create one.
|
|
170
|
+
:param timeout: Time to wait before failing with a TimeoutError.
|
|
171
|
+
:param force_reconnect:
|
|
172
|
+
If True, force a reconnect even if the last connect succeeded.
|
|
160
173
|
"""
|
|
161
174
|
if not hasattr(self, "_connector"):
|
|
162
175
|
msg = (
|
|
@@ -205,12 +218,9 @@ DeviceT = TypeVar("DeviceT", bound=Device)
|
|
|
205
218
|
|
|
206
219
|
|
|
207
220
|
class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
208
|
-
"""
|
|
209
|
-
Defines device components with indices.
|
|
221
|
+
"""Defines a dictionary of Device children with arbitrary integer keys.
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
|
|
213
|
-
:class:`~ophyd_async.epics.sim.DynamicSensorGroup`
|
|
223
|
+
:see-also: [](#implementing-devices) for examples of how to use this class.
|
|
214
224
|
"""
|
|
215
225
|
|
|
216
226
|
def __init__(
|
|
@@ -274,7 +284,7 @@ class DeviceProcessor:
|
|
|
274
284
|
self._locals_on_exit: dict[str, Any] = {}
|
|
275
285
|
|
|
276
286
|
def _caller_locals(self) -> dict[str, Any]:
|
|
277
|
-
"""Walk up until we find a stack frame that doesn't have us as self"""
|
|
287
|
+
"""Walk up until we find a stack frame that doesn't have us as self."""
|
|
278
288
|
try:
|
|
279
289
|
raise ValueError
|
|
280
290
|
except ValueError:
|
|
@@ -341,33 +351,29 @@ def init_devices(
|
|
|
341
351
|
connect=True,
|
|
342
352
|
mock=False,
|
|
343
353
|
timeout: float = 10.0,
|
|
344
|
-
)
|
|
345
|
-
"""Auto
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
connect
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
368
|
-
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
369
|
-
# Names and connects devices here
|
|
370
|
-
assert t1x.name == "t1x"
|
|
354
|
+
):
|
|
355
|
+
"""Auto initialize top level Device instances: to be used as a context manager.
|
|
356
|
+
|
|
357
|
+
:param set_name:
|
|
358
|
+
If True, call `device.set_name(variable_name)` on all Devices created
|
|
359
|
+
within the context manager that have an empty `name`.
|
|
360
|
+
:param child_name_separator: Separator for child names if `set_name` is True.
|
|
361
|
+
:param connect:
|
|
362
|
+
If True, call `device.connect(mock, timeout)` in parallel on all Devices
|
|
363
|
+
created within the context manager.
|
|
364
|
+
:param mock: If True, connect Signals in mock mode.
|
|
365
|
+
:param timeout: How long to wait for connect before logging an exception.
|
|
366
|
+
:raises RuntimeError: If used inside a plan, use [](#ensure_connected) instead.
|
|
367
|
+
:raises NotConnected: If devices could not be connected.
|
|
368
|
+
|
|
369
|
+
For example, to connect and name 2 motors in parallel:
|
|
370
|
+
```python
|
|
371
|
+
[async] with init_devices():
|
|
372
|
+
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
373
|
+
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
374
|
+
# Names and connects devices here
|
|
375
|
+
assert t1x.name == "t1x"
|
|
376
|
+
```
|
|
371
377
|
"""
|
|
372
378
|
|
|
373
379
|
async def process_devices(devices: dict[str, Device]):
|