Skip to content

Commit fd7e04f

Browse files
committed
Refactor HDF5 File and Group classes
Drop 1.0
1 parent a3d77b4 commit fd7e04f

7 files changed

Lines changed: 292 additions & 6398 deletions

File tree

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,64 @@
44

55
experimental Ruby bindings for the HDF5 library
66

7+
## Supported HDF5 Versions
8+
9+
- HDF5 1.14
10+
- HDF5 2.x
11+
12+
HDF5 1.10 and older are not supported.
13+
14+
## Basic I/O
15+
16+
Current high-level API supports these operations:
17+
18+
- open an existing HDF5 file
19+
- create a new HDF5 file
20+
- create groups
21+
- create, write, and read one-dimensional numeric datasets
22+
23+
Not supported yet:
24+
25+
- string datasets
26+
- attribute writes
27+
- multidimensional array writes
28+
29+
### Read an existing file
30+
31+
```ruby
32+
require 'hdf5'
33+
34+
file = HDF5::File.open('example.h5')
35+
group = file['foo']
36+
dataset = group['bar_int']
37+
38+
p dataset.shape
39+
p dataset.dtype
40+
p dataset.read
41+
42+
dataset.close
43+
group.close
44+
file.close
45+
```
46+
47+
### Create and write a file
48+
49+
```ruby
50+
require 'hdf5'
51+
52+
file = HDF5::File.create('numbers.h5')
53+
group = file.create_group('values')
54+
dataset = group.create_dataset('ints', [1, 2, 3, 4])
55+
56+
dataset.close
57+
group.close
58+
file.close
59+
60+
reopened = HDF5::File.open('numbers.h5')
61+
p reopened['values']['ints'].read
62+
reopened.close
63+
```
64+
765
## Development
866

967
- [c2ffi](https://github.com/rpav/c2ffi)

lib/hdf5/dataset.rb

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,84 @@
11
module HDF5
22
class Dataset
3+
H5P_DEFAULT = 0
4+
5+
class << self
6+
def create(parent_id, name, data)
7+
values = normalize_data(data)
8+
dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
9+
dims.write_array_of_ulong_long([values.length])
10+
datatype_id = datatype_id_for(values)
11+
dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
12+
raise "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
13+
14+
dataset_id = HDF5::FFI.H5Dcreate2(parent_id, name, datatype_id, dataspace_id, H5P_DEFAULT, H5P_DEFAULT,
15+
H5P_DEFAULT)
16+
dataset = from_id(dataset_id, name)
17+
dataset.write(values)
18+
dataset
19+
ensure
20+
HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
21+
end
22+
23+
def open(parent_id, name)
24+
from_id(HDF5::FFI.H5Dopen2(parent_id, name, H5P_DEFAULT), name)
25+
end
26+
27+
def normalize_data(data)
28+
values = data.is_a?(Array) ? data : [data]
29+
raise 'Dataset data must not be empty' if values.empty?
30+
raise 'Nested arrays are not supported' if values.any? { |value| value.is_a?(Array) }
31+
32+
values
33+
end
34+
35+
def datatype_id_for(data)
36+
if data.all? { |value| value.is_a?(Integer) }
37+
HDF5::FFI.H5T_NATIVE_INT
38+
elsif data.all? { |value| value.is_a?(Numeric) }
39+
HDF5::FFI.H5T_NATIVE_DOUBLE
40+
else
41+
raise 'Only numeric dataset data is supported'
42+
end
43+
end
44+
45+
def buffer_for(data)
46+
if data.all? { |value| value.is_a?(Integer) }
47+
buffer = ::FFI::MemoryPointer.new(:int, data.length)
48+
buffer.write_array_of_int(data)
49+
else
50+
buffer = ::FFI::MemoryPointer.new(:double, data.length)
51+
buffer.write_array_of_double(data.map(&:to_f))
52+
end
53+
54+
buffer
55+
end
56+
57+
private
58+
59+
def from_id(dataset_id, name)
60+
dataset = allocate
61+
dataset.send(:initialize_from_id, dataset_id, name)
62+
dataset
63+
end
64+
end
65+
366
def initialize(parent_id, name)
4-
@dataset_id = HDF5::FFI.H5Dopen1(parent_id, name)
67+
initialize_from_id(HDF5::FFI.H5Dopen2(parent_id, name, H5P_DEFAULT), name)
568
end
669

770
def attrs
871
@attrs ||= AttributeManager.new(@dataset_id)
972
end
1073

1174
def write(data)
12-
HDF5::FFI.H5Dwrite(@dataset_id, data)
75+
values = self.class.normalize_data(data)
76+
buffer = self.class.buffer_for(values)
77+
mem_type_id = self.class.datatype_id_for(values)
78+
status = HDF5::FFI.H5Dwrite(@dataset_id, mem_type_id, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT, buffer)
79+
raise 'Failed to write dataset' if status < 0
80+
81+
data
1382
end
1483

1584
def close
@@ -18,7 +87,11 @@ def close
1887

1988
def dtype
2089
datatype_id = HDF5::FFI.H5Dget_type(@dataset_id)
90+
raise 'Failed to get datatype' if datatype_id < 0
91+
2192
HDF5::FFI.H5Tget_class(datatype_id)
93+
ensure
94+
HDF5::FFI.H5Tclose(datatype_id) if datatype_id && datatype_id >= 0
2295
end
2396

2497
def shape
@@ -55,20 +128,32 @@ def read
55128

56129
def read_integer_data(total_elements)
57130
buffer = ::FFI::MemoryPointer.new(:int, total_elements)
58-
mem_type_id = HDF5::FFI.H5T_NATIVE_INT
59-
HDF5::FFI.H5Dread(@dataset_id, mem_type_id, 0, 0, 0, buffer)
131+
status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_INT, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT, buffer)
132+
raise 'Failed to read integer dataset' if status < 0
133+
60134
buffer.read_array_of_int(total_elements)
61135
end
62136

63137
def read_float_data(total_elements)
64-
buffer = ::FFI::MemoryPointer.new(:float, total_elements)
65-
mem_type_id = HDF5::FFI.H5T_NATIVE_FLOAT
66-
HDF5::FFI.H5Dread(@dataset_id, mem_type_id, 0, 0, 0, buffer)
67-
buffer.read_array_of_float(total_elements)
138+
buffer = ::FFI::MemoryPointer.new(:double, total_elements)
139+
status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_DOUBLE, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT,
140+
buffer)
141+
raise 'Failed to read float dataset' if status < 0
142+
143+
buffer.read_array_of_double(total_elements)
68144
end
69145

70146
def read_string_data(total_elements)
71147
raise NotImplementedError
72148
end
149+
150+
private
151+
152+
def initialize_from_id(dataset_id, name)
153+
raise "Failed to open dataset: #{name}" if dataset_id < 0
154+
155+
@dataset_id = dataset_id
156+
@name = name
157+
end
73158
end
74159
end

lib/hdf5/ffi.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ module HDF5
22
module FFI
33
extend ::FFI::Library
44

5+
class << self
6+
attr_reader :backend
7+
end
8+
59
begin
610
ffi_lib HDF5.lib_path
711
rescue LoadError => e
@@ -39,13 +43,15 @@ def self.attach_variable(*)
3943
minor = minor_ptr.read_uint
4044
release = release_ptr.read_uint
4145

42-
case [major, minor]
43-
in [1, 10]
44-
require_relative 'ffi_10'
45-
in [1, 14..] | [2.., _]
46-
require_relative 'ffi_14'
47-
else
48-
raise "Unsupported HDF5 version #{major}.#{minor}.#{release}"
49-
end
46+
@backend = case [major, minor]
47+
in [1, 14..]
48+
'ffi_14'
49+
in [2.., _]
50+
'ffi_14'
51+
else
52+
raise "Unsupported HDF5 version #{major}.#{minor}.#{release}"
53+
end
54+
55+
require_relative @backend
5056
end
5157
end

0 commit comments

Comments
 (0)