#![allow(missing_docs)]
#[cfg(test)]
mod sparse_maxsim_props {
use innr::sparse_maxsim;
use proptest::prelude::*;
proptest! {
#[test]
fn sparse_maxsim_sanity(
query_raw in proptest::collection::vec(
proptest::collection::vec((0u32..100, -1.0f32..1.0), 1..10), 1..5
),
doc_raw in proptest::collection::vec(
proptest::collection::vec((0u32..100, -1.0f32..1.0), 1..10), 1..10
)
) {
let query_vecs: Vec<(Vec<u32>, Vec<f32>)> = query_raw.into_iter().map(|raw| {
let mut sorted = raw;
sorted.sort_by_key(|(idx, _)| *idx);
sorted.dedup_by_key(|(idx, _)| *idx); let (indices, values): (Vec<u32>, Vec<f32>) = sorted.into_iter().unzip();
(indices, values)
}).collect();
let doc_vecs: Vec<(Vec<u32>, Vec<f32>)> = doc_raw.into_iter().map(|raw| {
let mut sorted = raw;
sorted.sort_by_key(|(idx, _)| *idx);
sorted.dedup_by_key(|(idx, _)| *idx);
let (indices, values): (Vec<u32>, Vec<f32>) = sorted.into_iter().unzip();
(indices, values)
}).collect();
let query_slices: Vec<(&[u32], &[f32])> = query_vecs.iter()
.map(|(i, v)| (i.as_slice(), v.as_slice()))
.collect();
let doc_slices: Vec<(&[u32], &[f32])> = doc_vecs.iter()
.map(|(i, v)| (i.as_slice(), v.as_slice()))
.collect();
let score = sparse_maxsim(&query_slices, &doc_slices);
prop_assert!(!score.is_nan());
let expected: f32 = query_slices.iter().map(|(q_idx, q_val)| {
doc_slices.iter().map(|(d_idx, d_val)| {
let mut dot = 0.0f32;
let mut i = 0;
let mut j = 0;
while i < q_idx.len() && j < d_idx.len() {
match q_idx[i].cmp(&d_idx[j]) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
dot += q_val[i] * d_val[j];
i += 1;
j += 1;
}
}
}
dot
})
.fold(f32::NEG_INFINITY, f32::max)
})
.sum();
prop_assert!(
(score - expected).abs() < 1e-4 * expected.abs().max(1.0),
"sparse_maxsim={} vs reference={}", score, expected
);
}
#[test]
fn sparse_dot_matches_dense(
dim in 10u32..50,
nnz in 1usize..10,
) {
use innr::{sparse_dot, dot};
let indices: Vec<u32> = (0..nnz as u32).collect();
let values: Vec<f32> = (0..nnz).map(|i| (i as f32 + 1.0) * 0.1).collect();
let mut dense = vec![0.0f32; dim as usize];
for (&idx, &val) in indices.iter().zip(values.iter()) {
if (idx as usize) < dense.len() {
dense[idx as usize] = val;
}
}
let sparse_result = sparse_dot(&indices, &values, &indices, &values);
let dense_result = dot(&dense, &dense);
prop_assert!(
(sparse_result - dense_result).abs() < 1e-5,
"sparse_dot={} vs dense dot={}", sparse_result, dense_result
);
}
#[test]
fn sparse_dot_symmetric(
a_raw in proptest::collection::vec((0u32..50, -1.0f32..1.0), 1..8),
b_raw in proptest::collection::vec((0u32..50, -1.0f32..1.0), 1..8),
) {
use innr::sparse_dot;
let mut a = a_raw;
a.sort_by_key(|(idx, _)| *idx);
a.dedup_by_key(|(idx, _)| *idx);
let (ai, av): (Vec<u32>, Vec<f32>) = a.into_iter().unzip();
let mut b = b_raw;
b.sort_by_key(|(idx, _)| *idx);
b.dedup_by_key(|(idx, _)| *idx);
let (bi, bv): (Vec<u32>, Vec<f32>) = b.into_iter().unzip();
let ab = sparse_dot(&ai, &av, &bi, &bv);
let ba = sparse_dot(&bi, &bv, &ai, &av);
prop_assert!(
(ab - ba).abs() < 1e-6,
"sparse_dot not symmetric: ab={}, ba={}", ab, ba
);
}
}
}