ciri_toolkit/fasta.rs
1//! FASTA module: High-speed sequence loading with Needletail.
2//!
3//! This module provides the `FastaReader` struct for loading reference genome sequences
4//! into memory for rapid retrieval during junction validation.
5
6use anyhow::Result;
7use needletail::parse_fastx_file;
8use std::collections::HashMap;
9
10/// In-memory reference FASTA store.
11///
12/// The full chromosome sequence is kept as uppercase text because both Scan1 and
13/// Scan2 perform many short substring comparisons, and the reference size used by
14/// CIRI is small enough that this tradeoff is simpler and faster than on-demand I/O.
15pub struct FastaReader {
16 /// Maps from chromosome name to its sequence length.
17 pub chr_len_map: HashMap<String, usize>,
18 /// Maps from chromosome name (e.g., "chr1") to its full uppercase sequence string.
19 pub chr_tcga_map: HashMap<String, String>,
20}
21
22impl FastaReader {
23 /// Initializes an empty FASTA store.
24 pub fn new() -> Self {
25 Self {
26 chr_len_map: HashMap::new(),
27 chr_tcga_map: HashMap::new(),
28 }
29 }
30
31 /// Reads reference sequences from a FASTA file.
32 ///
33 /// `needletail` handles parsing, while this wrapper normalizes names and case
34 /// so later junction-validation code can use cheap byte/substring comparisons.
35 ///
36 /// # Arguments
37 /// * `fasta_file` - Path to the reference genome FASTA file.
38 pub fn read_fasta(&mut self, fasta_file: &str) -> Result<()> {
39 let mut reader = parse_fastx_file(fasta_file)?;
40 while let Some(record) = reader.next() {
41 let seq_record = record?;
42 // Use the first whitespace-separated part of the ID as the chromosome name.
43 let chr_name = String::from_utf8_lossy(
44 seq_record.id().split(|&b| b == b' ').next().unwrap_or(b"*"),
45 )
46 .to_string();
47 let seq = String::from_utf8_lossy(seq_record.seq().as_ref())
48 .to_string()
49 .to_uppercase();
50
51 self.chr_len_map.insert(chr_name.clone(), seq.len());
52 self.chr_tcga_map.insert(chr_name, seq);
53 }
54 Ok(())
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use std::io::Write;
62 use tempfile::NamedTempFile;
63
64 #[test]
65 fn test_fasta_reader() {
66 let mut file = NamedTempFile::new().unwrap();
67 writeln!(file, ">chr1 test\nATGCATGC\n>chr2\nGGGGCCCC").unwrap();
68
69 let mut reader = FastaReader::new();
70 reader.read_fasta(file.path().to_str().unwrap()).unwrap();
71
72 assert_eq!(reader.chr_tcga_map.get("chr1").unwrap(), "ATGCATGC");
73 assert_eq!(reader.chr_tcga_map.get("chr2").unwrap(), "GGGGCCCC");
74 assert_eq!(reader.chr_tcga_map.len(), 2);
75 }
76}