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.
53pub struct MirrorState {
54    pub echoes: Vec<ResonanceEcho>,
55    pub block_resonance: HashMap<u32, BlockResonance>,
56}
57
58impl MirrorState {
59    /// Load or initialize mirror state.
60    pub fn load_or_init(output_dir: &Path) -> Self {
61        load_mirror_state(output_dir).unwrap_or_else(|| Self {
62            echoes: Vec::new(),
63            block_resonance: HashMap::new(),
64        })
65    }
66
67    /// Detect resonance between a new activation and all stored fingerprints.
68    /// Returns the resonance boost per block (block_idx → boost_score).
69    pub fn detect_resonance(
70        &mut self,
71        new_activations: &[(u32, f32)],
72        new_hash: u64,
73        fingerprints: &[ActivationFingerprint],
74    ) -> HashMap<u32, f32> {
75        let mut boosts: HashMap<u32, f32> = HashMap::new();
76
77        if new_activations.is_empty() || fingerprints.is_empty() {
78            return boosts;
79        }
80
81        // Build sparse vector for the new activation
82        let new_vec = activation_to_sparse(new_activations);
83
84        // Compare against all stored fingerprints (skip self — same hash)
85        for fp in fingerprints {
86            if fp.query_hash == new_hash {
87                continue;
88            }
89
90            let past_vec = activation_to_sparse(&fp.activations);
91            let sim = sparse_cosine(&new_vec, &past_vec);
92
93            if sim >= RESONANCE_THRESHOLD {
94                // Find shared blocks
95                let shared: Vec<u32> = new_vec
96                    .keys()
97                    .filter(|k| past_vec.contains_key(k))
98                    .copied()
99                    .collect();
100
101                // Boost shared blocks proportional to similarity
102                for &block_idx in &shared {
103                    let entry = boosts.entry(block_idx).or_insert(0.0);
104                    *entry += sim * 0.1; // resonance boost factor
105
106                    let res = self.block_resonance.entry(block_idx).or_default();
107                    res.echo_count += 1;
108                    res.strength += sim;
109                }
110
111                // Record the echo
112                let now_ms = crate::hebbian::now_epoch_ms_pub();
113                self.echoes.push(ResonanceEcho {
114                    timestamp_ms: now_ms,
115                    trigger_hash: new_hash,
116                    echo_hash: fp.query_hash,
117                    similarity: sim,
118                    shared_blocks: shared,
119                });
120            }
121        }
122
123        // Trim echoes
124        if self.echoes.len() > MAX_ECHOES {
125            self.echoes.drain(0..self.echoes.len() - MAX_ECHOES);
126        }
127
128        boosts
129    }
130
131    /// Decay all block resonance strengths (call periodically).
132    pub fn decay(&mut self) {
133        self.block_resonance.retain(|_, res| {
134            res.strength *= ECHO_DECAY;
135            res.strength > 0.01
136        });
137    }
138
139    /// Get resonance boost for a specific block.
140    pub fn boost_for(&self, block_idx: u32) -> f32 {
141        self.block_resonance
142            .get(&block_idx)
143            .map(|r| r.strength.min(0.5)) // cap boost at 0.5
144            .unwrap_or(0.0)
145    }
146
147    /// Get statistics.
148    pub fn stats(&self) -> MirrorStats {
149        let total_echoes = self.echoes.len();
150        let resonant_blocks = self.block_resonance.len();
151        let avg_similarity = if self.echoes.is_empty() {
152            0.0
153        } else {
154            self.echoes.iter().map(|e| e.similarity).sum::<f32>() / self.echoes.len() as f32
155        };
156        let strongest = self
157            .block_resonance
158            .iter()
159            .max_by(|a, b| a.1.strength.partial_cmp(&b.1.strength).unwrap())
160            .map(|(&idx, res)| (idx, res.strength));
161
162        MirrorStats {
163            total_echoes,
164            resonant_blocks,
165            avg_similarity,
166            strongest_block: strongest,
167        }
168    }
169
170    /// Get top-N most resonant blocks.
171    pub fn most_resonant(&self, n: usize) -> Vec<(u32, &BlockResonance)> {
172        let mut blocks: Vec<(u32, &BlockResonance)> =
173            self.block_resonance.iter().map(|(&k, v)| (k, v)).collect();
174        blocks.sort_by(|a, b| b.1.strength.partial_cmp(&a.1.strength).unwrap());
175        blocks.truncate(n);
176        blocks
177    }
178
179    /// Save mirror state to disk.
180    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
181        save_mirror_state(output_dir, self)
182    }
183}
184
185pub struct MirrorStats {
186    pub total_echoes: usize,
187    pub resonant_blocks: usize,
188    pub avg_similarity: f32,
189    pub strongest_block: Option<(u32, f32)>,
190}
191
192// ─── Sparse vector operations ───────────────────────
193
194fn activation_to_sparse(activations: &[(u32, f32)]) -> HashMap<u32, f32> {
195    activations.iter().copied().collect()
196}
197
198fn sparse_cosine(a: &HashMap<u32, f32>, b: &HashMap<u32, f32>) -> f32 {
199    let (smaller, larger) = if a.len() <= b.len() { (a, b) } else { (b, a) };
200
201    let mut dot = 0.0f32;
202    for (k, va) in smaller {
203        if let Some(vb) = larger.get(k) {
204            dot += va * vb;
205        }
206    }
207
208    let norm_a: f32 = a.values().map(|v| v * v).sum::<f32>().sqrt();
209    let norm_b: f32 = b.values().map(|v| v * v).sum::<f32>().sqrt();
210
211    if norm_a < 1e-9 || norm_b < 1e-9 {
212        return 0.0;
213    }
214
215    dot / (norm_a * norm_b)
216}
217
218// ─── Binary I/O ─────────────────────────────────────
219//
220// resonance.bin format:
221//   magic: b"RES1" (4 bytes)
222//   echo_count: u32 (4 bytes)
223//   block_count: u32 (4 bytes)
224//   echoes: [echo_count × variable-length echo records]
225//   block_resonance: [block_count × 12 bytes (u32 idx + u32 count + f32 strength)]
226
227fn load_mirror_state(output_dir: &Path) -> Option<MirrorState> {
228    let path = output_dir.join("resonance.bin");
229    let data = fs::read(&path).ok()?;
230    if data.len() < 12 || &data[0..4] != b"RES1" {
231        return None;
232    }
233
234    let echo_count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
235    let block_count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
236
237    let mut pos = 12;
238    let mut echoes = Vec::with_capacity(echo_count);
239    for _ in 0..echo_count {
240        if pos + 28 > data.len() {
241            break;
242        }
243        let timestamp_ms = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
244        let trigger_hash = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
245        let echo_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
246        let similarity = f32::from_le_bytes(data[pos + 24..pos + 28].try_into().unwrap());
247        let shared_count =
248            u16::from_le_bytes(data[pos + 28..pos + 30].try_into().unwrap()) as usize;
249        pos += 30;
250
251        let mut shared_blocks = Vec::with_capacity(shared_count);
252        for _ in 0..shared_count {
253            if pos + 4 > data.len() {
254                break;
255            }
256            shared_blocks.push(u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()));
257            pos += 4;
258        }
259
260        echoes.push(ResonanceEcho {
261            timestamp_ms,
262            trigger_hash,
263            echo_hash,
264            similarity,
265            shared_blocks,
266        });
267    }
268
269    let mut block_resonance = HashMap::with_capacity(block_count);
270    for _ in 0..block_count {
271        if pos + 12 > data.len() {
272            break;
273        }
274        let idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
275        let echo_count = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
276        let strength = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
277        pos += 12;
278        block_resonance.insert(
279            idx,
280            BlockResonance {
281                echo_count,
282                strength,
283            },
284        );
285    }
286
287    Some(MirrorState {
288        echoes,
289        block_resonance,
290    })
291}
292
293fn save_mirror_state(output_dir: &Path, state: &MirrorState) -> Result<(), String> {
294    let path = output_dir.join("resonance.bin");
295    let mut buf = Vec::new();
296
297    // Header
298    buf.extend_from_slice(b"RES1");
299    buf.extend_from_slice(&(state.echoes.len() as u32).to_le_bytes());
300    buf.extend_from_slice(&(state.block_resonance.len() as u32).to_le_bytes());
301
302    // Echoes
303    for echo in &state.echoes {
304        buf.extend_from_slice(&echo.timestamp_ms.to_le_bytes());
305        buf.extend_from_slice(&echo.trigger_hash.to_le_bytes());
306        buf.extend_from_slice(&echo.echo_hash.to_le_bytes());
307        buf.extend_from_slice(&echo.similarity.to_le_bytes());
308        buf.extend_from_slice(&(echo.shared_blocks.len() as u16).to_le_bytes());
309        for &block_idx in &echo.shared_blocks {
310            buf.extend_from_slice(&block_idx.to_le_bytes());
311        }
312    }
313
314    // Block resonance
315    for (&idx, res) in &state.block_resonance {
316        buf.extend_from_slice(&idx.to_le_bytes());
317        buf.extend_from_slice(&res.echo_count.to_le_bytes());
318        buf.extend_from_slice(&res.strength.to_le_bytes());
319    }
320
321    fs::write(&path, &buf).map_err(|e| format!("write resonance.bin: {}", e))
322}
323
324// ─── Integration helper ─────────────────────────────
325
326/// Full mirror neuron cycle: detect resonance and return boosted scores.
327/// Call this after Hebbian activation recording.
328pub fn mirror_boost(
329    hebb: &HebbianState,
330    mirror: &mut MirrorState,
331    new_activations: &[(u32, f32)],
332    query_hash: u64,
333) -> Vec<(u32, f32)> {
334    let boosts = mirror.detect_resonance(new_activations, query_hash, &hebb.fingerprints);
335
336    // Return sorted by boost strength
337    let mut result: Vec<(u32, f32)> = boosts.into_iter().collect();
338    result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
339    result
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_sparse_cosine_identical() {
348        let a: HashMap<u32, f32> = [(0, 1.0), (1, 0.5), (2, 0.3)].into();
349        let sim = sparse_cosine(&a, &a);
350        assert!((sim - 1.0).abs() < 0.001);
351    }
352
353    #[test]
354    fn test_sparse_cosine_orthogonal() {
355        let a: HashMap<u32, f32> = [(0, 1.0), (1, 0.5)].into();
356        let b: HashMap<u32, f32> = [(2, 1.0), (3, 0.5)].into();
357        let sim = sparse_cosine(&a, &b);
358        assert!(sim.abs() < 0.001);
359    }
360
361    #[test]
362    fn test_sparse_cosine_partial_overlap() {
363        let a: HashMap<u32, f32> = [(0, 1.0), (1, 0.5), (2, 0.3)].into();
364        let b: HashMap<u32, f32> = [(1, 0.8), (2, 0.6), (3, 0.4)].into();
365        let sim = sparse_cosine(&a, &b);
366        assert!(sim > 0.0);
367        assert!(sim < 1.0);
368    }
369
370    #[test]
371    fn test_detect_resonance_no_fingerprints() {
372        let mut mirror = MirrorState {
373            echoes: Vec::new(),
374            block_resonance: HashMap::new(),
375        };
376        let boosts = mirror.detect_resonance(&[(0, 1.0), (1, 0.5)], 42, &[]);
377        assert!(boosts.is_empty());
378    }
379
380    #[test]
381    fn test_detect_resonance_with_match() {
382        let mut mirror = MirrorState {
383            echoes: Vec::new(),
384            block_resonance: HashMap::new(),
385        };
386
387        // Past fingerprint: blocks 0, 1, 2 activated
388        let past = ActivationFingerprint {
389            timestamp_ms: 1000,
390            query_hash: 100,
391            activations: vec![(0, 0.9), (1, 0.7), (2, 0.5)],
392        };
393
394        // New activation: blocks 0, 1, 3 — overlaps on 0, 1
395        let new_act = vec![(0, 0.8), (1, 0.6), (3, 0.4)];
396        let boosts = mirror.detect_resonance(&new_act, 200, &[past]);
397
398        // Should have resonance on shared blocks (0, 1)
399        assert!(!boosts.is_empty());
400        assert!(boosts.contains_key(&0));
401        assert!(boosts.contains_key(&1));
402        assert!(!boosts.contains_key(&3)); // not shared
403
404        // Echo should be recorded
405        assert_eq!(mirror.echoes.len(), 1);
406        assert_eq!(mirror.echoes[0].trigger_hash, 200);
407        assert_eq!(mirror.echoes[0].echo_hash, 100);
408    }
409
410    #[test]
411    fn test_decay() {
412        let mut mirror = MirrorState {
413            echoes: Vec::new(),
414            block_resonance: HashMap::new(),
415        };
416
417        mirror.block_resonance.insert(
418            0,
419            BlockResonance {
420                echo_count: 5,
421                strength: 1.0,
422            },
423        );
424        mirror.block_resonance.insert(
425            1,
426            BlockResonance {
427                echo_count: 1,
428                strength: 0.005,
429            },
430        );
431
432        mirror.decay();
433
434        // Block 0 should still exist (0.95)
435        assert!(mirror.block_resonance.contains_key(&0));
436        // Block 1 should be removed (below 0.01)
437        assert!(!mirror.block_resonance.contains_key(&1));
438    }
439
440    #[test]
441    fn test_save_load_roundtrip() {
442        let tmp = tempfile::tempdir().expect("create temp dir");
443        let dir = tmp.path();
444
445        let mut mirror = MirrorState {
446            echoes: Vec::new(),
447            block_resonance: HashMap::new(),
448        };
449
450        mirror.echoes.push(ResonanceEcho {
451            timestamp_ms: 12345,
452            trigger_hash: 100,
453            echo_hash: 200,
454            similarity: 0.75,
455            shared_blocks: vec![1, 5, 10],
456        });
457        mirror.block_resonance.insert(
458            1,
459            BlockResonance {
460                echo_count: 3,
461                strength: 0.8,
462            },
463        );
464        mirror.block_resonance.insert(
465            5,
466            BlockResonance {
467                echo_count: 1,
468                strength: 0.3,
469            },
470        );
471
472        mirror.save(dir).expect("save");
473
474        let loaded = MirrorState::load_or_init(dir);
475        assert_eq!(loaded.echoes.len(), 1);
476        assert_eq!(loaded.echoes[0].trigger_hash, 100);
477        assert_eq!(loaded.echoes[0].shared_blocks, vec![1, 5, 10]);
478        assert_eq!(loaded.block_resonance.len(), 2);
479        assert_eq!(loaded.block_resonance[&1].echo_count, 3);
480        assert!((loaded.block_resonance[&5].strength - 0.3).abs() < 0.001);
481    }
482
483    #[test]
484    fn test_most_resonant() {
485        let mut mirror = MirrorState {
486            echoes: Vec::new(),
487            block_resonance: HashMap::new(),
488        };
489
490        mirror.block_resonance.insert(
491            0,
492            BlockResonance {
493                echo_count: 1,
494                strength: 0.1,
495            },
496        );
497        mirror.block_resonance.insert(
498            1,
499            BlockResonance {
500                echo_count: 5,
501                strength: 0.9,
502            },
503        );
504        mirror.block_resonance.insert(
505            2,
506            BlockResonance {
507                echo_count: 3,
508                strength: 0.5,
509            },
510        );
511
512        let top = mirror.most_resonant(2);
513        assert_eq!(top.len(), 2);
514        assert_eq!(top[0].0, 1); // strongest
515        assert_eq!(top[1].0, 2);
516    }
517
518    #[test]
519    fn test_mirror_boost_integration() {
520        let hebb = HebbianState {
521            activations: vec![crate::hebbian::ActivationRecord::default(); 5],
522            coactivations: HashMap::new(),
523            fingerprints: vec![ActivationFingerprint {
524                timestamp_ms: 1000,
525                query_hash: 100,
526                activations: vec![(0, 0.9), (1, 0.7), (2, 0.5)],
527            }],
528        };
529
530        let mut mirror = MirrorState {
531            echoes: Vec::new(),
532            block_resonance: HashMap::new(),
533        };
534
535        let new_act = vec![(0, 0.8), (1, 0.6), (3, 0.4)];
536        let result = mirror_boost(&hebb, &mut mirror, &new_act, 200);
537
538        // Should return boosted blocks that were shared
539        assert!(!result.is_empty());
540    }
541}