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.
@@ -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