1use serde::de::{Deserializer, Error as DeError};
31use serde::{Deserialize, Serialize};
32
33use crate::delay::{SpikeDelayBuffer, validate_current_tick};
34use crate::error::{MeshError, Result};
35use crate::topology::{SynapticGraph, TopologyDigest};
36
37#[derive(Clone, Debug, Serialize)]
49pub struct SynapticMesh {
50 graph: SynapticGraph,
52 delay_buffer: SpikeDelayBuffer,
54 tick: u64,
56 #[serde(skip)]
62 pending_scratch: Vec<(usize, usize, f32)>,
63}
64
65#[derive(Deserialize)]
66struct RawSynapticMesh {
67 graph: SynapticGraph,
68 delay_buffer: SpikeDelayBuffer,
69 tick: u64,
70}
71
72impl RawSynapticMesh {
73 fn into_mesh(self) -> std::result::Result<SynapticMesh, String> {
74 if self.graph.neuron_count() != self.delay_buffer.neuron_count() {
75 return Err(format!(
76 "mesh graph neuron_count {} does not match delay buffer neuron_count {}",
77 self.graph.neuron_count(),
78 self.delay_buffer.neuron_count()
79 ));
80 }
81 let graph_max = usize::from(self.graph.max_delay());
82 if self.delay_buffer.max_delay() < graph_max {
83 return Err(format!(
84 "delay buffer max_delay {} is smaller than the graph's maximum delay {graph_max}",
85 self.delay_buffer.max_delay()
86 ));
87 }
88 let max_delay = self.delay_buffer.max_delay();
89 validate_current_tick(self.tick, max_delay)?;
90 validate_current_tick(self.delay_buffer.current_tick(), max_delay)?;
91 if self.tick != self.delay_buffer.current_tick() {
92 return Err(format!(
93 "mesh tick {} does not match delay buffer current_tick {}",
94 self.tick,
95 self.delay_buffer.current_tick()
96 ));
97 }
98 Ok(SynapticMesh::from_parts(
99 self.graph,
100 self.delay_buffer,
101 self.tick,
102 ))
103 }
104}
105
106impl<'de> Deserialize<'de> for SynapticMesh {
107 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
108 where
109 D: Deserializer<'de>,
110 {
111 RawSynapticMesh::deserialize(deserializer)?
112 .into_mesh()
113 .map_err(DeError::custom)
114 }
115}
116
117impl SynapticMesh {
118 pub fn new(graph: SynapticGraph) -> Self {
122 let max_delay = usize::from(graph.max_delay());
123 let n = graph.neuron_count();
124 Self::from_parts(graph, SpikeDelayBuffer::new(n, max_delay), 0)
125 }
126
127 pub fn with_max_delay(graph: SynapticGraph, max_delay: usize) -> Self {
142 Self::try_with_max_delay(graph, max_delay).unwrap_or_else(|err| panic!("{err}"))
143 }
144
145 pub fn try_with_max_delay(graph: SynapticGraph, max_delay: usize) -> Result<Self> {
150 let graph_max = usize::from(graph.max_delay());
151 if max_delay < graph_max {
152 return Err(MeshError::DelayError(format!(
153 "max_delay {max_delay} is smaller than the graph's maximum delay {graph_max}"
154 )));
155 }
156 let n = graph.neuron_count();
157 Ok(Self::from_parts(
158 graph,
159 SpikeDelayBuffer::try_new(n, max_delay)?,
160 0,
161 ))
162 }
163
164 fn from_parts(graph: SynapticGraph, delay_buffer: SpikeDelayBuffer, tick: u64) -> Self {
165 let pending_scratch = Vec::with_capacity(graph.synapse_count());
166 Self {
167 graph,
168 delay_buffer,
169 tick,
170 pending_scratch,
171 }
172 }
173
174 pub fn propagate(&mut self, source_spikes: &[bool]) -> Result<Vec<f32>> {
247 let n = self.graph.neuron_count();
248 require_len(source_spikes.len(), n, "propagate source_spikes")?;
249 let mut currents = vec![0.0; n];
250 self.propagate_into(source_spikes, &mut currents)?;
251 Ok(currents)
252 }
253
254 pub fn propagate_into(&mut self, source_spikes: &[bool], output: &mut [f32]) -> Result<()> {
299 let n = self.graph.neuron_count();
300 require_len(source_spikes.len(), n, "propagate_into source_spikes")?;
301 require_len(output.len(), n, "propagate_into output")?;
302
303 for (src, &fired) in source_spikes.iter().enumerate() {
304 if !fired {
305 continue;
306 }
307 for (target, weight, delay, _polarity) in self.graph.outgoing(src) {
308 self.delay_buffer
309 .inject(target as usize, weight, delay as usize);
310 }
311 }
312
313 self.delay_buffer.drain_current_tick_into(output)?;
314 self.delay_buffer.advance();
315 self.tick += 1;
316 Ok(())
317 }
318
319 pub fn propagate_graded(&mut self, source_activations: &[f32]) -> Result<Vec<f32>> {
335 let n = self.graph.neuron_count();
336 require_len(
337 source_activations.len(),
338 n,
339 "propagate_graded source_activations",
340 )?;
341 let mut currents = vec![0.0; n];
342 self.propagate_graded_into(source_activations, &mut currents)?;
343 Ok(currents)
344 }
345
346 pub fn propagate_graded_into(
373 &mut self,
374 source_activations: &[f32],
375 output: &mut [f32],
376 ) -> Result<()> {
377 let n = self.graph.neuron_count();
378 require_len(
379 source_activations.len(),
380 n,
381 "propagate_graded_into source_activations",
382 )?;
383 require_len(output.len(), n, "propagate_graded_into output")?;
384
385 self.pending_scratch.clear();
388 for (src, &activation) in source_activations.iter().enumerate() {
389 if !activation.is_finite() {
390 self.pending_scratch.clear();
391 return Err(MeshError::InvalidConfig(format!(
392 "propagate_graded source_activations[{src}] must be finite, got {activation}"
393 )));
394 }
395 if activation.abs() < 1e-9 {
396 continue;
397 }
398 for (target, weight, delay, _) in self.graph.outgoing(src) {
399 let current = weight * activation;
400 if !current.is_finite() {
401 self.pending_scratch.clear();
402 return Err(MeshError::InvalidConfig(format!(
403 "propagate_graded source_activations[{src}] * synapse weight must be finite, got {current}"
404 )));
405 }
406 accumulate_slot_current(
407 &mut self.pending_scratch,
408 target as usize,
409 delay as usize,
410 current,
411 );
412 }
413 }
414
415 for &(target, delay, additional) in &self.pending_scratch {
416 let total = self.delay_buffer.scheduled_current(target, delay) + additional;
417 if !additional.is_finite() || !total.is_finite() {
418 self.pending_scratch.clear();
419 return Err(MeshError::InvalidConfig(format!(
420 "propagate_graded would produce a non-finite delay-buffer total for target {target}"
421 )));
422 }
423 }
424
425 for target in 0..n {
426 let current = self.delay_buffer.scheduled_current(target, 0);
427 if !current.is_finite() {
428 self.pending_scratch.clear();
429 return Err(MeshError::InvalidConfig(format!(
430 "propagate_graded would produce a non-finite delay-buffer total for target {target}"
431 )));
432 }
433 }
434
435 for (target, delay, additional) in self.pending_scratch.iter().copied() {
436 self.delay_buffer.inject(target, additional, delay);
437 }
438 self.pending_scratch.clear();
439
440 self.delay_buffer.drain_current_tick_into(output)?;
441 self.delay_buffer.advance();
442 self.tick += 1;
443 Ok(())
444 }
445
446 pub fn tick(&self) -> u64 {
448 self.tick
449 }
450
451 pub fn neuron_count(&self) -> usize {
453 self.graph.neuron_count()
454 }
455
456 pub fn synapse_count(&self) -> usize {
458 self.graph.synapse_count()
459 }
460
461 pub fn sparsity(&self) -> f32 {
463 self.graph.sparsity()
464 }
465
466 pub fn max_delay(&self) -> usize {
468 self.delay_buffer.max_delay()
469 }
470
471 pub fn mean_degree(&self) -> f32 {
473 self.graph.mean_degree()
474 }
475
476 pub fn graph(&self) -> &SynapticGraph {
478 &self.graph
479 }
480
481 #[must_use]
485 pub fn topology_digest(&self) -> TopologyDigest {
486 self.graph.topology_digest()
487 }
488
489 pub fn reset(&mut self) {
491 self.delay_buffer.reset();
492 self.pending_scratch.clear();
493 self.tick = 0;
494 }
495
496 pub fn to_gpu_arrays(&self) -> (Vec<u32>, Vec<u32>, Vec<f32>, Vec<u16>) {
498 self.graph.to_gpu_arrays()
499 }
500}
501
502fn require_len(got: usize, expected: usize, context: &str) -> Result<()> {
503 if got != expected {
504 return Err(MeshError::NeuronCountMismatch {
505 expected,
506 got,
507 context: context.into(),
508 });
509 }
510 Ok(())
511}
512
513fn accumulate_slot_current(
514 pending: &mut Vec<(usize, usize, f32)>,
515 target: usize,
516 delay: usize,
517 current: f32,
518) {
519 if let Some((_, _, total)) = pending
520 .iter_mut()
521 .find(|(t, d, _)| *t == target && *d == delay)
522 {
523 *total += current;
524 } else {
525 pending.push((target, delay, current));
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use crate::topology::{generate_layered, generate_random, generate_small_world};
533
534 #[test]
535 fn topology_digest_matches_graph_and_ignores_tick() {
536 let graph = two_neuron_delay_graph(1);
537 let mut mesh = SynapticMesh::new(graph.clone());
538 let before = mesh.topology_digest();
539 assert_eq!(before, graph.topology_digest());
540 let _ = mesh.propagate(&[true, false]).unwrap();
541 assert_eq!(
542 mesh.topology_digest(),
543 before,
544 "tick / delay-buffer state must not enter the topology digest"
545 );
546 }
547
548 fn two_neuron_delay_graph(delay: u16) -> SynapticGraph {
549 use crate::types::{Polarity, SynapseDescriptor};
550 SynapticGraph::from_descriptors(
551 2,
552 &[SynapseDescriptor {
553 source: 0,
554 target: 1,
555 weight: 1.0,
556 delay,
557 polarity: Polarity::Excitatory,
558 }],
559 )
560 .unwrap()
561 }
562
563 fn mesh_json_with_override(
564 mesh: &SynapticMesh,
565 patch: impl FnOnce(&mut serde_json::Value),
566 ) -> String {
567 let mut value = serde_json::to_value(mesh).unwrap();
568 patch(&mut value);
569 serde_json::to_string(&value).unwrap()
570 }
571
572 #[test]
573 fn deserialize_rejects_graph_buffer_neuron_count_mismatch() {
574 let mesh = SynapticMesh::new(two_neuron_delay_graph(0));
575 let json = mesh_json_with_override(&mesh, |v| {
576 v["delay_buffer"] = serde_json::to_value(SpikeDelayBuffer::new(3, 0)).unwrap();
577 });
578 assert!(serde_json::from_str::<SynapticMesh>(&json).is_err());
579 }
580
581 #[test]
582 fn deserialize_rejects_inadequate_buffer_capacity() {
583 let mesh = SynapticMesh::new(two_neuron_delay_graph(2));
584 let json = mesh_json_with_override(&mesh, |v| {
585 v["delay_buffer"] = serde_json::to_value(SpikeDelayBuffer::new(2, 1)).unwrap();
586 });
587 assert!(serde_json::from_str::<SynapticMesh>(&json).is_err());
588 }
589
590 #[test]
591 fn deserialize_rejects_tick_mismatch() {
592 let mesh = SynapticMesh::new(two_neuron_delay_graph(0));
593 let json = mesh_json_with_override(&mesh, |v| {
594 v["tick"] = serde_json::json!(3);
595 });
596 assert!(serde_json::from_str::<SynapticMesh>(&json).is_err());
597 }
598
599 #[test]
600 fn deserialize_rejects_tick_that_advances_to_usize_max() {
601 let tick = usize::MAX as u64 - 1;
602 for delay in [0_u16, 1] {
603 let mesh = SynapticMesh::new(two_neuron_delay_graph(delay));
604 let json = mesh_json_with_override(&mesh, |v| {
605 v["tick"] = serde_json::json!(tick);
606 v["delay_buffer"]["current_tick"] = serde_json::json!(tick);
607 });
608 assert!(
609 serde_json::from_str::<SynapticMesh>(&json).is_err(),
610 "synchronized tick usize::MAX - 1 with max_delay {delay} must be rejected"
611 );
612 }
613 }
614
615 #[test]
616 fn checkpoint_roundtrip_zero_delay_and_reset() {
617 let mut mesh = SynapticMesh::new(two_neuron_delay_graph(0));
618 let empty: SynapticMesh =
619 serde_json::from_str(&serde_json::to_string(&mesh).unwrap()).unwrap();
620 assert_eq!(empty.tick(), 0);
621
622 let _ = mesh.propagate(&[true, false]).unwrap();
623 mesh.reset();
624 let restored: SynapticMesh =
625 serde_json::from_str(&serde_json::to_string(&mesh).unwrap()).unwrap();
626 assert_eq!(restored.tick(), 0);
627 assert_eq!(
628 mesh.propagate(&[true, false]).unwrap(),
629 restored.clone().propagate(&[true, false]).unwrap()
630 );
631 }
632
633 #[test]
637 fn checkpoint_resume_with_spikes_in_flight_matches_live() {
638 let mut live = SynapticMesh::new(two_neuron_delay_graph(2));
639 assert_eq!(live.propagate(&[true, false]).unwrap()[1], 0.0);
641 let json = serde_json::to_string(&live).unwrap();
642 let mut restored: SynapticMesh = serde_json::from_str(&json).unwrap();
643
644 let inputs = [
646 [false, false],
647 [true, false],
648 [false, false],
649 [false, false],
650 [true, false],
651 [false, false],
652 [false, false],
653 [false, false],
654 ];
655 for spikes in inputs {
656 let a = live.propagate(&spikes).unwrap();
657 let b = restored.propagate(&spikes).unwrap();
658 assert_eq!(a, b);
659 assert_eq!(live.tick(), restored.tick());
660 }
661 }
662
663 #[test]
664 fn try_with_max_delay_rejects_insufficient_capacity() {
665 use crate::types::{Polarity, SynapseDescriptor};
666 let graph = SynapticGraph::from_descriptors(
667 2,
668 &[SynapseDescriptor {
669 source: 0,
670 target: 1,
671 weight: 1.0,
672 delay: 2,
673 polarity: Polarity::Excitatory,
674 }],
675 )
676 .unwrap();
677 let err = SynapticMesh::try_with_max_delay(graph, 1).unwrap_err();
678 assert!(
679 err.to_string().contains("smaller than the graph"),
680 "unexpected error: {err}"
681 );
682 }
683
684 #[test]
685 #[should_panic(expected = "smaller than the graph")]
686 fn with_max_delay_rejects_insufficient_capacity() {
687 use crate::types::{Polarity, SynapseDescriptor};
688 let graph = SynapticGraph::from_descriptors(
689 2,
690 &[SynapseDescriptor {
691 source: 0,
692 target: 1,
693 weight: 1.0,
694 delay: 2,
695 polarity: Polarity::Excitatory,
696 }],
697 )
698 .unwrap();
699 let _ = SynapticMesh::with_max_delay(graph, 1);
700 }
701
702 #[test]
703 fn with_max_delay_accepts_equal_and_larger_capacity() {
704 use crate::types::{Polarity, SynapseDescriptor};
705 let graph = SynapticGraph::from_descriptors(
706 2,
707 &[SynapseDescriptor {
708 source: 0,
709 target: 1,
710 weight: 1.0,
711 delay: 2,
712 polarity: Polarity::Excitatory,
713 }],
714 )
715 .unwrap();
716 let equal = SynapticMesh::try_with_max_delay(graph.clone(), 2).unwrap();
717 assert_eq!(equal.max_delay(), 2);
718 let larger = SynapticMesh::with_max_delay(graph, 5);
719 assert_eq!(larger.max_delay(), 5);
720 }
721
722 #[test]
723 fn propagate_length_mismatch_rejected() {
724 let graph = generate_random(10, 0.5, 3, 0.2).unwrap();
725 let mut mesh = SynapticMesh::new(graph);
726 let bad_spikes = vec![false; 5]; assert!(mesh.propagate(&bad_spikes).is_err());
728 }
729
730 #[test]
731 fn no_spikes_no_current() {
732 let graph = generate_random(10, 0.5, 3, 0.2).unwrap();
733 let mut mesh = SynapticMesh::new(graph);
734 let spikes = vec![false; 10];
735 let currents = mesh.propagate(&spikes).unwrap();
736 assert!(currents.iter().all(|&c| c == 0.0));
737 }
738
739 #[test]
740 fn instant_delivery_with_zero_delay() {
741 use crate::types::{Polarity, SynapseDescriptor};
743 let desc = vec![SynapseDescriptor {
744 source: 0,
745 target: 1,
746 weight: 0.8,
747 delay: 0,
748 polarity: Polarity::Excitatory,
749 }];
750 let graph = SynapticGraph::from_descriptors(2, &desc).unwrap();
751 let mut mesh = SynapticMesh::new(graph);
752
753 let mut spikes = vec![false; 2];
754 spikes[0] = true;
755 let currents = mesh.propagate(&spikes).unwrap();
756 assert!((currents[1] - 0.8).abs() < 1e-6);
757 }
758
759 #[test]
760 fn delayed_delivery_through_mesh() {
761 use crate::types::{Polarity, SynapseDescriptor};
762 let desc = vec![SynapseDescriptor {
763 source: 0,
764 target: 1,
765 weight: 1.0,
766 delay: 2,
767 polarity: Polarity::Excitatory,
768 }];
769 let graph = SynapticGraph::from_descriptors(2, &desc).unwrap();
770 let mut mesh = SynapticMesh::new(graph);
771
772 let mut spikes = vec![false; 2];
774 spikes[0] = true;
775 let c0 = mesh.propagate(&spikes).unwrap();
776 assert_eq!(c0[1], 0.0); spikes[0] = false;
780 let c1 = mesh.propagate(&spikes).unwrap();
781 assert_eq!(c1[1], 0.0); let c2 = mesh.propagate(&spikes).unwrap();
785 assert!((c2[1] - 1.0).abs() < 1e-6);
786 }
787
788 #[test]
789 fn propagate_graded_rejects_non_finite_before_mutation() {
790 use crate::types::{Polarity, SynapseDescriptor};
791 let graph = SynapticGraph::from_descriptors(
792 2,
793 &[SynapseDescriptor {
794 source: 0,
795 target: 1,
796 weight: 1.0,
797 delay: 2,
798 polarity: Polarity::Excitatory,
799 }],
800 )
801 .unwrap();
802 let mut mesh = SynapticMesh::new(graph);
803
804 mesh.propagate(&[true, false]).unwrap();
806 assert_eq!(mesh.tick(), 1);
807
808 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
809 let err = mesh.propagate_graded(&[0.0, bad]).unwrap_err();
810 assert!(
811 err.to_string().contains("must be finite"),
812 "unexpected error for {bad}: {err}"
813 );
814 assert_eq!(mesh.tick(), 1, "tick must not advance on rejection");
815 }
816
817 assert_eq!(mesh.propagate(&[false, false]).unwrap()[1], 0.0);
820 let arrived = mesh.propagate(&[false, false]).unwrap();
821 assert!((arrived[1] - 1.0).abs() < 1e-6);
822 }
823
824 #[test]
825 fn propagate_graded_prevalidates_later_elements() {
826 use crate::types::{Polarity, SynapseDescriptor};
827 let graph = SynapticGraph::from_descriptors(
828 3,
829 &[
830 SynapseDescriptor {
831 source: 0,
832 target: 1,
833 weight: 1.0,
834 delay: 0,
835 polarity: Polarity::Excitatory,
836 },
837 SynapseDescriptor {
838 source: 2,
839 target: 1,
840 weight: 1.0,
841 delay: 0,
842 polarity: Polarity::Excitatory,
843 },
844 ],
845 )
846 .unwrap();
847 let mut mesh = SynapticMesh::new(graph);
848 assert!(mesh.propagate_graded(&[1.0, 0.0, f32::NAN]).is_err());
850 assert_eq!(mesh.tick(), 0);
851 let currents = mesh.propagate(&[false, false, false]).unwrap();
852 assert!(currents.iter().all(|&c| c == 0.0));
853 }
854
855 #[test]
856 fn propagate_graded_rejects_non_finite_weight_activation_product() {
857 use crate::types::{Polarity, SynapseDescriptor};
858 let graph = SynapticGraph::from_descriptors(
859 3,
860 &[
861 SynapseDescriptor {
862 source: 0,
863 target: 1,
864 weight: 1.0,
865 delay: 2,
866 polarity: Polarity::Excitatory,
867 },
868 SynapseDescriptor {
869 source: 2,
870 target: 1,
871 weight: f32::MAX,
872 delay: 0,
873 polarity: Polarity::Excitatory,
874 },
875 ],
876 )
877 .unwrap();
878 let mut mesh = SynapticMesh::new(graph);
879
880 mesh.propagate(&[true, false, false]).unwrap();
881 assert_eq!(mesh.tick(), 1);
882
883 let err = mesh.propagate_graded(&[0.0, 0.0, f32::MAX]).unwrap_err();
884 assert!(
885 err.to_string().contains("must be finite"),
886 "unexpected error: {err}"
887 );
888 assert_eq!(
889 mesh.tick(),
890 1,
891 "tick must not advance on overflow rejection"
892 );
893
894 assert!(mesh.propagate_graded(&[1.0, 0.0, f32::MAX]).is_err());
896 assert_eq!(mesh.tick(), 1);
897
898 assert_eq!(mesh.propagate(&[false, false, false]).unwrap()[1], 0.0);
899 let arrived = mesh.propagate(&[false, false, false]).unwrap();
900 assert!(
901 (arrived[1] - 1.0).abs() < 1e-6,
902 "in-flight current must be unchanged, got {}",
903 arrived[1]
904 );
905 }
906
907 #[test]
908 fn propagate_graded_rejects_aggregate_overflow_before_mutation() {
909 use crate::types::{Polarity, SynapseDescriptor};
910 let graph = SynapticGraph::from_descriptors(
911 3,
912 &[
913 SynapseDescriptor {
914 source: 0,
915 target: 1,
916 weight: f32::MAX,
917 delay: 0,
918 polarity: Polarity::Excitatory,
919 },
920 SynapseDescriptor {
921 source: 2,
922 target: 1,
923 weight: f32::MAX,
924 delay: 0,
925 polarity: Polarity::Excitatory,
926 },
927 ],
928 )
929 .unwrap();
930 let mut mesh = SynapticMesh::new(graph);
931
932 let err = mesh.propagate_graded(&[1.0, 0.0, 1.0]).unwrap_err();
933 assert!(
934 err.to_string().contains("non-finite delay-buffer total"),
935 "unexpected error: {err}"
936 );
937 assert_eq!(mesh.tick(), 0);
938 let currents = mesh.propagate(&[false, false, false]).unwrap();
939 assert!(currents.iter().all(|&c| c == 0.0));
940 }
941
942 #[test]
943 fn propagate_graded_rejects_overflow_with_existing_buffer_current() {
944 use crate::types::{Polarity, SynapseDescriptor};
945 let graph = SynapticGraph::from_descriptors(
946 2,
947 &[
948 SynapseDescriptor {
949 source: 0,
950 target: 1,
951 weight: f32::MAX,
952 delay: 0,
953 polarity: Polarity::Excitatory,
954 },
955 SynapseDescriptor {
956 source: 0,
957 target: 1,
958 weight: f32::MAX,
959 delay: 1,
960 polarity: Polarity::Excitatory,
961 },
962 ],
963 )
964 .unwrap();
965 let mut mesh = SynapticMesh::new(graph);
966
967 let first = mesh.propagate_graded(&[1.0, 0.0]).unwrap();
969 assert_eq!(first[1], f32::MAX);
970 assert_eq!(mesh.tick(), 1);
971
972 let err = mesh.propagate_graded(&[1.0, 0.0]).unwrap_err();
973 assert!(
974 err.to_string().contains("non-finite delay-buffer total"),
975 "unexpected error: {err}"
976 );
977 assert_eq!(
978 mesh.tick(),
979 1,
980 "tick must not advance on aggregate rejection"
981 );
982
983 let arrived = mesh.propagate(&[false, false]).unwrap();
984 assert_eq!(
985 arrived[1],
986 f32::MAX,
987 "parked delay-1 current must be unchanged"
988 );
989 }
990
991 #[test]
992 fn propagate_graded_preserves_signed_finite_activations() {
993 use crate::types::{Polarity, SynapseDescriptor};
994 let graph = SynapticGraph::from_descriptors(
995 2,
996 &[SynapseDescriptor {
997 source: 0,
998 target: 1,
999 weight: 0.5,
1000 delay: 0,
1001 polarity: Polarity::Inhibitory,
1002 }],
1003 )
1004 .unwrap();
1005 let mut mesh = SynapticMesh::new(graph);
1006 let currents = mesh.propagate_graded(&[-0.5, 0.0]).unwrap();
1008 assert!((currents[1] - 0.25).abs() < 1e-6);
1009 }
1010
1011 #[test]
1012 fn graded_propagation() {
1013 use crate::types::{Polarity, SynapseDescriptor};
1014 let desc = vec![SynapseDescriptor {
1015 source: 0,
1016 target: 1,
1017 weight: 0.5,
1018 delay: 0,
1019 polarity: Polarity::Excitatory,
1020 }];
1021 let graph = SynapticGraph::from_descriptors(2, &desc).unwrap();
1022 let mut mesh = SynapticMesh::new(graph);
1023
1024 let activations = vec![0.6, 0.0];
1025 let currents = mesh.propagate_graded(&activations).unwrap();
1026 assert!((currents[1] - 0.3).abs() < 1e-6);
1028 }
1029
1030 #[test]
1031 fn tick_counter_increments() {
1032 let graph = generate_random(8, 0.3, 2, 0.2).unwrap();
1033 let mut mesh = SynapticMesh::new(graph);
1034 assert_eq!(mesh.tick(), 0);
1035 mesh.propagate(&[false; 8]).unwrap();
1036 mesh.propagate(&[false; 8]).unwrap();
1037 assert_eq!(mesh.tick(), 2);
1038 }
1039
1040 #[test]
1041 fn reset_clears_state() {
1042 let graph = generate_random(8, 0.3, 2, 0.2).unwrap();
1043 let mut mesh = SynapticMesh::new(graph);
1044 mesh.propagate(&[true; 8]).unwrap();
1045 mesh.propagate(&[true; 8]).unwrap();
1046 mesh.reset();
1047 assert_eq!(mesh.tick(), 0);
1048 }
1049
1050 #[test]
1051 fn small_world_mesh_propagation() {
1052 let graph = generate_small_world(32, 4, 0.2, 5, 0.2).unwrap();
1053 let mut mesh = SynapticMesh::new(graph);
1054
1055 let mut any_current = false;
1057 for tick in 0..10 {
1058 let mut spikes = vec![false; 32];
1059 if tick == 0 {
1060 spikes[0] = true;
1061 }
1062 let currents = mesh.propagate(&spikes).unwrap();
1063 if currents.iter().any(|&c| c.abs() > 1e-6) {
1064 any_current = true;
1065 }
1066 }
1067 assert!(any_current, "expected some delayed current delivery");
1068 }
1069
1070 #[test]
1071 fn layered_mesh_feed_forward() {
1072 let graph = generate_layered(&[4, 8, 2], 1.0, 3, 0.2).unwrap();
1081 let mut mesh = SynapticMesh::new(graph);
1082 assert_eq!(mesh.neuron_count(), 14);
1083 let mut spikes = vec![false; 14];
1084 for spike in spikes.iter_mut().take(4) {
1085 *spike = true; }
1087
1088 let mut last_layer_activated = false;
1091 for _ in 0..12 {
1092 let currents = mesh.propagate(&spikes).unwrap();
1093
1094 spikes = currents.iter().map(|&c| c > 0.5).collect();
1098
1099 if currents[12].abs() > 1e-6 || currents[13].abs() > 1e-6 {
1101 last_layer_activated = true;
1102 }
1103 }
1104 assert!(
1105 last_layer_activated,
1106 "feed-forward should eventually reach the output layer"
1107 );
1108 }
1109
1110 fn assert_meshes_match(left: &mut SynapticMesh, right: &mut SynapticMesh, spikes: &[bool]) {
1111 let allocated = left.propagate(spikes).unwrap();
1112 let mut reused = vec![7.0; spikes.len()];
1113 right.propagate_into(spikes, &mut reused).unwrap();
1114 assert_eq!(allocated, reused);
1115 assert_eq!(left.tick(), right.tick());
1116 }
1117
1118 #[test]
1119 fn propagate_into_matches_allocating_zero_and_delayed() {
1120 for delay in [0_u16, 2] {
1121 let graph = two_neuron_delay_graph(delay);
1122 let mut alloc = SynapticMesh::new(graph.clone());
1123 let mut reuse = SynapticMesh::new(graph);
1124 assert_meshes_match(&mut alloc, &mut reuse, &[true, false]);
1125 assert_meshes_match(&mut alloc, &mut reuse, &[false, false]);
1126 assert_meshes_match(&mut alloc, &mut reuse, &[false, false]);
1127 }
1128 }
1129
1130 #[test]
1131 fn propagate_graded_into_matches_allocating_signed_and_delayed() {
1132 let graph = two_neuron_delay_graph(2);
1133 let mut alloc = SynapticMesh::new(graph.clone());
1134 let mut reuse = SynapticMesh::new(graph);
1135 for activations in [[0.5, 0.0], [0.0, 0.0], [-0.25, 0.0], [0.0, 0.0]] {
1136 let allocated = alloc.propagate_graded(&activations).unwrap();
1137 let mut reused = vec![7.0; 2];
1138 reuse
1139 .propagate_graded_into(&activations, &mut reused)
1140 .unwrap();
1141 assert_eq!(allocated, reused);
1142 assert_eq!(alloc.tick(), reuse.tick());
1143 }
1144 }
1145
1146 #[test]
1147 fn propagate_into_rejects_wrong_sizes_without_mutation() {
1148 let mut mesh = SynapticMesh::new(two_neuron_delay_graph(2));
1149 mesh.propagate(&[true, false]).unwrap();
1150 let tick = mesh.tick();
1151 let mut sentinel = [42.0_f32, 42.0];
1152
1153 assert!(mesh.propagate_into(&[true], &mut sentinel).is_err());
1154 assert_eq!(mesh.tick(), tick);
1155 assert_eq!(sentinel, [42.0, 42.0]);
1156
1157 let mut too_short = [42.0_f32];
1158 assert!(mesh.propagate_into(&[true, false], &mut too_short).is_err());
1159 assert_eq!(mesh.tick(), tick);
1160 assert_eq!(too_short, [42.0]);
1161
1162 assert_eq!(mesh.propagate(&[false, false]).unwrap()[1], 0.0);
1164 let arrived = mesh.propagate(&[false, false]).unwrap();
1165 assert!((arrived[1] - 1.0).abs() < 1e-6);
1166 }
1167
1168 #[test]
1169 fn propagate_graded_into_rejects_non_finite_without_mutating_output() {
1170 let mut mesh = SynapticMesh::new(two_neuron_delay_graph(2));
1171 mesh.propagate(&[true, false]).unwrap();
1172 let tick = mesh.tick();
1173 let mut sentinel = [42.0_f32, 42.0];
1174
1175 for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1176 let err = mesh
1177 .propagate_graded_into(&[0.0, bad], &mut sentinel)
1178 .unwrap_err();
1179 assert!(
1180 err.to_string().contains("must be finite"),
1181 "unexpected error for {bad}: {err}"
1182 );
1183 assert_eq!(mesh.tick(), tick);
1184 assert_eq!(sentinel, [42.0, 42.0]);
1185 }
1186
1187 assert_eq!(mesh.propagate(&[false, false]).unwrap()[1], 0.0);
1188 let arrived = mesh.propagate(&[false, false]).unwrap();
1189 assert!((arrived[1] - 1.0).abs() < 1e-6);
1190 }
1191
1192 #[test]
1193 fn checkpoint_omits_pending_scratch() {
1194 let mesh = SynapticMesh::new(two_neuron_delay_graph(0));
1195 let value = serde_json::to_value(&mesh).unwrap();
1196 assert!(value.get("pending_scratch").is_none());
1197 let restored: SynapticMesh = serde_json::from_value(value).unwrap();
1198 assert_eq!(restored.neuron_count(), 2);
1199 assert_eq!(restored.tick(), 0);
1200 }
1201
1202 #[test]
1203 fn propagate_graded_into_rejects_non_finite_current_in_delay_buffer_with_empty_scratch() {
1204 let mut mesh = SynapticMesh::new(two_neuron_delay_graph(0));
1205 mesh.delay_buffer.inject(0, f32::NAN, 0);
1206 let tick = mesh.tick();
1207 let mut sentinel = [42.0_f32, 42.0];
1208
1209 let err = mesh
1210 .propagate_graded_into(&[0.0, 0.0], &mut sentinel)
1211 .unwrap_err();
1212 assert!(
1213 err.to_string().contains("non-finite delay-buffer total"),
1214 "unexpected error: {err}"
1215 );
1216 assert_eq!(mesh.tick(), tick);
1217 assert_eq!(sentinel, [42.0, 42.0]);
1218 assert!(mesh.delay_buffer.scheduled_current(0, 0).is_nan());
1219 }
1220}