use std::fs::File;
use std::io::{BufWriter, Write};
use itertools::Itertools;
use crate::combination::CombinationMatch;
use crate::combinations::{ObservedCombinations, ReadSummary};
use crate::filters::{FilterReason, FilteredReads};
use crate::interning::region_id_to_str;
use crate::utils::div_or_zero;
pub fn write_counts(
combinations: &ObservedCombinations,
file: File,
sort: bool,
skip_variants: bool,
) -> Result<(), anyhow::Error> {
let mut writer = BufWriter::new(file);
write!(writer, "group\tforward\treverse\t")?;
for r in &combinations.region_ids {
let s = region_id_to_str(r);
if skip_variants {
write!(writer, "{s}\t{s}_nearest\t{s}_distance\t{s}_n_matches\t")?;
} else {
write!(
writer,
"{s}\t{s}_nearest\t{s}_variants\t{s}_distance\t{s}_n_matches\t"
)?;
}
}
writeln!(
writer,
"combination_status\tcombination_distance\tcombinations_in_library\tcombination_id\tcount"
)?;
for comb in combinations.to_vector(sort) {
let (fwd, rev) = match &comb.sequence {
Some(seq) => match &seq.reverse {
Some(rev) => (seq.forward.to_str_or_log(), rev.to_str_or_log()),
None => (seq.forward.to_str_or_log(), "".to_string()),
},
None => ("".to_string(), "".to_string()),
};
let region_str = &combinations
.region_ids
.iter()
.map(|k| match comb.regions.get(k) {
Some(r) => {
let (seq, nearest, diff, dist, n) = r.lock().unwrap().to_strings();
if skip_variants {
format!("{seq}\t{nearest}\t{dist}\t{n}")
} else {
format!("{seq}\t{nearest}\t{diff}\t{dist}\t{n}")
}
}
None => {
if skip_variants {
"\t\t\t".to_string()
} else {
"\t\t\t\t".to_string()
}
}
})
.join("\t");
let name = comb.library_matches.id_string()?;
let comb_str = match &comb.library_matches {
CombinationMatch::Uncompared => "uncompared\t\t\t".to_string(),
CombinationMatch::Match { distance, .. } => format!("match\t{distance}\t1\t{name}"),
CombinationMatch::MultiMatch { inds, distance } => {
let n_matches = inds
.iter()
.map(|x| match x {
Some(x) => x.len(),
None => 1,
})
.reduce(|x, y| x * y)
.unwrap_or(0);
format!("match\t{}\t{}\t{}", distance, n_matches, name)
}
CombinationMatch::Recombination { distance } => {
format!("recombination\t{distance}\t0\t",)
}
CombinationMatch::Mismatch => "mismatch\t\t0\t".to_string(),
CombinationMatch::Nonmatch => "nonmatch\t\t0\t".to_string(),
};
for (group, count) in comb.counts.iter() {
writeln!(
writer,
"{group}\t{fwd}\t{rev}\t{region_str}\t{comb_str}\t{count}"
)?;
}
}
writer.flush()?;
Ok(())
}
pub fn write_library_counts(
combinations: &ObservedCombinations,
file: File,
sort: bool,
) -> Result<(), anyhow::Error> {
let mut writer = BufWriter::new(file);
write!(writer, "group\t")?;
for r in combinations.region_ids.iter() {
write!(writer, "{}\t", region_id_to_str(r))?;
}
writeln!(
writer,
"combination_status\tcombinations_in_library\tcombination_id\tcount"
)?;
for comb in combinations.to_library_vector(sort)? {
let region_str = combinations
.region_ids
.iter()
.map(|r| match comb.regions.get(r) {
Some(r) => r.str_sequence(),
None => "".to_string(),
})
.join("\t");
let name = comb.library_matches.id_string()?;
let lib_str = match &comb.library_matches {
CombinationMatch::Uncompared => "uncompared\t\t".to_string(),
CombinationMatch::Match { .. } => format!("match\t1\t{name}"),
CombinationMatch::MultiMatch { inds, .. } => {
let n_matches = inds
.iter()
.map(|x| match x {
Some(x) => x.len(),
None => 1,
})
.reduce(|x, y| x * y)
.unwrap_or(0);
format!("match\t{}\t{}", n_matches, name)
}
CombinationMatch::Recombination { .. } => "recombination\t0\t".to_string(),
CombinationMatch::Mismatch => "mismatch\t0\t".to_string(),
CombinationMatch::Nonmatch => "nonmatch\t0\t".to_string(),
};
for (group, count) in comb.counts.iter() {
writeln!(writer, "{group}\t{region_str}\t{lib_str}\t{count}")?;
}
}
writer.flush()?;
Ok(())
}
pub fn write_summary(summary: &ReadSummary, file: File) -> Result<(), anyhow::Error> {
let mut writer = BufWriter::new(file);
let filtered_reads = &summary.filtered_reads;
let total = summary.total();
let unfiltered_total = summary.total_unfiltered();
let filtered_total = filtered_reads.total();
writeln!(
writer,
"group\tmetric\tcount\toverall_proportion\tgroup_proportion"
)?;
write_summary_row(&mut writer, "all", "total", total, total, total)?;
let rows = [
("total", unfiltered_total),
("uncompared", summary.uncompared),
("exact_match", summary.exact_match),
("nearest_match", summary.nearest_match),
("multimatch", summary.multimatch),
("exact_recombination", summary.exact_recombination),
("nearest_recombination", summary.nearest_recombination),
("mismatch", summary.mismatch),
("nonmatch", summary.nonmatch),
];
for (metric, count) in rows {
write_summary_row(
&mut writer,
"unfiltered",
metric,
count,
total,
unfiltered_total,
)?;
}
write_summary_row(
&mut writer,
"filtered",
"total",
filtered_total,
total,
filtered_total,
)?;
for reason in FilterReason::ALL_FILTERS {
write_summary_row(
&mut writer,
"filtered",
reason.meta().id,
filtered_reads.get(reason),
total,
filtered_total,
)?;
}
writer.flush()?;
Ok(())
}
#[inline]
fn write_summary_row<W: Write>(
writer: &mut W,
group: &str,
metric: &str,
count: u64,
overall_total: u64,
group_total: u64,
) -> std::io::Result<()> {
writeln!(
writer,
"{group}\t{metric}\t{count}\t{:.4}\t{:.4}",
div_or_zero(count as f32, overall_total as f32),
div_or_zero(count as f32, group_total as f32),
)
}
pub fn write_filter_summary(
filtered_reads: &FilteredReads,
file: File,
sort: bool,
) -> Result<(), anyhow::Error> {
let total = filtered_reads.total() as f32;
let mut writer = BufWriter::new(file);
writeln!(
writer,
"group\tforward\treverse\tcount\tproportion\t{}",
FilterReason::ALL_FILTERS
.iter()
.map(|r| r.meta().id)
.join("\t")
)?;
for (read, group, counts) in filtered_reads.to_vector(sort) {
writeln!(
writer,
"{}\t{}\t{}\t{}\t{:.4}\t{}",
group,
read.forward.to_str_or_log(),
read.reverse
.clone()
.map_or("".to_string(), |x| x.to_str_or_log()),
counts.total(),
div_or_zero(counts.total() as f32, total),
counts.iter().join("\t")
)?;
}
writer.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use tempfile::NamedTempFile;
use crate::combinations::{ObservedCombinations, ReadSummary};
use crate::filters::FilteredReads;
use crate::interning::{RegionID, region_id_from_str};
use crate::library::Library;
use crate::{DistanceMetric, FilterConfig};
fn empty_filter_config() -> FilterConfig {
FilterConfig::new(None, None, None, None, true)
}
fn empty_combs(r: Vec<RegionID>) -> ObservedCombinations {
ObservedCombinations::new(r, empty_filter_config())
}
#[test]
fn write_summary_row_normal() -> std::io::Result<()> {
let mut buf = Vec::new();
write_summary_row(&mut buf, "test_group", "metric_x", 100, 1000, 500)?;
let output = String::from_utf8(buf).unwrap();
assert_eq!(output, "test_group\tmetric_x\t100\t0.1000\t0.2000\n");
Ok(())
}
#[test]
fn write_summary_row_zero_count() -> std::io::Result<()> {
let mut buf = Vec::new();
write_summary_row(&mut buf, "group", "metric", 0, 100, 100)?;
let output = String::from_utf8(buf).unwrap();
assert_eq!(output, "group\tmetric\t0\t0.0000\t0.0000\n");
Ok(())
}
#[test]
fn write_summary_row_count_equals_total() -> std::io::Result<()> {
let mut buf = Vec::new();
write_summary_row(&mut buf, "group", "metric", 100, 100, 100)?;
let output = String::from_utf8(buf).unwrap();
assert_eq!(output, "group\tmetric\t100\t1.0000\t1.0000\n");
Ok(())
}
#[test]
fn write_summary_row_zero_denominators() -> std::io::Result<()> {
let mut buf = Vec::new();
write_summary_row(&mut buf, "group", "metric", 0, 0, 0)?;
let output = String::from_utf8(buf).unwrap();
assert_eq!(output, "group\tmetric\t0\t0.0000\t0.0000\n");
Ok(())
}
#[test]
fn write_summary_row_large_numbers() -> std::io::Result<()> {
let mut buf = Vec::new();
write_summary_row(
&mut buf, "group", "metric", 1_000_000, 10_000_000, 5_000_000,
)?;
let output = String::from_utf8(buf).unwrap();
assert_eq!(output, "group\tmetric\t1000000\t0.1000\t0.2000\n");
Ok(())
}
#[test]
fn write_summary_row_fractional_results() -> std::io::Result<()> {
let mut buf = Vec::new();
write_summary_row(&mut buf, "group", "metric", 1, 3, 7)?;
let output = String::from_utf8(buf).unwrap();
assert_eq!(output, "group\tmetric\t1\t0.3333\t0.1429\n");
Ok(())
}
#[test]
fn write_counts_header_structure() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let combinations = empty_combs(vec![
region_id_from_str("region1"),
region_id_from_str("region2"),
]);
write_counts(&combinations, file.reopen()?, false, false)?;
let mut content = String::new();
file.reopen()?.read_to_string(&mut content)?;
let header = content.lines().next().unwrap();
assert_eq!(
header,
"group\tforward\treverse\tregion1\tregion1_nearest\tregion1_variants\tregion1_distance\tregion1_n_matches\tregion2\tregion2_nearest\tregion2_variants\tregion2_distance\tregion2_n_matches\tcombination_status\tcombination_distance\tcombinations_in_library\tcombination_id\tcount"
);
Ok(())
}
#[test]
fn write_counts_header_structure_no_variants() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let combinations = empty_combs(vec![
region_id_from_str("region1"),
region_id_from_str("region2"),
]);
write_counts(&combinations, file.reopen()?, false, true)?;
let mut content = String::new();
file.reopen()?.read_to_string(&mut content)?;
let header = content.lines().next().unwrap();
assert_eq!(
header,
"group\tforward\treverse\tregion1\tregion1_nearest\tregion1_distance\tregion1_n_matches\tregion2\tregion2_nearest\tregion2_distance\tregion2_n_matches\tcombination_status\tcombination_distance\tcombinations_in_library\tcombination_id\tcount"
);
Ok(())
}
#[test]
fn write_library_counts_header_structure() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut combinations = empty_combs(vec![region_id_from_str("region1")]);
combinations.compare_to_library(
Library::new(Vec::new()).unwrap(),
None,
DistanceMetric::Hamming,
1,
false,
1,
)?;
write_library_counts(&combinations, file.reopen()?, false)?;
let mut content = String::new();
file.reopen()?.read_to_string(&mut content)?;
let header = content.lines().next().unwrap();
assert_eq!(
header,
"group\tregion1\tcombination_status\tcombinations_in_library\tcombination_id\tcount"
);
Ok(())
}
#[test]
fn write_summary_basic_structure() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let summary = ReadSummary::empty();
write_summary(&summary, file.reopen()?)?;
let mut content = String::new();
file.reopen()?.read_to_string(&mut content)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 17);
let header = lines[0];
assert_eq!(
header,
"group\tmetric\tcount\toverall_proportion\tgroup_proportion"
);
Ok(())
}
#[test]
fn write_summary_all_rows_present() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut summary = ReadSummary::empty();
summary.exact_match = 50;
summary.nearest_match = 30;
summary.mismatch = 20;
write_summary(&summary, file.reopen()?)?;
let mut content = String::new();
file.reopen()?.read_to_string(&mut content)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 17);
assert!(content.contains("50"));
assert!(content.contains("30"));
assert!(content.contains("20"));
assert!(content.contains("100"));
Ok(())
}
#[test]
fn write_filter_summary_header() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let filtered = FilteredReads::new(empty_filter_config());
write_filter_summary(&filtered, file.reopen()?, false)?;
let mut content = String::new();
file.reopen()?.read_to_string(&mut content)?;
let header = content.lines().next().unwrap();
assert_eq!(
header,
"group\tforward\treverse\tcount\tproportion\tempty_read\tshort_read\tlong_read low_mean_quality\tbad_alignment"
);
Ok(())
}
#[test]
fn write_filter_summary_empty() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let filtered = FilteredReads::new(empty_filter_config());
write_filter_summary(&filtered, file.reopen()?, false)?;
let mut content = String::new();
file.reopen()?.read_to_string(&mut content)?;
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 1);
Ok(())
}
}