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.
- checksums.yaml +4 -4
- data/README.md +79 -49
- data/TUTORIAL.md +9 -20
- data/ext/htslib_native/extconf.rb +41 -0
- data/ext/htslib_native/htslib_native.h +11 -0
- data/ext/htslib_native/htslib_native_ext.c +48 -0
- data/ext/htslib_native/native_bam.c +1108 -0
- data/ext/htslib_native/native_bcf.c +369 -0
- data/ext/htslib_native/native_faidx.c +155 -0
- data/ext/htslib_native/native_tabix.c +246 -0
- data/lib/hts/bam/auxi.rb +87 -342
- data/lib/hts/bam/base_mod.rb +37 -73
- data/lib/hts/bam/cigar.rb +9 -55
- data/lib/hts/bam/flag.rb +19 -27
- data/lib/hts/bam/header.rb +29 -56
- data/lib/hts/bam/mpileup.rb +158 -132
- data/lib/hts/bam/pileup.rb +120 -145
- data/lib/hts/bam/record.rb +233 -270
- data/lib/hts/bam.rb +102 -42
- data/lib/hts/bcf/errors.rb +1 -0
- data/lib/hts/bcf/format.rb +278 -379
- data/lib/hts/bcf/header.rb +38 -123
- data/lib/hts/bcf/header_record.rb +9 -35
- data/lib/hts/bcf/info.rb +72 -320
- data/lib/hts/bcf/record.rb +61 -102
- data/lib/hts/bcf.rb +149 -323
- data/lib/hts/faidx.rb +28 -87
- data/lib/hts/hts.rb +6 -94
- data/lib/hts/native.rb +13 -0
- data/lib/hts/tabix.rb +93 -48
- data/lib/hts/version.rb +1 -1
- data/lib/htslib.rb +6 -52
- metadata +13 -64
- data/lib/hts/ffi_ext/README.md +0 -8
- data/lib/hts/ffi_ext/pointer.rb +0 -18
- data/lib/hts/ffi_ext/struct.rb +0 -45
- data/lib/hts/libhts/bgzf.rb +0 -199
- data/lib/hts/libhts/constants.rb +0 -658
- data/lib/hts/libhts/cram.rb +0 -471
- data/lib/hts/libhts/fai.rb +0 -146
- data/lib/hts/libhts/hfile.rb +0 -109
- data/lib/hts/libhts/hts.rb +0 -477
- data/lib/hts/libhts/kfunc.rb +0 -38
- data/lib/hts/libhts/sam.rb +0 -760
- data/lib/hts/libhts/sam_funcs.rb +0 -155
- data/lib/hts/libhts/tbx.rb +0 -79
- data/lib/hts/libhts/tbx_funcs.rb +0 -32
- data/lib/hts/libhts/thread_pool.rb +0 -139
- data/lib/hts/libhts/vcf.rb +0 -557
- data/lib/hts/libhts/vcf_funcs.rb +0 -353
- data/lib/hts/libhts.rb +0 -47
data/lib/hts/bam/mpileup.rb
CHANGED
|
@@ -2,173 +2,199 @@
|
|
|
2
2
|
|
|
3
3
|
module HTS
|
|
4
4
|
class Bam < Hts
|
|
5
|
-
# High-level mpileup iterator over multiple BAM/CRAM inputs
|
|
6
5
|
class Mpileup
|
|
7
6
|
include Enumerable
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
class DepthView
|
|
9
|
+
include Enumerable
|
|
10
|
+
attr_reader :length
|
|
11
|
+
alias size length
|
|
12
|
+
def initialize = reset([])
|
|
13
|
+
|
|
14
|
+
def reset(values)
|
|
15
|
+
@values = values
|
|
16
|
+
@length = values.length
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def [](index)
|
|
21
|
+
index = Integer(index)
|
|
22
|
+
index += @length if index.negative?
|
|
23
|
+
raise IndexError, "depth index #{index} outside of view" unless index.between?(0, @length - 1)
|
|
24
|
+
|
|
25
|
+
@values[index]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def each(&block)
|
|
29
|
+
return to_enum(__method__) unless block_given?
|
|
30
|
+
|
|
31
|
+
@values.each(&block)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ColumnsView
|
|
37
|
+
include Enumerable
|
|
38
|
+
attr_reader :tid, :pos, :length
|
|
39
|
+
alias size length
|
|
40
|
+
def initialize = @column_view = Pileup::BorrowedColumnView.new
|
|
41
|
+
|
|
42
|
+
def reset(tid, pos, rows)
|
|
43
|
+
@tid = tid
|
|
44
|
+
@pos = pos
|
|
45
|
+
@rows = rows
|
|
46
|
+
@length = rows.length
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def each
|
|
51
|
+
return to_enum(__method__) unless block_given?
|
|
52
|
+
|
|
53
|
+
@rows.each_with_index { |entries, index| yield index, @column_view.reset(entries, @tid, @pos) }
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.open(*args, **keywords)
|
|
59
|
+
mpileup = new(*args, **keywords)
|
|
60
|
+
return mpileup unless block_given?
|
|
16
61
|
|
|
17
62
|
begin
|
|
18
|
-
yield
|
|
63
|
+
yield mpileup
|
|
19
64
|
ensure
|
|
20
|
-
|
|
65
|
+
mpileup.close
|
|
21
66
|
end
|
|
22
|
-
|
|
67
|
+
mpileup
|
|
23
68
|
end
|
|
24
69
|
|
|
25
|
-
# Normalize inputs to HTS::Bam instances
|
|
26
|
-
# Accepts array of HTS::Bam or filenames (String)
|
|
27
70
|
def initialize(inputs, region: nil, beg: nil, end_: nil, maxcnt: nil, overlaps: false)
|
|
28
71
|
raise ArgumentError, "inputs must be non-empty" if inputs.nil? || inputs.empty?
|
|
29
72
|
|
|
30
|
-
@owned_bams = []
|
|
31
|
-
@bams = inputs.map do |
|
|
32
|
-
case
|
|
33
|
-
when HTS::Bam
|
|
34
|
-
x
|
|
73
|
+
@owned_bams = []
|
|
74
|
+
@bams = inputs.map do |input|
|
|
75
|
+
case input
|
|
76
|
+
when HTS::Bam then input
|
|
35
77
|
when String
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
b
|
|
39
|
-
else
|
|
40
|
-
raise ArgumentError, "Unsupported input type: #{x.class}"
|
|
78
|
+
HTS::Bam.open(input).tap { |bam| @owned_bams << bam }
|
|
79
|
+
else raise ArgumentError, "Unsupported input type: #{input.class}"
|
|
41
80
|
end
|
|
42
81
|
end
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@data_blocks = [] # per-input packed pointers kept alive
|
|
47
|
-
@data_entries = {}
|
|
48
|
-
|
|
49
|
-
# Prepare optional region iterators for each input
|
|
50
|
-
@bams.each_with_index do |bam, i|
|
|
51
|
-
itr = nil
|
|
52
|
-
if region && beg.nil? && end_.nil?
|
|
53
|
-
raise "Index required for region mpileup" unless bam.index_loaded?
|
|
54
|
-
|
|
55
|
-
itr = HTS::LibHTS.sam_itr_querys(bam.instance_variable_get(:@idx), bam.header.struct, region)
|
|
56
|
-
raise "Failed to query region on input ##{i}: #{region}" if itr.null?
|
|
57
|
-
elsif region && beg && end_
|
|
58
|
-
raise "Index required for region mpileup" unless bam.index_loaded?
|
|
59
|
-
|
|
60
|
-
tid = bam.header.get_tid(region)
|
|
61
|
-
itr = HTS::LibHTS.sam_itr_queryi(bam.instance_variable_get(:@idx), tid, beg, end_)
|
|
62
|
-
raise "Failed to query region on input ##{i}: #{region} #{beg} #{end_}" if itr.null?
|
|
63
|
-
elsif beg || end_
|
|
64
|
-
raise ArgumentError, "beg and end_ must be specified together"
|
|
65
|
-
end
|
|
66
|
-
@iters << itr
|
|
82
|
+
if region
|
|
83
|
+
missing = @bams.find { |bam| !bam.__send__(:ensure_index_loaded) }
|
|
84
|
+
raise "Index file is required to use region mpileup: #{missing.file_name}" if missing
|
|
67
85
|
end
|
|
86
|
+
files = @bams.map { |bam| bam.__send__(:native_handle) }
|
|
87
|
+
headers = @bams.map { |bam| bam.header.__send__(:native_handle) }
|
|
88
|
+
@native = Native::MpileupHandle.open(files, headers, region, beg, end_, maxcnt, overlaps)
|
|
89
|
+
@closed = false
|
|
90
|
+
end
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
itr = @iters[i]
|
|
78
|
-
block = FFI::MemoryPointer.new(:pointer, 3)
|
|
79
|
-
block.put_pointer(0 * ptr_size, hts_fp)
|
|
80
|
-
block.put_pointer(1 * ptr_size, hdr_struct)
|
|
81
|
-
block.put_pointer(2 * ptr_size, itr && !itr.null? ? itr : FFI::Pointer::NULL)
|
|
82
|
-
@data_blocks << block
|
|
83
|
-
@data_entries[block.address] = [hts_fp, hdr_struct, itr && !itr.null? ? itr : nil]
|
|
84
|
-
data_array.put_pointer(i * ptr_size, block)
|
|
85
|
-
end
|
|
86
|
-
# Keep the array of per-input blocks alive while the C side holds on to them
|
|
87
|
-
@data_array = data_array
|
|
88
|
-
|
|
89
|
-
@cb = FFI::Function.new(:int, %i[pointer pointer]) do |data, b|
|
|
90
|
-
hts_fp, hdr_struct, itr = @data_entries.fetch(data.address)
|
|
91
|
-
# HTSlib contract: return same as sam_itr_next/sam_read1 (>= 0 on success, -1 on EOF, < -1 on error)
|
|
92
|
-
if itr
|
|
93
|
-
HTS::LibHTS.sam_itr_next(hts_fp, itr, b)
|
|
94
|
-
else
|
|
95
|
-
HTS::LibHTS.sam_read1(hts_fp, hdr_struct, b)
|
|
92
|
+
def each
|
|
93
|
+
return to_enum(__method__) unless block_given?
|
|
94
|
+
|
|
95
|
+
headers = @bams.map(&:header)
|
|
96
|
+
each_column_raw do |tid, pos, _depths, rows_by_input, _|
|
|
97
|
+
columns = rows_by_input.each_with_index.map do |rows, index|
|
|
98
|
+
alignments = rows.map { |row| Pileup::PileupRecord.new(row, headers[index]) }
|
|
99
|
+
Pileup::PileupColumn.new(tid:, pos:, alignments:)
|
|
96
100
|
end
|
|
101
|
+
yield columns
|
|
97
102
|
end
|
|
103
|
+
self
|
|
104
|
+
end
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
def each_depth
|
|
107
|
+
return to_enum(__method__) unless block_given?
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
view = DepthView.new
|
|
110
|
+
each_column_raw { |tid, pos, depths, _, _| yield tid, pos, view.reset(depths) }
|
|
111
|
+
self
|
|
112
|
+
end
|
|
104
113
|
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
def each_view
|
|
115
|
+
return to_enum(__method__) unless block_given?
|
|
116
|
+
|
|
117
|
+
view = ColumnsView.new
|
|
118
|
+
each_column_raw { |tid, pos, _, rows, _| yield view.reset(tid, pos, rows) }
|
|
119
|
+
self
|
|
107
120
|
end
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
def each
|
|
122
|
+
def each_entry_raw
|
|
111
123
|
return to_enum(__method__) unless block_given?
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
n_ptr = FFI::MemoryPointer.new(:int, n)
|
|
117
|
-
plp_ptr = FFI::MemoryPointer.new(:pointer, n)
|
|
118
|
-
plp1_size = HTS::LibHTS::BamPileup1.size
|
|
119
|
-
headers = @bams.map(&:header)
|
|
120
|
-
|
|
121
|
-
while HTS::LibHTS.bam_mplp64_auto(@iter, tid_ptr, pos_ptr, n_ptr, plp_ptr) > 0
|
|
122
|
-
tid = tid_ptr.read_int
|
|
123
|
-
pos = pos_ptr.read_long_long
|
|
124
|
-
|
|
125
|
-
counts = n_ptr.read_array_of_int(n)
|
|
126
|
-
plp_arr = plp_ptr.read_array_of_pointer(n)
|
|
127
|
-
|
|
128
|
-
cols = Array.new(n)
|
|
129
|
-
i = 0
|
|
130
|
-
while i < n
|
|
131
|
-
c = counts[i]
|
|
132
|
-
if c <= 0 || plp_arr[i].null?
|
|
133
|
-
cols[i] = HTS::Bam::Pileup::PileupColumn.new(tid: tid, pos: pos, alignments: [])
|
|
134
|
-
else
|
|
135
|
-
base_ptr = plp_arr[i]
|
|
136
|
-
aligns = Array.new(c)
|
|
137
|
-
j = 0
|
|
138
|
-
while j < c
|
|
139
|
-
e_ptr = base_ptr + (j * plp1_size)
|
|
140
|
-
entry = HTS::LibHTS::BamPileup1.new(e_ptr)
|
|
141
|
-
aligns[j] = HTS::Bam::Pileup::PileupRecord.new(entry, headers[i])
|
|
142
|
-
j += 1
|
|
143
|
-
end
|
|
144
|
-
cols[i] = HTS::Bam::Pileup::PileupColumn.new(tid: tid, pos: pos, alignments: aligns)
|
|
145
|
-
end
|
|
146
|
-
i += 1
|
|
125
|
+
each_column_raw do |tid, pos, _, rows_by_input, _|
|
|
126
|
+
rows_by_input.each_with_index do |rows, input_index|
|
|
127
|
+
rows.each { |row| yield input_index, tid, pos, row[1], row[7], row[8], row[9] }
|
|
147
128
|
end
|
|
148
|
-
|
|
149
|
-
yield cols
|
|
150
129
|
end
|
|
151
|
-
|
|
152
130
|
self
|
|
153
131
|
end
|
|
154
132
|
|
|
155
|
-
def
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
133
|
+
def each_base_counts(min_base_quality: 0, min_mapping_quality: 0)
|
|
134
|
+
return to_enum(__method__, min_base_quality:, min_mapping_quality:) unless block_given?
|
|
135
|
+
|
|
136
|
+
min_base_quality = Integer(min_base_quality)
|
|
137
|
+
min_mapping_quality = Integer(min_mapping_quality)
|
|
138
|
+
if min_base_quality.negative? || min_mapping_quality.negative?
|
|
139
|
+
raise ArgumentError,
|
|
140
|
+
"quality thresholds must be non-negative"
|
|
159
141
|
end
|
|
160
|
-
|
|
161
|
-
|
|
142
|
+
|
|
143
|
+
counts = Array.new(@bams.length) { Array.new(Pileup::BASE_COUNT_FIELDS.length, 0) }
|
|
144
|
+
each_column_raw do |tid, pos, _, rows_by_input, _|
|
|
145
|
+
rows_by_input.each_with_index do |rows, input_index|
|
|
146
|
+
aggregate_counts(rows, counts[input_index], min_base_quality, min_mapping_quality)
|
|
147
|
+
end
|
|
148
|
+
yield tid, pos, counts
|
|
162
149
|
end
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The fourth value is an Array of opaque native-entry rows. It remains
|
|
154
|
+
# borrowed until the next iteration.
|
|
155
|
+
def each_column_raw
|
|
156
|
+
return to_enum(__method__) unless block_given?
|
|
157
|
+
|
|
158
|
+
while (column = @native.next)
|
|
159
|
+
tid, pos, depths, rows = column
|
|
160
|
+
yield tid, pos, depths, rows, @bams.length
|
|
170
161
|
end
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def reset
|
|
166
|
+
@native.reset
|
|
167
|
+
self
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def close
|
|
171
|
+
return if @closed
|
|
172
|
+
|
|
173
|
+
@native.close
|
|
174
|
+
@owned_bams.each(&:close)
|
|
171
175
|
@owned_bams.clear
|
|
176
|
+
@closed = true
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def aggregate_counts(rows, counts, min_base_quality, min_mapping_quality)
|
|
183
|
+
counts.fill(0)
|
|
184
|
+
rows.each do |row|
|
|
185
|
+
next if row[6] || row[10] < min_mapping_quality
|
|
186
|
+
|
|
187
|
+
if row[3] || row[1].negative?
|
|
188
|
+
counts[8] += 1
|
|
189
|
+
else
|
|
190
|
+
next if row[9] < min_base_quality
|
|
191
|
+
|
|
192
|
+
counts[{ 1 => 1, 2 => 2, 4 => 3, 8 => 4 }.fetch(row[8], 5)] += 1
|
|
193
|
+
end
|
|
194
|
+
counts[0] += 1
|
|
195
|
+
counts[(row[7] & 16).zero? ? 6 : 7] += 1
|
|
196
|
+
counts[9] += 1 if row[2].positive?
|
|
197
|
+
end
|
|
172
198
|
end
|
|
173
199
|
end
|
|
174
200
|
end
|
data/lib/hts/bam/pileup.rb
CHANGED
|
@@ -1,200 +1,175 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../native"
|
|
4
|
+
|
|
3
5
|
module HTS
|
|
4
6
|
class Bam < Hts
|
|
5
|
-
# High-level pileup iterator for a single SAM/BAM/CRAM
|
|
6
7
|
class Pileup
|
|
7
8
|
include Enumerable
|
|
9
|
+
BASE_COUNT_FIELDS = %i[depth a c g t n forward reverse deletion insertion].freeze
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# end
|
|
13
|
-
def self.open(*args, **kw)
|
|
14
|
-
pu = new(*args, **kw)
|
|
15
|
-
return pu unless block_given?
|
|
11
|
+
def self.open(*args, **keywords)
|
|
12
|
+
pileup = new(*args, **keywords)
|
|
13
|
+
return pileup unless block_given?
|
|
16
14
|
|
|
17
15
|
begin
|
|
18
|
-
yield
|
|
16
|
+
yield pileup
|
|
19
17
|
ensure
|
|
20
|
-
|
|
18
|
+
pileup.close
|
|
21
19
|
end
|
|
22
|
-
|
|
20
|
+
pileup
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
# A column at a reference position with pileup alignments
|
|
26
23
|
PileupColumn = Struct.new(:tid, :pos, :alignments, keyword_init: true) do
|
|
27
|
-
def depth
|
|
28
|
-
alignments.length
|
|
29
|
-
end
|
|
24
|
+
def depth = alignments.length
|
|
30
25
|
end
|
|
31
26
|
|
|
32
|
-
# A wrapper of one bam_pileup1_t entry
|
|
33
27
|
class PileupRecord
|
|
34
|
-
def initialize(
|
|
35
|
-
@
|
|
28
|
+
def initialize(values, header)
|
|
29
|
+
@values = values
|
|
36
30
|
@header = header
|
|
37
|
-
@record = nil
|
|
38
31
|
end
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
b_ptr = @entry[:b].is_a?(FFI::Pointer) ? @entry[:b] : @entry[:b].to_ptr
|
|
49
|
-
dup_ptr = HTS::LibHTS.bam_dup1(b_ptr)
|
|
50
|
-
raise "bam_dup1 failed" if dup_ptr.null?
|
|
51
|
-
|
|
52
|
-
# Build a Bam::Record backed by the duplicated bam1_t.
|
|
53
|
-
@record = HTS::Bam::Record.new(@header, dup_ptr)
|
|
54
|
-
end
|
|
33
|
+
def record = (@record ||= HTS::Bam::Record.new(@header, @values[0]))
|
|
34
|
+
def query_position = @values[1]
|
|
35
|
+
def indel = @values[2]
|
|
36
|
+
def del? = @values[3]
|
|
37
|
+
def head? = @values[4]
|
|
38
|
+
def tail? = @values[5]
|
|
39
|
+
def refskip? = @values[6]
|
|
40
|
+
end
|
|
55
41
|
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
class BorrowedEntryView
|
|
43
|
+
def reset(values, tid, pos)
|
|
44
|
+
@values = values
|
|
45
|
+
@tid = tid
|
|
46
|
+
@pos = pos
|
|
47
|
+
self
|
|
58
48
|
end
|
|
49
|
+
attr_reader :tid, :pos
|
|
50
|
+
|
|
51
|
+
def query_position = @values[1]
|
|
52
|
+
def indel = @values[2]
|
|
53
|
+
def del? = @values[3]
|
|
54
|
+
def refskip? = @values[6]
|
|
55
|
+
def flag = @values[7]
|
|
56
|
+
def base_code = @values[8]
|
|
57
|
+
def quality = @values[9]
|
|
58
|
+
end
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
class BorrowedColumnView
|
|
61
|
+
include Enumerable
|
|
62
|
+
attr_reader :tid, :pos, :depth
|
|
63
63
|
|
|
64
|
-
def
|
|
65
|
-
@entry[:is_del] == 1
|
|
66
|
-
end
|
|
64
|
+
def initialize = @entry_view = BorrowedEntryView.new
|
|
67
65
|
|
|
68
|
-
def
|
|
69
|
-
@
|
|
66
|
+
def reset(rows, tid, pos)
|
|
67
|
+
@rows = rows
|
|
68
|
+
@tid = tid
|
|
69
|
+
@pos = pos
|
|
70
|
+
@depth = rows.length
|
|
71
|
+
self
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
end
|
|
74
|
+
def each
|
|
75
|
+
return to_enum(__method__) unless block_given?
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
@rows.each { |row| yield @entry_view.reset(row, @tid, @pos) }
|
|
78
|
+
self
|
|
78
79
|
end
|
|
79
80
|
end
|
|
80
81
|
|
|
81
|
-
# Create a Pileup iterator
|
|
82
|
-
# @param bam [HTS::Bam]
|
|
83
|
-
# @param region [String, nil] Optional region string (requires index)
|
|
84
|
-
# @param beg [Integer, nil] Optional begin when using tid/beg/end form
|
|
85
|
-
# @param end_ [Integer, nil] Optional end when using tid/beg/end form
|
|
86
|
-
# @param maxcnt [Integer, nil] Max per-position depth (capped)
|
|
87
82
|
def initialize(bam, region: nil, beg: nil, end_: nil, maxcnt: nil)
|
|
88
|
-
|
|
83
|
+
raise ArgumentError, "beg and end_ must be specified together" if beg.nil? != end_.nil?
|
|
84
|
+
raise ArgumentError, "region is required when beg/end_ are specified" if !beg.nil? && region.nil?
|
|
85
|
+
|
|
86
|
+
@bam = bam
|
|
89
87
|
@header = bam.header
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@plp = nil
|
|
93
|
-
|
|
94
|
-
# Optional region iterator
|
|
95
|
-
if region && beg.nil? && end_.nil?
|
|
96
|
-
raise "Index file is required to use region pileup." unless bam.index_loaded?
|
|
97
|
-
|
|
98
|
-
@itr = HTS::LibHTS.sam_itr_querys(bam.instance_variable_get(:@idx), @header.struct, region)
|
|
99
|
-
raise "Failed to query region: #{region}" if @itr.null?
|
|
100
|
-
elsif region && beg && end_
|
|
101
|
-
raise "Index file is required to use region pileup." unless bam.index_loaded?
|
|
102
|
-
|
|
103
|
-
tid = @header.get_tid(region)
|
|
104
|
-
@itr = HTS::LibHTS.sam_itr_queryi(bam.instance_variable_get(:@idx), tid, beg, end_)
|
|
105
|
-
raise "Failed to query region: #{region} #{beg} #{end_}" if @itr.null?
|
|
106
|
-
elsif beg || end_
|
|
107
|
-
raise ArgumentError, "beg and end_ must be specified together"
|
|
88
|
+
if region && !bam.__send__(:ensure_index_loaded)
|
|
89
|
+
raise "Index file is required to use region pileup."
|
|
108
90
|
end
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
# - Specialize callbacks to avoid branching in the hot path.
|
|
113
|
-
hts_fp = @bam.instance_variable_get(:@hts_file)
|
|
114
|
-
hdr_struct = @header.struct
|
|
115
|
-
itr_local = @itr
|
|
116
|
-
|
|
117
|
-
@cb = if itr_local && !itr_local.null?
|
|
118
|
-
FFI::Function.new(:int, %i[pointer pointer]) do |_data, b|
|
|
119
|
-
# HTSlib contract: return same as sam_itr_next (>= 0 on success, -1 on EOF, < -1 on error)
|
|
120
|
-
HTS::LibHTS.sam_itr_next(hts_fp, itr_local, b)
|
|
121
|
-
end
|
|
122
|
-
else
|
|
123
|
-
FFI::Function.new(:int, %i[pointer pointer]) do |_data, b|
|
|
124
|
-
# HTSlib contract: return same as sam_read1 (>= 0 on success, -1 on EOF, < -1 on error)
|
|
125
|
-
HTS::LibHTS.sam_read1(hts_fp, hdr_struct, b)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
@plp = HTS::LibHTS.bam_plp_init(@cb, nil)
|
|
130
|
-
raise "bam_plp_init failed" if @plp.null?
|
|
131
|
-
|
|
132
|
-
HTS::LibHTS.bam_plp_set_maxcnt(@plp, maxcnt) if maxcnt
|
|
91
|
+
@native = Native::PileupHandle.open(
|
|
92
|
+
bam.__send__(:native_handle), @header.__send__(:native_handle), region, beg, end_, maxcnt
|
|
93
|
+
)
|
|
133
94
|
end
|
|
134
95
|
|
|
135
96
|
def each
|
|
136
97
|
return to_enum(__method__) unless block_given?
|
|
137
98
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
99
|
+
each_raw_column do |tid, pos, rows|
|
|
100
|
+
alignments = rows.map { |row| PileupRecord.new(row, @header) }
|
|
101
|
+
yield PileupColumn.new(tid:, pos:, alignments:)
|
|
102
|
+
end
|
|
103
|
+
self
|
|
104
|
+
end
|
|
141
105
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# - Hoist header reference outside the loop
|
|
145
|
-
plp1_size = HTS::LibHTS::BamPileup1.size
|
|
146
|
-
header_local = @header
|
|
106
|
+
def each_depth
|
|
107
|
+
return to_enum(__method__) unless block_given?
|
|
147
108
|
|
|
148
|
-
|
|
149
|
-
|
|
109
|
+
each_raw_column { |tid, pos, rows| yield tid, pos, rows.length }
|
|
110
|
+
self
|
|
111
|
+
end
|
|
150
112
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
n = n_ptr.read_int
|
|
154
|
-
raise "HTSlib pileup error (bam_plp64_auto)" if n < 0
|
|
113
|
+
def each_view
|
|
114
|
+
return to_enum(__method__) unless block_given?
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
|
|
116
|
+
view = BorrowedColumnView.new
|
|
117
|
+
each_raw_column { |tid, pos, rows| yield view.reset(rows, tid, pos) }
|
|
118
|
+
self
|
|
119
|
+
end
|
|
158
120
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
n = n_ptr.read_int
|
|
162
|
-
|
|
163
|
-
# Construct alignment entries with minimal allocations
|
|
164
|
-
if n.zero?
|
|
165
|
-
alignments = []
|
|
166
|
-
else
|
|
167
|
-
alignments = Array.new(n)
|
|
168
|
-
i = 0
|
|
169
|
-
while i < n
|
|
170
|
-
e_ptr = base_ptr + (i * plp1_size)
|
|
171
|
-
entry = HTS::LibHTS::BamPileup1.new(e_ptr)
|
|
172
|
-
alignments[i] = PileupRecord.new(entry, header_local)
|
|
173
|
-
i += 1
|
|
174
|
-
end
|
|
175
|
-
end
|
|
121
|
+
def each_entry_raw
|
|
122
|
+
return to_enum(__method__) unless block_given?
|
|
176
123
|
|
|
177
|
-
|
|
124
|
+
each_raw_column do |tid, pos, rows|
|
|
125
|
+
rows.each { |row| yield tid, pos, row[1], row[7], row[8], row[9] }
|
|
178
126
|
end
|
|
179
|
-
|
|
180
127
|
self
|
|
181
128
|
end
|
|
182
129
|
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
end
|
|
130
|
+
def each_base_counts(min_base_quality: 0, min_mapping_quality: 0)
|
|
131
|
+
return to_enum(__method__, min_base_quality:, min_mapping_quality:) unless block_given?
|
|
186
132
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
133
|
+
min_base_quality = Integer(min_base_quality)
|
|
134
|
+
min_mapping_quality = Integer(min_mapping_quality)
|
|
135
|
+
if min_base_quality.negative? || min_mapping_quality.negative?
|
|
136
|
+
raise ArgumentError,
|
|
137
|
+
"quality thresholds must be non-negative"
|
|
191
138
|
end
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
139
|
+
|
|
140
|
+
counts = Array.new(BASE_COUNT_FIELDS.length, 0)
|
|
141
|
+
each_raw_column do |tid, pos, rows|
|
|
142
|
+
counts.fill(0)
|
|
143
|
+
rows.each do |row|
|
|
144
|
+
next if row[6] || row[10] < min_mapping_quality
|
|
145
|
+
|
|
146
|
+
if row[3] || row[1].negative?
|
|
147
|
+
counts[8] += 1
|
|
148
|
+
else
|
|
149
|
+
next if row[9] < min_base_quality
|
|
150
|
+
|
|
151
|
+
counts[{ 1 => 1, 2 => 2, 4 => 3, 8 => 4 }.fetch(row[8], 5)] += 1
|
|
152
|
+
end
|
|
153
|
+
counts[0] += 1
|
|
154
|
+
counts[(row[7] & 16).zero? ? 6 : 7] += 1
|
|
155
|
+
counts[9] += 1 if row[2].positive?
|
|
156
|
+
end
|
|
157
|
+
yield tid, pos, counts
|
|
158
|
+
end
|
|
159
|
+
self
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def reset = @native.reset
|
|
163
|
+
def close = @native&.close
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def next_raw = @native.next
|
|
168
|
+
|
|
169
|
+
def each_raw_column
|
|
170
|
+
while (column = next_raw)
|
|
171
|
+
yield(*column)
|
|
195
172
|
end
|
|
196
|
-
# Keep @cb referenced by instance to avoid GC during iteration.
|
|
197
|
-
@cb
|
|
198
173
|
end
|
|
199
174
|
end
|
|
200
175
|
end
|