minibwa 0.0.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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +119 -0
  4. data/ext/minibwa/extconf.rb +105 -0
  5. data/ext/minibwa/mb_buffer.c +113 -0
  6. data/ext/minibwa/mb_hit.c +192 -0
  7. data/ext/minibwa/mb_index.c +462 -0
  8. data/ext/minibwa/mb_index_build.c +174 -0
  9. data/ext/minibwa/mb_options.c +301 -0
  10. data/ext/minibwa/minibwa/LICENSE.txt +37 -0
  11. data/ext/minibwa/minibwa/align.c +930 -0
  12. data/ext/minibwa/minibwa/bseq.h +45 -0
  13. data/ext/minibwa/minibwa/bwt.c +715 -0
  14. data/ext/minibwa/minibwa/bwt.h +86 -0
  15. data/ext/minibwa/minibwa/cs.c +161 -0
  16. data/ext/minibwa/minibwa/format.c +356 -0
  17. data/ext/minibwa/minibwa/index.c +342 -0
  18. data/ext/minibwa/minibwa/kalloc.c +224 -0
  19. data/ext/minibwa/minibwa/kalloc.h +54 -0
  20. data/ext/minibwa/minibwa/ketopt.h +123 -0
  21. data/ext/minibwa/minibwa/kommon.c +374 -0
  22. data/ext/minibwa/minibwa/kommon.h +85 -0
  23. data/ext/minibwa/minibwa/kseq.h +256 -0
  24. data/ext/minibwa/minibwa/ksort.h +163 -0
  25. data/ext/minibwa/minibwa/ksw2.h +220 -0
  26. data/ext/minibwa/minibwa/ksw2_extd2_sse.c +403 -0
  27. data/ext/minibwa/minibwa/ksw2_extz2_sse.c +296 -0
  28. data/ext/minibwa/minibwa/ksw2_ll_sse.c +341 -0
  29. data/ext/minibwa/minibwa/kthread.h +15 -0
  30. data/ext/minibwa/minibwa/l2bit.c +479 -0
  31. data/ext/minibwa/minibwa/l2bit.h +72 -0
  32. data/ext/minibwa/minibwa/lchain.c +231 -0
  33. data/ext/minibwa/minibwa/libsais.c +6985 -0
  34. data/ext/minibwa/minibwa/libsais.h +106 -0
  35. data/ext/minibwa/minibwa/libsais64.c +7064 -0
  36. data/ext/minibwa/minibwa/libsais64.h +81 -0
  37. data/ext/minibwa/minibwa/map-algo.c +769 -0
  38. data/ext/minibwa/minibwa/mbpriv.h +148 -0
  39. data/ext/minibwa/minibwa/minibwa.h +176 -0
  40. data/ext/minibwa/minibwa/options.c +116 -0
  41. data/ext/minibwa/minibwa/pe.c +559 -0
  42. data/ext/minibwa/minibwa/s2n-lite.h +59 -0
  43. data/ext/minibwa/minibwa/seed.c +354 -0
  44. data/ext/minibwa/minibwa.c +67 -0
  45. data/ext/minibwa/minibwa.h +51 -0
  46. data/lib/minibwa/hit.rb +110 -0
  47. data/lib/minibwa/index.rb +77 -0
  48. data/lib/minibwa/options.rb +235 -0
  49. data/lib/minibwa/sam.rb +85 -0
  50. data/lib/minibwa/version.rb +6 -0
  51. data/lib/minibwa.rb +11 -0
  52. metadata +88 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5d54a100aaa8439c2dc2fb9b686ca60f4898ea0873370f3dc5b58bfd5af1b7fa
4
+ data.tar.gz: 17911037009111f945b2f2621ccdc1e6b51928748585cc3753d6341250cca51e
5
+ SHA512:
6
+ metadata.gz: 0e3a98d1f7e2652f16d3537bbbe7a2e3b89ff05dad6b5f1c166db505a465cdb1f854bbd2970bca0c1c0152f8f5fa4bc10bf7ef4a716a4c9001663bf35ec77743
7
+ data.tar.gz: 0c99b08294dcaaff5f896da4516c4c52df07cb74e879e58c7eea40f795235ac95b92535e7a86dec93cd900ee6257d4ea4e5e42b199fe4705fe1faa851d0cdb39
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 kojix2
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # ruby-minibwa
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/minibwa?color=brightgreen)](https://rubygems.org/gems/minibwa)
4
+ [![Test](https://github.com/kojix2/ruby-minibwa/actions/workflows/test.yml/badge.svg)](https://github.com/kojix2/ruby-minibwa/actions/workflows/test.yml)
5
+ [![Lines of Code](https://img.shields.io/endpoint?url=https%3A%2F%2Ftokei.kojix2.net%2Fbadge%2Fgithub%2Fkojix2%2Fruby-minibwa%2Flines)](https://tokei.kojix2.net/github/kojix2/ruby-minibwa)
6
+
7
+ Ruby bindings for [minibwa](https://github.com/lh3/minibwa), a short-read
8
+ aligner combining bwa-mem seeding with minimap2 chaining and alignment.
9
+
10
+ ## Installation
11
+
12
+ Requires a C compiler, zlib, and Ruby >= 3.2.
13
+
14
+ ```sh
15
+ git clone --recursive https://github.com/kojix2/ruby-minibwa
16
+ cd ruby-minibwa
17
+ bundle install
18
+ bundle exec rake compile
19
+ ```
20
+
21
+ The extension builds the vendored minibwa sources, including libsais for
22
+ index construction. Expect the first build to take a few minutes.
23
+
24
+ ## Usage
25
+
26
+ Build an index:
27
+
28
+ ```ruby
29
+ require "minibwa"
30
+
31
+ Minibwa::Index.build("ref.fa", "ref", n_thread: 4)
32
+ ```
33
+
34
+ This writes `ref.l2b` and `ref.mbw`. Indexes made by the `minibwa index`
35
+ command are also usable.
36
+
37
+ Map reads:
38
+
39
+ ```ruby
40
+ opt = Minibwa::Options.new
41
+ opt.preset("sr") # or "adap", "lr"
42
+
43
+ idx = Minibwa::Index.load("ref")
44
+
45
+ hits = idx.map("ACGT...", name: "read1", opt: opt)
46
+ hits.each do |h|
47
+ puts [h.ctg, h.ts, h.te, h.strand, h.qs, h.qe, h.mapq, h.cigar_str].join("\t")
48
+ end
49
+ ```
50
+
51
+ Map many reads in one call:
52
+
53
+ ```ruby
54
+ seqs = ["ACGT...", "TGCA..."]
55
+ names = ["read1", "read2"]
56
+ hits = idx.map_batch(seqs, names: names, opt: opt)
57
+ # => [[Hit, ...], [Hit, ...]]
58
+ ```
59
+
60
+ Use a reusable buffer for repeated mapping:
61
+
62
+ ```ruby
63
+ buf = Minibwa::Buffer.new
64
+ idx.map(seq, buf: buf)
65
+ ```
66
+
67
+ Generate SAM output:
68
+
69
+ ```ruby
70
+ puts Minibwa::Sam.header(idx)
71
+ hits.each do |h|
72
+ puts Minibwa::Sam.format("read1", 0, h, seq)
73
+ end
74
+ ```
75
+
76
+ **Note on reverse-strand alignments:** For hits where `hit.rev` is true,
77
+ the SAM spec requires SEQ to be the reverse complement of the original
78
+ query and the FLAG to have 0x10 set. `Sam.format` does not do this
79
+ automatically — pass the already-oriented sequence and the correct flag.
80
+ The library does not retain the original query after mapping.
81
+
82
+ With `pe: true`, `map_batch` treats consecutive sequences as read pairs.
83
+
84
+ ### CIGAR
85
+
86
+ `Hit#cigar` and `#cigar_str` cover the aligned region only and contain no
87
+ clipping, which is how minibwa reports alignments. `#full_cigar_str`
88
+ takes the query length and adds soft clips derived from `qs` and `qe`.
89
+
90
+ ### Threads
91
+
92
+ `Options#n_thread` affects only `Index.build` (via libsais + OpenMP).
93
+ Mapping itself is single-threaded per call, but releases the GVL, so Ruby
94
+ threads run in parallel. To map from several Ruby threads, give each one
95
+ its own buffer:
96
+
97
+ ```ruby
98
+ buf = Minibwa::Buffer.new
99
+ idx.map(seq, opt: opt, buf: buf)
100
+ ```
101
+
102
+ A `Buffer` holds mutable scratch state and must not be shared between
103
+ threads mapping at the same time.
104
+
105
+ ## Development
106
+
107
+ ```sh
108
+ bundle exec rake compile
109
+ bundle exec rake test
110
+ ```
111
+
112
+ `bin/console` gives an IRB session with the extension loaded.
113
+
114
+ ## License
115
+
116
+ MIT. The vendored minibwa is MIT as well; its GPL-licensed files
117
+ (`bwtgen.c`, `QSufSort.c`, providing the low-memory BWT construction
118
+ option) are neither compiled nor distributed. See
119
+ `ext/minibwa/minibwa/LICENSE.txt`.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds the binding together with the minibwa sources from the submodule.
4
+ #
5
+ # There is no shared libminibwa to link against, so the C library is compiled
6
+ # into the extension. Upstream's Makefile is the reference for which files and
7
+ # flags are needed; api-test/Makefile shows that the public mapping API needs
8
+ # only libminibwa.a (the LOBJS group) plus -lz -lm.
9
+
10
+ require 'mkmf'
11
+
12
+ upstream = File.join(__dir__, 'minibwa')
13
+
14
+ unless File.exist?(File.join(upstream, 'minibwa.h'))
15
+ abort 'minibwa sources not found. Run: git submodule update --init --recursive'
16
+ end
17
+
18
+ # --- source selection ------------------------------------------------------
19
+
20
+ # The LOBJS group of upstream's Makefile: everything behind the public API.
21
+ core = %w[
22
+ kommon kalloc bwt l2bit options seed map-algo lchain align pe cs format
23
+ ]
24
+
25
+ # SSE alignment kernels. On ARM these fall back to s2n-lite.h (NEON) on their
26
+ # own, so no extra flag is needed there.
27
+ ksw2 = %w[ksw2_extz2_sse ksw2_extd2_sse ksw2_ll_sse]
28
+
29
+ # Suffix array construction, needed only by Index.build.
30
+ sais = %w[libsais libsais64]
31
+
32
+ # Deliberately NOT compiled:
33
+ #
34
+ # index.c mb_index_build.c #includes it to reach the static
35
+ # mb_bwt_libsais(); compiling it too would define every
36
+ # symbol in it twice.
37
+ # bwtgen.c GPL, and the low-memory BWT construction they implement is
38
+ # QSufSort.c not offered by this gem. Keeping them out keeps it MIT.
39
+ # mimalloc/ replaces malloc process-wide, which is not something a Ruby
40
+ # extension may do to its host. See HAVE_KALLOC below.
41
+ # main.c the command line tool.
42
+ # fastmap.c
43
+ # map-main.c
44
+ # bseq.c FASTX reading and thread pool for the CLI; reading sequences
45
+ # kthread.c is the caller's job in Ruby.
46
+
47
+ binding_srcs = %w[minibwa mb_options mb_index mb_index_build mb_buffer mb_hit]
48
+
49
+ $srcs = binding_srcs.map { |f| "#{f}.c" } +
50
+ (core + ksw2 + sais).map { |f| "minibwa/#{f}.c" }
51
+ $objs = $srcs.map { |f| "#{File.basename(f, '.c')}.o" }
52
+ $VPATH << '$(srcdir)/minibwa'
53
+
54
+ # So that our own sources can say #include "minibwa/minibwa.h".
55
+ $INCFLAGS << ' -I$(srcdir)'
56
+
57
+ # --- libraries -------------------------------------------------------------
58
+
59
+ # l2bit.c reads FASTA through kseq/gzip.
60
+ abort 'zlib is required' unless have_header('zlib.h') && have_library('z', 'gzopen')
61
+ have_library('m', 'log')
62
+
63
+ # --- flags -----------------------------------------------------------------
64
+
65
+ # Required, and quietly consequential: without it mb_opt_init() sets
66
+ # MB_F_NO_KALLOC and every allocation goes to the system allocator. Upstream
67
+ # defines it whenever mimalloc is not linked, which for us is always. The
68
+ # build succeeds either way, so a missing -DHAVE_KALLOC shows up only as lost
69
+ # performance.
70
+ $defs << '-DHAVE_KALLOC'
71
+
72
+ # Keep optional CPU instructions local to the alignment kernels so the rest
73
+ # of a precompiled extension remains compatible with baseline x86_64 CPUs.
74
+ sse_cflags = if RbConfig::CONFIG['host_cpu'].match?(/x86_64|x64|amd64/i)
75
+ %w[-msse4.2 -mpopcnt].select { |flag| try_cflags(flag) }
76
+ else
77
+ []
78
+ end
79
+
80
+ # libsais parallelises index construction with OpenMP; Options#n_thread has no
81
+ # effect on Index.build without it. Everything else is single-threaded here,
82
+ # so this is optional. Disable with --disable-openmp.
83
+ if enable_config('openmp', true) && try_compile("#include <omp.h>\nint main(void){return 0;}", '-fopenmp')
84
+ $defs << '-DLIBSAIS_OPENMP'
85
+ append_cflags('-fopenmp')
86
+ append_ldflags('-fopenmp')
87
+ end
88
+
89
+ # Hide upstream's symbols. ksw2 and kalloc names are shared with minimap2 and
90
+ # other lh3 libraries, which may be loaded into the same process by another
91
+ # gem.
92
+ append_cflags('-fvisibility=hidden')
93
+
94
+ create_makefile('minibwa/minibwa')
95
+
96
+ unless sse_cflags.empty?
97
+ File.open('Makefile', 'a') do |makefile|
98
+ ksw2.each do |source|
99
+ makefile.puts
100
+ makefile.puts "#{source}.o: $(srcdir)/minibwa/#{source}.c"
101
+ makefile.puts "\t$(ECHO) compiling $<"
102
+ makefile.puts "\t$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) #{sse_cflags.join(' ')} $(COUTFLAG)$@ -c $(CSRCFLAG)$<"
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,113 @@
1
+ /*
2
+ * Document-class: Minibwa::Buffer
3
+ *
4
+ * Minibwa::Buffer -- wraps mb_tbuf_t, the reusable per-thread scratch buffer.
5
+ *
6
+ * Passing a Buffer to Index#map saves an mb_tbuf_init/mb_tbuf_destroy pair on
7
+ * every call; passing nil lets mb_map() allocate one internally. A Buffer
8
+ * carries mutable working state and must never be shared between threads that
9
+ * map concurrently -- one Buffer per thread.
10
+ *
11
+ * Freed with mb_tbuf_destroy().
12
+ */
13
+
14
+ #include "minibwa.h"
15
+
16
+ static void
17
+ rb_minibwa_buffer_free(void *ptr)
18
+ {
19
+ mb_tbuf_t *b = (mb_tbuf_t *)ptr;
20
+ if (b) mb_tbuf_destroy(b);
21
+ }
22
+
23
+ static size_t
24
+ rb_minibwa_buffer_memsize(const void *ptr)
25
+ {
26
+ /* mb_tbuf_t is an opaque type; report the pointer size only. */
27
+ (void)ptr;
28
+ return 0;
29
+ }
30
+
31
+ const rb_data_type_t rb_minibwa_buffer_type = {
32
+ .wrap_struct_name = "Minibwa::Buffer",
33
+ .function = {
34
+ NULL,
35
+ rb_minibwa_buffer_free,
36
+ rb_minibwa_buffer_memsize,
37
+ },
38
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY
39
+ };
40
+
41
+ /* ------------------------------------------------------------------ */
42
+ /* Constructor */
43
+ /* ------------------------------------------------------------------ */
44
+
45
+ /*
46
+ * call-seq:
47
+ * Buffer.new(no_kalloc: false) -> Buffer
48
+ *
49
+ * Allocates a new thread-local scratch buffer. Set +no_kalloc:+ to +true+
50
+ * to disable the internal allocator.
51
+ */
52
+ static VALUE
53
+ rb_minibwa_buffer_initialize(int argc, VALUE *argv, VALUE self)
54
+ {
55
+ VALUE kwargs;
56
+ rb_scan_args(argc, argv, ":", &kwargs);
57
+
58
+ int no_kalloc = 0;
59
+ if (!NIL_P(kwargs)) {
60
+ VALUE v = rb_hash_aref(kwargs, ID2SYM(rb_intern("no_kalloc")));
61
+ if (RTEST(v)) no_kalloc = 1;
62
+ }
63
+
64
+ /* The buffer was allocated by alloc with the default (kalloc enabled).
65
+ * If the caller wants no_kalloc, rebuild it through the public API. */
66
+ if (no_kalloc) {
67
+ mb_tbuf_t *b = rb_minibwa_get_tbuf(self);
68
+ if (b) mb_tbuf_destroy(b);
69
+ b = mb_tbuf_init(1);
70
+ if (!b) rb_raise(rb_eMinibwaError, "mb_tbuf_init failed");
71
+ DATA_PTR(self) = b;
72
+ }
73
+ return Qnil;
74
+ }
75
+
76
+ static VALUE
77
+ rb_minibwa_buffer_alloc(VALUE klass)
78
+ {
79
+ mb_tbuf_t *b = mb_tbuf_init(0);
80
+ VALUE obj = TypedData_Wrap_Struct(klass, &rb_minibwa_buffer_type, b);
81
+ return obj;
82
+ }
83
+
84
+ /*
85
+ * call-seq:
86
+ * reset!(max_block_size) -> Integer
87
+ *
88
+ * Resets the buffer for reuse, optionally setting a new maximum block size.
89
+ * Returns the new capacity.
90
+ */
91
+ static VALUE
92
+ rb_minibwa_buffer_reset(VALUE self, VALUE max_block_size)
93
+ {
94
+ mb_tbuf_t *b = rb_minibwa_get_tbuf(self);
95
+ int64_t mbs = NUM2LL(max_block_size);
96
+ int32_t cap = mb_tbuf_reset(b, mbs);
97
+ return INT2NUM(cap);
98
+ }
99
+
100
+ /* ------------------------------------------------------------------ */
101
+ /* Registration */
102
+ /* ------------------------------------------------------------------ */
103
+
104
+ void
105
+ rb_minibwa_init_buffer(void)
106
+ {
107
+ VALUE klass = rb_define_class_under(rb_mMinibwa, "Buffer", rb_cObject);
108
+ rb_cMinibwaBuffer = klass;
109
+
110
+ rb_define_alloc_func(klass, rb_minibwa_buffer_alloc);
111
+ rb_define_method(klass, "initialize", rb_minibwa_buffer_initialize, -1);
112
+ rb_define_method(klass, "reset!", rb_minibwa_buffer_reset, 1);
113
+ }
@@ -0,0 +1,192 @@
1
+ /*
2
+ * Minibwa::Hit -- one alignment produced by mb_map() / mb_map_batch().
3
+ *
4
+ * mb_hit_t is converted eagerly into a plain Ruby object and the C memory is
5
+ * released immediately, because the caller owns both hit[i].p and the hit
6
+ * array. Copying up front costs one pass but removes every question about
7
+ * object lifetime, and keeps a Hit usable after its Index is gone.
8
+ *
9
+ * The CIGAR in mb_extra_t is unpacked into an array of [length, op] pairs.
10
+ * It describes the aligned region only and contains no soft or hard clipping;
11
+ * clips have to be reconstructed from qs/qe (see lib/minibwa/hit.rb).
12
+ */
13
+
14
+ #include "minibwa.h"
15
+
16
+ /* ------------------------------------------------------------------ */
17
+ /* Single hit conversion */
18
+ /* ------------------------------------------------------------------ */
19
+
20
+ /*
21
+ * Builds a Minibwa::Hit from an mb_hit_t. Takes ownership of hit->p
22
+ * (frees it). The idx is needed for ctg_name lookups.
23
+ */
24
+ VALUE
25
+ rb_minibwa_hit_new(const mb_idx_t *idx, mb_hit_t *hit)
26
+ {
27
+ VALUE klass = rb_cMinibwaHit;
28
+ VALUE obj = rb_obj_alloc(klass);
29
+
30
+ /* --- scalar fields --- */
31
+ rb_ivar_set(obj, rb_intern("@tid"), LL2NUM(hit->tid));
32
+ rb_ivar_set(obj, rb_intern("@ts"), LL2NUM(hit->ts));
33
+ rb_ivar_set(obj, rb_intern("@te"), LL2NUM(hit->te));
34
+ rb_ivar_set(obj, rb_intern("@qs"), INT2NUM(hit->qs));
35
+ rb_ivar_set(obj, rb_intern("@qe"), INT2NUM(hit->qe));
36
+ rb_ivar_set(obj, rb_intern("@score"), INT2NUM(hit->score));
37
+ rb_ivar_set(obj, rb_intern("@score0"), INT2NUM(hit->score0));
38
+ rb_ivar_set(obj, rb_intern("@mlen"), INT2NUM(hit->mlen));
39
+ rb_ivar_set(obj, rb_intern("@blen"), INT2NUM(hit->blen));
40
+ rb_ivar_set(obj, rb_intern("@mapq"), INT2NUM(hit->mapq));
41
+ rb_ivar_set(obj, rb_intern("@cnt"), INT2NUM(hit->cnt));
42
+ rb_ivar_set(obj, rb_intern("@n_sub"), INT2NUM(hit->n_sub));
43
+ rb_ivar_set(obj, rb_intern("@subsc"), INT2NUM(hit->subsc));
44
+ rb_ivar_set(obj, rb_intern("@hash"), UINT2NUM(hit->hash));
45
+
46
+ /* --- bit fields --- */
47
+ rb_ivar_set(obj, rb_intern("@rev"), hit->rev ? Qtrue : Qfalse);
48
+ rb_ivar_set(obj, rb_intern("@proper_pair"), hit->proper_pair ? Qtrue : Qfalse);
49
+ rb_ivar_set(obj, rb_intern("@sam_pri"), hit->sam_pri ? Qtrue : Qfalse);
50
+ rb_ivar_set(obj, rb_intern("@flt"), hit->flt ? Qtrue : Qfalse);
51
+ rb_ivar_set(obj, rb_intern("@inv"), hit->inv ? Qtrue : Qfalse);
52
+ rb_ivar_set(obj, rb_intern("@split"), INT2NUM(hit->split));
53
+ rb_ivar_set(obj, rb_intern("@split_inv"), hit->split_inv ? Qtrue : Qfalse);
54
+ rb_ivar_set(obj, rb_intern("@rescued"), hit->rescued ? Qtrue : Qfalse);
55
+ rb_ivar_set(obj, rb_intern("@frac_high"), INT2NUM(hit->frac_high));
56
+ rb_ivar_set(obj, rb_intern("@seed_ratio"), INT2NUM(hit->seed_ratio));
57
+
58
+ /* --- contig name (lazy: store tid, resolve in Ruby) --- */
59
+ const char *ctg = mb_idx_ctg_name(idx, (int32_t)hit->tid);
60
+ rb_ivar_set(obj, rb_intern("@ctg"),
61
+ ctg ? rb_str_new_cstr(ctg) : Qnil);
62
+
63
+ /* --- extra (CIGAR, DP scores) --- */
64
+ if (hit->p) {
65
+ mb_extra_t *p = hit->p;
66
+
67
+ rb_ivar_set(obj, rb_intern("@dp_score"), INT2NUM(p->dp_score));
68
+ rb_ivar_set(obj, rb_intern("@dp_max0"), INT2NUM(p->dp_max0));
69
+ rb_ivar_set(obj, rb_intern("@dp_max"), INT2NUM(p->dp_max));
70
+ rb_ivar_set(obj, rb_intern("@dp_max2"), INT2NUM(p->dp_max2));
71
+ rb_ivar_set(obj, rb_intern("@n_ambi"), UINT2NUM(p->n_ambi));
72
+ rb_ivar_set(obj, rb_intern("@cs_flag"), p->cs ? Qtrue : Qfalse);
73
+
74
+ /* CIGAR: array of [length, op] pairs */
75
+ VALUE cigar = rb_ary_new_capa(p->n_cigar);
76
+ for (int32_t i = 0; i < p->n_cigar; i++) {
77
+ uint32_t c = p->cigar[i];
78
+ VALUE pair = rb_ary_new_from_args(2,
79
+ UINT2NUM(c >> 4),
80
+ INT2NUM(c & 0xf));
81
+ rb_ary_push(cigar, pair);
82
+ }
83
+ rb_ivar_set(obj, rb_intern("@cigar"), cigar);
84
+
85
+ free(p);
86
+ hit->p = NULL; /* mark as consumed for the ensure handler */
87
+ } else {
88
+ rb_ivar_set(obj, rb_intern("@dp_score"), INT2NUM(0));
89
+ rb_ivar_set(obj, rb_intern("@dp_max0"), INT2NUM(0));
90
+ rb_ivar_set(obj, rb_intern("@dp_max"), INT2NUM(0));
91
+ rb_ivar_set(obj, rb_intern("@dp_max2"), INT2NUM(0));
92
+ rb_ivar_set(obj, rb_intern("@n_ambi"), INT2NUM(0));
93
+ rb_ivar_set(obj, rb_intern("@cs_flag"), Qfalse);
94
+ rb_ivar_set(obj, rb_intern("@cigar"), rb_ary_new());
95
+ }
96
+
97
+ return obj;
98
+ }
99
+
100
+ /* ------------------------------------------------------------------ */
101
+ /* Hit array conversion */
102
+ /* ------------------------------------------------------------------ */
103
+
104
+ struct hit_ary_ctx {
105
+ const mb_idx_t *idx;
106
+ mb_hit_t *hits;
107
+ int32_t n_hit;
108
+ VALUE ary;
109
+ };
110
+
111
+ static VALUE
112
+ rb_minibwa_hit_ary_convert(VALUE ptr)
113
+ {
114
+ struct hit_ary_ctx *ctx = (struct hit_ary_ctx *)ptr;
115
+ ctx->ary = rb_ary_new_capa(ctx->n_hit);
116
+ for (int32_t i = 0; i < ctx->n_hit; i++) {
117
+ VALUE hit = rb_minibwa_hit_new(ctx->idx, &ctx->hits[i]);
118
+ rb_ary_push(ctx->ary, hit);
119
+ }
120
+ return ctx->ary;
121
+ }
122
+
123
+ static VALUE
124
+ rb_minibwa_hit_ary_free_hits(VALUE ptr)
125
+ {
126
+ struct hit_ary_ctx *ctx = (struct hit_ary_ctx *)ptr;
127
+ /* If conversion raised, free any remaining hit[i].p and the array.
128
+ * rb_minibwa_hit_new() sets hit->p to NULL after freeing, so already-
129
+ * converted hits are skipped. */
130
+ for (int32_t i = 0; i < ctx->n_hit; i++)
131
+ free(ctx->hits[i].p);
132
+ free(ctx->hits);
133
+ return Qnil;
134
+ }
135
+
136
+ /*
137
+ * Converts an array of mb_hit_t to a Ruby Array of Minibwa::Hit.
138
+ * Takes ownership of the hit array and all hit[i].p (frees them).
139
+ * If conversion raises, the array is still freed before re-raising.
140
+ */
141
+ VALUE
142
+ rb_minibwa_hit_ary_new(const mb_idx_t *idx, mb_hit_t *hits, int32_t n_hit)
143
+ {
144
+ struct hit_ary_ctx ctx = { idx, hits, n_hit, Qnil };
145
+ return rb_ensure(rb_minibwa_hit_ary_convert, (VALUE)&ctx,
146
+ rb_minibwa_hit_ary_free_hits, (VALUE)&ctx);
147
+ }
148
+
149
+ /* ------------------------------------------------------------------ */
150
+ /* Registration */
151
+ /* ------------------------------------------------------------------ */
152
+
153
+ void
154
+ rb_minibwa_init_hit(void)
155
+ {
156
+ VALUE klass = rb_define_class_under(rb_mMinibwa, "Hit", rb_cObject);
157
+ rb_cMinibwaHit = klass;
158
+
159
+ /* attr_readers for all fields */
160
+ rb_define_attr(klass, "tid", 1, 0);
161
+ rb_define_attr(klass, "ts", 1, 0);
162
+ rb_define_attr(klass, "te", 1, 0);
163
+ rb_define_attr(klass, "qs", 1, 0);
164
+ rb_define_attr(klass, "qe", 1, 0);
165
+ rb_define_attr(klass, "score", 1, 0);
166
+ rb_define_attr(klass, "score0", 1, 0);
167
+ rb_define_attr(klass, "mlen", 1, 0);
168
+ rb_define_attr(klass, "blen", 1, 0);
169
+ rb_define_attr(klass, "mapq", 1, 0);
170
+ rb_define_attr(klass, "cnt", 1, 0);
171
+ rb_define_attr(klass, "n_sub", 1, 0);
172
+ rb_define_attr(klass, "subsc", 1, 0);
173
+ rb_define_attr(klass, "hash", 1, 0);
174
+ rb_define_attr(klass, "rev", 1, 0);
175
+ rb_define_attr(klass, "proper_pair", 1, 0);
176
+ rb_define_attr(klass, "sam_pri", 1, 0);
177
+ rb_define_attr(klass, "flt", 1, 0);
178
+ rb_define_attr(klass, "inv", 1, 0);
179
+ rb_define_attr(klass, "split", 1, 0);
180
+ rb_define_attr(klass, "split_inv", 1, 0);
181
+ rb_define_attr(klass, "rescued", 1, 0);
182
+ rb_define_attr(klass, "frac_high", 1, 0);
183
+ rb_define_attr(klass, "seed_ratio", 1, 0);
184
+ rb_define_attr(klass, "ctg", 1, 0);
185
+ rb_define_attr(klass, "dp_score", 1, 0);
186
+ rb_define_attr(klass, "dp_max0", 1, 0);
187
+ rb_define_attr(klass, "dp_max", 1, 0);
188
+ rb_define_attr(klass, "dp_max2", 1, 0);
189
+ rb_define_attr(klass, "n_ambi", 1, 0);
190
+ rb_define_attr(klass, "cs_flag", 1, 0);
191
+ rb_define_attr(klass, "cigar", 1, 0);
192
+ }