use bitvec::prelude::*;
use itertools::Itertools;
use std::cmp::Ordering;
use std::hash::Hash;
use std::marker::PhantomData;
pub trait LocalitySensitiveHasher {
type Domain;
type Image: Eq + Ord;
type DomainDistance: Ord;
type ImageDistance: Ord;
fn hash(&self, i: &Self::Domain) -> Self::Image;
fn domain_distance(&self, a: &Self::Domain, b: &Self::Domain) -> Self::DomainDistance;
fn image_distance(&self, a: &Self::Image, b: &Self::Image) -> Self::ImageDistance;
}
pub trait HammingPoint {
type Coord: Copy + Eq;
fn len(&self) -> usize;
fn get(&self, index: usize) -> Self::Coord;
fn sub_sample<B>(&self, indices: &[usize]) -> B
where
B: BuildHammingPoint<Coord = Self::Coord>,
{
debug_assert!(
indices.len() <= 64,
"cannot store more than 64 bits in a usize"
);
indices
.into_iter()
.map(|&i| self.get(i))
.fold(B::empty(), |acc, x| acc.concatenated(x))
}
fn distance(&self, other: &Self) -> u32 {
let len = self.len().max(other.len());
(0..len).filter(|&i| self.get(i) != other.get(i)).count() as u32
}
}
impl HammingPoint for usize {
type Coord = bool;
fn len(&self) -> usize {
64
}
fn get(&self, index: usize) -> bool {
((*self >> index) & 0b1) == 1
}
}
impl HammingPoint for BitSlice {
type Coord = bool;
fn len(&self) -> usize {
self.len()
}
fn get(&self, index: usize) -> bool {
if index < self.len() {
self[index]
} else {
false
}
}
}
impl HammingPoint for BitVec {
type Coord = bool;
fn len(&self) -> usize {
self.len()
}
fn get(&self, index: usize) -> bool {
if index < self.len() {
self[index]
} else {
false
}
}
}
pub trait BuildHammingPoint {
type Coord: Copy + Eq;
fn empty() -> Self;
fn push(&mut self, element: Self::Coord);
fn concatenated(mut self, element: Self::Coord) -> Self
where
Self: Sized,
{
self.push(element);
self
}
}
impl BuildHammingPoint for usize {
type Coord = bool;
fn empty() -> Self {
0
}
fn push(&mut self, element: Self::Coord) {
*self = (*self << 1) | (element as usize)
}
}
pub struct HammingLSH<HashInput, HashOutput> {
sub_sample_indices: Vec<usize>,
_input_type: PhantomData<HashInput>,
_output_type: PhantomData<HashOutput>,
}
impl<HashInput, HashOutput> HammingLSH<HashInput, HashOutput>
where
HashInput: HammingPoint,
HashOutput: BuildHammingPoint<Coord = HashInput::Coord> + Copy + Eq + Hash,
{
pub fn new(num_sub_samples: usize, points: &[HashInput]) -> Self {
assert!(num_sub_samples <= 64);
let mut indices = Vec::new();
let mut tmp_indices = Vec::new();
let max_len = points.iter().map(|p| p.len()).max().unwrap_or(0);
for _ in 0..num_sub_samples {
let best_index = (0..max_len)
.filter(|i| !indices.contains(i))
.map(|i| {
tmp_indices.push(i);
let hashed_points = points
.iter()
.map(|p| p.sub_sample::<HashOutput>(&tmp_indices));
let entropy = compute_entropy(hashed_points);
tmp_indices.pop();
(i, entropy)
})
.max_by(|(_, entropy1), (_, entropy2)| {
if entropy1 > entropy2 {
Ordering::Greater
} else {
Ordering::Less
}
})
.map(|(i, _)| i)
.unwrap();
indices.push(best_index);
tmp_indices.push(best_index);
}
assert_eq!(indices.len(), num_sub_samples);
indices.sort_unstable();
Self {
sub_sample_indices: indices,
_input_type: Default::default(),
_output_type: Default::default(),
}
}
}
impl<B> LocalitySensitiveHasher for HammingLSH<B, usize>
where
B: HammingPoint<Coord = bool>,
{
type Domain = B;
type Image = usize;
type DomainDistance = u32;
type ImageDistance = u32;
fn hash(&self, i: &Self::Domain) -> Self::Image {
i.sub_sample(&self.sub_sample_indices)
}
fn domain_distance(&self, a: &Self::Domain, b: &Self::Domain) -> Self::DomainDistance {
a.distance(b)
}
fn image_distance(&self, a: &Self::Image, b: &Self::Image) -> Self::ImageDistance {
(a ^ b).count_ones()
}
}
#[test]
fn test_high_entropy_bit_selection() {
let points = [0b000, 0b010];
let lsh: HammingLSH<_, usize> = HammingLSH::new(1, &points);
assert_eq!(lsh.sub_sample_indices, [1]);
let points = [0b000, 0b010, 0b110];
let lsh: HammingLSH<_, usize> = HammingLSH::new(2, &points);
assert_eq!(lsh.sub_sample_indices, [1, 2]);
}
#[test]
fn test_bitvec() {
let n = 0b101usize;
let bits = n.view_bits::<Lsb0>();
assert!(bits[0]);
assert!(!bits[1]);
assert!(bits[2]);
}
fn compute_entropy<I>(elements: I) -> f64
where
I: Iterator,
I::Item: Hash + Eq + Copy,
{
let counts = elements.counts();
let total_count = counts.values().sum::<usize>() as f64;
let probabilities = counts.into_values().map(|c| (c as f64) / total_count);
let entropy = -probabilities.map(|p| p * p.log2()).sum::<f64>();
entropy
}
#[test]
fn test_compute_entropy() {
let elements = [1, 1, 1, 1];
assert_eq!(compute_entropy(elements.iter()), 0.0);
let elements = [1, 2];
assert_eq!(compute_entropy(elements.iter()), 1.0);
let elements = [1, 2, 2, 1];
assert_eq!(compute_entropy(elements.iter()), 1.0);
let elements = [1, 2, 2, 1, 1];
assert!(compute_entropy(elements.iter()) < 1.0);
}