use crate::{all_finite, lcg_next, Error};
#[derive(Debug, Clone)]
pub struct HyperplaneHasher {
planes: Vec<f32>,
dim: usize,
num_bits: usize,
}
impl HyperplaneHasher {
pub fn new(dim: usize, num_bits: usize, seed: u64) -> Result<Self, Error> {
if dim == 0 {
return Err(Error::InvalidParam("dim must be >= 1"));
}
if num_bits == 0 {
return Err(Error::InvalidParam("num_bits must be >= 1"));
}
let planes = generate_planes(dim, num_bits, seed);
Ok(Self {
planes,
dim,
num_bits,
})
}
pub fn hash(&self, vector: &[f32]) -> Result<Vec<u8>, Error> {
if vector.len() != self.dim {
return Err(Error::DimensionMismatch {
expected: self.dim,
got: vector.len(),
});
}
if !all_finite(vector) {
return Err(Error::NonFiniteInput);
}
let n_bytes = self.num_bits.div_ceil(8);
let mut sig = vec![0u8; n_bytes];
for b in 0..self.num_bits {
let plane = &self.planes[b * self.dim..(b + 1) * self.dim];
let dot: f32 = plane.iter().zip(vector.iter()).map(|(p, v)| p * v).sum();
if dot > 0.0 {
sig[b / 8] |= 1u8 << (b % 8);
}
}
Ok(sig)
}
pub fn hash_batch(&self, vectors: &[&[f32]]) -> Result<Vec<Vec<u8>>, Error> {
vectors.iter().map(|v| self.hash(v)).collect()
}
pub fn distance(sig_a: &[u8], sig_b: &[u8]) -> u32 {
sig_a
.iter()
.zip(sig_b.iter())
.map(|(a, b)| (a ^ b).count_ones())
.sum()
}
pub fn estimated_cosine(&self, sig_a: &[u8], sig_b: &[u8]) -> f32 {
let hamming = Self::distance(sig_a, sig_b);
let theta = std::f32::consts::PI * hamming as f32 / self.num_bits as f32;
theta.cos()
}
pub fn num_bits(&self) -> usize {
self.num_bits
}
pub fn dim(&self) -> usize {
self.dim
}
}
fn generate_planes(dim: usize, num_bits: usize, seed: u64) -> Vec<f32> {
let mut state = seed;
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let total = num_bits * dim;
let mut raw: Vec<f32> = Vec::with_capacity(total);
let mut spare: Option<f32> = None;
while raw.len() < total {
if let Some(s) = spare.take() {
raw.push(s);
} else {
let (z0, z1) = box_muller(&mut state);
raw.push(z0);
spare = Some(z1);
}
}
raw.truncate(total);
for b in 0..num_bits {
let start = b * dim;
let end = start + dim;
let norm: f32 = raw[start..end].iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in &mut raw[start..end] {
*x /= norm;
}
}
}
raw
}
fn box_muller(state: &mut u64) -> (f32, f32) {
let u1 = lcg_u01(state);
let u2 = lcg_u01(state);
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f32::consts::PI * u2;
(r * theta.cos(), r * theta.sin())
}
fn lcg_u01(state: &mut u64) -> f32 {
let raw = lcg_next(state);
let u = (raw >> 32) as u32;
(u.wrapping_add(1) as f64 / (u32::MAX as f64 + 1.0)) as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_determinism() {
let h1 = HyperplaneHasher::new(8, 32, 0xDEAD_BEEF).unwrap();
let h2 = HyperplaneHasher::new(8, 32, 0xDEAD_BEEF).unwrap();
let v: Vec<f32> = (0..8).map(|i| (i as f32).sin()).collect();
assert_eq!(h1.hash(&v).unwrap(), h2.hash(&v).unwrap());
}
#[test]
fn different_seeds_give_different_planes() {
let h1 = HyperplaneHasher::new(16, 64, 1).unwrap();
let h2 = HyperplaneHasher::new(16, 64, 2).unwrap();
assert_ne!(h1.planes, h2.planes);
}
#[test]
fn same_vector_zero_distance() {
let h = HyperplaneHasher::new(4, 16, 42).unwrap();
let v = vec![1.0_f32, 2.0, 3.0, 4.0];
let sig = h.hash(&v).unwrap();
assert_eq!(HyperplaneHasher::distance(&sig, &sig), 0);
}
#[test]
fn distance_symmetric() {
let h = HyperplaneHasher::new(8, 32, 7).unwrap();
let a: Vec<f32> = (0..8).map(|i| (i as f32).sin()).collect();
let b: Vec<f32> = (0..8).map(|i| (i as f32).cos()).collect();
let sa = h.hash(&a).unwrap();
let sb = h.hash(&b).unwrap();
assert_eq!(
HyperplaneHasher::distance(&sa, &sb),
HyperplaneHasher::distance(&sb, &sa)
);
}
#[test]
fn distance_triangle_inequality() {
let h = HyperplaneHasher::new(8, 64, 99).unwrap();
let a: Vec<f32> = (0..8).map(|i| (i as f32).sin()).collect();
let b: Vec<f32> = (0..8).map(|i| (i as f32).cos()).collect();
let c: Vec<f32> = (0..8).map(|i| i as f32 * 0.1).collect();
let sa = h.hash(&a).unwrap();
let sb = h.hash(&b).unwrap();
let sc = h.hash(&c).unwrap();
let dab = HyperplaneHasher::distance(&sa, &sb);
let dac = HyperplaneHasher::distance(&sa, &sc);
let dbc = HyperplaneHasher::distance(&sb, &sc);
assert!(
dab <= dac + dbc,
"triangle inequality violated: {dab} > {dac} + {dbc}"
);
}
#[test]
fn identical_vectors_cosine_one() {
let h = HyperplaneHasher::new(4, 128, 1234).unwrap();
let v = vec![1.0_f32, 0.0, 0.0, 0.0];
let sig = h.hash(&v).unwrap();
let est = h.estimated_cosine(&sig, &sig);
assert!((est - 1.0_f32).abs() < 1e-6, "expected 1.0, got {est}");
}
#[test]
fn orthogonal_vectors_cosine_near_zero() {
let h = HyperplaneHasher::new(4, 256, 5678).unwrap();
let a = vec![1.0_f32, 0.0, 0.0, 0.0];
let b = vec![0.0_f32, 1.0, 0.0, 0.0];
let sa = h.hash(&a).unwrap();
let sb = h.hash(&b).unwrap();
let est = h.estimated_cosine(&sa, &sb);
assert!(
est.abs() < 0.15,
"expected cosine near 0 for orthogonal vectors, got {est}"
);
}
#[test]
fn opposite_vectors_cosine_near_minus_one() {
let h = HyperplaneHasher::new(4, 256, 9999).unwrap();
let a = vec![1.0_f32, 0.0, 0.0, 0.0];
let b = vec![-1.0_f32, 0.0, 0.0, 0.0];
let sa = h.hash(&a).unwrap();
let sb = h.hash(&b).unwrap();
let est = h.estimated_cosine(&sa, &sb);
assert!(
est < -0.8,
"expected cosine near -1 for opposite vectors, got {est}"
);
}
#[test]
fn hash_batch_matches_individual() {
let h = HyperplaneHasher::new(4, 16, 0).unwrap();
let vecs: Vec<Vec<f32>> = (0..5)
.map(|i| (0..4).map(|j| (i * 4 + j) as f32).collect())
.collect();
let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
let batch = h.hash_batch(&refs).unwrap();
for (i, v) in vecs.iter().enumerate() {
assert_eq!(batch[i], h.hash(v).unwrap());
}
}
#[test]
fn rejects_zero_dim() {
assert!(HyperplaneHasher::new(0, 8, 0).is_err());
}
#[test]
fn rejects_zero_bits() {
assert!(HyperplaneHasher::new(4, 0, 0).is_err());
}
#[test]
fn rejects_dimension_mismatch() {
let h = HyperplaneHasher::new(4, 8, 0).unwrap();
assert!(h.hash(&[1.0, 2.0]).is_err());
}
#[test]
fn rejects_non_finite_input() {
let h = HyperplaneHasher::new(4, 8, 0).unwrap();
assert!(h.hash(&[1.0, f32::NAN, 0.0, 0.0]).is_err());
assert!(h.hash(&[1.0, f32::INFINITY, 0.0, 0.0]).is_err());
}
#[test]
fn determinism_canary() {
let h = HyperplaneHasher::new(4, 8, 42).unwrap();
let v = [1.0_f32, 0.0, 0.0, 0.0];
let sig = h.hash(&v).unwrap();
assert_eq!(sig.len(), 1, "8 bits -> 1 byte");
let canary = sig[0];
let h2 = HyperplaneHasher::new(4, 8, 42).unwrap();
assert_eq!(h2.hash(&v).unwrap()[0], canary);
}
}