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 +4 -4
- data/README.md +72 -69
- data/lib/hdf5/attribute.rb +168 -76
- data/lib/hdf5/data_helpers.rb +54 -0
- data/lib/hdf5/dataset.rb +559 -99
- 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 +68 -10
- data/lib/hdf5/file_context.rb +70 -0
- data/lib/hdf5/group.rb +41 -15
- 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,72 +1,44 @@
|
|
|
1
1
|
module HDF5
|
|
2
2
|
class Dataset
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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)
|
|
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')
|
|
22
12
|
end
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
else
|
|
30
|
-
buffer = ::FFI::MemoryPointer.new(:double, data.length)
|
|
31
|
-
buffer.write_array_of_double(data.map(&:to_f))
|
|
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
|
|
32
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
|
|
33
25
|
|
|
34
|
-
|
|
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
|
|
26
|
+
raise HDF5::Error, 'Null datasets cannot have maxshape or chunks' if empty_data && (maxshape || chunks)
|
|
48
27
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private_constant :DataHelpers
|
|
55
|
-
|
|
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)
|
|
28
|
+
validate_maxshape(maxshape, shape) if maxshape
|
|
29
|
+
chunks = :auto if maxshape && chunks.nil?
|
|
30
|
+
dataspace_id = create_dataspace(shape, maxshape)
|
|
63
31
|
raise HDF5::Error, "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
|
|
64
32
|
|
|
33
|
+
dcpl_id = create_property_list(shape, dtype_object, chunks:, compression:, compression_opts:, shuffle:, fletcher32:,
|
|
34
|
+
fillvalue:)
|
|
35
|
+
|
|
65
36
|
dataset = from_id(
|
|
66
|
-
HDF5::FFI.H5Dcreate2(parent_id, name,
|
|
67
|
-
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
|
|
68
39
|
)
|
|
69
|
-
dataset.write(
|
|
40
|
+
dataset.write(data) if string_data
|
|
41
|
+
dataset.write(narray) if narray
|
|
70
42
|
return dataset unless block_given?
|
|
71
43
|
|
|
72
44
|
begin
|
|
@@ -74,12 +46,20 @@ module HDF5
|
|
|
74
46
|
ensure
|
|
75
47
|
dataset.close
|
|
76
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
|
|
77
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
|
|
78
58
|
HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
|
|
79
59
|
end
|
|
80
60
|
|
|
81
|
-
def open(parent_id, name)
|
|
82
|
-
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)
|
|
83
63
|
return dataset unless block_given?
|
|
84
64
|
|
|
85
65
|
begin
|
|
@@ -91,51 +71,223 @@ module HDF5
|
|
|
91
71
|
|
|
92
72
|
private
|
|
93
73
|
|
|
94
|
-
def
|
|
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)
|
|
88
|
+
end
|
|
89
|
+
|
|
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
|
|
98
|
+
end
|
|
99
|
+
raise HDF5::Error, 'maxshape dimensions must be nil or integers no smaller than shape' unless valid
|
|
100
|
+
end
|
|
101
|
+
|
|
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'
|
|
134
|
+
end
|
|
135
|
+
|
|
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
|
|
165
|
+
end
|
|
166
|
+
|
|
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
|
|
205
|
+
|
|
206
|
+
def from_id(dataset_id, name, context)
|
|
95
207
|
dataset = allocate
|
|
96
|
-
dataset.send(:initialize_from_id, dataset_id, name)
|
|
208
|
+
dataset.send(:initialize_from_id, dataset_id, name, context)
|
|
97
209
|
dataset
|
|
98
210
|
end
|
|
99
211
|
end
|
|
100
212
|
|
|
101
213
|
def initialize(parent_id, name)
|
|
102
|
-
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)
|
|
103
215
|
end
|
|
104
216
|
|
|
105
217
|
def attrs
|
|
106
|
-
|
|
218
|
+
ensure_open!
|
|
219
|
+
@attrs ||= AttributeManager.new(@dataset_id, @context)
|
|
107
220
|
end
|
|
108
221
|
|
|
109
|
-
def write(data)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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,
|
|
114
256
|
HDF5::DEFAULT_PROPERTY_LIST, buffer)
|
|
115
257
|
raise HDF5::Error, 'Failed to write dataset' if status < 0
|
|
116
258
|
|
|
117
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
|
|
118
263
|
end
|
|
119
264
|
|
|
120
265
|
def close
|
|
121
266
|
return if @dataset_id.nil?
|
|
122
267
|
|
|
123
|
-
HDF5::FFI.H5Dclose(@dataset_id)
|
|
268
|
+
@context ? @context.close(@dataset_id) : HDF5::FFI.H5Dclose(@dataset_id)
|
|
124
269
|
@dataset_id = nil
|
|
125
270
|
end
|
|
126
271
|
|
|
272
|
+
def closed?
|
|
273
|
+
@dataset_id.nil? || (@context && @context.closed?)
|
|
274
|
+
end
|
|
275
|
+
|
|
127
276
|
def dtype
|
|
277
|
+
ensure_open!
|
|
128
278
|
datatype_id = HDF5::FFI.H5Dget_type(@dataset_id)
|
|
129
279
|
raise HDF5::Error, 'Failed to get datatype' if datatype_id < 0
|
|
130
280
|
|
|
131
|
-
|
|
281
|
+
DType.for_hdf5(datatype_id)
|
|
132
282
|
ensure
|
|
133
283
|
HDF5::FFI.H5Tclose(datatype_id) if datatype_id && datatype_id >= 0
|
|
134
284
|
end
|
|
135
285
|
|
|
136
286
|
def shape
|
|
287
|
+
ensure_open!
|
|
137
288
|
dataspace_id = HDF5::FFI.H5Dget_space(@dataset_id)
|
|
138
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
|
|
139
291
|
|
|
140
292
|
ndims = HDF5::FFI.H5Sget_simple_extent_ndims(dataspace_id)
|
|
141
293
|
raise HDF5::Error, 'Failed to get number of dimensions' if ndims < 0
|
|
@@ -148,52 +300,360 @@ module HDF5
|
|
|
148
300
|
HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
|
|
149
301
|
end
|
|
150
302
|
|
|
151
|
-
def
|
|
152
|
-
|
|
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')
|
|
153
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
|
|
154
419
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
read_string_data(total_elements)
|
|
163
|
-
else
|
|
164
|
-
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'
|
|
165
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
|
|
466
|
+
end
|
|
467
|
+
|
|
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)
|
|
471
|
+
|
|
472
|
+
array = value.to_a
|
|
473
|
+
flatten ? array.flatten : array
|
|
166
474
|
end
|
|
167
475
|
|
|
168
|
-
def
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST, buffer)
|
|
172
|
-
raise HDF5::Error, 'Failed to read integer dataset' if status < 0
|
|
476
|
+
def [](*selection)
|
|
477
|
+
read(selection: selection)
|
|
478
|
+
end
|
|
173
479
|
|
|
174
|
-
|
|
480
|
+
def []=(*selection, value)
|
|
481
|
+
write(value, selection: selection)
|
|
175
482
|
end
|
|
176
483
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
182
493
|
|
|
183
|
-
|
|
494
|
+
destination.store(values)
|
|
184
495
|
end
|
|
185
496
|
|
|
186
|
-
def
|
|
187
|
-
|
|
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
|
|
188
533
|
end
|
|
189
534
|
|
|
190
535
|
private
|
|
191
536
|
|
|
192
|
-
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)
|
|
193
640
|
raise HDF5::Error, "Failed to open dataset: #{name}" if dataset_id < 0
|
|
194
641
|
|
|
195
642
|
@dataset_id = dataset_id
|
|
196
643
|
@name = name
|
|
644
|
+
@context = context
|
|
645
|
+
@context.register(dataset_id, :dataset) if @context
|
|
197
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
|
+
)
|
|
198
658
|
end
|
|
199
659
|
end
|