Skip to main content

chematic_fp/
bitvec.rs

1//! Fixed-size 2048-bit bitvector backed by 32 × u64 words.
2
3/// A 2048-bit bitvector.
4///
5/// The vector is stored as 32 little-endian 64-bit words.
6/// Bit `n` resides in word `n / 64`, at position `n % 64`.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct BitVec2048 {
9    words: [u64; 32],
10}
11
12impl Default for BitVec2048 {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl BitVec2048 {
19    /// Create a new all-zero bitvector.
20    pub fn new() -> Self {
21        Self { words: [0u64; 32] }
22    }
23
24    /// Set bit `bit` to 1.
25    ///
26    /// # Panics
27    /// Panics if `bit >= 2048`.
28    pub fn set(&mut self, bit: usize) {
29        assert!(
30            bit < 2048,
31            "bit index {bit} out of range for BitVec2048 (max 2047)"
32        );
33        self.words[bit / 64] |= 1u64 << (bit % 64);
34    }
35
36    /// Return the value of bit `bit`.
37    ///
38    /// # Panics
39    /// Panics if `bit >= 2048`.
40    pub fn get(&self, bit: usize) -> bool {
41        assert!(
42            bit < 2048,
43            "bit index {bit} out of range for BitVec2048 (max 2047)"
44        );
45        (self.words[bit / 64] >> (bit % 64)) & 1 == 1
46    }
47
48    // SIMD note: #![forbid(unsafe_code)] + wasm32 target means hand-rolled
49    // intrinsics (std::arch) are not an option. LLVM autovectorizes these loops
50    // to AVX2/NEON with --release; #[inline] lets it see the fixed trip count
51    // (32 words) and apply unrolling. u64::count_ones() → hardware POPCNT.
52
53    /// Count the number of bits set to 1 (popcount / Hamming weight).
54    #[inline]
55    pub fn popcount(&self) -> u32 {
56        self.words.iter().map(|w| w.count_ones()).sum()
57    }
58
59    /// Bitwise AND of two bitvectors.
60    #[inline]
61    pub fn and(&self, other: &Self) -> Self {
62        let mut result = Self::new();
63        for (out, (a, b)) in result
64            .words
65            .iter_mut()
66            .zip(self.words.iter().zip(other.words.iter()))
67        {
68            *out = a & b;
69        }
70        result
71    }
72
73    /// Bitwise OR of two bitvectors.
74    #[inline]
75    pub fn or(&self, other: &Self) -> Self {
76        let mut result = Self::new();
77        for (out, (a, b)) in result
78            .words
79            .iter_mut()
80            .zip(self.words.iter().zip(other.words.iter()))
81        {
82            *out = a | b;
83        }
84        result
85    }
86
87    /// Count the number of bits set in `A & B` without allocating a temporary.
88    ///
89    /// This is the fundamental primitive for bulk Tanimoto computation:
90    /// hoisting the query popcount and reusing this per db entry avoids
91    /// the per-pair allocation of `self.and(other).popcount()`.
92    #[inline]
93    pub fn intersection_popcount(&self, other: &Self) -> u32 {
94        self.words
95            .iter()
96            .zip(other.words.iter())
97            .map(|(a, b)| (a & b).count_ones())
98            .sum()
99    }
100
101    /// Tanimoto similarity given precomputed popcounts, returning `f32`.
102    ///
103    /// Use when calling one query against many db entries: hoist
104    /// `self_popcount = query.popcount()` outside the loop, compute
105    /// `other_popcount = db_entry.popcount()` once per entry, then call
106    /// this instead of `tanimoto()` to avoid redundant allocation and
107    /// recomputation.
108    ///
109    /// Returns `1.0` when union is zero (both all-zero convention).
110    #[inline]
111    pub fn tanimoto_with_counts(
112        &self,
113        other: &Self,
114        self_popcount: u32,
115        other_popcount: u32,
116    ) -> f32 {
117        let inter = self.intersection_popcount(other) as f32;
118        let union = self_popcount as f32 + other_popcount as f32 - inter;
119        if union == 0.0 { 1.0 } else { inter / union }
120    }
121
122    /// Tanimoto similarity: `|A & B| / |A | B|`.
123    ///
124    /// Returns `1.0` when both vectors are all-zero (the empty-set convention).
125    pub fn tanimoto(&self, other: &Self) -> f64 {
126        let intersection = self.and(other).popcount() as f64;
127        let a = self.popcount() as f64;
128        let b = other.popcount() as f64;
129        let union = a + b - intersection;
130        if union == 0.0 {
131            1.0
132        } else {
133            intersection / union
134        }
135    }
136
137    /// Dice similarity: `2|A & B| / (|A| + |B|)`.
138    ///
139    /// Returns `1.0` when both vectors are all-zero.
140    pub fn dice(&self, other: &Self) -> f64 {
141        let intersection = self.and(other).popcount() as f64;
142        let a = self.popcount() as f64;
143        let b = other.popcount() as f64;
144        let denom = a + b;
145        if denom == 0.0 {
146            1.0
147        } else {
148            2.0 * intersection / denom
149        }
150    }
151
152    /// XOR-fold this 2048-bit vector to `bits` bits.
153    ///
154    /// The upper half is XOR-folded into the lower half, and the upper half is zeroed.
155    /// This is applied recursively until the target width is reached.
156    ///
157    /// `bits` must be 1024, 512, or 256.
158    ///
159    /// # Panics
160    /// Panics if `bits` is not one of 1024, 512, or 256.
161    pub fn fold(&self, bits: usize) -> Self {
162        assert!(
163            matches!(bits, 256 | 512 | 1024),
164            "fold target must be 1024, 512, or 256; got {bits}"
165        );
166
167        let mut current = self.clone();
168        let mut current_bits = 2048usize;
169
170        while current_bits > bits {
171            let half_words = current_bits / 64 / 2; // number of 64-bit words in half
172            let mut folded = Self::new();
173            for i in 0..half_words {
174                folded.words[i] = current.words[i] ^ current.words[i + half_words];
175            }
176            current = folded;
177            current_bits /= 2;
178        }
179
180        current
181    }
182
183    /// Convert to a variable-length BitVecN with the same bits.
184    pub fn to_bitvecn(&self) -> BitVecN {
185        BitVecN {
186            words: self.words.to_vec(),
187            bits: 2048,
188        }
189    }
190}
191
192/// A variable-length bitvector with dynamic bit width.
193///
194/// Unlike `BitVec2048` which is fixed at 2048 bits, `BitVecN` allows
195/// arbitrary bit widths (512, 1024, 2048, 4096, etc.).
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct BitVecN {
198    words: Vec<u64>,
199    bits: usize,
200}
201
202impl BitVecN {
203    /// Create a new all-zero bitvector with specified bit width.
204    ///
205    /// # Panics
206    /// Panics if `bits == 0`.
207    pub fn new(bits: usize) -> Self {
208        assert!(bits > 0, "BitVecN must have at least 1 bit");
209        let num_words = bits.div_ceil(64);
210        Self {
211            words: vec![0u64; num_words],
212            bits,
213        }
214    }
215
216    /// Return the bit width.
217    pub fn bit_width(&self) -> usize {
218        self.bits
219    }
220
221    /// Set bit `bit` to 1.
222    ///
223    /// # Panics
224    /// Panics if `bit >= self.bits`.
225    pub fn set(&mut self, bit: usize) {
226        assert!(
227            bit < self.bits,
228            "bit index {bit} out of range for BitVecN (max {})",
229            self.bits - 1
230        );
231        self.words[bit / 64] |= 1u64 << (bit % 64);
232    }
233
234    /// Return the value of bit `bit`.
235    ///
236    /// # Panics
237    /// Panics if `bit >= self.bits`.
238    pub fn get(&self, bit: usize) -> bool {
239        assert!(
240            bit < self.bits,
241            "bit index {bit} out of range for BitVecN (max {})",
242            self.bits - 1
243        );
244        (self.words[bit / 64] >> (bit % 64)) & 1 == 1
245    }
246
247    /// Count the number of bits set to 1.
248    pub fn popcount(&self) -> u32 {
249        self.words.iter().map(|w| w.count_ones()).sum()
250    }
251
252    /// Tanimoto similarity with another BitVecN.
253    ///
254    /// Both vectors must have the same bit width.
255    ///
256    /// # Panics
257    /// Panics if the two vectors have different bit widths.
258    pub fn tanimoto(&self, other: &Self) -> f64 {
259        assert_eq!(
260            self.bits, other.bits,
261            "BitVecN tanimoto requires same bit width"
262        );
263        let mut intersection = 0u32;
264        for (a, b) in self.words.iter().zip(other.words.iter()) {
265            intersection += (a & b).count_ones();
266        }
267        let intersection = intersection as f64;
268        let a = self.popcount() as f64;
269        let b = other.popcount() as f64;
270        let union = a + b - intersection;
271        if union == 0.0 {
272            1.0
273        } else {
274            intersection / union
275        }
276    }
277
278    /// Convert from a BitVec2048 (truncates to same 2048 bits).
279    pub fn from_bitvec2048(bv: &BitVec2048) -> Self {
280        BitVecN {
281            words: bv.words.to_vec(),
282            bits: 2048,
283        }
284    }
285
286    /// Convert to a BitVec2048 if the bit width is exactly 2048.
287    ///
288    /// Returns None if the bit width is not 2048.
289    pub fn to_bitvec2048(&self) -> Option<BitVec2048> {
290        if self.bits != 2048 {
291            return None;
292        }
293        let mut arr = [0u64; 32];
294        for (i, &w) in self.words.iter().enumerate() {
295            arr[i] = w;
296        }
297        Some(BitVec2048 { words: arr })
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    // ===== BitVec2048 Tests =====
306
307    #[test]
308    fn new_bitvec_is_all_zero() {
309        let bv = BitVec2048::new();
310        assert_eq!(bv.popcount(), 0, "new bitvec must have popcount 0");
311    }
312
313    #[test]
314    fn set_and_get_boundary_bits() {
315        let mut bv = BitVec2048::new();
316        bv.set(0);
317        bv.set(2047);
318        assert_eq!(bv.popcount(), 2);
319        assert!(bv.get(0));
320        assert!(!bv.get(1));
321        assert!(bv.get(2047));
322    }
323
324    #[test]
325    fn tanimoto_identical_nonzero() {
326        let mut bv = BitVec2048::new();
327        bv.set(42);
328        bv.set(100);
329        assert_eq!(bv.tanimoto(&bv.clone()), 1.0);
330    }
331
332    #[test]
333    fn tanimoto_disjoint_is_zero() {
334        let mut a = BitVec2048::new();
335        a.set(10);
336        let mut b = BitVec2048::new();
337        b.set(20);
338        assert_eq!(a.tanimoto(&b), 0.0);
339    }
340
341    #[test]
342    fn dice_identical_nonzero() {
343        let mut bv = BitVec2048::new();
344        bv.set(7);
345        assert_eq!(bv.dice(&bv.clone()), 1.0);
346    }
347
348    #[test]
349    fn fold_1024_popcount_leq_original() {
350        // XOR-fold can only reduce or maintain the popcount.
351        let mut bv = BitVec2048::new();
352        for i in (0..2048).step_by(3) {
353            bv.set(i);
354        }
355        let folded = bv.fold(1024);
356        assert!(
357            folded.popcount() <= bv.popcount(),
358            "folded popcount ({}) must be <= original ({})",
359            folded.popcount(),
360            bv.popcount()
361        );
362    }
363
364    #[test]
365    fn and_or_basic_correctness() {
366        let mut a = BitVec2048::new();
367        a.set(5);
368        a.set(10);
369
370        let mut b = BitVec2048::new();
371        b.set(10);
372        b.set(15);
373
374        let and = a.and(&b);
375        assert!(and.get(10), "AND: shared bit 10 should be set");
376        assert!(!and.get(5), "AND: bit 5 only in A should be clear");
377        assert!(!and.get(15), "AND: bit 15 only in B should be clear");
378
379        let or = a.or(&b);
380        assert!(or.get(5), "OR: bit 5 should be set");
381        assert!(or.get(10), "OR: bit 10 should be set");
382        assert!(or.get(15), "OR: bit 15 should be set");
383        assert!(!or.get(0), "OR: bit 0 should be clear");
384    }
385
386    // ===== BitVecN Tests =====
387
388    #[test]
389    fn bitvecn_new_creates_zero_vector() {
390        let bv = BitVecN::new(512);
391        assert_eq!(bv.bit_width(), 512);
392        assert_eq!(bv.popcount(), 0);
393    }
394
395    #[test]
396    fn bitvecn_set_get_basic() {
397        let mut bv = BitVecN::new(1024);
398        bv.set(0);
399        bv.set(512);
400        bv.set(1023);
401        assert!(bv.get(0));
402        assert!(bv.get(512));
403        assert!(bv.get(1023));
404        assert!(!bv.get(1));
405        assert_eq!(bv.popcount(), 3);
406    }
407
408    #[test]
409    fn bitvecn_tanimoto_identical() {
410        let mut bv = BitVecN::new(256);
411        bv.set(10);
412        bv.set(50);
413        assert_eq!(bv.tanimoto(&bv.clone()), 1.0);
414    }
415
416    #[test]
417    fn bitvecn_tanimoto_disjoint() {
418        let mut a = BitVecN::new(256);
419        a.set(5);
420        let mut b = BitVecN::new(256);
421        b.set(10);
422        assert_eq!(a.tanimoto(&b), 0.0);
423    }
424
425    #[test]
426    fn bitvecn_conversion_to_from_2048() {
427        let mut bv2048 = BitVec2048::new();
428        bv2048.set(42);
429        bv2048.set(100);
430
431        let bvn = BitVecN::from_bitvec2048(&bv2048);
432        assert_eq!(bvn.bit_width(), 2048);
433        assert!(bvn.get(42));
434        assert!(bvn.get(100));
435        assert_eq!(bvn.popcount(), 2);
436
437        let bv2048_back = bvn.to_bitvec2048();
438        assert_eq!(bv2048_back, Some(bv2048));
439    }
440
441    #[test]
442    fn bitvecn_arbitrary_sizes() {
443        for size in [128, 256, 512, 1024, 2048, 4096] {
444            let mut bv = BitVecN::new(size);
445            bv.set(0);
446            bv.set(size - 1);
447            assert_eq!(bv.popcount(), 2);
448            assert_eq!(bv.bit_width(), size);
449        }
450    }
451}