rust_lib_reference_genome/
reference_genome.rs1
2use anyhow::ensure;
3use bio::io::fasta;
4use flate2::bufread::MultiGzDecoder;
5use indexmap::IndexMap;
6use log::{debug, warn};
7use std::io::{BufRead, BufReader};
8use std::path::{Path, PathBuf};
9
10pub struct ReferenceGenome {
12 filename: PathBuf,
14 contig_map: IndexMap<String, Vec<u8>>
16}
17
18impl ReferenceGenome {
19 pub fn empty_reference() -> Self {
21 Self {
22 filename: PathBuf::from(""),
23 contig_map: Default::default()
24 }
25 }
26
27 pub fn from_fasta(fasta_fn: &Path) -> anyhow::Result<ReferenceGenome> {
35 debug!("Loading {:?}...", fasta_fn);
36
37 let fasta_file: std::fs::File = std::fs::File::open(fasta_fn)?;
39 let file_reader = BufReader::new(fasta_file);
40 let fasta_reader: fasta::Reader<Box<dyn BufRead>> = if fasta_fn.extension().unwrap_or_default() == "gz" {
41 debug!("Detected gzip extension, loading reference with MultiGzDecoder...");
42 let gz_decoder = MultiGzDecoder::new(file_reader);
43 let bufreader = BufReader::new(gz_decoder);
44 fasta::Reader::from_bufread(Box::new(bufreader))
45 } else {
46 debug!("Loading reference as plain-text file...");
47 fasta::Reader::from_bufread(Box::new(file_reader))
48 };
49
50 let mut reference_genome = Self {
52 filename: fasta_fn.to_path_buf(),
53 contig_map: Default::default()
54 };
55
56 for entry in fasta_reader.records() {
58 let record: fasta::Record = entry?;
59 let seq_id = record.id().to_string();
60 let sequence = record.seq().to_vec();
61 reference_genome.add_contig(seq_id, sequence)?;
62 }
63 debug!("Finished loading {} contigs.", reference_genome.contig_map.len());
64
65 Ok(reference_genome)
66 }
67
68 pub fn add_contig(&mut self, contig_key: String, mut contig_sequence: Vec<u8>) -> anyhow::Result<()> {
75 ensure!(
77 !self.contig_map.contains_key(&contig_key),
78 "Duplicate contig key detected: {contig_key}"
79 );
80
81 contig_sequence.make_ascii_uppercase();
83 self.contig_map.insert(contig_key, contig_sequence);
84 Ok(())
85 }
86
87 pub fn add_contig_string(&mut self, contig_key: String, contig_sequence: String) -> anyhow::Result<()> {
94 self.add_contig(contig_key, contig_sequence.into_bytes())
95 }
96
97 pub fn filename(&self) -> &Path {
99 &self.filename
100 }
101
102 pub fn contig_keys(&self) -> impl Iterator<Item = &String> {
105 self.contig_map.keys()
106 }
107
108 pub fn get_slice(&self, chromosome: &str, start: usize, end: usize) -> anyhow::Result<&[u8]> {
118 let full_contig = self.contig_map.get(chromosome)
120 .ok_or_else(|| anyhow::anyhow!("chromosome {chromosome:?} was not in the reference file"))?;
121 ensure!(start <= end, "start > end: {start} > {end}");
122
123 let truncated_start = if start <= full_contig.len() { start } else {
125 warn!("Received get_slice({:?}, {}, {}), truncated start to {}", chromosome, start, end, full_contig.len());
126 full_contig.len()
127 };
128 let truncated_end = if end <= full_contig.len() { end } else {
129 warn!("Received get_slice({:?}, {}, {}), truncated end to {}", chromosome, start, end, full_contig.len());
130 full_contig.len()
131 };
132 Ok(&full_contig[truncated_start..truncated_end])
133 }
134
135 pub fn get_full_chromosome(&self, chromosome: &str) -> anyhow::Result<&[u8]> {
141 self.contig_map.get(chromosome)
142 .map(|s| s.as_slice())
143 .ok_or_else(|| anyhow::anyhow!("chromosome {chromosome:?} was not in the reference file"))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use std::path::PathBuf;
151
152 #[test]
153 fn test_simple_reference() {
154 let references = vec![
155 "./test_data/test_reference.fa",
156 "./test_data/test_reference.fa.gz"
157 ];
158 for &reference_fn in references.iter() {
159 let simple_reference_fn: PathBuf = PathBuf::from(reference_fn);
160 let reference_genome = ReferenceGenome::from_fasta(&simple_reference_fn).unwrap();
161
162 assert_eq!(
163 reference_genome.contig_keys().map(String::as_str).collect::<Vec<_>>(),
164 ["chr1", "chr2"]
165 );
166
167 let chr1_string: Vec<u8> = "ACGTACGT".as_bytes().to_vec();
169 for i in 0..8 {
170 assert_eq!(reference_genome.get_slice(&"chr1", i, 8).unwrap(), &chr1_string[i..]);
171 }
172
173 let chr2_string: Vec<u8> = "ACCATGTA".as_bytes().to_vec();
175 assert_eq!(reference_genome.get_slice(&"chr2", 0, 8).unwrap(), chr2_string);
176 }
177 }
178
179 #[test]
180 fn test_add_contig() {
181 let mut reference_genome = ReferenceGenome::empty_reference();
182 reference_genome.add_contig_string("test".to_string(), "Acgt".to_string()).unwrap();
183 reference_genome.add_contig_string("test2".to_string(), "TGNA".to_string()).unwrap();
184
185 assert_eq!(
186 reference_genome.contig_keys().map(String::as_str).collect::<Vec<_>>(),
187 ["test", "test2"]
188 );
189 assert_eq!(reference_genome.get_full_chromosome("test").unwrap(), b"ACGT");
190 assert_eq!(reference_genome.get_full_chromosome("test2").unwrap(), b"TGNA");
191 }
192
193 #[test]
194 fn test_lookup_errors() {
195 let mut reference_genome = ReferenceGenome::empty_reference();
196 reference_genome.add_contig("chr1".to_string(), b"ACGT".to_vec()).unwrap();
197
198 assert!(reference_genome.get_full_chromosome("chrX").is_err());
199 assert!(reference_genome.get_slice("chrX", 0, 1).is_err());
200 assert!(reference_genome.get_slice("chr1", 3, 1).is_err());
201 assert!(reference_genome.add_contig("chr1".to_string(), b"AAAA".to_vec()).is_err());
202 }
203}