use super::distance::*;
#[test]
fn test_cosine_similarity() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![1.0, 0.0, 0.0];
let similarity = DistanceMetric::Cosine.calculate(&a, &b);
assert!((similarity - 1.0).abs() < 1e-6);
let c = vec![0.0, 1.0, 0.0];
let similarity = DistanceMetric::Cosine.calculate(&a, &c);
assert!(similarity.abs() < 1e-6);
}
#[test]
fn test_euclidean_distance() {
let a = vec![0.0, 0.0, 0.0];
let b = vec![3.0, 4.0, 0.0];
let distance = DistanceMetric::Euclidean.calculate(&a, &b);
assert!((distance - 5.0).abs() < 1e-6);
}
#[test]
fn test_dot_product() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
let product = DistanceMetric::DotProduct.calculate(&a, &b);
assert!((product - 32.0).abs() < 1e-6);
}
#[test]
fn test_higher_is_better() {
assert!(DistanceMetric::Cosine.higher_is_better());
assert!(DistanceMetric::DotProduct.higher_is_better());
assert!(!DistanceMetric::Euclidean.higher_is_better());
}
#[test]
fn test_metric_serialization() {
let metric = DistanceMetric::Cosine;
let json = serde_json::to_string(&metric).unwrap();
let deserialized: DistanceMetric = serde_json::from_str(&json).unwrap();
assert_eq!(metric, deserialized);
let metric = DistanceMetric::Euclidean;
let json = serde_json::to_string(&metric).unwrap();
let deserialized: DistanceMetric = serde_json::from_str(&json).unwrap();
assert_eq!(metric, deserialized);
let metric = DistanceMetric::DotProduct;
let json = serde_json::to_string(&metric).unwrap();
let deserialized: DistanceMetric = serde_json::from_str(&json).unwrap();
assert_eq!(metric, deserialized);
let metric = DistanceMetric::Hamming;
let json = serde_json::to_string(&metric).unwrap();
let deserialized: DistanceMetric = serde_json::from_str(&json).unwrap();
assert_eq!(metric, deserialized);
let metric = DistanceMetric::Jaccard;
let json = serde_json::to_string(&metric).unwrap();
let deserialized: DistanceMetric = serde_json::from_str(&json).unwrap();
assert_eq!(metric, deserialized);
}
#[test]
fn test_hamming_distance_identical() {
let a = vec![1.0, 0.0, 1.0, 0.0];
let b = vec![1.0, 0.0, 1.0, 0.0];
let distance = DistanceMetric::Hamming.calculate(&a, &b);
assert!(
(distance - 0.0).abs() < 1e-6,
"Identical vectors: distance = 0"
);
}
#[test]
fn test_hamming_distance_completely_different() {
let a = vec![1.0, 1.0, 1.0, 1.0];
let b = vec![0.0, 0.0, 0.0, 0.0];
let distance = DistanceMetric::Hamming.calculate(&a, &b);
assert!(
(distance - 4.0).abs() < 1e-6,
"All bits differ: distance = 4"
);
}
#[test]
fn test_hamming_distance_partial() {
let a = vec![1.0, 0.0, 1.0, 0.0];
let b = vec![1.0, 1.0, 0.0, 0.0];
let distance = DistanceMetric::Hamming.calculate(&a, &b);
assert!((distance - 2.0).abs() < 1e-6, "2 bits differ: distance = 2");
}
#[test]
fn test_hamming_higher_is_better() {
assert!(!DistanceMetric::Hamming.higher_is_better());
}
#[test]
fn test_jaccard_similarity_identical() {
let a = vec![1.0, 0.0, 1.0, 1.0];
let b = vec![1.0, 0.0, 1.0, 1.0];
let similarity = DistanceMetric::Jaccard.calculate(&a, &b);
assert!(
(similarity - 1.0).abs() < 1e-6,
"Identical sets: similarity = 1.0"
);
}
#[test]
fn test_jaccard_similarity_disjoint() {
let a = vec![1.0, 1.0, 0.0, 0.0];
let b = vec![0.0, 0.0, 1.0, 1.0];
let similarity = DistanceMetric::Jaccard.calculate(&a, &b);
assert!(
(similarity - 0.0).abs() < 1e-6,
"Disjoint sets: similarity = 0.0"
);
}
#[test]
fn test_jaccard_similarity_partial_overlap() {
let a = vec![1.0, 1.0, 1.0, 0.0];
let b = vec![1.0, 1.0, 0.0, 1.0];
let similarity = DistanceMetric::Jaccard.calculate(&a, &b);
assert!(
(similarity - 0.5).abs() < 1e-6,
"Partial overlap: similarity = 0.5"
);
}
#[test]
fn test_jaccard_similarity_empty_sets() {
let a = vec![0.0, 0.0, 0.0, 0.0];
let b = vec![0.0, 0.0, 0.0, 0.0];
let similarity = DistanceMetric::Jaccard.calculate(&a, &b);
assert!(
(similarity - 1.0).abs() < 1e-6,
"Empty sets: similarity = 1.0"
);
}
#[test]
fn test_jaccard_higher_is_better() {
assert!(DistanceMetric::Jaccard.higher_is_better());
}
#[test]
fn test_sort_results_cosine_descending() {
let mut results = vec![(1, 0.7), (2, 0.9), (3, 0.8)];
DistanceMetric::Cosine.sort_results(&mut results);
assert_eq!(results[0].0, 2); assert_eq!(results[1].0, 3);
assert_eq!(results[2].0, 1);
}
#[test]
fn test_sort_results_euclidean_ascending() {
let mut results = vec![(1, 5.0), (2, 2.0), (3, 3.0)];
DistanceMetric::Euclidean.sort_results(&mut results);
assert_eq!(results[0].0, 2); assert_eq!(results[1].0, 3);
assert_eq!(results[2].0, 1);
}
#[test]
fn test_sort_results_dot_product_descending() {
let mut results = vec![(1, 10.0), (2, 30.0), (3, 20.0)];
DistanceMetric::DotProduct.sort_results(&mut results);
assert_eq!(results[0].0, 2); }
#[test]
fn test_sort_results_hamming_ascending() {
let mut results = vec![(1, 4.0), (2, 1.0), (3, 2.0)];
DistanceMetric::Hamming.sort_results(&mut results);
assert_eq!(results[0].0, 2); }
#[test]
fn test_sort_results_jaccard_descending() {
let mut results = vec![(1, 0.3), (2, 0.9), (3, 0.5)];
DistanceMetric::Jaccard.sort_results(&mut results);
assert_eq!(results[0].0, 2); }
#[test]
fn test_sort_results_handles_nan() {
let mut results = vec![(1, f32::NAN), (2, 0.5), (3, 0.8)];
DistanceMetric::Cosine.sort_results(&mut results);
assert!(results[0].1.is_nan()); assert_eq!(results[1].0, 3); assert_eq!(results[2].0, 2); }
#[test]
fn test_sort_results_empty_input_does_not_panic() {
let mut results: Vec<(u64, f32)> = vec![];
DistanceMetric::Cosine.sort_results(&mut results);
}
#[test]
fn test_parse_aliases() {
assert_eq!(
DistanceMetric::parse_alias("cosine"),
Some(DistanceMetric::Cosine)
);
assert_eq!(
DistanceMetric::parse_alias("L2"),
Some(DistanceMetric::Euclidean)
);
assert_eq!(
DistanceMetric::parse_alias("inner"),
Some(DistanceMetric::DotProduct)
);
assert_eq!(
DistanceMetric::parse_alias("hamming"),
Some(DistanceMetric::Hamming)
);
assert_eq!(
DistanceMetric::parse_alias("jaccard"),
Some(DistanceMetric::Jaccard)
);
assert_eq!(DistanceMetric::parse_alias("unknown"), None);
}
#[test]
fn test_canonical_names_and_from_str() {
use std::str::FromStr;
assert_eq!(DistanceMetric::Cosine.canonical_name(), "cosine");
assert_eq!(DistanceMetric::Euclidean.canonical_name(), "euclidean");
assert_eq!(DistanceMetric::DotProduct.canonical_name(), "dot");
assert_eq!(DistanceMetric::Hamming.canonical_name(), "hamming");
assert_eq!(DistanceMetric::Jaccard.canonical_name(), "jaccard");
assert_eq!(
DistanceMetric::from_str("dotproduct").unwrap(),
DistanceMetric::DotProduct
);
assert!(DistanceMetric::from_str("invalid").is_err());
}
#[test]
fn test_parse_ip_alias() {
use std::str::FromStr;
assert_eq!(
DistanceMetric::from_str("ip").unwrap(),
DistanceMetric::DotProduct
);
assert_eq!(
DistanceMetric::from_str("IP").unwrap(),
DistanceMetric::DotProduct
);
assert_eq!(
DistanceMetric::parse_alias("ip"),
Some(DistanceMetric::DotProduct)
);
assert_eq!(
DistanceMetric::parse_alias(" IP "),
Some(DistanceMetric::DotProduct)
);
}
#[test]
fn score_range_matches_what_calculate_produces() {
let aligned = vec![1.0_f32, 0.0, 0.0];
let opposed = vec![-1.0_f32, 0.0, 0.0];
let cosine = DistanceMetric::Cosine.calculate(&aligned, &opposed);
let (low, high) = DistanceMetric::Cosine
.score_range()
.expect("cosine is a bounded metric");
assert!(
(cosine - low).abs() < 1e-6,
"opposed vectors must reach the declared floor {low}, got {cosine}"
);
assert!((high - 1.0).abs() < f32::EPSILON);
let disjoint_a = vec![1.0_f32, 0.0];
let disjoint_b = vec![0.0_f32, 1.0];
let jaccard = DistanceMetric::Jaccard.calculate(&disjoint_a, &disjoint_b);
let (j_low, j_high) = DistanceMetric::Jaccard
.score_range()
.expect("jaccard is a bounded metric");
assert!((j_low - 0.0).abs() < f32::EPSILON);
assert!((j_high - 1.0).abs() < f32::EPSILON);
assert!(jaccard >= j_low && jaccard <= j_high);
}
#[test]
fn clamp_score_respects_each_metrics_own_floor() {
assert!((DistanceMetric::Cosine.clamp_score(-0.5) + 0.5).abs() < f32::EPSILON);
assert!(DistanceMetric::Jaccard.clamp_score(-0.5).abs() < f32::EPSILON);
}
#[test]
fn clamp_score_absorbs_boundary_drift_without_moving_interior_values() {
assert!((DistanceMetric::Cosine.clamp_score(1.000_001) - 1.0).abs() < f32::EPSILON);
assert!((DistanceMetric::Cosine.clamp_score(-1.000_001) + 1.0).abs() < f32::EPSILON);
assert!((DistanceMetric::Cosine.clamp_score(0.25) - 0.25).abs() < f32::EPSILON);
}
#[test]
fn clamp_score_leaves_unbounded_metrics_alone() {
for metric in [
DistanceMetric::Euclidean,
DistanceMetric::Hamming,
DistanceMetric::DotProduct,
] {
assert!(
metric.score_range().is_none(),
"{metric:?} is unbounded and must declare no range"
);
for value in [-42.0_f32, 0.0, 1.5, 9_000.0] {
assert!(
(metric.clamp_score(value) - value).abs() < f32::EPSILON,
"{metric:?} must pass {value} through unchanged"
);
}
}
}
#[test]
fn clamp_score_is_the_identity_on_real_calculate_output() {
let cosine_pairs: [(&[f32], &[f32]); 4] = [
(&[1.0, 0.0, 0.0], &[1.0, 0.0, 0.0]),
(&[1.0, 0.0, 0.0], &[-1.0, 0.0, 0.0]),
(&[1.0, 2.0, 3.0], &[3.0, 2.0, 1.0]),
(&[0.0, 0.0, 0.0], &[0.0, 0.0, 0.0]),
];
let jaccard_pairs: [(&[f32], &[f32]); 4] = [
(&[1.0, 0.0, 0.0], &[1.0, 0.0, 0.0]),
(&[1.0, 0.0, 0.0], &[0.0, 1.0, 0.0]),
(&[1.0, 2.0, 3.0], &[3.0, 2.0, 1.0]),
(&[0.0, 0.0, 0.0], &[0.0, 0.0, 0.0]),
];
for (metric, pairs) in [
(DistanceMetric::Cosine, cosine_pairs),
(DistanceMetric::Jaccard, jaccard_pairs),
] {
for (a, b) in pairs {
let raw = metric.calculate(a, b);
let clamped = metric.clamp_score(raw);
assert!(
(raw - clamped).abs() < f32::EPSILON,
"{metric:?} produced {raw} on {a:?} vs {b:?}, outside its declared range"
);
}
}
}
#[test]
fn jaccard_calculate_escapes_its_declared_range_on_negative_weights() {
let positive = vec![1.0_f32, 0.0, 0.0];
let negative = vec![-1.0_f32, 0.0, 0.0];
let raw = DistanceMetric::Jaccard.calculate(&positive, &negative);
let (low, _) = DistanceMetric::Jaccard
.score_range()
.expect("jaccard is bounded");
assert!(
raw < low,
"expected the known unclamped kernel output below the declared floor {low}, got {raw}"
);
assert!((DistanceMetric::Jaccard.clamp_score(raw) - low).abs() < f32::EPSILON);
}