ruby-hdf5 0.0.1 → 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,19 +1,44 @@
1
1
  module HDF5
2
2
  class Dataset
3
3
  class << self
4
- def create(parent_id, name, data)
5
- values = normalize_data(data)
6
- dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
7
- dims.write_array_of_ulong_long([values.length])
8
- datatype_id = datatype_id_for(values)
9
- dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
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')
12
+ end
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
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
25
+
26
+ raise HDF5::Error, 'Null datasets cannot have maxshape or chunks' if empty_data && (maxshape || chunks)
27
+
28
+ validate_maxshape(maxshape, shape) if maxshape
29
+ chunks = :auto if maxshape && chunks.nil?
30
+ dataspace_id = create_dataspace(shape, maxshape)
10
31
  raise HDF5::Error, "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
11
32
 
33
+ dcpl_id = create_property_list(shape, dtype_object, chunks:, compression:, compression_opts:, shuffle:, fletcher32:,
34
+ fillvalue:)
35
+
12
36
  dataset = from_id(
13
- HDF5::FFI.H5Dcreate2(parent_id, name, datatype_id, dataspace_id, HDF5::DEFAULT_PROPERTY_LIST,
14
- 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
15
39
  )
16
- dataset.write(values)
40
+ dataset.write(data) if string_data
41
+ dataset.write(narray) if narray
17
42
  return dataset unless block_given?
18
43
 
19
44
  begin
@@ -21,12 +46,20 @@ module HDF5
21
46
  ensure
22
47
  dataset.close
23
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
24
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
25
58
  HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
26
59
  end
27
60
 
28
- def open(parent_id, name)
29
- 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)
30
63
  return dataset unless block_given?
31
64
 
32
65
  begin
@@ -36,83 +69,225 @@ module HDF5
36
69
  end
37
70
  end
38
71
 
39
- def normalize_data(data)
40
- values = data.is_a?(Array) ? data : [data]
41
- raise HDF5::Error, 'Dataset data must not be empty' if values.empty?
42
- raise HDF5::Error, 'Nested arrays are not supported' if values.any? { |value| value.is_a?(Array) }
72
+ private
43
73
 
44
- values
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)
45
88
  end
46
89
 
47
- def datatype_id_for(data)
48
- if data.all? { |value| value.is_a?(Integer) }
49
- HDF5::FFI.H5T_NATIVE_INT
50
- elsif data.all? { |value| value.is_a?(Numeric) }
51
- HDF5::FFI.H5T_NATIVE_DOUBLE
52
- else
53
- raise HDF5::Error, 'Only numeric dataset data is supported'
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
54
98
  end
99
+ raise HDF5::Error, 'maxshape dimensions must be nil or integers no smaller than shape' unless valid
55
100
  end
56
101
 
57
- def buffer_for(data)
58
- if data.all? { |value| value.is_a?(Integer) }
59
- buffer = ::FFI::MemoryPointer.new(:int, data.length)
60
- buffer.write_array_of_int(data)
61
- else
62
- buffer = ::FFI::MemoryPointer.new(:double, data.length)
63
- buffer.write_array_of_double(data.map(&:to_f))
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'
64
134
  end
65
135
 
66
- buffer
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
67
165
  end
68
166
 
69
- private
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
70
205
 
71
- def from_id(dataset_id, name)
206
+ def from_id(dataset_id, name, context)
72
207
  dataset = allocate
73
- dataset.send(:initialize_from_id, dataset_id, name)
208
+ dataset.send(:initialize_from_id, dataset_id, name, context)
74
209
  dataset
75
210
  end
76
211
  end
77
212
 
78
213
  def initialize(parent_id, name)
79
- 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)
80
215
  end
81
216
 
82
217
  def attrs
83
- @attrs ||= AttributeManager.new(@dataset_id)
218
+ ensure_open!
219
+ @attrs ||= AttributeManager.new(@dataset_id, @context)
84
220
  end
85
221
 
86
- def write(data)
87
- values = self.class.normalize_data(data)
88
- buffer = self.class.buffer_for(values)
89
- mem_type_id = self.class.datatype_id_for(values)
90
- 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,
91
256
  HDF5::DEFAULT_PROPERTY_LIST, buffer)
92
257
  raise HDF5::Error, 'Failed to write dataset' if status < 0
93
258
 
94
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
95
263
  end
96
264
 
97
265
  def close
98
266
  return if @dataset_id.nil?
99
267
 
100
- HDF5::FFI.H5Dclose(@dataset_id)
268
+ @context ? @context.close(@dataset_id) : HDF5::FFI.H5Dclose(@dataset_id)
101
269
  @dataset_id = nil
102
270
  end
103
271
 
272
+ def closed?
273
+ @dataset_id.nil? || (@context && @context.closed?)
274
+ end
275
+
104
276
  def dtype
277
+ ensure_open!
105
278
  datatype_id = HDF5::FFI.H5Dget_type(@dataset_id)
106
279
  raise HDF5::Error, 'Failed to get datatype' if datatype_id < 0
107
280
 
108
- HDF5::FFI.H5Tget_class(datatype_id)
281
+ DType.for_hdf5(datatype_id)
109
282
  ensure
110
283
  HDF5::FFI.H5Tclose(datatype_id) if datatype_id && datatype_id >= 0
111
284
  end
112
285
 
113
286
  def shape
287
+ ensure_open!
114
288
  dataspace_id = HDF5::FFI.H5Dget_space(@dataset_id)
115
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
116
291
 
117
292
  ndims = HDF5::FFI.H5Sget_simple_extent_ndims(dataspace_id)
118
293
  raise HDF5::Error, 'Failed to get number of dimensions' if ndims < 0
@@ -125,52 +300,360 @@ module HDF5
125
300
  HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
126
301
  end
127
302
 
128
- def read
129
- 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')
130
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
131
419
 
132
- total_elements = current_shape.inject(:*)
133
- case current_dtype
134
- when :H5T_INTEGER
135
- read_integer_data(total_elements)
136
- when :H5T_FLOAT
137
- read_float_data(total_elements)
138
- when :H5T_STRING
139
- read_string_data(total_elements)
140
- else
141
- 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'
142
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
143
466
  end
144
467
 
145
- def read_integer_data(total_elements)
146
- buffer = ::FFI::MemoryPointer.new(:int, total_elements)
147
- status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_INT, HDF5::DEFAULT_PROPERTY_LIST,
148
- HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST, buffer)
149
- raise HDF5::Error, 'Failed to read integer dataset' if status < 0
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)
150
471
 
151
- buffer.read_array_of_int(total_elements)
472
+ array = value.to_a
473
+ flatten ? array.flatten : array
152
474
  end
153
475
 
154
- def read_float_data(total_elements)
155
- buffer = ::FFI::MemoryPointer.new(:double, total_elements)
156
- status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_DOUBLE, HDF5::DEFAULT_PROPERTY_LIST,
157
- HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST, buffer)
158
- raise HDF5::Error, 'Failed to read float dataset' if status < 0
476
+ def [](*selection)
477
+ read(selection: selection)
478
+ end
159
479
 
160
- buffer.read_array_of_double(total_elements)
480
+ def []=(*selection, value)
481
+ write(value, selection: selection)
161
482
  end
162
483
 
163
- def read_string_data(total_elements)
164
- raise NotImplementedError
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
493
+
494
+ destination.store(values)
495
+ end
496
+
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
165
533
  end
166
534
 
167
535
  private
168
536
 
169
- 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)
170
640
  raise HDF5::Error, "Failed to open dataset: #{name}" if dataset_id < 0
171
641
 
172
642
  @dataset_id = dataset_id
173
643
  @name = name
644
+ @context = context
645
+ @context.register(dataset_id, :dataset) if @context
174
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
+ )
175
658
  end
176
659
  end