Skip to main content

microscope_memory/
archetype.rs

1//! Archetype emergence layer for Microscope Memory.
2//!
3//! Archetypes are recurring activation patterns that crystallize from
4//! repeated Hebbian/mirror/resonance activity. When the same spatial
5//! regions fire together often enough, an archetype "emerges" —
6//! a stable attractor in the memory landscape.
7//!
8//! Archetypes are visible at depth 0 (D0) — the highest zoom level —
9//! as named clusters that represent concepts the system has learned.
10//!
11//! Binary format: archetypes.bin (ARC1)
12
13use std::collections::HashMap;
14use std::fs;
15use std::path::Path;
16
17use crate::hebbian;
18use crate::resonance::ResonanceState;
19
20// ─── Constants ──────────────────────────────────────
21
22/// Minimum resonance field energy to seed an archetype.
23const SEED_THRESHOLD: f32 = 2.0;
24/// Minimum number of member blocks for a valid archetype.
25const MIN_MEMBERS: usize = 3;
26/// Maximum number of archetypes.
27const MAX_ARCHETYPES: usize = 100;
28/// Spatial radius for clustering blocks into an archetype.
29const CLUSTER_RADIUS: f32 = 0.15;
30/// Archetype strength decay per cycle.
31const ARCHETYPE_DECAY: f32 = 0.98;
32
33// ─── Types ──────────────────────────────────────────
34
35/// An archetype — an emergent concept crystallized from activation patterns.
36#[derive(Clone, Debug)]
37pub struct Archetype {
38    /// Unique archetype ID.
39    pub id: u32,
40    /// Centroid in 3D space (average of member block coordinates).
41    pub centroid: (f32, f32, f32),
42    /// Member block indices.
43    pub members: Vec<u32>,
44    /// Archetype strength (accumulated from resonance + Hebbian energy).
45    pub strength: f32,
46    /// Number of times this archetype has been reinforced.
47    pub reinforcement_count: u32,
48    /// Timestamp of first emergence.
49    pub emerged_ms: u64,
50    /// Timestamp of last reinforcement.
51    pub last_reinforced_ms: u64,
52    /// Auto-generated label (derived from member block content).
53    pub label: String,
54}
55
56/// Archetype system state.
57#[derive(Clone)]
58pub struct ArchetypeState {
59    pub archetypes: Vec<Archetype>,
60    next_id: u32,
61}
62
63impl ArchetypeState {
64    /// Load or initialize archetype state.
65    pub fn load_or_init(output_dir: &Path) -> Self {
66        load_archetypes(output_dir).unwrap_or_else(|| Self {
67            archetypes: Vec::new(),
68            next_id: 1,
69        })
70    }
71
72    /// Remap member block indices after a rebuild changed the block layout.
73    /// `old_to_new[old_idx] = new_idx`; members that no longer exist are
74    /// dropped, and archetypes left without members are removed.
75    pub fn remap_indexes(&mut self, old_to_new: &[u32]) {
76        for a in &mut self.archetypes {
77            let mut new_members = Vec::with_capacity(a.members.len());
78            for &m in &a.members {
79                if (m as usize) < old_to_new.len() {
80                    let new_idx = old_to_new[m as usize];
81                    if new_idx != u32::MAX {
82                        new_members.push(new_idx);
83                    }
84                }
85            }
86            a.members = new_members;
87        }
88        self.archetypes.retain(|a| !a.members.is_empty());
89    }
90
91    /// Detect new archetypes from the resonance field and Hebbian state.
92    /// Returns the number of new archetypes emerged.
93    pub fn detect(
94        &mut self,
95        resonance: &ResonanceState,
96        hebb: &hebbian::HebbianState,
97        headers: &[(f32, f32, f32)],
98        block_texts: &[&str],
99    ) -> usize {
100        let now_ms = hebbian::now_epoch_ms_pub();
101        let mut new_count = 0;
102
103        // Find hot spots in the resonance field above threshold
104        let hot_cells: Vec<((i16, i16, i16), f32)> = resonance
105            .field
106            .iter()
107            .filter(|(_, &v)| v >= SEED_THRESHOLD)
108            .map(|(&k, &v)| (k, v))
109            .collect();
110
111        for ((qx, qy, qz), field_energy) in hot_cells {
112            // Convert quantized coords back to float
113            let cx = qx as f32 / 20.0;
114            let cy = qy as f32 / 20.0;
115            let cz = qz as f32 / 20.0;
116
117            // Check if an existing archetype already covers this region
118            if self
119                .archetypes
120                .iter()
121                .any(|a| spatial_dist(a.centroid, (cx, cy, cz)) < CLUSTER_RADIUS)
122            {
123                // Reinforce existing archetype instead
124                if let Some(a) = self.archetypes.iter_mut().min_by(|a, b| {
125                    spatial_dist(a.centroid, (cx, cy, cz))
126                        .partial_cmp(&spatial_dist(b.centroid, (cx, cy, cz)))
127                        .unwrap()
128                }) {
129                    a.strength += field_energy * 0.1;
130                    a.reinforcement_count += 1;
131                    a.last_reinforced_ms = now_ms;
132                }
133                continue;
134            }
135
136            // Find nearby blocks to form the archetype
137            let mut members = Vec::new();
138            for (idx, (bx, by, bz)) in headers.iter().enumerate() {
139                let dx = cx - bx;
140                let dy = cy - by;
141                let dz = cz - bz;
142                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
143
144                if dist < CLUSTER_RADIUS {
145                    // Extra requirement: block must have some Hebbian activity
146                    if idx < hebb.activations.len() && hebb.activations[idx].activation_count > 0 {
147                        members.push(idx as u32);
148                    }
149                }
150            }
151
152            if members.len() < MIN_MEMBERS {
153                continue;
154            }
155
156            // Compute centroid from actual member positions
157            let (sum_x, sum_y, sum_z) = members.iter().fold((0.0f32, 0.0f32, 0.0f32), |acc, &m| {
158                let (x, y, z) = headers[m as usize];
159                (acc.0 + x, acc.1 + y, acc.2 + z)
160            });
161            let n = members.len() as f32;
162            let centroid = (sum_x / n, sum_y / n, sum_z / n);
163
164            // Generate label from most common words in member blocks
165            let label = generate_label(&members, block_texts);
166
167            let archetype = Archetype {
168                id: self.next_id,
169                centroid,
170                members,
171                strength: field_energy,
172                reinforcement_count: 1,
173                emerged_ms: now_ms,
174                last_reinforced_ms: now_ms,
175                label,
176            };
177
178            self.archetypes.push(archetype);
179            self.next_id += 1;
180            new_count += 1;
181
182            if self.archetypes.len() >= MAX_ARCHETYPES {
183                break;
184            }
185        }
186
187        new_count
188    }
189
190    /// Reinforce archetypes based on a new activation pattern.
191    /// If activated blocks overlap with an archetype's members, strengthen it.
192    pub fn reinforce(&mut self, activated_blocks: &[(u32, f32)]) {
193        let now_ms = hebbian::now_epoch_ms_pub();
194        let activated_set: HashMap<u32, f32> = activated_blocks.iter().copied().collect();
195
196        for archetype in &mut self.archetypes {
197            let overlap: f32 = archetype
198                .members
199                .iter()
200                .filter_map(|m| activated_set.get(m))
201                .sum();
202
203            if overlap > 0.0 {
204                archetype.strength += overlap * 0.05;
205                archetype.reinforcement_count += 1;
206                archetype.last_reinforced_ms = now_ms;
207            }
208        }
209    }
210
211    /// Decay archetype strengths. Remove dead archetypes.
212    pub fn decay(&mut self) {
213        for a in &mut self.archetypes {
214            a.strength *= ARCHETYPE_DECAY;
215        }
216        // Remove archetypes that have decayed below threshold and have few reinforcements
217        self.archetypes
218            .retain(|a| a.strength > 0.1 || a.reinforcement_count > 5);
219    }
220
221    /// Find which archetype (if any) a query activation best matches.
222    pub fn match_archetype(&self, activated_blocks: &[(u32, f32)]) -> Option<(usize, f32)> {
223        let activated_set: HashMap<u32, f32> = activated_blocks.iter().copied().collect();
224
225        let mut best: Option<(usize, f32)> = None;
226        for (i, archetype) in self.archetypes.iter().enumerate() {
227            let overlap: f32 = archetype
228                .members
229                .iter()
230                .filter_map(|m| activated_set.get(m))
231                .sum();
232
233            let coverage = if archetype.members.is_empty() {
234                0.0
235            } else {
236                let matching = archetype
237                    .members
238                    .iter()
239                    .filter(|m| activated_set.contains_key(m))
240                    .count();
241                matching as f32 / archetype.members.len() as f32
242            };
243
244            let score = overlap * coverage * archetype.strength;
245            if score > 0.0 && (best.is_none() || score > best.unwrap().1) {
246                best = Some((i, score));
247            }
248        }
249        best
250    }
251
252    /// Get statistics.
253    pub fn stats(&self) -> ArchetypeStats {
254        let total_members: usize = self.archetypes.iter().map(|a| a.members.len()).sum();
255        let strongest = self
256            .archetypes
257            .iter()
258            .max_by(|a, b| a.strength.total_cmp(&b.strength));
259
260        ArchetypeStats {
261            archetype_count: self.archetypes.len(),
262            total_members,
263            strongest_label: strongest.map(|a| a.label.clone()),
264            strongest_strength: strongest.map(|a| a.strength),
265        }
266    }
267
268    /// Save to disk.
269    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
270        save_archetypes(output_dir, self)
271    }
272}
273
274pub struct ArchetypeStats {
275    pub archetype_count: usize,
276    pub total_members: usize,
277    pub strongest_label: Option<String>,
278    pub strongest_strength: Option<f32>,
279}
280
281// ─── Helpers ────────────────────────────────────────
282
283fn spatial_dist(a: (f32, f32, f32), b: (f32, f32, f32)) -> f32 {
284    let dx = a.0 - b.0;
285    let dy = a.1 - b.1;
286    let dz = a.2 - b.2;
287    (dx * dx + dy * dy + dz * dz).sqrt()
288}
289
290/// Generate a label from the most common meaningful words in member blocks.
291fn generate_label(members: &[u32], block_texts: &[&str]) -> String {
292    let mut word_counts: HashMap<&str, usize> = HashMap::new();
293    let stopwords = [
294        "a", "the", "is", "of", "and", "to", "in", "it", "on", "for", "that", "this", "with",
295        "was", "are", "be", "has", "had", "not", "but", "from", "or", "an", "at", "by",
296    ];
297
298    for &idx in members {
299        let i = idx as usize;
300        if i < block_texts.len() {
301            for word in block_texts[i].split_whitespace() {
302                let w = word.trim_matches(|c: char| !c.is_alphanumeric());
303                if w.len() > 2 && !stopwords.contains(&w.to_lowercase().as_str()) {
304                    *word_counts.entry(w).or_insert(0) += 1;
305                }
306            }
307        }
308    }
309
310    let mut words: Vec<(&&str, &usize)> = word_counts.iter().collect();
311    words.sort_by(|a, b| b.1.cmp(a.1));
312
313    words
314        .iter()
315        .take(3)
316        .map(|(w, _)| **w)
317        .collect::<Vec<&str>>()
318        .join("-")
319}
320
321// ─── Binary I/O ─────────────────────────────────────
322//
323// archetypes.bin format:
324//   magic: b"ARC1" (4 bytes)
325//   next_id: u32 (4 bytes)
326//   count: u32 (4 bytes)
327//   per archetype:
328//     id: u32, cx/cy/cz: f32×3, strength: f32
329//     reinforcement_count: u32, emerged_ms: u64, last_reinforced_ms: u64
330//     member_count: u16, members: [member_count × u32]
331//     label_len: u16, label_bytes: [label_len]
332
333fn load_archetypes(output_dir: &Path) -> Option<ArchetypeState> {
334    let path = output_dir.join("archetypes.bin");
335    let data = fs::read(&path).ok()?;
336    if data.len() < 12 || &data[0..4] != b"ARC1" {
337        return None;
338    }
339
340    let next_id = u32::from_le_bytes(data[4..8].try_into().unwrap());
341    let count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
342
343    let mut pos = 12;
344    let mut archetypes = Vec::with_capacity(count);
345
346    for _ in 0..count {
347        if pos + 40 > data.len() {
348            break;
349        }
350
351        let id = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
352        let cx = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
353        let cy = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
354        let cz = f32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
355        let strength = f32::from_le_bytes(data[pos + 16..pos + 20].try_into().unwrap());
356        let reinforcement_count = u32::from_le_bytes(data[pos + 20..pos + 24].try_into().unwrap());
357        let emerged_ms = u64::from_le_bytes(data[pos + 24..pos + 32].try_into().unwrap());
358        let last_reinforced_ms = u64::from_le_bytes(data[pos + 32..pos + 40].try_into().unwrap());
359        pos += 40;
360
361        if pos + 2 > data.len() {
362            break;
363        }
364        let member_count = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
365        pos += 2;
366
367        let mut members = Vec::with_capacity(member_count);
368        for _ in 0..member_count {
369            if pos + 4 > data.len() {
370                break;
371            }
372            members.push(u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()));
373            pos += 4;
374        }
375
376        if pos + 2 > data.len() {
377            break;
378        }
379        let label_len = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
380        pos += 2;
381
382        let label = if pos + label_len <= data.len() {
383            let s = String::from_utf8_lossy(&data[pos..pos + label_len]).to_string();
384            pos += label_len;
385            s
386        } else {
387            String::new()
388        };
389
390        archetypes.push(Archetype {
391            id,
392            centroid: (cx, cy, cz),
393            members,
394            strength,
395            reinforcement_count,
396            emerged_ms,
397            last_reinforced_ms,
398            label,
399        });
400    }
401
402    Some(ArchetypeState {
403        archetypes,
404        next_id,
405    })
406}
407
408fn save_archetypes(output_dir: &Path, state: &ArchetypeState) -> Result<(), String> {
409    let path = output_dir.join("archetypes.bin");
410    let tmp_path = output_dir.join("archetypes.bin.tmp");
411    let mut buf = Vec::new();
412
413    buf.extend_from_slice(b"ARC1");
414    buf.extend_from_slice(&state.next_id.to_le_bytes());
415    buf.extend_from_slice(&(state.archetypes.len() as u32).to_le_bytes());
416
417    for a in &state.archetypes {
418        buf.extend_from_slice(&a.id.to_le_bytes());
419        buf.extend_from_slice(&a.centroid.0.to_le_bytes());
420        buf.extend_from_slice(&a.centroid.1.to_le_bytes());
421        buf.extend_from_slice(&a.centroid.2.to_le_bytes());
422        buf.extend_from_slice(&a.strength.to_le_bytes());
423        buf.extend_from_slice(&a.reinforcement_count.to_le_bytes());
424        buf.extend_from_slice(&a.emerged_ms.to_le_bytes());
425        buf.extend_from_slice(&a.last_reinforced_ms.to_le_bytes());
426
427        buf.extend_from_slice(&(a.members.len() as u16).to_le_bytes());
428        for &m in &a.members {
429            buf.extend_from_slice(&m.to_le_bytes());
430        }
431
432        let label_bytes = a.label.as_bytes();
433        buf.extend_from_slice(&(label_bytes.len() as u16).to_le_bytes());
434        buf.extend_from_slice(label_bytes);
435    }
436
437    fs::write(&tmp_path, &buf).map_err(|e| format!("write archetypes.bin: {}", e))?;
438    fs::rename(&tmp_path, &path).map_err(|e| format!("rename archetypes.bin: {}", e))
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn test_spatial_dist() {
447        assert!((spatial_dist((0.0, 0.0, 0.0), (1.0, 0.0, 0.0)) - 1.0).abs() < 0.001);
448        assert!(spatial_dist((0.1, 0.2, 0.3), (0.1, 0.2, 0.3)) < 0.001);
449    }
450
451    #[test]
452    fn test_generate_label() {
453        let texts = [
454            "hello world Rust",
455            "Rust memory system",
456            "Rust binary format",
457        ];
458        let members = vec![0, 1, 2];
459        let label = generate_label(&members, &texts);
460        assert!(label.contains("Rust"));
461    }
462
463    #[test]
464    fn test_reinforce() {
465        let mut state = ArchetypeState {
466            archetypes: vec![Archetype {
467                id: 1,
468                centroid: (0.1, 0.2, 0.3),
469                members: vec![0, 1, 2],
470                strength: 1.0,
471                reinforcement_count: 1,
472                emerged_ms: 1000,
473                last_reinforced_ms: 1000,
474                label: "test".to_string(),
475            }],
476            next_id: 2,
477        };
478
479        state.reinforce(&[(0, 0.9), (1, 0.5), (5, 0.3)]);
480
481        assert!(state.archetypes[0].strength > 1.0); // reinforced
482        assert_eq!(state.archetypes[0].reinforcement_count, 2);
483    }
484
485    #[test]
486    fn test_match_archetype() {
487        let state = ArchetypeState {
488            archetypes: vec![
489                Archetype {
490                    id: 1,
491                    centroid: (0.1, 0.2, 0.3),
492                    members: vec![0, 1, 2],
493                    strength: 2.0,
494                    reinforcement_count: 5,
495                    emerged_ms: 1000,
496                    last_reinforced_ms: 2000,
497                    label: "alpha".to_string(),
498                },
499                Archetype {
500                    id: 2,
501                    centroid: (0.5, 0.5, 0.5),
502                    members: vec![10, 11, 12],
503                    strength: 1.0,
504                    reinforcement_count: 2,
505                    emerged_ms: 1500,
506                    last_reinforced_ms: 1800,
507                    label: "beta".to_string(),
508                },
509            ],
510            next_id: 3,
511        };
512
513        // Activate blocks that overlap with archetype "alpha"
514        let result = state.match_archetype(&[(0, 0.9), (1, 0.7), (2, 0.5)]);
515        assert!(result.is_some());
516        assert_eq!(result.unwrap().0, 0); // matched "alpha"
517
518        // No overlap
519        let result2 = state.match_archetype(&[(99, 0.9)]);
520        assert!(result2.is_none());
521    }
522
523    #[test]
524    fn test_decay() {
525        let mut state = ArchetypeState {
526            archetypes: vec![
527                Archetype {
528                    id: 1,
529                    centroid: (0.0, 0.0, 0.0),
530                    members: vec![0, 1, 2],
531                    strength: 5.0,
532                    reinforcement_count: 10,
533                    emerged_ms: 1000,
534                    last_reinforced_ms: 2000,
535                    label: "strong".to_string(),
536                },
537                Archetype {
538                    id: 2,
539                    centroid: (0.5, 0.5, 0.5),
540                    members: vec![3, 4, 5],
541                    strength: 0.05, // weak, few reinforcements
542                    reinforcement_count: 1,
543                    emerged_ms: 1000,
544                    last_reinforced_ms: 1000,
545                    label: "weak".to_string(),
546                },
547            ],
548            next_id: 3,
549        };
550
551        state.decay();
552
553        // Strong archetype survives
554        assert_eq!(state.archetypes.len(), 1);
555        assert_eq!(state.archetypes[0].id, 1);
556    }
557
558    #[test]
559    fn test_save_load_roundtrip() {
560        let tmp = tempfile::tempdir().expect("create temp dir");
561        let dir = tmp.path();
562
563        let mut state = ArchetypeState {
564            archetypes: Vec::new(),
565            next_id: 1,
566        };
567
568        state.archetypes.push(Archetype {
569            id: 1,
570            centroid: (0.1, 0.2, 0.3),
571            members: vec![0, 5, 10],
572            strength: 2.5,
573            reinforcement_count: 7,
574            emerged_ms: 12345,
575            last_reinforced_ms: 67890,
576            label: "test-archetype".to_string(),
577        });
578        state.next_id = 2;
579
580        state.save(dir).expect("save");
581
582        let loaded = ArchetypeState::load_or_init(dir);
583        assert_eq!(loaded.archetypes.len(), 1);
584        assert_eq!(loaded.archetypes[0].id, 1);
585        assert!((loaded.archetypes[0].centroid.0 - 0.1).abs() < 0.001);
586        assert_eq!(loaded.archetypes[0].members, vec![0, 5, 10]);
587        assert_eq!(loaded.archetypes[0].label, "test-archetype");
588        assert_eq!(loaded.next_id, 2);
589    }
590
591    #[test]
592    fn test_detect_no_field() {
593        let resonance = ResonanceState {
594            instance_id: 42,
595            outgoing: Vec::new(),
596            incoming: Vec::new(),
597            field: HashMap::new(), // empty field
598        };
599
600        let hebb = hebbian::HebbianState {
601            activations: vec![hebbian::ActivationRecord::default(); 5],
602            coactivations: HashMap::new(),
603            fingerprints: Vec::new(),
604        };
605
606        let headers = vec![(0.1, 0.2, 0.3); 5];
607        let texts: Vec<&str> = vec!["a"; 5];
608
609        let mut state = ArchetypeState {
610            archetypes: Vec::new(),
611            next_id: 1,
612        };
613
614        let emerged = state.detect(&resonance, &hebb, &headers, &texts);
615        assert_eq!(emerged, 0);
616    }
617
618    #[test]
619    fn test_stats() {
620        let state = ArchetypeState {
621            archetypes: vec![
622                Archetype {
623                    id: 1,
624                    centroid: (0.0, 0.0, 0.0),
625                    members: vec![0, 1, 2],
626                    strength: 3.0,
627                    reinforcement_count: 5,
628                    emerged_ms: 1000,
629                    last_reinforced_ms: 2000,
630                    label: "alpha".to_string(),
631                },
632                Archetype {
633                    id: 2,
634                    centroid: (0.5, 0.5, 0.5),
635                    members: vec![3, 4],
636                    strength: 1.0,
637                    reinforcement_count: 2,
638                    emerged_ms: 1500,
639                    last_reinforced_ms: 1800,
640                    label: "beta".to_string(),
641                },
642            ],
643            next_id: 3,
644        };
645
646        let stats = state.stats();
647        assert_eq!(stats.archetype_count, 2);
648        assert_eq!(stats.total_members, 5);
649        assert_eq!(stats.strongest_label.unwrap(), "alpha");
650    }
651}