use std::fmt::Write as _;
use std::io::Write as _;
use std::sync::Arc;
use ndarray::{Array1, Array2};
use crate::bbi::extract::Extraction;
use crate::bbi::header::{BbiHeader, BbiKind, TotalSummary, ZoomHeader};
use crate::error::{Error, Result};
use crate::genomic::{BinMode, ChrMap, IndexedLocs, LocBatch, Locs, Reduce};
use crate::parallel::Executor;
use crate::progress::{ProgressFn, ProgressTracker};
use crate::source::ByteSource;
#[derive(Debug, Clone, Copy, Default)]
pub enum Zoom {
#[default]
Full,
Auto,
Level(usize),
}
#[derive(Debug)]
struct Inner {
source: Arc<dyn ByteSource>,
executor: Executor,
}
#[derive(Debug)]
pub struct BbiReader {
inner: Option<Inner>,
path: String,
zoom_correction: f64,
pub(crate) header: BbiHeader,
pub(crate) zoom_headers: Vec<ZoomHeader>,
pub(crate) total_summary: TotalSummary,
pub(crate) chr_map: ChrMap,
pub(crate) chr_names: Vec<String>,
pub(crate) auto_sql: indexmap::IndexMap<String, String>,
}
#[derive(Default, Clone, Copy)]
struct Grid {
bin_count: Option<usize>,
snap: Option<f64>,
zoom: Option<f64>,
}
impl Grid {
fn entries() -> Self {
Self {
bin_count: Some(1),
snap: Some(1.0),
zoom: None,
}
}
}
impl BbiReader {
pub fn open(
path: &str,
parallel: i64,
zoom_correction: f64,
block_size: Option<u64>,
max_blocks: Option<usize>,
) -> Result<Self> {
let source = crate::source::open(path, block_size, max_blocks)?;
Self::from_source(source, path, parallel, zoom_correction)
}
pub(crate) fn from_source(
source: Arc<dyn ByteSource>,
path: &str,
parallel: i64,
zoom_correction: f64,
) -> Result<Self> {
let header = super::header::read_header(source.as_ref())?;
let zoom_headers = super::header::read_zoom_headers(source.as_ref(), header.zoom_levels)?;
let total_summary =
super::header::read_total_summary(source.as_ref(), header.total_summary_offset)?;
let (chr_map, _tree) = super::chr_tree::read(source.as_ref(), header.chr_tree_offset)?;
let auto_sql = if header.kind.is_bigbed() {
super::header::read_auto_sql(
source.as_ref(),
header.auto_sql_offset,
header.field_count,
)?
} else {
indexmap::IndexMap::new()
};
let mut chr_names =
vec![String::new(); chr_map.iter().map(|e| e.index + 1).max().unwrap_or(0)];
for entry in chr_map.iter() {
chr_names[entry.index] = entry.id.clone();
}
let executor = Executor::new(parallel)?;
Ok(Self {
inner: Some(Inner { source, executor }),
path: path.to_string(),
zoom_correction,
header,
zoom_headers,
total_summary,
chr_map,
chr_names,
auto_sql,
})
}
pub fn kind(&self) -> BbiKind {
self.header.kind
}
pub fn path(&self) -> &str {
&self.path
}
pub fn chr_sizes(&self) -> &ChrMap {
&self.chr_map
}
pub fn header(&self) -> &BbiHeader {
&self.header
}
pub fn zoom_headers(&self) -> &[ZoomHeader] {
&self.zoom_headers
}
pub fn total_summary(&self) -> &TotalSummary {
&self.total_summary
}
pub fn auto_sql(&self) -> &indexmap::IndexMap<String, String> {
&self.auto_sql
}
pub fn is_closed(&self) -> bool {
self.inner.is_none()
}
pub fn parallel(&self) -> usize {
self.inner.as_ref().map_or(0, |i| i.executor.parallel())
}
pub fn close(&mut self) {
if let Some(inner) = self.inner.take() {
inner.source.close();
}
}
fn inner(&self) -> Result<&Inner> {
self.inner.as_ref().ok_or_else(|| Error::Closed {
path: self.path.clone(),
})
}
#[allow(clippy::type_complexity)]
fn prepare<'a>(
&'a self,
inner: &'a Inner,
locs: &Locs,
common: &ReadCommon,
grid: Grid,
) -> Result<(
IndexedLocs,
Vec<LocBatch>,
Option<usize>,
u64,
ProgressTracker,
)> {
if self.header.kind.is_bigbed() && !matches!(common.zoom, Zoom::Full) {
return Err(Error::invalid("zoom is only supported for bigwig files"));
}
let indexed = IndexedLocs::build(
&self.chr_map,
locs,
grid.snap.unwrap_or(common.bin_size),
grid.bin_count,
common.full_bin,
)?;
let (batches, coverage) = indexed.batches(inner.executor.parallel());
let level = self.select_zoom(
grid.zoom
.unwrap_or_else(|| indexed.effective_bin_size(common.bin_size)),
common.zoom,
)?;
let index_offset = match level {
Some(i) => self.zoom_headers[i].index_offset,
None => self.header.full_index_offset,
};
super::header::check_data_tree_magic(inner.source.as_ref(), index_offset)?;
let tree_root = index_offset + super::header::DATA_TREE_HEADER_SIZE;
Ok((
indexed,
batches,
level,
tree_root,
ProgressTracker::with_callback(coverage, common.progress.clone()),
))
}
fn extraction<'a>(
&'a self,
inner: &'a Inner,
indexed: &'a IndexedLocs,
batches: &'a [LocBatch],
level: Option<usize>,
tree_root: u64,
tracker: &'a ProgressTracker,
) -> Extraction<'a> {
Extraction {
source: inner.source.as_ref(),
locs: indexed,
batches,
tree_root,
zoom: level.is_some(),
uncompress_buffer_size: self.header.uncompress_buffer_size,
tracker,
}
}
pub fn read_values(&self, req: &ValuesRequest) -> Result<Array2<f32>> {
let inner = self.inner()?;
let (indexed, batches, level, root, tracker) = self.prepare(
inner,
&req.common.locs,
&req.common,
Grid {
bin_count: req.common.bin_count,
..Grid::default()
},
)?;
let ex = self.extraction(inner, &indexed, &batches, level, root, &tracker);
let flat = if self.header.kind.is_bigbed() {
super::extract::entries_pileup(
&ex,
&inner.executor,
&self.auto_sql,
req.common.def_value,
)?
} else {
super::extract::values(&ex, &inner.executor, req.bin_mode, req.common.def_value)?
};
tracker.done_report();
let rows = indexed.locs.len();
let cols = indexed.bin_count;
Array2::from_shape_vec((rows, cols), flat)
.map_err(|e| Error::invalid(format!("output shape {rows}x{cols}: {e}")))
}
pub fn quantify(&self, req: &QuantifyRequest) -> Result<Array1<f32>> {
let inner = self.inner()?;
let is_bigbed = self.header.kind.is_bigbed();
let (indexed, batches, level, root, tracker) = self.prepare(
inner,
&req.common.locs,
&req.common,
Grid {
bin_count: if is_bigbed {
req.common.bin_count
} else {
Some(1)
},
zoom: Some(req.common.bin_size),
..Grid::default()
},
)?;
let ex = self.extraction(inner, &indexed, &batches, level, root, &tracker);
let mut stats = if is_bigbed {
let pileup = super::extract::entries_pileup(
&ex,
&inner.executor,
&self.auto_sql,
req.common.def_value,
)?;
super::extract::pileup_stats(&indexed, &pileup)
} else {
super::extract::values_stats(&ex, &inner.executor)?
};
tracker.done_report();
let def = req.common.def_value;
if !def.is_nan() {
for loc in &indexed.locs {
let s = &mut stats[loc.row(indexed.bin_count)];
let total = if is_bigbed {
(loc.output_end - loc.output_start) as i64
} else {
loc.binned_end - loc.binned_start
};
let missing = total - s.count;
if missing <= 0 {
continue;
}
s.add_repeated(def, missing);
}
}
Ok(Array1::from_vec(
stats.iter().map(|s| s.reduce(req.reduce, def)).collect(),
))
}
pub fn profile(&self, req: &ProfileRequest) -> Result<Array1<f32>> {
let inner = self.inner()?;
let (indexed, batches, level, root, tracker) = self.prepare(
inner,
&req.common.locs,
&req.common,
Grid {
bin_count: req.common.bin_count,
..Grid::default()
},
)?;
let ex = self.extraction(inner, &indexed, &batches, level, root, &tracker);
let mut stats = if self.header.kind.is_bigbed() {
let pileup = super::extract::entries_pileup(
&ex,
&inner.executor,
&self.auto_sql,
req.common.def_value,
)?;
super::extract::pileup_profile(&indexed, &pileup)
} else {
super::extract::values_profile(&ex, &inner.executor, req.bin_mode)?
};
tracker.done_report();
let def = req.common.def_value;
if !def.is_nan() {
let loc_count = indexed.locs.len() as i64;
for s in &mut stats {
let missing = loc_count - s.count;
if missing <= 0 {
continue;
}
s.add_repeated(def, missing);
}
}
Ok(Array1::from_vec(
stats.iter().map(|s| s.reduce(req.reduce, def)).collect(),
))
}
pub fn read_entries(&self, req: &EntriesRequest) -> Result<Vec<Vec<super::BedEntry>>> {
let inner = self.inner()?;
self.require_bigbed("read_entries")?;
self.check_col_count(req.col_count, 3)?;
let (indexed, batches, level, root, tracker) =
self.prepare(inner, &req.common.locs, &req.common, Grid::entries())?;
let ex = self.extraction(inner, &indexed, &batches, level, root, &tracker);
let out = super::extract::entries(
&ex,
&inner.executor,
&self.auto_sql,
&self.chr_names,
req.col_count,
)?;
tracker.done_report();
Ok(out)
}
pub fn read_all_entries(&self, req: &EntriesRequest) -> Result<Vec<super::BedEntry>> {
let inner = self.inner()?;
self.require_bigbed("read_all_entries")?;
self.check_col_count(req.col_count, 3)?;
let locs = Locs::whole_chromosomes(&self.chr_map, &req.common.locs.chr_ids)?;
let (indexed, batches, level, root, tracker) =
self.prepare(inner, &locs, &req.common, Grid::entries())?;
let ex = self.extraction(inner, &indexed, &batches, level, root, &tracker);
let by_chr = super::extract::entries(
&ex,
&inner.executor,
&self.auto_sql,
&self.chr_names,
req.col_count,
)?;
tracker.done_report();
Ok(by_chr.into_iter().flatten().collect())
}
fn require_bigbed(&self, what: &str) -> Result<()> {
if self.header.kind.is_bigbed() {
Ok(())
} else {
Err(Error::invalid(format!("{what} only for bigbed")))
}
}
fn require_bigwig(&self, what: &str) -> Result<()> {
if self.header.kind.is_bigbed() {
Err(Error::invalid(format!("{what} only for bigwig")))
} else {
Ok(())
}
}
pub(crate) fn check_col_count(&self, col_count: usize, min: usize) -> Result<()> {
if col_count == 0 {
return Ok(());
}
if col_count < min {
return Err(Error::invalid(format!(
"col_count {col_count} must be 0 or at least {min}"
)));
}
if col_count > self.header.field_count as usize {
return Err(Error::invalid(format!(
"col_count {col_count} exceeds number of fields {}",
self.header.field_count
)));
}
Ok(())
}
pub fn data_size(&self, zoom: Option<usize>) -> u64 {
match zoom.and_then(|i| self.zoom_headers.get(i)) {
Some(z) => z.index_offset.saturating_sub(z.data_offset),
None => self
.header
.full_index_offset
.saturating_sub(self.header.full_data_offset),
}
}
pub fn genome_size(&self) -> i64 {
self.chr_map.genome_size()
}
pub fn iter_all_values(
&self,
req: &ValuesRequest,
window: i64,
) -> Result<super::ValuesWindows<'_>> {
super::extract::ValuesWindows::plan(self, req, window)
}
pub fn iter_all_entries(
&self,
req: &EntriesRequest,
window: i64,
) -> Result<super::EntryWindows<'_>> {
super::extract::EntryWindows::plan(self, req, window)
}
pub fn to_bedgraph(
&self,
out: &std::path::Path,
req: &ValuesRequest,
merge_bins: bool,
) -> Result<()> {
self.require_bigwig("to_bedgraph")?;
self.export_bins(out, req, BedGraphSink::new(merge_bins))
}
pub fn to_wig(&self, out: &std::path::Path, req: &ValuesRequest) -> Result<()> {
self.require_bigwig("to_wig")?;
self.export_bins(out, req, WigSink::default())
}
pub fn to_bed(&self, out: &std::path::Path, req: &EntriesRequest) -> Result<()> {
let inner = self.inner()?;
self.require_bigbed("to_bed")?;
self.check_col_count(req.col_count, 1)?;
let col_count = if req.col_count == 0 {
self.header.field_count as usize
} else {
req.col_count
};
let locs = Locs::whole_chromosomes(&self.chr_map, &req.common.locs.chr_ids)?;
let (indexed, batches, level, root, tracker) =
self.prepare(inner, &locs, &req.common, Grid::entries())?;
let ex = self.extraction(inner, &indexed, &batches, level, root, &tracker);
let wanted = self.walked_chrs(&indexed);
let mut writer = std::io::BufWriter::new(
std::fs::File::create(out).map_err(|e| Error::io(out.to_string_lossy(), e))?,
);
let mut line = String::new();
for batch in &batches {
ex.walk_bed_batch(*batch, &self.auto_sql, col_count, |entry, _| {
if !wanted.contains(&entry.chr_index) {
return Ok(());
}
line.clear();
line.push_str(self.chr_name(entry.chr_index));
if col_count >= 2 {
let _ = write!(line, "\t{}", entry.start);
}
if col_count >= 3 {
let _ = write!(line, "\t{}", entry.end);
}
for (_, value) in entry.fields.iter().take(col_count.saturating_sub(3)) {
line.push('\t');
line.push_str(value);
}
line.push('\n');
write_line(&mut writer, &line, out)
})?;
}
flush(&mut writer, out)?;
tracker.done_report();
Ok(())
}
fn export_bins(
&self,
out: &std::path::Path,
req: &ValuesRequest,
mut sink: impl BinSink,
) -> Result<()> {
self.inner()?;
let walk = super::extract::ValuesWindows::plan(self, req, Self::export_window(req)?)?;
let bin_size = walk.bin_size();
let locs = walk.locs().to_vec();
let mut writer = std::io::BufWriter::new(
std::fs::File::create(out).map_err(|e| Error::io(out.to_string_lossy(), e))?,
);
let mut line = String::new();
for (index, values) in walk.enumerate() {
let values = values?;
let (chr, window_start, window_end) = &locs[index];
for (i, &value) in values.iter().enumerate() {
if value.is_nan() {
continue;
}
let start = window_start + i as i64 * bin_size;
let end = (start + bin_size).min(*window_end);
line.clear();
sink.bin(&mut line, chr, start, end, value);
if !line.is_empty() {
write_line(&mut writer, &line, out)?;
}
}
}
line.clear();
sink.finish(&mut line);
if !line.is_empty() {
write_line(&mut writer, &line, out)?;
}
flush(&mut writer, out)
}
fn export_window(req: &ValuesRequest) -> Result<i64> {
let bin_size =
crate::genomic::BinPlan::new(req.common.bin_size, None, req.common.full_bin)?
.whole_bin_size();
Ok(bin_size
.saturating_mul(EXPORT_WINDOW_BINS)
.min(EXPORT_WINDOW_BASES)
.max(bin_size))
}
fn walked_chrs(&self, locs: &IndexedLocs) -> std::collections::HashSet<u32> {
locs.locs.iter().map(|l| l.chr_index as u32).collect()
}
pub(crate) fn chr_name(&self, index: u32) -> &str {
self.chr_names
.get(index as usize)
.map(String::as_str)
.unwrap_or("")
}
pub(crate) fn select_zoom(&self, bin_size: f64, zoom: Zoom) -> Result<Option<usize>> {
let count = self.zoom_headers.len();
match zoom {
Zoom::Full => Ok(None),
Zoom::Level(level) => {
if level < count {
Ok(Some(level))
} else if count == 0 {
Err(Error::invalid("file has no zoom level"))
} else {
Err(Error::invalid(format!(
"requested zoom level {level} exceeds max zoom level {}",
count - 1
)))
}
}
Zoom::Auto => {
let threshold = (bin_size * self.zoom_correction).round() as i64;
let mut best: Option<usize> = None;
let mut best_reduction = 0i64;
for (i, zoom) in self.zoom_headers.iter().enumerate() {
let reduction = zoom.reduction_level as i64;
if reduction <= threshold && reduction > best_reduction {
best_reduction = reduction;
best = Some(i);
}
}
Ok(best)
}
}
}
}
pub struct ReadCommon {
pub locs: Locs,
pub bin_size: f64,
pub bin_count: Option<usize>,
pub full_bin: bool,
pub def_value: f32,
pub zoom: Zoom,
pub progress: Option<ProgressFn>,
}
impl ReadCommon {
pub fn new(locs: Locs) -> Self {
Self {
locs,
bin_size: 1.0,
bin_count: None,
full_bin: false,
def_value: 0.0,
zoom: Zoom::Full,
progress: None,
}
}
}
macro_rules! read_common_builders {
($t:ty) => {
impl $t {
pub fn bin_size(mut self, v: f64) -> Self {
self.common.bin_size = v;
self
}
pub fn bin_count(mut self, v: usize) -> Self {
self.common.bin_count = Some(v);
self
}
pub fn full_bin(mut self, v: bool) -> Self {
self.common.full_bin = v;
self
}
pub fn def_value(mut self, v: f32) -> Self {
self.common.def_value = v;
self
}
pub fn zoom(mut self, v: Zoom) -> Self {
self.common.zoom = v;
self
}
pub fn progress(mut self, f: ProgressFn) -> Self {
self.common.progress = Some(f);
self
}
}
};
}
pub struct ValuesRequest {
pub common: ReadCommon,
pub bin_mode: BinMode,
}
pub struct QuantifyRequest {
pub common: ReadCommon,
pub reduce: Reduce,
}
pub struct ProfileRequest {
pub common: ReadCommon,
pub bin_mode: BinMode,
pub reduce: Reduce,
}
pub struct EntriesRequest {
pub common: ReadCommon,
pub col_count: usize,
}
read_common_builders!(ValuesRequest);
read_common_builders!(QuantifyRequest);
read_common_builders!(ProfileRequest);
read_common_builders!(EntriesRequest);
impl ValuesRequest {
pub fn new(locs: Locs) -> Self {
Self {
common: ReadCommon::new(locs),
bin_mode: BinMode::Mean,
}
}
pub fn bin_mode(mut self, v: BinMode) -> Self {
self.bin_mode = v;
self
}
}
impl QuantifyRequest {
pub fn new(locs: Locs) -> Self {
Self {
common: ReadCommon::new(locs),
reduce: Reduce::Mean,
}
}
pub fn reduce(mut self, v: Reduce) -> Self {
self.reduce = v;
self
}
}
impl ProfileRequest {
pub fn new(locs: Locs) -> Self {
Self {
common: ReadCommon::new(locs),
bin_mode: BinMode::Mean,
reduce: Reduce::Mean,
}
}
pub fn bin_mode(mut self, v: BinMode) -> Self {
self.bin_mode = v;
self
}
pub fn reduce(mut self, v: Reduce) -> Self {
self.reduce = v;
self
}
}
impl EntriesRequest {
pub fn new(locs: Locs) -> Self {
Self {
common: ReadCommon::new(locs),
col_count: 0,
}
}
pub fn col_count(mut self, v: usize) -> Self {
self.col_count = v;
self
}
}
const EXPORT_WINDOW_BINS: i64 = 1 << 20;
const EXPORT_WINDOW_BASES: i64 = 16 << 20;
trait BinSink {
fn bin(&mut self, line: &mut String, chr: &str, start: i64, end: i64, value: f32);
fn finish(&mut self, line: &mut String);
}
#[derive(Default)]
struct BedGraphSink {
chr: String,
start: i64,
end: i64,
value: f32,
open: bool,
merge: bool,
}
impl BedGraphSink {
fn new(merge: bool) -> Self {
Self {
merge,
..Self::default()
}
}
fn flush(&mut self, line: &mut String) {
if !self.open {
return;
}
let _ = write!(line, "{}\t{}\t{}\t", self.chr, self.start, self.end);
super::text::push_float(line, self.value);
line.push('\n');
self.open = false;
}
}
impl BinSink for BedGraphSink {
fn bin(&mut self, line: &mut String, chr: &str, start: i64, end: i64, value: f32) {
if self.merge && self.open && self.end == start && self.value == value && self.chr == chr {
self.end = end;
return;
}
self.flush(line);
self.chr.clear();
self.chr.push_str(chr);
self.start = start;
self.end = end;
self.value = value;
self.open = true;
}
fn finish(&mut self, line: &mut String) {
self.flush(line);
}
}
#[derive(Default)]
struct WigSink {
chr: String,
span: i64,
next_start: i64,
open: bool,
}
impl BinSink for WigSink {
fn bin(&mut self, line: &mut String, chr: &str, start: i64, end: i64, value: f32) {
let span = end - start;
if !self.open || span != self.span || start != self.next_start || self.chr != chr {
let _ = writeln!(
line,
"fixedStep chrom={chr} start={} step={span} span={span}",
start + 1
);
self.chr.clear();
self.chr.push_str(chr);
self.span = span;
self.open = true;
}
super::text::push_float(line, value);
line.push('\n');
self.next_start = start + span;
}
fn finish(&mut self, _line: &mut String) {}
}
fn write_line(
writer: &mut std::io::BufWriter<std::fs::File>,
line: &str,
path: &std::path::Path,
) -> Result<()> {
writer
.write_all(line.as_bytes())
.map_err(|e| Error::io(path.to_string_lossy(), e))
}
fn flush(writer: &mut std::io::BufWriter<std::fs::File>, path: &std::path::Path) -> Result<()> {
writer
.flush()
.map_err(|e| Error::io(path.to_string_lossy(), e))
}