Skip to content

Repository files navigation

synaptic-wiring

synaptic-wiring

SNN wiring, topology generation, and temporal delay infrastructure

version 0.3.0 MIT OR Apache-2.0


synaptic-wiring is the connectivity layer of a spiking neural network: it answers which neuron connects to which, how strongly, and how long the spike takes to get there. You bring the neuron model and the simulation loop; this crate wires the network and delivers each spike to the right target on the right tick.

The Cargo package is synaptic-wiring (this crate was previously named synaptic-mesh). The GitHub repository is Limen-Neural/synaptic-wiring (GitHub redirects the former Limen-Neural/synaptic-mesh URL).

It is a plain library with two runtime dependencies (serde and sha2) — no framework, no runtime, no GPU requirement, and no assumptions about how your neurons integrate current.

Reach for it when you need to:

  • generate a biologically plausible network (small-world, scale-free, layered, or Erdős–Rényi) instead of hand-writing an adjacency matrix,
  • give synapses axonal delays so spikes arrive on a future tick, enabling polychronization and coincidence detection,
  • respect Dale's law — a neuron is excitatory or inhibitory, and all of its outgoing synapses follow,
  • store a large sparse weight matrix compactly (CSR) and hand it to a GPU,
  • get reproducible topologies: the generators hash neuron indices instead of drawing from an RNG, so the same parameters always produce the same network,
  • fingerprint a mesh's logical wiring with topology_digest() for manifests, replay, and cross-runtime comparison.

It deliberately does not implement neuron models (LIF, Izhikevich, …), learning rules, or training loops — those stay in your code or in a neuron-model crate. See Crate boundary.

Core Capabilities

  • Topology Generators — Deterministic generation of Erdős–Rényi (random), Watts–Strogatz (small-world), Barabási–Albert (scale-free), and Layered feed-forward topologies.
  • Temporal Propagation — Per-synapse axonal delays stored alongside weights. Spikes are delivered at the correct future tick via a high-performance ring-buffer queue.
  • Biologically Inspired Wiring — Support for Dale's Law (fixed neuron polarity) and position-based distance-dependent connectivity.
  • Sparse Synaptic Map (CSR) — Compressed Sparse Row format for memory-efficient weight storage (20× reduction for sparse networks).
  • Topology digest — Versioned SHA-256 of the canonical logical graph (sorted edges, IEEE weight bits, delay, polarity) for manifests and provenance.
  • Generic Channel Router (optional) — A configurable multi-channel router for sparse signal classification, usable on its own or ignored entirely.

Where to start

If you want to… Use Section
Wire up a network from a classic model topology::generate_small_world and friends Topology Generation
Run spikes through it, tick by tick SynapticMesh::propagate Quick Start
Hand-build an exact graph SynapticGraph::from_descriptors with SynapseDescriptor Spike delivery contract
Plan polarity or distance-based delays before building a graph topology::apply_dale_polarity (computes a per-neuron polarity vector), topology::assign_delays (rewrites descriptors in place) Temporal Delays
Store a big sparse weight matrix / upload to a GPU SparseSynapticMap, SynapticMesh::to_gpu_arrays Core Capabilities
Record topology in a manifest / compare two graphs SynapticGraph::topology_digest Topology digest
Pick a few active channels out of many inputs ChannelRouter Generic Channel Router

Installation

[dependencies]
synaptic-wiring = "0.3"

0.3.0 is experimental, pre-1.0: the API is usable and tested, but minor releases may still contain breaking changes until 1.0. Pin an exact version (= "0.3.0") if you need stability.

For Development / unreleased main — changes that are not in a release yet — depend on git instead:

synaptic-wiring = { git = "https://github.com/Limen-Neural/synaptic-wiring" }

Add tag = "v0.3.0" to pin a git dependency to a specific release instead of tracking main — see releases for the tags that exist today.

MSRV: Rust 1.98.1. Runtime dependencies: serde, sha2.

Contributors: see REVIEW.md for which cargo build profile (dev, test, release, bench) to use and why.

Quick Start: Building a Mesh

use synaptic_wiring::topology::generate_small_world;
use synaptic_wiring::mesh::SynapticMesh;

// 1. Build a 1024-neuron small-world network with delays up to 10 ticks
// (N=1024, k=6 neighbors, beta=0.1 rewiring, max_delay=10, inh_fraction=0.2)
let graph = generate_small_world(1024, 6, 0.1, 10, 0.2).unwrap();

// 2. Wrap in a Mesh orchestrator that manages temporal state
let mut mesh = SynapticMesh::new(graph);

// 3. Each tick: provide current spikes -> receive time-delayed synaptic currents
let mut spikes = vec![false; 1024];
spikes[0] = true; // neuron 0 fires

let currents = mesh.propagate(&spikes).unwrap();
// currents[i] = total incoming synaptic current at neuron i this tick,
// potentially including delayed spikes from previous ticks.

Spike delivery contract

propagate() is one hop: it converts the spikes you hand it into the currents arriving this tick. It never decides what fires next — that is your neuron model's job. A complete simulation loop is therefore just:

use synaptic_wiring::mesh::SynapticMesh;
use synaptic_wiring::topology::SynapticGraph;
use synaptic_wiring::types::{Polarity, SynapseDescriptor};

// Hand-built graph: 0 ──(w=0.75, delay 0, excitatory)──▶ 1
//                   0 ──(w=0.50, delay 2, excitatory)──▶ 2
let graph = SynapticGraph::from_descriptors(3, &[
    SynapseDescriptor { source: 0, target: 1, weight: 0.75, delay: 0, polarity: Polarity::Excitatory },
    SynapseDescriptor { source: 0, target: 2, weight: 0.50, delay: 2, polarity: Polarity::Excitatory },
]).unwrap();
let mut mesh = SynapticMesh::new(graph);

let mut spikes = vec![false, false, false];
spikes[0] = true;

for _tick in 0..3 {
    let currents = mesh.propagate(&spikes).unwrap();
    // Your neuron model goes here; this one is a bare threshold.
    spikes = currents.iter().map(|&c| c > 0.3).collect();
}

The rules it guarantees:

Question Answer
Who receives the current? Every target of the firing neuron, per the graph.
What sign? The synapse's polarity — inhibitory synapses subtract. The generators assign polarity per source neuron (Dale's law); a hand-built graph gets whatever each descriptor declares.
What magnitude? The synapse weight, multiplied by the activation when using propagate_graded (a negative activation therefore flips the sign).
Which tick? tick_fired + delay; delay = 0 arrives in the same call.
What if two spikes land together? They sum at the destination.
Is it reproducible? Yes — same graph and spikes give the same currents, including after reset().

tests/propagate_contract.rs pins every one of these against a fixed four-neuron graph and is a copyable starting point for your own loop.

A fixed-rate simulation that already owns a current vector can reuse it with propagate_into / propagate_graded_into instead of allocating every tick. output is overwritten with this tick's currents (not accumulated into). Length mismatches and non-finite graded input are rejected before the mesh tick, delay buffers, or output change. After SynapticMesh::new, a successful reuse-path tick does not heap-allocate.

use synaptic_wiring::mesh::SynapticMesh;
use synaptic_wiring::topology::SynapticGraph;
use synaptic_wiring::types::{Polarity, SynapseDescriptor};

let graph = SynapticGraph::from_descriptors(2, &[
    SynapseDescriptor { source: 0, target: 1, weight: 0.75, delay: 0, polarity: Polarity::Excitatory },
]).unwrap();
let mut mesh = SynapticMesh::new(graph);
let spikes = [true, false];
let mut currents = [0.0; 2];
mesh.propagate_into(&spikes, &mut currents).unwrap();
assert_eq!(currents, [0.0, 0.75]);

Topology Generation

synaptic-wiring provides several deterministic models for growing network graphs. All generators use golden-ratio fractional hashing for reproducibility across runs without external RNG dependencies.

Model Generator Best For
Small-World generate_small_world Local clustering with short path lengths (mimics cortical connectivity).
Scale-Free generate_scale_free Networks with "hubs" following a power-law degree distribution.
Random generate_random Erdős–Rényi random graphs for baseline comparisons.
Layered generate_layered Classical feed-forward structures (Input -> Hidden -> Output).

Topology digest

SynapticGraph::topology_digest() (and SynapticMesh::topology_digest()) returns a printable, schema-versioned SHA-256 of the logical graph: neuron count and every edge's source, target, IEEE-754 weight bit pattern, delay, and polarity. Edges are sorted before hashing, so insertion order, CSR layout, host endianness, and JSON formatting cannot change the value.

The string is safe to store in a manifest:

synaptic-wiring.topology.digest.v1:sha256:<64 hex chars>

IEEE +0.0 and -0.0 digest differently (bit patterns are hashed as-is). NaN and infinities cannot appear: graph construction and serde already reject them. This is an identifier, not a cryptographic signature.

See tests/topology_digest.rs for golden values and an insertion-order permutation test.

Temporal Delays & Spike Propagation

In biological networks, spikes do not arrive instantly. synaptic-wiring implements a temporal logic layer using a Ring-Buffer Delay Queue:

  1. Each synapse in the SynapticGraph stores a DelayTicks value.
  2. When a neuron fires, its spike is projected through its outgoing synapses.
  3. The SpikeDelayBuffer schedules delivery at current_tick + delay.
  4. At each tick, propagate() drains the current slot and returns the accumulated currents. propagate_into does the same write into a caller-owned buffer.

This enables complex temporal dynamics like polychronization and coincidence detection.

Generic Channel Router

Optional — skip this section if you only need wiring and delays.

ChannelRouter is a standalone, configurable multi-channel classifier: it integrates input pulses over a bank of internal integrate-and-fire units and returns a sparse activation mask (which channels won, and at what firing rate). It does not use SynapticMesh and SynapticMesh does not use it — they are independent halves of the crate.

use synaptic_wiring::ChannelRouter;

// Default 3-channel router. `route` returns a Result: it errors if the
// signal slice length doesn't match the configured channel count, or if
// any sample is NaN / ±infinity. Finite signed samples are accepted.
let mut router = ChannelRouter::new();
let decision = router.route([0.8, 0.2, 0.1]).unwrap();

assert_eq!(decision.active_channels, vec![0]);
assert_eq!(decision.firing_rates.len(), 3);

Configurable Channel Count

use synaptic_wiring::{ChannelRouter, RouterConfig};

let config = RouterConfig {
    channel_count: 8,
    ..RouterConfig::default()
};
let mut router = ChannelRouter::try_with_config(config).unwrap();
let decision = router.route([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]).unwrap();

assert_eq!(decision.active_channels, vec![3]);

Adaptation-Aware Routing

  • Neuron State Snapshots — Per-neuron adaptation and error tracking for dynamic routing decisions.
  • Routing Policies — Configurable scoring equations that balance spike activity, adaptation penalties, and error bonuses.

Crate boundary

This crate owns connectivity and timing, and nothing else:

In scope Out of scope (bring your own)
Topology generation and hand-built graphs Neuron models — LIF, Izhikevich, Hodgkin-Huxley, …
Per-synapse weights, polarity, axonal delays Learning rules, training loops, optimizers
Tick-aligned spike delivery Simulation scheduling, I/O, encoding of stimuli
CSR sparse maps and GPU-ready arrays GPU kernels, hardware backends
Optional ChannelRouter classifier —

Neuron models are a deliberate omission, not a gap: pair this crate with whatever integrator you already use, or with a dedicated crate such as neuromod (LIF, Izhikevich, Hodgkin-Huxley, GIF, FitzHugh-Nagumo, Lapicque). synaptic-wiring takes no dependency on it, so the two evolve independently.

The one neuron-like type here, NeuromodNeuron in router, is an integration primitive internal to ChannelRouter — not a general-purpose neuron model.

Used by

Spikenaut-SNN uses this crate for Dale-polarity wiring and multi-channel routing. That is a downstream consumer, not a requirement: nothing in the API assumes it, and depending on synaptic-wiring pulls in serde and sha2.

Architecture

┌──────────────────────────────────────────────────┐
│  Source spike vector  [bool; N]                   │
└────────────────┬─────────────────────────────────┘
                 │
       ┌─────────▼─────────┐
       │   SynapticGraph   │  CSR adjacency + delays + polarities
       │   (topology)      │  Generators: random, small-world, etc.
       └─────────┬─────────┘
                 │  per-synapse: (target, weight, delay)
       ┌─────────▼─────────┐
       │  SpikeDelayBuffer │  Ring-buffer delay queue
       │   (delay)         │  inject() → advance() → drain()
       └─────────┬─────────┘
                 │  tick-aligned delivery
       ┌─────────▼─────────┐
       │  Synaptic current │  Vec<f32> of length N
       │  per target neuron│
       └───────────────────┘

References

Network Topology & Dynamics:

  • Watts, D. J. & Strogatz, S. H. (1998). Collective dynamics of 'small-world' networks. Nature.
  • Barabási, A.-L. & Albert, R. (1999). Emergence of scaling in random networks. Science.
  • Dale, H. H. (1935). Pharmacology and Nerve-endings.

SNN Logic:

  • Bi, G. Q., & Poo, M. M. (1998). Synaptic modifications... Journal of Neuroscience.
  • Maass, W. (2000). On the computational power of winner-take-all. Neural Computation.

License

Dual-licensed under either of

at your option. Contributions intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.

About

An SNN based mesh for managing the wiring, topology, and temporal delays between neurons.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages