ruby-hdf5 0.0.2 → 0.0.3

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.
data/lib/hdf5/dataset.rb CHANGED
@@ -1,72 +1,44 @@
1
1
  module HDF5
2
2
  class Dataset
3
- module DataHelpers
4
- module_function
5
-
6
- def normalize_data(data)
7
- values = data.is_a?(Array) ? data : [data]
8
- raise HDF5::Error, 'Dataset data must not be empty' if values.empty?
9
- raise HDF5::Error, 'Nested arrays are not supported' if values.any? { |value| value.is_a?(Array) }
10
-
11
- values
12
- end
13
-
14
- def datatype_id_for(data)
15
- if data.all? { |value| value.is_a?(Integer) }
16
- validate_native_int_range!(data)
17
- HDF5::FFI.H5T_NATIVE_INT
18
- elsif data.all? { |value| value.is_a?(Numeric) }
19
- HDF5::FFI.H5T_NATIVE_DOUBLE
20
- else
21
- raise HDF5::Error, 'Only numeric dataset data is supported'
3
+ class << self
4
+ def create(parent_id, name, data = nil, shape: nil, dtype: nil, maxshape: nil, chunks: nil, compression: nil,
5
+ compression_opts: nil, shuffle: false, fletcher32: false, fillvalue: nil, context: nil)
6
+ empty_data = data.is_a?(HDF5::Empty)
7
+ string_data = HDF5::StringCodec.string_data?(data)
8
+ _string_values, string_shape = HDF5::StringCodec.normalize_data(data) if string_data
9
+ unless data.nil? || string_data || empty_data
10
+ narray = HDF5::DataHelpers.normalize_data(data,
11
+ label: 'Dataset data')
22
12
  end
23
- end
24
-
25
- def buffer_for(data)
26
- if data.all? { |value| value.is_a?(Integer) }
27
- buffer = ::FFI::MemoryPointer.new(:int, data.length)
28
- buffer.write_array_of_int(data)
29
- else
30
- buffer = ::FFI::MemoryPointer.new(:double, data.length)
31
- buffer.write_array_of_double(data.map(&:to_f))
13
+ unless string_data
14
+ dtype_object = if empty_data
15
+ data.dtype
16
+ else
17
+ (dtype ? DType.for_symbol(dtype) : DType.for_numo(narray))
18
+ end
32
19
  end
20
+ type_id = string_data ? HDF5::StringCodec.datatype_id : dtype_object.storage_type_id
21
+ shape = string_data ? string_shape : narray.shape if shape.nil? && !data.nil? && !empty_data
22
+ raise HDF5::Error, 'shape: and dtype: are required when data: is omitted' if data.nil? && (!shape || !dtype)
23
+ raise HDF5::Error, 'Dataset shape must match data shape' if narray && shape != narray.shape
24
+ raise HDF5::ShapeError, 'Dataset shape must match string data shape' if string_data && shape != string_shape
33
25
 
34
- buffer
35
- end
36
-
37
- def native_int_bounds
38
- bits = ::FFI.type_size(:int) * 8
39
- max = (1 << (bits - 1)) - 1
40
- min = -(1 << (bits - 1))
41
- [min, max]
42
- end
43
-
44
- def validate_native_int_range!(values)
45
- min, max = native_int_bounds
46
- out_of_range = values.find { |value| value < min || value > max }
47
- return unless out_of_range
26
+ raise HDF5::Error, 'Null datasets cannot have maxshape or chunks' if empty_data && (maxshape || chunks)
48
27
 
49
- raise HDF5::Error,
50
- "Integer value #{out_of_range} is outside native int range (#{min}..#{max}). Use a smaller value."
51
- end
52
- end
53
-
54
- private_constant :DataHelpers
55
-
56
- class << self
57
- def create(parent_id, name, data)
58
- values = DataHelpers.normalize_data(data)
59
- dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
60
- dims.write_array_of_ulong_long([values.length])
61
- datatype_id = DataHelpers.datatype_id_for(values)
62
- dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
28
+ validate_maxshape(maxshape, shape) if maxshape
29
+ chunks = :auto if maxshape && chunks.nil?
30
+ dataspace_id = create_dataspace(shape, maxshape)
63
31
  raise HDF5::Error, "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
64
32
 
33
+ dcpl_id = create_property_list(shape, dtype_object, chunks:, compression:, compression_opts:, shuffle:, fletcher32:,
34
+ fillvalue:)
35
+
65
36
  dataset = from_id(
66
- HDF5::FFI.H5Dcreate2(parent_id, name, datatype_id, dataspace_id, HDF5::DEFAULT_PROPERTY_LIST,
67
- HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST), name
37
+ HDF5::FFI.H5Dcreate2(parent_id, name, type_id, dataspace_id, HDF5::DEFAULT_PROPERTY_LIST,
38
+ dcpl_id || HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST), name, context
68
39
  )
69
- dataset.write(values)
40
+ dataset.write(data) if string_data
41
+ dataset.write(narray) if narray
70
42
  return dataset unless block_given?
71
43
 
72
44
  begin
@@ -74,12 +46,20 @@ module HDF5
74
46
  ensure
75
47
  dataset.close
76
48
  end
49
+ rescue StandardError
50
+ if dataset
51
+ dataset.close unless dataset.closed?
52
+ HDF5::FFI.H5Ldelete(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST)
53
+ end
54
+ raise
77
55
  ensure
56
+ HDF5::FFI.H5Tclose(type_id) if string_data && type_id && type_id >= 0
57
+ HDF5::FFI.H5Pclose(dcpl_id) if dcpl_id && dcpl_id >= 0
78
58
  HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
79
59
  end
80
60
 
81
- def open(parent_id, name)
82
- dataset = from_id(HDF5::FFI.H5Dopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
61
+ def open(parent_id, name, context: nil)
62
+ dataset = from_id(HDF5::FFI.H5Dopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name, context)
83
63
  return dataset unless block_given?
84
64
 
85
65
  begin
@@ -91,51 +71,223 @@ module HDF5
91
71
 
92
72
  private
93
73
 
94
- def from_id(dataset_id, name)
74
+ def create_dataspace(shape, maxshape = nil)
75
+ return HDF5::FFI.H5Screate(:H5S_NULL) if shape.nil?
76
+ return HDF5::FFI.H5Screate(:H5S_SCALAR) if shape.empty?
77
+
78
+ dims = ::FFI::MemoryPointer.new(:ulong_long, shape.length)
79
+ dims.write_array_of_ulong_long(shape)
80
+ maxdims = if maxshape
81
+ ::FFI::MemoryPointer.new(:ulong_long, maxshape.length).tap do |pointer|
82
+ pointer.write_array_of_ulong_long(maxshape.map do |dimension|
83
+ dimension.nil? ? unlimited_dimension : dimension
84
+ end)
85
+ end
86
+ end
87
+ HDF5::FFI.H5Screate_simple(shape.length, dims, maxdims)
88
+ end
89
+
90
+ def validate_maxshape(maxshape, shape)
91
+ unless maxshape.is_a?(Array) && maxshape.length == shape.length
92
+ raise HDF5::Error,
93
+ 'maxshape must be an Array matching dataset rank'
94
+ end
95
+
96
+ valid = maxshape.zip(shape).all? do |maximum, dimension|
97
+ maximum.nil? || maximum.is_a?(Integer) && maximum >= dimension
98
+ end
99
+ raise HDF5::Error, 'maxshape dimensions must be nil or integers no smaller than shape' unless valid
100
+ end
101
+
102
+ def unlimited_dimension
103
+ (1 << (::FFI.type_size(:ulong_long) * 8)) - 1
104
+ end
105
+
106
+ def create_property_list(shape, dtype_object, chunks:, compression:, compression_opts:, shuffle:, fletcher32:,
107
+ fillvalue:)
108
+ chunked = chunks || compression || compression_opts || shuffle || fletcher32
109
+ return unless chunked || !fillvalue.nil?
110
+ raise HDF5::Error, 'Chunked storage is not supported for scalar datasets' if chunked && shape.empty?
111
+ if dtype_object.nil? && !fillvalue.nil?
112
+ raise UnsupportedFeatureError,
113
+ 'fillvalue is not supported for string datasets'
114
+ end
115
+ raise HDF5::Error, 'Unsupported compression' unless compression.nil? || compression == :gzip
116
+ raise HDF5::Error, 'compression_opts requires compression: :gzip' if compression_opts && compression != :gzip
117
+
118
+ itemsize = dtype_object ? dtype_object.itemsize : ::FFI.type_size(:pointer)
119
+ if chunked
120
+ chunk_shape = if chunks == :auto || chunks.nil?
121
+ auto_chunk_shape(shape,
122
+ itemsize)
123
+ else
124
+ validate_chunk_shape(chunks,
125
+ shape)
126
+ end
127
+ end
128
+ compression_level = compression_opts || 4
129
+ unless compression.nil? || compression_level.between?(
130
+ 0, 9
131
+ )
132
+ raise HDF5::Error,
133
+ 'gzip compression_opts must be between 0 and 9'
134
+ end
135
+
136
+ validate_filter_available(1, 'gzip', capability: 1) if compression == :gzip
137
+ validate_filter_available(2, 'shuffle', capability: 1) if shuffle
138
+ validate_filter_available(3, 'Fletcher32', capability: 1) if fletcher32
139
+
140
+ dcpl_id = HDF5::FFI.H5Pcreate(HDF5::FFI.H5P_CLS_DATASET_CREATE_ID_g)
141
+ raise HDF5::Error, 'Failed to create dataset property list' if dcpl_id < 0
142
+
143
+ if chunked
144
+ dims = ::FFI::MemoryPointer.new(:ulong_long, chunk_shape.length)
145
+ dims.write_array_of_ulong_long(chunk_shape)
146
+ check_property_status(HDF5::FFI.H5Pset_chunk(dcpl_id, chunk_shape.length, dims), 'set chunk dimensions')
147
+ end
148
+ check_property_status(HDF5::FFI.H5Pset_shuffle(dcpl_id), 'enable shuffle') if shuffle
149
+ if compression == :gzip
150
+ check_property_status(HDF5::FFI.H5Pset_deflate(dcpl_id, compression_level),
151
+ 'enable gzip')
152
+ end
153
+ check_property_status(HDF5::FFI.H5Pset_fletcher32(dcpl_id), 'enable Fletcher32') if fletcher32
154
+ unless fillvalue.nil?
155
+ value = dtype_object.numo_class.cast(fillvalue)
156
+ raise HDF5::Error, 'fillvalue must be scalar' unless value.shape.empty?
157
+
158
+ check_property_status(HDF5::FFI.H5Pset_fill_value(dcpl_id, dtype_object.memory_type_id, HDF5::DataHelpers.buffer_for(value)),
159
+ 'set fill value')
160
+ end
161
+ dcpl_id
162
+ rescue StandardError
163
+ HDF5::FFI.H5Pclose(dcpl_id) if dcpl_id && dcpl_id >= 0
164
+ raise
165
+ end
166
+
167
+ def auto_chunk_shape(shape, itemsize)
168
+ target_bytes = 256 * 1024
169
+ chunk_shape = shape.map { |dimension| [dimension, 1].max }
170
+
171
+ while chunk_shape.inject(itemsize, :*) > target_bytes
172
+ axis = chunk_shape.each_index.max_by { |index| chunk_shape[index] }
173
+ chunk_shape[axis] = (chunk_shape[axis] / 2.0).ceil
174
+ end
175
+
176
+ chunk_shape
177
+ end
178
+
179
+ def validate_chunk_shape(chunks, shape)
180
+ unless chunks.is_a?(Array) && chunks.length == shape.length
181
+ raise HDF5::Error,
182
+ 'chunks must be an Array matching dataset rank'
183
+ end
184
+ unless chunks.all? { |dimension| dimension.is_a?(Integer) && dimension.positive? }
185
+ raise HDF5::Error, 'chunk dimensions must be positive integers'
186
+ end
187
+
188
+ chunks
189
+ end
190
+
191
+ def check_property_status(status, operation)
192
+ raise HDF5::Error, "Failed to #{operation}" if status < 0
193
+ end
194
+
195
+ def validate_filter_available(filter_id, name, capability:)
196
+ unless HDF5::FFI.H5Zfilter_avail(filter_id).positive?
197
+ raise UnsupportedFeatureError, "HDF5 #{name} filter is unavailable"
198
+ end
199
+
200
+ flags = ::FFI::MemoryPointer.new(:uint)
201
+ status = HDF5::FFI.H5Zget_filter_info(filter_id, flags)
202
+ raise HDF5::Error, "Failed to inspect HDF5 #{name} filter" if status < 0
203
+ raise UnsupportedFeatureError, "HDF5 #{name} filter cannot encode data" if (flags.read_uint & capability).zero?
204
+ end
205
+
206
+ def from_id(dataset_id, name, context)
95
207
  dataset = allocate
96
- dataset.send(:initialize_from_id, dataset_id, name)
208
+ dataset.send(:initialize_from_id, dataset_id, name, context)
97
209
  dataset
98
210
  end
99
211
  end
100
212
 
101
213
  def initialize(parent_id, name)
102
- initialize_from_id(HDF5::FFI.H5Dopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
214
+ initialize_from_id(HDF5::FFI.H5Dopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name, nil)
103
215
  end
104
216
 
105
217
  def attrs
106
- @attrs ||= AttributeManager.new(@dataset_id)
218
+ ensure_open!
219
+ @attrs ||= AttributeManager.new(@dataset_id, @context)
107
220
  end
108
221
 
109
- def write(data)
110
- values = DataHelpers.normalize_data(data)
111
- mem_type_id = DataHelpers.datatype_id_for(values)
112
- buffer = DataHelpers.buffer_for(values)
113
- status = HDF5::FFI.H5Dwrite(@dataset_id, mem_type_id, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST,
222
+ def write(data, selection: nil, casting: :safe)
223
+ ensure_open!
224
+ return write_string(data, selection:) if HDF5::StringCodec.string_data?(data)
225
+
226
+ normalized_selection = Selection.normalize(selection, shape)
227
+ values = if data.is_a?(Numeric)
228
+ target_dtype = dtype
229
+ if normalized_selection.scalar?
230
+ target_dtype.numo_class.cast(data)
231
+ else
232
+ target_dtype.numo_class.ones(*normalized_selection.result_shape) * data
233
+ end
234
+ else
235
+ HDF5::DataHelpers.normalize_data(data, label: 'Dataset data')
236
+ end
237
+ raise HDF5::Error, 'Dataset shape must match data shape' unless values.shape == normalized_selection.result_shape
238
+
239
+ dtype_object = DType.for_numo(values)
240
+ target_dtype = dtype
241
+ raise ConversionError, "Cannot safely cast #{dtype_object.to_sym} to #{target_dtype.to_sym}" unless
242
+ dtype_object.castable_to?(target_dtype, casting:)
243
+ return data if normalized_selection.size.zero?
244
+
245
+ buffer = HDF5::DataHelpers.buffer_for(values)
246
+ file_space_id = HDF5::FFI.H5Dget_space(@dataset_id)
247
+ raise HDF5::Error, 'Failed to get dataset dataspace' if file_space_id < 0
248
+
249
+ select_hyperslab(file_space_id, normalized_selection)
250
+ memory_space_id = create_memory_dataspace(normalized_selection.result_shape)
251
+ raise HDF5::Error, 'Failed to create memory dataspace' if memory_space_id < 0
252
+ raise HDF5::Error, 'File and memory selections have different sizes' unless
253
+ HDF5::FFI.H5Sget_select_npoints(file_space_id) == HDF5::FFI.H5Sget_select_npoints(memory_space_id)
254
+
255
+ status = HDF5::FFI.H5Dwrite(@dataset_id, dtype_object.memory_type_id, memory_space_id, file_space_id,
114
256
  HDF5::DEFAULT_PROPERTY_LIST, buffer)
115
257
  raise HDF5::Error, 'Failed to write dataset' if status < 0
116
258
 
117
259
  data
260
+ ensure
261
+ HDF5::FFI.H5Sclose(memory_space_id) if memory_space_id && memory_space_id >= 0
262
+ HDF5::FFI.H5Sclose(file_space_id) if file_space_id && file_space_id >= 0
118
263
  end
119
264
 
120
265
  def close
121
266
  return if @dataset_id.nil?
122
267
 
123
- HDF5::FFI.H5Dclose(@dataset_id)
268
+ @context ? @context.close(@dataset_id) : HDF5::FFI.H5Dclose(@dataset_id)
124
269
  @dataset_id = nil
125
270
  end
126
271
 
272
+ def closed?
273
+ @dataset_id.nil? || (@context && @context.closed?)
274
+ end
275
+
127
276
  def dtype
277
+ ensure_open!
128
278
  datatype_id = HDF5::FFI.H5Dget_type(@dataset_id)
129
279
  raise HDF5::Error, 'Failed to get datatype' if datatype_id < 0
130
280
 
131
- HDF5::FFI.H5Tget_class(datatype_id)
281
+ DType.for_hdf5(datatype_id)
132
282
  ensure
133
283
  HDF5::FFI.H5Tclose(datatype_id) if datatype_id && datatype_id >= 0
134
284
  end
135
285
 
136
286
  def shape
287
+ ensure_open!
137
288
  dataspace_id = HDF5::FFI.H5Dget_space(@dataset_id)
138
289
  raise HDF5::Error, 'Failed to get dataspace' if dataspace_id < 0
290
+ return nil if HDF5::FFI.H5Sget_simple_extent_type(dataspace_id) == :H5S_NULL
139
291
 
140
292
  ndims = HDF5::FFI.H5Sget_simple_extent_ndims(dataspace_id)
141
293
  raise HDF5::Error, 'Failed to get number of dimensions' if ndims < 0
@@ -148,52 +300,360 @@ module HDF5
148
300
  HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
149
301
  end
150
302
 
151
- def read
152
- current_dtype = dtype
303
+ def ndim
304
+ shape&.length
305
+ end
306
+
307
+ def size
308
+ shape&.inject(1, :*) || 0
309
+ end
310
+
311
+ def chunks
312
+ ensure_open!
313
+ property_list_id = HDF5::FFI.H5Dget_create_plist(@dataset_id)
314
+ raise HDF5::Error, 'Failed to get dataset creation properties' if property_list_id < 0
315
+ return nil unless HDF5::FFI.H5Pget_layout(property_list_id) == :H5D_CHUNKED
316
+
317
+ dimensions = ::FFI::MemoryPointer.new(:ulong_long, shape.length)
318
+ rank = HDF5::FFI.H5Pget_chunk(property_list_id, shape.length, dimensions)
319
+ raise HDF5::Error, 'Failed to get chunk dimensions' if rank < 0
320
+
321
+ dimensions.read_array_of_uint64(rank)
322
+ ensure
323
+ HDF5::FFI.H5Pclose(property_list_id) if property_list_id && property_list_id >= 0
324
+ end
325
+
326
+ def maxshape
327
+ ensure_open!
328
+ dataspace_id = HDF5::FFI.H5Dget_space(@dataset_id)
329
+ raise HDF5::Error, 'Failed to get dataset dataspace' if dataspace_id < 0
330
+
331
+ rank = HDF5::FFI.H5Sget_simple_extent_ndims(dataspace_id)
332
+ return [] if rank.zero?
333
+
334
+ maximums = ::FFI::MemoryPointer.new(:ulong_long, rank)
335
+ status = HDF5::FFI.H5Sget_simple_extent_dims(dataspace_id, nil, maximums)
336
+ raise HDF5::Error, 'Failed to get dataset maximum shape' if status < 0
337
+
338
+ unlimited = (1 << (::FFI.type_size(:ulong_long) * 8)) - 1
339
+ maximums.read_array_of_uint64(rank).map { |dimension| dimension == unlimited ? nil : dimension }
340
+ ensure
341
+ HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
342
+ end
343
+
344
+ def fillvalue
345
+ ensure_open!
346
+ dtype_object = dtype
347
+ property_list_id = HDF5::FFI.H5Dget_create_plist(@dataset_id)
348
+ raise HDF5::Error, 'Failed to get dataset creation properties' if property_list_id < 0
349
+
350
+ buffer = ::FFI::MemoryPointer.new(:char, dtype_object.itemsize)
351
+ status = HDF5::FFI.H5Pget_fill_value(property_list_id, dtype_object.memory_type_id, buffer)
352
+ raise HDF5::Error, 'Failed to get dataset fill value' if status < 0
353
+
354
+ dtype_object.numo_class.from_binary(buffer.read_bytes(dtype_object.itemsize), []).extract
355
+ ensure
356
+ HDF5::FFI.H5Pclose(property_list_id) if property_list_id && property_list_id >= 0
357
+ end
358
+
359
+ def resize(new_shape)
360
+ ensure_open!
361
+ raise HDF5::Error, 'Cannot resize a Null dataset' if shape.nil?
362
+ unless new_shape.is_a?(Array) && new_shape.length == shape.length
363
+ raise HDF5::Error,
364
+ 'Dataset shape must be an Array matching dataset rank'
365
+ end
366
+ unless new_shape.all? { |dimension| dimension.is_a?(Integer) && dimension >= 0 }
367
+ raise HDF5::Error, 'Dataset dimensions must be non-negative integers'
368
+ end
369
+
370
+ maxshape.zip(new_shape).each do |maximum, dimension|
371
+ raise HDF5::Error, 'Dataset shape exceeds maxshape' if maximum && dimension > maximum
372
+ end
373
+
374
+ dimensions = ::FFI::MemoryPointer.new(:ulong_long, new_shape.length)
375
+ dimensions.write_array_of_ulong_long(new_shape)
376
+ status = HDF5::FFI.H5Dset_extent(@dataset_id, dimensions)
377
+ raise HDF5::Error, 'Failed to resize dataset' if status < 0
378
+
379
+ self
380
+ end
381
+
382
+ def append(data, axis: 0)
383
+ ensure_open!
384
+ values = HDF5::DataHelpers.normalize_data(data, label: 'Dataset data')
153
385
  current_shape = shape
386
+ raise HDF5::Error, 'Cannot append to a Null dataset' if current_shape.nil?
387
+ raise HDF5::Error, 'Cannot append to a scalar dataset' if current_shape.empty?
388
+ raise IndexError, "Invalid append axis: #{axis}" unless axis.is_a?(Integer) && axis.between?(0,
389
+ current_shape.length - 1)
390
+ raise HDF5::Error, 'Appended data rank must match dataset rank' unless values.shape.length == current_shape.length
391
+ raise HDF5::Error, 'Appended data shape must match all non-appended dimensions' unless
392
+ values.shape.each_with_index.all? { |dimension, index| index == axis || dimension == current_shape[index] }
393
+
394
+ source_dtype = DType.for_numo(values)
395
+ target_dtype = dtype
396
+ raise ConversionError, "Cannot safely cast #{source_dtype.to_sym} to #{target_dtype.to_sym}" unless
397
+ source_dtype.castable_to?(target_dtype)
398
+ return self if values.shape[axis].zero?
399
+
400
+ new_shape = current_shape.dup
401
+ new_shape[axis] += values.shape[axis]
402
+ resize(new_shape)
403
+ selection = current_shape.each_with_index.map do |dimension, index|
404
+ index == axis ? dimension...new_shape[index] : 0...dimension
405
+ end
406
+ write(values, selection: selection)
407
+ self
408
+ rescue StandardError => e
409
+ raise unless current_shape && new_shape && shape == new_shape
410
+
411
+ begin
412
+ resize(current_shape)
413
+ rescue StandardError => rollback_error
414
+ raise HDF5::Error,
415
+ "Append failed (#{e.message}) and extent rollback failed (#{rollback_error.message}); current shape: #{shape.inspect}"
416
+ end
417
+ raise e
418
+ end
154
419
 
155
- total_elements = current_shape.inject(:*)
156
- case current_dtype
157
- when :H5T_INTEGER
158
- read_integer_data(total_elements)
159
- when :H5T_FLOAT
160
- read_float_data(total_elements)
161
- when :H5T_STRING
162
- read_string_data(total_elements)
163
- else
164
- raise HDF5::Error, 'Unsupported datatype'
420
+ def read(selection: nil, dtype: nil, casting: :safe)
421
+ ensure_open!
422
+ type_id = HDF5::FFI.H5Dget_type(@dataset_id)
423
+ raise HDF5::Error, 'Failed to get dataset datatype' if type_id < 0
424
+ if dtype && HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
425
+ raise ConversionError,
426
+ 'dtype is not supported for string datasets'
165
427
  end
428
+ return read_string(type_id, selection:) if HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
429
+
430
+ source_dtype = DType.for_hdf5(type_id)
431
+ current_dtype = dtype ? DType.for_symbol(dtype) : source_dtype
432
+ raise ConversionError, "Cannot safely cast #{source_dtype.to_sym} to #{current_dtype.to_sym}" unless
433
+ source_dtype.castable_to?(current_dtype, casting:)
434
+
435
+ current_shape = shape
436
+ return HDF5::Empty.new(current_dtype) if current_shape.nil?
437
+
438
+ normalized_selection = Selection.normalize(selection, current_shape)
439
+ return current_dtype.numo_class.zeros(*normalized_selection.result_shape) if normalized_selection.size.zero?
440
+
441
+ file_space_id = HDF5::FFI.H5Dget_space(@dataset_id)
442
+ raise HDF5::Error, 'Failed to get dataset dataspace' if file_space_id < 0
443
+
444
+ select_hyperslab(file_space_id, normalized_selection)
445
+ memory_space_id = create_memory_dataspace(normalized_selection.result_shape)
446
+ raise HDF5::Error, 'Failed to create memory dataspace' if memory_space_id < 0
447
+ raise HDF5::Error, 'File and memory selections have different sizes' unless
448
+ HDF5::FFI.H5Sget_select_npoints(file_space_id) == HDF5::FFI.H5Sget_select_npoints(memory_space_id)
449
+
450
+ bytesize = normalized_selection.size * current_dtype.itemsize
451
+ buffer = ::FFI::MemoryPointer.new(:char, bytesize)
452
+ status = HDF5::FFI.H5Dread(@dataset_id, current_dtype.memory_type_id, memory_space_id, file_space_id,
453
+ HDF5::DEFAULT_PROPERTY_LIST, buffer)
454
+ raise HDF5::Error, 'Failed to read dataset' if status < 0
455
+
456
+ result = HDF5::DataHelpers.from_binary(current_dtype, buffer.read_bytes(bytesize),
457
+ normalized_selection.result_shape)
458
+ return result unless normalized_selection.scalar?
459
+
460
+ scalar = result.extract
461
+ current_dtype.kind == :bool ? !scalar.zero? : scalar
462
+ ensure
463
+ HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
464
+ HDF5::FFI.H5Sclose(memory_space_id) if memory_space_id && memory_space_id >= 0
465
+ HDF5::FFI.H5Sclose(file_space_id) if file_space_id && file_space_id >= 0
466
+ end
467
+
468
+ def read_array(selection: nil, flatten: false, dtype: nil, casting: :safe)
469
+ value = read(selection:, dtype:, casting:)
470
+ return value unless value.is_a?(Numo::NArray)
471
+
472
+ array = value.to_a
473
+ flatten ? array.flatten : array
166
474
  end
167
475
 
168
- def read_integer_data(total_elements)
169
- buffer = ::FFI::MemoryPointer.new(:int, total_elements)
170
- status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_INT, HDF5::DEFAULT_PROPERTY_LIST,
171
- HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST, buffer)
172
- raise HDF5::Error, 'Failed to read integer dataset' if status < 0
476
+ def [](*selection)
477
+ read(selection: selection)
478
+ end
173
479
 
174
- buffer.read_array_of_int(total_elements)
480
+ def []=(*selection, value)
481
+ write(value, selection: selection)
175
482
  end
176
483
 
177
- def read_float_data(total_elements)
178
- buffer = ::FFI::MemoryPointer.new(:double, total_elements)
179
- status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_DOUBLE, HDF5::DEFAULT_PROPERTY_LIST,
180
- HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST, buffer)
181
- raise HDF5::Error, 'Failed to read float dataset' if status < 0
484
+ def read_into(destination, selection: nil, casting: :safe)
485
+ ensure_open!
486
+ raise HDF5::Error, 'read_into destination must be a Numo::NArray' unless destination.is_a?(Numo::NArray)
487
+
488
+ values = read(selection:, dtype: DType.for_numo(destination).to_sym, casting:)
489
+ unless destination.shape == values.shape
490
+ raise HDF5::Error,
491
+ 'read_into destination shape must match selection shape'
492
+ end
182
493
 
183
- buffer.read_array_of_double(total_elements)
494
+ destination.store(values)
184
495
  end
185
496
 
186
- def read_string_data(_total_elements)
187
- raise HDF5::Error, 'String dataset reading is not supported yet'
497
+ def each_block(max_bytes:)
498
+ return enum_for(__method__, max_bytes:) unless block_given?
499
+
500
+ ensure_open!
501
+ raise ArgumentError, 'max_bytes must be a positive integer' unless max_bytes.is_a?(Integer) && max_bytes.positive?
502
+
503
+ current_dtype = dtype
504
+ raise ArgumentError, 'max_bytes is smaller than one dataset element' if max_bytes < current_dtype.itemsize
505
+
506
+ current_shape = shape
507
+ if current_shape.empty?
508
+ yield [], read
509
+ return
510
+ end
511
+ return if current_shape.any?(&:zero?)
512
+
513
+ block_shape = block_shape_for(current_shape, max_bytes / current_dtype.itemsize)
514
+ each_block_selection(current_shape, block_shape) do |selection|
515
+ yield selection, read(selection: selection)
516
+ end
517
+ end
518
+
519
+ def each_chunk
520
+ return enum_for(__method__) unless block_given?
521
+
522
+ ensure_open!
523
+
524
+ chunk_shape = chunks
525
+ raise HDF5::Error, 'each_chunk requires a chunked dataset' unless chunk_shape
526
+
527
+ current_shape = shape
528
+ return if current_shape.any?(&:zero?)
529
+
530
+ each_block_selection(current_shape, chunk_shape) do |selection|
531
+ yield selection, read(selection: selection)
532
+ end
188
533
  end
189
534
 
190
535
  private
191
536
 
192
- def initialize_from_id(dataset_id, name)
537
+ def write_string(data, selection:)
538
+ normalized_selection = Selection.normalize(selection, shape)
539
+ values, values_shape = HDF5::StringCodec.normalize_data(data)
540
+ unless values_shape == normalized_selection.result_shape
541
+ raise HDF5::ShapeError,
542
+ 'Dataset shape must match string data shape'
543
+ end
544
+ return data if normalized_selection.size.zero?
545
+
546
+ type_id = HDF5::FFI.H5Dget_type(@dataset_id)
547
+ raise HDF5::Error, 'Failed to get dataset datatype' if type_id < 0
548
+ unless HDF5::StringCodec.variable?(type_id)
549
+ raise UnsupportedTypeError, 'Fixed-length string datasets are not yet supported'
550
+ end
551
+
552
+ file_space_id = HDF5::FFI.H5Dget_space(@dataset_id)
553
+ select_hyperslab(file_space_id, normalized_selection)
554
+ memory_space_id = create_memory_dataspace(normalized_selection.result_shape)
555
+ buffer, _string_pointers = HDF5::StringCodec.buffer_for_values(values)
556
+ status = HDF5::FFI.H5Dwrite(@dataset_id, type_id, memory_space_id, file_space_id,
557
+ HDF5::DEFAULT_PROPERTY_LIST, buffer)
558
+ raise HDF5::Error, 'Failed to write string dataset' if status < 0
559
+
560
+ data
561
+ ensure
562
+ HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
563
+ HDF5::FFI.H5Sclose(memory_space_id) if memory_space_id && memory_space_id >= 0
564
+ HDF5::FFI.H5Sclose(file_space_id) if file_space_id && file_space_id >= 0
565
+ end
566
+
567
+ def read_string(type_id, selection:)
568
+ normalized_selection = Selection.normalize(selection, shape)
569
+ unless HDF5::StringCodec.variable?(type_id)
570
+ raise UnsupportedTypeError, 'Fixed-length string datasets are not yet supported'
571
+ end
572
+ return Numo::RObject.new(*normalized_selection.result_shape) if normalized_selection.size.zero?
573
+
574
+ file_space_id = HDF5::FFI.H5Dget_space(@dataset_id)
575
+ select_hyperslab(file_space_id, normalized_selection)
576
+ memory_space_id = create_memory_dataspace(normalized_selection.result_shape)
577
+ buffer = ::FFI::MemoryPointer.new(:pointer, normalized_selection.size)
578
+ status = HDF5::FFI.H5Dread(@dataset_id, type_id, memory_space_id, file_space_id,
579
+ HDF5::DEFAULT_PROPERTY_LIST, buffer)
580
+ raise HDF5::Error, 'Failed to read string dataset' if status < 0
581
+
582
+ HDF5::StringCodec.read_values(buffer, normalized_selection.size, normalized_selection.result_shape)
583
+ ensure
584
+ if buffer && type_id && memory_space_id
585
+ active_error = $ERROR_INFO
586
+ reclaim_status = HDF5::FFI.H5Dvlen_reclaim(type_id, memory_space_id, HDF5::DEFAULT_PROPERTY_LIST, buffer)
587
+ if reclaim_status.negative? && active_error.nil?
588
+ raise HDF5::Error,
589
+ 'Failed to reclaim variable-length string data'
590
+ end
591
+ end
592
+ HDF5::FFI.H5Sclose(memory_space_id) if memory_space_id && memory_space_id >= 0
593
+ HDF5::FFI.H5Sclose(file_space_id) if file_space_id && file_space_id >= 0
594
+ end
595
+
596
+ def block_shape_for(dataset_shape, max_elements)
597
+ remaining = max_elements
598
+ dataset_shape.reverse.map do |dimension|
599
+ block_dimension = [dimension, remaining].min
600
+ remaining /= block_dimension
601
+ block_dimension
602
+ end.reverse
603
+ end
604
+
605
+ def each_block_selection(dataset_shape, block_shape, axis = 0, prefix = [], &block)
606
+ if axis == dataset_shape.length
607
+ yield prefix
608
+ return
609
+ end
610
+
611
+ 0.step(dataset_shape[axis] - 1, block_shape[axis]) do |start|
612
+ length = [block_shape[axis], dataset_shape[axis] - start].min
613
+ each_block_selection(dataset_shape, block_shape, axis + 1, prefix + [start...(start + length)], &block)
614
+ end
615
+ end
616
+
617
+ def create_memory_dataspace(shape)
618
+ return HDF5::FFI.H5Screate(:H5S_SCALAR) if shape.empty?
619
+
620
+ dims = ::FFI::MemoryPointer.new(:ulong_long, shape.length)
621
+ dims.write_array_of_ulong_long(shape)
622
+ HDF5::FFI.H5Screate_simple(shape.length, dims, nil)
623
+ end
624
+
625
+ def select_hyperslab(dataspace_id, selection)
626
+ rank = selection.start.length
627
+ return if rank.zero?
628
+
629
+ start = ::FFI::MemoryPointer.new(:ulong_long, rank)
630
+ stride = ::FFI::MemoryPointer.new(:ulong_long, rank)
631
+ count = ::FFI::MemoryPointer.new(:ulong_long, rank)
632
+ start.write_array_of_ulong_long(selection.start)
633
+ stride.write_array_of_ulong_long(selection.stride)
634
+ count.write_array_of_ulong_long(selection.count)
635
+ status = HDF5::FFI.H5Sselect_hyperslab(dataspace_id, :H5S_SELECT_SET, start, stride, count, nil)
636
+ raise HDF5::Error, 'Failed to select dataset region' if status < 0
637
+ end
638
+
639
+ def initialize_from_id(dataset_id, name, context)
193
640
  raise HDF5::Error, "Failed to open dataset: #{name}" if dataset_id < 0
194
641
 
195
642
  @dataset_id = dataset_id
196
643
  @name = name
644
+ @context = context
645
+ @context.register(dataset_id, :dataset) if @context
197
646
  end
647
+
648
+ def ensure_open!
649
+ raise ClosedError, 'HDF5 dataset is closed' if @dataset_id.nil?
650
+
651
+ @context&.ensure_open!(@dataset_id)
652
+ end
653
+
654
+ prepend FileContext.guard(
655
+ :attrs, :write, :dtype, :shape, :chunks, :maxshape, :fillvalue, :resize, :append, :read, :read_array,
656
+ :[], :[]=, :read_into, :each_block, :each_chunk
657
+ )
198
658
  end
199
659
  end