ruby-hdf5 0.0.2 → 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 -69
- data/lib/hdf5/attribute.rb +168 -76
- data/lib/hdf5/data_helpers.rb +54 -0
- data/lib/hdf5/dataset.rb +559 -99
- 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 +68 -10
- data/lib/hdf5/file_context.rb +70 -0
- data/lib/hdf5/group.rb +41 -15
- 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
data/lib/hdf5/file.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module HDF5
|
|
2
2
|
class File
|
|
3
|
+
include Hierarchy
|
|
4
|
+
|
|
3
5
|
H5F_ACC_RDONLY = 0x0000
|
|
4
6
|
H5F_ACC_RDWR = 0x0001
|
|
5
7
|
H5F_ACC_TRUNC = 0x0002
|
|
8
|
+
H5F_ACC_EXCL = 0x0004
|
|
6
9
|
|
|
7
10
|
class << self
|
|
8
11
|
def create(filename, flags = H5F_ACC_TRUNC)
|
|
@@ -17,8 +20,9 @@ module HDF5
|
|
|
17
20
|
end
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
def open(filename, mode =
|
|
21
|
-
|
|
23
|
+
def open(filename, mode = 'r')
|
|
24
|
+
file_id, flags = open_file(filename, mode)
|
|
25
|
+
file = from_id(file_id, filename, flags)
|
|
22
26
|
return file unless block_given?
|
|
23
27
|
|
|
24
28
|
begin
|
|
@@ -30,6 +34,29 @@ module HDF5
|
|
|
30
34
|
|
|
31
35
|
private
|
|
32
36
|
|
|
37
|
+
def open_file(filename, mode)
|
|
38
|
+
return [HDF5::FFI.H5Fopen(filename, mode, HDF5::DEFAULT_PROPERTY_LIST), mode] if mode.is_a?(Integer)
|
|
39
|
+
|
|
40
|
+
case mode
|
|
41
|
+
when 'r'
|
|
42
|
+
[HDF5::FFI.H5Fopen(filename, H5F_ACC_RDONLY, HDF5::DEFAULT_PROPERTY_LIST), H5F_ACC_RDONLY]
|
|
43
|
+
when 'r+'
|
|
44
|
+
[HDF5::FFI.H5Fopen(filename, H5F_ACC_RDWR, HDF5::DEFAULT_PROPERTY_LIST), H5F_ACC_RDWR]
|
|
45
|
+
when 'w'
|
|
46
|
+
[HDF5::FFI.H5Fcreate(filename, H5F_ACC_TRUNC, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST),
|
|
47
|
+
H5F_ACC_TRUNC]
|
|
48
|
+
when 'x'
|
|
49
|
+
[HDF5::FFI.H5Fcreate(filename, H5F_ACC_EXCL, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST),
|
|
50
|
+
H5F_ACC_EXCL]
|
|
51
|
+
when 'a'
|
|
52
|
+
file_id = HDF5::FFI.H5Fcreate(filename, H5F_ACC_EXCL, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST)
|
|
53
|
+
file_id = HDF5::FFI.H5Fopen(filename, H5F_ACC_RDWR, HDF5::DEFAULT_PROPERTY_LIST) if file_id < 0
|
|
54
|
+
[file_id, H5F_ACC_RDWR]
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, "Unsupported file mode: #{mode.inspect}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
33
60
|
def from_id(file_id, filename, mode)
|
|
34
61
|
file = allocate
|
|
35
62
|
file.send(:initialize_from_id, file_id, filename, mode)
|
|
@@ -42,21 +69,36 @@ module HDF5
|
|
|
42
69
|
end
|
|
43
70
|
|
|
44
71
|
def close
|
|
45
|
-
return if @file_id.nil?
|
|
72
|
+
return if @file_id.nil? || @context.closed?
|
|
46
73
|
|
|
47
|
-
|
|
74
|
+
@context.close_file
|
|
48
75
|
@file_id = nil
|
|
49
76
|
end
|
|
50
77
|
|
|
78
|
+
def closed?
|
|
79
|
+
@file_id.nil? || @context.closed?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def flush
|
|
83
|
+
ensure_open!
|
|
84
|
+
status = HDF5::FFI.H5Fflush(@file_id, :H5F_SCOPE_GLOBAL)
|
|
85
|
+
raise HDF5::Error, 'Failed to flush file' if status < 0
|
|
86
|
+
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
51
90
|
def create_group(name, &block)
|
|
52
|
-
|
|
91
|
+
ensure_open!
|
|
92
|
+
Group.create(@file_id, name, @context, &block)
|
|
53
93
|
end
|
|
54
94
|
|
|
55
|
-
def create_dataset(name, data, &block)
|
|
56
|
-
|
|
95
|
+
def create_dataset(name, data = nil, **options, &block)
|
|
96
|
+
ensure_open!
|
|
97
|
+
Dataset.create(@file_id, name, data, context: @context, **options, &block)
|
|
57
98
|
end
|
|
58
99
|
|
|
59
100
|
def list_entries
|
|
101
|
+
ensure_open!
|
|
60
102
|
list = []
|
|
61
103
|
callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
|
|
62
104
|
list << name
|
|
@@ -74,27 +116,41 @@ module HDF5
|
|
|
74
116
|
end
|
|
75
117
|
|
|
76
118
|
def [](name)
|
|
119
|
+
ensure_open!
|
|
77
120
|
if group?(name)
|
|
78
|
-
Group.open(@file_id, name)
|
|
121
|
+
Group.open(@file_id, name, @context)
|
|
79
122
|
elsif dataset?(name)
|
|
80
|
-
Dataset.open(@file_id, name)
|
|
123
|
+
Dataset.open(@file_id, name, context: @context)
|
|
81
124
|
else
|
|
82
125
|
raise HDF5::Error, "Group or dataset not found: #{name}"
|
|
83
126
|
end
|
|
84
127
|
end
|
|
85
128
|
|
|
86
129
|
def attrs
|
|
87
|
-
|
|
130
|
+
ensure_open!
|
|
131
|
+
@attrs ||= AttributeManager.new(@file_id, @context)
|
|
88
132
|
end
|
|
89
133
|
|
|
90
134
|
private
|
|
91
135
|
|
|
136
|
+
def hdf5_id
|
|
137
|
+
ensure_open!
|
|
138
|
+
@file_id
|
|
139
|
+
end
|
|
140
|
+
|
|
92
141
|
def initialize_from_id(file_id, filename, mode)
|
|
93
142
|
raise HDF5::Error, "Failed to open file: #{filename}" if file_id < 0
|
|
94
143
|
|
|
95
144
|
@filename = filename
|
|
96
145
|
@mode = mode
|
|
97
146
|
@file_id = file_id
|
|
147
|
+
@context = FileContext.new(file_id)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def ensure_open!
|
|
151
|
+
raise ClosedError, 'HDF5 file is closed' if @file_id.nil?
|
|
152
|
+
|
|
153
|
+
@context.ensure_open!(@file_id)
|
|
98
154
|
end
|
|
99
155
|
|
|
100
156
|
def group?(name)
|
|
@@ -122,5 +178,7 @@ module HDF5
|
|
|
122
178
|
end
|
|
123
179
|
info[:type] == :H5O_TYPE_DATASET
|
|
124
180
|
end
|
|
181
|
+
|
|
182
|
+
prepend FileContext.guard(:flush, :create_group, :create_dataset, :list_entries, :[], :attrs)
|
|
125
183
|
end
|
|
126
184
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module HDF5
|
|
2
|
+
class FileContext
|
|
3
|
+
CLOSE_FUNCTIONS = {
|
|
4
|
+
file: :H5Fclose,
|
|
5
|
+
group: :H5Gclose,
|
|
6
|
+
dataset: :H5Dclose
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(file_id)
|
|
10
|
+
@file_id = file_id
|
|
11
|
+
@handles = { file_id => :file }
|
|
12
|
+
@closed = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register(id, type)
|
|
16
|
+
raise ClosedError, 'HDF5 file is closed' if closed?
|
|
17
|
+
|
|
18
|
+
@handles[id] = type
|
|
19
|
+
id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def close(id)
|
|
23
|
+
synchronize do
|
|
24
|
+
type = @handles[id]
|
|
25
|
+
return if type.nil?
|
|
26
|
+
|
|
27
|
+
status = HDF5::FFI.public_send(CLOSE_FUNCTIONS.fetch(type), id)
|
|
28
|
+
raise HDF5::Error, "Failed to close HDF5 #{type}" if status < 0
|
|
29
|
+
|
|
30
|
+
@handles.delete(id)
|
|
31
|
+
@closed = true if type == :file
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def close_file
|
|
37
|
+
synchronize do
|
|
38
|
+
return if closed?
|
|
39
|
+
|
|
40
|
+
children = @handles.keys.reject { |id| id == @file_id }
|
|
41
|
+
children.reverse_each { |id| close(id) }
|
|
42
|
+
close(@file_id)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ensure_open!(id = nil)
|
|
47
|
+
raise ClosedError, 'HDF5 file is closed' if closed?
|
|
48
|
+
raise ClosedError, 'HDF5 object is closed' if id && !@handles.key?(id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def closed?
|
|
52
|
+
@closed
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def synchronize(&block)
|
|
56
|
+
HDF5::FFI::CALL_LOCK.synchronize(&block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.guard(*methods)
|
|
60
|
+
Module.new do
|
|
61
|
+
methods.each do |method|
|
|
62
|
+
define_method(method) do |*args, **kwargs, &block|
|
|
63
|
+
lock = @context || HDF5::FFI::CALL_LOCK
|
|
64
|
+
lock.synchronize { super(*args, **kwargs, &block) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/hdf5/group.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
module HDF5
|
|
2
2
|
class Group
|
|
3
|
+
include Hierarchy
|
|
4
|
+
|
|
3
5
|
class << self
|
|
4
|
-
def create(parent_id, name)
|
|
6
|
+
def create(parent_id, name, context = nil)
|
|
5
7
|
group = from_id(
|
|
6
8
|
HDF5::FFI.H5Gcreate2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST,
|
|
7
|
-
HDF5::DEFAULT_PROPERTY_LIST), name
|
|
9
|
+
HDF5::DEFAULT_PROPERTY_LIST), name, context
|
|
8
10
|
)
|
|
9
11
|
return group unless block_given?
|
|
10
12
|
|
|
@@ -15,8 +17,8 @@ module HDF5
|
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def open(parent_id, name)
|
|
19
|
-
group = from_id(HDF5::FFI.H5Gopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
|
|
20
|
+
def open(parent_id, name, context = nil)
|
|
21
|
+
group = from_id(HDF5::FFI.H5Gopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name, context)
|
|
20
22
|
return group unless block_given?
|
|
21
23
|
|
|
22
24
|
begin
|
|
@@ -28,33 +30,40 @@ module HDF5
|
|
|
28
30
|
|
|
29
31
|
private
|
|
30
32
|
|
|
31
|
-
def from_id(group_id, name)
|
|
33
|
+
def from_id(group_id, name, context)
|
|
32
34
|
group = allocate
|
|
33
|
-
group.send(:initialize_from_id, group_id, name)
|
|
35
|
+
group.send(:initialize_from_id, group_id, name, context)
|
|
34
36
|
group
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def initialize(file_id, name)
|
|
39
|
-
initialize_from_id(HDF5::FFI.H5Gopen2(file_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
|
|
41
|
+
initialize_from_id(HDF5::FFI.H5Gopen2(file_id, name, HDF5::DEFAULT_PROPERTY_LIST), name, nil)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def close
|
|
43
45
|
return if @group_id.nil?
|
|
44
46
|
|
|
45
|
-
HDF5::FFI.H5Gclose(@group_id)
|
|
47
|
+
@context ? @context.close(@group_id) : HDF5::FFI.H5Gclose(@group_id)
|
|
46
48
|
@group_id = nil
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
def closed?
|
|
52
|
+
@group_id.nil? || (@context && @context.closed?)
|
|
53
|
+
end
|
|
54
|
+
|
|
49
55
|
def create_group(name, &block)
|
|
50
|
-
|
|
56
|
+
ensure_open!
|
|
57
|
+
self.class.create(@group_id, name, @context, &block)
|
|
51
58
|
end
|
|
52
59
|
|
|
53
|
-
def create_dataset(name, data, &block)
|
|
54
|
-
|
|
60
|
+
def create_dataset(name, data = nil, **options, &block)
|
|
61
|
+
ensure_open!
|
|
62
|
+
Dataset.create(@group_id, name, data, context: @context, **options, &block)
|
|
55
63
|
end
|
|
56
64
|
|
|
57
65
|
def list_entries
|
|
66
|
+
ensure_open!
|
|
58
67
|
entries = []
|
|
59
68
|
callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
|
|
60
69
|
entries << name
|
|
@@ -76,26 +85,41 @@ module HDF5
|
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
def [](name)
|
|
88
|
+
ensure_open!
|
|
79
89
|
if group?(name)
|
|
80
|
-
self.class.open(@group_id, name)
|
|
90
|
+
self.class.open(@group_id, name, @context)
|
|
81
91
|
elsif dataset?(name)
|
|
82
|
-
Dataset.open(@group_id, name)
|
|
92
|
+
Dataset.open(@group_id, name, context: @context)
|
|
83
93
|
else
|
|
84
94
|
raise HDF5::Error, "Group or dataset not found: #{name}"
|
|
85
95
|
end
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
def attrs
|
|
89
|
-
|
|
99
|
+
ensure_open!
|
|
100
|
+
@attrs ||= AttributeManager.new(@group_id, @context)
|
|
90
101
|
end
|
|
91
102
|
|
|
92
103
|
private
|
|
93
104
|
|
|
94
|
-
def
|
|
105
|
+
def hdf5_id
|
|
106
|
+
ensure_open!
|
|
107
|
+
@group_id
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def initialize_from_id(group_id, name, context)
|
|
95
111
|
raise HDF5::Error, "Failed to open group: #{name}" if group_id < 0
|
|
96
112
|
|
|
97
113
|
@group_id = group_id
|
|
98
114
|
@name = name
|
|
115
|
+
@context = context
|
|
116
|
+
@context.register(group_id, :group) if @context
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ensure_open!
|
|
120
|
+
raise ClosedError, 'HDF5 group is closed' if @group_id.nil?
|
|
121
|
+
|
|
122
|
+
@context&.ensure_open!(@group_id)
|
|
99
123
|
end
|
|
100
124
|
|
|
101
125
|
def group?(name)
|
|
@@ -123,5 +147,7 @@ module HDF5
|
|
|
123
147
|
end
|
|
124
148
|
info[:type] == :H5O_TYPE_DATASET
|
|
125
149
|
end
|
|
150
|
+
|
|
151
|
+
prepend FileContext.guard(:create_group, :create_dataset, :list_entries, :list_datasets, :[], :attrs)
|
|
126
152
|
end
|
|
127
153
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module HDF5
|
|
2
|
+
module Hierarchy
|
|
3
|
+
def keys
|
|
4
|
+
list_entries
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def each_key(&block)
|
|
8
|
+
return enum_for(__method__) unless block
|
|
9
|
+
|
|
10
|
+
keys.each(&block)
|
|
11
|
+
self
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def require_group(path)
|
|
15
|
+
path.split('/').reject(&:empty?).inject(self) do |parent, name|
|
|
16
|
+
if parent.key?(name)
|
|
17
|
+
child = parent[name]
|
|
18
|
+
raise HDF5::Error, "Existing object is not a group: #{name}" unless child.is_a?(Group)
|
|
19
|
+
|
|
20
|
+
child
|
|
21
|
+
else
|
|
22
|
+
parent.create_group(name)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def open_dataset(name)
|
|
28
|
+
object = self[name]
|
|
29
|
+
raise HDF5::Error, "Object is not a dataset: #{name}" unless object.is_a?(Dataset)
|
|
30
|
+
return object unless block_given?
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
yield object
|
|
34
|
+
ensure
|
|
35
|
+
object.close
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def key?(name)
|
|
40
|
+
exists = HDF5::FFI.H5Lexists(hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST)
|
|
41
|
+
raise HDF5::Error, "Failed to check link existence: #{name}" if exists.negative?
|
|
42
|
+
|
|
43
|
+
exists.positive?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def delete(name)
|
|
47
|
+
status = HDF5::FFI.H5Ldelete(hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST)
|
|
48
|
+
raise HDF5::Error, "Failed to delete link: #{name}" if status < 0
|
|
49
|
+
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_hard_link(target, name)
|
|
54
|
+
status = HDF5::FFI.H5Lcreate_hard(hdf5_id, target, hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST,
|
|
55
|
+
HDF5::DEFAULT_PROPERTY_LIST)
|
|
56
|
+
raise HDF5::Error, "Failed to create hard link: #{name}" if status < 0
|
|
57
|
+
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_soft_link(target, name)
|
|
62
|
+
status = HDF5::FFI.H5Lcreate_soft(target, hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST,
|
|
63
|
+
HDF5::DEFAULT_PROPERTY_LIST)
|
|
64
|
+
raise HDF5::Error, "Failed to create soft link: #{name}" if status < 0
|
|
65
|
+
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def link_info(name)
|
|
70
|
+
info = HDF5::FFI::MiV == 10 ? HDF5::FFI::H5LInfoT.new : HDF5::FFI::H5LInfo2T.new
|
|
71
|
+
status = if HDF5::FFI::MiV == 10
|
|
72
|
+
HDF5::FFI.H5Lget_info(hdf5_id, name, info, HDF5::DEFAULT_PROPERTY_LIST)
|
|
73
|
+
else
|
|
74
|
+
HDF5::FFI.H5Lget_info2(hdf5_id, name, info, HDF5::DEFAULT_PROPERTY_LIST)
|
|
75
|
+
end
|
|
76
|
+
raise HDF5::Error, "Failed to get link information: #{name}" if status < 0
|
|
77
|
+
|
|
78
|
+
type = info[:type]
|
|
79
|
+
return { type: :hard } if type == :H5L_TYPE_HARD
|
|
80
|
+
|
|
81
|
+
size = info[:u][:val_size]
|
|
82
|
+
buffer = ::FFI::MemoryPointer.new(:char, size)
|
|
83
|
+
status = HDF5::FFI.H5Lget_val(hdf5_id, name, buffer, size, HDF5::DEFAULT_PROPERTY_LIST)
|
|
84
|
+
raise HDF5::Error, "Failed to get link value: #{name}" if status < 0
|
|
85
|
+
|
|
86
|
+
raw = buffer.read_bytes(size)
|
|
87
|
+
return { type: :soft, target: raw.delete_suffix("\0") } if type == :H5L_TYPE_SOFT
|
|
88
|
+
|
|
89
|
+
if type == :H5L_TYPE_EXTERNAL
|
|
90
|
+
filename, path = raw.byteslice(1..).split("\0", 2)
|
|
91
|
+
return { type: :external, filename:, path: path&.delete_suffix("\0") }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
raise UnsupportedTypeError, "Unsupported link type: #{type}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def move(source, destination)
|
|
98
|
+
status = HDF5::FFI.H5Lmove(hdf5_id, source, hdf5_id, destination, HDF5::DEFAULT_PROPERTY_LIST,
|
|
99
|
+
HDF5::DEFAULT_PROPERTY_LIST)
|
|
100
|
+
raise HDF5::Error, "Failed to move link: #{source}" if status < 0
|
|
101
|
+
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
prepend FileContext.guard(*instance_methods(false))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module HDF5
|
|
2
|
+
class Selection
|
|
3
|
+
attr_reader :start, :stride, :count, :result_shape
|
|
4
|
+
|
|
5
|
+
def self.normalize(selection, shape)
|
|
6
|
+
return new(Array.new(shape.length, true), shape) if selection.nil?
|
|
7
|
+
|
|
8
|
+
selectors = selection.is_a?(Array) ? selection.dup : [selection]
|
|
9
|
+
raise IndexError, 'Too many indices for dataset' if selectors.length > shape.length
|
|
10
|
+
|
|
11
|
+
selectors.concat(Array.new(shape.length - selectors.length, true))
|
|
12
|
+
new(selectors, shape)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(selectors, shape)
|
|
16
|
+
@start = []
|
|
17
|
+
@stride = []
|
|
18
|
+
@count = []
|
|
19
|
+
@result_shape = []
|
|
20
|
+
|
|
21
|
+
selectors.zip(shape).each do |selector, dimension|
|
|
22
|
+
normalize_axis(selector, dimension)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def scalar?
|
|
27
|
+
result_shape.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def size
|
|
31
|
+
count.inject(1, :*)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def normalize_axis(selector, dimension)
|
|
37
|
+
case selector
|
|
38
|
+
when true
|
|
39
|
+
@start << 0
|
|
40
|
+
@stride << 1
|
|
41
|
+
@count << dimension
|
|
42
|
+
@result_shape << dimension
|
|
43
|
+
when Integer
|
|
44
|
+
index = selector.negative? ? dimension + selector : selector
|
|
45
|
+
raise IndexError, "Index #{selector} is outside dimension of size #{dimension}" unless index.between?(0,
|
|
46
|
+
dimension - 1)
|
|
47
|
+
|
|
48
|
+
@start << index
|
|
49
|
+
@stride << 1
|
|
50
|
+
@count << 1
|
|
51
|
+
when Range
|
|
52
|
+
first, last = normalize_range(selector, dimension)
|
|
53
|
+
@start << first
|
|
54
|
+
@stride << 1
|
|
55
|
+
@count << last - first
|
|
56
|
+
@result_shape << last - first
|
|
57
|
+
when HDF5::Slice
|
|
58
|
+
first, last = normalize_range(selector.range, dimension)
|
|
59
|
+
count = (last - first).fdiv(selector.step).ceil
|
|
60
|
+
@start << first
|
|
61
|
+
@stride << selector.step
|
|
62
|
+
@count << count
|
|
63
|
+
@result_shape << count
|
|
64
|
+
else
|
|
65
|
+
raise IndexError, "Unsupported index: #{selector.inspect}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def normalize_range(range, dimension)
|
|
70
|
+
first = range.begin.nil? ? 0 : range.begin
|
|
71
|
+
last = range.end.nil? ? dimension : range.end
|
|
72
|
+
first += dimension if first.negative?
|
|
73
|
+
last += dimension if last.negative?
|
|
74
|
+
last += 1 unless range.exclude_end? || range.end.nil?
|
|
75
|
+
|
|
76
|
+
first = [[first, 0].max, dimension].min
|
|
77
|
+
last = [[last, 0].max, dimension].min
|
|
78
|
+
[first, [last, first].max]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module HDF5
|
|
2
|
+
module StringCodec
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
UTF8 = 1
|
|
6
|
+
VARIABLE_SIZE = (1 << (::FFI.type_size(:size_t) * 8)) - 1
|
|
7
|
+
|
|
8
|
+
def datatype_id
|
|
9
|
+
type_id = HDF5::FFI.H5Tcopy(HDF5::FFI.H5T_C_S1)
|
|
10
|
+
raise HDF5::Error, 'Failed to create string datatype' if type_id < 0
|
|
11
|
+
|
|
12
|
+
check(HDF5::FFI.H5Tset_size(type_id, VARIABLE_SIZE), 'set variable string size')
|
|
13
|
+
check(HDF5::FFI.H5Tset_cset(type_id, UTF8), 'set UTF-8 string encoding')
|
|
14
|
+
type_id
|
|
15
|
+
rescue StandardError
|
|
16
|
+
HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
|
|
17
|
+
raise
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def buffer_for(value)
|
|
21
|
+
buffer_for_values([normalize(value)])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def buffer_for_values(values)
|
|
25
|
+
pointers = values.map { |value| ::FFI::MemoryPointer.from_string(normalize(value)) }
|
|
26
|
+
buffer = ::FFI::MemoryPointer.new(:pointer, pointers.length)
|
|
27
|
+
buffer.write_array_of_pointer(pointers)
|
|
28
|
+
[buffer, pointers]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def read(buffer)
|
|
32
|
+
pointer = buffer.read_pointer
|
|
33
|
+
pointer.null? ? nil : pointer.read_string.force_encoding(Encoding::UTF_8)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read_values(buffer, count, shape)
|
|
37
|
+
values = buffer.read_array_of_pointer(count).map do |pointer|
|
|
38
|
+
pointer.null? ? nil : pointer.read_string.force_encoding(Encoding::UTF_8)
|
|
39
|
+
end
|
|
40
|
+
return values.first if shape.empty?
|
|
41
|
+
|
|
42
|
+
Numo::RObject.cast(values).reshape(*shape)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def string_data?(value)
|
|
46
|
+
return true if value.is_a?(String)
|
|
47
|
+
|
|
48
|
+
values = if value.is_a?(Numo::RObject)
|
|
49
|
+
value.to_a.flatten
|
|
50
|
+
elsif value.is_a?(Array)
|
|
51
|
+
value.flatten
|
|
52
|
+
else
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
!values.empty? && values.all? { |item| item.is_a?(String) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def normalize_data(value)
|
|
59
|
+
return [[normalize(value)], []] if value.is_a?(String)
|
|
60
|
+
return [value.to_a.flatten.map { |item| normalize(item) }, value.shape] if value.is_a?(Numo::RObject)
|
|
61
|
+
|
|
62
|
+
shape = array_shape(value)
|
|
63
|
+
[value.flatten.map { |item| normalize(item) }, shape]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def array_shape(value)
|
|
67
|
+
return [] unless value.is_a?(Array)
|
|
68
|
+
raise HDF5::Error, 'String data must not be empty' if value.empty?
|
|
69
|
+
|
|
70
|
+
child_shapes = value.map { |item| array_shape(item) }
|
|
71
|
+
raise HDF5::ShapeError, 'String data must be rectangular' unless child_shapes.uniq.length == 1
|
|
72
|
+
|
|
73
|
+
[value.length, *child_shapes.first]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def variable?(type_id)
|
|
77
|
+
HDF5::FFI.H5Tis_variable_str(type_id).positive?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize(value)
|
|
81
|
+
raise HDF5::Error, 'String dataset data must be a String' unless value.is_a?(String)
|
|
82
|
+
raise HDF5::Error, 'Variable-length strings cannot contain NUL bytes' if value.include?("\0")
|
|
83
|
+
|
|
84
|
+
value.encode(Encoding::UTF_8)
|
|
85
|
+
rescue EncodingError
|
|
86
|
+
raise HDF5::Error, 'String dataset data must be valid UTF-8'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def check(status, operation)
|
|
90
|
+
raise HDF5::Error, "Failed to #{operation}" if status < 0
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/hdf5/version.rb
CHANGED
data/lib/hdf5.rb
CHANGED
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'ffi'
|
|
4
|
+
require 'English'
|
|
5
|
+
require 'numo/narray/alt'
|
|
4
6
|
require_relative 'hdf5/version'
|
|
5
7
|
|
|
6
8
|
module HDF5
|
|
7
9
|
class Error < StandardError; end
|
|
10
|
+
class ClosedError < Error; end
|
|
11
|
+
class UnsupportedTypeError < Error; end
|
|
12
|
+
class UnsupportedFeatureError < Error; end
|
|
13
|
+
class ShapeError < Error; end
|
|
14
|
+
class ConversionError < Error; end
|
|
15
|
+
class NativeError < Error; end
|
|
16
|
+
|
|
17
|
+
class Empty
|
|
18
|
+
attr_reader :dtype
|
|
19
|
+
|
|
20
|
+
def initialize(dtype)
|
|
21
|
+
@dtype = dtype.is_a?(DType) ? dtype : DType.for_symbol(dtype)
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
Slice = Struct.new(:range, :step, keyword_init: true)
|
|
8
26
|
DEFAULT_PROPERTY_LIST = 0
|
|
9
27
|
|
|
10
28
|
class << self
|
|
11
29
|
attr_accessor :lib_path
|
|
12
30
|
|
|
31
|
+
def slice(range, step:)
|
|
32
|
+
raise ArgumentError, 'slice step must be a positive integer' unless step.is_a?(Integer) && step.positive?
|
|
33
|
+
raise ArgumentError, 'slice range must be a Range' unless range.is_a?(Range)
|
|
34
|
+
|
|
35
|
+
Slice.new(range:, step:)
|
|
36
|
+
end
|
|
37
|
+
|
|
13
38
|
def search_hdf5lib
|
|
14
39
|
name = "libhdf5.#{FFI::Platform::LIBSUFFIX}"
|
|
15
40
|
env_path = ENV['HDF5_LIB_PATH']
|
|
@@ -37,6 +62,12 @@ end
|
|
|
37
62
|
|
|
38
63
|
require_relative 'hdf5/ffi'
|
|
39
64
|
|
|
65
|
+
require_relative 'hdf5/file_context'
|
|
66
|
+
require_relative 'hdf5/dtype'
|
|
67
|
+
require_relative 'hdf5/data_helpers'
|
|
68
|
+
require_relative 'hdf5/selection'
|
|
69
|
+
require_relative 'hdf5/hierarchy'
|
|
70
|
+
require_relative 'hdf5/string_codec'
|
|
40
71
|
require_relative 'hdf5/file'
|
|
41
72
|
require_relative 'hdf5/group'
|
|
42
73
|
require_relative 'hdf5/dataset'
|