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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e7932dfc6c7cc1ffcfb6dc30c8963785e1e1e2ba206add86e634878d999e166d
|
|
4
|
+
data.tar.gz: cd6e121152d557821c64deb08a08286528911db013a48a0d7d7a1bff8c6da7a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7270f19f0bd3732147faad5f94f0c429f02c93e81ad66985adefddff43ac41fbacb2834073d6b195c14f4ff905df4a60735ea143fee79fab1116a96f76c7e54
|
|
7
|
+
data.tar.gz: 060cb8b49f0373ca6ac788274c1bb5bc187fe92bab799b240f731034b59abebd789c956e23da6ccdbef4dd577e37be38e20121879ce98caea20c326829cb2e2e
|
data/README.md
CHANGED
|
@@ -2,114 +2,122 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/red-data-tools/ruby-hdf5/actions/workflows/test.yml)
|
|
4
4
|
|
|
5
|
-
Ruby bindings for
|
|
5
|
+
Ruby bindings for HDF5 with Numo::NArray support.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Requirements
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- opening and creating files
|
|
12
|
-
- creating groups
|
|
13
|
-
- creating, writing, and reading one-dimensional numeric datasets
|
|
14
|
-
- reading attributes
|
|
15
|
-
|
|
16
|
-
Unsupported at this stage:
|
|
17
|
-
|
|
18
|
-
- string dataset read/write
|
|
19
|
-
- attribute write
|
|
20
|
-
- multidimensional array write
|
|
21
|
-
|
|
22
|
-
## Supported HDF5 Versions
|
|
23
|
-
|
|
24
|
-
- HDF5 1.10
|
|
25
|
-
- HDF5 1.14
|
|
26
|
-
- HDF5 2.x
|
|
27
|
-
|
|
28
|
-
HDF5 versions older than 1.10 are not supported.
|
|
9
|
+
- Ruby 3.4 or later
|
|
10
|
+
- HDF5 1.10 or later (`libhdf5` shared library)
|
|
29
11
|
|
|
30
12
|
## Install
|
|
31
13
|
|
|
32
|
-
Add to your Gemfile:
|
|
14
|
+
Add the gem to your Gemfile:
|
|
33
15
|
|
|
34
16
|
```ruby
|
|
35
17
|
gem 'ruby-hdf5'
|
|
36
18
|
```
|
|
37
19
|
|
|
38
|
-
|
|
20
|
+
Then install dependencies:
|
|
39
21
|
|
|
40
22
|
```sh
|
|
41
23
|
bundle install
|
|
42
24
|
```
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## Runtime Notes
|
|
47
|
-
|
|
48
|
-
- The gem loads `libhdf5` through FFI.
|
|
49
|
-
- If the shared library cannot be found automatically, set `HDF5_LIB_PATH`.
|
|
50
|
-
|
|
51
|
-
Examples:
|
|
26
|
+
Set `HDF5_LIB_PATH` only when `libhdf5` cannot be found automatically. It may be a library directory or a shared-library path.
|
|
52
27
|
|
|
53
28
|
```sh
|
|
54
|
-
# Point to a directory containing libhdf5.so
|
|
55
|
-
export HDF5_LIB_PATH=/usr/lib
|
|
56
|
-
|
|
57
|
-
# Or point directly to the shared object
|
|
58
29
|
export HDF5_LIB_PATH=/usr/lib/libhdf5.so
|
|
59
30
|
```
|
|
60
31
|
|
|
61
|
-
##
|
|
32
|
+
## Usage
|
|
62
33
|
|
|
63
|
-
|
|
34
|
+
Create a file and write a Numo array:
|
|
64
35
|
|
|
65
36
|
```ruby
|
|
66
37
|
require 'hdf5'
|
|
67
38
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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']
|
|
71
52
|
p dataset.shape
|
|
72
|
-
p dataset.dtype
|
|
53
|
+
p dataset.dtype.to_sym
|
|
73
54
|
p dataset.read
|
|
74
55
|
end
|
|
75
56
|
```
|
|
76
57
|
|
|
77
|
-
|
|
58
|
+
Use a block with `HDF5::File.open` to close the file automatically. Supported modes are `r`, `r+`, `w`, `x`, and `a`.
|
|
78
59
|
|
|
79
|
-
|
|
80
|
-
require 'hdf5'
|
|
60
|
+
## Common Tasks
|
|
81
61
|
|
|
82
|
-
|
|
83
|
-
file.create_group('values') do |group|
|
|
84
|
-
group.create_dataset('ints', [1, 2, 3, 4])
|
|
85
|
-
end
|
|
86
|
-
end
|
|
62
|
+
Read a row, a column, or a strided selection without reading the complete dataset:
|
|
87
63
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
91
71
|
```
|
|
92
72
|
|
|
93
|
-
|
|
73
|
+
Append rows to an extendible dataset. Extendible datasets must use chunked storage:
|
|
94
74
|
|
|
95
|
-
|
|
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
|
+
```
|
|
87
|
+
|
|
88
|
+
Process a dataset in bounded-memory blocks:
|
|
96
89
|
|
|
97
90
|
```ruby
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
102
97
|
end
|
|
103
98
|
```
|
|
104
99
|
|
|
105
|
-
##
|
|
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
|
|
106
112
|
|
|
107
|
-
|
|
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.
|
|
108
116
|
|
|
109
|
-
##
|
|
117
|
+
## Examples
|
|
110
118
|
|
|
111
|
-
[
|
|
119
|
+
See [examples/README.md](examples/README.md) for standalone examples, ordered from basic file I/O through chunking, resizing, and links.
|
|
112
120
|
|
|
113
121
|
## License
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/lib/hdf5/attribute.rb
CHANGED
|
@@ -1,60 +1,248 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
6
|
+
context&.ensure_open!(dataset_id)
|
|
7
|
+
@attr_id = HDF5::FFI.H5Aopen(@dataset_id, @attr_name, HDF5::DEFAULT_PROPERTY_LIST)
|
|
8
|
+
raise HDF5::Error, 'Failed to open attribute' if @attr_id < 0
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def read
|
|
11
|
-
type_id = FFI.H5Aget_type(@attr_id)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
size = FFI.H5Sget_simple_extent_npoints(space_id)
|
|
15
|
-
|
|
16
|
-
buffer = \
|
|
17
|
-
case 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 'Unsupported data type'
|
|
26
|
-
end
|
|
12
|
+
type_id = HDF5::FFI.H5Aget_type(@attr_id)
|
|
13
|
+
raise HDF5::Error, 'Failed to get attribute datatype' if type_id < 0
|
|
27
14
|
|
|
28
|
-
|
|
29
|
-
raise 'Failed to
|
|
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
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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)
|
|
24
|
+
raise HDF5::Error, 'Failed to read attribute' if status < 0
|
|
25
|
+
|
|
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
|
|
32
|
+
ensure
|
|
33
|
+
HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
|
|
34
|
+
HDF5::FFI.H5Sclose(space_id) if space_id && space_id >= 0
|
|
41
35
|
end
|
|
42
36
|
|
|
43
37
|
def close
|
|
44
|
-
|
|
38
|
+
return if @attr_id.nil?
|
|
39
|
+
|
|
40
|
+
HDF5::FFI.H5Aclose(@attr_id)
|
|
41
|
+
@attr_id = nil
|
|
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
|
|
45
79
|
end
|
|
46
80
|
end
|
|
47
81
|
|
|
48
82
|
class AttributeManager
|
|
49
|
-
def initialize(dataset_id)
|
|
83
|
+
def initialize(dataset_id, context = nil)
|
|
50
84
|
@dataset_id = dataset_id
|
|
85
|
+
@context = context
|
|
51
86
|
end
|
|
52
87
|
|
|
53
88
|
def [](attr_name)
|
|
54
|
-
|
|
89
|
+
@context&.ensure_open!(@dataset_id)
|
|
90
|
+
attr = Attribute.new(@dataset_id, attr_name, @context)
|
|
55
91
|
attr.read
|
|
56
92
|
ensure
|
|
57
93
|
attr.close if attr
|
|
58
94
|
end
|
|
95
|
+
|
|
96
|
+
def []=(attr_name, value)
|
|
97
|
+
write(attr_name, value)
|
|
98
|
+
end
|
|
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
|
+
|
|
179
|
+
def write(attr_name, value)
|
|
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
|
|
186
|
+
|
|
187
|
+
exists = HDF5::FFI.H5Aexists(@dataset_id, attr_name)
|
|
188
|
+
raise HDF5::Error, "Failed to check attribute existence: #{attr_name}" if exists.negative?
|
|
189
|
+
|
|
190
|
+
if exists.positive?
|
|
191
|
+
status = HDF5::FFI.H5Adelete(@dataset_id, attr_name)
|
|
192
|
+
raise HDF5::Error, "Failed to replace attribute: #{attr_name}" if status < 0
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
dataspace_id = create_dataspace(string_data ? string_shape : values.shape)
|
|
196
|
+
raise HDF5::Error, 'Failed to create attribute dataspace' if dataspace_id < 0
|
|
197
|
+
|
|
198
|
+
attr_id = HDF5::FFI.H5Acreate2(
|
|
199
|
+
@dataset_id,
|
|
200
|
+
attr_name,
|
|
201
|
+
type_id,
|
|
202
|
+
dataspace_id,
|
|
203
|
+
HDF5::DEFAULT_PROPERTY_LIST,
|
|
204
|
+
HDF5::DEFAULT_PROPERTY_LIST
|
|
205
|
+
)
|
|
206
|
+
raise HDF5::Error, "Failed to create attribute: #{attr_name}" if attr_id < 0
|
|
207
|
+
|
|
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)
|
|
215
|
+
raise HDF5::Error, "Failed to write attribute: #{attr_name}" if status < 0
|
|
216
|
+
|
|
217
|
+
value
|
|
218
|
+
ensure
|
|
219
|
+
HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
|
|
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
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
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?
|
|
230
|
+
|
|
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
|
|
234
|
+
|
|
235
|
+
dimensions.read_array_of_uint64(rank)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def create_dataspace(shape)
|
|
239
|
+
return HDF5::FFI.H5Screate(:H5S_SCALAR) if shape.empty?
|
|
240
|
+
|
|
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)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
prepend FileContext.guard(:[], :[]=, :keys, :key?, :delete, :create, :modify, :write)
|
|
59
247
|
end
|
|
60
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
|