ruby-hdf5 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4591bafb2585ec00df81fc3831cc7037cc02dee81f15919ec7cf1b5d2f08a4c2
4
- data.tar.gz: beb661f3c825adaab6b132cee69a402c1319d05143a0bec1516f2e0dec1180b2
3
+ metadata.gz: e7932dfc6c7cc1ffcfb6dc30c8963785e1e1e2ba206add86e634878d999e166d
4
+ data.tar.gz: cd6e121152d557821c64deb08a08286528911db013a48a0d7d7a1bff8c6da7a0
5
5
  SHA512:
6
- metadata.gz: bf13704291dfbdb2d0d61cf03504574a553e3d16e6db980355df5408ee425e340cfa1bc7b3850c4f877aa066958f20ab4cb5cbb07935f6302eb403771c6b4458
7
- data.tar.gz: ac8511ab873fea84933c38c65c15c899cfb3dbc7a63d2a062f41d78722bcc87288d84997e5f296c38691deb2660e065046e7fdf13e2c611127ef8fc84b1ce2ea
6
+ metadata.gz: f7270f19f0bd3732147faad5f94f0c429f02c93e81ad66985adefddff43ac41fbacb2834073d6b195c14f4ff905df4a60735ea143fee79fab1116a96f76c7e54
7
+ data.tar.gz: 060cb8b49f0373ca6ac788274c1bb5bc187fe92bab799b240f731034b59abebd789c956e23da6ccdbef4dd577e37be38e20121879ce98caea20c326829cb2e2e
data/README.md CHANGED
@@ -2,119 +2,122 @@
2
2
 
3
3
  [![test](https://github.com/red-data-tools/ruby-hdf5/actions/workflows/test.yml/badge.svg)](https://github.com/red-data-tools/ruby-hdf5/actions/workflows/test.yml)
4
4
 
5
- Ruby bindings for the HDF5 library.
5
+ Ruby bindings for HDF5 with Numo::NArray support.
6
6
 
7
- ## Scope
7
+ ## Requirements
8
8
 
9
- This gem currently provides practical high-level wrappers for:
10
-
11
- - opening and creating files
12
- - creating groups
13
- - creating, writing, and reading one-dimensional numeric datasets
14
- - reading and writing numeric attributes
15
-
16
- Unsupported at this stage:
17
-
18
- - string dataset read/write
19
- - multidimensional array write
20
-
21
- Integer datasets and integer attributes currently use native C `int` under the hood.
22
- Values outside that range are rejected with `HDF5::Error` to avoid silent overflow.
23
-
24
- `Group#list_datasets` filters datasets from group entries by checking object type per entry.
25
- For very large groups, this may be slower than `Group#list_entries`.
26
-
27
- ## Supported HDF5 Versions
28
-
29
- - HDF5 1.10
30
- - HDF5 1.14
31
- - HDF5 2.x
32
-
33
- HDF5 versions older than 1.10 are not supported.
9
+ - Ruby 3.4 or later
10
+ - HDF5 1.10 or later (`libhdf5` shared library)
34
11
 
35
12
  ## Install
36
13
 
37
- Add to your Gemfile:
14
+ Add the gem to your Gemfile:
38
15
 
39
16
  ```ruby
40
17
  gem 'ruby-hdf5'
41
18
  ```
42
19
 
43
- Install:
20
+ Then install dependencies:
44
21
 
45
22
  ```sh
46
23
  bundle install
47
24
  ```
48
25
 
49
- System library (`libhdf5`) is required.
50
-
51
- ## Runtime Notes
52
-
53
- - The gem loads `libhdf5` through FFI.
54
- - If the shared library cannot be found automatically, set `HDF5_LIB_PATH`.
55
-
56
- Examples:
26
+ Set `HDF5_LIB_PATH` only when `libhdf5` cannot be found automatically. It may be a library directory or a shared-library path.
57
27
 
58
28
  ```sh
59
- # Point to a directory containing libhdf5.so
60
- export HDF5_LIB_PATH=/usr/lib
61
-
62
- # Or point directly to the shared object
63
29
  export HDF5_LIB_PATH=/usr/lib/libhdf5.so
64
30
  ```
65
31
 
66
- ## Quick Start
32
+ ## Usage
67
33
 
68
- ### Read an existing file
34
+ Create a file and write a Numo array:
69
35
 
70
36
  ```ruby
71
37
  require 'hdf5'
72
38
 
73
- HDF5::File.open('example.h5') do |file|
74
- group = file['foo']
75
- dataset = group['bar_int']
39
+ matrix = Numo::SFloat.new(100, 64).seq
40
+
41
+ HDF5::File.open('numbers.h5', 'w') do |file|
42
+ dataset = file.require_group('measurements').create_dataset('signal', matrix)
43
+ dataset.attrs['unit'] = 'a.u.'
44
+ end
45
+ ```
46
+
47
+ Read data and inspect its type:
48
+
49
+ ```ruby
50
+ HDF5::File.open('numbers.h5') do |file|
51
+ dataset = file['measurements/signal']
76
52
  p dataset.shape
77
- p dataset.dtype
53
+ p dataset.dtype.to_sym
78
54
  p dataset.read
79
55
  end
80
56
  ```
81
57
 
82
- ### Create and write a file
58
+ Use a block with `HDF5::File.open` to close the file automatically. Supported modes are `r`, `r+`, `w`, `x`, and `a`.
83
59
 
84
- ```ruby
85
- require 'hdf5'
60
+ ## Common Tasks
86
61
 
87
- HDF5::File.create('numbers.h5') do |file|
88
- file.create_group('values') do |group|
89
- group.create_dataset('ints', [1, 2, 3, 4])
90
- end
91
- end
62
+ Read a row, a column, or a strided selection without reading the complete dataset:
92
63
 
93
- reopened = HDF5::File.open('numbers.h5')
94
- p reopened['values']['ints'].read
95
- reopened.close
64
+ ```ruby
65
+ HDF5::File.open('numbers.h5') do |file|
66
+ dataset = file['measurements/signal']
67
+ row = dataset[10, true]
68
+ column = dataset[true, 0]
69
+ every_tenth_row = dataset[HDF5.slice(0...100, step: 10), true]
70
+ end
96
71
  ```
97
72
 
98
- ## Error Handling
73
+ Append rows to an extendible dataset. Extendible datasets must use chunked storage:
74
+
75
+ ```ruby
76
+ HDF5::File.open('samples.h5', 'w') do |file|
77
+ samples = file.create_dataset(
78
+ 'samples',
79
+ shape: [0, 2],
80
+ dtype: :float32,
81
+ maxshape: [nil, 2],
82
+ chunks: [256, 2]
83
+ )
84
+ samples.append(Numo::SFloat[[1.0, 2.0], [3.0, 4.0]])
85
+ end
86
+ ```
99
87
 
100
- High-level API failures raise `HDF5::Error`.
88
+ Process a dataset in bounded-memory blocks:
101
89
 
102
90
  ```ruby
103
- begin
104
- HDF5::File.open('missing.h5')
105
- rescue HDF5::Error => e
106
- warn e.message
91
+ sum = 0.0
92
+
93
+ HDF5::File.open('numbers.h5') do |file|
94
+ file['measurements/signal'].each_block(max_bytes: 4 * 1024 * 1024) do |_selection, block|
95
+ sum += block.sum
96
+ end
107
97
  end
108
98
  ```
109
99
 
110
- ## Development
100
+ ## Main Operations
101
+
102
+ - Hierarchy: `[]`, `create_group`, `require_group`, `keys`, `delete`, `move`
103
+ - Datasets: `create_dataset`, `read`, `write`, `[]`, `[]=`, `read_into`
104
+ - Dataset metadata: `shape`, `ndim`, `size`, `dtype`, `chunks`, `maxshape`, `fillvalue`
105
+ - Storage: chunking, gzip, shuffle, Fletcher32, resize, and append
106
+ - Iteration: `each_block(max_bytes:)` and `each_chunk`
107
+ - Attributes: `attrs[]`, `attrs[]=`, `attrs.create`, `attrs.modify`, `attrs.delete`
108
+
109
+ Datasets support Numo numeric arrays, scalar values, variable-length UTF-8 strings, h5py-compatible bool values, and h5py-compatible complex values.
110
+
111
+ ## Limitations
111
112
 
112
- After more than a decade, it is clear that the Ruby community does not have enough resources to sustainably maintain an HDF5 library. For that reason, development of this library is intentionally AI-assisted. Something is better than nothing.
113
+ - Fixed-length strings, general compound / enum / reference types, and variable-length numeric types are unsupported.
114
+ - Fancy indexing, boolean masks, negative slice steps, and general broadcasting are unsupported.
115
+ - SWMR, MPI, and VDS creation are unsupported.
113
116
 
114
- ## Acknowledgement
117
+ ## Examples
115
118
 
116
- [https://github.com/edmundhighcock/hdf5](https://github.com/edmundhighcock/hdf5)
119
+ See [examples/README.md](examples/README.md) for standalone examples, ordered from basic file I/O through chunking, resizing, and links.
117
120
 
118
121
  ## License
119
122
 
120
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
123
+ MIT. See [LICENSE.txt](LICENSE.txt).
@@ -1,43 +1,34 @@
1
1
  module HDF5
2
2
  class Attribute
3
- def initialize(dataset_id, attr_name)
3
+ def initialize(dataset_id, attr_name, context = nil)
4
4
  @dataset_id = dataset_id
5
5
  @attr_name = attr_name
6
+ context&.ensure_open!(dataset_id)
6
7
  @attr_id = HDF5::FFI.H5Aopen(@dataset_id, @attr_name, HDF5::DEFAULT_PROPERTY_LIST)
7
8
  raise HDF5::Error, 'Failed to open attribute' if @attr_id < 0
8
9
  end
9
10
 
10
11
  def read
11
12
  type_id = HDF5::FFI.H5Aget_type(@attr_id)
12
- space_id = HDF5::FFI.H5Aget_space(@attr_id)
13
-
14
- size = HDF5::FFI.H5Sget_simple_extent_npoints(space_id)
15
-
16
- buffer = \
17
- case HDF5::FFI.H5Tget_class(type_id)
18
- when :H5T_INTEGER
19
- ::FFI::MemoryPointer.new(:int, size)
20
- when :H5T_FLOAT
21
- ::FFI::MemoryPointer.new(:double, size)
22
- when :H5T_STRING
23
- ::FFI::MemoryPointer.new(:pointer, size)
24
- else
25
- raise HDF5::Error, 'Unsupported data type'
26
- end
13
+ raise HDF5::Error, 'Failed to get attribute datatype' if type_id < 0
27
14
 
28
- status = HDF5::FFI.H5Aread(@attr_id, type_id, buffer)
15
+ space_id = HDF5::FFI.H5Aget_space(@attr_id)
16
+ raise HDF5::Error, 'Failed to get attribute dataspace' if space_id < 0
17
+ return read_string(type_id, space_id) if HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
18
+
19
+ dtype_object = DType.for_hdf5(type_id)
20
+ attribute_shape = shape(space_id)
21
+ size = attribute_shape.empty? ? 1 : attribute_shape.inject(:*)
22
+ buffer = ::FFI::MemoryPointer.new(:char, size * dtype_object.itemsize)
23
+ status = HDF5::FFI.H5Aread(@attr_id, dtype_object.memory_type_id, buffer)
29
24
  raise HDF5::Error, 'Failed to read attribute' if status < 0
30
25
 
31
- case HDF5::FFI.H5Tget_class(type_id)
32
- when :H5T_INTEGER
33
- buffer.read_array_of_int(size)
34
- when :H5T_FLOAT
35
- buffer.read_array_of_double(size)
36
- when :H5T_STRING
37
- buffer.read_pointer.read_string
38
- else
39
- raise HDF5::Error, 'Unsupported data type'
40
- end
26
+ result = HDF5::DataHelpers.from_binary(dtype_object, buffer.read_bytes(size * dtype_object.itemsize),
27
+ attribute_shape)
28
+ return result unless attribute_shape.empty?
29
+
30
+ scalar = result.extract
31
+ dtype_object.kind == :bool ? !scalar.zero? : scalar
41
32
  ensure
42
33
  HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
43
34
  HDF5::FFI.H5Sclose(space_id) if space_id && space_id >= 0
@@ -49,15 +40,54 @@ module HDF5
49
40
  HDF5::FFI.H5Aclose(@attr_id)
50
41
  @attr_id = nil
51
42
  end
43
+
44
+ private
45
+
46
+ def shape(space_id)
47
+ rank = HDF5::FFI.H5Sget_simple_extent_ndims(space_id)
48
+ raise HDF5::Error, 'Failed to get attribute rank' if rank < 0
49
+ return [] if rank.zero?
50
+
51
+ dimensions = ::FFI::MemoryPointer.new(:ulong_long, rank)
52
+ status = HDF5::FFI.H5Sget_simple_extent_dims(space_id, dimensions, nil)
53
+ raise HDF5::Error, 'Failed to get attribute shape' if status < 0
54
+
55
+ dimensions.read_array_of_uint64(rank)
56
+ end
57
+
58
+ def read_string(type_id, space_id)
59
+ unless HDF5::StringCodec.variable?(type_id)
60
+ raise UnsupportedTypeError, 'Fixed-length string attributes are not yet supported'
61
+ end
62
+
63
+ attribute_shape = shape(space_id)
64
+ count = attribute_shape.empty? ? 1 : attribute_shape.inject(:*)
65
+ buffer = ::FFI::MemoryPointer.new(:pointer, count)
66
+ status = HDF5::FFI.H5Aread(@attr_id, type_id, buffer)
67
+ raise HDF5::Error, 'Failed to read string attribute' if status < 0
68
+
69
+ HDF5::StringCodec.read_values(buffer, count, attribute_shape)
70
+ ensure
71
+ if buffer
72
+ active_error = $ERROR_INFO
73
+ reclaim_status = HDF5::FFI.H5Dvlen_reclaim(type_id, space_id, HDF5::DEFAULT_PROPERTY_LIST, buffer)
74
+ if reclaim_status.negative? && active_error.nil?
75
+ raise HDF5::Error,
76
+ 'Failed to reclaim variable-length string attribute'
77
+ end
78
+ end
79
+ end
52
80
  end
53
81
 
54
82
  class AttributeManager
55
- def initialize(dataset_id)
83
+ def initialize(dataset_id, context = nil)
56
84
  @dataset_id = dataset_id
85
+ @context = context
57
86
  end
58
87
 
59
88
  def [](attr_name)
60
- attr = Attribute.new(@dataset_id, attr_name)
89
+ @context&.ensure_open!(@dataset_id)
90
+ attr = Attribute.new(@dataset_id, attr_name, @context)
61
91
  attr.read
62
92
  ensure
63
93
  attr.close if attr
@@ -67,9 +97,92 @@ module HDF5
67
97
  write(attr_name, value)
68
98
  end
69
99
 
100
+ def keys
101
+ @context&.ensure_open!(@dataset_id)
102
+ names = []
103
+ index = ::FFI::MemoryPointer.new(:ulong_long)
104
+ index.write_ulong_long(0)
105
+ callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
106
+ names << name
107
+ 0
108
+ end
109
+ status = HDF5::FFI.H5Aiterate2(@dataset_id, :H5_INDEX_NAME, :H5_ITER_NATIVE, index, callback, nil)
110
+ raise HDF5::Error, 'Failed to iterate over attributes' if status < 0
111
+
112
+ names
113
+ end
114
+
115
+ def key?(attr_name)
116
+ @context&.ensure_open!(@dataset_id)
117
+ exists = HDF5::FFI.H5Aexists(@dataset_id, attr_name)
118
+ raise HDF5::Error, "Failed to check attribute existence: #{attr_name}" if exists.negative?
119
+
120
+ exists.positive?
121
+ end
122
+
123
+ def delete(attr_name)
124
+ @context&.ensure_open!(@dataset_id)
125
+ status = HDF5::FFI.H5Adelete(@dataset_id, attr_name)
126
+ raise HDF5::Error, "Failed to delete attribute: #{attr_name}" if status < 0
127
+
128
+ self
129
+ end
130
+
131
+ def create(attr_name, value)
132
+ raise HDF5::Error, "Attribute already exists: #{attr_name}" if key?(attr_name)
133
+
134
+ write(attr_name, value)
135
+ end
136
+
137
+ def modify(attr_name, value)
138
+ @context&.ensure_open!(@dataset_id)
139
+ raise HDF5::Error, "Attribute not found: #{attr_name}" unless key?(attr_name)
140
+
141
+ attr_id = HDF5::FFI.H5Aopen(@dataset_id, attr_name, HDF5::DEFAULT_PROPERTY_LIST)
142
+ raise HDF5::Error, "Failed to open attribute: #{attr_name}" if attr_id < 0
143
+
144
+ type_id = HDF5::FFI.H5Aget_type(attr_id)
145
+ space_id = HDF5::FFI.H5Aget_space(attr_id)
146
+ raise HDF5::Error, "Failed to inspect attribute: #{attr_name}" if type_id < 0 || space_id < 0
147
+
148
+ if HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
149
+ unless HDF5::StringCodec.variable?(type_id)
150
+ raise UnsupportedTypeError, 'Fixed-length string attributes are not yet supported'
151
+ end
152
+
153
+ string_values, string_shape = HDF5::StringCodec.normalize_data(value)
154
+ unless string_shape == attribute_shape(space_id)
155
+ raise HDF5::ShapeError,
156
+ 'Attribute shape must not change when modifying'
157
+ end
158
+
159
+ buffer, _string_pointers = HDF5::StringCodec.buffer_for_values(string_values)
160
+ status = HDF5::FFI.H5Awrite(attr_id, type_id, buffer)
161
+ else
162
+ dtype_object = DType.for_hdf5(type_id)
163
+ values = HDF5::DataHelpers.normalize_data(value, label: 'Attribute data')
164
+ expected_shape = attribute_shape(space_id)
165
+ raise HDF5::Error, 'Attribute shape must not change when modifying' unless values.shape == expected_shape
166
+
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))
169
+ end
170
+ raise HDF5::Error, "Failed to modify attribute: #{attr_name}" if status < 0
171
+
172
+ value
173
+ ensure
174
+ HDF5::FFI.H5Sclose(space_id) if space_id && space_id >= 0
175
+ HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
176
+ HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
177
+ end
178
+
70
179
  def write(attr_name, value)
71
- values = normalize_data(value)
72
- datatype_id = datatype_id_for(values)
180
+ @context&.ensure_open!(@dataset_id)
181
+ string_data = HDF5::StringCodec.string_data?(value)
182
+ 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
185
+ type_id = string_data ? HDF5::StringCodec.datatype_id : dtype_object.storage_type_id
73
186
 
74
187
  exists = HDF5::FFI.H5Aexists(@dataset_id, attr_name)
75
188
  raise HDF5::Error, "Failed to check attribute existence: #{attr_name}" if exists.negative?
@@ -79,78 +192,57 @@ module HDF5
79
192
  raise HDF5::Error, "Failed to replace attribute: #{attr_name}" if status < 0
80
193
  end
81
194
 
82
- dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
83
- dims.write_array_of_ulong_long([values.length])
84
- dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
195
+ dataspace_id = create_dataspace(string_data ? string_shape : values.shape)
85
196
  raise HDF5::Error, 'Failed to create attribute dataspace' if dataspace_id < 0
86
197
 
87
198
  attr_id = HDF5::FFI.H5Acreate2(
88
199
  @dataset_id,
89
200
  attr_name,
90
- datatype_id,
201
+ type_id,
91
202
  dataspace_id,
92
203
  HDF5::DEFAULT_PROPERTY_LIST,
93
204
  HDF5::DEFAULT_PROPERTY_LIST
94
205
  )
95
206
  raise HDF5::Error, "Failed to create attribute: #{attr_name}" if attr_id < 0
96
207
 
97
- buffer = buffer_for(values)
98
- status = HDF5::FFI.H5Awrite(attr_id, datatype_id, buffer)
208
+ buffer, _string_pointers = if string_data
209
+ HDF5::StringCodec.buffer_for_values(string_values)
210
+ else
211
+ [HDF5::DataHelpers.buffer_for(values), nil]
212
+ end
213
+ memory_type_id = string_data ? type_id : dtype_object.memory_type_id
214
+ status = HDF5::FFI.H5Awrite(attr_id, memory_type_id, buffer)
99
215
  raise HDF5::Error, "Failed to write attribute: #{attr_name}" if status < 0
100
216
 
101
217
  value
102
218
  ensure
103
219
  HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
104
220
  HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
221
+ HDF5::FFI.H5Tclose(type_id) if string_data && type_id && type_id >= 0
105
222
  end
106
223
 
107
224
  private
108
225
 
109
- def normalize_data(value)
110
- values = value.is_a?(Array) ? value : [value]
111
- raise HDF5::Error, 'Attribute data must not be empty' if values.empty?
112
- raise HDF5::Error, 'Nested arrays are not supported for attributes' if values.any? { |item| item.is_a?(Array) }
226
+ def attribute_shape(space_id)
227
+ rank = HDF5::FFI.H5Sget_simple_extent_ndims(space_id)
228
+ raise HDF5::Error, 'Failed to get attribute rank' if rank < 0
229
+ return [] if rank.zero?
113
230
 
114
- values
115
- end
231
+ dimensions = ::FFI::MemoryPointer.new(:ulong_long, rank)
232
+ status = HDF5::FFI.H5Sget_simple_extent_dims(space_id, dimensions, nil)
233
+ raise HDF5::Error, 'Failed to get attribute shape' if status < 0
116
234
 
117
- def datatype_id_for(values)
118
- if values.all? { |item| item.is_a?(Integer) }
119
- validate_native_int_range!(values)
120
- HDF5::FFI.H5T_NATIVE_INT
121
- elsif values.all? { |item| item.is_a?(Numeric) }
122
- HDF5::FFI.H5T_NATIVE_DOUBLE
123
- else
124
- raise HDF5::Error, 'Only numeric attribute data is supported'
125
- end
235
+ dimensions.read_array_of_uint64(rank)
126
236
  end
127
237
 
128
- def buffer_for(values)
129
- if values.all? { |item| item.is_a?(Integer) }
130
- buffer = ::FFI::MemoryPointer.new(:int, values.length)
131
- buffer.write_array_of_int(values)
132
- else
133
- buffer = ::FFI::MemoryPointer.new(:double, values.length)
134
- buffer.write_array_of_double(values.map(&:to_f))
135
- end
136
-
137
- buffer
138
- end
238
+ def create_dataspace(shape)
239
+ return HDF5::FFI.H5Screate(:H5S_SCALAR) if shape.empty?
139
240
 
140
- def native_int_bounds
141
- bits = ::FFI.type_size(:int) * 8
142
- max = (1 << (bits - 1)) - 1
143
- min = -(1 << (bits - 1))
144
- [min, max]
241
+ dimensions = ::FFI::MemoryPointer.new(:ulong_long, shape.length)
242
+ dimensions.write_array_of_ulong_long(shape)
243
+ HDF5::FFI.H5Screate_simple(shape.length, dimensions, nil)
145
244
  end
146
245
 
147
- def validate_native_int_range!(values)
148
- min, max = native_int_bounds
149
- out_of_range = values.find { |value| value < min || value > max }
150
- return unless out_of_range
151
-
152
- raise HDF5::Error,
153
- "Integer value #{out_of_range} is outside native int range (#{min}..#{max}). Use a smaller value."
154
- end
246
+ prepend FileContext.guard(:[], :[]=, :keys, :key?, :delete, :create, :modify, :write)
155
247
  end
156
248
  end
@@ -0,0 +1,54 @@
1
+ module HDF5
2
+ module DataHelpers
3
+ module_function
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
22
+ normalized = dtype.kind == :bool ? normalize_booleans(data) : data
23
+ dtype.numo_class.cast(normalized)
24
+ end
25
+
26
+ def buffer_for(narray)
27
+ dtype = DType.for_numo(narray)
28
+ binary = if dtype.kind == :bool
29
+ narray.to_a.flatten.map { |value| value.zero? ? 0 : 1 }.pack('C*')
30
+ else
31
+ narray.to_binary
32
+ end
33
+ expected_bytes = narray.size * dtype.itemsize
34
+ raise HDF5::Error, 'Numo binary representation has an unexpected size' unless binary.bytesize == expected_bytes
35
+
36
+ ::FFI::MemoryPointer.new(:char, expected_bytes).tap { |buffer| buffer.put_bytes(0, binary) }
37
+ end
38
+
39
+ def from_binary(dtype, binary, shape)
40
+ return dtype.numo_class.from_binary(binary, shape) unless dtype.kind == :bool
41
+
42
+ bytes = binary.unpack('C*')
43
+ return Numo::Bit.new.store(bytes.first) if shape.empty?
44
+
45
+ Numo::UInt8.cast(bytes).reshape(*shape).ne(0)
46
+ end
47
+
48
+ def normalize_booleans(value)
49
+ return value.map { |item| normalize_booleans(item) } if value.is_a?(Array)
50
+
51
+ value ? 1 : 0
52
+ end
53
+ end
54
+ end