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.
Files changed (67) hide show
  1. blissdata/__init__.py +17 -0
  2. blissdata/beacon/__init__.py +1 -0
  3. blissdata/beacon/_base.py +106 -0
  4. blissdata/beacon/config.py +24 -0
  5. blissdata/beacon/data.py +45 -0
  6. blissdata/beacon/files.py +141 -0
  7. blissdata/client.py +62 -0
  8. blissdata/common/__init__.py +15 -0
  9. blissdata/common/utils.py +159 -0
  10. blissdata/data/__init__.py +15 -0
  11. blissdata/data/events/__init__.py +22 -0
  12. blissdata/data/events/channel.py +149 -0
  13. blissdata/data/events/lima.py +478 -0
  14. blissdata/data/events/node.py +28 -0
  15. blissdata/data/events/scan.py +49 -0
  16. blissdata/data/events/walk.py +42 -0
  17. blissdata/data/expiration.py +42 -0
  18. blissdata/data/lima_image.py +465 -0
  19. blissdata/data/node.py +1619 -0
  20. blissdata/data/nodes/__init__.py +11 -0
  21. blissdata/data/nodes/channel.py +394 -0
  22. blissdata/data/nodes/dataset.py +82 -0
  23. blissdata/data/nodes/dataset_collection.py +12 -0
  24. blissdata/data/nodes/lima.py +422 -0
  25. blissdata/data/nodes/node_ref_channel.py +34 -0
  26. blissdata/data/nodes/proposal.py +12 -0
  27. blissdata/data/nodes/scan.py +192 -0
  28. blissdata/data/nodes/scan_group.py +12 -0
  29. blissdata/data/remote_node.py +204 -0
  30. blissdata/data/scan.py +666 -0
  31. blissdata/h5api/__init__.py +1 -0
  32. blissdata/h5api/abstract.py +97 -0
  33. blissdata/h5api/dynamic_hdf5.py +153 -0
  34. blissdata/h5api/file_arguments.py +28 -0
  35. blissdata/h5api/static_hdf5.py +139 -0
  36. blissdata/h5api/utils/__init__.py +0 -0
  37. blissdata/h5api/utils/bliss.py +138 -0
  38. blissdata/h5api/utils/hdf5.py +280 -0
  39. blissdata/h5api/utils/hdf5_retry.py +98 -0
  40. blissdata/h5api/utils/lima.py +286 -0
  41. blissdata/h5api/utils/types.py +13 -0
  42. blissdata/redis/__init__.py +12 -0
  43. blissdata/redis/caching.py +390 -0
  44. blissdata/redis/connection.py +169 -0
  45. blissdata/redis/manager.py +164 -0
  46. blissdata/redis/proxy.py +971 -0
  47. blissdata/redis/scripting.py +24 -0
  48. blissdata/settings.py +1174 -0
  49. blissdata/streaming.py +819 -0
  50. blissdata/streaming_events.py +355 -0
  51. blissdata/tests/__init__.py +0 -0
  52. blissdata/tests/beacon/__init__.py +0 -0
  53. blissdata/tests/beacon/test_config.py +26 -0
  54. blissdata/tests/beacon/test_data.py +50 -0
  55. blissdata/tests/beacon/test_files.py +80 -0
  56. blissdata/tests/conftest.py +0 -0
  57. blissdata/tests/h5api/__init__.py +0 -0
  58. blissdata/tests/h5api/scanner.py +390 -0
  59. blissdata/tests/h5api/test_dynamic_files.py +309 -0
  60. blissdata/tests/h5api/test_static_files.py +169 -0
  61. blissdata/tests/redis/__init__.py +0 -0
  62. blissdata/tests/redis/manager.py +23 -0
  63. blissdata-0.3.4.dist-info/LICENSE +165 -0
  64. blissdata-0.3.4.dist-info/METADATA +79 -0
  65. blissdata-0.3.4.dist-info/RECORD +67 -0
  66. blissdata-0.3.4.dist-info/WHEEL +5 -0
  67. blissdata-0.3.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,149 @@
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 numpy
10
+ from blissdata import streaming_events
11
+
12
+
13
+ __all__ = ["ChannelDataEvent"]
14
+
15
+
16
+ class ChannelDataEvent(streaming_events.StreamEvent):
17
+ TYPE = b"CHANNELDATA"
18
+ DATA_KEY = b"__DATA__"
19
+ DESC_KEY = b"__DESC__"
20
+ NPOINTS_KEY = b"__NPOINTS__"
21
+
22
+ def init(self, data, description):
23
+ """
24
+ :param Any data:
25
+ :param dict description:
26
+ """
27
+ self.description = description
28
+ self.data = data
29
+
30
+ @property
31
+ def shape(self):
32
+ return self.description["shape"]
33
+
34
+ @property
35
+ def ndim(self):
36
+ return len(self.shape)
37
+
38
+ @property
39
+ def dtype(self):
40
+ return self.description["dtype"]
41
+
42
+ @property
43
+ def data(self):
44
+ """Single item (npoints!=1) or sequence (npoints!=1)"""
45
+ return self._data
46
+
47
+ @property
48
+ def sequence(self):
49
+ """Sequence of items"""
50
+ if self.npoints == 1:
51
+ return [self.data]
52
+ else:
53
+ return self.data
54
+
55
+ @property
56
+ def array(self):
57
+ """numpy.ndarray"""
58
+ return numpy.asarray(self.sequence)
59
+
60
+ @property
61
+ def npoints(self):
62
+ return self._npoints
63
+
64
+ @data.setter
65
+ def data(self, value):
66
+ if isinstance(value, numpy.ndarray):
67
+ if value.size == 0:
68
+ npoints = 0
69
+ elif self.ndim == value.ndim:
70
+ # Only one data point provided
71
+ npoints = 1
72
+ else:
73
+ # Each element is a new data point
74
+ npoints = len(value)
75
+ if npoints == 1:
76
+ value = value[0]
77
+ elif isinstance(value, (list, tuple)):
78
+ # Each element is a new data point
79
+ npoints = len(value)
80
+ value = numpy.array(value, dtype=self.dtype)
81
+ if npoints == 1:
82
+ value = value[0]
83
+ else:
84
+ # Only one data point provided
85
+ npoints = 1
86
+ self._data = value
87
+ self._npoints = npoints
88
+
89
+ def _encode(self):
90
+ raw = super()._encode()
91
+ raw[self.DESC_KEY] = self.generic_encode(self.description)
92
+ raw[self.NPOINTS_KEY] = self.encode_integral(self._npoints)
93
+ raw[self.DATA_KEY] = self.generic_encode(self._data)
94
+ return raw
95
+
96
+ def _decode(self, raw):
97
+ super()._decode(raw)
98
+ self.description = self.generic_decode(raw[self.DESC_KEY])
99
+ self._npoints = self.decode_npoints(raw)
100
+ self._data = self.generic_decode(raw[self.DATA_KEY])
101
+
102
+ @classmethod
103
+ def decode_npoints(cls, raw):
104
+ return cls.decode_integral(raw[cls.NPOINTS_KEY])
105
+
106
+ @classmethod
107
+ def merge(cls, events):
108
+ """Merging means stack individual event data.
109
+ The description is assumed to be the same for
110
+ all events (not checked).
111
+
112
+ :param list((index, raw)) events:
113
+ :returns ChannelDataEvent:
114
+ """
115
+ data = []
116
+ description = {}
117
+ dtype = None
118
+ for i, (index, raw) in enumerate(events):
119
+ ev = cls(raw=raw)
120
+ if i == 0:
121
+ dtype = ev.dtype
122
+ description = ev.description
123
+ if ev.npoints == 1:
124
+ data.append(ev.data)
125
+ else:
126
+ data.extend(ev.data)
127
+ data = cls.as_array(data, dtype)
128
+ description["shape"] = data.shape[1:]
129
+ description["dtype"] = dtype
130
+ return cls(data, description)
131
+
132
+ @staticmethod
133
+ def as_array(sequence, dtype):
134
+ """Convert a sequence of sequences to a numpy array.
135
+ Pad with NaN's when sequences have unequal size.
136
+
137
+ :param Sequence sequence:
138
+ :param dtype:
139
+ :returns numpy.ndarray:
140
+ """
141
+ try:
142
+ return numpy.asarray(sequence, dtype=dtype)
143
+ except ValueError:
144
+ # Sequences have unequal length
145
+ shape = (len(sequence), numpy.max([len(x) for x in sequence]))
146
+ arr = numpy.full(shape, numpy.nan, dtype=dtype)
147
+ for src, dest in zip(sequence, arr):
148
+ dest[: len(src)] = src
149
+ return arr
@@ -0,0 +1,478 @@
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 os
9
+ from typing import Optional, Tuple
10
+
11
+ try:
12
+ from tango.gevent import DeviceProxy
13
+ from tango import DevSource
14
+ except ImportError:
15
+ DeviceProxy = None
16
+ DevSource = None
17
+ from blissdata import streaming_events
18
+ from blissdata.data import lima_image
19
+ from blissdata.settings import HashObjSetting
20
+
21
+
22
+ __all__ = ["LimaImageStatusEvent", "ImageNotSaved"]
23
+
24
+
25
+ class ImageNotSaved(RuntimeError):
26
+ """Raised when an API have to return data related to saved images."""
27
+
28
+ pass
29
+
30
+
31
+ class LimaImageStatusEvent(streaming_events.StreamEvent):
32
+ """There is no one-to-one correspondance between raw events
33
+ and images. An event repesents an acquisition status and
34
+ contains information on the progress of the image collection.
35
+
36
+ The decoding to images or image references needs an extra
37
+ info dict with information on lima saving settings. This is
38
+ actually a subset of the LimaImageChannelDataNode's info dictionary.
39
+ """
40
+
41
+ TYPE = b"LIMAIMAGESTATUS"
42
+ STATUS_KEY = b"__STATUS__"
43
+
44
+ DEFAULT_STATUS = {
45
+ "server_url": "",
46
+ "lima_acq_nb": -1,
47
+ "buffer_max_number": -1,
48
+ "last_image_acquired": -1,
49
+ "last_image_ready": -1,
50
+ "last_counter_ready": -1,
51
+ "last_image_saved": -1,
52
+ }
53
+
54
+ DEFAULT_INFO = {
55
+ "acq_trigger_mode": None,
56
+ "saving_mode": "NOSAVING", # files are not saved
57
+ "saving_overwrite": "ABORT", # TODO: still exists ????
58
+ "acq_nb_frames": None, # TODO: still exists ????
59
+ "saving_frame_per_file": None,
60
+ "saving_frame_per_acquisition": None,
61
+ "saving_next_number": 0,
62
+ "saving_index_format": "%04d",
63
+ "lima_version": "<1.9.1",
64
+ "user_instrument_name": "instrument",
65
+ "user_detector_name": None,
66
+ }
67
+
68
+ def __init__(self, *args, **kwargs):
69
+ self._proxy = None
70
+ super().__init__(*args, **kwargs)
71
+
72
+ def init(self, status, info=None, connection=None):
73
+ """
74
+ :param dict status: acquisition status
75
+ :param dict info: lima saving info, comes from
76
+ `node.info.get_all()` or from the reference list in Redis
77
+ :param connection: Redis db=1 connection needed to
78
+ get the last image from the server
79
+ """
80
+ self.status = status
81
+ self.info = info
82
+ self.connection = connection
83
+
84
+ @property
85
+ def status(self):
86
+ return self._status
87
+
88
+ @status.setter
89
+ def status(self, adict):
90
+ self._status = self._add_missing(adict, self.DEFAULT_STATUS)
91
+
92
+ @property
93
+ def info(self):
94
+ return self._info
95
+
96
+ @info.setter
97
+ def info(self, adict):
98
+ self._info = self._add_missing(adict, self.DEFAULT_INFO)
99
+
100
+ @staticmethod
101
+ def _add_missing(adict, defaults):
102
+ """
103
+ :param dict, HashObjSetting or None adict:
104
+ :param dict defaults:
105
+ :returns dict: different object than the given one
106
+ """
107
+ if adict is None:
108
+ adict = {}
109
+ elif isinstance(adict, HashObjSetting):
110
+ adict = adict.get_all()
111
+ return {**defaults, **adict}
112
+
113
+ def _encode(self):
114
+ raw = super()._encode()
115
+ raw[self.STATUS_KEY] = self.generic_encode(self.status)
116
+ # Remark: info and connection are not part of the raw events
117
+ return raw
118
+
119
+ def _decode(self, raw):
120
+ super()._decode(raw)
121
+ self.status = self.generic_decode(raw[self.STATUS_KEY])
122
+ self.info = dict(self.DEFAULT_INFO)
123
+ self.connection = None
124
+
125
+ @classmethod
126
+ def merge(cls, events):
127
+ """Keep only the last event.
128
+
129
+ :param list((index, raw)) events:
130
+ :returns LimaImageStatusEvent:
131
+ """
132
+ return cls(raw=events[-1][1])
133
+
134
+ def __getattr__(self, attr):
135
+ if attr in self.status:
136
+ return self.status[attr]
137
+ elif attr in self.info:
138
+ return self.info[attr]
139
+ raise AttributeError(attr)
140
+
141
+ def get_last_index(self, saved=False):
142
+ if saved:
143
+ return self.last_image_saved
144
+ else:
145
+ return self.last_image_ready
146
+
147
+ def all_image_references(self, saved=False):
148
+ """Get the image references.
149
+
150
+ :param bool saved: ready or ready and saved
151
+ :returns list(tuple): file name, path-in-file, image index, file format
152
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
153
+ :raise RuntimeError: images will never be saved
154
+ """
155
+ return list(self.iter_image_references(saved=saved))
156
+
157
+ def image_reference_range(self, from_index, to_index=None, saved=False):
158
+ """Get the image references.
159
+
160
+ :param int from_index:
161
+ :param int to_index: maximal ready/saved by default
162
+ :param bool saved: ready or ready and saved
163
+ :returns list(tuple): file name, path-in-file, image index, file format
164
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
165
+ :raise RuntimeError: fixed range which is not ready or saved yet
166
+ or images will never be saved
167
+ """
168
+ refs = list()
169
+ to_max = to_index is None
170
+ if to_max:
171
+ to_index = self.get_last_index(saved=saved)
172
+ if to_index >= from_index:
173
+ image_indices_in_scan = list(range(from_index, to_index + 1))
174
+ refs = list(self.iter_image_references(image_indices_in_scan, saved=saved))
175
+ if not to_max and len(refs) != len(image_indices_in_scan):
176
+ if saved:
177
+ reason = "saved"
178
+ else:
179
+ reason = "ready"
180
+ raise RuntimeError(f"Some images are not {reason} yet")
181
+ return refs
182
+
183
+ def image_references(self, image_indices_in_scan, saved=False):
184
+ """Get the image references.
185
+
186
+ :param sequence image_indices_in_scan:
187
+ :param bool saved: ready or ready and saved
188
+ :returns list(tuple): file name, path-in-file, image index, file format
189
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
190
+ :raise RuntimeError: some images are not ready or saved yet
191
+ or images will never be saved
192
+ """
193
+ refs = list(self.iter_image_references(image_indices_in_scan, saved=saved))
194
+ if len(refs) != len(image_indices_in_scan):
195
+ if saved:
196
+ reason = "saved"
197
+ else:
198
+ reason = "ready"
199
+ raise RuntimeError(f"Some images are not {reason} yet")
200
+ return refs
201
+
202
+ def image_reference(self, image_index_in_scan, saved=False):
203
+ """Get the image references.
204
+
205
+ :param int image_index_in_scan:
206
+ :param bool saved: ready or ready and saved
207
+ :returns tuple: file name, path-in-file, image index, file format
208
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
209
+ :raise RuntimeError: image is not ready or saved yet
210
+ or images will never be saved
211
+ """
212
+ return self.image_references([image_index_in_scan], saved=saved)[0]
213
+
214
+ def iter_image_references(self, image_indices_in_scan=None, saved=False):
215
+ """Get the image references.
216
+
217
+ Stops iterating when it encounters an image that is not
218
+ ready or saved yet, regardless of how many images you
219
+ asked for.
220
+
221
+ :param sequence image_indices_in_scan:
222
+ :param bool saved: ready or ready and saved
223
+ :yields list(tuple): file name, path-in-file, image index, file format
224
+ File format (HDF5, HDF5BS, EDFLZ4, ...) is not file extension!
225
+ :raises ImageNotSaved: In case the detector was not setup to save images
226
+ """
227
+ info = self.info
228
+ if info["saving_mode"] == "NOSAVING":
229
+ raise ImageNotSaved("Detector was not setup to save the images")
230
+
231
+ max_image_index_in_scan = self.get_last_index(saved=saved)
232
+ if image_indices_in_scan is None:
233
+ max_image_index_in_scan += 1
234
+ from_index = 0
235
+ to_index = max_image_index_in_scan
236
+ image_indices_in_scan = list(range(from_index, to_index))
237
+
238
+ # TODO: still exists ???
239
+ if info["saving_overwrite"] == "multiset":
240
+ images_per_file = info["acq_nb_frames"]
241
+ images_per_acquisition = max_image_index_in_scan
242
+ else:
243
+ images_per_file = info["saving_frame_per_file"]
244
+ images_per_acquisition = info["saving_frame_per_acquisition"]
245
+
246
+ first_file_index_in_scan = info["saving_next_number"]
247
+ if "saving_managed_mode" not in info:
248
+ raise RuntimeError(
249
+ "Lima info does not have 'saving_managed_mode'. Upgrade Bliss to solve it."
250
+ )
251
+ if info["saving_managed_mode"] == "HARDWARE":
252
+ # Assume Dectris hardware saving
253
+ subdir_format = (
254
+ info["saving_prefix"]
255
+ + "_data_"
256
+ + info["saving_index_format"]
257
+ + info["saving_suffix"]
258
+ )
259
+ else:
260
+ subdir_format = (
261
+ info["saving_prefix"]
262
+ + info["saving_index_format"]
263
+ + info["saving_suffix"]
264
+ )
265
+ path_format = os.path.join(info["saving_directory"], subdir_format)
266
+ file_format = info["saving_format"]
267
+
268
+ for image_index_in_scan in image_indices_in_scan:
269
+ file_index_in_scan, image_index_in_file = self.image_index_to_file_index(
270
+ image_index_in_scan,
271
+ max_image_index_in_scan,
272
+ images_per_file,
273
+ images_per_acquisition,
274
+ first_file_index_in_scan,
275
+ )
276
+ if file_index_in_scan is None:
277
+ break
278
+ if file_format.lower().startswith("hdf5"):
279
+ if info["saving_managed_mode"] == "HARDWARE":
280
+ # Assume Dectris hardware saving
281
+ path_in_file = "/entry/data/data"
282
+ # Dectris starts counting from 1 instead of 0 like lima does
283
+ file_index_in_scan += 1
284
+ elif info["lima_version"] == "<1.9.1":
285
+ # 'old' lima
286
+ path_in_file = (
287
+ "/entry_0000/instrument/"
288
+ + info["user_detector_name"]
289
+ + "/data/array"
290
+ )
291
+ else:
292
+ # 'new' lima
293
+ path_in_file = (
294
+ f"/entry_0000/{info['user_instrument_name']}"
295
+ + f"/{info['user_detector_name']}/data"
296
+ )
297
+ else:
298
+ path_in_file = ""
299
+ file_path = path_format % file_index_in_scan
300
+ yield file_path, path_in_file, image_index_in_file, file_format
301
+
302
+ @staticmethod
303
+ def image_index_to_file_index(
304
+ image_index_in_scan: int,
305
+ max_image_index_in_scan: int,
306
+ images_per_file: int,
307
+ images_per_acquisition: int,
308
+ first_file_index_in_scan: int,
309
+ ) -> Tuple[Optional[int], Optional[int]]:
310
+ """Example:
311
+
312
+ first_file_index_in_scan = 0
313
+ images_per_file = 10
314
+ images_per_acquisition = 23
315
+ max_image_index_in_scan = 46 # increases during the scan
316
+
317
+ Calculated:
318
+
319
+ files_per_acquisition = 3
320
+
321
+ image_index_in_scan= 0-9 acquisition_index_in_scan=0 image_index_in_acquisition= 0-9 file_index_in_acquisition=0 image_index_in_file=0-9 file_index_in_scan=0
322
+ image_index_in_scan=10-19 acquisition_index_in_scan=0 image_index_in_acquisition=10-19 file_index_in_acquisition=1 image_index_in_file=0-9 file_index_in_scan=1
323
+ image_index_in_scan=20-22 acquisition_index_in_scan=0 image_index_in_acquisition=20-22 file_index_in_acquisition=2 image_index_in_file=0-2 file_index_in_scan=2
324
+ image_index_in_scan=23-32 acquisition_index_in_scan=1 image_index_in_acquisition= 0-9 file_index_in_acquisition=0 image_index_in_file=0-9 file_index_in_scan=3
325
+ image_index_in_scan=33-42 acquisition_index_in_scan=1 image_index_in_acquisition=10-19 file_index_in_acquisition=1 image_index_in_file=0-9 file_index_in_scan=4
326
+ image_index_in_scan=43-45 acquisition_index_in_scan=1 image_index_in_acquisition=20-22 file_index_in_acquisition=2 image_index_in_file=0-2 file_index_in_scan=5
327
+ """
328
+ if image_index_in_scan > max_image_index_in_scan:
329
+ return None, None
330
+
331
+ if images_per_acquisition == 0: # timescan
332
+ acquisition_index_in_scan = 0
333
+ image_index_in_acquisition = image_index_in_scan
334
+ else:
335
+ acquisition_index_in_scan = image_index_in_scan // images_per_acquisition
336
+ image_index_in_acquisition = image_index_in_scan % images_per_acquisition
337
+
338
+ file_index_in_acquisition = image_index_in_acquisition // images_per_file
339
+ image_index_in_file = image_index_in_acquisition % images_per_file
340
+
341
+ file_index_in_scan = first_file_index_in_scan + file_index_in_acquisition
342
+ if acquisition_index_in_scan:
343
+ files_per_acquisition = images_per_acquisition // images_per_file + bool(
344
+ images_per_acquisition % images_per_file
345
+ )
346
+ file_index_in_scan += acquisition_index_in_scan * files_per_acquisition
347
+
348
+ return file_index_in_scan, image_index_in_file
349
+
350
+ @property
351
+ def proxy(self):
352
+ if DeviceProxy is None:
353
+ raise RuntimeError("requires 'pytango' to be installed")
354
+ if self._proxy is None and self.server_url:
355
+ self._proxy = DeviceProxy(self.server_url)
356
+ self._proxy.set_source(DevSource.DEV)
357
+ return self._proxy
358
+
359
+ def get_last_live_image(self):
360
+ """Returns the last image data from stream within it's frame number.
361
+
362
+ If no data is available, the function returns tuple (None, None).
363
+
364
+ If camera device is not configured with INTERNAL_TRIGGER_MULTI, and
365
+ then the reached frame number have no meaning, a None is returned.
366
+
367
+ :returns Frame:
368
+ """
369
+ proxy = self.proxy
370
+ if proxy is None:
371
+ # FIXME: It should return None
372
+ return lima_image.Frame(None, None, None)
373
+
374
+ result = lima_image.read_video_last_image(proxy)
375
+ if result is None:
376
+ # FIXME: It should return None
377
+ return lima_image.Frame(None, None, None)
378
+
379
+ frame, frame_number = result
380
+ if not self.is_video_frame_have_meaning():
381
+ # In this case the reached frame have no meaning within the full
382
+ # scan. It is better not to provide it
383
+ frame_number = None
384
+ return lima_image.Frame(frame, frame_number, "video")
385
+
386
+ def is_video_frame_have_meaning(self):
387
+ """Returns True if the frame number reached from the header from
388
+ the Lima video have a meaning in the full scan.
389
+
390
+ Returns a boolean, else None if this information is not yet known.
391
+ """
392
+ if self.acq_trigger_mode is None:
393
+ return None
394
+ # FIXME: This still can be wrong for a scan with many groups of MULTI images
395
+ # The function is_video_frame_have_meaning itself have not meaning and
396
+ # should be removed
397
+ return self.acq_trigger_mode in [
398
+ "INTERNAL_TRIGGER_MULTI",
399
+ "EXTERNAL_TRIGGER_MULTI",
400
+ "EXTERNAL_TRIGGER",
401
+ "EXTERNAL_GATE",
402
+ ]
403
+
404
+ def get_last_image(self):
405
+ """Returns the last image from the received one, together with the frame id.
406
+
407
+ :returns lima_image.Frame:
408
+ """
409
+ frame_number = self.last_image_ready
410
+ if frame_number < 0:
411
+ raise IndexError("No image has been taken yet")
412
+ data = None
413
+ if self.proxy is not None:
414
+ data = self._get_from_server_memory(frame_number)
415
+ source = "memory"
416
+ if data is None:
417
+ data = self._get_from_file(frame_number)
418
+ source = "file"
419
+ return lima_image.Frame(data, frame_number, source)
420
+
421
+ def get_image(self, image_index_in_scan):
422
+ """
423
+ :param int image_index_in_scan:
424
+ :returns numpy.ndarray:
425
+ """
426
+ if image_index_in_scan < 0:
427
+ raise ValueError("image_index_in_scan cannot be a negative number")
428
+ data = None
429
+ if self.proxy is not None:
430
+ data = self._get_from_server_memory(image_index_in_scan)
431
+ if data is None:
432
+ data = self._get_from_file(image_index_in_scan)
433
+ return data
434
+
435
+ def _get_from_server_memory(self, image_index_in_scan):
436
+ """
437
+ :param int image_index_in_scan:
438
+ :returns numpy.ndarray or None:
439
+ """
440
+ if self.current_lima_acq == self.lima_acq_nb: # current acquisition is this one
441
+ if self.last_image_ready < 0:
442
+ raise IndexError("No image has been taken yet")
443
+ if self.last_image_ready < image_index_in_scan: # image not yet available
444
+ raise IndexError("Image is not available yet")
445
+ # should be in memory
446
+ if self.buffer_max_number > (self.last_image_ready - image_index_in_scan):
447
+ try:
448
+ image_data = lima_image.image_from_server(
449
+ self.proxy, image_index_in_scan
450
+ )
451
+ return image_data.array
452
+ except RuntimeError:
453
+ # As it's asynchronous, image seems to be no
454
+ # longer available so read it from file
455
+ pass
456
+ return None
457
+
458
+ def _get_from_file(self, image_index_in_scan):
459
+ """
460
+ :param int image_index_in_scan:
461
+ :returns numpy.ndarray or None:
462
+ """
463
+ try:
464
+ values = self.image_references([image_index_in_scan], saved=True)
465
+ except IndexError:
466
+ raise IndexError("Cannot retrieve image %d from file" % image_index_in_scan)
467
+ return lima_image.image_from_file(*values[0])
468
+
469
+ @property
470
+ def current_lima_acq(self):
471
+ """The current server acquisition number
472
+ :returns int:
473
+ """
474
+ if self.server_url:
475
+ lima_acq = self.connection.get(self.server_url)
476
+ else:
477
+ lima_acq = None
478
+ return int(lima_acq if lima_acq is not None else -1)
@@ -0,0 +1,28 @@
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 import streaming_events
9
+
10
+ __all__ = ["NewNodeEvent"]
11
+
12
+
13
+ class NewNodeEvent(streaming_events.StreamEvent):
14
+
15
+ TYPE = b"NEW_NODE"
16
+ DB_KEY = b"db_name"
17
+
18
+ def init(self, db_name):
19
+ self.db_name = db_name
20
+
21
+ def _encode(self):
22
+ raw = super()._encode()
23
+ raw[self.DB_KEY] = self.encode_string(self.db_name)
24
+ return raw
25
+
26
+ def _decode(self, raw):
27
+ super()._decode(raw)
28
+ self.db_name = self.decode_string(raw[self.DB_KEY])
@@ -0,0 +1,49 @@
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
+ from blissdata import streaming_events
10
+
11
+ __all__ = ["EndScanEvent", "PreparedScanEvent"]
12
+
13
+
14
+ class EndScanEvent(streaming_events.EndEvent):
15
+
16
+ TYPE = b"END_SCAN"
17
+
18
+ @classmethod
19
+ def merge(cls, events):
20
+ """Keep only the first event.
21
+
22
+ :param list((index, raw)) events:
23
+ :returns EndScanEvent:
24
+ """
25
+ return cls(raw=events[0][1])
26
+
27
+ @property
28
+ def description(self):
29
+ """Used to generate EventData description"""
30
+ return self.exception
31
+
32
+
33
+ class PreparedScanEvent(streaming_events.PreparedEvent):
34
+
35
+ TYPE = b"PREPARED_SCAN"
36
+
37
+ @classmethod
38
+ def merge(cls, events):
39
+ """Keep only the first event.
40
+
41
+ :param list((index, raw)) events:
42
+ :returns PreparedScanEvent:
43
+ """
44
+ return cls(raw=events[0][1])
45
+
46
+ @property
47
+ def description(self):
48
+ """Used to generate EventData description"""
49
+ return None