rammap Architecture Reference
May 12, 2026 · View on GitHub
rammap is a pure-Rust minimap2-compatible sequence aligner producing byte-identical output via the same algorithms. It supports all major minimap2 presets (map-ont, map-hifi, sr, splice, asm, ava) with full CIGAR, CS/MD/DS tag, SAM, and PAF output.
Standalone crate with both src/lib.rs (library) and src/main.rs (binary). The alignment engine lives under src/align/; FASTA/FASTQ I/O under src/fasta/.
Key dependencies: rayon (parallelism, optional), clap (CLI, optional), serde + bincode (index serialization), hashbrown (per-bucket hash tables), flate2 (gzip), wasm-bindgen + wasm-bindgen-rayon (WASM support, latter optional).
Build profile: opt-level=3, lto="fat", codegen-units=1, target-cpu=native via .cargo/config.toml.
Table of Contents
- Source Tree
- CLI Layer
- Alignment Pipeline Overview
- Module Reference: src/align/
- Key Data Structures
- Algorithm Details
- SIMD Architecture
- Performance Patterns
- I/O Layer
- Testing
- Preset Reference
- Memory Safety
- Porting Lessons
1. Source Tree
src/
main.rs # Binary entry: CLI parse + alignment orchestration
lib.rs # Library exports: align, fasta, api
api.rs # Programmatic API entry points
align/ # Alignment engine
mod.rs # Module declarations
sketch.rs # Minimizer generation (rolling hash, windowing)
sort.rs # MSD radix sort (parallel two-level for index build)
index.rs # Reference index: build (Index::build, IndexBuilder), save, load
index_bucket.rs # Per-bucket hash table backend
seed.rs # Seed collection: index lookup, occurrence filtering, heap mode
map.rs # Mapping orchestration: MapOptions, MapContext, map_query()
chain.rs # Standard DP chaining (O(n^2) with skip limits)
chain_simd.rs # SIMD chaining: AVX2 (8-wide) + NEON (4-wide) + WASM SIMD128
chain_rmq.rs # RMQ-accelerated chaining (arena treap, O(log n))
chain_simple.rs # Greedy chain scoring (simple/fast)
filter.rs # Parent assignment + secondary filtering
extend.rs # Anchor extension, gap-fill, CIGAR generation, CS/MD/DS formatters
dp/ # SIMD DP engine (split by algorithm)
mod.rs # Re-exports public API, tests (SIMD concordance)
common.rs # Types (DpResult), constants, flags, memory mgmt, traceback
single.rs # Single-affine gap: SSE2/4.1/AVX2/AVX512/NEON/WASM/scalar
dual.rs # Dual-affine gap: SSE2/4.1/AVX2/AVX512/NEON/WASM/scalar
splice.rs # Splice-aware: SSE2/4.1/AVX2/AVX512/NEON/WASM/scalar
lw.rs # Lightweight i16 Smith-Waterman + global NW alignment
pipeline.rs # End-to-end pipeline: process_query, format_output, MAPQ, PE
pair.rs # Paired-end logic (concordant pairing, PE MAPQ)
junc.rs # Junction annotation scoring (BED/SPSC for splice)
jump.rs # Jump splice extension (BED12 junction rescue)
split.rs # Split index: temp file I/O, multi-part merge
align_simple.rs # Simple NW alignment kernel (alternative Aligner impl)
stats.rs # AlignmentStats timing struct
wasm_lib.rs # wasm-bindgen entry points (align_wasm_full, AlignSession)
syncmer.rs # Open-syncmer sketcher (alternative Sketcher impl)
strobemer.rs # Randstrobe sketcher (alternative Sketcher impl)
fasta/ # Custom FASTA/FASTQ I/O
mod.rs # Module declarations
reader.rs # Block / file-based parser (zero-copy RefRecord, gzip auto-detect)
record.rs # Record / RefRecord types
stream.rs # Chunk-fed streaming FASTA/FASTQ parsers (FastaStreamer, FastqStreamer)
2. CLI Layer
Entry Point (src/main.rs)
rammap's CLI lives entirely in main.rs (no separate cli.rs or commands/ directory). The AlignArgs struct defines all command-line arguments via clap, and run() orchestrates the alignment pipeline.
main() → AlignArgs::parse() → run(args)
├─ apply_preset(opt, k, w, is_hpc, preset)
├─ load or build Index (single-part or batched)
├─ for each index part:
│ ├─ load JunctionDb, JumpDb (if splice mode)
│ └─ map_one_part(mi, opt, queries, out_cfg, ...)
│ ├─ spawn read-ahead thread (sync_channel(1))
│ └─ rayon parallel over query batches:
│ ├─ map_query() / map_query_multi()
│ ├─ process_query()
│ ├─ align_and_format_pair() / align_and_format_query()
│ └─ format_output()
└─ if --split-prefix: merge_split_results(prefix, n_parts, opt, mi)
CLI Arguments (AlignArgs)
Core: target (FASTA/index), queries (FASTA/FASTQ), -x preset, -k kmer, -w window, -t threads, -o output
Scoring: -A match, -B mismatch, -O gap_open[,gap_open2], -E gap_extend[,gap_extend2], --transition, --score-n
Chaining: --bw, --min-chain-score, --min-cnt, --max-gap, --chain-gap-scale
Seeding: --mid-occ, --mid-occ-frac, --mid-occ-range, --max-occ
Output: -c (CIGAR), --cs (CS tag), -a (SAM), --MD, --ds, --eqx, -N best_n
Paired-end: -F frag_len, --pairing no|weak|strong, --frag (interleaved)
Splice: -G max_intron, --junc-bed, --spsc, --junc-bonus, --junc-pen, -j (jump), --pass1
Split index: -I batch_size, --split-prefix
Advanced: --rmq, --heap-sort, --all-chains, -X (ava), --qstrand, --no-hash-name, --write-junc
3. Alignment Pipeline Overview
End-to-End Flow
FASTA Reference Query FASTA/FASTQ
| |
v v
Index::build() sketch_sequence()
(sketch.rs → sort.rs → index.rs) (sketch.rs)
| |
v v
Index { entries, bucket_offsets } Vec<Minimizer>
\ /
\ /
v v
collect_seed_hits() (seed.rs)
|
v
Vec<Minimizer> (anchors, sorted by ref_pos)
|
v
chain_anchors() / chain_anchors_rmq()
(chain.rs / chain_rmq.rs)
|
v
Vec<Mapping> (chains)
|
v
assign_parents + select_sub (filter.rs)
|
v
Vec<Mapping> (filtered)
|
v
align_anchors() (extend.rs)
calls extend_dual_affine / extend_splice (dp.rs)
|
v
Vec<AlnResult> (with CIGAR)
|
v
compute_mapqs() (pipeline.rs)
|
v
format_output() (pipeline.rs)
|
v
SAM / PAF output
Paired-End Variant
align_and_format_pair(opt, mi, segs, names, qlens, ...)
├─ process_query() for read 1
├─ process_query() for read 2
├─ pair_alignments(regs1, regs2, opt) [concordant pair detection]
└─ format_output() for both reads [with PE SAM flags]
Split Index Variant
map_one_part_split(mi, opt, queries, split_writer, ...)
├─ same alignment as map_one_part()
└─ split_write_query(writer, results) [serialize to temp file]
merge_split_results(prefix, n_parts, opt, mi)
├─ split_merge_prep() [load headers, compute rid_shifts]
├─ for each query across all parts:
│ ├─ split_read_query() per part
│ ├─ merge all results
│ └─ refilter_merged_results() [mm_hit_sort + mm_set_parent + mm_select_sub]
└─ format_output()
4. Module Reference: src/align/
sketch.rs — Minimizer Generation
Purpose: Generate minimizer anchors from DNA sequences using rolling k-mer hash and window selection.
Key type: Minimizer — packed 128-bit anchor representation (see Data Structures).
Key functions:
sketch_sequence(seq, len, w, k, rid, is_hpc, p: &mut Vec<Minimizer>)— Clearsp, generates minimizers for sequencesketch_sequence_append(...)— Same but appends (for multi-segment reads)- Both delegate to
sketch_sequence_impl()(shared core logic)
Internal helpers: encode_base() (256-byte LUT), kmer_hash() (invertible 64-bit hash).
Constants: MM_SEED_SEG_SHIFT=48, MM_SEED_SEG_MASK, MM_SEED_SELF=1<<43.
sort.rs — Radix Sort
Purpose: MSD in-place radix sort. NOT stable.
Key trait: RadixKey — fn radix_key(&self) -> u64 implemented for Minimizer (key=x) and (u64, u64) (key=first element).
Key functions:
radix_sort_128x(arr: &mut [Minimizer])— Sort minimizers by x fieldradix_sort_pair(arr: &mut [(u64, u64)])— Sort index entries by hash, then position within ties. Uses adaptive two-level partitioning (detects highest occupied byte, creates up to 65K buckets) with parallel sub-bucket sorting via rayon.
Algorithm: American Flag sort with 8-bit digits, 256 buckets, in-place cyclic permutation. Falls back to insertion sort for arrays < 64 elements. radix_sort_pair adds a parallel recursion phase: after two sequential partition passes, independent sub-buckets are sorted in parallel.
index.rs + index_bucket.rs — Reference Index
Purpose: Build, save, and load searchable minimizer hash tables from reference sequences. Uses per-bucket hash tables for lookup.
Key types:
Index—{kmer_size, window_size, homopolymer_compressed, index (part#), seqs: Vec<TargetSequence>, backend: LookupBackend, packed_seqs: Vec<u32>}IndexBuilder— Incremental, streaming-friendly builder. Pack + sketch one sequence at a time, drop the bytes after — designed for WASM where the caller can't materialize the fullVec<(String, Vec<u8>)>of a multi-GB reference.TargetSequence—{name, len, offset, is_alt}(metadata only; sequence data in packed_seqs)SeedLookuptrait —get(),get_range(),get_by_range(),occurrence_counts(),is_empty()LookupBackendenum — currentlyBucketHash(BucketHashLookup), extensible for future backendsBucketHashLookup(index_bucket.rs) — per-bucket open-addressing hash tables with shared flat positions array. Each bucket maps hash suffixes to (offset, count) ranges.
Key methods:
Index::build(seqs, w, k, is_hpc, max_occ) -> Self— Bulk build path used by the native CLI. Sequences are processed in 32-sequence chunks: pack sequentially (adjacent seqs may share boundaryu32s), sketch each chunk in parallel via rayon, distribute minimizers into global buckets, drop the chunk. Buckets are then sorted in parallel and consumed one-at-a-time into the per-bucket hash backend.IndexBuilder::new(w, k, is_hpc, max_occ)/add_sequence(name, seq)/reserve_bases(n)/finish() -> Index— Streaming alternative. Eachadd_sequencepacks into the global buffer, sketches, distributes to buckets, and drops the input bytes before the next call. Sequential (no parallel sketching), so slower thanIndex::buildon multi-sequence references — used by the WASMAlignSessionto keep peak memory bounded.Index::save(path)/save_part(writer)— Serialize (RMMI format with magic prefix)Index::load(path)/load_part(reader)— Detect format (RMMI, MMI v2, old bincode fallback)Index::load_minimap2(reader)— Read minimap2 .mmi format, constructingBucketHashLookupdirectly from per-bucket hash tablesIndex::get(hash)/get_range(hash)/get_by_range(range)— Delegated to backendIndex::cal_mid_occ(frac, min, max) -> usize— Compute occurrence threshold via backend'soccurrence_counts()Index::get_nt4(rid, pos) -> u8— Single base as nt4 (0=A,1=C,2=G,3=T,4=N)Index::get_region_nt4(rid, start, end) -> Vec<u8>— Region as nt4 bytes
Lookup: Per-bucket hash table indexed by hash & mask (low bits), then open-addressing probe for hash >> bucket_bits (high bits). O(1) average case.
Build memory model (Index::build): Chunked pack + sketch processes 32 sequences at a time; the 32-sequence chunk is the only ASCII held in memory beyond what's been packed. Bucket Vecs grow incrementally as minimizers are distributed, then are sorted in parallel and consumed one-at-a-time during hash table build. Peak memory ≈ max(in-flight chunk + buckets, positions + hash tables).
Build memory model (IndexBuilder): Same shape, but one sequence in flight at a time instead of 32, and no rayon parallelism. The streaming caller drives the loop — bytes can come from a chunked file read, a ReadableStream reader, etc. Reserve the packed buffer up front via reserve_bases(expected_total) if you know the total reference size; avoids the 2-3× allocator transient peak that Vec::push-driven growth produces on multi-GB inputs.
seed.rs — Seed Collection
Purpose: Find query minimizer matches in the reference index, filter repetitive seeds.
Key functions:
collect_seed_hits(opt, mi, qlen, minimizers, anchors, mini_pos, qname_opt) -> rep_len— Main seed collection: index lookup per query minimizer, filters bymid_occ/max_occ, returns representative lengthcollect_seed_hits_heap(...)— Min-heap extraction mode forMM_F_HEAP_SORT(sr preset)collect_seed_hits_with_occ(...)— Direct occurrence lookup variantselect_seeds(seeds, qlen, max_occ, max_max_occ, dist)— Heap-based selection for high-occurrence seed runs: keeps spatially diverse subsetfilter_minimizers_by_occ(minimizers, mid_occ, q_occ_frac) -> usize— Remove high-occurrence query minimizerscompute_read_hash(qname, qlen, seed, flags) -> u32— X31 hash for deterministic seeding
Key types: SeedInfo (query pos, span, hit count, tandem flag), MatchedSeed.
Helper hashes: hash64(), wang_hash(), x31_hash_string().
map.rs — Mapping Orchestration
Purpose: Core mapping pipeline — sketching, seeding, chaining, post-chain filtering. Owns MapOptions, MapContext, Mapping.
Key types: See Data Structures for MapOptions, AlignFlags, MapContext, Mapping.
Key functions:
map_query(opt, mi, qname, qseq, ctx) -> (Vec<Mapping>, rep_len, AlignmentStats, Vec<Minimizer>)— Single-segment mapping: sketch → seed → chain → filter → return chainsmap_query_multi(opt, mi, qname, seqs, qlens, ctx) -> MultiMapResult— Multi-segment (PE strong-pairing): handles segment boundaries during chainingcompute_bounds_from_squeezed(squeezed, sq_start, sq_cnt, tlen, qlen, min_cnt) -> (rs1, qs1, re1, qe1)— Extension boundary computation from nearby seedsestimate_divergence(regs, anchors, qlen, mini_pos, mi)— K-mer based divergence estimation (dv:f tag)chain_post(regs, opt, mi, ...)— Post-chain filtering:assign_parents()+select_sub()+s2_scorecomputation
Internal flow of map_query():
sketch_sequence()on queryfilter_minimizers_by_occ()to remove high-frequency minimizerscollect_seed_hits()orcollect_seed_hits_heap()to find index matchesradix_sort_128x()anchors by (ref_id,ref_pos)chain_anchors()orchain_anchors_rmq()(ifMM_F_RMQ_CHAIN)- Backtrack + compact chains
chain_post()for parent/secondary filtering- Optional RMQ rescue re-chaining
- Return
Vec<Mapping>+ metadata
chain.rs — Standard Chaining DP
Purpose: Connect compatible anchors into high-scoring chains via dynamic programming.
Key functions:
chain_anchors(anchors, scores, predecessors, visited, max_chain_skip, max_chain_iter, bw, chain_gap_scale, max_gap_ref, chn_pen_gap, chn_pen_skip, is_cdna, n_seg) -> Vec<Minimizer>— O(n^2) DP with skip limitscompute_chain_score(ai, aj, max_dist_x, max_dist_y, bw, chn_pen_gap, chn_pen_skip, is_cdna, n_seg) -> i32— Pairwise scoring between anchorschain_backtrack_end(...)— Traceback from endpoint with max-drop thresholdfast_log2(x) -> f32— Fast log2 approximation
DP recurrence (for anchors sorted by ref_pos):
score[i] = max(
qi_span, // base: single anchor
max over j<i { score[j] + connect_score(i, j) } // extend chain
)
Scoring function compute_chain_score(ai, aj):
query_diff = ai.query_pos - aj.query_pos(must be > 0)ref_diff = ai.ref_pos - aj.ref_pos(must be > 0)gap = |ref_diff - query_diff|(must be <= bandwidth)score = min(aj.span, min(ref_diff, query_diff))minus gap penalty- Gap penalty =
chn_pen_gap * gap + 0.5 * log2(gap)(logarithmic + linear)
chain_rmq.rs — RMQ-Accelerated Chaining
Purpose: O(log n) chaining via augmented treap for range minimum queries. Critical for long reads and assembly presets.
Key types:
RmqTree— Arena-based treap with subtree-min augmentation. Fields:nodes: Vec<Node>,root,free_head,rng(xorshift64)Node—{key_y, key_i, priority, left, right, parent, sub_min_val, sub_min_i, val}RmqRevIter— Stack-based reverse in-order iterator for tree traversal
Key functions:
chain_anchors_rmq(anchors, ...) -> Vec<Minimizer>— RMQ-based chaining with rescue re-chainingRmqTree::insert_elem(y, i, val)— O(log n) treap insert with subtree-min updateRmqTree::erase(y, i)— O(log n) deletionRmqTree::rmq(lo_y, hi_y) -> (min_val, min_i)— Range minimum query on closed interval [lo_y,hi_y]
filter.rs — Parent Assignment & Secondary Filtering
Purpose: Determines primary/secondary status and filters low-quality chains.
Key types:
ParentState—{parent: Vec<usize>, parent_score: Vec<i32>, ...}tracking primary regionsFilterParams—{pri_ratio, best_n, min_diff, mask_level, mask_len, hard_mask_level}FilterableItemtrait — abstraction for items that can be filtered by query overlap
Key functions:
assign_parents<T: FilterableItem>(items: &mut [T])— Set parent for each item based on query overlap. First item = primary; subsequent assigned to highest-scoring non-overlapping parent.select_sub(items, parent_state, params)— Remove suboptimal secondaries: keepbest_nper parent, filter bypri_ratiocheck_secondary_filter(item, parent, params) -> bool— Single-item filter decisionscale_alt_score(score, alt_diff_frac) -> i32— ALT contig scoring adjustment
extend.rs — Anchor Extension & CIGAR Generation
Purpose: Convert chains of anchors into full alignments by extending left/right boundaries and filling gaps with SIMD DP. Also handles CIGAR formatting (CS/MD/DS tags).
Key types:
CigarOp—{op: char, len: u32}where op is one of=,X,M,I,D,N,S,HAlignmentContext— Reusable DP buffers:{dp_curr, dp_prev: Vec<DpCell>, traceback: Vec<u8>}AlignmentKerneltrait — Polymorphic DP:fn align(qseq, tseq, ...) -> Vec<CigarOp>TrimResult— Output oftrim_and_prepare_anchors(): anchor array with coordinatesCigarTagVisitortrait — Visitor pattern for CIGAR tag formatting:on_aligned(),on_insertion(),on_deletion(),on_intron(),finish()
Key functions:
align_anchors(anchors, qseq, tseq, ..., junc_db, rid) -> AlignResult— Main entry: wrapper foralign_anchors_full()align_anchors_full(...)— Full implementation: left extension → gap-fill loop → right extension → CIGAR finalization. Handles z-drop splitting, inversion detection, splice modes.trim_and_prepare_anchors(...)— Anchor trimming, bad-end fixing, coordinate setupcompute_left_boundary(...)/compute_right_boundary(...)— Extension boundary from nearby seedsfinalize_cigar(...)— CIGAR post-processing:fix_cigar+ =/X conversionbuild_scoring_matrix(a, b) -> [i8; 25]— Simple 5x5 substitution matrixbuild_scoring_matrix_full(a, b, transition, sc_ambi) -> [i8; 25]— Full matrix with transition + ambiguityfmt_cigar(ops, eqx) -> String— CIGAR text formatfmt_cs(ops, qseq, tseq, qs, rs) -> String— CS tag viaCsVisitorfmt_md(ops, qseq, tseq, qs, rs) -> String— MD tag viaMdVisitorfmt_ds(ops, ...) -> String— DS tag viaDsVisitorwalk_cigar_ops(ops, visitor) -> String— Shared CIGAR iteration driving all three visitorsconvert_cigar_to_eqx_pub(raw_cigar, qseq, tseq, qs, rs) -> Vec<CigarOp>— Expand M → =/Xrev_comp(seq) -> Vec<u8>— Reverse complement (ASCII)rev_comp_nt4(seq) -> Vec<u8>— Reverse complement (nt4)encode_nt4_byte(b) -> u8— ASCII base to nt4
Constants: SEED_IGNORE=1<<41, SEED_LONG_JOIN=1<<40, SEED_TANDEM=1<<42 (defined in sketch.rs, re-exported from extend.rs).
dp/ — SIMD Dynamic Programming
Purpose: SIMD-accelerated banded DP alignment. Three algorithm variants across six platform targets (SSE2, SSE4.1, AVX2, AVX512BW, NEON, WASM SIMD128) plus scalar fallback. Split into sub-modules by algorithm:
| Module | Contents |
|---|---|
dp/common.rs | Types (DpResult, LightweightProfile), constants, alignment flags, memory management (AlignedMemory, DP_MEM_CACHE), WASM SIMD compat layer, x86 SIMD helpers, traceback functions |
dp/single.rs | Single-affine gap: cost = q + k*e. All SIMD variants + scalar + dispatch |
dp/dual.rs | Dual-affine gap: cost = min(q+k*e, q2+k*e2). All SIMD variants + scalar + dispatch |
dp/splice.rs | Splice-aware: GT-AG junction detection + bonus. All SIMD variants + scalar + dispatch |
dp/lw.rs | Lightweight i16 Smith-Waterman (no traceback) + global NW alignment |
dp/mod.rs | Module declarations, pub use re-exports, SIMD concordance tests |
Each algorithm module contains all its SIMD variants (SSE2/SSE4.1 via macro, AVX2 macro, AVX512 macro, NEON, WASM) plus scalar fallback — keeping all code for one algorithm together.
Public API (re-exported from dp/mod.rs):
extend_single_affine(...)— Single-affine gap dispatchextend_dual_affine(...)— Dual-affine gap dispatch (used for most long-read modes)extend_splice(...)— Splice-aware dispatchlightweight_profile_init(...)/lightweight_align_i16(...)— Quick scoring without tracebackglobal_align(...)— Global NW (SIMD or Gotoh scalar)
Key constants (alignment flags, in dp/common.rs):
| Flag | Value | Meaning |
|---|---|---|
SCORE_ONLY | 0x01 | Skip traceback |
RIGHT_ALIGN | 0x02 | Right-align gaps |
GENERIC_SCORING | 0x04 | Use full 5x5 scoring matrix |
APPROX_MAX | 0x08 | Approximate max tracking (faster) |
APPROX_DROP | 0x10 | Enable z-drop heuristic |
EXTENSION_ONLY | 0x40 | Stop at first max (extension only) |
REV_CIGAR | 0x80 | Output CIGAR in reverse |
SPLICE_FORWARD | 0x100 | Forward splice mode |
SPLICE_REVERSE | 0x200 | Reverse splice mode |
SPLICE_FLANK | 0x400 | Splice flank bonus |
SPLICE_COMPLEX | 0x800 | Complex splice handling |
SPLICE_SCORE | 0x1000 | Use junction bonus array |
Shared helpers (in dp/common.rs):
push_cigar(cigar, op, len)— Append CIGAR op (merges consecutive same-op)alloc_h_array(approx_max, tlen_, simd_width)— Allocate 32-bit H array for exact-max trackingtraceback_dual_affine()/traceback_single_affine_safe()/traceback_splice()— CIGAR reconstruction from DP matrixtraceback_start_position()— Find start position for traceback
SIMD macro architecture: See SIMD Architecture.
pipeline.rs — End-to-End Pipeline
Purpose: Orchestrates full alignment from mapping results to formatted output. Handles single-read, paired-end, split-index, and inversion modes.
Key types:
OutputConfig—{do_cigar, do_cs, do_md, do_ds, eqx, output_sam: bool}CigarStats—{matches, edit_distance, block_len, num_ambiguous, divergence, has_n_skip, gap_opens}withCigarStats::from_cigar(ops, qseq, tseq)constructorAlnResult— Full alignment result (~30 fields, see Data Structures)ProcessedQuery—{results, mapqs, sam_pri, parent_indices, rep_len, stats}DpRecalcInfo—{match_len, block_len, num_ambiguous, gap_bases, gap_opens, sum_log_gap}
Core pipeline functions:
align_and_format_query(opt, mi, qseq, qname, qlen, regs, out_cfg, ctx, junc_db, jump_db, split_mode) -> ProcessedQuery— Single-segment entry: map → align → formatprocess_query(opt, mi, qseq, qname, qlen, regs, out_cfg, ctx, junc_db, jump_db, split_mode) -> ProcessedQuery— Core alignment loop callingalign_single_mapping()per chainprocess_query_core(...)— Orchestrator: alignment loop →filter_alignment_results()→assign_parents_and_select()→compute_mapqs()align_single_mapping(mapping, opt, mi, qseq, qlen, ctx, junc_db) -> AlnResult— Align one chain: extracts sequences, callsalign_anchors(), computes CigarStatsalign_and_format_pair(opt, mi, segs, names, qlens, junc_db, jump_db, ctx, out_cfg) -> Vec<(String, String)>— PE entry point
Post-alignment functions:
filter_alignment_results(results, recalc_infos, opt)— Threshold filtering (min_dp_max,min_cnt, etc.)assign_parents_and_select(results, recalc_infos, opt, mi, ...)— Parent assignment + secondary selection +dp_maxrankingcompute_mapqs(results, mapqs, ...)— MAPQ calculation (log-odds ratio between top 2dp_maxscores)update_dp_max(dp_max_vals, recalc_infos, qlen, ...)— Recalculatedp_maxfrom CIGAR statscompute_alignment_score_max(ops, mat, q, e, qseq, tseq, log_gap) -> i32— Walk CIGAR to find max local score
Output functions:
format_output(qname, qlen, results, mapqs, out_cfg, opt, seq_data) -> (String, bool)— Thin dispatcher callingformat_sam_recordorformat_paf_recordformat_sam_record(buf, result, ...)— Single SAM record: all columns + tagsformat_paf_record(buf, result, ...)— Single PAF recordformat_unmapped_record(buf, ...)— Unmapped read output
Inversion handling:
try_align_inversion(opt, mi, qlen, qseq_fwd, qseq_rc, r1, r2, out) -> Option<AlnResult>— Detect and align inversions via lightweight SW scoring + full DP extension
SAM tags output: NM, AS, ms, s1, s2, cm, nn, tp, de:f, dv:f, rl:i, cs, MD, ds, SA (supplementary).
pair.rs — Paired-End Pairing
Purpose: Concordant pair detection and PE MAPQ adjustment.
Key type: PeReg — {dp_score, ref_id, ref_start, ref_end, is_reverse, hash, id, parent, sam_pri, proper_frag}
Key function: pair_alignments(max_gap_ref, pe_bonus, sub_diff, match_sc, qlens, regs1, regs2) — Finds best concordant pair by scanning combined sorted array of (ref_id, position, strand). Adjusts MAPQ for concordant pairs.
Algorithm: Sort both reads' alignments by position. Scan for proper pairs (forward-first + reverse-second compatible, within max_frag_len). Apply pe_bonus to best pair's MAPQ.
junc.rs — Junction Annotation Scoring
Purpose: Load and query splice junction annotations for bonus/penalty scoring in splice-aware alignment.
Key type: JunctionDb enum — Bed(Vec<Vec<BedInterval>>) or Spsc(Vec<SpscContig>)
Key functions:
load_bed_junctions(path, names, n_seq) -> JunctionDb— Parse BED6/BED12 junctionsload_spsc_scores(path, names, n_seq, scale) -> JunctionDb— Per-position SPSC scoresget_junc(db, rid, strand, pos) -> Option<u8>— O(log n) lookup via binary search
Integration: Called during extend_splice() gap-fill in extend.rs. Junction presence adds bonus; unannotated splice adds penalty.
jump.rs — Jump Splice Extension
Purpose: Extend clipped alignment regions into annotated exon junctions.
Key type: JumpDb — {junctions: Vec<Vec<JumpJunc>>} where JumpJunc = {left_pos, right_pos, count, strand, flag}
Key functions:
JumpDb::load(mi, path, flag, min_sc) -> Self— Parse BED12 junctionsjump_split_left()/jump_split_right()— Extend clipped ends into junctions- Applied in pipeline after
process_query()for single-segment splice reads
Modes: -j file sets MM_JUNC_ANNO (min_sc=-1); --pass1 file sets MM_JUNC_MISC (min_sc=5).
split.rs — Split Index Support
Purpose: Multi-part index handling: serialize per-part results to temp files, then merge globally.
Key functions:
split_init(prefix, part_index, mi) -> BufWriter<File>— Create temp file with headersplit_write_query(writer, results, rep_len, frag_gap)— Bincode-serialize AlnResult batchsplit_merge_prep(prefix, n_parts) -> (k, seqs, rid_shifts, readers)— Load headers, compute coordinate shiftssplit_read_query(reader, k, rid_shifts) -> Vec<AlnResult>— Deserialize resultsmerge_split_results(prefix, n_parts, opt, mi, out, stats)— Full merge pipeline
Critical: rl:i:0 always in merge output. mid_occ calculated once from first part only.
stats.rs — Timing Statistics
Type: AlignmentStats — {t_sketch, t_seed, t_chain, t_align, t_post: Duration, n_reads, n_seeds, n_anchors, n_chains: u64}. Implements Add for aggregation across threads/parts.
wasm_lib.rs — WebAssembly Support
wasm_lib.rs exposes the wasm-bindgen surface for the browser demo and any other WASM host:
align_wasm_full(target_text, query_text, preset, output_sam, output_cigar) -> String— Single-call entry: build an index fromtarget_text(FASTA), align all reads inquery_text(FASTA/FASTQ auto-detected), return PAF/SAM concatenated with a---LOG---\nseparator. Constrained by V8's ~512 MB max-string length on each input — for anything larger, useAlignSession.AlignSession(class) — Streaming entry for multi-GB inputs.new(preset, output_sam, output_cigar)→ optionalreserve_ref_bases(n)hint → repeatedappend_ref(chunk)→finalize_ref()→ repeatedappend_query(chunk)(returns PAF/SAM for reads completed by that chunk) →finalize()(returns trailing output + log). Internally drivesFastaStreamer→IndexBuilderfor the reference,FastqStreamer→ per-read alignment for queries; raw bytes are dropped as records complete so peak WASM linear memory stays bounded.align_wasm(target_fasta, query_fasta, output_sam, is_splice) -> String— Legacy wrapper aroundalign_wasm_fullwith preset =spliceormap-ont. Kept for backwards compatibility.force_align_wasm(tseq, qseq) -> String— Force-align two short sequences via full DP, no seeding/chaining. Returns CIGAR.init_thread_pool(re-exported fromwasm_bindgen_rayon, threaded build only) — JS mustawait initThreadPool(n)before any parallel work runs.
A #[wasm_bindgen(start)] panic hook is installed at module init so Rust panics surface to JS as console.error with file:line + payload, instead of the opaque "unreachable executed" trap.
Uses WASM SIMD128 intrinsics for DP and chaining when available; falls back to scalar.
5. Key Data Structures
Minimizer Bit-Field Encoding (sketch.rs)
Minimizer { x: u64, y: u64 }
x field (reference):
[63] unused
[62:32] ref_id (31 bits) << 33 | strand (1 bit) << 32
[31:0] ref_pos (32 bits, as i32)
y field (query):
[55:48] seg_id (8 bits, for multi-segment reads)
[43] MM_SEED_SELF flag (self-mapping in ava mode)
[39:32] kmer_span (8 bits)
[31:1] query_pos (31 bits)
[0] strand (0=forward, 1=reverse)
Accessor methods:
.ref_pos() -> i32 // x as u32 as i32
.ref_id() -> usize // (x >> 32) & 0x7FFFFFFF
.ref_id_strand() -> u64 // x >> 32
.query_pos() -> i32 // y as i32
.query_span() -> i32 // (y >> 32) & 0xff
.segment_id() -> usize // (y >> 48) & 0xff
.is_self_anchor() -> bool // y & (1 << 43) != 0
MapOptions (map.rs)
MapOptions {
seeding: SeedingParams {
mid_occ: usize, // Occurrence threshold for filtering seeds
max_occ: usize, // Hard max occurrence (re-chain trigger)
max_max_occ: usize, // Absolute max (default 4095)
occ_dist: i32, // Distance threshold for select_seeds
q_occ_frac: f32, // Fraction-based occurrence filter
min_mid_occ: i32, // Lower bound for cal_mid_occ
max_mid_occ: i32, // Upper bound for cal_mid_occ
},
chaining: ChainingParams {
min_cnt: i32, // Min anchors per chain
min_chain_score: i32, // Min total chain score
max_gap: i32, // Max gap in query or ref
max_gap_ref: i32, // Max gap in ref only (for splice)
max_dist_x: i32, // Max query distance between anchors
max_dist_y: i32, // Max ref distance between anchors
bandwidth: i32, // Max diagonal distance in chain
bandwidth_long: i32, // Bandwidth for long gaps
max_chain_skip: i32, // Max consecutive skipped anchors
max_chain_iter: i32, // Max predecessors to check per anchor
chn_pen_gap: f32, // Gap penalty coefficient
chn_pen_skip: f32, // Skip penalty coefficient
chain_gap_scale: f32, // Scale factor for chn_pen_gap derivation
rmq_rescue_size: i32, // Min chain size for RMQ rescue
rmq_rescue_ratio: f32, // Score ratio threshold for rescue
rmq_inner_dist: i32, // Inner distance for RMQ tree
rmq_size_cap: i32, // Cap on RMQ tree size
},
scoring: ScoringParams {
match_score: i32, // Match bonus (default 2)
mismatch_penalty: i32, // Mismatch cost (default 4)
gap_open: i32, // Short gap open (default 4)
gap_extend: i32, // Short gap extend (default 2)
gap_open2: i32, // Long gap open (default 24)
gap_extend2: i32, // Long gap extend (default 1)
transition: i32, // Transition bonus (A<->G, C<->T)
ambig_penalty: i32, // N-base penalty
noncanon_penalty: i32, // Non-canonical splice penalty
junc_bonus: i32, // Junction annotation bonus
junc_pen: i32, // Junction annotation penalty
},
alignment: AlignmentParams {
zdrop: i32, // Z-drop threshold
zdrop_inv: i32, // Z-drop for inversions
end_bonus: i32, // Bonus for reaching sequence end
max_sw_mat: i64, // Max DP matrix size (SW memory cap)
min_dp_max: i32, // Min dp_max to keep alignment
min_dp_len: i32, // Min length for DP alignment (vs short-circuit)
anchor_ext_len: i32, // Anchor seed extension length
anchor_ext_shift: i32, // Anchor seed extension shift
max_clip_ratio: f32, // Max clipping ratio
},
filtering: FilteringParams {
best_n: i32, // Max secondary alignments to report
pri_ratio: f32, // Min score ratio vs primary
mask_level: f32, // Query overlap threshold for masking
mask_len: i32, // Min overlap length for masking
is_splice: bool, // Splice-aware mode
alt_drop: f32, // ALT contig score drop
seed: i32, // Random seed
chain_skip_scale: f32, // Scale for chn_pen_skip derivation
max_qlen: i32, // Max query length
jump_min_match: i32, // Min matches for jump extension
},
pairing: PairedEndParams {
max_frag_len: i32, // Max fragment length for PE
pe_ori: i32, // Expected orientation (FR=0<<1|1)
pe_bonus: i32, // MAPQ bonus for concordant pairs
},
flags: AlignFlags, // Bitflags (see below)
}
AlignFlags (map.rs)
NO_DIAG 0x001 No diagonal self-anchors (ava)
NO_DUAL 0x002 No qname > tname (ava)
NO_QUAL 0x010 Don't output quality
OUT_CIGAR 0x020 Produce CIGAR string
SPLICE 0x080 Splice-aware alignment
SPLICE_FOR 0x100 Forward splice strand
SPLICE_REV 0x200 Reverse splice strand
NO_LJOIN 0x400 Skip long-join rescue
INDEPEND_SEG 0x800 Independent segment mapping
SHORT_READ 0x1000 Short-read optimizations
FRAG_MODE 0x2000 Fragment/PE mode
NO_PRINT_2ND 0x4000 Suppress secondary output
EQX 0x8000 Use =/X instead of M in CIGAR
LONG_CIGAR 0x10000 Long CIGAR format
SOFTCLIP 0x20000 Soft-clip ends
SPLICE_FLANK 0x40000 Splice flank bonus
FOR_ONLY 0x100000 Forward strand only
REV_ONLY 0x200000 Reverse strand only
HEAP_SORT 0x400000 Heap-based seed sorting
ALL_CHAINS 0x800000 Output all chains (skip filtering)
NO_END_FLT 0x1000000 No end filtering
HARD_MASK_LEVEL 0x2000000 Hard mask level
SAM_HIT_ONLY 0x4000000 SAM output for hits only
PAF_NO_HIT 0x8000000 PAF no-hit output
NO_HASH_NAME 0x40000000 Don't hash read name
RMQ_CHAIN 0x80000000 Use RMQ chaining
QSTRAND 0x100000000 Query-strand alignment
NO_INV 0x200000000 Skip inversion detection
SPLICE_OLD 0x400000000 Old splice mode
SECONDARY_SEQ 0x800000000 Output sequence for secondaries
WEAK_PAIRING 0x4000000000 Weak PE pairing
SR_RNA 0x8000000000 Short-read RNA mode
OUT_JUNC 0x10000000000 Output junction BED
AlnResult (pipeline.rs)
AlnResult {
// Mapping metadata
ref_id: usize, // Target sequence index
is_reverse: bool, // Reverse strand
chain_score: i32, // s1 tag
initial_chain_score: i32, // Before z-drop split
anchor_count: usize, // cm tag
s2_score: Option<i32>, // Suboptimal chain score
hash: u32, // Read hash for deterministic seeding
// Alignment coordinates
query_start: usize,
query_end: usize,
ref_start: usize,
ref_end: usize,
// Alignment quality
align_score: i32, // AS tag
matches: usize, // PAF col 10
block_len: usize, // PAF col 11
edit_distance: u32, // NM tag
num_ambiguous: usize, // nn tag
divergence: f64, // de:f tag
// Formatted strings
cigar_str: String,
cs_str: String,
ds_str: String,
md_str: String,
// Status flags
is_secondary: bool,
is_spliced: bool, // Contains N_SKIP ops
trans_strand: u8, // 0=unknown, 1=+, 2=-, 3=ambiguous
split: u8, // z-drop split: 1=left, 2=right
split_inv: bool, // Trigger inversion alignment
inv: bool, // Is inversion result
proper_frag: bool, // Concordant PE pair
seg_split: bool, // From seg_gen splitting
is_alt: bool, // ALT contig
is_root_chain: bool, // Root chain (parent==self)
div: f32, // dv:f tag (seed-based divergence)
// Scores for sorting/filtering
dp_score: i32, // For ranking (may be recalculated)
dp_score_original: i32, // ms:i tag (original)
effective_cnt: i32, // For mm_filter_regs
pre_num_suboptimal: i32, // Pre-alignment n_sub
dp_score_secondary: i32, // Best dp_max among children
secondary_chain_score: i32, // Best chain_score among children
num_suboptimal: i32, // Count of similar-scoring children
}
Index (index.rs + index_bucket.rs)
Index {
kmer_size: usize, // K-mer length
window_size: usize, // Minimizer window length
homopolymer_compressed: bool, // HPC mode
index: usize, // Part number (0-based)
seqs: Vec<TargetSequence>, // Reference sequence metadata
backend: LookupBackend, // Seed lookup (BucketHash)
packed_seqs: Vec<u32>, // 4-bit packed sequences (8 bases/u32)
}
LookupBackend::BucketHash(BucketHashLookup {
bucket_bits: u32, // log2(n_buckets) for hash partitioning
buckets: Vec<Bucket>, // Per-bucket open-addressing hash tables
positions: Vec<u64>, // Shared flat position array
})
Bucket {
keys: Vec<u32>, // Hash suffixes (EMPTY_KEY = u32::MAX)
vals: Vec<u64>, // Packed (offset << 32) | count
mask: u32, // capacity - 1 (power of 2)
}
TargetSequence {
name: String,
len: usize,
offset: u64, // Cumulative offset into packed_seqs
is_alt: bool,
}
DpResult (dp.rs)
DpResult {
max: i32, // Best score found
max_score_query_pos: i32, // Query pos of max
max_score_target_pos: i32, // Target pos of max
max_query_end_score: i32, // Score when query exhausted
max_query_end_target_pos: i32, // Target pos for above
max_target_end_score: i32, // Score when target exhausted
max_target_end_query_pos: i32, // Query pos for above
score: i32, // Final alignment score
cigar_capacity: i32,
cigar_len: i32,
reach_end: i32, // 1 if reached sequence end
zdropped: i32, // 1 if z-drop triggered
cigar: Vec<u32>, // Packed: len << 4 | op (0=M, 1=I, 2=D, 3=N)
}
MapContext (map.rs)
MapContext {
anchors: Vec<Minimizer>, // Anchor buffer (reused per read)
minimizers: Vec<Minimizer>, // Query minimizer buffer
chain_bufs: ChainingBuffers, // DP + SoA chaining buffers (see below)
mini_pos: Vec<u64>, // Position array for divergence
}
ChainingBuffers {
predecessors: Vec<i64>, // DP predecessor array
scores: Vec<i32>, // DP score array
peak_scores: Vec<i32>, // DP peak score array
visited: Vec<i32>, // Backtrack visited flags
soa_ref_pos: Vec<i32>, // SoA chaining buffers for SIMD
soa_query_pos: Vec<i32>,
soa_query_span: Vec<i32>,
soa_ref_id_strand: Vec<u32>,
soa_scores_buf: Vec<i32>,
}
6. Algorithm Details
Minimizer Sketching
For a sequence of length L with k-mer size k and window size w:
- Scan positions 0..L, maintaining forward and reverse-complement k-mer hashes
- For each valid position (no ambiguous bases, k consecutive bases seen):
- Forward:
kmer[0] = (kmer[0] << 2 | base) & mask - Reverse:
kmer[1] = (kmer[1] >> 2) | (3 ^ base) << shift - Skip palindromic k-mers (
kmer[0] == kmer[1]) - Select canonical strand:
z = if kmer[0] < kmer[1] { 0 } else { 1 } - Hash:
h = kmer_hash(kmer[z], mask) << 8 | span
- Forward:
- Maintain circular buffer of w k-mer hashes. Output minimizer when the minimum changes.
- HPC mode: compress homopolymer runs, adjust k-mer span accordingly.
Seed Collection
For each query minimizer with hash h:
- Lookup via
get_range(h): select bucket byh & mask, probe forh >> bucket_bitsin per-bucket hash table - Retrieve positions via
get_by_range(start, end): slice into shared positions array - For each matching reference position: create anchor
(ref_id, ref_pos, query_pos, strand) - Filter: skip if
hit_count>mid_occ(unless inselect_seedsheap) - Result:
Vec<Minimizer>anchors sorted by reference position via radix sort
Chaining DP Recurrence
For anchors a[0..n] sorted by (ref_pos):
f[i] = max(
a[i].span, // base case
max over j < i where compatible(i,j) {
f[j] + score(a[i], a[j])
}
)
compatible(i, j):
same ref_id and strand
0 < query_diff <= max_dist_x
0 < ref_diff <= max_dist_y
|ref_diff - query_diff| <= bandwidth
score(a[i], a[j]):
min_diff = min(ref_diff, query_diff)
base = min(a[j].span, min_diff)
gap = |ref_diff - query_diff|
penalty = chn_pen_gap * gap + 0.5 * log2(gap + 1) if gap > 0
return base - ceil(penalty)
Backtrack from highest f[i] via predecessor links. Skip limit (max_chain_skip) prevents O(n^2) worst case.
RMQ Chaining
Uses arena-based treap (randomized BST) augmented with subtree-minimum for O(log n) range queries:
- Process anchors left-to-right by
ref_pos - For anchor i, query treap for best predecessor in
query_posrange[i.qpos - max_dist, i.qpos] - Insert anchor i into treap with
key=query_pos,value=negative_score - Erase out-of-range anchors (
ref_pos distance > max_dist_x) rmq(lo, hi)traverses treap branches, using subtree-min to prune entire subtrees
The treap uses xorshift64 random priorities for expected O(log n) height.
Banded DP (Suzuki-Kasahara Formulation)
Processes anti-diagonals of the DP matrix with SIMD vectors:
- Query profile: Pre-compute score lookup
qp[q][t] = mat[t*5 + q_base[q]] - Band: For each target position r, compute query positions
[st, en]within bandwidth - DP states (dual-affine):
- H: best score ending in match/mismatch
- E1: best score ending in short gap (penalty q+k*e)
- F1: best score ending in short gap (query direction)
- E2: best score ending in long gap (penalty q2+k*e2)
- F2: best score ending in long gap (query direction)
- SIMD: Process 16 positions per
__m128iregister (i8 values with periodic overflow check) - Z-drop: If
current_max < global_max - zdrop, stop and setzdropped=1 - Traceback: Store 2-bit decisions per cell, reconstruct CIGAR in reverse
MAPQ Calculation
if has_cigar:
mapq = 40 * (1 - dp_max2 / dp_max) * ln(dp_max) // log-odds
else:
mapq = 40 * (1 - s2 / s1) * min(1, matches/10) * ln(s1)
Adjustments:
if dp_max > dp_max2 && mapq == 0: mapq = 1 // promote unique
if is_inversion: mapq = 0
if PE concordant: mapq += pe_bonus
clamp to [0, 60]
Post-Chain Filtering
assign_parents(): For each chain sorted by score:
- First chain = primary (parent = self)
- Subsequent chains: find highest-scoring earlier chain with query overlap >
mask_level - If found: child of that parent. If not: new primary.
select_sub(): For each parent:
- Keep at most
best_nchildren - Remove children with score <
pri_ratio * parent_score - Hard mask: remove if overlap >
mask_leveland score < threshold
7. SIMD Architecture
Platform Support
DP kernels (dp/single.rs, dp/dual.rs, dp/splice.rs):
| Platform | Register Type | Width | Dispatch |
|---|---|---|---|
| x86_64 AVX512BW | __m512i | 64 lanes | Runtime is_x86_feature_detected!("avx512bw") |
| x86_64 AVX2 | __m256i | 32 lanes | Runtime is_x86_feature_detected!("avx2") |
| x86_64 SSE4.1 | __m128i | 16 lanes | Runtime is_x86_feature_detected!("sse4.1") |
| x86_64 SSE2 | __m128i | 16 lanes | Fallback (always available on x86_64) |
| aarch64 NEON | uint8x16_t | 16 lanes | Compile-time #[cfg(target_arch = "aarch64")] |
| wasm32 SIMD128 | v128 | 16 lanes | Compile-time #[cfg(target_feature = "simd128")] |
| Scalar | i32 arrays | 1 | Fallback for any platform |
Chaining (chain_simd.rs):
| Platform | Width | Dispatch |
|---|---|---|
| x86_64 AVX2 | 8 predecessors/iter | Runtime is_x86_feature_detected!("avx2") |
| aarch64 NEON | 4 predecessors/iter | Compile-time #[cfg(target_arch = "aarch64")] |
| Scalar | 1 predecessor/iter | Fallback |
SSE2/SSE4.1 Macro Unification
SSE2 and SSE4.1 share __m128i register type and differ in only 3 operations. Each algorithm file (dp/single.rs, dp/dual.rs, dp/splice.rs) defines its own SSE macro:
// In dp/single.rs:
macro_rules! extend_single_affine_impl { ... }
// In dp/dual.rs:
macro_rules! extend_dual_affine_impl { ... }
// In dp/splice.rs:
macro_rules! extend_splice_impl { ... }
Each macro is instantiated for SSE2, SSE4.1, and WASM within the same file. AVX2 and AVX512 have separate macros (also in the same file) due to different SIMD widths.
Operation differences:
| Operation | SSE4.1 | SSE2 Emulation |
|---|---|---|
max_epi8 | _mm_max_epi8(a, b) | cmpgt + and + andnot + or |
min_epi8 | _mm_min_epi8(a, b) | cmpgt + andnot + and + or |
blendv_epi8 | _mm_blendv_epi8(a, b, mask) | andnot(mask, a) | and(mask, b) |
Compile-time dispatch: $is_sse41 is a bool constant — compiler eliminates dead branches via constant folding.
NEON Kept Separate
NEON implementations are NOT unified with SSE via macros due to structural differences:
- Different register types (
uint8x16_tvs__m128i) vextq_u8(concatenate-extract) vs_mm_slli_si128(byte shift)- Signed/unsigned reinterpretation required (
vreinterpretq_s8_u8) vbslq_u8(bit select) vs blend- Reversed
andnotargument order
NEON shares only the non-SIMD helpers from dp/common.rs (init, push_cigar, traceback). All SIMD variants for one algorithm live in the same file (e.g., dp/dual.rs contains the SSE, AVX2, AVX512, NEON, WASM, and scalar dual-affine implementations).
Runtime Dispatch Pattern
DP alignment (dp.rs):
pub fn extend_dual_affine(...) {
if FORCE_SCALAR { return ..._scalar(...); }
#[cfg(target_arch = "x86_64")] {
if avx512bw { return ..._avx512(...); } // 64 lanes
if avx2 { return ..._avx2(...); } // 32 lanes
if sse4.1 { return ..._sse41(...); } // 16 lanes
return ..._sse2(...); // 16 lanes (fallback)
}
#[cfg(target_arch = "aarch64")]
return ..._neon(...); // 16 lanes
#[cfg(target_arch = "wasm32")]
return ..._wasm(...); // 16 lanes
..._scalar(...) // 1 lane
}
Chaining (chain.rs → chain_simd.rs):
pub fn chain_anchors(...) {
if is_cdna || n_seg > 1 || a.len() < 32 { return ..._scalar(...); }
#[cfg(target_arch = "x86_64")]
if avx2 { return chain_anchors_avx2(...); } // 8 predecessors/iter
#[cfg(target_arch = "aarch64")]
return chain_anchors_neon(...); // 4 predecessors/iter
..._scalar(...) // 1 predecessor/iter
}
---
## 8. Performance Patterns
### Overlapped I/O
Read-ahead thread Worker threads (rayon) | | | sync_channel(1) | |------ batch of reads ------->| | |-- map_query() --| |------ next batch ----------->|-- map_query() --| | |-- format_output() --| | |-- write to stdout --|
One batch reads ahead while the previous batch aligns. `sync_channel(1)` bounds memory to 2 batches.
### Buffer Reuse via MapContext
```rust
// Per-thread, reused across reads:
let mut ctx = MapContext::new();
for read in batch {
// mem::take moves buffers out, avoiding allocation
let mut anchors = std::mem::take(&mut ctx.anchors);
map_query(opt, mi, &read, &mut anchors, ...);
ctx.anchors = anchors; // Return buffers
}
Chaining DP buffers (chain_bufs.predecessors, .scores, .peak_scores, .visited) and seed buffers (mini_pos) are all reused via ChainingBuffers.
Thread-Local DP Matrix Caching
thread_local! {
static CACHED_MEM: Cell<Option<AlignedMemory>> = Cell::new(None);
}
DP matrices (~17MB for typical long reads) allocated once per thread, reused across all alignments. Avoids mmap/munmap syscalls per alignment call.
Targeted Memory Zeroing
Only the DP scoring region is zeroed before each alignment. Traceback memory (95%+ of allocation) is NOT zeroed — it's written before read during DP. This reduced zeroing overhead from ~10% to <0.5% of alignment time.
9. I/O Layer
Custom FASTA/FASTQ Reader (src/fasta/reader.rs)
Zero-copy streaming parser used by the native CLI.
// Zero-copy mode (preferred for alignment):
reader.for_each_record(|record: RefRecord| {
// record.seq() returns &[u8] into reader's buffer
// No allocation per record
});
// Owned mode (when records need to outlive reader):
for record in reader.records() {
// record.seq: Vec<u8> (allocated)
}
Auto-detects FASTA (> header) vs FASTQ (@ header). Handles multi-line FASTA sequences.
fasta::open(path) peeks the first two bytes of the file for the gzip magic (0x1f 0x8b) and pipes through MultiGzDecoder if present — the extension is just informational. If the extension and the content disagree either way, a one-line warning goes to stderr and the content wins.
read_fasta(path) reads the full file into memory via std::fs::read for reference loading. Memory mapping was tried and dropped — the parser sequentially scans the whole buffer anyway, so the OS page cache covers the same access pattern without the mmap setup overhead.
Chunk-fed Streaming Parsers (src/fasta/stream.rs)
FastaStreamer and FastqStreamer are pull-shaped parsers: push raw bytes in arbitrarily-sized chunks via .push(chunk), pop completed (name, Vec<u8>) records via .next_record(), flush the in-flight record at EOF with .finalize(). They do not own the input — bytes accumulate into a small line buffer plus the in-flight sequence's Vec, and the completed records are owned hand-offs.
Used by the WASM AlignSession so the JS host can stream a multi-GB file (with optional DecompressionStream('gzip') decoding upstream) into WASM in 16 MB chunks without ever holding the full uncompressed bytes in a single allocation.
10. Testing
Unit Tests
Located in #[cfg(test)] mod tests blocks within source files. Cover:
- Radix sort correctness (sort.rs)
- Minimizer sketching (sketch.rs)
- DP correctness: simple match, mismatch, gaps, dual-affine, splice (dp.rs)
- Chain scoring (chain.rs)
- CIGAR formatting: CS, MD, DS tags (extend.rs)
- Index build/save/load round-trip (index.rs)
- Various edge cases
WASM Tests
7 WASM tests pass via wasm-pack test --node -- --lib.
11. Preset Reference
| Preset | k | w | HPC | Key Flags | Scoring (A/B/O/E/O2/E2) | Special |
|---|---|---|---|---|---|---|
| map-ont | 15 | 10 | - | - | 2/4/4/2/24/1 | Default |
| map-pb | 19 | 10 | yes | - | 2/4/4/2/24/1 | HPC |
| lr:hq | 19 | 19 | - | RMQ | 2/4/4/2/24/1 | mid_occ 50-500 |
| map-hifi | 19 | 19 | - | - | 1/4/6/2/26/1 | min_dp_max=200 |
| lr:hqae | 25 | 51 | - | RMQ,HEAP | 2/4/4/2/24/1 | rmq_inner_dist=5000 |
| map-iclr | 19 | 10 | - | - | 2/6/10/2/50/1 | transition=4 |
| asm5 | 19 | 19 | - | RMQ | 1/19/39/3/81/1 | bw=1000 |
| asm10 | 19 | 19 | - | RMQ | 1/9/16/2/41/1 | bw=1000 |
| asm20 | 19 | 10 | - | RMQ | 1/4/6/2/26/1 | bw=1000 |
| sr | 21 | 11 | - | SHORT,FRAG,HEAP,NO_2ND | 2/8/12/2/24/1 | PE FR, bw=100 |
| splice | 15 | 5 | - | SPLICE,FLANK | 1/2/2/1/32/0 | max_gap_ref=200K |
| splice:hq | 15 | 5 | - | SPLICE,FLANK | 1/4/6/1/24/0 | noncanon=5 |
| splice:sr | 15 | 5 | - | SPLICE,FRAG,HEAP,WEAK,SR_RNA | 1/4/6/1/24/0 | PE weak |
| cdna | 15 | 5 | - | SPLICE,FLANK | 1/2/2/1/32/0 | Same as splice |
| ava-ont | 15 | 5 | - | ALL_CHAINS,NO_DIAG,NO_DUAL,NO_LJOIN | 2/4/4/2/24/1 | All-vs-all |
| ava-pb | 19 | 5 | yes | ALL_CHAINS,NO_DIAG,NO_DUAL,NO_LJOIN | 2/4/4/2/24/1 | HPC, all-vs-all |
Derived values: chn_pen_gap = chain_gap_scale * 0.01 * k, chn_pen_skip = chain_skip_scale * match_score * 0.01.
12. Memory Safety
Unsafe Code Inventory
All unsafe code is confined to SIMD kernels and parallel sort:
| File | Unsafe blocks | Reason |
|---|---|---|
dp/single.rs | ~30 | SIMD intrinsics, raw pointer DP matrix access |
dp/dual.rs | ~35 | SIMD intrinsics, raw pointer DP matrix access, pointer-based traceback |
dp/splice.rs | ~35 | SIMD intrinsics, raw pointer DP matrix access, pointer-based traceback |
dp/common.rs | ~5 | SIMD helpers, memory allocation, traceback pointer access |
dp/lw.rs | ~5 | SIMD intrinsics for lightweight alignment |
chain_simd.rs | ~15 | AVX2 + NEON chaining kernels with SIMD intrinsics |
chain.rs | 2 | Dispatch calls to chain_anchors_avx2 / chain_anchors_neon |
sort.rs | 1 | from_raw_parts_mut for parallel non-overlapping mutable slice access in radix_sort_pair |
Zero unsafe in all other files: dp/mod.rs, extend.rs, pipeline.rs, map.rs,
filter.rs, seed.rs, sketch.rs, index.rs, index_bucket.rs, fasta/, api.rs,
main.rs, jump.rs, junc.rs, pair.rs, split.rs, stats.rs.
Why SIMD Requires Unsafe
Rust's std::arch SIMD intrinsics are unsafe fn for two reasons:
-
Target feature guarantee: Calling an AVX2 intrinsic on a CPU without AVX2 is undefined behavior. The caller must verify CPU support (via runtime
is_x86_feature_detected!or compile-time#[target_feature]). -
Raw pointer access: SIMD load/store operations (
_mm_loadu_si128,_mm_storeu_si128) dereference raw pointers into the DP matrix buffers.
Pure arithmetic intrinsics (_mm_add_epi8, _mm_max_epi8, etc.) are inherently
safe operations but are marked unsafe fn in std::arch by convention. As of
Rust edition 2024 (1.94+), calling these within a #[target_feature] function
no longer requires an explicit unsafe block for the target feature check —
only pointer-based operations still need it.
Fully-Safe Scalar Code Path
A complete alignment pipeline exists without any unsafe code, used as the
fallback on architectures without SIMD (or via FORCE_SCALAR=1):
Query FASTQ
→ sketch.rs (safe)
→ seed.rs (safe)
→ chain.rs scalar DP (safe)
→ filter.rs (safe)
→ extend.rs orchestration(safe)
→ dp.rs scalar kernels:
extend_single_affine_scalar (safe DP fill + safe traceback)
lightweight_align_i16_scalar (safe)
global_align_gotoh (safe)
→ pipeline.rs formatting (safe)
The scalar DP kernels produce identical results to SIMD. The safe traceback (traceback_single_affine_safe)
uses bounds-checked slice indexing instead of raw pointer arithmetic, with zero
performance difference at -O3 (compiler eliminates the bounds checks).
Index and FASTA I/O
Index loading (index.rs) and FASTA reading (fasta/reader.rs) are fully safe.
Index binary parsing uses read_u32_vec/read_u64_vec helpers that read into
byte buffers and parse with from_le_bytes. FASTA loading uses std::fs::read
(no memory mapping).