1use crate::core::entangled::EntangledHVec;
21use crate::core::types::RetrievalResult;
22
23#[derive(Clone, Debug)]
25pub struct HopfieldConfig {
26 pub beta: f64,
29 pub alpha: f64,
34 pub max_iter: usize,
37}
38
39impl Default for HopfieldConfig {
40 fn default() -> Self {
41 Self {
42 beta: 100.0,
43 alpha: 2.0,
44 max_iter: 1,
45 }
46 }
47}
48
49pub fn hopfield_query(
55 query: &EntangledHVec,
56 patterns: &[(String, EntangledHVec)],
57 config: &HopfieldConfig,
58 max_results: usize,
59) -> Vec<RetrievalResult> {
60 if patterns.is_empty() || config.beta <= 0.0 {
61 return Vec::new();
62 }
63
64 let scores: Vec<f64> = patterns
68 .iter()
69 .map(|(_, pat)| config.beta * query.similarity(pat))
70 .collect();
71
72 let weights = if config.alpha <= 1.0 + f64::EPSILON {
74 softmax(&scores)
75 } else if (config.alpha - 1.5).abs() < f64::EPSILON {
76 entmax_15(&scores)
77 } else {
78 sparsemax(&scores)
79 };
80
81 let mut results: Vec<RetrievalResult> = weights
83 .iter()
84 .enumerate()
85 .filter(|(_, &w)| w > 1e-12)
86 .map(|(i, &w)| RetrievalResult {
87 id: patterns[i].0.clone(),
88 similarity: w,
89 })
90 .collect();
91
92 results.sort_unstable_by(|a, b| {
93 b.similarity
94 .partial_cmp(&a.similarity)
95 .unwrap_or(std::cmp::Ordering::Equal)
96 });
97 results.truncate(max_results);
98 results
99}
100
101pub fn hopfield_query_iterative(
106 query: &EntangledHVec,
107 patterns: &[(String, EntangledHVec)],
108 config: &HopfieldConfig,
109 max_results: usize,
110) -> Vec<RetrievalResult> {
111 if config.max_iter <= 1 {
112 return hopfield_query(query, patterns, config, max_results);
113 }
114
115 let mut current_query = query.clone();
116
117 for _ in 0..config.max_iter - 1 {
118 let scores: Vec<f64> = patterns
119 .iter()
120 .map(|(_, pat)| config.beta * current_query.similarity(pat))
121 .collect();
122
123 let weights = if config.alpha <= 1.0 + f64::EPSILON {
124 softmax(&scores)
125 } else if (config.alpha - 1.5).abs() < f64::EPSILON {
126 entmax_15(&scores)
127 } else {
128 sparsemax(&scores)
129 };
130
131 current_query = weighted_reconstruct(patterns, &weights, query.dim);
133 if current_query.indices.is_empty() {
134 break;
135 }
136 }
137
138 hopfield_query(¤t_query, patterns, config, max_results)
139}
140
141fn sparsemax(scores: &[f64]) -> Vec<f64> {
152 let n = scores.len();
153 if n == 0 {
154 return Vec::new();
155 }
156 if n == 1 {
157 return vec![1.0];
158 }
159
160 let mut sorted_indices: Vec<usize> = (0..n).collect();
162 sorted_indices.sort_unstable_by(|&a, &b| {
163 scores[b]
164 .partial_cmp(&scores[a])
165 .unwrap_or(std::cmp::Ordering::Equal)
166 });
167
168 let mut cumsum = 0.0;
169 let mut support_size = 0;
170
171 for (j, &idx) in sorted_indices.iter().enumerate() {
172 cumsum += scores[idx];
173 if 1.0 + (j + 1) as f64 * scores[idx] > cumsum {
175 support_size = j + 1;
176 } else {
177 break;
178 }
179 }
180
181 if support_size == 0 {
182 let uniform = 1.0 / n as f64;
184 return vec![uniform; n];
185 }
186
187 let tau_sum: f64 = sorted_indices[..support_size]
189 .iter()
190 .map(|&i| scores[i])
191 .sum();
192 let tau = (tau_sum - 1.0) / support_size as f64;
193
194 scores.iter().map(|&s| (s - tau).max(0.0)).collect()
195}
196
197fn entmax_15(scores: &[f64]) -> Vec<f64> {
205 let n = scores.len();
206 if n == 0 {
207 return Vec::new();
208 }
209 if n == 1 {
210 return vec![1.0];
211 }
212
213 let max_score = scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
214
215 let mut lo = max_score - 1.0;
217 let mut hi = max_score;
218
219 while sum_clipped_sq(scores, lo) < 1.0 {
221 lo -= 1.0;
222 }
223
224 for _ in 0..64 {
225 let mid = (lo + hi) / 2.0;
226 let s = sum_clipped_sq(scores, mid);
227 if s > 1.0 {
228 lo = mid;
229 } else {
230 hi = mid;
231 }
232 }
233
234 let tau = (lo + hi) / 2.0;
235 scores.iter().map(|&s| (s - tau).max(0.0).powi(2)).collect()
236}
237
238fn sum_clipped_sq(scores: &[f64], tau: f64) -> f64 {
239 scores.iter().map(|&s| (s - tau).max(0.0).powi(2)).sum()
240}
241
242fn softmax(scores: &[f64]) -> Vec<f64> {
244 if scores.is_empty() {
245 return Vec::new();
246 }
247 let max = scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
248 let exps: Vec<f64> = scores.iter().map(|&s| (s - max).exp()).collect();
249 let sum: f64 = exps.iter().sum();
250 if sum == 0.0 {
251 let uniform = 1.0 / scores.len() as f64;
252 return vec![uniform; scores.len()];
253 }
254 exps.iter().map(|&e| e / sum).collect()
255}
256
257fn weighted_reconstruct(
263 patterns: &[(String, EntangledHVec)],
264 weights: &[f64],
265 dim: usize,
266) -> EntangledHVec {
267 use crate::core::entangled::DEFAULT_RHO_DENOM;
268
269 let target = (dim / DEFAULT_RHO_DENOM).max(1);
270 let mut freq: fxhash::FxHashMap<u32, f64> =
271 fxhash::FxHashMap::with_capacity_and_hasher(target * 2, Default::default());
272
273 for (i, (_, pat)) in patterns.iter().enumerate() {
274 let w = weights[i];
275 if w < 1e-12 {
276 continue;
277 }
278 for &idx in &pat.indices {
279 *freq.entry(idx).or_insert(0.0) += w;
280 }
281 }
282
283 let mut scored: Vec<(u32, f64)> = freq.into_iter().collect();
284 if scored.len() > target {
285 scored.select_nth_unstable_by(target - 1, |a, b| {
286 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
287 });
288 scored.truncate(target);
289 }
290
291 let mut indices: Vec<u32> = scored.into_iter().map(|(idx, _)| idx).collect();
292 indices.sort_unstable();
293 EntangledHVec::from_indices(indices, dim)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn make_pattern(dim: usize, seed: u64) -> EntangledHVec {
301 EntangledHVec::new_deterministic(dim, seed)
302 }
303
304 #[test]
305 fn test_sparsemax_single() {
306 let result = sparsemax(&[5.0]);
307 assert_eq!(result.len(), 1);
308 assert!((result[0] - 1.0).abs() < 1e-10);
309 }
310
311 #[test]
312 fn test_sparsemax_sparsity() {
313 let result = sparsemax(&[10.0, 1.0, 1.0, 1.0]);
315 assert!(result[0] > 0.9, "Dominant score should get most weight");
316 let nonzero = result.iter().filter(|&&w| w > 1e-10).count();
317 assert!(nonzero < 4, "Sparsemax should produce zeros");
318 }
319
320 #[test]
321 fn test_sparsemax_uniform() {
322 let result = sparsemax(&[3.0, 3.0, 3.0]);
324 for &w in &result {
325 assert!(
326 (w - 1.0 / 3.0).abs() < 1e-10,
327 "Equal scores should give uniform weights"
328 );
329 }
330 }
331
332 #[test]
333 fn test_sparsemax_sums_to_one() {
334 let result = sparsemax(&[5.0, 3.0, 1.0, 0.5, 0.1]);
335 let sum: f64 = result.iter().sum();
336 assert!(
337 (sum - 1.0).abs() < 1e-8,
338 "Sparsemax output should sum to 1, got {}",
339 sum
340 );
341 }
342
343 #[test]
344 fn test_entmax_15_sparsity() {
345 let result = entmax_15(&[10.0, 1.0, 1.0, 1.0]);
346 let nonzero = result.iter().filter(|&&w| w > 1e-10).count();
347 assert!(nonzero <= 3, "1.5-entmax should produce some zeros");
348 let sum: f64 = result.iter().sum();
349 assert!(
350 (sum - 1.0).abs() < 1e-6,
351 "1.5-entmax should sum to 1, got {}",
352 sum
353 );
354 }
355
356 #[test]
357 fn test_softmax_no_zeros() {
358 let result = softmax(&[10.0, 1.0, 1.0, 1.0]);
359 for &w in &result {
360 assert!(w > 0.0, "Softmax should never produce exact zeros");
361 }
362 let sum: f64 = result.iter().sum();
363 assert!((sum - 1.0).abs() < 1e-10);
364 }
365
366 #[test]
367 fn test_hopfield_exact_retrieval() {
368 let dim = 16384;
369 let p1 = make_pattern(dim, 100);
370 let p2 = make_pattern(dim, 200);
371 let p3 = make_pattern(dim, 300);
372
373 let patterns = vec![
374 ("p1".to_string(), p1.clone()),
375 ("p2".to_string(), p2.clone()),
376 ("p3".to_string(), p3),
377 ];
378
379 let config = HopfieldConfig {
380 beta: 100.0,
381 alpha: 2.0,
382 max_iter: 1,
383 };
384
385 let results = hopfield_query(&p1, &patterns, &config, 10);
387 assert!(!results.is_empty(), "Should retrieve at least one pattern");
388 assert_eq!(results[0].id, "p1", "Should retrieve the exact pattern");
389
390 let results = hopfield_query(&p2, &patterns, &config, 10);
392 assert!(!results.is_empty());
393 assert_eq!(results[0].id, "p2");
394 }
395
396 #[test]
397 fn test_hopfield_sparse_output() {
398 let dim = 16384;
399 let patterns: Vec<(String, EntangledHVec)> = (0..20)
400 .map(|i| (format!("p{}", i), make_pattern(dim, i * 100 + 1)))
401 .collect();
402
403 let config = HopfieldConfig {
404 beta: 100.0,
405 alpha: 2.0,
406 max_iter: 1,
407 };
408
409 let query = patterns[5].1.clone();
410 let results = hopfield_query(&query, &patterns, &config, 20);
411
412 assert!(
414 results.len() < 10,
415 "Sparsemax should produce sparse output, got {} results",
416 results.len()
417 );
418 }
419
420 #[test]
421 fn test_hopfield_iterative_convergence() {
422 let dim = 16384;
423 let p1 = make_pattern(dim, 100);
424 let p2 = make_pattern(dim, 200);
425
426 let noise = make_pattern(dim, 999);
428 let noisy_query = p1.bind(&noise);
429
430 let patterns = vec![("p1".to_string(), p1.clone()), ("p2".to_string(), p2)];
431
432 let config = HopfieldConfig {
433 beta: 100.0,
434 alpha: 2.0,
435 max_iter: 3,
436 };
437
438 let results = hopfield_query_iterative(&noisy_query, &patterns, &config, 10);
439 assert!(!results.is_empty(), "Iterative query should return results");
441 }
442}