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
@@ -133,11 +133,10 @@ module HTS
133
133
  # @param auto_parse [Boolean] If true, parse MM/ML lazily on first access
134
134
  def initialize(record, auto_parse: true)
135
135
  @record = record
136
- @state = LibHTS.hts_base_mod_state_alloc
136
+ @state = Native::BaseModHandle.open(record.__send__(:native_handle))
137
137
  @closed = false
138
138
  @auto_parse = !!auto_parse
139
139
  @parsed = false
140
- raise Error, "Failed to allocate hts_base_mod_state" if @state.null?
141
140
  end
142
141
 
143
142
  # Explicitly free the state
@@ -145,8 +144,7 @@ module HTS
145
144
  def close
146
145
  return if @closed
147
146
 
148
- # With HtsBaseModState as an AutoPointer, releasing the Ruby object
149
- # is sufficient. Avoid manual free to prevent double-free.
147
+ @state.close
150
148
  @state = nil
151
149
  @closed = true
152
150
  end
@@ -173,7 +171,7 @@ module HTS
173
171
  # @return [Integer] Number of modification types found, or -1 on error
174
172
  # @raise [Error] If parsing fails
175
173
  def parse(flags = 0)
176
- ret = LibHTS.bam_parse_basemod2(@record.struct, @state, flags)
174
+ ret = @state.parse(flags)
177
175
  raise Error, "Failed to parse base modifications" if ret < 0
178
176
 
179
177
  @parsed = true
@@ -188,13 +186,10 @@ module HTS
188
186
  # Reset state to ensure deterministic results even after prior iteration
189
187
  parsed? ? parse : ensure_parsed!
190
188
 
191
- mods_ptr = FFI::MemoryPointer.new(LibHTS::HtsBaseMod, max_mods)
189
+ values = @state.at(position, max_mods)
190
+ return nil unless values
192
191
 
193
- ret = LibHTS.bam_mods_at_qpos(@record.struct, position, @state,
194
- mods_ptr, max_mods)
195
- return nil if ret <= 0
196
-
197
- build_position(position, mods_ptr, [ret, max_mods].min)
192
+ build_position(position, values)
198
193
  end
199
194
 
200
195
  # Array-style access to modifications at a position
@@ -211,36 +206,41 @@ module HTS
211
206
  def each_position(max_mods: 10)
212
207
  return enum_for(__method__, max_mods: max_mods) unless block_given?
213
208
 
214
- # Reset state at the start of iteration to allow repeated enumerations
215
- parsed? ? parse : ensure_parsed!
216
-
217
- pos_ptr = FFI::MemoryPointer.new(:int)
218
- mods_ptr = FFI::MemoryPointer.new(LibHTS::HtsBaseMod, max_mods)
219
-
220
- loop do
221
- ret = LibHTS.bam_next_basemod(@record.struct, @state,
222
- mods_ptr, max_mods, pos_ptr)
223
- break if ret <= 0
224
-
225
- position = pos_ptr.read_int
226
- yield build_position(position, mods_ptr, [ret, max_mods].min)
209
+ current_position = nil
210
+ modifications = []
211
+ each_raw(max_mods: max_mods) do |position, canonical, modified, strand, qual|
212
+ if current_position && position != current_position
213
+ yield Position.new(current_position, modifications)
214
+ modifications = []
215
+ end
216
+ current_position = position
217
+ modifications << Modification.new(
218
+ modified_base: modified, canonical_base: canonical,
219
+ strand: strand, qual: qual
220
+ )
227
221
  end
222
+ yield Position.new(current_position, modifications) if current_position
223
+ self
228
224
  end
229
225
 
230
226
  alias each each_position
231
227
 
228
+ # Iterate primitive modification values without Position/Modification
229
+ # object allocation.
230
+ def each_raw(max_mods: 10)
231
+ return enum_for(__method__, max_mods: max_mods) unless block_given?
232
+
233
+ parsed? ? parse : ensure_parsed!
234
+ @state.each_raw(max_mods) { |*values| yield(*values) }
235
+ self
236
+ end
237
+
232
238
  # Get list of modification types present in this record
233
239
  # @return [Array<Integer>] Array of modification codes (char code or -ChEBI)
234
240
  def modification_types
235
241
  ensure_parsed!
236
242
 
237
- ntype_ptr = FFI::MemoryPointer.new(:int)
238
- codes_ptr = LibHTS.bam_mods_recorded(@state, ntype_ptr)
239
-
240
- ntype = ntype_ptr.read_int
241
- return [] if ntype <= 0 || codes_ptr.null?
242
-
243
- codes_ptr.read_array_of_int(ntype)
243
+ @state.types
244
244
  end
245
245
 
246
246
  alias recorded_types modification_types
@@ -253,19 +253,7 @@ module HTS
253
253
 
254
254
  code = code.ord if code.is_a?(String)
255
255
 
256
- strand_ptr = FFI::MemoryPointer.new(:int)
257
- implicit_ptr = FFI::MemoryPointer.new(:int)
258
- canonical_ptr = FFI::MemoryPointer.new(:char, 1)
259
-
260
- ret = LibHTS.bam_mods_query_type(@state, code, strand_ptr,
261
- implicit_ptr, canonical_ptr)
262
- return nil if ret < 0
263
-
264
- {
265
- canonical: canonical_ptr.read_char.chr,
266
- strand: strand_ptr.read_int,
267
- implicit: implicit_ptr.read_int != 0
268
- }
256
+ @state.query(code)
269
257
  end
270
258
 
271
259
  # Query information about i-th modification type
@@ -274,21 +262,7 @@ module HTS
274
262
  def query_type_at(index)
275
263
  ensure_parsed!
276
264
 
277
- strand_ptr = FFI::MemoryPointer.new(:int)
278
- implicit_ptr = FFI::MemoryPointer.new(:int)
279
- canonical_ptr = FFI::MemoryPointer.new(:char, 1)
280
-
281
- ret = LibHTS.bam_mods_queryi(@state, index, strand_ptr,
282
- implicit_ptr, canonical_ptr)
283
- return nil if ret < 0
284
-
285
- types = modification_types
286
- {
287
- code: types[index],
288
- canonical: canonical_ptr.read_char.chr,
289
- strand: strand_ptr.read_int,
290
- implicit: implicit_ptr.read_int != 0
291
- }
265
+ @state.query_at(index)
292
266
  end
293
267
 
294
268
  # Get all modifications as an array
@@ -319,21 +293,11 @@ module HTS
319
293
 
320
294
  # Build Position object from hts_base_mod array
321
295
  # @param position [Integer] Query position
322
- # @param mods_ptr [FFI::Pointer] Pointer to array of HtsBaseMod structures
323
- # @param n_mods [Integer] Number of modifications
296
+ # @param values [Array<Array>] Native modification values
324
297
  # @return [Position] Position object
325
- def build_position(position, mods_ptr, n_mods)
326
- modifications = []
327
-
328
- n_mods.times do |i|
329
- mod_struct = LibHTS::HtsBaseMod.new(mods_ptr + i * LibHTS::HtsBaseMod.size)
330
-
331
- modifications << Modification.new(
332
- modified_base: mod_struct[:modified_base],
333
- canonical_base: mod_struct[:canonical_base],
334
- strand: mod_struct[:strand],
335
- qual: mod_struct[:qual]
336
- )
298
+ def build_position(position, values)
299
+ modifications = values.map do |canonical, modified, strand, qual|
300
+ Modification.new(modified_base: modified, canonical_base: canonical, strand:, qual:)
337
301
  end
338
302
 
339
303
  Position.new(position, modifications)
data/lib/hts/bam/cigar.rb CHANGED
@@ -2,78 +2,32 @@
2
2
 
3
3
  module HTS
4
4
  class Bam < Hts
5
- # CIGAR string
6
5
  class Cigar
7
6
  include Enumerable
8
7
 
9
- # a uint32_t array (with 32 bits for every CIGAR op: length<<4|operation)
8
+ OP_CHARS = "MIDNSHP=XB"
10
9
  attr_accessor :array
11
10
 
12
- # Create a new Cigar object from a string.
13
- # @param [String] cigar_str
14
- # The CIGAR string is converted to a uint32_t array in htslib.
15
11
  def self.parse(str)
16
- c = FFI::MemoryPointer.new(:pointer)
17
- m = FFI::MemoryPointer.new(:size_t)
18
- c.write_pointer(FFI::Pointer::NULL)
19
- m.write(:size_t, 0)
20
- ptr = nil
21
- n_cigar = LibHTS.sam_parse_cigar(str, FFI::Pointer::NULL, c, m)
22
- raise "sam_parse_cigar failed: #{n_cigar}" if n_cigar.negative?
23
-
24
- ptr = c.read_pointer
25
- cigar_array = ptr.null? ? [] : ptr.read_array_of_uint32(n_cigar)
26
- obj = new
27
- obj.array = cigar_array
28
- obj
29
- ensure
30
- LibHTS.hts_free(ptr) if ptr && !ptr.null?
12
+ new.tap { |cigar| cigar.array = Native.cigar_parse(str.to_s) }
31
13
  end
32
14
 
33
15
  def initialize(record = nil)
34
- if record
35
- # The record is used at initialization and is not retained after that.
36
- bam1 = record.struct
37
- n_cigar = bam1[:core][:n_cigar]
38
- @array = LibHTS.bam_get_cigar(bam1).read_array_of_uint32(n_cigar)
39
- else
40
- @array = []
41
- end
16
+ @array = record ? record.__send__(:native_handle).cigar_values : []
42
17
  end
43
18
 
44
- def to_s
45
- map { |op, len| "#{len}#{op}" }.join
46
- end
19
+ def to_s = map { |op, len| "#{len}#{op}" }.join
47
20
 
48
21
  def each
49
22
  return to_enum(__method__) unless block_given?
50
23
 
51
- @array.each do |c|
52
- op = LibHTS.bam_cigar_opchr(c)
53
- len = LibHTS.bam_cigar_oplen(c)
54
- yield [op, len]
55
- end
24
+ @array.each { |encoded| yield OP_CHARS[encoded & 15], encoded >> 4 }
56
25
  end
57
26
 
58
- def qlen
59
- a = FFI::MemoryPointer.new(:uint32, @array.size)
60
- a.write_array_of_uint32(@array)
61
- LibHTS.bam_cigar2qlen(@array.size, a)
62
- end
63
-
64
- def rlen
65
- a = FFI::MemoryPointer.new(:uint32, @array.size)
66
- a.write_array_of_uint32(@array)
67
- LibHTS.bam_cigar2rlen(@array.size, a)
68
- end
69
-
70
- def ==(other)
71
- other.is_a?(Cigar) && (@array == other.array)
72
- end
73
-
74
- def eql?(other)
75
- other.is_a?(Cigar) && @array.eql?(other.array)
76
- end
27
+ def qlen = Native.cigar_qlen(@array)
28
+ def rlen = Native.cigar_rlen(@array)
29
+ def ==(other) = other.is_a?(Cigar) && @array == other.array
30
+ def eql?(other) = other.is_a?(Cigar) && @array.eql?(other.array)
77
31
  end
78
32
  end
79
33
  end
data/lib/hts/bam/flag.rb CHANGED
@@ -15,31 +15,24 @@ module HTS
15
15
 
16
16
  attr_accessor :value
17
17
 
18
- # BAM_FPAIRED = 1
19
- # BAM_FPROPER_PAIR = 2
20
- # BAM_FUNMAP = 4
21
- # BAM_FMUNMAP = 8
22
- # BAM_FREVERSE = 16
23
- # BAM_FMREVERSE = 32
24
- # BAM_FREAD1 = 64
25
- # BAM_FREAD2 = 128
26
- # BAM_FSECONDARY = 256
27
- # BAM_FQCFAIL = 512
28
- # BAM_FDUP = 1024
29
- # BAM_FSUPPLEMENTARY = 2048
30
-
31
- TABLE = { paired?: LibHTS::BAM_FPAIRED,
32
- proper_pair?: LibHTS::BAM_FPROPER_PAIR,
33
- unmapped?: LibHTS::BAM_FUNMAP,
34
- mate_unmapped?: LibHTS::BAM_FMUNMAP,
35
- reverse?: LibHTS::BAM_FREVERSE,
36
- mate_reverse?: LibHTS::BAM_FMREVERSE,
37
- read1?: LibHTS::BAM_FREAD1,
38
- read2?: LibHTS::BAM_FREAD2,
39
- secondary?: LibHTS::BAM_FSECONDARY,
40
- qcfail?: LibHTS::BAM_FQCFAIL,
41
- duplicate?: LibHTS::BAM_FDUP,
42
- supplementary?: LibHTS::BAM_FSUPPLEMENTARY }.freeze
18
+ PAIRED = 1
19
+ PROPER_PAIR = 2
20
+ UNMAPPED = 4
21
+ MATE_UNMAPPED = 8
22
+ REVERSE = 16
23
+ MATE_REVERSE = 32
24
+ READ1 = 64
25
+ READ2 = 128
26
+ SECONDARY = 256
27
+ QCFAIL = 512
28
+ DUPLICATE = 1024
29
+ SUPPLEMENTARY = 2048
30
+
31
+ TABLE = { paired?: PAIRED, proper_pair?: PROPER_PAIR, unmapped?: UNMAPPED,
32
+ mate_unmapped?: MATE_UNMAPPED, reverse?: REVERSE,
33
+ mate_reverse?: MATE_REVERSE, read1?: READ1, read2?: READ2,
34
+ secondary?: SECONDARY, qcfail?: QCFAIL, duplicate?: DUPLICATE,
35
+ supplementary?: SUPPLEMENTARY }.freeze
43
36
 
44
37
  # @!macro [attach] generate_flag_methods
45
38
  # @!method $1
@@ -99,8 +92,7 @@ module HTS
99
92
  end
100
93
 
101
94
  def to_s
102
- LibHTS.bam_flag2str(@value)
103
- # "0x#{format('%x', @value)}\t#{@value}\t#{LibHTS.bam_flag2str(@value)}"
95
+ Native.bam_flag_string(@value)
104
96
  end
105
97
  end
106
98
  end
@@ -40,17 +40,15 @@ module HTS
40
40
  }.freeze
41
41
 
42
42
  def self.parse(text)
43
- new(LibHTS.sam_hdr_parse(text.size, text))
43
+ new(Native::SamHeaderHandle.parse(text))
44
44
  end
45
45
 
46
46
  def initialize(arg = nil)
47
47
  case arg
48
- when LibHTS::HtsFile
49
- @sam_hdr = LibHTS.sam_hdr_read(arg)
50
- when LibHTS::SamHdr
51
- @sam_hdr = arg
48
+ when Native::SamHeaderHandle
49
+ @native = arg
52
50
  when nil
53
- @sam_hdr = LibHTS.sam_hdr_init
51
+ @native = Native::SamHeaderHandle.create
54
52
  else
55
53
  raise TypeError, "Invalid argument"
56
54
  end
@@ -58,25 +56,16 @@ module HTS
58
56
  yield self if block_given?
59
57
  end
60
58
 
61
- def struct
62
- @sam_hdr
63
- end
64
-
65
- def to_ptr
66
- @sam_hdr.to_ptr
67
- end
68
-
69
59
  def targets
70
60
  Array.new(target_count) do |i|
71
- name = LibHTS.sam_hdr_tid2name(@sam_hdr, i)
72
- len = LibHTS.sam_hdr_tid2len(@sam_hdr, i)
61
+ name = @native.target_name(i)
62
+ len = @native.target_length(i)
73
63
  { name:, len: }
74
64
  end
75
65
  end
76
66
 
77
67
  def target_count
78
- # FIXME: sam_hdr_nref
79
- @sam_hdr[:n_targets]
68
+ @native.target_count
80
69
  end
81
70
 
82
71
  def target_name(tid)
@@ -85,13 +74,13 @@ module HTS
85
74
 
86
75
  def target_names
87
76
  Array.new(target_count) do |i|
88
- LibHTS.sam_hdr_tid2name(@sam_hdr, i)
77
+ @native.target_name(i)
89
78
  end
90
79
  end
91
80
 
92
81
  def target_len
93
82
  Array.new(target_count) do |i|
94
- LibHTS.sam_hdr_tid2len(@sam_hdr, i)
83
+ @native.target_length(i)
95
84
  end
96
85
  end
97
86
 
@@ -118,68 +107,50 @@ module HTS
118
107
 
119
108
  # experimental
120
109
  def find_line(type, key, value)
121
- ks = LibHTS::KString.new
122
- begin
123
- r = LibHTS.sam_hdr_find_line_id(@sam_hdr, type, key, value, ks)
124
- r == 0 ? ks.read_string_copy : nil
125
- ensure
126
- ks.free_buffer
127
- end
110
+ @native.find_line(type, key, value)
128
111
  end
129
112
 
130
113
  def find_tag(type, id_key, id_value, key)
131
- ks = LibHTS::KString.new
132
- begin
133
- r = LibHTS.sam_hdr_find_tag_id(@sam_hdr, type, id_key, id_value, key, ks)
134
- r == 0 ? ks.read_string_copy : nil
135
- ensure
136
- ks.free_buffer
137
- end
114
+ @native.find_tag(type, id_key, id_value, key)
138
115
  end
139
116
 
140
117
  # experimental
141
118
  def find_line_at(type, pos)
142
- ks = LibHTS::KString.new
143
- begin
144
- r = LibHTS.sam_hdr_find_line_pos(@sam_hdr, type, pos, ks)
145
- r == 0 ? ks.read_string_copy : nil
146
- ensure
147
- ks.free_buffer
148
- end
119
+ @native.find_line_at(type, pos)
149
120
  end
150
121
 
151
122
  # experimental
152
123
  def remove_line(type, key, value)
153
- LibHTS.sam_hdr_remove_line_id(@sam_hdr, type, key, value)
124
+ @native.remove_line(type, key, value)
154
125
  end
155
126
 
156
127
  # experimental
157
128
  def remove_line_at(type, pos)
158
- LibHTS.sam_hdr_remove_line_pos(@sam_hdr, type, pos)
129
+ @native.remove_line_at(type, pos)
159
130
  end
160
131
 
161
132
  def delete_line(type, key = nil, value = nil)
162
- LibHTS.sam_hdr_remove_line_id(@sam_hdr, type, key, value).zero?
133
+ @native.remove_line(type, key, value).zero?
163
134
  end
164
135
 
165
136
  def delete_tag(type, id_key, id_value, key)
166
- LibHTS.sam_hdr_remove_tag_id(@sam_hdr, type, id_key, id_value, key) == 1
137
+ @native.remove_tag(type, id_key, id_value, key) == 1
167
138
  end
168
139
 
169
140
  def count_lines(type)
170
- LibHTS.sam_hdr_count_lines(@sam_hdr, type)
141
+ @native.count_lines(type)
171
142
  end
172
143
 
173
144
  def line_index(type, key)
174
- LibHTS.sam_hdr_line_index(@sam_hdr, type, key)
145
+ @native.line_index(type, key)
175
146
  end
176
147
 
177
148
  def line_name(type, pos)
178
- LibHTS.sam_hdr_line_name(@sam_hdr, type, pos)
149
+ @native.line_name(type, pos)
179
150
  end
180
151
 
181
152
  def to_s
182
- LibHTS.sam_hdr_str(@sam_hdr)
153
+ @native.to_s
183
154
  end
184
155
 
185
156
  # experimental
@@ -241,7 +212,7 @@ module HTS
241
212
  # header.add_pg("samtools", VN: "1.15", PP: "bwa")
242
213
  def add_pg(program_name, **options)
243
214
  line = build_pg_line(program_name.to_s, options)
244
- result = LibHTS.sam_hdr_add_lines(@sam_hdr, line, line.bytesize)
215
+ result = @native.add_lines(line)
245
216
  raise "Failed to add @PG line" if result < 0
246
217
 
247
218
  self
@@ -418,26 +389,28 @@ module HTS
418
389
  end
419
390
 
420
391
  def name2tid(name)
421
- LibHTS.sam_hdr_name2tid(@sam_hdr, name)
392
+ @native.name2tid(name)
422
393
  end
423
394
 
424
395
  def tid2name(tid)
425
- LibHTS.sam_hdr_tid2name(@sam_hdr, tid)
396
+ @native.target_name(tid)
426
397
  end
427
398
 
428
399
  def add_lines(str)
429
- LibHTS.sam_hdr_add_lines(@sam_hdr, str, 0)
400
+ @native.add_lines(str)
430
401
  end
431
402
 
432
403
  def add_line(*args)
433
404
  type = args.shift
434
- args = args.flat_map { |arg| [:string, arg] }
435
- LibHTS.sam_hdr_add_line(@sam_hdr, type, *args, :pointer, FFI::Pointer::NULL)
405
+ pairs = args.each_slice(2).map { |key, value| "#{key}:#{value}" }
406
+ @native.add_lines("@#{type}\t#{pairs.join("\t")}\n")
436
407
  end
437
408
 
438
409
  def initialize_copy(orig)
439
- @sam_hdr = LibHTS.sam_hdr_dup(orig.struct)
410
+ @native = orig.__send__(:native_handle).duplicate
440
411
  end
412
+
413
+ def native_handle = @native
441
414
  end
442
415
  end
443
416
  end