ewoksdata 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. ewoksdata/__init__.py +0 -0
  2. ewoksdata/data/__init__.py +0 -0
  3. ewoksdata/data/bliss.py +341 -0
  4. ewoksdata/data/blissdata/__init__.py +31 -0
  5. ewoksdata/data/blissdata/blissdatav0.py +119 -0
  6. ewoksdata/data/blissdata/blissdatav1.py +279 -0
  7. ewoksdata/data/blissdata/blissdatav2.py +342 -0
  8. ewoksdata/data/contextiterator.py +43 -0
  9. ewoksdata/data/hdf5/__init__.py +23 -0
  10. ewoksdata/data/hdf5/config.py +186 -0
  11. ewoksdata/data/hdf5/dataset_writer/__init__.py +4 -0
  12. ewoksdata/data/hdf5/dataset_writer/_base.py +55 -0
  13. ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +139 -0
  14. ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +158 -0
  15. ewoksdata/data/hdf5/dataset_writer/_sequential.py +143 -0
  16. ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +195 -0
  17. ewoksdata/data/hdf5/types.py +14 -0
  18. ewoksdata/data/nexus.py +187 -0
  19. ewoksdata/data/url.py +30 -0
  20. ewoksdata/data/utils.py +53 -0
  21. ewoksdata/tests/__init__.py +0 -0
  22. ewoksdata/tests/conftest.py +30 -0
  23. ewoksdata/tests/data/__init__.py +1 -0
  24. ewoksdata/tests/data/bliss_scans.py +137 -0
  25. ewoksdata/tests/test_contextiterator.py +61 -0
  26. ewoksdata/tests/test_data_bliss.py +98 -0
  27. ewoksdata/tests/test_data_hdf5.py +14 -0
  28. ewoksdata/tests/test_data_nexus.py +80 -0
  29. ewoksdata/tests/test_dataset_writer.py +275 -0
  30. ewoksdata-2.1.0.dist-info/METADATA +70 -0
  31. ewoksdata-2.1.0.dist-info/RECORD +34 -0
  32. ewoksdata-2.1.0.dist-info/WHEEL +5 -0
  33. ewoksdata-2.1.0.dist-info/licenses/LICENSE.md +20 -0
  34. ewoksdata-2.1.0.dist-info/top_level.txt +1 -0
ewoksdata/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,341 @@
1
+ """Utilities for reading Bliss scan data from HDF5/Nexus files
2
+ or from memory (Redis or device servers).
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import sys
8
+ import time
9
+ from numbers import Integral
10
+ from numbers import Number
11
+ from typing import Any
12
+ from typing import Dict
13
+ from typing import Iterator
14
+ from typing import List
15
+ from typing import Optional
16
+ from typing import Sequence
17
+ from typing import Tuple
18
+ from typing import Union
19
+
20
+ import h5py
21
+ import hdf5plugin # noqa: F401 - needed for transparent compression handling
22
+ import numpy
23
+ from numpy.typing import ArrayLike
24
+ from silx.io import h5py_utils
25
+ from silx.io.url import DataUrl
26
+ from silx.io.utils import get_data as silx_get_data
27
+ from silx.utils import retry as retrymod
28
+
29
+ from . import hdf5
30
+ from . import nexus
31
+ from . import url
32
+ from .blissdata import BUFFER_IN_MEMORY_DEFAULT
33
+ from .blissdata import dynamic_hdf5
34
+ from .blissdata import iter_bliss_scan_data_from_memory
35
+ from .blissdata import iter_bliss_scan_data_from_memory_slice # noqa: F401
36
+ from .blissdata import last_lima_image # noqa: F401
37
+ from .contextiterator import contextiterator
38
+ from .utils import validate_and_init_names
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ def get_data(
44
+ data: Union[str, DataUrl, ArrayLike, Number],
45
+ retry_timeout: Optional[float] = None,
46
+ retry_period: Optional[float] = None,
47
+ **hdf5_options: Any,
48
+ ) -> Union[numpy.ndarray, Number, Sequence]:
49
+ """
50
+ Retrieve numerical data from a data source and return it as a NumPy array or scalar.
51
+
52
+ This function accepts various data representations, including data URLs,
53
+ array-like objects, and scalar numbers. The data source may not be
54
+ immediately available so a retry mechanism is used.
55
+
56
+ :param data: The input data source. May be:
57
+ - A string or :class:`DataUrl` referencing a dataset (e.g., HDF5),
58
+ - An array-like object convertible to a NumPy array,
59
+ - A scalar numerical value.
60
+ :param retry_timeout: Maximum time in seconds to wait for the data to become
61
+ available. If ``None`` (default), no timeout is applied.
62
+ :param retry_period: Interval in seconds between availability checks when
63
+ retrying. If ``None`` (default), a period of 0.1 seconds is used.
64
+ :param hdf5_options: Additional keyword arguments forwarded to
65
+ :class:`silx.io.h5py_utils.File` when opening HDF5 resources.
66
+
67
+ :returns: The resolved data. Array-like inputs are returned as NumPy arrays,
68
+ while scalar inputs are returned as numbers.
69
+
70
+ :raises TimeoutError: If the data does not become available within ``retry_timeout``.
71
+ :raises ValueError: If the input type is unsupported or the data cannot be resolved.
72
+ """
73
+ if isinstance(data, (str, DataUrl)):
74
+ data_url = url.as_dataurl(data)
75
+ filename, h5path, idx = url.h5dataset_url_parse(data_url)
76
+
77
+ _wait_for_file(filename, retry_timeout=retry_timeout, retry_period=retry_period)
78
+
79
+ if _is_hdf5_filename(filename):
80
+ return _get_hdf5_data(
81
+ filename,
82
+ h5path,
83
+ idx=idx,
84
+ retry_timeout=retry_timeout,
85
+ retry_period=retry_period,
86
+ **hdf5_options,
87
+ )
88
+
89
+ if not data_url.scheme():
90
+ for scheme in ("fabio", "silx"):
91
+ if sys.platform == "win32":
92
+ new_data_url = url.as_dataurl(f"{scheme}:///{data}")
93
+ else:
94
+ new_data_url = url.as_dataurl(f"{scheme}://{data}")
95
+ if new_data_url.is_valid():
96
+ data_url = new_data_url
97
+ break
98
+
99
+ return silx_get_data(data_url)
100
+
101
+ if isinstance(data, (Sequence, Number, numpy.ndarray)):
102
+ return data
103
+
104
+ raise TypeError(f"Unsupported data type: {type(data)!r}")
105
+
106
+
107
+ def _wait_for_file(
108
+ filename: str,
109
+ retry_timeout: Optional[float] = None,
110
+ retry_period: Optional[float] = None,
111
+ **_,
112
+ ) -> None:
113
+ if retry_period is None:
114
+ retry_period = 0.1
115
+ t0 = time.time()
116
+ while True:
117
+ if os.path.exists(filename):
118
+ return
119
+ if retry_timeout is not None:
120
+ if (time.time() - t0) > retry_timeout:
121
+ raise TimeoutError(
122
+ f"File {filename} is not created within {retry_timeout} sec"
123
+ )
124
+ time.sleep(retry_period)
125
+
126
+
127
+ def _is_hdf5_filename(filename: str) -> bool:
128
+ _, ext = os.path.splitext(filename)
129
+ return ext in (".h5", ".hdf5", ".nx", ".nxs", ".nexus")
130
+
131
+
132
+ def get_image(
133
+ data: Union[str, DataUrl, ArrayLike, Number], **hdf5_options: Any
134
+ ) -> numpy.ndarray:
135
+ """
136
+ Return a NumPy array or a scalar from *data*.
137
+
138
+ Retry options, such as `retry_timeout` and `retry_period` are forwarded to the internal HDF5 data reader.
139
+
140
+ :param data: Either a data-URL, an array-like object or a scalar number.
141
+ :param hdf5_options: Additional options forwarded to :class:`silx.io.h5py_utils.File`.
142
+ :return: A 2-D image.
143
+ """
144
+ data = get_data(data, **hdf5_options)
145
+ return numpy.atleast_2d(numpy.squeeze(data))
146
+
147
+
148
+ @h5py_utils.retry()
149
+ def _get_hdf5_data(
150
+ filename: str,
151
+ h5path: str,
152
+ idx: Optional[Tuple[int, ...]] = None,
153
+ **hdf5_options: Any,
154
+ ) -> numpy.ndarray:
155
+ """
156
+ Read a slice of a dataset inside an HDF5 / Nexus file.
157
+
158
+ :param filename: Path to the HDF5 / Nexus file.
159
+ :param h5path: Internal path to the dataset (e.g. ``/entry/data``).
160
+ :param idx: Optional index or slice tuple used to read a subset of the dataset.
161
+ :param **hdf5_options: Additional options forwarded to :class:`silx.io.h5py_utils.File`.
162
+ :return: The requested slice.
163
+ """
164
+ with hdf5.h5context(filename, h5path, **hdf5_options) as dset:
165
+ if _is_bliss_file(dset):
166
+ # ``end_time`` is added by the Bliss writer when the scan is
167
+ # finished. If it is missing we wait for the file to be
168
+ # completed.
169
+ if "end_time" not in nexus.get_nxentry(dset):
170
+ raise retrymod.RetryError("Bliss file not yet complete")
171
+ if idx is None:
172
+ idx = tuple()
173
+ return dset[idx]
174
+
175
+
176
+ @contextiterator
177
+ def iter_bliss_scan_data_from_hdf5(
178
+ filename: str,
179
+ scan_nr: Integral,
180
+ subscan: Optional[Integral] = None,
181
+ lima_names: Optional[List[str]] = None,
182
+ counter_names: Optional[List[str]] = None,
183
+ retry_timeout: Optional[float] = None,
184
+ retry_period: Optional[float] = None,
185
+ **hdf5_options: Any,
186
+ ) -> Iterator[Dict[str, Any]]:
187
+ """
188
+ Iterate over the data of a single Bliss scan stored in an HDF5 file.
189
+
190
+ :param filename: Bliss dataset filename (HDF5 / Nexus).
191
+ :param scan_nr: Scan number inside the file.
192
+ :param lima_names: List of LIMA detector names.
193
+ :param counter_names: List of non-LIMA counter names.
194
+ :param subscan: Sub-scan number (e.g. ``10.2`` -> ``scan_nr=10`` and ``subscan=2``).
195
+ :param retry_timeout: Timeout for the retry mechanism.
196
+ :param retry_period: Interval between retries.
197
+ :param **hdf5_options: Additional options forwarded to :class:`blissdata.h5api.dynamic_hdf5.File`.
198
+ :yields: Mapping ``detector_name -> value`` for the each scan point.
199
+ """
200
+ if not subscan:
201
+ subscan = 1
202
+ lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
203
+
204
+ with dynamic_hdf5.File(
205
+ filename,
206
+ lima_names=lima_names,
207
+ retry_timeout=retry_timeout,
208
+ retry_period=retry_period,
209
+ **hdf5_options,
210
+ ) as root:
211
+ scan = root[f"{scan_nr}.{subscan}"]
212
+ # assert _is_bliss_file(scan), "Not a Bliss dataset file"
213
+ measurement = scan["measurement"]
214
+ instrument = scan["instrument"]
215
+
216
+ datasets: Dict[str, Any] = {name: measurement[name] for name in counter_names}
217
+ for name in lima_names:
218
+ datasets[name] = instrument[f"{name}/data"]
219
+
220
+ names = list(datasets.keys())
221
+ for values in zip(*datasets.values()):
222
+ yield dict(zip(names, values))
223
+
224
+
225
+ @contextiterator
226
+ def iter_bliss_data(
227
+ filename: str,
228
+ scan_nr: Integral,
229
+ subscan: Optional[Integral] = None,
230
+ scan_key: Optional[str] = None,
231
+ lima_names: Optional[List[str]] = None,
232
+ counter_names: Optional[List[str]] = None,
233
+ start_index: Optional[int] = None,
234
+ retry_timeout: Optional[float] = None,
235
+ retry_period: Optional[float] = None,
236
+ buffer_in_memory: bool = BUFFER_IN_MEMORY_DEFAULT,
237
+ **options: Any,
238
+ ) -> Iterator[Tuple[int, Dict[str, Any]]]:
239
+ """
240
+ Iterate over a Bliss scan and also return the scan-point index.
241
+
242
+ :param filename: Bliss dataset filename.
243
+ :param scan_nr: Scan number inside the dataset.
244
+ :param subscan: Sub-scan number (e.g. ``10.2`` -> ``scan_nr=10`` and ``subscan=2``).
245
+ :param scan_key: Identifier of a scan for blissdata. Iterate scan data from blissdata-cache.
246
+ :param lima_names: List of LIMA detector names.
247
+ :param counter_names: List of non-LIMA counter names.
248
+ :param start_index: First point to yield (default ``0`` -> all points).
249
+ :param retry_timeout: Timeout for the retry mechanism.
250
+ :param retry_period: Interval between retries.
251
+ :param buffer_in_memory: Blissdata buffering in memory. Only applies when `scan_key` is provided.
252
+ Use only when you have enough memory or the data processing can keep up with the scan.
253
+ :param **hdf5_options: Additional options forwarded to :class:`blissdata.h5api.dynamic_hdf5.File`.
254
+ :yields: ``(point_index, data_dict)`` where ``point_index`` is the zero-based
255
+ scan-point number and ``data_dict`` maps detector names to values.
256
+ """
257
+ start_index = 0 if start_index is None else start_index
258
+
259
+ for idx, data in enumerate(
260
+ iter_bliss_scan_data(
261
+ filename,
262
+ scan_nr,
263
+ subscan=subscan,
264
+ scan_key=scan_key,
265
+ lima_names=lima_names,
266
+ counter_names=counter_names,
267
+ retry_timeout=retry_timeout,
268
+ retry_period=retry_period,
269
+ buffer_in_memory=buffer_in_memory,
270
+ **options,
271
+ )
272
+ ):
273
+ if idx >= start_index:
274
+ yield idx, data
275
+
276
+
277
+ @contextiterator
278
+ def iter_bliss_scan_data(
279
+ filename: str,
280
+ scan_nr: Integral,
281
+ subscan: Optional[Integral] = None,
282
+ scan_key: Optional[str] = None,
283
+ lima_names: Optional[List[str]] = None,
284
+ counter_names: Optional[List[str]] = None,
285
+ retry_timeout: Optional[float] = None,
286
+ retry_period: Optional[float] = None,
287
+ buffer_in_memory: bool = BUFFER_IN_MEMORY_DEFAULT,
288
+ **hdf5_options: Any,
289
+ ) -> Iterator[Dict[str, Any]]:
290
+ """
291
+ Yield the data of a Bliss scan either from an HDF5 file or from memory (Redis or device servers).
292
+
293
+ :param filename: Path to the HDF5 / Nexus file (ignored when *scan_key* is supplied).
294
+ :param scan_nr: Scan number inside the file.
295
+ :param subscan: Sub-scan number (e.g. ``10.2`` -> ``scan_nr=10`` and ``subscan=2``).
296
+ :param scan_key: Identifier of a scan for blissdata. Iterate scan data from blissdata-cache.
297
+ :param lima_names: List of LIMA detector names.
298
+ :param counter_names: List of non-LIMA counter names.
299
+ :param retry_timeout: Timeout for the retry mechanism.
300
+ :param retry_period: Interval between retries.
301
+ :param buffer_in_memory: Blissdata buffering in memory. Only applies when `scan_key` is provided.
302
+ Use only when you have enough memory or the data processing can keep up with the scan.
303
+ :param **hdf5_options: Additional options forwarded to :class:`blissdata.h5api.dynamic_hdf5.File`.
304
+ :yields: Mapping ``detector_name -> value`` for each scan point.
305
+ """
306
+ if scan_key:
307
+ yield from iter_bliss_scan_data_from_memory(
308
+ scan_key,
309
+ lima_names=lima_names,
310
+ counter_names=counter_names,
311
+ retry_timeout=retry_timeout,
312
+ retry_period=retry_period,
313
+ buffer_in_memory=buffer_in_memory,
314
+ )
315
+ else:
316
+ yield from iter_bliss_scan_data_from_hdf5(
317
+ filename,
318
+ scan_nr,
319
+ lima_names=lima_names,
320
+ counter_names=counter_names,
321
+ subscan=subscan,
322
+ retry_timeout=retry_timeout,
323
+ retry_period=retry_period,
324
+ **hdf5_options,
325
+ )
326
+
327
+
328
+ def _is_bliss_file(h5item: Union[h5py.Dataset, h5py.Group]) -> bool:
329
+ """
330
+ Return ``True`` if *h5item* belongs to a Bliss-generated file.
331
+
332
+ :param h5item: HDF5 dataset or group.
333
+ :return: ``True`` when the file attributes indicate a Bliss creator/publisher.
334
+ """
335
+ attrs = h5item.file.attrs
336
+ creator = attrs.get("creator", "").lower()
337
+ publisher = attrs.get("publisher", "").lower()
338
+ return creator in _BLISS_PUBLISHERS or publisher in _BLISS_PUBLISHERS
339
+
340
+
341
+ _BLISS_PUBLISHERS = ("bliss", "blissdata")
@@ -0,0 +1,31 @@
1
+ from importlib.metadata import version
2
+
3
+ from packaging.version import Version
4
+
5
+
6
+ def _not_supported_by_blissdata_version(*_, **kw):
7
+ raise RuntimeError(f"Not supported by blissdata {_BLISSDATA_VERSION}")
8
+
9
+
10
+ from blissdata.h5api import dynamic_hdf5 # noqa F401
11
+
12
+ _BLISSDATA_VERSION = Version(version("blissdata"))
13
+
14
+ if _BLISSDATA_VERSION >= Version("2.0.0rc1"):
15
+ from .blissdatav2 import iter_bliss_scan_data_from_memory # noqa F401
16
+ from .blissdatav2 import iter_bliss_scan_data_from_memory_slice # noqa F401
17
+ from .blissdatav2 import last_lima_image # noqa F401
18
+
19
+ BUFFER_IN_MEMORY_DEFAULT = False
20
+ elif _BLISSDATA_VERSION >= Version("1"):
21
+ from .blissdatav1 import iter_bliss_scan_data_from_memory # noqa F401
22
+ from .blissdatav1 import iter_bliss_scan_data_from_memory_slice # noqa F401
23
+ from .blissdatav1 import last_lima_image # noqa F401
24
+
25
+ BUFFER_IN_MEMORY_DEFAULT = True
26
+ else:
27
+ from .blissdatav0 import iter_bliss_scan_data_from_memory # noqa F401
28
+ from .blissdatav0 import last_lima_image # noqa F401
29
+
30
+ iter_bliss_scan_data_from_memory_slice = _not_supported_by_blissdata_version
31
+ BUFFER_IN_MEMORY_DEFAULT = True
@@ -0,0 +1,119 @@
1
+ import logging
2
+ from numbers import Number
3
+ from typing import Any
4
+ from typing import Dict
5
+ from typing import Generator
6
+ from typing import List
7
+ from typing import Optional
8
+
9
+ from blissdata.data.events.lima import ImageNotSaved
10
+ from blissdata.data.node import get_node
11
+ from numpy.typing import ArrayLike
12
+ from silx.utils import retry as retrymod
13
+
14
+ from ..utils import validate_and_init_names
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def iter_bliss_scan_data_from_memory(
20
+ db_name: str,
21
+ lima_names: Optional[List[str]] = None,
22
+ counter_names: Optional[List[str]] = None,
23
+ retry_timeout: Optional[Number] = None,
24
+ retry_period: Optional[Number] = None,
25
+ buffer_in_memory: bool = True,
26
+ ) -> Generator[Dict[str, Any], None, None]:
27
+ if not buffer_in_memory:
28
+ raise NotImplementedError("Requires blissdata>=2.0.0rc1")
29
+ lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
30
+
31
+ scan_node = _get_node(
32
+ db_name, "scan", retry_timeout=retry_timeout, retry_period=retry_period
33
+ )
34
+ indices = {name: 0 for name in lima_names + counter_names}
35
+ buffers = {name: list() for name in lima_names + counter_names}
36
+ if not buffers:
37
+ return
38
+
39
+ lima_acq_nb = dict()
40
+ for event_type, node, event_data in scan_node.walk_events():
41
+ if node.type == "lima":
42
+ name = node.db_name.split(":")[-2]
43
+ if name not in lima_names:
44
+ continue
45
+ dataview = _get_lima_dataview(
46
+ node,
47
+ indices[name],
48
+ retry_timeout=retry_timeout,
49
+ retry_period=retry_period,
50
+ )
51
+ current_lima_acq_nb = dataview.status_event.status["lima_acq_nb"]
52
+ first_lima_acq_nb = lima_acq_nb.setdefault(name, current_lima_acq_nb)
53
+ if first_lima_acq_nb != current_lima_acq_nb:
54
+ logger.warning("lima is already acquiring the next scan")
55
+ continue
56
+ try:
57
+ data = list(dataview)
58
+ except ImageNotSaved:
59
+ logger.warning(
60
+ "cannot read lima data from file because images are not being saved"
61
+ )
62
+ continue
63
+ except Exception as e:
64
+ logger.warning("cannot read lima data (%s)", str(e))
65
+ continue
66
+ indices[name] += len(data)
67
+ buffers[name].extend(data)
68
+ elif node.type == "channel":
69
+ name = node.db_name.split(":")[-1]
70
+ if name not in counter_names:
71
+ continue
72
+ if event_data:
73
+ data = event_data.data
74
+ else:
75
+ data = node.get_as_array(indices[name], -1)
76
+ indices[name] += len(data)
77
+ buffers[name].extend(data)
78
+ nyield = min(len(v) for v in buffers.values())
79
+ if nyield:
80
+ for i in range(nyield):
81
+ yield {name: values[i] for name, values in buffers.items()}
82
+ buffers = {name: values[nyield:] for name, values in buffers.items()}
83
+ if event_type == event_type.END_SCAN:
84
+ break
85
+
86
+
87
+ def last_lima_image(db_name: str) -> ArrayLike:
88
+ """Get last lima image from memory"""
89
+ node = _get_node(db_name, "lima")
90
+ node.from_stream = True
91
+ dataview = node.get(-1)
92
+ try:
93
+ image = dataview.get_last_live_image()
94
+ except AttributeError:
95
+ image = None
96
+ if image is None or image.data is None:
97
+ raise RuntimeError("Cannot get last image from lima")
98
+ return image.data
99
+
100
+
101
+ @retrymod.retry()
102
+ def _get_node(db_name: str, node_type: str):
103
+ node = get_node(db_name)
104
+ if node is None:
105
+ raise retrymod.RetryError(f"Redis node {db_name} does not exist")
106
+ if node.type != node_type:
107
+ raise RuntimeError(f"Not a Redis {node_type} node")
108
+ return node
109
+
110
+
111
+ @retrymod.retry()
112
+ def _get_lima_dataview(node, start_index: int):
113
+ dataview = node.get(start_index, -1)
114
+ try:
115
+ if dataview.status_event.proxy is None:
116
+ raise retrymod.RetryError("Lima proxy not known (yet)")
117
+ except Exception:
118
+ raise retrymod.RetryError("Lima proxy not known (yet)")
119
+ return dataview