ruby-hdf5 0.0.2 → 0.0.4

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