Skip to content

Commit d3694f8

Browse files
committed
added ChunkIterator
1 parent 1d34b7c commit d3694f8

6 files changed

Lines changed: 250 additions & 968 deletions

File tree

src/h5json/h5reader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def getDatasetValues(self, obj_id, sel=None, dtype=None):
8181
"""
8282
pass
8383

84-
def queryDataset(self, obj_id, query, sel=None):
84+
def queryDataset(self, obj_id, query, sel=None, limit=0):
8585
"""
8686
Query the given dataset using the selection and query expression
8787

src/h5json/hdf5db.py

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .hdf5dtype import numpy_integer_types, numpy_float_types
1717
from .array_util import jsonToArray, bytesArrayToList
1818
from .query_util import arrayQuery
19-
from .dset_util import resize_dataset, getDatasetLayoutClass
19+
from .dset_util import resize_dataset, getDatasetLayoutClass, getChunkDims
2020
from .shape_util import getShapeClass, getShapeDims, getShapeJson
2121
from .filters import validateFilters
2222
from .objid import createObjId, getCollectionForId, isValidUuid, getUuidFromId, getHashTagForId
@@ -84,6 +84,107 @@ def _decode(item, encoding="ascii"):
8484
return ret_val
8585

8686

87+
class ChunkIterator:
88+
"""
89+
Iterate through the chunks of a dataset, yielding the chunk's data as an
90+
ndarray on each iteration. This lets a caller read through a large,
91+
chunked dataset one chunk at a time without loading the whole dataset
92+
into memory.
93+
94+
Modeled on h5py's chunk iterator (h5py.Dataset.iter_chunks() /
95+
h5py._hl.dataset.ChunkIterator), but each chunk's data is fetched via
96+
Hdf5db.getDatasetValues() rather than by slicing an h5py.Dataset, so it
97+
works uniformly across storage backends and picks up any not-yet-flushed
98+
in-memory updates.
99+
100+
Use Hdf5db.getChunkIterator() rather than constructing this directly.
101+
"""
102+
103+
def __init__(self, db, dset_id, sel=None):
104+
dset_json = db.getObjectById(dset_id)
105+
shape_json = dset_json["shape"]
106+
dims = getShapeDims(shape_json)
107+
rank = len(dims)
108+
if rank == 0:
109+
raise ValueError("ChunkIterator can't be used with scalar datasets")
110+
111+
if sel is None:
112+
sel = selections.select(dims, ...)
113+
if not isinstance(sel, selections.Selection):
114+
raise TypeError("Expected Selection class")
115+
if sel.shape != dims:
116+
raise TypeError("Selection shape does not match dataset shape")
117+
if sel.select_type not in (selections.H5S_SEL_ALL, selections.H5S_SEL_HYPERSLABS):
118+
raise ValueError("ChunkIterator only supports hyperslab selections")
119+
120+
self._db = db
121+
self._dset_id = dset_id
122+
self._shape = dims
123+
self._layout = getChunkDims(dset_json)
124+
125+
sel_slices = []
126+
for s in sel.slices:
127+
if s.step not in (None, 1):
128+
raise ValueError("ChunkIterator does not support stepped selections")
129+
sel_slices.append(slice(s.start, s.stop, 1))
130+
self._sel = tuple(sel_slices)
131+
132+
self._chunk_index = []
133+
for dim in range(rank):
134+
s = self._sel[dim]
135+
if s.start < 0 or s.stop > self._shape[dim] or s.stop <= s.start:
136+
raise ValueError("Invalid selection - selection region must be within dataset space")
137+
self._chunk_index.append(s.start // self._layout[dim])
138+
139+
self._current_sel = None
140+
141+
@property
142+
def sel(self):
143+
""" Selection (within the full dataset) of the chunk most recently returned by __next__ """
144+
return self._current_sel
145+
146+
def __iter__(self):
147+
return self
148+
149+
def __next__(self):
150+
rank = len(self._shape)
151+
if self._chunk_index[0] * self._layout[0] >= self._sel[0].stop:
152+
# ran past the last chunk, end iteration
153+
raise StopIteration()
154+
155+
slices = []
156+
for dim in range(rank):
157+
s = self._sel[dim]
158+
start = self._chunk_index[dim] * self._layout[dim]
159+
stop = (self._chunk_index[dim] + 1) * self._layout[dim]
160+
# adjust the start if this is an edge chunk
161+
if start < s.start:
162+
start = s.start
163+
if stop > s.stop:
164+
stop = s.stop # trim to end of the selection
165+
slices.append(slice(start, stop, 1))
166+
slices = tuple(slices)
167+
168+
# bump up the last index and carry forward if we run outside the selection
169+
dim = rank - 1
170+
while dim >= 0:
171+
s = self._sel[dim]
172+
self._chunk_index[dim] += 1
173+
174+
chunk_end = self._chunk_index[dim] * self._layout[dim]
175+
if chunk_end < s.stop:
176+
# we still have room to extend along this dimension
177+
break
178+
179+
if dim > 0:
180+
# reset to the start and continue iterating with higher dimension
181+
self._chunk_index[dim] = s.start // self._layout[dim]
182+
dim -= 1
183+
184+
self._current_sel = selections.select(self._shape, slices)
185+
return self._db.getDatasetValues(self._dset_id, self._current_sel)
186+
187+
87188
class Hdf5db:
88189
"""
89190
This class is used to manage id lookup tables for primary HDF objects (Groups, Datasets,
@@ -921,6 +1022,16 @@ def init_arr(rdtype, cpl):
9211022

9221023
return arr
9231024

1025+
def getChunkIterator(self, dset_id, sel=None):
1026+
"""
1027+
Return a ChunkIterator that reads through the given dataset's values
1028+
chunk by chunk, without loading the entire dataset into memory.
1029+
If sel is provided, only chunks intersecting that selection are
1030+
iterated over (each still trimmed to the selection's bounds),
1031+
otherwise the entire dataset is iterated over.
1032+
"""
1033+
return ChunkIterator(self, dset_id, sel=sel)
1034+
9241035
def queryDataset(self, dset_id, query, sel=None, limit=0):
9251036
"""
9261037
Query the given dataset using the selection and query expression
@@ -941,12 +1052,33 @@ def queryReader(dset_id, query, sel=None, limit=0):
9411052
if result is not None:
9421053
return result
9431054

944-
# query the dataset by fetching the data and applying the query locally
945-
arr = self.getDatasetValues(dset_id, sel)
946-
947-
result = arrayQuery(query, arr, limit=limit)
948-
result = _query_rel_to_abs(sel, result, len(sel.shape))
1055+
rank = len(sel.shape)
1056+
try:
1057+
chunk_iter = ChunkIterator(self, dset_id, sel=sel)
1058+
except ValueError:
1059+
# ChunkIterator doesn't support this selection (e.g. a fancy/point
1060+
# selection, or a scalar dataset) - fall back to querying the
1061+
# entire selection at once
1062+
arr = self.getDatasetValues(dset_id, sel)
1063+
result = arrayQuery(query, arr, limit=limit)
1064+
return _query_rel_to_abs(sel, result, rank)
1065+
1066+
# query the dataset chunk by chunk so the whole selection is never
1067+
# loaded into memory at once
1068+
hits = []
1069+
nhits = 0
1070+
for chunk_arr in chunk_iter:
1071+
chunk_rel = arrayQuery(query, chunk_arr)
1072+
if len(chunk_rel) == 0:
1073+
continue
1074+
hits.append(_query_rel_to_abs(chunk_iter.sel, chunk_rel, rank))
1075+
nhits += len(chunk_rel)
1076+
if limit > 0 and nhits >= limit:
1077+
break
9491078

1079+
result = np.concatenate(hits, axis=0) if hits else np.zeros((0, rank), dtype='u8')
1080+
if limit > 0 and len(result) > limit:
1081+
result = result[:limit]
9501082
return result
9511083
#
9521084
# start of queryDataset

test/unit/h5py_reader_test.py

Lines changed: 0 additions & 124 deletions
This file was deleted.

0 commit comments

Comments
 (0)