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