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.
- checksums.yaml +4 -4
- data/README.md +72 -64
- data/lib/hdf5/attribute.rb +222 -34
- data/lib/hdf5/data_helpers.rb +54 -0
- data/lib/hdf5/dataset.rb +553 -70
- data/lib/hdf5/dtype.rb +200 -0
- data/lib/hdf5/ffi.rb +48 -10
- data/lib/hdf5/ffi_20.rb +10213 -0
- data/lib/hdf5/file.rb +69 -11
- data/lib/hdf5/file_context.rb +70 -0
- data/lib/hdf5/group.rb +51 -21
- data/lib/hdf5/hierarchy.rb +107 -0
- data/lib/hdf5/selection.rb +81 -0
- data/lib/hdf5/string_codec.rb +93 -0
- data/lib/hdf5/version.rb +1 -1
- data/lib/hdf5.rb +31 -0
- metadata +24 -3
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
HDF5::
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
+
ensure_open!
|
|
219
|
+
@attrs ||= AttributeManager.new(@dataset_id, @context)
|
|
84
220
|
end
|
|
85
221
|
|
|
86
|
-
def write(data)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
472
|
+
array = value.to_a
|
|
473
|
+
flatten ? array.flatten : array
|
|
152
474
|
end
|
|
153
475
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
480
|
+
def []=(*selection, value)
|
|
481
|
+
write(value, selection: selection)
|
|
161
482
|
end
|
|
162
483
|
|
|
163
|
-
def
|
|
164
|
-
|
|
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
|
|
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
|