use crate::block::expect_len;
use comnum_core::{Result, ensure};
use comnum_math::bigint::BigUint;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Ccdm {
composition: Vec<usize>,
n: usize,
k: usize,
total: BigUint,
}
impl Ccdm {
pub fn new(composition: Vec<usize>) -> Result<Ccdm> {
let n: usize = composition.iter().sum();
ensure(n > 0, || "the composition is empty".into())?;
let mut total = BigUint::from(1);
let mut left = n as u64;
for &c in &composition {
for i in 1..=c as u64 {
total = total.mul_small(left - c as u64 + i).div_small(i).0;
}
left -= c as u64;
}
let k = total.bits().saturating_sub(1);
ensure(k >= 1, || "the composition has a single sequence: it carries no bit".into())?;
Ok(Ccdm { composition, n, k, total })
}
pub fn from_distribution(p: &[f64], n: usize) -> Result<Ccdm> {
let s: f64 = p.iter().sum();
ensure(!p.is_empty() && s > 0.0 && p.iter().all(|v| *v >= 0.0), || {
"the distribution must be non-negative with a positive sum".into()
})?;
let target: Vec<f64> = p.iter().map(|v| v / s * n as f64).collect();
let mut c: Vec<usize> = target.iter().map(|t| t.floor() as usize).collect();
let mut order: Vec<usize> = (0..p.len()).collect();
order.sort_by(|&a, &b| {
(target[b] - target[b].floor()).total_cmp(&(target[a] - target[a].floor())).then(a.cmp(&b))
});
let missing = n - c.iter().sum::<usize>();
for &a in order.iter().take(missing) {
c[a] += 1;
}
Ccdm::new(c)
}
pub fn composition(&self) -> &[usize] {
&self.composition
}
pub fn n(&self) -> usize {
self.n
}
pub fn k(&self) -> usize {
self.k
}
pub fn rate(&self) -> f64 {
self.k as f64 / self.n as f64
}
pub fn distribution(&self) -> Vec<f64> {
self.composition.iter().map(|&c| c as f64 / self.n as f64).collect()
}
pub fn match_bits(&self, bits: &[u8]) -> Result<Vec<usize>> {
expect_len(bits.len(), self.k, "bits")?;
let mut r = BigUint::from_bits(bits);
let mut count = self.composition.clone();
let mut total = self.total.clone();
let mut left = self.n as u64;
let mut out = Vec::with_capacity(self.n);
for _ in 0..self.n {
for (a, c) in count.iter_mut().enumerate() {
if *c == 0 {
continue;
}
let with_a = total.mul_small(*c as u64).div_small(left).0;
if r < with_a {
out.push(a);
total = with_a;
*c -= 1;
left -= 1;
break;
}
r = r.sub(&with_a);
}
}
Ok(out)
}
pub fn dematch(&self, symbols: &[usize]) -> Result<Vec<u8>> {
expect_len(symbols.len(), self.n, "symbols")?;
let mut r = BigUint::default();
let mut count = self.composition.clone();
let mut total = self.total.clone();
let mut left = self.n as u64;
for &s in symbols {
let s = (0..count.len()).filter(|&a| count[a] > 0).min_by_key(|&a| (a.abs_diff(s), a)).unwrap_or(0);
for &c in count.iter().take(s) {
if c > 0 {
r = r.add(&total.mul_small(c as u64).div_small(left).0);
}
}
total = total.mul_small(count[s] as u64).div_small(left).0;
count[s] -= 1;
left -= 1;
}
Ok(r.to_bits(self.k))
}
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
#[test]
fn matching_is_a_bijection_onto_the_composition() {
let dm = Ccdm::new(vec![3, 2, 1]).unwrap(); assert_eq!(dm.k(), 5);
let mut seen = std::collections::HashSet::new();
for r in 0..32u8 {
let bits: Vec<u8> = (0..5).rev().map(|i| (r >> i) & 1).collect();
let a = dm.match_bits(&bits).unwrap();
assert_eq!(a.iter().filter(|&&s| s == 0).count(), 3);
assert_eq!(dm.dematch(&a).unwrap(), bits);
assert!(seen.insert(a));
}
}
#[test]
fn long_blocks_approach_the_entropy() {
let p = [0.35, 0.3, 0.2, 0.15];
let h: f64 = p.iter().map(|v: &f64| -v * v.log2()).sum();
let mut rng = Rng::new(1);
for n in [64, 512, 4096] {
let dm = Ccdm::from_distribution(&p, n).unwrap();
let bits: Vec<u8> = (0..dm.k()).map(|_| rng.bit()).collect();
assert_eq!(dm.dematch(&dm.match_bits(&bits).unwrap()).unwrap(), bits);
assert!(h - dm.rate() < 4.0 * (n as f64).log2() / n as f64, "{n}: {}", h - dm.rate());
}
}
#[test]
fn a_wrong_sequence_still_gives_k_bits() {
let dm = Ccdm::from_distribution(&[0.5, 0.3, 0.2], 50).unwrap();
let wrong = vec![2; 50];
assert_eq!(dm.dematch(&wrong).unwrap().len(), dm.k());
}
}