Skip to main content

csm_core_lib/
bundle.rs

1//! Incremental bundle accumulator for streaming/sliding-window memory.
2
3#[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
4use crate::bundle_simd::{finalize_simd_avx2, update_counts_simd_avx2};
5#[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
6use crate::bundle_simd::{finalize_simd_neon, update_counts_simd_neon};
7use crate::error::{MemoryError, Result};
8use crate::hyperdim::HVec10240;
9
10/// Incremental bundle accumulator for streaming/sliding-window memory.
11///
12/// Maintains signed bit counts for efficient add/remove operations.
13/// Finalize applies majority threshold to produce a bundled hypervector.
14#[derive(Debug, Clone)]
15pub struct BundleAccumulator {
16    counts: Box<[i32; HVec10240::DIMENSION]>,
17    n: u32,
18}
19
20impl Default for BundleAccumulator {
21    fn default() -> Self {
22        Self {
23            counts: Box::new([0i32; HVec10240::DIMENSION]),
24            n: 0,
25        }
26    }
27}
28
29impl BundleAccumulator {
30    /// Create a new empty accumulator.
31    pub fn new() -> Self {
32        Self {
33            counts: Box::new([0i32; HVec10240::DIMENSION]),
34            n: 0,
35        }
36    }
37
38    /// Add a hypervector to the accumulator.
39    pub fn add(&mut self, hv: &HVec10240) {
40        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
41        {
42            if is_x86_feature_detected!("avx2") {
43                // SAFETY: AVX2 feature detected at runtime.
44                unsafe { update_counts_simd_avx2(&mut self.counts, &hv.data, 1) };
45                self.n += 1;
46                return;
47            }
48        }
49
50        #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
51        {
52            // SAFETY: update_counts_simd_neon is safe on aarch64.
53            unsafe { update_counts_simd_neon(&mut self.counts, &hv.data, 1) };
54            self.n += 1;
55        }
56
57        #[cfg(not(all(not(target_arch = "wasm32"), target_arch = "aarch64")))]
58        {
59            for i in 0..80 {
60                let mut val = hv.data[i];
61                while val != 0 {
62                    let j = val.trailing_zeros() as usize;
63                    self.counts[i * 128 + j] += 1;
64                    val &= val - 1;
65                }
66            }
67            self.n += 1;
68        }
69    }
70
71    /// Remove a hypervector from the accumulator.
72    ///
73    /// Saturates at zero: removing from an empty accumulator is a no-op.
74    /// Use [`Self::try_remove`] if you need to detect underflow.
75    pub fn remove(&mut self, hv: &HVec10240) {
76        if self.n == 0 {
77            return;
78        }
79
80        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
81        {
82            if is_x86_feature_detected!("avx2") {
83                // SAFETY: AVX2 feature detected at runtime.
84                unsafe { update_counts_simd_avx2(&mut self.counts, &hv.data, -1) };
85                self.n -= 1;
86                return;
87            }
88        }
89
90        #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
91        {
92            // SAFETY: update_counts_simd_neon is safe on aarch64.
93            unsafe { update_counts_simd_neon(&mut self.counts, &hv.data, -1) };
94            self.n -= 1;
95        }
96
97        #[cfg(not(all(not(target_arch = "wasm32"), target_arch = "aarch64")))]
98        {
99            for i in 0..80 {
100                let mut val = hv.data[i];
101                while val != 0 {
102                    let j = val.trailing_zeros() as usize;
103                    self.counts[i * 128 + j] -= 1;
104                    val &= val - 1;
105                }
106            }
107            self.n -= 1;
108        }
109    }
110
111    /// Remove a hypervector from the accumulator, returning an error if empty.
112    ///
113    /// Returns `Err(MemoryError::InvalidInput)` when the accumulator is empty.
114    pub fn try_remove(&mut self, hv: &HVec10240) -> Result<()> {
115        if self.n == 0 {
116            return Err(MemoryError::InvalidInput {
117                field: "accumulator".to_string(),
118                reason: "cannot remove from empty BundleAccumulator".to_string(),
119            });
120        }
121
122        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
123        {
124            if is_x86_feature_detected!("avx2") {
125                // SAFETY: AVX2 feature detected at runtime.
126                unsafe { update_counts_simd_avx2(&mut self.counts, &hv.data, -1) };
127                self.n -= 1;
128                return Ok(());
129            }
130        }
131
132        #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
133        {
134            // SAFETY: update_counts_simd_neon is safe on aarch64.
135            unsafe { update_counts_simd_neon(&mut self.counts, &hv.data, -1) };
136            self.n -= 1;
137            return Ok(());
138        }
139
140        #[cfg(not(all(not(target_arch = "wasm32"), target_arch = "aarch64")))]
141        {
142            for i in 0..80 {
143                let mut val = hv.data[i];
144                while val != 0 {
145                    let j = val.trailing_zeros() as usize;
146                    self.counts[i * 128 + j] -= 1;
147                    val &= val - 1;
148                }
149            }
150            self.n -= 1;
151            Ok(())
152        }
153    }
154
155    /// Finalize the accumulator into a bundled hypervector.
156    ///
157    /// Applies majority threshold: bits with count > 0 are set to 1.
158    /// Returns zero vector if accumulator is empty.
159    pub fn finalize(&self) -> HVec10240 {
160        if self.n == 0 {
161            return HVec10240::zero();
162        }
163
164        let threshold = (self.n / 2) as i32;
165
166        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
167        {
168            if is_x86_feature_detected!("avx2") {
169                // SAFETY: AVX2 feature detected at runtime.
170                return HVec10240 {
171                    data: unsafe { finalize_simd_avx2(&self.counts, threshold) },
172                };
173            }
174        }
175
176        #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
177        {
178            // SAFETY: finalize_simd_neon is safe on aarch64.
179            return HVec10240 {
180                data: unsafe { finalize_simd_neon(&self.counts, threshold) },
181            };
182        }
183
184        #[cfg(not(all(not(target_arch = "wasm32"), target_arch = "aarch64")))]
185        {
186            let mut data = [0u128; 80];
187
188            for (i, word) in data.iter_mut().enumerate() {
189                let offset = i * 128;
190                for j in 0..128 {
191                    // Branchless bit construction to reduce misprediction penalties
192                    let condition = self.counts[offset + j] > threshold;
193                    *word |= (condition as u128) << j;
194                }
195            }
196
197            HVec10240 { data }
198        }
199    }
200
201    /// Get the number of hypervectors in the accumulator.
202    pub const fn len(&self) -> u32 {
203        self.n
204    }
205
206    /// Check if the accumulator is empty.
207    pub const fn is_empty(&self) -> bool {
208        self.n == 0
209    }
210
211    /// Clear the accumulator.
212    pub fn clear(&mut self) {
213        *self.counts = [0i32; HVec10240::DIMENSION];
214        self.n = 0;
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
221    use super::*;
222
223    #[test]
224    fn test_bundle_accumulator_add_finalize() {
225        let v1 = HVec10240::random();
226        let v2 = HVec10240::random();
227        let v3 = HVec10240::random();
228
229        let mut acc = BundleAccumulator::new();
230        acc.add(&v1);
231        acc.add(&v2);
232        acc.add(&v3);
233
234        let bundled = acc.finalize();
235        // Bundle should be valid (not zero)
236        assert_ne!(bundled, HVec10240::zero());
237        // Should have 3 vectors
238        assert_eq!(acc.len(), 3);
239    }
240
241    #[test]
242    fn test_bundle_accumulator_remove() {
243        let v1 = HVec10240::random();
244        let v2 = HVec10240::random();
245
246        let mut acc = BundleAccumulator::new();
247        acc.add(&v1);
248        acc.add(&v2);
249        acc.remove(&v2);
250
251        assert_eq!(acc.len(), 1);
252        let bundled = acc.finalize();
253        // Single vector bundle should be close to the original
254        assert!(bundled.cosine_similarity(&v1) > 0.9);
255    }
256
257    #[test]
258    fn test_bundle_accumulator_empty() {
259        let acc = BundleAccumulator::new();
260        assert!(acc.is_empty());
261        assert_eq!(acc.finalize(), HVec10240::zero());
262    }
263
264    #[test]
265    fn test_bundle_accumulator_clear() {
266        let mut acc = BundleAccumulator::new();
267        acc.add(&HVec10240::random());
268        acc.clear();
269        assert!(acc.is_empty());
270    }
271}