ewoksdata 2.2.1__tar.gz → 2.3.0rc1__tar.gz

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