htslib 0.4.2 → 0.6.0

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -51
  3. data/TUTORIAL.md +11 -22
  4. data/ext/htslib_native/extconf.rb +41 -0
  5. data/ext/htslib_native/htslib_native.h +11 -0
  6. data/ext/htslib_native/htslib_native_ext.c +48 -0
  7. data/ext/htslib_native/native_bam.c +1167 -0
  8. data/ext/htslib_native/native_bcf.c +447 -0
  9. data/ext/htslib_native/native_faidx.c +164 -0
  10. data/ext/htslib_native/native_tabix.c +255 -0
  11. data/lib/hts/bam/auxi.rb +87 -342
  12. data/lib/hts/bam/base_mod.rb +38 -73
  13. data/lib/hts/bam/cigar.rb +9 -55
  14. data/lib/hts/bam/flag.rb +24 -30
  15. data/lib/hts/bam/header.rb +29 -56
  16. data/lib/hts/bam/mpileup.rb +158 -132
  17. data/lib/hts/bam/pileup.rb +120 -145
  18. data/lib/hts/bam/record.rb +233 -270
  19. data/lib/hts/bam.rb +154 -45
  20. data/lib/hts/bcf/errors.rb +4 -0
  21. data/lib/hts/bcf/format.rb +283 -377
  22. data/lib/hts/bcf/header.rb +40 -122
  23. data/lib/hts/bcf/header_record.rb +9 -35
  24. data/lib/hts/bcf/info.rb +73 -320
  25. data/lib/hts/bcf/record.rb +73 -101
  26. data/lib/hts/bcf.rb +178 -321
  27. data/lib/hts/faidx.rb +30 -87
  28. data/lib/hts/hts.rb +6 -94
  29. data/lib/hts/native.rb +13 -0
  30. data/lib/hts/tabix.rb +109 -51
  31. data/lib/hts/version.rb +1 -1
  32. data/lib/htslib.rb +6 -52
  33. metadata +13 -64
  34. data/lib/hts/ffi_ext/README.md +0 -8
  35. data/lib/hts/ffi_ext/pointer.rb +0 -18
  36. data/lib/hts/ffi_ext/struct.rb +0 -45
  37. data/lib/hts/libhts/bgzf.rb +0 -199
  38. data/lib/hts/libhts/constants.rb +0 -658
  39. data/lib/hts/libhts/cram.rb +0 -471
  40. data/lib/hts/libhts/fai.rb +0 -146
  41. data/lib/hts/libhts/hfile.rb +0 -121
  42. data/lib/hts/libhts/hts.rb +0 -477
  43. data/lib/hts/libhts/kfunc.rb +0 -38
  44. data/lib/hts/libhts/sam.rb +0 -760
  45. data/lib/hts/libhts/sam_funcs.rb +0 -155
  46. data/lib/hts/libhts/tbx.rb +0 -94
  47. data/lib/hts/libhts/tbx_funcs.rb +0 -36
  48. data/lib/hts/libhts/thread_pool.rb +0 -139
  49. data/lib/hts/libhts/vcf.rb +0 -567
  50. data/lib/hts/libhts/vcf_funcs.rb +0 -366
  51. data/lib/hts/libhts.rb +0 -47
@@ -3,500 +3,406 @@
3
3
  module HTS
4
4
  class Bcf < Hts
5
5
  class Format
6
- def initialize(record)
7
- @record = record
8
- end
6
+ GT_MISSING = 0
7
+ GT_VECTOR_END = Native::BCF_INT32_VECTOR_END
9
8
 
10
- # @note: Why is this method named "get" instead of "fetch"?
11
- # This is for compatibility with the Crystal language
12
- # which provides methods like `get_int`, `get_float`, etc.
13
- # I think they are better than `fetch_int`` and `fetch_float`.
14
- def get(key, type = nil)
15
- return get_typed(key, type) unless type.nil?
16
-
17
- return decode_genotypes if key == "GT"
18
-
19
- case header_format_type(key)
20
- when :int
21
- decode_integer_values(key)
22
- when :float
23
- decode_float_values(key)
24
- when :flag
25
- raise_unsupported_format_flag(key)
26
- when :string
27
- get_string_values(key)
28
- end
9
+ class << self
10
+ def gt_unphased(allele) = (Integer(allele) + 1) << 1
11
+ def gt_phased(allele) = ((Integer(allele) + 1) << 1) | 1
12
+ def gt_allele(value) = (Integer(value) >> 1) - 1
13
+ def gt_missing?(value) = (Integer(value) >> 1).zero?
14
+ def gt_phased?(value) = (Integer(value) & 1) == 1
15
+ def gt_vector_end?(value) = Integer(value) == GT_VECTOR_END
29
16
  end
30
17
 
31
- def get_raw(key, type = nil)
32
- # The GT FORMAT field is special in that it is marked as a string in the header,
33
- # but it is actually encoded as an integer.
34
- type = if type.nil?
35
- key == "GT" ? :int : header_format_type(key)
36
- else
37
- type.to_sym
38
- end
18
+ BufferState = Struct.new(:generation)
39
19
 
40
- case type
41
- when :int, :int32
42
- raise_unsupported_format_flag(key)
43
- get_numeric_values(key, LibHTS::BCF_HT_INT, "integer") { |dst, len| dst.read_array_of_int32(len) }
44
- when :float, :real
45
- raise_unsupported_format_flag(key)
46
- get_float_words(key)
47
- when :flag
48
- raise_unsupported_format_flag(key)
49
- when :string, :str
50
- return decode_genotypes if key == "GT"
51
-
52
- raise_unsupported_format_flag(key)
53
- get_string_values(key)
20
+ class NumericVectorView
21
+ include Enumerable
22
+ def initialize(type) = @type = type
23
+
24
+ def reset(values, buffer, generation)
25
+ @values = values
26
+ @buffer = buffer
27
+ @generation = generation
28
+ self
54
29
  end
55
- end
56
30
 
57
- # For compatibility with HTS.cr.
58
- def get_int(key)
59
- get_raw(key, :int)
60
- end
31
+ def each(&block)
32
+ return to_enum(__method__) unless block_given?
61
33
 
62
- # For compatibility with HTS.cr.
63
- def get_float(key)
64
- get_typed(key, :float)
65
- end
34
+ ensure_valid!
35
+ @values.each(&block)
36
+ self
37
+ end
66
38
 
67
- # For compatibility with HTS.cr.
68
- def get_flag(key)
69
- get_raw(key, :flag)
70
- end
39
+ def to_a = each.to_a
71
40
 
72
- # For compatibility with HTS.cr.
73
- def get_string(key)
74
- get_raw(key, :string)
75
- end
41
+ private
76
42
 
77
- # For compatibility with HTS.cr.
78
- def get_genotypes
79
- get_numeric_values("GT", LibHTS::BCF_HT_INT, "genotype") { |dst, len| dst.read_array_of_int32(len) }
80
- end
43
+ def ensure_valid!
44
+ return if @buffer.generation == @generation
81
45
 
82
- def [](key)
83
- get(key)
46
+ raise InvalidBorrowedViewError,
47
+ "borrowed FORMAT view is no longer valid"
48
+ end
84
49
  end
85
50
 
86
- def update_int(key, values)
87
- raise UnsupportedFormatOperationError, "Use update_genotypes for GT" if key == "GT"
51
+ class GenotypeView
52
+ include Enumerable
88
53
 
89
- ensure_expected_format_type!(key, :int, "integer")
90
- values = normalize_int_values(values)
91
- validate_numeric_sample_count!(key, values.size)
54
+ def reset(values, buffer, generation)
55
+ @values = values
56
+ @buffer = buffer
57
+ @generation = generation
58
+ self
59
+ end
92
60
 
93
- ptr = FFI::MemoryPointer.new(:int32, values.size)
94
- ptr.write_array_of_int32(values)
95
- check_update_rc!(LibHTS.bcf_update_format_int32(@record.header.struct, @record.struct, key, ptr, values.size),
96
- key)
97
- end
61
+ def each_allele
62
+ return to_enum(__method__) unless block_given?
98
63
 
99
- def update_float(key, values)
100
- ensure_expected_format_type!(key, :float, "float")
101
- values = normalize_float_values(values)
102
- validate_numeric_sample_count!(key, values.size)
64
+ ensure_valid!
65
+ @values.each_with_index do |encoded, index|
66
+ break if encoded == GT_VECTOR_END
103
67
 
104
- ptr = FFI::MemoryPointer.new(:float, values.size)
105
- ptr.write_array_of_float(values)
106
- check_update_rc!(LibHTS.bcf_update_format_float(@record.header.struct, @record.struct, key, ptr, values.size),
107
- key)
108
- end
68
+ missing = gt_missing?(encoded)
69
+ # The phase bit describes the separator before this allele. It has
70
+ # no semantic meaning for the first allele, and HTSlib versions do
71
+ # not consistently clear it while parsing VCF text.
72
+ phased = index.positive? && gt_phased?(encoded)
73
+ yield(missing ? nil : gt_allele(encoded), phased, missing)
74
+ end
75
+ self
76
+ end
77
+ alias each each_allele
78
+ def to_s
79
+ result = +""
80
+ each_allele.with_index do |(allele, phased, missing), index|
81
+ result << (phased && index.positive? ? "|" : "/") if index.positive?
82
+ result << (missing ? "." : allele.to_s)
83
+ end
84
+ result
85
+ end
109
86
 
110
- def update_string(key, values)
111
- raise UnsupportedFormatOperationError, "Use update_genotypes for GT" if key == "GT"
87
+ private
112
88
 
113
- ensure_expected_format_type!(key, :string, "string")
114
- values = normalize_string_values(values)
115
- validate_string_sample_count!(key, values.size)
89
+ def ensure_valid!
90
+ return if @buffer.generation == @generation
91
+
92
+ raise InvalidBorrowedViewError,
93
+ "borrowed FORMAT view is no longer valid"
94
+ end
116
95
 
117
- strings = values.map { |value| FFI::MemoryPointer.from_string(value) }
118
- ptr = FFI::MemoryPointer.new(:pointer, strings.size)
119
- ptr.write_array_of_pointer(strings)
120
- check_update_rc!(LibHTS.bcf_update_format_string(@record.header.struct, @record.struct, key, ptr, values.size),
121
- key)
96
+ def gt_missing?(value) = (value >> 1).zero?
97
+ def gt_allele(value) = (value >> 1) - 1
98
+ def gt_phased?(value) = (value & 1) == 1
122
99
  end
123
100
 
124
- def update_genotypes(values)
125
- ensure_gt_defined!
101
+ def initialize(record)
102
+ @record = record
103
+ @buffers = {}
104
+ end
126
105
 
127
- values = normalize_int_values(values)
128
- validate_numeric_sample_count!("GT", values.size)
106
+ def get(key, type = nil)
107
+ key = key.to_s
108
+ schema = format_schema(key)
109
+ return nil unless schema
129
110
 
130
- ptr = FFI::MemoryPointer.new(:int32, values.size)
131
- ptr.write_array_of_int32(values)
132
- check_update_rc!(LibHTS.bcf_update_genotypes(@record.header.struct, @record.struct, ptr, values.size), "GT")
133
- end
111
+ raise_unsupported_flag(key) if schema.first == :flag
134
112
 
135
- def delete(key)
136
- return false if header_format_type(key).nil?
137
- return false unless format_present?(key)
138
-
139
- type = key == "GT" ? LibHTS::BCF_HT_INT : header_format_type_code(key)
140
- ret = LibHTS.bcf_update_format(@record.header.struct, @record.struct, key, FFI::Pointer::NULL, 0, type)
141
- raise FormatUpdateError, "Failed to delete FORMAT field '#{key}': #{ret}" if ret < 0
142
-
143
- true
144
- end
145
-
146
- def fields
147
- ids.map do |id|
148
- name = LibHTS.bcf_hdr_int2id(@record.header.struct, LibHTS::BCF_DT_ID, id)
149
- num = LibHTS.bcf_hdr_id2number(@record.header.struct, LibHTS::BCF_HL_FMT, id)
150
- type = LibHTS.bcf_hdr_id2type(@record.header.struct, LibHTS::BCF_HL_FMT, id)
151
- {
152
- name:,
153
- n: num,
154
- type: ht_type_to_sym(type),
155
- id:
156
- }
113
+ requested = type&.to_sym
114
+ if requested && !type_compatible?(schema.first, requested, key)
115
+ raise FormatTypeError, "Tag #{key} is not #{type_label(requested)} FORMAT field"
157
116
  end
158
- end
117
+ return genotype_strings(key) if key == "GT" && (!requested || %i[string str].include?(requested))
159
118
 
160
- def length
161
- @record.struct[:n_fmt]
162
- end
119
+ return get_float(key) if %i[float real].include?(requested)
163
120
 
164
- def size
165
- length
121
+ raw = get_raw(key, requested)
122
+ return raw if requested
123
+ return raw if schema.first == :string
124
+
125
+ shape_values(raw, key, schema)
166
126
  end
167
127
 
168
- def to_h
169
- ret = {}
170
- ids.each do |id|
171
- name = LibHTS.bcf_hdr_int2id(@record.header.struct, LibHTS::BCF_DT_ID, id)
172
- ret[name] = get(name)
128
+ def get_raw(key, type = nil)
129
+ key = key.to_s
130
+ schema = format_schema(key)
131
+ return nil unless schema
132
+
133
+ requested = type&.to_sym
134
+ if requested && !type_compatible?(schema.first, requested, key)
135
+ raise FormatTypeError, "Tag #{key} is not #{type_label(requested)} FORMAT field"
173
136
  end
174
- ret
175
- end
176
137
 
177
- # def genotypes; end
138
+ code = key == "GT" ? Native::BCF_HT_INT : type_code(requested || schema.first)
139
+ raw_float = code == Native::BCF_HT_REAL
140
+ invalidate_views!(key)
141
+ native.format_get(header_native, key, code, raw_float)
142
+ end
178
143
 
179
- private
144
+ def get_int_raw(key) = get_raw(key, :int)
180
145
 
181
- def get_numeric_values(key, hts_type, expected_type)
182
- ndst = FFI::MemoryPointer.new(:int)
183
- ndst.write_int(0)
184
- dst_ptr = FFI::MemoryPointer.new(:pointer)
185
- dst_ptr.write_pointer(FFI::Pointer::NULL)
186
-
187
- ret = LibHTS.bcf_get_format_values(@record.header.struct, @record.struct, key, dst_ptr, ndst, hts_type)
188
- ret = normalize_format_rc(ret, key, expected_type)
189
- return nil unless ret
190
-
191
- dst = dst_ptr.read_pointer
192
- begin
193
- yield(dst, ret)
194
- ensure
195
- LibHTS.hts_free(dst) unless dst.null?
196
- dst_ptr.write_pointer(FFI::Pointer::NULL)
146
+ def get_int(key)
147
+ values = get_int_raw(key)
148
+ values&.each_with_object([]) do |value, decoded|
149
+ decoded << missing_int(value) unless value == Native::BCF_INT32_VECTOR_END
197
150
  end
198
151
  end
199
152
 
200
- def get_string_values(key)
201
- ndst = FFI::MemoryPointer.new(:int)
202
- ndst.write_int(0)
203
- dst_ptr = FFI::MemoryPointer.new(:pointer)
204
- dst_ptr.write_pointer(FFI::Pointer::NULL)
205
-
206
- ret = LibHTS.bcf_get_format_string(@record.header.struct, @record.struct, key, dst_ptr, ndst)
207
- ret = normalize_format_rc(ret, key, "string")
208
- return nil unless ret
209
-
210
- dst = dst_ptr.read_pointer
211
- sample_count = @record.header.nsamples
212
- begin
213
- dst.read_array_of_pointer(sample_count).map(&:read_string)
214
- ensure
215
- unless dst.null?
216
- collapsed = sample_count.positive? ? dst.get_pointer(0) : FFI::Pointer::NULL
217
- LibHTS.hts_free(collapsed) unless collapsed.null?
218
- LibHTS.hts_free(dst)
219
- end
220
- dst_ptr.write_pointer(FFI::Pointer::NULL)
221
- end
153
+ def get_float(key)
154
+ words = get_raw(key, :float)
155
+ words&.map { |word| decode_float_word(word) }
222
156
  end
223
157
 
224
- def decode_integer_values(key)
158
+ def get_flag(key) = get(key, :flag)
159
+ def get_string(key) = get(key, :string)
160
+ def get_genotypes = get_raw("GT", :int)
161
+ def [](key) = get(key)
162
+
163
+ def each_genotype(key = "GT")
164
+ return enum_for(__method__, key) unless block_given?
165
+ raise ArgumentError, "genotype FORMAT key must be GT" unless key.to_s == "GT"
166
+
225
167
  values = get_raw(key, :int)
226
168
  return nil unless values
227
169
 
228
- sample_values = split_sample_values(values)
229
- if scalar_format?(key)
230
- sample_values.map do |values_per_sample|
231
- map_integer_missing_value(trim_integer_vector_end(values_per_sample).first)
232
- end
233
- else
234
- sample_values.map do |values_per_sample|
235
- map_integer_missing(trim_integer_vector_end(values_per_sample))
236
- end
170
+ count, width = sample_layout(values.length)
171
+ buffer, generation = advance_buffer(key, :genotype)
172
+ view = GenotypeView.new
173
+ count.times do |sample|
174
+ yield sample, view.reset(values.slice(sample * width, width), buffer, generation)
237
175
  end
176
+ self
238
177
  end
239
178
 
240
- def decode_float_values(key)
241
- values = get_float_words(key)
179
+ def genotype_at(key, sample_index)
180
+ values = get_raw(key, :int)
242
181
  return nil unless values
243
182
 
244
- sample_values = split_sample_values(values)
245
- if scalar_format?(key)
246
- sample_values.map do |values_per_sample|
247
- decode_float_word(trim_float_vector_end(values_per_sample).first)
248
- end
249
- else
250
- sample_values.map do |values_per_sample|
251
- map_float_words(trim_float_vector_end(values_per_sample))
252
- end
253
- end
254
- end
183
+ count, width = sample_layout(values.length)
184
+ sample_index = Integer(sample_index)
185
+ sample_index += count if sample_index.negative?
186
+ raise IndexError, "sample index #{sample_index} outside of FORMAT" unless sample_index.between?(0, count - 1)
255
187
 
256
- def decode_genotypes
257
- genotypes = get_genotypes
258
- return nil unless genotypes
188
+ buffer, generation = advance_buffer(key, :genotype)
189
+ GenotypeView.new.reset(values.slice(sample_index * width, width), buffer, generation)
190
+ end
259
191
 
260
- split_sample_values(genotypes).map do |sample_values|
261
- decode_genotype_sample(trim_genotype_vector_end(sample_values))
262
- end
192
+ def genotype_strings(key = "GT")
193
+ strings = []
194
+ found = each_genotype(key) { |_, genotype| strings << genotype.to_s }
195
+ found ? strings : nil
263
196
  end
264
197
 
265
- def decode_genotype_sample(values)
266
- values.each_with_index.map do |value, index|
267
- allele = if LibHTS.bcf_gt_is_missing(value) != 0
268
- "."
269
- else
270
- LibHTS.bcf_gt_allele(value).to_s
271
- end
198
+ def each_i32(key)
199
+ return enum_for(__method__, key) unless block_given?
272
200
 
273
- next allele if index.zero?
201
+ ensure_scalar!(key, :int)
202
+ values = get_raw(key, :int)
203
+ return self unless values
274
204
 
275
- separator = LibHTS.bcf_gt_is_phased(value) != 0 ? "|" : "/"
276
- "#{separator}#{allele}"
277
- end.join
205
+ values.each_with_index { |value, index| yield index, missing_int(value) }
206
+ self
278
207
  end
279
208
 
280
- def split_sample_values(values)
281
- sample_count = @record.header.nsamples
282
- return [] if sample_count <= 0
209
+ def each_i32_vector(key, &block) = each_vector(key, :int, &block)
210
+ def each_f32_vector(key, &block) = each_vector(key, :float, &block)
283
211
 
284
- raise FormatReadError, "Failed to split FORMAT values by sample" unless (values.size % sample_count).zero?
212
+ def update_int(key, values)
213
+ raise UnsupportedFormatOperationError, "Use update_genotypes for GT" if key.to_s == "GT"
285
214
 
286
- values_per_sample = values.size / sample_count
287
- Array.new(sample_count) do |sample_index|
288
- start = sample_index * values_per_sample
289
- values[start, values_per_sample]
290
- end
215
+ values = normalize_values(values) { |value| Integer(value) }
216
+ validate_sample_divisibility!(key, values.length)
217
+ update_format(key, Native::BCF_HT_INT, values)
291
218
  end
292
219
 
293
- def trim_genotype_vector_end(values)
294
- end_index = values.index { |value| LibHTS.bcf_gt_is_vector_end(value) != 0 } || values.size
295
- values[0, end_index]
220
+ def update_float(key, values)
221
+ values = normalize_values(values, &:to_f)
222
+ validate_sample_divisibility!(key, values.length)
223
+ update_format(key, Native::BCF_HT_REAL, values)
296
224
  end
297
225
 
298
- def trim_integer_vector_end(values)
299
- end_index = values.index { |value| value == LibHTS.bcf_int32_vector_end } || values.size
300
- values[0, end_index]
301
- end
226
+ def update_float_words(key, values)
227
+ values = normalize_values(values) { |value| Integer(value) }
228
+ validate_sample_divisibility!(key, values.length)
229
+ raise FormatDefinitionError, "FORMAT tag #{key} not defined in header" unless format_schema(key)
302
230
 
303
- def trim_float_vector_end(values)
304
- end_index = values.index(LibHTS.bcf_float_vector_end) || values.size
305
- values[0, end_index]
306
- end
231
+ result = native.format_update_float_words(header_native, key.to_s, values)
232
+ raise FormatUpdateError, "Failed to update FORMAT field '#{key}': #{result}" if result.negative?
307
233
 
308
- def map_integer_missing(values)
309
- values.map { |value| map_integer_missing_value(value) }
234
+ result
310
235
  end
311
236
 
312
- def map_integer_missing_value(value)
313
- value == LibHTS.bcf_int32_missing ? nil : value
314
- end
237
+ def update_string(key, values)
238
+ values = Array(values).map(&:to_s)
239
+ expected = sample_count
240
+ unless values.length == expected
241
+ raise ArgumentError, "FORMAT string values for #{key} must provide one entry per sample (#{expected})"
242
+ end
315
243
 
316
- def map_float_words(values)
317
- values.map { |value| decode_float_word(value) }
244
+ update_format(key, Native::BCF_HT_STR, values)
318
245
  end
319
246
 
320
- def decode_float_word(value)
321
- return nil if value == LibHTS.bcf_float_missing
322
- return nil if value == LibHTS.bcf_float_vector_end
247
+ def update_genotypes(values)
248
+ values = normalize_values(values) { |value| Integer(value) }
249
+ validate_sample_divisibility!("GT", values.length)
250
+ result = native.genotype_update(header_native, values)
251
+ raise FormatUpdateError, "Failed to update FORMAT field 'GT': #{result}" if result.negative?
323
252
 
324
- [value].pack("V").unpack1("e")
253
+ result
325
254
  end
326
255
 
327
- def get_float_words(key)
328
- get_numeric_values(key, LibHTS::BCF_HT_REAL, "float") { |dst, len| dst.get_array_of_uint32(0, len) }
329
- end
256
+ def delete(key)
257
+ schema = format_schema(key)
258
+ return false unless schema && !get_raw(key).nil?
330
259
 
331
- def get_typed(key, type)
332
- case type.to_sym
333
- when :float, :real
334
- raise_unsupported_format_flag(key)
335
- words = get_float_words(key)
336
- words&.map { |word| decode_float_word(word) }
337
- else
338
- get_raw(key, type)
339
- end
260
+ result = native.format_delete(header_native, key.to_s, type_code(schema.first))
261
+ result >= 0
340
262
  end
341
263
 
342
- def normalize_format_rc(rc, key, expected_type)
343
- case rc
344
- when -1, -3
345
- nil
346
- when -2
347
- raise FormatTypeError, "Tag #{key} is not #{expected_type} FORMAT field"
348
- when -4
349
- raise FormatReadError, "Failed to read FORMAT/#{key}"
350
- else
351
- rc
352
- end
353
- end
264
+ def fields = native.format_fields(header_native)
265
+ def ids = fields.map { |field| field[:id] }
266
+ def get_float_words(key) = get_raw(key, :float)
267
+ def length = fields.length
268
+ alias size length
269
+ def to_h = fields.to_h { |field| [field[:name], get(field[:name])] }
354
270
 
355
- def raise_unsupported_format_flag(key)
356
- return unless header_format_type(key) == :flag
271
+ private
357
272
 
358
- raise UnsupportedFormatOperationError,
359
- "FORMAT flag fields are not supported: #{key}"
273
+ def native = @record.__send__(:native_handle)
274
+ def header_native = @record.header.__send__(:native_handle)
275
+ def sample_count = @record.header.nsamples
276
+ def format_schema(key) = header_native.schema("FORMAT", key.to_s)
277
+
278
+ def type_code(type)
279
+ { int: Native::BCF_HT_INT, int32: Native::BCF_HT_INT,
280
+ float: Native::BCF_HT_REAL, real: Native::BCF_HT_REAL,
281
+ string: Native::BCF_HT_STR, str: Native::BCF_HT_STR }.fetch(type)
360
282
  end
361
283
 
362
- def ensure_expected_format_type!(key, expected_type, label)
363
- actual_type = header_format_type(key)
364
- raise FormatDefinitionError, "FORMAT tag #{key} not defined in header" if actual_type.nil?
284
+ def type_compatible?(actual, requested, key)
285
+ return true if key == "GT" && %i[int int32 string str].include?(requested)
365
286
 
366
- raise_unsupported_format_flag(key)
367
- raise FormatTypeError, "Tag #{key} is not #{label} FORMAT field" unless actual_type == expected_type
287
+ actual == case requested
288
+ when :int, :int32 then :int
289
+ when :float, :real then :float
290
+ when :string, :str then :string
291
+ when :flag then :flag
292
+ else requested
293
+ end
368
294
  end
369
295
 
370
- def ensure_gt_defined!
371
- raise FormatDefinitionError, "FORMAT tag GT not defined in header" if header_format_type("GT").nil?
296
+ def type_label(type)
297
+ case type
298
+ when :int, :int32 then "integer"
299
+ when :float, :real then "float"
300
+ when :string, :str then "string"
301
+ else type.to_s
302
+ end
372
303
  end
373
304
 
374
- def check_update_rc!(rc, key)
375
- case rc
376
- when -1
377
- raise FormatDefinitionError, "FORMAT tag #{key} not defined in header"
378
- when 0
379
- rc
380
- else
381
- raise FormatUpdateError, "Failed to update FORMAT field '#{key}': #{rc}" if rc.negative?
382
-
383
- rc
384
- end
305
+ def raise_unsupported_flag(key)
306
+ raise UnsupportedFormatOperationError, "FORMAT flag fields are not supported: #{key}"
385
307
  end
386
308
 
387
- def validate_numeric_sample_count!(key, value_count)
388
- sample_count = @record.header.nsamples
389
- raise ArgumentError, "FORMAT fields require at least one sample" if sample_count <= 0
390
- return if (value_count % sample_count).zero?
309
+ def sample_layout(value_count)
310
+ count = sample_count
311
+ raise FormatReadError, "invalid FORMAT sample layout" if count <= 0 || (value_count % count) != 0
391
312
 
392
- raise ArgumentError, "FORMAT values for #{key} must be divisible by sample count (#{sample_count})"
313
+ [count, value_count / count]
393
314
  end
394
315
 
395
- def validate_string_sample_count!(key, value_count)
396
- sample_count = @record.header.nsamples
397
- raise ArgumentError, "FORMAT fields require at least one sample" if sample_count <= 0
398
- return if value_count == sample_count
316
+ def shape_values(values, _key, schema)
317
+ return nil unless values
399
318
 
400
- raise ArgumentError, "FORMAT string values for #{key} must provide one entry per sample (#{sample_count})"
319
+ count, width = sample_layout(values.length)
320
+ scalar = schema[1] == 1
321
+ Array.new(count) do |sample|
322
+ row = values.slice(sample * width, width)
323
+ row = trim_vector(row, schema.first)
324
+ scalar ? row.first : row
325
+ end
401
326
  end
402
327
 
403
- def normalize_int_values(values)
404
- values = Array(values)
405
- raise ArgumentError, "Cannot update FORMAT field with empty array. Use delete instead." if values.empty?
406
- raise ArgumentError, "FORMAT integer values must all be Integer" unless values.all?(Integer)
407
- raise RangeError, "FORMAT integer values must fit int32" unless values.all? { |value| int32_range?(value) }
328
+ def trim_vector(values, type)
329
+ if type == :int
330
+ values.take_while { |value| value != Native::BCF_INT32_VECTOR_END }.map { |value| missing_int(value) }
331
+ else
332
+ values.take_while { |word| word != Native::BCF_FLOAT_VECTOR_END }.map { |word| decode_float_word(word) }
333
+ end
334
+ end
408
335
 
409
- values
336
+ def missing_int(value)
337
+ [Native::BCF_INT32_MISSING, Native::BCF_INT32_VECTOR_END].include?(value) ? nil : value
410
338
  end
411
339
 
412
- def normalize_float_values(values)
413
- values = Array(values)
414
- raise ArgumentError, "Cannot update FORMAT field with empty array. Use delete instead." if values.empty?
415
- raise ArgumentError, "FORMAT float values must all be Numeric" unless values.all?(Numeric)
340
+ def decode_float_word(word)
341
+ return nil if [Native::BCF_FLOAT_MISSING, Native::BCF_FLOAT_VECTOR_END].include?(word)
416
342
 
417
- values.map(&:to_f)
343
+ [word].pack("L<").unpack1("e")
418
344
  end
419
345
 
420
- def normalize_string_values(values)
421
- values = Array(values)
422
- raise ArgumentError, "Cannot update FORMAT field with empty array. Use delete instead." if values.empty?
423
- raise ArgumentError, "FORMAT string values must all be String" unless values.all?(String)
424
-
425
- values
346
+ def advance_buffer(key, type)
347
+ buffer = (@buffers[[key.to_s, type]] ||= BufferState.new(0))
348
+ buffer.generation += 1
349
+ [buffer, buffer.generation]
426
350
  end
427
351
 
428
- def format_present?(key)
429
- if key == "GT"
430
- !get_genotypes.nil?
431
- else
432
- case header_format_type(key)
433
- when :int then !get_int(key).nil?
434
- when :float then !get_float(key).nil?
435
- when :string then !get_string(key).nil?
436
- else false
437
- end
352
+ def invalidate_views!(key)
353
+ @buffers.each do |(buffer_key, _type), buffer|
354
+ buffer.generation += 1 if buffer_key == key.to_s
438
355
  end
439
356
  end
440
357
 
441
- def fmt_ptr
442
- @record.struct[:d][:fmt].to_ptr
443
- end
358
+ def each_vector(key, type)
359
+ return enum_for(type == :int ? :each_i32_vector : :each_f32_vector, key) unless block_given?
444
360
 
445
- def ids
446
- fmt_ptr.read_array_of_struct(LibHTS::BcfFmt, length).map do |fmt|
447
- fmt[:id]
448
- end
449
- end
361
+ values = get_raw(key, type)
362
+ return self unless values
450
363
 
451
- def get_fmt_type(qname)
452
- @record.struct[:n_fmt].times do |i|
453
- fmt = LibHTS::BcfFmt.new(@record.struct[:d][:fmt] + i * LibHTS::BcfFmt.size)
454
- id = fmt[:id]
455
- name = LibHTS.bcf_hdr_int2id(@record.header.struct, LibHTS::BCF_DT_ID, id)
456
- if name == qname
457
- type = LibHTS.bcf_hdr_id2type(@record.header.struct, LibHTS::BCF_HL_FMT, id)
458
- return type
459
- end
364
+ count, width = sample_layout(values.length)
365
+ buffer, generation = advance_buffer(key, type)
366
+ view = NumericVectorView.new(type)
367
+ count.times do |sample|
368
+ decoded = trim_vector(values.slice(sample * width, width), type)
369
+ yield sample, view.reset(decoded, buffer, generation)
460
370
  end
461
- nil
371
+ self
462
372
  end
463
373
 
464
- def scalar_format?(key)
465
- header_format_number(key) == 1
374
+ def ensure_scalar!(key, expected)
375
+ schema = format_schema(key)
376
+ return unless schema
377
+ raise FormatTypeError, "Tag #{key} is not #{type_label(expected)} FORMAT field" unless schema.first == expected
378
+ raise FormatReadError, "FORMAT field #{key} is not scalar" unless schema[1] == 1
466
379
  end
467
380
 
468
- def header_format_number(key)
469
- id = LibHTS.bcf_hdr_id2int(@record.header.struct, LibHTS::BCF_DT_ID, key)
470
- return nil if id.negative?
471
- return nil unless LibHTS.bcf_hdr_idinfo_exists(@record.header.struct, LibHTS::BCF_HL_FMT, id)
472
-
473
- LibHTS.bcf_hdr_id2number(@record.header.struct, LibHTS::BCF_HL_FMT, id)
381
+ def normalize_values(values, &block)
382
+ Array(values).map(&block)
474
383
  end
475
384
 
476
- def header_format_type_code(key)
477
- id = LibHTS.bcf_hdr_id2int(@record.header.struct, LibHTS::BCF_DT_ID, key)
478
- return nil if id.negative?
479
- return nil unless LibHTS.bcf_hdr_idinfo_exists(@record.header.struct, LibHTS::BCF_HL_FMT, id)
385
+ def validate_sample_divisibility!(key, count)
386
+ samples = sample_count
387
+ return if samples.positive? && (count % samples).zero?
480
388
 
481
- LibHTS.bcf_hdr_id2type(@record.header.struct, LibHTS::BCF_HL_FMT, id)
389
+ raise ArgumentError, "FORMAT values for #{key} must be divisible by sample count (#{samples})"
482
390
  end
483
391
 
484
- def header_format_type(key)
485
- ht_type_to_sym(header_format_type_code(key))
486
- end
392
+ def update_format(key, type, values)
393
+ schema = format_schema(key)
394
+ raise FormatDefinitionError, "FORMAT tag #{key} not defined in header" unless schema
487
395
 
488
- def ht_type_to_sym(t)
489
- case t
490
- when LibHTS::BCF_HT_FLAG then :flag
491
- when LibHTS::BCF_HT_INT then :int
492
- when LibHTS::BCF_HT_REAL then :float
493
- when LibHTS::BCF_HT_STR then :string
494
- when LibHTS::BCF_HT_LONG then :int64
396
+ expected = type_code(schema.first)
397
+ unless expected == type
398
+ requested = { Native::BCF_HT_INT => :int, Native::BCF_HT_REAL => :float,
399
+ Native::BCF_HT_STR => :string }.fetch(type)
400
+ raise FormatTypeError, "Tag #{key} is not #{type_label(requested)} FORMAT field"
495
401
  end
496
- end
402
+ result = native.format_update(header_native, key.to_s, type, values)
403
+ raise FormatUpdateError, "Failed to update FORMAT field '#{key}': #{result}" if result.negative?
497
404
 
498
- def int32_range?(value)
499
- value >= -2_147_483_648 && value <= 2_147_483_647
405
+ result
500
406
  end
501
407
  end
502
408
  end