nx_align_chain_stranded.nx source
↩ module page · 162 lines · 6822 B
1// nx_align_chain_stranded.nx -- strand-partitioned co-linear chain DP.
2//
3// license_tier: INDEPENDENT_REDERIVE
4// genealogy_id: international-research-sources/li-2018-minimap2-section-2.2-strand-aware
5//
6// G1.3d of NISHI_GENOMICS_SUBSTRATE_ROADMAP.md. Closes the
7// strand-aware-pipeline gap: nx_align_chain (G1.3) is strand-blind
8// and would happily chain forward-strand seeds together with
9// reverse-strand seeds, producing geometrically incoherent
10// alignments. This primitive partitions seeds by strand tag,
11// runs the strand-blind chain DP on each partition, and returns
12// the LONGER of the two chains plus its strand orientation.
13//
14// Why partition not unified-DP:
15// Forward and reverse alignments live in DIFFERENT coordinate
16// systems on the query side (the reverse case uses revcomp-query
17// coordinates). A single DP that mixed them would chain across
18// strand boundaries and produce alignments that don't correspond
19// to any real read placement. Partition + per-strand DP is the
20// simplest correct primitive.
21//
22// Input contract:
23// Caller provides pre-sorted (by r_pos asc) seed-pair arrays
24// that come ALREADY PARTITIONED into a forward block then a
25// reverse block, with the partition boundary count given by
26// n_fwd (so seeds [0..n_fwd) are forward, [n_fwd..n_total) are
27// reverse). This matches the output layout of
28// nx_align_match_stranded after a per-block nx_seed_sort.
29//
30// Tie-break:
31// When forward and reverse produce equal-length chains, the
32// FORWARD chain wins. Deterministic and matches the "default
33// to + strand on tie" convention used by minimap2.
34//
35// API:
36// chain_pick_best_strand(
37// q_arr, r_arr, n_fwd, n_total,
38// out_chain_idx, out_strand,
39// max_out
40// ) -> i64 length of winning chain (>= 0); -1 on bad input
41//
42// - q_arr / r_arr : combined fwd+rev seed arrays, contiguous,
43// each partition pre-sorted by r ascending
44// - n_fwd : count of forward seeds at the head
45// - n_total : total seed count (so rev count = n_total - n_fwd)
46// - out_chain_idx : indices INTO the combined arrays (not into
47// per-partition arrays); for the rev winner
48// these indices are >= n_fwd
49// - out_strand : single i64 written; NX_STRAND_FWD or
50// NX_STRAND_REV, or NX_STRAND_UNKNOWN if both
51// partitions are empty
52//
53// What G1.3d does NOT do (deferred):
54// - Affine-cost chain (minimap2 alpha + beta gap penalty) -- G1.5
55// - Secondary chain reporting (both strands when both meaningful) -- G1.4c
56// - Chain joining across small inversions -- G1.7
57//
58// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml)
59// intended_use: "Best-of-both-strands chain selection;
60// the orchestration primitive that turns a
61// strand-tagged seed stream into a single
62// coherent alignment hypothesis"
63// sil_target: SIL2
64// asil_target: QM
65// dal_target: DAL C
66// iec_62304_class: B
67// evidence: [no_floating_point, deterministic,
68// bit_equal_reproducible,
69// composes_nx_align_chain_KAT,
70// fwd_wins_tie_stable,
71// partition_boundary_explicit_no_silent_mix,
72// license_tier_INDEPENDENT_REDERIVE]
73// hazard_register: [bug-tape-chain-stranded-mixes-strands-silently,
74// bug-tape-rev-indices-not-offset-by-n_fwd,
75// bug-tape-tie-break-reversed-from-spec,
76// bug-tape-empty-partition-junk-out_strand]
77// residual_risk: "Discards the runner-up strand entirely;
78// for reads with similar fwd + rev evidence
79// (chimeras / inverted repeats) downstream
80// consumers should request both via the
81// G1.4c multi-chain primitive."
82// verdict: NOT_YET_EVALUATED
83
84import "nx_syscalls.nx"
85import "nx_const.nx"
86import "nx_align_chain.nx"
87
88// Pick the longer co-linear chain across forward and reverse partitions.
89// Returns winning chain length (>=0); writes indices INTO the combined
90// q_arr/r_arr to out_chain_idx and winning strand to out_strand[0].
91func chain_pick_best_strand(q_arr: *i64, r_arr: *i64,
92 n_fwd: i64, n_total: i64,
93 out_chain_idx: *i64,
94 out_strand: *i64,
95 max_out: i64) -> i64 {
96 if n_total < 0 { return -1 }
97 if n_fwd < 0 { return -1 }
98 if n_fwd > n_total { return -1 }
99 if max_out <= 0 { return -1 }
100
101 let n_rev: i64 = n_total - n_fwd
102
103 // Scratch buffers for each partition's chain output. Allocate
104 // for the worst case = full partition length.
105 let fwd_buf: *i64 = sys_mmap((n_fwd + 8) * 8) as *i64
106 let rev_buf: *i64 = sys_mmap((n_rev + 8) * 8) as *i64
107
108 // Run chain DP on the forward partition (q_arr+0 .. n_fwd).
109 var fwd_len: i64 = 0
110 if n_fwd > 0 {
111 fwd_len = seed_chain(q_arr, r_arr, n_fwd, fwd_buf, n_fwd + 8)
112 if fwd_len < 0 { return -1 }
113 }
114
115 // Run chain DP on the reverse partition. Pointer arithmetic via
116 // cast-int-add-cast (pattern from nx_arith.nx) to get the offset
117 // sub-array pointer; rev seeds live at q_arr[n_fwd..n_total).
118 var rev_len: i64 = 0
119 if n_rev > 0 {
120 let off_bytes: i64 = n_fwd * 8
121 let rev_q: *i64 = ((q_arr as i64) + off_bytes) as *i64
122 let rev_r: *i64 = ((r_arr as i64) + off_bytes) as *i64
123 rev_len = seed_chain(rev_q, rev_r, n_rev, rev_buf, n_rev + 8)
124 if rev_len < 0 { return -1 }
125 }
126
127 // Both partitions empty -> no alignment.
128 if fwd_len == 0 {
129 if rev_len == 0 {
130 out_strand[0] = NX_STRAND_UNKNOWN
131 return 0
132 }
133 }
134
135 // Tie-break: forward wins on equal length.
136 var pick_strand: i64 = NX_STRAND_FWD
137 var pick_len: i64 = fwd_len
138 if rev_len > fwd_len {
139 pick_strand = NX_STRAND_REV
140 pick_len = rev_len
141 }
142
143 if pick_len > max_out { return -1 }
144
145 // Copy winning chain into out_chain_idx. For reverse winner,
146 // offset indices by n_fwd so they index into the combined array.
147 var i: i64 = 0
148 if pick_strand == NX_STRAND_FWD {
149 while i < pick_len {
150 out_chain_idx[i] = fwd_buf[i]
151 i = i + 1
152 }
153 } else {
154 while i < pick_len {
155 out_chain_idx[i] = rev_buf[i] + n_fwd
156 i = i + 1
157 }
158 }
159
160 out_strand[0] = pick_strand
161 return pick_len
162}