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}