ophyd-async 0.5.2__py3-none-any.whl → 0.7.0a1__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 +10 -1
- ophyd_async/__main__.py +12 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +15 -7
- ophyd_async/core/_detector.py +133 -87
- ophyd_async/core/_device.py +13 -15
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -19
- ophyd_async/core/_hdf_dataset.py +8 -9
- ophyd_async/core/_log.py +3 -1
- ophyd_async/core/_mock_signal_backend.py +11 -9
- ophyd_async/core/_mock_signal_utils.py +8 -5
- ophyd_async/core/_protocol.py +7 -7
- ophyd_async/core/_providers.py +11 -11
- ophyd_async/core/_readable.py +30 -22
- ophyd_async/core/_signal.py +52 -51
- ophyd_async/core/_signal_backend.py +20 -7
- ophyd_async/core/_soft_signal_backend.py +62 -32
- ophyd_async/core/_status.py +7 -9
- ophyd_async/core/_table.py +146 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -19
- ophyd_async/epics/adaravis/_aravis_io.py +2 -1
- ophyd_async/epics/adcore/_core_io.py +2 -0
- ophyd_async/epics/adcore/_core_logic.py +4 -5
- ophyd_async/epics/adcore/_hdf_writer.py +19 -8
- ophyd_async/epics/adcore/_single_trigger.py +1 -1
- ophyd_async/epics/adcore/_utils.py +5 -6
- ophyd_async/epics/adkinetix/_kinetix_controller.py +20 -15
- ophyd_async/epics/adpilatus/_pilatus_controller.py +22 -18
- ophyd_async/epics/adsimdetector/_sim.py +7 -6
- ophyd_async/epics/adsimdetector/_sim_controller.py +22 -17
- ophyd_async/epics/advimba/_vimba_controller.py +22 -17
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/sensor.db +0 -1
- ophyd_async/epics/eiger/_eiger.py +1 -1
- ophyd_async/epics/eiger/_eiger_controller.py +18 -18
- ophyd_async/epics/eiger/_odin_io.py +6 -5
- ophyd_async/epics/motor.py +8 -10
- ophyd_async/epics/pvi/_pvi.py +30 -33
- ophyd_async/epics/signal/_aioca.py +55 -25
- ophyd_async/epics/signal/_common.py +3 -10
- ophyd_async/epics/signal/_epics_transport.py +11 -8
- ophyd_async/epics/signal/_p4p.py +79 -30
- ophyd_async/epics/signal/_signal.py +6 -8
- ophyd_async/fastcs/panda/__init__.py +0 -6
- ophyd_async/fastcs/panda/_control.py +16 -17
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +77 -138
- ophyd_async/fastcs/panda/_trigger.py +4 -5
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +28 -13
- ophyd_async/plan_stubs/_fly.py +15 -17
- ophyd_async/plan_stubs/_nd_attributes.py +12 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +27 -21
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
- ophyd_async/sim/demo/_sim_motor.py +2 -1
- ophyd_async/tango/__init__.py +45 -0
- ophyd_async/tango/base_devices/__init__.py +4 -0
- ophyd_async/tango/base_devices/_base_device.py +225 -0
- ophyd_async/tango/base_devices/_tango_readable.py +33 -0
- ophyd_async/tango/demo/__init__.py +12 -0
- ophyd_async/tango/demo/_counter.py +37 -0
- ophyd_async/tango/demo/_detector.py +42 -0
- ophyd_async/tango/demo/_mover.py +77 -0
- ophyd_async/tango/demo/_tango/__init__.py +3 -0
- ophyd_async/tango/demo/_tango/_servers.py +108 -0
- ophyd_async/tango/signal/__init__.py +39 -0
- ophyd_async/tango/signal/_signal.py +223 -0
- ophyd_async/tango/signal/_tango_transport.py +764 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/METADATA +50 -45
- ophyd_async-0.7.0a1.dist-info/RECORD +108 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/WHEEL +1 -1
- ophyd_async-0.5.2.dist-info/RECORD +0 -95
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/top_level.txt +0 -0
ophyd_async/__init__.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
"""Top level API.
|
|
2
|
+
|
|
3
|
+
.. data:: __version__
|
|
4
|
+
:type: str
|
|
5
|
+
|
|
6
|
+
Version number as calculated by https://github.com/pypa/setuptools_scm
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from . import core
|
|
1
10
|
from ._version import __version__
|
|
2
11
|
|
|
3
|
-
__all__ = ["__version__"]
|
|
12
|
+
__all__ = ["__version__", "core"]
|
ophyd_async/__main__.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
"""Interface for ``python -m ophyd_async``."""
|
|
2
|
+
|
|
1
3
|
from argparse import ArgumentParser
|
|
4
|
+
from collections.abc import Sequence
|
|
2
5
|
|
|
3
6
|
from . import __version__
|
|
4
7
|
|
|
5
8
|
__all__ = ["main"]
|
|
6
9
|
|
|
7
10
|
|
|
8
|
-
def main(args=None):
|
|
11
|
+
def main(args: Sequence[str] | None = None) -> None:
|
|
12
|
+
"""Argument parser for the CLI."""
|
|
9
13
|
parser = ArgumentParser()
|
|
10
|
-
parser.add_argument(
|
|
11
|
-
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"-v",
|
|
16
|
+
"--version",
|
|
17
|
+
action="version",
|
|
18
|
+
version=__version__,
|
|
19
|
+
)
|
|
20
|
+
parser.parse_args(args)
|
|
12
21
|
|
|
13
22
|
|
|
14
|
-
# test with: python -m ophyd_async
|
|
15
23
|
if __name__ == "__main__":
|
|
16
24
|
main()
|
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from ._detector import (
|
|
2
|
-
|
|
2
|
+
DetectorController,
|
|
3
3
|
DetectorTrigger,
|
|
4
4
|
DetectorWriter,
|
|
5
5
|
StandardDetector,
|
|
@@ -16,7 +16,7 @@ from ._device_save_loader import (
|
|
|
16
16
|
set_signal_values,
|
|
17
17
|
walk_rw_signals,
|
|
18
18
|
)
|
|
19
|
-
from ._flyer import
|
|
19
|
+
from ._flyer import FlyerController, StandardFlyer
|
|
20
20
|
from ._hdf_dataset import HDFDataset, HDFFile
|
|
21
21
|
from ._log import config_ophyd_async_logging
|
|
22
22
|
from ._mock_signal_backend import MockSignalBackend
|
|
@@ -61,13 +61,18 @@ from ._signal import (
|
|
|
61
61
|
soft_signal_rw,
|
|
62
62
|
wait_for_value,
|
|
63
63
|
)
|
|
64
|
-
from ._signal_backend import
|
|
64
|
+
from ._signal_backend import (
|
|
65
|
+
RuntimeSubsetEnum,
|
|
66
|
+
SignalBackend,
|
|
67
|
+
SubsetEnum,
|
|
68
|
+
)
|
|
65
69
|
from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
|
|
66
70
|
from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
|
|
71
|
+
from ._table import Table
|
|
67
72
|
from ._utils import (
|
|
73
|
+
CALCULATE_TIMEOUT,
|
|
68
74
|
DEFAULT_TIMEOUT,
|
|
69
75
|
CalculatableTimeout,
|
|
70
|
-
CalculateTimeout,
|
|
71
76
|
NotConnected,
|
|
72
77
|
ReadingValueCallback,
|
|
73
78
|
T,
|
|
@@ -75,11 +80,12 @@ from ._utils import (
|
|
|
75
80
|
get_dtype,
|
|
76
81
|
get_unique,
|
|
77
82
|
in_micros,
|
|
83
|
+
is_pydantic_model,
|
|
78
84
|
wait_for_connection,
|
|
79
85
|
)
|
|
80
86
|
|
|
81
87
|
__all__ = [
|
|
82
|
-
"
|
|
88
|
+
"DetectorController",
|
|
83
89
|
"DetectorTrigger",
|
|
84
90
|
"DetectorWriter",
|
|
85
91
|
"StandardDetector",
|
|
@@ -96,7 +102,7 @@ __all__ = [
|
|
|
96
102
|
"set_signal_values",
|
|
97
103
|
"walk_rw_signals",
|
|
98
104
|
"StandardFlyer",
|
|
99
|
-
"
|
|
105
|
+
"FlyerController",
|
|
100
106
|
"HDFDataset",
|
|
101
107
|
"HDFFile",
|
|
102
108
|
"config_ophyd_async_logging",
|
|
@@ -149,14 +155,16 @@ __all__ = [
|
|
|
149
155
|
"WatchableAsyncStatus",
|
|
150
156
|
"DEFAULT_TIMEOUT",
|
|
151
157
|
"CalculatableTimeout",
|
|
152
|
-
"
|
|
158
|
+
"CALCULATE_TIMEOUT",
|
|
153
159
|
"NotConnected",
|
|
154
160
|
"ReadingValueCallback",
|
|
161
|
+
"Table",
|
|
155
162
|
"T",
|
|
156
163
|
"WatcherUpdate",
|
|
157
164
|
"get_dtype",
|
|
158
165
|
"get_unique",
|
|
159
166
|
"in_micros",
|
|
167
|
+
"is_pydantic_model",
|
|
160
168
|
"wait_for_connection",
|
|
161
169
|
"completed_status",
|
|
162
170
|
]
|
ophyd_async/core/_detector.py
CHANGED
|
@@ -3,21 +3,15 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import time
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
|
|
6
7
|
from enum import Enum
|
|
8
|
+
from functools import cached_property
|
|
7
9
|
from typing import (
|
|
8
|
-
AsyncGenerator,
|
|
9
|
-
AsyncIterator,
|
|
10
|
-
Callable,
|
|
11
|
-
Dict,
|
|
12
10
|
Generic,
|
|
13
|
-
List,
|
|
14
|
-
Optional,
|
|
15
|
-
Sequence,
|
|
16
11
|
)
|
|
17
12
|
|
|
18
13
|
from bluesky.protocols import (
|
|
19
14
|
Collectable,
|
|
20
|
-
DataKey,
|
|
21
15
|
Flyable,
|
|
22
16
|
Preparable,
|
|
23
17
|
Reading,
|
|
@@ -26,10 +20,12 @@ from bluesky.protocols import (
|
|
|
26
20
|
Triggerable,
|
|
27
21
|
WritesStreamAssets,
|
|
28
22
|
)
|
|
29
|
-
from
|
|
23
|
+
from event_model import DataKey
|
|
24
|
+
from pydantic import BaseModel, Field, NonNegativeInt, computed_field
|
|
30
25
|
|
|
31
26
|
from ._device import Device
|
|
32
27
|
from ._protocol import AsyncConfigurable, AsyncReadable
|
|
28
|
+
from ._signal import SignalR
|
|
33
29
|
from ._status import AsyncStatus, WatchableAsyncStatus
|
|
34
30
|
from ._utils import DEFAULT_TIMEOUT, T, WatcherUpdate, merge_gathered_dicts
|
|
35
31
|
|
|
@@ -50,24 +46,41 @@ class DetectorTrigger(str, Enum):
|
|
|
50
46
|
class TriggerInfo(BaseModel):
|
|
51
47
|
"""Minimal set of information required to setup triggering on a detector"""
|
|
52
48
|
|
|
53
|
-
#: Number of triggers that will be sent, 0 means infinite
|
|
54
|
-
|
|
49
|
+
#: Number of triggers that will be sent, (0 means infinite) Can be:
|
|
50
|
+
# - A single integer or
|
|
51
|
+
# - A list of integers for multiple triggers
|
|
52
|
+
# Example for tomography: TriggerInfo(number=[2,3,100,3])
|
|
53
|
+
#: This would trigger:
|
|
54
|
+
#: - 2 times for dark field images
|
|
55
|
+
#: - 3 times for initial flat field images
|
|
56
|
+
#: - 100 times for projections
|
|
57
|
+
#: - 3 times for final flat field images
|
|
58
|
+
number_of_triggers: NonNegativeInt | list[NonNegativeInt]
|
|
55
59
|
#: Sort of triggers that will be sent
|
|
56
|
-
trigger: DetectorTrigger = Field()
|
|
60
|
+
trigger: DetectorTrigger = Field(default=DetectorTrigger.internal)
|
|
57
61
|
#: What is the minimum deadtime between triggers
|
|
58
|
-
deadtime: float | None = Field(ge=0)
|
|
62
|
+
deadtime: float | None = Field(default=None, ge=0)
|
|
59
63
|
#: What is the maximum high time of the triggers
|
|
60
|
-
livetime: float | None = Field(ge=0)
|
|
64
|
+
livetime: float | None = Field(default=None, ge=0)
|
|
61
65
|
#: What is the maximum timeout on waiting for a frame
|
|
62
|
-
frame_timeout: float | None = Field(None, gt=0)
|
|
66
|
+
frame_timeout: float | None = Field(default=None, gt=0)
|
|
63
67
|
#: How many triggers make up a single StreamDatum index, to allow multiple frames
|
|
64
68
|
#: from a faster detector to be zipped with a single frame from a slow detector
|
|
65
69
|
#: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
|
|
66
70
|
#: but publish 2 indices, and describe() will show a shape of (5, h, w)
|
|
67
71
|
multiplier: int = 1
|
|
68
72
|
|
|
73
|
+
@computed_field
|
|
74
|
+
@cached_property
|
|
75
|
+
def total_number_of_triggers(self) -> int:
|
|
76
|
+
return (
|
|
77
|
+
sum(self.number_of_triggers)
|
|
78
|
+
if isinstance(self.number_of_triggers, list)
|
|
79
|
+
else self.number_of_triggers
|
|
80
|
+
)
|
|
81
|
+
|
|
69
82
|
|
|
70
|
-
class
|
|
83
|
+
class DetectorController(ABC):
|
|
71
84
|
"""
|
|
72
85
|
Classes implementing this interface should hold the logic for
|
|
73
86
|
arming and disarming a detector
|
|
@@ -78,27 +91,35 @@ class DetectorControl(ABC):
|
|
|
78
91
|
"""For a given exposure, how long should the time between exposures be"""
|
|
79
92
|
|
|
80
93
|
@abstractmethod
|
|
81
|
-
async def
|
|
82
|
-
self,
|
|
83
|
-
num: int,
|
|
84
|
-
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
85
|
-
exposure: Optional[float] = None,
|
|
86
|
-
) -> AsyncStatus:
|
|
94
|
+
async def prepare(self, trigger_info: TriggerInfo):
|
|
87
95
|
"""
|
|
88
|
-
|
|
96
|
+
Do all necessary steps to prepare the detector for triggers.
|
|
89
97
|
|
|
90
98
|
Args:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
trigger_info: This is a Pydantic model which contains
|
|
100
|
+
number Expected number of frames.
|
|
101
|
+
trigger Type of trigger for which to prepare the detector. Defaults
|
|
102
|
+
to DetectorTrigger.internal.
|
|
103
|
+
livetime Livetime / Exposure time with which to set up the detector.
|
|
104
|
+
Defaults to None
|
|
105
|
+
if not applicable or the detector is expected to use its previously-set
|
|
106
|
+
exposure time.
|
|
107
|
+
deadtime Defaults to None. This is the minimum deadtime between
|
|
108
|
+
triggers.
|
|
109
|
+
multiplier The number of triggers grouped into a single StreamDatum
|
|
110
|
+
index.
|
|
111
|
+
"""
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
@abstractmethod
|
|
114
|
+
async def arm(self) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Arm the detector
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
async def wait_for_idle(self):
|
|
121
|
+
"""
|
|
122
|
+
This will wait on the internal _arm_status and wait for it to get disarmed/idle
|
|
102
123
|
"""
|
|
103
124
|
|
|
104
125
|
@abstractmethod
|
|
@@ -111,7 +132,7 @@ class DetectorWriter(ABC):
|
|
|
111
132
|
(e.g. an HDF5 file)"""
|
|
112
133
|
|
|
113
134
|
@abstractmethod
|
|
114
|
-
async def open(self, multiplier: int = 1) ->
|
|
135
|
+
async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
|
|
115
136
|
"""Open writer and wait for it to be ready for data.
|
|
116
137
|
|
|
117
138
|
Args:
|
|
@@ -160,9 +181,9 @@ class StandardDetector(
|
|
|
160
181
|
|
|
161
182
|
def __init__(
|
|
162
183
|
self,
|
|
163
|
-
controller:
|
|
184
|
+
controller: DetectorController,
|
|
164
185
|
writer: DetectorWriter,
|
|
165
|
-
config_sigs: Sequence[
|
|
186
|
+
config_sigs: Sequence[SignalR] = (),
|
|
166
187
|
name: str = "",
|
|
167
188
|
) -> None:
|
|
168
189
|
"""
|
|
@@ -177,22 +198,26 @@ class StandardDetector(
|
|
|
177
198
|
"""
|
|
178
199
|
self._controller = controller
|
|
179
200
|
self._writer = writer
|
|
180
|
-
self._describe:
|
|
201
|
+
self._describe: dict[str, DataKey] = {}
|
|
181
202
|
self._config_sigs = list(config_sigs)
|
|
182
203
|
# For prepare
|
|
183
|
-
self._arm_status:
|
|
184
|
-
self._trigger_info:
|
|
204
|
+
self._arm_status: AsyncStatus | None = None
|
|
205
|
+
self._trigger_info: TriggerInfo | None = None
|
|
185
206
|
# For kickoff
|
|
186
|
-
self._watchers:
|
|
187
|
-
self._fly_status:
|
|
188
|
-
self._fly_start: float
|
|
207
|
+
self._watchers: list[Callable] = []
|
|
208
|
+
self._fly_status: WatchableAsyncStatus | None = None
|
|
209
|
+
self._fly_start: float | None = None
|
|
210
|
+
self._frames_to_complete: int = 0
|
|
211
|
+
# Represents the total number of frames that will have been completed at the
|
|
212
|
+
# end of the next `complete`.
|
|
213
|
+
self._completable_frames: int = 0
|
|
214
|
+
self._number_of_triggers_iter: Iterator[int] | None = None
|
|
215
|
+
self._initial_frame: int = 0
|
|
189
216
|
|
|
190
|
-
self._intial_frame: int
|
|
191
|
-
self._last_frame: int
|
|
192
217
|
super().__init__(name)
|
|
193
218
|
|
|
194
219
|
@property
|
|
195
|
-
def controller(self) ->
|
|
220
|
+
def controller(self) -> DetectorController:
|
|
196
221
|
return self._controller
|
|
197
222
|
|
|
198
223
|
@property
|
|
@@ -201,7 +226,7 @@ class StandardDetector(
|
|
|
201
226
|
|
|
202
227
|
@AsyncStatus.wrap
|
|
203
228
|
async def stage(self) -> None:
|
|
204
|
-
# Disarm the detector, stop
|
|
229
|
+
# Disarm the detector, stop file writing.
|
|
205
230
|
await self._check_config_sigs()
|
|
206
231
|
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
207
232
|
self._trigger_info = None
|
|
@@ -215,28 +240,28 @@ class StandardDetector(
|
|
|
215
240
|
)
|
|
216
241
|
try:
|
|
217
242
|
await signal.get_value()
|
|
218
|
-
except NotImplementedError:
|
|
243
|
+
except NotImplementedError as e:
|
|
219
244
|
raise Exception(
|
|
220
245
|
f"config signal {signal.name} must be connected before it is "
|
|
221
246
|
+ "passed to the detector"
|
|
222
|
-
)
|
|
247
|
+
) from e
|
|
223
248
|
|
|
224
249
|
@AsyncStatus.wrap
|
|
225
250
|
async def unstage(self) -> None:
|
|
226
251
|
# Stop data writing.
|
|
227
|
-
await self.writer.close()
|
|
252
|
+
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
228
253
|
|
|
229
|
-
async def read_configuration(self) ->
|
|
254
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
230
255
|
return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
|
|
231
256
|
|
|
232
|
-
async def describe_configuration(self) ->
|
|
257
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
233
258
|
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
234
259
|
|
|
235
|
-
async def read(self) ->
|
|
260
|
+
async def read(self) -> dict[str, Reading]:
|
|
236
261
|
# All data is in StreamResources, not Events, so nothing to output here
|
|
237
262
|
return {}
|
|
238
263
|
|
|
239
|
-
async def describe(self) ->
|
|
264
|
+
async def describe(self) -> dict[str, DataKey]:
|
|
240
265
|
return self._describe
|
|
241
266
|
|
|
242
267
|
@AsyncStatus.wrap
|
|
@@ -244,19 +269,19 @@ class StandardDetector(
|
|
|
244
269
|
if self._trigger_info is None:
|
|
245
270
|
await self.prepare(
|
|
246
271
|
TriggerInfo(
|
|
247
|
-
|
|
272
|
+
number_of_triggers=1,
|
|
248
273
|
trigger=DetectorTrigger.internal,
|
|
249
274
|
deadtime=None,
|
|
250
275
|
livetime=None,
|
|
276
|
+
frame_timeout=None,
|
|
251
277
|
)
|
|
252
278
|
)
|
|
279
|
+
assert self._trigger_info
|
|
280
|
+
assert self._trigger_info.trigger is DetectorTrigger.internal
|
|
253
281
|
# Arm the detector and wait for it to finish.
|
|
254
282
|
indices_written = await self.writer.get_indices_written()
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
trigger=self._trigger_info.trigger,
|
|
258
|
-
)
|
|
259
|
-
await written_status
|
|
283
|
+
await self.controller.arm()
|
|
284
|
+
await self.controller.wait_for_idle()
|
|
260
285
|
end_observation = indices_written + 1
|
|
261
286
|
|
|
262
287
|
async for index in self.writer.observe_indices_written(
|
|
@@ -283,61 +308,82 @@ class StandardDetector(
|
|
|
283
308
|
Args:
|
|
284
309
|
value: TriggerInfo describing how to trigger the detector
|
|
285
310
|
"""
|
|
286
|
-
self._trigger_info = value
|
|
287
311
|
if value.trigger != DetectorTrigger.internal:
|
|
288
312
|
assert (
|
|
289
313
|
value.deadtime
|
|
290
314
|
), "Deadtime must be supplied when in externally triggered mode"
|
|
291
315
|
if value.deadtime:
|
|
292
|
-
required = self.controller.get_deadtime(
|
|
316
|
+
required = self.controller.get_deadtime(value.livetime)
|
|
293
317
|
assert required <= value.deadtime, (
|
|
294
318
|
f"Detector {self.controller} needs at least {required}s deadtime, "
|
|
295
319
|
f"but trigger logic provides only {value.deadtime}s"
|
|
296
320
|
)
|
|
321
|
+
self._trigger_info = value
|
|
322
|
+
self._number_of_triggers_iter = iter(
|
|
323
|
+
self._trigger_info.number_of_triggers
|
|
324
|
+
if isinstance(self._trigger_info.number_of_triggers, list)
|
|
325
|
+
else [self._trigger_info.number_of_triggers]
|
|
326
|
+
)
|
|
297
327
|
self._initial_frame = await self.writer.get_indices_written()
|
|
298
|
-
self.
|
|
299
|
-
|
|
300
|
-
num=self._trigger_info.number,
|
|
301
|
-
trigger=self._trigger_info.trigger,
|
|
302
|
-
exposure=self._trigger_info.livetime,
|
|
328
|
+
self._describe, _ = await asyncio.gather(
|
|
329
|
+
self.writer.open(value.multiplier), self.controller.prepare(value)
|
|
303
330
|
)
|
|
304
|
-
|
|
305
|
-
|
|
331
|
+
if value.trigger != DetectorTrigger.internal:
|
|
332
|
+
await self.controller.arm()
|
|
333
|
+
self._fly_start = time.monotonic()
|
|
306
334
|
|
|
307
335
|
@AsyncStatus.wrap
|
|
308
336
|
async def kickoff(self):
|
|
309
|
-
if
|
|
310
|
-
raise
|
|
337
|
+
if self._trigger_info is None or self._number_of_triggers_iter is None:
|
|
338
|
+
raise RuntimeError("Prepare must be called before kickoff!")
|
|
339
|
+
try:
|
|
340
|
+
self._frames_to_complete = next(self._number_of_triggers_iter)
|
|
341
|
+
self._completable_frames += self._frames_to_complete
|
|
342
|
+
except StopIteration as err:
|
|
343
|
+
raise RuntimeError(
|
|
344
|
+
f"Kickoff called more than the configured number of "
|
|
345
|
+
f"{self._trigger_info.total_number_of_triggers} iteration(s)!"
|
|
346
|
+
) from err
|
|
311
347
|
|
|
312
348
|
@WatchableAsyncStatus.wrap
|
|
313
349
|
async def complete(self):
|
|
314
|
-
assert self._arm_status, "Prepare not run"
|
|
315
350
|
assert self._trigger_info
|
|
316
|
-
|
|
351
|
+
indices_written = self.writer.observe_indices_written(
|
|
317
352
|
self._trigger_info.frame_timeout
|
|
318
353
|
or (
|
|
319
354
|
DEFAULT_TIMEOUT
|
|
320
355
|
+ (self._trigger_info.livetime or 0)
|
|
321
356
|
+ (self._trigger_info.deadtime or 0)
|
|
322
357
|
)
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
358
|
+
)
|
|
359
|
+
try:
|
|
360
|
+
async for index in indices_written:
|
|
361
|
+
yield WatcherUpdate(
|
|
362
|
+
name=self.name,
|
|
363
|
+
current=index,
|
|
364
|
+
initial=self._initial_frame,
|
|
365
|
+
target=self._frames_to_complete,
|
|
366
|
+
unit="",
|
|
367
|
+
precision=0,
|
|
368
|
+
time_elapsed=time.monotonic() - self._fly_start
|
|
369
|
+
if self._fly_start
|
|
370
|
+
else None,
|
|
371
|
+
)
|
|
372
|
+
if index >= self._frames_to_complete:
|
|
373
|
+
break
|
|
374
|
+
finally:
|
|
375
|
+
await indices_written.aclose()
|
|
376
|
+
if self._completable_frames >= self._trigger_info.total_number_of_triggers:
|
|
377
|
+
self._completable_frames = 0
|
|
378
|
+
self._frames_to_complete = 0
|
|
379
|
+
self._number_of_triggers_iter = None
|
|
380
|
+
await self.controller.wait_for_idle()
|
|
381
|
+
|
|
382
|
+
async def describe_collect(self) -> dict[str, DataKey]:
|
|
337
383
|
return self._describe
|
|
338
384
|
|
|
339
385
|
async def collect_asset_docs(
|
|
340
|
-
self, index:
|
|
386
|
+
self, index: int | None = None
|
|
341
387
|
) -> AsyncIterator[StreamAsset]:
|
|
342
388
|
# Collect stream datum documents for all indices written.
|
|
343
389
|
# The index is optional, and provided for fly scans, however this needs to be
|
ophyd_async/core/_device.py
CHANGED
|
@@ -2,17 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
+
from collections.abc import Coroutine, Generator, Iterator
|
|
5
6
|
from functools import cached_property
|
|
6
7
|
from logging import LoggerAdapter, getLogger
|
|
7
8
|
from typing import (
|
|
8
9
|
Any,
|
|
9
|
-
Coroutine,
|
|
10
|
-
Dict,
|
|
11
|
-
Generator,
|
|
12
|
-
Iterator,
|
|
13
10
|
Optional,
|
|
14
|
-
Set,
|
|
15
|
-
Tuple,
|
|
16
11
|
TypeVar,
|
|
17
12
|
)
|
|
18
13
|
|
|
@@ -32,7 +27,7 @@ class Device(HasName):
|
|
|
32
27
|
#: The parent Device if it exists
|
|
33
28
|
parent: Optional["Device"] = None
|
|
34
29
|
# None if connect hasn't started, a Task if it has
|
|
35
|
-
_connect_task:
|
|
30
|
+
_connect_task: asyncio.Task | None = None
|
|
36
31
|
|
|
37
32
|
# Used to check if the previous connect was mocked,
|
|
38
33
|
# if the next mock value differs then we fail
|
|
@@ -52,7 +47,7 @@ class Device(HasName):
|
|
|
52
47
|
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
53
48
|
)
|
|
54
49
|
|
|
55
|
-
def children(self) -> Iterator[
|
|
50
|
+
def children(self) -> Iterator[tuple[str, "Device"]]:
|
|
56
51
|
for attr_name, attr in self.__dict__.items():
|
|
57
52
|
if attr_name != "parent" and isinstance(attr, Device):
|
|
58
53
|
yield attr_name, attr
|
|
@@ -127,7 +122,7 @@ class Device(HasName):
|
|
|
127
122
|
VT = TypeVar("VT", bound=Device)
|
|
128
123
|
|
|
129
124
|
|
|
130
|
-
class DeviceVector(
|
|
125
|
+
class DeviceVector(dict[int, VT], Device):
|
|
131
126
|
"""
|
|
132
127
|
Defines device components with indices.
|
|
133
128
|
|
|
@@ -136,7 +131,7 @@ class DeviceVector(Dict[int, VT], Device):
|
|
|
136
131
|
:class:`~ophyd_async.epics.demo.DynamicSensorGroup`
|
|
137
132
|
"""
|
|
138
133
|
|
|
139
|
-
def children(self) -> Generator[
|
|
134
|
+
def children(self) -> Generator[tuple[str, Device], None, None]:
|
|
140
135
|
for attr_name, attr in self.items():
|
|
141
136
|
if isinstance(attr, Device):
|
|
142
137
|
yield str(attr_name), attr
|
|
@@ -182,8 +177,8 @@ class DeviceCollector:
|
|
|
182
177
|
self._connect = connect
|
|
183
178
|
self._mock = mock
|
|
184
179
|
self._timeout = timeout
|
|
185
|
-
self._names_on_enter:
|
|
186
|
-
self._objects_on_exit:
|
|
180
|
+
self._names_on_enter: set[str] = set()
|
|
181
|
+
self._objects_on_exit: dict[str, Any] = {}
|
|
187
182
|
|
|
188
183
|
def _caller_locals(self):
|
|
189
184
|
"""Walk up until we find a stack frame that doesn't have us as self"""
|
|
@@ -195,6 +190,9 @@ class DeviceCollector:
|
|
|
195
190
|
caller_frame = tb.tb_frame
|
|
196
191
|
while caller_frame.f_locals.get("self", None) is self:
|
|
197
192
|
caller_frame = caller_frame.f_back
|
|
193
|
+
assert (
|
|
194
|
+
caller_frame
|
|
195
|
+
), "No previous frame to the one with self in it, this shouldn't happen"
|
|
198
196
|
return caller_frame.f_locals
|
|
199
197
|
|
|
200
198
|
def __enter__(self) -> "DeviceCollector":
|
|
@@ -207,7 +205,7 @@ class DeviceCollector:
|
|
|
207
205
|
|
|
208
206
|
async def _on_exit(self) -> None:
|
|
209
207
|
# Name and kick off connect for devices
|
|
210
|
-
connect_coroutines:
|
|
208
|
+
connect_coroutines: dict[str, Coroutine] = {}
|
|
211
209
|
for name, obj in self._objects_on_exit.items():
|
|
212
210
|
if name not in self._names_on_enter and isinstance(obj, Device):
|
|
213
211
|
if self._set_name and not obj.name:
|
|
@@ -229,10 +227,10 @@ class DeviceCollector:
|
|
|
229
227
|
self._objects_on_exit = self._caller_locals()
|
|
230
228
|
try:
|
|
231
229
|
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
232
|
-
except RuntimeError:
|
|
230
|
+
except RuntimeError as e:
|
|
233
231
|
raise NotConnected(
|
|
234
232
|
"Could not connect devices. Is the bluesky event loop running? See "
|
|
235
233
|
"https://blueskyproject.io/ophyd-async/main/"
|
|
236
234
|
"user/explanations/event-loop-choice.html for more info."
|
|
237
|
-
)
|
|
235
|
+
) from e
|
|
238
236
|
return fut
|