ruby-hdf5 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a5d336f6b45f913a829873d20aba6c80c5724db14038965e073235f36477733
4
- data.tar.gz: 1fe31c4c72258d31d57ca6ce043aee21f7cd9083700c77405a501a79f1ea5323
3
+ metadata.gz: 4591bafb2585ec00df81fc3831cc7037cc02dee81f15919ec7cf1b5d2f08a4c2
4
+ data.tar.gz: beb661f3c825adaab6b132cee69a402c1319d05143a0bec1516f2e0dec1180b2
5
5
  SHA512:
6
- metadata.gz: 786821e23be9bdc249352c34dc5e293be4a6f3e931c69cc89a1f51ee284d53e00d2ccdb130ee49a2e5bf467ff89cf60f184c455ffc3271309ea0c37a189b28eb
7
- data.tar.gz: 98150386a48675d3208416cca2c1d4f1668f9519c2acafbb2935ae0d8111789a1de890d1516640c2649bc463d8c07f1144acad050a018f209fe429cf5d394821
6
+ metadata.gz: bf13704291dfbdb2d0d61cf03504574a553e3d16e6db980355df5408ee425e340cfa1bc7b3850c4f877aa066958f20ab4cb5cbb07935f6302eb403771c6b4458
7
+ data.tar.gz: ac8511ab873fea84933c38c65c15c899cfb3dbc7a63d2a062f41d78722bcc87288d84997e5f296c38691deb2660e065046e7fdf13e2c611127ef8fc84b1ce2ea
data/README.md CHANGED
@@ -11,14 +11,19 @@ This gem currently provides practical high-level wrappers for:
11
11
  - opening and creating files
12
12
  - creating groups
13
13
  - creating, writing, and reading one-dimensional numeric datasets
14
- - reading attributes
14
+ - reading and writing numeric attributes
15
15
 
16
16
  Unsupported at this stage:
17
17
 
18
18
  - string dataset read/write
19
- - attribute write
20
19
  - multidimensional array write
21
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
+
22
27
  ## Supported HDF5 Versions
23
28
 
24
29
  - HDF5 1.10
@@ -3,18 +3,18 @@ module HDF5
3
3
  def initialize(dataset_id, attr_name)
4
4
  @dataset_id = dataset_id
5
5
  @attr_name = attr_name
6
- @attr_id = FFI.H5Aopen(@dataset_id, @attr_name, 0)
7
- raise 'Failed to open attribute' if @attr_id < 0
6
+ @attr_id = HDF5::FFI.H5Aopen(@dataset_id, @attr_name, HDF5::DEFAULT_PROPERTY_LIST)
7
+ raise HDF5::Error, 'Failed to open attribute' if @attr_id < 0
8
8
  end
9
9
 
10
10
  def read
11
- type_id = FFI.H5Aget_type(@attr_id)
12
- space_id = FFI.H5Aget_space(@attr_id)
11
+ type_id = HDF5::FFI.H5Aget_type(@attr_id)
12
+ space_id = HDF5::FFI.H5Aget_space(@attr_id)
13
13
 
14
- size = FFI.H5Sget_simple_extent_npoints(space_id)
14
+ size = HDF5::FFI.H5Sget_simple_extent_npoints(space_id)
15
15
 
16
16
  buffer = \
17
- case FFI.H5Tget_class(type_id)
17
+ case HDF5::FFI.H5Tget_class(type_id)
18
18
  when :H5T_INTEGER
19
19
  ::FFI::MemoryPointer.new(:int, size)
20
20
  when :H5T_FLOAT
@@ -22,13 +22,13 @@ module HDF5
22
22
  when :H5T_STRING
23
23
  ::FFI::MemoryPointer.new(:pointer, size)
24
24
  else
25
- raise 'Unsupported data type'
25
+ raise HDF5::Error, 'Unsupported data type'
26
26
  end
27
27
 
28
- status = FFI.H5Aread(@attr_id, type_id, buffer)
29
- raise 'Failed to read attribute' if status < 0
28
+ status = HDF5::FFI.H5Aread(@attr_id, type_id, buffer)
29
+ raise HDF5::Error, 'Failed to read attribute' if status < 0
30
30
 
31
- case FFI.H5Tget_class(type_id)
31
+ case HDF5::FFI.H5Tget_class(type_id)
32
32
  when :H5T_INTEGER
33
33
  buffer.read_array_of_int(size)
34
34
  when :H5T_FLOAT
@@ -36,12 +36,18 @@ module HDF5
36
36
  when :H5T_STRING
37
37
  buffer.read_pointer.read_string
38
38
  else
39
- raise 'Unsupported data type'
39
+ raise HDF5::Error, 'Unsupported data type'
40
40
  end
41
+ ensure
42
+ HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
43
+ HDF5::FFI.H5Sclose(space_id) if space_id && space_id >= 0
41
44
  end
42
45
 
43
46
  def close
44
- FFI.H5Aclose(@attr_id)
47
+ return if @attr_id.nil?
48
+
49
+ HDF5::FFI.H5Aclose(@attr_id)
50
+ @attr_id = nil
45
51
  end
46
52
  end
47
53
 
@@ -56,5 +62,95 @@ module HDF5
56
62
  ensure
57
63
  attr.close if attr
58
64
  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
+ exists = HDF5::FFI.H5Aexists(@dataset_id, attr_name)
75
+ raise HDF5::Error, "Failed to check attribute existence: #{attr_name}" if exists.negative?
76
+
77
+ if exists.positive?
78
+ status = HDF5::FFI.H5Adelete(@dataset_id, attr_name)
79
+ raise HDF5::Error, "Failed to replace attribute: #{attr_name}" if status < 0
80
+ end
81
+
82
+ dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
83
+ dims.write_array_of_ulong_long([values.length])
84
+ dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
85
+ raise HDF5::Error, 'Failed to create attribute dataspace' if dataspace_id < 0
86
+
87
+ attr_id = HDF5::FFI.H5Acreate2(
88
+ @dataset_id,
89
+ attr_name,
90
+ datatype_id,
91
+ dataspace_id,
92
+ HDF5::DEFAULT_PROPERTY_LIST,
93
+ HDF5::DEFAULT_PROPERTY_LIST
94
+ )
95
+ raise HDF5::Error, "Failed to create attribute: #{attr_name}" if attr_id < 0
96
+
97
+ buffer = buffer_for(values)
98
+ status = HDF5::FFI.H5Awrite(attr_id, datatype_id, buffer)
99
+ raise HDF5::Error, "Failed to write attribute: #{attr_name}" if status < 0
100
+
101
+ value
102
+ ensure
103
+ HDF5::FFI.H5Aclose(attr_id) if attr_id && attr_id >= 0
104
+ HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
105
+ end
106
+
107
+ private
108
+
109
+ def normalize_data(value)
110
+ values = value.is_a?(Array) ? value : [value]
111
+ raise HDF5::Error, 'Attribute data must not be empty' if values.empty?
112
+ raise HDF5::Error, 'Nested arrays are not supported for attributes' if values.any? { |item| item.is_a?(Array) }
113
+
114
+ values
115
+ end
116
+
117
+ def datatype_id_for(values)
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
126
+ end
127
+
128
+ def buffer_for(values)
129
+ if values.all? { |item| item.is_a?(Integer) }
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
136
+
137
+ buffer
138
+ end
139
+
140
+ def native_int_bounds
141
+ bits = ::FFI.type_size(:int) * 8
142
+ max = (1 << (bits - 1)) - 1
143
+ min = -(1 << (bits - 1))
144
+ [min, max]
145
+ end
146
+
147
+ def validate_native_int_range!(values)
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
59
155
  end
60
156
  end
data/lib/hdf5/dataset.rb CHANGED
@@ -1,11 +1,64 @@
1
1
  module HDF5
2
2
  class Dataset
3
+ module DataHelpers
4
+ module_function
5
+
6
+ def normalize_data(data)
7
+ values = data.is_a?(Array) ? data : [data]
8
+ raise HDF5::Error, 'Dataset data must not be empty' if values.empty?
9
+ raise HDF5::Error, 'Nested arrays are not supported' if values.any? { |value| value.is_a?(Array) }
10
+
11
+ values
12
+ end
13
+
14
+ def datatype_id_for(data)
15
+ if data.all? { |value| value.is_a?(Integer) }
16
+ validate_native_int_range!(data)
17
+ HDF5::FFI.H5T_NATIVE_INT
18
+ elsif data.all? { |value| value.is_a?(Numeric) }
19
+ HDF5::FFI.H5T_NATIVE_DOUBLE
20
+ else
21
+ raise HDF5::Error, 'Only numeric dataset data is supported'
22
+ end
23
+ end
24
+
25
+ def buffer_for(data)
26
+ if data.all? { |value| value.is_a?(Integer) }
27
+ buffer = ::FFI::MemoryPointer.new(:int, data.length)
28
+ buffer.write_array_of_int(data)
29
+ else
30
+ buffer = ::FFI::MemoryPointer.new(:double, data.length)
31
+ buffer.write_array_of_double(data.map(&:to_f))
32
+ end
33
+
34
+ buffer
35
+ end
36
+
37
+ def native_int_bounds
38
+ bits = ::FFI.type_size(:int) * 8
39
+ max = (1 << (bits - 1)) - 1
40
+ min = -(1 << (bits - 1))
41
+ [min, max]
42
+ end
43
+
44
+ def validate_native_int_range!(values)
45
+ min, max = native_int_bounds
46
+ out_of_range = values.find { |value| value < min || value > max }
47
+ return unless out_of_range
48
+
49
+ raise HDF5::Error,
50
+ "Integer value #{out_of_range} is outside native int range (#{min}..#{max}). Use a smaller value."
51
+ end
52
+ end
53
+
54
+ private_constant :DataHelpers
55
+
3
56
  class << self
4
57
  def create(parent_id, name, data)
5
- values = normalize_data(data)
58
+ values = DataHelpers.normalize_data(data)
6
59
  dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
7
60
  dims.write_array_of_ulong_long([values.length])
8
- datatype_id = datatype_id_for(values)
61
+ datatype_id = DataHelpers.datatype_id_for(values)
9
62
  dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
10
63
  raise HDF5::Error, "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
11
64
 
@@ -36,36 +89,6 @@ module HDF5
36
89
  end
37
90
  end
38
91
 
39
- def normalize_data(data)
40
- values = data.is_a?(Array) ? data : [data]
41
- raise HDF5::Error, 'Dataset data must not be empty' if values.empty?
42
- raise HDF5::Error, 'Nested arrays are not supported' if values.any? { |value| value.is_a?(Array) }
43
-
44
- values
45
- end
46
-
47
- def datatype_id_for(data)
48
- if data.all? { |value| value.is_a?(Integer) }
49
- HDF5::FFI.H5T_NATIVE_INT
50
- elsif data.all? { |value| value.is_a?(Numeric) }
51
- HDF5::FFI.H5T_NATIVE_DOUBLE
52
- else
53
- raise HDF5::Error, 'Only numeric dataset data is supported'
54
- end
55
- end
56
-
57
- def buffer_for(data)
58
- if data.all? { |value| value.is_a?(Integer) }
59
- buffer = ::FFI::MemoryPointer.new(:int, data.length)
60
- buffer.write_array_of_int(data)
61
- else
62
- buffer = ::FFI::MemoryPointer.new(:double, data.length)
63
- buffer.write_array_of_double(data.map(&:to_f))
64
- end
65
-
66
- buffer
67
- end
68
-
69
92
  private
70
93
 
71
94
  def from_id(dataset_id, name)
@@ -84,9 +107,9 @@ module HDF5
84
107
  end
85
108
 
86
109
  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)
110
+ values = DataHelpers.normalize_data(data)
111
+ mem_type_id = DataHelpers.datatype_id_for(values)
112
+ buffer = DataHelpers.buffer_for(values)
90
113
  status = HDF5::FFI.H5Dwrite(@dataset_id, mem_type_id, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST,
91
114
  HDF5::DEFAULT_PROPERTY_LIST, buffer)
92
115
  raise HDF5::Error, 'Failed to write dataset' if status < 0
@@ -160,8 +183,8 @@ module HDF5
160
183
  buffer.read_array_of_double(total_elements)
161
184
  end
162
185
 
163
- def read_string_data(total_elements)
164
- raise NotImplementedError
186
+ def read_string_data(_total_elements)
187
+ raise HDF5::Error, 'String dataset reading is not supported yet'
165
188
  end
166
189
 
167
190
  private
data/lib/hdf5/file.rb CHANGED
@@ -79,7 +79,7 @@ module HDF5
79
79
  elsif dataset?(name)
80
80
  Dataset.open(@file_id, name)
81
81
  else
82
- raise HDF5::Error, 'Unknown object type'
82
+ raise HDF5::Error, "Group or dataset not found: #{name}"
83
83
  end
84
84
  end
85
85
 
data/lib/hdf5/group.rb CHANGED
@@ -54,10 +54,10 @@ module HDF5
54
54
  Dataset.create(@group_id, name, data, &block)
55
55
  end
56
56
 
57
- def list_datasets
58
- datasets = []
57
+ def list_entries
58
+ entries = []
59
59
  callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
60
- datasets << name
60
+ entries << name
61
61
  0 # continue
62
62
  end
63
63
 
@@ -66,9 +66,13 @@ module HDF5
66
66
  else
67
67
  HDF5::FFI.H5Literate2(@group_id, :H5_INDEX_NAME, :H5_ITER_NATIVE, nil, callback, nil)
68
68
  end).negative? &&
69
- raise('Failed to list datasets')
69
+ raise(HDF5::Error, 'Failed to list entries')
70
+
71
+ entries
72
+ end
70
73
 
71
- datasets
74
+ def list_datasets
75
+ list_entries.select { |name| dataset?(name) }
72
76
  end
73
77
 
74
78
  def [](name)
@@ -77,7 +81,7 @@ module HDF5
77
81
  elsif dataset?(name)
78
82
  Dataset.open(@group_id, name)
79
83
  else
80
- raise HDF5::Error, 'Group or Dataset not found'
84
+ raise HDF5::Error, "Group or dataset not found: #{name}"
81
85
  end
82
86
  end
83
87
 
data/lib/hdf5/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HDF5
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-hdf5
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2