use rand::distributions::Uniform;
use rand::Rng;
use std::f32::consts::PI;
use crate::{TieBreaker, VSA};
#[derive(Debug, Clone, PartialEq)]
pub struct FHRR {
pub data: Vec<f32>,
}
impl FHRR {
fn mod_2pi(x: f32) -> f32 {
let two_pi = 2.0 * PI;
let mut r = x % two_pi;
if r < 0.0 {
r += two_pi;
}
r
}
}
impl VSA for FHRR {
type Elem = f32;
fn generate(dim: usize, rng: &mut impl Rng) -> Self {
let uniform = Uniform::new(0.0, 2.0 * PI);
let data = (0..dim).map(|_| rng.sample(uniform)).collect();
FHRR { data }
}
fn bundle(&self, other: &Self, tie_breaker: TieBreaker, rng: &mut impl Rng) -> Self {
let dim = self.data.len();
let mut result = Vec::with_capacity(dim);
for i in 0..dim {
let a = self.data[i];
let b = other.data[i];
let real_sum = a.cos() + b.cos();
let imag_sum = a.sin() + b.sin();
let magnitude = (real_sum * real_sum + imag_sum * imag_sum).sqrt();
let angle = if magnitude.abs() < 1e-6 {
match tie_breaker {
TieBreaker::AlwaysPositive => 0.0,
TieBreaker::AlwaysNegative => PI,
TieBreaker::Random => rng.sample(Uniform::new(0.0, 2.0 * PI)),
}
} else {
imag_sum.atan2(real_sum)
};
result.push(FHRR::mod_2pi(angle));
}
FHRR { data: result }
}
fn bind(&self, other: &Self) -> Self {
let dim = self.data.len();
let mut result = Vec::with_capacity(dim);
for i in 0..dim {
let sum = self.data[i] + other.data[i];
result.push(FHRR::mod_2pi(sum));
}
FHRR { data: result }
}
fn cosine_similarity(&self, other: &Self) -> f32 {
let n = self.data.len();
if n == 0 {
return 0.0;
}
let sum: f32 = self
.data
.iter()
.zip(other.data.iter())
.map(|(&a, &b)| (a - b).cos())
.sum();
sum / (n as f32)
}
fn hamming_distance(&self, other: &Self) -> f32 {
(1.0 - self.cosine_similarity(other)) / 2.0
}
fn to_vec(&self) -> Vec<f32> {
self.data.iter().map(|&theta| theta.cos()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TieBreaker, VSA};
use rand::thread_rng;
use std::f32::consts::PI;
#[test]
fn test_generate_consistency() {
let mut rng = thread_rng();
let a = FHRR::generate(100, &mut rng);
let b = FHRR::generate(100, &mut rng);
assert_eq!(a.data.len(), 100);
assert_eq!(b.data.len(), 100);
assert_ne!(a, b);
}
#[test]
fn test_bind_inverse() {
let mut rng = thread_rng();
let x = FHRR::generate(50, &mut rng);
let x_inv = FHRR {
data: x.data.iter().map(|&theta| FHRR::mod_2pi(-theta)).collect(),
};
let identity = x.bind(&x_inv);
for angle in identity.data {
assert!(
(angle).abs() < 1e-5 || (angle - 2.0 * PI).abs() < 1e-5,
"Angle {} is not close to 0 or 2π",
angle
);
}
}
#[test]
fn test_bundle_commutative() {
let mut rng = thread_rng();
let x = FHRR::generate(100, &mut rng);
let y = FHRR::generate(100, &mut rng);
let b1 = x.bundle(&y, TieBreaker::AlwaysPositive, &mut rng);
let b2 = y.bundle(&x, TieBreaker::AlwaysPositive, &mut rng);
let sim = b1.cosine_similarity(&b2);
assert!(
(sim - 1.0).abs() < 1e-5,
"Cosine similarity {} not close to 1 for bundled vectors",
sim
);
}
#[test]
fn test_cosine_similarity_self() {
let mut rng = thread_rng();
let x = FHRR::generate(100, &mut rng);
let sim = x.cosine_similarity(&x);
assert!(
(sim - 1.0).abs() < 1e-5,
"Self cosine similarity {} not equal to 1",
sim
);
}
#[test]
fn test_hamming_distance_self() {
let mut rng = thread_rng();
let x = FHRR::generate(100, &mut rng);
let hd = x.hamming_distance(&x);
assert!(
hd.abs() < 1e-5,
"Self hamming distance {} not equal to 0",
hd
);
}
#[test]
fn test_to_vec() {
let x = FHRR {
data: vec![0.0; 50],
};
let vec_f32 = x.to_vec();
for &val in &vec_f32 {
assert!((val - 1.0).abs() < 1e-5, "Expected 1.0 but found {}", val);
}
}
}