htslib 0.4.2 → 0.6.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 +83 -51
  3. data/TUTORIAL.md +11 -22
  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 +1167 -0
  8. data/ext/htslib_native/native_bcf.c +447 -0
  9. data/ext/htslib_native/native_faidx.c +164 -0
  10. data/ext/htslib_native/native_tabix.c +255 -0
  11. data/lib/hts/bam/auxi.rb +87 -342
  12. data/lib/hts/bam/base_mod.rb +38 -73
  13. data/lib/hts/bam/cigar.rb +9 -55
  14. data/lib/hts/bam/flag.rb +24 -30
  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 +154 -45
  20. data/lib/hts/bcf/errors.rb +4 -0
  21. data/lib/hts/bcf/format.rb +283 -377
  22. data/lib/hts/bcf/header.rb +40 -122
  23. data/lib/hts/bcf/header_record.rb +9 -35
  24. data/lib/hts/bcf/info.rb +73 -320
  25. data/lib/hts/bcf/record.rb +73 -101
  26. data/lib/hts/bcf.rb +178 -321
  27. data/lib/hts/faidx.rb +30 -87
  28. data/lib/hts/hts.rb +6 -94
  29. data/lib/hts/native.rb +13 -0
  30. data/lib/hts/tabix.rb +109 -51
  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
data/lib/hts/bam.rb CHANGED
@@ -17,6 +17,25 @@ module HTS
17
17
  class Bam
18
18
  include Enumerable
19
19
 
20
+ class ReadError < HTS::Error; end
21
+ class WriteError < HTS::Error; end
22
+ class OpenError < HTS::Error; end
23
+ class MissingIndexError < HTS::Error; end
24
+
25
+ # Filter an owning batch of records in one native pass when available.
26
+ def self.filter_records(records, required_flags: 0, excluded_flags: 0,
27
+ min_mapq: 0, tid: nil, beg: nil, end_: nil)
28
+ required_flags = Integer(required_flags)
29
+ excluded_flags = Integer(excluded_flags)
30
+ min_mapq = Integer(min_mapq)
31
+ Array(records).select do |record|
32
+ flags = record.flag_value
33
+ (flags & required_flags) == required_flags && (flags & excluded_flags).zero? &&
34
+ record.mapq >= min_mapq && (tid.nil? || record.tid == Integer(tid)) &&
35
+ (beg.nil? || record.endpos > Integer(beg)) && (end_.nil? || record.pos < Integer(end_))
36
+ end
37
+ end
38
+
20
39
  attr_reader :file_name, :index_name, :mode, :header, :nthreads
21
40
 
22
41
  def self.open(*args, **kw)
@@ -24,11 +43,11 @@ module HTS
24
43
  return file unless block_given?
25
44
 
26
45
  begin
27
- yield file
46
+ result = yield file
28
47
  ensure
29
48
  file.close
30
49
  end
31
- file
50
+ result
32
51
  end
33
52
 
34
53
  def self.build_index(file_name, index_name = nil, min_shift = 0, threads = 0, verbose = true)
@@ -40,7 +59,7 @@ module HTS
40
59
  end
41
60
  end
42
61
 
43
- case LibHTS.sam_index_build3(file_name, index_name, min_shift, threads)
62
+ case Native::BamFileHandle.build_index(file_name, index_name, min_shift, threads)
44
63
  when 0 # successful
45
64
  when -1 then raise "indexing failed"
46
65
  when -2 then raise "opening #{file_name} failed"
@@ -63,9 +82,8 @@ module HTS
63
82
  @index_name = index
64
83
  @mode = mode
65
84
  @nthreads = threads
66
- @hts_file = LibHTS.hts_open(@file_name, mode)
67
-
68
- raise Errno::ENOENT, "Failed to open #{@file_name}" if @hts_file.null?
85
+ @index_load_attempted = false
86
+ @native = Native::BamFileHandle.open(@file_name, mode)
69
87
 
70
88
  # Auto-detect and set reference for CRAM files
71
89
  if fai.nil? && @file_name.end_with?(".cram")
@@ -79,17 +97,25 @@ module HTS
79
97
  end
80
98
 
81
99
  if fai
82
- r = LibHTS.hts_set_fai_filename(@hts_file, fai)
100
+ r = @native.set_fai(fai)
83
101
  raise "Failed to load fasta index: #{fai}" if r < 0
84
102
  end
85
103
 
86
104
  set_threads(threads) if threads
87
105
 
88
- return if @mode[0] == "w"
106
+ if writing?
107
+ @auto_index_on_close = build_index
108
+ @index_name_on_close = index
109
+ return
110
+ end
89
111
 
90
- @header = Bam::Header.new(@hts_file)
91
- build_index(index) if build_index
92
- @idx = load_index(index)
112
+ @header = Bam::Header.new(@native.read_header)
113
+ if build_index
114
+ build_index(index)
115
+ load_index(index)
116
+ elsif index
117
+ load_index(index)
118
+ end
93
119
  @start_position = tell
94
120
  end
95
121
 
@@ -97,36 +123,87 @@ module HTS
97
123
  check_closed
98
124
 
99
125
  self.class.build_index(@file_name, index_name, min_shift, @nthreads || 0, verbose)
126
+ @index_name = index_name
127
+ @index_load_attempted = false
100
128
  self # for method chaining
101
129
  end
102
130
 
103
131
  def load_index(index_name = nil)
132
+ return self if try_load_index(index_name)
133
+
134
+ raise MissingIndexError, "Failed to load index #{index_name || "for #{@file_name}"}"
135
+ end
136
+
137
+ def try_load_index(index_name = nil)
104
138
  check_closed
105
139
 
106
- if index_name
107
- LibHTS.sam_index_load2(@hts_file, @file_name, index_name)
108
- else
109
- LibHTS.sam_index_load3(@hts_file, @file_name, nil, 2) # should be 3 ? (copy remote file to local?)
110
- end
140
+ @index_name = index_name
141
+ @index_load_attempted = true
142
+ @native.load_index(index_name)
111
143
  end
112
144
 
113
145
  def index_loaded?
114
146
  check_closed
115
147
 
116
- !@idx.null?
148
+ @native.index_loaded?
117
149
  end
118
150
 
119
151
  def close
120
- LibHTS.hts_idx_destroy(@idx) if @idx && !@idx.null?
121
- @idx = nil
122
- super
152
+ was_closed = closed?
153
+ result = @native&.close
154
+ raise WriteError, "Failed to close #{@file_name}: buffered output may be incomplete" if writing? && result&.negative?
155
+
156
+ if writing? && @auto_index_on_close && !was_closed
157
+ @auto_index_on_close = false
158
+ self.class.build_index(@file_name, @index_name_on_close, 0, @nthreads || 0, false)
159
+ end
160
+
161
+ nil
162
+ end
163
+
164
+ def closed? = @native.nil? || @native.closed?
165
+ def file_format = @native.file_format
166
+ def file_format_version = @native.file_format_version
167
+
168
+ def set_threads(n = nil)
169
+ if n.nil?
170
+ require "etc"
171
+ n = [Etc.nprocessors - 1, 1].max
172
+ end
173
+ raise TypeError unless n.is_a?(Integer)
174
+ raise ArgumentError, "Number of threads must be positive" if n < 1
175
+ raise "Failed to set number of threads: #{n}" if @native.set_threads(n).negative?
176
+
177
+ @nthreads = n
178
+ self
179
+ end
180
+
181
+ def seek(offset) = @native.seek(offset)
182
+ def tell = @native.tell
183
+
184
+ def rewind
185
+ raise "Cannot rewind: no start position" unless @start_position
186
+
187
+ result = seek(@start_position)
188
+ raise "Failed to rewind: #{result}" if result.negative?
189
+
190
+ tell
123
191
  end
124
192
 
193
+ private
194
+
195
+ def writing? = @mode&.start_with?("w", "a")
196
+
197
+ def native_handle = @native
198
+
199
+ public
200
+
125
201
  def write_header(header)
126
202
  check_closed
127
203
 
128
204
  @header = header.dup
129
- LibHTS.sam_hdr_write(@hts_file, header)
205
+ @native.write_header(header.__send__(:native_handle))
206
+ nil
130
207
  end
131
208
 
132
209
  def header=(header)
@@ -136,12 +213,15 @@ module HTS
136
213
  def write(record)
137
214
  check_closed
138
215
 
139
- r = LibHTS.sam_write1(@hts_file, header, record)
216
+ r = @native.write(header.__send__(:native_handle), record.__send__(:native_handle))
140
217
  raise "Failed to write record" if r < 0
218
+
219
+ nil
141
220
  end
142
221
 
143
222
  def <<(record)
144
223
  write(record)
224
+ self
145
225
  end
146
226
 
147
227
  # @!macro [attach] define_getter
@@ -172,6 +252,20 @@ module HTS
172
252
  seek(position) if position
173
253
  ary
174
254
  end
255
+ alias aux_array aux
256
+
257
+ # Materialize independent records from the current stream position.
258
+ # Unlike each.to_a, every element owns its bam1_t storage.
259
+ def collect_records
260
+ each(copy: true).to_a
261
+ end
262
+
263
+ # Materialize independent, owning records. Enumerable#to_a is unsafe for
264
+ # the default reused-record iterator because every array element would
265
+ # otherwise refer to the same native bam1_t buffer.
266
+ def to_a
267
+ collect_records
268
+ end
175
269
 
176
270
  # @!macro [attach] define_iterator
177
271
  # @method each_$1
@@ -236,7 +330,7 @@ module HTS
236
330
  # bam.query(["chr1:100-200", "chr2:500-600"]) { |r| puts r.qname }
237
331
  def query(region, beg = nil, end_ = nil, copy: false, &block)
238
332
  check_closed
239
- raise "Index file is required to call the query method." unless index_loaded?
333
+ raise "Index file is required to call the query method." unless ensure_index_loaded
240
334
 
241
335
  case region
242
336
  when Array
@@ -280,6 +374,13 @@ module HTS
280
374
 
281
375
  private
282
376
 
377
+ def ensure_index_loaded
378
+ return true if index_loaded?
379
+ return false if @index_load_attempted
380
+
381
+ load_index(@index_name)
382
+ end
383
+
283
384
  def queryi(tid, beg, end_, copy: false, &block)
284
385
  if copy
285
386
  queryi_copy(tid, beg, end_, &block)
@@ -313,9 +414,14 @@ module HTS
313
414
  # This is the common behavior of IO objects in Ruby.
314
415
  return to_enum(__method__) unless block_given?
315
416
 
316
- bam1 = LibHTS.bam_init1
317
- record = Record.new(header, bam1)
318
- yield record while LibHTS.sam_read1(@hts_file, header, bam1) != -1
417
+ record = Record.new(header)
418
+ loop do
419
+ result = @native.read(header.__send__(:native_handle), record.__send__(:native_handle))
420
+ break if result == -1
421
+ raise ReadError, "Failed to read BAM/SAM record (HTSlib error #{result})" if result < -1
422
+
423
+ yield record
424
+ end
319
425
  self
320
426
  end
321
427
 
@@ -324,17 +430,22 @@ module HTS
324
430
  check_closed
325
431
  return to_enum(__method__) unless block_given?
326
432
 
327
- bam1 = LibHTS.bam_init1
328
- record = Record.new(header, bam1)
329
- yield record.dup while LibHTS.sam_read1(@hts_file, header, bam1) != -1
433
+ record = Record.new(header)
434
+ loop do
435
+ result = @native.read(header.__send__(:native_handle), record.__send__(:native_handle))
436
+ break if result == -1
437
+ raise ReadError, "Failed to read BAM/SAM record (HTSlib error #{result})" if result < -1
438
+
439
+ yield record.dup
440
+ end
330
441
  self
331
442
  end
332
443
 
333
444
  def queryi_reuse(tid, beg, end_, &block)
334
445
  return to_enum(__method__, tid, beg, end_) unless block_given?
335
446
 
336
- qiter = LibHTS.sam_itr_queryi(@idx, tid, beg, end_)
337
- raise "Failed to query region: #{tid} #{beg} #{end_}" if qiter.null?
447
+ qiter = @native.query_interval(tid, beg, end_)
448
+ raise "Failed to query region: #{tid} #{beg} #{end_}" unless qiter
338
449
 
339
450
  query_reuse_yield(qiter, &block)
340
451
  self
@@ -343,8 +454,8 @@ module HTS
343
454
  def queryi_copy(tid, beg, end_, &block)
344
455
  return to_enum(__method__, tid, beg, end_) unless block_given?
345
456
 
346
- qiter = LibHTS.sam_itr_queryi(@idx, tid, beg, end_)
347
- raise "Failed to query region: #{tid} #{beg} #{end_}" if qiter.null?
457
+ qiter = @native.query_interval(tid, beg, end_)
458
+ raise "Failed to query region: #{tid} #{beg} #{end_}" unless qiter
348
459
 
349
460
  query_copy(qiter, &block)
350
461
  self
@@ -353,8 +464,8 @@ module HTS
353
464
  def querys_reuse(region, &block)
354
465
  return to_enum(__method__, region) unless block_given?
355
466
 
356
- qiter = LibHTS.sam_itr_querys(@idx, header, region)
357
- raise "Failed to query region: #{region}" if qiter.null?
467
+ qiter = @native.query_region(header.__send__(:native_handle), region)
468
+ raise "Failed to query region: #{region}" unless qiter
358
469
 
359
470
  query_reuse_yield(qiter, &block)
360
471
  self
@@ -363,8 +474,8 @@ module HTS
363
474
  def querys_copy(region, &block)
364
475
  return to_enum(__method__, region) unless block_given?
365
476
 
366
- qiter = LibHTS.sam_itr_querys(@idx, header, region)
367
- raise "Failed to query region: #{region}" if qiter.null?
477
+ qiter = @native.query_region(header.__send__(:native_handle), region)
478
+ raise "Failed to query region: #{region}" unless qiter
368
479
 
369
480
  query_copy(qiter, &block)
370
481
  self
@@ -372,30 +483,28 @@ module HTS
372
483
 
373
484
  # Internal: reused-Record iterator over a query iterator.
374
485
  def query_reuse_yield(qiter)
375
- bam1 = LibHTS.bam_init1
376
- record = Record.new(header, bam1)
486
+ record = Record.new(header)
377
487
  begin
378
- while (slen = LibHTS.sam_itr_next(@hts_file, qiter, bam1)) >= 0
488
+ while (slen = qiter.next(record.__send__(:native_handle))) >= 0
379
489
  yield record
380
490
  end
381
491
  raise if slen < -1
382
492
  ensure
383
- LibHTS.hts_itr_destroy(qiter)
493
+ qiter.close
384
494
  end
385
495
  end
386
496
 
387
497
  def query_copy(qiter)
388
- bam1 = LibHTS.bam_init1
389
- record = Record.new(header, bam1)
498
+ record = Record.new(header)
390
499
  loop do
391
- slen = LibHTS.sam_itr_next(@hts_file, qiter, bam1)
500
+ slen = qiter.next(record.__send__(:native_handle))
392
501
  break if slen == -1
393
502
  raise if slen < -1
394
503
 
395
504
  yield record.dup
396
505
  end
397
506
  ensure
398
- LibHTS.hts_itr_destroy(qiter)
507
+ qiter.close
399
508
  end
400
509
 
401
510
  # Multi-region query using sequential single-region queries
@@ -5,14 +5,17 @@ module HTS
5
5
  class Error < HTS::Error; end
6
6
 
7
7
  class OpenError < Error; end
8
+ class WriteError < Error; end
8
9
  class IndexError < Error; end
9
10
  class MissingIndexError < IndexError; end
10
11
  class QueryError < Error; end
12
+ class RecordError < Error; end
11
13
  class HeaderError < Error; end
12
14
  class SubsetError < HeaderError; end
13
15
  class UnknownSampleError < SubsetError; end
14
16
  class FieldError < Error; end
15
17
  class InfoError < FieldError; end
18
+ class InfoDefinitionError < InfoError; end
16
19
  class InfoTypeError < InfoError; end
17
20
  class InfoReadError < InfoError; end
18
21
  class InfoUpdateError < InfoError; end
@@ -21,6 +24,7 @@ module HTS
21
24
  class FormatDefinitionError < FormatError; end
22
25
  class FormatTypeError < FormatError; end
23
26
  class FormatReadError < FormatError; end
27
+ class InvalidBorrowedViewError < FormatError; end
24
28
  class FormatUpdateError < FormatError; end
25
29
  class UnsupportedFormatOperationError < FormatError; end
26
30
  end