hypervector/
lib.rs

1//! # Hypervector Crate
2//!
3//! This crate implements high-dimensional vectors for hyperdimensional computing and
4//! Vector Symbolic Architectures (VSAs). It currently provides two implementations:
5//!
6//! - **MBAT**: Bipolar vectors (elements in {-1, +1}).
7//! - **SSP**: Semantic Spatial Parameters (implemented in a separate module).
8//!
9//! Core components such as a global RNG, the `VSA` trait, and a generic `Hypervector` type
10//! are defined here.
11
12use once_cell::sync::Lazy;
13use rand::{rngs::StdRng, Rng, SeedableRng};
14use serde::{Deserialize, Serialize};
15use std::ops::{Add, Mul};
16use std::sync::Mutex;
17
18/// A lazily-initialized, thread-safe global RNG used for hypervector generation and operations.
19///
20/// This global RNG is used by all methods that do not receive their own RNG.
21static GLOBAL_RNG: Lazy<Mutex<StdRng>> = Lazy::new(|| Mutex::new(StdRng::from_entropy()));
22
23/// Sets the seed for the global RNG. This affects all hypervector generation and operations that
24/// use the global RNG.
25///
26/// # Example
27///
28/// ```rust
29/// use hypervector::set_global_seed;
30///
31/// // Set the global RNG seed to 42 for reproducibility.
32/// set_global_seed(42);
33/// ```
34pub fn set_global_seed(seed: u64) {
35    let mut rng = GLOBAL_RNG.lock().unwrap();
36    *rng = StdRng::seed_from_u64(seed);
37}
38
39/// Options for tie-breaking during the bundling operation.
40///
41/// When bundling two hypervectors, an element-wise sum may result in a tie (i.e. a zero).
42/// This enum specifies how such ties are resolved.
43#[derive(Debug, Clone, Copy)]
44pub enum TieBreaker {
45    /// Always choose +1 when a tie occurs.
46    AlwaysPositive,
47    /// Always choose -1 when a tie occurs.
48    AlwaysNegative,
49    /// Randomly choose between +1 and -1 when a tie occurs.
50    Random,
51}
52
53/// The `VSA` trait defines the interface for a Vector Symbolic Architecture.
54/// New VSA implementations (such as SSP, MBAT, or FHRR) can be added by implementing this trait.
55///
56/// # Associated Types
57///
58/// * `Elem` - The type used to represent each element in the hypervector.
59pub trait VSA: Sized + Clone {
60    /// The type used to represent each element in the hypervector.
61    type Elem: Copy + std::fmt::Debug + PartialEq + Into<f32>;
62
63    /// Generates a random hypervector of the given dimension.
64    ///
65    /// # Arguments
66    ///
67    /// * `dim` - The dimensionality of the hypervector.
68    /// * `rng` - A mutable reference to a random number generator.
69    fn generate(dim: usize, rng: &mut impl Rng) -> Self;
70
71    /// Bundles (superposes) two hypervectors.
72    ///
73    /// For example, for MBAT this is typically implemented as the element-wise sum followed by a
74    /// sign function with tie-breaking.
75    ///
76    /// # Arguments
77    ///
78    /// * `other` - The hypervector to bundle with.
79    /// * `tie_breaker` - The rule to resolve ties.
80    /// * `rng` - A mutable reference to a random number generator.
81    fn bundle(&self, other: &Self, tie_breaker: TieBreaker, rng: &mut impl Rng) -> Self;
82
83    /// Binds two hypervectors.
84    ///
85    /// For MBAT, this is implemented as the element-wise product.
86    ///
87    /// # Arguments
88    ///
89    /// * `other` - The hypervector to bind with.
90    fn bind(&self, other: &Self) -> Self;
91
92    /// Computes the cosine similarity between two hypervectors.
93    ///
94    /// # Arguments
95    ///
96    /// * `other` - The hypervector to compare with.
97    fn cosine_similarity(&self, other: &Self) -> f32;
98
99    /// Computes the normalized Hamming distance between two hypervectors.
100    ///
101    /// # Arguments
102    ///
103    /// * `other` - The hypervector to compare with.
104    fn hamming_distance(&self, other: &Self) -> f32;
105
106    /// Converts the hypervector into a plain `Vec<f32>`.
107    fn to_vec(&self) -> Vec<f32>;
108
109    /// Creates a Hypervector from a plain vector of `f32` by using the VSA's from_vec conversion.
110    fn from_vec(v: Vec<f32>) -> Self;
111
112    /// Bundles many hypervectors (folding a slice using the bundling operation).
113    ///
114    /// # Panics
115    ///
116    /// Panics if `vectors` is empty.
117    fn bundle_many(vectors: &[Self], tie_breaker: TieBreaker, rng: &mut impl Rng) -> Self {
118        assert!(
119            !vectors.is_empty(),
120            "Cannot bundle an empty slice of hypervectors"
121        );
122        let mut result = vectors[0].clone();
123        for vec in &vectors[1..] {
124            result = result.bundle(vec, tie_breaker, rng);
125        }
126        result
127    }
128
129    /// Binds many hypervectors (folding a slice using the binding operation).
130    ///
131    /// # Panics
132    ///
133    /// Panics if `vectors` is empty.
134    fn bind_many(vectors: &[Self]) -> Self {
135        assert!(
136            !vectors.is_empty(),
137            "Cannot bind an empty slice of hypervectors"
138        );
139        let mut result = vectors[0].clone();
140        for vec in &vectors[1..] {
141            result = result.bind(vec);
142        }
143        result
144    }
145}
146
147/// A generic hypervector type parameterized over a VSA implementation.
148///
149/// This type wraps an inner hypervector and provides high-level operations for generation,
150/// bundling, binding, similarity comparisons, and conversion to a plain vector.
151///
152/// # Type Parameters
153///
154/// * `V` - A type that implements the `VSA` trait.
155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
156pub struct Hypervector<V: VSA> {
157    /// The underlying hypervector.
158    pub inner: V,
159}
160
161impl<V: VSA> Hypervector<V> {
162    /// Generates a random hypervector of the given dimension using the global RNG.
163    ///
164    /// # Arguments
165    ///
166    /// * `dim` - The dimensionality of the hypervector.
167    ///
168    /// # Example
169    ///
170    /// ```rust
171    /// use hypervector::Hypervector;
172    /// use hypervector::mbat::MBAT;
173    ///
174    /// let hv = Hypervector::<MBAT>::generate(1000);
175    /// ```
176    pub fn generate(dim: usize) -> Self {
177        let mut rng = GLOBAL_RNG.lock().unwrap();
178        Self {
179            inner: V::generate(dim, &mut *rng),
180        }
181    }
182
183    /// Generates many random hypervectors.
184    ///
185    /// # Arguments
186    ///
187    /// * `dim` - The dimensionality of each hypervector.
188    /// * `count` - The number of hypervectors to generate.
189    pub fn generate_many(dim: usize, count: usize) -> Vec<Self> {
190        let mut rng = GLOBAL_RNG.lock().unwrap();
191        (0..count)
192            .map(|_| Self {
193                inner: V::generate(dim, &mut *rng),
194            })
195            .collect()
196    }
197
198    /// Bundles (superposes) this hypervector with another using the specified tie-breaking rule.
199    ///
200    /// # Arguments
201    ///
202    /// * `other` - The hypervector to bundle with.
203    /// * `tie_breaker` - The tie-breaking rule to use.
204    pub fn bundle(&self, other: &Self, tie_breaker: TieBreaker) -> Self {
205        let mut rng = GLOBAL_RNG.lock().unwrap();
206        Self {
207            inner: self.inner.bundle(&other.inner, tie_breaker, &mut *rng),
208        }
209    }
210
211    /// Binds this hypervector with another.
212    ///
213    /// # Arguments
214    ///
215    /// * `other` - The hypervector to bind with.
216    pub fn bind(&self, other: &Self) -> Self {
217        Self {
218            inner: self.inner.bind(&other.inner),
219        }
220    }
221
222    /// Computes the cosine similarity between this hypervector and another.
223    ///
224    /// # Arguments
225    ///
226    /// * `other` - The hypervector to compare with.
227    pub fn cosine_similarity(&self, other: &Self) -> f32 {
228        self.inner.cosine_similarity(&other.inner)
229    }
230
231    /// Computes the normalized Hamming distance between this hypervector and another.
232    ///
233    /// # Arguments
234    ///
235    /// * `other` - The hypervector to compare with.
236    pub fn hamming_distance(&self, other: &Self) -> f32 {
237        self.inner.hamming_distance(&other.inner)
238    }
239
240    /// Converts this hypervector into a plain vector of `f32`.
241    pub fn to_vec(&self) -> Vec<f32> {
242        self.inner.to_vec()
243    }
244
245    /// Creates a Hypervector from a plain vector of `f32` by using the VSA's from_vec conversion.
246    pub fn from_vec(v: Vec<f32>) -> Self {
247        Self {
248            inner: V::from_vec(v),
249        }
250    }
251
252    /// Bundles many hypervectors by extracting their inner representations and then wrapping
253    /// the bundled result back into a `Hypervector`.
254    ///
255    /// # Arguments
256    ///
257    /// * `vectors` - A slice of hypervectors to bundle.
258    /// * `tie_breaker` - The tie-breaking rule to use.
259    pub fn bundle_many(vectors: &[Self], tie_breaker: TieBreaker) -> Self {
260        let mut rng = GLOBAL_RNG.lock().unwrap();
261        let inners: Vec<V> = vectors.iter().map(|hv| hv.inner.clone()).collect();
262        Self {
263            inner: V::bundle_many(&inners, tie_breaker, &mut *rng),
264        }
265    }
266
267    /// Binds many hypervectors by extracting their inner representations and then wrapping
268    /// the bound result back into a `Hypervector`.
269    ///
270    /// # Arguments
271    ///
272    /// * `vectors` - A slice of hypervectors to bind.
273    pub fn bind_many(vectors: &[Self]) -> Self {
274        let inners: Vec<V> = vectors.iter().map(|hv| hv.inner.clone()).collect();
275        Self {
276            inner: V::bind_many(&inners),
277        }
278    }
279}
280
281/// Overloads the `+` operator for bundling hypervectors.
282///
283/// This implementation uses a default tie-breaker of `Random`.
284impl<V: VSA> Add for Hypervector<V> {
285    type Output = Self;
286
287    fn add(self, rhs: Self) -> Self::Output {
288        let tie_breaker = TieBreaker::Random;
289        let mut rng = GLOBAL_RNG.lock().unwrap();
290        Self {
291            inner: self.inner.bundle(&rhs.inner, tie_breaker, &mut *rng),
292        }
293    }
294}
295
296/// Overloads the `*` operator for binding hypervectors.
297impl<V: VSA> Mul for Hypervector<V> {
298    type Output = Self;
299
300    fn mul(self, rhs: Self) -> Self::Output {
301        Self {
302            inner: self.inner.bind(&rhs.inner),
303        }
304    }
305}
306
307// Declare your submodules. Ensure that these files exist in the src/ folder.
308pub mod encoder;
309pub mod fhrr;
310pub mod mbat;
311pub mod ssp;
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    // Bring the mbat module into scope.
317    use crate::mbat;
318    // Alias HV for ease of use.
319    type HV = Hypervector<mbat::MBAT>;
320
321    #[test]
322    fn test_codebook_pair_bindings() {
323        let dim = 10000;
324        let codebook: Vec<HV> = HV::generate_many(dim, 10);
325
326        // Compute bindings for all unique pairs (i, j) with i < j.
327        let mut pair_bindings = Vec::new();
328        for i in 0..codebook.len() {
329            for j in (i + 1)..codebook.len() {
330                let binding = codebook[i].bind(&codebook[j]);
331                pair_bindings.push(((i, j), binding));
332            }
333        }
334
335        // For each binding, recompute it and check that the cosine similarity is 1.
336        for &((i, j), ref binding) in &pair_bindings {
337            let recomputed = codebook[i].bind(&codebook[j]);
338            let sim: f32 = binding.cosine_similarity(&recomputed);
339            assert!(
340                (sim - 1.0).abs() < 1e-6,
341                "Recomputed binding differs for pair ({}, {})",
342                i,
343                j
344            );
345        }
346
347        // Compare bindings from different pairs; they should be nearly orthogonal.
348        for (idx1, &((i, j), ref binding1)) in pair_bindings.iter().enumerate() {
349            for (idx2, &((k, l), ref binding2)) in pair_bindings.iter().enumerate() {
350                if idx1 == idx2 {
351                    continue;
352                }
353                let sim: f32 = binding1.cosine_similarity(&binding2);
354                assert!(
355                    sim.abs() < 0.1,
356                    "Binding for pair ({}, {}) has cosine similarity {} with binding for pair ({}, {})",
357                    i,
358                    j,
359                    sim,
360                    k,
361                    l
362                );
363            }
364        }
365    }
366
367    /// Helper function to generate two random hypervectors.
368    fn generate_two(dim: usize) -> (HV, HV) {
369        (HV::generate(dim), HV::generate(dim))
370    }
371
372    /// Verifies that two random high-dimensional bipolar vectors are nearly orthogonal.
373    #[test]
374    fn test_random_vectors_orthogonal() {
375        let dim = 10000;
376        let (a, b) = generate_two(dim);
377        let cos_sim = a.cosine_similarity(&b);
378        assert!(
379            cos_sim.abs() < 0.1,
380            "Expected near-orthogonality but got cosine similarity {}",
381            cos_sim
382        );
383    }
384
385    /// Verifies that bundling two hypervectors yields a vector that is similar to each constituent.
386    #[test]
387    fn test_bundling_similarity() {
388        let dim = 10000;
389        let a = HV::generate(dim);
390        let b = HV::generate(dim);
391        let bundled = a.bundle(&b, TieBreaker::Random);
392        let sim_a = bundled.cosine_similarity(&a);
393        let sim_b = bundled.cosine_similarity(&b);
394        assert!(
395            (sim_a - 0.5).abs() < 0.1,
396            "Bundled vector similarity with first constituent was {}",
397            sim_a
398        );
399        assert!(
400            (sim_b - 0.5).abs() < 0.1,
401            "Bundled vector similarity with second constituent was {}",
402            sim_b
403        );
404    }
405
406    /// Verifies that binding two hypervectors produces a result nearly orthogonal to each constituent.
407    #[test]
408    fn test_binding_orthogonality() {
409        let dim = 10000;
410        let a = HV::generate(dim);
411        let b = HV::generate(dim);
412        let bound = a.bind(&b);
413        let sim_a = a.cosine_similarity(&bound);
414        let sim_b = b.cosine_similarity(&bound);
415        assert!(
416            sim_a.abs() < 0.1,
417            "Binding similarity with first constituent was {}",
418            sim_a
419        );
420        assert!(
421            sim_b.abs() < 0.1,
422            "Binding similarity with second constituent was {}",
423            sim_b
424        );
425    }
426
427    /// Verifies that converting a hypervector to a plain vector yields the expected values.
428    #[test]
429    fn test_to_vec_conversion() {
430        let dim = 100;
431        let hv = HV::generate(dim);
432        let vec_f32 = hv.to_vec();
433        assert_eq!(vec_f32.len(), dim);
434        // For MBAT hypervectors, each element should be either 1.0 or -1.0.
435        for &x in &vec_f32 {
436            assert!(x == 1.0 || x == -1.0, "Element {} is not 1.0 or -1.0", x);
437        }
438    }
439
440    #[test]
441    fn test_from_vec_conversion() {
442        let dim = 100;
443        // Create a vector of 100 elements alternating between 1.0 and -1.0.
444        let vec_f32: Vec<f32> = (0..dim)
445            .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
446            .collect();
447        // Create a hypervector from the vector.
448        let hv = HV::from_vec(vec_f32.clone());
449        // Convert it back to a plain vector.
450        let converted = hv.to_vec();
451        // Verify that the conversion round-trip produces the original vector.
452        assert_eq!(
453            converted, vec_f32,
454            "from_vec conversion did not round-trip correctly"
455        );
456    }
457}