1use std::collections::HashMap;
14use std::fs;
15use std::path::Path;
16
17use crate::hebbian;
18use crate::resonance::ResonanceState;
19
20const SEED_THRESHOLD: f32 = 2.0;
24const MIN_MEMBERS: usize = 3;
26const MAX_ARCHETYPES: usize = 100;
28const CLUSTER_RADIUS: f32 = 0.15;
30const ARCHETYPE_DECAY: f32 = 0.98;
32
33#[derive(Clone, Debug)]
37pub struct Archetype {
38 pub id: u32,
40 pub centroid: (f32, f32, f32),
42 pub members: Vec<u32>,
44 pub strength: f32,
46 pub reinforcement_count: u32,
48 pub emerged_ms: u64,
50 pub last_reinforced_ms: u64,
52 pub label: String,
54}
55
56#[derive(Clone)]
58pub struct ArchetypeState {
59 pub archetypes: Vec<Archetype>,
60 next_id: u32,
61}
62
63impl ArchetypeState {
64 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 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 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 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 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 if self
119 .archetypes
120 .iter()
121 .any(|a| spatial_dist(a.centroid, (cx, cy, cz)) < CLUSTER_RADIUS)
122 {
123 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 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 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 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 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 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 pub fn decay(&mut self) {
213 for a in &mut self.archetypes {
214 a.strength *= ARCHETYPE_DECAY;
215 }
216 self.archetypes
218 .retain(|a| a.strength > 0.1 || a.reinforcement_count > 5);
219 }
220
221 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 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 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
281fn 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
290fn 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
321fn 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); 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 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); 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, 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 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(), };
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}