ewoksdata 2.0.2__tar.gz → 2.1.0__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.2/src/ewoksdata.egg-info → ewoksdata-2.1.0}/PKG-INFO +1 -1
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/pyproject.toml +1 -1
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/bliss.py +41 -10
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/blissdatav0.py +3 -4
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/blissdatav1.py +3 -4
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/blissdatav2.py +3 -4
- ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +4 -0
- ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_base.py +55 -0
- ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +139 -0
- ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +158 -0
- ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +143 -0
- ewoksdata-2.0.2/src/ewoksdata/data/hdf5/dataset_writer.py → ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +26 -190
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/utils.py +23 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_data_bliss.py +25 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_dataset_writer.py +98 -8
- {ewoksdata-2.0.2 → ewoksdata-2.1.0/src/ewoksdata.egg-info}/PKG-INFO +1 -1
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata.egg-info/SOURCES.txt +6 -1
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/LICENSE.md +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/README.md +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/setup.cfg +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/__init__.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/__init__.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/__init__.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/contextiterator.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/hdf5/__init__.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/hdf5/config.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/hdf5/types.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/nexus.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/data/url.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/__init__.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/conftest.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/data/__init__.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/data/bliss_scans.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_contextiterator.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_data_hdf5.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_data_nexus.py +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/src/ewoksdata.egg-info/requires.txt +0 -0
- {ewoksdata-2.0.2 → ewoksdata-2.1.0}/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.0
|
|
7
|
+
version = "2.1.0"
|
|
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,29 +35,56 @@ 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
|
|
|
41
42
|
|
|
42
43
|
def get_data(
|
|
43
44
|
data: Union[str, DataUrl, ArrayLike, Number],
|
|
45
|
+
retry_timeout: Optional[float] = None,
|
|
46
|
+
retry_period: Optional[float] = None,
|
|
44
47
|
**hdf5_options: Any,
|
|
45
48
|
) -> Union[numpy.ndarray, Number, Sequence]:
|
|
46
49
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
Retrieve numerical data from a data source and return it as a NumPy array or scalar.
|
|
51
|
+
|
|
52
|
+
This function accepts various data representations, including data URLs,
|
|
53
|
+
array-like objects, and scalar numbers. The data source may not be
|
|
54
|
+
immediately available so a retry mechanism is used.
|
|
55
|
+
|
|
56
|
+
:param data: The input data source. May be:
|
|
57
|
+
- A string or :class:`DataUrl` referencing a dataset (e.g., HDF5),
|
|
58
|
+
- An array-like object convertible to a NumPy array,
|
|
59
|
+
- A scalar numerical value.
|
|
60
|
+
:param retry_timeout: Maximum time in seconds to wait for the data to become
|
|
61
|
+
available. If ``None`` (default), no timeout is applied.
|
|
62
|
+
:param retry_period: Interval in seconds between availability checks when
|
|
63
|
+
retrying. If ``None`` (default), a period of 0.1 seconds is used.
|
|
64
|
+
:param hdf5_options: Additional keyword arguments forwarded to
|
|
65
|
+
:class:`silx.io.h5py_utils.File` when opening HDF5 resources.
|
|
66
|
+
|
|
67
|
+
:returns: The resolved data. Array-like inputs are returned as NumPy arrays,
|
|
68
|
+
while scalar inputs are returned as numbers.
|
|
69
|
+
|
|
70
|
+
:raises TimeoutError: If the data does not become available within ``retry_timeout``.
|
|
71
|
+
:raises ValueError: If the input type is unsupported or the data cannot be resolved.
|
|
52
72
|
"""
|
|
53
73
|
if isinstance(data, (str, DataUrl)):
|
|
54
74
|
data_url = url.as_dataurl(data)
|
|
55
75
|
filename, h5path, idx = url.h5dataset_url_parse(data_url)
|
|
56
76
|
|
|
57
|
-
_wait_for_file(filename,
|
|
77
|
+
_wait_for_file(filename, retry_timeout=retry_timeout, retry_period=retry_period)
|
|
58
78
|
|
|
59
|
-
if
|
|
60
|
-
return _get_hdf5_data(
|
|
79
|
+
if _is_hdf5_filename(filename):
|
|
80
|
+
return _get_hdf5_data(
|
|
81
|
+
filename,
|
|
82
|
+
h5path,
|
|
83
|
+
idx=idx,
|
|
84
|
+
retry_timeout=retry_timeout,
|
|
85
|
+
retry_period=retry_period,
|
|
86
|
+
**hdf5_options,
|
|
87
|
+
)
|
|
61
88
|
|
|
62
89
|
if not data_url.scheme():
|
|
63
90
|
for scheme in ("fabio", "silx"):
|
|
@@ -97,6 +124,11 @@ def _wait_for_file(
|
|
|
97
124
|
time.sleep(retry_period)
|
|
98
125
|
|
|
99
126
|
|
|
127
|
+
def _is_hdf5_filename(filename: str) -> bool:
|
|
128
|
+
_, ext = os.path.splitext(filename)
|
|
129
|
+
return ext in (".h5", ".hdf5", ".nx", ".nxs", ".nexus")
|
|
130
|
+
|
|
131
|
+
|
|
100
132
|
def get_image(
|
|
101
133
|
data: Union[str, DataUrl, ArrayLike, Number], **hdf5_options: Any
|
|
102
134
|
) -> numpy.ndarray:
|
|
@@ -167,8 +199,7 @@ def iter_bliss_scan_data_from_hdf5(
|
|
|
167
199
|
"""
|
|
168
200
|
if not subscan:
|
|
169
201
|
subscan = 1
|
|
170
|
-
lima_names = lima_names
|
|
171
|
-
counter_names = counter_names or []
|
|
202
|
+
lima_names, counter_names = validate_and_init_names(lima_names, counter_names)
|
|
172
203
|
|
|
173
204
|
with dynamic_hdf5.File(
|
|
174
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)
|
|
@@ -357,3 +186,10 @@ class StackDatasetWriter(_DatasetWriterBase):
|
|
|
357
186
|
self._flush_hdf5()
|
|
358
187
|
self._last_flush = time.time()
|
|
359
188
|
return True
|
|
189
|
+
|
|
190
|
+
def _get_buffer(self, stack_index: int) -> List[ArrayLike]:
|
|
191
|
+
# Add stack buffers when needed
|
|
192
|
+
for _ in range(max(stack_index - len(self._buffers) + 1, 0)):
|
|
193
|
+
self._buffers.append(list())
|
|
194
|
+
self._flushed_size_dim1 = numpy.append(self._flushed_size_dim1, 0)
|
|
195
|
+
return self._buffers[stack_index]
|
|
@@ -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
|
+
)
|
|
@@ -31,6 +31,10 @@ def test_get_data_hdf5(tmpdir):
|
|
|
31
31
|
with pytest.raises(TimeoutError):
|
|
32
32
|
_ = bliss.get_data(f"{filename_non_existing}?path=/img", retry_timeout=0)
|
|
33
33
|
|
|
34
|
+
filename_non_existing = str(tmpdir / "data_non_existing.edf")
|
|
35
|
+
with pytest.raises(TimeoutError):
|
|
36
|
+
_ = bliss.get_data(filename_non_existing, retry_timeout=0)
|
|
37
|
+
|
|
34
38
|
|
|
35
39
|
@pytest.mark.parametrize("lima_names", [(), ("p3",), ("p3", "p4")])
|
|
36
40
|
@pytest.mark.parametrize("counter_names", [(), ("diode1",), ("diode1", "diode2")])
|
|
@@ -58,6 +62,27 @@ def test_iter_bliss_data(lima_names, counter_names, bliss_scan):
|
|
|
58
62
|
assert index is None
|
|
59
63
|
|
|
60
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
|
+
|
|
61
86
|
def test_closure_of_iter_bliss_data(bliss_scan):
|
|
62
87
|
filename, npoints, scan_nb = bliss_scan
|
|
63
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
|