Skip to main content

lc_core/
math.rs

1// src/core/math.rs
2//! Shared math utilities.
3
4/// Error type for math operations.
5#[derive(Debug, Clone, thiserror::Error)]
6pub enum MathError {
7    /// The input vectors have different lengths.
8    #[error("vector length mismatch: {0} vs {1}")]
9    LengthMismatch(usize, usize),
10}
11
12/// Compute cosine similarity between two vectors.
13///
14/// Returns a value in [-1, 1], where 1 means identical direction.
15/// Returns an error if vectors have different lengths.
16/// Returns 0.0 if either vector has zero norm (below epsilon threshold).
17///
18/// Internally computes in f64 for precision, but accepts and returns f32
19/// to match the project's embedding type.
20///
21/// # Examples
22/// ```no_run
23/// use lc_core::math::cosine_similarity;
24///
25/// let a = vec![1.0_f32, 0.0, 0.0];
26/// let b = vec![0.0_f32, 1.0, 0.0];
27/// let sim = cosine_similarity(&a, &b).unwrap();
28/// assert!((sim - 0.0).abs() < 1e-6);
29/// ```
30pub fn cosine_similarity(a: &[f32], b: &[f32]) -> Result<f32, MathError> {
31    if a.len() != b.len() {
32        return Err(MathError::LengthMismatch(a.len(), b.len()));
33    }
34
35    // Compute in f64 for precision (M30), then cast back to f32.
36    let dot_product: f64 = a
37        .iter()
38        .zip(b.iter())
39        .map(|(x, y)| (*x as f64) * (*y as f64))
40        .sum();
41    let norm_a: f64 = a
42        .iter()
43        .map(|x| (*x as f64) * (*x as f64))
44        .sum::<f64>()
45        .sqrt();
46    let norm_b: f64 = b
47        .iter()
48        .map(|x| (*x as f64) * (*x as f64))
49        .sum::<f64>()
50        .sqrt();
51
52    // Use epsilon comparison instead of exact == 0.0 (C22)
53    if norm_a < f64::EPSILON || norm_b < f64::EPSILON {
54        return Ok(0.0);
55    }
56
57    Ok((dot_product / (norm_a * norm_b)) as f32)
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_identical_vectors() {
66        let v = vec![1.0, 2.0, 3.0];
67        let sim = cosine_similarity(&v, &v).unwrap();
68        assert!((sim - 1.0).abs() < 1e-6);
69    }
70
71    #[test]
72    fn test_orthogonal_vectors() {
73        let a = vec![1.0, 0.0, 0.0];
74        let b = vec![0.0, 1.0, 0.0];
75        let sim = cosine_similarity(&a, &b).unwrap();
76        assert!((sim - 0.0).abs() < 1e-6);
77    }
78
79    #[test]
80    fn test_opposite_vectors() {
81        let a = vec![1.0, 0.0];
82        let b = vec![-1.0, 0.0];
83        let sim = cosine_similarity(&a, &b).unwrap();
84        assert!((sim - (-1.0)).abs() < 1e-6);
85    }
86
87    #[test]
88    fn test_different_lengths_returns_error() {
89        let a = vec![1.0, 2.0];
90        let b = vec![1.0];
91        assert!(cosine_similarity(&a, &b).is_err());
92    }
93
94    #[test]
95    fn test_zero_vector() {
96        let a = vec![0.0, 0.0];
97        let b = vec![1.0, 2.0];
98        assert_eq!(cosine_similarity(&a, &b).unwrap(), 0.0);
99    }
100}