ewoksdata 2.0.3__tar.gz → 2.1.1__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.0.3/src/ewoksdata.egg-info → ewoksdata-2.1.1}/PKG-INFO +1 -1
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/pyproject.toml +1 -1
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/bliss.py +2 -2
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/blissdata/blissdatav0.py +3 -4
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/blissdata/blissdatav1.py +3 -4
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/blissdata/blissdatav2.py +3 -4
- ewoksdata-2.1.1/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +4 -0
- ewoksdata-2.1.1/src/ewoksdata/data/hdf5/dataset_writer/_base.py +55 -0
- ewoksdata-2.1.1/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +139 -0
- ewoksdata-2.1.1/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +158 -0
- ewoksdata-2.1.1/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +143 -0
- ewoksdata-2.0.3/src/ewoksdata/data/hdf5/dataset_writer.py → ewoksdata-2.1.1/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +32 -193
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/utils.py +23 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/test_data_bliss.py +21 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/test_dataset_writer.py +98 -8
- {ewoksdata-2.0.3 → ewoksdata-2.1.1/src/ewoksdata.egg-info}/PKG-INFO +1 -1
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata.egg-info/SOURCES.txt +6 -1
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/LICENSE.md +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/README.md +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/setup.cfg +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/__init__.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/__init__.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/blissdata/__init__.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/contextiterator.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/hdf5/__init__.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/hdf5/config.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/hdf5/types.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/nexus.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/data/url.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/__init__.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/conftest.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/data/__init__.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/data/bliss_scans.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/test_contextiterator.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/test_data_hdf5.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata/tests/test_data_nexus.py +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/src/ewoksdata.egg-info/requires.txt +0 -0
- {ewoksdata-2.0.3 → ewoksdata-2.1.1}/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.1.1"
|
|
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" }
|
|
@@ -35,6 +35,7 @@ from .blissdata import iter_bliss_scan_data_from_memory
|
|
|
35
35
|
from .blissdata import iter_bliss_scan_data_from_memory_slice # noqa: F401
|
|
36
36
|
from .blissdata import last_lima_image # noqa: F401
|
|
37
37
|
from .contextiterator import contextiterator
|
|
38
|
+
from .utils import validate_and_init_names
|
|
38
39
|
|
|
39
40
|
logger = logging.getLogger(__name__)
|
|
40
41
|
|
|
@@ -198,8 +199,7 @@ def iter_bliss_scan_data_from_hdf5(
|
|
|
198
199
|
"""
|
|
199
200
|
if not subscan:
|
|
200
201
|
subscan = 1
|
|
201
|
-
lima_names = lima_names
|
|
202
|
-
counter_names = counter_names or []
|
|
202
|
+
lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
|
|
203
203
|
|
|
204
204
|
with dynamic_hdf5.File(
|
|
205
205
|
filename,
|
|
@@ -11,6 +11,8 @@ from blissdata.data.node import get_node
|
|
|
11
11
|
from numpy.typing import ArrayLike
|
|
12
12
|
from silx.utils import retry as retrymod
|
|
13
13
|
|
|
14
|
+
from ..utils import validate_and_init_names
|
|
15
|
+
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
@@ -24,10 +26,7 @@ def iter_bliss_scan_data_from_memory(
|
|
|
24
26
|
) -> Generator[Dict[str, Any], None, None]:
|
|
25
27
|
if not buffer_in_memory:
|
|
26
28
|
raise NotImplementedError("Requires blissdata>=2.0.0rc1")
|
|
27
|
-
|
|
28
|
-
lima_names = list()
|
|
29
|
-
if counter_names is None:
|
|
30
|
-
counter_names = list()
|
|
29
|
+
lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
|
|
31
30
|
|
|
32
31
|
scan_node = _get_node(
|
|
33
32
|
db_name, "scan", retry_timeout=retry_timeout, retry_period=retry_period
|
|
@@ -23,6 +23,8 @@ from blissdata.redis_engine.store import DataStore
|
|
|
23
23
|
from blissdata.redis_engine.stream import StreamingClient
|
|
24
24
|
from numpy.typing import ArrayLike
|
|
25
25
|
|
|
26
|
+
from ..utils import validate_and_init_names
|
|
27
|
+
|
|
26
28
|
try:
|
|
27
29
|
from blissdata.stream import LimaStream
|
|
28
30
|
except ImportError:
|
|
@@ -49,10 +51,7 @@ def iter_bliss_scan_data_from_memory(
|
|
|
49
51
|
) -> Generator[Dict[str, Any], None, None]:
|
|
50
52
|
if not buffer_in_memory:
|
|
51
53
|
raise NotImplementedError("Requires blissdata>=2.0.0rc1")
|
|
52
|
-
|
|
53
|
-
lima_names = list()
|
|
54
|
-
if counter_names is None:
|
|
55
|
-
counter_names = list()
|
|
54
|
+
lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
|
|
56
55
|
|
|
57
56
|
data_store = _get_data_store()
|
|
58
57
|
scan = data_store.load_scan(scan_key, scan_cls=Scan)
|
|
@@ -18,6 +18,8 @@ from blissdata.redis_engine.store import DataStore
|
|
|
18
18
|
from numpy.typing import ArrayLike
|
|
19
19
|
from packaging.version import Version
|
|
20
20
|
|
|
21
|
+
from ..utils import validate_and_init_names
|
|
22
|
+
|
|
21
23
|
_BLISSDATA_VERSION = Version(version("blissdata"))
|
|
22
24
|
if _BLISSDATA_VERSION >= Version("2.3.0"):
|
|
23
25
|
from blissdata.exceptions import EndOfStream
|
|
@@ -50,10 +52,7 @@ def iter_bliss_scan_data_from_memory(
|
|
|
50
52
|
retry_period: Optional[float] = None,
|
|
51
53
|
buffer_in_memory: bool = False,
|
|
52
54
|
) -> Generator[Dict[str, Any], None, None]:
|
|
53
|
-
|
|
54
|
-
lima_names = list()
|
|
55
|
-
if counter_names is None:
|
|
56
|
-
counter_names = list()
|
|
55
|
+
lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
|
|
57
56
|
|
|
58
57
|
streams = _get_streams(scan_key, lima_names, counter_names)
|
|
59
58
|
if not streams:
|
|
@@ -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
|
|
@@ -6,184 +6,12 @@ import h5py
|
|
|
6
6
|
import numpy
|
|
7
7
|
from numpy.typing import ArrayLike
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
9
|
+
from ..config import guess_dataset_config
|
|
10
|
+
from ..types import StrictPositiveIntegral
|
|
11
|
+
from ._base import DatasetWriterBase
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class
|
|
14
|
-
def __init__(
|
|
15
|
-
self,
|
|
16
|
-
parent: h5py.Group,
|
|
17
|
-
name: str,
|
|
18
|
-
attrs: Optional[dict] = None,
|
|
19
|
-
flush_period: Optional[float] = None,
|
|
20
|
-
) -> None:
|
|
21
|
-
self._file = parent.file
|
|
22
|
-
self._parent = parent
|
|
23
|
-
self._name = name
|
|
24
|
-
self._attrs = attrs
|
|
25
|
-
self._dataset_name = f"{parent.name}/{name}"
|
|
26
|
-
self._dataset = None
|
|
27
|
-
self._flush_period = flush_period
|
|
28
|
-
self._last_flush = None
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
def dataset_name(self) -> str:
|
|
32
|
-
return self._dataset_name
|
|
33
|
-
|
|
34
|
-
def __enter__(self) -> "_DatasetWriterBase":
|
|
35
|
-
return self
|
|
36
|
-
|
|
37
|
-
def __exit__(self, *args) -> None:
|
|
38
|
-
self.flush_buffer()
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def dataset(self) -> Optional[h5py.Dataset]:
|
|
42
|
-
return self._dataset
|
|
43
|
-
|
|
44
|
-
def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
|
|
45
|
-
raise NotImplementedError
|
|
46
|
-
|
|
47
|
-
def flush_buffer(self, align: bool = False) -> bool:
|
|
48
|
-
raise NotImplementedError
|
|
49
|
-
|
|
50
|
-
def _flush_time_expired(self) -> bool:
|
|
51
|
-
if self._flush_period is None:
|
|
52
|
-
return False
|
|
53
|
-
if self._last_flush is None:
|
|
54
|
-
self._last_flush = time.time()
|
|
55
|
-
return False
|
|
56
|
-
return (time.time() - self._last_flush) >= self._flush_period
|
|
57
|
-
|
|
58
|
-
def _flush_hdf5(self) -> None:
|
|
59
|
-
"""Explicit HDF5 flush for non-locking readers."""
|
|
60
|
-
self._file.flush()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class DatasetWriter(_DatasetWriterBase):
|
|
64
|
-
"""Append arrays of the same shape to a new HDF5 dataset in a sequential manner.
|
|
65
|
-
|
|
66
|
-
Instead of creating a dataset with the :code:`h5py` API
|
|
67
|
-
|
|
68
|
-
.. code-block::python
|
|
69
|
-
|
|
70
|
-
h5group["mydataset"] = [[1,2,3], [4,5,6], [7,8,9]]
|
|
71
|
-
|
|
72
|
-
it can be done like this
|
|
73
|
-
|
|
74
|
-
.. code-block::python
|
|
75
|
-
|
|
76
|
-
with DatasetWriter(h5group, "mydataset") as writer:
|
|
77
|
-
writer.add_point([1,2,3])
|
|
78
|
-
writer.add_points([[4,5,6], [7,8,9]])
|
|
79
|
-
|
|
80
|
-
Chunk size determination, chunk-aligned writing, compression and flushing is handled.
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
def __init__(
|
|
84
|
-
self,
|
|
85
|
-
parent: h5py.Group,
|
|
86
|
-
name: str,
|
|
87
|
-
npoints: Optional[StrictPositiveIntegral] = None,
|
|
88
|
-
attrs: Optional[dict] = None,
|
|
89
|
-
flush_period: Optional[float] = None,
|
|
90
|
-
overwrite: bool = False,
|
|
91
|
-
) -> None:
|
|
92
|
-
super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
|
|
93
|
-
self._npoints = npoints
|
|
94
|
-
self._overwrite = overwrite
|
|
95
|
-
self._chunked: bool = False
|
|
96
|
-
self._npoints_added: int = 0
|
|
97
|
-
|
|
98
|
-
self._buffer: List[ArrayLike] = list()
|
|
99
|
-
self._chunk_size: int = 0
|
|
100
|
-
self._flushed_size: int = 0
|
|
101
|
-
|
|
102
|
-
def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
|
|
103
|
-
scan_shape = (self._npoints,)
|
|
104
|
-
detector_shape = first_data_point.shape
|
|
105
|
-
dtype = first_data_point.dtype
|
|
106
|
-
if self._npoints is None:
|
|
107
|
-
max_shape = scan_shape + detector_shape
|
|
108
|
-
shape = (1,) + first_data_point.shape
|
|
109
|
-
else:
|
|
110
|
-
max_shape = None
|
|
111
|
-
shape = scan_shape + first_data_point.shape
|
|
112
|
-
|
|
113
|
-
options = guess_dataset_config(
|
|
114
|
-
scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
|
|
115
|
-
)
|
|
116
|
-
options["shape"] = shape
|
|
117
|
-
options["dtype"] = dtype
|
|
118
|
-
options["fillvalue"] = numpy.nan # converts to 0 for integers
|
|
119
|
-
if max_shape:
|
|
120
|
-
options["maxshape"] = max_shape
|
|
121
|
-
if options["chunks"]:
|
|
122
|
-
self._chunked = True
|
|
123
|
-
self._chunk_size = options["chunks"][0]
|
|
124
|
-
if self._overwrite and self._name in self._parent:
|
|
125
|
-
del self._parent[self._name]
|
|
126
|
-
dset = self._parent.create_dataset(self._name, **options)
|
|
127
|
-
if self._attrs:
|
|
128
|
-
dset.attrs.update(self._attrs)
|
|
129
|
-
return dset
|
|
130
|
-
|
|
131
|
-
@property
|
|
132
|
-
def npoints_added(self) -> int:
|
|
133
|
-
return self._npoints_added
|
|
134
|
-
|
|
135
|
-
def add_point(self, data: ArrayLike) -> bool:
|
|
136
|
-
"""Append one array to the dataset."""
|
|
137
|
-
if self._dataset is None:
|
|
138
|
-
self._dataset = self._create_dataset(data)
|
|
139
|
-
self._buffer.append(data)
|
|
140
|
-
self._npoints_added += 1
|
|
141
|
-
return self.flush_buffer(align=True)
|
|
142
|
-
|
|
143
|
-
def add_points(self, data: ArrayLike) -> bool:
|
|
144
|
-
"""Append several arrays at once to the dataset."""
|
|
145
|
-
if self._dataset is None:
|
|
146
|
-
self._dataset = self._create_dataset(data[0])
|
|
147
|
-
self._buffer.extend(data)
|
|
148
|
-
self._npoints_added += len(data)
|
|
149
|
-
return self.flush_buffer(align=True)
|
|
150
|
-
|
|
151
|
-
def flush_buffer(self, align: bool = False) -> bool:
|
|
152
|
-
# Determine how many points to flush
|
|
153
|
-
chunk_size = len(self._buffer)
|
|
154
|
-
|
|
155
|
-
if self._flush_time_expired():
|
|
156
|
-
flush_size = chunk_size
|
|
157
|
-
elif align and self._chunked:
|
|
158
|
-
n = chunk_size + (self._flushed_size % self._chunk_size)
|
|
159
|
-
flush_size = n // self._chunk_size * self._chunk_size
|
|
160
|
-
flush_size = min(flush_size, chunk_size)
|
|
161
|
-
else:
|
|
162
|
-
flush_size = chunk_size
|
|
163
|
-
|
|
164
|
-
if flush_size == 0:
|
|
165
|
-
return False
|
|
166
|
-
|
|
167
|
-
# Enlarge the dataset when needed
|
|
168
|
-
nalloc = self._dataset.shape[0]
|
|
169
|
-
istart = self._flushed_size
|
|
170
|
-
flushed_size = istart + flush_size
|
|
171
|
-
if self._chunked and flushed_size > nalloc:
|
|
172
|
-
self._dataset.resize(flushed_size, axis=0)
|
|
173
|
-
|
|
174
|
-
# Copy data from buffer to HDF5
|
|
175
|
-
self._dataset[istart : istart + flush_size] = self._buffer[:flush_size]
|
|
176
|
-
|
|
177
|
-
# Remove copied data from buffer
|
|
178
|
-
self._buffer = self._buffer[flush_size:]
|
|
179
|
-
self._flushed_size = flushed_size
|
|
180
|
-
|
|
181
|
-
self._flush_hdf5()
|
|
182
|
-
self._last_flush = time.time()
|
|
183
|
-
return True
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class StackDatasetWriter(_DatasetWriterBase):
|
|
14
|
+
class StackDatasetWriter(DatasetWriterBase):
|
|
187
15
|
"""Append arrays of the same shape to each item of a new HDF5 dataset
|
|
188
16
|
in a sequential manner per item. So each item of the HDF5 dataset is a
|
|
189
17
|
stack to which we can append data in a sequential manner.
|
|
@@ -228,45 +56,45 @@ class StackDatasetWriter(_DatasetWriterBase):
|
|
|
228
56
|
self._flushed_size_dim1: ArrayLike = numpy.array(list(), dtype=int)
|
|
229
57
|
|
|
230
58
|
def _create_dataset(
|
|
231
|
-
self, first_data_point:
|
|
59
|
+
self, first_data_point: ArrayLike, stack_index: int
|
|
232
60
|
) -> h5py.Dataset:
|
|
233
61
|
scan_shape = (self._nstack, self._npoints)
|
|
62
|
+
first_data_point = numpy.asarray(first_data_point)
|
|
234
63
|
detector_shape = first_data_point.shape
|
|
235
64
|
dtype = first_data_point.dtype
|
|
236
|
-
|
|
65
|
+
|
|
66
|
+
if self._nstack is None or self._npoints is None:
|
|
67
|
+
shape = (stack_index + 1, 1) + detector_shape
|
|
237
68
|
max_shape = scan_shape + detector_shape
|
|
238
|
-
shape = (stack_index + 1, 1) + first_data_point.shape
|
|
239
69
|
else:
|
|
70
|
+
shape = scan_shape + detector_shape
|
|
240
71
|
max_shape = None
|
|
241
|
-
shape = scan_shape + first_data_point.shape
|
|
242
72
|
|
|
243
73
|
options = guess_dataset_config(
|
|
244
74
|
scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
|
|
245
75
|
)
|
|
246
|
-
options
|
|
247
|
-
|
|
248
|
-
|
|
76
|
+
options.update(
|
|
77
|
+
shape=shape,
|
|
78
|
+
dtype=dtype,
|
|
79
|
+
fillvalue=numpy.nan, # converts to 0 for integers
|
|
80
|
+
)
|
|
249
81
|
if max_shape:
|
|
250
82
|
options["maxshape"] = max_shape
|
|
83
|
+
|
|
251
84
|
if options["chunks"]:
|
|
252
85
|
self._chunked = True
|
|
253
86
|
self._chunk_size = numpy.array(options["chunks"][:2], dtype=int)
|
|
87
|
+
|
|
254
88
|
dset = self._parent.create_dataset(self._name, **options)
|
|
255
89
|
if self._attrs:
|
|
256
90
|
dset.attrs.update(self._attrs)
|
|
257
91
|
return dset
|
|
258
92
|
|
|
259
|
-
def _get_buffer(self, stack_index: int) -> List[ArrayLike]:
|
|
260
|
-
# Add stack buffers when needed
|
|
261
|
-
for _ in range(max(stack_index - len(self._buffers) + 1, 0)):
|
|
262
|
-
self._buffers.append(list())
|
|
263
|
-
self._flushed_size_dim1 = numpy.append(self._flushed_size_dim1, 0)
|
|
264
|
-
return self._buffers[stack_index]
|
|
265
|
-
|
|
266
93
|
def add_point(self, data: ArrayLike, stack_index: int) -> bool:
|
|
267
94
|
"""Append one array to one stack of the dataset."""
|
|
268
95
|
if self._dataset is None:
|
|
269
96
|
self._dataset = self._create_dataset(data, stack_index)
|
|
97
|
+
|
|
270
98
|
buffer = self._get_buffer(stack_index)
|
|
271
99
|
buffer.append(data)
|
|
272
100
|
return self.flush_buffer(align=True)
|
|
@@ -275,6 +103,7 @@ class StackDatasetWriter(_DatasetWriterBase):
|
|
|
275
103
|
"""Append several arrays at once to one stack of the dataset."""
|
|
276
104
|
if self._dataset is None:
|
|
277
105
|
self._dataset = self._create_dataset(data[0], stack_index)
|
|
106
|
+
|
|
278
107
|
buffer = self._get_buffer(stack_index)
|
|
279
108
|
buffer.extend(data)
|
|
280
109
|
return self.flush_buffer(align=True)
|
|
@@ -335,6 +164,8 @@ class StackDatasetWriter(_DatasetWriterBase):
|
|
|
335
164
|
non_ragged_destination = len(set(istart0_dim1)) == 1
|
|
336
165
|
|
|
337
166
|
if non_ragged_destination and non_ragged_buffers:
|
|
167
|
+
if flush_sizes_dim1[0] == 0:
|
|
168
|
+
continue # h5py=3.16 cannot handle this anymore
|
|
338
169
|
data = [buffer[: flush_sizes_dim1[0]] for buffer in buffers]
|
|
339
170
|
idx_dim1 = slice(istart0_dim1[0], istart0_dim1[0] + flush_sizes_dim1[0])
|
|
340
171
|
self._dataset[idx_dim0, idx_dim1] = data
|
|
@@ -345,9 +176,10 @@ class StackDatasetWriter(_DatasetWriterBase):
|
|
|
345
176
|
istart0_dim1,
|
|
346
177
|
flush_sizes_dim1,
|
|
347
178
|
):
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
179
|
+
if i_flush_size_dim1 == 0:
|
|
180
|
+
continue # h5py=3.16 cannot handle this anymore
|
|
181
|
+
idx_dim1 = slice(istart_dim1, istart_dim1 + i_flush_size_dim1)
|
|
182
|
+
self._dataset[i_dim0, idx_dim1, ...] = buffer[:i_flush_size_dim1]
|
|
351
183
|
|
|
352
184
|
# Remove copied data from buffer
|
|
353
185
|
for i0 in range(size_dim0):
|
|
@@ -357,3 +189,10 @@ class StackDatasetWriter(_DatasetWriterBase):
|
|
|
357
189
|
self._flush_hdf5()
|
|
358
190
|
self._last_flush = time.time()
|
|
359
191
|
return True
|
|
192
|
+
|
|
193
|
+
def _get_buffer(self, stack_index: int) -> List[ArrayLike]:
|
|
194
|
+
# Add stack buffers when needed
|
|
195
|
+
for _ in range(max(stack_index - len(self._buffers) + 1, 0)):
|
|
196
|
+
self._buffers.append(list())
|
|
197
|
+
self._flushed_size_dim1 = numpy.append(self._flushed_size_dim1, 0)
|
|
198
|
+
return self._buffers[stack_index]
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from numbers import Number
|
|
2
|
+
from typing import List
|
|
2
3
|
from typing import Mapping
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Tuple
|
|
3
6
|
|
|
4
7
|
import numpy
|
|
5
8
|
|
|
@@ -28,3 +31,23 @@ def data_from_storage(data, remove_numpy=True):
|
|
|
28
31
|
}
|
|
29
32
|
else:
|
|
30
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
|
+
)
|
|
@@ -62,6 +62,27 @@ def test_iter_bliss_data(lima_names, counter_names, bliss_scan):
|
|
|
62
62
|
assert index is None
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
@pytest.mark.parametrize(
|
|
66
|
+
"lima_names,counter_names",
|
|
67
|
+
[
|
|
68
|
+
([""], []),
|
|
69
|
+
([" "], []),
|
|
70
|
+
([], [""]),
|
|
71
|
+
([], [" "]),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
def test_iter_bliss_data_invalid_names(bliss_scan, lima_names, counter_names):
|
|
75
|
+
filename, _, scan_nb = bliss_scan
|
|
76
|
+
with bliss.iter_bliss_data(
|
|
77
|
+
filename, scan_nb, lima_names=lima_names, counter_names=counter_names
|
|
78
|
+
) as bliss_data_iterator:
|
|
79
|
+
with pytest.raises(ValueError) as exc_info:
|
|
80
|
+
next(bliss_data_iterator)
|
|
81
|
+
assert "Invalid LIMA name" in str(exc_info.value) or "Invalid Counter name" in str(
|
|
82
|
+
exc_info.value
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
65
86
|
def test_closure_of_iter_bliss_data(bliss_scan):
|
|
66
87
|
filename, npoints, scan_nb = bliss_scan
|
|
67
88
|
|
|
@@ -13,9 +13,9 @@ from ..data.hdf5 import dataset_writer
|
|
|
13
13
|
@pytest.mark.parametrize("npoints", (1, 3, 1000))
|
|
14
14
|
@pytest.mark.parametrize("flush_period", (None, 0.1))
|
|
15
15
|
@pytest.mark.parametrize("known_npoints", (True, False))
|
|
16
|
-
def test_dataset_writer(
|
|
16
|
+
def test_dataset_writer(tmp_path, npoints, flush_period, known_npoints):
|
|
17
17
|
expected = list()
|
|
18
|
-
filename = str(
|
|
18
|
+
filename = str(tmp_path / "test.h5")
|
|
19
19
|
if flush_period is None:
|
|
20
20
|
sleep_time = None
|
|
21
21
|
else:
|
|
@@ -47,7 +47,7 @@ def test_dataset_writer(tmpdir, npoints, flush_period, known_npoints):
|
|
|
47
47
|
@pytest.mark.parametrize("known_nstack", (True, False))
|
|
48
48
|
@pytest.mark.parametrize("append_stacks_in_parallel", (True, False))
|
|
49
49
|
def test_stack_dataset_writer(
|
|
50
|
-
|
|
50
|
+
tmp_path,
|
|
51
51
|
nstack,
|
|
52
52
|
npoints,
|
|
53
53
|
flush_period,
|
|
@@ -56,7 +56,7 @@ def test_stack_dataset_writer(
|
|
|
56
56
|
append_stacks_in_parallel,
|
|
57
57
|
):
|
|
58
58
|
expected = [list() for _ in range(nstack)]
|
|
59
|
-
filename = str(
|
|
59
|
+
filename = str(tmp_path / "test.h5")
|
|
60
60
|
if flush_period is None:
|
|
61
61
|
sleep_time = None
|
|
62
62
|
else:
|
|
@@ -92,11 +92,101 @@ def test_stack_dataset_writer(
|
|
|
92
92
|
numpy.testing.assert_allclose(data, expected)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
@pytest.mark.parametrize("npoints", (1, 3, 1000))
|
|
96
|
+
@pytest.mark.parametrize("flush_period", (None, 0.1))
|
|
97
|
+
@pytest.mark.parametrize("known_npoints", (True, False))
|
|
98
|
+
def test_indexed_dataset_writer(tmp_path, npoints, flush_period, known_npoints):
|
|
99
|
+
filename = str(tmp_path / "test.h5")
|
|
100
|
+
|
|
101
|
+
if flush_period is None:
|
|
102
|
+
sleep_time = None
|
|
103
|
+
else:
|
|
104
|
+
sleep_time = flush_period + 0.1
|
|
105
|
+
|
|
106
|
+
isleep = npoints // 3
|
|
107
|
+
|
|
108
|
+
kwargs = {"flush_period": flush_period}
|
|
109
|
+
if known_npoints:
|
|
110
|
+
kwargs["npoints"] = npoints
|
|
111
|
+
|
|
112
|
+
# Generate data and shuffled indices
|
|
113
|
+
indices = numpy.arange(npoints)
|
|
114
|
+
numpy.random.shuffle(indices)
|
|
115
|
+
|
|
116
|
+
expected = [None] * npoints
|
|
117
|
+
data_points = [numpy.random.random((10, 20)) for _ in range(npoints)]
|
|
118
|
+
for idx, data in zip(indices, data_points):
|
|
119
|
+
expected[idx] = data
|
|
120
|
+
|
|
121
|
+
with h5py.File(filename, mode="w") as f:
|
|
122
|
+
with dataset_writer.IndexedDatasetWriter(f, "data", **kwargs) as writer:
|
|
123
|
+
for i, (idx, data) in enumerate(zip(indices, data_points)):
|
|
124
|
+
writer.add_point(data, idx)
|
|
125
|
+
if sleep_time and i == isleep:
|
|
126
|
+
time.sleep(sleep_time)
|
|
127
|
+
|
|
128
|
+
with h5py.File(filename, mode="r") as f:
|
|
129
|
+
data = f["data"][()]
|
|
130
|
+
|
|
131
|
+
numpy.testing.assert_allclose(data, expected)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.parametrize("nstack", (1, 4))
|
|
135
|
+
@pytest.mark.parametrize("npoints", (1, 3, 100))
|
|
136
|
+
@pytest.mark.parametrize("flush_period", (None, 0.1))
|
|
137
|
+
@pytest.mark.parametrize("known_npoints", (True, False))
|
|
138
|
+
@pytest.mark.parametrize("known_nstack", (True, False))
|
|
139
|
+
def test_indexed_stack_dataset_writer(
|
|
140
|
+
tmp_path,
|
|
141
|
+
nstack,
|
|
142
|
+
npoints,
|
|
143
|
+
flush_period,
|
|
144
|
+
known_npoints,
|
|
145
|
+
known_nstack,
|
|
146
|
+
):
|
|
147
|
+
filename = str(tmp_path / "test.h5")
|
|
148
|
+
|
|
149
|
+
if flush_period is None:
|
|
150
|
+
sleep_time = None
|
|
151
|
+
else:
|
|
152
|
+
sleep_time = flush_period + 0.1
|
|
153
|
+
|
|
154
|
+
isleep = (nstack * npoints) // 3
|
|
155
|
+
|
|
156
|
+
kwargs = {"flush_period": flush_period}
|
|
157
|
+
if known_npoints:
|
|
158
|
+
kwargs["npoints"] = npoints
|
|
159
|
+
if known_nstack:
|
|
160
|
+
kwargs["nstack"] = nstack
|
|
161
|
+
|
|
162
|
+
# Generate data and shuffled indices
|
|
163
|
+
indices = list(itertools.product(range(nstack), range(npoints)))
|
|
164
|
+
numpy.random.shuffle(indices)
|
|
165
|
+
|
|
166
|
+
expected = [[None for _ in range(npoints)] for _ in range(nstack)]
|
|
167
|
+
values = [numpy.random.random((10, 20)) for _ in indices]
|
|
168
|
+
|
|
169
|
+
for (istack, ipoint), value in zip(indices, values):
|
|
170
|
+
expected[istack][ipoint] = value
|
|
171
|
+
|
|
172
|
+
with h5py.File(filename, mode="w") as f:
|
|
173
|
+
with dataset_writer.IndexedStackDatasetWriter(f, "data", **kwargs) as writer:
|
|
174
|
+
for i, ((istack, ipoint), value) in enumerate(zip(indices, values)):
|
|
175
|
+
writer.add_point(value, istack, ipoint)
|
|
176
|
+
if sleep_time and i == isleep:
|
|
177
|
+
time.sleep(sleep_time)
|
|
178
|
+
|
|
179
|
+
with h5py.File(filename, mode="r") as f:
|
|
180
|
+
data = f["data"][()]
|
|
181
|
+
|
|
182
|
+
numpy.testing.assert_allclose(data, expected)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_concurrent_reader(tmp_path):
|
|
96
186
|
npoints = 50
|
|
97
|
-
hdf5_filename = str(
|
|
98
|
-
read_timestamps = str(
|
|
99
|
-
write_timestamps = str(
|
|
187
|
+
hdf5_filename = str(tmp_path / "test.h5")
|
|
188
|
+
read_timestamps = str(tmp_path / "read_timestamps.log")
|
|
189
|
+
write_timestamps = str(tmp_path / "write_timestamps.log")
|
|
100
190
|
dataset_name = "data"
|
|
101
191
|
|
|
102
192
|
# Run read loop in a sub-process
|
|
@@ -19,8 +19,13 @@ src/ewoksdata/data/blissdata/blissdatav1.py
|
|
|
19
19
|
src/ewoksdata/data/blissdata/blissdatav2.py
|
|
20
20
|
src/ewoksdata/data/hdf5/__init__.py
|
|
21
21
|
src/ewoksdata/data/hdf5/config.py
|
|
22
|
-
src/ewoksdata/data/hdf5/dataset_writer.py
|
|
23
22
|
src/ewoksdata/data/hdf5/types.py
|
|
23
|
+
src/ewoksdata/data/hdf5/dataset_writer/__init__.py
|
|
24
|
+
src/ewoksdata/data/hdf5/dataset_writer/_base.py
|
|
25
|
+
src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py
|
|
26
|
+
src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py
|
|
27
|
+
src/ewoksdata/data/hdf5/dataset_writer/_sequential.py
|
|
28
|
+
src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py
|
|
24
29
|
src/ewoksdata/tests/__init__.py
|
|
25
30
|
src/ewoksdata/tests/conftest.py
|
|
26
31
|
src/ewoksdata/tests/test_contextiterator.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|