1use std::borrow::Borrow;
21
22use crate::core::entangled::EntangledHVec;
23use crate::core::types::RetrievalResult;
24
25#[derive(Clone, Debug)]
27pub struct HopfieldConfig {
28 pub beta: f64,
31 pub alpha: f64,
36 pub max_iter: usize,
39}
40
41impl Default for HopfieldConfig {
42 fn default() -> Self {
43 Self {
44 beta: 100.0,
45 alpha: 2.0,
46 max_iter: 1,
47 }
48 }
49}
50
51pub fn hopfield_query<S: AsRef<str>, V: Borrow<EntangledHVec>>(
57 query: &EntangledHVec,
58 patterns: &[(S, V)],
59 config: &HopfieldConfig,
60 max_results: usize,
61) -> Vec<RetrievalResult> {
62 if max_results == 0 || patterns.is_empty() || config.beta <= 0.0 {
63 return Vec::new();
64 }
65
66 let scores: Vec<f64> = patterns
69 .iter()
70 .map(|(_, pat)| config.beta * query.similarity(pat.borrow()))
71 .collect();
72
73 let weights = entmax(&scores, config.alpha);
74
75 let mut results: Vec<RetrievalResult> = weights
76 .iter()
77 .enumerate()
78 .filter(|(_, &w)| w > 1e-12)
79 .map(|(i, &w)| RetrievalResult {
80 id: patterns[i].0.as_ref().to_owned(),
81 similarity: w,
82 })
83 .collect();
84
85 results.sort_unstable_by(|a, b| {
86 b.similarity
87 .partial_cmp(&a.similarity)
88 .unwrap_or(std::cmp::Ordering::Equal)
89 });
90 results.truncate(max_results);
91 results
92}
93
94pub fn hopfield_query_iterative<S: AsRef<str>, V: Borrow<EntangledHVec>>(
99 query: &EntangledHVec,
100 patterns: &[(S, V)],
101 config: &HopfieldConfig,
102 max_results: usize,
103) -> Vec<RetrievalResult> {
104 if max_results == 0 || patterns.is_empty() {
105 return Vec::new();
106 }
107 if config.max_iter <= 1 {
108 return hopfield_query(query, patterns, config, max_results);
109 }
110
111 let mut current_query = query.clone();
112 let mut scores = Vec::with_capacity(patterns.len());
113 for _ in 0..config.max_iter - 1 {
114 scores.clear();
115 scores.extend(
116 patterns
117 .iter()
118 .map(|(_, pat)| config.beta * current_query.similarity(pat.borrow())),
119 );
120
121 let weights = entmax(&scores, config.alpha);
122
123 current_query = weighted_reconstruct(patterns, &weights, query.dim);
125 if current_query.indices.is_empty() {
126 break;
127 }
128 }
129
130 drop(scores);
131 hopfield_query(¤t_query, patterns, config, max_results)
132}
133
134fn entmax(scores: &[f64], alpha: f64) -> Vec<f64> {
136 if alpha <= 1.0 + f64::EPSILON {
137 softmax(scores)
138 } else if (alpha - 1.5).abs() < f64::EPSILON {
139 entmax_15(scores)
140 } else {
141 sparsemax(scores)
142 }
143}
144
145fn sparsemax(scores: &[f64]) -> Vec<f64> {
156 let n = scores.len();
157 if n == 0 {
158 return Vec::new();
159 }
160 if n == 1 {
161 return vec![1.0];
162 }
163
164 let mut sorted_indices: Vec<usize> = (0..n).collect();
165 sorted_indices.sort_unstable_by(|&a, &b| {
166 scores[b]
167 .partial_cmp(&scores[a])
168 .unwrap_or(std::cmp::Ordering::Equal)
169 });
170
171 let mut cumsum = 0.0;
172 let mut support_size = 0;
173
174 for (j, &idx) in sorted_indices.iter().enumerate() {
175 cumsum += scores[idx];
176 if 1.0 + (j + 1) as f64 * scores[idx] > cumsum {
178 support_size = j + 1;
179 } else {
180 break;
181 }
182 }
183
184 if support_size == 0 {
185 let uniform = 1.0 / n as f64;
187 return vec![uniform; n];
188 }
189
190 let tau_sum: f64 = sorted_indices[..support_size]
191 .iter()
192 .map(|&i| scores[i])
193 .sum();
194 let tau = (tau_sum - 1.0) / support_size as f64;
195
196 scores.iter().map(|&s| (s - tau).max(0.0)).collect()
197}
198
199fn entmax_15(scores: &[f64]) -> Vec<f64> {
207 let n = scores.len();
208 if n == 0 {
209 return Vec::new();
210 }
211 if n == 1 {
212 return vec![1.0];
213 }
214
215 let max_score = scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
216
217 let mut lo = max_score - 1.0;
219 let mut hi = max_score;
220
221 while sum_clipped_sq(scores, lo) < 1.0 {
222 lo -= 1.0;
223 }
224
225 for _ in 0..64 {
226 let mid = (lo + hi) / 2.0;
227 let s = sum_clipped_sq(scores, mid);
228 if s > 1.0 {
229 lo = mid;
230 } else {
231 hi = mid;
232 }
233 }
234
235 let tau = (lo + hi) / 2.0;
236 scores.iter().map(|&s| (s - tau).max(0.0).powi(2)).collect()
237}
238
239fn sum_clipped_sq(scores: &[f64], tau: f64) -> f64 {
240 scores.iter().map(|&s| (s - tau).max(0.0).powi(2)).sum()
241}
242
243fn softmax(scores: &[f64]) -> Vec<f64> {
245 if scores.is_empty() {
246 return Vec::new();
247 }
248 let max = scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
249 let exps: Vec<f64> = scores.iter().map(|&s| (s - max).exp()).collect();
250 let sum: f64 = exps.iter().sum();
251 if sum == 0.0 {
252 let uniform = 1.0 / scores.len() as f64;
253 return vec![uniform; scores.len()];
254 }
255 exps.iter().map(|&e| e / sum).collect()
256}
257
258fn weighted_reconstruct<S, V: Borrow<EntangledHVec>>(
264 patterns: &[(S, V)],
265 weights: &[f64],
266 dim: usize,
267) -> EntangledHVec {
268 use crate::core::entangled::DEFAULT_RHO_DENOM;
269
270 let target = (dim / DEFAULT_RHO_DENOM).max(1);
271 let mut freq: crate::core::hash::FastMap<u32, f64> =
272 crate::core::hash::FastMap::with_capacity_and_hasher(target * 2, Default::default());
273
274 for (i, (_, pat)) in patterns.iter().enumerate() {
275 let w = weights[i];
276 if w < 1e-12 {
277 continue;
278 }
279 for &idx in &pat.borrow().indices {
280 *freq.entry(idx).or_insert(0.0) += w;
281 }
282 }
283
284 let mut scored: Vec<(u32, f64)> = freq.into_iter().collect();
285 if scored.len() > target {
286 scored.select_nth_unstable_by(target - 1, |a, b| {
287 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
288 });
289 scored.truncate(target);
290 }
291
292 let mut indices: Vec<u32> = scored.into_iter().map(|(idx, _)| idx).collect();
293 indices.sort_unstable();
294 EntangledHVec::from_indices(indices, dim)
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn make_pattern(dim: usize, seed: u64) -> EntangledHVec {
302 EntangledHVec::new_deterministic(dim, seed)
303 }
304
305 type Transform = fn(&[f64]) -> Vec<f64>;
306 const TRANSFORMS: [(&str, Transform); 3] = [
307 ("softmax", softmax),
308 ("entmax_15", entmax_15),
309 ("sparsemax", sparsemax),
310 ];
311
312 #[test]
313 fn entmax_family_outputs_probability_distributions() {
314 let inputs: [&[f64]; 4] = [
315 &[5.0],
316 &[3.0, 3.0, 3.0],
317 &[10.0, 1.0, 1.0, 1.0],
318 &[5.0, 3.0, 1.0, 0.5, 0.1],
319 ];
320 for (name, transform) in TRANSFORMS {
321 for scores in inputs {
322 let weights = transform(scores);
323 assert_eq!(weights.len(), scores.len(), "{name} {scores:?}");
324 assert!(weights.iter().all(|&w| w >= 0.0), "{name} {scores:?}");
325 let sum: f64 = weights.iter().sum();
326 assert!((sum - 1.0).abs() < 1e-6, "{name} {scores:?} sums to {sum}");
327 if scores.windows(2).all(|pair| pair[0] == pair[1]) {
328 let uniform = 1.0 / scores.len() as f64;
329 assert!(
330 weights.iter().all(|&w| (w - uniform).abs() < 1e-6),
331 "{name} {scores:?} is not uniform: {weights:?}"
332 );
333 }
334 }
335 }
336 }
337
338 #[test]
339 fn only_softmax_keeps_dominated_scores_nonzero() {
340 let scores = [10.0, 1.0, 1.0, 1.0];
341 for ((name, transform), expected_support) in TRANSFORMS.into_iter().zip([4, 1, 1]) {
342 let support = transform(&scores).iter().filter(|&&w| w > 1e-10).count();
343 assert_eq!(support, expected_support, "{name}");
344 }
345 }
346
347 #[test]
348 fn test_hopfield_exact_retrieval() {
349 let dim = 16384;
350 let p1 = make_pattern(dim, 100);
351 let p2 = make_pattern(dim, 200);
352 let p3 = make_pattern(dim, 300);
353
354 let patterns = vec![
355 ("p1".to_string(), p1.clone()),
356 ("p2".to_string(), p2.clone()),
357 ("p3".to_string(), p3),
358 ];
359
360 let config = HopfieldConfig {
361 beta: 100.0,
362 alpha: 2.0,
363 max_iter: 1,
364 };
365
366 let results = hopfield_query(&p1, &patterns, &config, 10);
367 assert!(!results.is_empty(), "Should retrieve at least one pattern");
368 assert_eq!(results[0].id, "p1", "Should retrieve the exact pattern");
369
370 let results = hopfield_query(&p2, &patterns, &config, 10);
371 assert!(!results.is_empty());
372 assert_eq!(results[0].id, "p2");
373 }
374
375 #[test]
376 fn test_hopfield_sparse_output() {
377 let dim = 16384;
378 let patterns: Vec<(String, EntangledHVec)> = (0..20)
379 .map(|i| (format!("p{}", i), make_pattern(dim, i * 100 + 1)))
380 .collect();
381
382 let config = HopfieldConfig {
383 beta: 100.0,
384 alpha: 2.0,
385 max_iter: 1,
386 };
387
388 let query = patterns[5].1.clone();
389 let results = hopfield_query(&query, &patterns, &config, 20);
390
391 assert!(
392 results.len() < 10,
393 "Sparsemax should produce sparse output, got {} results",
394 results.len()
395 );
396 }
397
398 #[test]
399 fn test_hopfield_iterative_convergence() {
400 let dim = 16384;
401 let p1 = make_pattern(dim, 100);
402 let p2 = make_pattern(dim, 200);
403
404 let noise = make_pattern(dim, 999);
405 let noisy_query = p1.bind(&noise);
406
407 let patterns = vec![("p1".to_string(), p1.clone()), ("p2".to_string(), p2)];
408
409 let config = HopfieldConfig {
410 beta: 100.0,
411 alpha: 2.0,
412 max_iter: 3,
413 };
414
415 let results = hopfield_query_iterative(&noisy_query, &patterns, &config, 10);
416 assert_eq!(results[0].id, "p1");
417 }
418}