use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::rc::Rc;
use noodles::sam::alignment::Record;
use noodles::sam::header::record::value::map::Map;
use noodles::sam::header::record::value::map::ReferenceSequence;
use noodles::sam::record::ReferenceSequenceName;
use serde::Deserialize;
use serde::Serialize;
use tracing::error;
use crate::qc::results;
use crate::qc::ComputationalLoad;
use crate::qc::SequenceBasedQualityControlFacet;
use crate::utils::genome::get_primary_assembly;
use crate::utils::genome::ReferenceGenome;
use crate::utils::genome::Sequence;
use crate::utils::histogram::Histogram;
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct IgnoredMetrics {
pub nonsensical_records: usize,
pub pileup_too_large_positions: HashMap<String, usize>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct CoverageMetrics {
pub mean_coverage: HashMap<String, f64>,
pub mean_coverage_per_bin: HashMap<String, Vec<f64>>,
pub median_coverage: HashMap<String, f64>,
pub median_over_mean_coverage: HashMap<String, f64>,
pub ignored: IgnoredMetrics,
pub coverage_distribution: Histogram,
pub genome_covered_by: HashMap<String, f32>,
}
const COVERAGE_DISTRIBUTION_HISTOGRAM_SIZE: usize = 2048;
impl Default for CoverageMetrics {
fn default() -> Self {
Self {
mean_coverage: Default::default(),
mean_coverage_per_bin: Default::default(),
median_coverage: Default::default(),
median_over_mean_coverage: Default::default(),
ignored: Default::default(),
coverage_distribution: Histogram::zero_based_with_capacity(
COVERAGE_DISTRIBUTION_HISTOGRAM_SIZE,
),
genome_covered_by: Default::default(),
}
}
}
pub struct CoverageFacet {
coverage_per_position: HashMap<String, Histogram>,
metrics: CoverageMetrics,
primary_assembly: Vec<Sequence>,
bin_size: NonZeroUsize,
}
impl CoverageFacet {
pub fn new(reference_genome: Rc<Box<dyn ReferenceGenome>>, bin_size: NonZeroUsize) -> Self {
Self {
coverage_per_position: HashMap::default(),
metrics: CoverageMetrics::default(),
primary_assembly: get_primary_assembly(reference_genome),
bin_size,
}
}
}
impl SequenceBasedQualityControlFacet for CoverageFacet {
fn name(&self) -> &'static str {
"Coverage"
}
fn computational_load(&self) -> ComputationalLoad {
ComputationalLoad::Moderate
}
fn supports_sequence_name(&self, name: &str) -> bool {
self.primary_assembly
.iter()
.map(|s| s.name())
.any(|x| x == name)
}
fn setup(
&mut self,
_: &ReferenceSequenceName,
_: &Map<ReferenceSequence>,
) -> anyhow::Result<()> {
Ok(())
}
fn process(
&mut self,
name: &ReferenceSequenceName,
sequence: &Map<ReferenceSequence>,
record: &Record,
) -> anyhow::Result<()> {
let h = self
.coverage_per_position
.entry(name.to_string())
.or_insert_with(|| Histogram::zero_based_with_capacity(usize::from(sequence.length())));
let record_start = usize::from(record.alignment_start().unwrap());
let record_end = usize::from(record.alignment_end().unwrap());
for i in record_start..=record_end {
if h.increment(i).is_err() {
error!(
"Record crosses the sequence boundaries in an expected way. \
This usually means that the record is malformed. Please examine \
the record closely to ensure it fits within the sequence. \
Ignoring record. Read name: {}, Start Alignment: {}, End \
Alignment: {}, Cigar: {}",
record.read_name().unwrap(),
record.alignment_start().unwrap(),
record.alignment_end().unwrap(),
record.cigar()
);
self.metrics.ignored.nonsensical_records += 1;
}
}
Ok(())
}
fn teardown(
&mut self,
name: &ReferenceSequenceName,
_: &Map<ReferenceSequence>,
) -> anyhow::Result<()> {
let positions = match self.coverage_per_position.get(&name.to_string()) {
Some(s) => s,
None => return Ok(()),
};
let mut coverages =
Histogram::zero_based_with_capacity(COVERAGE_DISTRIBUTION_HISTOGRAM_SIZE);
let mut ignored = 0;
let mut total_coverage_for_bin = 0;
let coverage_per_bin_vec = self
.metrics
.mean_coverage_per_bin
.entry(name.to_string())
.or_default();
for i in positions.range_start()..=positions.range_stop() {
let coverage_at_position = positions.get(i);
if coverages.increment(coverage_at_position).is_err() {
ignored += 1;
}
total_coverage_for_bin += coverage_at_position;
if i % self.bin_size == 0 {
let mean = total_coverage_for_bin as f64 / usize::from(self.bin_size) as f64;
coverage_per_bin_vec.push(mean);
total_coverage_for_bin = 0;
}
}
let modulo = positions.range_stop() % self.bin_size;
if modulo != 0 {
let mean = total_coverage_for_bin as f64 / modulo as f64;
coverage_per_bin_vec.push(mean);
}
let mean = coverages.mean();
let median = coverages.median().unwrap();
let median_over_mean = median / mean;
self.coverage_per_position.remove(&name.to_string());
for i in coverages.range_start()..=coverages.range_stop() {
self.metrics
.coverage_distribution
.increment_by(i, coverages.get(i))
.unwrap();
}
self.metrics.mean_coverage.insert(name.to_string(), mean);
self.metrics
.median_coverage
.insert(name.to_string(), median);
self.metrics
.median_over_mean_coverage
.insert(name.to_string(), median_over_mean);
self.metrics
.ignored
.pileup_too_large_positions
.insert(name.to_string(), ignored);
Ok(())
}
fn aggregate(&mut self, results: &mut results::Results) {
let mut total_positions = self.metrics.coverage_distribution.sum();
for v in self.metrics.ignored.pileup_too_large_positions.values() {
total_positions += v;
}
const COVERAGES_TO_CHECK: [usize; 6] = [10, 20, 30, 40, 50, 60];
for c in COVERAGES_TO_CHECK {
let k = format!("{}x", c);
let positions_supported_by_at_least_nx =
self.metrics.coverage_distribution.count_from_top_until(c);
let v = (positions_supported_by_at_least_nx as f32 / total_positions as f32) * 100.0;
self.metrics.genome_covered_by.insert(k, v);
}
results.coverage = Some(self.metrics.clone());
}
}