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.
Files changed (40) hide show
  1. core/__init__.py +1 -0
  2. core/__main__.py +81 -0
  3. core/compiler/__init__.py +0 -0
  4. core/compiler/compiler.py +522 -0
  5. core/encoder/__init__.py +0 -0
  6. core/encoder/encoder.py +221 -0
  7. core/examples/agent_brain.spk +82 -0
  8. core/examples/signal_detector.spk +78 -0
  9. core/examples/signal_test.py +121 -0
  10. core/examples/sound_localizer.spk +18 -0
  11. core/examples/test_gating_accuracy_regression.py +83 -0
  12. core/examples/test_tone_detector.py +78 -0
  13. core/examples/tone_detector.spk +22 -0
  14. core/examples/word_learner.spk +297 -0
  15. core/hardware/__init__.py +0 -0
  16. core/hardware/acoustic_adapter.py +69 -0
  17. core/hardware/acoustic_anomaly_detector.py +189 -0
  18. core/hardware/baseline_deviation.py +192 -0
  19. core/hardware/emg_adapter.py +154 -0
  20. core/hardware/environmental_adapter.py +155 -0
  21. core/hardware/sensor_adapter.py +112 -0
  22. core/hardware/system_telemetry_adapter.py +249 -0
  23. core/hardware/video_adapter.py +146 -0
  24. core/memory/__init__.py +0 -0
  25. core/memory/memory.py +172 -0
  26. core/runtime/__init__.py +0 -0
  27. core/runtime/runtime.py +676 -0
  28. core/stdlib/neurons.spk +21 -0
  29. core/trainer/__init__.py +0 -0
  30. core/trainer/trainer.py +466 -0
  31. pyspike.py +417 -0
  32. pyspike_causal.py +276 -0
  33. pyspike_neuron_models.py +209 -0
  34. pyspike_surrogate_gradient.py +215 -0
  35. spikeling-1.0.2.dist-info/METADATA +136 -0
  36. spikeling-1.0.2.dist-info/RECORD +40 -0
  37. spikeling-1.0.2.dist-info/WHEEL +5 -0
  38. spikeling-1.0.2.dist-info/entry_points.txt +2 -0
  39. spikeling-1.0.2.dist-info/licenses/LICENSE +21 -0
  40. 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=".")
File without changes