hypervector 0.1.1

A crate that implements hyperdimensional vectors and VSAs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//! # Hypervector Crate
//!
//! This crate implements high-dimensional vectors for hyperdimensional computing and
//! Vector Symbolic Architectures (VSAs). It currently provides two implementations:
//!
//! - **MBAT**: Bipolar vectors (elements in {-1, +1}).
//! - **SSP**: Semantic Spatial Parameters (implemented in a separate module).
//!
//! Core components such as a global RNG, the `VSA` trait, and a generic `Hypervector` type
//! are defined here. Additional VSA implementations (e.g. FHRR) can be added as separate modules.

use once_cell::sync::Lazy;
use rand::{rngs::StdRng, Rng, SeedableRng};
use serde::{Deserialize, Serialize};
use std::ops::{Add, Mul};
use std::sync::Mutex;

/// A lazily-initialized, thread-safe global RNG used for hypervector generation and operations.
///
/// This global RNG is used by all methods that do not receive their own RNG.
static GLOBAL_RNG: Lazy<Mutex<StdRng>> = Lazy::new(|| Mutex::new(StdRng::from_entropy()));

/// Sets the seed for the global RNG. This affects all hypervector generation and operations that
/// use the global RNG.
///
/// # Example
///
/// ```rust
/// use hypervector::set_global_seed;
///
/// // Set the global RNG seed to 42 for reproducibility.
/// set_global_seed(42);
/// ```
pub fn set_global_seed(seed: u64) {
    let mut rng = GLOBAL_RNG.lock().unwrap();
    *rng = StdRng::seed_from_u64(seed);
}

/// Options for tie-breaking during the bundling operation.
///
/// When bundling two hypervectors, an element-wise sum may result in a tie (i.e. a zero).
/// This enum specifies how such ties are resolved.
#[derive(Debug, Clone, Copy)]
pub enum TieBreaker {
    /// Always choose +1 when a tie occurs.
    AlwaysPositive,
    /// Always choose -1 when a tie occurs.
    AlwaysNegative,
    /// Randomly choose between +1 and -1 when a tie occurs.
    Random,
}

/// The `VSA` trait defines the interface for a Vector Symbolic Architecture.
/// New VSA implementations (such as SSP, MBAT, or FHRR) can be added by implementing this trait.
///
/// # Associated Types
///
/// * `Elem` - The type used to represent each element in the hypervector.
pub trait VSA: Sized + Clone {
    /// The type used to represent each element in the hypervector.
    type Elem: Copy + std::fmt::Debug + PartialEq + Into<f32>;

    /// Generates a random hypervector of the given dimension.
    ///
    /// # Arguments
    ///
    /// * `dim` - The dimensionality of the hypervector.
    /// * `rng` - A mutable reference to a random number generator.
    fn generate(dim: usize, rng: &mut impl Rng) -> Self;

    /// Bundles (superposes) two hypervectors.
    ///
    /// For example, for MBAT this is typically implemented as the element-wise sum followed by a
    /// sign function with tie-breaking.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to bundle with.
    /// * `tie_breaker` - The rule to resolve ties.
    /// * `rng` - A mutable reference to a random number generator.
    fn bundle(&self, other: &Self, tie_breaker: crate::TieBreaker, rng: &mut impl Rng) -> Self;

    /// Binds two hypervectors.
    ///
    /// For MBAT, this is implemented as the element-wise product.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to bind with.
    fn bind(&self, other: &Self) -> Self;

    /// Computes the cosine similarity between two hypervectors.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to compare with.
    fn cosine_similarity(&self, other: &Self) -> f32;

    /// Computes the normalized Hamming distance between two hypervectors.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to compare with.
    fn hamming_distance(&self, other: &Self) -> f32;

    /// Converts the hypervector into a plain `Vec<f32>`.
    fn to_vec(&self) -> Vec<f32>;

    /// Bundles many hypervectors (folding a slice using the bundling operation).
    ///
    /// # Panics
    ///
    /// Panics if `vectors` is empty.
    fn bundle_many(vectors: &[Self], tie_breaker: crate::TieBreaker, rng: &mut impl Rng) -> Self {
        assert!(
            !vectors.is_empty(),
            "Cannot bundle an empty slice of hypervectors"
        );
        let mut result = vectors[0].clone();
        for vec in &vectors[1..] {
            result = result.bundle(vec, tie_breaker, rng);
        }
        result
    }

    /// Binds many hypervectors (folding a slice using the binding operation).
    ///
    /// # Panics
    ///
    /// Panics if `vectors` is empty.
    fn bind_many(vectors: &[Self]) -> Self {
        assert!(
            !vectors.is_empty(),
            "Cannot bind an empty slice of hypervectors"
        );
        let mut result = vectors[0].clone();
        for vec in &vectors[1..] {
            result = result.bind(vec);
        }
        result
    }
}

/// A generic hypervector type parameterized over a VSA implementation.
///
/// This type wraps an inner hypervector and provides high-level operations for generation,
/// bundling, binding, similarity comparisons, and conversion to a plain vector.
///
/// # Type Parameters
///
/// * `V` - A type that implements the `VSA` trait.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Hypervector<V: VSA> {
    /// The underlying hypervector.
    pub inner: V,
}

impl<V: VSA> Hypervector<V> {
    /// Generates a random hypervector of the given dimension using the global RNG.
    ///
    /// # Arguments
    ///
    /// * `dim` - The dimensionality of the hypervector.
    ///
    /// # Example
    ///
    /// ```rust
    /// use hypervector::Hypervector;
    /// use hypervector::mbat::MBAT;
    ///
    /// let hv = Hypervector::<MBAT>::generate(1000);
    /// ```
    pub fn generate(dim: usize) -> Self {
        let mut rng = GLOBAL_RNG.lock().unwrap();
        Self {
            inner: V::generate(dim, &mut *rng),
        }
    }

    /// Generates many random hypervectors.
    ///
    /// # Arguments
    ///
    /// * `dim` - The dimensionality of each hypervector.
    /// * `count` - The number of hypervectors to generate.
    pub fn generate_many(dim: usize, count: usize) -> Vec<Self> {
        let mut rng = GLOBAL_RNG.lock().unwrap();
        (0..count)
            .map(|_| Self {
                inner: V::generate(dim, &mut *rng),
            })
            .collect()
    }

    /// Bundles (superposes) this hypervector with another using the specified tie-breaking rule.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to bundle with.
    /// * `tie_breaker` - The tie-breaking rule to use.
    pub fn bundle(&self, other: &Self, tie_breaker: crate::TieBreaker) -> Self {
        let mut rng = GLOBAL_RNG.lock().unwrap();
        Self {
            inner: self.inner.bundle(&other.inner, tie_breaker, &mut *rng),
        }
    }

    /// Binds this hypervector with another.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to bind with.
    pub fn bind(&self, other: &Self) -> Self {
        Self {
            inner: self.inner.bind(&other.inner),
        }
    }

    /// Computes the cosine similarity between this hypervector and another.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to compare with.
    pub fn cosine_similarity(&self, other: &Self) -> f32 {
        self.inner.cosine_similarity(&other.inner)
    }

    /// Computes the normalized Hamming distance between this hypervector and another.
    ///
    /// # Arguments
    ///
    /// * `other` - The hypervector to compare with.
    pub fn hamming_distance(&self, other: &Self) -> f32 {
        self.inner.hamming_distance(&other.inner)
    }

    /// Converts this hypervector into a plain vector of `f32`.
    pub fn to_vec(&self) -> Vec<f32> {
        self.inner.to_vec()
    }

    /// Bundles many hypervectors by extracting their inner representations and then wrapping
    /// the bundled result back into a `Hypervector`.
    ///
    /// # Arguments
    ///
    /// * `vectors` - A slice of hypervectors to bundle.
    /// * `tie_breaker` - The tie-breaking rule to use.
    pub fn bundle_many(vectors: &[Self], tie_breaker: crate::TieBreaker) -> Self {
        let mut rng = GLOBAL_RNG.lock().unwrap();
        let inners: Vec<V> = vectors.iter().map(|hv| hv.inner.clone()).collect();
        Self {
            inner: V::bundle_many(&inners, tie_breaker, &mut *rng),
        }
    }

    /// Binds many hypervectors by extracting their inner representations and then wrapping
    /// the bound result back into a `Hypervector`.
    ///
    /// # Arguments
    ///
    /// * `vectors` - A slice of hypervectors to bind.
    pub fn bind_many(vectors: &[Self]) -> Self {
        let inners: Vec<V> = vectors.iter().map(|hv| hv.inner.clone()).collect();
        Self {
            inner: V::bind_many(&inners),
        }
    }
}

/// Overloads the `+` operator for bundling hypervectors.
///
/// This implementation uses a default tie-breaker of `Random`.
impl<V: VSA> Add for Hypervector<V> {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        let tie_breaker = crate::TieBreaker::Random;
        let mut rng = GLOBAL_RNG.lock().unwrap();
        Self {
            inner: self.inner.bundle(&rhs.inner, tie_breaker, &mut *rng),
        }
    }
}

/// Overloads the `*` operator for binding hypervectors.
impl<V: VSA> Mul for Hypervector<V> {
    type Output = Self;

    fn mul(self, rhs: Self) -> Self::Output {
        Self {
            inner: self.inner.bind(&rhs.inner),
        }
    }
}

pub mod encoder; // VSA ObjectEncoder is implemented in src/encoder.rs
pub mod fhrr;
pub mod mbat;
pub mod ssp; // SSP is implemented in src/ssp.rs

#[cfg(test)]
mod tests {
    use super::*;
    // For testing MBAT, we alias Hypervector<mbat::MBAT> as HV.
    type HV = Hypervector<mbat::MBAT>;

    /// Helper function to generate two random hypervectors.
    fn generate_two(dim: usize) -> (HV, HV) {
        (HV::generate(dim), HV::generate(dim))
    }

    /// Verifies that two random high-dimensional bipolar vectors are nearly orthogonal.
    #[test]
    fn test_random_vectors_orthogonal() {
        let dim = 10000;
        let (a, b) = generate_two(dim);
        let cos_sim = a.cosine_similarity(&b);
        assert!(
            cos_sim.abs() < 0.1,
            "Expected near-orthogonality but got cosine similarity {}",
            cos_sim
        );
    }

    /// Verifies that bundling two hypervectors yields a vector that is similar to each constituent.
    #[test]
    fn test_bundling_similarity() {
        let dim = 10000;
        let a = HV::generate(dim);
        let b = HV::generate(dim);
        let bundled = a.bundle(&b, TieBreaker::Random);
        let sim_a = bundled.cosine_similarity(&a);
        let sim_b = bundled.cosine_similarity(&b);
        assert!(
            (sim_a - 0.5).abs() < 0.1,
            "Bundled vector similarity with first constituent was {}",
            sim_a
        );
        assert!(
            (sim_b - 0.5).abs() < 0.1,
            "Bundled vector similarity with second constituent was {}",
            sim_b
        );
    }

    /// Verifies that binding two hypervectors produces a result nearly orthogonal to each constituent.
    #[test]
    fn test_binding_orthogonality() {
        let dim = 10000;
        let a = HV::generate(dim);
        let b = HV::generate(dim);
        let bound = a.bind(&b);
        let sim_a = a.cosine_similarity(&bound);
        let sim_b = b.cosine_similarity(&bound);
        assert!(
            sim_a.abs() < 0.1,
            "Binding similarity with first constituent was {}",
            sim_a
        );
        assert!(
            sim_b.abs() < 0.1,
            "Binding similarity with second constituent was {}",
            sim_b
        );
    }

    /// Tests creating a codebook of 10 hypervectors, bundling each unique pair,
    /// and verifying that recomputed bindings for the same pair yield a cosine similarity of 1.
    #[test]
    fn test_codebook_pair_bindings() {
        let dim = 10000;
        let codebook: Vec<HV> = HV::generate_many(dim, 10);

        // Compute bindings for all unique pairs (i, j) with i < j.
        let mut pair_bindings = Vec::new();
        for i in 0..codebook.len() {
            for j in (i + 1)..codebook.len() {
                let binding = codebook[i].bind(&codebook[j]);
                pair_bindings.push(((i, j), binding));
            }
        }

        // For each binding, recompute it and check that the cosine similarity is 1.
        for &((i, j), ref binding) in &pair_bindings {
            let recomputed = codebook[i].bind(&codebook[j]);
            let sim = binding.cosine_similarity(&recomputed);
            assert!(
                (sim - 1.0).abs() < 1e-6,
                "Recomputed binding differs for pair ({}, {})",
                i,
                j
            );
        }

        // Compare bindings from different pairs; they should be nearly orthogonal.
        for (idx1, &((i, j), ref binding1)) in pair_bindings.iter().enumerate() {
            for (idx2, &((k, l), ref binding2)) in pair_bindings.iter().enumerate() {
                if idx1 == idx2 {
                    continue;
                }
                let sim = binding1.cosine_similarity(binding2);
                assert!(
                    sim.abs() < 0.1,
                    "Binding for pair ({}, {}) has cosine similarity {} with binding for pair ({}, {})",
                    i, j, sim, k, l
                );
            }
        }
    }

    /// Verifies that converting a hypervector to a plain vector yields the expected values.
    #[test]
    fn test_to_vec_conversion() {
        let dim = 100;
        let hv = HV::generate(dim);
        let vec_f32 = hv.to_vec();
        assert_eq!(vec_f32.len(), dim);
        // For MBAT hypervectors, each element should be either 1.0 or -1.0.
        for &x in &vec_f32 {
            assert!(x == 1.0 || x == -1.0, "Element {} is not 1.0 or -1.0", x);
        }
    }
}