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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85ca4ff79d4a34553188284efb980628cd4f4931aa29d12bd37b09e0a89d3ca8
|
|
4
|
+
data.tar.gz: 1126e3458c88e00b00a6919ed832c35196673a5d785da524a218a9ff0772bd03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15f984a33a2c9036364e4bd4cef11acd041a1684f531cd30f05c235e764df5ff5a69cea2e7e727f0c34fd1438272673ed3764661bf1f0e19b0322f0879b4fea0
|
|
7
|
+
data.tar.gz: 9ef886f63b1c207e6c58e9b5a00462207d74239e522cb2aaeb9344237e93e14e6ad29326d3e79d7e9a1ea05b5a3d0215b0f5f5a250c412e9459a9810835f2aeb
|
data/README.md
CHANGED
|
@@ -2,119 +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 and writing numeric attributes
|
|
15
|
-
|
|
16
|
-
Unsupported at this stage:
|
|
17
|
-
|
|
18
|
-
- string dataset read/write
|
|
19
|
-
- multidimensional array write
|
|
20
|
-
|
|
21
|
-
Integer datasets and integer attributes currently use native C `int` under the hood.
|
|
22
|
-
Values outside that range are rejected with `HDF5::Error` to avoid silent overflow.
|
|
23
|
-
|
|
24
|
-
`Group#list_datasets` filters datasets from group entries by checking object type per entry.
|
|
25
|
-
For very large groups, this may be slower than `Group#list_entries`.
|
|
26
|
-
|
|
27
|
-
## Supported HDF5 Versions
|
|
28
|
-
|
|
29
|
-
- HDF5 1.10
|
|
30
|
-
- HDF5 1.14
|
|
31
|
-
- HDF5 2.x
|
|
32
|
-
|
|
33
|
-
HDF5 versions older than 1.10 are not supported.
|
|
9
|
+
- Ruby 3.4 or later
|
|
10
|
+
- HDF5 1.10 or later (`libhdf5` shared library)
|
|
34
11
|
|
|
35
12
|
## Install
|
|
36
13
|
|
|
37
|
-
Add to your Gemfile:
|
|
14
|
+
Add the gem to your Gemfile:
|
|
38
15
|
|
|
39
16
|
```ruby
|
|
40
17
|
gem 'ruby-hdf5'
|
|
41
18
|
```
|
|
42
19
|
|
|
43
|
-
|
|
20
|
+
Then install dependencies:
|
|
44
21
|
|
|
45
22
|
```sh
|
|
46
23
|
bundle install
|
|
47
24
|
```
|
|
48
25
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
## Runtime Notes
|
|
52
|
-
|
|
53
|
-
- The gem loads `libhdf5` through FFI.
|
|
54
|
-
- If the shared library cannot be found automatically, set `HDF5_LIB_PATH`.
|
|
55
|
-
|
|
56
|
-
Examples:
|
|
26
|
+
Set `HDF5_LIB_PATH` only when `libhdf5` cannot be found automatically. It may be a library directory or a shared-library path.
|
|
57
27
|
|
|
58
28
|
```sh
|
|
59
|
-
# Point to a directory containing libhdf5.so
|
|
60
|
-
export HDF5_LIB_PATH=/usr/lib
|
|
61
|
-
|
|
62
|
-
# Or point directly to the shared object
|
|
63
29
|
export HDF5_LIB_PATH=/usr/lib/libhdf5.so
|
|
64
30
|
```
|
|
65
31
|
|
|
66
|
-
##
|
|
32
|
+
## Usage
|
|
67
33
|
|
|
68
|
-
|
|
34
|
+
Create a file and write a Numo array:
|
|
69
35
|
|
|
70
36
|
```ruby
|
|
71
37
|
require 'hdf5'
|
|
72
38
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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']
|
|
76
52
|
p dataset.shape
|
|
77
|
-
p dataset.dtype
|
|
53
|
+
p dataset.dtype.to_sym
|
|
78
54
|
p dataset.read
|
|
79
55
|
end
|
|
80
56
|
```
|
|
81
57
|
|
|
82
|
-
|
|
58
|
+
Use a block with `HDF5::File.open` to close the file automatically. Supported modes are `r`, `r+`, `w`, `x`, and `a`.
|
|
83
59
|
|
|
84
|
-
|
|
85
|
-
require 'hdf5'
|
|
60
|
+
## Common Tasks
|
|
86
61
|
|
|
87
|
-
|
|
88
|
-
file.create_group('values') do |group|
|
|
89
|
-
group.create_dataset('ints', [1, 2, 3, 4])
|
|
90
|
-
end
|
|
91
|
-
end
|
|
62
|
+
Read a row, a column, or a strided selection without reading the complete dataset:
|
|
92
63
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
96
71
|
```
|
|
97
72
|
|
|
98
|
-
|
|
73
|
+
Append rows to an extendible dataset. Extendible datasets must use chunked storage:
|
|
74
|
+
|
|
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
|
+
```
|
|
99
87
|
|
|
100
|
-
|
|
88
|
+
Process a dataset in bounded-memory blocks:
|
|
101
89
|
|
|
102
90
|
```ruby
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
107
97
|
end
|
|
108
98
|
```
|
|
109
99
|
|
|
110
|
-
##
|
|
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
|
|
111
112
|
|
|
112
|
-
|
|
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.
|
|
113
116
|
|
|
114
|
-
##
|
|
117
|
+
## Examples
|
|
115
118
|
|
|
116
|
-
[
|
|
119
|
+
See [examples/README.md](examples/README.md) for standalone examples, ordered from basic file I/O through chunking, resizing, and links.
|
|
117
120
|
|
|
118
121
|
## License
|
|
119
122
|
|
|
120
|
-
|
|
123
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/lib/hdf5/attribute.rb
CHANGED
|
@@ -1,43 +1,36 @@
|
|
|
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
|
+
context&.ensure_open!(dataset_id)
|
|
6
7
|
@attr_id = HDF5::FFI.H5Aopen(@dataset_id, @attr_name, HDF5::DEFAULT_PROPERTY_LIST)
|
|
7
8
|
raise HDF5::Error, 'Failed to open attribute' if @attr_id < 0
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def read
|
|
11
12
|
type_id = HDF5::FFI.H5Aget_type(@attr_id)
|
|
13
|
+
raise HDF5::Error, 'Failed to get attribute datatype' if type_id < 0
|
|
14
|
+
|
|
12
15
|
space_id = HDF5::FFI.H5Aget_space(@attr_id)
|
|
16
|
+
raise HDF5::Error, 'Failed to get attribute dataspace' if space_id < 0
|
|
17
|
+
return HDF5::Empty.new(DType.for_hdf5(type_id)) if HDF5::FFI.H5Sget_simple_extent_type(space_id) == :H5S_NULL
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
buffer = \
|
|
17
|
-
case HDF5::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 HDF5::Error, 'Unsupported data type'
|
|
26
|
-
end
|
|
19
|
+
return read_string(type_id, space_id) if HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
|
|
27
20
|
|
|
28
|
-
|
|
21
|
+
dtype_object = DType.for_hdf5(type_id)
|
|
22
|
+
attribute_shape = shape(space_id)
|
|
23
|
+
size = attribute_shape.empty? ? 1 : attribute_shape.inject(:*)
|
|
24
|
+
buffer = ::FFI::MemoryPointer.new(:char, size * dtype_object.itemsize)
|
|
25
|
+
status = HDF5::FFI.H5Aread(@attr_id, dtype_object.memory_type_id, buffer)
|
|
29
26
|
raise HDF5::Error, 'Failed to read attribute' if status < 0
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
buffer.read_pointer.read_string
|
|
38
|
-
else
|
|
39
|
-
raise HDF5::Error, 'Unsupported data type'
|
|
40
|
-
end
|
|
28
|
+
result = HDF5::DataHelpers.from_binary(dtype_object, buffer.read_bytes(size * dtype_object.itemsize),
|
|
29
|
+
attribute_shape)
|
|
30
|
+
return result unless attribute_shape.empty?
|
|
31
|
+
|
|
32
|
+
scalar = result.extract
|
|
33
|
+
dtype_object.kind == :bool ? !scalar.zero? : scalar
|
|
41
34
|
ensure
|
|
42
35
|
HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
|
|
43
36
|
HDF5::FFI.H5Sclose(space_id) if space_id && space_id >= 0
|
|
@@ -49,15 +42,54 @@ module HDF5
|
|
|
49
42
|
HDF5::FFI.H5Aclose(@attr_id)
|
|
50
43
|
@attr_id = nil
|
|
51
44
|
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def shape(space_id)
|
|
49
|
+
rank = HDF5::FFI.H5Sget_simple_extent_ndims(space_id)
|
|
50
|
+
raise HDF5::Error, 'Failed to get attribute rank' if rank < 0
|
|
51
|
+
return [] if rank.zero?
|
|
52
|
+
|
|
53
|
+
dimensions = ::FFI::MemoryPointer.new(:ulong_long, rank)
|
|
54
|
+
status = HDF5::FFI.H5Sget_simple_extent_dims(space_id, dimensions, nil)
|
|
55
|
+
raise HDF5::Error, 'Failed to get attribute shape' if status < 0
|
|
56
|
+
|
|
57
|
+
dimensions.read_array_of_uint64(rank)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_string(type_id, space_id)
|
|
61
|
+
unless HDF5::StringCodec.variable?(type_id)
|
|
62
|
+
raise UnsupportedTypeError, 'Fixed-length string attributes are not yet supported'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
attribute_shape = shape(space_id)
|
|
66
|
+
count = attribute_shape.empty? ? 1 : attribute_shape.inject(:*)
|
|
67
|
+
buffer = ::FFI::MemoryPointer.new(:pointer, count)
|
|
68
|
+
status = HDF5::FFI.H5Aread(@attr_id, type_id, buffer)
|
|
69
|
+
raise HDF5::Error, 'Failed to read string attribute' if status < 0
|
|
70
|
+
|
|
71
|
+
HDF5::StringCodec.read_values(buffer, count, attribute_shape, encoding: HDF5::StringCodec.encoding_for(type_id))
|
|
72
|
+
ensure
|
|
73
|
+
if buffer
|
|
74
|
+
active_error = $ERROR_INFO
|
|
75
|
+
reclaim_status = HDF5::FFI.H5Dvlen_reclaim(type_id, space_id, HDF5::DEFAULT_PROPERTY_LIST, buffer)
|
|
76
|
+
if reclaim_status.negative? && active_error.nil?
|
|
77
|
+
raise HDF5::Error,
|
|
78
|
+
'Failed to reclaim variable-length string attribute'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
52
82
|
end
|
|
53
83
|
|
|
54
84
|
class AttributeManager
|
|
55
|
-
def initialize(dataset_id)
|
|
85
|
+
def initialize(dataset_id, context = nil)
|
|
56
86
|
@dataset_id = dataset_id
|
|
87
|
+
@context = context
|
|
57
88
|
end
|
|
58
89
|
|
|
59
90
|
def [](attr_name)
|
|
60
|
-
|
|
91
|
+
@context&.ensure_open!(@dataset_id)
|
|
92
|
+
attr = Attribute.new(@dataset_id, attr_name, @context)
|
|
61
93
|
attr.read
|
|
62
94
|
ensure
|
|
63
95
|
attr.close if attr
|
|
@@ -67,9 +99,102 @@ module HDF5
|
|
|
67
99
|
write(attr_name, value)
|
|
68
100
|
end
|
|
69
101
|
|
|
102
|
+
def keys
|
|
103
|
+
@context&.ensure_open!(@dataset_id)
|
|
104
|
+
names = []
|
|
105
|
+
index = ::FFI::MemoryPointer.new(:ulong_long)
|
|
106
|
+
index.write_ulong_long(0)
|
|
107
|
+
callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
|
|
108
|
+
names << name
|
|
109
|
+
0
|
|
110
|
+
end
|
|
111
|
+
status = HDF5::FFI.H5Aiterate2(@dataset_id, :H5_INDEX_NAME, :H5_ITER_NATIVE, index, callback, nil)
|
|
112
|
+
raise HDF5::Error, 'Failed to iterate over attributes' if status < 0
|
|
113
|
+
|
|
114
|
+
names
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def key?(attr_name)
|
|
118
|
+
@context&.ensure_open!(@dataset_id)
|
|
119
|
+
exists = HDF5::FFI.H5Aexists(@dataset_id, attr_name)
|
|
120
|
+
raise HDF5::Error, "Failed to check attribute existence: #{attr_name}" if exists.negative?
|
|
121
|
+
|
|
122
|
+
exists.positive?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def delete(attr_name)
|
|
126
|
+
@context&.ensure_open!(@dataset_id)
|
|
127
|
+
status = HDF5::FFI.H5Adelete(@dataset_id, attr_name)
|
|
128
|
+
raise HDF5::Error, "Failed to delete attribute: #{attr_name}" if status < 0
|
|
129
|
+
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def create(attr_name, value)
|
|
134
|
+
raise HDF5::Error, "Attribute already exists: #{attr_name}" if key?(attr_name)
|
|
135
|
+
|
|
136
|
+
write(attr_name, value)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def modify(attr_name, value, casting: :safe)
|
|
140
|
+
@context&.ensure_open!(@dataset_id)
|
|
141
|
+
raise HDF5::Error, "Attribute not found: #{attr_name}" unless key?(attr_name)
|
|
142
|
+
|
|
143
|
+
attr_id = HDF5::FFI.H5Aopen(@dataset_id, attr_name, HDF5::DEFAULT_PROPERTY_LIST)
|
|
144
|
+
raise HDF5::Error, "Failed to open attribute: #{attr_name}" if attr_id < 0
|
|
145
|
+
|
|
146
|
+
type_id = HDF5::FFI.H5Aget_type(attr_id)
|
|
147
|
+
space_id = HDF5::FFI.H5Aget_space(attr_id)
|
|
148
|
+
raise HDF5::Error, "Failed to inspect attribute: #{attr_name}" if type_id < 0 || space_id < 0
|
|
149
|
+
if HDF5::FFI.H5Sget_simple_extent_type(space_id) == :H5S_NULL
|
|
150
|
+
unless value.is_a?(HDF5::Empty) && value.dtype.to_sym == DType.for_hdf5(type_id).to_sym
|
|
151
|
+
raise HDF5::ShapeError, 'Cannot assign a value to a Null attribute'
|
|
152
|
+
end
|
|
153
|
+
return value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if HDF5::FFI.H5Tget_class(type_id) == :H5T_STRING
|
|
157
|
+
unless HDF5::StringCodec.variable?(type_id)
|
|
158
|
+
raise UnsupportedTypeError, 'Fixed-length string attributes are not yet supported'
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
encoding = HDF5::StringCodec.encoding_for(type_id)
|
|
162
|
+
string_values, string_shape = HDF5::StringCodec.normalize_data(value, encoding:)
|
|
163
|
+
unless string_shape == attribute_shape(space_id)
|
|
164
|
+
raise HDF5::ShapeError,
|
|
165
|
+
'Attribute shape must not change when modifying'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
buffer, _string_pointers = HDF5::StringCodec.buffer_for_values(string_values, encoding:)
|
|
169
|
+
status = HDF5::FFI.H5Awrite(attr_id, type_id, buffer)
|
|
170
|
+
else
|
|
171
|
+
dtype_object = DType.for_hdf5(type_id)
|
|
172
|
+
values = HDF5::DataHelpers.normalize_data(value, label: 'Attribute data', dtype: dtype_object, casting:, convert: false)
|
|
173
|
+
expected_shape = attribute_shape(space_id)
|
|
174
|
+
raise HDF5::Error, 'Attribute shape must not change when modifying' unless values.shape == expected_shape
|
|
175
|
+
|
|
176
|
+
status = HDF5::FFI.H5Awrite(attr_id, DType.for_numo(values).memory_type_id, HDF5::DataHelpers.buffer_for(values))
|
|
177
|
+
end
|
|
178
|
+
raise HDF5::Error, "Failed to modify attribute: #{attr_name}" if status < 0
|
|
179
|
+
|
|
180
|
+
value
|
|
181
|
+
ensure
|
|
182
|
+
HDF5::FFI.H5Sclose(space_id) if space_id && space_id >= 0
|
|
183
|
+
HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
|
|
184
|
+
HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
|
|
185
|
+
end
|
|
186
|
+
|
|
70
187
|
def write(attr_name, value)
|
|
71
|
-
|
|
72
|
-
|
|
188
|
+
@context&.ensure_open!(@dataset_id)
|
|
189
|
+
empty_data = value.is_a?(HDF5::Empty)
|
|
190
|
+
if empty_data && value.dtype.kind == :string
|
|
191
|
+
raise UnsupportedFeatureError, 'Creating Null string attributes is not yet supported'
|
|
192
|
+
end
|
|
193
|
+
string_data = HDF5::StringCodec.string_data?(value)
|
|
194
|
+
string_values, string_shape = HDF5::StringCodec.normalize_data(value) if string_data
|
|
195
|
+
values = HDF5::DataHelpers.normalize_data(value, label: 'Attribute data') unless string_data || empty_data
|
|
196
|
+
dtype_object = empty_data ? value.dtype : DType.for_numo(values) unless string_data
|
|
197
|
+
type_id = string_data ? HDF5::StringCodec.datatype_id : dtype_object.storage_type_id
|
|
73
198
|
|
|
74
199
|
exists = HDF5::FFI.H5Aexists(@dataset_id, attr_name)
|
|
75
200
|
raise HDF5::Error, "Failed to check attribute existence: #{attr_name}" if exists.negative?
|
|
@@ -79,78 +204,60 @@ module HDF5
|
|
|
79
204
|
raise HDF5::Error, "Failed to replace attribute: #{attr_name}" if status < 0
|
|
80
205
|
end
|
|
81
206
|
|
|
82
|
-
|
|
83
|
-
dims.write_array_of_ulong_long([values.length])
|
|
84
|
-
dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
|
|
207
|
+
dataspace_id = create_dataspace(empty_data ? nil : (string_data ? string_shape : values.shape))
|
|
85
208
|
raise HDF5::Error, 'Failed to create attribute dataspace' if dataspace_id < 0
|
|
86
209
|
|
|
87
210
|
attr_id = HDF5::FFI.H5Acreate2(
|
|
88
211
|
@dataset_id,
|
|
89
212
|
attr_name,
|
|
90
|
-
|
|
213
|
+
type_id,
|
|
91
214
|
dataspace_id,
|
|
92
215
|
HDF5::DEFAULT_PROPERTY_LIST,
|
|
93
216
|
HDF5::DEFAULT_PROPERTY_LIST
|
|
94
217
|
)
|
|
95
218
|
raise HDF5::Error, "Failed to create attribute: #{attr_name}" if attr_id < 0
|
|
219
|
+
return value if empty_data
|
|
96
220
|
|
|
97
|
-
buffer =
|
|
98
|
-
|
|
221
|
+
buffer, _string_pointers = if string_data
|
|
222
|
+
HDF5::StringCodec.buffer_for_values(string_values)
|
|
223
|
+
else
|
|
224
|
+
[HDF5::DataHelpers.buffer_for(values), nil]
|
|
225
|
+
end
|
|
226
|
+
memory_type_id = string_data ? type_id : dtype_object.memory_type_id
|
|
227
|
+
status = HDF5::FFI.H5Awrite(attr_id, memory_type_id, buffer)
|
|
99
228
|
raise HDF5::Error, "Failed to write attribute: #{attr_name}" if status < 0
|
|
100
229
|
|
|
101
230
|
value
|
|
102
231
|
ensure
|
|
103
232
|
HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
|
|
104
233
|
HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
|
|
234
|
+
HDF5::FFI.H5Tclose(type_id) if string_data && type_id && type_id >= 0
|
|
105
235
|
end
|
|
106
236
|
|
|
107
237
|
private
|
|
108
238
|
|
|
109
|
-
def
|
|
110
|
-
|
|
111
|
-
raise HDF5::Error, '
|
|
112
|
-
|
|
239
|
+
def attribute_shape(space_id)
|
|
240
|
+
rank = HDF5::FFI.H5Sget_simple_extent_ndims(space_id)
|
|
241
|
+
raise HDF5::Error, 'Failed to get attribute rank' if rank < 0
|
|
242
|
+
return [] if rank.zero?
|
|
113
243
|
|
|
114
|
-
|
|
115
|
-
|
|
244
|
+
dimensions = ::FFI::MemoryPointer.new(:ulong_long, rank)
|
|
245
|
+
status = HDF5::FFI.H5Sget_simple_extent_dims(space_id, dimensions, nil)
|
|
246
|
+
raise HDF5::Error, 'Failed to get attribute shape' if status < 0
|
|
116
247
|
|
|
117
|
-
|
|
118
|
-
if values.all? { |item| item.is_a?(Integer) }
|
|
119
|
-
validate_native_int_range!(values)
|
|
120
|
-
HDF5::FFI.H5T_NATIVE_INT
|
|
121
|
-
elsif values.all? { |item| item.is_a?(Numeric) }
|
|
122
|
-
HDF5::FFI.H5T_NATIVE_DOUBLE
|
|
123
|
-
else
|
|
124
|
-
raise HDF5::Error, 'Only numeric attribute data is supported'
|
|
125
|
-
end
|
|
248
|
+
dimensions.read_array_of_uint64(rank)
|
|
126
249
|
end
|
|
127
250
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
buffer = ::FFI::MemoryPointer.new(:int, values.length)
|
|
131
|
-
buffer.write_array_of_int(values)
|
|
132
|
-
else
|
|
133
|
-
buffer = ::FFI::MemoryPointer.new(:double, values.length)
|
|
134
|
-
buffer.write_array_of_double(values.map(&:to_f))
|
|
135
|
-
end
|
|
251
|
+
def create_dataspace(shape)
|
|
252
|
+
return HDF5::FFI.H5Screate(:H5S_NULL) if shape.nil?
|
|
136
253
|
|
|
137
|
-
|
|
138
|
-
end
|
|
254
|
+
return HDF5::FFI.H5Screate(:H5S_SCALAR) if shape.empty?
|
|
139
255
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
min = -(1 << (bits - 1))
|
|
144
|
-
[min, max]
|
|
256
|
+
dimensions = ::FFI::MemoryPointer.new(:ulong_long, shape.length)
|
|
257
|
+
dimensions.write_array_of_ulong_long(shape)
|
|
258
|
+
HDF5::FFI.H5Screate_simple(shape.length, dimensions, nil)
|
|
145
259
|
end
|
|
146
260
|
|
|
147
|
-
|
|
148
|
-
min, max = native_int_bounds
|
|
149
|
-
out_of_range = values.find { |value| value < min || value > max }
|
|
150
|
-
return unless out_of_range
|
|
151
|
-
|
|
152
|
-
raise HDF5::Error,
|
|
153
|
-
"Integer value #{out_of_range} is outside native int range (#{min}..#{max}). Use a smaller value."
|
|
154
|
-
end
|
|
261
|
+
prepend FileContext.guard(:[], :[]=, :keys, :key?, :delete, :create, :modify, :write)
|
|
155
262
|
end
|
|
156
263
|
end
|