ruby-hdf5 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +72 -69
- data/lib/hdf5/attribute.rb +180 -73
- data/lib/hdf5/data_helpers.rb +190 -0
- data/lib/hdf5/dataset.rb +622 -101
- data/lib/hdf5/dtype.rb +214 -0
- data/lib/hdf5/ffi.rb +48 -10
- data/lib/hdf5/ffi_10.rb +0 -485
- data/lib/hdf5/ffi_14.rb +0 -479
- data/lib/hdf5/ffi_20.rb +9697 -0
- data/lib/hdf5/file.rb +86 -10
- data/lib/hdf5/file_context.rb +70 -0
- data/lib/hdf5/group.rb +59 -15
- data/lib/hdf5/hierarchy.rb +107 -0
- data/lib/hdf5/selection.rb +97 -0
- data/lib/hdf5/string_codec.rb +106 -0
- data/lib/hdf5/version.rb +1 -1
- data/lib/hdf5.rb +31 -0
- metadata +24 -3
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
module HDF5
|
|
2
|
+
module DataHelpers
|
|
3
|
+
module_function
|
|
4
|
+
|
|
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:)
|
|
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
|
+
|
|
44
|
+
dtype.numo_class.cast(normalized)
|
|
45
|
+
end
|
|
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
|
+
|
|
162
|
+
def buffer_for(narray)
|
|
163
|
+
dtype = DType.for_numo(narray)
|
|
164
|
+
binary = if dtype.kind == :bool
|
|
165
|
+
narray.to_a.flatten.map { |value| value.zero? ? 0 : 1 }.pack('C*')
|
|
166
|
+
else
|
|
167
|
+
narray.to_binary
|
|
168
|
+
end
|
|
169
|
+
expected_bytes = narray.size * dtype.itemsize
|
|
170
|
+
raise HDF5::Error, 'Numo binary representation has an unexpected size' unless binary.bytesize == expected_bytes
|
|
171
|
+
|
|
172
|
+
::FFI::MemoryPointer.new(:char, expected_bytes).tap { |buffer| buffer.put_bytes(0, binary) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def from_binary(dtype, binary, shape)
|
|
176
|
+
return dtype.numo_class.from_binary(binary, shape) unless dtype.kind == :bool
|
|
177
|
+
|
|
178
|
+
bytes = binary.unpack('C*')
|
|
179
|
+
return Numo::Bit.new.store(bytes.first) if shape.empty?
|
|
180
|
+
|
|
181
|
+
Numo::UInt8.cast(bytes).reshape(*shape).ne(0)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def normalize_booleans(value)
|
|
185
|
+
return value.map { |item| normalize_booleans(item) } if value.is_a?(Array)
|
|
186
|
+
|
|
187
|
+
value ? 1 : 0
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|