Neuromorphic Signal Processing¶
Process real-world signals (audio, sensor data, time series) using SC-NeuroCore's stochastic bitstream pipeline. Neuromorphic signal processing replaces DSP multiply-accumulate operations with single-gate logic, achieving orders-of-magnitude power savings on FPGA.
Prerequisites: pip install sc-neurocore scipy matplotlib
1. SC as a signal processing primitive¶
Traditional DSP computes weighted sums with multipliers and adders. In stochastic computing:
| DSP operation | SC equivalent | Gate count |
|---|---|---|
| Multiply | AND gate | 1 |
| Weighted sum | MUX tree | N + 1 |
| Integration | Counter | log₂(L) |
| Filtering | Shift-register + AND | N + 1 |
SC trades precision for area: a 16-bit multiplier needs ~256 gates; an SC multiplier (AND) needs 1. The precision scales as 1/√L with bitstream length L.
2. Encode an audio signal¶
Convert a 1 kHz sine wave into a unipolar bitstream:
import numpy as np
from sc_neurocore import BitstreamEncoder
# Generate 1 kHz sine at 16 kHz sample rate
fs = 16000
duration = 0.01 # 10 ms segment
t = np.arange(int(fs * duration)) / fs
signal = 0.5 + 0.4 * np.sin(2 * np.pi * 1000 * t) # unipolar [0.1, 0.9]
L = 256 # bitstream length per sample
encoder = BitstreamEncoder(x_min=0.0, x_max=1.0, length=L, seed=42)
# Encode each sample as an L-bit stream
bitstreams = np.zeros((len(signal), L), dtype=np.uint8)
for i, s in enumerate(signal):
bitstreams[i] = encoder.encode(float(s))
print(f"Signal: {len(signal)} samples")
print(f"Bitstreams: {bitstreams.shape} (samples × bits)")
print(f"Mean bit density: {bitstreams.mean():.3f} (expected: {signal.mean():.3f})")
3. SC low-pass filter¶
A first-order IIR low-pass filter in SC uses a shift register to maintain state and an AND gate for multiplication:
from sc_neurocore import BitstreamSynapse
def sc_lowpass(bitstreams, alpha=0.9):
"""First-order IIR filter: y[n] = α·y[n-1] + (1-α)·x[n].
In SC: α is a bitstream, multiplication is AND.
"""
n_samples, L = bitstreams.shape
alpha_bits = (np.random.rand(L) < alpha).astype(np.uint8)
output = np.zeros((n_samples, L), dtype=np.uint8)
state = np.zeros(L, dtype=np.uint8)
for n in range(n_samples):
# y[n] = α·y[n-1] (AND gate)
feedback = state & alpha_bits
# (1-α)·x[n]: use inverted α mask
new_input = bitstreams[n] & (~alpha_bits & 1)
# Combine via OR (approximate addition for low duty cycles)
output[n] = feedback | new_input
state = output[n]
return output
filtered = sc_lowpass(bitstreams, alpha=0.95)
decoded_filtered = filtered.mean(axis=1)
print(f"Filtered signal range: [{decoded_filtered.min():.3f}, {decoded_filtered.max():.3f}]")
4. SC FIR filter¶
An N-tap FIR filter uses N AND gates (one per tap) and a MUX tree for summation:
def sc_fir(bitstreams, taps):
"""SC FIR filter: y[n] = Σ h[k] · x[n-k].
Each tap weight h[k] is a bitstream probability.
Multiplication by AND, summation by MUX (random select).
"""
n_samples, L = bitstreams.shape
n_taps = len(taps)
output = np.zeros((n_samples, L), dtype=np.uint8)
for n in range(n_taps - 1, n_samples):
# For each bit position, randomly select one tap (MUX)
tap_select = np.random.randint(0, n_taps, size=L)
for b in range(L):
k = tap_select[b]
tap_bits = (np.random.rand() < taps[k])
output[n, b] = bitstreams[n - k, b] & int(tap_bits)
return output
# 5-tap moving average (equal weights = 1/5 = 0.2)
taps = [0.2] * 5
fir_out = sc_fir(bitstreams, taps)
decoded_fir = fir_out.mean(axis=1)
print(f"FIR output mean: {decoded_fir[10:].mean():.3f}")
5. Spike-based edge detection¶
Detect edges (transients) in a signal using the difference between fast and slow LIF neurons — the neuromorphic equivalent of a band-pass filter:
from sc_neurocore import StochasticLIFNeuron
fast_neuron = StochasticLIFNeuron(length=L)
slow_neuron = StochasticLIFNeuron(length=L)
fast_spikes = []
slow_spikes = []
edge_signal = []
# Test signal: step function with noise
test_signal = np.concatenate([
np.full(50, 0.2),
np.full(50, 0.8),
np.full(50, 0.2),
np.full(50, 0.8),
])
test_signal += np.random.normal(0, 0.02, len(test_signal))
test_signal = np.clip(test_signal, 0.01, 0.99)
for t in range(len(test_signal)):
x = int(test_signal[t] * 255)
fs, _ = fast_neuron.step(x_value=x)
ss, _ = slow_neuron.step(x_value=max(1, x // 2))
fast_spikes.append(fs)
slow_spikes.append(ss)
edge_signal.append(abs(fs - ss))
fast_rate = np.convolve(fast_spikes, np.ones(10) / 10, mode="same")
slow_rate = np.convolve(slow_spikes, np.ones(10) / 10, mode="same")
edge_rate = np.convolve(edge_signal, np.ones(10) / 10, mode="same")
print(f"Edge peaks at steps: {np.where(edge_rate > 0.3)[0]}")
6. Spectral analysis via spike trains¶
Extract frequency content from spike trains using inter-spike interval (ISI) histograms:
from scipy import signal as scipy_signal
# Generate a two-tone signal
fs = 16000
t = np.arange(8000) / fs
two_tone = 0.5 + 0.2 * np.sin(2 * np.pi * 440 * t) + 0.15 * np.sin(2 * np.pi * 880 * t)
two_tone = np.clip(two_tone, 0.01, 0.99)
# Encode and pass through LIF neuron
neuron = StochasticLIFNeuron(length=128)
spike_times = []
for step in range(len(two_tone)):
spike, _ = neuron.step(x_value=int(two_tone[step] * 255))
if spike:
spike_times.append(step)
# ISI histogram → frequency estimate
isis = np.diff(spike_times)
if len(isis) > 10:
freqs_hz = fs / isis # convert ISI to frequency
hist, bin_edges = np.histogram(freqs_hz, bins=100, range=(100, 2000))
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
peak_freq = bin_centers[hist.argmax()]
print(f"Dominant ISI frequency: {peak_freq:.0f} Hz (expected: 440 Hz)")
7. Comparison: SC vs conventional DSP¶
# Conventional FIR for reference
from scipy.signal import firwin, lfilter
# Design 5-tap low-pass at 2 kHz cutoff, 16 kHz sample rate
h = firwin(5, 2000, fs=16000)
conventional = lfilter(h, 1.0, signal)
# SC version (decode back to float for comparison)
sc_version = decoded_fir[:len(conventional)]
# NMSE
valid = slice(10, None) # skip transient
nmse = np.mean((sc_version[valid] - conventional[valid]) ** 2) / np.var(conventional[valid])
print(f"SC vs conventional NMSE: {nmse:.4f}")
print(f"SC uses {5} AND gates + 1 MUX vs {5} multipliers + {4} adders")
8. Hardware cost comparison¶
| Component | Conventional (16-bit) | SC (L=256) |
|---|---|---|
| Multiplier | ~256 LUTs | 1 LUT (AND) |
| 5-tap FIR | ~1280 LUTs | ~10 LUTs |
| Power (est.) | ~50 mW | ~0.5 mW |
| Precision | 16-bit exact | ~8-bit effective |
| Latency | 1 cycle | L=256 cycles |
SC trades latency for area and power. For applications where latency tolerance > 256 clock cycles (audio, slow sensors), SC wins on power by 100x.
9. Real-time processing pipeline¶
Combine encoding, filtering, and spike-based feature extraction into a streaming pipeline:
class NeuromorphicDSP:
"""Streaming SC signal processing pipeline."""
def __init__(self, n_filters=8, length=128):
self.encoder = BitstreamEncoder(length=length, seed=0)
self.length = length
self.filters = [StochasticLIFNeuron(length=length) for _ in range(n_filters)]
def process_sample(self, sample):
"""Process one sample, return n_filters spike outputs."""
bits = self.encoder.encode(float(sample))
x_val = int(np.clip(sample, 0, 1) * 255)
return [n.step(x_value=x_val)[0] for n in self.filters]
pipeline = NeuromorphicDSP(n_filters=8, length=128)
features = []
for s in signal[:100]:
features.append(pipeline.process_sample(s))
features = np.array(features)
print(f"Feature matrix: {features.shape} (samples × filters)")
print(f"Active filters per step: {features.sum(axis=1).mean():.1f}")
What you learned¶
- SC replaces DSP multipliers with AND gates (1 LUT vs ~256)
- Low-pass IIR filter: shift register + AND + OR
- FIR filter: N AND gates + MUX tree
- Spike-based edge detection: fast LIF minus slow LIF
- ISI histograms extract frequency content from spike trains
- SC trades latency for 100x power reduction — ideal for always-on sensors
- Streaming pipeline: encode → filter → spike features
Next steps¶
- Implement a full SC filterbank for audio preprocessing
- Deploy the FIR filter on FPGA and measure real power consumption
- Use neuromorphic features as input to an SC classifier (Tutorial 07)
- Compare SC filterbank against Mel filterbank for speech recognition