Skip to content

Commit 98fc4a9

Browse files
committed
Enhance HDF5 attribute handling
1 parent fb208eb commit 98fc4a9

5 files changed

Lines changed: 102 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ This gem currently provides practical high-level wrappers for:
1111
- opening and creating files
1212
- creating groups
1313
- creating, writing, and reading one-dimensional numeric datasets
14-
- reading attributes
14+
- reading and writing numeric attributes
1515

1616
Unsupported at this stage:
1717

1818
- string dataset read/write
19-
- attribute write
2019
- multidimensional array write
2120

2221
## Supported HDF5 Versions

lib/hdf5/attribute.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,70 @@ def [](attr_name)
6262
ensure
6363
attr.close if attr
6464
end
65+
66+
def []=(attr_name, value)
67+
write(attr_name, value)
68+
end
69+
70+
def write(attr_name, value)
71+
values = normalize_data(value)
72+
datatype_id = datatype_id_for(values)
73+
74+
dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
75+
dims.write_array_of_ulong_long([values.length])
76+
dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
77+
raise HDF5::Error, 'Failed to create attribute dataspace' if dataspace_id < 0
78+
79+
attr_id = HDF5::FFI.H5Acreate2(
80+
@dataset_id,
81+
attr_name,
82+
datatype_id,
83+
dataspace_id,
84+
HDF5::DEFAULT_PROPERTY_LIST,
85+
HDF5::DEFAULT_PROPERTY_LIST
86+
)
87+
raise HDF5::Error, "Failed to create attribute: #{attr_name}" if attr_id < 0
88+
89+
buffer = buffer_for(values)
90+
status = HDF5::FFI.H5Awrite(attr_id, datatype_id, buffer)
91+
raise HDF5::Error, "Failed to write attribute: #{attr_name}" if status < 0
92+
93+
value
94+
ensure
95+
HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
96+
HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
97+
end
98+
99+
private
100+
101+
def normalize_data(value)
102+
values = value.is_a?(Array) ? value : [value]
103+
raise HDF5::Error, 'Attribute data must not be empty' if values.empty?
104+
raise HDF5::Error, 'Nested arrays are not supported for attributes' if values.any? { |item| item.is_a?(Array) }
105+
106+
values
107+
end
108+
109+
def datatype_id_for(values)
110+
if values.all? { |item| item.is_a?(Integer) }
111+
HDF5::FFI.H5T_NATIVE_INT
112+
elsif values.all? { |item| item.is_a?(Numeric) }
113+
HDF5::FFI.H5T_NATIVE_DOUBLE
114+
else
115+
raise HDF5::Error, 'Only numeric attribute data is supported'
116+
end
117+
end
118+
119+
def buffer_for(values)
120+
if values.all? { |item| item.is_a?(Integer) }
121+
buffer = ::FFI::MemoryPointer.new(:int, values.length)
122+
buffer.write_array_of_int(values)
123+
else
124+
buffer = ::FFI::MemoryPointer.new(:double, values.length)
125+
buffer.write_array_of_double(values.map(&:to_f))
126+
end
127+
128+
buffer
129+
end
65130
end
66131
end

lib/hdf5/dataset.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def buffer_for(data)
6666
buffer
6767
end
6868

69+
private :normalize_data, :datatype_id_for, :buffer_for
70+
6971
private
7072

7173
def from_id(dataset_id, name)
@@ -84,9 +86,9 @@ def attrs
8486
end
8587

8688
def write(data)
87-
values = self.class.normalize_data(data)
88-
buffer = self.class.buffer_for(values)
89-
mem_type_id = self.class.datatype_id_for(values)
89+
values = self.class.send(:normalize_data, data)
90+
buffer = self.class.send(:buffer_for, values)
91+
mem_type_id = self.class.send(:datatype_id_for, values)
9092
status = HDF5::FFI.H5Dwrite(@dataset_id, mem_type_id, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST,
9193
HDF5::DEFAULT_PROPERTY_LIST, buffer)
9294
raise HDF5::Error, 'Failed to write dataset' if status < 0
@@ -160,8 +162,8 @@ def read_float_data(total_elements)
160162
buffer.read_array_of_double(total_elements)
161163
end
162164

163-
def read_string_data(total_elements)
164-
raise NotImplementedError
165+
def read_string_data(_total_elements)
166+
raise HDF5::Error, 'String dataset reading is not supported yet'
165167
end
166168

167169
private

lib/hdf5/group.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ def create_dataset(name, data, &block)
5454
Dataset.create(@group_id, name, data, &block)
5555
end
5656

57-
def list_datasets
58-
datasets = []
57+
def list_entries
58+
entries = []
5959
callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
60-
datasets << name
60+
entries << name
6161
0 # continue
6262
end
6363

@@ -66,9 +66,13 @@ def list_datasets
6666
else
6767
HDF5::FFI.H5Literate2(@group_id, :H5_INDEX_NAME, :H5_ITER_NATIVE, nil, callback, nil)
6868
end).negative? &&
69-
raise(HDF5::Error, 'Failed to list datasets')
69+
raise(HDF5::Error, 'Failed to list entries')
70+
71+
entries
72+
end
7073

71-
datasets
74+
def list_datasets
75+
list_entries.select { |name| dataset?(name) }
7276
end
7377

7478
def [](name)

test/hdf5_test.rb

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -96,27 +96,7 @@ class HDF5Test < Test::Unit::TestCase
9696

9797
HDF5::File.create(path) do |file|
9898
dataset = file.create_dataset('values', [1, 2, 3])
99-
dataset_id = dataset.instance_variable_get(:@dataset_id)
100-
101-
dims = FFI::MemoryPointer.new(:ulong_long, 1)
102-
dims.write_array_of_ulong_long([1])
103-
attr_space_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
104-
attr_id = HDF5::FFI.H5Acreate2(
105-
dataset_id,
106-
'scale',
107-
HDF5::FFI.H5T_NATIVE_INT,
108-
attr_space_id,
109-
HDF5::DEFAULT_PROPERTY_LIST,
110-
HDF5::DEFAULT_PROPERTY_LIST
111-
)
112-
value = FFI::MemoryPointer.new(:int, 1)
113-
value.write_int(42)
114-
status = HDF5::FFI.H5Awrite(attr_id, HDF5::FFI.H5T_NATIVE_INT, value)
115-
assert_equal(0, status)
116-
ensure
117-
HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
118-
HDF5::FFI.H5Sclose(attr_space_id) if attr_space_id && attr_space_id >= 0
119-
dataset.close if dataset
99+
dataset.attrs['scale'] = 42
120100
end
121101

122102
HDF5::File.open(path) do |file|
@@ -125,6 +105,25 @@ class HDF5Test < Test::Unit::TestCase
125105
end
126106
end
127107

108+
test 'group list_datasets returns only datasets' do
109+
Dir.mktmpdir do |dir|
110+
path = File.join(dir, 'group-list.h5')
111+
112+
HDF5::File.create(path) do |file|
113+
file.create_group('parent') do |parent|
114+
parent.create_group('child')
115+
parent.create_dataset('numbers', [1, 2, 3])
116+
end
117+
end
118+
119+
HDF5::File.open(path) do |file|
120+
parent = file['parent']
121+
assert_equal(%w[child numbers].sort, parent.list_entries.sort)
122+
assert_equal(%w[numbers], parent.list_datasets)
123+
end
124+
end
125+
end
126+
128127
test 'dataset attrs raises HDF5 error for missing attribute' do
129128
Dir.mktmpdir do |dir|
130129
path = File.join(dir, 'missing-attribute.h5')

0 commit comments

Comments
 (0)