use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Write;
use std::fmt;
use std::path::Path;
use rust_htslib::bam::{self, Reader, Read};
use bigtools::BigWigWrite;
use bigtools::beddata::BedParserStreamingIterator;
use rayon::prelude::*;
use crate::data_iter::DataIter;
use gtf_splice_index::types::RefBlock; use crate::core::ref_block::record_to_blocks;
use crate::core::alignment_policy::AlignmentPolicy;
use crate::cli::CoverageCli;
use crate::compute_io_threads;
use clap::ValueEnum;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Value {
pub start: u32,
pub end: u32,
pub value: f32,
}
impl Value {
pub fn flat(&self) -> (u32, u32, f32) {
(self.start, self.end, self.value)
}
}
#[derive(ValueEnum, Clone, Debug)]
pub enum Normalize {
Not,
Rpkm,
Cpm,
Bpm,
Rpgc,
}
#[derive(Debug)]
pub struct BedData {
pub genome_info: Vec<(String, usize, usize)>, pub search: HashMap<String, usize>, pub coverage_data: Vec<f32>, pub bin_width: usize,
pub threads: usize,
pub nreads: usize,
}
impl fmt::Display for BedData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "BedData Report:")?;
writeln!(f, " Bin width: {}", self.bin_width)?;
writeln!(f, " Processed reads: {}", self.nreads)?;
writeln!(f, " Genome Info:")?;
for (chr, len, offset) in &self.genome_info {
writeln!(f, " - Chr: {}, Length: {}, Bin offset: {}", chr, len, offset)?;
}
Ok(())
}
}
impl BedData {
pub fn from_bam_with_policy(opts: &CoverageCli) -> Result<BedData, String> {
let mut reader = Reader::from_path(&opts.bam)
.map_err(|e| format!("bam file could not be read: {e:?}"))?;
let hts_threads = compute_io_threads(opts.threads);
if let Err(e) = reader.set_threads(hts_threads) {
eprintln!(
"Warning: failed to enable HTSlib threading ({}). Continuing single-threaded.",
e
)
};
let header = reader.header().clone();
let mut bed = BedData::init_from_header_view(
&header,
opts.width as usize,
1,
None,
);
let policy = AlignmentPolicy::from_cli( opts );
for rec in reader.records() {
let rec = rec.map_err(|e| format!("BAM read error: {e:?}"))?;
if !policy.passes_filter(&rec) { continue; }
let chr = std::str::from_utf8(header.tid2name(rec.tid() as u32))
.map_err(|e| format!("Invalid chromosome name in BAM header: {e:?}"))?;
let blocks = record_to_blocks(&rec);
bed.add_ref_blocks(chr, &blocks);
}
bed.normalize(&opts.normalize);
Ok(bed)
}
pub fn init_from_header_view(
header: &bam::HeaderView,
bin_width: usize,
threads: usize,
limit_to: Option<&[String]>,
) -> Self {
let genome_info = Self::create_ref_id_to_name_vec(header, bin_width, limit_to);
let search = Self::genome_info_to_search(&genome_info);
let num_bins = genome_info
.iter()
.map(|(_, length, _)| (length + bin_width - 1) / bin_width)
.sum::<usize>();
let coverage_data = vec![0.0_f32; num_bins];
Self {
genome_info,
search,
coverage_data,
bin_width,
threads,
nreads: 0,
}
}
pub fn add_ref_blocks(&mut self, chr: &str, blocks: &[RefBlock]) {
let chr_id = match self.search.get(chr) {
Some(id) => *id,
None => return,
};
let (_chrom_name, chrom_length, chrom_offset) =
&self.genome_info[chr_id];
self.nreads += 1;
let mut hit_bins: HashSet<usize> = HashSet::new();
for block in blocks {
let start = block.start as usize;
let end = block.end.min(*chrom_length as u32) as usize;
if end <= start {
continue;
}
let start_window = start / self.bin_width;
let end_window = (end - 1) / self.bin_width;
for id in start_window..=end_window {
let bin_start = id * self.bin_width;
let bin_end = bin_start + self.bin_width;
let overlap =
(end.min(bin_end)).saturating_sub(start.max(bin_start));
if overlap > 0 {
hit_bins.insert(id);
}
}
}
for id in hit_bins {
let index = *chrom_offset + id;
self.coverage_data[index] += 1.0;
}
}
pub fn normalize(&mut self, by: &Normalize) {
match by {
Normalize::Not => {
}
Normalize::Cpm => {
let denom = (self.nreads as f32) / 1_000_000.0;
if denom > 0.0 {
self.coverage_data.par_iter_mut().for_each(|x| *x /= denom);
}
}
Normalize::Rpkm => {
let rpm = (self.nreads as f32) / 1_000_000.0;
if rpm > 0.0 {
self.coverage_data
.par_iter_mut()
.for_each(|x| *x = (*x / rpm) * 1000.0);
}
}
Normalize::Bpm => {
let total: f32 = self.coverage_data.par_iter().sum();
if total > 0.0 {
self.coverage_data.par_iter_mut().for_each(|x| *x /= total);
}
}
Normalize::Rpgc => {
panic!("Rpgc not implemented");
}
}
}
pub fn genome_info_to_search(
genome_info: &Vec<(String, usize, usize)>
) -> HashMap<String, usize> {
genome_info
.iter()
.enumerate()
.map(|(index, (name, _, _))| (name.clone(), index))
.collect()
}
pub fn create_ref_id_to_name_vec(
header: &bam::HeaderView,
bin_width: usize,
limit_to: Option<&[String]>,
) -> Vec<(String, usize, usize)> {
let mut result = Vec::new();
let mut total_bins = 0;
for (rid, name) in header.target_names().iter().enumerate() {
let chr = String::from_utf8_lossy(name).to_string();
let len = header.target_len(rid as u32).unwrap() as usize;
if let Some(allowed) = limit_to {
if !allowed.iter().any(|x| x == &chr) {
continue;
}
}
let bins = (len + bin_width - 1) / bin_width;
result.push((chr, len, total_bins));
total_bins += bins;
}
result
}
pub fn id_for_chr_start(&self, chr: &str, start: usize) -> Option<usize> {
self.search.get(chr).map(|id| {
self.genome_info[*id].2 + start / self.bin_width
})
}
pub fn current_chr_for_id(&self, id: usize) -> Option<(String, usize, usize)> {
for (chr, length, offset) in &self.genome_info {
if id >= *offset && (id - offset) * self.bin_width < *length {
return Some((chr.clone(), *length, *offset));
}
}
None
}
pub fn idx_to_ucsc_pos(&self, idx: usize) -> Option<(String, usize, usize)> {
let (chr, chr_len, offset) = self.current_chr_for_id(idx)?;
let rel = idx - offset;
let start = rel * self.bin_width;
if start >= chr_len {
return None;
}
let end = (start + self.bin_width).min(chr_len);
Some((chr, start, end))
}
pub fn write_bedgraph(&self, file_path: &str) -> std::io::Result<()> {
let mut file = File::create(file_path)?;
let mut iter = DataIter::new(self);
while let Some(values) = iter.next() {
writeln!(
file,
"{}\t{}\t{}\t{}",
values.0,
values.1.start,
values.1.end,
values.1.value
)?;
}
Ok(())
}
pub fn write_bigwig(&self, file: &str) -> Result<(), String> {
let outfile = Path::new(file);
let chrom_map: HashMap<String, u32> =
self.genome_info
.iter()
.map(|(chrom, len, _)| (chrom.clone(), *len as u32))
.collect();
let mut outb = BigWigWrite::create_file(outfile, chrom_map)
.map_err(|e| format!("Failed to create BigWig file: {}", e))?;
outb.options.channel_size = 0;
outb.options.max_zooms = 1;
outb.options.compress = true;
outb.options.inmemory = false;
let runtime = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let iter = DataIter::new(self);
let data = BedParserStreamingIterator::wrap_infallible_iter(iter, true);
outb.write(data, runtime)
.map_err(|e| format!("Failed to write BigWig file: {}", e))?;
Ok(())
}
}
#[cfg(test)]
mod binning_tests {
use super::*;
use gtf_splice_index::types::RefBlock;
const EPS: f32 = 1e-6;
fn bed_one_chr(chr: &str, chr_len: usize, bin_width: usize) -> BedData {
let bins = (chr_len + bin_width - 1) / bin_width;
let genome_info = vec![(chr.to_string(), chr_len, 0usize)];
let search = BedData::genome_info_to_search(&genome_info);
let coverage_data = vec![0.0_f32; bins];
BedData {
genome_info,
search,
coverage_data,
bin_width,
threads: 1,
nreads: 0,
}
}
fn get_bin(bed: &BedData, chr: &str, bin_id: usize) -> f32 {
let idx = bed.search.get(chr).unwrap();
let offset = bed.genome_info[*idx].2;
bed.coverage_data[offset + bin_id]
}
#[test]
fn test_add_ref_blocks_single_bin_full_overlap() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chr1", &[RefBlock { start: 10, end: 20 }]);
assert_eq!(bed.nreads, 1);
assert!((get_bin(&bed, "chr1", 0) - 0.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 1) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 2) - 0.0).abs() < EPS);
}
#[test]
fn test_add_ref_blocks_crosses_two_bins_partial() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chr1", &[RefBlock { start: 5, end: 15 }]);
assert_eq!(bed.nreads, 1);
assert!((get_bin(&bed, "chr1", 0) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 1) - 1.0).abs() < EPS);
}
#[test]
fn test_add_ref_blocks_exact_bin_boundary_no_spill() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chr1", &[RefBlock { start: 0, end: 10 }]);
assert_eq!(bed.nreads, 1);
assert!((get_bin(&bed, "chr1", 0) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 1) - 0.0).abs() < EPS);
}
#[test]
fn test_add_ref_blocks_multi_bins() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chr1", &[RefBlock { start: 0, end: 25 }]);
assert_eq!(bed.nreads, 1);
assert!((get_bin(&bed, "chr1", 0) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 1) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 2) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 3) - 0.0).abs() < EPS);
}
#[test]
fn test_add_ref_blocks_clips_to_chr_end() {
let mut bed = bed_one_chr("chr1", 23, 10);
bed.add_ref_blocks("chr1", &[RefBlock { start: 15, end: 30 }]);
assert_eq!(bed.nreads, 1);
assert!((get_bin(&bed, "chr1", 1) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 2) - 1.0).abs() < EPS);
}
#[test]
fn test_add_ref_blocks_ignores_empty_and_outside() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chr1", &[
RefBlock { start: 10, end: 10 },
RefBlock { start: 200, end: 210 },
]);
assert_eq!(bed.nreads, 1);
for b in 0..10 {
assert!((get_bin(&bed, "chr1", b) - 0.0).abs() < EPS);
}
}
#[test]
fn test_add_ref_blocks_multiple_blocks_one_read_counts_once() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chr1", &[
RefBlock { start: 0, end: 10 }, RefBlock { start: 20, end: 25 }, ]);
assert_eq!(bed.nreads, 1);
assert!((get_bin(&bed, "chr1", 0) - 1.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 2) - 1.0).abs() < EPS);
}
#[test]
fn test_add_ref_blocks_accumulates_across_reads() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chr1", &[RefBlock { start: 0, end: 10 }]); bed.add_ref_blocks("chr1", &[RefBlock { start: 5, end: 15 }]);
assert_eq!(bed.nreads, 2);
assert!((get_bin(&bed, "chr1", 0) - 2.0).abs() < EPS);
assert!((get_bin(&bed, "chr1", 1) - 1.0).abs() < EPS);
}
#[test]
fn test_add_ref_blocks_unknown_chr_is_error_or_noop() {
let mut bed = bed_one_chr("chr1", 100, 10);
bed.add_ref_blocks("chrX", &[RefBlock { start: 0, end: 10 }]);
assert_eq!(bed.nreads, 0);
assert!((get_bin(&bed, "chr1", 0) - 0.0).abs() < EPS);
}
#[test]
fn add_ref_blocks_counts_read_once_per_bin_even_with_two_blocks() {
use std::collections::HashMap;
let mut bed = BedData {
genome_info: vec![("MT".to_string(), 200usize, 0usize)],
search: {
let mut h = HashMap::new();
h.insert("MT".to_string(), 0usize);
h
},
coverage_data: vec![0.0_f32; 4],
bin_width: 50,
threads: 1,
nreads: 0,
};
let blocks = vec![
RefBlock::new(10, 20),
RefBlock::new(30, 40),
];
bed.add_ref_blocks("MT", &blocks);
assert_eq!(bed.nreads, 1);
assert_eq!(bed.coverage_data[0], 1.0, "bin 0 should be incremented once");
assert_eq!(bed.coverage_data[1], 0.0);
assert_eq!(bed.coverage_data[2], 0.0);
assert_eq!(bed.coverage_data[3], 0.0);
}
}