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,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
|