ophyd-async 0.1.0__py3-none-any.whl → 0.3a1__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/_version.py +2 -2
- ophyd_async/core/__init__.py +47 -12
- ophyd_async/core/_providers.py +66 -0
- ophyd_async/core/async_status.py +7 -5
- ophyd_async/core/detector.py +321 -0
- ophyd_async/core/device.py +184 -0
- ophyd_async/core/device_save_loader.py +286 -0
- ophyd_async/core/flyer.py +94 -0
- ophyd_async/core/{_device/_signal/signal.py → signal.py} +46 -18
- ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +6 -2
- ophyd_async/core/{_device/_backend/sim_signal_backend.py → sim_signal_backend.py} +6 -2
- ophyd_async/core/{_device/standard_readable.py → standard_readable.py} +3 -3
- ophyd_async/core/utils.py +79 -29
- ophyd_async/epics/_backend/_aioca.py +38 -25
- ophyd_async/epics/_backend/_p4p.py +62 -27
- ophyd_async/epics/_backend/common.py +20 -0
- ophyd_async/epics/areadetector/__init__.py +10 -13
- ophyd_async/epics/areadetector/controllers/__init__.py +4 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +49 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +15 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +111 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +18 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +4 -4
- ophyd_async/epics/areadetector/utils.py +91 -3
- ophyd_async/epics/areadetector/writers/__init__.py +5 -0
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
- ophyd_async/epics/areadetector/writers/hdf_writer.py +133 -0
- ophyd_async/epics/areadetector/{nd_file_hdf.py → writers/nd_file_hdf.py} +22 -5
- ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
- ophyd_async/epics/demo/__init__.py +3 -2
- ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
- ophyd_async/epics/motion/motor.py +2 -1
- ophyd_async/epics/pvi.py +70 -0
- ophyd_async/epics/signal/__init__.py +0 -2
- ophyd_async/epics/signal/signal.py +1 -1
- ophyd_async/panda/__init__.py +12 -8
- ophyd_async/panda/panda.py +43 -134
- ophyd_async/panda/panda_controller.py +41 -0
- ophyd_async/panda/table.py +158 -0
- ophyd_async/panda/utils.py +15 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +49 -42
- ophyd_async-0.3a1.dist-info/RECORD +56 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device/__init__.py +0 -0
- ophyd_async/core/_device/_backend/__init__.py +0 -0
- ophyd_async/core/_device/_signal/__init__.py +0 -0
- ophyd_async/core/_device/device.py +0 -60
- ophyd_async/core/_device/device_collector.py +0 -121
- ophyd_async/core/_device/device_vector.py +0 -14
- ophyd_async/epics/areadetector/ad_driver.py +0 -18
- ophyd_async/epics/areadetector/directory_provider.py +0 -18
- ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
- ophyd_async/epics/areadetector/nd_plugin.py +0 -13
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async-0.1.0.dist-info/RECORD +0 -45
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
from .
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from ._providers import (
|
|
2
|
+
DirectoryInfo,
|
|
3
|
+
DirectoryProvider,
|
|
4
|
+
NameProvider,
|
|
5
|
+
ShapeProvider,
|
|
6
|
+
StaticDirectoryProvider,
|
|
7
|
+
)
|
|
8
|
+
from .async_status import AsyncStatus
|
|
9
|
+
from .detector import DetectorControl, DetectorTrigger, DetectorWriter, StandardDetector
|
|
10
|
+
from .device import Device, DeviceCollector, DeviceVector
|
|
11
|
+
from .device_save_loader import (
|
|
12
|
+
get_signal_values,
|
|
13
|
+
load_device,
|
|
14
|
+
load_from_yaml,
|
|
15
|
+
save_device,
|
|
16
|
+
save_to_yaml,
|
|
17
|
+
set_signal_values,
|
|
18
|
+
walk_rw_signals,
|
|
19
|
+
)
|
|
20
|
+
from .flyer import HardwareTriggeredFlyable, TriggerInfo, TriggerLogic
|
|
21
|
+
from .signal import (
|
|
4
22
|
Signal,
|
|
5
23
|
SignalR,
|
|
6
24
|
SignalRW,
|
|
@@ -13,11 +31,9 @@ from ._device._signal.signal import (
|
|
|
13
31
|
set_sim_value,
|
|
14
32
|
wait_for_value,
|
|
15
33
|
)
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
19
|
-
from ._device.standard_readable import StandardReadable
|
|
20
|
-
from .async_status import AsyncStatus
|
|
34
|
+
from .signal_backend import SignalBackend
|
|
35
|
+
from .sim_signal_backend import SimSignalBackend
|
|
36
|
+
from .standard_readable import StandardReadable
|
|
21
37
|
from .utils import (
|
|
22
38
|
DEFAULT_TIMEOUT,
|
|
23
39
|
Callback,
|
|
@@ -33,6 +49,13 @@ from .utils import (
|
|
|
33
49
|
__all__ = [
|
|
34
50
|
"SignalBackend",
|
|
35
51
|
"SimSignalBackend",
|
|
52
|
+
"DetectorControl",
|
|
53
|
+
"DetectorTrigger",
|
|
54
|
+
"DetectorWriter",
|
|
55
|
+
"StandardDetector",
|
|
56
|
+
"Device",
|
|
57
|
+
"DeviceCollector",
|
|
58
|
+
"DeviceVector",
|
|
36
59
|
"Signal",
|
|
37
60
|
"SignalR",
|
|
38
61
|
"SignalW",
|
|
@@ -44,11 +67,16 @@ __all__ = [
|
|
|
44
67
|
"set_sim_put_proceeds",
|
|
45
68
|
"set_sim_value",
|
|
46
69
|
"wait_for_value",
|
|
47
|
-
"Device",
|
|
48
|
-
"DeviceCollector",
|
|
49
|
-
"DeviceVector",
|
|
50
|
-
"StandardReadable",
|
|
51
70
|
"AsyncStatus",
|
|
71
|
+
"DirectoryInfo",
|
|
72
|
+
"DirectoryProvider",
|
|
73
|
+
"NameProvider",
|
|
74
|
+
"ShapeProvider",
|
|
75
|
+
"StaticDirectoryProvider",
|
|
76
|
+
"StandardReadable",
|
|
77
|
+
"TriggerInfo",
|
|
78
|
+
"TriggerLogic",
|
|
79
|
+
"HardwareTriggeredFlyable",
|
|
52
80
|
"DEFAULT_TIMEOUT",
|
|
53
81
|
"Callback",
|
|
54
82
|
"NotConnected",
|
|
@@ -58,4 +86,11 @@ __all__ = [
|
|
|
58
86
|
"get_unique",
|
|
59
87
|
"merge_gathered_dicts",
|
|
60
88
|
"wait_for_connection",
|
|
89
|
+
"get_signal_values",
|
|
90
|
+
"load_from_yaml",
|
|
91
|
+
"save_to_yaml",
|
|
92
|
+
"set_signal_values",
|
|
93
|
+
"walk_rw_signals",
|
|
94
|
+
"load_device",
|
|
95
|
+
"save_device",
|
|
61
96
|
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Protocol, Sequence, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class DirectoryInfo:
|
|
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] = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DirectoryProvider(Protocol):
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def __call__(self) -> DirectoryInfo:
|
|
33
|
+
"""Get the current directory to write files into"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StaticDirectoryProvider(DirectoryProvider):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
directory_path: Union[str, Path],
|
|
40
|
+
filename_prefix: str = "",
|
|
41
|
+
filename_suffix: str = "",
|
|
42
|
+
resource_dir: Path = Path("."),
|
|
43
|
+
) -> None:
|
|
44
|
+
if isinstance(directory_path, str):
|
|
45
|
+
directory_path = Path(directory_path)
|
|
46
|
+
self._directory_info = DirectoryInfo(
|
|
47
|
+
root=directory_path,
|
|
48
|
+
resource_dir=resource_dir,
|
|
49
|
+
prefix=filename_prefix,
|
|
50
|
+
suffix=filename_suffix,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def __call__(self) -> DirectoryInfo:
|
|
54
|
+
return self._directory_info
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NameProvider(Protocol):
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def __call__(self) -> str:
|
|
60
|
+
"""Get the name to be used as a data_key in the descriptor document"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ShapeProvider(Protocol):
|
|
64
|
+
@abstractmethod
|
|
65
|
+
async def __call__(self) -> Sequence[int]:
|
|
66
|
+
"""Get the shape of the data collection"""
|
ophyd_async/core/async_status.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Equivalent of bluesky.
|
|
1
|
+
"""Equivalent of bluesky.protocols.Status for asynchronous tasks."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
@@ -62,7 +62,9 @@ class AsyncStatus(Status):
|
|
|
62
62
|
@property
|
|
63
63
|
def success(self) -> bool:
|
|
64
64
|
return (
|
|
65
|
-
self.task.done()
|
|
65
|
+
self.task.done()
|
|
66
|
+
and not self.task.cancelled()
|
|
67
|
+
and self.task.exception() is None
|
|
66
68
|
)
|
|
67
69
|
|
|
68
70
|
def watch(self, watcher: Callable):
|
|
@@ -83,12 +85,12 @@ class AsyncStatus(Status):
|
|
|
83
85
|
|
|
84
86
|
def __repr__(self) -> str:
|
|
85
87
|
if self.done:
|
|
86
|
-
if self.exception()
|
|
87
|
-
status = "errored"
|
|
88
|
+
if e := self.exception():
|
|
89
|
+
status = f"errored: {repr(e)}"
|
|
88
90
|
else:
|
|
89
91
|
status = "done"
|
|
90
92
|
else:
|
|
91
93
|
status = "pending"
|
|
92
|
-
return f"<{type(self).__name__} {status}>"
|
|
94
|
+
return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
|
|
93
95
|
|
|
94
96
|
__str__ = __repr__
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Module which defines abstract classes to work with detectors"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import (
|
|
9
|
+
AsyncGenerator,
|
|
10
|
+
AsyncIterator,
|
|
11
|
+
Callable,
|
|
12
|
+
Dict,
|
|
13
|
+
List,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
TypeVar,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from bluesky.protocols import (
|
|
20
|
+
Collectable,
|
|
21
|
+
Configurable,
|
|
22
|
+
Descriptor,
|
|
23
|
+
Flyable,
|
|
24
|
+
Preparable,
|
|
25
|
+
Readable,
|
|
26
|
+
Reading,
|
|
27
|
+
Stageable,
|
|
28
|
+
StreamAsset,
|
|
29
|
+
Triggerable,
|
|
30
|
+
WritesStreamAssets,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .async_status import AsyncStatus
|
|
34
|
+
from .device import Device
|
|
35
|
+
from .signal import SignalR
|
|
36
|
+
from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
|
|
37
|
+
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DetectorTrigger(str, Enum):
|
|
42
|
+
#: Detector generates internal trigger for given rate
|
|
43
|
+
internal = "internal"
|
|
44
|
+
#: Expect a series of arbitrary length trigger signals
|
|
45
|
+
edge_trigger = "edge_trigger"
|
|
46
|
+
#: Expect a series of constant width external gate signals
|
|
47
|
+
constant_gate = "constant_gate"
|
|
48
|
+
#: Expect a series of variable width external gate signals
|
|
49
|
+
variable_gate = "variable_gate"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class TriggerInfo:
|
|
54
|
+
#: Number of triggers that will be sent
|
|
55
|
+
num: int
|
|
56
|
+
#: Sort of triggers that will be sent
|
|
57
|
+
trigger: DetectorTrigger
|
|
58
|
+
#: What is the minimum deadtime between triggers
|
|
59
|
+
deadtime: float
|
|
60
|
+
#: What is the maximum high time of the triggers
|
|
61
|
+
livetime: float
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DetectorControl(ABC):
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def get_deadtime(self, exposure: float) -> float:
|
|
67
|
+
"""For a given exposure, how long should the time between exposures be"""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def arm(
|
|
71
|
+
self,
|
|
72
|
+
num: int,
|
|
73
|
+
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
74
|
+
exposure: Optional[float] = None,
|
|
75
|
+
) -> AsyncStatus:
|
|
76
|
+
"""Arm the detector and return AsyncStatus.
|
|
77
|
+
|
|
78
|
+
Awaiting the return value will wait for num frames to be written.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def disarm(self):
|
|
83
|
+
"""Disarm the detector"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DetectorWriter(ABC):
|
|
87
|
+
@abstractmethod
|
|
88
|
+
async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
|
|
89
|
+
"""Open writer and wait for it to be ready for data.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
multiplier: Each StreamDatum index corresponds to this many
|
|
93
|
+
written exposures
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Output for ``describe()``
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def observe_indices_written(
|
|
101
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
102
|
+
) -> AsyncGenerator[int, None]:
|
|
103
|
+
"""Yield each index as it is written"""
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def get_indices_written(self) -> int:
|
|
107
|
+
"""Get the number of indices written"""
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
|
|
111
|
+
"""Create Stream docs up to given number written"""
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
async def close(self) -> None:
|
|
115
|
+
"""Close writer and wait for it to be finished"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class StandardDetector(
|
|
119
|
+
Device,
|
|
120
|
+
Stageable,
|
|
121
|
+
Configurable,
|
|
122
|
+
Readable,
|
|
123
|
+
Triggerable,
|
|
124
|
+
Preparable,
|
|
125
|
+
Flyable,
|
|
126
|
+
Collectable,
|
|
127
|
+
WritesStreamAssets,
|
|
128
|
+
):
|
|
129
|
+
"""Detector with useful step and flyscan behaviour.
|
|
130
|
+
|
|
131
|
+
Must be supplied instances of classes that inherit from DetectorControl and
|
|
132
|
+
DetectorData, to dictate how the detector will be controlled (i.e. arming and
|
|
133
|
+
disarming) as well as how the detector data will be written (i.e. opening and
|
|
134
|
+
closing the writer, and handling data writing indices).
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
controller: DetectorControl,
|
|
141
|
+
writer: DetectorWriter,
|
|
142
|
+
config_sigs: Sequence[SignalR] = (),
|
|
143
|
+
name: str = "",
|
|
144
|
+
writer_timeout: float = DEFAULT_TIMEOUT,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
control:
|
|
150
|
+
instance of class which inherits from :class:`DetectorControl`
|
|
151
|
+
data:
|
|
152
|
+
instance of class which inherits from :class:`DetectorData`
|
|
153
|
+
name:
|
|
154
|
+
detector name
|
|
155
|
+
"""
|
|
156
|
+
self._controller = controller
|
|
157
|
+
self._writer = writer
|
|
158
|
+
self._describe: Dict[str, Descriptor] = {}
|
|
159
|
+
self._config_sigs = list(config_sigs)
|
|
160
|
+
self._frame_writing_timeout = writer_timeout
|
|
161
|
+
# For prepare
|
|
162
|
+
self._arm_status: Optional[AsyncStatus] = None
|
|
163
|
+
self._trigger_info: Optional[TriggerInfo] = None
|
|
164
|
+
# For kickoff
|
|
165
|
+
self._watchers: List[Callable] = []
|
|
166
|
+
self._fly_status: Optional[AsyncStatus] = None
|
|
167
|
+
self._fly_start: float
|
|
168
|
+
|
|
169
|
+
self._intial_frame: int
|
|
170
|
+
self._last_frame: int
|
|
171
|
+
super().__init__(name)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def controller(self) -> DetectorControl:
|
|
175
|
+
return self._controller
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def writer(self) -> DetectorWriter:
|
|
179
|
+
return self._writer
|
|
180
|
+
|
|
181
|
+
@AsyncStatus.wrap
|
|
182
|
+
async def stage(self) -> None:
|
|
183
|
+
"""Disarm the detector, stop filewriting, and open file for writing."""
|
|
184
|
+
await self.check_config_sigs()
|
|
185
|
+
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
186
|
+
self._describe = await self.writer.open()
|
|
187
|
+
|
|
188
|
+
async def check_config_sigs(self):
|
|
189
|
+
"""Checks configuration signals are named and connected."""
|
|
190
|
+
for signal in self._config_sigs:
|
|
191
|
+
if signal._name == "":
|
|
192
|
+
raise Exception(
|
|
193
|
+
"config signal must be named before it is passed to the detector"
|
|
194
|
+
)
|
|
195
|
+
try:
|
|
196
|
+
await signal.get_value()
|
|
197
|
+
except NotImplementedError:
|
|
198
|
+
raise Exception(
|
|
199
|
+
f"config signal {signal._name} must be connected before it is "
|
|
200
|
+
+ "passed to the detector"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@AsyncStatus.wrap
|
|
204
|
+
async def unstage(self) -> None:
|
|
205
|
+
"""Stop data writing."""
|
|
206
|
+
await self.writer.close()
|
|
207
|
+
|
|
208
|
+
async def read_configuration(self) -> Dict[str, Reading]:
|
|
209
|
+
return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
|
|
210
|
+
|
|
211
|
+
async def describe_configuration(self) -> Dict[str, Descriptor]:
|
|
212
|
+
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
213
|
+
|
|
214
|
+
async def read(self) -> Dict[str, Reading]:
|
|
215
|
+
"""Read the detector"""
|
|
216
|
+
# All data is in StreamResources, not Events, so nothing to output here
|
|
217
|
+
return {}
|
|
218
|
+
|
|
219
|
+
def describe(self) -> Dict[str, Descriptor]:
|
|
220
|
+
return self._describe
|
|
221
|
+
|
|
222
|
+
@AsyncStatus.wrap
|
|
223
|
+
async def trigger(self) -> None:
|
|
224
|
+
"""Arm the detector and wait for it to finish."""
|
|
225
|
+
indices_written = await self.writer.get_indices_written()
|
|
226
|
+
written_status = await self.controller.arm(
|
|
227
|
+
num=1,
|
|
228
|
+
trigger=DetectorTrigger.internal,
|
|
229
|
+
)
|
|
230
|
+
await written_status
|
|
231
|
+
end_observation = indices_written + 1
|
|
232
|
+
|
|
233
|
+
async for index in self.writer.observe_indices_written(
|
|
234
|
+
self._frame_writing_timeout
|
|
235
|
+
):
|
|
236
|
+
if index >= end_observation:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
def prepare(
|
|
240
|
+
self,
|
|
241
|
+
value: T,
|
|
242
|
+
) -> AsyncStatus:
|
|
243
|
+
"""Arm detector"""
|
|
244
|
+
return AsyncStatus(self._prepare(value))
|
|
245
|
+
|
|
246
|
+
async def _prepare(self, value: T) -> None:
|
|
247
|
+
"""Arm detector.
|
|
248
|
+
|
|
249
|
+
Prepare the detector with trigger information. This is determined at and passed
|
|
250
|
+
in from the plan level.
|
|
251
|
+
|
|
252
|
+
This currently only prepares detectors for flyscans and stepscans just use the
|
|
253
|
+
trigger information determined in trigger.
|
|
254
|
+
|
|
255
|
+
To do: Unify prepare to be use for both fly and step scans.
|
|
256
|
+
"""
|
|
257
|
+
assert type(value) is TriggerInfo
|
|
258
|
+
self._trigger_info = value
|
|
259
|
+
self._initial_frame = await self.writer.get_indices_written()
|
|
260
|
+
self._last_frame = self._initial_frame + self._trigger_info.num
|
|
261
|
+
|
|
262
|
+
required = self.controller.get_deadtime(self._trigger_info.livetime)
|
|
263
|
+
assert required <= self._trigger_info.deadtime, (
|
|
264
|
+
f"Detector {self.controller} needs at least {required}s deadtime, "
|
|
265
|
+
f"but trigger logic provides only {self._trigger_info.deadtime}s"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
self._arm_status = await self.controller.arm(
|
|
269
|
+
num=self._trigger_info.num,
|
|
270
|
+
trigger=self._trigger_info.trigger,
|
|
271
|
+
exposure=self._trigger_info.livetime,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@AsyncStatus.wrap
|
|
275
|
+
async def kickoff(self) -> None:
|
|
276
|
+
self._fly_status = AsyncStatus(self._fly(), self._watchers)
|
|
277
|
+
self._fly_start = time.monotonic()
|
|
278
|
+
|
|
279
|
+
async def _fly(self) -> None:
|
|
280
|
+
await self._observe_writer_indicies(self._last_frame)
|
|
281
|
+
|
|
282
|
+
async def _observe_writer_indicies(self, end_observation: int):
|
|
283
|
+
async for index in self.writer.observe_indices_written(
|
|
284
|
+
self._frame_writing_timeout
|
|
285
|
+
):
|
|
286
|
+
for watcher in self._watchers:
|
|
287
|
+
watcher(
|
|
288
|
+
name=self.name,
|
|
289
|
+
current=index,
|
|
290
|
+
initial=self._initial_frame,
|
|
291
|
+
target=end_observation,
|
|
292
|
+
unit="",
|
|
293
|
+
precision=0,
|
|
294
|
+
time_elapsed=time.monotonic() - self._fly_start,
|
|
295
|
+
)
|
|
296
|
+
if index >= end_observation:
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
@AsyncStatus.wrap
|
|
300
|
+
async def complete(self) -> AsyncStatus:
|
|
301
|
+
assert self._fly_status, "Kickoff not run"
|
|
302
|
+
return await self._fly_status
|
|
303
|
+
|
|
304
|
+
async def describe_collect(self) -> Dict[str, Descriptor]:
|
|
305
|
+
return self._describe
|
|
306
|
+
|
|
307
|
+
async def collect_asset_docs(
|
|
308
|
+
self, index: Optional[int] = None
|
|
309
|
+
) -> AsyncIterator[StreamAsset]:
|
|
310
|
+
"""Collect stream datum documents for all indices written.
|
|
311
|
+
|
|
312
|
+
The index is optional, and provided for flyscans, however this needs to be
|
|
313
|
+
retrieved for stepscans.
|
|
314
|
+
"""
|
|
315
|
+
if not index:
|
|
316
|
+
index = await self.writer.get_indices_written()
|
|
317
|
+
async for doc in self.writer.collect_stream_docs(index):
|
|
318
|
+
yield doc
|
|
319
|
+
|
|
320
|
+
async def get_index(self) -> int:
|
|
321
|
+
return await self.writer.get_indices_written()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Base device"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Coroutine,
|
|
9
|
+
Dict,
|
|
10
|
+
Generator,
|
|
11
|
+
Iterator,
|
|
12
|
+
Optional,
|
|
13
|
+
Set,
|
|
14
|
+
Tuple,
|
|
15
|
+
TypeVar,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from bluesky.protocols import HasName
|
|
19
|
+
from bluesky.run_engine import call_in_bluesky_event_loop
|
|
20
|
+
|
|
21
|
+
from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Device(HasName):
|
|
25
|
+
"""Common base class for all Ophyd Async Devices.
|
|
26
|
+
|
|
27
|
+
By default, names and connects all Device children.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_name: str = ""
|
|
31
|
+
#: The parent Device if it exists
|
|
32
|
+
parent: Optional[Device] = None
|
|
33
|
+
|
|
34
|
+
def __init__(self, name: str = "") -> None:
|
|
35
|
+
self.set_name(name)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self) -> str:
|
|
39
|
+
"""Return the name of the Device"""
|
|
40
|
+
return self._name
|
|
41
|
+
|
|
42
|
+
def children(self) -> Iterator[Tuple[str, Device]]:
|
|
43
|
+
for attr_name, attr in self.__dict__.items():
|
|
44
|
+
if attr_name != "parent" and isinstance(attr, Device):
|
|
45
|
+
yield attr_name, attr
|
|
46
|
+
|
|
47
|
+
def set_name(self, name: str):
|
|
48
|
+
"""Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
name:
|
|
53
|
+
New name to set
|
|
54
|
+
"""
|
|
55
|
+
self._name = name
|
|
56
|
+
for attr_name, child in self.children():
|
|
57
|
+
child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
|
|
58
|
+
child.set_name(child_name)
|
|
59
|
+
child.parent = self
|
|
60
|
+
|
|
61
|
+
async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
|
|
62
|
+
"""Connect self and all child Devices.
|
|
63
|
+
|
|
64
|
+
Contains a timeout that gets propagated to child.connect methods.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
sim:
|
|
69
|
+
If True then connect in simulation mode.
|
|
70
|
+
timeout:
|
|
71
|
+
Time to wait before failing with a TimeoutError.
|
|
72
|
+
"""
|
|
73
|
+
coros = {
|
|
74
|
+
name: child_device.connect(sim, timeout=timeout)
|
|
75
|
+
for name, child_device in self.children()
|
|
76
|
+
}
|
|
77
|
+
if coros:
|
|
78
|
+
await wait_for_connection(**coros)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
VT = TypeVar("VT", bound=Device)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class DeviceVector(Dict[int, VT], Device):
|
|
85
|
+
def children(self) -> Generator[Tuple[str, Device], None, None]:
|
|
86
|
+
for attr_name, attr in self.items():
|
|
87
|
+
if isinstance(attr, Device):
|
|
88
|
+
yield str(attr_name), attr
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DeviceCollector:
|
|
92
|
+
"""Collector of top level Device instances to be used as a context manager
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
set_name:
|
|
97
|
+
If True, call ``device.set_name(variable_name)`` on all collected
|
|
98
|
+
Devices
|
|
99
|
+
connect:
|
|
100
|
+
If True, call ``device.connect(sim)`` in parallel on all
|
|
101
|
+
collected Devices
|
|
102
|
+
sim:
|
|
103
|
+
If True, connect Signals in simulation mode
|
|
104
|
+
timeout:
|
|
105
|
+
How long to wait for connect before logging an exception
|
|
106
|
+
|
|
107
|
+
Notes
|
|
108
|
+
-----
|
|
109
|
+
Example usage::
|
|
110
|
+
|
|
111
|
+
[async] with DeviceCollector():
|
|
112
|
+
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
113
|
+
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
114
|
+
# Names and connects devices here
|
|
115
|
+
assert t1x.comm.velocity.source
|
|
116
|
+
assert t1x.name == "t1x"
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
set_name=True,
|
|
123
|
+
connect=True,
|
|
124
|
+
sim=False,
|
|
125
|
+
timeout: float = 10.0,
|
|
126
|
+
):
|
|
127
|
+
self._set_name = set_name
|
|
128
|
+
self._connect = connect
|
|
129
|
+
self._sim = sim
|
|
130
|
+
self._timeout = timeout
|
|
131
|
+
self._names_on_enter: Set[str] = set()
|
|
132
|
+
self._objects_on_exit: Dict[str, Any] = {}
|
|
133
|
+
|
|
134
|
+
def _caller_locals(self):
|
|
135
|
+
"""Walk up until we find a stack frame that doesn't have us as self"""
|
|
136
|
+
try:
|
|
137
|
+
raise ValueError
|
|
138
|
+
except ValueError:
|
|
139
|
+
_, _, tb = sys.exc_info()
|
|
140
|
+
assert tb, "Can't get traceback, this shouldn't happen"
|
|
141
|
+
caller_frame = tb.tb_frame
|
|
142
|
+
while caller_frame.f_locals.get("self", None) is self:
|
|
143
|
+
caller_frame = caller_frame.f_back
|
|
144
|
+
return caller_frame.f_locals
|
|
145
|
+
|
|
146
|
+
def __enter__(self) -> "DeviceCollector":
|
|
147
|
+
# Stash the names that were defined before we were called
|
|
148
|
+
self._names_on_enter = set(self._caller_locals())
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
async def __aenter__(self) -> "DeviceCollector":
|
|
152
|
+
return self.__enter__()
|
|
153
|
+
|
|
154
|
+
async def _on_exit(self) -> None:
|
|
155
|
+
# Name and kick off connect for devices
|
|
156
|
+
connect_coroutines: Dict[str, Coroutine] = {}
|
|
157
|
+
for name, obj in self._objects_on_exit.items():
|
|
158
|
+
if name not in self._names_on_enter and isinstance(obj, Device):
|
|
159
|
+
if self._set_name and not obj.name:
|
|
160
|
+
obj.set_name(name)
|
|
161
|
+
if self._connect:
|
|
162
|
+
connect_coroutines[name] = obj.connect(
|
|
163
|
+
self._sim, timeout=self._timeout
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Connect to all the devices
|
|
167
|
+
if connect_coroutines:
|
|
168
|
+
await wait_for_connection(**connect_coroutines)
|
|
169
|
+
|
|
170
|
+
async def __aexit__(self, type, value, traceback):
|
|
171
|
+
self._objects_on_exit = self._caller_locals()
|
|
172
|
+
await self._on_exit()
|
|
173
|
+
|
|
174
|
+
def __exit__(self, type_, value, traceback):
|
|
175
|
+
self._objects_on_exit = self._caller_locals()
|
|
176
|
+
try:
|
|
177
|
+
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
178
|
+
except RuntimeError:
|
|
179
|
+
raise NotConnected(
|
|
180
|
+
"Could not connect devices. Is the bluesky event loop running? See "
|
|
181
|
+
"https://blueskyproject.io/ophyd-async/main/"
|
|
182
|
+
"user/explanations/event-loop-choice.html for more info."
|
|
183
|
+
)
|
|
184
|
+
return fut
|