pub(crate) use crate::structures::{Region, Strand, Transcript, TranscriptRegion};
use anyhow::Result;
use bio::alphabets::dna;
use bio::data_structures::interval_tree::IntervalTree;
use bio::io::fasta;
use std::collections::HashMap;
impl Transcript {
pub fn new(id: String, chromosome: String, mut regions: Vec<Region>) -> Result<Self> {
if regions.is_empty() {
anyhow::bail!("Transcript {} has no regions.", id);
}
let first_strand = regions[0].strand;
if regions.iter().any(|r| r.strand != first_strand) {
anyhow::bail!("Transcript {} has mixed strands.", id);
}
match first_strand {
Strand::Plus => regions.sort_by_key(|r| r.start),
Strand::Minus => regions.sort_by(|a, b| b.start.cmp(&a.start)),
}
let mut interval_tree: IntervalTree<usize, &Region> = IntervalTree::new();
for region in ®ions {
if region.start > region.end {
panic!("Negative width region {}..{}, region {} strand {}", region.start, region.end, region.id, region.strand);
} else if region.end - region.start + 1 <= 3 {
println!(" Suspicious: {} is only {} nucleoptide length: {} .. {}"
, region.id, region.end - region.start + 1, region.start, region.end);
}
let interval = region.start..region.end + 1; if let Some(overlap) = interval_tree.find(interval.clone()).next() {
panic!(
"Transcript {} in chromosome {} has overlapping regions: {} and {} overlap with interval {:?}.",
id,
chromosome,
region.id,
overlap.data().id,
overlap.interval()
);
}
interval_tree.insert(interval, region);
}
Ok(Self {
id,
chromosome,
regions,
})
}
}
pub fn build_transcripts_from_regions(
transcript_regions: Vec<TranscriptRegion>,
) -> Result<Vec<Transcript>> {
let mut transcript_map: HashMap<String, (String, Vec<Region>)> = HashMap::new();
for tr in transcript_regions {
let entry = transcript_map
.entry(tr.transcript_id.clone())
.or_insert_with(|| (tr.chromosome.clone(), Vec::new()));
if entry.0 != tr.chromosome {
panic!(
"Transcript {} has regions from multiple chromosomes: {} vs {}",
tr.transcript_id,
entry.0,
tr.chromosome
);
}
entry.1.push(Region {
id: tr.region_id.clone(),
start: tr.start,
end: tr.end,
strand: tr.strand,
});
}
let mut transcripts = Vec::new();
for (id, (chromosome, regions)) in transcript_map {
let transcript = Transcript::new(id, chromosome, regions)?;
transcripts.push(transcript);
}
Ok(transcripts)
}
fn load_genome_to_memory(fasta_path: &str) -> Result<HashMap<String, Vec<u8>>> {
let reader = fasta::Reader::from_file(fasta_path)?;
let mut genome = HashMap::new();
for record in reader.records() {
let record = record?;
genome.insert(record.id().to_owned(), record.seq().to_owned());
}
Ok(genome)
}
fn extract_transcript_sequence(
genome: &HashMap<String, Vec<u8>>,
transcript: &Transcript,
) -> Result<Vec<u8>> {
let chromosome_seq = genome.get(&transcript.chromosome).ok_or_else(|| {
anyhow::anyhow!(
"Chromosome '{}' not found in genome.",
transcript.chromosome
)
})?;
let mut sequence = Vec::with_capacity(transcript.size());
let mut sorted_regions = transcript.regions.clone();
sorted_regions.sort_by_key(|r| r.start);
for region in &sorted_regions {
let start = region.start - 1;
let end = region.end;
sequence.extend_from_slice(&chromosome_seq[start..end]);
}
if transcript.regions[0].strand == Strand::Minus {
sequence = dna::revcomp(sequence);
}
Ok(sequence)
}
pub fn build_transcriptome_sequences(
transcripts: &[Transcript],
genome_fasta_path: &str,
output_fasta_path: &str,
) -> Result<()> {
let genome = load_genome_to_memory(genome_fasta_path)?;
let mut writer = fasta::Writer::to_file(output_fasta_path)?;
for transcript in transcripts {
let seq = extract_transcript_sequence(&genome, transcript)?;
writer.write(&transcript.id, None, &seq)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn build_region(id: &str, start: usize, end: usize, strand: Strand) -> Region {
Region { id: id.to_string(), start, end, strand }
}
#[test]
fn test_transcript_sort_plus() {
let r1 = build_region("r1", 20, 25, Strand::Plus);
let r2 = build_region("r2", 10, 15, Strand::Plus);
let t = Transcript::new("tx1".into(), "chr1".into(), vec![r1, r2]).unwrap();
assert!(t.regions[0].start < t.regions[1].start);
}
#[test]
fn test_transcript_sort_minus() {
let r1 = build_region("r1", 10, 15, Strand::Minus);
let r2 = build_region("r2", 20, 25, Strand::Minus);
let t = Transcript::new("tx1".into(), "chr1".into(), vec![r1, r2]).unwrap();
assert!(t.regions[0].start > t.regions[1].start);
}
#[test]
#[should_panic]
fn test_transcript_overlap_panic() {
let r1 = build_region("r1", 10, 20, Strand::Plus);
let r2 = build_region("r2", 15, 25, Strand::Plus);
Transcript::new("tx1".into(), "chr1".into(), vec![r1, r2]).unwrap();
}
#[test]
fn test_extract_transcript_sequence_plus() {
let genome = HashMap::from([("chr1".to_string(), b"ACGTAACCGGTT".to_vec())]);
let regions = vec![build_region("r1", 1, 4, Strand::Plus), build_region("r2", 5, 8, Strand::Plus)];
let t = Transcript::new("tx1".into(), "chr1".into(), regions).unwrap();
let seq = extract_transcript_sequence(&genome, &t).unwrap();
assert_eq!(seq, b"ACGTAACC");
}
#[test]
fn test_extract_transcript_sequence_minus() {
let genome = HashMap::from([("chr1".to_string(), b"ACGTAACCGGTT".to_vec())]);
let regions = vec![build_region("r1", 1, 4, Strand::Minus), build_region("r2", 5, 8, Strand::Minus)];
let t = Transcript::new("tx1".into(), "chr1".into(), regions).unwrap();
let seq = extract_transcript_sequence(&genome, &t).unwrap();
assert_eq!(seq, b"GGTTACGT");
}
#[test]
fn test_build_transcripts_from_regions() {
let trs = vec![TranscriptRegion { chromosome: "chr1".into(), start: 1, end: 3, strand: Strand::Plus, transcript_id: "tx1".into(), region_id: "r1".into(), gene_id: None },
TranscriptRegion { chromosome: "chr1".into(), start: 5, end: 6, strand: Strand::Plus, transcript_id: "tx1".into(), region_id: "r2".into(), gene_id: None }];
let ts = build_transcripts_from_regions(trs).unwrap();
assert_eq!(ts.len(), 1);
assert_eq!(ts[0].regions.len(), 2);
}
}