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.
- ewoksdata/__init__.py +0 -0
- ewoksdata/data/__init__.py +0 -0
- ewoksdata/data/bliss.py +341 -0
- ewoksdata/data/blissdata/__init__.py +31 -0
- ewoksdata/data/blissdata/blissdatav0.py +119 -0
- ewoksdata/data/blissdata/blissdatav1.py +279 -0
- ewoksdata/data/blissdata/blissdatav2.py +342 -0
- ewoksdata/data/contextiterator.py +43 -0
- ewoksdata/data/hdf5/__init__.py +23 -0
- ewoksdata/data/hdf5/config.py +186 -0
- ewoksdata/data/hdf5/dataset_writer/__init__.py +4 -0
- ewoksdata/data/hdf5/dataset_writer/_base.py +55 -0
- ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +139 -0
- ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +158 -0
- ewoksdata/data/hdf5/dataset_writer/_sequential.py +143 -0
- ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +195 -0
- ewoksdata/data/hdf5/types.py +14 -0
- ewoksdata/data/nexus.py +187 -0
- ewoksdata/data/url.py +30 -0
- ewoksdata/data/utils.py +53 -0
- ewoksdata/tests/__init__.py +0 -0
- ewoksdata/tests/conftest.py +30 -0
- ewoksdata/tests/data/__init__.py +1 -0
- ewoksdata/tests/data/bliss_scans.py +137 -0
- ewoksdata/tests/test_contextiterator.py +61 -0
- ewoksdata/tests/test_data_bliss.py +98 -0
- ewoksdata/tests/test_data_hdf5.py +14 -0
- ewoksdata/tests/test_data_nexus.py +80 -0
- ewoksdata/tests/test_dataset_writer.py +275 -0
- ewoksdata-2.1.0.dist-info/METADATA +70 -0
- ewoksdata-2.1.0.dist-info/RECORD +34 -0
- ewoksdata-2.1.0.dist-info/WHEEL +5 -0
- ewoksdata-2.1.0.dist-info/licenses/LICENSE.md +20 -0
- ewoksdata-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import List
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import h5py
|
|
6
|
+
import numpy
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
|
|
9
|
+
from ..config import guess_dataset_config
|
|
10
|
+
from ..types import StrictPositiveIntegral
|
|
11
|
+
from ._base import DatasetWriterBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StackDatasetWriter(DatasetWriterBase):
|
|
15
|
+
"""Append arrays of the same shape to each item of a new HDF5 dataset
|
|
16
|
+
in a sequential manner per item. So each item of the HDF5 dataset is a
|
|
17
|
+
stack to which we can append data in a sequential manner.
|
|
18
|
+
|
|
19
|
+
Instead of creating a dataset with the :code:`h5py` API
|
|
20
|
+
|
|
21
|
+
.. code-block::python
|
|
22
|
+
|
|
23
|
+
stack0 = [[1,2,3], [4,5,6], [7,8,9]]
|
|
24
|
+
stack1 = [[10,11,12], [13,14,15], [16,17,18]]
|
|
25
|
+
h5group["mydataset"] = [stack0, stack1]
|
|
26
|
+
|
|
27
|
+
it can be done like this
|
|
28
|
+
|
|
29
|
+
.. code-block::python
|
|
30
|
+
|
|
31
|
+
with StackDatasetWriter(h5group, "mydataset") as writer:
|
|
32
|
+
writer.add_point([1,2,3], 0)
|
|
33
|
+
writer.add_point([10,11,12], 1)
|
|
34
|
+
writer.add_points([[13,14,15], [16,17,18]], 1)
|
|
35
|
+
writer.add_points([[4,5,6], [7,8,9]], 0)
|
|
36
|
+
|
|
37
|
+
Chunk size determination, chunk-aligned writing, compression and flushing is handled.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
parent: h5py.Group,
|
|
43
|
+
name: str,
|
|
44
|
+
npoints: Optional[StrictPositiveIntegral] = None,
|
|
45
|
+
nstack: Optional[StrictPositiveIntegral] = None,
|
|
46
|
+
attrs: Optional[dict] = None,
|
|
47
|
+
flush_period: Optional[float] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
|
|
50
|
+
self._npoints = npoints
|
|
51
|
+
self._chunked: bool = False
|
|
52
|
+
self._nstack = nstack
|
|
53
|
+
|
|
54
|
+
self._buffers: List[List[ArrayLike]] = list()
|
|
55
|
+
self._chunk_size: ArrayLike = numpy.zeros(2, dtype=int)
|
|
56
|
+
self._flushed_size_dim1: ArrayLike = numpy.array(list(), dtype=int)
|
|
57
|
+
|
|
58
|
+
def _create_dataset(
|
|
59
|
+
self, first_data_point: ArrayLike, stack_index: int
|
|
60
|
+
) -> h5py.Dataset:
|
|
61
|
+
scan_shape = (self._nstack, self._npoints)
|
|
62
|
+
first_data_point = numpy.asarray(first_data_point)
|
|
63
|
+
detector_shape = first_data_point.shape
|
|
64
|
+
dtype = first_data_point.dtype
|
|
65
|
+
|
|
66
|
+
if self._nstack is None or self._npoints is None:
|
|
67
|
+
shape = (stack_index + 1, 1) + detector_shape
|
|
68
|
+
max_shape = scan_shape + detector_shape
|
|
69
|
+
else:
|
|
70
|
+
shape = scan_shape + detector_shape
|
|
71
|
+
max_shape = None
|
|
72
|
+
|
|
73
|
+
options = guess_dataset_config(
|
|
74
|
+
scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
|
|
75
|
+
)
|
|
76
|
+
options.update(
|
|
77
|
+
shape=shape,
|
|
78
|
+
dtype=dtype,
|
|
79
|
+
fillvalue=numpy.nan, # converts to 0 for integers
|
|
80
|
+
)
|
|
81
|
+
if max_shape:
|
|
82
|
+
options["maxshape"] = max_shape
|
|
83
|
+
|
|
84
|
+
if options["chunks"]:
|
|
85
|
+
self._chunked = True
|
|
86
|
+
self._chunk_size = numpy.array(options["chunks"][:2], dtype=int)
|
|
87
|
+
|
|
88
|
+
dset = self._parent.create_dataset(self._name, **options)
|
|
89
|
+
if self._attrs:
|
|
90
|
+
dset.attrs.update(self._attrs)
|
|
91
|
+
return dset
|
|
92
|
+
|
|
93
|
+
def add_point(self, data: ArrayLike, stack_index: int) -> bool:
|
|
94
|
+
"""Append one array to one stack of the dataset."""
|
|
95
|
+
if self._dataset is None:
|
|
96
|
+
self._dataset = self._create_dataset(data, stack_index)
|
|
97
|
+
|
|
98
|
+
buffer = self._get_buffer(stack_index)
|
|
99
|
+
buffer.append(data)
|
|
100
|
+
return self.flush_buffer(align=True)
|
|
101
|
+
|
|
102
|
+
def add_points(self, data: ArrayLike, stack_index: int) -> bool:
|
|
103
|
+
"""Append several arrays at once to one stack of the dataset."""
|
|
104
|
+
if self._dataset is None:
|
|
105
|
+
self._dataset = self._create_dataset(data[0], stack_index)
|
|
106
|
+
|
|
107
|
+
buffer = self._get_buffer(stack_index)
|
|
108
|
+
buffer.extend(data)
|
|
109
|
+
return self.flush_buffer(align=True)
|
|
110
|
+
|
|
111
|
+
def flush_buffer(self, align: bool = False) -> bool:
|
|
112
|
+
# Determine how many points to flush for each buffer in the stack
|
|
113
|
+
chunk_sizes = numpy.array([len(buffer) for buffer in self._buffers])
|
|
114
|
+
flushed_size_dim1 = self._flushed_size_dim1
|
|
115
|
+
size_dim0 = len(chunk_sizes)
|
|
116
|
+
assert size_dim0 == len(
|
|
117
|
+
flushed_size_dim1
|
|
118
|
+
), "Number of buffers and number of flushed dim1 points must be the same"
|
|
119
|
+
|
|
120
|
+
chunk_size_dim0, chunk_size_dim1 = self._chunk_size[:2]
|
|
121
|
+
if chunk_size_dim0 == 0:
|
|
122
|
+
chunk_size_dim0 = size_dim0
|
|
123
|
+
|
|
124
|
+
if self._flush_time_expired():
|
|
125
|
+
flush_sizes = chunk_sizes
|
|
126
|
+
elif align and self._chunked:
|
|
127
|
+
size_dim0 = size_dim0 // chunk_size_dim0 * chunk_size_dim0
|
|
128
|
+
chunk_sizes = chunk_sizes[:size_dim0]
|
|
129
|
+
flushed_size_dim1 = flushed_size_dim1[:size_dim0]
|
|
130
|
+
if size_dim0:
|
|
131
|
+
n1 = chunk_sizes + (flushed_size_dim1 % chunk_size_dim1)
|
|
132
|
+
flush_sizes = n1 // chunk_size_dim1 * chunk_size_dim1
|
|
133
|
+
flush_sizes = numpy.minimum(flush_sizes, chunk_sizes)
|
|
134
|
+
for i0_chunk0 in range(0, size_dim0, chunk_size_dim0):
|
|
135
|
+
flush_sizes[i0_chunk0 : i0_chunk0 + chunk_size_dim0] = min(
|
|
136
|
+
flush_sizes[i0_chunk0 : i0_chunk0 + chunk_size_dim0]
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
flush_sizes = list()
|
|
140
|
+
else:
|
|
141
|
+
flush_sizes = chunk_sizes
|
|
142
|
+
|
|
143
|
+
if not any(flush_sizes):
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Enlarge the dataset when needed
|
|
147
|
+
nalloc = self._dataset.shape[:2]
|
|
148
|
+
istart_dim1 = flushed_size_dim1
|
|
149
|
+
flushed_size_dim1 = istart_dim1 + flush_sizes
|
|
150
|
+
nalloc_new = numpy.array([size_dim0, max(flushed_size_dim1)])
|
|
151
|
+
if self._chunked and any(nalloc_new > nalloc):
|
|
152
|
+
for axis, n in enumerate(nalloc_new):
|
|
153
|
+
self._dataset.resize(n, axis=axis)
|
|
154
|
+
|
|
155
|
+
# Copy data from buffer to HDF5
|
|
156
|
+
for i0_chunk0 in range(0, size_dim0, chunk_size_dim0):
|
|
157
|
+
idx_dim0 = slice(i0_chunk0, i0_chunk0 + chunk_size_dim0)
|
|
158
|
+
buffers = self._buffers[idx_dim0]
|
|
159
|
+
|
|
160
|
+
flush_sizes_dim1 = flush_sizes[idx_dim0]
|
|
161
|
+
non_ragged_buffers = len(set(flush_sizes_dim1)) == 1
|
|
162
|
+
|
|
163
|
+
istart0_dim1 = istart_dim1[idx_dim0]
|
|
164
|
+
non_ragged_destination = len(set(istart0_dim1)) == 1
|
|
165
|
+
|
|
166
|
+
if non_ragged_destination and non_ragged_buffers:
|
|
167
|
+
data = [buffer[: flush_sizes_dim1[0]] for buffer in buffers]
|
|
168
|
+
idx_dim1 = slice(istart0_dim1[0], istart0_dim1[0] + flush_sizes_dim1[0])
|
|
169
|
+
self._dataset[idx_dim0, idx_dim1] = data
|
|
170
|
+
else:
|
|
171
|
+
for buffer, i_dim0, istart_dim1, i_flush_size_dim1 in zip(
|
|
172
|
+
buffers,
|
|
173
|
+
range(i0_chunk0, i0_chunk0 + chunk_size_dim0),
|
|
174
|
+
istart0_dim1,
|
|
175
|
+
flush_sizes_dim1,
|
|
176
|
+
):
|
|
177
|
+
self._dataset[
|
|
178
|
+
i_dim0, istart_dim1 : istart_dim1 + i_flush_size_dim1, ...
|
|
179
|
+
] = buffer[:i_flush_size_dim1]
|
|
180
|
+
|
|
181
|
+
# Remove copied data from buffer
|
|
182
|
+
for i0 in range(size_dim0):
|
|
183
|
+
self._buffers[i0] = self._buffers[i0][flush_sizes[i0] :]
|
|
184
|
+
self._flushed_size_dim1[i0] = flushed_size_dim1[i0]
|
|
185
|
+
|
|
186
|
+
self._flush_hdf5()
|
|
187
|
+
self._last_flush = time.time()
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
def _get_buffer(self, stack_index: int) -> List[ArrayLike]:
|
|
191
|
+
# Add stack buffers when needed
|
|
192
|
+
for _ in range(max(stack_index - len(self._buffers) + 1, 0)):
|
|
193
|
+
self._buffers.append(list())
|
|
194
|
+
self._flushed_size_dim1 = numpy.append(self._flushed_size_dim1, 0)
|
|
195
|
+
return self._buffers[stack_index]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from numbers import Integral
|
|
2
|
+
from typing import NewType
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
PositiveIntegral = NewType("PositiveIntegral", Integral) # >= 0
|
|
7
|
+
StrictPositiveIntegral = NewType("StrictPositiveIntegral", Integral) # > 0
|
|
8
|
+
ShapeType = Tuple[StrictPositiveIntegral]
|
|
9
|
+
SizeType = PositiveIntegral
|
|
10
|
+
|
|
11
|
+
VarShapeType = Tuple[Optional[PositiveIntegral]] # 0 or None mark a variable dimension
|
|
12
|
+
VarH5pyShapeType = Tuple[
|
|
13
|
+
Optional[StrictPositiveIntegral]
|
|
14
|
+
] # None marks a variable dimension
|
ewoksdata/data/nexus.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Iterator
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import h5py
|
|
10
|
+
from silx.io import h5py_utils
|
|
11
|
+
|
|
12
|
+
from .url import DataUrl
|
|
13
|
+
from .url import as_dataurl
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_nxclass(group: h5py.Group) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Ensure the HDF5 group has an appropriate ``NX_class`` attribute.
|
|
19
|
+
|
|
20
|
+
The attribute is assigned based on the group's depth in the hierarchy:
|
|
21
|
+
* Root group (``/``): ``NXroot``
|
|
22
|
+
* First-level group: ``NXentry``
|
|
23
|
+
* Deeper groups: ``NXcollection``
|
|
24
|
+
|
|
25
|
+
:param group: The HDF5 group to update.
|
|
26
|
+
:type group: h5py.Group
|
|
27
|
+
"""
|
|
28
|
+
if group.attrs.get("NX_class"):
|
|
29
|
+
return
|
|
30
|
+
groups = [s for s in group.name.split("/") if s]
|
|
31
|
+
n = len(groups)
|
|
32
|
+
if n == 0:
|
|
33
|
+
group.attrs["NX_class"] = "NXroot"
|
|
34
|
+
elif n == 1:
|
|
35
|
+
group.attrs["NX_class"] = "NXentry"
|
|
36
|
+
else:
|
|
37
|
+
group.attrs["NX_class"] = "NXcollection"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def select_default_plot(nxdata: h5py.Group) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Set the default NXdata group for visualization in the parent hierarchy.
|
|
43
|
+
|
|
44
|
+
The function walks up the group tree and sets the ``default`` attribute
|
|
45
|
+
in each parent to point to the given NXdata group.
|
|
46
|
+
|
|
47
|
+
:param nxdata: An HDF5 group representing an NXdata node.
|
|
48
|
+
:type nxdata: h5py.Group
|
|
49
|
+
"""
|
|
50
|
+
parent = nxdata.parent
|
|
51
|
+
for name in nxdata.name.split("/")[::-1]:
|
|
52
|
+
if not name:
|
|
53
|
+
continue
|
|
54
|
+
parent.attrs["default"] = name
|
|
55
|
+
parent = parent.parent
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def create_url(url: str, overwrite: bool = False, **open_options) -> DataUrl:
|
|
59
|
+
"""
|
|
60
|
+
Create (or ensure the existence of) an HDF5 group specified by a URL.
|
|
61
|
+
|
|
62
|
+
:param url: The HDF5 URL (e.g. ``path/to/file.h5::/entry/data``).
|
|
63
|
+
:param overwrite: If True, overwrite the final group if it exists.
|
|
64
|
+
If False, reuse existing groups. Default is False.
|
|
65
|
+
:param open_options: Additional options passed to ``h5py.File``.
|
|
66
|
+
:return: A DataUrl object pointing to the created or existing group.
|
|
67
|
+
"""
|
|
68
|
+
url = as_dataurl(url)
|
|
69
|
+
filename = url.file_path()
|
|
70
|
+
data_path = url.data_path() or "/"
|
|
71
|
+
dirname = os.path.dirname(filename)
|
|
72
|
+
if dirname:
|
|
73
|
+
os.makedirs(dirname, exist_ok=True)
|
|
74
|
+
_ = open_options.setdefault("mode", "a")
|
|
75
|
+
with h5py_utils.open_item(filename, "/", **open_options) as parent:
|
|
76
|
+
h5item, _ = _create_h5group(parent, data_path, overwrite=overwrite)
|
|
77
|
+
return as_dataurl(f"{filename}::{h5item.name}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_nxentry(h5item: Union[h5py.Dataset, h5py.Group]) -> h5py.Group:
|
|
81
|
+
"""
|
|
82
|
+
Return the top-level NXentry group containing the given HDF5 item.
|
|
83
|
+
|
|
84
|
+
:param h5item: An HDF5 dataset or group.
|
|
85
|
+
:return: The NXentry group containing ``h5item``.
|
|
86
|
+
:raises ValueError: If the item does not belong to an NXentry hierarchy.
|
|
87
|
+
"""
|
|
88
|
+
parts = [s for s in h5item.name.split("/") if s]
|
|
89
|
+
if parts:
|
|
90
|
+
return h5item.file[parts[0]]
|
|
91
|
+
raise ValueError("HDF5 item must be part of an NXentry")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@contextmanager
|
|
95
|
+
def create_nexus_group(
|
|
96
|
+
url: Union[str, DataUrl],
|
|
97
|
+
retry_timeout=None,
|
|
98
|
+
retry_period=None,
|
|
99
|
+
default_levels: Optional[Sequence[str]] = None,
|
|
100
|
+
**open_options,
|
|
101
|
+
) -> Iterator[Tuple[h5py.Group, bool]]:
|
|
102
|
+
"""
|
|
103
|
+
Context manager to create or access an HDF5 group specified by a URL.
|
|
104
|
+
|
|
105
|
+
The data path inside HDF5 is ensured to have at least ``default_levels`` levels.
|
|
106
|
+
|
|
107
|
+
:param url: The HDF5 URL (e.g. ``path/to/file.h5::/entry/data``).
|
|
108
|
+
:param retry_timeout: Maximum time in seconds to retry opening the file.
|
|
109
|
+
:param retry_period: Time in seconds between retries.
|
|
110
|
+
:param default_levels: Minimum path hierarchy touse when missing. Default is ``["/", "results"]``.
|
|
111
|
+
:param open_options: Additional options passed to ``h5py.File``.
|
|
112
|
+
:yields: Tuple of (HDF5 group, already_existed flag).
|
|
113
|
+
"""
|
|
114
|
+
url = as_dataurl(url)
|
|
115
|
+
filename = url.file_path()
|
|
116
|
+
itemname = url.data_path() or "/"
|
|
117
|
+
dirname = os.path.dirname(filename)
|
|
118
|
+
if dirname:
|
|
119
|
+
os.makedirs(dirname, exist_ok=True)
|
|
120
|
+
open_options.setdefault("mode", "a")
|
|
121
|
+
with h5py_utils.open_item(
|
|
122
|
+
filename,
|
|
123
|
+
"/",
|
|
124
|
+
retry_timeout=retry_timeout,
|
|
125
|
+
retry_period=retry_period,
|
|
126
|
+
**open_options,
|
|
127
|
+
) as root:
|
|
128
|
+
yield _create_h5group(root, itemname, default_levels=default_levels)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _create_h5group(
|
|
132
|
+
parent: h5py.Group,
|
|
133
|
+
data_path: str,
|
|
134
|
+
default_levels: Optional[Sequence[str]] = None,
|
|
135
|
+
overwrite: bool = False,
|
|
136
|
+
) -> Tuple[h5py.Group, bool]:
|
|
137
|
+
"""
|
|
138
|
+
Create or retrieve an HDF5 group under a given parent.
|
|
139
|
+
|
|
140
|
+
Ensures that intermediate groups exist, optionally enforcing a minimum
|
|
141
|
+
depth using ``default_levels``.
|
|
142
|
+
|
|
143
|
+
Missing ``NX_class`` attributes are automatically created on all levels.
|
|
144
|
+
The final group can be replaced if ``overwrite=True``.
|
|
145
|
+
|
|
146
|
+
:param parent: Parent group under which the path should be created.
|
|
147
|
+
:param data_path: Group path to create relative to ``parent``.
|
|
148
|
+
:param default_levels: Minimum hierarchy to enforce.
|
|
149
|
+
If ``data_path`` is shorter, extra levels are added.
|
|
150
|
+
:param overwrite: If True, delete and recreate the final group if it exists.
|
|
151
|
+
Default is False.
|
|
152
|
+
:return: Tuple of (HDF5 group, already_existed flag).
|
|
153
|
+
"""
|
|
154
|
+
# Ensure the data path has at least N levels as defined by ``default_levels``
|
|
155
|
+
groups = [s for s in data_path.split("/") if s]
|
|
156
|
+
if default_levels:
|
|
157
|
+
default_levels = list(default_levels)
|
|
158
|
+
else:
|
|
159
|
+
default_levels = ["results"]
|
|
160
|
+
if len(groups) < len(default_levels):
|
|
161
|
+
groups += default_levels[len(groups) :]
|
|
162
|
+
|
|
163
|
+
# Ensure parent groups exist + ensure NX_class
|
|
164
|
+
create = False
|
|
165
|
+
ensure_nxclass(parent)
|
|
166
|
+
for group in groups[:-1]:
|
|
167
|
+
if group in parent:
|
|
168
|
+
parent = parent[group]
|
|
169
|
+
else:
|
|
170
|
+
parent = parent.create_group(group)
|
|
171
|
+
create = True
|
|
172
|
+
ensure_nxclass(parent)
|
|
173
|
+
|
|
174
|
+
# Final group: create, overwrite or confirm existence + ensure NX_class
|
|
175
|
+
group = groups[-1]
|
|
176
|
+
if group in parent:
|
|
177
|
+
if overwrite:
|
|
178
|
+
del parent[group]
|
|
179
|
+
parent = parent.create_group(group)
|
|
180
|
+
else:
|
|
181
|
+
parent = parent[group]
|
|
182
|
+
else:
|
|
183
|
+
parent = parent.create_group(group)
|
|
184
|
+
create = True
|
|
185
|
+
ensure_nxclass(parent)
|
|
186
|
+
|
|
187
|
+
return parent, not create
|
ewoksdata/data/url.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from silx.io.url import DataUrl
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def as_dataurl(url: Union[str, DataUrl]) -> DataUrl:
|
|
9
|
+
if isinstance(url, str) and sys.platform == "win32":
|
|
10
|
+
url = url.replace("\\", "/")
|
|
11
|
+
if not isinstance(url, DataUrl):
|
|
12
|
+
url = DataUrl(url)
|
|
13
|
+
return url
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def as_h5url(url: Union[str, DataUrl]) -> DataUrl:
|
|
17
|
+
url = as_dataurl(url)
|
|
18
|
+
if not url.data_path():
|
|
19
|
+
return DataUrl(f"{url.file_path()}::/")
|
|
20
|
+
return url
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def h5dataset_url_parse(url: Union[str, DataUrl]) -> Tuple[str, str, Tuple]:
|
|
24
|
+
url = as_h5url(url)
|
|
25
|
+
filename = str(url.file_path())
|
|
26
|
+
h5path = url.data_path()
|
|
27
|
+
idx = url.data_slice()
|
|
28
|
+
if idx is None:
|
|
29
|
+
idx = tuple()
|
|
30
|
+
return filename, h5path, idx
|
ewoksdata/data/utils.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from numbers import Number
|
|
2
|
+
from typing import List
|
|
3
|
+
from typing import Mapping
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
import numpy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_data(data):
|
|
11
|
+
if isinstance(data, (numpy.ndarray, Number)):
|
|
12
|
+
return True
|
|
13
|
+
if isinstance(data, (str, list)) and data:
|
|
14
|
+
return True
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def data_from_storage(data, remove_numpy=True):
|
|
19
|
+
if isinstance(data, numpy.ndarray):
|
|
20
|
+
if not remove_numpy:
|
|
21
|
+
return data
|
|
22
|
+
elif data.ndim == 0:
|
|
23
|
+
return data.item()
|
|
24
|
+
else:
|
|
25
|
+
return data.tolist()
|
|
26
|
+
elif isinstance(data, Mapping):
|
|
27
|
+
return {
|
|
28
|
+
k: data_from_storage(v, remove_numpy=remove_numpy)
|
|
29
|
+
for k, v in data.items()
|
|
30
|
+
if not k.startswith("@")
|
|
31
|
+
}
|
|
32
|
+
else:
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate_names(names: Optional[List[str]], name_type: str) -> None:
|
|
37
|
+
if names is None:
|
|
38
|
+
return
|
|
39
|
+
for n in names:
|
|
40
|
+
if not isinstance(n, str) or not n.strip():
|
|
41
|
+
raise ValueError(f"Invalid {name_type} name: {n!r}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_and_init_names(
|
|
45
|
+
lima_names: Optional[List[str]],
|
|
46
|
+
counter_names: Optional[List[str]],
|
|
47
|
+
) -> Tuple[List[str], List[str]]:
|
|
48
|
+
_validate_names(lima_names, "LIMA")
|
|
49
|
+
_validate_names(counter_names, "Counter")
|
|
50
|
+
return (
|
|
51
|
+
lima_names if lima_names is not None else [],
|
|
52
|
+
counter_names if counter_names is not None else [],
|
|
53
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from . import data
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture(scope="session")
|
|
8
|
+
def bliss_scan(tmpdir_factory):
|
|
9
|
+
tmpdir = tmpdir_factory.mktemp("sample_dataset")
|
|
10
|
+
npoints_per_file = 3
|
|
11
|
+
npoints = 31
|
|
12
|
+
scannr = 2
|
|
13
|
+
image = numpy.zeros((10, 12))
|
|
14
|
+
return (
|
|
15
|
+
str(
|
|
16
|
+
data.save_bliss_scan(
|
|
17
|
+
tmpdir,
|
|
18
|
+
image,
|
|
19
|
+
npoints_per_file,
|
|
20
|
+
npoints,
|
|
21
|
+
scannr,
|
|
22
|
+
subscannr=1,
|
|
23
|
+
lima_names=("p3", "p4"),
|
|
24
|
+
counter_names=("diode1", "diode2"),
|
|
25
|
+
sequence="add",
|
|
26
|
+
)
|
|
27
|
+
),
|
|
28
|
+
npoints,
|
|
29
|
+
scannr,
|
|
30
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .bliss_scans import * # noqa F403
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
import h5py
|
|
8
|
+
import numpy
|
|
9
|
+
|
|
10
|
+
__all__ = ["save_bliss_scan"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_bliss_scan(
|
|
14
|
+
tmpdir: Path,
|
|
15
|
+
image: numpy.ndarray,
|
|
16
|
+
npoints_per_file: int,
|
|
17
|
+
npoints: int,
|
|
18
|
+
scannr: int,
|
|
19
|
+
subscannr: int = 1,
|
|
20
|
+
sequence: str = "add",
|
|
21
|
+
lima_names: Tuple[str, ...] = ("p3",),
|
|
22
|
+
counter_names: Tuple[str, ...] = ("diode1", "diode2"),
|
|
23
|
+
positioner_names: Tuple[str, ...] = ("diffty", "diffrz"),
|
|
24
|
+
title: Optional[str] = None,
|
|
25
|
+
) -> Path:
|
|
26
|
+
dataset_filename = tmpdir / "sample_dataset.h5"
|
|
27
|
+
lima_dirname = tmpdir / f"scan{scannr:04d}"
|
|
28
|
+
lima_dirname.mkdir()
|
|
29
|
+
with h5py.File(str(dataset_filename), mode="w") as root:
|
|
30
|
+
root.attrs["NX_class"] = "NXroot"
|
|
31
|
+
root.attrs["creator"] = "bliss"
|
|
32
|
+
scan = root.create_group(f"{scannr}.{subscannr}")
|
|
33
|
+
scan.attrs["NX_class"] = "NXentry"
|
|
34
|
+
if title is not None:
|
|
35
|
+
scan["title"] = title
|
|
36
|
+
|
|
37
|
+
instrument = scan.create_group("instrument")
|
|
38
|
+
instrument.attrs["NX_class"] = "NXinstrument"
|
|
39
|
+
measurement = scan.create_group("measurement")
|
|
40
|
+
measurement.attrs["NX_class"] = "NXcollection"
|
|
41
|
+
|
|
42
|
+
if positioner_names:
|
|
43
|
+
positioners = instrument.create_group("positioners")
|
|
44
|
+
positioners.attrs["NX_class"] = "NXcollection"
|
|
45
|
+
|
|
46
|
+
for name in counter_names:
|
|
47
|
+
_save_counter(scan, name, npoints)
|
|
48
|
+
for name in lima_names:
|
|
49
|
+
_save_lima(
|
|
50
|
+
scan,
|
|
51
|
+
name,
|
|
52
|
+
image,
|
|
53
|
+
npoints,
|
|
54
|
+
npoints_per_file,
|
|
55
|
+
lima_dirname,
|
|
56
|
+
scannr,
|
|
57
|
+
sequence,
|
|
58
|
+
)
|
|
59
|
+
for name in positioner_names:
|
|
60
|
+
_save_positioner(scan, name, npoints)
|
|
61
|
+
|
|
62
|
+
scan["end_time"] = datetime.now().astimezone().isoformat()
|
|
63
|
+
return dataset_filename
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _save_counter(scan: h5py.Group, name: str, npoints: int) -> None:
|
|
67
|
+
diode = _add_detector(scan, name)
|
|
68
|
+
diode["data"] = numpy.arange(npoints)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _save_lima(
|
|
72
|
+
scan: h5py.Group,
|
|
73
|
+
name: str,
|
|
74
|
+
image: numpy.ndarray,
|
|
75
|
+
npoints: int,
|
|
76
|
+
npoints_per_file: int,
|
|
77
|
+
lima_dirname: Path,
|
|
78
|
+
scannr: int,
|
|
79
|
+
sequence: str,
|
|
80
|
+
) -> None:
|
|
81
|
+
tot_shape = (npoints,) + image.shape
|
|
82
|
+
lima_shape = (npoints_per_file,) + image.shape
|
|
83
|
+
nlima = npoints // npoints_per_file + bool(npoints % npoints_per_file)
|
|
84
|
+
lima_dtype = float
|
|
85
|
+
layout = h5py.VirtualLayout(tot_shape, dtype=lima_dtype)
|
|
86
|
+
for i in range(nlima):
|
|
87
|
+
with h5py.File(str(lima_dirname / f"{name}_{i}.h5"), mode="w") as limaroot:
|
|
88
|
+
limaroot.attrs["NX_class"] = "NXroot"
|
|
89
|
+
entry = limaroot.create_group("entry_0000")
|
|
90
|
+
entry.attrs["NX_class"] = "NXentry"
|
|
91
|
+
lmeasurement = entry.create_group("measurement")
|
|
92
|
+
lmeasurement.attrs["NX_class"] = "NXcollection"
|
|
93
|
+
lmeasurement["data"] = numpy.full(lima_shape, numpy.nan)
|
|
94
|
+
|
|
95
|
+
i0 = i * npoints_per_file
|
|
96
|
+
i1 = min(i0 + npoints_per_file, npoints)
|
|
97
|
+
for itot in range(i0, i1):
|
|
98
|
+
if sequence == "add":
|
|
99
|
+
lmeasurement["data"][itot - i0, ...] = image + itot
|
|
100
|
+
else:
|
|
101
|
+
m = itot / (npoints - 1) + 1
|
|
102
|
+
if itot == 0:
|
|
103
|
+
monitor = _add_detector(scan, "monitor")
|
|
104
|
+
monitor["data"] = numpy.zeros(npoints)
|
|
105
|
+
monitor["data"][itot] = m
|
|
106
|
+
lmeasurement["data"][itot - i0, ...] = image * m
|
|
107
|
+
|
|
108
|
+
spath = os.path.join(f"scan{scannr:04d}", f"{name}_{i}.h5")
|
|
109
|
+
sdset = "/entry_0000/measurement/data"
|
|
110
|
+
|
|
111
|
+
sshape = (i1 - i0,) + lima_shape[1:]
|
|
112
|
+
vsource = h5py.VirtualSource(spath, sdset, shape=sshape, dtype=lima_dtype)
|
|
113
|
+
|
|
114
|
+
layout[i0:i1] = vsource[:]
|
|
115
|
+
detector = _add_detector(scan, name)
|
|
116
|
+
detector.create_virtual_dataset("data", layout, fillvalue=numpy.nan)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _add_detector(scan: h5py.Group, name: str) -> h5py.Group:
|
|
120
|
+
grp = scan["instrument"].create_group(name)
|
|
121
|
+
grp.attrs["NX_class"] = "NXdetector"
|
|
122
|
+
scan["measurement"][name] = h5py.SoftLink(f"{grp.name}/data")
|
|
123
|
+
return grp
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _save_positioner(scan: h5py.Group, name: str, npoints: int) -> None:
|
|
127
|
+
positioner = _add_positioner(scan, name)
|
|
128
|
+
positioner["value"] = numpy.arange(npoints)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _add_positioner(scan: h5py.Group, name: str) -> h5py.Group:
|
|
132
|
+
instrument = scan["instrument"]
|
|
133
|
+
grp = instrument.create_group(name)
|
|
134
|
+
grp.attrs["NX_class"] = "Nxpositioner"
|
|
135
|
+
scan["measurement"][name] = h5py.SoftLink(f"{grp.name}/value")
|
|
136
|
+
instrument["positioners"][name] = h5py.SoftLink(f"{grp.name}/value")
|
|
137
|
+
return grp
|