1616from .hdf5dtype import numpy_integer_types , numpy_float_types
1717from .array_util import jsonToArray , bytesArrayToList
1818from .query_util import arrayQuery
19- from .dset_util import resize_dataset , getDatasetLayoutClass
19+ from .dset_util import resize_dataset , getDatasetLayoutClass , getChunkDims
2020from .shape_util import getShapeClass , getShapeDims , getShapeJson
2121from .filters import validateFilters
2222from .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+
87188class 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
0 commit comments