ruby-hdf5 0.0.3 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7932dfc6c7cc1ffcfb6dc30c8963785e1e1e2ba206add86e634878d999e166d
4
- data.tar.gz: cd6e121152d557821c64deb08a08286528911db013a48a0d7d7a1bff8c6da7a0
3
+ metadata.gz: 85ca4ff79d4a34553188284efb980628cd4f4931aa29d12bd37b09e0a89d3ca8
4
+ data.tar.gz: 1126e3458c88e00b00a6919ed832c35196673a5d785da524a218a9ff0772bd03
5
5
  SHA512:
6
- metadata.gz: f7270f19f0bd3732147faad5f94f0c429f02c93e81ad66985adefddff43ac41fbacb2834073d6b195c14f4ff905df4a60735ea143fee79fab1116a96f76c7e54
7
- data.tar.gz: 060cb8b49f0373ca6ac788274c1bb5bc187fe92bab799b240f731034b59abebd789c956e23da6ccdbef4dd577e37be38e20121879ce98caea20c326829cb2e2e
6
+ metadata.gz: 15f984a33a2c9036364e4bd4cef11acd041a1684f531cd30f05c235e764df5ff5a69cea2e7e727f0c34fd1438272673ed3764661bf1f0e19b0322f0879b4fea0
7
+ data.tar.gz: 9ef886f63b1c207e6c58e9b5a00462207d74239e522cb2aaeb9344237e93e14e6ad29326d3e79d7e9a1ea05b5a3d0215b0f5f5a250c412e9459a9810835f2aeb
@@ -14,6 +14,8 @@ module HDF5
14
14
 
15
15
  space_id = HDF5::FFI.H5Aget_space(@attr_id)
16
16
  raise HDF5::Error, 'Failed to get attribute dataspace' if space_id < 0
17
+ return HDF5::Empty.new(DType.for_hdf5(type_id)) if HDF5::FFI.H5Sget_simple_extent_type(space_id) == :H5S_NULL
18
+
17
19
  return read_string(type_id, space_id) if HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
18
20
 
19
21
  dtype_object = DType.for_hdf5(type_id)
@@ -66,7 +68,7 @@ module HDF5
66
68
  status = HDF5::FFI.H5Aread(@attr_id, type_id, buffer)
67
69
  raise HDF5::Error, 'Failed to read string attribute' if status < 0
68
70
 
69
- HDF5::StringCodec.read_values(buffer, count, attribute_shape)
71
+ HDF5::StringCodec.read_values(buffer, count, attribute_shape, encoding: HDF5::StringCodec.encoding_for(type_id))
70
72
  ensure
71
73
  if buffer
72
74
  active_error = $ERROR_INFO
@@ -134,7 +136,7 @@ module HDF5
134
136
  write(attr_name, value)
135
137
  end
136
138
 
137
- def modify(attr_name, value)
139
+ def modify(attr_name, value, casting: :safe)
138
140
  @context&.ensure_open!(@dataset_id)
139
141
  raise HDF5::Error, "Attribute not found: #{attr_name}" unless key?(attr_name)
140
142
 
@@ -144,28 +146,34 @@ module HDF5
144
146
  type_id = HDF5::FFI.H5Aget_type(attr_id)
145
147
  space_id = HDF5::FFI.H5Aget_space(attr_id)
146
148
  raise HDF5::Error, "Failed to inspect attribute: #{attr_name}" if type_id < 0 || space_id < 0
149
+ if HDF5::FFI.H5Sget_simple_extent_type(space_id) == :H5S_NULL
150
+ unless value.is_a?(HDF5::Empty) && value.dtype.to_sym == DType.for_hdf5(type_id).to_sym
151
+ raise HDF5::ShapeError, 'Cannot assign a value to a Null attribute'
152
+ end
153
+ return value
154
+ end
147
155
 
148
156
  if HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
149
157
  unless HDF5::StringCodec.variable?(type_id)
150
158
  raise UnsupportedTypeError, 'Fixed-length string attributes are not yet supported'
151
159
  end
152
160
 
153
- string_values, string_shape = HDF5::StringCodec.normalize_data(value)
161
+ encoding = HDF5::StringCodec.encoding_for(type_id)
162
+ string_values, string_shape = HDF5::StringCodec.normalize_data(value, encoding:)
154
163
  unless string_shape == attribute_shape(space_id)
155
164
  raise HDF5::ShapeError,
156
165
  'Attribute shape must not change when modifying'
157
166
  end
158
167
 
159
- buffer, _string_pointers = HDF5::StringCodec.buffer_for_values(string_values)
168
+ buffer, _string_pointers = HDF5::StringCodec.buffer_for_values(string_values, encoding:)
160
169
  status = HDF5::FFI.H5Awrite(attr_id, type_id, buffer)
161
170
  else
162
171
  dtype_object = DType.for_hdf5(type_id)
163
- values = HDF5::DataHelpers.normalize_data(value, label: 'Attribute data')
172
+ values = HDF5::DataHelpers.normalize_data(value, label: 'Attribute data', dtype: dtype_object, casting:, convert: false)
164
173
  expected_shape = attribute_shape(space_id)
165
174
  raise HDF5::Error, 'Attribute shape must not change when modifying' unless values.shape == expected_shape
166
175
 
167
- converted = dtype_object.numo_class.cast(values)
168
- status = HDF5::FFI.H5Awrite(attr_id, dtype_object.memory_type_id, HDF5::DataHelpers.buffer_for(converted))
176
+ status = HDF5::FFI.H5Awrite(attr_id, DType.for_numo(values).memory_type_id, HDF5::DataHelpers.buffer_for(values))
169
177
  end
170
178
  raise HDF5::Error, "Failed to modify attribute: #{attr_name}" if status < 0
171
179
 
@@ -178,10 +186,14 @@ module HDF5
178
186
 
179
187
  def write(attr_name, value)
180
188
  @context&.ensure_open!(@dataset_id)
189
+ empty_data = value.is_a?(HDF5::Empty)
190
+ if empty_data && value.dtype.kind == :string
191
+ raise UnsupportedFeatureError, 'Creating Null string attributes is not yet supported'
192
+ end
181
193
  string_data = HDF5::StringCodec.string_data?(value)
182
194
  string_values, string_shape = HDF5::StringCodec.normalize_data(value) if string_data
183
- values = HDF5::DataHelpers.normalize_data(value, label: 'Attribute data') unless string_data
184
- dtype_object = DType.for_numo(values) unless string_data
195
+ values = HDF5::DataHelpers.normalize_data(value, label: 'Attribute data') unless string_data || empty_data
196
+ dtype_object = empty_data ? value.dtype : DType.for_numo(values) unless string_data
185
197
  type_id = string_data ? HDF5::StringCodec.datatype_id : dtype_object.storage_type_id
186
198
 
187
199
  exists = HDF5::FFI.H5Aexists(@dataset_id, attr_name)
@@ -192,7 +204,7 @@ module HDF5
192
204
  raise HDF5::Error, "Failed to replace attribute: #{attr_name}" if status < 0
193
205
  end
194
206
 
195
- dataspace_id = create_dataspace(string_data ? string_shape : values.shape)
207
+ dataspace_id = create_dataspace(empty_data ? nil : (string_data ? string_shape : values.shape))
196
208
  raise HDF5::Error, 'Failed to create attribute dataspace' if dataspace_id < 0
197
209
 
198
210
  attr_id = HDF5::FFI.H5Acreate2(
@@ -204,6 +216,7 @@ module HDF5
204
216
  HDF5::DEFAULT_PROPERTY_LIST
205
217
  )
206
218
  raise HDF5::Error, "Failed to create attribute: #{attr_name}" if attr_id < 0
219
+ return value if empty_data
207
220
 
208
221
  buffer, _string_pointers = if string_data
209
222
  HDF5::StringCodec.buffer_for_values(string_values)
@@ -236,6 +249,8 @@ module HDF5
236
249
  end
237
250
 
238
251
  def create_dataspace(shape)
252
+ return HDF5::FFI.H5Screate(:H5S_NULL) if shape.nil?
253
+
239
254
  return HDF5::FFI.H5Screate(:H5S_SCALAR) if shape.empty?
240
255
 
241
256
  dimensions = ::FFI::MemoryPointer.new(:ulong_long, shape.length)
@@ -2,27 +2,163 @@ module HDF5
2
2
  module DataHelpers
3
3
  module_function
4
4
 
5
- def normalize_data(data, label: 'Data')
6
- return data if data.is_a?(Numo::NArray) && DType.for_numo(data)
7
-
8
- values = data.is_a?(Array) ? data.flatten : [data]
9
- raise HDF5::Error, "#{label} must not be empty" if values.empty?
10
-
11
- dtype = if values.all? { |value| value.is_a?(Integer) }
12
- DType.for_symbol(:int64)
13
- elsif values.all? { |value| [true, false].include?(value) }
14
- DType.for_symbol(:bool)
15
- elsif values.all? { |value| value.is_a?(Numeric) } && values.any? { |value| value.is_a?(Complex) }
16
- DType.for_symbol(:complex128)
17
- elsif values.all? { |value| value.is_a?(Numeric) }
18
- DType.for_symbol(:float64)
19
- else
20
- raise HDF5::Error, "Only numeric #{label.downcase} is supported"
21
- end
5
+ def normalize_data(data, label: 'Data', dtype: nil, casting: :safe, convert: true)
6
+ raise ArgumentError, "Unsupported casting mode: #{casting.inspect}" unless %i[safe unsafe].include?(casting)
7
+
8
+ if data.is_a?(Numo::NArray)
9
+ source = DType.for_numo(data)
10
+ return data unless dtype
11
+ unless source.castable_to?(dtype, casting:)
12
+ raise ConversionError, "Cannot safely cast #{source.to_sym} to #{dtype.to_sym}"
13
+ end
14
+ return data if source.to_sym == dtype.to_sym
15
+ # HDF5 converts numeric widths and matching complex compounds directly,
16
+ # but cannot convert a real numeric datatype to a complex compound.
17
+ native_conversion = dtype.kind != :complex || source.kind == :complex
18
+ return data if !convert && casting == :safe && native_conversion
19
+
20
+ return dtype.numo_class.cast(data)
21
+ end
22
+
23
+ # Homogeneous flat Arrays need no recursive shape traversal or per-value
24
+ # conversion checks: integer extrema cover the range; Ruby Float is float64.
25
+ if dtype.nil? && data.is_a?(Array) && !data.empty?
26
+ homogeneous = normalize_homogeneous_array(data, data, casting:)
27
+ return homogeneous if homogeneous
28
+ end
29
+
30
+ shape = array_shape(data)
31
+ values = data.is_a?(Array) ? (shape.length <= 1 ? data : data.flatten) : [data]
32
+ raise HDF5::Error, "#{label} must not be empty without an explicit dtype" if values.empty? && !dtype
33
+ if dtype.nil? && shape.length > 1
34
+ homogeneous = normalize_homogeneous_array(data, values, casting:)
35
+ return homogeneous if homogeneous
36
+ end
37
+
38
+ dtype ||= inferred_dtype(values, label:)
39
+ validate_values(values, dtype, casting:)
22
40
  normalized = dtype.kind == :bool ? normalize_booleans(data) : data
41
+ return dtype.numo_class.new(*shape) if values.empty?
42
+ return Numo::Bit.new.store(normalized) if dtype.kind == :bool && !data.is_a?(Array)
43
+
23
44
  dtype.numo_class.cast(normalized)
24
45
  end
25
46
 
47
+ def normalize_homogeneous_array(data, values, casting:)
48
+ if values.all? { |value| value.is_a?(Integer) }
49
+ minimum, maximum = values.minmax
50
+ dtype = DType.for_symbol(minimum >= 0 && maximum >= (1 << 63) ? :uint64 : :int64)
51
+ validate_values([minimum, maximum], dtype, casting:)
52
+ dtype.numo_class.cast(data)
53
+ elsif values.all? { |value| value.is_a?(Float) }
54
+ Numo::DFloat.cast(data)
55
+ end
56
+ end
57
+
58
+ def inferred_dtype(values, label:)
59
+ kind = nil
60
+ minimum = maximum = 0
61
+ values.each do |value|
62
+ if value.is_a?(Integer)
63
+ current = :int64
64
+ minimum = value if value < minimum
65
+ maximum = value if value > maximum
66
+ elsif value.is_a?(Complex)
67
+ current = :complex128
68
+ elsif value.is_a?(Numeric)
69
+ current = :float64
70
+ elsif value.equal?(true) || value.equal?(false)
71
+ current = :bool
72
+ else
73
+ raise HDF5::Error, "Only numeric #{label.downcase} is supported"
74
+ end
75
+ if kind && (kind == :bool) != (current == :bool)
76
+ raise HDF5::Error, "Only numeric #{label.downcase} is supported"
77
+ end
78
+ kind = current if kind.nil? || current == :complex128 || current == :float64 && kind == :int64
79
+ end
80
+ kind = :uint64 if kind == :int64 && minimum >= 0 && maximum >= (1 << 63)
81
+ DType.for_symbol(kind)
82
+ end
83
+
84
+ def array_shape(value)
85
+ return [] unless value.is_a?(Array)
86
+ return [0] if value.empty?
87
+
88
+ unless value.first.is_a?(Array)
89
+ raise ShapeError, 'Data must be rectangular' if value.any? { |item| item.is_a?(Array) }
90
+
91
+ return [value.length]
92
+ end
93
+
94
+ child_shape = array_shape(value.first)
95
+ index = 1
96
+ while index < value.length
97
+ item = value[index]
98
+ unless item.is_a?(Array) && array_shape(item) == child_shape
99
+ raise ShapeError, 'Data must be rectangular'
100
+ end
101
+ index += 1
102
+ end
103
+
104
+ [value.length, *child_shape]
105
+ end
106
+
107
+ def scalar?(value)
108
+ value.is_a?(Numeric) || value.equal?(true) || value.equal?(false) ||
109
+ value.is_a?(Numo::NArray) && value.shape.empty?
110
+ end
111
+
112
+ def validate_values(values, dtype, casting:)
113
+ if dtype.kind == :bool
114
+ unless values.all? { |value| value.equal?(true) || value.equal?(false) }
115
+ raise ConversionError, 'Bool data must contain true or false'
116
+ end
117
+ return
118
+ end
119
+ if casting == :unsafe
120
+ raise ConversionError, 'Data must contain numeric values' unless values.all? { |value| value.is_a?(Numeric) }
121
+ return
122
+ end
123
+
124
+ case dtype.kind
125
+ when :integer
126
+ bits = dtype.itemsize * 8
127
+ minimum = dtype.unsigned? ? 0 : -(1 << (bits - 1))
128
+ maximum = dtype.unsigned? ? (1 << bits) - 1 : (1 << (bits - 1)) - 1
129
+ values.each do |value|
130
+ unless value.is_a?(Integer) && value.between?(minimum, maximum)
131
+ raise ConversionError, "Value #{value.inspect} cannot safely be represented as #{dtype.to_sym}"
132
+ end
133
+ end
134
+ when :float, :complex
135
+ size = dtype.kind == :complex ? dtype.itemsize / 2 : dtype.itemsize
136
+ values.each do |value|
137
+ raise ConversionError, 'Data must contain numeric values' unless value.is_a?(Numeric)
138
+ if dtype.kind == :complex
139
+ validate_component(value.real, size, dtype)
140
+ validate_component(value.imag, size, dtype)
141
+ else
142
+ validate_component(value, size, dtype)
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def validate_component(component, size, dtype)
149
+ return if size == 8 && component.is_a?(Float)
150
+
151
+ raise ConversionError, "Cannot safely cast complex data to #{dtype.to_sym}" if component.is_a?(Complex)
152
+
153
+ converted = component.to_f
154
+ converted = [converted].pack('f').unpack1('f') if size == 4
155
+ return if component.is_a?(Float) && !component.finite? && !converted.finite?
156
+ return if component.is_a?(Float) && component == converted
157
+ return if converted.finite? && converted.to_r == component.to_r
158
+
159
+ raise ConversionError, "Value #{component.inspect} cannot safely be represented as #{dtype.to_sym}"
160
+ end
161
+
26
162
  def buffer_for(narray)
27
163
  dtype = DType.for_numo(narray)
28
164
  binary = if dtype.kind == :bool
data/lib/hdf5/dataset.rb CHANGED
@@ -2,13 +2,22 @@ module HDF5
2
2
  class Dataset
3
3
  class << self
4
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)
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
+
6
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
14
+ end
7
15
  string_data = HDF5::StringCodec.string_data?(data)
8
16
  _string_values, string_shape = HDF5::StringCodec.normalize_data(data) if string_data
9
17
  unless data.nil? || string_data || empty_data
10
18
  narray = HDF5::DataHelpers.normalize_data(data,
11
- label: 'Dataset data')
19
+ label: 'Dataset data', dtype: dtype && DType.for_symbol(dtype), casting:,
20
+ convert: false)
12
21
  end
13
22
  unless string_data
14
23
  dtype_object = if empty_data
@@ -19,7 +28,6 @@ module HDF5
19
28
  end
20
29
  type_id = string_data ? HDF5::StringCodec.datatype_id : dtype_object.storage_type_id
21
30
  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
31
  raise HDF5::Error, 'Dataset shape must match data shape' if narray && shape != narray.shape
24
32
  raise HDF5::ShapeError, 'Dataset shape must match string data shape' if string_data && shape != string_shape
25
33
 
@@ -31,7 +39,7 @@ module HDF5
31
39
  raise HDF5::Error, "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
32
40
 
33
41
  dcpl_id = create_property_list(shape, dtype_object, chunks:, compression:, compression_opts:, shuffle:, fletcher32:,
34
- fillvalue:)
42
+ fillvalue:, casting:)
35
43
 
36
44
  dataset = from_id(
37
45
  HDF5::FFI.H5Dcreate2(parent_id, name, type_id, dataspace_id, HDF5::DEFAULT_PROPERTY_LIST,
@@ -39,6 +47,7 @@ module HDF5
39
47
  )
40
48
  dataset.write(data) if string_data
41
49
  dataset.write(narray) if narray
50
+ initialized = true
42
51
  return dataset unless block_given?
43
52
 
44
53
  begin
@@ -47,7 +56,7 @@ module HDF5
47
56
  dataset.close
48
57
  end
49
58
  rescue StandardError
50
- if dataset
59
+ if dataset && !initialized
51
60
  dataset.close unless dataset.closed?
52
61
  HDF5::FFI.H5Ldelete(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST)
53
62
  end
@@ -104,7 +113,7 @@ module HDF5
104
113
  end
105
114
 
106
115
  def create_property_list(shape, dtype_object, chunks:, compression:, compression_opts:, shuffle:, fletcher32:,
107
- fillvalue:)
116
+ fillvalue:, casting:)
108
117
  chunked = chunks || compression || compression_opts || shuffle || fletcher32
109
118
  return unless chunked || !fillvalue.nil?
110
119
  raise HDF5::Error, 'Chunked storage is not supported for scalar datasets' if chunked && shape.empty?
@@ -152,7 +161,7 @@ module HDF5
152
161
  end
153
162
  check_property_status(HDF5::FFI.H5Pset_fletcher32(dcpl_id), 'enable Fletcher32') if fletcher32
154
163
  unless fillvalue.nil?
155
- value = dtype_object.numo_class.cast(fillvalue)
164
+ value = HDF5::DataHelpers.normalize_data(fillvalue, dtype: dtype_object, casting:, label: 'Fill value')
156
165
  raise HDF5::Error, 'fillvalue must be scalar' unless value.shape.empty?
157
166
 
158
167
  check_property_status(HDF5::FFI.H5Pset_fill_value(dcpl_id, dtype_object.memory_type_id, HDF5::DataHelpers.buffer_for(value)),
@@ -223,43 +232,24 @@ module HDF5
223
232
  ensure_open!
224
233
  return write_string(data, selection:) if HDF5::StringCodec.string_data?(data)
225
234
 
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
235
+ current_shape = shape
236
+ raise HDF5::Error, 'Cannot write to a Null dataset' if current_shape.nil?
238
237
 
239
- dtype_object = DType.for_numo(values)
238
+ normalized_selection = Selection.normalize(selection, current_shape)
240
239
  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?
240
+ raise ConversionError, 'String datasets require string data' if target_dtype.kind == :string
244
241
 
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)
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
254
248
 
255
- status = HDF5::FFI.H5Dwrite(@dataset_id, dtype_object.memory_type_id, memory_space_id, file_space_id,
256
- HDF5::DEFAULT_PROPERTY_LIST, buffer)
257
- raise HDF5::Error, 'Failed to write dataset' if status < 0
249
+ return data if normalized_selection.size.zero?
258
250
 
251
+ write_numeric_buffer(HDF5::DataHelpers.buffer_for(values), DType.for_numo(values), normalized_selection)
259
252
  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
263
253
  end
264
254
 
265
255
  def close
@@ -344,6 +334,9 @@ module HDF5
344
334
  def fillvalue
345
335
  ensure_open!
346
336
  dtype_object = dtype
337
+ if dtype_object.kind == :string
338
+ raise UnsupportedFeatureError, 'fillvalue is not supported for string datasets'
339
+ end
347
340
  property_list_id = HDF5::FFI.H5Dget_create_plist(@dataset_id)
348
341
  raise HDF5::Error, 'Failed to get dataset creation properties' if property_list_id < 0
349
342
 
@@ -351,7 +344,8 @@ module HDF5
351
344
  status = HDF5::FFI.H5Pget_fill_value(property_list_id, dtype_object.memory_type_id, buffer)
352
345
  raise HDF5::Error, 'Failed to get dataset fill value' if status < 0
353
346
 
354
- dtype_object.numo_class.from_binary(buffer.read_bytes(dtype_object.itemsize), []).extract
347
+ value = HDF5::DataHelpers.from_binary(dtype_object, buffer.read_bytes(dtype_object.itemsize), []).extract
348
+ dtype_object.kind == :bool ? !value.zero? : value
355
349
  ensure
356
350
  HDF5::FFI.H5Pclose(property_list_id) if property_list_id && property_list_id >= 0
357
351
  end
@@ -381,7 +375,7 @@ module HDF5
381
375
 
382
376
  def append(data, axis: 0)
383
377
  ensure_open!
384
- values = HDF5::DataHelpers.normalize_data(data, label: 'Dataset data')
378
+ values = HDF5::DataHelpers.normalize_data(data, label: 'Dataset data', dtype: dtype, convert: false)
385
379
  current_shape = shape
386
380
  raise HDF5::Error, 'Cannot append to a Null dataset' if current_shape.nil?
387
381
  raise HDF5::Error, 'Cannot append to a scalar dataset' if current_shape.empty?
@@ -421,6 +415,16 @@ module HDF5
421
415
  ensure_open!
422
416
  type_id = HDF5::FFI.H5Dget_type(@dataset_id)
423
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)
427
+ end
424
428
  if dtype && HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
425
429
  raise ConversionError,
426
430
  'dtype is not supported for string datasets'
@@ -432,11 +436,11 @@ module HDF5
432
436
  raise ConversionError, "Cannot safely cast #{source_dtype.to_sym} to #{current_dtype.to_sym}" unless
433
437
  source_dtype.castable_to?(current_dtype, casting:)
434
438
 
435
- current_shape = shape
436
- return HDF5::Empty.new(current_dtype) if current_shape.nil?
437
-
438
439
  normalized_selection = Selection.normalize(selection, current_shape)
439
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
440
444
 
441
445
  file_space_id = HDF5::FFI.H5Dget_space(@dataset_id)
442
446
  raise HDF5::Error, 'Failed to get dataset dataspace' if file_space_id < 0
@@ -485,12 +489,17 @@ module HDF5
485
489
  ensure_open!
486
490
  raise HDF5::Error, 'read_into destination must be a Numo::NArray' unless destination.is_a?(Numo::NArray)
487
491
 
488
- values = read(selection:, dtype: DType.for_numo(destination).to_sym, casting:)
489
- unless destination.shape == values.shape
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
490
497
  raise HDF5::Error,
491
498
  'read_into destination shape must match selection shape'
492
499
  end
493
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)
494
503
  destination.store(values)
495
504
  end
496
505
 
@@ -500,10 +509,13 @@ module HDF5
500
509
  ensure_open!
501
510
  raise ArgumentError, 'max_bytes must be a positive integer' unless max_bytes.is_a?(Integer) && max_bytes.positive?
502
511
 
503
- current_dtype = dtype
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
504
517
  raise ArgumentError, 'max_bytes is smaller than one dataset element' if max_bytes < current_dtype.itemsize
505
518
 
506
- current_shape = shape
507
519
  if current_shape.empty?
508
520
  yield [], read
509
521
  return
@@ -521,10 +533,9 @@ module HDF5
521
533
 
522
534
  ensure_open!
523
535
 
524
- chunk_shape = chunks
536
+ current_shape, chunk_shape = HDF5::FFI::CALL_LOCK.synchronize { [shape, chunks] }
525
537
  raise HDF5::Error, 'each_chunk requires a chunked dataset' unless chunk_shape
526
538
 
527
- current_shape = shape
528
539
  return if current_shape.any?(&:zero?)
529
540
 
530
541
  each_block_selection(current_shape, chunk_shape) do |selection|
@@ -534,25 +545,64 @@ module HDF5
534
545
 
535
546
  private
536
547
 
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
+
537
576
  def write_string(data, selection:)
538
- normalized_selection = Selection.normalize(selection, shape)
539
- values, values_shape = HDF5::StringCodec.normalize_data(data)
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:)
540
588
  unless values_shape == normalized_selection.result_shape
541
589
  raise HDF5::ShapeError,
542
590
  'Dataset shape must match string data shape'
543
591
  end
544
592
  return data if normalized_selection.size.zero?
545
593
 
546
- type_id = HDF5::FFI.H5Dget_type(@dataset_id)
547
- raise HDF5::Error, 'Failed to get dataset datatype' if type_id < 0
548
594
  unless HDF5::StringCodec.variable?(type_id)
549
595
  raise UnsupportedTypeError, 'Fixed-length string datasets are not yet supported'
550
596
  end
551
597
 
552
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
+
553
601
  select_hyperslab(file_space_id, normalized_selection)
554
602
  memory_space_id = create_memory_dataspace(normalized_selection.result_shape)
555
- buffer, _string_pointers = HDF5::StringCodec.buffer_for_values(values)
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:)
556
606
  status = HDF5::FFI.H5Dwrite(@dataset_id, type_id, memory_space_id, file_space_id,
557
607
  HDF5::DEFAULT_PROPERTY_LIST, buffer)
558
608
  raise HDF5::Error, 'Failed to write string dataset' if status < 0
@@ -572,25 +622,36 @@ module HDF5
572
622
  return Numo::RObject.new(*normalized_selection.result_shape) if normalized_selection.size.zero?
573
623
 
574
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
+
575
627
  select_hyperslab(file_space_id, normalized_selection)
576
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
+
577
631
  buffer = ::FFI::MemoryPointer.new(:pointer, normalized_selection.size)
578
632
  status = HDF5::FFI.H5Dread(@dataset_id, type_id, memory_space_id, file_space_id,
579
633
  HDF5::DEFAULT_PROPERTY_LIST, buffer)
580
634
  raise HDF5::Error, 'Failed to read string dataset' if status < 0
581
635
 
582
- HDF5::StringCodec.read_values(buffer, normalized_selection.size, normalized_selection.result_shape)
636
+ HDF5::StringCodec.read_values(buffer, normalized_selection.size, normalized_selection.result_shape,
637
+ encoding: HDF5::StringCodec.encoding_for(type_id))
583
638
  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'
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
590
653
  end
591
654
  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
655
  end
595
656
 
596
657
  def block_shape_for(dataset_shape, max_elements)
@@ -653,7 +714,7 @@ module HDF5
653
714
 
654
715
  prepend FileContext.guard(
655
716
  :attrs, :write, :dtype, :shape, :chunks, :maxshape, :fillvalue, :resize, :append, :read, :read_array,
656
- :[], :[]=, :read_into, :each_block, :each_chunk
717
+ :[], :[]=, :read_into, :close, :closed?
657
718
  )
658
719
  end
659
720
  end