use crate::bucket_code::BucketCode;
use crate::util::and_popcount;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConstantWeightBitmap {
dim: usize,
weight: usize,
bits: Vec<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackedConstantWeightBitmap {
dim: usize,
weight: usize,
words: Vec<u64>,
}
impl ConstantWeightBitmap {
pub fn from_top_bucket(code: &BucketCode) -> Self {
let bits = code.top_bitmap();
let weight = bits.iter().filter(|&&bit| bit).count();
Self {
dim: bits.len(),
weight,
bits,
}
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn weight(&self) -> usize {
self.weight
}
pub fn bits(&self) -> &[bool] {
&self.bits
}
pub fn overlap(&self, other: &Self) -> usize {
assert_eq!(self.dim, other.dim, "bitmap dimensions must match");
self.bits
.iter()
.zip(&other.bits)
.filter(|&(lhs, rhs)| *lhs && *rhs)
.count()
}
}
impl PackedConstantWeightBitmap {
pub fn from_bucket_range(code: &BucketCode, start_bucket: usize, end_bucket: usize) -> Self {
assert!(start_bucket <= end_bucket, "bucket range must be ordered");
assert!(
end_bucket < code.spec().buckets(),
"bucket range must fit code spec"
);
let dim = code.codes().len();
let mut weight = 0usize;
let mut words = vec![0u64; dim.div_ceil(64)];
for (coordinate, &bucket) in code.codes().iter().enumerate() {
let bucket = bucket as usize;
if (start_bucket..=end_bucket).contains(&bucket) {
weight += 1;
words[coordinate / 64] |= 1u64 << (coordinate % 64);
}
}
Self { dim, weight, words }
}
pub fn from_top_group(code: &BucketCode, width: usize) -> Self {
assert!(width > 0, "top-group width must be positive");
assert!(
width <= code.spec().buckets(),
"top-group width must fit code spec"
);
let start = code.spec().buckets() - width;
Self::from_bucket_range(code, start, code.spec().buckets() - 1)
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn weight(&self) -> usize {
self.weight
}
pub fn words(&self) -> &[u64] {
&self.words
}
pub fn overlap(&self, other: &Self) -> usize {
assert_eq!(self.dim, other.dim, "bitmap dimensions must match");
assert!(
self.dim <= u32::MAX as usize,
"bitmap dim {} exceeds u32::MAX; and_popcount accumulates in u32 and would overflow",
self.dim
);
and_popcount(&self.words, &other.words) as usize
}
}
pub fn top_group_overlap_vector(
lhs: &BucketCode,
rhs: &BucketCode,
widths: &[usize],
) -> Vec<usize> {
assert_eq!(
lhs.spec(),
rhs.spec(),
"top_group_overlap_vector: lhs and rhs must share the same spec \
(dim and buckets must match); got lhs={:?}, rhs={:?}",
lhs.spec(),
rhs.spec()
);
widths
.iter()
.map(|&width| {
let lhs_bitmap = PackedConstantWeightBitmap::from_top_group(lhs, width);
let rhs_bitmap = PackedConstantWeightBitmap::from_top_group(rhs, width);
lhs_bitmap.overlap(&rhs_bitmap)
})
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BitmapNull {
dim: usize,
weight: usize,
}
impl BitmapNull {
pub fn new(dim: usize, weight: usize) -> Self {
assert!(dim > 0, "dim must be > 0");
assert!(weight <= dim, "weight must be <= dim");
Self { dim, weight }
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn weight(&self) -> usize {
self.weight
}
pub fn space_size(&self) -> u128 {
choose(self.dim, self.weight)
}
pub fn fiber_count(&self, overlap: usize) -> u128 {
if overlap > self.weight {
return 0;
}
let outside = self.weight - overlap;
if outside > self.dim - self.weight {
return 0;
}
choose(self.weight, overlap)
.checked_mul(choose(self.dim - self.weight, outside))
.expect("fiber count overflows u128")
}
pub fn tail_count(&self, threshold: usize) -> u128 {
if threshold == 0 {
return self.space_size();
}
if threshold > self.weight {
return 0;
}
(threshold..=self.weight)
.map(|overlap| self.fiber_count(overlap))
.sum()
}
pub fn tail_probability(&self, observed: usize) -> f64 {
if observed == 0 {
return 1.0;
}
if observed > self.weight {
return 0.0;
}
let space = self.space_size();
if space == 0 {
return 0.0;
}
let count = self.tail_count(observed);
let g = gcd(count, space);
(count / g) as f64 / (space / g) as f64
}
}
pub fn choose(n: usize, k: usize) -> u128 {
if k > n {
return 0;
}
let k = k.min(n - k);
let mut acc = 1u128;
for i in 0..k {
let num = (n - i) as u128;
let den = (i + 1) as u128;
let g = gcd(num, den);
acc = (acc / (den / g))
.checked_mul(num / g)
.expect("binomial coefficient C(n, k) overflows u128");
}
acc
}
fn gcd(mut a: u128, mut b: u128) -> u128 {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bucket_code::{BucketCode, CompositionSpec};
fn code(values: &[u8]) -> BucketCode {
BucketCode::new(
CompositionSpec::new(values.len(), 4).unwrap(),
values.to_vec(),
)
.unwrap()
}
fn naive_packed_overlap(
a: &PackedConstantWeightBitmap,
b: &PackedConstantWeightBitmap,
) -> usize {
a.words()
.iter()
.zip(b.words())
.map(|(x, y)| (x & y).count_ones() as usize)
.sum()
}
#[test]
fn top_bitmap_has_expected_constant_weight() {
let code = code(&[0, 0, 1, 1, 2, 2, 3, 3]);
let bitmap = ConstantWeightBitmap::from_top_bucket(&code);
assert_eq!(bitmap.dim(), 8);
assert_eq!(bitmap.weight(), 2);
}
#[test]
fn top_overlap_matches_naive_top_top_count() {
let query = code(&[0, 0, 1, 1, 2, 2, 3, 3]);
let doc = code(&[0, 1, 1, 2, 2, 3, 3, 0]);
let query_bitmap = ConstantWeightBitmap::from_top_bucket(&query);
let doc_bitmap = ConstantWeightBitmap::from_top_bucket(&doc);
assert_eq!(query_bitmap.overlap(&doc_bitmap), 1);
}
#[test]
fn packed_top_overlap_matches_naive_top_top_count() {
let query = code(&[0, 0, 1, 1, 2, 2, 3, 3]);
let doc = code(&[0, 1, 1, 2, 2, 3, 3, 0]);
let query_bitmap = PackedConstantWeightBitmap::from_top_group(&query, 1);
let doc_bitmap = PackedConstantWeightBitmap::from_top_group(&doc, 1);
assert_eq!(query_bitmap.dim(), 8);
assert_eq!(query_bitmap.weight(), 2);
assert_eq!(query_bitmap.overlap(&doc_bitmap), 1);
assert_eq!(
query_bitmap.overlap(&doc_bitmap),
naive_packed_overlap(&query_bitmap, &doc_bitmap)
);
}
#[test]
fn top_group_overlap_vector_uses_popcount_backed_bitmaps() {
let query = code(&[0, 0, 1, 1, 2, 2, 3, 3]);
let doc = code(&[0, 1, 1, 2, 2, 3, 3, 0]);
assert_eq!(
top_group_overlap_vector(&query, &doc, &[1, 2, 4]),
[1, 3, 8]
);
}
#[test]
fn bitmap_null_fibers_sum_to_space_size() {
let null = BitmapNull::new(10, 3);
let fiber_sum: u128 = (0..=3).map(|overlap| null.fiber_count(overlap)).sum();
assert_eq!(fiber_sum, choose(10, 3));
assert_eq!(null.space_size(), choose(10, 3));
}
#[test]
fn bitmap_tail_counts_have_boundary_values_and_are_monotone() {
let null = BitmapNull::new(10, 3);
assert_eq!(null.tail_count(0), choose(10, 3));
assert_eq!(null.tail_count(4), 0);
assert!(null.tail_count(2) <= null.tail_count(1));
assert!(null.tail_count(3) <= null.tail_count(2));
}
#[test]
fn null_fibers_partition_space_for_several_params() {
for (dim, weight) in [(8, 2), (10, 3), (16, 4), (20, 5), (32, 8), (5, 0), (5, 5)] {
let null = BitmapNull::new(dim, weight);
let fiber_sum: u128 = (0..=weight).map(|o| null.fiber_count(o)).sum();
assert_eq!(
fiber_sum,
null.space_size(),
"fibers must partition the space for (dim={dim}, weight={weight})"
);
}
}
#[test]
fn overlap_parity_const_vs_packed_vs_naive() {
let query = code(&[0, 0, 1, 1, 2, 2, 3, 3]);
let doc = code(&[3, 2, 1, 0, 0, 1, 2, 3]);
for width in 1..=4 {
let packed_q = PackedConstantWeightBitmap::from_top_group(&query, width);
let packed_d = PackedConstantWeightBitmap::from_top_group(&doc, width);
let packed_overlap = packed_q.overlap(&packed_d);
let naive = naive_packed_overlap(&packed_q, &packed_d);
assert_eq!(packed_overlap, naive, "packed vs naive at width {width}");
if width == 1 {
let const_q = ConstantWeightBitmap::from_top_bucket(&query);
let const_d = ConstantWeightBitmap::from_top_bucket(&doc);
assert_eq!(
const_q.overlap(&const_d),
packed_overlap,
"bool vs packed at the top bucket"
);
}
}
}
#[test]
fn packed_overlap_handles_multi_word_dim() {
let values: Vec<u8> = (0..128).map(|i| (i % 4) as u8).collect();
let code = BucketCode::new(CompositionSpec::new(128, 4).unwrap(), values).unwrap();
let bitmap = PackedConstantWeightBitmap::from_top_group(&code, 1);
assert_eq!(bitmap.dim(), 128);
assert_eq!(bitmap.words().len(), 2);
assert_eq!(bitmap.weight(), 32);
assert_eq!(bitmap.overlap(&bitmap), 32);
}
#[test]
fn choose_matches_known_small_binomials() {
assert_eq!(choose(0, 0), 1);
assert_eq!(choose(5, 0), 1);
assert_eq!(choose(5, 5), 1);
assert_eq!(choose(5, 2), 10);
assert_eq!(choose(10, 3), 120);
assert_eq!(choose(6, 3), 20);
assert_eq!(choose(52, 5), 2_598_960);
assert_eq!(choose(3, 4), 0);
}
#[test]
fn choose_is_symmetric() {
for n in 0..=30usize {
for k in 0..=n {
assert_eq!(
choose(n, k),
choose(n, n - k),
"C({n},{k}) == C({n},{})",
n - k
);
}
}
}
#[test]
fn choose_extends_range_via_gcd_cancellation() {
assert_eq!(choose(128, 64), choose(127, 63) + choose(127, 64));
assert!(choose(128, 64) > 0);
}
#[test]
#[should_panic(expected = "overflows u128")]
fn choose_panics_fail_loud_on_overflow() {
let _ = choose(300, 150);
}
#[test]
fn fiber_count_zero_outside_feasible_overlap() {
let null = BitmapNull::new(10, 3);
assert_eq!(null.fiber_count(4), 0);
assert_eq!(null.fiber_count(3), 1);
}
#[test]
fn tail_probability_is_well_formed() {
let null = BitmapNull::new(16, 4);
let space = null.space_size();
assert_eq!(null.tail_count(0), space);
for threshold in 0..=5 {
assert!(null.tail_count(threshold) <= space);
}
}
#[test]
fn packed_overlap_within_u32_max_does_not_panic() {
let values: Vec<u8> = (0..128).map(|i| (i % 4) as u8).collect();
let c = BucketCode::new(CompositionSpec::new(128, 4).unwrap(), values).unwrap();
let bm = PackedConstantWeightBitmap::from_top_group(&c, 1);
let _ = bm.overlap(&bm);
}
#[test]
#[should_panic(expected = "lhs and rhs must share the same spec")]
fn top_group_overlap_vector_panics_on_mismatched_spec() {
let lhs = BucketCode::new(
CompositionSpec::new(8, 4).unwrap(),
vec![0, 0, 1, 1, 2, 2, 3, 3],
)
.unwrap();
let rhs = BucketCode::new(
CompositionSpec::new(8, 2).unwrap(),
vec![0, 0, 0, 0, 1, 1, 1, 1],
)
.unwrap();
let _ = top_group_overlap_vector(&lhs, &rhs, &[1]);
}
#[test]
fn top_group_overlap_vector_passes_on_matching_spec() {
let lhs = code(&[0, 0, 1, 1, 2, 2, 3, 3]);
let rhs = code(&[0, 1, 1, 2, 2, 3, 3, 0]);
let _ = top_group_overlap_vector(&lhs, &rhs, &[1]);
}
#[test]
fn tail_probability_boundary_values() {
let null = BitmapNull::new(10, 3);
assert_eq!(null.tail_probability(0), 1.0);
assert_eq!(null.tail_probability(4), 0.0);
}
#[test]
fn tail_probability_known_value() {
let null = BitmapNull::new(10, 3);
let expected = 1.0_f64 / 120.0_f64;
let got = null.tail_probability(3);
assert!(
(got - expected).abs() < 1e-12,
"tail_probability(3) expected {expected} got {got}"
);
}
#[test]
fn tail_probability_is_in_unit_interval_and_monotone() {
let null = BitmapNull::new(16, 4);
let mut prev = 1.0_f64;
for threshold in 0..=5 {
let p = null.tail_probability(threshold);
assert!(
(0.0..=1.0).contains(&p),
"probability out of [0,1] at threshold={threshold}"
);
assert!(p <= prev, "tail_probability must be non-increasing");
prev = p;
}
}
#[test]
fn tail_probability_matches_exact_gcd_reduced_ratio() {
for &(dim, weight) in &[(10usize, 3usize), (16, 4), (64, 32), (100, 50)] {
let null = BitmapNull::new(dim, weight);
let space = null.space_size();
for observed in 0..=weight + 1 {
let count = null.tail_count(observed);
let g = gcd(count, space);
let expected = (count / g) as f64 / (space / g) as f64;
assert_eq!(
null.tail_probability(observed),
expected,
"dim={dim} weight={weight} observed={observed}"
);
assert!((0.0..=1.0).contains(&null.tail_probability(observed)));
}
}
}
}