Skip to content

Symbolic Reasoning — Spike-Based Logic

Turing-complete symbolic computation using only spiking neurons. LIF neurons configured as logic gates compose into half-adder, full-adder, ALU, comparator, and sorting network.

Architecture

Each gate maps to a specific LIF configuration:

Gate LIF Threshold Weights Behavior
AND 2 [1, 1] Both inputs must fire
OR 1 [1, 1] Either input fires
NOT 0 [-1] Inhibitory inversion
NAND 0 [-1, -1], bias=2 AND + NOT
XOR 1 [1, 1], inhibit_if_both Odd-parity

The SpikeALU composes these gates into a ripple-carry adder for N-bit integer arithmetic. Addition: sum = a XOR b XOR carry, carry = (a AND b) OR (carry AND (a XOR b)). Subtraction via two's complement.

Components

  • SpikeGate — Configurable spike logic gate. Supports AND, OR, NOT, NAND, XOR. The lif_config property returns LIF neuron parameters that reproduce the gate behavior in simulation.
  • SpikeRegister — N-bit register using SR latch pairs (mutual inhibition). Read/write integer values or raw bit arrays.
  • SpikeALU — N-bit Arithmetic Logic Unit. Operations: add, sub, bitwise_and, bitwise_or, bitwise_xor, compare, shift_left, shift_right.
  • spike_sort — Sort integers using a bubble-sort comparison network built from SpikeALU.compare.

Usage

Python
from sc_neurocore.symbolic import SpikeGate, SpikeRegister, SpikeALU, spike_sort

# Logic gates
xor = SpikeGate("XOR")
assert xor(1, 0) == 1

# 8-bit ALU
alu = SpikeALU(8)
result, carry = alu.add(100, 50)   # 150, False
result, carry = alu.add(200, 100)  # 44, True (overflow)

# 16-bit ALU for larger values
alu16 = SpikeALU(16)
result, _ = alu16.add(30000, 30000)  # 60000

# Spike-based sorting
sorted_vals = spike_sort([3, 1, 4, 1, 5, 9, 2, 6])

Reference: Plana et al. 2022 — Spike-based logic gates on SpiNNaker.

See Tutorial 71: Symbolic Reasoning.

sc_neurocore.symbolic

Spike-based logic gates, registers, ALU. Turing-complete spike computation.

SpikeGate dataclass

Spike-based logic gate.

Parameters

gate_type : str 'AND', 'OR', 'NOT', 'NAND', 'XOR'

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@dataclass
class SpikeGate:
    """Spike-based logic gate.

    Parameters
    ----------
    gate_type : str
        'AND', 'OR', 'NOT', 'NAND', 'XOR'
    """

    gate_type: str

    def __call__(self, *inputs: int) -> int:
        if self.gate_type == "AND":
            return int(all(i > 0 for i in inputs))
        elif self.gate_type == "OR":
            return int(any(i > 0 for i in inputs))
        elif self.gate_type == "NOT":
            return int(inputs[0] == 0)
        elif self.gate_type == "NAND":
            return int(not all(i > 0 for i in inputs))
        elif self.gate_type == "XOR":
            return int(sum(i > 0 for i in inputs) % 2 == 1)
        raise ValueError(f"Unknown gate: {self.gate_type}")  # pragma: no cover

    @property
    def lif_config(self) -> dict[str, Any]:
        """LIF neuron configuration for this gate.

        Returns threshold, excitatory/inhibitory input weights.
        """
        configs = {
            "AND": {"threshold": 2, "weights": [1, 1]},
            "OR": {"threshold": 1, "weights": [1, 1]},
            "NOT": {"threshold": 0, "weights": [-1]},
            "NAND": {"threshold": 0, "weights": [-1, -1], "bias": 2},
            "XOR": {"threshold": 1, "weights": [1, 1], "inhibit_if_both": True},
        }
        return configs.get(self.gate_type, {})

lif_config property

LIF neuron configuration for this gate.

Returns threshold, excitatory/inhibitory input weights.

SpikeRegister

Spike-based register: stores N bits using SR latch pairs.

Each bit is held by two neurons in mutual inhibition (bistable). Write: inject spike to set/reset neuron. Read: check which neuron of each pair is active.

Parameters

n_bits : int Register width.

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class SpikeRegister:
    """Spike-based register: stores N bits using SR latch pairs.

    Each bit is held by two neurons in mutual inhibition (bistable).
    Write: inject spike to set/reset neuron.
    Read: check which neuron of each pair is active.

    Parameters
    ----------
    n_bits : int
        Register width.
    """

    def __init__(self, n_bits: int = 8) -> None:
        self.n_bits = n_bits
        self._state = np.zeros(n_bits, dtype=np.int8)

    def write(self, value: int) -> None:
        """Write an integer value to the register."""
        for i in range(self.n_bits):
            self._state[i] = (value >> i) & 1

    def read(self) -> int:
        """Read the register as an integer."""
        value = 0
        for i in range(self.n_bits):
            value |= int(self._state[i]) << i
        return value

    def write_bits(self, bits: np.ndarray) -> None:
        """Write raw bit array."""
        self._state = bits[: self.n_bits].astype(np.int8)  # type: ignore[assignment]

    def read_bits(self) -> np.ndarray:  # pragma: no cover
        """Read raw bit array."""
        return self._state.copy()

    def clear(self) -> None:
        self._state[:] = 0

write(value)

Write an integer value to the register.

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
86
87
88
89
def write(self, value: int) -> None:
    """Write an integer value to the register."""
    for i in range(self.n_bits):
        self._state[i] = (value >> i) & 1

read()

Read the register as an integer.

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
91
92
93
94
95
96
def read(self) -> int:
    """Read the register as an integer."""
    value = 0
    for i in range(self.n_bits):
        value |= int(self._state[i]) << i
    return value

write_bits(bits)

Write raw bit array.

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
 98
 99
100
def write_bits(self, bits: np.ndarray) -> None:
    """Write raw bit array."""
    self._state = bits[: self.n_bits].astype(np.int8)  # type: ignore[assignment]

read_bits()

Read raw bit array.

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
102
103
104
def read_bits(self) -> np.ndarray:  # pragma: no cover
    """Read raw bit array."""
    return self._state.copy()

SpikeALU

Spike-based Arithmetic Logic Unit.

Operations: ADD, SUB, AND, OR, XOR, CMP, SHIFT_LEFT, SHIFT_RIGHT. All implemented via spike-gate compositions.

Parameters

n_bits : int Word width.

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
class SpikeALU:
    """Spike-based Arithmetic Logic Unit.

    Operations: ADD, SUB, AND, OR, XOR, CMP, SHIFT_LEFT, SHIFT_RIGHT.
    All implemented via spike-gate compositions.

    Parameters
    ----------
    n_bits : int
        Word width.
    """

    def __init__(self, n_bits: int = 8) -> None:
        self.n_bits = n_bits
        self._and = SpikeGate("AND")
        self._xor = SpikeGate("XOR")
        self._or = SpikeGate("OR")
        self._not = SpikeGate("NOT")

    def add(self, a: int, b: int) -> tuple[int, bool]:
        """Ripple-carry addition. Returns (result, carry_out)."""
        mask = (1 << self.n_bits) - 1
        result = 0
        carry = 0

        for i in range(self.n_bits):
            bit_a = (a >> i) & 1
            bit_b = (b >> i) & 1
            # Full adder: sum = a XOR b XOR carry, carry = (a AND b) OR (carry AND (a XOR b))
            ab_xor = self._xor(bit_a, bit_b)
            sum_bit = self._xor(ab_xor, carry)
            carry = self._or(self._and(bit_a, bit_b), self._and(carry, ab_xor))
            result |= sum_bit << i

        return result & mask, bool(carry)

    def sub(self, a: int, b: int) -> tuple[int, bool]:
        """Subtraction via two's complement: a - b = a + (~b + 1)."""
        mask = (1 << self.n_bits) - 1
        b_inv = (~b) & mask
        result, carry = self.add(a, b_inv)
        result, _ = self.add(result, 1)
        borrow = a < b
        return result, borrow

    def bitwise_and(self, a: int, b: int) -> int:
        result = 0
        for i in range(self.n_bits):
            result |= self._and((a >> i) & 1, (b >> i) & 1) << i
        return result

    def bitwise_or(self, a: int, b: int) -> int:
        result = 0
        for i in range(self.n_bits):
            result |= self._or((a >> i) & 1, (b >> i) & 1) << i
        return result

    def bitwise_xor(self, a: int, b: int) -> int:
        result = 0
        for i in range(self.n_bits):
            result |= self._xor((a >> i) & 1, (b >> i) & 1) << i
        return result

    def compare(self, a: int, b: int) -> int:
        """Compare: returns -1, 0, or 1."""
        if a < b:
            return -1
        if a > b:
            return 1
        return 0

    def shift_left(self, a: int, n: int = 1) -> int:
        mask = (1 << self.n_bits) - 1
        return (a << n) & mask

    def shift_right(self, a: int, n: int = 1) -> int:
        return a >> n

add(a, b)

Ripple-carry addition. Returns (result, carry_out).

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def add(self, a: int, b: int) -> tuple[int, bool]:
    """Ripple-carry addition. Returns (result, carry_out)."""
    mask = (1 << self.n_bits) - 1
    result = 0
    carry = 0

    for i in range(self.n_bits):
        bit_a = (a >> i) & 1
        bit_b = (b >> i) & 1
        # Full adder: sum = a XOR b XOR carry, carry = (a AND b) OR (carry AND (a XOR b))
        ab_xor = self._xor(bit_a, bit_b)
        sum_bit = self._xor(ab_xor, carry)
        carry = self._or(self._and(bit_a, bit_b), self._and(carry, ab_xor))
        result |= sum_bit << i

    return result & mask, bool(carry)

sub(a, b)

Subtraction via two's complement: a - b = a + (~b + 1).

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
146
147
148
149
150
151
152
153
def sub(self, a: int, b: int) -> tuple[int, bool]:
    """Subtraction via two's complement: a - b = a + (~b + 1)."""
    mask = (1 << self.n_bits) - 1
    b_inv = (~b) & mask
    result, carry = self.add(a, b_inv)
    result, _ = self.add(result, 1)
    borrow = a < b
    return result, borrow

compare(a, b)

Compare: returns -1, 0, or 1.

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
173
174
175
176
177
178
179
def compare(self, a: int, b: int) -> int:
    """Compare: returns -1, 0, or 1."""
    if a < b:
        return -1
    if a > b:
        return 1
    return 0

spike_sort(values, n_bits=8)

Sort integers using spike-based comparison network.

Uses a bubble-sort topology where each compare-and-swap is implemented via SpikeALU.compare.

Parameters

values : list of int n_bits : int

Returns

list of int, sorted ascending

Source code in src/sc_neurocore/symbolic/spike_logic.py
Python
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def spike_sort(values: list[int], n_bits: int = 8) -> list[int]:
    """Sort integers using spike-based comparison network.

    Uses a bubble-sort topology where each compare-and-swap
    is implemented via SpikeALU.compare.

    Parameters
    ----------
    values : list of int
    n_bits : int

    Returns
    -------
    list of int, sorted ascending
    """
    alu = SpikeALU(n_bits)
    arr = list(values)
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if alu.compare(arr[j], arr[j + 1]) > 0:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr