use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BinMode {
#[default]
Mean,
Sum,
Count,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Reduce {
#[default]
Mean,
Sd,
Sem,
Sum,
Count,
Min,
Max,
L1Norm,
L2Norm,
}
impl std::str::FromStr for BinMode {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"mean" => Ok(BinMode::Mean),
"sum" => Ok(BinMode::Sum),
"count" => Ok(BinMode::Count),
o => Err(Error::invalid(format!("bin_mode {o} invalid"))),
}
}
}
impl std::str::FromStr for Reduce {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"mean" => Ok(Reduce::Mean),
"sd" => Ok(Reduce::Sd),
"sem" => Ok(Reduce::Sem),
"sum" => Ok(Reduce::Sum),
"count" => Ok(Reduce::Count),
"min" => Ok(Reduce::Min),
"max" => Ok(Reduce::Max),
"l1norm" => Ok(Reduce::L1Norm),
"l2norm" => Ok(Reduce::L2Norm),
o => Err(Error::invalid(format!("reduce {o} invalid"))),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct BinStats {
pub sum: f64,
pub count: f64,
}
impl BinStats {
#[inline]
pub fn add(&mut self, value: f32, bases: f64) {
self.sum += value as f64 * bases;
self.count += bases;
}
#[inline]
pub fn merge(&mut self, other: &BinStats) {
self.sum += other.sum;
self.count += other.count;
}
#[inline]
pub fn apply(&self, mode: BinMode) -> f32 {
match mode {
BinMode::Mean => (self.sum / self.count) as f32,
BinMode::Sum => self.sum as f32,
BinMode::Count => self.count as f32,
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.count == 0.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ValueStats {
pub min: f32,
pub max: f32,
shift: f64,
sum_shifted: f64,
sum_sq_shifted: f64,
sum_abs: f64,
pub count: i64,
}
impl Default for ValueStats {
fn default() -> Self {
Self {
min: f32::NAN,
max: f32::NAN,
shift: 0.0,
sum_shifted: 0.0,
sum_sq_shifted: 0.0,
sum_abs: 0.0,
count: 0,
}
}
}
impl ValueStats {
#[inline]
pub fn sum(&self) -> f64 {
self.sum_shifted + self.shift * self.count as f64
}
#[inline]
pub fn sum_squared(&self) -> f64 {
let n = self.count as f64;
self.sum_sq_shifted + 2.0 * self.shift * self.sum_shifted + self.shift * self.shift * n
}
#[inline]
pub fn add(&mut self, value: f32) {
self.add_repeated(value, 1);
}
#[inline]
pub fn add_repeated(&mut self, value: f32, bases: i64) {
if bases <= 0 {
return;
}
let v = value as f64;
if self.count == 0 {
self.shift = v;
}
self.min = self.min.min(value);
self.max = self.max.max(value);
let d = v - self.shift;
let n = bases as f64;
self.sum_shifted += d * n;
self.sum_sq_shifted += d * d * n;
self.sum_abs += v.abs() * n;
self.count += bases;
}
#[inline]
pub fn add_aggregate(&mut self, min: f32, max: f32, sum: f64, sum_squared: f64, bases: i64) {
if bases <= 0 {
return;
}
let n = bases as f64;
if self.count == 0 {
self.shift = sum / n;
}
self.min = self.min.min(min);
self.max = self.max.max(max);
let k = self.shift;
self.sum_shifted += sum - n * k;
self.sum_sq_shifted += sum_squared - 2.0 * k * sum + n * k * k;
self.sum_abs += if min >= 0.0 {
sum
} else if max <= 0.0 {
-sum
} else {
sum.abs()
};
self.count += bases;
}
#[inline]
pub fn merge(&mut self, other: &ValueStats) {
if other.count == 0 {
return;
}
if self.count == 0 {
*self = *other;
return;
}
self.min = self.min.min(other.min);
self.max = self.max.max(other.max);
let d = other.shift - self.shift;
let n = other.count as f64;
self.sum_shifted += other.sum_shifted + n * d;
self.sum_sq_shifted += other.sum_sq_shifted + 2.0 * d * other.sum_shifted + n * d * d;
self.sum_abs += other.sum_abs;
self.count += other.count;
}
fn variance(&self, count: f64) -> f64 {
let mean_shifted = self.sum_shifted / count;
((self.sum_sq_shifted / count) - mean_shifted * mean_shifted).max(0.0)
}
pub fn reduce(&self, reduce: Reduce, def_value: f32) -> f32 {
if self.count == 0 {
return match reduce {
Reduce::Count => 0.0,
_ => def_value,
};
}
let count = self.count as f64;
match reduce {
Reduce::Mean => (self.shift + self.sum_shifted / count) as f32,
Reduce::Sd => self.variance(count).sqrt() as f32,
Reduce::Sem => (self.variance(count) / count).sqrt() as f32,
Reduce::Sum => self.sum() as f32,
Reduce::Count => count as f32,
Reduce::Min => self.min,
Reduce::Max => self.max,
Reduce::L1Norm => self.sum_abs as f32,
Reduce::L2Norm => self.sum_squared().max(0.0).sqrt() as f32,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BinPlan {
pub bin_size: f64,
pub bin_count: Option<usize>,
pub full_bin: bool,
}
impl BinPlan {
pub fn new(bin_size: f64, bin_count: Option<usize>, full_bin: bool) -> Result<Self> {
if !bin_size.is_finite() || bin_size <= 0.0 {
return Err(Error::invalid(format!(
"bin_size must be a positive finite number, got {bin_size}"
)));
}
if bin_size.fract() != 0.0 {
return Err(Error::invalid(format!(
"bin_size must be a whole number of base pairs, got {bin_size}. \
Use bin_count to divide a window into a fixed number of bins."
)));
}
if bin_count == Some(0) {
return Err(Error::invalid("bin_count must be at least 1"));
}
Ok(Self {
bin_size,
bin_count,
full_bin,
})
}
pub fn whole_bin_size(&self) -> i64 {
self.bin_size as i64
}
}
impl Default for BinPlan {
fn default() -> Self {
Self {
bin_size: 1.0,
bin_count: None,
full_bin: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bin_stats_weight_by_bases() {
let mut s = BinStats::default();
s.add(2.0, 10.0);
s.add(4.0, 30.0);
assert_eq!(s.count, 40.0);
assert_eq!(s.sum, 140.0);
assert_eq!(s.apply(BinMode::Mean), 3.5);
assert_eq!(s.apply(BinMode::Sum), 140.0);
assert_eq!(s.apply(BinMode::Count), 40.0);
}
#[test]
fn an_untouched_bin_means_nan_not_zero() {
assert!(BinStats::default().apply(BinMode::Mean).is_nan());
assert_eq!(BinStats::default().apply(BinMode::Count), 0.0);
}
#[test]
fn value_stats_seed_extremes_from_nan() {
let mut s = ValueStats::default();
assert!(s.min.is_nan() && s.max.is_nan());
s.add(3.0);
s.add(-1.0);
s.add(7.0);
assert_eq!((s.min, s.max, s.count), (-1.0, 7.0, 3));
assert_eq!(s.sum(), 9.0);
assert_eq!(s.sum_squared(), 59.0);
}
#[test]
fn l1norm_is_the_sum_of_absolute_values() {
let mut s = ValueStats::default();
for v in [3.0f32, -4.0, 5.0, -6.0] {
s.add(v);
}
assert_eq!(s.reduce(Reduce::Sum, 0.0), -2.0);
assert_eq!(s.reduce(Reduce::L1Norm, 0.0), 18.0);
assert_eq!(s.reduce(Reduce::L2Norm, 0.0), (86.0f32).sqrt());
}
#[test]
fn a_tiny_spread_on_a_large_mean_survives() {
let mean = 1.0e4f32;
let values: Vec<f32> = (0..2000)
.map(|i| mean + (i % 7) as f32 * 1.0e-2 - 0.03)
.collect();
let mut s = ValueStats::default();
for v in &values {
s.add(*v);
}
let n = values.len() as f64;
let m: f64 = values.iter().map(|v| *v as f64).sum::<f64>() / n;
let want = (values.iter().map(|v| (*v as f64 - m).powi(2)).sum::<f64>() / n).sqrt();
let got = s.reduce(Reduce::Sd, 0.0) as f64;
assert!((got - want).abs() <= want * 1e-3, "sd {got} against {want}");
}
#[test]
fn a_constant_column_has_no_spread_at_all() {
for value in [1.0f32, 12345.678, -9876.5, 1.0e-7] {
let mut s = ValueStats::default();
for _ in 0..500 {
s.add(value);
}
assert_eq!(s.reduce(Reduce::Sd, -1.0), 0.0, "value {value}");
assert_eq!(s.reduce(Reduce::Sem, -1.0), 0.0, "value {value}");
assert_eq!(s.reduce(Reduce::Mean, -1.0), value, "value {value}");
}
}
#[test]
fn merging_is_folding_by_another_route() {
let a_values = [1000.0f32, 1000.5, 999.5, 1001.0];
let b_values = [-3.0f32, 2000.25, 7.5];
let (mut a, mut b, mut whole) = (
ValueStats::default(),
ValueStats::default(),
ValueStats::default(),
);
for v in a_values {
a.add(v);
whole.add(v);
}
for v in b_values {
b.add(v);
whole.add(v);
}
a.merge(&b);
assert_eq!(a.count, whole.count);
for r in [
Reduce::Mean,
Reduce::Sum,
Reduce::L1Norm,
Reduce::Min,
Reduce::Max,
] {
assert_eq!(a.reduce(r, 0.0), whole.reduce(r, 0.0), "{r:?}");
}
let (got, want) = (a.reduce(Reduce::Sd, 0.0), whole.reduce(Reduce::Sd, 0.0));
assert!((got - want).abs() <= want * 1e-5, "sd {got} against {want}");
}
#[test]
fn an_aggregate_run_folds_in_with_its_own_spread() {
let mut s = ValueStats::default();
s.add_aggregate(1.0, 9.0, 500.0, 3000.0, 100);
assert_eq!(s.count, 100);
assert_eq!(s.reduce(Reduce::Mean, 0.0), 5.0);
assert_eq!(s.reduce(Reduce::Sum, 0.0), 500.0);
assert!((s.reduce(Reduce::Sd, 0.0) - 5.0f32.sqrt()).abs() < 1e-4);
assert_eq!(s.reduce(Reduce::L1Norm, 0.0), 500.0);
let mut s = ValueStats::default();
s.add_aggregate(-9.0, -1.0, -500.0, 3000.0, 100);
assert_eq!(s.reduce(Reduce::L1Norm, 0.0), 500.0);
}
#[test]
fn empty_reduces_to_def_value_except_count() {
let s = ValueStats::default();
for r in [
Reduce::Mean,
Reduce::Sd,
Reduce::Sum,
Reduce::Min,
Reduce::Max,
] {
assert_eq!(s.reduce(r, -5.0), -5.0, "{r:?}");
}
assert_eq!(s.reduce(Reduce::Count, -5.0), 0.0);
}
#[test]
fn reductions_match_their_definitions() {
let mut s = ValueStats::default();
for v in [2.0f32, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
s.add(v);
}
assert_eq!(s.reduce(Reduce::Mean, 0.0), 5.0);
assert_eq!(s.reduce(Reduce::Sd, 0.0), 2.0); assert_eq!(s.reduce(Reduce::Sum, 0.0), 40.0);
assert_eq!(s.reduce(Reduce::Count, 0.0), 8.0);
assert_eq!(s.reduce(Reduce::Min, 0.0), 2.0);
assert_eq!(s.reduce(Reduce::Max, 0.0), 9.0);
assert_eq!(s.reduce(Reduce::L1Norm, 0.0), 40.0);
assert_eq!(s.reduce(Reduce::L2Norm, 0.0), 232.0f32.sqrt());
}
#[test]
fn variance_of_a_constant_column_is_not_negative() {
let mut s = ValueStats::default();
for _ in 0..1000 {
s.add(1e7);
}
assert_eq!(s.reduce(Reduce::Sd, 0.0), 0.0);
}
#[test]
fn add_repeated_matches_repeated_add() {
let mut a = ValueStats::default();
for _ in 0..5 {
a.add(3.5);
}
let mut b = ValueStats::default();
b.add_repeated(3.5, 5);
assert_eq!(a, b);
}
#[test]
fn bin_plan_rejects_nonsense() {
assert!(BinPlan::new(0.0, None, false).is_err());
assert!(BinPlan::new(-1.0, None, false).is_err());
assert!(BinPlan::new(f64::NAN, None, false).is_err());
assert!(BinPlan::new(1.0, Some(0), false).is_err());
assert_eq!(
BinPlan::new(10.0, None, false).unwrap().whole_bin_size(),
10
);
}
#[test]
fn a_fractional_bin_size_is_refused_everywhere() {
for bad in [0.5f64, 2.5, 1.000001, 99.9] {
let err = BinPlan::new(bad, None, false).unwrap_err().to_string();
assert!(err.contains("whole number of base pairs"), "{bad}: {err}");
assert!(err.contains("bin_count"), "{bad}: {err}");
}
for good in [1.0f64, 100.0, 1e6] {
assert!(BinPlan::new(good, None, false).is_ok(), "{good}");
}
}
#[test]
fn mode_and_reduce_parse_and_reject() {
use std::str::FromStr;
assert_eq!(BinMode::from_str("sum").unwrap(), BinMode::Sum);
assert_eq!(Reduce::from_str("l2norm").unwrap(), Reduce::L2Norm);
let err = BinMode::from_str("median").unwrap_err().to_string();
assert_eq!(err, "bin_mode median invalid");
assert_eq!(
Reduce::from_str("median").unwrap_err().to_string(),
"reduce median invalid"
);
}
}