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
@@ -0,0 +1,186 @@
1
+ """HDF5 configuration of optimal data storage (IO speed, compression, ...)"""
2
+
3
+ from collections.abc import Mapping
4
+ from numbers import Integral
5
+ from typing import Optional
6
+
7
+ import numpy
8
+ from numpy.typing import DTypeLike
9
+
10
+ try:
11
+ import hdf5plugin
12
+ except ImportError:
13
+ hdf5plugin = None
14
+
15
+ from .types import ShapeType
16
+ from .types import VarH5pyShapeType
17
+ from .types import VarShapeType
18
+
19
+ DEFAULT_CHUNK_NBYTES = 1 << 20
20
+ DEFAULT_COMPRESSION_LIMIT_NBYTES = 1 << 20
21
+ DEFAULT_CHUNK_SPLIT = 4
22
+ DEFAULT_COMPRESSION_SCHEME = "gzip-byteshuffle"
23
+
24
+ # Default data size
25
+ # 0D detector: 2 KB
26
+ # 1D detector: 2 MB
27
+ # 2D detector: 2 GB
28
+ DEFAULT_SCAN_DIM_SIZE = 512
29
+ DEFAULT_DETECTOR_DIM_SIZE = 1024
30
+ DEFAULT_DTYPE = numpy.int32
31
+
32
+
33
+ def dtype_nbytes(dtype: DTypeLike) -> int:
34
+ return numpy.dtype(dtype).itemsize
35
+
36
+
37
+ def shape_to_size(shape: ShapeType) -> int:
38
+ # numpy.prod gives problems on windows
39
+ n = 1
40
+ for x in shape:
41
+ n *= x
42
+ return n
43
+
44
+
45
+ def shape_to_nbytes(shape: ShapeType, dtype: DTypeLike) -> int:
46
+ return shape_to_size(shape) * dtype_nbytes(dtype)
47
+
48
+
49
+ def guess_data_shape(
50
+ scan_shape: VarShapeType,
51
+ detector_shape: VarShapeType,
52
+ max_shape: Optional[VarH5pyShapeType],
53
+ ) -> ShapeType:
54
+ scan_shape = tuple(n if n else DEFAULT_SCAN_DIM_SIZE for n in scan_shape)
55
+ detector_shape = tuple(n if n else DEFAULT_SCAN_DIM_SIZE for n in detector_shape)
56
+ data_shape = scan_shape + detector_shape
57
+ if max_shape:
58
+ assert len(max_shape) == len(
59
+ data_shape
60
+ ), "HDF5 dataset shape must have the same dimensions as maxshape"
61
+ data_shape = tuple(
62
+ n1 if not n2 else max(n1, n2) for n1, n2 in zip(data_shape, max_shape)
63
+ )
64
+ return data_shape
65
+
66
+
67
+ def guess_chunk_shape(
68
+ data_shape: ShapeType,
69
+ dtype: DTypeLike,
70
+ chunk_split: Optional[Integral] = None,
71
+ chunk_nbytes: Optional[Integral] = None,
72
+ ) -> Optional[ShapeType]:
73
+ """Try to guess the optimal chunk shape with these constraints:
74
+ * Split any dimension for partial access
75
+ * Below the maximal chunk size (1 MB by default, uncompressed)
76
+
77
+ The inner-most dimensions are split in `chunk_split` parts
78
+ until chunk_nbytes is reached. The chunk size in the outer
79
+ dimensions will be 1, unless the data size is too small.
80
+ """
81
+ if chunk_nbytes is None:
82
+ chunk_nbytes = DEFAULT_CHUNK_NBYTES
83
+ if chunk_split is None:
84
+ chunk_split = DEFAULT_CHUNK_SPLIT
85
+ itemsize = dtype_nbytes(dtype)
86
+ size = shape_to_size(data_shape)
87
+ nbytes = size * itemsize
88
+ if nbytes <= chunk_nbytes:
89
+ return None
90
+
91
+ max_size = chunk_nbytes // itemsize
92
+ current_size = 1
93
+ chunk_shape = []
94
+ for n_i in data_shape[-1::-1]:
95
+ if current_size >= max_size:
96
+ c_i = 1
97
+ else:
98
+ a = int(numpy.ceil(n_i / chunk_split))
99
+ b = int(numpy.ceil(max_size / current_size))
100
+ c_i = min(a, b)
101
+ chunk_shape.append(c_i)
102
+ current_size *= c_i
103
+ chunk_shape = tuple(chunk_shape[::-1])
104
+ if chunk_shape == data_shape:
105
+ return None
106
+ return chunk_shape
107
+
108
+
109
+ def guess_compression(
110
+ data_shape: ShapeType,
111
+ dtype: DTypeLike,
112
+ compression_limit_nbytes: Optional[Integral] = None,
113
+ ) -> bool:
114
+ """Compression is needed when the total data size exceeds the limits (1 MB by default)."""
115
+ if compression_limit_nbytes is None:
116
+ compression_limit_nbytes = DEFAULT_COMPRESSION_LIMIT_NBYTES
117
+ nbytes = shape_to_nbytes(data_shape, dtype)
118
+ return nbytes > compression_limit_nbytes
119
+
120
+
121
+ def get_compression_arguments(compression_scheme: Optional[str] = None) -> Mapping:
122
+ if compression_scheme:
123
+ compression_scheme = compression_scheme.lower()
124
+ if compression_scheme is None:
125
+ compression_scheme = DEFAULT_COMPRESSION_SCHEME
126
+ if compression_scheme == "none":
127
+ return dict()
128
+ elif compression_scheme == "gzip":
129
+ return {"compression": "gzip"}
130
+ elif compression_scheme == "byteshuffle":
131
+ return {"shuffle": True}
132
+ elif compression_scheme == "gzip-byteshuffle":
133
+ return {"compression": "gzip", "shuffle": True}
134
+ elif compression_scheme == "bitshuffle":
135
+ if hdf5plugin is None:
136
+ raise RuntimeError(
137
+ "Writer does not support HDF5 'bitshuffle' compression. Install the hdf5plugin library"
138
+ )
139
+ return hdf5plugin.Bitshuffle(nelems=0, lz4=False)
140
+ elif compression_scheme == "lz4-bitshuffle":
141
+ if hdf5plugin is None:
142
+ raise RuntimeError(
143
+ "Writer does not support HDF5 'bitshuffle' compression. Install the hdf5plugin library"
144
+ )
145
+ return hdf5plugin.Bitshuffle(nelems=0, lz4=True)
146
+ else:
147
+ raise ValueError(f"Unknown HDF5 compression '{compression_scheme}'")
148
+
149
+
150
+ def guess_dataset_config(
151
+ scan_shape: VarShapeType,
152
+ detector_shape: VarShapeType,
153
+ dtype: Optional[DTypeLike] = None,
154
+ chunk_split: Optional[Integral] = None,
155
+ chunk_nbytes: Optional[Integral] = None,
156
+ compression_limit_nbytes: Optional[Integral] = None,
157
+ compression_scheme: Optional[str] = None,
158
+ max_shape: Optional[VarH5pyShapeType] = None,
159
+ ) -> dict:
160
+ """Dataset configuration passed the `h5py.Group.create_dataset` for optimal
161
+ storage (IO speed, compression, ...)
162
+ """
163
+ data_shape = guess_data_shape(
164
+ scan_shape=scan_shape, detector_shape=detector_shape, max_shape=max_shape
165
+ )
166
+ if dtype is None:
167
+ dtype = DEFAULT_DTYPE
168
+ chunk_shape = guess_chunk_shape(
169
+ data_shape=data_shape,
170
+ dtype=dtype,
171
+ chunk_split=chunk_split,
172
+ chunk_nbytes=chunk_nbytes,
173
+ )
174
+ config = {"chunks": chunk_shape}
175
+ compression = guess_compression(
176
+ data_shape=data_shape,
177
+ dtype=dtype,
178
+ compression_limit_nbytes=compression_limit_nbytes,
179
+ )
180
+ if compression:
181
+ config.update(get_compression_arguments(compression_scheme=compression_scheme))
182
+ chunking_required = compression or max_shape is not None
183
+ if chunking_required and chunk_shape is None:
184
+ # Do not let h5py guess the chunk size
185
+ config["chunks"] = data_shape
186
+ return config
@@ -0,0 +1,4 @@
1
+ from ._non_sequential import IndexedDatasetWriter # noqa F401
2
+ from ._non_sequential_stack import IndexedStackDatasetWriter # noqa F401
3
+ from ._sequential import DatasetWriter # noqa F401
4
+ from ._sequential_stack import StackDatasetWriter # noqa F401
@@ -0,0 +1,55 @@
1
+ import time
2
+ from typing import Optional
3
+
4
+ import h5py
5
+ import numpy
6
+
7
+
8
+ class DatasetWriterBase:
9
+ def __init__(
10
+ self,
11
+ parent: h5py.Group,
12
+ name: str,
13
+ attrs: Optional[dict] = None,
14
+ flush_period: Optional[float] = None,
15
+ ) -> None:
16
+ self._file = parent.file
17
+ self._parent = parent
18
+ self._name = name
19
+ self._attrs = attrs
20
+ self._dataset_name = f"{parent.name}/{name}"
21
+ self._dataset = None
22
+ self._flush_period = flush_period
23
+ self._last_flush = None
24
+
25
+ @property
26
+ def dataset_name(self) -> str:
27
+ return self._dataset_name
28
+
29
+ def __enter__(self) -> "DatasetWriterBase":
30
+ return self
31
+
32
+ def __exit__(self, *args) -> None:
33
+ self.flush_buffer()
34
+
35
+ @property
36
+ def dataset(self) -> Optional[h5py.Dataset]:
37
+ return self._dataset
38
+
39
+ def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
40
+ raise NotImplementedError
41
+
42
+ def flush_buffer(self, align: bool = False) -> bool:
43
+ raise NotImplementedError
44
+
45
+ def _flush_time_expired(self) -> bool:
46
+ if self._flush_period is None:
47
+ return False
48
+ if self._last_flush is None:
49
+ self._last_flush = time.time()
50
+ return False
51
+ return (time.time() - self._last_flush) >= self._flush_period
52
+
53
+ def _flush_hdf5(self) -> None:
54
+ """Explicit HDF5 flush for non-locking readers."""
55
+ self._file.flush()
@@ -0,0 +1,139 @@
1
+ import logging
2
+ import time
3
+ from typing import List
4
+ from typing import Optional
5
+
6
+ import h5py
7
+ import numpy
8
+ from numpy.typing import ArrayLike
9
+
10
+ from ..config import guess_dataset_config
11
+ from ..types import StrictPositiveIntegral
12
+ from ._base import DatasetWriterBase
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IndexedDatasetWriter(DatasetWriterBase):
18
+ """Append arrays of the same shape to a new HDF5 dataset in a potentially non-sequential manner.
19
+
20
+ Instead of creating a dataset with the :code:`h5py` API
21
+
22
+ .. code-block::python
23
+
24
+ h5group["mydataset"] = [[1,2,3], [4,5,6], [7,8,9]]
25
+
26
+ it can be done like this
27
+
28
+ .. code-block::python
29
+
30
+ with IndexedDatasetWriter(h5group, "mydataset") as writer:
31
+ writer.add_point(1, [4,5,6])
32
+ writer.add_points([0, 2], [[1,2,3], [7,8,9]])
33
+
34
+ Chunk size determination, compression and flushing is handled.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ parent: h5py.Group,
40
+ name: str,
41
+ npoints: Optional[StrictPositiveIntegral] = None,
42
+ attrs: Optional[dict] = None,
43
+ flush_period: Optional[float] = None,
44
+ overwrite: bool = False,
45
+ ) -> None:
46
+ super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
47
+ self._npoints = npoints
48
+ self._overwrite = overwrite
49
+ self._npoints_added: int = 0
50
+
51
+ self._buffer: List[tuple[int, ArrayLike]] = []
52
+
53
+ def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
54
+ scan_shape = (self._npoints,)
55
+ detector_shape = first_data_point.shape
56
+ dtype = first_data_point.dtype
57
+
58
+ if self._npoints is None:
59
+ shape = (1,) + detector_shape
60
+ max_shape = scan_shape + detector_shape
61
+ else:
62
+ shape = scan_shape + detector_shape
63
+ max_shape = None
64
+
65
+ options = guess_dataset_config(
66
+ scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
67
+ )
68
+ options.update(
69
+ shape=shape,
70
+ dtype=dtype,
71
+ fillvalue=numpy.nan, # converts to 0 for integers
72
+ )
73
+ if max_shape:
74
+ options["maxshape"] = max_shape
75
+
76
+ if self._overwrite and self._name in self._parent:
77
+ del self._parent[self._name]
78
+
79
+ dset = self._parent.create_dataset(self._name, **options)
80
+ if self._attrs:
81
+ dset.attrs.update(self._attrs)
82
+ return dset
83
+
84
+ @property
85
+ def npoints_added(self) -> int:
86
+ return self._npoints_added
87
+
88
+ def add_point(
89
+ self,
90
+ data: ArrayLike,
91
+ index: int,
92
+ ) -> bool:
93
+ """Append one array to the dataset."""
94
+ if self._dataset is None:
95
+ self._dataset = self._create_dataset(numpy.asarray(data))
96
+
97
+ self._buffer.append((index, data))
98
+ self._npoints_added += 1
99
+ return self.flush_buffer()
100
+
101
+ def add_points(self, data: ArrayLike, indices: ArrayLike) -> bool:
102
+ """Append several arrays at once to the dataset."""
103
+ if self._dataset is None:
104
+ self._dataset = self._create_dataset(numpy.asarray(data[0]))
105
+
106
+ self._buffer.extend(zip(indices, data))
107
+ self._npoints_added += len(data)
108
+ return self.flush_buffer()
109
+
110
+ def flush_buffer(self, align: bool = False) -> bool:
111
+ if not self._buffer:
112
+ return False
113
+
114
+ if align:
115
+ logger.debug(
116
+ "Chunk-aligned flushing is only supported by DatasetWriter. "
117
+ "Only flush when `flush_period` has passed since the last flush."
118
+ )
119
+ if self._flush_time_expired():
120
+ return False
121
+
122
+ indices, values = zip(*self._buffer)
123
+ indices = numpy.asarray(indices)
124
+ values = numpy.asarray(values)
125
+
126
+ # Enlarge the dataset when needed
127
+ max_index = int(indices.max())
128
+ if max_index >= self._dataset.shape[0]:
129
+ self._dataset.resize(max_index + 1, axis=0)
130
+
131
+ # Copy data from buffer to HDF5
132
+ self._dataset[indices] = values
133
+
134
+ # Remove copied data from buffer
135
+ self._buffer.clear()
136
+
137
+ self._flush_hdf5()
138
+ self._last_flush = time.time()
139
+ return True
@@ -0,0 +1,158 @@
1
+ import logging
2
+ import time
3
+ from typing import List
4
+ from typing import Optional
5
+
6
+ import h5py
7
+ import numpy
8
+ from numpy.typing import ArrayLike
9
+
10
+ from ..config import guess_dataset_config
11
+ from ..types import StrictPositiveIntegral
12
+ from ._base import DatasetWriterBase
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IndexedStackDatasetWriter(DatasetWriterBase):
18
+ """Append arrays of the same shape to each item of a new HDF5 dataset
19
+ in a potentially non-sequential manner per item. So each item of the HDF5 dataset is a
20
+ stack to which we can append data in a sequential manner.
21
+
22
+ Instead of creating a dataset with the :code:`h5py` API
23
+
24
+ .. code-block::python
25
+
26
+ stack0 = [[1,2,3], [4,5,6], [7,8,9]]
27
+ stack1 = [[10,11,12], [13,14,15], [16,17,18]]
28
+ h5group["mydataset"] = [stack0, stack1]
29
+
30
+ it can be done like this
31
+
32
+ .. code-block::python
33
+
34
+ with IndexedStackDatasetWriter(h5group, "mydataset") as writer:
35
+ writer.add_point([4,5,6], 0, 1)
36
+ writer.add_point([13,14,15], 1, 1)
37
+ writer.add_points([[10,11,12], [16,17,18]], [1, 1], [0, 2])
38
+ writer.add_points([[1,2,3], [7,8,9]], [0, 0], [0, 2])
39
+
40
+ Chunk size determination, compression and flushing is handled.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ parent: h5py.Group,
46
+ name: str,
47
+ npoints: Optional[StrictPositiveIntegral] = None,
48
+ nstack: Optional[StrictPositiveIntegral] = None,
49
+ attrs: Optional[dict] = None,
50
+ flush_period: Optional[float] = None,
51
+ ) -> None:
52
+ super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
53
+ self._npoints = npoints
54
+ self._nstack = nstack
55
+ self._buffer: List[tuple[int, int, ArrayLike]] = []
56
+
57
+ def _create_dataset(
58
+ self, first_data_point: ArrayLike, stack_index: int
59
+ ) -> h5py.Dataset:
60
+ scan_shape = (self._nstack, self._npoints)
61
+ first_data_point = numpy.asarray(first_data_point)
62
+ detector_shape = first_data_point.shape
63
+ dtype = first_data_point.dtype
64
+
65
+ if self._nstack is None or self._npoints is None:
66
+ shape = (1, 1) + detector_shape
67
+ max_shape = scan_shape + detector_shape
68
+ else:
69
+ shape = scan_shape + detector_shape
70
+ max_shape = None
71
+
72
+ options = guess_dataset_config(
73
+ scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
74
+ )
75
+ options.update(
76
+ shape=shape,
77
+ dtype=dtype,
78
+ fillvalue=numpy.nan, # converts to 0 for integers
79
+ )
80
+ if max_shape:
81
+ options["maxshape"] = max_shape
82
+
83
+ dset = self._parent.create_dataset(self._name, **options)
84
+ if self._attrs:
85
+ dset.attrs.update(self._attrs)
86
+ return dset
87
+
88
+ def add_point(self, data: ArrayLike, stack_index: int, point_index: int) -> bool:
89
+ """Append one array to one stack of the dataset."""
90
+ if self._dataset is None:
91
+ self._dataset = self._create_dataset(data, stack_index)
92
+
93
+ self._buffer.append((stack_index, point_index, data))
94
+ return self.flush_buffer()
95
+
96
+ def add_points(
97
+ self,
98
+ data: ArrayLike,
99
+ stack_indices: ArrayLike,
100
+ point_indices: ArrayLike,
101
+ ) -> bool:
102
+ """Append several arrays at once to one stack of the dataset."""
103
+ if not (len(stack_indices) == len(point_indices) == len(data)):
104
+ raise ValueError(
105
+ "stack_indices, point_indices, and data must have same length"
106
+ )
107
+
108
+ if self._dataset is None:
109
+ self._dataset = self._create_dataset(data[0], stack_indices[0])
110
+
111
+ self._buffer.extend(
112
+ zip(
113
+ map(int, stack_indices),
114
+ map(int, point_indices),
115
+ data,
116
+ )
117
+ )
118
+ return self.flush_buffer()
119
+
120
+ def flush_buffer(self, align: bool = False) -> bool:
121
+ if not self._buffer:
122
+ return False
123
+
124
+ if align:
125
+ logger.debug(
126
+ "Chunk-aligned flushing is only supported by StackDatasetWriter. "
127
+ "Only flush when `flush_period` has passed since the last flush."
128
+ )
129
+ if self._flush_time_expired():
130
+ return False
131
+
132
+ stack_indices, point_indices, values = zip(*self._buffer)
133
+ stack_indices = numpy.asarray(stack_indices)
134
+ point_indices = numpy.asarray(point_indices)
135
+ values = numpy.asarray(values)
136
+
137
+ # Enlarge the dataset when needed
138
+ max_stack = int(stack_indices.max())
139
+ max_point = int(point_indices.max())
140
+
141
+ shape0, shape1 = self._dataset.shape[:2]
142
+
143
+ if max_stack >= shape0:
144
+ self._dataset.resize(max_stack + 1, axis=0)
145
+
146
+ if max_point >= shape1:
147
+ self._dataset.resize(max_point + 1, axis=1)
148
+
149
+ # Copy data from buffer to HDF5
150
+ for i0, i1, value in zip(stack_indices, point_indices, values):
151
+ self._dataset[i0, i1] = value
152
+
153
+ # Remove copied data from buffer
154
+ self._buffer.clear()
155
+
156
+ self._flush_hdf5()
157
+ self._last_flush = time.time()
158
+ return True
@@ -0,0 +1,143 @@
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 DatasetWriter(DatasetWriterBase):
15
+ """Append arrays of the same shape to a new HDF5 dataset in a sequential manner.
16
+
17
+ Instead of creating a dataset with the :code:`h5py` API
18
+
19
+ .. code-block::python
20
+
21
+ h5group["mydataset"] = [[1,2,3], [4,5,6], [7,8,9]]
22
+
23
+ it can be done like this
24
+
25
+ .. code-block::python
26
+
27
+ with DatasetWriter(h5group, "mydataset") as writer:
28
+ writer.add_point([1,2,3])
29
+ writer.add_points([[4,5,6], [7,8,9]])
30
+
31
+ Chunk size determination, chunk-aligned writing, compression and flushing is handled.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ parent: h5py.Group,
37
+ name: str,
38
+ npoints: Optional[StrictPositiveIntegral] = None,
39
+ attrs: Optional[dict] = None,
40
+ flush_period: Optional[float] = None,
41
+ overwrite: bool = False,
42
+ ) -> None:
43
+ super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
44
+ self._npoints = npoints
45
+ self._overwrite = overwrite
46
+ self._chunked: bool = False
47
+ self._npoints_added: int = 0
48
+
49
+ self._buffer: List[ArrayLike] = list()
50
+ self._chunk_size: int = 0
51
+ self._flushed_size: int = 0
52
+
53
+ def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
54
+ scan_shape = (self._npoints,)
55
+ detector_shape = first_data_point.shape
56
+ dtype = first_data_point.dtype
57
+
58
+ if self._npoints is None:
59
+ shape = (1,) + detector_shape
60
+ max_shape = scan_shape + detector_shape
61
+ else:
62
+ shape = scan_shape + detector_shape
63
+ max_shape = None
64
+
65
+ options = guess_dataset_config(
66
+ scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
67
+ )
68
+ options.update(
69
+ shape=shape,
70
+ dtype=dtype,
71
+ fillvalue=numpy.nan, # converts to 0 for integers
72
+ )
73
+
74
+ if max_shape:
75
+ options["maxshape"] = max_shape
76
+
77
+ if options["chunks"]:
78
+ self._chunked = True
79
+ self._chunk_size = options["chunks"][0]
80
+
81
+ if self._overwrite and self._name in self._parent:
82
+ del self._parent[self._name]
83
+
84
+ dset = self._parent.create_dataset(self._name, **options)
85
+ if self._attrs:
86
+ dset.attrs.update(self._attrs)
87
+ return dset
88
+
89
+ @property
90
+ def npoints_added(self) -> int:
91
+ return self._npoints_added
92
+
93
+ def add_point(self, data: ArrayLike) -> bool:
94
+ """Append one array to the dataset."""
95
+ if self._dataset is None:
96
+ self._dataset = self._create_dataset(data)
97
+
98
+ self._buffer.append(data)
99
+ self._npoints_added += 1
100
+ return self.flush_buffer(align=True)
101
+
102
+ def add_points(self, data: ArrayLike) -> bool:
103
+ """Append several arrays at once to the dataset."""
104
+ if self._dataset is None:
105
+ self._dataset = self._create_dataset(data[0])
106
+
107
+ self._buffer.extend(data)
108
+ self._npoints_added += len(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
113
+ chunk_size = len(self._buffer)
114
+
115
+ if self._flush_time_expired():
116
+ flush_size = chunk_size
117
+ elif align and self._chunked:
118
+ n = chunk_size + (self._flushed_size % self._chunk_size)
119
+ flush_size = n // self._chunk_size * self._chunk_size
120
+ flush_size = min(flush_size, chunk_size)
121
+ else:
122
+ flush_size = chunk_size
123
+
124
+ if flush_size == 0:
125
+ return False
126
+
127
+ # Enlarge the dataset when needed
128
+ nalloc = self._dataset.shape[0]
129
+ istart = self._flushed_size
130
+ flushed_size = istart + flush_size
131
+ if self._chunked and flushed_size > nalloc:
132
+ self._dataset.resize(flushed_size, axis=0)
133
+
134
+ # Copy data from buffer to HDF5
135
+ self._dataset[istart : istart + flush_size] = self._buffer[:flush_size]
136
+
137
+ # Remove copied data from buffer
138
+ self._buffer = self._buffer[flush_size:]
139
+ self._flushed_size = flushed_size
140
+
141
+ self._flush_hdf5()
142
+ self._last_flush = time.time()
143
+ return True