use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BitVec {
words: Vec<u64>,
len: usize,
}
impl BitVec {
pub fn zeros(len: usize) -> Self {
let words = vec![0u64; len.div_ceil(64)];
Self { words, len }
}
pub fn ones(len: usize) -> Self {
let mut bv = Self::zeros(len);
for i in 0..len {
bv.set(i, true);
}
bv
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn get(&self, i: usize) -> bool {
assert!(
i < self.len,
"BitVec::get index {i} out of bounds {}",
self.len
);
let (w, b) = (i / 64, i % 64);
(self.words[w] >> b) & 1 == 1
}
#[inline]
pub fn set(&mut self, i: usize, v: bool) {
assert!(
i < self.len,
"BitVec::set index {i} out of bounds {}",
self.len
);
let (w, b) = (i / 64, i % 64);
if v {
self.words[w] |= 1u64 << b;
} else {
self.words[w] &= !(1u64 << b);
}
}
pub fn count_ones(&self) -> usize {
self.words.iter().map(|w| w.count_ones() as usize).sum()
}
pub fn iter_ones(&self) -> impl Iterator<Item = usize> + '_ {
(0..self.len).filter(move |&i| self.get(i))
}
pub fn clear(&mut self) {
for w in self.words.iter_mut() {
*w = 0;
}
}
}
#[derive(Debug, Clone)]
pub struct SparseAtomCode {
pub active_mask: BitVec,
pub weights: Vec<f64>,
}
impl SparseAtomCode {
pub fn empty(k_atoms: usize) -> Self {
Self {
active_mask: BitVec::zeros(k_atoms),
weights: vec![0.0; k_atoms],
}
}
pub fn k_atoms(&self) -> usize {
self.weights.len()
}
pub fn n_active(&self) -> usize {
self.active_mask.count_ones()
}
pub fn assign(&mut self, k: usize, w: f64) {
assert!(k < self.k_atoms());
self.active_mask.set(k, true);
self.weights[k] = w;
}
}
#[derive(Debug, Clone)]
pub struct SparseAtomCodes {
codes: Vec<SparseAtomCode>,
k_atoms: usize,
}
impl SparseAtomCodes {
pub fn empty(n_obs: usize, k_atoms: usize) -> Self {
let codes = (0..n_obs).map(|_| SparseAtomCode::empty(k_atoms)).collect();
Self { codes, k_atoms }
}
pub fn n_obs(&self) -> usize {
self.codes.len()
}
pub fn k_atoms(&self) -> usize {
self.k_atoms
}
pub fn row(&self, n: usize) -> &SparseAtomCode {
&self.codes[n]
}
pub fn row_mut(&mut self, n: usize) -> &mut SparseAtomCode {
&mut self.codes[n]
}
pub fn iter(&self) -> impl Iterator<Item = &SparseAtomCode> {
self.codes.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut SparseAtomCode> {
self.codes.iter_mut()
}
pub fn coactive_pair_stats(&self) -> Vec<(usize, usize, CoactivationStats)> {
#[derive(Clone, Copy, Debug, Default)]
struct PairAccum {
n_joint: usize,
sum_a: f64,
sum_b: f64,
sum_a2: f64,
sum_b2: f64,
sum_ab: f64,
}
let n_obs = self.n_obs();
let mut marg = vec![0usize; self.k_atoms];
let mut pairs: BTreeMap<(usize, usize), PairAccum> = BTreeMap::new();
for code in &self.codes {
let active: Vec<usize> = code.active_mask.iter_ones().collect();
for &atom in &active {
marg[atom] += 1;
}
for (idx, &u) in active.iter().enumerate() {
for &v in &active[idx + 1..] {
let (a, b) = if u < v { (u, v) } else { (v, u) };
let wa = code.weights[a];
let wb = code.weights[b];
let acc = pairs.entry((a, b)).or_default();
acc.n_joint += 1;
acc.sum_a += wa;
acc.sum_b += wb;
acc.sum_a2 += wa * wa;
acc.sum_b2 += wb * wb;
acc.sum_ab += wa * wb;
}
}
}
pairs
.into_iter()
.map(|((a, b), acc)| {
let weight_correlation = if acc.n_joint < 2 {
0.0
} else {
let n = acc.n_joint as f64;
let cov = acc.sum_ab - acc.sum_a * acc.sum_b / n;
let var_a = acc.sum_a2 - acc.sum_a * acc.sum_a / n;
let var_b = acc.sum_b2 - acc.sum_b * acc.sum_b / n;
if var_a > 0.0 && var_b > 0.0 {
(cov / (var_a.sqrt() * var_b.sqrt())).clamp(-1.0, 1.0)
} else {
0.0
}
};
let stats = CoactivationStats::from_counts(
n_obs,
marg[a],
marg[b],
acc.n_joint,
weight_correlation,
);
(a, b, stats)
})
.collect()
}
pub fn support_entropy(&self) -> SupportEntropy {
let n = self.n_obs();
let g = self.k_atoms();
if n == 0 || g == 0 {
return SupportEntropy {
tree_bits: 0.0,
independent_bits: 0.0,
combinatorial_bits: 0.0,
mean_support: 0.0,
};
}
let mut marg = vec![0.0_f64; g];
let mut co = vec![0.0_f64; g * g]; let mut total_active = 0.0_f64;
for code in &self.codes {
let active: Vec<usize> = code.active_mask.iter_ones().collect();
total_active += active.len() as f64;
for (idx, &u) in active.iter().enumerate() {
marg[u] += 1.0;
for &v in &active[idx + 1..] {
co[u * g + v] += 1.0;
}
}
}
let nn = n as f64;
let independent_total: f64 = (0..g)
.map(|atom| kt_bernoulli_bits(self.codes.iter().map(|code| code.active_mask.get(atom))))
.sum();
let mi = |u: usize, v: usize| -> f64 {
let (a, b) = if u < v { (u, v) } else { (v, u) };
mutual_information_bits(nn, marg[a], marg[b], co[a * g + b])
};
let mut in_tree = vec![false; g];
let mut best_mi = vec![f64::NEG_INFINITY; g];
let mut best_parent = vec![0usize; g];
let mut parent = vec![0usize; g];
in_tree[0] = true;
for v in 1..g {
best_mi[v] = mi(0, v);
best_parent[v] = 0;
}
for _ in 1..g {
let mut pick = usize::MAX;
let mut pick_w = f64::NEG_INFINITY;
for v in 0..g {
if !in_tree[v] && best_mi[v] > pick_w {
pick_w = best_mi[v];
pick = v;
}
}
if pick == usize::MAX {
break;
}
in_tree[pick] = true;
parent[pick] = best_parent[pick];
for v in 0..g {
if !in_tree[v] {
let w = mi(pick, v);
if w > best_mi[v] {
best_mi[v] = w;
best_parent[v] = pick;
}
}
}
}
let tree_structure_bits = if g <= 2 {
0.0
} else {
(g as f64 - 2.0) * (g as f64).log2()
};
let mut tree_total = tree_structure_bits
+ kt_bernoulli_bits(self.codes.iter().map(|code| code.active_mask.get(0)));
for child in 1..g {
tree_total += kt_conditional_bernoulli_bits(self.codes.iter().map(|code| {
(
code.active_mask.get(parent[child]),
code.active_mask.get(child),
)
}));
}
let mean_support = total_active / nn;
let cardinality_bits = (g as f64 + 1.0).log2();
let combinatorial_bits = cardinality_bits
+ self
.codes
.iter()
.map(|code| log2_binom(g as i64, code.active_mask.count_ones() as i64))
.sum::<f64>()
/ nn;
SupportEntropy {
tree_bits: tree_total / nn,
independent_bits: independent_total / nn,
combinatorial_bits,
mean_support,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SupportEntropy {
pub tree_bits: f64,
pub independent_bits: f64,
pub combinatorial_bits: f64,
pub mean_support: f64,
}
fn kt_bernoulli_bits(values: impl IntoIterator<Item = bool>) -> f64 {
let mut counts = [0.5_f64, 0.5_f64];
let mut bits = 0.0;
for value in values {
let index = usize::from(value);
let probability = counts[index] / (counts[0] + counts[1]);
bits -= probability.log2();
counts[index] += 1.0;
}
bits
}
fn kt_conditional_bernoulli_bits(values: impl IntoIterator<Item = (bool, bool)>) -> f64 {
let mut counts = [[0.5_f64, 0.5_f64], [0.5_f64, 0.5_f64]];
let mut bits = 0.0;
for (context, value) in values {
let context_index = usize::from(context);
let value_index = usize::from(value);
let probability = counts[context_index][value_index]
/ (counts[context_index][0] + counts[context_index][1]);
bits -= probability.log2();
counts[context_index][value_index] += 1.0;
}
bits
}
fn mutual_information_bits(n: f64, n_u: f64, n_v: f64, n_uv: f64) -> f64 {
if n <= 0.0 {
return 0.0;
}
let p1x = n_u / n;
let px1 = n_v / n;
let p11 = n_uv / n;
let p10 = (p1x - p11).max(0.0);
let p01 = (px1 - p11).max(0.0);
let p00 = (1.0 - p11 - p10 - p01).max(0.0);
let cell = |p: f64, pa: f64, pb: f64| -> f64 {
if p > 0.0 && pa > 0.0 && pb > 0.0 {
p * (p / (pa * pb)).log2()
} else {
0.0
}
};
let mi = cell(p11, p1x, px1)
+ cell(p10, p1x, 1.0 - px1)
+ cell(p01, 1.0 - p1x, px1)
+ cell(p00, 1.0 - p1x, 1.0 - px1);
mi.max(0.0)
}
fn log2_binom(g: i64, k: i64) -> f64 {
if g <= 0 || k <= 0 {
return 0.0;
}
let k = k.min(g);
let mut bits = 0.0;
for i in 1..=k {
bits += ((g - k + i) as f64 / i as f64).log2();
}
bits
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CoactivationStats {
pub n_obs: usize,
pub n_a: usize,
pub n_b: usize,
pub n_joint: usize,
pub p_a_given_b: f64,
pub p_b_given_a: f64,
pub lift: f64,
pub weight_correlation: f64,
}
impl CoactivationStats {
fn from_counts(
n_obs: usize,
n_a: usize,
n_b: usize,
n_joint: usize,
weight_correlation: f64,
) -> Self {
let cond = |joint: usize, marg: usize| {
if marg == 0 {
0.0
} else {
joint as f64 / marg as f64
}
};
let lift = if n_a == 0 || n_b == 0 || n_obs == 0 {
0.0
} else {
(n_joint as f64 * n_obs as f64) / (n_a as f64 * n_b as f64)
};
Self {
n_obs,
n_a,
n_b,
n_joint,
p_a_given_b: cond(n_joint, n_b),
p_b_given_a: cond(n_joint, n_a),
lift,
weight_correlation,
}
}
pub fn dependence(&self) -> f64 {
self.p_a_given_b.min(self.p_b_given_a)
}
pub fn absorption_asymmetry(&self) -> f64 {
(self.p_a_given_b - self.p_b_given_a).abs()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bitvec_basic() {
let mut bv = BitVec::zeros(70);
assert_eq!(bv.len(), 70);
assert!(!bv.get(5));
bv.set(5, true);
bv.set(64, true);
assert!(bv.get(5));
assert!(bv.get(64));
assert_eq!(bv.count_ones(), 2);
let ones: Vec<usize> = bv.iter_ones().collect();
assert_eq!(ones, vec![5, 64]);
bv.set(5, false);
assert_eq!(bv.count_ones(), 1);
}
}