htslib 0.4.1 → 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 -123
  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 -109
  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 -79
  47. data/lib/hts/libhts/tbx_funcs.rb +0 -32
  48. data/lib/hts/libhts/thread_pool.rb +0 -139
  49. data/lib/hts/libhts/vcf.rb +0 -557
  50. data/lib/hts/libhts/vcf_funcs.rb +0 -353
  51. data/lib/hts/libhts.rb +0 -47
@@ -6,374 +6,337 @@ require_relative "auxi"
6
6
 
7
7
  module HTS
8
8
  class Bam < Hts
9
- # A class for working with alignment records.
10
9
  class Record
11
10
  SEQ_NT16_STR = "=ACMGRSVTWYHKDBN"
12
-
11
+ UNSET = Object.new.freeze
12
+ private_constant :UNSET
13
13
  attr_reader :header
14
14
 
15
- # Initialization API is experimental.
16
-
17
- def initialize(
18
- header,
19
- bam1 = nil,
20
- qname: nil,
21
- flag: nil,
22
- tid: nil,
23
- pos: nil,
24
- mapq: nil,
25
- cigar: nil,
26
- mtid: nil,
27
- mpos: nil,
28
- isize: nil,
29
- seq: nil,
30
- qual: nil,
31
- l_aux: nil
32
- )
33
- @bam1 = bam1 || LibHTS.bam_init1
15
+ # Build a BAM record from Ruby values. Coordinates are zero-based, matching
16
+ # the rest of this API. +qualities+ is an Array of numeric Phred scores;
17
+ # use +quality_string+ for a FASTQ/SAM-style Phred+33 String.
18
+ def initialize(header, native_record = nil, qname: "*", flag: 0, tid: nil, chrom: nil,
19
+ pos: -1, mapq: 0, cigar: nil, mtid: nil, mate_chrom: nil,
20
+ mate_pos: -1, insert_size: 0, seq: UNSET, sequence: UNSET,
21
+ qual: UNSET, qualities: UNSET, quality_string: UNSET, aux: nil)
22
+ @native = native_record || Native::BamRecordHandle.create
34
23
  @header = header
35
-
36
- params = [qname, flag, tid, pos, mapq, cigar, mtid, mpos, isize, seq, qual, l_aux]
37
- return if params.all? { |x| x.nil? }
38
-
39
- if params.all?
40
- cigar_array = Cigar.parse(cigar).array
41
- cigar_pointer = if cigar_array.empty?
42
- FFI::Pointer::NULL
43
- else
44
- FFI::MemoryPointer.new(:uint32, cigar_array.length).tap do |pointer|
45
- pointer.write_array_of_uint32(cigar_array)
46
- end
47
- end
48
- if qual.is_a?(Array)
49
- qual = qual.pack("C*")
50
- elsif qual.is_a?(String)
51
- raise "Which implementation is better? +33 or not? Please tell me."
52
- end
53
- r = LibHTS.bam_set1(
54
- @bam1,
55
- qname.length,
56
- qname,
57
- flag,
58
- tid,
59
- pos,
60
- mapq,
61
- cigar_array.length,
62
- cigar_pointer,
63
- mtid,
64
- mpos,
65
- isize,
66
- seq.length,
67
- seq,
68
- qual,
69
- l_aux
70
- )
71
- raise "bam_set1 failed: #{r}" if r < 0
72
- else
73
- warn "Ignore bam_set1 because some arguments are missing."
74
- end
75
- end
76
-
77
- # Return the FFI::Struct object.
78
- def struct
79
- @bam1
80
- end
81
-
82
- def to_ptr
83
- @bam1.to_ptr
24
+ return if native_record
25
+
26
+ sequence = normalize_sequence(seq, sequence)
27
+ qualities = normalize_qualities(qual, qualities, quality_string, sequence.bytesize)
28
+ cigar_values = normalize_cigar(cigar)
29
+ validate_cigar_length!(cigar_values, sequence)
30
+
31
+ @native.replace(
32
+ normalize_qname(qname), normalize_flag(flag), resolve_tid(tid, chrom, "chrom"),
33
+ normalize_position(pos, "pos"), normalize_mapq(mapq), cigar_values,
34
+ resolve_tid(mtid, mate_chrom, "mate_chrom"), normalize_position(mate_pos, "mate_pos"),
35
+ Integer(insert_size), sequence, qualities
36
+ )
37
+ assign_aux(aux) if aux
84
38
  end
85
39
 
86
- # Get the read name. (a.k.a QNAME)
87
- # @return [String] query template name
88
- def qname
89
- LibHTS.bam_get_qname(@bam1).read_string
90
- end
40
+ def qname = @native.qname
91
41
 
92
42
  def qname=(name)
93
- LibHTS.bam_set_qname(@bam1, name)
43
+ @native.qname = name
94
44
  end
95
45
 
96
- # Get the chromosome ID of the alignment. -1 if not mapped.
97
- # @return [Integer] chromosome ID
98
- def tid
99
- @bam1[:core][:tid]
100
- end
46
+ def tid = core_get(:tid)
101
47
 
102
- def tid=(tid)
103
- @bam1[:core][:tid] = tid
48
+ def tid=(value)
49
+ core_set(:tid, value)
104
50
  end
105
51
 
106
- # Get the chromosome ID of the mate. -1 if not mapped.
107
- # @return [Integer] chromosome ID
108
- def mtid
109
- @bam1[:core][:mtid]
110
- end
52
+ def mtid = core_get(:mtid)
111
53
 
112
- def mtid=(mtid)
113
- @bam1[:core][:mtid] = mtid
54
+ def mtid=(value)
55
+ core_set(:mtid, value)
114
56
  end
115
57
 
116
- # Get the 0-based leftmost coordinate of the alignment.
117
- # @return [Integer] 0-based leftmost coordinate
118
- def pos
119
- @bam1[:core][:pos]
120
- end
58
+ def pos = core_get(:pos)
121
59
 
122
- def pos=(pos)
123
- @bam1[:core][:pos] = pos
60
+ def pos=(value)
61
+ core_set(:pos, value)
124
62
  end
125
63
 
126
- # Get the 0-based leftmost coordinate of the mate.
127
- # @return [Integer] 0-based leftmost coordinate
128
- def mate_pos
129
- @bam1[:core][:mpos]
130
- end
64
+ def mate_pos = core_get(:mpos)
131
65
 
132
- def mate_pos=(mpos)
133
- @bam1[:core][:mpos] = mpos
66
+ def mate_pos=(value)
67
+ core_set(:mpos, value)
134
68
  end
135
-
136
69
  alias mpos mate_pos
137
70
  alias mpos= mate_pos=
71
+ def bin = core_get(:bin)
138
72
 
139
- # Get the bin calculated by bam_reg2bin().
140
- # @return [Integer] bin
141
- def bin
142
- @bam1[:core][:bin]
73
+ def bin=(value)
74
+ core_set(:bin, value)
143
75
  end
144
76
 
145
- def bin=(bin)
146
- @bam1[:core][:bin] = bin
147
- end
148
-
149
- # Get the rightmost base position of the alignment on the reference genome.
150
- # @return [Integer] 0-based rightmost coordinate
151
- def endpos
152
- LibHTS.bam_endpos @bam1
153
- end
77
+ def endpos = @native.endpos
154
78
 
155
- # Get the reference sequence name of the alignment. (a.k.a RNAME)
156
- # '' if not mapped.
157
- # @return [String] reference sequence name
158
79
  def chrom
159
80
  return "" if tid == -1
160
81
 
161
- LibHTS.sam_hdr_tid2name(@header, tid)
82
+ @header.__send__(:native_handle).target_name(tid)
162
83
  end
163
-
164
84
  alias contig chrom
85
+ def chrom=(name)
86
+ self.tid = resolve_tid(nil, name, "chrom")
87
+ end
88
+ alias contig= chrom=
165
89
 
166
- # Get the reference sequence name of the mate.
167
- # '' if not mapped.
168
- # @return [String] reference sequence name
169
90
  def mate_chrom
170
91
  return "" if mtid == -1
171
92
 
172
- LibHTS.sam_hdr_tid2name(@header, mtid)
93
+ @header.__send__(:native_handle).target_name(mtid)
173
94
  end
174
-
175
95
  alias mate_contig mate_chrom
96
+ def mate_chrom=(name)
97
+ self.mtid = resolve_tid(nil, name, "mate_chrom")
98
+ end
99
+ alias mate_contig= mate_chrom=
176
100
 
177
- # Get whether the query is on the reverse strand.
178
- # @return [String] strand "+" or "-"
179
- def strand
180
- LibHTS.bam_is_rev(@bam1) ? "-" : "+"
101
+ def strand = reverse? ? "-" : "+"
102
+ def mate_strand = mate_reverse? ? "-" : "+"
103
+ def insert_size = core_get(:isize)
104
+
105
+ def insert_size=(value)
106
+ core_set(:isize, value)
181
107
  end
108
+ alias isize insert_size
109
+ alias isize= insert_size=
110
+ def mapq = core_get(:mapq)
182
111
 
183
- # Get whether the query's mate is on the reverse strand.
184
- # @return [String] strand "+" or "-"
185
- def mate_strand
186
- LibHTS.bam_is_mrev(@bam1) ? "-" : "+"
112
+ def mapq=(value)
113
+ core_set(:mapq, value)
187
114
  end
188
115
 
189
- # Get the observed template length. (a.k.a TLEN)
190
- # @return [Integer] isize
191
- def insert_size
192
- @bam1[:core][:isize]
116
+ def cigar = Cigar.new(self)
117
+
118
+ def cigar=(value)
119
+ raise ArgumentError, "cigar must be a String or Bam::Cigar" unless value.is_a?(String) || value.is_a?(Cigar)
120
+
121
+ @native.cigar = value.to_s
193
122
  end
194
123
 
195
- def insert_size=(isize)
196
- @bam1[:core][:isize] = isize
124
+ def qlen = @native.qlen
125
+ def rlen = @native.rlen
126
+ def seq = @native.sequence
127
+ alias sequence seq
128
+
129
+ def each_base(&block)
130
+ return to_enum(__method__) unless block_given?
131
+
132
+ seq.each_char(&block)
133
+ self
197
134
  end
198
135
 
199
- alias isize insert_size
200
- alias isize= insert_size=
136
+ def each_base_code(&block)
137
+ return to_enum(__method__) unless block_given?
201
138
 
202
- # Get the mapping quality of the alignment. (a.k.a MAPQ)
203
- # @return [Integer] mapping quality
204
- def mapq
205
- @bam1[:core][:qual]
139
+ @native.sequence_codes.each(&block)
140
+ self
206
141
  end
207
142
 
208
- def mapq=(mapq)
209
- @bam1[:core][:qual] = mapq
143
+ def len = core_get(:length)
144
+
145
+ def base(index)
146
+ index += len if index.negative?
147
+ code = @native.base_code(index)
148
+ code ? SEQ_NT16_STR[code] : "."
210
149
  end
150
+ alias base_at base
211
151
 
212
- # Get the Bam::Cigar object.
213
- # @return [Bam::Cigar] cigar
214
- def cigar
215
- Cigar.new(self)
152
+ def qual = @native.qualities
153
+ def qual_string = @native.quality_string
154
+
155
+ def each_qual(&block)
156
+ return to_enum(__method__) unless block_given?
157
+
158
+ @native.qualities.each(&block)
159
+ self
216
160
  end
217
161
 
218
- def cigar=(str)
219
- case str
220
- when Cigar, String
221
- r = LibHTS.bam_parse_cigar(str.to_s, FFI::Pointer::NULL, @bam1)
222
- raise "bam_parse_cigar failed: #{r}" if r.negative?
223
- else
224
- raise ArgumentError, "cigar must be a String or Bam::Cigar"
225
- end
162
+ def base_qual(index)
163
+ index += len if index.negative?
164
+ @native.quality_at(index) || 0
226
165
  end
166
+ alias qual_at base_qual
227
167
 
228
- # Calculate query length from CIGAR.
229
- # @return [Integer] query length
230
- def qlen
231
- # cigar.qlen will be slower because it converts to a Ruby array.
232
- LibHTS.bam_cigar2qlen(
233
- @bam1[:core][:n_cigar],
234
- LibHTS.bam_get_cigar(@bam1)
235
- )
168
+ def flag = Flag.new(flag_value)
169
+ def flag_value = core_get(:flag)
170
+
171
+ def flag=(value)
172
+ case value
173
+ when Integer then core_set(:flag, value)
174
+ when Flag then core_set(:flag, value.value)
175
+ else raise "Invalid flag type: #{value.class}"
176
+ end
236
177
  end
237
178
 
238
- # Calculate reference length from CIGAR.
239
- # @return [Integer] reference length
240
- def rlen
241
- LibHTS.bam_cigar2rlen(
242
- @bam1[:core][:n_cigar],
243
- LibHTS.bam_get_cigar(@bam1)
244
- )
179
+ def aux(key = nil)
180
+ accessor = (@aux_accessor ||= Aux.new(self))
181
+ key ? accessor.get(key) : accessor
245
182
  end
246
183
 
247
- # Get the sequence. (a.k.a SEQ)
248
- # @return [String] sequence
249
- def seq
250
- r = LibHTS.bam_get_seq(@bam1)
251
- seq = String.new
252
- len.times do |i|
253
- seq << SEQ_NT16_STR[LibHTS.bam_seqi(r, i)]
184
+ def base_mod(auto_parse: true) = BaseMod.new(self, auto_parse:)
185
+
186
+ def each_base_mod_raw(max_mods: 10, &block)
187
+ return enum_for(__method__, max_mods:) unless block
188
+
189
+ mods = BaseMod.new(self)
190
+ begin
191
+ mods.each_raw(max_mods:, &block)
192
+ ensure
193
+ mods.close
254
194
  end
255
- seq
195
+ self
256
196
  end
257
- alias sequence seq
258
197
 
259
- # Get the length of the query sequence.
260
- # @return [Integer] query length
261
- def len
262
- @bam1[:core][:l_qseq]
198
+ Flag::TABLE.each do |method_name, mask|
199
+ define_method(method_name) { (flag_value & mask) != 0 }
263
200
  end
264
201
 
265
- # Get the base of the requested index "i" of the query sequence.
266
- # @param [Integer] i index
267
- # @return [String] base
268
- def base(n)
269
- n += len if n < 0
270
- return "." if (n >= len) || (n < 0) # eg. base(-1000)
202
+ def each_cigar_raw
203
+ return to_enum(__method__) unless block_given?
271
204
 
272
- r = LibHTS.bam_get_seq(@bam1)
273
- SEQ_NT16_STR[LibHTS.bam_seqi(r, n)]
205
+ @native.cigar_values.each { |encoded| yield encoded & 15, encoded >> 4 }
206
+ self
274
207
  end
275
208
 
276
- # Get the base qualities as raw PHRED bytes.
277
- # Ruby has no UInt8 type, so this returns an Array<Integer> with values in 0..255,
278
- # corresponding to Crystal's Array(UInt8).
279
- # Use qual_string for the SAM-style ASCII representation.
280
- # @return [Array<Integer>] base qualities as unsigned bytes
281
- def qual
282
- q_ptr = LibHTS.bam_get_qual(@bam1)
283
- q_ptr.read_array_of_uint8(len)
209
+ def to_s = @native.format(@header.__send__(:native_handle))
210
+
211
+ private
212
+
213
+ def native_handle = @native
214
+ def core_get(field) = @native.core_get(field)
215
+ def core_set(field, value) = @native.core_set(field, value)
216
+
217
+ def normalize_qname(value)
218
+ value = value.to_s
219
+ raise ArgumentError, "qname must not contain NUL bytes" if value.include?("\0")
220
+
221
+ value
284
222
  end
285
223
 
286
- # Get the base qualities as a string. (a.k.a QUAL)
287
- # ASCII of base quality + 33.
288
- # @return [String] base qualities
289
- def qual_string
290
- qual.map { |q| (q + 33).chr }.join
224
+ def normalize_flag(value)
225
+ integer = value.is_a?(Flag) ? value.value : Integer(value)
226
+ raise RangeError, "flag must be between 0 and 65535" unless integer.between?(0, 65_535)
227
+
228
+ integer
291
229
  end
292
230
 
293
- # Get the base quality of the requested index "i" of the query sequence.
294
- # @param [Integer] i index
295
- # @return [Integer] base quality
296
- def base_qual(n)
297
- n += len if n < 0
298
- return 0 if (n >= len) || (n < 0) # eg. base_qual(-1000)
231
+ def normalize_mapq(value)
232
+ integer = Integer(value)
233
+ raise RangeError, "mapq must be between 0 and 255" unless integer.between?(0, 255)
299
234
 
300
- q_ptr = LibHTS.bam_get_qual(@bam1)
301
- q_ptr.get_uint8(n)
235
+ integer
302
236
  end
303
237
 
304
- # Get Bam::Flag object of the alignment.
305
- # @return [Bam::Flag] flag
306
- def flag
307
- Flag.new(@bam1[:core][:flag])
238
+ def normalize_position(value, name)
239
+ integer = Integer(value)
240
+ raise RangeError, "#{name} must be -1 or greater" if integer < -1
241
+
242
+ integer
308
243
  end
309
244
 
310
- def flag=(flag)
311
- case flag
312
- when Integer
313
- @bam1[:core][:flag] = flag
314
- when Flag
315
- @bam1[:core][:flag] = flag.value
316
- else
317
- raise "Invalid flag type: #{flag.class}"
318
- end
245
+ def resolve_tid(numeric, name, label)
246
+ raise ArgumentError, "specify either #{label} or its numeric id, not both" if !numeric.nil? && !name.nil?
247
+
248
+ id = if name.nil?
249
+ numeric.nil? ? -1 : Integer(numeric)
250
+ elsif name.to_s == "*"
251
+ -1
252
+ else
253
+ @header.get_tid(name.to_s)
254
+ end
255
+ raise ArgumentError, "unknown reference #{name.inspect}" if !name.nil? && id.negative? && name.to_s != "*"
256
+ raise RangeError, "reference id must be -1 or greater" if id < -1
257
+ raise RangeError, "reference id #{id} is not present in the header" if id >= @header.target_count
258
+
259
+ id
319
260
  end
320
261
 
321
- # Get the auxiliary data.
322
- # @param [String] key tag name
323
- # @return [String] value
324
- def aux(key = nil)
325
- aux = Aux.new(self)
326
- if key
327
- aux.get(key)
328
- else
329
- aux
262
+ def normalize_sequence(short, long)
263
+ raise ArgumentError, "specify either seq or sequence, not both" unless short.equal?(UNSET) || long.equal?(UNSET)
264
+
265
+ value = long.equal?(UNSET) ? short : long
266
+ value = "" if value.equal?(UNSET) || value.nil? || value == "*"
267
+ value = value.to_s
268
+ unless /\A[=ACMGRSVTWYHKDBNacmgrsvtwyhkdbn]*\z/.match?(value)
269
+ raise ArgumentError, "sequence contains an invalid BAM base"
330
270
  end
271
+
272
+ value
331
273
  end
332
274
 
333
- # Get base modification information from MM/ML tags
334
- # @param auto_parse [Boolean] If true (default), parse lazily on first access
335
- # @return [BaseMod] Base modification object
336
- def base_mod(auto_parse: true)
337
- BaseMod.new(self, auto_parse: auto_parse)
275
+ def normalize_qualities(short, long, string, sequence_length)
276
+ supplied = [short, long, string].count { |value| !value.equal?(UNSET) }
277
+ raise ArgumentError, "specify only one of qual, qualities, or quality_string" if supplied > 1
278
+
279
+ value = if !string.equal?(UNSET)
280
+ quality_string_to_bytes(string)
281
+ elsif !long.equal?(UNSET)
282
+ quality_values_to_bytes(long)
283
+ elsif !short.equal?(UNSET)
284
+ short.is_a?(String) ? quality_string_to_bytes(short) : quality_values_to_bytes(short)
285
+ end
286
+ raise ArgumentError, "qualities length must match sequence length" if value && value.bytesize != sequence_length
287
+
288
+ value
338
289
  end
339
290
 
340
- # TODO: add a method to get the auxiliary fields as a hash.
291
+ def quality_values_to_bytes(values)
292
+ return nil if values.nil?
341
293
 
342
- # TODO: add a method to set the auxiliary fields.
294
+ array = Array(values).map { |value| Integer(value) }
295
+ invalid = array.find { |value| !value.between?(0, 93) }
296
+ raise RangeError, "quality scores must be between 0 and 93" if invalid
343
297
 
344
- # TODO: add a method to remove the auxiliary fields.
298
+ array.pack("C*")
299
+ end
345
300
 
346
- # TODO: add a method to set variable length data (qname, cigar, seq, qual).
301
+ def quality_string_to_bytes(value)
302
+ return nil if value.nil? || value == "*"
347
303
 
348
- # Calling flag is delegated to the Flag object.
349
- Flag::TABLE.each_key do |m|
350
- define_method(m) do
351
- flag.send(m)
304
+ string = value.to_s
305
+ unless string.ascii_only? && string.bytes.all? { |byte| byte.between?(33, 126) }
306
+ raise ArgumentError, "quality_string must contain only Phred+33 characters from ! to ~"
352
307
  end
308
+
309
+ string.bytes.map { |byte| byte - 33 }.pack("C*")
353
310
  end
354
311
 
355
- # @return [String] a string representation of the alignment.
356
- def to_s
357
- kstr = LibHTS::KString.new
358
- begin
359
- raise "Failed to format bam record" if LibHTS.sam_format1(@header.struct, @bam1, kstr) == -1
312
+ def normalize_cigar(value)
313
+ return [] if value.nil? || value.to_s == "*"
360
314
 
361
- kstr.read_string_copy
362
- ensure
363
- kstr.free_buffer
315
+ case value
316
+ when String then Cigar.parse(value).array
317
+ when Cigar then value.array.dup
318
+ else raise ArgumentError, "cigar must be a String or Bam::Cigar"
364
319
  end
365
320
  end
366
321
 
367
- private
322
+ def validate_cigar_length!(values, sequence)
323
+ query_length = Native.cigar_qlen(values)
324
+ return if values.empty? || query_length == sequence.bytesize
368
325
 
369
- def initialize_copy(orig)
370
- super
371
- @header = orig.header
372
- # Deep-copy underlying bam1_t to detach from original buffer
373
- dup_bam1 = LibHTS.bam_dup1(orig.struct)
374
- raise "bam_dup1 failed" if dup_bam1.null?
326
+ raise ArgumentError, "CIGAR query length #{query_length} does not match sequence length #{sequence.bytesize}"
327
+ end
375
328
 
376
- @bam1 = dup_bam1
329
+ def assign_aux(values)
330
+ raise ArgumentError, "aux must be a Hash-like object" unless values.respond_to?(:each_pair)
331
+
332
+ values.each_pair { |key, value| aux[key.to_s] = value }
333
+ end
334
+
335
+ def initialize_copy(original)
336
+ super
337
+ @header = original.header
338
+ @native = original.__send__(:native_handle).duplicate
339
+ @aux_accessor = nil
377
340
  end
378
341
  end
379
342
  end