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
|
@@ -0,0 +1,422 @@
|
|
|
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
|
+
import numpy
|
|
9
|
+
import warnings
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from blissdata.data.nodes.channel import _ChannelDataNodeBase
|
|
13
|
+
from blissdata.data.events import EventData, LimaImageStatusEvent, ImageNotSaved
|
|
14
|
+
from blissdata.settings import QueueObjSetting
|
|
15
|
+
from blissdata.data import lima_image
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LimaDataView:
|
|
22
|
+
def __init__(self, queue, queue_ref, from_index, to_index, from_stream=False):
|
|
23
|
+
"""
|
|
24
|
+
:param DataStream queue: acquisition status events
|
|
25
|
+
:param QueueObjSetting queue_ref: acquisition info
|
|
26
|
+
:param int from_index:
|
|
27
|
+
:param int to_index: when <0 we will take the last image
|
|
28
|
+
that is "ready" which may change after `update`
|
|
29
|
+
:param bool from_stream:
|
|
30
|
+
"""
|
|
31
|
+
self._queue = queue
|
|
32
|
+
self._queue_ref = queue_ref
|
|
33
|
+
self.from_index = from_index
|
|
34
|
+
self._to_index = to_index
|
|
35
|
+
self.from_stream = from_stream
|
|
36
|
+
self._status_event = None
|
|
37
|
+
|
|
38
|
+
def __getattr__(self, attr):
|
|
39
|
+
# Get attribute from the status event
|
|
40
|
+
try:
|
|
41
|
+
return getattr(self.status_event, attr)
|
|
42
|
+
except AttributeError:
|
|
43
|
+
raise AttributeError(attr)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def to_index(self):
|
|
47
|
+
"""Either a fixed number or the last image that is "ready" """
|
|
48
|
+
self.update()
|
|
49
|
+
last_index = self.last_image_ready
|
|
50
|
+
|
|
51
|
+
if self._to_index >= 0:
|
|
52
|
+
return min(self._to_index, last_index)
|
|
53
|
+
else:
|
|
54
|
+
return last_index
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def last_index(self):
|
|
58
|
+
"""WARNING: this is the last image index + 1"""
|
|
59
|
+
return self.to_index + 1
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def connection(self):
|
|
63
|
+
return self._queue._cnx()
|
|
64
|
+
|
|
65
|
+
def update(self):
|
|
66
|
+
"""Get the latest status event from the data stream
|
|
67
|
+
and add the first element of the reference settings.
|
|
68
|
+
It is safe to call this as much as you want.
|
|
69
|
+
"""
|
|
70
|
+
events = self._queue.rev_range(count=1)
|
|
71
|
+
if events:
|
|
72
|
+
index, raw = events[-1] # [0]
|
|
73
|
+
ev = LimaImageStatusEvent(raw=raw)
|
|
74
|
+
else: # Lima acqusition has not yet started.
|
|
75
|
+
ev = LimaImageStatusEvent({})
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
ev.info = self.first_ref_data
|
|
79
|
+
except IndexError:
|
|
80
|
+
pass
|
|
81
|
+
ev.connection = self.connection
|
|
82
|
+
self._status_event = ev
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def status_event(self):
|
|
86
|
+
if self._status_event is None:
|
|
87
|
+
self.update()
|
|
88
|
+
return self._status_event
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def ref_status(self):
|
|
92
|
+
warnings.warn(
|
|
93
|
+
"ref_status is deprecated. Use 'status_event.status' instead.",
|
|
94
|
+
FutureWarning,
|
|
95
|
+
)
|
|
96
|
+
return self.status_event.status
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def all_ref_data(self):
|
|
100
|
+
"""
|
|
101
|
+
:returns list(dict):
|
|
102
|
+
"""
|
|
103
|
+
return self._queue_ref[0:]
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def first_ref_data(self):
|
|
107
|
+
"""
|
|
108
|
+
:returns dict:
|
|
109
|
+
"""
|
|
110
|
+
return self._queue_ref[0]
|
|
111
|
+
|
|
112
|
+
def is_video_frame_have_meaning(self):
|
|
113
|
+
"""Returns True if the frame number reached from the header from
|
|
114
|
+
the Lima video have a meaning in the full scan.
|
|
115
|
+
|
|
116
|
+
Returns a boolean, else None if this information is not yet known.
|
|
117
|
+
"""
|
|
118
|
+
self.update()
|
|
119
|
+
return self.status_event.is_video_frame_have_meaning()
|
|
120
|
+
|
|
121
|
+
def get_last_live_image(self):
|
|
122
|
+
"""Returns the last image data from stream within it's frame number.
|
|
123
|
+
|
|
124
|
+
If no data is available, the function returns tuple (None, None).
|
|
125
|
+
|
|
126
|
+
If camera device is not configured with INTERNAL_TRIGGER_MULTI, and
|
|
127
|
+
then the reached frame number have no meaning, a None is returned.
|
|
128
|
+
|
|
129
|
+
:returns Frame:
|
|
130
|
+
"""
|
|
131
|
+
if not self.from_stream:
|
|
132
|
+
# FIXME: It should return None
|
|
133
|
+
return lima_image.Frame(None, None, None)
|
|
134
|
+
self.update()
|
|
135
|
+
return self.status_event.get_last_live_image()
|
|
136
|
+
|
|
137
|
+
def get_last_image(self):
|
|
138
|
+
"""Returns the last image from the received one, together with the frame id.
|
|
139
|
+
|
|
140
|
+
:returns Frame:
|
|
141
|
+
"""
|
|
142
|
+
self.update()
|
|
143
|
+
return self.status_event.get_last_image()
|
|
144
|
+
|
|
145
|
+
def get_image(self, image_index_in_scan):
|
|
146
|
+
"""
|
|
147
|
+
:param int image_index_in_scan:
|
|
148
|
+
:returns numpy.ndarray:
|
|
149
|
+
"""
|
|
150
|
+
if image_index_in_scan < 0:
|
|
151
|
+
raise ValueError("image_index_in_scan cannot be a negative number")
|
|
152
|
+
self.update()
|
|
153
|
+
return self.status_event.get_image(image_index_in_scan)
|
|
154
|
+
|
|
155
|
+
def __getitem__(self, idx):
|
|
156
|
+
"""Get images from server or file"""
|
|
157
|
+
if isinstance(idx, slice):
|
|
158
|
+
start, stop, step = idx.indices(len(self))
|
|
159
|
+
start += self.from_index
|
|
160
|
+
stop += self.from_index
|
|
161
|
+
return numpy.asarray(list(self._image_range(start, stop, step)))
|
|
162
|
+
elif isinstance(idx, list):
|
|
163
|
+
idx = numpy.asarray(idx)
|
|
164
|
+
if isinstance(idx[0].item(), bool):
|
|
165
|
+
idx = numpy.nonzero(idx)
|
|
166
|
+
idx += self.from_index
|
|
167
|
+
return numpy.asarray(list(self._image_iter(idx)))
|
|
168
|
+
elif isinstance(idx, tuple):
|
|
169
|
+
# This would slice the image dimensions
|
|
170
|
+
raise NotImplementedError
|
|
171
|
+
else:
|
|
172
|
+
try:
|
|
173
|
+
idx = int(idx)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
raise IndexError from e
|
|
176
|
+
if self.from_stream and idx == -1:
|
|
177
|
+
img = self.get_image(-1)
|
|
178
|
+
else:
|
|
179
|
+
if idx < 0:
|
|
180
|
+
index = self.to_index + 1 + idx
|
|
181
|
+
if index < 0:
|
|
182
|
+
raise IndexError("No image available")
|
|
183
|
+
else:
|
|
184
|
+
index = self.from_index + idx
|
|
185
|
+
img = self.get_image(index)
|
|
186
|
+
if img is None:
|
|
187
|
+
raise IndexError
|
|
188
|
+
return img
|
|
189
|
+
|
|
190
|
+
def __iter__(self):
|
|
191
|
+
"""Iterator over images from server or file"""
|
|
192
|
+
yield from self._image_range(self.from_index, self.to_index + 1)
|
|
193
|
+
|
|
194
|
+
def _image_range(self, start, stop, step=1):
|
|
195
|
+
"""Iterator over images from server or file"""
|
|
196
|
+
yield from self._image_iter(range(start, stop, step))
|
|
197
|
+
|
|
198
|
+
def _image_iter(self, image_nb_iterator):
|
|
199
|
+
"""Iterator over images from server or file"""
|
|
200
|
+
for image_index_in_scan in image_nb_iterator:
|
|
201
|
+
try:
|
|
202
|
+
img = self.get_image(image_index_in_scan)
|
|
203
|
+
except IndexError:
|
|
204
|
+
img = None
|
|
205
|
+
if img is None:
|
|
206
|
+
break
|
|
207
|
+
yield img
|
|
208
|
+
|
|
209
|
+
def as_array(self):
|
|
210
|
+
if len(self) == 1:
|
|
211
|
+
# To be consistant with ChannelDataNode
|
|
212
|
+
return list(self)[0]
|
|
213
|
+
else:
|
|
214
|
+
return numpy.asarray(list(self))
|
|
215
|
+
|
|
216
|
+
def __len__(self):
|
|
217
|
+
length = self.to_index - self.from_index + 1
|
|
218
|
+
return 0 if length < 0 else length
|
|
219
|
+
|
|
220
|
+
def all_image_references(self, saved=False):
|
|
221
|
+
"""Get the image references.
|
|
222
|
+
|
|
223
|
+
:param bool saved: ready or ready and saved
|
|
224
|
+
:returns list(tuple): file name, path-in-file, image index, file format
|
|
225
|
+
File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
|
|
226
|
+
:raise RuntimeError: images will never be saved
|
|
227
|
+
"""
|
|
228
|
+
self.update()
|
|
229
|
+
return self.status_event.all_image_references(saved=saved)
|
|
230
|
+
|
|
231
|
+
def image_references(self, image_indices_in_scan, saved=False):
|
|
232
|
+
"""Get the image references.
|
|
233
|
+
|
|
234
|
+
:param sequence image_indices_in_scan:
|
|
235
|
+
:param bool saved: ready or ready and saved
|
|
236
|
+
:returns list(tuple): file name, path-in-file, image index, file format
|
|
237
|
+
File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
|
|
238
|
+
:raise RuntimeError: some images are not ready or saved yet
|
|
239
|
+
or images will never be saved
|
|
240
|
+
"""
|
|
241
|
+
self.update()
|
|
242
|
+
return self.status_event.image_references(image_indices_in_scan, saved=saved)
|
|
243
|
+
|
|
244
|
+
def image_reference(self, image_index_in_scan, saved=False):
|
|
245
|
+
"""Get the image references.
|
|
246
|
+
|
|
247
|
+
:param int image_index_in_scan:
|
|
248
|
+
:param bool saved: ready or ready and saved
|
|
249
|
+
:returns tuple: file name, path-in-file, image index, file format
|
|
250
|
+
File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
|
|
251
|
+
:raise RuntimeError: image is not ready or saved yet
|
|
252
|
+
or images will never be saved
|
|
253
|
+
"""
|
|
254
|
+
self.update()
|
|
255
|
+
return self.status_event.image_reference(image_index_in_scan, saved=saved)
|
|
256
|
+
|
|
257
|
+
def iter_image_references(self, image_indices_in_scan=None, saved=False):
|
|
258
|
+
"""Get the image references.
|
|
259
|
+
|
|
260
|
+
Stops iterating when it encounters an image that is not
|
|
261
|
+
ready or saved yet, regardless of how many images you
|
|
262
|
+
asked for.
|
|
263
|
+
|
|
264
|
+
:param sequence image_indices_in_scan:
|
|
265
|
+
:param bool saved: ready or ready and saved
|
|
266
|
+
:yields list(tuple): file name, path-in-file, image index, file format
|
|
267
|
+
File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
|
|
268
|
+
:raises RuntimeError: images will never be saved
|
|
269
|
+
"""
|
|
270
|
+
self.update()
|
|
271
|
+
yield from self.status_event.iter_image_references(
|
|
272
|
+
image_indices_in_scan=image_indices_in_scan, saved=saved
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def get_filenames(self):
|
|
276
|
+
warnings.warn(
|
|
277
|
+
"'get_filenames' is deprecated. Use 'all_image_references' instead.",
|
|
278
|
+
FutureWarning,
|
|
279
|
+
)
|
|
280
|
+
return self.all_image_references()
|
|
281
|
+
|
|
282
|
+
def _get_filenames(self, ref_data, *image_indices_in_scan):
|
|
283
|
+
warnings.warn(
|
|
284
|
+
"'_get_filenames' is deprecated. Use 'image_references' itself.",
|
|
285
|
+
FutureWarning,
|
|
286
|
+
)
|
|
287
|
+
return self.image_references(image_indices_in_scan)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class LimaImageChannelDataNode(_ChannelDataNodeBase):
|
|
291
|
+
_NODE_TYPE = "lima"
|
|
292
|
+
|
|
293
|
+
def __init__(self, *args, **kwargs):
|
|
294
|
+
super().__init__(*args, **kwargs)
|
|
295
|
+
# There is good reason for this name not to end with "*_data"
|
|
296
|
+
# (see DataNodeContainer._subscribe_streams)
|
|
297
|
+
self._queue_ref = QueueObjSetting(
|
|
298
|
+
f"{self.db_name}_data_ref", connection=self.db_connection
|
|
299
|
+
)
|
|
300
|
+
self.from_stream = False
|
|
301
|
+
self._local_ref_status = dict()
|
|
302
|
+
self._stream_image_count = 0
|
|
303
|
+
|
|
304
|
+
def store(self, event_dict, cnx=None):
|
|
305
|
+
"""Publish lima reference in Redis"""
|
|
306
|
+
data = event_dict["data"]
|
|
307
|
+
if data.get("in_prepare", False): # in prepare phase
|
|
308
|
+
ref_data = event_dict["description"]
|
|
309
|
+
self.info.update(ref_data)
|
|
310
|
+
self._queue_ref.append(ref_data)
|
|
311
|
+
self._local_ref_status = data
|
|
312
|
+
self._local_ref_status["lima_acq_nb"] = self.db_connection.incr(
|
|
313
|
+
data["server_url"]
|
|
314
|
+
)
|
|
315
|
+
else: # during acquisition
|
|
316
|
+
self._local_ref_status.update(data)
|
|
317
|
+
ev = LimaImageStatusEvent(self._local_ref_status)
|
|
318
|
+
self._queue.add_event(ev, id=self._last_index, cnx=cnx)
|
|
319
|
+
self._last_index += 1
|
|
320
|
+
|
|
321
|
+
def get(self, from_index, to_index=None):
|
|
322
|
+
"""
|
|
323
|
+
Return a view on data references.
|
|
324
|
+
|
|
325
|
+
**from_index** from which image index you want to get
|
|
326
|
+
**to_index** to which index you want images
|
|
327
|
+
if to_index is None => only one image which as index from_index
|
|
328
|
+
if to_index < 0 => to the end of acquisition
|
|
329
|
+
"""
|
|
330
|
+
return LimaDataView(
|
|
331
|
+
self._queue,
|
|
332
|
+
self._queue_ref,
|
|
333
|
+
from_index,
|
|
334
|
+
to_index if to_index is not None else from_index,
|
|
335
|
+
from_stream=self.from_stream,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def get_as_array(self, from_index, to_index=None):
|
|
339
|
+
"""Like `get` but ensures the result is a numpy array."""
|
|
340
|
+
return numpy.asarray(self.get(from_index, to_index).as_array(), self.dtype)
|
|
341
|
+
|
|
342
|
+
def decode_raw_events(self, events):
|
|
343
|
+
"""Decode raw stream data and get image URI's.
|
|
344
|
+
|
|
345
|
+
:param list((index, raw)) events:
|
|
346
|
+
:returns EventData:
|
|
347
|
+
"""
|
|
348
|
+
data = list()
|
|
349
|
+
first_index = -1
|
|
350
|
+
description = None
|
|
351
|
+
if events:
|
|
352
|
+
# The number of events is NOT equal to the number of images
|
|
353
|
+
# The number of images can be derived from the event data though
|
|
354
|
+
# TODO: first_index is only accurate if we use the same DataNode instance!!!
|
|
355
|
+
|
|
356
|
+
if len(self._queue_ref) == 0:
|
|
357
|
+
# Workaround for the issue https://gitlab.esrf.fr/bliss/bliss/-/issues/2128
|
|
358
|
+
_logger.debug(
|
|
359
|
+
"Inconsistency on the Redis data structure. 'image_data_ref' was removed (or is empty)"
|
|
360
|
+
)
|
|
361
|
+
return EventData(
|
|
362
|
+
first_index=first_index, data=data, description=description
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
ev = LimaImageStatusEvent.merge(events)
|
|
366
|
+
ev.info = self.first_ref_data
|
|
367
|
+
first_index = self._stream_image_count
|
|
368
|
+
try:
|
|
369
|
+
data = ev.image_reference_range(first_index)
|
|
370
|
+
except ImageNotSaved:
|
|
371
|
+
# If images are not supposed to be saved we can't expect to
|
|
372
|
+
# find references
|
|
373
|
+
pass
|
|
374
|
+
self._stream_image_count += len(data)
|
|
375
|
+
description = ev.status
|
|
376
|
+
return EventData(first_index=first_index, data=data, description=description)
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def all_ref_data(self):
|
|
380
|
+
"""
|
|
381
|
+
:returns list(dict):
|
|
382
|
+
"""
|
|
383
|
+
return self._queue_ref[0:]
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def first_ref_data(self):
|
|
387
|
+
"""
|
|
388
|
+
:returns dict:
|
|
389
|
+
"""
|
|
390
|
+
return self._queue_ref[0]
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def images_per_file(self):
|
|
394
|
+
try:
|
|
395
|
+
return self.first_ref_data.get("saving_frame_per_file")
|
|
396
|
+
except IndexError:
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def images_per_acquisition(self):
|
|
401
|
+
try:
|
|
402
|
+
return self.first_ref_data.get("saving_frame_per_acquisition")
|
|
403
|
+
except IndexError:
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
def get_db_names(self, **kw):
|
|
407
|
+
db_names = super().get_db_names(**kw)
|
|
408
|
+
db_names.append(self.db_name + "_data_ref")
|
|
409
|
+
events = self._queue.range(count=1)
|
|
410
|
+
if events:
|
|
411
|
+
index, raw = events[0]
|
|
412
|
+
ev = LimaImageStatusEvent(raw=raw)
|
|
413
|
+
url = ev.server_url
|
|
414
|
+
if url:
|
|
415
|
+
db_names.append(url)
|
|
416
|
+
return db_names
|
|
417
|
+
|
|
418
|
+
def get_settings(self):
|
|
419
|
+
return super().get_settings() + [self._queue_ref]
|
|
420
|
+
|
|
421
|
+
def __len__(self):
|
|
422
|
+
return len(self.get(0, -1))
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
from blissdata.data.nodes.channel import ChannelDataNode
|
|
9
|
+
from blissdata.data.events import EventData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeRefChannel(ChannelDataNode):
|
|
13
|
+
"""
|
|
14
|
+
A data node that stores references to other DataNodes. It is intended to be
|
|
15
|
+
used to keep references e.g. of individual scans to group them together as
|
|
16
|
+
one group or sequence of scans.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_NODE_TYPE = "node_ref_channel"
|
|
20
|
+
|
|
21
|
+
def decode_raw_events(self, events):
|
|
22
|
+
"""Decode raw stream data
|
|
23
|
+
|
|
24
|
+
:param list((index, raw)) events:
|
|
25
|
+
:returns EventData:
|
|
26
|
+
"""
|
|
27
|
+
event_data = super().decode_raw_events(events)
|
|
28
|
+
nodes = self.get_nodes(*event_data.data)
|
|
29
|
+
return EventData(
|
|
30
|
+
first_index=event_data.first_index,
|
|
31
|
+
data=nodes,
|
|
32
|
+
description=event_data.description,
|
|
33
|
+
block_size=event_data.block_size,
|
|
34
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
from blissdata.data.nodes.dataset import _DataPolicyNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProposalNode(_DataPolicyNode):
|
|
12
|
+
_NODE_TYPE = "proposal"
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
from blissdata.data.node import DataNodeContainer
|
|
9
|
+
from blissdata.data.nodes.channel import ChannelDataNode, RedisDataExpiredError
|
|
10
|
+
from blissdata.data.nodes.lima import LimaImageChannelDataNode
|
|
11
|
+
from blissdata.streaming_events import StreamEvent
|
|
12
|
+
from blissdata.data.events import (
|
|
13
|
+
Event,
|
|
14
|
+
EventType,
|
|
15
|
+
EventData,
|
|
16
|
+
EndScanEvent,
|
|
17
|
+
PreparedScanEvent,
|
|
18
|
+
)
|
|
19
|
+
from blissdata import settings
|
|
20
|
+
from blissdata.common.utils import update_node_info
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ScanNode(DataNodeContainer):
|
|
24
|
+
_NODE_TYPE = "scan"
|
|
25
|
+
|
|
26
|
+
_EVENT_TYPE_MAPPING = {
|
|
27
|
+
EndScanEvent.TYPE.decode("ascii"): EventType.END_SCAN,
|
|
28
|
+
PreparedScanEvent.TYPE.decode("ascii"): EventType.PREPARED_SCAN,
|
|
29
|
+
}
|
|
30
|
+
"""Mapping from event name to EventType
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, name, **kwargs):
|
|
34
|
+
super().__init__(self._NODE_TYPE, name, **kwargs)
|
|
35
|
+
self._end_stream = self._create_stream("end")
|
|
36
|
+
self._prepared_stream = self._create_stream("prepared")
|
|
37
|
+
# Register to priority as the following way: NEW DATA > PREPARED > NEW NODE > END
|
|
38
|
+
self._register_stream_priority(self._end_stream.name, 3)
|
|
39
|
+
self._register_stream_priority(self._prepared_stream.name, 1)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def dataset(self):
|
|
43
|
+
return self.parent
|
|
44
|
+
|
|
45
|
+
def prepared(self, scan_info):
|
|
46
|
+
"""Publish PREPARED event in Redis"""
|
|
47
|
+
if not self.new_node:
|
|
48
|
+
return
|
|
49
|
+
# to avoid to have multiple modification events
|
|
50
|
+
# TODO: what does the comment above mean?
|
|
51
|
+
with settings.pipeline(self._prepared_stream, self._info):
|
|
52
|
+
event = PreparedScanEvent()
|
|
53
|
+
update_node_info(self, scan_info)
|
|
54
|
+
self._prepared_stream.add_event(event)
|
|
55
|
+
|
|
56
|
+
def end(self, scan_info, exception=None):
|
|
57
|
+
"""Update the info in Redis and publish the END event."""
|
|
58
|
+
if not self.new_node:
|
|
59
|
+
return
|
|
60
|
+
# to avoid to have multiple modification events
|
|
61
|
+
# TODO: what does the comment above mean?
|
|
62
|
+
with settings.pipeline(self._end_stream, self._info):
|
|
63
|
+
event = EndScanEvent(exception=exception)
|
|
64
|
+
scan_info["end_time"] = event.time
|
|
65
|
+
scan_info["end_time_str"] = event.strftime
|
|
66
|
+
scan_info["end_timestamp"] = event.timestamp
|
|
67
|
+
update_node_info(self, scan_info)
|
|
68
|
+
self._end_stream.add_event(event)
|
|
69
|
+
|
|
70
|
+
def decode_raw_events(self, events):
|
|
71
|
+
"""Decode raw stream data
|
|
72
|
+
|
|
73
|
+
:param list((index, raw)) events:
|
|
74
|
+
:returns EventData:
|
|
75
|
+
"""
|
|
76
|
+
if not events:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
assert len(events) == 1 # Else you are about to lose events
|
|
80
|
+
event = events[0]
|
|
81
|
+
timestamp, raw_data = event
|
|
82
|
+
first_index = self._streamid_to_idx(timestamp)
|
|
83
|
+
ev = StreamEvent.factory(raw_data)
|
|
84
|
+
data = type(ev).TYPE.decode()
|
|
85
|
+
return EventData(first_index=first_index, data=data, description=ev.description)
|
|
86
|
+
|
|
87
|
+
def get_db_names(self, **kw):
|
|
88
|
+
db_names = super().get_db_names(**kw)
|
|
89
|
+
db_names.append(self._end_stream.name)
|
|
90
|
+
db_names.append(self._prepared_stream.name)
|
|
91
|
+
return db_names
|
|
92
|
+
|
|
93
|
+
def get_settings(self):
|
|
94
|
+
return super().get_settings() + [self._end_stream, self._prepared_stream]
|
|
95
|
+
|
|
96
|
+
def _subscribe_streams(self, reader, first_index=None, **kw):
|
|
97
|
+
"""Subscribe to all associated streams of this node.
|
|
98
|
+
|
|
99
|
+
:param DataStreamReader reader:
|
|
100
|
+
:param **kw: see DataNodeContainer
|
|
101
|
+
"""
|
|
102
|
+
super()._subscribe_streams(reader, first_index=first_index, **kw)
|
|
103
|
+
suffix = self._end_stream.name.rsplit("_", 1)[-1]
|
|
104
|
+
self._subscribe_stream(
|
|
105
|
+
suffix, reader, first_index=0, create=True, ignore_excluded=True
|
|
106
|
+
)
|
|
107
|
+
suffix = self._prepared_stream.name.rsplit("_", 1)[-1]
|
|
108
|
+
self._subscribe_stream(
|
|
109
|
+
suffix, reader, first_index=0, create=True, ignore_excluded=True
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def get_stream_event_handler(self, stream):
|
|
113
|
+
"""
|
|
114
|
+
:param DataStream stream:
|
|
115
|
+
:returns callable:
|
|
116
|
+
"""
|
|
117
|
+
if stream.name == self._end_stream.name:
|
|
118
|
+
return self._iter_data_stream_events
|
|
119
|
+
elif stream.name == self._prepared_stream.name:
|
|
120
|
+
return self._iter_data_stream_events
|
|
121
|
+
return super(ScanNode, self).get_stream_event_handler(stream)
|
|
122
|
+
|
|
123
|
+
def _iter_data_stream_events(
|
|
124
|
+
self,
|
|
125
|
+
reader,
|
|
126
|
+
events,
|
|
127
|
+
include_filter=None,
|
|
128
|
+
exclude_children=None,
|
|
129
|
+
first_index=None,
|
|
130
|
+
yield_events=False,
|
|
131
|
+
):
|
|
132
|
+
"""
|
|
133
|
+
:param DataStreamReader reader:
|
|
134
|
+
:param list(2-tuple) events:
|
|
135
|
+
:param include_filter: only these nodes are included (all by default)
|
|
136
|
+
:param exclude_children: ignore children of these nodes recursively
|
|
137
|
+
:param str or int first_index: Redis stream index (None is now)
|
|
138
|
+
:param bool yield_events: yield Event or DataNode
|
|
139
|
+
:yields Event:
|
|
140
|
+
"""
|
|
141
|
+
for event in events:
|
|
142
|
+
data = self.decode_raw_events([event])
|
|
143
|
+
if data is None:
|
|
144
|
+
return
|
|
145
|
+
if yield_events and self._included(include_filter):
|
|
146
|
+
kind = data.data
|
|
147
|
+
event_id = self._EVENT_TYPE_MAPPING[kind]
|
|
148
|
+
event = Event(type=event_id, node=self, data=data)
|
|
149
|
+
yield event
|
|
150
|
+
if event_id is EventType.END_SCAN:
|
|
151
|
+
# Stop reading events from this node's streams
|
|
152
|
+
# and the streams of its children
|
|
153
|
+
reader.remove_matching_streams(f"{self.db_name}*")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_data_from_nodes(pipeline, *nodes):
|
|
157
|
+
scan_channel_get_data_func = dict() # { channel_name: function }
|
|
158
|
+
scan_image_get_view = dict()
|
|
159
|
+
for node in nodes:
|
|
160
|
+
if isinstance(node, LimaImageChannelDataNode):
|
|
161
|
+
scan_image_get_view[node.fullname] = node.get(0, -1)
|
|
162
|
+
elif isinstance(node, ChannelDataNode):
|
|
163
|
+
chan = node
|
|
164
|
+
channel_name = chan.fullname
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
saved_db_connection = chan.db_connection
|
|
168
|
+
chan.db_connection = pipeline
|
|
169
|
+
# append channel name and get all data from channel;
|
|
170
|
+
# as it is in a Redis pipeline, .get() returns the
|
|
171
|
+
# conversion function only - data will be received
|
|
172
|
+
# after .execute()
|
|
173
|
+
scan_channel_get_data_func[channel_name] = chan.get(0, -1)
|
|
174
|
+
finally:
|
|
175
|
+
chan.db_connection = saved_db_connection
|
|
176
|
+
|
|
177
|
+
result = pipeline.execute()
|
|
178
|
+
|
|
179
|
+
for i, (channel_name, get_data_func) in enumerate(
|
|
180
|
+
scan_channel_get_data_func.items()
|
|
181
|
+
):
|
|
182
|
+
if not result[i]:
|
|
183
|
+
raise RedisDataExpiredError("Redis data channel expired.")
|
|
184
|
+
|
|
185
|
+
if result[i][0][0] != b"1-0": # first event is 1-0, not 0-0
|
|
186
|
+
raise RedisDataExpiredError(
|
|
187
|
+
f"Redis data channel {channel_name} have been trimmed out."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
yield channel_name, get_data_func(result[i])
|
|
191
|
+
for channel_name, view in scan_image_get_view.items():
|
|
192
|
+
yield channel_name, view
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
from blissdata.data.nodes.scan import ScanNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GroupScanNode(ScanNode):
|
|
12
|
+
_NODE_TYPE = "scan_group"
|