1use std::collections::HashMap;
15use std::fs;
16use std::path::Path;
17
18use crate::hebbian;
19use crate::types::{BlockHeader, MemoryQueryOptions, ProjectId};
20
21const MAX_PULSES: usize = 2000;
25const PULSE_TTL_MS: u64 = 172_800_000;
27const MIN_ACTIVATIONS_FOR_PULSE: usize = 2;
29
30#[inline(always)]
32pub fn is_block_visible(block: &BlockHeader, opts: &MemoryQueryOptions) -> bool {
33 block.is_visible_to(opts.active_project, opts.include_global)
34 && block.importance >= opts.min_importance
35}
36
37#[derive(Clone, Debug)]
41pub struct Pulse {
42 pub source_id: u64,
44 pub timestamp_ms: u64,
46 pub query_hash: u64,
48 pub activations: Vec<(f32, f32, f32, f32)>,
51 pub layer_hint: u8,
53 pub strength: f32,
55}
56
57#[derive(Clone, Debug)]
59pub struct ReceivedPulse {
60 pub pulse: Pulse,
61 pub local_matches: u32,
63 pub integrated: bool,
65}
66
67#[derive(Clone)]
69pub struct ResonanceState {
70 pub instance_id: u64,
72 pub outgoing: Vec<Pulse>,
74 pub incoming: Vec<ReceivedPulse>,
76 pub field: HashMap<(i16, i16, i16), f32>,
79}
80
81impl ResonanceState {
82 pub fn load_or_init(output_dir: &Path) -> Self {
84 let instance_id = path_hash(output_dir);
85 load_resonance_state(output_dir, instance_id).unwrap_or_else(|| Self {
86 instance_id,
87 outgoing: Vec::new(),
88 incoming: Vec::new(),
89 field: HashMap::new(),
90 })
91 }
92
93 pub fn emit_pulse(
96 &mut self,
97 activations: &[(u32, f32)],
98 query_hash: u64,
99 headers: &[(f32, f32, f32)],
100 layer_hint: u8,
101 ) {
102 if activations.len() < MIN_ACTIVATIONS_FOR_PULSE {
103 return;
104 }
105
106 let now_ms = hebbian::now_epoch_ms_pub();
107
108 let spatial: Vec<(f32, f32, f32, f32)> = activations
110 .iter()
111 .filter_map(|&(idx, score)| {
112 let i = idx as usize;
113 if i < headers.len() {
114 let (x, y, z) = headers[i];
115 Some((x, y, z, score))
116 } else {
117 None
118 }
119 })
120 .collect();
121
122 if spatial.is_empty() {
123 return;
124 }
125
126 let avg_score = spatial.iter().map(|s| s.3).sum::<f32>() / spatial.len() as f32;
127
128 self.outgoing.push(Pulse {
129 source_id: self.instance_id,
130 timestamp_ms: now_ms,
131 query_hash,
132 activations: spatial,
133 layer_hint,
134 strength: avg_score,
135 });
136
137 if self.outgoing.len() > MAX_PULSES {
139 self.outgoing.drain(0..self.outgoing.len() - MAX_PULSES);
140 }
141 }
142
143 pub fn receive_pulse(
146 &mut self,
147 pulse: Pulse,
148 local_headers: &[(f32, f32, f32)],
149 proximity_threshold: f32,
150 ) -> u32 {
151 if pulse.source_id == self.instance_id {
152 return 0; }
154
155 let mut local_matches = 0u32;
156
157 for &(px, py, pz, score) in &pulse.activations {
160 let qx = quantize(px);
162 let qy = quantize(py);
163 let qz = quantize(pz);
164 let field_entry = self.field.entry((qx, qy, qz)).or_insert(0.0);
165 *field_entry += score * pulse.strength;
166
167 for (lx, ly, lz) in local_headers {
169 let dx = px - lx;
170 let dy = py - ly;
171 let dz = pz - lz;
172 let dist_sq = dx * dx + dy * dy + dz * dz;
173 if dist_sq < proximity_threshold * proximity_threshold {
174 local_matches += 1;
175 break; }
177 }
178 }
179
180 self.incoming.push(ReceivedPulse {
181 pulse,
182 local_matches,
183 integrated: false,
184 });
185
186 if self.incoming.len() > MAX_PULSES {
188 self.incoming.drain(0..self.incoming.len() - MAX_PULSES);
189 }
190
191 local_matches
192 }
193
194 pub fn integrate_into_hebbian(
197 &mut self,
198 hebb: &mut hebbian::HebbianState,
199 local_headers: &[(f32, f32, f32)],
200 proximity_threshold: f32,
201 ) -> usize {
202 self.integrate_into_hebbian_with_projects(
203 hebb,
204 local_headers,
205 proximity_threshold,
206 None,
207 None,
208 )
209 }
210
211 pub fn integrate_into_hebbian_with_projects(
213 &mut self,
214 hebb: &mut hebbian::HebbianState,
215 local_headers: &[(f32, f32, f32)],
216 proximity_threshold: f32,
217 project_ids: Option<&[ProjectId]>,
218 opts: Option<&MemoryQueryOptions>,
219 ) -> usize {
220 let opts = opts.cloned().unwrap_or_default();
221 let mut influenced = 0usize;
222
223 for received in &mut self.incoming {
224 if received.integrated {
225 continue;
226 }
227
228 for &(px, py, pz, score) in &received.pulse.activations {
229 for (block_idx, (lx, ly, lz)) in local_headers.iter().enumerate() {
230 if block_idx >= hebb.activations.len() {
231 continue;
232 }
233 if let Some(pids) = project_ids {
234 if let Some(&pid) = pids.get(block_idx) {
235 if pid != opts.active_project
236 && !(opts.include_global && pid.is_global())
237 {
238 continue;
239 }
240 }
241 }
242
243 let dx = px - lx;
244 let dy = py - ly;
245 let dz = pz - lz;
246 let dist_sq = dx * dx + dy * dy + dz * dz;
247
248 if dist_sq < proximity_threshold * proximity_threshold {
249 let boost = score
251 * 0.1
252 * (1.0 - dist_sq / (proximity_threshold * proximity_threshold));
253 hebb.activations[block_idx].energy =
254 (hebb.activations[block_idx].energy + boost).min(1.0);
255 influenced += 1;
256 }
257 }
258 }
259
260 received.integrated = true;
261 }
262
263 influenced
264 }
265
266 pub fn field_strength(&self, x: f32, y: f32, z: f32) -> f32 {
268 let qx = quantize(x);
269 let qy = quantize(y);
270 let qz = quantize(z);
271
272 let mut total = 0.0f32;
274 for dx in -1..=1i16 {
275 for dy in -1..=1i16 {
276 for dz in -1..=1i16 {
277 if let Some(&v) = self.field.get(&(qx + dx, qy + dy, qz + dz)) {
278 let dist = ((dx * dx + dy * dy + dz * dz) as f32).sqrt();
279 let weight = 1.0 / (1.0 + dist);
280 total += v * weight;
281 }
282 }
283 }
284 }
285 total
286 }
287
288 pub fn decay_field(&mut self, factor: f32) {
290 self.field.retain(|_, v| {
291 *v *= factor;
292 *v > 0.01
293 });
294 }
295
296 pub fn expire_pulses(&mut self) {
298 let now_ms = hebbian::now_epoch_ms_pub();
299 self.outgoing
300 .retain(|p| now_ms - p.timestamp_ms < PULSE_TTL_MS);
301 self.incoming
302 .retain(|r| now_ms - r.pulse.timestamp_ms < PULSE_TTL_MS);
303 }
304
305 pub fn stats(&self) -> ResonanceStats {
307 let pending_incoming = self.incoming.iter().filter(|r| !r.integrated).count();
308 let field_cells = self.field.len();
309 let field_energy: f32 = self.field.values().sum();
310 let unique_sources: usize = {
311 let mut s: Vec<u64> = self.incoming.iter().map(|r| r.pulse.source_id).collect();
312 s.sort_unstable();
313 s.dedup();
314 s.len()
315 };
316
317 ResonanceStats {
318 instance_id: self.instance_id,
319 outgoing_pulses: self.outgoing.len(),
320 incoming_pulses: self.incoming.len(),
321 pending_integration: pending_incoming,
322 unique_sources,
323 field_cells,
324 field_energy,
325 }
326 }
327
328 pub fn export_pulses(&self) -> Vec<u8> {
330 encode_pulses(&self.outgoing)
331 }
332
333 pub fn import_pulses(data: &[u8]) -> Vec<Pulse> {
335 decode_pulses(data)
336 }
337
338 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
340 save_resonance_state(output_dir, self)
341 }
342}
343
344pub struct ResonanceStats {
345 pub instance_id: u64,
346 pub outgoing_pulses: usize,
347 pub incoming_pulses: usize,
348 pub pending_integration: usize,
349 pub unique_sources: usize,
350 pub field_cells: usize,
351 pub field_energy: f32,
352}
353
354fn quantize(v: f32) -> i16 {
358 (v * 20.0).round() as i16
359}
360
361fn path_hash(path: &Path) -> u64 {
363 let s = path.to_string_lossy();
364 let mut h: u64 = 0xcbf29ce484222325;
365 for &b in s.as_bytes() {
366 h = h.wrapping_mul(0x100000001b3) ^ b as u64;
367 }
368 h
369}
370
371fn encode_pulses(pulses: &[Pulse]) -> Vec<u8> {
381 let mut buf = Vec::new();
382 buf.extend_from_slice(b"PXC1");
383 buf.extend_from_slice(&(pulses.len() as u32).to_le_bytes());
384
385 for p in pulses {
386 buf.extend_from_slice(&p.source_id.to_le_bytes());
387 buf.extend_from_slice(&p.timestamp_ms.to_le_bytes());
388 buf.extend_from_slice(&p.query_hash.to_le_bytes());
389 buf.push(p.layer_hint);
390 buf.extend_from_slice(&p.strength.to_le_bytes());
391 buf.extend_from_slice(&(p.activations.len() as u16).to_le_bytes());
392 for &(x, y, z, s) in &p.activations {
393 buf.extend_from_slice(&x.to_le_bytes());
394 buf.extend_from_slice(&y.to_le_bytes());
395 buf.extend_from_slice(&z.to_le_bytes());
396 buf.extend_from_slice(&s.to_le_bytes());
397 }
398 }
399 buf
400}
401
402fn decode_pulses(data: &[u8]) -> Vec<Pulse> {
403 let mut pulses = Vec::new();
404 if data.len() < 8 || &data[0..4] != b"PXC1" {
405 return pulses;
406 }
407
408 let count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
409 let mut pos = 8;
410
411 for _ in 0..count {
412 if pos + 29 > data.len() {
413 break;
414 }
415 let source_id = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
416 let timestamp_ms = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
417 let query_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
418 let layer_hint = data[pos + 24];
419 let strength = f32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap());
420 let act_count = u16::from_le_bytes(data[pos + 29..pos + 31].try_into().unwrap()) as usize;
421 pos += 31;
422
423 let mut activations = Vec::with_capacity(act_count);
424 for _ in 0..act_count {
425 if pos + 16 > data.len() {
426 break;
427 }
428 let x = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
429 let y = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
430 let z = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
431 let s = f32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
432 activations.push((x, y, z, s));
433 pos += 16;
434 }
435
436 pulses.push(Pulse {
437 source_id,
438 timestamp_ms,
439 query_hash,
440 activations,
441 layer_hint,
442 strength,
443 });
444 }
445 pulses
446}
447
448fn load_resonance_state(output_dir: &Path, _instance_id: u64) -> Option<ResonanceState> {
454 let path = output_dir.join("pulses.bin");
455 let data = fs::read(&path).ok()?;
456 if data.len() < 24 || &data[0..4] != b"PLS1" {
457 return None;
458 }
459
460 let stored_id = u64::from_le_bytes(data[4..12].try_into().unwrap());
461 let outgoing_count = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize;
462 let incoming_count = u32::from_le_bytes(data[16..20].try_into().unwrap()) as usize;
463 let field_count = u32::from_le_bytes(data[20..24].try_into().unwrap()) as usize;
464
465 let mut pos = 24;
466
467 let mut outgoing = Vec::with_capacity(outgoing_count);
469 for _ in 0..outgoing_count {
470 if pos + 31 > data.len() {
471 break;
472 }
473 let source_id = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
474 let timestamp_ms = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
475 let query_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
476 let layer_hint = data[pos + 24];
477 let strength = f32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap());
478 let act_count = u16::from_le_bytes(data[pos + 29..pos + 31].try_into().unwrap()) as usize;
479 pos += 31;
480
481 let mut activations = Vec::with_capacity(act_count);
482 for _ in 0..act_count {
483 if pos + 16 > data.len() {
484 break;
485 }
486 let x = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
487 let y = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
488 let z = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
489 let s = f32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
490 activations.push((x, y, z, s));
491 pos += 16;
492 }
493
494 outgoing.push(Pulse {
495 source_id,
496 timestamp_ms,
497 query_hash,
498 activations,
499 layer_hint,
500 strength,
501 });
502 }
503
504 let mut incoming = Vec::with_capacity(incoming_count);
506 for _ in 0..incoming_count {
507 if pos + 36 > data.len() {
508 break;
509 }
510 let source_id = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
511 let timestamp_ms = u64::from_le_bytes(data[pos + 8..pos + 16].try_into().unwrap());
512 let query_hash = u64::from_le_bytes(data[pos + 16..pos + 24].try_into().unwrap());
513 let layer_hint = data[pos + 24];
514 let strength = f32::from_le_bytes(data[pos + 25..pos + 29].try_into().unwrap());
515 let act_count = u16::from_le_bytes(data[pos + 29..pos + 31].try_into().unwrap()) as usize;
516 pos += 31;
517
518 let mut activations = Vec::with_capacity(act_count);
519 for _ in 0..act_count {
520 if pos + 16 > data.len() {
521 break;
522 }
523 let x = f32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
524 let y = f32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
525 let z = f32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
526 let s = f32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
527 activations.push((x, y, z, s));
528 pos += 16;
529 }
530
531 if pos + 5 > data.len() {
532 break;
533 }
534 let local_matches = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
535 let integrated = data[pos + 4] != 0;
536 pos += 5;
537
538 incoming.push(ReceivedPulse {
539 pulse: Pulse {
540 source_id,
541 timestamp_ms,
542 query_hash,
543 activations,
544 layer_hint,
545 strength,
546 },
547 local_matches,
548 integrated,
549 });
550 }
551
552 let mut field = HashMap::with_capacity(field_count);
554 for _ in 0..field_count {
555 if pos + 10 > data.len() {
556 break;
557 }
558 let qx = i16::from_le_bytes(data[pos..pos + 2].try_into().unwrap());
559 let qy = i16::from_le_bytes(data[pos + 2..pos + 4].try_into().unwrap());
560 let qz = i16::from_le_bytes(data[pos + 4..pos + 6].try_into().unwrap());
561 let v = f32::from_le_bytes(data[pos + 6..pos + 10].try_into().unwrap());
562 pos += 10;
563 field.insert((qx, qy, qz), v);
564 }
565
566 Some(ResonanceState {
567 instance_id: stored_id,
568 outgoing,
569 incoming,
570 field,
571 })
572}
573
574fn save_resonance_state(output_dir: &Path, state: &ResonanceState) -> Result<(), String> {
575 let path = output_dir.join("pulses.bin");
576 let tmp_path = output_dir.join("pulses.bin.tmp");
577 let mut buf = Vec::new();
578
579 buf.extend_from_slice(b"PLS1");
581 buf.extend_from_slice(&state.instance_id.to_le_bytes());
582 buf.extend_from_slice(&(state.outgoing.len() as u32).to_le_bytes());
583 buf.extend_from_slice(&(state.incoming.len() as u32).to_le_bytes());
584 buf.extend_from_slice(&(state.field.len() as u32).to_le_bytes());
585
586 for p in &state.outgoing {
588 buf.extend_from_slice(&p.source_id.to_le_bytes());
589 buf.extend_from_slice(&p.timestamp_ms.to_le_bytes());
590 buf.extend_from_slice(&p.query_hash.to_le_bytes());
591 buf.push(p.layer_hint);
592 buf.extend_from_slice(&p.strength.to_le_bytes());
593 buf.extend_from_slice(&(p.activations.len() as u16).to_le_bytes());
594 for &(x, y, z, s) in &p.activations {
595 buf.extend_from_slice(&x.to_le_bytes());
596 buf.extend_from_slice(&y.to_le_bytes());
597 buf.extend_from_slice(&z.to_le_bytes());
598 buf.extend_from_slice(&s.to_le_bytes());
599 }
600 }
601
602 for r in &state.incoming {
604 buf.extend_from_slice(&r.pulse.source_id.to_le_bytes());
605 buf.extend_from_slice(&r.pulse.timestamp_ms.to_le_bytes());
606 buf.extend_from_slice(&r.pulse.query_hash.to_le_bytes());
607 buf.push(r.pulse.layer_hint);
608 buf.extend_from_slice(&r.pulse.strength.to_le_bytes());
609 buf.extend_from_slice(&(r.pulse.activations.len() as u16).to_le_bytes());
610 for &(x, y, z, s) in &r.pulse.activations {
611 buf.extend_from_slice(&x.to_le_bytes());
612 buf.extend_from_slice(&y.to_le_bytes());
613 buf.extend_from_slice(&z.to_le_bytes());
614 buf.extend_from_slice(&s.to_le_bytes());
615 }
616 buf.extend_from_slice(&r.local_matches.to_le_bytes());
617 buf.push(if r.integrated { 1 } else { 0 });
618 }
619
620 for (&(qx, qy, qz), &v) in &state.field {
622 buf.extend_from_slice(&qx.to_le_bytes());
623 buf.extend_from_slice(&qy.to_le_bytes());
624 buf.extend_from_slice(&qz.to_le_bytes());
625 buf.extend_from_slice(&v.to_le_bytes());
626 }
627
628 fs::write(&tmp_path, &buf).map_err(|e| format!("write pulses.bin: {}", e))?;
629 fs::rename(&tmp_path, &path).map_err(|e| format!("rename pulses.bin: {}", e))
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 #[test]
637 fn test_quantize() {
638 assert_eq!(quantize(0.0), 0);
639 assert_eq!(quantize(0.5), 10);
640 assert_eq!(quantize(1.0), 20);
641 assert_eq!(quantize(-0.25), -5);
642 }
643
644 #[test]
645 fn test_path_hash_deterministic() {
646 let h1 = path_hash(Path::new("/tmp/a"));
647 let h2 = path_hash(Path::new("/tmp/a"));
648 assert_eq!(h1, h2);
649 assert_ne!(h1, path_hash(Path::new("/tmp/b")));
650 }
651
652 #[test]
653 fn test_emit_pulse() {
654 let mut state = ResonanceState {
655 instance_id: 42,
656 outgoing: Vec::new(),
657 incoming: Vec::new(),
658 field: HashMap::new(),
659 };
660
661 let headers = vec![(0.1, 0.2, 0.3), (0.4, 0.5, 0.6), (0.7, 0.8, 0.9)];
662 state.emit_pulse(&[(0, 0.9), (2, 0.7)], 100, &headers, 1);
663
664 assert_eq!(state.outgoing.len(), 1);
665 assert_eq!(state.outgoing[0].activations.len(), 2);
666 assert_eq!(state.outgoing[0].source_id, 42);
667 }
668
669 #[test]
670 fn test_emit_pulse_too_few() {
671 let mut state = ResonanceState {
672 instance_id: 42,
673 outgoing: Vec::new(),
674 incoming: Vec::new(),
675 field: HashMap::new(),
676 };
677
678 let headers = vec![(0.1, 0.2, 0.3)];
679 state.emit_pulse(&[(0, 0.9)], 100, &headers, 1);
680
681 assert!(state.outgoing.is_empty());
683 }
684
685 #[test]
686 fn test_receive_pulse_ignores_self() {
687 let mut state = ResonanceState {
688 instance_id: 42,
689 outgoing: Vec::new(),
690 incoming: Vec::new(),
691 field: HashMap::new(),
692 };
693
694 let pulse = Pulse {
695 source_id: 42, timestamp_ms: 1000,
697 query_hash: 100,
698 activations: vec![(0.1, 0.2, 0.3, 0.9)],
699 layer_hint: 1,
700 strength: 0.8,
701 };
702
703 let matches = state.receive_pulse(pulse, &[(0.1, 0.2, 0.3)], 0.1);
704 assert_eq!(matches, 0);
705 assert!(state.incoming.is_empty());
706 }
707
708 #[test]
709 fn test_receive_pulse_from_other() {
710 let mut state = ResonanceState {
711 instance_id: 42,
712 outgoing: Vec::new(),
713 incoming: Vec::new(),
714 field: HashMap::new(),
715 };
716
717 let pulse = Pulse {
718 source_id: 99, timestamp_ms: 1000,
720 query_hash: 100,
721 activations: vec![(0.1, 0.2, 0.3, 0.9)],
722 layer_hint: 1,
723 strength: 0.8,
724 };
725
726 let local_headers = vec![(0.1, 0.2, 0.3), (0.5, 0.5, 0.5)];
727 let matches = state.receive_pulse(pulse, &local_headers, 0.1);
728
729 assert!(matches > 0); assert_eq!(state.incoming.len(), 1);
731 assert!(!state.incoming[0].integrated);
732 }
733
734 #[test]
735 fn test_field_strength() {
736 let mut state = ResonanceState {
737 instance_id: 42,
738 outgoing: Vec::new(),
739 incoming: Vec::new(),
740 field: HashMap::new(),
741 };
742
743 state.field.insert((2, 4, 6), 1.0); let s = state.field_strength(0.1, 0.2, 0.3);
745 assert!(s > 0.0);
746
747 let s2 = state.field_strength(5.0, 5.0, 5.0);
749 assert!(s2.abs() < 0.001);
750 }
751
752 #[test]
753 fn test_wire_format_roundtrip() {
754 let pulses = vec![
755 Pulse {
756 source_id: 42,
757 timestamp_ms: 12345,
758 query_hash: 999,
759 activations: vec![(0.1, 0.2, 0.3, 0.9), (0.4, 0.5, 0.6, 0.7)],
760 layer_hint: 1,
761 strength: 0.8,
762 },
763 Pulse {
764 source_id: 99,
765 timestamp_ms: 67890,
766 query_hash: 888,
767 activations: vec![(0.7, 0.8, 0.9, 0.5)],
768 layer_hint: 3,
769 strength: 0.6,
770 },
771 ];
772
773 let encoded = encode_pulses(&pulses);
774 let decoded = decode_pulses(&encoded);
775
776 assert_eq!(decoded.len(), 2);
777 assert_eq!(decoded[0].source_id, 42);
778 assert_eq!(decoded[0].activations.len(), 2);
779 assert!((decoded[0].activations[0].0 - 0.1).abs() < 0.001);
780 assert_eq!(decoded[1].query_hash, 888);
781 assert_eq!(decoded[1].layer_hint, 3);
782 }
783
784 #[test]
785 fn test_save_load_roundtrip() {
786 let tmp = tempfile::tempdir().expect("create temp dir");
787 let dir = tmp.path();
788
789 let mut state = ResonanceState {
790 instance_id: 42,
791 outgoing: Vec::new(),
792 incoming: Vec::new(),
793 field: HashMap::new(),
794 };
795
796 state.outgoing.push(Pulse {
798 source_id: 42,
799 timestamp_ms: 1000,
800 query_hash: 100,
801 activations: vec![(0.1, 0.2, 0.3, 0.9)],
802 layer_hint: 1,
803 strength: 0.8,
804 });
805
806 state.incoming.push(ReceivedPulse {
807 pulse: Pulse {
808 source_id: 99,
809 timestamp_ms: 2000,
810 query_hash: 200,
811 activations: vec![(0.4, 0.5, 0.6, 0.7)],
812 layer_hint: 2,
813 strength: 0.6,
814 },
815 local_matches: 3,
816 integrated: true,
817 });
818
819 state.field.insert((2, 4, 6), 1.5);
820
821 state.save(dir).expect("save");
822
823 let loaded = ResonanceState::load_or_init(dir);
824 assert_eq!(loaded.instance_id, 42);
825 assert_eq!(loaded.outgoing.len(), 1);
826 assert_eq!(loaded.outgoing[0].query_hash, 100);
827 assert_eq!(loaded.incoming.len(), 1);
828 assert_eq!(loaded.incoming[0].local_matches, 3);
829 assert!(loaded.incoming[0].integrated);
830 assert!((loaded.field[&(2, 4, 6)] - 1.5).abs() < 0.001);
831 }
832
833 #[test]
834 fn test_integrate_into_hebbian() {
835 let mut state = ResonanceState {
836 instance_id: 42,
837 outgoing: Vec::new(),
838 incoming: Vec::new(),
839 field: HashMap::new(),
840 };
841
842 state.incoming.push(ReceivedPulse {
843 pulse: Pulse {
844 source_id: 99,
845 timestamp_ms: 1000,
846 query_hash: 100,
847 activations: vec![(0.1, 0.2, 0.3, 0.9)],
848 layer_hint: 1,
849 strength: 0.8,
850 },
851 local_matches: 1,
852 integrated: false,
853 });
854
855 let mut hebb = hebbian::HebbianState {
856 activations: vec![hebbian::ActivationRecord::default(); 3],
857 coactivations: HashMap::new(),
858 fingerprints: Vec::new(),
859 };
860
861 let headers = vec![(0.1, 0.2, 0.3), (0.5, 0.5, 0.5), (0.9, 0.9, 0.9)];
862 let influenced = state.integrate_into_hebbian(&mut hebb, &headers, 0.1);
863
864 assert!(influenced > 0);
865 assert!(hebb.activations[0].energy > 0.0); assert!(state.incoming[0].integrated);
867 }
868
869 #[test]
870 fn test_decay_field() {
871 let mut state = ResonanceState {
872 instance_id: 42,
873 outgoing: Vec::new(),
874 incoming: Vec::new(),
875 field: HashMap::new(),
876 };
877
878 state.field.insert((0, 0, 0), 1.0);
879 state.field.insert((1, 1, 1), 0.005); state.decay_field(0.9);
882
883 assert!(state.field.contains_key(&(0, 0, 0)));
884 assert!(!state.field.contains_key(&(1, 1, 1))); }
886}