1use std::collections::HashMap;
12use std::fs;
13use std::path::Path;
14
15use crate::hebbian::{ActivationFingerprint, HebbianState};
16
17const RESONANCE_THRESHOLD: f32 = 0.3;
21const MAX_ECHOES: usize = 500;
23const ECHO_DECAY: f32 = 0.95;
25
26#[derive(Clone, Debug)]
30pub struct ResonanceEcho {
31 pub timestamp_ms: u64,
33 pub trigger_hash: u64,
35 pub echo_hash: u64,
37 pub similarity: f32,
39 pub shared_blocks: Vec<u32>,
41}
42
43#[derive(Clone, Debug, Default)]
45pub struct BlockResonance {
46 pub echo_count: u32,
48 pub strength: f32,
50}
51
52#[derive(Clone)]
54pub struct MirrorState {
55 pub echoes: Vec<ResonanceEcho>,
56 pub block_resonance: HashMap<u32, BlockResonance>,
57}
58
59impl MirrorState {
60 pub fn load_or_init(output_dir: &Path) -> Self {
62 load_mirror_state(output_dir).unwrap_or_else(|| Self {
63 echoes: Vec::new(),
64 block_resonance: HashMap::new(),
65 })
66 }
67
68 pub fn detect_resonance(
71 &mut self,
72 new_activations: &[(u32, f32)],
73 new_hash: u64,
74 fingerprints: &[ActivationFingerprint],
75 ) -> HashMap<u32, f32> {
76 let mut boosts: HashMap<u32, f32> = HashMap::new();
77
78 if new_activations.is_empty() || fingerprints.is_empty() {
79 return boosts;
80 }
81
82 let new_vec = activation_to_sparse(new_activations);
84
85 for fp in fingerprints {
87 if fp.query_hash == new_hash {
88 continue;
89 }
90
91 let past_vec = activation_to_sparse(&fp.activations);
92 let sim = sparse_cosine(&new_vec, &past_vec);
93
94 if sim >= RESONANCE_THRESHOLD {
95 let shared: Vec<u32> = new_vec
97 .keys()
98 .filter(|k| past_vec.contains_key(k))
99 .copied()
100 .collect();
101
102 for &block_idx in &shared {
104 let entry = boosts.entry(block_idx).or_insert(0.0);
105 *entry += sim * 0.1; let res = self.block_resonance.entry(block_idx).or_default();
108 res.echo_count += 1;
109 res.strength += sim;
110 }
111
112 let now_ms = crate::hebbian::now_epoch_ms_pub();
114 self.echoes.push(ResonanceEcho {
115 timestamp_ms: now_ms,
116 trigger_hash: new_hash,
117 echo_hash: fp.query_hash,
118 similarity: sim,
119 shared_blocks: shared,
120 });
121 }
122 }
123
124 if self.echoes.len() > MAX_ECHOES {
126 self.echoes.drain(0..self.echoes.len() - MAX_ECHOES);
127 }
128
129 boosts
130 }
131
132 pub fn decay(&mut self) {
134 self.block_resonance.retain(|_, res| {
135 res.strength *= ECHO_DECAY;
136 res.strength > 0.01
137 });
138 }
139
140 pub fn boost_for(&self, block_idx: u32) -> f32 {
142 self.block_resonance
143 .get(&block_idx)
144 .map(|r| r.strength.min(0.5)) .unwrap_or(0.0)
146 }
147
148 pub fn stats(&self) -> MirrorStats {
150 let total_echoes = self.echoes.len();
151 let resonant_blocks = self.block_resonance.len();
152 let avg_similarity = if self.echoes.is_empty() {
153 0.0
154 } else {
155 self.echoes.iter().map(|e| e.similarity).sum::<f32>() / self.echoes.len() as f32
156 };
157 let strongest = self
158 .block_resonance
159 .iter()
160 .max_by(|a, b| a.1.strength.total_cmp(&b.1.strength))
161 .map(|(&idx, res)| (idx, res.strength));
162
163 MirrorStats {
164 total_echoes,
165 resonant_blocks,
166 avg_similarity,
167 strongest_block: strongest,
168 }
169 }
170
171 pub fn most_resonant(&self, n: usize) -> Vec<(u32, &BlockResonance)> {
173 let mut blocks: Vec<(u32, &BlockResonance)> =
174 self.block_resonance.iter().map(|(&k, v)| (k, v)).collect();
175 blocks.sort_by(|a, b| b.1.strength.total_cmp(&a.1.strength));
176 blocks.truncate(n);
177 blocks
178 }
179
180 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
182 save_mirror_state(output_dir, self)
183 }
184
185 pub fn remap_indexes(&mut self, old_to_new: &[u32]) {
187 let mut new_resonance = HashMap::new();
188 for (old_idx, block_res) in self.block_resonance.drain() {
189 if (old_idx as usize) < old_to_new.len() {
190 let new_idx = old_to_new[old_idx as usize];
191 if new_idx != u32::MAX {
192 new_resonance.insert(new_idx, block_res);
193 }
194 }
195 }
196 self.block_resonance = new_resonance;
197
198 for echo in &mut self.echoes {
200 echo.shared_blocks = echo
201 .shared_blocks
202 .iter()
203 .filter_map(|&idx| {
204 if (idx as usize) < old_to_new.len() {
205 let new = old_to_new[idx as usize];
206 if new != u32::MAX {
207 Some(new)
208 } else {
209 None
210 }
211 } else {
212 None
213 }
214 })
215 .collect();
216 }
217 self.echoes.retain(|e| !e.shared_blocks.is_empty());
218 }
219}
220
221pub struct MirrorStats {
222 pub total_echoes: usize,
223 pub resonant_blocks: usize,
224 pub avg_similarity: f32,
225 pub strongest_block: Option<(u32, f32)>,
226}
227
228fn activation_to_sparse(activations: &[(u32, f32)]) -> HashMap<u32, f32> {
231 activations.iter().copied().collect()
232}
233
234fn sparse_cosine(a: &HashMap<u32, f32>, b: &HashMap<u32, f32>) -> f32 {
235 let (smaller, larger) = if a.len() <= b.len() { (a, b) } else { (b, a) };
236
237 let mut dot = 0.0f32;
238 for (k, va) in smaller {
239 if let Some(vb) = larger.get(k) {
240 dot += va * vb;
241 }
242 }
243
244 let norm_a: f32 = a.values().map(|v| v * v).sum::<f32>().sqrt();
245 let norm_b: f32 = b.values().map(|v| v * v).sum::<f32>().sqrt();
246
247 if norm_a < 1e-9 || norm_b < 1e-9 {
248 return 0.0;
249 }
250
251 dot / (norm_a * norm_b)
252}
253
254fn load_mirror_state(output_dir: &Path) -> Option<MirrorState> {
264 let path = output_dir.join("resonance.bin");
265 let data = fs::read(&path).ok()?;
266 if data.len() < 12 || &data[0..4] != b"RES1" {
267 return None;
268 }
269
270 let echo_count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
271 let block_count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
272
273 let mut pos = 12;
274 let mut echoes = Vec::with_capacity(echo_count);
275 for _ in 0..echo_count {
276 if pos + 28 > data.len() {
277 break;
278 }
279 let timestamp_ms = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
280 let trigger_hash = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
281 let echo_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
282 let similarity = f32::from_le_bytes(data[pos + 24..pos + 28].try_into().unwrap());
283 let shared_count =
284 u16::from_le_bytes(data[pos + 28..pos + 30].try_into().unwrap()) as usize;
285 pos += 30;
286
287 let mut shared_blocks = Vec::with_capacity(shared_count);
288 for _ in 0..shared_count {
289 if pos + 4 > data.len() {
290 break;
291 }
292 shared_blocks.push(u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()));
293 pos += 4;
294 }
295
296 echoes.push(ResonanceEcho {
297 timestamp_ms,
298 trigger_hash,
299 echo_hash,
300 similarity,
301 shared_blocks,
302 });
303 }
304
305 let mut block_resonance = HashMap::with_capacity(block_count);
306 for _ in 0..block_count {
307 if pos + 12 > data.len() {
308 break;
309 }
310 let idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
311 let echo_count = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
312 let strength = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
313 pos += 12;
314 block_resonance.insert(
315 idx,
316 BlockResonance {
317 echo_count,
318 strength,
319 },
320 );
321 }
322
323 Some(MirrorState {
324 echoes,
325 block_resonance,
326 })
327}
328
329fn save_mirror_state(output_dir: &Path, state: &MirrorState) -> Result<(), String> {
330 let path = output_dir.join("resonance.bin");
331 let tmp_path = output_dir.join("resonance.bin.tmp");
332 let mut buf = Vec::new();
333
334 buf.extend_from_slice(b"RES1");
336 buf.extend_from_slice(&(state.echoes.len() as u32).to_le_bytes());
337 buf.extend_from_slice(&(state.block_resonance.len() as u32).to_le_bytes());
338
339 for echo in &state.echoes {
341 buf.extend_from_slice(&echo.timestamp_ms.to_le_bytes());
342 buf.extend_from_slice(&echo.trigger_hash.to_le_bytes());
343 buf.extend_from_slice(&echo.echo_hash.to_le_bytes());
344 buf.extend_from_slice(&echo.similarity.to_le_bytes());
345 buf.extend_from_slice(&(echo.shared_blocks.len() as u16).to_le_bytes());
346 for &block_idx in &echo.shared_blocks {
347 buf.extend_from_slice(&block_idx.to_le_bytes());
348 }
349 }
350
351 for (&idx, res) in &state.block_resonance {
353 buf.extend_from_slice(&idx.to_le_bytes());
354 buf.extend_from_slice(&res.echo_count.to_le_bytes());
355 buf.extend_from_slice(&res.strength.to_le_bytes());
356 }
357
358 fs::write(&tmp_path, &buf).map_err(|e| format!("write resonance.bin: {}", e))?;
359 fs::rename(&tmp_path, &path).map_err(|e| format!("rename resonance.bin: {}", e))
360}
361
362pub fn mirror_boost(
367 hebb: &HebbianState,
368 mirror: &mut MirrorState,
369 new_activations: &[(u32, f32)],
370 query_hash: u64,
371) -> Vec<(u32, f32)> {
372 let boosts = mirror.detect_resonance(new_activations, query_hash, &hebb.fingerprints);
373
374 let mut result: Vec<(u32, f32)> = boosts.into_iter().collect();
376 result.sort_by(|a, b| b.1.total_cmp(&a.1));
377 result
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn test_sparse_cosine_identical() {
386 let a: HashMap<u32, f32> = [(0, 1.0), (1, 0.5), (2, 0.3)].into();
387 let sim = sparse_cosine(&a, &a);
388 assert!((sim - 1.0).abs() < 0.001);
389 }
390
391 #[test]
392 fn test_sparse_cosine_orthogonal() {
393 let a: HashMap<u32, f32> = [(0, 1.0), (1, 0.5)].into();
394 let b: HashMap<u32, f32> = [(2, 1.0), (3, 0.5)].into();
395 let sim = sparse_cosine(&a, &b);
396 assert!(sim.abs() < 0.001);
397 }
398
399 #[test]
400 fn test_sparse_cosine_partial_overlap() {
401 let a: HashMap<u32, f32> = [(0, 1.0), (1, 0.5), (2, 0.3)].into();
402 let b: HashMap<u32, f32> = [(1, 0.8), (2, 0.6), (3, 0.4)].into();
403 let sim = sparse_cosine(&a, &b);
404 assert!(sim > 0.0);
405 assert!(sim < 1.0);
406 }
407
408 #[test]
409 fn test_detect_resonance_no_fingerprints() {
410 let mut mirror = MirrorState {
411 echoes: Vec::new(),
412 block_resonance: HashMap::new(),
413 };
414 let boosts = mirror.detect_resonance(&[(0, 1.0), (1, 0.5)], 42, &[]);
415 assert!(boosts.is_empty());
416 }
417
418 #[test]
419 fn test_detect_resonance_with_match() {
420 let mut mirror = MirrorState {
421 echoes: Vec::new(),
422 block_resonance: HashMap::new(),
423 };
424
425 let past = ActivationFingerprint {
427 timestamp_ms: 1000,
428 query_hash: 100,
429 activations: vec![(0, 0.9), (1, 0.7), (2, 0.5)],
430 };
431
432 let new_act = vec![(0, 0.8), (1, 0.6), (3, 0.4)];
434 let boosts = mirror.detect_resonance(&new_act, 200, &[past]);
435
436 assert!(!boosts.is_empty());
438 assert!(boosts.contains_key(&0));
439 assert!(boosts.contains_key(&1));
440 assert!(!boosts.contains_key(&3)); assert_eq!(mirror.echoes.len(), 1);
444 assert_eq!(mirror.echoes[0].trigger_hash, 200);
445 assert_eq!(mirror.echoes[0].echo_hash, 100);
446 }
447
448 #[test]
449 fn test_decay() {
450 let mut mirror = MirrorState {
451 echoes: Vec::new(),
452 block_resonance: HashMap::new(),
453 };
454
455 mirror.block_resonance.insert(
456 0,
457 BlockResonance {
458 echo_count: 5,
459 strength: 1.0,
460 },
461 );
462 mirror.block_resonance.insert(
463 1,
464 BlockResonance {
465 echo_count: 1,
466 strength: 0.005,
467 },
468 );
469
470 mirror.decay();
471
472 assert!(mirror.block_resonance.contains_key(&0));
474 assert!(!mirror.block_resonance.contains_key(&1));
476 }
477
478 #[test]
479 fn test_save_load_roundtrip() {
480 let tmp = tempfile::tempdir().expect("create temp dir");
481 let dir = tmp.path();
482
483 let mut mirror = MirrorState {
484 echoes: Vec::new(),
485 block_resonance: HashMap::new(),
486 };
487
488 mirror.echoes.push(ResonanceEcho {
489 timestamp_ms: 12345,
490 trigger_hash: 100,
491 echo_hash: 200,
492 similarity: 0.75,
493 shared_blocks: vec![1, 5, 10],
494 });
495 mirror.block_resonance.insert(
496 1,
497 BlockResonance {
498 echo_count: 3,
499 strength: 0.8,
500 },
501 );
502 mirror.block_resonance.insert(
503 5,
504 BlockResonance {
505 echo_count: 1,
506 strength: 0.3,
507 },
508 );
509
510 mirror.save(dir).expect("save");
511
512 let loaded = MirrorState::load_or_init(dir);
513 assert_eq!(loaded.echoes.len(), 1);
514 assert_eq!(loaded.echoes[0].trigger_hash, 100);
515 assert_eq!(loaded.echoes[0].shared_blocks, vec![1, 5, 10]);
516 assert_eq!(loaded.block_resonance.len(), 2);
517 assert_eq!(loaded.block_resonance[&1].echo_count, 3);
518 assert!((loaded.block_resonance[&5].strength - 0.3).abs() < 0.001);
519 }
520
521 #[test]
522 fn test_most_resonant() {
523 let mut mirror = MirrorState {
524 echoes: Vec::new(),
525 block_resonance: HashMap::new(),
526 };
527
528 mirror.block_resonance.insert(
529 0,
530 BlockResonance {
531 echo_count: 1,
532 strength: 0.1,
533 },
534 );
535 mirror.block_resonance.insert(
536 1,
537 BlockResonance {
538 echo_count: 5,
539 strength: 0.9,
540 },
541 );
542 mirror.block_resonance.insert(
543 2,
544 BlockResonance {
545 echo_count: 3,
546 strength: 0.5,
547 },
548 );
549
550 let top = mirror.most_resonant(2);
551 assert_eq!(top.len(), 2);
552 assert_eq!(top[0].0, 1); assert_eq!(top[1].0, 2);
554 }
555
556 #[test]
557 fn test_mirror_boost_integration() {
558 let hebb = HebbianState {
559 activations: vec![crate::hebbian::ActivationRecord::default(); 5],
560 coactivations: HashMap::new(),
561 fingerprints: vec![ActivationFingerprint {
562 timestamp_ms: 1000,
563 query_hash: 100,
564 activations: vec![(0, 0.9), (1, 0.7), (2, 0.5)],
565 }],
566 };
567
568 let mut mirror = MirrorState {
569 echoes: Vec::new(),
570 block_resonance: HashMap::new(),
571 };
572
573 let new_act = vec![(0, 0.8), (1, 0.6), (3, 0.4)];
574 let result = mirror_boost(&hebb, &mut mirror, &new_act, 200);
575
576 assert!(!result.is_empty());
578 }
579}