use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use anyhow::bail;
use anyhow::Context;
use fasta::record::Sequence;
use noodles::fasta;
use noodles::sam::alignment::Record;
use noodles::sam::header::record::value::{map::ReferenceSequence, Map};
use noodles::sam::record::cigar::op::Kind;
use noodles::sam::record::sequence::base::TryFromCharError;
use noodles::sam::record::sequence::Base;
use noodles::sam::record::ReferenceSequenceName;
use serde::Deserialize;
use serde::Serialize;
use crate::qc::results;
use crate::qc::ComputationalLoad;
use crate::qc::SequenceBasedQualityControlFacet;
use crate::utils::alignment::ReferenceRecordStepThrough;
use crate::utils::formats;
use crate::utils::histogram::Histogram;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct EditMetricsSummary {
pub mean_edits_read_one: f64,
pub mean_edits_read_two: f64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EditMetrics {
pub read_one_edits: Histogram,
pub read_two_edits: Histogram,
pub vaf_histogram: Histogram,
pub summary: Option<EditMetricsSummary>,
}
impl Default for EditMetrics {
fn default() -> Self {
Self {
read_one_edits: Histogram::default(),
read_two_edits: Histogram::default(),
vaf_histogram: Histogram::zero_based_with_capacity(100),
summary: None,
}
}
}
pub struct EditsFacet {
pub metrics: EditMetrics,
pub refs_per_position: Histogram,
pub alts_per_position: Histogram,
pub reference_fasta_file_path: PathBuf,
pub current_sequence: Option<Sequence>,
pub vaf_file: Option<File>,
}
impl EditsFacet {
pub fn try_from(
reference_fasta: &PathBuf,
vaf_file_path: Option<PathBuf>,
) -> anyhow::Result<Self> {
formats::fasta::open(reference_fasta).with_context(|| {
format!(
"opening reference FASTA file: {}.",
reference_fasta.display()
)
})?;
let vaf_file = match vaf_file_path {
Some(file_path) => {
if file_path.exists() {
bail!(
"refusing to overwrite existing VAF file: {}. \
Please delete and rerun if you'd like to replace it.",
file_path.display()
)
}
let mut f = File::create(file_path).with_context(|| "creating VAF file")?;
writeln!(f, "Sequence\tPosition\tVAF")
.with_context(|| "writing VAF file header")?;
Some(f)
}
None => None,
};
Ok(EditsFacet {
metrics: EditMetrics::default(),
refs_per_position: Histogram::default(),
alts_per_position: Histogram::default(),
reference_fasta_file_path: reference_fasta.clone(),
current_sequence: None,
vaf_file,
})
}
}
impl SequenceBasedQualityControlFacet for EditsFacet {
fn name(&self) -> &'static str {
"Edits"
}
fn computational_load(&self) -> ComputationalLoad {
ComputationalLoad::Heavy
}
fn supports_sequence_name(&self, _: &str) -> bool {
true
}
fn setup(
&mut self,
name: &ReferenceSequenceName,
sequence: &Map<ReferenceSequence>,
) -> anyhow::Result<()> {
let seq_name = name.to_string();
let mut fasta =
formats::fasta::open(&self.reference_fasta_file_path).with_context(|| {
format!(
"opening reference FASTA file: {}.",
self.reference_fasta_file_path.display()
)
})?;
for result in fasta.records() {
let record = result?;
if seq_name == record.name() {
self.current_sequence = Some(record.sequence().clone());
break;
}
}
if self.current_sequence.is_none() {
bail!("sequence {} not found in reference FASTA.", seq_name)
}
let seq_length = usize::from(sequence.length());
self.refs_per_position = Histogram::zero_based_with_capacity(seq_length);
self.alts_per_position = Histogram::zero_based_with_capacity(seq_length);
Ok(())
}
fn process<'b>(
&mut self,
_: &ReferenceSequenceName,
_: &Map<ReferenceSequence>,
record: &Record,
) -> anyhow::Result<()> {
if record.flags().is_unmapped() || record.flags().is_duplicate() {
return Ok(());
}
let read_name = match record.read_name() {
Some(name) => name,
_ => bail!("Could not parse read name"),
};
let cigar = record.cigar();
let reference_start = record.alignment_start().unwrap();
let reference_end = reference_start.checked_add(cigar.alignment_span()).unwrap();
let current_sequence = match &self.current_sequence {
Some(s) => s,
None => bail!(
"could not lookup reference sequence for read: {}",
read_name
),
};
let reference_seq_vec: Result<Vec<Base>, TryFromCharError> = current_sequence
.get(reference_start..reference_end)
.map(|x| x.iter().copied().map(Base::try_from))
.unwrap()
.collect();
let reference_seq = reference_seq_vec?;
let reference_seq = reference_seq.as_ref();
let record_seq = record.sequence().as_ref();
let rrs = ReferenceRecordStepThrough::new(reference_seq, record_seq, cigar.clone());
let mut edits = 0;
rrs.stepthrough(|cigar, reference_base, reference_ptr, record_base, _| {
if cigar == Kind::Match {
let reference_position = usize::from(reference_start)
.checked_add(reference_ptr)
.unwrap();
if reference_base != record_base {
edits += 1;
self.alts_per_position
.increment(reference_position)
.unwrap();
} else {
self.refs_per_position
.increment(reference_position)
.unwrap();
}
}
Ok(())
})?;
if record.flags().is_first_segment() {
self.metrics.read_one_edits.increment(edits).unwrap();
} else {
self.metrics.read_two_edits.increment(edits).unwrap();
}
Ok(())
}
fn teardown(
&mut self,
name: &ReferenceSequenceName,
_: &Map<ReferenceSequence>,
) -> anyhow::Result<()> {
let seq_name = name.to_string();
self.current_sequence = None;
for i in self.refs_per_position.range_start()..=self.refs_per_position.range_stop() {
let refs_at_this_position = self.refs_per_position.get(i);
let alts_at_this_position = self.alts_per_position.get(i);
let total_at_this_position = refs_at_this_position + alts_at_this_position;
if total_at_this_position == 0 {
continue;
}
let vaf_at_this_position = alts_at_this_position as f32 / total_at_this_position as f32;
self.metrics
.vaf_histogram
.increment((vaf_at_this_position * 100.0) as usize)
.unwrap();
if let Some(f) = &mut self.vaf_file {
writeln!(f, "{}\t{}\t{}", seq_name, i, vaf_at_this_position)
.with_context(|| "writing VAF file")?;
}
}
Ok(())
}
fn aggregate(&mut self, results: &mut results::Results) {
self.metrics.summary = Some(EditMetricsSummary {
mean_edits_read_one: self.metrics.read_one_edits.mean(),
mean_edits_read_two: self.metrics.read_two_edits.mean(),
});
results.edits = Some(self.metrics.clone());
}
}