use super::chrom_names_and_lengths;
use super::fdr::{lookup_fdr, FdrEntry};
use crate::cli::CallPeaksOptions;
use crate::subcommands::pileup::{
FiberseqPileup, FiberseqPileupOptions, FireTrack, FireTrackOptions,
};
use crate::utils::bio_io;
use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::io::Write;
pub fn reciprocal_overlap_raw(
a_chrom: &str,
a_start: usize,
a_end: usize,
b_chrom: &str,
b_start: usize,
b_end: usize,
) -> f64 {
if a_chrom != b_chrom {
return 0.0;
}
let overlap_start = a_start.max(b_start);
let overlap_end = a_end.min(b_end);
if overlap_start >= overlap_end {
return 0.0;
}
let overlap_len = (overlap_end - overlap_start) as f64;
let a_len = (a_end - a_start) as f64;
let b_len = (b_end - b_start) as f64;
(overlap_len / a_len).min(overlap_len / b_len)
}
#[derive(Debug, Clone, Copy)]
struct PeakThresholds {
max_fdr: f64,
min_fire_frac: Option<f64>,
min_fire_frac_filter: f64,
min_cov: i32,
max_cov: i32,
}
#[derive(Debug)]
pub struct Peak<'a> {
pub chrom: String,
pub start: usize,
pub end: usize,
pub score: f32,
pub fdr: f64,
pub pass_coverage: bool,
pub peak_index: usize,
pub pileup: &'a FiberseqPileup<'a>,
}
impl<'a> Peak<'a> {
pub fn header() -> String {
let mut header = String::from("#chrom\tpeak_start\tpeak_end\tpeak_max\tFDR");
for suffix in &["", "_H1", "_H2"] {
header.push_str(&format!(
"\tcoverage{suffix}\tfire_coverage{suffix}\tscore{suffix}\tnuc_coverage{suffix}\tmsp_coverage{suffix}"
));
}
header.push_str("\tpass_coverage");
header
}
fn format_fire_track(&self, track: &FireTrack) -> String {
format!(
"{}\t{}\t{:.5}\t{}\t{}\t",
track.coverage[self.peak_index],
track.fire_coverage[self.peak_index],
track.scores[self.peak_index],
track.nuc_coverage[self.peak_index],
track.msp_coverage[self.peak_index],
)
}
}
impl<'a> std::fmt::Display for Peak<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let local_max = self.pileup.chrom_start + self.peak_index;
let mut output = format!(
"{}\t{}\t{}\t{}\t{:.10}\t",
self.chrom, self.start, self.end, local_max, self.fdr, );
output.push_str(&self.format_fire_track(&self.pileup.all_data));
for t in [&self.pileup.hap1_data, &self.pileup.hap2_data] {
if let Some(ref track) = t {
output.push_str(&self.format_fire_track(track));
} else {
output.push_str("0\t0\t-1.0\t0\t0\t");
}
}
let pass_cov_str = if self.pass_coverage { "true" } else { "false" };
output.push_str(pass_cov_str);
write!(f, "{}", output)
}
}
impl<'a> Peak<'a> {
pub fn from_pileup(
pileup: &'a FiberseqPileup<'a>,
fdr_table: &[FdrEntry],
max_fdr: f64,
min_fire_frac: Option<f64>,
min_fire_frac_filter: f64,
min_cov: i32,
max_cov: i32,
) -> Vec<Self> {
let mut peaks = Vec::new();
let scores = &pileup.all_data.scores;
let rolling_max_scores = pileup
.rolling_max
.as_ref()
.expect("Rolling max scores should be calculated");
let thresholds = PeakThresholds {
max_fdr,
min_fire_frac,
min_fire_frac_filter,
min_cov,
max_cov,
};
let mut consecutive_maxima = Vec::new();
for i in 0..scores.len() {
if scores[i] < 0.0 {
if !consecutive_maxima.is_empty() {
if let Some(peak) = Self::create_peak_if_significant(
pileup,
consecutive_maxima.as_slice(),
fdr_table,
&thresholds,
) {
peaks.push(peak);
}
consecutive_maxima.clear();
}
continue;
}
if (scores[i] - rolling_max_scores[i]).abs() < 1e-6 {
consecutive_maxima.push(i);
} else {
if !consecutive_maxima.is_empty() {
if let Some(peak) = Self::create_peak_if_significant(
pileup,
consecutive_maxima.as_slice(),
fdr_table,
&thresholds,
) {
peaks.push(peak);
}
consecutive_maxima.clear();
}
}
}
if !consecutive_maxima.is_empty() {
if let Some(peak) = Self::create_peak_if_significant(
pileup,
consecutive_maxima.as_slice(),
fdr_table,
&thresholds,
) {
peaks.push(peak);
}
}
peaks
}
fn create_peak_if_significant(
pileup: &'a FiberseqPileup<'a>,
positions: &[usize],
fdr_table: &[FdrEntry],
thresholds: &PeakThresholds,
) -> Option<Self> {
if positions.is_empty() {
return None;
}
let middle_idx = positions.len() / 2;
let middle_pos = positions[middle_idx];
let score = pileup.all_data.scores[middle_pos];
let coverage = pileup.all_data.coverage[middle_pos] as f64;
let fire_cov = pileup.all_data.fire_coverage[middle_pos] as f64;
let fire_frac = fire_cov / coverage;
let passes_threshold = if let Some(min_frac) = thresholds.min_fire_frac {
fire_frac >= min_frac
} else {
let fdr = lookup_fdr(score, fdr_table);
fdr <= thresholds.max_fdr && fire_frac >= thresholds.min_fire_frac_filter
};
let fdr = lookup_fdr(score, fdr_table);
let coverage = pileup.all_data.coverage[middle_pos];
let pass_coverage = coverage >= thresholds.min_cov && coverage <= thresholds.max_cov;
if passes_threshold {
let (start, end) = if let Some(ref fire_elements_vec) = pileup.all_data.fire_elements {
let mut starts = Vec::new();
let mut ends = Vec::new();
for &pos in positions {
for fire_elem in &fire_elements_vec[pos] {
starts.push(fire_elem.start);
ends.push(fire_elem.end);
}
}
if starts.is_empty() {
let start = pileup.chrom_start + positions[0];
let end = pileup.chrom_start + positions[positions.len() - 1] + 1;
(start, end)
} else {
starts.sort_unstable();
ends.sort_unstable();
let median_start = starts[starts.len() / 2] as usize;
let median_end = ends[ends.len() / 2] as usize;
(median_start, median_end)
}
} else {
let start = pileup.chrom_start + positions[0];
let end = pileup.chrom_start + positions[positions.len() - 1] + 1;
(start, end)
};
Some(Self {
chrom: pileup.chrom.clone(),
start,
end,
score,
fdr,
pass_coverage,
peak_index: middle_pos,
pileup,
})
} else {
None
}
}
pub fn get_fire_ids(&self) -> HashSet<usize> {
let mut fire_ids = HashSet::new();
if let Some(ref fire_elements_vec) = self.pileup.all_data.fire_elements {
assert!(
self.peak_index < fire_elements_vec.len(),
"peak_index {} out of bounds (len: {})",
self.peak_index,
fire_elements_vec.len()
);
for fire_elem in &fire_elements_vec[self.peak_index] {
fire_ids.insert(fire_elem.id);
}
}
fire_ids
}
pub fn get_fire_elements(&self) -> HashMap<usize, (i64, i64)> {
let mut fire_elements = HashMap::new();
if let Some(ref fire_elements_vec) = self.pileup.all_data.fire_elements {
if self.peak_index < fire_elements_vec.len() {
for fire_elem in &fire_elements_vec[self.peak_index] {
fire_elements.insert(fire_elem.id, (fire_elem.start, fire_elem.end));
}
}
}
fire_elements
}
pub fn fire_overlap_fraction(&self, other: &Peak) -> f64 {
let self_ids = self.get_fire_ids();
let other_ids = other.get_fire_ids();
if self_ids.is_empty() || other_ids.is_empty() {
return 0.0;
}
let intersection_count = self_ids.intersection(&other_ids).count();
let min_count = self_ids.len().min(other_ids.len());
intersection_count as f64 / min_count as f64
}
pub fn reciprocal_overlap(&self, other: &Peak) -> f64 {
reciprocal_overlap_raw(
&self.chrom,
self.start,
self.end,
&other.chrom,
other.start,
other.end,
)
}
pub fn should_merge_with(
&self,
other: &Peak,
min_fire_overlap: f64,
min_reciprocal_overlap: f64,
) -> bool {
if self.chrom != other.chrom {
return false;
}
if min_reciprocal_overlap > 0.0 {
let recip_overlap = self.reciprocal_overlap(other);
if recip_overlap >= min_reciprocal_overlap {
return true;
}
}
if min_fire_overlap > 0.0 {
let fire_overlap = self.fire_overlap_fraction(other);
if fire_overlap >= min_fire_overlap {
return true;
}
}
false
}
}
fn merge_peak_group<'a>(peaks: &[&Peak<'a>]) -> Peak<'a> {
assert!(!peaks.is_empty(), "Cannot merge empty peak group");
let best_peak = peaks
.iter()
.max_by(|a, b| a.score.partial_cmp(&b.score).unwrap())
.unwrap();
let mut all_fire_elements: HashMap<usize, (i64, i64)> = HashMap::new();
for peak in peaks {
all_fire_elements.extend(peak.get_fire_elements());
}
let (merged_start, merged_end) = if all_fire_elements.is_empty() {
(best_peak.start, best_peak.end)
} else {
let mut starts: Vec<i64> = all_fire_elements.values().map(|(s, _)| *s).collect();
let mut ends: Vec<i64> = all_fire_elements.values().map(|(_, e)| *e).collect();
starts.sort_unstable();
ends.sort_unstable();
let median_start = starts[starts.len() / 2] as usize;
let median_end = ends[ends.len() / 2] as usize;
(median_start, median_end)
};
Peak {
chrom: best_peak.chrom.clone(),
start: merged_start,
end: merged_end,
score: best_peak.score,
fdr: best_peak.fdr,
pass_coverage: best_peak.pass_coverage,
peak_index: best_peak.peak_index,
pileup: best_peak.pileup,
}
}
fn merge_peaks_single_iteration<'a>(
peaks: Vec<Peak<'a>>,
min_fire_overlap: f64,
min_reciprocal_overlap: f64,
) -> Vec<Peak<'a>> {
if peaks.is_empty() {
return Vec::new();
}
let mut result = Vec::new();
let mut current_group: Vec<usize> = vec![0];
for i in 1..peaks.len() {
let should_merge = current_group.iter().any(|&group_idx| {
peaks[i].should_merge_with(&peaks[group_idx], min_fire_overlap, min_reciprocal_overlap)
});
if should_merge {
current_group.push(i);
} else {
if current_group.len() > 1 {
let group_peaks: Vec<&Peak> =
current_group.iter().map(|&idx| &peaks[idx]).collect();
result.push(merge_peak_group(&group_peaks));
} else {
let idx = current_group[0];
result.push(Peak {
chrom: peaks[idx].chrom.clone(),
start: peaks[idx].start,
end: peaks[idx].end,
score: peaks[idx].score,
fdr: peaks[idx].fdr,
pass_coverage: peaks[idx].pass_coverage,
peak_index: peaks[idx].peak_index,
pileup: peaks[idx].pileup,
});
}
current_group = vec![i];
}
}
if current_group.len() > 1 {
let group_peaks: Vec<&Peak> = current_group.iter().map(|&idx| &peaks[idx]).collect();
result.push(merge_peak_group(&group_peaks));
} else {
let idx = current_group[0];
result.push(Peak {
chrom: peaks[idx].chrom.clone(),
start: peaks[idx].start,
end: peaks[idx].end,
score: peaks[idx].score,
fdr: peaks[idx].fdr,
pass_coverage: peaks[idx].pass_coverage,
peak_index: peaks[idx].peak_index,
pileup: peaks[idx].pileup,
});
}
result
}
fn merge_peaks_iterative<'a>(mut peaks: Vec<Peak<'a>>, opts: &CallPeaksOptions) -> Vec<Peak<'a>> {
let initial_count = peaks.len();
log::debug!(
" Phase 1: Merging peaks with reciprocal overlap >= {}",
opts.high_reciprocal_overlap
);
for iteration in 0..opts.max_grouping_iterations {
let prev_count = peaks.len();
peaks = merge_peaks_single_iteration(peaks, 0.0, opts.high_reciprocal_overlap);
log::debug!(
" Iteration {}: {} -> {} peaks",
iteration + 1,
prev_count,
peaks.len()
);
if peaks.len() == prev_count {
log::debug!(" Phase 1 converged after {} iterations", iteration + 1);
break;
}
}
log::debug!(
" Phase 2: Merging peaks with FIRE element overlap >= {}",
opts.min_frac_overlap
);
for iteration in 0..opts.max_grouping_iterations {
let prev_count = peaks.len();
peaks = merge_peaks_single_iteration(peaks, opts.min_frac_overlap, 0.0);
log::debug!(
" Iteration {}: {} -> {} peaks",
iteration + 1,
prev_count,
peaks.len()
);
if peaks.len() == prev_count {
log::debug!(" Phase 2 converged after {} iterations", iteration + 1);
break;
}
}
log::debug!(
" Phase 3: Merging peaks with reciprocal overlap >= {}",
opts.min_reciprocal_overlap
);
for iteration in 0..opts.max_grouping_iterations {
let prev_count = peaks.len();
peaks = merge_peaks_single_iteration(peaks, 0.0, opts.min_reciprocal_overlap);
log::debug!(
" Iteration {}: {} -> {} peaks",
iteration + 1,
prev_count,
peaks.len()
);
if peaks.len() == prev_count {
log::debug!(" Phase 3 converged after {} iterations", iteration + 1);
break;
}
}
let final_count = peaks.len();
log::debug!(
" Merged {} peaks into {} peaks",
initial_count,
final_count
);
peaks
}
pub fn call_peaks(
opts: &mut CallPeaksOptions,
bam: &mut rust_htslib::bam::IndexedReader,
header: &rust_htslib::bam::HeaderView,
fdr_table: &[FdrEntry],
) -> Result<()> {
if opts.min_fire_frac.is_some() {
log::info!("Calling peaks using FIRE fraction threshold");
} else {
log::info!(
"Calling peaks using FDR table with {} entries",
fdr_table.len()
);
if fdr_table.is_empty() {
anyhow::bail!(
"FDR table is empty. Cannot call peaks without an FDR table. \
Please generate an FDR table first or provide a valid FDR table file."
);
}
}
let mut writer = bio_io::writer(&opts.out)?;
writeln!(writer, "{}", Peak::header())?;
let mut total_peaks_before_merge = 0;
let mut total_peaks_after_merge = 0;
for (chrom, chrom_len) in chrom_names_and_lengths(header)? {
if !super::chromosome_has_fibers(&chrom, bam, opts)? {
log::debug!("Skipping chromosome {} (no fibers)", chrom);
continue;
}
log::debug!(
"Finding peaks on chromosome {} with length {}",
chrom,
chrom_len
);
let pileup_opts = FiberseqPileupOptions {
fire_track_opts: FireTrackOptions {
no_nuc: false,
no_msp: false,
m6a: false,
cpg: false,
fiber_coverage: true,
shuffle: false,
random_shuffle: false,
shuffle_seed: None,
rolling_max: Some(opts.window_size),
track_fire_elements: true, },
rolling_max: Some(opts.window_size),
haps: false,
per_base: false,
keep_zeros: false,
min_fire_coverage: Some(opts.min_fire_coverage),
};
let mut pileup =
FiberseqPileup::new(&chrom, 0, chrom_len as usize, pileup_opts, &None, None);
let fibers = opts.input.fetch_fibers(bam, &chrom, None, None)?;
pileup.add_fibers(fibers);
let (median, std_dev, _) = pileup.all_data.median_and_std_coverage();
let min_cov = opts.min_cov.unwrap_or_else(|| {
let calculated_min = (median - opts.sd_cov * std_dev).round() as i32;
calculated_min.max(4) });
let max_cov = opts
.max_cov
.unwrap_or_else(|| (median + opts.sd_cov * std_dev).round() as i32);
let peaks = Peak::from_pileup(
&pileup,
fdr_table,
opts.max_fdr,
opts.min_fire_frac,
opts.min_fire_frac_filter,
min_cov,
max_cov,
);
let peaks_before = peaks.len();
total_peaks_before_merge += peaks_before;
let merged_peaks = merge_peaks_iterative(peaks, opts);
total_peaks_after_merge += merged_peaks.len();
log::info!(
"Peaks: {} ({} Mbp) - found: {}, merged: {}",
chrom,
chrom_len / 1_000_000,
peaks_before,
merged_peaks.len(),
);
for peak in &merged_peaks {
writeln!(writer, "{}", peak)?;
}
}
log::info!("Total peaks before merging: {}", total_peaks_before_merge);
log::info!("Total peaks after merging: {}", total_peaks_after_merge);
log::info!("Peaks written to {}", opts.out);
Ok(())
}