Skip to main content

semantic_memory/archive/
hubness_rust.rs

1//! ARCHIVED: Pure-Rust cosine_similarity and compute_hubness_scores.
2//!
3//! This file preserves the original Rust implementations that were replaced
4//! by the C SIMD kernel in `c-kernels/similarity.c` (via FFI in `hubness.rs`).
5//!
6//! Archived on: 2026-07-12
7//! Reason: Performance — C kernel with compiler auto-vectorization; AVX2/FMA
8//!         is enabled only for targets that advertise those features.
9//!         replaces the pure-Rust dot product / norm computation.
10//!
11//! The `cosine_similarity` function below is the original pure-Rust version.
12//! `compute_hubness_scores` remains in Rust (in hubness.rs) and calls the
13//! C-backed `cosine_similarity` — only the inner math was moved to C.
14//!
15//! To restore the pure-Rust version, copy these functions back into
16//! hubness.rs and remove the FFI `extern "C"` block.
17
18/// Cosine similarity between two equal-length vectors.
19///
20/// Returns `None` if the slices have different lengths, either has zero norm,
21/// or either is empty.
22#[allow(dead_code)]
23pub fn cosine_similarity(a: &[f32], b: &[f32]) -> Option<f32> {
24    if a.len() != b.len() || a.is_empty() {
25        return None;
26    }
27    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
28    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
29    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
30    if norm_a == 0.0 || norm_b == 0.0 {
31        return None;
32    }
33    Some(dot / (norm_a * norm_b))
34}
35
36/// Compute hubness scores for a collection of embeddings.
37///
38/// For each vector, the top-`top_k` most similar other vectors (by cosine
39/// similarity) are found. Each neighbour's `neighbor_hits` counter is
40/// incremented. Pairs whose embeddings differ in dimension or have a zero
41/// norm are skipped rather than panicked.
42///
43/// The returned list is sorted descending by `neighbor_hits`, with ties
44/// broken by `item_id` ascending for full determinism.
45#[allow(dead_code)]
46pub fn compute_hubness_scores(
47    embeddings: &[(String, Vec<f32>)],
48    top_k: usize,
49) -> Vec<HubnessScore> {
50    let n = embeddings.len();
51    let mut hits = vec![0usize; n];
52
53    if top_k == 0 || n < 2 {
54        // Nothing to accumulate; still return correctly-zeroed scores.
55        let max_hits = n.saturating_sub(1);
56        let mut scores: Vec<HubnessScore> = embeddings
57            .iter()
58            .map(|(id, _)| HubnessScore {
59                item_id: id.clone(),
60                neighbor_hits: 0,
61                normalized_score: 0.0,
62            })
63            .collect();
64        scores.sort_unstable_by(|a, b| {
65            b.neighbor_hits
66                .cmp(&a.neighbor_hits)
67                .then_with(|| a.item_id.cmp(&b.item_id))
68        });
69        let _ = max_hits; // explicitly used below in the normal path
70        return scores;
71    }
72
73    let max_possible_hits = n.saturating_sub(1);
74
75    for i in 0..n {
76        let (_, ref qi) = embeddings[i];
77        // Collect similarities to all other items with matching dimension.
78        let mut sims: Vec<(f32, &str)> = embeddings
79            .iter()
80            .enumerate()
81            .filter(|(j, _)| *j != i)
82            .filter_map(|(_, (id, emb))| cosine_similarity(qi, emb).map(|s| (s, id.as_str())))
83            .collect();
84
85        // Sort descending by similarity; stable tie-break by item_id ascending.
86        sims.sort_unstable_by(|a, b| {
87            b.0.partial_cmp(&a.0)
88                .unwrap_or(std::cmp::Ordering::Equal)
89                .then_with(|| a.1.cmp(b.1))
90        });
91
92        // Increment hit counter for each top-k neighbour.
93        for (_, neighbour_id) in sims.iter().take(top_k) {
94            if let Some(j) = embeddings
95                .iter()
96                .position(|(id, _)| id.as_str() == *neighbour_id)
97            {
98                hits[j] += 1;
99            }
100        }
101    }
102
103    let mut scores: Vec<HubnessScore> = embeddings
104        .iter()
105        .enumerate()
106        .map(|(i, (id, _))| {
107            let h = hits[i];
108            let norm = if max_possible_hits == 0 {
109                0.0
110            } else {
111                h as f32 / max_possible_hits as f32
112            };
113            HubnessScore {
114                item_id: id.clone(),
115                neighbor_hits: h,
116                normalized_score: norm,
117            }
118        })
119        .collect();
120
121    scores.sort_unstable_by(|a, b| {
122        b.neighbor_hits
123            .cmp(&a.neighbor_hits)
124            .then_with(|| a.item_id.cmp(&b.item_id))
125    });
126
127    scores
128}
129
130/// Per-item hubness score (archived copy — the live one is in hubness.rs).
131#[allow(dead_code)]
132#[derive(Debug, Clone, PartialEq)]
133pub struct HubnessScore {
134    pub item_id: String,
135    pub neighbor_hits: usize,
136    pub normalized_score: f32,
137}