ewoksdata 2.2.0__tar.gz → 2.3.0rc1__tar.gz
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-2.2.0/src/ewoksdata.egg-info → ewoksdata-2.3.0rc1}/PKG-INFO +1 -1
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/pyproject.toml +1 -1
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/blissdata/blissdatav2.py +31 -18
- ewoksdata-2.3.0rc1/src/ewoksdata/data/hdf5/dataset_writer/_base.py +112 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +74 -25
- ewoksdata-2.3.0rc1/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +238 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +7 -12
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +14 -19
- ewoksdata-2.3.0rc1/src/ewoksdata/data/hdf5/mirror.py +626 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/types.py +5 -3
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/test_data_bliss.py +2 -2
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/test_dataset_writer.py +202 -8
- ewoksdata-2.3.0rc1/src/ewoksdata/tests/test_mirror_hdf5.py +980 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1/src/ewoksdata.egg-info}/PKG-INFO +1 -1
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata.egg-info/SOURCES.txt +2 -0
- ewoksdata-2.2.0/src/ewoksdata/data/hdf5/dataset_writer/_base.py +0 -55
- ewoksdata-2.2.0/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +0 -158
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/LICENSE.md +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/README.md +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/setup.cfg +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/__init__.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/__init__.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/bliss.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/blissdata/__init__.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/blissdata/blissdatav0.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/blissdata/blissdatav1.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/contextiterator.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/__init__.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/config.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/nexus.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/url.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/utils.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/__init__.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/conftest.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/data/__init__.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/data/bliss_scans.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/test_contextiterator.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/test_data_hdf5.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/tests/test_data_nexus.py +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata.egg-info/requires.txt +0 -0
- {ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ewoksdata"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0rc1"
|
|
8
8
|
authors = [{ name = "ESRF", email = "dau-pydev@esrf.fr" }]
|
|
9
9
|
description = "Ewoks tasks for data access"
|
|
10
10
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
@@ -26,12 +26,14 @@ if _BLISSDATA_VERSION >= Version("2.3.0"):
|
|
|
26
26
|
from blissdata.exceptions import IndexNoMoreThereError
|
|
27
27
|
from blissdata.exceptions import IndexNotYetThereError
|
|
28
28
|
from blissdata.exceptions import IndexWontBeThereError
|
|
29
|
+
from blissdata.streams import BaseStream
|
|
29
30
|
from blissdata.streams import CursorGroup
|
|
30
31
|
else:
|
|
31
32
|
from blissdata.redis_engine.exceptions import EndOfStream
|
|
32
33
|
from blissdata.redis_engine.exceptions import IndexNoMoreThereError
|
|
33
34
|
from blissdata.redis_engine.exceptions import IndexNotYetThereError
|
|
34
35
|
from blissdata.redis_engine.exceptions import IndexWontBeThereError
|
|
36
|
+
from blissdata.streams.base import BaseStream
|
|
35
37
|
from blissdata.streams.base import CursorGroup
|
|
36
38
|
|
|
37
39
|
logger = logging.getLogger(__name__)
|
|
@@ -178,10 +180,8 @@ def last_lima_image(scan_key: str, lima_name: str) -> ArrayLike:
|
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
def _get_streams(
|
|
181
|
-
scan_key: str,
|
|
182
|
-
|
|
183
|
-
counter_names: List[str],
|
|
184
|
-
):
|
|
183
|
+
scan_key: str, lima_names: List[str], counter_names: List[str]
|
|
184
|
+
) -> Dict[str, BaseStream]:
|
|
185
185
|
data_store = _get_data_store()
|
|
186
186
|
scan = data_store.load_scan(scan_key)
|
|
187
187
|
|
|
@@ -191,20 +191,17 @@ def _get_streams(
|
|
|
191
191
|
streams = dict()
|
|
192
192
|
|
|
193
193
|
for name, stream in scan.streams.items():
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
elif name.split(":")[-1] in counter_names:
|
|
207
|
-
streams[name.split(":")[-1]] = stream
|
|
194
|
+
name_parts = name.split(":")
|
|
195
|
+
if _is_lima_image_stream(stream, name_parts):
|
|
196
|
+
input_name = name_parts[0]
|
|
197
|
+
if input_name not in lima_names:
|
|
198
|
+
continue
|
|
199
|
+
elif name_parts[-1] in counter_names:
|
|
200
|
+
input_name = name_parts[-1]
|
|
201
|
+
else:
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
streams[input_name] = stream
|
|
208
205
|
|
|
209
206
|
nnames = len(lima_names) + len(counter_names)
|
|
210
207
|
nstreams = len(streams)
|
|
@@ -215,6 +212,22 @@ def _get_streams(
|
|
|
215
212
|
return streams
|
|
216
213
|
|
|
217
214
|
|
|
215
|
+
def _is_lima_image_stream(stream: BaseStream, name_parts: Tuple[str]) -> bool:
|
|
216
|
+
if "plugin" in stream.info:
|
|
217
|
+
if stream.info["plugin"] == "lima2":
|
|
218
|
+
return (
|
|
219
|
+
name_parts[-1] == "frame" and stream.info.get("master_file") is not None
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
return stream.info["plugin"] == "lima"
|
|
223
|
+
|
|
224
|
+
if stream.event_stream.encoding["type"] == "json":
|
|
225
|
+
# lima_v1, lima_v2
|
|
226
|
+
return "lima" in stream.info.get("format", "")
|
|
227
|
+
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
|
|
218
231
|
def iter_bliss_scan_data_from_memory_slice(
|
|
219
232
|
scan_key: str,
|
|
220
233
|
lima_names: List[str],
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
import h5py
|
|
8
|
+
import numpy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DatasetWriterBase:
|
|
12
|
+
"""Write an HDF5 dataset incrementally with support for
|
|
13
|
+
|
|
14
|
+
- chunk size determination
|
|
15
|
+
- compression algorithm selection
|
|
16
|
+
- chunk-aligned flushing of the buffer to libhdf5 memory
|
|
17
|
+
- opt-out explicit flushing of libhdf5 memory to disk
|
|
18
|
+
|
|
19
|
+
Flushing happens in two stages, both controlled by the `flush_period`
|
|
20
|
+
argument:
|
|
21
|
+
|
|
22
|
+
- Buffer -> libhdf5: adding data writes the buffered data that covers
|
|
23
|
+
entire chunks to HDF5. The remaining buffered data is written on an
|
|
24
|
+
explicit `flush_buffer()` call, when the writer exits and depending
|
|
25
|
+
on `flush_period`:
|
|
26
|
+
|
|
27
|
+
- `None`: never when adding data.
|
|
28
|
+
- `0`: every time data is added.
|
|
29
|
+
- `> 0`: when data is added after the period expired.
|
|
30
|
+
|
|
31
|
+
Datasets without chunking never buffer.
|
|
32
|
+
|
|
33
|
+
- libhdf5 -> disk: an explicit HDF5 flush makes updates visible to
|
|
34
|
+
non-locking readers. It happens when the writer exits and depending
|
|
35
|
+
on `flush_period`:
|
|
36
|
+
|
|
37
|
+
- `None` (default): never before the writer exits.
|
|
38
|
+
- `0`: every time data is written to HDF5.
|
|
39
|
+
- `> 0`: when data is written to HDF5 after the period expired.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
parent: h5py.Group,
|
|
45
|
+
name: str,
|
|
46
|
+
attrs: Optional[dict] = None,
|
|
47
|
+
flush_period: Optional[float] = 1.0,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._file = parent.file
|
|
50
|
+
self._parent = parent
|
|
51
|
+
self._name = name
|
|
52
|
+
self._attrs = attrs
|
|
53
|
+
self._dataset_name = f"{parent.name}/{name}"
|
|
54
|
+
self._dataset = None
|
|
55
|
+
self._flush_period = flush_period
|
|
56
|
+
self._last_flush = None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def dataset_name(self) -> str:
|
|
60
|
+
return self._dataset_name
|
|
61
|
+
|
|
62
|
+
def __enter__(self) -> "DatasetWriterBase":
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, *args) -> None:
|
|
66
|
+
self.flush_buffer()
|
|
67
|
+
self._flush_hdf5() # in case throttled
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def dataset(self) -> Optional[h5py.Dataset]:
|
|
71
|
+
return self._dataset
|
|
72
|
+
|
|
73
|
+
def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
def flush_buffer(self, align: bool = False) -> bool:
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
def _flush_time_expired(self) -> bool:
|
|
80
|
+
"""Never expires when no flush period is defined."""
|
|
81
|
+
if self._flush_period is None:
|
|
82
|
+
return False
|
|
83
|
+
if self._last_flush is None:
|
|
84
|
+
self._last_flush = time.time()
|
|
85
|
+
return False
|
|
86
|
+
return (time.time() - self._last_flush) >= self._flush_period
|
|
87
|
+
|
|
88
|
+
def _flush_hdf5(self) -> None:
|
|
89
|
+
"""Explicit HDF5 flush so non-locking readers to see updates on retry."""
|
|
90
|
+
# NOTE: flushing a file with compressed datasets is expensive
|
|
91
|
+
self._file.flush()
|
|
92
|
+
self._last_flush = time.time()
|
|
93
|
+
|
|
94
|
+
def _flush_hdf5_throttled(self) -> bool:
|
|
95
|
+
"""HDF5 flush when the flush period expired."""
|
|
96
|
+
if self._flush_time_expired():
|
|
97
|
+
self._flush_hdf5()
|
|
98
|
+
return True
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def iter_contiguous_slices(
|
|
103
|
+
indices: Sequence[int],
|
|
104
|
+
) -> Iterator[Tuple[numpy.ndarray, slice]]:
|
|
105
|
+
"""Split indices in runs of consecutive values."""
|
|
106
|
+
indices = numpy.asarray(indices)
|
|
107
|
+
order = numpy.argsort(indices, kind="stable")
|
|
108
|
+
sorted_indices = indices[order]
|
|
109
|
+
breaks = numpy.nonzero(numpy.diff(sorted_indices) != 1)[0] + 1
|
|
110
|
+
for positions in numpy.split(order, breaks):
|
|
111
|
+
start = int(indices[positions[0]])
|
|
112
|
+
yield positions, slice(start, start + len(positions))
|
{ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import
|
|
2
|
+
from typing import Dict
|
|
3
3
|
from typing import List
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
@@ -10,6 +10,7 @@ from numpy.typing import ArrayLike
|
|
|
10
10
|
from ..config import guess_dataset_config
|
|
11
11
|
from ..types import StrictPositiveIntegral
|
|
12
12
|
from ._base import DatasetWriterBase
|
|
13
|
+
from ._base import iter_contiguous_slices
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -30,8 +31,6 @@ class IndexedDatasetWriter(DatasetWriterBase):
|
|
|
30
31
|
with IndexedDatasetWriter(h5group, "mydataset") as writer:
|
|
31
32
|
writer.add_point(1, [4,5,6])
|
|
32
33
|
writer.add_points([0, 2], [[1,2,3], [7,8,9]])
|
|
33
|
-
|
|
34
|
-
Chunk size determination, compression and flushing is handled.
|
|
35
34
|
"""
|
|
36
35
|
|
|
37
36
|
def __init__(
|
|
@@ -40,15 +39,19 @@ class IndexedDatasetWriter(DatasetWriterBase):
|
|
|
40
39
|
name: str,
|
|
41
40
|
npoints: Optional[StrictPositiveIntegral] = None,
|
|
42
41
|
attrs: Optional[dict] = None,
|
|
43
|
-
flush_period: Optional[float] =
|
|
42
|
+
flush_period: Optional[float] = 1.0,
|
|
44
43
|
overwrite: bool = False,
|
|
45
44
|
) -> None:
|
|
46
45
|
super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
|
|
47
46
|
self._npoints = npoints
|
|
48
47
|
self._overwrite = overwrite
|
|
48
|
+
self._chunked: bool = False
|
|
49
|
+
self._chunk_size: int = 0
|
|
49
50
|
self._npoints_added: int = 0
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
# Points are buffered by index
|
|
53
|
+
self._buffer: Dict[int, ArrayLike] = dict()
|
|
54
|
+
self._nbuffered_per_chunk: Dict[int, int] = dict()
|
|
52
55
|
|
|
53
56
|
def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
|
|
54
57
|
scan_shape = (self._npoints,)
|
|
@@ -73,6 +76,10 @@ class IndexedDatasetWriter(DatasetWriterBase):
|
|
|
73
76
|
if max_shape:
|
|
74
77
|
options["maxshape"] = max_shape
|
|
75
78
|
|
|
79
|
+
if options["chunks"]:
|
|
80
|
+
self._chunked = True
|
|
81
|
+
self._chunk_size = options["chunks"][0]
|
|
82
|
+
|
|
76
83
|
if self._overwrite and self._name in self._parent:
|
|
77
84
|
del self._parent[self._name]
|
|
78
85
|
|
|
@@ -94,46 +101,88 @@ class IndexedDatasetWriter(DatasetWriterBase):
|
|
|
94
101
|
if self._dataset is None:
|
|
95
102
|
self._dataset = self._create_dataset(numpy.asarray(data))
|
|
96
103
|
|
|
97
|
-
self.
|
|
104
|
+
self._buffer_point(index, data)
|
|
98
105
|
self._npoints_added += 1
|
|
99
|
-
return self.flush_buffer()
|
|
106
|
+
return self.flush_buffer(align=True)
|
|
100
107
|
|
|
101
108
|
def add_points(self, data: ArrayLike, indices: ArrayLike) -> bool:
|
|
102
109
|
"""Append several arrays at once to the dataset."""
|
|
103
110
|
if self._dataset is None:
|
|
104
111
|
self._dataset = self._create_dataset(numpy.asarray(data[0]))
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
for index, value in zip(indices, data):
|
|
114
|
+
self._buffer_point(index, value)
|
|
107
115
|
self._npoints_added += len(data)
|
|
108
|
-
return self.flush_buffer()
|
|
116
|
+
return self.flush_buffer(align=True)
|
|
117
|
+
|
|
118
|
+
def _buffer_point(self, index: int, data: ArrayLike) -> None:
|
|
119
|
+
index = int(index)
|
|
120
|
+
if self._chunked and index not in self._buffer:
|
|
121
|
+
chunk_index = index // self._chunk_size
|
|
122
|
+
nbuffered = self._nbuffered_per_chunk.get(chunk_index, 0)
|
|
123
|
+
self._nbuffered_per_chunk[chunk_index] = nbuffered + 1
|
|
124
|
+
self._buffer[index] = data
|
|
109
125
|
|
|
110
126
|
def flush_buffer(self, align: bool = False) -> bool:
|
|
127
|
+
# Determine which buffered points to flush
|
|
111
128
|
if not self._buffer:
|
|
112
129
|
return False
|
|
113
130
|
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return False
|
|
131
|
+
if self._flush_time_expired():
|
|
132
|
+
flush_indices = list(self._buffer)
|
|
133
|
+
elif align and self._chunked:
|
|
134
|
+
flush_indices = self._complete_chunk_indices()
|
|
135
|
+
else:
|
|
136
|
+
flush_indices = list(self._buffer)
|
|
121
137
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
values = numpy.asarray(values)
|
|
138
|
+
if not flush_indices:
|
|
139
|
+
return False
|
|
125
140
|
|
|
126
141
|
# Enlarge the dataset when needed
|
|
127
|
-
max_index =
|
|
142
|
+
max_index = max(flush_indices)
|
|
128
143
|
if max_index >= self._dataset.shape[0]:
|
|
129
144
|
self._dataset.resize(max_index + 1, axis=0)
|
|
130
145
|
|
|
131
146
|
# Copy data from buffer to HDF5
|
|
132
|
-
self.
|
|
147
|
+
values = numpy.asarray([self._buffer[index] for index in flush_indices])
|
|
148
|
+
for source, destination in iter_contiguous_slices(flush_indices):
|
|
149
|
+
self._dataset[destination] = values[source]
|
|
133
150
|
|
|
134
151
|
# Remove copied data from buffer
|
|
135
|
-
self._buffer
|
|
152
|
+
if len(flush_indices) == len(self._buffer):
|
|
153
|
+
self._buffer.clear()
|
|
154
|
+
self._nbuffered_per_chunk.clear()
|
|
155
|
+
else:
|
|
156
|
+
for index in flush_indices:
|
|
157
|
+
del self._buffer[index]
|
|
158
|
+
chunk_index = index // self._chunk_size
|
|
159
|
+
nbuffered = self._nbuffered_per_chunk[chunk_index] - 1
|
|
160
|
+
if nbuffered:
|
|
161
|
+
self._nbuffered_per_chunk[chunk_index] = nbuffered
|
|
162
|
+
else:
|
|
163
|
+
del self._nbuffered_per_chunk[chunk_index]
|
|
164
|
+
|
|
165
|
+
return self._flush_hdf5_throttled()
|
|
166
|
+
|
|
167
|
+
def _complete_chunk_indices(self) -> List[int]:
|
|
168
|
+
"""Indices of the buffered points that cover entire chunks."""
|
|
169
|
+
chunk_indices = sorted(
|
|
170
|
+
chunk_index
|
|
171
|
+
for chunk_index, nbuffered in self._nbuffered_per_chunk.items()
|
|
172
|
+
if nbuffered == self._chunk_npoints(chunk_index)
|
|
173
|
+
)
|
|
174
|
+
return [
|
|
175
|
+
index
|
|
176
|
+
for chunk_index in chunk_indices
|
|
177
|
+
for index in range(
|
|
178
|
+
chunk_index * self._chunk_size,
|
|
179
|
+
chunk_index * self._chunk_size + self._chunk_npoints(chunk_index),
|
|
180
|
+
)
|
|
181
|
+
]
|
|
136
182
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
def _chunk_npoints(self, chunk_index: int) -> int:
|
|
184
|
+
"""Number of points in a chunk. The last chunk can be smaller when
|
|
185
|
+
the number of points is fixed."""
|
|
186
|
+
if self._npoints is None:
|
|
187
|
+
return self._chunk_size
|
|
188
|
+
return min(self._chunk_size, self._npoints - chunk_index * self._chunk_size)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from typing import Tuple
|
|
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
|
+
from ._base import iter_contiguous_slices
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IndexedStackDatasetWriter(DatasetWriterBase):
|
|
19
|
+
"""Append arrays of the same shape to each item of a new HDF5 dataset
|
|
20
|
+
in a potentially non-sequential manner per item. So each item of the HDF5 dataset is a
|
|
21
|
+
stack to which we can append data in a sequential manner.
|
|
22
|
+
|
|
23
|
+
Instead of creating a dataset with the :code:`h5py` API
|
|
24
|
+
|
|
25
|
+
.. code-block::python
|
|
26
|
+
|
|
27
|
+
stack0 = [[1,2,3], [4,5,6], [7,8,9]]
|
|
28
|
+
stack1 = [[10,11,12], [13,14,15], [16,17,18]]
|
|
29
|
+
h5group["mydataset"] = [stack0, stack1]
|
|
30
|
+
|
|
31
|
+
it can be done like this
|
|
32
|
+
|
|
33
|
+
.. code-block::python
|
|
34
|
+
|
|
35
|
+
with IndexedStackDatasetWriter(h5group, "mydataset") as writer:
|
|
36
|
+
writer.add_point([4,5,6], 0, 1)
|
|
37
|
+
writer.add_point([13,14,15], 1, 1)
|
|
38
|
+
writer.add_points([[10,11,12], [16,17,18]], [1, 1], [0, 2])
|
|
39
|
+
writer.add_points([[1,2,3], [7,8,9]], [0, 0], [0, 2])
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
parent: h5py.Group,
|
|
45
|
+
name: str,
|
|
46
|
+
npoints: Optional[StrictPositiveIntegral] = None,
|
|
47
|
+
nstack: Optional[StrictPositiveIntegral] = None,
|
|
48
|
+
attrs: Optional[dict] = None,
|
|
49
|
+
flush_period: Optional[float] = 1.0,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
|
|
52
|
+
self._npoints = npoints
|
|
53
|
+
self._nstack = nstack
|
|
54
|
+
self._chunked: bool = False
|
|
55
|
+
self._chunk_shape: Tuple[int, int] = (0, 0)
|
|
56
|
+
|
|
57
|
+
# Points are buffered by (stack index, point index).
|
|
58
|
+
self._buffer: Dict[Tuple[int, int], ArrayLike] = dict()
|
|
59
|
+
self._nbuffered_per_chunk: Dict[Tuple[int, int], int] = dict()
|
|
60
|
+
|
|
61
|
+
def _create_dataset(
|
|
62
|
+
self, first_data_point: ArrayLike, stack_index: int
|
|
63
|
+
) -> h5py.Dataset:
|
|
64
|
+
scan_shape = (self._nstack, self._npoints)
|
|
65
|
+
first_data_point = numpy.asarray(first_data_point)
|
|
66
|
+
detector_shape = first_data_point.shape
|
|
67
|
+
dtype = first_data_point.dtype
|
|
68
|
+
|
|
69
|
+
if self._nstack is None or self._npoints is None:
|
|
70
|
+
shape = (1, 1) + detector_shape
|
|
71
|
+
max_shape = scan_shape + detector_shape
|
|
72
|
+
else:
|
|
73
|
+
shape = scan_shape + detector_shape
|
|
74
|
+
max_shape = None
|
|
75
|
+
|
|
76
|
+
options = guess_dataset_config(
|
|
77
|
+
scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
|
|
78
|
+
)
|
|
79
|
+
options.update(
|
|
80
|
+
shape=shape,
|
|
81
|
+
dtype=dtype,
|
|
82
|
+
fillvalue=numpy.nan, # converts to 0 for integers
|
|
83
|
+
)
|
|
84
|
+
if max_shape:
|
|
85
|
+
options["maxshape"] = max_shape
|
|
86
|
+
|
|
87
|
+
if options["chunks"]:
|
|
88
|
+
self._chunked = True
|
|
89
|
+
self._chunk_shape = tuple(options["chunks"][:2])
|
|
90
|
+
|
|
91
|
+
dset = self._parent.create_dataset(self._name, **options)
|
|
92
|
+
if self._attrs:
|
|
93
|
+
dset.attrs.update(self._attrs)
|
|
94
|
+
return dset
|
|
95
|
+
|
|
96
|
+
def add_point(self, data: ArrayLike, stack_index: int, point_index: int) -> bool:
|
|
97
|
+
"""Append one array to one stack of the dataset."""
|
|
98
|
+
if self._dataset is None:
|
|
99
|
+
self._dataset = self._create_dataset(data, stack_index)
|
|
100
|
+
|
|
101
|
+
self._buffer_point(stack_index, point_index, data)
|
|
102
|
+
return self.flush_buffer(align=True)
|
|
103
|
+
|
|
104
|
+
def add_points(
|
|
105
|
+
self,
|
|
106
|
+
data: ArrayLike,
|
|
107
|
+
stack_indices: ArrayLike,
|
|
108
|
+
point_indices: ArrayLike,
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""Append several arrays at once to one stack of the dataset."""
|
|
111
|
+
if not (len(stack_indices) == len(point_indices) == len(data)):
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"stack_indices, point_indices, and data must have same length"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if self._dataset is None:
|
|
117
|
+
self._dataset = self._create_dataset(data[0], stack_indices[0])
|
|
118
|
+
|
|
119
|
+
for stack_index, point_index, value in zip(stack_indices, point_indices, data):
|
|
120
|
+
self._buffer_point(stack_index, point_index, value)
|
|
121
|
+
return self.flush_buffer(align=True)
|
|
122
|
+
|
|
123
|
+
def _buffer_point(
|
|
124
|
+
self, stack_index: int, point_index: int, data: ArrayLike
|
|
125
|
+
) -> None:
|
|
126
|
+
position = int(stack_index), int(point_index)
|
|
127
|
+
if self._chunked and position not in self._buffer:
|
|
128
|
+
chunk_index = (
|
|
129
|
+
position[0] // self._chunk_shape[0],
|
|
130
|
+
position[1] // self._chunk_shape[1],
|
|
131
|
+
)
|
|
132
|
+
self._nbuffered_per_chunk[chunk_index] = (
|
|
133
|
+
self._nbuffered_per_chunk.get(chunk_index, 0) + 1
|
|
134
|
+
)
|
|
135
|
+
self._buffer[position] = data
|
|
136
|
+
|
|
137
|
+
def flush_buffer(self, align: bool = False) -> bool:
|
|
138
|
+
if not self._buffer:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
if self._flush_time_expired():
|
|
142
|
+
flushed = self._flush_all()
|
|
143
|
+
elif align and self._chunked:
|
|
144
|
+
flushed = self._flush_complete_chunks()
|
|
145
|
+
else:
|
|
146
|
+
flushed = self._flush_all()
|
|
147
|
+
|
|
148
|
+
if not flushed:
|
|
149
|
+
return False
|
|
150
|
+
return self._flush_hdf5_throttled()
|
|
151
|
+
|
|
152
|
+
def _flush_all(self) -> bool:
|
|
153
|
+
"""Write all buffered points to HDF5."""
|
|
154
|
+
positions = list(self._buffer)
|
|
155
|
+
stack_indices = numpy.array([stack_index for stack_index, _ in positions])
|
|
156
|
+
point_indices = numpy.array([point_index for _, point_index in positions])
|
|
157
|
+
values = numpy.asarray(list(self._buffer.values()))
|
|
158
|
+
|
|
159
|
+
self._enlarge_dataset(int(stack_indices.max()), int(point_indices.max()))
|
|
160
|
+
|
|
161
|
+
for stack in numpy.unique(stack_indices):
|
|
162
|
+
mask = stack_indices == stack
|
|
163
|
+
stack_values = values[mask]
|
|
164
|
+
for source, destination in iter_contiguous_slices(point_indices[mask]):
|
|
165
|
+
self._dataset[int(stack), destination] = stack_values[source]
|
|
166
|
+
|
|
167
|
+
self._buffer.clear()
|
|
168
|
+
self._nbuffered_per_chunk.clear()
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
def _flush_complete_chunks(self) -> bool:
|
|
172
|
+
"""Write the buffered points that cover entire chunks to HDF5."""
|
|
173
|
+
chunk_indices = sorted(
|
|
174
|
+
chunk_index
|
|
175
|
+
for chunk_index, nbuffered in self._nbuffered_per_chunk.items()
|
|
176
|
+
if nbuffered == self._chunk_npoints(chunk_index)
|
|
177
|
+
)
|
|
178
|
+
if not chunk_indices:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
chunk_extents = [
|
|
182
|
+
(
|
|
183
|
+
chunk_index,
|
|
184
|
+
self._chunk_slice(0, chunk_index),
|
|
185
|
+
self._chunk_slice(1, chunk_index),
|
|
186
|
+
)
|
|
187
|
+
for chunk_index in chunk_indices
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
self._enlarge_dataset(
|
|
191
|
+
max(stacks.stop for _, stacks, _ in chunk_extents) - 1,
|
|
192
|
+
max(points.stop for _, _, points in chunk_extents) - 1,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for chunk_index, stacks, points in chunk_extents:
|
|
196
|
+
block = numpy.asarray(
|
|
197
|
+
[
|
|
198
|
+
[
|
|
199
|
+
self._buffer[(stack_index, point_index)]
|
|
200
|
+
for point_index in range(points.start, points.stop)
|
|
201
|
+
]
|
|
202
|
+
for stack_index in range(stacks.start, stacks.stop)
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
self._dataset[stacks, points] = block
|
|
206
|
+
|
|
207
|
+
for stack_index in range(stacks.start, stacks.stop):
|
|
208
|
+
for point_index in range(points.start, points.stop):
|
|
209
|
+
del self._buffer[(stack_index, point_index)]
|
|
210
|
+
del self._nbuffered_per_chunk[chunk_index]
|
|
211
|
+
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def _enlarge_dataset(self, max_stack: int, max_point: int) -> None:
|
|
215
|
+
"""Enlarge the dataset when needed."""
|
|
216
|
+
shape0, shape1 = self._dataset.shape[:2]
|
|
217
|
+
if max_stack >= shape0:
|
|
218
|
+
self._dataset.resize(max_stack + 1, axis=0)
|
|
219
|
+
if max_point >= shape1:
|
|
220
|
+
self._dataset.resize(max_point + 1, axis=1)
|
|
221
|
+
|
|
222
|
+
def _chunk_slice(self, axis: int, chunk_index: Tuple[int, int]) -> slice:
|
|
223
|
+
"""Dataset range covered by a chunk along the stack axis (0) or
|
|
224
|
+
point axis (1). Edge chunks can be smaller when the number of
|
|
225
|
+
stacks or points is fixed."""
|
|
226
|
+
nmax = self._nstack if axis == 0 else self._npoints
|
|
227
|
+
start = chunk_index[axis] * self._chunk_shape[axis]
|
|
228
|
+
stop = start + self._chunk_shape[axis]
|
|
229
|
+
if nmax is not None:
|
|
230
|
+
stop = min(stop, nmax)
|
|
231
|
+
return slice(start, stop)
|
|
232
|
+
|
|
233
|
+
def _chunk_npoints(self, chunk_index: Tuple[int, int]) -> int:
|
|
234
|
+
"""Number of points in a chunk. Edge chunks can be smaller when the
|
|
235
|
+
number of stacks or points is fixed."""
|
|
236
|
+
stacks = self._chunk_slice(0, chunk_index)
|
|
237
|
+
points = self._chunk_slice(1, chunk_index)
|
|
238
|
+
return (stacks.stop - stacks.start) * (points.stop - points.start)
|
{ewoksdata-2.2.0 → ewoksdata-2.3.0rc1}/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py
RENAMED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import time
|
|
2
1
|
from typing import List
|
|
3
2
|
from typing import Optional
|
|
4
3
|
|
|
@@ -27,8 +26,6 @@ class DatasetWriter(DatasetWriterBase):
|
|
|
27
26
|
with DatasetWriter(h5group, "mydataset") as writer:
|
|
28
27
|
writer.add_point([1,2,3])
|
|
29
28
|
writer.add_points([[4,5,6], [7,8,9]])
|
|
30
|
-
|
|
31
|
-
Chunk size determination, chunk-aligned writing, compression and flushing is handled.
|
|
32
29
|
"""
|
|
33
30
|
|
|
34
31
|
def __init__(
|
|
@@ -37,7 +34,7 @@ class DatasetWriter(DatasetWriterBase):
|
|
|
37
34
|
name: str,
|
|
38
35
|
npoints: Optional[StrictPositiveIntegral] = None,
|
|
39
36
|
attrs: Optional[dict] = None,
|
|
40
|
-
flush_period: Optional[float] =
|
|
37
|
+
flush_period: Optional[float] = 1.0,
|
|
41
38
|
overwrite: bool = False,
|
|
42
39
|
) -> None:
|
|
43
40
|
super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
|
|
@@ -110,16 +107,16 @@ class DatasetWriter(DatasetWriterBase):
|
|
|
110
107
|
|
|
111
108
|
def flush_buffer(self, align: bool = False) -> bool:
|
|
112
109
|
# Determine how many points to flush
|
|
113
|
-
|
|
110
|
+
buffer_size = len(self._buffer)
|
|
114
111
|
|
|
115
112
|
if self._flush_time_expired():
|
|
116
|
-
flush_size =
|
|
113
|
+
flush_size = buffer_size
|
|
117
114
|
elif align and self._chunked:
|
|
118
|
-
n =
|
|
115
|
+
n = buffer_size + (self._flushed_size % self._chunk_size)
|
|
119
116
|
flush_size = n // self._chunk_size * self._chunk_size
|
|
120
|
-
flush_size = min(flush_size,
|
|
117
|
+
flush_size = min(flush_size, buffer_size)
|
|
121
118
|
else:
|
|
122
|
-
flush_size =
|
|
119
|
+
flush_size = buffer_size
|
|
123
120
|
|
|
124
121
|
if flush_size == 0:
|
|
125
122
|
return False
|
|
@@ -138,6 +135,4 @@ class DatasetWriter(DatasetWriterBase):
|
|
|
138
135
|
self._buffer = self._buffer[flush_size:]
|
|
139
136
|
self._flushed_size = flushed_size
|
|
140
137
|
|
|
141
|
-
self.
|
|
142
|
-
self._last_flush = time.time()
|
|
143
|
-
return True
|
|
138
|
+
return self._flush_hdf5_throttled()
|