#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
use crate::arch;
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
pub(crate) const MIN_SLOTS_SIMD: usize = 8;
#[cfg(target_arch = "x86_64")]
pub(crate) const MIN_SLOTS_AVX512: usize = 16;
#[inline]
#[must_use]
#[allow(unsafe_code)]
pub fn slot_hamming_u32(a: &[u32], b: &[u32]) -> u32 {
assert_eq!(
a.len(),
b.len(),
"innr::slot_hamming_u32: slice length mismatch ({} vs {})",
a.len(),
b.len()
);
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
let n = a.len();
#[cfg(target_arch = "x86_64")]
{
if n >= MIN_SLOTS_AVX512 && is_x86_feature_detected!("avx512f") {
return unsafe { arch::x86_64::slot_hamming_u32_avx512(a, b) };
}
if n >= MIN_SLOTS_SIMD && is_x86_feature_detected!("avx2") {
return unsafe { arch::x86_64::slot_hamming_u32_avx2(a, b) };
}
}
#[cfg(target_arch = "aarch64")]
{
if n >= MIN_SLOTS_SIMD {
return unsafe { arch::aarch64::slot_hamming_u32_neon(a, b) };
}
}
#[allow(unreachable_code)]
slot_hamming_u32_portable(a, b)
}
#[inline]
#[must_use]
pub fn slot_hamming_u32_portable(a: &[u32], b: &[u32]) -> u32 {
a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() as u32
}
#[inline]
#[must_use]
#[allow(unsafe_code)]
pub fn slot_hamming_u16(a: &[u16], b: &[u16]) -> u32 {
assert_eq!(
a.len(),
b.len(),
"innr::slot_hamming_u16: slice length mismatch ({} vs {})",
a.len(),
b.len()
);
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
let n = a.len();
#[cfg(target_arch = "x86_64")]
{
if n >= 32 && is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512bw") {
return unsafe { arch::x86_64::slot_hamming_u16_avx512(a, b) };
}
if n >= 16 && is_x86_feature_detected!("avx2") {
return unsafe { arch::x86_64::slot_hamming_u16_avx2(a, b) };
}
}
#[cfg(target_arch = "aarch64")]
{
if n >= 8 {
return unsafe { arch::aarch64::slot_hamming_u16_neon(a, b) };
}
}
#[allow(unreachable_code)]
{
a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() as u32
}
}
#[inline]
#[must_use]
#[allow(unsafe_code)]
pub fn slot_hamming_u64(a: &[u64], b: &[u64]) -> u64 {
assert_eq!(
a.len(),
b.len(),
"innr::slot_hamming_u64: slice length mismatch ({} vs {})",
a.len(),
b.len()
);
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
let n = a.len();
#[cfg(target_arch = "x86_64")]
{
if n >= 8 && is_x86_feature_detected!("avx512f") {
return unsafe { arch::x86_64::slot_hamming_u64_avx512(a, b) };
}
if n >= 4 && is_x86_feature_detected!("avx2") {
return unsafe { arch::x86_64::slot_hamming_u64_avx2(a, b) };
}
}
#[cfg(target_arch = "aarch64")]
{
if n >= 2 {
return unsafe { arch::aarch64::slot_hamming_u64_neon(a, b) };
}
}
#[allow(unreachable_code)]
{
a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() as u64
}
}
#[inline]
#[must_use]
pub fn slot_hamming<T: PartialEq>(a: &[T], b: &[T]) -> usize {
a.iter().zip(b.iter()).filter(|(x, y)| x != y).count()
}
#[inline]
#[must_use]
pub fn slot_compare_counts<T: Ord>(a: &[T], b: &[T]) -> SlotCounts {
let mut c = SlotCounts::default();
for (x, y) in a.iter().zip(b.iter()) {
match x.cmp(y) {
core::cmp::Ordering::Equal => c.eq += 1,
core::cmp::Ordering::Less => c.lt += 1,
core::cmp::Ordering::Greater => c.gt += 1,
}
}
c
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SlotCounts {
pub eq: usize,
pub lt: usize,
pub gt: usize,
}
#[inline]
#[must_use]
pub fn minhash_jaccard(a: &[u32], b: &[u32]) -> f32 {
assert_eq!(
a.len(),
b.len(),
"innr::minhash_jaccard: slice length mismatch ({} vs {})",
a.len(),
b.len()
);
let len = a.len();
if len == 0 {
return 1.0;
}
let diff = slot_hamming_u32(a, b);
let matches = len as u32 - diff;
matches as f32 / len as f32
}
#[inline]
#[must_use]
pub fn jaccard_distance(a: &[u32], b: &[u32]) -> f32 {
assert_eq!(
a.len(),
b.len(),
"innr::jaccard_distance: slice length mismatch ({} vs {})",
a.len(),
b.len()
);
let len = a.len();
if len == 0 {
return 0.0;
}
slot_hamming_u32(a, b) as f32 / len as f32
}
#[cfg(test)]
mod tests {
use super::*;
fn slot_hamming_u64_ref(a: &[u64], b: &[u64]) -> u64 {
a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() as u64
}
#[test]
fn slot_hamming_u64_matches_reference_across_boundaries() {
for n in [1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 64, 100, 257] {
for seed in 0..4u64 {
let a: Vec<u64> = (0..n as u64)
.map(|i| i.wrapping_mul(2_654_435_761) ^ seed)
.collect();
let mut b = a.clone();
for (i, slot) in b.iter_mut().enumerate() {
if (i as u64 + seed).is_multiple_of(3) {
*slot = slot.wrapping_add(1);
}
}
assert_eq!(
slot_hamming_u64(&a, &b),
slot_hamming_u64_ref(&a, &b),
"n={n}, seed={seed}"
);
}
}
}
#[test]
fn slot_hamming_u64_edges() {
assert_eq!(slot_hamming_u64(&[], &[]), 0);
assert_eq!(slot_hamming_u64(&[1, 2, 3], &[1, 2, 3]), 0);
assert_eq!(slot_hamming_u64(&[1, 2, 3], &[9, 9, 9]), 3);
}
#[test]
fn slot_hamming_u16_matches_reference_across_boundaries() {
for n in [1, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 100, 257, 1024] {
for seed in 0..4u32 {
let a: Vec<u16> = (0..n as u32)
.map(|i| (i.wrapping_mul(40_503) ^ seed) as u16)
.collect();
let mut b = a.clone();
for (i, slot) in b.iter_mut().enumerate() {
if (i as u32 + seed).is_multiple_of(3) {
*slot = slot.wrapping_add(1);
}
}
let want = a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() as u32;
assert_eq!(slot_hamming_u16(&a, &b), want, "n={n}, seed={seed}");
}
}
}
#[test]
fn slot_hamming_u16_edges() {
assert_eq!(slot_hamming_u16(&[], &[]), 0);
assert_eq!(slot_hamming_u16(&[1, 2, 3], &[1, 2, 3]), 0);
assert_eq!(slot_hamming_u16(&[1, 2, 3], &[9, 9, 9]), 3);
}
#[test]
fn slot_compare_counts_basic_and_generalizes_hamming() {
let a = [3u16, 1, 4, 1, 5, 9];
let b = [3u16, 1, 2, 9, 5, 9];
let c = slot_compare_counts(&a, &b);
assert_eq!(
c,
SlotCounts {
eq: 4,
lt: 1,
gt: 1
}
);
assert_eq!(c.eq + c.lt + c.gt, a.len());
assert_eq!(slot_hamming(&a, &b), a.len() - c.eq);
assert_eq!(slot_compare_counts::<u32>(&[], &[]), SlotCounts::default());
}
#[test]
fn test_slot_hamming_u32_basic() {
let a = [1u32, 2, 3, 4];
let b = [1u32, 0, 3, 9];
assert_eq!(slot_hamming_u32(&a, &b), 2);
}
#[test]
fn test_slot_hamming_u32_empty() {
assert_eq!(slot_hamming_u32(&[], &[]), 0);
}
#[test]
fn test_slot_hamming_u32_identical() {
let v = [7u32, 11, 13, 17, 19];
assert_eq!(slot_hamming_u32(&v, &v), 0);
}
#[test]
fn test_slot_hamming_u32_all_differ() {
let a = [1u32; 32];
let b = [2u32; 32];
assert_eq!(slot_hamming_u32(&a, &b), 32);
}
#[test]
fn test_slot_hamming_u32_symmetric() {
let a = [1u32, 5, 9, 13, 2, 6, 10, 14, 3];
let b = [1u32, 0, 9, 0, 2, 0, 10, 0, 3];
assert_eq!(slot_hamming_u32(&a, &b), slot_hamming_u32(&b, &a));
}
#[test]
fn test_slot_hamming_u32_boundary_sizes() {
for size in [1usize, 7, 8, 15, 16, 17, 31, 32, 33, 64, 128] {
let a: Vec<u32> = (0..size as u32).collect();
let b: Vec<u32> = (0..size as u32)
.map(|i| if i % 3 == 0 { i + 1000 } else { i })
.collect();
let expected = a.iter().zip(&b).filter(|(x, y)| x != y).count() as u32;
assert_eq!(
slot_hamming_u32(&a, &b),
expected,
"slot_hamming_u32 mismatch at size={size}"
);
}
}
#[test]
fn test_slot_hamming_u32_matches_portable() {
let a: Vec<u32> = (0..100u32).map(|i| i.wrapping_mul(2654435761)).collect();
let b: Vec<u32> = (0..100u32).map(|i| i.wrapping_mul(40503)).collect();
assert_eq!(slot_hamming_u32(&a, &b), slot_hamming_u32_portable(&a, &b));
}
#[test]
#[should_panic(expected = "innr::slot_hamming_u32: slice length mismatch")]
fn test_slot_hamming_u32_length_mismatch() {
let _ = slot_hamming_u32(&[1, 2], &[1, 2, 3]);
}
#[test]
fn test_slot_hamming_generic_u16() {
let a = [10u16, 20, 30];
let b = [10u16, 99, 30];
assert_eq!(slot_hamming(&a, &b), 1);
}
#[test]
fn test_slot_hamming_generic_u64() {
let a = [1u64, 2, 3, 4];
let b = [1u64, 9, 3, 9];
assert_eq!(slot_hamming(&a, &b), 2);
}
#[test]
fn test_slot_hamming_generic_agrees_with_u32() {
let a: Vec<u32> = (0..50u32).map(|i| i % 7).collect();
let b: Vec<u32> = (0..50u32).map(|i| i % 5).collect();
assert_eq!(slot_hamming(&a, &b) as u32, slot_hamming_u32(&a, &b));
}
#[test]
fn test_minhash_jaccard_basic() {
let a = [1u32, 2, 3, 4];
let b = [1u32, 2, 3, 9];
assert_eq!(minhash_jaccard(&a, &b), 0.75);
}
#[test]
fn test_minhash_jaccard_identical() {
let v = [5u32, 6, 7, 8];
assert_eq!(minhash_jaccard(&v, &v), 1.0);
}
#[test]
fn test_minhash_jaccard_disjoint() {
let a = [1u32, 2, 3, 4];
let b = [5u32, 6, 7, 8];
assert_eq!(minhash_jaccard(&a, &b), 0.0);
}
#[test]
fn test_minhash_jaccard_empty() {
assert_eq!(minhash_jaccard(&[], &[]), 1.0);
}
#[test]
fn test_jaccard_distance_basic() {
let a = [1u32, 2, 3, 4];
let b = [1u32, 2, 3, 9];
assert_eq!(jaccard_distance(&a, &b), 0.25);
}
#[test]
fn test_jaccard_distance_complements_similarity() {
let a = [1u32, 5, 9, 2, 6, 10, 3, 7];
let b = [1u32, 0, 9, 0, 6, 0, 3, 0];
assert_eq!(jaccard_distance(&a, &b), 1.0 - minhash_jaccard(&a, &b));
}
#[test]
fn test_jaccard_distance_identical() {
let v = [5u32, 6, 7, 8];
assert_eq!(jaccard_distance(&v, &v), 0.0);
}
#[test]
fn test_jaccard_distance_empty() {
assert_eq!(jaccard_distance(&[], &[]), 0.0);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
const SIZES: &[usize] = &[0, 1, 7, 8, 15, 16, 31, 32, 64, 96, 128];
fn arb_u32_pair(len: usize) -> impl Strategy<Value = (Vec<u32>, Vec<u32>)> {
(
prop::collection::vec(0u32..8, len),
prop::collection::vec(0u32..8, len),
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(300))]
#[test]
fn slot_hamming_u32_matches_portable(
(a, b) in prop::sample::select(SIZES).prop_flat_map(arb_u32_pair)
) {
prop_assert_eq!(slot_hamming_u32(&a, &b), slot_hamming_u32_portable(&a, &b));
}
#[test]
fn slot_hamming_u32_symmetric(
(a, b) in prop::sample::select(SIZES).prop_flat_map(arb_u32_pair)
) {
prop_assert_eq!(slot_hamming_u32(&a, &b), slot_hamming_u32(&b, &a));
}
#[test]
fn slot_hamming_u32_self_is_zero(
a in prop::sample::select(SIZES)
.prop_flat_map(|s| prop::collection::vec(0u32..8, s))
) {
prop_assert_eq!(slot_hamming_u32(&a, &a), 0);
}
#[test]
fn slot_hamming_u32_bounded(
(a, b) in prop::sample::select(SIZES).prop_flat_map(arb_u32_pair)
) {
prop_assert!(slot_hamming_u32(&a, &b) <= a.len() as u32);
}
#[test]
fn generic_agrees_with_u32(
(a, b) in prop::sample::select(SIZES).prop_flat_map(arb_u32_pair)
) {
prop_assert_eq!(slot_hamming(&a, &b) as u32, slot_hamming_u32(&a, &b));
}
#[test]
fn minhash_jaccard_in_unit_interval(
(a, b) in prop::sample::select(SIZES).prop_flat_map(arb_u32_pair)
) {
let j = minhash_jaccard(&a, &b);
prop_assert!((0.0..=1.0).contains(&j), "jaccard {} out of range", j);
}
}
}