ophyd-async 0.5.2__py3-none-any.whl → 0.6.0__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 +11 -3
- ophyd_async/core/_detector.py +72 -63
- ophyd_async/core/_device.py +13 -15
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -4
- 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 +63 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
- 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 +2 -3
- 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 +19 -14
- ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
- ophyd_async/epics/adsimdetector/_sim.py +6 -5
- ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
- ophyd_async/epics/advimba/_vimba_controller.py +21 -16
- 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 +16 -16
- 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 +14 -15
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +111 -138
- ophyd_async/fastcs/panda/_trigger.py +1 -2
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +28 -13
- ophyd_async/plan_stubs/_fly.py +16 -16
- 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 +24 -20
- 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-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
- ophyd_async-0.6.0.dist-info/RECORD +96 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.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.6.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.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
|
@@ -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,6 +80,7 @@ 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
|
|
|
@@ -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,14 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import time
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Sequence
|
|
6
7
|
from enum import Enum
|
|
7
8
|
from typing import (
|
|
8
|
-
AsyncGenerator,
|
|
9
|
-
AsyncIterator,
|
|
10
|
-
Callable,
|
|
11
|
-
Dict,
|
|
12
9
|
Generic,
|
|
13
|
-
List,
|
|
14
|
-
Optional,
|
|
15
|
-
Sequence,
|
|
16
10
|
)
|
|
17
11
|
|
|
18
12
|
from bluesky.protocols import (
|
|
19
13
|
Collectable,
|
|
20
|
-
DataKey,
|
|
21
14
|
Flyable,
|
|
22
15
|
Preparable,
|
|
23
16
|
Reading,
|
|
@@ -26,10 +19,12 @@ from bluesky.protocols import (
|
|
|
26
19
|
Triggerable,
|
|
27
20
|
WritesStreamAssets,
|
|
28
21
|
)
|
|
22
|
+
from event_model import DataKey
|
|
29
23
|
from pydantic import BaseModel, Field
|
|
30
24
|
|
|
31
25
|
from ._device import Device
|
|
32
26
|
from ._protocol import AsyncConfigurable, AsyncReadable
|
|
27
|
+
from ._signal import SignalR
|
|
33
28
|
from ._status import AsyncStatus, WatchableAsyncStatus
|
|
34
29
|
from ._utils import DEFAULT_TIMEOUT, T, WatcherUpdate, merge_gathered_dicts
|
|
35
30
|
|
|
@@ -51,20 +46,24 @@ class TriggerInfo(BaseModel):
|
|
|
51
46
|
"""Minimal set of information required to setup triggering on a detector"""
|
|
52
47
|
|
|
53
48
|
#: Number of triggers that will be sent, 0 means infinite
|
|
54
|
-
number: int = Field(
|
|
49
|
+
number: int = Field(ge=0)
|
|
55
50
|
#: Sort of triggers that will be sent
|
|
56
|
-
trigger: DetectorTrigger = Field()
|
|
51
|
+
trigger: DetectorTrigger = Field(default=DetectorTrigger.internal)
|
|
57
52
|
#: What is the minimum deadtime between triggers
|
|
58
|
-
deadtime: float | None = Field(ge=0)
|
|
53
|
+
deadtime: float | None = Field(default=None, ge=0)
|
|
59
54
|
#: What is the maximum high time of the triggers
|
|
60
|
-
livetime: float | None = Field(ge=0)
|
|
55
|
+
livetime: float | None = Field(default=None, ge=0)
|
|
61
56
|
#: What is the maximum timeout on waiting for a frame
|
|
62
|
-
frame_timeout: float | None = Field(None, gt=0)
|
|
57
|
+
frame_timeout: float | None = Field(default=None, gt=0)
|
|
63
58
|
#: How many triggers make up a single StreamDatum index, to allow multiple frames
|
|
64
59
|
#: from a faster detector to be zipped with a single frame from a slow detector
|
|
65
60
|
#: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
|
|
66
61
|
#: but publish 2 indices, and describe() will show a shape of (5, h, w)
|
|
67
62
|
multiplier: int = 1
|
|
63
|
+
#: The number of times the detector can go through a complete cycle of kickoff and
|
|
64
|
+
#: complete without needing to re-arm. This is important for detectors where the
|
|
65
|
+
#: process of arming is expensive in terms of time
|
|
66
|
+
iteration: int = 1
|
|
68
67
|
|
|
69
68
|
|
|
70
69
|
class DetectorControl(ABC):
|
|
@@ -78,27 +77,35 @@ class DetectorControl(ABC):
|
|
|
78
77
|
"""For a given exposure, how long should the time between exposures be"""
|
|
79
78
|
|
|
80
79
|
@abstractmethod
|
|
81
|
-
async def
|
|
82
|
-
self,
|
|
83
|
-
num: int,
|
|
84
|
-
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
85
|
-
exposure: Optional[float] = None,
|
|
86
|
-
) -> AsyncStatus:
|
|
80
|
+
async def prepare(self, trigger_info: TriggerInfo):
|
|
87
81
|
"""
|
|
88
|
-
|
|
82
|
+
Do all necessary steps to prepare the detector for triggers.
|
|
89
83
|
|
|
90
84
|
Args:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
trigger_info: This is a Pydantic model which contains
|
|
86
|
+
number Expected number of frames.
|
|
87
|
+
trigger Type of trigger for which to prepare the detector. Defaults
|
|
88
|
+
to DetectorTrigger.internal.
|
|
89
|
+
livetime Livetime / Exposure time with which to set up the detector.
|
|
90
|
+
Defaults to None
|
|
91
|
+
if not applicable or the detector is expected to use its previously-set
|
|
92
|
+
exposure time.
|
|
93
|
+
deadtime Defaults to None. This is the minimum deadtime between
|
|
94
|
+
triggers.
|
|
95
|
+
multiplier The number of triggers grouped into a single StreamDatum
|
|
96
|
+
index.
|
|
97
|
+
"""
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
@abstractmethod
|
|
100
|
+
async def arm(self) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Arm the detector
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def wait_for_idle(self):
|
|
107
|
+
"""
|
|
108
|
+
This will wait on the internal _arm_status and wait for it to get disarmed/idle
|
|
102
109
|
"""
|
|
103
110
|
|
|
104
111
|
@abstractmethod
|
|
@@ -111,7 +118,7 @@ class DetectorWriter(ABC):
|
|
|
111
118
|
(e.g. an HDF5 file)"""
|
|
112
119
|
|
|
113
120
|
@abstractmethod
|
|
114
|
-
async def open(self, multiplier: int = 1) ->
|
|
121
|
+
async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
|
|
115
122
|
"""Open writer and wait for it to be ready for data.
|
|
116
123
|
|
|
117
124
|
Args:
|
|
@@ -162,7 +169,7 @@ class StandardDetector(
|
|
|
162
169
|
self,
|
|
163
170
|
controller: DetectorControl,
|
|
164
171
|
writer: DetectorWriter,
|
|
165
|
-
config_sigs: Sequence[
|
|
172
|
+
config_sigs: Sequence[SignalR] = (),
|
|
166
173
|
name: str = "",
|
|
167
174
|
) -> None:
|
|
168
175
|
"""
|
|
@@ -177,16 +184,16 @@ class StandardDetector(
|
|
|
177
184
|
"""
|
|
178
185
|
self._controller = controller
|
|
179
186
|
self._writer = writer
|
|
180
|
-
self._describe:
|
|
187
|
+
self._describe: dict[str, DataKey] = {}
|
|
181
188
|
self._config_sigs = list(config_sigs)
|
|
182
189
|
# For prepare
|
|
183
|
-
self._arm_status:
|
|
184
|
-
self._trigger_info:
|
|
190
|
+
self._arm_status: AsyncStatus | None = None
|
|
191
|
+
self._trigger_info: TriggerInfo | None = None
|
|
185
192
|
# For kickoff
|
|
186
|
-
self._watchers:
|
|
187
|
-
self._fly_status:
|
|
193
|
+
self._watchers: list[Callable] = []
|
|
194
|
+
self._fly_status: WatchableAsyncStatus | None = None
|
|
188
195
|
self._fly_start: float
|
|
189
|
-
|
|
196
|
+
self._iterations_completed: int = 0
|
|
190
197
|
self._intial_frame: int
|
|
191
198
|
self._last_frame: int
|
|
192
199
|
super().__init__(name)
|
|
@@ -215,28 +222,28 @@ class StandardDetector(
|
|
|
215
222
|
)
|
|
216
223
|
try:
|
|
217
224
|
await signal.get_value()
|
|
218
|
-
except NotImplementedError:
|
|
225
|
+
except NotImplementedError as e:
|
|
219
226
|
raise Exception(
|
|
220
227
|
f"config signal {signal.name} must be connected before it is "
|
|
221
228
|
+ "passed to the detector"
|
|
222
|
-
)
|
|
229
|
+
) from e
|
|
223
230
|
|
|
224
231
|
@AsyncStatus.wrap
|
|
225
232
|
async def unstage(self) -> None:
|
|
226
233
|
# Stop data writing.
|
|
227
|
-
await self.writer.close()
|
|
234
|
+
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
228
235
|
|
|
229
|
-
async def read_configuration(self) ->
|
|
236
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
230
237
|
return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
|
|
231
238
|
|
|
232
|
-
async def describe_configuration(self) ->
|
|
239
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
233
240
|
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
234
241
|
|
|
235
|
-
async def read(self) ->
|
|
242
|
+
async def read(self) -> dict[str, Reading]:
|
|
236
243
|
# All data is in StreamResources, not Events, so nothing to output here
|
|
237
244
|
return {}
|
|
238
245
|
|
|
239
|
-
async def describe(self) ->
|
|
246
|
+
async def describe(self) -> dict[str, DataKey]:
|
|
240
247
|
return self._describe
|
|
241
248
|
|
|
242
249
|
@AsyncStatus.wrap
|
|
@@ -248,15 +255,15 @@ class StandardDetector(
|
|
|
248
255
|
trigger=DetectorTrigger.internal,
|
|
249
256
|
deadtime=None,
|
|
250
257
|
livetime=None,
|
|
258
|
+
frame_timeout=None,
|
|
251
259
|
)
|
|
252
260
|
)
|
|
261
|
+
assert self._trigger_info
|
|
262
|
+
assert self._trigger_info.trigger is DetectorTrigger.internal
|
|
253
263
|
# Arm the detector and wait for it to finish.
|
|
254
264
|
indices_written = await self.writer.get_indices_written()
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
trigger=self._trigger_info.trigger,
|
|
258
|
-
)
|
|
259
|
-
await written_status
|
|
265
|
+
await self.controller.arm()
|
|
266
|
+
await self.controller.wait_for_idle()
|
|
260
267
|
end_observation = indices_written + 1
|
|
261
268
|
|
|
262
269
|
async for index in self.writer.observe_indices_written(
|
|
@@ -283,35 +290,35 @@ class StandardDetector(
|
|
|
283
290
|
Args:
|
|
284
291
|
value: TriggerInfo describing how to trigger the detector
|
|
285
292
|
"""
|
|
286
|
-
self._trigger_info = value
|
|
287
293
|
if value.trigger != DetectorTrigger.internal:
|
|
288
294
|
assert (
|
|
289
295
|
value.deadtime
|
|
290
296
|
), "Deadtime must be supplied when in externally triggered mode"
|
|
291
297
|
if value.deadtime:
|
|
292
|
-
required = self.controller.get_deadtime(
|
|
298
|
+
required = self.controller.get_deadtime(value.livetime)
|
|
293
299
|
assert required <= value.deadtime, (
|
|
294
300
|
f"Detector {self.controller} needs at least {required}s deadtime, "
|
|
295
301
|
f"but trigger logic provides only {value.deadtime}s"
|
|
296
302
|
)
|
|
303
|
+
self._trigger_info = value
|
|
297
304
|
self._initial_frame = await self.writer.get_indices_written()
|
|
298
305
|
self._last_frame = self._initial_frame + self._trigger_info.number
|
|
299
|
-
self.
|
|
300
|
-
|
|
301
|
-
trigger=self._trigger_info.trigger,
|
|
302
|
-
exposure=self._trigger_info.livetime,
|
|
306
|
+
self._describe, _ = await asyncio.gather(
|
|
307
|
+
self.writer.open(value.multiplier), self.controller.prepare(value)
|
|
303
308
|
)
|
|
304
|
-
|
|
305
|
-
|
|
309
|
+
if value.trigger != DetectorTrigger.internal:
|
|
310
|
+
await self.controller.arm()
|
|
311
|
+
self._fly_start = time.monotonic()
|
|
306
312
|
|
|
307
313
|
@AsyncStatus.wrap
|
|
308
314
|
async def kickoff(self):
|
|
309
|
-
|
|
310
|
-
|
|
315
|
+
assert self._trigger_info, "Prepare must be called before kickoff!"
|
|
316
|
+
if self._iterations_completed >= self._trigger_info.iteration:
|
|
317
|
+
raise Exception(f"Kickoff called more than {self._trigger_info.iteration}")
|
|
318
|
+
self._iterations_completed += 1
|
|
311
319
|
|
|
312
320
|
@WatchableAsyncStatus.wrap
|
|
313
321
|
async def complete(self):
|
|
314
|
-
assert self._arm_status, "Prepare not run"
|
|
315
322
|
assert self._trigger_info
|
|
316
323
|
async for index in self.writer.observe_indices_written(
|
|
317
324
|
self._trigger_info.frame_timeout
|
|
@@ -332,12 +339,14 @@ class StandardDetector(
|
|
|
332
339
|
)
|
|
333
340
|
if index >= self._trigger_info.number:
|
|
334
341
|
break
|
|
342
|
+
if self._iterations_completed == self._trigger_info.iteration:
|
|
343
|
+
await self.controller.wait_for_idle()
|
|
335
344
|
|
|
336
|
-
async def describe_collect(self) ->
|
|
345
|
+
async def describe_collect(self) -> dict[str, DataKey]:
|
|
337
346
|
return self._describe
|
|
338
347
|
|
|
339
348
|
async def collect_asset_docs(
|
|
340
|
-
self, index:
|
|
349
|
+
self, index: int | None = None
|
|
341
350
|
) -> AsyncIterator[StreamAsset]:
|
|
342
351
|
# Collect stream datum documents for all indices written.
|
|
343
352
|
# 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
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from collections.abc import Callable, Generator, Sequence
|
|
1
2
|
from enum import Enum
|
|
2
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
3
5
|
|
|
4
6
|
import numpy as np
|
|
5
7
|
import numpy.typing as npt
|
|
@@ -7,6 +9,7 @@ import yaml
|
|
|
7
9
|
from bluesky.plan_stubs import abs_set, wait
|
|
8
10
|
from bluesky.protocols import Location
|
|
9
11
|
from bluesky.utils import Msg
|
|
12
|
+
from pydantic import BaseModel
|
|
10
13
|
|
|
11
14
|
from ._device import Device
|
|
12
15
|
from ._signal import SignalRW
|
|
@@ -18,16 +21,22 @@ def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.No
|
|
|
18
21
|
)
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
def pydantic_model_abstraction_representer(
|
|
25
|
+
dumper: yaml.Dumper, model: BaseModel
|
|
26
|
+
) -> yaml.Node:
|
|
27
|
+
return dumper.represent_data(model.model_dump(mode="python"))
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
class OphydDumper(yaml.Dumper):
|
|
22
31
|
def represent_data(self, data: Any) -> Any:
|
|
23
32
|
if isinstance(data, Enum):
|
|
24
33
|
return self.represent_data(data.value)
|
|
25
|
-
return super(
|
|
34
|
+
return super().represent_data(data)
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
def get_signal_values(
|
|
29
|
-
signals:
|
|
30
|
-
) -> Generator[Msg, Sequence[Location[Any]],
|
|
38
|
+
signals: dict[str, SignalRW[Any]], ignore: list[str] | None = None
|
|
39
|
+
) -> Generator[Msg, Sequence[Location[Any]], dict[str, Any]]:
|
|
31
40
|
"""Get signal values in bulk.
|
|
32
41
|
|
|
33
42
|
Used as part of saving the signals of a device to a yaml file.
|
|
@@ -59,13 +68,10 @@ def get_signal_values(
|
|
|
59
68
|
}
|
|
60
69
|
selected_values = yield Msg("locate", *selected_signals.values())
|
|
61
70
|
|
|
62
|
-
# TODO: investigate wrong type hints
|
|
63
|
-
if isinstance(selected_values, dict):
|
|
64
|
-
selected_values = [selected_values] # type: ignore
|
|
65
|
-
|
|
66
71
|
assert selected_values is not None, "No signalRW's were able to be located"
|
|
67
72
|
named_values = {
|
|
68
|
-
key: value["setpoint"]
|
|
73
|
+
key: value["setpoint"]
|
|
74
|
+
for key, value in zip(selected_signals, selected_values, strict=False)
|
|
69
75
|
}
|
|
70
76
|
# Ignored values place in with value None so we know which ones were ignored
|
|
71
77
|
named_values.update({key: None for key in ignore})
|
|
@@ -73,8 +79,8 @@ def get_signal_values(
|
|
|
73
79
|
|
|
74
80
|
|
|
75
81
|
def walk_rw_signals(
|
|
76
|
-
device: Device, path_prefix:
|
|
77
|
-
) ->
|
|
82
|
+
device: Device, path_prefix: str | None = ""
|
|
83
|
+
) -> dict[str, SignalRW[Any]]:
|
|
78
84
|
"""Retrieve all SignalRWs from a device.
|
|
79
85
|
|
|
80
86
|
Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
|
|
@@ -104,7 +110,7 @@ def walk_rw_signals(
|
|
|
104
110
|
if not path_prefix:
|
|
105
111
|
path_prefix = ""
|
|
106
112
|
|
|
107
|
-
signals:
|
|
113
|
+
signals: dict[str, SignalRW[Any]] = {}
|
|
108
114
|
for attr_name, attr in device.children():
|
|
109
115
|
dot_path = f"{path_prefix}{attr_name}"
|
|
110
116
|
if type(attr) is SignalRW:
|
|
@@ -114,7 +120,7 @@ def walk_rw_signals(
|
|
|
114
120
|
return signals
|
|
115
121
|
|
|
116
122
|
|
|
117
|
-
def save_to_yaml(phases: Sequence[
|
|
123
|
+
def save_to_yaml(phases: Sequence[dict[str, Any]], save_path: str | Path) -> None:
|
|
118
124
|
"""Plan which serialises a phase or set of phases of SignalRWs to a yaml file.
|
|
119
125
|
|
|
120
126
|
Parameters
|
|
@@ -134,12 +140,17 @@ def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
|
|
|
134
140
|
"""
|
|
135
141
|
|
|
136
142
|
yaml.add_representer(np.ndarray, ndarray_representer, Dumper=yaml.Dumper)
|
|
143
|
+
yaml.add_multi_representer(
|
|
144
|
+
BaseModel,
|
|
145
|
+
pydantic_model_abstraction_representer,
|
|
146
|
+
Dumper=yaml.Dumper,
|
|
147
|
+
)
|
|
137
148
|
|
|
138
149
|
with open(save_path, "w") as file:
|
|
139
150
|
yaml.dump(phases, file, Dumper=OphydDumper, default_flow_style=False)
|
|
140
151
|
|
|
141
152
|
|
|
142
|
-
def load_from_yaml(save_path: str) -> Sequence[
|
|
153
|
+
def load_from_yaml(save_path: str) -> Sequence[dict[str, Any]]:
|
|
143
154
|
"""Plan that returns a list of dicts with saved signal values from a yaml file.
|
|
144
155
|
|
|
145
156
|
Parameters
|
|
@@ -152,12 +163,12 @@ def load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:
|
|
|
152
163
|
:func:`ophyd_async.core.save_to_yaml`
|
|
153
164
|
:func:`ophyd_async.core.set_signal_values`
|
|
154
165
|
"""
|
|
155
|
-
with open(save_path
|
|
166
|
+
with open(save_path) as file:
|
|
156
167
|
return yaml.full_load(file)
|
|
157
168
|
|
|
158
169
|
|
|
159
170
|
def set_signal_values(
|
|
160
|
-
signals:
|
|
171
|
+
signals: dict[str, SignalRW[Any]], values: Sequence[dict[str, Any]]
|
|
161
172
|
) -> Generator[Msg, None, None]:
|
|
162
173
|
"""Maps signals from a yaml file into device signals.
|
|
163
174
|
|
|
@@ -217,7 +228,7 @@ def load_device(device: Device, path: str):
|
|
|
217
228
|
yield from set_signal_values(signals_to_set, values)
|
|
218
229
|
|
|
219
230
|
|
|
220
|
-
def all_at_once(values:
|
|
231
|
+
def all_at_once(values: dict[str, Any]) -> Sequence[dict[str, Any]]:
|
|
221
232
|
"""Sort all the values into a single phase so they are set all at once"""
|
|
222
233
|
return [values]
|
|
223
234
|
|
|
@@ -225,8 +236,8 @@ def all_at_once(values: Dict[str, Any]) -> Sequence[Dict[str, Any]]:
|
|
|
225
236
|
def save_device(
|
|
226
237
|
device: Device,
|
|
227
238
|
path: str,
|
|
228
|
-
sorter: Callable[[
|
|
229
|
-
ignore:
|
|
239
|
+
sorter: Callable[[dict[str, Any]], Sequence[dict[str, Any]]] = all_at_once,
|
|
240
|
+
ignore: list[str] | None = None,
|
|
230
241
|
):
|
|
231
242
|
"""Plan that saves the state of all PV's on a device using a sorter.
|
|
232
243
|
|
ophyd_async/core/_flyer.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Generic
|
|
3
4
|
|
|
4
|
-
from bluesky.protocols import
|
|
5
|
+
from bluesky.protocols import Flyable, Preparable, Reading, Stageable
|
|
6
|
+
from event_model import DataKey
|
|
5
7
|
|
|
6
8
|
from ._device import Device
|
|
7
9
|
from ._signal import SignalR
|
|
@@ -72,12 +74,12 @@ class StandardFlyer(
|
|
|
72
74
|
async def complete(self) -> None:
|
|
73
75
|
await self._trigger_logic.complete()
|
|
74
76
|
|
|
75
|
-
async def describe_configuration(self) ->
|
|
77
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
76
78
|
return await merge_gathered_dicts(
|
|
77
79
|
[sig.describe() for sig in self._configuration_signals]
|
|
78
80
|
)
|
|
79
81
|
|
|
80
|
-
async def read_configuration(self) ->
|
|
82
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
81
83
|
return await merge_gathered_dicts(
|
|
82
84
|
[sig.read() for sig in self._configuration_signals]
|
|
83
85
|
)
|