Skip to main content

microscope_memory/
mirror.rs

1//! Mirror neuron layer for Microscope Memory.
2//!
3//! "When one pattern fires, similar past patterns resonate."
4//!
5//! Compares incoming activation fingerprints against stored ones.
6//! High-resonance matches boost recall — the system "recognizes" familiar
7//! activation shapes and strengthens related pathways.
8//!
9//! Binary format: resonance.bin (RES1)
10
11use std::collections::HashMap;
12use std::fs;
13use std::path::Path;
14
15use crate::hebbian::{ActivationFingerprint, HebbianState};
16
17// ─── Constants ──────────────────────────────────────
18
19/// Minimum cosine similarity to count as resonance.
20const RESONANCE_THRESHOLD: f32 = 0.3;
21/// Maximum number of resonance echoes to store.
22const MAX_ECHOES: usize = 500;
23/// Decay factor for older echoes (per-step multiplier).
24const ECHO_DECAY: f32 = 0.95;
25
26// ─── Types ──────────────────────────────────────────
27
28/// A resonance echo — records when a new query matched a past fingerprint.
29#[derive(Clone, Debug)]
30pub struct ResonanceEcho {
31    /// Timestamp of the resonance event.
32    pub timestamp_ms: u64,
33    /// The new query's fingerprint hash.
34    pub trigger_hash: u64,
35    /// The matched past fingerprint hash.
36    pub echo_hash: u64,
37    /// Cosine similarity between the two activation patterns.
38    pub similarity: f32,
39    /// Which blocks were in the intersection (amplified blocks).
40    pub shared_blocks: Vec<u32>,
41}
42
43/// Per-block resonance accumulator.
44#[derive(Clone, Debug, Default)]
45pub struct BlockResonance {
46    /// How many times this block appeared in resonance echoes.
47    pub echo_count: u32,
48    /// Accumulated resonance strength (decays over time).
49    pub strength: f32,
50}
51
52/// Mirror neuron state — loaded from resonance.bin.
53#[derive(Clone)]
54pub struct MirrorState {
55    pub echoes: Vec<ResonanceEcho>,
56    pub block_resonance: HashMap<u32, BlockResonance>,
57}
58
59impl MirrorState {
60    /// Load or initialize mirror state.
61    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    /// Detect resonance between a new activation and all stored fingerprints.
69    /// Returns the resonance boost per block (block_idx → boost_score).
70    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        // Build sparse vector for the new activation
83        let new_vec = activation_to_sparse(new_activations);
84
85        // Compare against all stored fingerprints (skip self — same hash)
86        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                // Find shared blocks
96                let shared: Vec<u32> = new_vec
97                    .keys()
98                    .filter(|k| past_vec.contains_key(k))
99                    .copied()
100                    .collect();
101
102                // Boost shared blocks proportional to similarity
103                for &block_idx in &shared {
104                    let entry = boosts.entry(block_idx).or_insert(0.0);
105                    *entry += sim * 0.1; // resonance boost factor
106
107                    let res = self.block_resonance.entry(block_idx).or_default();
108                    res.echo_count += 1;
109                    res.strength += sim;
110                }
111
112                // Record the echo
113                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        // Trim echoes
125        if self.echoes.len() > MAX_ECHOES {
126            self.echoes.drain(0..self.echoes.len() - MAX_ECHOES);
127        }
128
129        boosts
130    }
131
132    /// Decay all block resonance strengths (call periodically).
133    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    /// Get resonance boost for a specific block.
141    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)) // cap boost at 0.5
145            .unwrap_or(0.0)
146    }
147
148    /// Get statistics.
149    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    /// Get top-N most resonant blocks.
172    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    /// Save mirror state to disk.
181    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
182        save_mirror_state(output_dir, self)
183    }
184
185    /// Remap block indexes after rebuild.
186    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        // Remap echo shared_blocks
199        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
228// ─── Sparse vector operations ───────────────────────
229
230fn 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
254// ─── Binary I/O ─────────────────────────────────────
255//
256// resonance.bin format:
257//   magic: b"RES1" (4 bytes)
258//   echo_count: u32 (4 bytes)
259//   block_count: u32 (4 bytes)
260//   echoes: [echo_count × variable-length echo records]
261//   block_resonance: [block_count × 12 bytes (u32 idx + u32 count + f32 strength)]
262
263fn 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    // Header
335    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    // Echoes
340    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    // Block resonance
352    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
362// ─── Integration helper ─────────────────────────────
363
364/// Full mirror neuron cycle: detect resonance and return boosted scores.
365/// Call this after Hebbian activation recording.
366pub 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    // Return sorted by boost strength
375    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        // Past fingerprint: blocks 0, 1, 2 activated
426        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        // New activation: blocks 0, 1, 3 — overlaps on 0, 1
433        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        // Should have resonance on shared blocks (0, 1)
437        assert!(!boosts.is_empty());
438        assert!(boosts.contains_key(&0));
439        assert!(boosts.contains_key(&1));
440        assert!(!boosts.contains_key(&3)); // not shared
441
442        // Echo should be recorded
443        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        // Block 0 should still exist (0.95)
473        assert!(mirror.block_resonance.contains_key(&0));
474        // Block 1 should be removed (below 0.01)
475        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); // strongest
553        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        // Should return boosted blocks that were shared
577        assert!(!result.is_empty());
578    }
579}