htslib 0.4.2 → 0.5.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 +79 -49
  3. data/TUTORIAL.md +9 -20
  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 +1108 -0
  8. data/ext/htslib_native/native_bcf.c +369 -0
  9. data/ext/htslib_native/native_faidx.c +155 -0
  10. data/ext/htslib_native/native_tabix.c +246 -0
  11. data/lib/hts/bam/auxi.rb +87 -342
  12. data/lib/hts/bam/base_mod.rb +37 -73
  13. data/lib/hts/bam/cigar.rb +9 -55
  14. data/lib/hts/bam/flag.rb +19 -27
  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 +102 -42
  20. data/lib/hts/bcf/errors.rb +1 -0
  21. data/lib/hts/bcf/format.rb +278 -379
  22. data/lib/hts/bcf/header.rb +38 -122
  23. data/lib/hts/bcf/header_record.rb +9 -35
  24. data/lib/hts/bcf/info.rb +72 -320
  25. data/lib/hts/bcf/record.rb +61 -102
  26. data/lib/hts/bcf.rb +149 -323
  27. data/lib/hts/faidx.rb +28 -87
  28. data/lib/hts/hts.rb +6 -94
  29. data/lib/hts/native.rb +13 -0
  30. data/lib/hts/tabix.rb +93 -48
  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,399 @@
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
166
- end
121
+ raw = get_raw(key, requested)
122
+ return raw if requested
123
+ return raw if schema.first == :string
167
124
 
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)
173
- end
174
- ret
125
+ shape_values(raw, key, schema)
175
126
  end
176
127
 
177
- # def genotypes; end
178
-
179
- private
128
+ def get_raw(key, type = nil)
129
+ key = key.to_s
130
+ schema = format_schema(key)
131
+ return nil unless schema
180
132
 
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)
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"
197
136
  end
137
+
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)
198
142
  end
199
143
 
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
144
+ def get_int(key) = get(key, :int)
145
+
146
+ def get_float(key)
147
+ words = get_raw(key, :float)
148
+ words&.map { |word| decode_float_word(word) }
222
149
  end
223
150
 
224
- def decode_integer_values(key)
151
+ def get_flag(key) = get(key, :flag)
152
+ def get_string(key) = get(key, :string)
153
+ def get_genotypes = get_raw("GT", :int)
154
+ def [](key) = get(key)
155
+
156
+ def each_genotype(key = "GT")
157
+ return enum_for(__method__, key) unless block_given?
158
+ raise ArgumentError, "genotype FORMAT key must be GT" unless key.to_s == "GT"
159
+
225
160
  values = get_raw(key, :int)
226
161
  return nil unless values
227
162
 
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
163
+ count, width = sample_layout(values.length)
164
+ buffer, generation = advance_buffer(key, :genotype)
165
+ view = GenotypeView.new
166
+ count.times do |sample|
167
+ yield sample, view.reset(values.slice(sample * width, width), buffer, generation)
237
168
  end
169
+ self
238
170
  end
239
171
 
240
- def decode_float_values(key)
241
- values = get_float_words(key)
172
+ def genotype_at(key, sample_index)
173
+ values = get_raw(key, :int)
242
174
  return nil unless values
243
175
 
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
176
+ count, width = sample_layout(values.length)
177
+ sample_index = Integer(sample_index)
178
+ sample_index += count if sample_index.negative?
179
+ raise IndexError, "sample index #{sample_index} outside of FORMAT" unless sample_index.between?(0, count - 1)
255
180
 
256
- def decode_genotypes
257
- genotypes = get_genotypes
258
- return nil unless genotypes
181
+ buffer, generation = advance_buffer(key, :genotype)
182
+ GenotypeView.new.reset(values.slice(sample_index * width, width), buffer, generation)
183
+ end
259
184
 
260
- split_sample_values(genotypes).map do |sample_values|
261
- decode_genotype_sample(trim_genotype_vector_end(sample_values))
262
- end
185
+ def genotype_strings(key = "GT")
186
+ strings = []
187
+ found = each_genotype(key) { |_, genotype| strings << genotype.to_s }
188
+ found ? strings : nil
263
189
  end
264
190
 
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
191
+ def each_i32(key)
192
+ return enum_for(__method__, key) unless block_given?
272
193
 
273
- next allele if index.zero?
194
+ ensure_scalar!(key, :int)
195
+ values = get_raw(key, :int)
196
+ return self unless values
274
197
 
275
- separator = LibHTS.bcf_gt_is_phased(value) != 0 ? "|" : "/"
276
- "#{separator}#{allele}"
277
- end.join
198
+ values.each_with_index { |value, index| yield index, missing_int(value) }
199
+ self
278
200
  end
279
201
 
280
- def split_sample_values(values)
281
- sample_count = @record.header.nsamples
282
- return [] if sample_count <= 0
202
+ def each_i32_vector(key, &block) = each_vector(key, :int, &block)
203
+ def each_f32_vector(key, &block) = each_vector(key, :float, &block)
283
204
 
284
- raise FormatReadError, "Failed to split FORMAT values by sample" unless (values.size % sample_count).zero?
205
+ def update_int(key, values)
206
+ raise UnsupportedFormatOperationError, "Use update_genotypes for GT" if key.to_s == "GT"
285
207
 
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
208
+ values = normalize_values(values) { |value| Integer(value) }
209
+ validate_sample_divisibility!(key, values.length)
210
+ update_format(key, Native::BCF_HT_INT, values)
291
211
  end
292
212
 
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]
213
+ def update_float(key, values)
214
+ values = normalize_values(values, &:to_f)
215
+ validate_sample_divisibility!(key, values.length)
216
+ update_format(key, Native::BCF_HT_REAL, values)
296
217
  end
297
218
 
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
219
+ def update_float_words(key, values)
220
+ values = normalize_values(values) { |value| Integer(value) }
221
+ validate_sample_divisibility!(key, values.length)
222
+ raise FormatDefinitionError, "FORMAT tag #{key} not defined in header" unless format_schema(key)
302
223
 
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
224
+ result = native.format_update_float_words(header_native, key.to_s, values)
225
+ raise FormatUpdateError, "Failed to update FORMAT field '#{key}': #{result}" if result.negative?
307
226
 
308
- def map_integer_missing(values)
309
- values.map { |value| map_integer_missing_value(value) }
227
+ result
310
228
  end
311
229
 
312
- def map_integer_missing_value(value)
313
- value == LibHTS.bcf_int32_missing ? nil : value
314
- end
230
+ def update_string(key, values)
231
+ values = Array(values).map(&:to_s)
232
+ expected = sample_count
233
+ unless values.length == expected
234
+ raise ArgumentError, "FORMAT string values for #{key} must provide one entry per sample (#{expected})"
235
+ end
315
236
 
316
- def map_float_words(values)
317
- values.map { |value| decode_float_word(value) }
237
+ update_format(key, Native::BCF_HT_STR, values)
318
238
  end
319
239
 
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
240
+ def update_genotypes(values)
241
+ values = normalize_values(values) { |value| Integer(value) }
242
+ validate_sample_divisibility!("GT", values.length)
243
+ result = native.genotype_update(header_native, values)
244
+ raise FormatUpdateError, "Failed to update FORMAT field 'GT': #{result}" if result.negative?
323
245
 
324
- [value].pack("V").unpack1("e")
246
+ result
325
247
  end
326
248
 
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
249
+ def delete(key)
250
+ schema = format_schema(key)
251
+ return false unless schema && !get_raw(key).nil?
330
252
 
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
253
+ result = native.format_delete(header_native, key.to_s, type_code(schema.first))
254
+ result >= 0
340
255
  end
341
256
 
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
257
+ def fields = native.format_fields(header_native)
258
+ def ids = fields.map { |field| field[:id] }
259
+ def get_float_words(key) = get_raw(key, :float)
260
+ def length = fields.length
261
+ alias size length
262
+ def to_h = fields.to_h { |field| [field[:name], get(field[:name])] }
354
263
 
355
- def raise_unsupported_format_flag(key)
356
- return unless header_format_type(key) == :flag
264
+ private
357
265
 
358
- raise UnsupportedFormatOperationError,
359
- "FORMAT flag fields are not supported: #{key}"
266
+ def native = @record.__send__(:native_handle)
267
+ def header_native = @record.header.__send__(:native_handle)
268
+ def sample_count = @record.header.nsamples
269
+ def format_schema(key) = header_native.schema("FORMAT", key.to_s)
270
+
271
+ def type_code(type)
272
+ { int: Native::BCF_HT_INT, int32: Native::BCF_HT_INT,
273
+ float: Native::BCF_HT_REAL, real: Native::BCF_HT_REAL,
274
+ string: Native::BCF_HT_STR, str: Native::BCF_HT_STR }.fetch(type)
360
275
  end
361
276
 
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?
277
+ def type_compatible?(actual, requested, key)
278
+ return true if key == "GT" && %i[int int32 string str].include?(requested)
365
279
 
366
- raise_unsupported_format_flag(key)
367
- raise FormatTypeError, "Tag #{key} is not #{label} FORMAT field" unless actual_type == expected_type
280
+ actual == case requested
281
+ when :int, :int32 then :int
282
+ when :float, :real then :float
283
+ when :string, :str then :string
284
+ when :flag then :flag
285
+ else requested
286
+ end
368
287
  end
369
288
 
370
- def ensure_gt_defined!
371
- raise FormatDefinitionError, "FORMAT tag GT not defined in header" if header_format_type("GT").nil?
289
+ def type_label(type)
290
+ case type
291
+ when :int, :int32 then "integer"
292
+ when :float, :real then "float"
293
+ when :string, :str then "string"
294
+ else type.to_s
295
+ end
372
296
  end
373
297
 
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
298
+ def raise_unsupported_flag(key)
299
+ raise UnsupportedFormatOperationError, "FORMAT flag fields are not supported: #{key}"
385
300
  end
386
301
 
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?
302
+ def sample_layout(value_count)
303
+ count = sample_count
304
+ raise FormatReadError, "invalid FORMAT sample layout" if count <= 0 || (value_count % count) != 0
391
305
 
392
- raise ArgumentError, "FORMAT values for #{key} must be divisible by sample count (#{sample_count})"
306
+ [count, value_count / count]
393
307
  end
394
308
 
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
309
+ def shape_values(values, _key, schema)
310
+ return nil unless values
399
311
 
400
- raise ArgumentError, "FORMAT string values for #{key} must provide one entry per sample (#{sample_count})"
312
+ count, width = sample_layout(values.length)
313
+ scalar = schema[1] == 1
314
+ Array.new(count) do |sample|
315
+ row = values.slice(sample * width, width)
316
+ row = trim_vector(row, schema.first)
317
+ scalar ? row.first : row
318
+ end
401
319
  end
402
320
 
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) }
321
+ def trim_vector(values, type)
322
+ if type == :int
323
+ values.take_while { |value| value != Native::BCF_INT32_VECTOR_END }.map { |value| missing_int(value) }
324
+ else
325
+ values.take_while { |word| word != Native::BCF_FLOAT_VECTOR_END }.map { |word| decode_float_word(word) }
326
+ end
327
+ end
408
328
 
409
- values
329
+ def missing_int(value)
330
+ [Native::BCF_INT32_MISSING, Native::BCF_INT32_VECTOR_END].include?(value) ? nil : value
410
331
  end
411
332
 
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)
333
+ def decode_float_word(word)
334
+ return nil if [Native::BCF_FLOAT_MISSING, Native::BCF_FLOAT_VECTOR_END].include?(word)
416
335
 
417
- values.map(&:to_f)
336
+ [word].pack("L<").unpack1("e")
418
337
  end
419
338
 
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
339
+ def advance_buffer(key, type)
340
+ buffer = (@buffers[[key.to_s, type]] ||= BufferState.new(0))
341
+ buffer.generation += 1
342
+ [buffer, buffer.generation]
426
343
  end
427
344
 
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
345
+ def invalidate_views!(key)
346
+ @buffers.each do |(buffer_key, _type), buffer|
347
+ buffer.generation += 1 if buffer_key == key.to_s
438
348
  end
439
349
  end
440
350
 
441
- def fmt_ptr
442
- @record.struct[:d][:fmt].to_ptr
443
- end
351
+ def each_vector(key, type)
352
+ return enum_for(type == :int ? :each_i32_vector : :each_f32_vector, key) unless block_given?
444
353
 
445
- def ids
446
- fmt_ptr.read_array_of_struct(LibHTS::BcfFmt, length).map do |fmt|
447
- fmt[:id]
448
- end
449
- end
354
+ values = get_raw(key, type)
355
+ return self unless values
450
356
 
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
357
+ count, width = sample_layout(values.length)
358
+ buffer, generation = advance_buffer(key, type)
359
+ view = NumericVectorView.new(type)
360
+ count.times do |sample|
361
+ decoded = trim_vector(values.slice(sample * width, width), type)
362
+ yield sample, view.reset(decoded, buffer, generation)
460
363
  end
461
- nil
364
+ self
462
365
  end
463
366
 
464
- def scalar_format?(key)
465
- header_format_number(key) == 1
367
+ def ensure_scalar!(key, expected)
368
+ schema = format_schema(key)
369
+ return unless schema
370
+ raise FormatTypeError, "Tag #{key} is not #{type_label(expected)} FORMAT field" unless schema.first == expected
371
+ raise FormatReadError, "FORMAT field #{key} is not scalar" unless schema[1] == 1
466
372
  end
467
373
 
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)
374
+ def normalize_values(values, &block)
375
+ Array(values).map(&block)
474
376
  end
475
377
 
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)
378
+ def validate_sample_divisibility!(key, count)
379
+ samples = sample_count
380
+ return if samples.positive? && (count % samples).zero?
480
381
 
481
- LibHTS.bcf_hdr_id2type(@record.header.struct, LibHTS::BCF_HL_FMT, id)
382
+ raise ArgumentError, "FORMAT values for #{key} must be divisible by sample count (#{samples})"
482
383
  end
483
384
 
484
- def header_format_type(key)
485
- ht_type_to_sym(header_format_type_code(key))
486
- end
385
+ def update_format(key, type, values)
386
+ schema = format_schema(key)
387
+ raise FormatDefinitionError, "FORMAT tag #{key} not defined in header" unless schema
487
388
 
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
389
+ expected = type_code(schema.first)
390
+ unless expected == type
391
+ requested = { Native::BCF_HT_INT => :int, Native::BCF_HT_REAL => :float,
392
+ Native::BCF_HT_STR => :string }.fetch(type)
393
+ raise FormatTypeError, "Tag #{key} is not #{type_label(requested)} FORMAT field"
495
394
  end
496
- end
395
+ result = native.format_update(header_native, key.to_s, type, values)
396
+ raise FormatUpdateError, "Failed to update FORMAT field '#{key}': #{result}" if result.negative?
497
397
 
498
- def int32_range?(value)
499
- value >= -2_147_483_648 && value <= 2_147_483_647
398
+ result
500
399
  end
501
400
  end
502
401
  end