File size: 10,875 Bytes
e4cdd5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
67
68
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
108
109
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
"""Stress tests for the neuromorphic chip SDK.



Validates long-running stability, edge cases, and resource limits.



Usage:

    python stress_test.py                  # Run all stress tests

    python stress_test.py --test saturation  # Run specific test

"""

import os
import sys
import time
import argparse
import numpy as np

_SDK_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
if _SDK_DIR not in sys.path:
    sys.path.insert(0, _SDK_DIR)

import neurocore as nc
from neurocore.simulator import Simulator
from neurocore.constants import (
    NEURONS_PER_CORE, WEIGHT_MIN, WEIGHT_MAX,
    DEFAULT_THRESHOLD, DEFAULT_LEAK,
)


def test_all_core_saturation(num_cores=16, timesteps=1000):
    """All cores, all neurons spiking every timestep.



    Creates 16 cores x 1024 neurons = 16,384 neurons, each receiving

    enough stimulus to fire every timestep.

    """
    print(f"\n--- Test: All-Core Saturation ({num_cores} cores, {timesteps} ts) ---")
    net = nc.Network()

    pops = []
    for c in range(num_cores):
        pop = net.population(
            NEURONS_PER_CORE,
            params={"threshold": 100, "leak": 0, "refrac": 0},
            label=f"core_{c}",
        )
        pops.append(pop)

    sim = Simulator(num_cores=num_cores)
    sim.deploy(net)

    total_neurons = num_cores * NEURONS_PER_CORE
    total_spikes = 0
    t_start = time.perf_counter()

    for t in range(timesteps):
        for pop in pops:
            sim.inject(pop, current=200)
        result = sim.run(1)
        total_spikes += result.total_spikes

    elapsed = time.perf_counter() - t_start
    ts_per_sec = timesteps / elapsed

    expected_min = total_neurons * timesteps * 0.9  # allow 10% margin for refractory
    print(f"  Neurons: {total_neurons}")
    print(f"  Total spikes: {total_spikes:,} (expected ~{total_neurons * timesteps:,})")
    print(f"  Throughput: {ts_per_sec:.0f} ts/sec")
    print(f"  Elapsed: {elapsed:.1f}s")

    assert total_spikes >= expected_min, \
        f"Expected at least {expected_min:,} spikes, got {total_spikes:,}"
    print("  PASSED")
    return True


def test_long_running_stability(timesteps=10000):
    """Run a small network for many timesteps, verify state consistency."""
    print(f"\n--- Test: Long-Running Stability ({timesteps} ts) ---")
    net = nc.Network()
    exc = net.population(64, params={"threshold": 500, "leak": 3, "refrac": 2})
    inh = net.population(16, params={"threshold": 300, "leak": 5, "refrac": 1})
    net.connect(exc, exc, topology="random_sparse", weight=100, p=0.1, seed=42)
    net.connect(exc, inh, topology="all_to_all", weight=200)
    net.connect(inh, exc, topology="all_to_all", weight=-150)

    sim = Simulator()
    sim.deploy(net)

    total_spikes = 0
    spike_history = []
    t_start = time.perf_counter()

    # Inject for first 100 timesteps, then let network evolve
    for t in range(timesteps):
        if t < 100:
            sim.inject(exc[:8], current=600)
        result = sim.run(1)
        total_spikes += result.total_spikes
        if t % 1000 == 0:
            spike_history.append(total_spikes)

    elapsed = time.perf_counter() - t_start
    print(f"  Total spikes: {total_spikes:,}")
    print(f"  Throughput: {timesteps / elapsed:.0f} ts/sec")

    # Verify membrane potentials are in valid range
    for i in range(sim._n):
        assert 0 <= sim._potential[i] <= 65535, \
            f"Neuron {i} potential {sim._potential[i]} out of range"

    # Verify no NaN or corruption
    assert not np.any(np.isnan(sim._potential.astype(float))), "NaN in potentials"
    assert not np.any(np.isnan(sim._trace.astype(float))), "NaN in traces"

    print(f"  Elapsed: {elapsed:.1f}s")
    print("  PASSED")
    return True


def test_max_fan_out():
    """One neuron connecting to 1023 targets (max per core)."""
    print("\n--- Test: Max Fan-Out (1 -> 1023) ---")
    net = nc.Network()
    src = net.population(1, params={"threshold": 100, "leak": 0, "refrac": 0})
    tgt = net.population(1023, params={"threshold": 100, "leak": 0, "refrac": 0})
    net.connect(src, tgt, topology="all_to_all", weight=200)

    sim = Simulator()
    sim.deploy(net)

    # Fire the source
    sim.inject(src, current=200)
    sim.run(1)  # src fires
    result = sim.run(1)  # targets receive and fire

    print(f"  Connections: 1 -> 1023")
    print(f"  Spikes on delivery timestep: {result.total_spikes}")

    # All 1023 targets should spike (200 weight > 100 threshold)
    assert result.total_spikes >= 1023, \
        f"Expected >= 1023 spikes, got {result.total_spikes}"
    print("  PASSED")
    return True


def test_weight_extremes():
    """Test with extreme weight values: max positive, max negative, and zero."""
    print("\n--- Test: Weight Extremes ---")

    # Max positive weight
    net = nc.Network()
    src = net.population(1, params={"threshold": 100, "leak": 0, "refrac": 0})
    tgt = net.population(1, params={"threshold": 30000, "leak": 0, "refrac": 0})
    net.connect(src, tgt, weight=WEIGHT_MAX)

    sim = Simulator()
    sim.deploy(net)
    sim.inject(src, current=200)
    sim.run(1)
    result = sim.run(1)
    assert result.total_spikes >= 1, f"Max positive weight should cause spike, got {result.total_spikes}"
    print(f"  Max positive weight ({WEIGHT_MAX}): PASS")

    # Max negative weight (inhibition)
    net2 = nc.Network()
    src2 = net2.population(1, params={"threshold": 100, "leak": 0, "refrac": 0})
    tgt2 = net2.population(1, params={"threshold": 100, "leak": 0, "refrac": 0})
    net2.connect(src2, tgt2, weight=WEIGHT_MIN)

    sim2 = Simulator()
    sim2.deploy(net2)
    # Pre-charge target, then inhibit
    sim2.inject(tgt2, current=50)
    sim2.run(1)  # t0: tgt potential = 50
    sim2.inject(src2, current=200)
    sim2.run(1)  # t1: src fires (200 >= 100), spike pending for tgt
    sim2.run(1)  # t2: spike delivered to tgt: 50 + (-32768) -> clamped to 0
    tgt_core, tgt_neuron = sim2._compiled.placement.neuron_map[(tgt2.id, 0)]
    tgt_gid = tgt_core * 1024 + tgt_neuron
    assert sim2._potential[tgt_gid] == 0, \
        f"Negative weight should clamp to 0, got {sim2._potential[tgt_gid]}"
    print(f"  Max negative weight ({WEIGHT_MIN}): PASS")

    # Zero weight
    net3 = nc.Network()
    src3 = net3.population(1, params={"threshold": 100, "leak": 0, "refrac": 0})
    tgt3 = net3.population(1, params={"threshold": 100, "leak": 0, "refrac": 0})
    net3.connect(src3, tgt3, weight=0)

    sim3 = Simulator()
    sim3.deploy(net3)
    sim3.inject(src3, current=200)
    sim3.run(1)  # src fires
    result3 = sim3.run(5)
    # tgt should not spike from 0-weight connection
    tgt_core3, tgt_neuron3 = sim3._compiled.placement.neuron_map[(tgt3.id, 0)]
    tgt_gid3 = tgt_core3 * 1024 + tgt_neuron3
    assert sim3._potential[tgt_gid3] == 0, \
        f"Zero weight should not charge target, got {sim3._potential[tgt_gid3]}"
    print(f"  Zero weight: PASS")

    print("  PASSED")
    return True


def test_pool_depth_fill():
    """Fill the CSR pool to near capacity on one core."""
    print("\n--- Test: Pool Depth Fill ---")
    # 64 source neurons each connecting to 500 targets = 32,000 pool entries
    # (close to POOL_DEPTH=32768 for simulation, well above FPGA's 4096)
    net = nc.Network()
    src = net.population(64, params={"threshold": 100, "leak": 0, "refrac": 0})
    tgt = net.population(500, params={"threshold": 100, "leak": 0, "refrac": 0})
    net.connect(src, tgt, topology="all_to_all", weight=200)

    sim = Simulator()
    sim.deploy(net)

    total_pool_entries = sum(len(v) for v in sim._compiled.adjacency.values())
    print(f"  Pool entries used: {total_pool_entries:,}")
    print(f"  Neurons: {sim._compiled.placement.total_neurons}")

    sim.inject(src[:4], current=200)
    result = sim.run(2)
    print(f"  Spikes in 2 ts: {result.total_spikes}")
    assert result.total_spikes > 0, "Should produce spikes"
    print("  PASSED")
    return True


def test_cross_core_chain(num_cores=16):
    """Spike chain through all cores: core0->core1->...->core15.



    Uses core-filling populations to force each node onto a separate core,

    plus 1-neuron relay populations for the chain.

    """
    print(f"\n--- Test: Cross-Core Chain ({num_cores} cores) ---")
    net = nc.Network()

    # Create 1-neuron relay populations (one per core in the chain)
    # Also create filler populations to push each relay to its own core.
    relays = []
    for c in range(num_cores):
        relay = net.population(
            1,
            params={"threshold": 100, "leak": 0, "refrac": 2},
            label=f"relay_{c}",
        )
        relays.append(relay)
        if c < num_cores - 1:
            # Filler to push next relay to next core
            net.population(NEURONS_PER_CORE - 1, label=f"filler_{c}")

    # Chain: relay[i] -> relay[i+1]
    for i in range(num_cores - 1):
        net.connect(relays[i], relays[i + 1], topology="all_to_all", weight=200)

    sim = Simulator(num_cores=num_cores)
    sim.deploy(net)

    # Fire first relay
    sim.inject(relays[0], current=200)

    total_spikes = 0
    for t in range(num_cores * 2 + 5):
        result = sim.run(1)
        total_spikes += result.total_spikes

    print(f"  Total spikes through {num_cores}-core chain: {total_spikes}")
    assert total_spikes >= num_cores, \
        f"Expected >= {num_cores} spikes, got {total_spikes}"
    print("  PASSED")
    return True


TESTS = {
    "saturation": test_all_core_saturation,
    "stability": test_long_running_stability,
    "fanout": test_max_fan_out,
    "weights": test_weight_extremes,
    "pool": test_pool_depth_fill,
    "chain": test_cross_core_chain,
}


def main():
    parser = argparse.ArgumentParser(description="SDK Stress Tests")
    parser.add_argument("--test", choices=list(TESTS.keys()),
                        help="Run specific test (default: all)")
    parser.add_argument("--cores", type=int, default=16)
    args = parser.parse_args()

    if args.test:
        tests = {args.test: TESTS[args.test]}
    else:
        tests = TESTS

    passed = 0
    failed = 0
    for name, func in tests.items():
        try:
            func()
            passed += 1
        except Exception as e:
            print(f"  FAILED: {e}")
            failed += 1

    print(f"\n{'='*50}")
    print(f"Stress Tests: {passed} passed, {failed} failed out of {passed + failed}")
    if failed == 0:
        print("ALL STRESS TESTS PASSED")
    else:
        sys.exit(1)


if __name__ == "__main__":
    main()