Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MEM / SMEM Finding

Exact-match queries answer “is this whole pattern present?”. Seeding a read aligner instead needs the longest exact stretches shared between a query and the reference — Maximal Exact Matches (MEMs) and Super-Maximal Exact Matches (SMEMs). These require the bidirectional index, BidirFmIndex.

Build a bidirectional index

use haystackfm::{DnaSequence, BidirFmIndex, FmIndexConfig};

let refs = vec![DnaSequence::from_str("ACGTACGTACGT")?];
let config = FmIndexConfig::default();
let bidir = BidirFmIndex::build_cpu(&refs, &config)?;

CPU MEM / SMEM

let query = DnaSequence::from_str("ACGT")?;

// Vec<Mem>. `min_len` filters short matches; `locate` resolves positions.
let smems = bidir.find_smems(query.as_slice(), /*min_len=*/18, /*locate=*/true);
let mems  = bidir.find_mems(query.as_slice(),  /*min_len=*/18, /*locate=*/true);

Both are IUPAC-aware (N matches any of A/C/G/T). Passing locate = false skips position resolution and leaves Mem::positions empty — cheaper when you only need match extents and counts.

Mem

pub struct Mem {
    pub query_start: usize,              // 0-based inclusive
    pub query_end:   usize,              // 0-based exclusive
    pub match_count: u32,                // number of occurrences
    pub positions:   Vec<(SeqId, u32)>,  // (seq id, offset) — empty when locate=false
}

positions identifies each reference by SeqId — its 0-based build order, not its FASTA name. Resolve one with bidir.seq_header(id), or build a label table once from bidir.seq_headers() and index it by id.index(). This is the hot path the id representation exists for: a conserved seed can occur in hundreds of references, and positions are resolved per occurrence.

GPU MEM / SMEM

For batches of queries, run the GPU pipeline. Queries are passed as &[DnaSequence], and reference boundaries come from the index:

#[cfg(feature = "gpu")]
async fn run(bidir: &haystackfm::BidirFmIndex, query: haystackfm::DnaSequence) -> Result<(), Box<dyn std::error::Error>> {
let boundaries = bidir.seq_boundaries();     // reference-sequence boundaries
let queries = [query.clone()];               // &[DnaSequence]

// Vec<Vec<MemHit>> — the GPU context is drawn from a process-wide cache.
let smem_hits = bidir.find_smems_gpu(&queries, /*min_len=*/18, boundaries, /*max_hits_per_mem=*/1024).await?;
let mem_hits  = bidir.find_mems_gpu(&queries,  18, boundaries, 1024).await?;
// smem_hits[query_i] = Vec<MemHit> with resolved reference positions
Ok(()) }

MemHit

pub struct MemHit {                     // GPU result type
    pub query_start: u32,
    pub query_end:   u32,
    pub match_count: u32,
    pub positions:   Vec<(SeqId, u32)>, // same shape as Mem::positions
    pub truncated:   bool,              // true if positions capped at max_hits_per_mem
}

positions has the same (SeqId, offset) shape as the CPU Mem::positions, so the same label-resolution code works against either path.

max_hits_per_mem caps how many positions each MEM resolves; when a MEM has more occurrences than the cap, truncated is set. GPU results are parity-tested against the CPU find_mems / find_smems output.