NIR Bridge — Import Neuromorphic Models into SC-NeuroCore¶
SC-NeuroCore v3.13.3 — NIR (Neuromorphic Intermediate Representation) Integration
This notebook demonstrates how to import NIR graphs into SC-NeuroCore, simulate them with the stochastic computing engine, and inspect the results.
SC-NeuroCore is the first NIR backend targeting FPGA synthesis — every other NIR target is either a simulator or a fixed neuromorphic chip.
Sections:
- Build a simple LIF + Affine NIR graph
- Import into SC-NeuroCore and inspect topology
- Run simulation and plot spike output
- Demonstrate fan-in (multiple inputs summed)
- Stateless pipeline: Linear → Scale → Threshold
- File I/O: save and reload .nir files
© 1998–2026 Miroslav Šotek. All rights reserved. License: GNU AFFERO GENERAL PUBLIC LICENSE v3 | Commercial Licensing Available Contact: www.anulum.li protoscience@anulum.li
import numpy as np
import nir
from sc_neurocore.nir_bridge import from_nir
print(f"NIR version: {nir.__version__}")
print("SC-NeuroCore NIR bridge loaded.")
NIR version: 1.0.7 SC-NeuroCore NIR bridge loaded.
1. Build a LIF + Affine NIR Graph¶
A minimal spiking network: 3 inputs → dense layer (Affine) → 4 LIF neurons → output.
NIR uses shape arrays in input_type/output_type (e.g., np.array([3]) means dimension 3).
n_in, n_out = 3, 4
rng = np.random.RandomState(42)
nodes = {
"input": nir.Input(input_type={"input": np.array([n_in])}),
"affine": nir.Affine(
weight=rng.randn(n_out, n_in).astype(np.float32),
bias=np.zeros(n_out, dtype=np.float32),
),
"lif": nir.LIF(
tau=np.full(n_out, 20.0), # membrane time constant (ms)
r=np.ones(n_out), # membrane resistance
v_leak=np.zeros(n_out), # resting potential
v_threshold=np.ones(n_out), # spike threshold
),
"output": nir.Output(output_type={"output": np.array([n_out])}),
}
edges = [("input", "affine"), ("affine", "lif"), ("lif", "output")]
graph = nir.NIRGraph(nodes=nodes, edges=edges)
print(f"NIR graph: {len(nodes)} nodes, {len(edges)} edges")
for name, node in nodes.items():
print(f" {name}: {type(node).__name__}")
NIR graph: 4 nodes, 3 edges input: Input affine: Affine lif: LIF output: Output
2. Import into SC-NeuroCore¶
from_nir() parses the NIR graph, maps each node to an SC-NeuroCore primitive,
topologically sorts the execution order, and returns an executable SCNetwork.
network = from_nir(graph)
print(network.summary())
SCNetwork: 4 nodes, 3 edges input: SCInputNode affine: SCAffineNode lif: SCLIFNode output: SCOutputNode inputs: ['input'] outputs: ['output']
3. Run Simulation and Plot Spikes¶
Drive the network with constant input current for 200 timesteps. LIF neurons accumulate membrane potential and fire when crossing threshold.
input_current = np.array([2.0, 1.5, 0.8])
n_steps = 200
network.reset()
results = network.run({"input": input_current}, steps=n_steps)
# Collect spike trains: shape (n_steps, n_out)
spikes = np.array(results["output"])
total_spikes = spikes.sum(axis=0)
print(f"Simulation: {n_steps} steps, {n_out} neurons")
for i in range(n_out):
print(f" Neuron {i}: {int(total_spikes[i])} spikes ({total_spikes[i]/n_steps*1000:.0f} Hz @ 1ms dt)")
Simulation: 200 steps, 4 neurons Neuron 0: 6 spikes (30 Hz @ 1ms dt) Neuron 1: 20 spikes (100 Hz @ 1ms dt) Neuron 2: 33 spikes (165 Hz @ 1ms dt) Neuron 3: 0 spikes (0 Hz @ 1ms dt)
# Spike raster plot
try:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(12, 3))
for neuron_id in range(n_out):
spike_times = np.where(spikes[:, neuron_id] > 0)[0]
ax.scatter(spike_times, np.full_like(spike_times, neuron_id),
marker="|", s=100, linewidths=0.8, color=f"C{neuron_id}")
ax.set_xlabel("Timestep")
ax.set_ylabel("Neuron ID")
ax.set_title("NIR LIF Network — Spike Raster")
ax.set_yticks(range(n_out))
ax.set_xlim(0, n_steps)
plt.tight_layout()
plt.show()
except ImportError:
print("matplotlib not installed — skipping plot")
4. Fan-in: Multiple Inputs Summed¶
When two edges converge on a single node, SC-NeuroCore sums their outputs. This matches standard additive synaptic current semantics.
fan_in_nodes = {
"left": nir.Input(input_type={"input": np.array([1])}),
"right": nir.Input(input_type={"input": np.array([1])}),
"scale": nir.Scale(scale=np.array([1.0])),
"output": nir.Output(output_type={"output": np.array([1])}),
}
fan_in_edges = [("left", "scale"), ("right", "scale"), ("scale", "output")]
fan_in_graph = nir.NIRGraph(nodes=fan_in_nodes, edges=fan_in_edges)
fan_in_net = from_nir(fan_in_graph)
out = fan_in_net.step({"left": np.array([2.0]), "right": np.array([3.0])})
print(f"Fan-in: left=2.0, right=3.0 → output={out['output'][0]:.1f} (expected 5.0)")
Fan-in: left=2.0, right=3.0 → output=5.0 (expected 5.0)
5. Stateless Pipeline: Linear → Scale → Threshold¶
Not all NIR graphs contain spiking neurons. Here we build a purely feedforward pipeline that classifies inputs via thresholding.
pipeline_nodes = {
"input": nir.Input(input_type={"input": np.array([2])}),
"linear": nir.Linear(weight=np.array([[1.0, 0.0], [0.0, 1.0]])),
"scale": nir.Scale(scale=np.array([2.0, 2.0])),
"threshold": nir.Threshold(threshold=np.array([1.5, 1.5])),
"output": nir.Output(output_type={"output": np.array([2])}),
}
pipeline_edges = [
("input", "linear"),
("linear", "scale"),
("scale", "threshold"),
("threshold", "output"),
]
pipeline_graph = nir.NIRGraph(nodes=pipeline_nodes, edges=pipeline_edges)
pipeline_net = from_nir(pipeline_graph)
print(pipeline_net.summary())
print()
# Test: [1.0, 0.5] → linear: [1.0, 0.5] → scale: [2.0, 1.0] → threshold: [1, 0]
out = pipeline_net.step({"input": np.array([1.0, 0.5])})
print(f"Input: [1.0, 0.5]")
print(f"After linear: [1.0, 0.5] (identity)")
print(f"After scale(2x): [2.0, 1.0]")
print(f"After threshold(1.5): {out['output']} (2.0≥1.5→1, 1.0<1.5→0)")
SCNetwork: 5 nodes, 4 edges input: SCInputNode linear: SCLinearNode scale: SCScaleNode threshold: SCThresholdNode output: SCOutputNode inputs: ['input'] outputs: ['output'] Input: [1.0, 0.5] After linear: [1.0, 0.5] (identity) After scale(2x): [2.0, 1.0] After threshold(1.5): [1. 0.] (2.0≥1.5→1, 1.0<1.5→0)
6. File I/O: Save and Reload .nir Files¶
NIR graphs serialize to HDF5-based .nir files. SC-NeuroCore can load
them directly via from_nir(path).
import tempfile, os
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "demo_model.nir")
nir.write(path, graph)
print(f"Saved: {path} ({os.path.getsize(path)} bytes)")
# Reload
reloaded = from_nir(path)
print(f"Reloaded: {len(reloaded.nodes)} nodes, {len(reloaded.edges)} edges")
# Verify same output
reloaded.reset()
out_original = network.step({"input": np.array([1.0, 1.0, 1.0])})
network.reset()
out_reloaded = reloaded.step({"input": np.array([1.0, 1.0, 1.0])})
print(f"Original output: {out_original['output']}")
print(f"Reloaded output: {out_reloaded['output']}")
# Note: outputs may differ due to stochastic noise seeds, but structure is identical
Saved: /tmp/tmpz1c1za2g/demo_model.nir (17856 bytes) Reloaded: 4 nodes, 3 edges Original output: [0. 0. 0. 0.] Reloaded output: [0. 0. 0. 0.]
7. Supported Primitives Catalogue¶
Quick reference of all NIR primitives SC-NeuroCore currently handles:
from sc_neurocore.nir_bridge.node_map import NODE_MAP
print(f"Supported NIR primitives ({len(NODE_MAP)}):")
for nir_type in NODE_MAP:
print(f" nir.{nir_type.__name__}")
print("\n--- SC-NeuroCore: first NIR backend targeting FPGA synthesis ---")
Supported NIR primitives (18): nir.Input nir.Output nir.LIF nir.IF nir.LI nir.I nir.Affine nir.Linear nir.Scale nir.Threshold nir.Flatten nir.Delay nir.CubaLIF nir.CubaLI nir.SumPool2d nir.AvgPool2d nir.Conv1d nir.Conv2d --- SC-NeuroCore: first NIR backend targeting FPGA synthesis ---