Skip to content

Commit b3a500f

Browse files
committed
Improve CLI alignment workflow
1 parent c34ad8c commit b3a500f

3 files changed

Lines changed: 198 additions & 66 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ cargo run --release -- variants \
216216
--output examples/data/variants.vcf
217217
```
218218

219+
> **Note:** `rosalind align` indexes the first FASTA record via the O(√t) FM-index before emitting SAM/BAM output. Additional records are ignored (a warning is printed) until multi-contig sequencing is supported.
220+
219221
Key knobs:
220222
- `--max-mismatches` bounds per-read Hamming distance during seeding.
221223
- `--format {sam|bam}` toggles between plain-text SAM and BGZF-compressed BAM (requires `--output` for BAM).

src/genomics/fm_index.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,47 @@ impl BlockedFMIndex {
299299
pub fn boundaries(&self) -> &CompressedBoundaries {
300300
&self.boundaries
301301
}
302+
303+
/// Retrieve the symbol stored at `index` in the BWT string.
304+
pub fn symbol_at(&self, index: usize) -> FmSymbol {
305+
assert!(index < self.bwt_len, "BWT index out of range");
306+
if index == self.sentinel_pos {
307+
return FmSymbol::Sentinel;
308+
}
309+
310+
let block_idx = index / self.block_size;
311+
let block = &self.blocks[block_idx];
312+
let offset = index - block.start;
313+
let base = block
314+
.bwt
315+
.base_at(offset)
316+
.expect("BWT block should contain sequence data");
317+
let code = BaseCode::from_ascii(base)
318+
.expect("BWT symbol must be a valid DNA base except sentinel");
319+
FmSymbol::Base(code)
320+
}
321+
322+
fn lf_index(&self, index: usize) -> usize {
323+
let symbol = self.symbol_at(index);
324+
let occ_inclusive = self.rank(symbol, index + 1);
325+
let c_row = self.c_table()[symbol.order()] as usize;
326+
c_row + occ_inclusive as usize - 1
327+
}
328+
329+
/// Compute the suffix array value corresponding to the provided BWT index.
330+
pub fn sa_at(&self, index: usize) -> usize {
331+
let mut current = index;
332+
let mut steps = 0usize;
333+
334+
loop {
335+
let symbol = self.symbol_at(current);
336+
if symbol == FmSymbol::Sentinel {
337+
return steps;
338+
}
339+
current = self.lf_index(current);
340+
steps += 1;
341+
}
342+
}
302343
}
303344

304345
fn sanitize_reference(reference: &[u8]) -> Result<Vec<u8>, FMIndexError> {
@@ -391,6 +432,7 @@ fn build_c_table(totals: [u32; ALPHABET_SIZE]) -> [u32; 6] {
391432
#[cfg(test)]
392433
mod tests {
393434
use super::*;
435+
use crate::genomics::BWTAligner;
394436

395437
#[test]
396438
fn fm_index_builds_and_ranks() {
@@ -420,4 +462,19 @@ mod tests {
420462
let bounded = position.min(bwt.len());
421463
bwt[..bounded].iter().filter(|&&ch| ch == base).count() as u32
422464
}
465+
466+
#[test]
467+
fn sa_at_recovers_reference_position() {
468+
let reference = b"ACGTACGT";
469+
let mut aligner = BWTAligner::new(reference).expect("aligner should initialize");
470+
let result = aligner
471+
.align_read(b"ACGT")
472+
.expect("alignment should succeed");
473+
assert!(result.has_candidates());
474+
475+
let index = BlockedFMIndex::build(reference, 4).expect("index build should succeed");
476+
let position = index.sa_at(result.interval.lower as usize);
477+
assert!(position + 4 <= reference.len());
478+
assert_eq!(&reference[position..position + 4], b"ACGT");
479+
}
423480
}

0 commit comments

Comments
 (0)