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}