spikeling 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/__init__.py +1 -0
- core/__main__.py +81 -0
- core/compiler/__init__.py +0 -0
- core/compiler/compiler.py +522 -0
- core/encoder/__init__.py +0 -0
- core/encoder/encoder.py +221 -0
- core/examples/agent_brain.spk +82 -0
- core/examples/signal_detector.spk +78 -0
- core/examples/signal_test.py +121 -0
- core/examples/sound_localizer.spk +18 -0
- core/examples/test_gating_accuracy_regression.py +83 -0
- core/examples/test_tone_detector.py +78 -0
- core/examples/tone_detector.spk +22 -0
- core/examples/word_learner.spk +297 -0
- core/hardware/__init__.py +0 -0
- core/hardware/acoustic_adapter.py +69 -0
- core/hardware/acoustic_anomaly_detector.py +189 -0
- core/hardware/baseline_deviation.py +192 -0
- core/hardware/emg_adapter.py +154 -0
- core/hardware/environmental_adapter.py +155 -0
- core/hardware/sensor_adapter.py +112 -0
- core/hardware/system_telemetry_adapter.py +249 -0
- core/hardware/video_adapter.py +146 -0
- core/memory/__init__.py +0 -0
- core/memory/memory.py +172 -0
- core/runtime/__init__.py +0 -0
- core/runtime/runtime.py +676 -0
- core/stdlib/neurons.spk +21 -0
- core/trainer/__init__.py +0 -0
- core/trainer/trainer.py +466 -0
- pyspike.py +417 -0
- pyspike_causal.py +276 -0
- pyspike_neuron_models.py +209 -0
- pyspike_surrogate_gradient.py +215 -0
- spikeling-1.0.2.dist-info/METADATA +136 -0
- spikeling-1.0.2.dist-info/RECORD +40 -0
- spikeling-1.0.2.dist-info/WHEEL +5 -0
- spikeling-1.0.2.dist-info/entry_points.txt +2 -0
- spikeling-1.0.2.dist-info/licenses/LICENSE +21 -0
- spikeling-1.0.2.dist-info/top_level.txt +5 -0
core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Spikeling — Neuromorphic AI Runtime
|
core/__main__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
spikeling/__main__.py
|
|
3
|
+
=====================
|
|
4
|
+
Entry point for the Spikeling neuromorphic runtime.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python -m spikeling # reflex demo (sound localizer)
|
|
8
|
+
python -m spikeling --train # interactive trainer
|
|
9
|
+
python -m spikeling --train path/to/network.spk # train a specific network
|
|
10
|
+
python -m spikeling path/to/network.spk # run any network interactively
|
|
11
|
+
python -m spikeling --no-interactive # compile only
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
# Windows' legacy console codepage (cp1252 etc.) can't encode the box-drawing
|
|
18
|
+
# characters the interactive runtime prints (runtime.py's banner) -- crashes the
|
|
19
|
+
# very first `python -m core` a new user tries, on a default Windows terminal,
|
|
20
|
+
# with no non-ASCII input involved at all. Reconfigure as early as possible,
|
|
21
|
+
# before any print happens; a no-op on already-UTF-8 setups (Linux/macOS, or a
|
|
22
|
+
# UTF-8 Windows Terminal).
|
|
23
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
24
|
+
if hasattr(_stream, "reconfigure"):
|
|
25
|
+
try:
|
|
26
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
31
|
+
|
|
32
|
+
from compiler.compiler import compile_file
|
|
33
|
+
from runtime.runtime import SpikelingRuntime
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
args = sys.argv[1:]
|
|
38
|
+
train_mode = "--train" in args
|
|
39
|
+
no_ui = "--no-interactive" in args
|
|
40
|
+
spk_args = [a for a in args if not a.startswith("--")]
|
|
41
|
+
|
|
42
|
+
# Default networks
|
|
43
|
+
base = os.path.dirname(__file__)
|
|
44
|
+
default_demo = os.path.join(base, "examples", "sound_localizer.spk")
|
|
45
|
+
default_learner = os.path.join(base, "examples", "word_learner.spk")
|
|
46
|
+
|
|
47
|
+
if train_mode:
|
|
48
|
+
spk_path = spk_args[0] if spk_args else default_learner
|
|
49
|
+
else:
|
|
50
|
+
spk_path = spk_args[0] if spk_args else default_demo
|
|
51
|
+
|
|
52
|
+
if not os.path.exists(spk_path):
|
|
53
|
+
print(f"[spikeling] error: file not found - {spk_path}")
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
print(f"[spikeling] compiling {spk_path} ...")
|
|
57
|
+
output_dir = os.path.dirname(os.path.abspath(spk_path))
|
|
58
|
+
ast = compile_file(spk_path, output_dir=output_dir)
|
|
59
|
+
rt = SpikelingRuntime(ast)
|
|
60
|
+
|
|
61
|
+
if no_ui:
|
|
62
|
+
print("[spikeling] compiled successfully (--no-interactive mode)")
|
|
63
|
+
print(rt.state_report())
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if train_mode:
|
|
67
|
+
# Wire in the trainer
|
|
68
|
+
from trainer.trainer import SpikelingTrainer
|
|
69
|
+
memory_path = os.path.join(output_dir, "spikeling_memory.json")
|
|
70
|
+
trainer = SpikelingTrainer(rt, memory_path=memory_path)
|
|
71
|
+
trainer.run_interactive()
|
|
72
|
+
else:
|
|
73
|
+
# Reflex / benchmark demo
|
|
74
|
+
for action in ast.actions:
|
|
75
|
+
cmd = action.command
|
|
76
|
+
rt.register_handler(cmd, lambda c=cmd: None)
|
|
77
|
+
rt.run_interactive()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
spikeling/compiler/compiler.py
|
|
3
|
+
==============================
|
|
4
|
+
Spikeling DSL Compiler
|
|
5
|
+
|
|
6
|
+
Parses .spk source files and emits:
|
|
7
|
+
- A SpikelingNetwork object (for the Python runtime)
|
|
8
|
+
- A .c / .h file pair (for production C compilation)
|
|
9
|
+
|
|
10
|
+
DSL Grammar:
|
|
11
|
+
neuron <name> threshold=<int> leak=<int> [type=LIF|Izhikevich|AdEx]
|
|
12
|
+
connect <src> -> <dst> weight=<float> [delay=<float>]
|
|
13
|
+
action <neuron> -> [<COMMAND>]
|
|
14
|
+
refractory=<int>ms
|
|
15
|
+
learn=STDP rate=<float>
|
|
16
|
+
|
|
17
|
+
delay= is optional and defaults to 0.0 (instant delivery, unchanged
|
|
18
|
+
behavior for every .spk file written before this grammar addition --
|
|
19
|
+
see runtime.py's Synapse.delay_ms). Units are milliseconds, on the
|
|
20
|
+
runtime's own real tick/timestamp scale (whatever unit the caller's
|
|
21
|
+
stimulate()/tick() timestamps are already in) -- NOT a separate
|
|
22
|
+
biological-ms constant needing rescaling; delay_ms is compared directly
|
|
23
|
+
against the same current_time_ms values _fire()/tick() already use
|
|
24
|
+
throughout this runtime.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
import os
|
|
29
|
+
import math
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────────
|
|
35
|
+
# AST Nodes
|
|
36
|
+
# ─────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class NeuronDef:
|
|
40
|
+
name: str
|
|
41
|
+
threshold: int
|
|
42
|
+
leak: int
|
|
43
|
+
neuron_type: str = "LIF"
|
|
44
|
+
# Resonator-only fields (None for LIF/Izhikevich/AdEx neurons).
|
|
45
|
+
# A Resonator is a damped harmonic oscillator tuned to `freq_hz`,
|
|
46
|
+
# used for frequency-selective detection (see
|
|
47
|
+
# resonator-prototype/accuracy_benchmark.py for why this beats a
|
|
48
|
+
# naive amplitude threshold at telling specific signals apart).
|
|
49
|
+
freq_hz: Optional[float] = None
|
|
50
|
+
damping: Optional[float] = None
|
|
51
|
+
coupling: Optional[float] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ConnectionDef:
|
|
56
|
+
src: str
|
|
57
|
+
dst: str
|
|
58
|
+
weight: float
|
|
59
|
+
delay_ms: float = 0.0 # see runtime.py's Synapse.delay_ms -- default 0.0 == instant, unchanged behavior
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ActionDef:
|
|
64
|
+
neuron: str
|
|
65
|
+
command: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class SpikelingAST:
|
|
70
|
+
neurons: list[NeuronDef] = field(default_factory=list)
|
|
71
|
+
connections: list[ConnectionDef] = field(default_factory=list)
|
|
72
|
+
actions: list[ActionDef] = field(default_factory=list)
|
|
73
|
+
refractory_ms: int = 0
|
|
74
|
+
learn_rule: Optional[str] = None
|
|
75
|
+
learn_rate: float = 0.01
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ─────────────────────────────────────────────
|
|
79
|
+
# Parser
|
|
80
|
+
# ─────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
class SpikelingParser:
|
|
83
|
+
"""Tokenises and parses a .spk source string into a SpikelingAST."""
|
|
84
|
+
|
|
85
|
+
NEURON_RE = re.compile(r"neuron\s+(\w+)\s+threshold=(\d+)\s+leak=(\d+)(?:\s+type=(\w+))?")
|
|
86
|
+
# Resonator neurons use a different parameter set (no threshold/leak --
|
|
87
|
+
# they're driven continuously, not pulsed): freq_hz + damping, with an
|
|
88
|
+
# optional explicit coupling override (auto-derived from freq if omitted,
|
|
89
|
+
# see runtime.ResonatorState — gain falls off at higher frequencies
|
|
90
|
+
# otherwise, see benchmarks/ for why that matters).
|
|
91
|
+
RESONATOR_RE = re.compile(r"neuron\s+(\w+)\s+type=Resonator\s+freq=([\d.]+)\s+damping=([\d.]+)(?:\s+coupling=([\d.]+))?")
|
|
92
|
+
# weight may be NEGATIVE now: a negative weight is an INHIBITORY synapse --
|
|
93
|
+
# when src fires it DRAINS the target's membrane potential instead of adding
|
|
94
|
+
# to it, so one neuron can veto/suppress another (see runtime propagation).
|
|
95
|
+
#
|
|
96
|
+
# delay= is optional (trailing, real per-synapse transmission delay --
|
|
97
|
+
# see runtime.py's Synapse.delay_ms / ConnectionDef.delay_ms above).
|
|
98
|
+
# Before this, the .spk TEXT grammar had NO way to express a delayed
|
|
99
|
+
# synapse at all -- the runtime and the pyspike.py Python builder API
|
|
100
|
+
# (NeuronRef.to(..., delay_ms=...)) already fully supported delay
|
|
101
|
+
# (commit 108ab13, 2026-08-16), but CONNECT_RE never captured it, so
|
|
102
|
+
# every .spk-file-parsed connection was silently forced to the default
|
|
103
|
+
# delay_ms=0.0 regardless of intent -- there was no syntax to even
|
|
104
|
+
# attempt a delayed connect from a text file, so nothing was being
|
|
105
|
+
# silently dropped mid-pipeline (the Izhikevich/AdEx pattern); the
|
|
106
|
+
# syntax itself simply didn't exist yet. This adds it.
|
|
107
|
+
CONNECT_RE = re.compile(
|
|
108
|
+
r"connect\s+(\w+)\s*->\s*(\w+)\s+weight=(-?[\d.]+)(?:\s+delay=([\d.]+))?")
|
|
109
|
+
ACTION_RE = re.compile(r"action\s+(\w+)\s*->\s*\[(\w+)\]")
|
|
110
|
+
REFRACTORY_RE = re.compile(r"refractory=(\d+)ms")
|
|
111
|
+
LEARN_RE = re.compile(r"learn=(\w+)\s+rate=([\d.]+)")
|
|
112
|
+
|
|
113
|
+
def parse(self, source: str) -> SpikelingAST:
|
|
114
|
+
ast = SpikelingAST()
|
|
115
|
+
errors = []
|
|
116
|
+
|
|
117
|
+
for lineno, raw_line in enumerate(source.splitlines(), start=1):
|
|
118
|
+
line = raw_line.strip()
|
|
119
|
+
if not line or line.startswith("#"):
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if line.startswith("neuron"):
|
|
123
|
+
m = self.NEURON_RE.match(line)
|
|
124
|
+
if m:
|
|
125
|
+
name, thresh, leak, ntype = m.groups()
|
|
126
|
+
ast.neurons.append(NeuronDef(
|
|
127
|
+
name=name,
|
|
128
|
+
threshold=int(thresh),
|
|
129
|
+
leak=int(leak),
|
|
130
|
+
neuron_type=ntype or "LIF"
|
|
131
|
+
))
|
|
132
|
+
else:
|
|
133
|
+
rm = self.RESONATOR_RE.match(line)
|
|
134
|
+
if rm:
|
|
135
|
+
name, freq, damping, coupling = rm.groups()
|
|
136
|
+
ast.neurons.append(NeuronDef(
|
|
137
|
+
name=name,
|
|
138
|
+
threshold=0, # unused for Resonator; kept for C codegen compatibility
|
|
139
|
+
leak=0, # unused for Resonator
|
|
140
|
+
neuron_type="Resonator",
|
|
141
|
+
freq_hz=float(freq),
|
|
142
|
+
damping=float(damping),
|
|
143
|
+
coupling=float(coupling) if coupling else None,
|
|
144
|
+
))
|
|
145
|
+
else:
|
|
146
|
+
errors.append(f"Line {lineno}: malformed neuron — '{line}'")
|
|
147
|
+
|
|
148
|
+
elif line.startswith("connect"):
|
|
149
|
+
m = self.CONNECT_RE.match(line)
|
|
150
|
+
if m:
|
|
151
|
+
src, dst, weight, delay = m.groups()
|
|
152
|
+
ast.connections.append(ConnectionDef(
|
|
153
|
+
src, dst, float(weight),
|
|
154
|
+
delay_ms=float(delay) if delay is not None else 0.0))
|
|
155
|
+
else:
|
|
156
|
+
errors.append(f"Line {lineno}: malformed connect — '{line}'")
|
|
157
|
+
|
|
158
|
+
elif line.startswith("action"):
|
|
159
|
+
m = self.ACTION_RE.match(line)
|
|
160
|
+
if m:
|
|
161
|
+
neuron, command = m.groups()
|
|
162
|
+
ast.actions.append(ActionDef(neuron, command))
|
|
163
|
+
else:
|
|
164
|
+
errors.append(f"Line {lineno}: malformed action — '{line}'")
|
|
165
|
+
|
|
166
|
+
elif line.startswith("refractory"):
|
|
167
|
+
m = self.REFRACTORY_RE.match(line)
|
|
168
|
+
if m:
|
|
169
|
+
ast.refractory_ms = int(m.group(1))
|
|
170
|
+
else:
|
|
171
|
+
errors.append(f"Line {lineno}: malformed refractory — '{line}'")
|
|
172
|
+
|
|
173
|
+
elif line.startswith("learn"):
|
|
174
|
+
m = self.LEARN_RE.match(line)
|
|
175
|
+
if m:
|
|
176
|
+
ast.learn_rule = m.group(1)
|
|
177
|
+
ast.learn_rate = float(m.group(2))
|
|
178
|
+
else:
|
|
179
|
+
errors.append(f"Line {lineno}: malformed learn — '{line}'")
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
errors.append(f"Line {lineno}: unknown directive — '{line}'")
|
|
183
|
+
|
|
184
|
+
if errors:
|
|
185
|
+
raise SyntaxError("Spikeling parse errors:\n" + "\n".join(errors))
|
|
186
|
+
|
|
187
|
+
self._validate(ast)
|
|
188
|
+
return ast
|
|
189
|
+
|
|
190
|
+
def _validate(self, ast: SpikelingAST):
|
|
191
|
+
"""Semantic checks — names must exist before they're referenced."""
|
|
192
|
+
names = {n.name for n in ast.neurons}
|
|
193
|
+
for c in ast.connections:
|
|
194
|
+
if c.src not in names:
|
|
195
|
+
raise NameError(f"connect: unknown neuron '{c.src}'")
|
|
196
|
+
if c.dst not in names:
|
|
197
|
+
raise NameError(f"connect: unknown neuron '{c.dst}'")
|
|
198
|
+
for a in ast.actions:
|
|
199
|
+
if a.neuron not in names:
|
|
200
|
+
raise NameError(f"action: unknown neuron '{a.neuron}'")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ─────────────────────────────────────────────
|
|
204
|
+
# C Code Generator
|
|
205
|
+
# ─────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
C_HEADER_TEMPLATE = """\
|
|
208
|
+
/* Auto-generated by Spikeling Compiler — DO NOT EDIT */
|
|
209
|
+
#ifndef SPIKELING_HW_H
|
|
210
|
+
#define SPIKELING_HW_H
|
|
211
|
+
|
|
212
|
+
#include <stdint.h>
|
|
213
|
+
#include <stdio.h>
|
|
214
|
+
#include <string.h>
|
|
215
|
+
#include <math.h>
|
|
216
|
+
|
|
217
|
+
#ifndef M_PI
|
|
218
|
+
#define M_PI 3.14159265358979323846
|
|
219
|
+
#endif
|
|
220
|
+
|
|
221
|
+
/* ── Neuron ── */
|
|
222
|
+
typedef struct {{
|
|
223
|
+
const char* name;
|
|
224
|
+
uint32_t threshold;
|
|
225
|
+
uint32_t leak;
|
|
226
|
+
float membrane_potential;
|
|
227
|
+
float last_spike_time;
|
|
228
|
+
uint32_t fire_count;
|
|
229
|
+
}} Neuron;
|
|
230
|
+
|
|
231
|
+
/* ── Synapse ── */
|
|
232
|
+
typedef struct {{
|
|
233
|
+
uint32_t src_idx;
|
|
234
|
+
uint32_t dst_idx;
|
|
235
|
+
float weight;
|
|
236
|
+
}} Synapse;
|
|
237
|
+
|
|
238
|
+
/* ── Resonator ──
|
|
239
|
+
* A damped harmonic oscillator tuned to freq_hz, for frequency-selective
|
|
240
|
+
* detection (e.g. telling a specific tone/signature apart from other
|
|
241
|
+
* tones + noise) -- not pulsed like Neuron, continuously driven via
|
|
242
|
+
* spikeling_resonator_step(). See runtime/runtime.py's ResonatorState
|
|
243
|
+
* for the Python reference implementation this mirrors; gain is
|
|
244
|
+
* normalized by coupling ~ omega^2 so higher-frequency channels in the
|
|
245
|
+
* same bank don't respond weaker than low-frequency ones (measured in
|
|
246
|
+
* resonator-prototype/resonator_bank.py). */
|
|
247
|
+
typedef struct {{
|
|
248
|
+
const char* name;
|
|
249
|
+
float freq_hz;
|
|
250
|
+
float damping;
|
|
251
|
+
float coupling;
|
|
252
|
+
float threshold; /* RMS-amplitude level that counts as "detected" */
|
|
253
|
+
float gate_threshold; /* |x| below this: skip the x*x energy multiply */
|
|
254
|
+
float energy_time_constant; /* seconds -- see spikeling_resonator_step()'s
|
|
255
|
+
comment for why this must scale with dt,
|
|
256
|
+
not be a fixed per-step decay constant */
|
|
257
|
+
float x;
|
|
258
|
+
float v;
|
|
259
|
+
float energy_ema; /* mean-square amplitude, exponential moving average */
|
|
260
|
+
}} Resonator;
|
|
261
|
+
|
|
262
|
+
/* ── Network dimensions ── */
|
|
263
|
+
#define NEURON_COUNT {neuron_count}
|
|
264
|
+
#define SYNAPSE_COUNT {synapse_count}
|
|
265
|
+
#define RESONATOR_COUNT {resonator_count}
|
|
266
|
+
#define REFRACTORY_MS {refractory_ms}
|
|
267
|
+
#define LEARN_RATE {learn_rate}f
|
|
268
|
+
|
|
269
|
+
/* ── Neuron table ── */
|
|
270
|
+
extern Neuron neurons[NEURON_COUNT];
|
|
271
|
+
extern Synapse synapses[SYNAPSE_COUNT];
|
|
272
|
+
extern Resonator resonators[RESONATOR_COUNT];
|
|
273
|
+
|
|
274
|
+
/* ── Spike dispatch ── */
|
|
275
|
+
void spikeling_tick(uint32_t neuron_idx, float current_time_ms);
|
|
276
|
+
void spikeling_stdp_update(uint32_t pre_idx, uint32_t post_idx, float dt);
|
|
277
|
+
|
|
278
|
+
/* Advance one Resonator by one timestep with a shared drive signal
|
|
279
|
+
* (e.g. an audio sample). Returns 1 the instant it crosses its
|
|
280
|
+
* detection threshold (edge-triggered, like a neuron firing), else 0. */
|
|
281
|
+
int spikeling_resonator_step(uint32_t resonator_idx, float drive, float dt, float current_time_ms);
|
|
282
|
+
|
|
283
|
+
#endif /* SPIKELING_HW_H */
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
C_SOURCE_TEMPLATE = """\
|
|
287
|
+
/* Auto-generated by Spikeling Compiler — DO NOT EDIT */
|
|
288
|
+
#include "spikeling_hw.h"
|
|
289
|
+
#include <math.h>
|
|
290
|
+
|
|
291
|
+
/* ── Neuron definitions ── */
|
|
292
|
+
Neuron neurons[NEURON_COUNT] = {{
|
|
293
|
+
{neuron_rows}
|
|
294
|
+
}};
|
|
295
|
+
|
|
296
|
+
/* ── Synapse definitions ── */
|
|
297
|
+
Synapse synapses[SYNAPSE_COUNT] = {{
|
|
298
|
+
{synapse_rows}
|
|
299
|
+
}};
|
|
300
|
+
|
|
301
|
+
/* ── Action dispatch table ── */
|
|
302
|
+
static const char* action_table[NEURON_COUNT] = {{
|
|
303
|
+
{action_rows}
|
|
304
|
+
}};
|
|
305
|
+
|
|
306
|
+
/* ── Resonator definitions ── */
|
|
307
|
+
Resonator resonators[RESONATOR_COUNT] = {{
|
|
308
|
+
{resonator_rows}
|
|
309
|
+
}};
|
|
310
|
+
|
|
311
|
+
/* ── Resonator action dispatch table ── */
|
|
312
|
+
static const char* resonator_action_table[RESONATOR_COUNT] = {{
|
|
313
|
+
{resonator_action_rows}
|
|
314
|
+
}};
|
|
315
|
+
|
|
316
|
+
/* ── Spike tick ── */
|
|
317
|
+
void spikeling_tick(uint32_t idx, float current_time_ms) {{
|
|
318
|
+
Neuron* n = &neurons[idx];
|
|
319
|
+
float elapsed = current_time_ms - n->last_spike_time;
|
|
320
|
+
|
|
321
|
+
if (elapsed < REFRACTORY_MS) {{
|
|
322
|
+
return; /* Refractory lockout */
|
|
323
|
+
}}
|
|
324
|
+
|
|
325
|
+
/* Leak */
|
|
326
|
+
n->membrane_potential -= n->leak;
|
|
327
|
+
if (n->membrane_potential < 0) n->membrane_potential = 0;
|
|
328
|
+
|
|
329
|
+
/* Threshold check */
|
|
330
|
+
n->membrane_potential += n->threshold + 1; /* simulated input drive */
|
|
331
|
+
if (n->membrane_potential >= n->threshold) {{
|
|
332
|
+
n->membrane_potential = 0;
|
|
333
|
+
n->last_spike_time = current_time_ms;
|
|
334
|
+
n->fire_count++;
|
|
335
|
+
|
|
336
|
+
/* Emit action */
|
|
337
|
+
if (action_table[idx]) {{
|
|
338
|
+
printf("[SPIKE] %s -> %s\\n", n->name, action_table[idx]);
|
|
339
|
+
}}
|
|
340
|
+
|
|
341
|
+
/* Propagate to downstream synapses */
|
|
342
|
+
for (int s = 0; s < SYNAPSE_COUNT; s++) {{
|
|
343
|
+
if (synapses[s].src_idx == idx) {{
|
|
344
|
+
neurons[synapses[s].dst_idx].membrane_potential +=
|
|
345
|
+
synapses[s].weight * 50.0f;
|
|
346
|
+
}}
|
|
347
|
+
}}
|
|
348
|
+
}}
|
|
349
|
+
}}
|
|
350
|
+
|
|
351
|
+
/* ── Resonator step ──
|
|
352
|
+
* SAMPLE-RATE BUG, FOUND AND FIXED: the energy EMA's decay rate must be
|
|
353
|
+
* derived from dt and energy_time_constant, not a fixed per-step
|
|
354
|
+
* constant -- a fixed 0.01f implicitly assumes whatever dt it was tuned
|
|
355
|
+
* at (here, 2.5e-5s / 40kHz). Run this at a different sample rate (e.g.
|
|
356
|
+
* 1MHz, needed to accurately resolve a 40kHz signal -- see
|
|
357
|
+
* a separate 40kHz resonator sample-rate test) without rescaling,
|
|
358
|
+
* and the averaging window silently shrinks far below this resonator's
|
|
359
|
+
* own settling time, which collapsed Python-side detection accuracy
|
|
360
|
+
* from ~99% to ~45%/0%-recall before this was caught and fixed there
|
|
361
|
+
* first (see runtime.ResonatorState.step's docstring for the full
|
|
362
|
+
* story). Mirrored here for parity. */
|
|
363
|
+
int spikeling_resonator_step(uint32_t idx, float drive, float dt, float current_time_ms) {{
|
|
364
|
+
Resonator* r = &resonators[idx];
|
|
365
|
+
float omega = 2.0f * (float)M_PI * r->freq_hz;
|
|
366
|
+
float accel = -(omega * omega) * r->x - 2.0f * r->damping * omega * r->v;
|
|
367
|
+
accel += r->coupling * drive;
|
|
368
|
+
r->v += accel * dt;
|
|
369
|
+
r->x += r->v * dt;
|
|
370
|
+
|
|
371
|
+
float alpha = dt / r->energy_time_constant;
|
|
372
|
+
if (alpha > 1.0f) alpha = 1.0f;
|
|
373
|
+
|
|
374
|
+
float was_above = sqrtf(r->energy_ema) >= r->threshold;
|
|
375
|
+
/* Amplitude gating: skip the x*x multiply when |x| is below the
|
|
376
|
+
* noise floor (see runtime.ResonatorState.step's docstring for the
|
|
377
|
+
* full rationale + benchmarks/BENCHMARKS.md entry #6). Measured to
|
|
378
|
+
* be a wash on a CPU (~1.0x, modern multiply units are already
|
|
379
|
+
* pipelined/cheap) -- kept here only for behavioral parity with the
|
|
380
|
+
* Python runtime, not as a performance claim for this backend. */
|
|
381
|
+
if (fabsf(r->x) >= r->gate_threshold) {{
|
|
382
|
+
r->energy_ema += alpha * (r->x * r->x - r->energy_ema);
|
|
383
|
+
}} else {{
|
|
384
|
+
r->energy_ema -= alpha * r->energy_ema;
|
|
385
|
+
}}
|
|
386
|
+
float now_above = sqrtf(r->energy_ema) >= r->threshold;
|
|
387
|
+
|
|
388
|
+
if (now_above && !was_above) {{
|
|
389
|
+
if (resonator_action_table[idx]) {{
|
|
390
|
+
printf("[SPIKE] %s -> %s (t=%.2fms)\\n", r->name, resonator_action_table[idx], current_time_ms);
|
|
391
|
+
}}
|
|
392
|
+
return 1;
|
|
393
|
+
}}
|
|
394
|
+
return 0;
|
|
395
|
+
}}
|
|
396
|
+
|
|
397
|
+
/* ── STDP weight update ── */
|
|
398
|
+
void spikeling_stdp_update(uint32_t pre_idx, uint32_t post_idx, float dt) {{
|
|
399
|
+
float delta = LEARN_RATE * expf(-fabsf(dt) / 20.0f);
|
|
400
|
+
for (int s = 0; s < SYNAPSE_COUNT; s++) {{
|
|
401
|
+
if (synapses[s].src_idx == pre_idx && synapses[s].dst_idx == post_idx) {{
|
|
402
|
+
synapses[s].weight += (dt > 0) ? delta : -delta * 0.5f;
|
|
403
|
+
if (synapses[s].weight > 1.0f) synapses[s].weight = 1.0f;
|
|
404
|
+
if (synapses[s].weight < 0.0f) synapses[s].weight = 0.0f;
|
|
405
|
+
}}
|
|
406
|
+
}}
|
|
407
|
+
}}
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class CCodeGenerator:
|
|
412
|
+
"""Emits a .h + .c file pair from a SpikelingAST."""
|
|
413
|
+
|
|
414
|
+
# Mirrors runtime.DEFAULT_RESONATOR_BASE_GAIN -- duplicated rather than
|
|
415
|
+
# imported because the C backend is meant to be usable standalone
|
|
416
|
+
# (see Spikeling-Project/sdk-verilog), without a dependency on the
|
|
417
|
+
# Python runtime package. Keep these two constants in sync by hand.
|
|
418
|
+
DEFAULT_RESONATOR_BASE_GAIN = 4.0e-4
|
|
419
|
+
DEFAULT_RESONATOR_THRESHOLD = 0.0008
|
|
420
|
+
DEFAULT_RESONATOR_GATE_THRESHOLD = 0.00024
|
|
421
|
+
DEFAULT_RESONATOR_ENERGY_TIME_CONSTANT = 0.0025 # seconds; see spikeling_resonator_step()
|
|
422
|
+
|
|
423
|
+
def generate(self, ast: SpikelingAST, output_dir: str = "."):
|
|
424
|
+
lif_neurons = [n for n in ast.neurons if n.neuron_type != "Resonator"]
|
|
425
|
+
resonator_neurons = [n for n in ast.neurons if n.neuron_type == "Resonator"]
|
|
426
|
+
neuron_index = {n.name: i for i, n in enumerate(lif_neurons)}
|
|
427
|
+
|
|
428
|
+
# Header
|
|
429
|
+
header = C_HEADER_TEMPLATE.format(
|
|
430
|
+
neuron_count = len(lif_neurons),
|
|
431
|
+
synapse_count = len(ast.connections),
|
|
432
|
+
resonator_count = max(1, len(resonator_neurons)),
|
|
433
|
+
refractory_ms = ast.refractory_ms,
|
|
434
|
+
learn_rate = ast.learn_rate,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Neuron rows (LIF/Izhikevich/AdEx only -- Resonators have their own table)
|
|
438
|
+
neuron_rows = []
|
|
439
|
+
for n in lif_neurons:
|
|
440
|
+
neuron_rows.append(
|
|
441
|
+
f' {{"{n.name}", {n.threshold}, {n.leak}, 0.0f, 0.0f, 0}}'
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Synapse rows (Resonators don't currently participate in synapses)
|
|
445
|
+
synapse_rows = []
|
|
446
|
+
for c in ast.connections:
|
|
447
|
+
synapse_rows.append(
|
|
448
|
+
f" {{{neuron_index[c.src]}, {neuron_index[c.dst]}, {c.weight}f}}"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Action rows (NULL if no action defined for that neuron)
|
|
452
|
+
action_map = {a.neuron: a.command for a in ast.actions}
|
|
453
|
+
action_rows = []
|
|
454
|
+
for n in lif_neurons:
|
|
455
|
+
cmd = action_map.get(n.name)
|
|
456
|
+
action_rows.append(f' "{cmd}"' if cmd else " NULL")
|
|
457
|
+
|
|
458
|
+
# Resonator rows -- auto-derive coupling from frequency if the
|
|
459
|
+
# .spk file didn't specify one explicitly (same gain-normalization
|
|
460
|
+
# as runtime.SpikelingRuntime.__init__).
|
|
461
|
+
resonator_rows = []
|
|
462
|
+
resonator_action_rows = []
|
|
463
|
+
if resonator_neurons:
|
|
464
|
+
for n in resonator_neurons:
|
|
465
|
+
omega = 2 * math.pi * n.freq_hz
|
|
466
|
+
coupling = n.coupling if n.coupling is not None else self.DEFAULT_RESONATOR_BASE_GAIN * (omega ** 2)
|
|
467
|
+
resonator_rows.append(
|
|
468
|
+
f' {{"{n.name}", {n.freq_hz}f, {n.damping}f, {coupling}f, '
|
|
469
|
+
f'{self.DEFAULT_RESONATOR_THRESHOLD}f, {self.DEFAULT_RESONATOR_GATE_THRESHOLD}f, '
|
|
470
|
+
f'{self.DEFAULT_RESONATOR_ENERGY_TIME_CONSTANT}f, 0.0f, 0.0f, 0.0f}}'
|
|
471
|
+
)
|
|
472
|
+
cmd = action_map.get(n.name)
|
|
473
|
+
resonator_action_rows.append(f' "{cmd}"' if cmd else " NULL")
|
|
474
|
+
else:
|
|
475
|
+
# RESONATOR_COUNT is clamped to >= 1 for valid C array syntax
|
|
476
|
+
# even with no Resonator neurons in this network -- this is an
|
|
477
|
+
# inert placeholder entry that can never fire (threshold
|
|
478
|
+
# unreachable) and has no action.
|
|
479
|
+
resonator_rows.append(' {"_none_", 1.0f, 1.0f, 0.0f, 1e9f, 1e9f, 1.0f, 0.0f, 0.0f, 0.0f}')
|
|
480
|
+
resonator_action_rows.append(" NULL")
|
|
481
|
+
|
|
482
|
+
source = C_SOURCE_TEMPLATE.format(
|
|
483
|
+
neuron_rows = ",\n".join(neuron_rows) if neuron_rows else " /* none */",
|
|
484
|
+
synapse_rows = ",\n".join(synapse_rows) if synapse_rows else " /* none */",
|
|
485
|
+
action_rows = ",\n".join(action_rows) if action_rows else " /* none */",
|
|
486
|
+
resonator_rows = ",\n".join(resonator_rows),
|
|
487
|
+
resonator_action_rows = ",\n".join(resonator_action_rows),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
h_path = os.path.join(output_dir, "spikeling_hw.h")
|
|
491
|
+
c_path = os.path.join(output_dir, "spikeling_hw.c")
|
|
492
|
+
|
|
493
|
+
with open(h_path, "w", encoding="utf-8") as f:
|
|
494
|
+
f.write(header)
|
|
495
|
+
with open(c_path, "w", encoding="utf-8") as f:
|
|
496
|
+
f.write(source)
|
|
497
|
+
|
|
498
|
+
print(f"[compiler] emitted {h_path}")
|
|
499
|
+
print(f"[compiler] emitted {c_path}")
|
|
500
|
+
return h_path, c_path
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# ─────────────────────────────────────────────
|
|
504
|
+
# Public API
|
|
505
|
+
# ─────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
def compile_file(spk_path: str, output_dir: str = ".") -> SpikelingAST:
|
|
508
|
+
"""Parse a .spk file and emit C output. Returns the AST for the Python runtime."""
|
|
509
|
+
with open(spk_path, encoding="utf-8") as f:
|
|
510
|
+
source = f.read()
|
|
511
|
+
|
|
512
|
+
parser = SpikelingParser()
|
|
513
|
+
ast = parser.parse(source)
|
|
514
|
+
codegen = CCodeGenerator()
|
|
515
|
+
codegen.generate(ast, output_dir=output_dir)
|
|
516
|
+
return ast
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if __name__ == "__main__":
|
|
520
|
+
import sys
|
|
521
|
+
path = sys.argv[1] if len(sys.argv) > 1 else "examples/sound_localizer.spk"
|
|
522
|
+
compile_file(path, output_dir=".")
|
core/encoder/__init__.py
ADDED
|
File without changes
|