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
@@ -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
- # Usage:
10
- # HTS::Bam::Mpileup.open([bam1, bam2], region: "chr1:1-100") do |mpl|
11
- # mpl.each { |cols| ... }
12
- # end
13
- def self.open(*args, **kw)
14
- m = new(*args, **kw)
15
- return m unless block_given?
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 m
63
+ yield mpileup
19
64
  ensure
20
- m.close
65
+ mpileup.close
21
66
  end
22
- m
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 = [] # Bams we opened here; will be closed on close
31
- @bams = inputs.map do |x|
32
- case x
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
- b = HTS::Bam.open(x)
37
- @owned_bams << b
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
- n = @bams.length
45
- @iters = []
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
- # Build per-input packed pointer blocks so C passes them back to the callback.
70
- # Keep the Ruby FFI structs in @data_entries to avoid rebuilding wrappers
71
- # in the per-record callback.
72
- ptr_size = FFI.type_size(:pointer)
73
- data_array = FFI::MemoryPointer.new(:pointer, n)
74
- @bams.each_with_index do |bam, i|
75
- hts_fp = bam.instance_variable_get(:@hts_file)
76
- hdr_struct = bam.header.struct
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
- @iter = HTS::LibHTS.bam_mplp_init(n, @cb, @data_array)
100
- raise "bam_mplp_init failed" if @iter.null?
106
+ def each_depth
107
+ return to_enum(__method__) unless block_given?
101
108
 
102
- HTS::LibHTS.bam_mplp_set_maxcnt(@iter, maxcnt) if maxcnt
103
- return unless overlaps
109
+ view = DepthView.new
110
+ each_column_raw { |tid, pos, depths, _, _| yield tid, pos, view.reset(depths) }
111
+ self
112
+ end
104
113
 
105
- rc = HTS::LibHTS.bam_mplp_init_overlaps(@iter)
106
- raise "bam_mplp_init_overlaps failed" if rc < 0
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
- # Yields an array of Pileup::PileupColumn (one per input) for each position
110
- def each
122
+ def each_entry_raw
111
123
  return to_enum(__method__) unless block_given?
112
124
 
113
- n = @bams.length
114
- tid_ptr = FFI::MemoryPointer.new(:int)
115
- pos_ptr = FFI::MemoryPointer.new(:long_long)
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 close
156
- if @iter && !@iter.null?
157
- HTS::LibHTS.bam_mplp_destroy(@iter)
158
- @iter = FFI::Pointer::NULL
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
- @iters.each do |itr|
161
- HTS::LibHTS.hts_itr_destroy(itr) if itr && !itr.null?
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
- @iters.clear
164
- # Keep references to callback and data blocks to prevent GC
165
- @_keepalive = [@cb, @data_array, @data_entries, *@data_blocks]
166
- # Close owned bams opened by this object
167
- @owned_bams.each do |b|
168
- b.close
169
- rescue StandardError
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
@@ -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
- # Usage:
10
- # HTS::Bam::Pileup.open(bam, region: "chr1:1-100") do |pl|
11
- # pl.each { |col| ... }
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 pu
16
+ yield pileup
19
17
  ensure
20
- pu.close
18
+ pileup.close
21
19
  end
22
- pu
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(entry, header)
35
- @entry = entry
28
+ def initialize(values, header)
29
+ @values = values
36
30
  @header = header
37
- @record = nil
38
31
  end
39
32
 
40
- # Return Bam::Record. On the first call, duplicate the underlying bam1_t (bam_dup1)
41
- # so the record becomes safe to keep beyond the current pileup step. Subsequent calls
42
- # return the cached Bam::Record instance.
43
- # NOTE: Without duplication, bam1_t memory may be reused by HTSlib on the next step.
44
- def record
45
- return @record if @record
46
-
47
- # Normalize to a raw pointer and duplicate to obtain owned memory.
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
- def query_position
57
- @entry[:qpos]
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
- def indel
61
- @entry[:indel]
62
- end
60
+ class BorrowedColumnView
61
+ include Enumerable
62
+ attr_reader :tid, :pos, :depth
63
63
 
64
- def del?
65
- @entry[:is_del] == 1
66
- end
64
+ def initialize = @entry_view = BorrowedEntryView.new
67
65
 
68
- def head?
69
- @entry[:is_head] == 1
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 tail?
73
- @entry[:is_tail] == 1
74
- end
74
+ def each
75
+ return to_enum(__method__) unless block_given?
75
76
 
76
- def refskip?
77
- @entry[:is_refskip] == 1
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
- @bam = bam
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
- @itr = nil
91
- @cb = nil
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
- # Build the auto callback for bam_plp_init (micro-optimized)
111
- # - Hoist ivar/constant lookups out of the callback to reduce per-call overhead.
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
- tid_ptr = FFI::MemoryPointer.new(:int)
139
- pos_ptr = FFI::MemoryPointer.new(:long_long) # hts_pos_t
140
- n_ptr = FFI::MemoryPointer.new(:int)
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
- # Micro-optimizations:
143
- # - Compute constant struct size once
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
- loop do
149
- base_ptr = HTS::LibHTS.bam_plp64_auto(@plp, tid_ptr, pos_ptr, n_ptr)
109
+ each_raw_column { |tid, pos, rows| yield tid, pos, rows.length }
110
+ self
111
+ end
150
112
 
151
- # When base_ptr is NULL, check n to distinguish EOF (n == 0) from error (n < 0)
152
- if base_ptr.null?
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
- break
157
- end
116
+ view = BorrowedColumnView.new
117
+ each_raw_column { |tid, pos, rows| yield view.reset(rows, tid, pos) }
118
+ self
119
+ end
158
120
 
159
- tid = tid_ptr.read_int
160
- pos = pos_ptr.read_long_long
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
- yield PileupColumn.new(tid: tid, pos: pos, alignments: alignments)
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 reset
184
- HTS::LibHTS.bam_plp_reset(@plp) if @plp && !@plp.null?
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
- def close
188
- if @plp && !@plp.null?
189
- HTS::LibHTS.bam_plp_destroy(@plp)
190
- @plp = FFI::Pointer::NULL
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
- if @itr && !@itr.null?
193
- HTS::LibHTS.hts_itr_destroy(@itr)
194
- @itr = FFI::Pointer::NULL
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