Skip to main content

rust_lib_reference_genome/
reference_genome.rs

1
2use bio::io::fasta;
3use flate2::bufread::MultiGzDecoder;
4use log::{debug, warn};
5use rustc_hash::FxHashMap as HashMap;
6use simple_error::{bail, SimpleError};
7use std::io::{BufRead, BufReader};
8use std::path::{Path, PathBuf};
9
10/// Wrapper structure for a reference genome
11pub struct ReferenceGenome {
12    /// The filename we loaded 
13    filename: PathBuf,
14    /// Contains the keys in order of the reference load
15    contig_keys: Vec<String>,
16    /// Map where keys are contig names and value is ASCII formatted sequence
17    contig_map: HashMap<String, Vec<u8>>
18}
19
20impl ReferenceGenome {
21    /// Creates an empty reference genome, which we can be populated through `add_contig(...)`
22    pub fn empty_reference() -> Self {
23        Self {
24            filename: PathBuf::from(""),
25            contig_keys: vec![],
26            contig_map: Default::default()
27        }
28    }
29
30    /// Loads a reference genome from a given FASTA file
31    /// # Arguments
32    /// * `fasta_fn` - the FASTA filename, gzip is allowed
33    /// # Errors
34    /// This will pass through any error detected from loading the provided FASTA file.
35    /// This includes file reading and/or record reading errors.
36    pub fn from_fasta(fasta_fn: &Path) -> Result<ReferenceGenome, Box<dyn std::error::Error>> {
37        debug!("Loading {:?}...", fasta_fn);
38        let mut contig_keys: Vec<String> = Default::default();
39        let mut contig_map: HashMap<String, Vec<u8>> = Default::default();
40        
41        // needletail can technically read FASTA and FASTQ, not sure we can check for that easy though
42        let fasta_file: std::fs::File = std::fs::File::open(fasta_fn)?;
43        let file_reader = BufReader::new(fasta_file);
44        let fasta_reader: fasta::Reader<Box<dyn BufRead>> = if fasta_fn.extension().unwrap_or_default() == "gz" {
45            debug!("Detected gzip extension, loading reference with MultiGzDecoder...");
46            let gz_decoder = MultiGzDecoder::new(file_reader);
47            let bufreader = BufReader::new(gz_decoder);
48            fasta::Reader::from_bufread(Box::new(bufreader))
49        } else {
50            debug!("Loading reference as plain-text file...");
51            fasta::Reader::from_bufread(Box::new(file_reader))
52        };
53
54        for entry in fasta_reader.records() {
55            let record: fasta::Record = entry?;
56            let seq_id: String = record.id().to_string();
57            let sequence: Vec<u8> = record.seq().to_ascii_uppercase();
58
59            contig_keys.push(seq_id.clone());
60            contig_map.insert(seq_id, sequence);
61        }
62        debug!("Finished loading {} contigs.", contig_map.len());
63
64        Ok(ReferenceGenome {
65            filename: fasta_fn.to_path_buf(),
66            contig_keys,
67            contig_map
68        })
69    }
70
71    /// Adds a new contig to the reference genome
72    /// # Arguments
73    /// * `contig_key` - the name of the contig
74    /// * `contig_sequence` - the sequence to add; all sequence is automatically upper-cased
75    pub fn add_contig(&mut self, contig_key: String, contig_sequence: &str) -> Result<(), SimpleError> {
76        if self.contig_map.contains_key(&contig_key) {
77            bail!("Contig key \"{contig_key}\" is already in the reference genome");
78        }
79
80        // create the uppercase byte form
81        let byte_form = contig_sequence.to_ascii_uppercase().into_bytes();
82        
83        // save everything
84        self.contig_keys.push(contig_key.clone());
85        self.contig_map.insert(contig_key, byte_form);
86        Ok(())
87    }
88
89    pub fn filename(&self) -> &Path {
90        &self.filename
91    }
92
93    pub fn contig_keys(&self) -> &[String] {
94        &self.contig_keys
95    }
96
97    /// Retrieves a reference slice from a given 0-based coordinates.
98    /// If `start` or `end` goes past the full contig length, it will be truncated to the full contig length.
99    /// # Arguments
100    /// * `chromosome` - the chromosome to slice from
101    /// * `start` - the 0-based start index (included)
102    /// * `end` - the 0-based end index (excluded)
103    /// # Panics
104    /// * if `chromosome` was not in the FASTA file
105    /// * if `start` > `end`
106    pub fn get_slice(&self, chromosome: &str, start: usize, end: usize) -> &[u8] {
107        let full_contig = self.contig_map.get(chromosome).expect("a chromosome from the reference file");
108        assert!(start <= end, "start > end: {start} > {end}");
109        let truncated_start = if start <= full_contig.len() { start } else {
110            warn!("Received get_slice({:?}, {}, {}), truncated start to {}", chromosome, start, end, full_contig.len());
111            full_contig.len()
112        };
113        let truncated_end = if end <= full_contig.len() { end } else {
114            warn!("Received get_slice({:?}, {}, {}), truncated end to {}", chromosome, start, end, full_contig.len());
115            full_contig.len()
116        };
117        &full_contig[truncated_start..truncated_end]
118    }
119
120    /// Retrieves a full chromosome by name
121    /// # Arguments
122    /// * `chromosome` - the chromosome to slice from
123    /// # Panics
124    /// * if `chromosome` was not in the FASTA file
125    pub fn get_full_chromosome(&self, chromosome: &str) -> &[u8] {
126        let full_contig = self.contig_map.get(chromosome).expect("a chromosome from the reference file");
127        full_contig
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use std::path::PathBuf;
135    #[test]
136    fn test_simple_reference() {
137        let references = vec![
138            "./test_data/test_reference.fa",
139            "./test_data/test_reference.fa.gz"
140        ];
141        for &reference_fn in references.iter() {
142            let simple_reference_fn: PathBuf = PathBuf::from(reference_fn);
143            let reference_genome = ReferenceGenome::from_fasta(&simple_reference_fn).unwrap();
144
145            assert_eq!(reference_genome.contig_keys(), &[
146                "chr1".to_string(),
147                "chr2".to_string()
148            ]);
149
150            //chr1 = ACGTACGT
151            let chr1_string: Vec<u8> = "ACGTACGT".as_bytes().to_vec();
152            for i in 0..8 {
153                assert_eq!(reference_genome.get_slice(&"chr1", i, 8), &chr1_string[i..]);
154            }
155
156            //chr2 = ACCATGTA
157            let chr1_string: Vec<u8> = "ACCATGTA".as_bytes().to_vec();
158            assert_eq!(reference_genome.get_slice(&"chr2", 0, 8), chr1_string);
159        }
160    }
161
162    #[test]
163    fn test_add_contig() {
164        let mut reference_genome = ReferenceGenome::empty_reference();
165        reference_genome.add_contig("test".to_string(), "Acgt").unwrap();
166        reference_genome.add_contig("test2".to_string(), "TGNA").unwrap();
167
168        assert_eq!(reference_genome.contig_keys(), &["test".to_string(), "test2".to_string()]);
169        assert_eq!(reference_genome.get_full_chromosome("test"), b"ACGT");
170        assert_eq!(reference_genome.get_full_chromosome("test2"), b"TGNA");
171    }
172}