use thiserror::Error;
#[derive(Error, Debug)]
pub enum CosineSimilarityError {
#[error("Vector length mismatch: {a_len} vs {b_len}")]
LengthMismatch { a_len: usize, b_len: usize },
#[error("Empty vector provided")]
EmptyVector,
#[error("Zero-norm vector: cannot compute cosine similarity")]
ZeroNorm,
#[error("Invalid values: contains NaN or infinite")]
InvalidValues,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CosineSimilarityResult {
pub per_layer: Vec<f64>,
pub average: f64,
pub min: f64,
pub min_layer_idx: usize,
}
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> Result<f64, CosineSimilarityError> {
if a.is_empty() || b.is_empty() {
return Err(CosineSimilarityError::EmptyVector);
}
if a.len() != b.len() {
return Err(CosineSimilarityError::LengthMismatch {
a_len: a.len(),
b_len: b.len(),
});
}
if a.iter().any(|x| x.is_nan() || x.is_infinite())
|| b.iter().any(|x| x.is_nan() || x.is_infinite())
{
return Err(CosineSimilarityError::InvalidValues);
}
let mut dot_product = 0.0_f64;
let mut norm_a_sq = 0.0_f64;
let mut norm_b_sq = 0.0_f64;
for (ai, bi) in a.iter().zip(b.iter()) {
let ai = *ai as f64;
let bi = *bi as f64;
dot_product += ai * bi;
norm_a_sq += ai * ai;
norm_b_sq += bi * bi;
}
let norm_a = norm_a_sq.sqrt();
let norm_b = norm_b_sq.sqrt();
if norm_a < 1e-12 || norm_b < 1e-12 {
return Err(CosineSimilarityError::ZeroNorm);
}
let similarity = dot_product / (norm_a * norm_b);
Ok(similarity.clamp(-1.0, 1.0))
}
pub fn per_layer_cosine_similarity(
original_layers: &[Vec<f32>],
quantized_layers: &[Vec<f32>],
) -> Result<CosineSimilarityResult, CosineSimilarityError> {
if original_layers.is_empty() {
return Err(CosineSimilarityError::EmptyVector);
}
let num_layers = original_layers.len().min(quantized_layers.len());
let mut per_layer = Vec::with_capacity(num_layers);
for i in 0..num_layers {
let orig = &original_layers[i];
let quant = &quantized_layers[i];
let len = orig.len().min(quant.len());
if len == 0 {
per_layer.push(1.0); continue;
}
let sim = cosine_similarity(&orig[..len], &quant[..len])?;
per_layer.push(sim);
}
let average = if per_layer.is_empty() {
1.0
} else {
per_layer.iter().sum::<f64>() / per_layer.len() as f64
};
let (min_layer_idx, min) = per_layer
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, &val)| (idx, val))
.unwrap_or((0, 1.0));
Ok(CosineSimilarityResult {
per_layer,
average,
min,
min_layer_idx,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identical_vectors_similarity_one() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let sim = cosine_similarity(&a, &a).unwrap();
assert!((sim - 1.0).abs() < 1e-10, "Identical vectors should have similarity 1.0, got {}", sim);
}
#[test]
fn test_opposite_vectors_similarity_negative_one() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![-1.0, -2.0, -3.0];
let sim = cosine_similarity(&a, &b).unwrap();
assert!((sim - (-1.0)).abs() < 1e-10, "Opposite vectors should have similarity -1.0, got {}", sim);
}
#[test]
fn test_orthogonal_vectors_similarity_zero() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![0.0, 1.0, 0.0];
let sim = cosine_similarity(&a, &b).unwrap();
assert!(sim.abs() < 1e-10, "Orthogonal vectors should have similarity ~0, got {}", sim);
}
#[test]
fn test_similar_vectors_high_similarity() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = vec![1.01, 2.01, 3.01, 4.01, 5.01]; let sim = cosine_similarity(&a, &b).unwrap();
assert!(sim > 0.999, "Similar vectors should have high similarity, got {}", sim);
}
#[test]
fn test_length_mismatch() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![1.0, 2.0];
let result = cosine_similarity(&a, &b);
assert!(result.is_err());
}
#[test]
fn test_empty_vector() {
let result = cosine_similarity(&[], &[]);
assert!(result.is_err());
}
#[test]
fn test_zero_vector() {
let a = vec![0.0, 0.0, 0.0];
let b = vec![1.0, 2.0, 3.0];
let result = cosine_similarity(&a, &b);
assert!(result.is_err());
}
#[test]
fn test_nan_detection() {
let a = vec![1.0, f32::NAN, 3.0];
let b = vec![1.0, 2.0, 3.0];
let result = cosine_similarity(&a, &b);
assert!(result.is_err());
}
#[test]
fn test_per_layer_cosine_similarity() {
let original = vec![
vec![1.0, 2.0, 3.0],
vec![4.0, 5.0, 6.0],
vec![7.0, 8.0, 9.0],
];
let quantized = vec![
vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.01], vec![-7.0, -8.0, -9.0], ];
let result = per_layer_cosine_similarity(&original, &quantized).unwrap();
assert_eq!(result.per_layer.len(), 3);
assert!((result.per_layer[0] - 1.0).abs() < 1e-10);
assert!(result.per_layer[1] > 0.99);
assert!((result.per_layer[2] - (-1.0)).abs() < 1e-10);
assert_eq!(result.min_layer_idx, 2);
}
#[test]
fn test_per_layer_empty() {
let result = per_layer_cosine_similarity(&[], &[]);
assert!(result.is_err());
}
#[test]
fn test_cosine_similarity_range() {
let a = vec![0.1, -0.5, 0.3, 0.8, -0.2];
let b = vec![-0.3, 0.7, -0.1, 0.4, 0.6];
let sim = cosine_similarity(&a, &b).unwrap();
assert!((-1.0..=1.0).contains(&sim), "Cosine similarity should be in [-1, 1], got {}", sim);
}
#[test]
fn test_scaling_invariance() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![2.0, 4.0, 6.0]; let sim = cosine_similarity(&a, &b).unwrap();
assert!((sim - 1.0).abs() < 1e-10, "Scaled vectors should have similarity 1.0, got {}", sim);
}
}