use synaptic_wiring::mesh::SynapticMesh;
use synaptic_wiring::topology::SynapticGraph;
use synaptic_wiring::types::{Polarity, SynapseDescriptor};
const N: usize = 4;
const EPS: f32 = 1e-6;
fn fixture() -> SynapticMesh {
let descriptors = [
(0, 1, 0.75, 0, Polarity::Excitatory),
(0, 2, 0.50, 1, Polarity::Excitatory),
(0, 3, 0.25, 2, Polarity::Excitatory),
(1, 3, 0.40, 1, Polarity::Excitatory),
(2, 3, 0.60, 1, Polarity::Inhibitory),
]
.map(
|(source, target, weight, delay, polarity)| SynapseDescriptor {
source,
target,
weight,
delay,
polarity,
},
);
let graph = SynapticGraph::from_descriptors(N, &descriptors)
.expect("fixture descriptors are in bounds");
SynapticMesh::new(graph)
}
fn spikes(firing: &[usize]) -> Vec<bool> {
let mut v = vec![false; N];
for &neuron in firing {
v[neuron] = true;
}
v
}
fn assert_currents(tick: u64, actual: &[f32], expected: [f32; N]) {
assert_eq!(actual.len(), N, "tick {tick}: wrong current vector length");
for (neuron, (&got, want)) in actual.iter().zip(expected).enumerate() {
assert!(
(got - want).abs() < EPS,
"tick {tick}, neuron {neuron}: expected {want}, got {got}"
);
}
}
#[test]
fn single_spike_delivers_to_known_destination_at_known_tick() {
let mut mesh = fixture();
let t0 = mesh.propagate(&spikes(&[0])).unwrap();
assert_currents(0, &t0, [0.0, 0.75, 0.0, 0.0]);
let t1 = mesh.propagate(&spikes(&[])).unwrap();
assert_currents(1, &t1, [0.0, 0.0, 0.50, 0.0]);
let t2 = mesh.propagate(&spikes(&[])).unwrap();
assert_currents(2, &t2, [0.0, 0.0, 0.0, 0.25]);
let t3 = mesh.propagate(&spikes(&[])).unwrap();
assert_currents(3, &t3, [0.0; N]);
assert_eq!(mesh.tick(), 4, "one tick advanced per propagate() call");
}
#[test]
fn polarity_signs_the_current_and_co_arrivals_sum() {
let mut mesh = fixture();
let t0 = mesh.propagate(&spikes(&[1, 2])).unwrap();
assert_currents(0, &t0, [0.0; N]);
let t1 = mesh.propagate(&spikes(&[])).unwrap();
assert_currents(1, &t1, [0.0, 0.0, 0.0, -0.20]);
assert!(t1[3] < 0.0, "net inhibition must stay negative");
}
#[test]
fn threshold_and_fire_loop_is_deterministic() {
const THRESHOLD: f32 = 0.3;
fn run() -> Vec<Vec<f32>> {
let mut mesh = fixture();
let mut firing = spikes(&[0]);
let mut trace = Vec::new();
for _ in 0..4 {
let currents = mesh.propagate(&firing).unwrap();
firing = currents.iter().map(|&c| c > THRESHOLD).collect();
trace.push(currents);
}
trace
}
let trace = run();
assert_currents(0, &trace[0], [0.0, 0.75, 0.0, 0.0]);
assert_currents(1, &trace[1], [0.0, 0.0, 0.50, 0.0]);
assert_currents(2, &trace[2], [0.0, 0.0, 0.0, 0.65]);
assert_currents(3, &trace[3], [0.0, 0.0, 0.0, -0.60]);
assert_eq!(trace, run(), "same graph + same spikes ⇒ same currents");
}
#[test]
fn reset_replays_identically_and_drops_in_flight_spikes() {
let mut mesh = fixture();
let first = mesh.propagate(&spikes(&[0])).unwrap();
mesh.reset();
assert_eq!(mesh.tick(), 0);
for tick in 0..=mesh.max_delay() as u64 {
let idle = mesh.propagate(&spikes(&[])).unwrap();
assert_currents(tick, &idle, [0.0; N]);
}
mesh.reset();
let replay = mesh.propagate(&spikes(&[0])).unwrap();
assert_eq!(first, replay);
}
#[test]
fn graded_activation_scales_the_delivered_current() {
let mut mesh = fixture();
let t0 = mesh.propagate_graded(&[0.5, 0.0, 0.0, 0.0]).unwrap();
assert_currents(0, &t0, [0.0, 0.375, 0.0, 0.0]);
let t1 = mesh.propagate_graded(&[0.0; N]).unwrap();
assert_currents(1, &t1, [0.0, 0.0, 0.25, 0.0]);
}
#[test]
fn negative_graded_activation_inverts_polarity() {
let mut mesh = fixture();
let t0 = mesh.propagate_graded(&[-0.5, 0.0, 0.0, 0.0]).unwrap();
assert_currents(0, &t0, [0.0, -0.375, 0.0, 0.0]);
let mut mesh = fixture();
let t0 = mesh.propagate_graded(&[0.0, 0.0, -0.5, 0.0]).unwrap();
assert_currents(0, &t0, [0.0; N]);
let t1 = mesh.propagate_graded(&[0.0; N]).unwrap();
assert_currents(1, &t1, [0.0, 0.0, 0.0, 0.30]);
}
#[test]
fn mesh_reports_fixture_shape_and_rejects_mismatched_input() {
let mut mesh = fixture();
assert_eq!(mesh.neuron_count(), N);
assert_eq!(mesh.synapse_count(), 5);
assert_eq!(
mesh.max_delay(),
2,
"buffer is sized from the graph's delays"
);
assert!(mesh.propagate(&[false; N - 1]).is_err());
assert!(mesh.propagate(&[false; N + 1]).is_err());
}
#[test]
fn propagate_into_matches_allocating_on_fixture() {
let mut alloc = fixture();
let mut reuse = fixture();
let sequence = [spikes(&[0]), spikes(&[]), spikes(&[]), spikes(&[1, 2])];
let mut output = [0.0; N];
for firing in &sequence {
let allocated = alloc.propagate(firing).unwrap();
reuse.propagate_into(firing, &mut output).unwrap();
assert_eq!(allocated.as_slice(), output.as_slice());
assert_eq!(alloc.tick(), reuse.tick());
}
let mut alloc = fixture();
let mut reuse = fixture();
let graded = [[0.5, 0.0, 0.0, 0.0], [0.0; N], [0.0, 0.0, -0.5, 0.0]];
for activations in graded {
let allocated = alloc.propagate_graded(&activations).unwrap();
reuse
.propagate_graded_into(&activations, &mut output)
.unwrap();
assert_eq!(allocated.as_slice(), output.as_slice());
assert_eq!(alloc.tick(), reuse.tick());
}
}