blissdata 0.3.4__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.
- blissdata/__init__.py +17 -0
- blissdata/beacon/__init__.py +1 -0
- blissdata/beacon/_base.py +106 -0
- blissdata/beacon/config.py +24 -0
- blissdata/beacon/data.py +45 -0
- blissdata/beacon/files.py +141 -0
- blissdata/client.py +62 -0
- blissdata/common/__init__.py +15 -0
- blissdata/common/utils.py +159 -0
- blissdata/data/__init__.py +15 -0
- blissdata/data/events/__init__.py +22 -0
- blissdata/data/events/channel.py +149 -0
- blissdata/data/events/lima.py +478 -0
- blissdata/data/events/node.py +28 -0
- blissdata/data/events/scan.py +49 -0
- blissdata/data/events/walk.py +42 -0
- blissdata/data/expiration.py +42 -0
- blissdata/data/lima_image.py +465 -0
- blissdata/data/node.py +1619 -0
- blissdata/data/nodes/__init__.py +11 -0
- blissdata/data/nodes/channel.py +394 -0
- blissdata/data/nodes/dataset.py +82 -0
- blissdata/data/nodes/dataset_collection.py +12 -0
- blissdata/data/nodes/lima.py +422 -0
- blissdata/data/nodes/node_ref_channel.py +34 -0
- blissdata/data/nodes/proposal.py +12 -0
- blissdata/data/nodes/scan.py +192 -0
- blissdata/data/nodes/scan_group.py +12 -0
- blissdata/data/remote_node.py +204 -0
- blissdata/data/scan.py +666 -0
- blissdata/h5api/__init__.py +1 -0
- blissdata/h5api/abstract.py +97 -0
- blissdata/h5api/dynamic_hdf5.py +153 -0
- blissdata/h5api/file_arguments.py +28 -0
- blissdata/h5api/static_hdf5.py +139 -0
- blissdata/h5api/utils/__init__.py +0 -0
- blissdata/h5api/utils/bliss.py +138 -0
- blissdata/h5api/utils/hdf5.py +280 -0
- blissdata/h5api/utils/hdf5_retry.py +98 -0
- blissdata/h5api/utils/lima.py +286 -0
- blissdata/h5api/utils/types.py +13 -0
- blissdata/redis/__init__.py +12 -0
- blissdata/redis/caching.py +390 -0
- blissdata/redis/connection.py +169 -0
- blissdata/redis/manager.py +164 -0
- blissdata/redis/proxy.py +971 -0
- blissdata/redis/scripting.py +24 -0
- blissdata/settings.py +1174 -0
- blissdata/streaming.py +819 -0
- blissdata/streaming_events.py +355 -0
- blissdata/tests/__init__.py +0 -0
- blissdata/tests/beacon/__init__.py +0 -0
- blissdata/tests/beacon/test_config.py +26 -0
- blissdata/tests/beacon/test_data.py +50 -0
- blissdata/tests/beacon/test_files.py +80 -0
- blissdata/tests/conftest.py +0 -0
- blissdata/tests/h5api/__init__.py +0 -0
- blissdata/tests/h5api/scanner.py +390 -0
- blissdata/tests/h5api/test_dynamic_files.py +309 -0
- blissdata/tests/h5api/test_static_files.py +169 -0
- blissdata/tests/redis/__init__.py +0 -0
- blissdata/tests/redis/manager.py +23 -0
- blissdata-0.3.4.dist-info/LICENSE +165 -0
- blissdata-0.3.4.dist-info/METADATA +79 -0
- blissdata-0.3.4.dist-info/RECORD +67 -0
- blissdata-0.3.4.dist-info/WHEEL +5 -0
- blissdata-0.3.4.dist-info/top_level.txt +1 -0
blissdata/data/scan.py
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of the bliss project
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2015-2023 Beamline Control Unit, ESRF
|
|
6
|
+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import numpy
|
|
11
|
+
import gevent
|
|
12
|
+
import gevent.event
|
|
13
|
+
import typing
|
|
14
|
+
import warnings
|
|
15
|
+
import contextlib
|
|
16
|
+
|
|
17
|
+
from typing import Dict
|
|
18
|
+
|
|
19
|
+
from blissdata.data.nodes.channel import ChannelDataNode
|
|
20
|
+
from blissdata.data.node import get_or_create_node
|
|
21
|
+
from blissdata.data.events import Event
|
|
22
|
+
from blissdata.data.events import EventType
|
|
23
|
+
from blissdata.streaming import DataStreamReaderStopHandler
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_data(scan):
|
|
27
|
+
# keep get_data() wrapper for scripts backward compatibility until the
|
|
28
|
+
# new data API provide the same functionality (expected for Bliss 1.12)
|
|
29
|
+
return scan.get_data()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_counter_names(scan):
|
|
33
|
+
"""
|
|
34
|
+
Return a list of counter names
|
|
35
|
+
"""
|
|
36
|
+
return [
|
|
37
|
+
node.name for node in scan.nodes.values() if isinstance(node, ChannelDataNode)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ScansObserver:
|
|
42
|
+
"""
|
|
43
|
+
Observer for the `ScansWatcher`.
|
|
44
|
+
|
|
45
|
+
Provides methods which can be inherited to follow the life cycle of the
|
|
46
|
+
scans of a session.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def on_event_received(self, event: Event):
|
|
50
|
+
"""
|
|
51
|
+
Called upon new event
|
|
52
|
+
|
|
53
|
+
Mostly used for backward compatibility with `DefaultScanObserver`.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def on_scan_created(self, scan_db_name: str, scan_info: Dict):
|
|
58
|
+
"""
|
|
59
|
+
Called upon scan created (devices are not yet prepared).
|
|
60
|
+
|
|
61
|
+
Arguments:
|
|
62
|
+
scan_db_name: Identifier of the scan
|
|
63
|
+
scan_info: Dictionary containing scan metadata
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def on_scan_started(self, scan_db_name: str, scan_info: Dict):
|
|
68
|
+
"""
|
|
69
|
+
Called upon scan started (the devices was prepared).
|
|
70
|
+
|
|
71
|
+
Arguments:
|
|
72
|
+
scan_db_name: Identifier of the scan
|
|
73
|
+
scan_info: Dictionary containing scan metadata updated with metadata
|
|
74
|
+
prepared metadata from controllers
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def on_child_created(self, scan_db_name: str, node):
|
|
79
|
+
"""
|
|
80
|
+
Called upon scan child creation (e.g. channel node)
|
|
81
|
+
|
|
82
|
+
Arguments:
|
|
83
|
+
scan_db_name: Identifier of the parent scan
|
|
84
|
+
node: Redis node of this child
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def on_scalar_data_received(
|
|
89
|
+
self,
|
|
90
|
+
scan_db_name: str,
|
|
91
|
+
channel_name: str,
|
|
92
|
+
index: int,
|
|
93
|
+
data_bunch: typing.Union[list, numpy.ndarray],
|
|
94
|
+
):
|
|
95
|
+
"""
|
|
96
|
+
Called upon a bunch of scalar data (0dim) from a `top_master` was
|
|
97
|
+
received.
|
|
98
|
+
|
|
99
|
+
Arguments:
|
|
100
|
+
scan_db_name: Identifier of the parent scan
|
|
101
|
+
channel_name: Name of the updated channel
|
|
102
|
+
index: Start index of the data bunch in the real data stream.
|
|
103
|
+
There could be wholes between 2 bunches of data.
|
|
104
|
+
data_bunch: The list of data received, as a bunch of data.
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def on_ndim_data_received(
|
|
109
|
+
self,
|
|
110
|
+
scan_db_name: str,
|
|
111
|
+
channel_name: str,
|
|
112
|
+
dim: int,
|
|
113
|
+
index: int,
|
|
114
|
+
data_bunch: typing.Union[list, numpy.ndarray],
|
|
115
|
+
):
|
|
116
|
+
"""Called upon a ndim data (except 0dim, except data ref) data was
|
|
117
|
+
received.
|
|
118
|
+
|
|
119
|
+
- For 0dim data, see `on_scalar_data_received`.
|
|
120
|
+
|
|
121
|
+
Arguments:
|
|
122
|
+
scan_db_name: Identifier of the parent scan
|
|
123
|
+
channel_name: Name of the channel emitting the data
|
|
124
|
+
dim: Dimension of this data (MCA is 1, image is 2)
|
|
125
|
+
index: Start index of the data bunch in the real data stream.
|
|
126
|
+
There could be wholes between 2 bunches of data.
|
|
127
|
+
data_bunch: The list of data received, as a bunch of data.
|
|
128
|
+
"""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def on_lima_ref_received(
|
|
132
|
+
self, scan_db_name: str, channel_name: str, dim: int, source_node, event_data
|
|
133
|
+
):
|
|
134
|
+
"""Called upon a ndim (except 0dim) data was received.
|
|
135
|
+
|
|
136
|
+
For 0dim data, see `on_scalar_data_received`.
|
|
137
|
+
|
|
138
|
+
Arguments:
|
|
139
|
+
scan_db_name: Identifier of the parent scan
|
|
140
|
+
channel_name: Name of the channel emitting the data
|
|
141
|
+
dim: Dimension of this data
|
|
142
|
+
source_node: Node containing the updated data
|
|
143
|
+
event_data: Data of the event
|
|
144
|
+
"""
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
def on_scan_finished(self, scan_db_name: str, scan_info: Dict):
|
|
148
|
+
"""
|
|
149
|
+
Called upon scan end.
|
|
150
|
+
|
|
151
|
+
Arguments:
|
|
152
|
+
scan_db_name: Identifier of the parent scan
|
|
153
|
+
scan_info: Dictionary containing scan metadata updated with
|
|
154
|
+
prepared and finished metadata from controllers
|
|
155
|
+
Other fields like positioners and datetime are also
|
|
156
|
+
updated.
|
|
157
|
+
"""
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ScansWatcher:
|
|
162
|
+
"""
|
|
163
|
+
Watch scans from a specific session.
|
|
164
|
+
|
|
165
|
+
Arguments:
|
|
166
|
+
session_name: Name of the BLISS session
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(self, session_name: str):
|
|
170
|
+
self._session_name = session_name
|
|
171
|
+
self._exclude_existing_scans = False
|
|
172
|
+
self._watch_scan_group = False
|
|
173
|
+
self._observer: ScansObserver = None
|
|
174
|
+
|
|
175
|
+
self._running = False
|
|
176
|
+
"""True if the watcher was started."""
|
|
177
|
+
|
|
178
|
+
self._running_scans = set()
|
|
179
|
+
"""Store running scans"""
|
|
180
|
+
|
|
181
|
+
self._ready_event = gevent.event.Event()
|
|
182
|
+
"""Handle the ready event"""
|
|
183
|
+
|
|
184
|
+
self._terminated_event = gevent.event.Event()
|
|
185
|
+
"""Handle the end event"""
|
|
186
|
+
|
|
187
|
+
self._no_scans_event = gevent.event.Event()
|
|
188
|
+
"""Handle the amount of listened scans"""
|
|
189
|
+
|
|
190
|
+
self._stop_handler = DataStreamReaderStopHandler()
|
|
191
|
+
"""Handler to be able to stop the event loop"""
|
|
192
|
+
|
|
193
|
+
self._no_scans_event.set()
|
|
194
|
+
|
|
195
|
+
def wait_ready(self, timeout: float = None):
|
|
196
|
+
"""Wait until the scan watcher is ready to receive new event.
|
|
197
|
+
|
|
198
|
+
The steps between `started` and `ready` can takes few seconds depending
|
|
199
|
+
on the amount of data and the load of Redis.
|
|
200
|
+
|
|
201
|
+
Arguments:
|
|
202
|
+
timeout: If not `None`, it should be a floating point number
|
|
203
|
+
specifying a timeout for the operation in seconds
|
|
204
|
+
(or fractions thereof).
|
|
205
|
+
"""
|
|
206
|
+
self._ready_event.wait(timeout=timeout)
|
|
207
|
+
|
|
208
|
+
def wait_no_more_running_scans(self, timeout: float = None):
|
|
209
|
+
"""Wait until there is no more running scans in this watcher."""
|
|
210
|
+
self._no_scans_event.wait(timeout=timeout)
|
|
211
|
+
|
|
212
|
+
def wait_terminated(self, timeout: float = None):
|
|
213
|
+
"""Wait until the scan watcher is terminated.
|
|
214
|
+
|
|
215
|
+
The steps between `started` and `ready` can takes few seconds depending
|
|
216
|
+
on the amount of data and the load of Redis.
|
|
217
|
+
|
|
218
|
+
Arguments:
|
|
219
|
+
timeout: If not `None`, it should be a floating point number
|
|
220
|
+
specifying a timeout for the operation in seconds
|
|
221
|
+
(or fractions thereof).
|
|
222
|
+
"""
|
|
223
|
+
self._terminated_event.wait(timeout=timeout)
|
|
224
|
+
|
|
225
|
+
def running_scan_names(self) -> typing.Sequence[str]:
|
|
226
|
+
"""
|
|
227
|
+
Returns the known running scans with there db names.
|
|
228
|
+
|
|
229
|
+
Only managed scans are listed here. For example, if `watch_scan_group`
|
|
230
|
+
was set to False, this scans will not be shown.
|
|
231
|
+
"""
|
|
232
|
+
return list(self._running_scans)
|
|
233
|
+
|
|
234
|
+
def set_exclude_existing_scans(self, exclude: bool):
|
|
235
|
+
"""
|
|
236
|
+
Include or exclude existing scans. Default is False.
|
|
237
|
+
|
|
238
|
+
Will become True by default in the future.
|
|
239
|
+
|
|
240
|
+
It have to be set before start.
|
|
241
|
+
"""
|
|
242
|
+
assert not self._running
|
|
243
|
+
self._exclude_existing_scans = exclude
|
|
244
|
+
|
|
245
|
+
def set_watch_scan_group(self, watch: bool):
|
|
246
|
+
"""
|
|
247
|
+
Set to True to include scan groups like any other scans. Default is False.
|
|
248
|
+
|
|
249
|
+
It have to be set before start.
|
|
250
|
+
"""
|
|
251
|
+
assert not self._running
|
|
252
|
+
self._watch_scan_group = watch
|
|
253
|
+
|
|
254
|
+
def set_observer(self, observer: ScansObserver):
|
|
255
|
+
"""
|
|
256
|
+
Set the observer to use with this watcher process.
|
|
257
|
+
|
|
258
|
+
If not set, the `run` method will raise an exception.
|
|
259
|
+
"""
|
|
260
|
+
assert not self._running
|
|
261
|
+
self._observer = observer
|
|
262
|
+
|
|
263
|
+
def _set_stop_handler(self, stop_handler):
|
|
264
|
+
"""
|
|
265
|
+
Backward compatibility code with `watch_session_scans`.
|
|
266
|
+
|
|
267
|
+
This function have to be removed with `watch_session_scans`.
|
|
268
|
+
"""
|
|
269
|
+
assert not self._running
|
|
270
|
+
self._stop_handler = stop_handler
|
|
271
|
+
|
|
272
|
+
def _get_scan_db_name_from_child(self, db_name: str) -> str:
|
|
273
|
+
"""
|
|
274
|
+
Returns the scan_db_name from the db_name of a child.
|
|
275
|
+
|
|
276
|
+
It also works with the `scan_db_name`.
|
|
277
|
+
"""
|
|
278
|
+
for key in self._running_scans:
|
|
279
|
+
if db_name.startswith(key):
|
|
280
|
+
return key
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
@contextlib.contextmanager
|
|
284
|
+
def watch(self):
|
|
285
|
+
"""Context manager to start and stop the watcher.
|
|
286
|
+
|
|
287
|
+
It uses a gevent spawn.
|
|
288
|
+
|
|
289
|
+
Yield:
|
|
290
|
+
The spawned greenlet
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
gwatcher = gevent.spawn(self.run)
|
|
294
|
+
gwatcher.name = "bliss_scans_watcher"
|
|
295
|
+
self.wait_ready(timeout=3)
|
|
296
|
+
yield gwatcher
|
|
297
|
+
finally:
|
|
298
|
+
self.wait_no_more_running_scans(timeout=2)
|
|
299
|
+
self.stop()
|
|
300
|
+
try:
|
|
301
|
+
self.wait_terminated(timeout=1)
|
|
302
|
+
finally:
|
|
303
|
+
try:
|
|
304
|
+
gwatcher.join(timeout=1)
|
|
305
|
+
except Exception:
|
|
306
|
+
gwatcher.kill()
|
|
307
|
+
|
|
308
|
+
def run(self):
|
|
309
|
+
"""
|
|
310
|
+
Run watching scan events.
|
|
311
|
+
|
|
312
|
+
This method is blocking. But can be terminated by calling `stop`.
|
|
313
|
+
|
|
314
|
+
Any scan node that is created before the `ready_event` will not be watched
|
|
315
|
+
when `exclude_existing_scans` is True.
|
|
316
|
+
"""
|
|
317
|
+
assert not self._running
|
|
318
|
+
self._terminated_event.clear()
|
|
319
|
+
self._running = True
|
|
320
|
+
try:
|
|
321
|
+
session_node = get_or_create_node(self._session_name, node_type="session")
|
|
322
|
+
if session_node is None:
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
observer = self._observer
|
|
326
|
+
if observer is None:
|
|
327
|
+
raise RuntimeError("No observer was set")
|
|
328
|
+
|
|
329
|
+
if self._exclude_existing_scans:
|
|
330
|
+
exclude_existing_children = "scan", "scan_group"
|
|
331
|
+
else:
|
|
332
|
+
exclude_existing_children = None
|
|
333
|
+
|
|
334
|
+
for event in session_node.walk_on_new_events(
|
|
335
|
+
stop_handler=self._stop_handler,
|
|
336
|
+
exclude_existing_children=exclude_existing_children,
|
|
337
|
+
started_event=self._ready_event,
|
|
338
|
+
):
|
|
339
|
+
event_type, node, event_data = event
|
|
340
|
+
try:
|
|
341
|
+
observer.on_event_received(event)
|
|
342
|
+
except Exception:
|
|
343
|
+
sys.excepthook(*sys.exc_info())
|
|
344
|
+
|
|
345
|
+
if event_type == EventType.NEW_NODE:
|
|
346
|
+
node_type = node.type
|
|
347
|
+
db_name = node.db_name
|
|
348
|
+
if node_type == "scan":
|
|
349
|
+
# New scan was created
|
|
350
|
+
scan_info = node.info.get_all()
|
|
351
|
+
self._running_scans.add(db_name)
|
|
352
|
+
self._no_scans_event.clear()
|
|
353
|
+
observer.on_scan_created(db_name, scan_info)
|
|
354
|
+
elif node_type == "scan_group":
|
|
355
|
+
if self._watch_scan_group:
|
|
356
|
+
# New scan was created
|
|
357
|
+
scan_info = node.info.get_all()
|
|
358
|
+
self._running_scans.add(db_name)
|
|
359
|
+
self._no_scans_event.clear()
|
|
360
|
+
observer.on_scan_created(db_name, scan_info)
|
|
361
|
+
else:
|
|
362
|
+
scan_db_name = self._get_scan_db_name_from_child(db_name)
|
|
363
|
+
if scan_db_name is not None:
|
|
364
|
+
try:
|
|
365
|
+
observer.on_child_created(scan_db_name, node)
|
|
366
|
+
except Exception:
|
|
367
|
+
sys.excepthook(*sys.exc_info())
|
|
368
|
+
elif event_type == EventType.NEW_DATA:
|
|
369
|
+
db_name = node.db_name
|
|
370
|
+
if not hasattr(node, "fullname"):
|
|
371
|
+
# not a node we want to do anything with here
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
fullname = node.fullname
|
|
375
|
+
|
|
376
|
+
scan_db_name = self._get_scan_db_name_from_child(db_name)
|
|
377
|
+
if scan_db_name is not None:
|
|
378
|
+
if node.type == "channel":
|
|
379
|
+
description = event_data.description
|
|
380
|
+
shape = description.get("shape")
|
|
381
|
+
dim = len(shape)
|
|
382
|
+
if dim == 0:
|
|
383
|
+
try:
|
|
384
|
+
observer.on_scalar_data_received(
|
|
385
|
+
scan_db_name=scan_db_name,
|
|
386
|
+
channel_name=fullname,
|
|
387
|
+
index=event_data.first_index,
|
|
388
|
+
data_bunch=event_data.data,
|
|
389
|
+
)
|
|
390
|
+
except Exception:
|
|
391
|
+
sys.excepthook(*sys.exc_info())
|
|
392
|
+
else:
|
|
393
|
+
try:
|
|
394
|
+
observer.on_ndim_data_received(
|
|
395
|
+
scan_db_name=scan_db_name,
|
|
396
|
+
channel_name=fullname,
|
|
397
|
+
dim=dim,
|
|
398
|
+
index=event_data.first_index,
|
|
399
|
+
data_bunch=event_data.data,
|
|
400
|
+
)
|
|
401
|
+
except Exception:
|
|
402
|
+
sys.excepthook(*sys.exc_info())
|
|
403
|
+
elif node.type == "lima":
|
|
404
|
+
# Lima and only Lima deals with ref for now
|
|
405
|
+
# FIXME: It would be good to have a dedicated event type for that
|
|
406
|
+
try:
|
|
407
|
+
observer.on_lima_ref_received(
|
|
408
|
+
scan_db_name=scan_db_name,
|
|
409
|
+
channel_name=fullname,
|
|
410
|
+
source_node=node,
|
|
411
|
+
dim=2,
|
|
412
|
+
event_data=event_data,
|
|
413
|
+
)
|
|
414
|
+
except Exception:
|
|
415
|
+
sys.excepthook(*sys.exc_info())
|
|
416
|
+
elif event_type == EventType.PREPARED_SCAN:
|
|
417
|
+
node_type = node.type
|
|
418
|
+
if self._watch_scan_group or node_type == "scan":
|
|
419
|
+
db_name = node.db_name
|
|
420
|
+
if db_name in self._running_scans:
|
|
421
|
+
scan_info = node.info.get_all()
|
|
422
|
+
try:
|
|
423
|
+
observer.on_scan_started(db_name, scan_info)
|
|
424
|
+
except Exception:
|
|
425
|
+
sys.excepthook(*sys.exc_info())
|
|
426
|
+
elif event_type == EventType.END_SCAN:
|
|
427
|
+
node_type = node.type
|
|
428
|
+
if self._watch_scan_group or node_type == "scan":
|
|
429
|
+
db_name = node.db_name
|
|
430
|
+
if db_name in self._running_scans:
|
|
431
|
+
try:
|
|
432
|
+
scan_info = node.info.get_all()
|
|
433
|
+
try:
|
|
434
|
+
observer.on_scan_finished(db_name, scan_info)
|
|
435
|
+
except Exception:
|
|
436
|
+
sys.excepthook(*sys.exc_info())
|
|
437
|
+
finally:
|
|
438
|
+
self._running_scans.discard(db_name)
|
|
439
|
+
if len(self._running_scans) == 0:
|
|
440
|
+
self._no_scans_event.set()
|
|
441
|
+
gevent.idle()
|
|
442
|
+
finally:
|
|
443
|
+
self._running = False
|
|
444
|
+
self._terminated_event.set()
|
|
445
|
+
|
|
446
|
+
def stop(self):
|
|
447
|
+
"""Call it to stop the event loop."""
|
|
448
|
+
if self._running:
|
|
449
|
+
self._stop_handler.stop()
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class DefaultScansObserver(ScansObserver):
|
|
453
|
+
"""Default scan observer.
|
|
454
|
+
|
|
455
|
+
This observer provides a compatibility with the previous implementation:
|
|
456
|
+
|
|
457
|
+
- Backward compatible API for callbacks (BLISS <= 1.7)
|
|
458
|
+
- Storing scan_info per scans
|
|
459
|
+
- Storing the whole data for each scalar channels
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
class _ScanDescription(typing.NamedTuple):
|
|
463
|
+
scan_info: Dict
|
|
464
|
+
"""Scan_info of the scan"""
|
|
465
|
+
channels_to_master: Dict[str, str]
|
|
466
|
+
"""Describe the master for each channels"""
|
|
467
|
+
channels_data: Dict[str, numpy.ndarray]
|
|
468
|
+
"""Store the full data per scalar channels"""
|
|
469
|
+
|
|
470
|
+
def __init__(self):
|
|
471
|
+
self._running_scans: Dict[str, self._ScanDescription] = {}
|
|
472
|
+
self.scan_new_callback: typing.Callable[[Dict], None] = None
|
|
473
|
+
self.scan_new_child_callback: typing.Callable[[Dict, typing.Any], None] = None
|
|
474
|
+
self.scan_data_callback: typing.Callable[[str, str, Dict], None] = None
|
|
475
|
+
self.scan_end_callback: typing.Callable[[Dict], None] = None
|
|
476
|
+
self._current_event: Event = None
|
|
477
|
+
"""
|
|
478
|
+
Used to store a tuple with `event_type`, `node`, `event_data`
|
|
479
|
+
during a callback event.
|
|
480
|
+
|
|
481
|
+
Never None inside callbacks
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def _get_scan_description(self, scan_db_name) -> _ScanDescription:
|
|
485
|
+
return self._running_scans.get(scan_db_name)
|
|
486
|
+
|
|
487
|
+
def on_event_received(self, event):
|
|
488
|
+
"""
|
|
489
|
+
Called upon new event
|
|
490
|
+
|
|
491
|
+
Mostly used for backward compatibility with `DefaultScanObserver`.
|
|
492
|
+
"""
|
|
493
|
+
self._current_event = event
|
|
494
|
+
|
|
495
|
+
def on_scan_created(self, scan_db_name: str, scan_info: Dict):
|
|
496
|
+
# Pre-compute mapping from each channels to its master
|
|
497
|
+
top_master_per_channels = {}
|
|
498
|
+
for top_master, meta in scan_info["acquisition_chain"].items():
|
|
499
|
+
for device_name in meta["devices"]:
|
|
500
|
+
device_meta = scan_info["devices"][device_name]
|
|
501
|
+
for channel_name in device_meta.get("channels", []):
|
|
502
|
+
top_master_per_channels[channel_name] = top_master
|
|
503
|
+
self._running_scans[scan_db_name] = self._ScanDescription(
|
|
504
|
+
scan_info, top_master_per_channels, {}
|
|
505
|
+
)
|
|
506
|
+
if self.scan_new_callback is not None:
|
|
507
|
+
self.scan_new_callback(scan_info)
|
|
508
|
+
|
|
509
|
+
def on_scan_finished(self, scan_db_name: str, scan_info: Dict):
|
|
510
|
+
self._running_scans.pop(scan_db_name)
|
|
511
|
+
if self.scan_end_callback is not None:
|
|
512
|
+
self.scan_end_callback(scan_info)
|
|
513
|
+
|
|
514
|
+
def on_child_created(self, scan_db_name: str, node):
|
|
515
|
+
scan_desciption = self._get_scan_description(scan_db_name)
|
|
516
|
+
if scan_desciption is None:
|
|
517
|
+
# Scan not part of the listened scans
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
if self.scan_new_child_callback is not None:
|
|
521
|
+
self.scan_new_child_callback(scan_desciption.scan_info, node)
|
|
522
|
+
|
|
523
|
+
def on_scalar_data_received(
|
|
524
|
+
self,
|
|
525
|
+
scan_db_name: str,
|
|
526
|
+
channel_name: str,
|
|
527
|
+
index: int,
|
|
528
|
+
data_bunch: typing.Union[list, numpy.ndarray],
|
|
529
|
+
):
|
|
530
|
+
if self.scan_data_callback is None:
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
scan_desciption = self._get_scan_description(scan_db_name)
|
|
534
|
+
if scan_desciption is None:
|
|
535
|
+
# Scan not part of the listened scans
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
# in case of zerod, we keep all data value during the scan
|
|
539
|
+
prev_data = scan_desciption.channels_data.get(channel_name, [])
|
|
540
|
+
data = numpy.concatenate((prev_data, data_bunch))
|
|
541
|
+
scan_desciption.channels_data[channel_name] = data
|
|
542
|
+
|
|
543
|
+
top_master = scan_desciption.channels_to_master[channel_name]
|
|
544
|
+
self.scan_data_callback(
|
|
545
|
+
"0d",
|
|
546
|
+
top_master,
|
|
547
|
+
{
|
|
548
|
+
"data": scan_desciption.channels_data,
|
|
549
|
+
"scan_info": scan_desciption.scan_info,
|
|
550
|
+
},
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
def on_ndim_data_received(
|
|
554
|
+
self,
|
|
555
|
+
scan_db_name: str,
|
|
556
|
+
channel_name: str,
|
|
557
|
+
dim: int,
|
|
558
|
+
index: int,
|
|
559
|
+
data_bunch: typing.Union[list, numpy.ndarray],
|
|
560
|
+
):
|
|
561
|
+
if self.scan_data_callback is None:
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
scan_desciption = self._get_scan_description(scan_db_name)
|
|
565
|
+
if scan_desciption is None:
|
|
566
|
+
# Scan not part of the listened scans
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
source_node = self._current_event.node
|
|
570
|
+
event_data = self._current_event.data
|
|
571
|
+
|
|
572
|
+
top_master = scan_desciption.channels_to_master[channel_name]
|
|
573
|
+
self.scan_data_callback(
|
|
574
|
+
f"{dim}d",
|
|
575
|
+
top_master,
|
|
576
|
+
{
|
|
577
|
+
"index": index,
|
|
578
|
+
"data": data_bunch,
|
|
579
|
+
"description": event_data.description,
|
|
580
|
+
"channel_name": channel_name,
|
|
581
|
+
"channel_data_node": source_node,
|
|
582
|
+
"scan_info": scan_desciption.scan_info,
|
|
583
|
+
},
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
def on_lima_ref_received(
|
|
587
|
+
self, scan_db_name: str, channel_name: str, dim: int, source_node, event_data
|
|
588
|
+
):
|
|
589
|
+
if self.scan_data_callback is None:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
scan_desciption = self._get_scan_description(scan_db_name)
|
|
593
|
+
if scan_desciption is None:
|
|
594
|
+
# Scan not part of the listened scans
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
top_master = scan_desciption.channels_to_master[channel_name]
|
|
598
|
+
self.scan_data_callback(
|
|
599
|
+
f"{dim}d",
|
|
600
|
+
top_master,
|
|
601
|
+
{
|
|
602
|
+
"index": event_data.first_index,
|
|
603
|
+
"data": event_data.data,
|
|
604
|
+
"description": event_data.description,
|
|
605
|
+
"channel_name": channel_name,
|
|
606
|
+
"channel_data_node": source_node,
|
|
607
|
+
"scan_info": scan_desciption.scan_info,
|
|
608
|
+
},
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def watch_session_scans(
|
|
613
|
+
session_name,
|
|
614
|
+
scan_new_callback,
|
|
615
|
+
scan_new_child_callback,
|
|
616
|
+
scan_data_callback,
|
|
617
|
+
scan_end_callback=None,
|
|
618
|
+
ready_event=None,
|
|
619
|
+
stop_handler=None,
|
|
620
|
+
watch_scan_group: bool = False,
|
|
621
|
+
exclude_existing_scans=None,
|
|
622
|
+
):
|
|
623
|
+
"""Any scan node that is created before the `ready_event` will not be watched
|
|
624
|
+
when `exclude_existing_scans=True`.
|
|
625
|
+
|
|
626
|
+
:param str session_name:
|
|
627
|
+
:param callable scan_new_callback: called upon scan start
|
|
628
|
+
:param callable scan_new_child_callback: called upon scan child creation (e.g. channel node)
|
|
629
|
+
:param callable scan_data_callback: data processing callback
|
|
630
|
+
:param callable scan_end_callback: called upon scan end
|
|
631
|
+
:param Event ready_event: started listening to Redis
|
|
632
|
+
:param DataStreamReaderStopHandler stop_handler:
|
|
633
|
+
:param bool watch_scan_group: If True the scan groups are also listed like any other scans
|
|
634
|
+
:param bool exclude_existing_scans: False by default (will become True by default in the future)
|
|
635
|
+
"""
|
|
636
|
+
if exclude_existing_scans is None:
|
|
637
|
+
exclude_existing_scans = False
|
|
638
|
+
warnings.warn("'exclude_existing_scans' will be True by default", FutureWarning)
|
|
639
|
+
|
|
640
|
+
watcher = ScansWatcher(session_name)
|
|
641
|
+
watcher.set_exclude_existing_scans(exclude_existing_scans)
|
|
642
|
+
watcher.set_watch_scan_group(watch_scan_group)
|
|
643
|
+
if stop_handler is not None:
|
|
644
|
+
watcher._set_stop_handler(stop_handler)
|
|
645
|
+
|
|
646
|
+
if ready_event is not None:
|
|
647
|
+
|
|
648
|
+
def wait_ready():
|
|
649
|
+
nonlocal ready_event
|
|
650
|
+
watcher.wait_ready()
|
|
651
|
+
ready_event.set()
|
|
652
|
+
|
|
653
|
+
local_store_g = gevent.spawn(wait_ready)
|
|
654
|
+
else:
|
|
655
|
+
local_store_g = None
|
|
656
|
+
|
|
657
|
+
observer = DefaultScansObserver()
|
|
658
|
+
observer.scan_new_callback = scan_new_callback
|
|
659
|
+
observer.scan_new_child_callback = scan_new_child_callback
|
|
660
|
+
observer.scan_data_callback = scan_data_callback
|
|
661
|
+
observer.scan_end_callback = scan_end_callback
|
|
662
|
+
|
|
663
|
+
watcher.set_observer(observer)
|
|
664
|
+
watcher.run()
|
|
665
|
+
if local_store_g is not None:
|
|
666
|
+
local_store_g.kill()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Definition and implementation of the h5py-like Bliss Data API"""
|