ewoksdata 2.0.3__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.
Files changed (39) hide show
  1. {ewoksdata-2.0.3/src/ewoksdata.egg-info → ewoksdata-2.1.0}/PKG-INFO +1 -1
  2. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/pyproject.toml +1 -1
  3. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/bliss.py +2 -2
  4. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/blissdatav0.py +3 -4
  5. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/blissdatav1.py +3 -4
  6. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/blissdatav2.py +3 -4
  7. ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +4 -0
  8. ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_base.py +55 -0
  9. ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +139 -0
  10. ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +158 -0
  11. ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +143 -0
  12. ewoksdata-2.0.3/src/ewoksdata/data/hdf5/dataset_writer.py → ewoksdata-2.1.0/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +26 -190
  13. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/utils.py +23 -0
  14. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_data_bliss.py +21 -0
  15. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_dataset_writer.py +98 -8
  16. {ewoksdata-2.0.3 → ewoksdata-2.1.0/src/ewoksdata.egg-info}/PKG-INFO +1 -1
  17. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata.egg-info/SOURCES.txt +6 -1
  18. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/LICENSE.md +0 -0
  19. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/README.md +0 -0
  20. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/setup.cfg +0 -0
  21. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/__init__.py +0 -0
  22. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/__init__.py +0 -0
  23. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/blissdata/__init__.py +0 -0
  24. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/contextiterator.py +0 -0
  25. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/hdf5/__init__.py +0 -0
  26. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/hdf5/config.py +0 -0
  27. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/hdf5/types.py +0 -0
  28. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/nexus.py +0 -0
  29. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/data/url.py +0 -0
  30. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/__init__.py +0 -0
  31. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/conftest.py +0 -0
  32. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/data/__init__.py +0 -0
  33. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/data/bliss_scans.py +0 -0
  34. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_contextiterator.py +0 -0
  35. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_data_hdf5.py +0 -0
  36. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata/tests/test_data_nexus.py +0 -0
  37. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
  38. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata.egg-info/requires.txt +0 -0
  39. {ewoksdata-2.0.3 → ewoksdata-2.1.0}/src/ewoksdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ewoksdata
3
- Version: 2.0.3
3
+ Version: 2.1.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ewoksdata"
7
- version = "2.0.3"
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,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 or []
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
- if lima_names is None:
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
- if lima_names is None:
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
- if lima_names is None:
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,4 @@
1
+ from ._non_sequential import IndexedDatasetWriter # noqa F401
2
+ from ._non_sequential_stack import IndexedStackDatasetWriter # noqa F401
3
+ from ._sequential import DatasetWriter # noqa F401
4
+ from ._sequential_stack import StackDatasetWriter # noqa F401
@@ -0,0 +1,55 @@
1
+ import time
2
+ from typing import Optional
3
+
4
+ import h5py
5
+ import numpy
6
+
7
+
8
+ class DatasetWriterBase:
9
+ def __init__(
10
+ self,
11
+ parent: h5py.Group,
12
+ name: str,
13
+ attrs: Optional[dict] = None,
14
+ flush_period: Optional[float] = None,
15
+ ) -> None:
16
+ self._file = parent.file
17
+ self._parent = parent
18
+ self._name = name
19
+ self._attrs = attrs
20
+ self._dataset_name = f"{parent.name}/{name}"
21
+ self._dataset = None
22
+ self._flush_period = flush_period
23
+ self._last_flush = None
24
+
25
+ @property
26
+ def dataset_name(self) -> str:
27
+ return self._dataset_name
28
+
29
+ def __enter__(self) -> "DatasetWriterBase":
30
+ return self
31
+
32
+ def __exit__(self, *args) -> None:
33
+ self.flush_buffer()
34
+
35
+ @property
36
+ def dataset(self) -> Optional[h5py.Dataset]:
37
+ return self._dataset
38
+
39
+ def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
40
+ raise NotImplementedError
41
+
42
+ def flush_buffer(self, align: bool = False) -> bool:
43
+ raise NotImplementedError
44
+
45
+ def _flush_time_expired(self) -> bool:
46
+ if self._flush_period is None:
47
+ return False
48
+ if self._last_flush is None:
49
+ self._last_flush = time.time()
50
+ return False
51
+ return (time.time() - self._last_flush) >= self._flush_period
52
+
53
+ def _flush_hdf5(self) -> None:
54
+ """Explicit HDF5 flush for non-locking readers."""
55
+ self._file.flush()
@@ -0,0 +1,139 @@
1
+ import logging
2
+ import time
3
+ from typing import List
4
+ from typing import Optional
5
+
6
+ import h5py
7
+ import numpy
8
+ from numpy.typing import ArrayLike
9
+
10
+ from ..config import guess_dataset_config
11
+ from ..types import StrictPositiveIntegral
12
+ from ._base import DatasetWriterBase
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IndexedDatasetWriter(DatasetWriterBase):
18
+ """Append arrays of the same shape to a new HDF5 dataset in a potentially non-sequential manner.
19
+
20
+ Instead of creating a dataset with the :code:`h5py` API
21
+
22
+ .. code-block::python
23
+
24
+ h5group["mydataset"] = [[1,2,3], [4,5,6], [7,8,9]]
25
+
26
+ it can be done like this
27
+
28
+ .. code-block::python
29
+
30
+ with IndexedDatasetWriter(h5group, "mydataset") as writer:
31
+ writer.add_point(1, [4,5,6])
32
+ writer.add_points([0, 2], [[1,2,3], [7,8,9]])
33
+
34
+ Chunk size determination, compression and flushing is handled.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ parent: h5py.Group,
40
+ name: str,
41
+ npoints: Optional[StrictPositiveIntegral] = None,
42
+ attrs: Optional[dict] = None,
43
+ flush_period: Optional[float] = None,
44
+ overwrite: bool = False,
45
+ ) -> None:
46
+ super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
47
+ self._npoints = npoints
48
+ self._overwrite = overwrite
49
+ self._npoints_added: int = 0
50
+
51
+ self._buffer: List[tuple[int, ArrayLike]] = []
52
+
53
+ def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
54
+ scan_shape = (self._npoints,)
55
+ detector_shape = first_data_point.shape
56
+ dtype = first_data_point.dtype
57
+
58
+ if self._npoints is None:
59
+ shape = (1,) + detector_shape
60
+ max_shape = scan_shape + detector_shape
61
+ else:
62
+ shape = scan_shape + detector_shape
63
+ max_shape = None
64
+
65
+ options = guess_dataset_config(
66
+ scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
67
+ )
68
+ options.update(
69
+ shape=shape,
70
+ dtype=dtype,
71
+ fillvalue=numpy.nan, # converts to 0 for integers
72
+ )
73
+ if max_shape:
74
+ options["maxshape"] = max_shape
75
+
76
+ if self._overwrite and self._name in self._parent:
77
+ del self._parent[self._name]
78
+
79
+ dset = self._parent.create_dataset(self._name, **options)
80
+ if self._attrs:
81
+ dset.attrs.update(self._attrs)
82
+ return dset
83
+
84
+ @property
85
+ def npoints_added(self) -> int:
86
+ return self._npoints_added
87
+
88
+ def add_point(
89
+ self,
90
+ data: ArrayLike,
91
+ index: int,
92
+ ) -> bool:
93
+ """Append one array to the dataset."""
94
+ if self._dataset is None:
95
+ self._dataset = self._create_dataset(numpy.asarray(data))
96
+
97
+ self._buffer.append((index, data))
98
+ self._npoints_added += 1
99
+ return self.flush_buffer()
100
+
101
+ def add_points(self, data: ArrayLike, indices: ArrayLike) -> bool:
102
+ """Append several arrays at once to the dataset."""
103
+ if self._dataset is None:
104
+ self._dataset = self._create_dataset(numpy.asarray(data[0]))
105
+
106
+ self._buffer.extend(zip(indices, data))
107
+ self._npoints_added += len(data)
108
+ return self.flush_buffer()
109
+
110
+ def flush_buffer(self, align: bool = False) -> bool:
111
+ if not self._buffer:
112
+ return False
113
+
114
+ if align:
115
+ logger.debug(
116
+ "Chunk-aligned flushing is only supported by DatasetWriter. "
117
+ "Only flush when `flush_period` has passed since the last flush."
118
+ )
119
+ if self._flush_time_expired():
120
+ return False
121
+
122
+ indices, values = zip(*self._buffer)
123
+ indices = numpy.asarray(indices)
124
+ values = numpy.asarray(values)
125
+
126
+ # Enlarge the dataset when needed
127
+ max_index = int(indices.max())
128
+ if max_index >= self._dataset.shape[0]:
129
+ self._dataset.resize(max_index + 1, axis=0)
130
+
131
+ # Copy data from buffer to HDF5
132
+ self._dataset[indices] = values
133
+
134
+ # Remove copied data from buffer
135
+ self._buffer.clear()
136
+
137
+ self._flush_hdf5()
138
+ self._last_flush = time.time()
139
+ return True
@@ -0,0 +1,158 @@
1
+ import logging
2
+ import time
3
+ from typing import List
4
+ from typing import Optional
5
+
6
+ import h5py
7
+ import numpy
8
+ from numpy.typing import ArrayLike
9
+
10
+ from ..config import guess_dataset_config
11
+ from ..types import StrictPositiveIntegral
12
+ from ._base import DatasetWriterBase
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class IndexedStackDatasetWriter(DatasetWriterBase):
18
+ """Append arrays of the same shape to each item of a new HDF5 dataset
19
+ in a potentially non-sequential manner per item. So each item of the HDF5 dataset is a
20
+ stack to which we can append data in a sequential manner.
21
+
22
+ Instead of creating a dataset with the :code:`h5py` API
23
+
24
+ .. code-block::python
25
+
26
+ stack0 = [[1,2,3], [4,5,6], [7,8,9]]
27
+ stack1 = [[10,11,12], [13,14,15], [16,17,18]]
28
+ h5group["mydataset"] = [stack0, stack1]
29
+
30
+ it can be done like this
31
+
32
+ .. code-block::python
33
+
34
+ with IndexedStackDatasetWriter(h5group, "mydataset") as writer:
35
+ writer.add_point([4,5,6], 0, 1)
36
+ writer.add_point([13,14,15], 1, 1)
37
+ writer.add_points([[10,11,12], [16,17,18]], [1, 1], [0, 2])
38
+ writer.add_points([[1,2,3], [7,8,9]], [0, 0], [0, 2])
39
+
40
+ Chunk size determination, compression and flushing is handled.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ parent: h5py.Group,
46
+ name: str,
47
+ npoints: Optional[StrictPositiveIntegral] = None,
48
+ nstack: Optional[StrictPositiveIntegral] = None,
49
+ attrs: Optional[dict] = None,
50
+ flush_period: Optional[float] = None,
51
+ ) -> None:
52
+ super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
53
+ self._npoints = npoints
54
+ self._nstack = nstack
55
+ self._buffer: List[tuple[int, int, ArrayLike]] = []
56
+
57
+ def _create_dataset(
58
+ self, first_data_point: ArrayLike, stack_index: int
59
+ ) -> h5py.Dataset:
60
+ scan_shape = (self._nstack, self._npoints)
61
+ first_data_point = numpy.asarray(first_data_point)
62
+ detector_shape = first_data_point.shape
63
+ dtype = first_data_point.dtype
64
+
65
+ if self._nstack is None or self._npoints is None:
66
+ shape = (1, 1) + detector_shape
67
+ max_shape = scan_shape + detector_shape
68
+ else:
69
+ shape = scan_shape + detector_shape
70
+ max_shape = None
71
+
72
+ options = guess_dataset_config(
73
+ scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
74
+ )
75
+ options.update(
76
+ shape=shape,
77
+ dtype=dtype,
78
+ fillvalue=numpy.nan, # converts to 0 for integers
79
+ )
80
+ if max_shape:
81
+ options["maxshape"] = max_shape
82
+
83
+ dset = self._parent.create_dataset(self._name, **options)
84
+ if self._attrs:
85
+ dset.attrs.update(self._attrs)
86
+ return dset
87
+
88
+ def add_point(self, data: ArrayLike, stack_index: int, point_index: int) -> bool:
89
+ """Append one array to one stack of the dataset."""
90
+ if self._dataset is None:
91
+ self._dataset = self._create_dataset(data, stack_index)
92
+
93
+ self._buffer.append((stack_index, point_index, data))
94
+ return self.flush_buffer()
95
+
96
+ def add_points(
97
+ self,
98
+ data: ArrayLike,
99
+ stack_indices: ArrayLike,
100
+ point_indices: ArrayLike,
101
+ ) -> bool:
102
+ """Append several arrays at once to one stack of the dataset."""
103
+ if not (len(stack_indices) == len(point_indices) == len(data)):
104
+ raise ValueError(
105
+ "stack_indices, point_indices, and data must have same length"
106
+ )
107
+
108
+ if self._dataset is None:
109
+ self._dataset = self._create_dataset(data[0], stack_indices[0])
110
+
111
+ self._buffer.extend(
112
+ zip(
113
+ map(int, stack_indices),
114
+ map(int, point_indices),
115
+ data,
116
+ )
117
+ )
118
+ return self.flush_buffer()
119
+
120
+ def flush_buffer(self, align: bool = False) -> bool:
121
+ if not self._buffer:
122
+ return False
123
+
124
+ if align:
125
+ logger.debug(
126
+ "Chunk-aligned flushing is only supported by StackDatasetWriter. "
127
+ "Only flush when `flush_period` has passed since the last flush."
128
+ )
129
+ if self._flush_time_expired():
130
+ return False
131
+
132
+ stack_indices, point_indices, values = zip(*self._buffer)
133
+ stack_indices = numpy.asarray(stack_indices)
134
+ point_indices = numpy.asarray(point_indices)
135
+ values = numpy.asarray(values)
136
+
137
+ # Enlarge the dataset when needed
138
+ max_stack = int(stack_indices.max())
139
+ max_point = int(point_indices.max())
140
+
141
+ shape0, shape1 = self._dataset.shape[:2]
142
+
143
+ if max_stack >= shape0:
144
+ self._dataset.resize(max_stack + 1, axis=0)
145
+
146
+ if max_point >= shape1:
147
+ self._dataset.resize(max_point + 1, axis=1)
148
+
149
+ # Copy data from buffer to HDF5
150
+ for i0, i1, value in zip(stack_indices, point_indices, values):
151
+ self._dataset[i0, i1] = value
152
+
153
+ # Remove copied data from buffer
154
+ self._buffer.clear()
155
+
156
+ self._flush_hdf5()
157
+ self._last_flush = time.time()
158
+ return True
@@ -0,0 +1,143 @@
1
+ import time
2
+ from typing import List
3
+ from typing import Optional
4
+
5
+ import h5py
6
+ import numpy
7
+ from numpy.typing import ArrayLike
8
+
9
+ from ..config import guess_dataset_config
10
+ from ..types import StrictPositiveIntegral
11
+ from ._base import DatasetWriterBase
12
+
13
+
14
+ class DatasetWriter(DatasetWriterBase):
15
+ """Append arrays of the same shape to a new HDF5 dataset in a sequential manner.
16
+
17
+ Instead of creating a dataset with the :code:`h5py` API
18
+
19
+ .. code-block::python
20
+
21
+ h5group["mydataset"] = [[1,2,3], [4,5,6], [7,8,9]]
22
+
23
+ it can be done like this
24
+
25
+ .. code-block::python
26
+
27
+ with DatasetWriter(h5group, "mydataset") as writer:
28
+ writer.add_point([1,2,3])
29
+ writer.add_points([[4,5,6], [7,8,9]])
30
+
31
+ Chunk size determination, chunk-aligned writing, compression and flushing is handled.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ parent: h5py.Group,
37
+ name: str,
38
+ npoints: Optional[StrictPositiveIntegral] = None,
39
+ attrs: Optional[dict] = None,
40
+ flush_period: Optional[float] = None,
41
+ overwrite: bool = False,
42
+ ) -> None:
43
+ super().__init__(parent, name, attrs=attrs, flush_period=flush_period)
44
+ self._npoints = npoints
45
+ self._overwrite = overwrite
46
+ self._chunked: bool = False
47
+ self._npoints_added: int = 0
48
+
49
+ self._buffer: List[ArrayLike] = list()
50
+ self._chunk_size: int = 0
51
+ self._flushed_size: int = 0
52
+
53
+ def _create_dataset(self, first_data_point: numpy.ndarray) -> h5py.Dataset:
54
+ scan_shape = (self._npoints,)
55
+ detector_shape = first_data_point.shape
56
+ dtype = first_data_point.dtype
57
+
58
+ if self._npoints is None:
59
+ shape = (1,) + detector_shape
60
+ max_shape = scan_shape + detector_shape
61
+ else:
62
+ shape = scan_shape + detector_shape
63
+ max_shape = None
64
+
65
+ options = guess_dataset_config(
66
+ scan_shape, detector_shape, dtype=dtype, max_shape=max_shape
67
+ )
68
+ options.update(
69
+ shape=shape,
70
+ dtype=dtype,
71
+ fillvalue=numpy.nan, # converts to 0 for integers
72
+ )
73
+
74
+ if max_shape:
75
+ options["maxshape"] = max_shape
76
+
77
+ if options["chunks"]:
78
+ self._chunked = True
79
+ self._chunk_size = options["chunks"][0]
80
+
81
+ if self._overwrite and self._name in self._parent:
82
+ del self._parent[self._name]
83
+
84
+ dset = self._parent.create_dataset(self._name, **options)
85
+ if self._attrs:
86
+ dset.attrs.update(self._attrs)
87
+ return dset
88
+
89
+ @property
90
+ def npoints_added(self) -> int:
91
+ return self._npoints_added
92
+
93
+ def add_point(self, data: ArrayLike) -> bool:
94
+ """Append one array to the dataset."""
95
+ if self._dataset is None:
96
+ self._dataset = self._create_dataset(data)
97
+
98
+ self._buffer.append(data)
99
+ self._npoints_added += 1
100
+ return self.flush_buffer(align=True)
101
+
102
+ def add_points(self, data: ArrayLike) -> bool:
103
+ """Append several arrays at once to the dataset."""
104
+ if self._dataset is None:
105
+ self._dataset = self._create_dataset(data[0])
106
+
107
+ self._buffer.extend(data)
108
+ self._npoints_added += len(data)
109
+ return self.flush_buffer(align=True)
110
+
111
+ def flush_buffer(self, align: bool = False) -> bool:
112
+ # Determine how many points to flush
113
+ chunk_size = len(self._buffer)
114
+
115
+ if self._flush_time_expired():
116
+ flush_size = chunk_size
117
+ elif align and self._chunked:
118
+ n = chunk_size + (self._flushed_size % self._chunk_size)
119
+ flush_size = n // self._chunk_size * self._chunk_size
120
+ flush_size = min(flush_size, chunk_size)
121
+ else:
122
+ flush_size = chunk_size
123
+
124
+ if flush_size == 0:
125
+ return False
126
+
127
+ # Enlarge the dataset when needed
128
+ nalloc = self._dataset.shape[0]
129
+ istart = self._flushed_size
130
+ flushed_size = istart + flush_size
131
+ if self._chunked and flushed_size > nalloc:
132
+ self._dataset.resize(flushed_size, axis=0)
133
+
134
+ # Copy data from buffer to HDF5
135
+ self._dataset[istart : istart + flush_size] = self._buffer[:flush_size]
136
+
137
+ # Remove copied data from buffer
138
+ self._buffer = self._buffer[flush_size:]
139
+ self._flushed_size = flushed_size
140
+
141
+ self._flush_hdf5()
142
+ self._last_flush = time.time()
143
+ return True
@@ -6,184 +6,12 @@ import h5py
6
6
  import numpy
7
7
  from numpy.typing import ArrayLike
8
8
 
9
- from .config import guess_dataset_config
10
- from .types import StrictPositiveIntegral
9
+ from ..config import guess_dataset_config
10
+ from ..types import StrictPositiveIntegral
11
+ from ._base import DatasetWriterBase
11
12
 
12
13
 
13
- class _DatasetWriterBase:
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: numpy.ndarray, stack_index: int
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
- if self._npoints is None or self._nstack is None:
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["shape"] = shape
247
- options["dtype"] = dtype
248
- options["fillvalue"] = numpy.nan # converts to 0 for integers
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
+ )
@@ -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(tmpdir, npoints, flush_period, known_npoints):
16
+ def test_dataset_writer(tmp_path, npoints, flush_period, known_npoints):
17
17
  expected = list()
18
- filename = str(tmpdir / "test.h5")
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
- tmpdir,
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(tmpdir / "test.h5")
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
- def test_concurrent_reader(tmpdir):
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(tmpdir / "test.h5")
98
- read_timestamps = str(tmpdir / "read_timestamps.log")
99
- write_timestamps = str(tmpdir / "write_timestamps.log")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ewoksdata
3
- Version: 2.0.3
3
+ Version: 2.1.0
4
4
  Summary: Ewoks tasks for data access
5
5
  Author-email: ESRF <dau-pydev@esrf.fr>
6
6
  License: # MIT License
@@ -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