ophyd-async 0.2.0__py3-none-any.whl → 0.3.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 +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +52 -19
- ophyd_async/core/_providers.py +38 -5
- ophyd_async/core/async_status.py +86 -40
- ophyd_async/core/detector.py +214 -72
- ophyd_async/core/device.py +91 -50
- ophyd_async/core/device_save_loader.py +96 -23
- ophyd_async/core/flyer.py +32 -246
- ophyd_async/core/mock_signal_backend.py +82 -0
- ophyd_async/core/mock_signal_utils.py +145 -0
- ophyd_async/core/signal.py +225 -58
- ophyd_async/core/signal_backend.py +8 -5
- ophyd_async/core/{sim_signal_backend.py → soft_signal_backend.py} +51 -49
- ophyd_async/core/standard_readable.py +212 -23
- ophyd_async/core/utils.py +123 -30
- ophyd_async/epics/_backend/_aioca.py +42 -44
- ophyd_async/epics/_backend/_p4p.py +96 -52
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +8 -4
- ophyd_async/epics/areadetector/aravis.py +63 -0
- ophyd_async/epics/areadetector/controllers/__init__.py +2 -1
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +78 -0
- ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +37 -25
- ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +6 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +8 -12
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +38 -0
- ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +8 -5
- ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
- ophyd_async/epics/areadetector/kinetix.py +46 -0
- ophyd_async/epics/areadetector/pilatus.py +45 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +14 -6
- ophyd_async/epics/areadetector/utils.py +2 -12
- ophyd_async/epics/areadetector/vimba.py +43 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +21 -7
- ophyd_async/epics/areadetector/writers/hdf_writer.py +32 -17
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +19 -18
- ophyd_async/epics/areadetector/writers/nd_plugin.py +15 -7
- ophyd_async/epics/demo/__init__.py +75 -49
- ophyd_async/epics/motion/motor.py +67 -53
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +8 -3
- ophyd_async/epics/signal/signal.py +26 -9
- ophyd_async/log.py +130 -0
- ophyd_async/panda/__init__.py +21 -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/plan_stubs/__init__.py +13 -0
- ophyd_async/plan_stubs/ensure_connected.py +22 -0
- ophyd_async/plan_stubs/fly.py +149 -0
- ophyd_async/protocols.py +126 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +103 -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.3.0.dist-info}/METADATA +31 -70
- ophyd_async-0.3.0.dist-info/RECORD +86 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.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.3.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -45,30 +45,29 @@ class OphydDumper(yaml.Dumper):
|
|
|
45
45
|
def get_signal_values(
|
|
46
46
|
signals: Dict[str, SignalRW[Any]], ignore: Optional[List[str]] = None
|
|
47
47
|
) -> Generator[Msg, Sequence[Location[Any]], Dict[str, Any]]:
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
of saving a device.
|
|
48
|
+
"""Get signal values in bulk.
|
|
49
|
+
|
|
50
|
+
Used as part of saving the signals of a device to a yaml file.
|
|
51
|
+
|
|
51
52
|
Parameters
|
|
52
53
|
----------
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
signals : Dict[str, SignalRW]
|
|
55
|
+
Dictionary with pv names and matching SignalRW values. Often the direct result
|
|
56
|
+
of :func:`walk_rw_signals`.
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
ignore : Optional[List[str]]
|
|
59
|
+
Optional list of PVs that should be ignored.
|
|
58
60
|
|
|
59
61
|
Returns
|
|
60
62
|
-------
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Yields:
|
|
65
|
-
Iterator[Dict[str, Any]]: The Location of a signal
|
|
63
|
+
Dict[str, Any]
|
|
64
|
+
A dictionary containing pv names and their associated values. Ignored pvs are
|
|
65
|
+
set to None.
|
|
66
66
|
|
|
67
67
|
See Also
|
|
68
68
|
--------
|
|
69
69
|
:func:`ophyd_async.core.walk_rw_signals`
|
|
70
70
|
:func:`ophyd_async.core.save_to_yaml`
|
|
71
|
-
|
|
72
71
|
"""
|
|
73
72
|
|
|
74
73
|
ignore = ignore or []
|
|
@@ -93,9 +92,11 @@ def get_signal_values(
|
|
|
93
92
|
def walk_rw_signals(
|
|
94
93
|
device: Device, path_prefix: Optional[str] = ""
|
|
95
94
|
) -> Dict[str, SignalRW[Any]]:
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
paths in a dictionary. Used as
|
|
95
|
+
"""Retrieve all SignalRWs from a device.
|
|
96
|
+
|
|
97
|
+
Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
|
|
98
|
+
part of saving and loading a device.
|
|
99
|
+
|
|
99
100
|
Parameters
|
|
100
101
|
----------
|
|
101
102
|
device : Device
|
|
@@ -131,9 +132,7 @@ def walk_rw_signals(
|
|
|
131
132
|
|
|
132
133
|
|
|
133
134
|
def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
|
|
134
|
-
"""
|
|
135
|
-
Serialises and saves a phase or a set of phases of a device's SignalRW's to a
|
|
136
|
-
yaml file.
|
|
135
|
+
"""Plan which serialises a phase or set of phases of SignalRWs to a yaml file.
|
|
137
136
|
|
|
138
137
|
Parameters
|
|
139
138
|
----------
|
|
@@ -163,8 +162,7 @@ def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
|
|
|
163
162
|
|
|
164
163
|
|
|
165
164
|
def load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:
|
|
166
|
-
"""
|
|
167
|
-
Returns a list of dicts with saved signal values from a yaml file
|
|
165
|
+
"""Plan that returns a list of dicts with saved signal values from a yaml file.
|
|
168
166
|
|
|
169
167
|
Parameters
|
|
170
168
|
----------
|
|
@@ -183,18 +181,22 @@ def load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:
|
|
|
183
181
|
def set_signal_values(
|
|
184
182
|
signals: Dict[str, SignalRW[Any]], values: Sequence[Dict[str, Any]]
|
|
185
183
|
) -> Generator[Msg, None, None]:
|
|
186
|
-
"""
|
|
187
|
-
|
|
184
|
+
"""Maps signals from a yaml file into device signals.
|
|
185
|
+
|
|
186
|
+
``values`` contains signal values in phases, which are loaded in sequentially
|
|
187
|
+
into the provided signals, to ensure signals are set in the correct order.
|
|
188
188
|
|
|
189
189
|
Parameters
|
|
190
190
|
----------
|
|
191
191
|
signals : Dict[str, SignalRW[Any]]
|
|
192
192
|
Dictionary of named signals to be updated if value found in values argument.
|
|
193
|
+
Can be the output of :func:`walk_rw_signals()` for a device.
|
|
193
194
|
|
|
194
195
|
values : Sequence[Dict[str, Any]]
|
|
195
196
|
List of dictionaries of signal name and value pairs, if a signal matches
|
|
196
197
|
the name of one in the signals argument, sets the signal to that value.
|
|
197
198
|
The groups of signals are loaded in their list order.
|
|
199
|
+
Can be the output of :func:`load_from_yaml()` for a yaml file.
|
|
198
200
|
|
|
199
201
|
See Also
|
|
200
202
|
--------
|
|
@@ -206,8 +208,79 @@ def set_signal_values(
|
|
|
206
208
|
for phase_number, phase in enumerate(values):
|
|
207
209
|
# Key is signal name
|
|
208
210
|
for key, value in phase.items():
|
|
211
|
+
# Skip ignored values
|
|
212
|
+
if value is None:
|
|
213
|
+
continue
|
|
214
|
+
|
|
209
215
|
if key in signals:
|
|
210
216
|
yield from abs_set(
|
|
211
217
|
signals[key], value, group=f"load-phase{phase_number}"
|
|
212
218
|
)
|
|
219
|
+
|
|
213
220
|
yield from wait(f"load-phase{phase_number}")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def load_device(device: Device, path: str):
|
|
224
|
+
"""Plan which loads PVs from a yaml file into a device.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
device: Device
|
|
229
|
+
The device to load PVs into
|
|
230
|
+
path: str
|
|
231
|
+
Path of the yaml file to load
|
|
232
|
+
|
|
233
|
+
See Also
|
|
234
|
+
--------
|
|
235
|
+
:func:`ophyd_async.core.save_device`
|
|
236
|
+
"""
|
|
237
|
+
values = load_from_yaml(path)
|
|
238
|
+
signals_to_set = walk_rw_signals(device)
|
|
239
|
+
yield from set_signal_values(signals_to_set, values)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def all_at_once(values: Dict[str, Any]) -> Sequence[Dict[str, Any]]:
|
|
243
|
+
"""Sort all the values into a single phase so they are set all at once"""
|
|
244
|
+
return [values]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def save_device(
|
|
248
|
+
device: Device,
|
|
249
|
+
path: str,
|
|
250
|
+
sorter: Callable[[Dict[str, Any]], Sequence[Dict[str, Any]]] = all_at_once,
|
|
251
|
+
ignore: Optional[List[str]] = None,
|
|
252
|
+
):
|
|
253
|
+
"""Plan that saves the state of all PV's on a device using a sorter.
|
|
254
|
+
|
|
255
|
+
The default sorter assumes all saved PVs can be loaded at once, and therefore
|
|
256
|
+
can be saved at one time, i.e. all PVs will appear on one list in the
|
|
257
|
+
resulting yaml file.
|
|
258
|
+
|
|
259
|
+
This can be a problem, because when the yaml is ingested with
|
|
260
|
+
:func:`ophyd_async.core.load_device`, it will set all of those PVs at once.
|
|
261
|
+
However, some PV's need to be set before others - this is device specific.
|
|
262
|
+
|
|
263
|
+
Therefore, users should consider the order of device loading and write their
|
|
264
|
+
own sorter algorithms accordingly.
|
|
265
|
+
|
|
266
|
+
See :func:`ophyd_async.panda.phase_sorter` for a valid implementation of the
|
|
267
|
+
sorter.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
device : Device
|
|
272
|
+
The device whose PVs should be saved.
|
|
273
|
+
|
|
274
|
+
path : str
|
|
275
|
+
The path where the resulting yaml should be saved to
|
|
276
|
+
|
|
277
|
+
sorter : Callable[[Dict[str, Any]], Sequence[Dict[str, Any]]]
|
|
278
|
+
|
|
279
|
+
ignore : Optional[List[str]]
|
|
280
|
+
|
|
281
|
+
See Also
|
|
282
|
+
--------
|
|
283
|
+
:func:`ophyd_async.core.load_device`
|
|
284
|
+
"""
|
|
285
|
+
values = yield from get_signal_values(walk_rw_signals(device), ignore=ignore)
|
|
286
|
+
save_to_yaml(sorter(values), path)
|
ophyd_async/core/flyer.py
CHANGED
|
@@ -1,170 +1,29 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import time
|
|
3
1
|
from abc import ABC, abstractmethod
|
|
4
|
-
from
|
|
5
|
-
from typing import (
|
|
6
|
-
AsyncIterator,
|
|
7
|
-
Callable,
|
|
8
|
-
Dict,
|
|
9
|
-
Generic,
|
|
10
|
-
List,
|
|
11
|
-
Optional,
|
|
12
|
-
Sequence,
|
|
13
|
-
TypeVar,
|
|
14
|
-
)
|
|
2
|
+
from typing import Dict, Generic, Sequence, TypeVar
|
|
15
3
|
|
|
16
|
-
from bluesky.protocols import
|
|
17
|
-
Asset,
|
|
18
|
-
Collectable,
|
|
19
|
-
Descriptor,
|
|
20
|
-
Flyable,
|
|
21
|
-
HasHints,
|
|
22
|
-
Hints,
|
|
23
|
-
Movable,
|
|
24
|
-
Reading,
|
|
25
|
-
Stageable,
|
|
26
|
-
WritesExternalAssets,
|
|
27
|
-
)
|
|
4
|
+
from bluesky.protocols import DataKey, Flyable, Preparable, Reading, Stageable
|
|
28
5
|
|
|
29
6
|
from .async_status import AsyncStatus
|
|
30
|
-
from .detector import DetectorControl, DetectorTrigger, DetectorWriter
|
|
31
7
|
from .device import Device
|
|
32
8
|
from .signal import SignalR
|
|
33
|
-
from .utils import
|
|
9
|
+
from .utils import merge_gathered_dicts
|
|
34
10
|
|
|
35
11
|
T = TypeVar("T")
|
|
36
12
|
|
|
37
13
|
|
|
38
|
-
@dataclass(frozen=True)
|
|
39
|
-
class TriggerInfo:
|
|
40
|
-
#: Number of triggers that will be sent
|
|
41
|
-
num: int
|
|
42
|
-
#: Sort of triggers that will be sent
|
|
43
|
-
trigger: DetectorTrigger
|
|
44
|
-
#: What is the minimum deadtime between triggers
|
|
45
|
-
deadtime: float
|
|
46
|
-
#: What is the maximum high time of the triggers
|
|
47
|
-
livetime: float
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class DetectorGroupLogic(ABC):
|
|
51
|
-
# Read multipliers here, exposure is set in the plan
|
|
52
|
-
|
|
53
|
-
@abstractmethod
|
|
54
|
-
async def open(self) -> Dict[str, Descriptor]:
|
|
55
|
-
"""Open all writers, wait for them to be open and return their descriptors"""
|
|
56
|
-
|
|
57
|
-
@abstractmethod
|
|
58
|
-
async def ensure_armed(self, trigger_info: TriggerInfo):
|
|
59
|
-
"""Ensure the detectors are armed, return AsyncStatus that waits for disarm."""
|
|
60
|
-
|
|
61
|
-
@abstractmethod
|
|
62
|
-
def collect_asset_docs(self) -> AsyncIterator[Asset]:
|
|
63
|
-
"""Collect asset docs from all writers"""
|
|
64
|
-
|
|
65
|
-
@abstractmethod
|
|
66
|
-
async def wait_for_index(
|
|
67
|
-
self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
|
|
68
|
-
):
|
|
69
|
-
"""Wait until a specific index is ready to be collected"""
|
|
70
|
-
|
|
71
|
-
@abstractmethod
|
|
72
|
-
async def disarm(self):
|
|
73
|
-
"""Disarm detectors"""
|
|
74
|
-
|
|
75
|
-
@abstractmethod
|
|
76
|
-
async def close(self):
|
|
77
|
-
"""Close all writers and wait for them to be closed"""
|
|
78
|
-
|
|
79
|
-
@abstractmethod
|
|
80
|
-
def hints(self) -> Hints:
|
|
81
|
-
"""Produce hints specifying which dataset(s) are most important"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class SameTriggerDetectorGroupLogic(DetectorGroupLogic):
|
|
85
|
-
def __init__(
|
|
86
|
-
self,
|
|
87
|
-
controllers: Sequence[DetectorControl],
|
|
88
|
-
writers: Sequence[DetectorWriter],
|
|
89
|
-
) -> None:
|
|
90
|
-
self._controllers = controllers
|
|
91
|
-
self._writers = writers
|
|
92
|
-
self._arm_statuses: Sequence[AsyncStatus] = ()
|
|
93
|
-
self._trigger_info: Optional[TriggerInfo] = None
|
|
94
|
-
|
|
95
|
-
async def open(self) -> Dict[str, Descriptor]:
|
|
96
|
-
return await merge_gathered_dicts(writer.open() for writer in self._writers)
|
|
97
|
-
|
|
98
|
-
async def ensure_armed(self, trigger_info: TriggerInfo):
|
|
99
|
-
if (
|
|
100
|
-
not self._arm_statuses
|
|
101
|
-
or any(status.done for status in self._arm_statuses)
|
|
102
|
-
or trigger_info != self._trigger_info
|
|
103
|
-
):
|
|
104
|
-
# We need to re-arm
|
|
105
|
-
await self.disarm()
|
|
106
|
-
for controller in self._controllers:
|
|
107
|
-
required = controller.get_deadtime(trigger_info.livetime)
|
|
108
|
-
assert required <= trigger_info.deadtime, (
|
|
109
|
-
f"Detector {controller} needs at least {required}s deadtime, "
|
|
110
|
-
f"but trigger logic provides only {trigger_info.deadtime}s"
|
|
111
|
-
)
|
|
112
|
-
self._arm_statuses = await gather_list(
|
|
113
|
-
controller.arm(
|
|
114
|
-
trigger=trigger_info.trigger, exposure=trigger_info.livetime
|
|
115
|
-
)
|
|
116
|
-
for controller in self._controllers
|
|
117
|
-
)
|
|
118
|
-
self._trigger_info = trigger_info
|
|
119
|
-
|
|
120
|
-
async def collect_asset_docs(self) -> AsyncIterator[Asset]:
|
|
121
|
-
# the below is confusing: gather_list does return an awaitable, but it itself
|
|
122
|
-
# is a coroutine so we must call await twice...
|
|
123
|
-
indices_written = min(
|
|
124
|
-
await gather_list(writer.get_indices_written() for writer in self._writers)
|
|
125
|
-
)
|
|
126
|
-
for writer in self._writers:
|
|
127
|
-
async for doc in writer.collect_stream_docs(indices_written):
|
|
128
|
-
yield doc
|
|
129
|
-
|
|
130
|
-
async def wait_for_index(
|
|
131
|
-
self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
|
|
132
|
-
):
|
|
133
|
-
await gather_list(
|
|
134
|
-
writer.wait_for_index(index, timeout=timeout) for writer in self._writers
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
async def disarm(self):
|
|
138
|
-
await gather_list(controller.disarm() for controller in self._controllers)
|
|
139
|
-
await gather_list(self._arm_statuses)
|
|
140
|
-
|
|
141
|
-
async def close(self):
|
|
142
|
-
await gather_list(writer.close() for writer in self._writers)
|
|
143
|
-
|
|
144
|
-
def hints(self) -> Hints:
|
|
145
|
-
return {
|
|
146
|
-
"fields": [
|
|
147
|
-
field
|
|
148
|
-
for writer in self._writers
|
|
149
|
-
if hasattr(writer, "hints")
|
|
150
|
-
for field in writer.hints.get("fields")
|
|
151
|
-
]
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
14
|
class TriggerLogic(ABC, Generic[T]):
|
|
156
|
-
@abstractmethod
|
|
157
|
-
def trigger_info(self, value: T) -> TriggerInfo:
|
|
158
|
-
"""Return info about triggers that will be produced for a given value"""
|
|
159
|
-
|
|
160
15
|
@abstractmethod
|
|
161
16
|
async def prepare(self, value: T):
|
|
162
17
|
"""Move to the start of the flyscan"""
|
|
163
18
|
|
|
164
19
|
@abstractmethod
|
|
165
|
-
async def
|
|
20
|
+
async def kickoff(self):
|
|
166
21
|
"""Start the flyscan"""
|
|
167
22
|
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def complete(self):
|
|
25
|
+
"""Block until the flyscan is done"""
|
|
26
|
+
|
|
168
27
|
@abstractmethod
|
|
169
28
|
async def stop(self):
|
|
170
29
|
"""Stop flying and wait everything to be stopped"""
|
|
@@ -172,63 +31,50 @@ class TriggerLogic(ABC, Generic[T]):
|
|
|
172
31
|
|
|
173
32
|
class HardwareTriggeredFlyable(
|
|
174
33
|
Device,
|
|
175
|
-
Movable,
|
|
176
34
|
Stageable,
|
|
35
|
+
Preparable,
|
|
177
36
|
Flyable,
|
|
178
|
-
Collectable,
|
|
179
|
-
WritesExternalAssets,
|
|
180
|
-
HasHints,
|
|
181
37
|
Generic[T],
|
|
182
38
|
):
|
|
183
39
|
def __init__(
|
|
184
40
|
self,
|
|
185
|
-
detector_group_logic: DetectorGroupLogic,
|
|
186
41
|
trigger_logic: TriggerLogic[T],
|
|
187
|
-
configuration_signals: Sequence[SignalR],
|
|
188
|
-
trigger_to_frame_timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
42
|
+
configuration_signals: Sequence[SignalR] = (),
|
|
189
43
|
name: str = "",
|
|
190
44
|
):
|
|
191
|
-
self._detector_group_logic = detector_group_logic
|
|
192
45
|
self._trigger_logic = trigger_logic
|
|
193
46
|
self._configuration_signals = tuple(configuration_signals)
|
|
194
|
-
self._describe: Dict[str, Descriptor] = {}
|
|
195
|
-
self._watchers: List[Callable] = []
|
|
196
|
-
self._fly_status: Optional[AsyncStatus] = None
|
|
197
|
-
self._fly_start = 0.0
|
|
198
|
-
self._offset = 0 # Add this to index to get frame number
|
|
199
|
-
self._current_frame = 0 # The current frame we are on
|
|
200
|
-
self._last_frame = 0 # The last frame that will be emitted
|
|
201
|
-
self._trigger_to_frame_timeout = trigger_to_frame_timeout
|
|
202
47
|
super().__init__(name=name)
|
|
203
48
|
|
|
49
|
+
@property
|
|
50
|
+
def trigger_logic(self) -> TriggerLogic[T]:
|
|
51
|
+
return self._trigger_logic
|
|
52
|
+
|
|
204
53
|
@AsyncStatus.wrap
|
|
205
54
|
async def stage(self) -> None:
|
|
206
55
|
await self.unstage()
|
|
207
|
-
self._describe = await self._detector_group_logic.open()
|
|
208
|
-
self._offset = 0
|
|
209
|
-
self._current_frame = 0
|
|
210
56
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
# so subtract current_frame from both sides
|
|
215
|
-
return AsyncStatus(self._set(value))
|
|
57
|
+
@AsyncStatus.wrap
|
|
58
|
+
async def unstage(self) -> None:
|
|
59
|
+
await self._trigger_logic.stop()
|
|
216
60
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
await self._prepare(value)
|
|
61
|
+
def prepare(self, value: T) -> AsyncStatus:
|
|
62
|
+
"""Setup trajectories"""
|
|
63
|
+
return AsyncStatus(self._prepare(value))
|
|
221
64
|
|
|
222
|
-
async def _prepare(self, value: T):
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await asyncio.gather(
|
|
226
|
-
self._detector_group_logic.ensure_armed(trigger_info),
|
|
227
|
-
self._trigger_logic.prepare(value),
|
|
228
|
-
)
|
|
229
|
-
self._last_frame = self._current_frame + trigger_info.num
|
|
65
|
+
async def _prepare(self, value: T) -> None:
|
|
66
|
+
# Move to start and setup the flyscan
|
|
67
|
+
await self._trigger_logic.prepare(value)
|
|
230
68
|
|
|
231
|
-
|
|
69
|
+
@AsyncStatus.wrap
|
|
70
|
+
async def kickoff(self) -> None:
|
|
71
|
+
await self._trigger_logic.kickoff()
|
|
72
|
+
|
|
73
|
+
@AsyncStatus.wrap
|
|
74
|
+
async def complete(self) -> None:
|
|
75
|
+
await self._trigger_logic.complete()
|
|
76
|
+
|
|
77
|
+
async def describe_configuration(self) -> Dict[str, DataKey]:
|
|
232
78
|
return await merge_gathered_dicts(
|
|
233
79
|
[sig.describe() for sig in self._configuration_signals]
|
|
234
80
|
)
|
|
@@ -237,63 +83,3 @@ class HardwareTriggeredFlyable(
|
|
|
237
83
|
return await merge_gathered_dicts(
|
|
238
84
|
[sig.read() for sig in self._configuration_signals]
|
|
239
85
|
)
|
|
240
|
-
|
|
241
|
-
async def describe_collect(self) -> Dict[str, Descriptor]:
|
|
242
|
-
return self._describe
|
|
243
|
-
|
|
244
|
-
@AsyncStatus.wrap
|
|
245
|
-
async def kickoff(self) -> None:
|
|
246
|
-
self._watchers = []
|
|
247
|
-
self._fly_status = AsyncStatus(self._fly(), self._watchers)
|
|
248
|
-
self._fly_start = time.monotonic()
|
|
249
|
-
|
|
250
|
-
async def _fly(self) -> None:
|
|
251
|
-
await self._trigger_logic.start()
|
|
252
|
-
# Wait for all detectors to have written up to a particular frame
|
|
253
|
-
await self._detector_group_logic.wait_for_index(
|
|
254
|
-
self._last_frame - self._offset, timeout=self._trigger_to_frame_timeout
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
async def collect_asset_docs(self) -> AsyncIterator[Asset]:
|
|
258
|
-
current_frame = self._current_frame
|
|
259
|
-
stream_datums: List[Asset] = []
|
|
260
|
-
async for asset in self._detector_group_logic.collect_asset_docs():
|
|
261
|
-
name, doc = asset
|
|
262
|
-
if name == "stream_datum":
|
|
263
|
-
current_frame = doc["indices"]["stop"] + self._offset
|
|
264
|
-
# Defer stream_datums until all stream_resources have been produced
|
|
265
|
-
# In a single collect, all the stream_resources are produced first
|
|
266
|
-
# followed by their stream_datums
|
|
267
|
-
stream_datums.append(asset)
|
|
268
|
-
else:
|
|
269
|
-
yield asset
|
|
270
|
-
for asset in stream_datums:
|
|
271
|
-
yield asset
|
|
272
|
-
if current_frame != self._current_frame:
|
|
273
|
-
self._current_frame = current_frame
|
|
274
|
-
for watcher in self._watchers:
|
|
275
|
-
watcher(
|
|
276
|
-
name=self.name,
|
|
277
|
-
current=current_frame,
|
|
278
|
-
initial=0,
|
|
279
|
-
target=self._last_frame,
|
|
280
|
-
unit="",
|
|
281
|
-
precision=0,
|
|
282
|
-
time_elapsed=time.monotonic() - self._fly_start,
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
def complete(self) -> AsyncStatus:
|
|
286
|
-
assert self._fly_status, "Kickoff not run"
|
|
287
|
-
return self._fly_status
|
|
288
|
-
|
|
289
|
-
@AsyncStatus.wrap
|
|
290
|
-
async def unstage(self) -> None:
|
|
291
|
-
await asyncio.gather(
|
|
292
|
-
self._trigger_logic.stop(),
|
|
293
|
-
self._detector_group_logic.close(),
|
|
294
|
-
self._detector_group_logic.disarm(),
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
@property
|
|
298
|
-
def hints(self) -> Hints:
|
|
299
|
-
return self._detector_group_logic.hints()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from typing import Callable, Optional, Type
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
from bluesky.protocols import Descriptor, Reading
|
|
7
|
+
|
|
8
|
+
from ophyd_async.core.signal_backend import SignalBackend
|
|
9
|
+
from ophyd_async.core.soft_signal_backend import SoftSignalBackend
|
|
10
|
+
from ophyd_async.core.utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MockSignalBackend(SignalBackend[T]):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
datatype: Optional[Type[T]] = None,
|
|
17
|
+
initial_backend: Optional[SignalBackend[T]] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
if isinstance(initial_backend, MockSignalBackend):
|
|
20
|
+
raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackends")
|
|
21
|
+
|
|
22
|
+
self.initial_backend = initial_backend
|
|
23
|
+
|
|
24
|
+
if datatype is None:
|
|
25
|
+
assert (
|
|
26
|
+
self.initial_backend
|
|
27
|
+
), "Must supply either initial_backend or datatype"
|
|
28
|
+
datatype = self.initial_backend.datatype
|
|
29
|
+
|
|
30
|
+
self.datatype = datatype
|
|
31
|
+
|
|
32
|
+
if not isinstance(self.initial_backend, SoftSignalBackend):
|
|
33
|
+
# If the backend is a hard signal backend, or not provided,
|
|
34
|
+
# then we create a soft signal to mimic it
|
|
35
|
+
|
|
36
|
+
self.soft_backend = SoftSignalBackend(datatype=datatype)
|
|
37
|
+
else:
|
|
38
|
+
self.soft_backend = self.initial_backend
|
|
39
|
+
|
|
40
|
+
def source(self, name: str) -> str:
|
|
41
|
+
if self.initial_backend:
|
|
42
|
+
return f"mock+{self.initial_backend.source(name)}"
|
|
43
|
+
return f"mock+{name}"
|
|
44
|
+
|
|
45
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def put_mock(self) -> Mock:
|
|
50
|
+
return Mock(name="put", spec=Callable)
|
|
51
|
+
|
|
52
|
+
@cached_property
|
|
53
|
+
def put_proceeds(self) -> asyncio.Event:
|
|
54
|
+
put_proceeds = asyncio.Event()
|
|
55
|
+
put_proceeds.set()
|
|
56
|
+
return put_proceeds
|
|
57
|
+
|
|
58
|
+
async def put(self, value: Optional[T], wait=True, timeout=None):
|
|
59
|
+
self.put_mock(value, wait=wait, timeout=timeout)
|
|
60
|
+
await self.soft_backend.put(value, wait=wait, timeout=timeout)
|
|
61
|
+
|
|
62
|
+
if wait:
|
|
63
|
+
await asyncio.wait_for(self.put_proceeds.wait(), timeout=timeout)
|
|
64
|
+
|
|
65
|
+
def set_value(self, value: T):
|
|
66
|
+
self.soft_backend.set_value(value)
|
|
67
|
+
|
|
68
|
+
async def get_reading(self) -> Reading:
|
|
69
|
+
return await self.soft_backend.get_reading()
|
|
70
|
+
|
|
71
|
+
async def get_value(self) -> T:
|
|
72
|
+
return await self.soft_backend.get_value()
|
|
73
|
+
|
|
74
|
+
async def get_setpoint(self) -> T:
|
|
75
|
+
"""For a soft signal, the setpoint and readback values are the same."""
|
|
76
|
+
return await self.soft_backend.get_setpoint()
|
|
77
|
+
|
|
78
|
+
async def get_datakey(self, source: str) -> Descriptor:
|
|
79
|
+
return await self.soft_backend.get_datakey(source)
|
|
80
|
+
|
|
81
|
+
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
|
82
|
+
self.soft_backend.set_callback(callback)
|