ophyd-async 0.2.0__py3-none-any.whl → 0.3a2__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 +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +16 -10
- ophyd_async/core/_providers.py +38 -5
- ophyd_async/core/async_status.py +3 -3
- ophyd_async/core/detector.py +211 -62
- ophyd_async/core/device.py +45 -38
- ophyd_async/core/device_save_loader.py +96 -23
- ophyd_async/core/flyer.py +30 -244
- ophyd_async/core/signal.py +47 -21
- ophyd_async/core/signal_backend.py +7 -4
- ophyd_async/core/sim_signal_backend.py +30 -18
- ophyd_async/core/standard_readable.py +4 -2
- ophyd_async/core/utils.py +93 -30
- ophyd_async/epics/_backend/_aioca.py +30 -36
- ophyd_async/epics/_backend/_p4p.py +75 -41
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +4 -0
- ophyd_async/epics/areadetector/aravis.py +69 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +37 -25
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
- ophyd_async/epics/areadetector/pilatus.py +50 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +21 -7
- ophyd_async/epics/areadetector/writers/hdf_writer.py +26 -15
- ophyd_async/epics/demo/__init__.py +33 -3
- ophyd_async/epics/motion/motor.py +20 -14
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +0 -2
- ophyd_async/epics/signal/signal.py +26 -9
- ophyd_async/panda/__init__.py +19 -5
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/_panda_controller.py +37 -0
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/_utils.py +15 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/planstubs/__init__.py +5 -0
- ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
- ophyd_async/protocols.py +73 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +116 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +20 -76
- ophyd_async-0.3a2.dist-info/RECORD +76 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async/panda/panda.py +0 -294
- ophyd_async-0.2.0.dist-info/RECORD +0 -53
- /ophyd_async/panda/{table.py → _table.py} +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/top_level.txt +0 -0
ophyd_async/__init__.py
CHANGED
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -6,22 +6,24 @@ from ._providers import (
|
|
|
6
6
|
StaticDirectoryProvider,
|
|
7
7
|
)
|
|
8
8
|
from .async_status import AsyncStatus
|
|
9
|
-
from .detector import
|
|
9
|
+
from .detector import (
|
|
10
|
+
DetectorControl,
|
|
11
|
+
DetectorTrigger,
|
|
12
|
+
DetectorWriter,
|
|
13
|
+
StandardDetector,
|
|
14
|
+
TriggerInfo,
|
|
15
|
+
)
|
|
10
16
|
from .device import Device, DeviceCollector, DeviceVector
|
|
11
17
|
from .device_save_loader import (
|
|
12
18
|
get_signal_values,
|
|
19
|
+
load_device,
|
|
13
20
|
load_from_yaml,
|
|
21
|
+
save_device,
|
|
14
22
|
save_to_yaml,
|
|
15
23
|
set_signal_values,
|
|
16
24
|
walk_rw_signals,
|
|
17
25
|
)
|
|
18
|
-
from .flyer import
|
|
19
|
-
DetectorGroupLogic,
|
|
20
|
-
HardwareTriggeredFlyable,
|
|
21
|
-
SameTriggerDetectorGroupLogic,
|
|
22
|
-
TriggerInfo,
|
|
23
|
-
TriggerLogic,
|
|
24
|
-
)
|
|
26
|
+
from .flyer import HardwareTriggeredFlyable, TriggerLogic
|
|
25
27
|
from .signal import (
|
|
26
28
|
Signal,
|
|
27
29
|
SignalR,
|
|
@@ -33,6 +35,8 @@ from .signal import (
|
|
|
33
35
|
set_sim_callback,
|
|
34
36
|
set_sim_put_proceeds,
|
|
35
37
|
set_sim_value,
|
|
38
|
+
soft_signal_r_and_backend,
|
|
39
|
+
soft_signal_rw,
|
|
36
40
|
wait_for_value,
|
|
37
41
|
)
|
|
38
42
|
from .signal_backend import SignalBackend
|
|
@@ -65,6 +69,8 @@ __all__ = [
|
|
|
65
69
|
"SignalW",
|
|
66
70
|
"SignalRW",
|
|
67
71
|
"SignalX",
|
|
72
|
+
"soft_signal_r_and_backend",
|
|
73
|
+
"soft_signal_rw",
|
|
68
74
|
"observe_value",
|
|
69
75
|
"set_and_wait_for_value",
|
|
70
76
|
"set_sim_callback",
|
|
@@ -79,8 +85,6 @@ __all__ = [
|
|
|
79
85
|
"StaticDirectoryProvider",
|
|
80
86
|
"StandardReadable",
|
|
81
87
|
"TriggerInfo",
|
|
82
|
-
"DetectorGroupLogic",
|
|
83
|
-
"SameTriggerDetectorGroupLogic",
|
|
84
88
|
"TriggerLogic",
|
|
85
89
|
"HardwareTriggeredFlyable",
|
|
86
90
|
"DEFAULT_TIMEOUT",
|
|
@@ -97,4 +101,6 @@ __all__ = [
|
|
|
97
101
|
"save_to_yaml",
|
|
98
102
|
"set_signal_values",
|
|
99
103
|
"walk_rw_signals",
|
|
104
|
+
"load_device",
|
|
105
|
+
"save_device",
|
|
100
106
|
]
|
ophyd_async/core/_providers.py
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Protocol, Sequence, Union
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
@dataclass
|
|
7
8
|
class DirectoryInfo:
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
"""
|
|
10
|
+
Information about where and how to write a file.
|
|
11
|
+
|
|
12
|
+
The bluesky event model splits the URI for a resource into two segments to aid in
|
|
13
|
+
different applications mounting filesystems at different mount points.
|
|
14
|
+
The portion of this path which is relevant only for the writer is the 'root',
|
|
15
|
+
while the path from an agreed upon mutual mounting is the resource_path.
|
|
16
|
+
The resource_dir is used with the filename to construct the resource_path.
|
|
17
|
+
|
|
18
|
+
:param root: Path of a root directory, relevant only for the file writer
|
|
19
|
+
:param resource_dir: Directory into which files should be written, relative to root
|
|
20
|
+
:param prefix: Optional filename prefix to add to all files
|
|
21
|
+
:param suffix: Optional filename suffix to add to all files
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
root: Path
|
|
25
|
+
resource_dir: Path
|
|
26
|
+
prefix: Optional[str] = ""
|
|
27
|
+
suffix: Optional[str] = ""
|
|
10
28
|
|
|
11
29
|
|
|
12
30
|
class DirectoryProvider(Protocol):
|
|
@@ -16,8 +34,23 @@ class DirectoryProvider(Protocol):
|
|
|
16
34
|
|
|
17
35
|
|
|
18
36
|
class StaticDirectoryProvider(DirectoryProvider):
|
|
19
|
-
def __init__(
|
|
20
|
-
self
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
directory_path: Union[str, Path],
|
|
40
|
+
filename_prefix: str = "",
|
|
41
|
+
filename_suffix: str = "",
|
|
42
|
+
resource_dir: Optional[Path] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
if resource_dir is None:
|
|
45
|
+
resource_dir = Path(".")
|
|
46
|
+
if isinstance(directory_path, str):
|
|
47
|
+
directory_path = Path(directory_path)
|
|
48
|
+
self._directory_info = DirectoryInfo(
|
|
49
|
+
root=directory_path,
|
|
50
|
+
resource_dir=resource_dir,
|
|
51
|
+
prefix=filename_prefix,
|
|
52
|
+
suffix=filename_suffix,
|
|
53
|
+
)
|
|
21
54
|
|
|
22
55
|
def __call__(self) -> DirectoryInfo:
|
|
23
56
|
return self._directory_info
|
ophyd_async/core/async_status.py
CHANGED
|
@@ -85,12 +85,12 @@ class AsyncStatus(Status):
|
|
|
85
85
|
|
|
86
86
|
def __repr__(self) -> str:
|
|
87
87
|
if self.done:
|
|
88
|
-
if self.exception()
|
|
89
|
-
status = "errored"
|
|
88
|
+
if e := self.exception():
|
|
89
|
+
status = f"errored: {repr(e)}"
|
|
90
90
|
else:
|
|
91
91
|
status = "done"
|
|
92
92
|
else:
|
|
93
93
|
status = "pending"
|
|
94
|
-
return f"<{type(self).__name__} {status}>"
|
|
94
|
+
return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
|
|
95
95
|
|
|
96
96
|
__str__ = __repr__
|
ophyd_async/core/detector.py
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
"""Module which defines abstract classes to work with detectors"""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
4
|
+
import time
|
|
3
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
4
7
|
from enum import Enum
|
|
5
|
-
from typing import
|
|
8
|
+
from typing import (
|
|
9
|
+
AsyncGenerator,
|
|
10
|
+
AsyncIterator,
|
|
11
|
+
Callable,
|
|
12
|
+
Dict,
|
|
13
|
+
Generic,
|
|
14
|
+
List,
|
|
15
|
+
Optional,
|
|
16
|
+
Sequence,
|
|
17
|
+
TypeVar,
|
|
18
|
+
)
|
|
6
19
|
|
|
7
20
|
from bluesky.protocols import (
|
|
8
|
-
|
|
9
|
-
Configurable,
|
|
21
|
+
Collectable,
|
|
10
22
|
Descriptor,
|
|
11
|
-
|
|
23
|
+
Flyable,
|
|
24
|
+
Preparable,
|
|
12
25
|
Reading,
|
|
13
26
|
Stageable,
|
|
27
|
+
StreamAsset,
|
|
14
28
|
Triggerable,
|
|
15
|
-
|
|
29
|
+
WritesStreamAssets,
|
|
16
30
|
)
|
|
17
31
|
|
|
32
|
+
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
|
|
33
|
+
|
|
18
34
|
from .async_status import AsyncStatus
|
|
19
35
|
from .device import Device
|
|
20
36
|
from .signal import SignalR
|
|
21
37
|
from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
|
|
22
38
|
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
23
41
|
|
|
24
42
|
class DetectorTrigger(str, Enum):
|
|
43
|
+
"""Type of mechanism for triggering a detector to take frames"""
|
|
44
|
+
|
|
25
45
|
#: Detector generates internal trigger for given rate
|
|
26
46
|
internal = "internal"
|
|
27
47
|
#: Expect a series of arbitrary length trigger signals
|
|
@@ -32,7 +52,26 @@ class DetectorTrigger(str, Enum):
|
|
|
32
52
|
variable_gate = "variable_gate"
|
|
33
53
|
|
|
34
54
|
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class TriggerInfo:
|
|
57
|
+
"""Minimal set of information required to setup triggering on a detector"""
|
|
58
|
+
|
|
59
|
+
#: Number of triggers that will be sent
|
|
60
|
+
num: int
|
|
61
|
+
#: Sort of triggers that will be sent
|
|
62
|
+
trigger: DetectorTrigger
|
|
63
|
+
#: What is the minimum deadtime between triggers
|
|
64
|
+
deadtime: float
|
|
65
|
+
#: What is the maximum high time of the triggers
|
|
66
|
+
livetime: float
|
|
67
|
+
|
|
68
|
+
|
|
35
69
|
class DetectorControl(ABC):
|
|
70
|
+
"""
|
|
71
|
+
Classes implementing this interface should hold the logic for
|
|
72
|
+
arming and disarming a detector
|
|
73
|
+
"""
|
|
74
|
+
|
|
36
75
|
@abstractmethod
|
|
37
76
|
def get_deadtime(self, exposure: float) -> float:
|
|
38
77
|
"""For a given exposure, how long should the time between exposures be"""
|
|
@@ -40,21 +79,36 @@ class DetectorControl(ABC):
|
|
|
40
79
|
@abstractmethod
|
|
41
80
|
async def arm(
|
|
42
81
|
self,
|
|
82
|
+
num: int,
|
|
43
83
|
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
44
|
-
num: int = 0,
|
|
45
84
|
exposure: Optional[float] = None,
|
|
46
85
|
) -> AsyncStatus:
|
|
47
|
-
"""
|
|
86
|
+
"""
|
|
87
|
+
Arm detector, do all necessary steps to prepare detector for triggers.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
num: Expected number of frames
|
|
91
|
+
trigger: Type of trigger for which to prepare the detector. Defaults to
|
|
92
|
+
DetectorTrigger.internal.
|
|
93
|
+
exposure: Exposure time with which to set up the detector. Defaults to None
|
|
94
|
+
if not applicable or the detector is expected to use its previously-set
|
|
95
|
+
exposure time.
|
|
48
96
|
|
|
49
|
-
|
|
97
|
+
Returns:
|
|
98
|
+
AsyncStatus: Status representing the arm operation. This function returning
|
|
99
|
+
represents the start of the arm. The returned status completing means
|
|
100
|
+
the detector is now armed.
|
|
50
101
|
"""
|
|
51
102
|
|
|
52
103
|
@abstractmethod
|
|
53
104
|
async def disarm(self):
|
|
54
|
-
"""Disarm the detector"""
|
|
105
|
+
"""Disarm the detector, return detector to an idle state"""
|
|
55
106
|
|
|
56
107
|
|
|
57
108
|
class DetectorWriter(ABC):
|
|
109
|
+
"""Logic for making a detector write data to somewhere persistent
|
|
110
|
+
(e.g. an HDF5 file)"""
|
|
111
|
+
|
|
58
112
|
@abstractmethod
|
|
59
113
|
async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
|
|
60
114
|
"""Open writer and wait for it to be ready for data.
|
|
@@ -68,40 +122,39 @@ class DetectorWriter(ABC):
|
|
|
68
122
|
"""
|
|
69
123
|
|
|
70
124
|
@abstractmethod
|
|
71
|
-
|
|
72
|
-
self,
|
|
73
|
-
) -> None:
|
|
74
|
-
"""
|
|
125
|
+
def observe_indices_written(
|
|
126
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
127
|
+
) -> AsyncGenerator[int, None]:
|
|
128
|
+
"""Yield the index of each frame (or equivalent data point) as it is written"""
|
|
75
129
|
|
|
76
130
|
@abstractmethod
|
|
77
131
|
async def get_indices_written(self) -> int:
|
|
78
132
|
"""Get the number of indices written"""
|
|
79
133
|
|
|
80
134
|
@abstractmethod
|
|
81
|
-
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[
|
|
135
|
+
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
|
|
82
136
|
"""Create Stream docs up to given number written"""
|
|
83
137
|
|
|
84
138
|
@abstractmethod
|
|
85
139
|
async def close(self) -> None:
|
|
86
|
-
"""Close writer
|
|
140
|
+
"""Close writer, blocks until I/O is complete"""
|
|
87
141
|
|
|
88
142
|
|
|
89
143
|
class StandardDetector(
|
|
90
144
|
Device,
|
|
91
145
|
Stageable,
|
|
92
|
-
|
|
93
|
-
|
|
146
|
+
AsyncConfigurable,
|
|
147
|
+
AsyncReadable,
|
|
94
148
|
Triggerable,
|
|
95
|
-
|
|
149
|
+
Preparable,
|
|
150
|
+
Flyable,
|
|
151
|
+
Collectable,
|
|
152
|
+
WritesStreamAssets,
|
|
153
|
+
Generic[T],
|
|
96
154
|
):
|
|
97
|
-
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
DetectorData, to dictate how the detector will be controlled (i.e. arming and
|
|
101
|
-
disarming) as well as how the detector data will be written (i.e. opening and
|
|
102
|
-
closing the writer, and handling data writing indices).
|
|
103
|
-
|
|
104
|
-
NOTE: only for step-scans.
|
|
155
|
+
"""
|
|
156
|
+
Useful detector base class for step and fly scanning detectors.
|
|
157
|
+
Aggregates controller and writer logic together.
|
|
105
158
|
"""
|
|
106
159
|
|
|
107
160
|
def __init__(
|
|
@@ -113,20 +166,34 @@ class StandardDetector(
|
|
|
113
166
|
writer_timeout: float = DEFAULT_TIMEOUT,
|
|
114
167
|
) -> None:
|
|
115
168
|
"""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
169
|
+
Constructor
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
controller: Logic for arming and disarming the detector
|
|
173
|
+
writer: Logic for making the detector write persistent data
|
|
174
|
+
config_sigs: Signals to read when describe and read
|
|
175
|
+
configuration are called. Defaults to ().
|
|
176
|
+
name: Device name. Defaults to "".
|
|
177
|
+
writer_timeout: Timeout for frame writing to start, if the
|
|
178
|
+
timeout is reached, ophyd-async assumes the detector
|
|
179
|
+
has a problem and raises an error.
|
|
180
|
+
Defaults to DEFAULT_TIMEOUT.
|
|
124
181
|
"""
|
|
125
182
|
self._controller = controller
|
|
126
183
|
self._writer = writer
|
|
127
184
|
self._describe: Dict[str, Descriptor] = {}
|
|
128
185
|
self._config_sigs = list(config_sigs)
|
|
129
186
|
self._frame_writing_timeout = writer_timeout
|
|
187
|
+
# For prepare
|
|
188
|
+
self._arm_status: Optional[AsyncStatus] = None
|
|
189
|
+
self._trigger_info: Optional[TriggerInfo] = None
|
|
190
|
+
# For kickoff
|
|
191
|
+
self._watchers: List[Callable] = []
|
|
192
|
+
self._fly_status: Optional[AsyncStatus] = None
|
|
193
|
+
self._fly_start: float
|
|
194
|
+
|
|
195
|
+
self._intial_frame: int
|
|
196
|
+
self._last_frame: int
|
|
130
197
|
super().__init__(name)
|
|
131
198
|
|
|
132
199
|
@property
|
|
@@ -137,14 +204,20 @@ class StandardDetector(
|
|
|
137
204
|
def writer(self) -> DetectorWriter:
|
|
138
205
|
return self._writer
|
|
139
206
|
|
|
140
|
-
|
|
207
|
+
@AsyncStatus.wrap
|
|
208
|
+
async def stage(self) -> None:
|
|
209
|
+
# Disarm the detector, stop filewriting, and open file for writing.
|
|
210
|
+
await self._check_config_sigs()
|
|
211
|
+
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
212
|
+
self._describe = await self.writer.open()
|
|
213
|
+
|
|
214
|
+
async def _check_config_sigs(self):
|
|
141
215
|
"""Checks configuration signals are named and connected."""
|
|
142
216
|
for signal in self._config_sigs:
|
|
143
217
|
if signal._name == "":
|
|
144
218
|
raise Exception(
|
|
145
219
|
"config signal must be named before it is passed to the detector"
|
|
146
220
|
)
|
|
147
|
-
|
|
148
221
|
try:
|
|
149
222
|
await signal.get_value()
|
|
150
223
|
except NotImplementedError:
|
|
@@ -154,46 +227,122 @@ class StandardDetector(
|
|
|
154
227
|
)
|
|
155
228
|
|
|
156
229
|
@AsyncStatus.wrap
|
|
157
|
-
async def
|
|
158
|
-
|
|
159
|
-
await self.
|
|
160
|
-
|
|
161
|
-
|
|
230
|
+
async def unstage(self) -> None:
|
|
231
|
+
# Stop data writing.
|
|
232
|
+
await self.writer.close()
|
|
233
|
+
|
|
234
|
+
async def read_configuration(self) -> Dict[str, Reading]:
|
|
235
|
+
return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
|
|
162
236
|
|
|
163
237
|
async def describe_configuration(self) -> Dict[str, Descriptor]:
|
|
164
238
|
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
165
239
|
|
|
166
|
-
async def
|
|
167
|
-
|
|
240
|
+
async def read(self) -> Dict[str, Reading]:
|
|
241
|
+
# All data is in StreamResources, not Events, so nothing to output here
|
|
242
|
+
return {}
|
|
168
243
|
|
|
169
|
-
def describe(self) -> Dict[str, Descriptor]:
|
|
244
|
+
async def describe(self) -> Dict[str, Descriptor]:
|
|
170
245
|
return self._describe
|
|
171
246
|
|
|
172
247
|
@AsyncStatus.wrap
|
|
173
248
|
async def trigger(self) -> None:
|
|
174
|
-
|
|
249
|
+
# Arm the detector and wait for it to finish.
|
|
175
250
|
indices_written = await self.writer.get_indices_written()
|
|
176
|
-
written_status = await self.controller.arm(
|
|
251
|
+
written_status = await self.controller.arm(
|
|
252
|
+
num=1,
|
|
253
|
+
trigger=DetectorTrigger.internal,
|
|
254
|
+
)
|
|
177
255
|
await written_status
|
|
178
|
-
|
|
179
|
-
|
|
256
|
+
end_observation = indices_written + 1
|
|
257
|
+
|
|
258
|
+
async for index in self.writer.observe_indices_written(
|
|
259
|
+
self._frame_writing_timeout
|
|
260
|
+
):
|
|
261
|
+
if index >= end_observation:
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
def prepare(
|
|
265
|
+
self,
|
|
266
|
+
value: T,
|
|
267
|
+
) -> AsyncStatus:
|
|
268
|
+
# Just arm detector for the time being
|
|
269
|
+
return AsyncStatus(self._prepare(value))
|
|
270
|
+
|
|
271
|
+
async def _prepare(self, value: T) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Arm detector.
|
|
274
|
+
|
|
275
|
+
Prepare the detector with trigger information. This is determined at and passed
|
|
276
|
+
in from the plan level.
|
|
277
|
+
|
|
278
|
+
This currently only prepares detectors for flyscans and stepscans just use the
|
|
279
|
+
trigger information determined in trigger.
|
|
280
|
+
|
|
281
|
+
To do: Unify prepare to be use for both fly and step scans.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
value: TriggerInfo describing how to trigger the detector
|
|
285
|
+
"""
|
|
286
|
+
assert type(value) is TriggerInfo
|
|
287
|
+
self._trigger_info = value
|
|
288
|
+
self._initial_frame = await self.writer.get_indices_written()
|
|
289
|
+
self._last_frame = self._initial_frame + self._trigger_info.num
|
|
290
|
+
|
|
291
|
+
required = self.controller.get_deadtime(self._trigger_info.livetime)
|
|
292
|
+
assert required <= self._trigger_info.deadtime, (
|
|
293
|
+
f"Detector {self.controller} needs at least {required}s deadtime, "
|
|
294
|
+
f"but trigger logic provides only {self._trigger_info.deadtime}s"
|
|
180
295
|
)
|
|
181
296
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
297
|
+
self._arm_status = await self.controller.arm(
|
|
298
|
+
num=self._trigger_info.num,
|
|
299
|
+
trigger=self._trigger_info.trigger,
|
|
300
|
+
exposure=self._trigger_info.livetime,
|
|
301
|
+
)
|
|
186
302
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
303
|
+
@AsyncStatus.wrap
|
|
304
|
+
async def kickoff(self) -> None:
|
|
305
|
+
self._fly_status = AsyncStatus(self._fly(), self._watchers)
|
|
306
|
+
self._fly_start = time.monotonic()
|
|
307
|
+
|
|
308
|
+
async def _fly(self) -> None:
|
|
309
|
+
await self._observe_writer_indicies(self._last_frame)
|
|
310
|
+
|
|
311
|
+
async def _observe_writer_indicies(self, end_observation: int):
|
|
312
|
+
async for index in self.writer.observe_indices_written(
|
|
313
|
+
self._frame_writing_timeout
|
|
314
|
+
):
|
|
315
|
+
for watcher in self._watchers:
|
|
316
|
+
watcher(
|
|
317
|
+
name=self.name,
|
|
318
|
+
current=index,
|
|
319
|
+
initial=self._initial_frame,
|
|
320
|
+
target=end_observation,
|
|
321
|
+
unit="",
|
|
322
|
+
precision=0,
|
|
323
|
+
time_elapsed=time.monotonic() - self._fly_start,
|
|
324
|
+
)
|
|
325
|
+
if index >= end_observation:
|
|
326
|
+
break
|
|
327
|
+
|
|
328
|
+
@AsyncStatus.wrap
|
|
329
|
+
async def complete(self) -> AsyncStatus:
|
|
330
|
+
assert self._fly_status, "Kickoff not run"
|
|
331
|
+
return await self._fly_status
|
|
332
|
+
|
|
333
|
+
async def describe_collect(self) -> Dict[str, Descriptor]:
|
|
334
|
+
return self._describe
|
|
190
335
|
|
|
191
|
-
|
|
336
|
+
async def collect_asset_docs(
|
|
337
|
+
self, index: Optional[int] = None
|
|
338
|
+
) -> AsyncIterator[StreamAsset]:
|
|
339
|
+
# Collect stream datum documents for all indices written.
|
|
340
|
+
# The index is optional, and provided for fly scans, however this needs to be
|
|
341
|
+
# retrieved for step scans.
|
|
342
|
+
if not index:
|
|
343
|
+
index = await self.writer.get_indices_written()
|
|
344
|
+
async for doc in self.writer.collect_stream_docs(index):
|
|
192
345
|
yield doc
|
|
193
|
-
# async for doc in self.writer.collect_stream_docs(indices_written):
|
|
194
|
-
# yield doc
|
|
195
346
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"""Stop data writing."""
|
|
199
|
-
await self.writer.close()
|
|
347
|
+
async def get_index(self) -> int:
|
|
348
|
+
return await self.writer.get_indices_written()
|
ophyd_async/core/device.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
"""Base device"""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
|
-
import asyncio
|
|
5
|
-
import logging
|
|
6
5
|
import sys
|
|
7
|
-
from
|
|
8
|
-
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Coroutine,
|
|
9
|
+
Dict,
|
|
10
|
+
Generator,
|
|
11
|
+
Iterator,
|
|
12
|
+
Optional,
|
|
13
|
+
Set,
|
|
14
|
+
Tuple,
|
|
15
|
+
TypeVar,
|
|
16
|
+
)
|
|
9
17
|
|
|
10
18
|
from bluesky.protocols import HasName
|
|
11
19
|
from bluesky.run_engine import call_in_bluesky_event_loop
|
|
12
20
|
|
|
13
|
-
from .utils import NotConnected, wait_for_connection
|
|
21
|
+
from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
class Device(HasName):
|
|
@@ -50,16 +58,21 @@ class Device(HasName):
|
|
|
50
58
|
child.set_name(child_name)
|
|
51
59
|
child.parent = self
|
|
52
60
|
|
|
53
|
-
async def connect(self, sim: bool = False):
|
|
61
|
+
async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
|
|
54
62
|
"""Connect self and all child Devices.
|
|
55
63
|
|
|
64
|
+
Contains a timeout that gets propagated to child.connect methods.
|
|
65
|
+
|
|
56
66
|
Parameters
|
|
57
67
|
----------
|
|
58
68
|
sim:
|
|
59
69
|
If True then connect in simulation mode.
|
|
70
|
+
timeout:
|
|
71
|
+
Time to wait before failing with a TimeoutError.
|
|
60
72
|
"""
|
|
61
73
|
coros = {
|
|
62
|
-
name: child_device.connect(sim
|
|
74
|
+
name: child_device.connect(sim, timeout=timeout)
|
|
75
|
+
for name, child_device in self.children()
|
|
63
76
|
}
|
|
64
77
|
if coros:
|
|
65
78
|
await wait_for_connection(**coros)
|
|
@@ -69,6 +82,14 @@ VT = TypeVar("VT", bound=Device)
|
|
|
69
82
|
|
|
70
83
|
|
|
71
84
|
class DeviceVector(Dict[int, VT], Device):
|
|
85
|
+
"""
|
|
86
|
+
Defines device components with indices.
|
|
87
|
+
|
|
88
|
+
In the below example, foos becomes a dictionary on the parent device
|
|
89
|
+
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
|
|
90
|
+
:class:`~ophyd_async.epics.demo.DynamicSensorGroup`
|
|
91
|
+
"""
|
|
92
|
+
|
|
72
93
|
def children(self) -> Generator[Tuple[str, Device], None, None]:
|
|
73
94
|
for attr_name, attr in self.items():
|
|
74
95
|
if isinstance(attr, Device):
|
|
@@ -140,41 +161,19 @@ class DeviceCollector:
|
|
|
140
161
|
|
|
141
162
|
async def _on_exit(self) -> None:
|
|
142
163
|
# Name and kick off connect for devices
|
|
143
|
-
|
|
164
|
+
connect_coroutines: Dict[str, Coroutine] = {}
|
|
144
165
|
for name, obj in self._objects_on_exit.items():
|
|
145
166
|
if name not in self._names_on_enter and isinstance(obj, Device):
|
|
146
167
|
if self._set_name and not obj.name:
|
|
147
168
|
obj.set_name(name)
|
|
148
169
|
if self._connect:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
done, pending = await asyncio.wait(tasks, timeout=self._timeout)
|
|
157
|
-
if pending:
|
|
158
|
-
msg = f"{len(pending)} Devices did not connect:"
|
|
159
|
-
for t in pending:
|
|
160
|
-
t.cancel()
|
|
161
|
-
with suppress(Exception):
|
|
162
|
-
await t
|
|
163
|
-
e = t.exception()
|
|
164
|
-
msg += f"\n {tasks[t]}: {type(e).__name__}"
|
|
165
|
-
lines = str(e).splitlines()
|
|
166
|
-
if len(lines) <= 1:
|
|
167
|
-
msg += f": {e}"
|
|
168
|
-
else:
|
|
169
|
-
msg += "".join(f"\n {line}" for line in lines)
|
|
170
|
-
logging.error(msg)
|
|
171
|
-
raised = [t for t in done if t.exception()]
|
|
172
|
-
if raised:
|
|
173
|
-
logging.error(f"{len(raised)} Devices raised an error:")
|
|
174
|
-
for t in raised:
|
|
175
|
-
logging.exception(f" {tasks[t]}:", exc_info=t.exception())
|
|
176
|
-
if pending or raised:
|
|
177
|
-
raise NotConnected("Not all Devices connected")
|
|
170
|
+
connect_coroutines[name] = obj.connect(
|
|
171
|
+
self._sim, timeout=self._timeout
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Connect to all the devices
|
|
175
|
+
if connect_coroutines:
|
|
176
|
+
await wait_for_connection(**connect_coroutines)
|
|
178
177
|
|
|
179
178
|
async def __aexit__(self, type, value, traceback):
|
|
180
179
|
self._objects_on_exit = self._caller_locals()
|
|
@@ -182,4 +181,12 @@ class DeviceCollector:
|
|
|
182
181
|
|
|
183
182
|
def __exit__(self, type_, value, traceback):
|
|
184
183
|
self._objects_on_exit = self._caller_locals()
|
|
185
|
-
|
|
184
|
+
try:
|
|
185
|
+
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
186
|
+
except RuntimeError:
|
|
187
|
+
raise NotConnected(
|
|
188
|
+
"Could not connect devices. Is the bluesky event loop running? See "
|
|
189
|
+
"https://blueskyproject.io/ophyd-async/main/"
|
|
190
|
+
"user/explanations/event-loop-choice.html for more info."
|
|
191
|
+
)
|
|
192
|
+
return fut
|