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