HDL Generation + Hardware Safety
Two cooperating paths out of the Python design space and into silicon:
- Verilog / SystemVerilog emission —
VerilogGenerator converts a
Python description of an SC network (dense SC-layer instances,
neuron cores, LFSR encoders, popcount trees, event-driven AER) into
a synthesisable top-level module.
- SPICE emission —
SpiceGenerator converts a NumPy weight matrix
into a memristor-crossbar SPICE netlist for analogue simulation and
post-layout verification.
- Formally grounded safety RTL — a hand-written
neuro_safe_monitor SystemVerilog module with six runtime
invariants, each of which is the mirror of a theorem in
safety_bounds.lean (see Formal Proofs). An OpenROAD
ASIC-flow driver pushes the monitor (or any user SV) through Yosys
synthesis + optional OpenROAD place-and-route.
Pythonfrom sc_neurocore.hdl_gen import VerilogGenerator, SpiceGenerator
1.1 Q8.8 fixed-point number line
All monitor signals are Q8.8 (8 integer, 8 fractional), giving
$$
x_{\text{Q8.8}} = \lfloor x \cdot 2^{8} \rfloor,
\qquad
x \in \bigl[-128,\; 128 - 2^{-8}\bigr],
\qquad
\Delta = 2^{-8} \approx 0.0039.
$$
The sign-extended 16-bit representation wraps at $2^{15}$; the monitor
treats probe_scc_numer as signed, everything else as unsigned. Parameter
defaults correspond to:
MAX_CURRENT = 16'h7FFF → $+127.9961$,
MAX_VOLTAGE = 16'hC000 → $-16384/256 = -64.0$ in the sign-extended
reading, used as the upper bound on the saturation sign-magnitude,
COHERENCE_LIMIT = 16'h0100 → $+1.0$,
SC_DENOM = 16'h0100 → stream length $N = 256$,
LIF_V_MAX = 16'hC000 → same upper bound on the LIF membrane.
1.2 Safety invariants (SystemVerilog ↔ Lean)
The monitor's six properties each latch a 1-bit violation flag when
true; any of the seven Boolean literals below asserts hardware_halt:
$$
\begin{aligned}
[P1]\; & v_{\text{cv}} = (I > I_{\max}) \vee (V > V_{\max}), \
[P1]\; & v_{\text{coh}} = (\mathrm{coh} < \Theta), \
[P2]\; & v_{\text{mono}} = (\mathrm{coh}{t} < \mathrm{coh}), \
[P3]\; & v_{\text{prec}} = (k > N), \
[P4]\; & v_{\text{sc+}} = (a \oplus b > N), \
[P5]\; & v_{\text{mem}} = (V_{\text{mem}} > V_{\max}), \
[P6]\; & v_{\text{scc}} = (|\eta_{\mathrm{SCC}}| > d_{\mathrm{SCC}}).
\end{aligned}
$$
[P1–P2] correspond to the Lean theorems monitor_soundness and
safe_transition (pure-core Lean 4, proved). [P3–P6] correspond to
three axiomatised theorems (sc_precision_numerator_bound,
sc_add_preserves_range, scc_bounded) for which the Mathlib proof
roadmap is documented in safety_bounds.lean; [P6] is additionally
proved constructively at the hardware level by the absolute-value
computation on line 87 of the monitor.
1.3 Popcount tree latency
For an $N$-bit stream, the popcount tree has depth $\lceil \log_{2} N
\rceil$ — with $N = 256$ that is 8 pipeline stages. At a clock
period $T_{\text{clk}}$ the popcount result lags the input by
$$
t_{\text{pc}} = \lceil \log_{2} N \rceil \cdot T_{\text{clk}}.
$$
This is one of the input probes to [P3], so the monitor's end-to-end
response time is bounded by $t_{\text{pc}} + t_{\text{mon}}$ where
$t_{\text{mon}} \leq 1$ clock cycle. Absolute nanosecond figures
depend on the PDK's cell delays and are emitted by OpenROAD's
report_checks; they are not claimed here without a measured run.
1.4 SPICE crossbar conductance mapping
SpiceGenerator.generate_crossbar(weights, …) maps weights $w_{ij} \in
[0,\,1]$ to memristor conductances
$$
G_{ij} = G_{\mathrm{off}} + w_{ij} \cdot (G_{\mathrm{on}} - G_{\mathrm{off}}),
\qquad
R_{ij} = 1 / G_{ij},
$$
with $G_{\mathrm{on}} = 100\,\mu\mathrm{S}$ (10 kΩ) and
$G_{\mathrm{off}} = 1\,\mu\mathrm{S}$ (1 MΩ). Each row drives an
independent voltage source (Vin_r) and each column ties to a 1 kΩ
load resistor (Rload_c), giving a column voltage
$$
V_{\text{out},c} = \sum_{r} V_{\text{in},r} \cdot \frac{R_{\text{load}}}
{R_{r,c} + R_{\text{load}}},
$$
which is the analogue MAC (multiply-accumulate) that the crossbar
implements physically.
2. Theory (why this particular design)
2.1 Separate compilers for synth vs analogue
The :class:VerilogGenerator is intentionally thin (~90 LOC) because
the heavy lifting lives in the 46 hand-written Verilog cores under
the repo-root hdl/ tree (e.g. sc_dense_layer_core.v,
sc_bitstream_encoder.v, sc_aer_router.v, sc_lif_neuron.v,
sc_firing_rate_bank.v, plus 8 matching formal-property files under
hdl/formal/). The Python generator wires the pre-verified cores
together by name; it does not try to synthesise novel RTL on the
fly. This separation keeps the RTL part verifiable (the cores each
have their own testbench) and the Python part trivial (string
templating). Under src/sc_neurocore/hdl_gen/safety/ the repo
additionally ships two SystemVerilog files — safety_monitor.sv and
its testbench — which is what the neuro_safe_monitor described in
the rest of this page refers to.
Every property the monitor checks has a named Lean theorem. Crucially,
the monitor's expression is the same shape as the theorem's
conclusion — e.g. v_monotone = (probe_coherence < prev_coherence)
is the negation of monotone_coherence (c1 c2 : Q8_8) : c2 ≥ c1.
This makes it straightforward to audit the RTL against the proofs by
diff, and it is a precondition for future work that would turn the
correspondence into a machine-checked bridge (SymbiYosys + SVA → Lean).
2.3 Sticky violation flags
violation_flags are latched with |=-style sticky behaviour; once
a property has fired, it remains set until rst_n deasserts the
register file. This mirrors the way aviation flight-control monitors
behave (Rushby 1993): a single violation must not be "washed out" by a
subsequent good cycle — the safety-case analysis depends on every
violation being observable.
2.4 Nanosecond response budget
hardware_halt is a pure combinational OR of the seven violation
signals, latched on the next clock edge. The worst-case latency from
any input signal going bad to hardware_halt rising is therefore one
clock period plus the fan-in delay of the combinational OR tree.
Closed-loop f_max on a real PDK remains to be measured — the
generic-cell synth run in §7 does not produce a mapped critical
path number. Once run against SKY130 hd (or comparable) Liberty,
we expect the combinational path to stay well under the SC-tile
clock period (typically 2–4 ns at 250–500 MHz), but that is a
pending measurement, not a claim.
Either way, this is three to four orders of magnitude faster than the
~500 µs loop of the Python
:class:sc_neurocore.safety_cert.stochastic_doctor.StochasticDoctor
runtime check. The two layers are complementary: the hardware monitor
catches single-cycle excursions; the Python doctor catches slow
statistical drift.
The flow driver supports both "Yosys only" (always available) and
"Yosys + OpenROAD" (optional) modes. OpenROAD is chosen over
commercial PnR because (a) it is open-source and reproducible inside
Docker, (b) the safety-monitor module is small enough that OpenROAD's
gate counts and PPA (power-performance-area) results are directly
comparable to commercial tools on this design size, and (c) the
provenance chain is end-to-end inspectable — an auditor can re-run
every step.
2.6 SPICE as a sanity layer, not a specification
The memristor-crossbar netlist is emitted from Python so that the
same weight matrix used for the SC network can be pushed through
analogue SPICE and the two outputs compared. It is not the system's
source of truth — the Q8.8 / SC stream is — but it serves as a
ground-truth cross-check on the post-layout behaviour of mixed-signal
tiles.
3. Position in the pipeline
Text Only ┌─────────────────────┐ ┌───────────────────────┐
│ Python SC network │──────▶│ VerilogGenerator │
│ (layers + cores) │ │ (string templating) │
└─────────────────────┘ └──────────┬────────────┘
│ │
│ ▼
│ top.sv + cores/*.sv
│ │
│ ▼
│ ┌──────────────────┐
│ │ neuro_safe_monitor│◀── formal.md theorems
│ └─────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────┐
│ SpiceGenerator │ │ run_asic_ │
│ memristor x-bar │ │ flow.sh │
└─────────────────┘ └──────┬───────┘
│ │
▼ ▼
analogue .sp netlist Yosys synth → OpenROAD PnR
- Upstream. The :class:
VerilogGenerator is called by the
OrganismEmitter in evo_substrate.md whenever a fit organism
needs hardware deployment.
- Downstream. The generated SV feeds into the ASIC flow driver;
the safety monitor hooks every tile's probe bus unconditionally.
4. Features
- Python-driven top-level Verilog emission.
- 46 hand-written Verilog cores under
hdl/ (dense SC layer, LFSR,
AER router, popcount tree, bitstream encoder, LIF neuron, firing-
rate bank, AXI-Lite cfg, DMA controller, …) plus 8 formal-property
files under hdl/formal/, each with its own testbench.
- Equation-to-Verilog compiler for arbitrary ODEs (used by the HH,
Izhikevich, FitzHugh-Nagumo tiles).
- 6-property runtime safety monitor mirroring Lean 4 theorems.
- Adversarial testbench (
tb_safety_monitor.sv) that forces every
property to fire.
- Sticky per-property violation flags.
- Nanosecond-budget
hardware_halt output.
- OpenROAD / Yosys ASIC-flow driver with optional Docker fallback.
- Memristor crossbar SPICE netlist emitter with configurable
$G_{\mathrm{on}}$ / $G_{\mathrm{off}}$ / load resistance.
5. Usage
5.1 Emit a 3-layer SC network
Pythonfrom sc_neurocore.hdl_gen import VerilogGenerator
gen = VerilogGenerator(module_name="my_sc_net_top")
gen.add_layer("Dense", "l1", {"n_neurons": 32})
gen.add_layer("Dense", "l2", {"n_neurons": 32})
gen.add_layer("Dense", "l3", {"n_neurons": 10})
rtl = gen.generate()
gen.save_to_file("build/my_sc_net_top.sv")
Emits a module with clk, rst_n, input_bus[7:0], output_bus[7:0]
and three sc_dense_layer_core instances chained via 8-bit wires.
5.2 Synthesise with the safety monitor
Bashcd src/sc_neurocore/hdl_gen/openroad_flow
./run_asic_flow.sh # default: safety_monitor.sv
./run_asic_flow.sh --target my_sc_net_top.sv # point at generated RTL
./run_asic_flow.sh --docker # run through the OpenROAD image
Outputs:
build/synth/ — Yosys synthesis results (gate-level .v, stats).
build/reports/ — area, timing, cell-utilisation.
Real Yosys 0.33 run on the default monitor design (synth command,
generic cell library — no PDK mapping):
Text Only=== neuro_safe_monitor ===
Number of wires: 333
Number of wire bits: 493
Number of public wires: 14
Number of cells: 347
$_ANDNOT_ 104 $_AND_ 2 $_DFFE_PN0P_ 1
$_DFF_PN0_ 22 $_MUX_ 15 $_NAND_ 17
$_NOR_ 12 $_NOT_ 18 $_ORNOT_ 17
$_OR_ 92 $_XNOR_ 14 $_XOR_ 33
Wall: 0.25 s (Yosys 0.33 + abc)
Those are the exact numbers emitted by
Text Onlyyosys -p "read_verilog -sv .../safety_monitor.sv;
hierarchy -top neuro_safe_monitor;
synth; stat"
on 2026-04-20. Mapping to SKY130 hd (for tape-out area / timing)
requires dfflibmap -liberty + abc -liberty with a Liberty file
that is not bundled with Yosys Debian — install the sky130_fd_sc_hd
PDK and re-run the OpenROAD flow driver to get PPA numbers.
5.3 Emit a memristor-crossbar SPICE netlist
Pythonimport numpy as np
from sc_neurocore.hdl_gen import SpiceGenerator
W = np.random.default_rng(7).random((16, 16))
SpiceGenerator.generate_crossbar(W, "build/xbar_16x16.sp")
Example generated block:
Text Only* Memristor Crossbar 16x16
.PARAM VDD=1.0
Vin_0 in_0 0 DC 0.0
...
R_0_0 in_0 out_0 12345.67
R_0_1 in_0 out_1 56789.01
...
Rload_0 out_0 0 1k
...
.END
6. API reference
6.1 VerilogGenerator
| Method |
Purpose |
__init__(module_name) |
Names the top-level module. |
add_layer(layer_type, name, params) |
Appends a Dense / LFSR / AER / popcount layer spec. |
generate() -> str |
Returns the top-level Verilog source as a string. |
save_to_file(path) |
Writes generated Verilog to disk; OSError raised on failure. |
6.2 SpiceGenerator
| Method |
Purpose |
generate_crossbar(weights, filename) (static) |
Emits <filename>.sp with sources, memristors, loads. |
6.3 neuro_safe_monitor (SystemVerilog)
| Port / parameter |
Direction |
Width |
Purpose |
MAX_CURRENT |
param |
16 |
Q8.8 current cap |
MAX_VOLTAGE |
param |
16 |
Q8.8 voltage cap |
COHERENCE_LIMIT |
param |
16 |
Q8.8 floor for [P1] |
SC_DENOM |
param |
16 |
SC stream length $N$ |
LIF_V_MAX |
param |
16 |
upper bound for LIF membrane |
clk, rst_n |
in |
1 |
standard |
probe_current |
in |
16 |
[P1] |
probe_voltage |
in |
16 |
[P1] |
probe_coherence |
in |
16 |
[P1/P2] |
probe_popcount_k |
in |
16 |
[P3] |
probe_sc_add_result |
in |
16 |
[P4] |
probe_membrane |
in |
16 |
[P5] |
probe_scc_numer |
in |
16 (signed) |
[P6] |
probe_scc_denom |
in |
16 |
[P6] |
hardware_halt |
out |
1 |
asserts on any violation (sticky) |
violation_flags[5:0] |
out |
6 |
one sticky bit per property |
6.4 ASIC-flow driver (run_asic_flow.sh)
| Flag |
Purpose |
--target <file.sv> |
Override the default safety_monitor.sv target. |
--docker |
Run the full Yosys + OpenROAD stack in the openroad/flow image. |
| (no flag) |
Run Yosys synthesis only; skip OpenROAD if the binary is missing. |
7. Verified benchmarks
The HDL subsystem is not latency-critical on the Python side — the
heavy lifting runs once at synthesis time. Still, we measure the
three Python entry points for repeatability:
| Operation |
Throughput |
Latency |
VerilogGenerator.generate (3-layer top, in-memory) |
281 822 gen/s |
3.55 µs |
SpiceGenerator.generate_crossbar (16×16, disk write) |
2 551 gen/s |
392 µs |
SpiceGenerator.generate_crossbar (64×64, disk write) |
231 gen/s |
4.33 ms |
yosys synth; stat on safety_monitor.sv |
4.06 runs/s |
247 ms |
Yosys stat report (Yosys 0.33, default abc mapping to generic
cell library — no PDK):
| Metric |
Value |
| Wires |
333 |
| Wire bits |
493 |
| Public wires |
14 |
| Cells |
347 |
DFFs ($_DFF_PN0_) |
22 |
DFF-enable ($_DFFE_PN0P_) |
1 |
Max combinational depth (reported by abc) |
not emitted without liberty |
Interpretation.
- The Python emitters are negligible on the design-time path: one
3-layer top costs 5 µs, one 64×64 memristor netlist costs 4 ms
(dominated by
open(..., "w") syscalls, not the string build).
- The full
synth; stat flow on the monitor completes in ~250 ms
cold from shell, so the safety-monitor synth gate fits comfortably
in a pre-commit hook budget.
- Cell count (347) is ~2.5× the DFF count; the majority are combinational
AND-NOT / MUX / OR terms implementing the seven Boolean violation
conditions and their sticky-flag muxes. That matches the design: 6
properties × ~50 gates each, plus the six 1-bit sticky registers
and the 16-bit
prev_coherence register (22 DFFs total = 16 +
6).
- Mapped timing (f_max, critical path ns) is not emitted without a
Liberty file — those numbers appear only after
abc -liberty <lib>.
The claim that the monitor "closes timing at ≥500 MHz" is therefore
deferred to the real PDK run; the current release gate only asserts
the synth completes without errors.
Python timings are time.perf_counter deltas from
benchmarks/bench_hdl_gen.py; Yosys figures are the literal stat
output of Yosys 0.33 on Ubuntu 24.04.
8. Citations
- Rushby J. (1993). Formal methods and digital systems validation for
airborne systems. NASA Contractor Report 4551. (Sticky-violation
rationale.)
- Wolf C. et al. (2012–present). Yosys Open Synthesis Suite.
https://yosyshq.net/yosys/
- Ajayi T. et al. (2019). OpenROAD: Toward a Self-Driving, Open-Source
Digital Layout Implementation Tool Chain. GOMAC Tech.
- Strukov D.B., Snider G.S., Stewart D.R., Williams R.S. (2008). The
missing memristor found. Nature 453:80–83. (Memristor model basis.)
- Nagel L.W., Pederson D.O. (1973). SPICE (Simulation Program with
Integrated Circuit Emphasis). UC Berkeley ERL Memo ERL-M382.
- Chakrabarti C. et al. (2018). Designing for reliability in
stochastic computing. ACM TRETS 11(3), Article 21. (Safety-monitor
background.)
- Šotek M. (2026). SC-NeuroCore: formally grounded safety RTL.
Internal report, ANULUM.
9. Cell-level breakdown — where the 347 gates go
The Yosys stat output in §7 lists 12 cell types; the physical reason
each category exists is worth documenting because it gives a direct
handle on future optimisation.
| Cell type |
Count |
What it implements in this design |
$_ANDNOT_ |
104 |
violation terms of shape a & !b (range checks, coh < Θ, k > N, …) |
$_OR_ |
92 |
inner OR trees of the seven violation expressions |
$_XOR_ |
33 |
the probe_scc_numer < 0 ? ~x+1 : x two's-complement path |
$_DFF_PN0_ |
22 |
16-bit prev_coherence + 6-bit sticky violation_flags |
$_NOT_ |
18 |
inverters for the !rst_n + signed-abs path |
$_ORNOT_ |
17 |
a \| !b fragments of the monotone check |
$_NAND_ |
17 |
synth-mapper output of mixed AND-OR patterns |
$_MUX_ |
15 |
latch-vs-new selection of the 6 sticky violation bits on the DFF clock |
$_XNOR_ |
14 |
equality comparators on the 16-bit probes |
$_NOR_ |
12 |
synth-mapper output |
$_AND_ |
2 |
residual AND gates |
$_DFFE_PN0P_ |
1 |
hardware_halt edge-triggered register |
22 DFFs + 1 DFFE = 23 stateful cells. Of the 324 combinational cells,
~220 are directly attributable to the six violation expressions (≈37
gates per property after sharing); the remainder are the
two's-complement path for signed probe_scc_numer and the sticky-flag
muxes.
10. Reproducibility + determinism
Every number in §7 and §9 can be re-derived from the committed repo
with a clean clone + two commands:
Bashpython benchmarks/bench_hdl_gen.py # Python emission + yosys synth
./src/sc_neurocore/hdl_gen/openroad_flow/run_asic_flow.sh # full flow
The benchmark script writes benchmarks/results/bench_hdl_gen.json
atomically; a CI check that diffs this JSON across runs flags any
regression in generation throughput. The Yosys cell counts are
deterministic — the same read_verilog; synth; stat sequence on Yosys
0.33 against the same safety_monitor.sv produces byte-identical
stat output across runs. If your Yosys version differs, the cell
breakdown will too; pin Yosys through the nixpkgs.yosys_0_33 or
apt install yosys=0.33* channel for bit-reproducible gate counts.
run_asic_flow.sh writes its artefacts into build/synth/ and
build/reports/, both of which are gitignored; the script writes a
build/run_metadata.json with host, Yosys version, commit SHA, and a
SHA-256 digest of the input .sv file so post-hoc audit of any
synthesised bitstream can verify the provenance chain.
11. Known limitations
- No equivalence check between Python-emitted SV and the pre-built
cores. The top-level module chains instances by name; if a user
mistyped
sc_dense_layer_core's port list in Python, synthesis will
fail with a port-mismatch error rather than a helpful Python-side
diagnostic.
- ODE-to-Verilog compiler lives outside this module. The
VerilogGenerator class does not expose it yet — users who want the
HH / Izhikevich / FHN RTL paths consume the pre-generated .sv
files under the repo-root hdl/ tree directly.
- SPICE emitter ignores wire parasitics. The netlist contains only
ideal memristors and load resistors; BEOL stack capacitance and
access-transistor resistance must be added by hand for sub-100 nm
nodes.
- No power reporting. Yosys synthesis does not produce switching-
activity estimates; real power figures require OpenROAD with a VCD
trace or a commercial tool. The flow driver does not wire that yet.
- Monitor parameters are hard-coded in the RTL. Changing
COHERENCE_LIMIT from 1.0 to 0.75 requires editing the SV file or
overriding the parameter on instantiation — there is no Python-side
API for reparametrising a generated top-level monitor yet.
- No SVA (SystemVerilog Assertions). The six properties are
encoded as combinational Boolean expressions plus sticky flags. A
future refactor will express them as
assert property
SystemVerilog assertions so they can be proved by SymbiYosys
directly, closing the gap to the Lean specification.
- No formal RTL-vs-spec equivalence. The SystemVerilog monitor and
the Lean theorems are hand-aligned 1:1 by matching the shape of the
Boolean expressions (see §2.2). A machine-checked proof that the RTL
implements the Lean statements would require a SystemVerilog → Lean
embedding such as Kôika or Verilog-Lean; neither is wired in yet.
- Flow driver is Yosys-only by default. The OpenROAD path is
optional and needs the OpenROAD binary (or Docker image) on the
host. Without it, the driver prints a clear diagnostic and exits
zero after the Yosys stage; the release gate does not block on
OpenROAD.
- Memristor model is ideal-linear.
SpiceGenerator maps weights
linearly onto $[G_{\mathrm{off}},\,G_{\mathrm{on}}]$; no non-linear
I–V curve, no drift, no endurance / retention model. Analogue
verification against device silicon requires an augmented model
(e.g. the VTEAM or Yakopcic memristor).
Reference
- Python API:
src/sc_neurocore/hdl_gen/__init__.py (package root, 19 LOC)
src/sc_neurocore/hdl_gen/verilog_generator.py (86 LOC)
src/sc_neurocore/hdl_gen/spice_generator.py (54 LOC)
- Safety RTL:
src/sc_neurocore/hdl_gen/safety/safety_monitor.sv (118 LOC)
src/sc_neurocore/hdl_gen/safety/tb_safety_monitor.sv (202 LOC)
- Flow driver:
src/sc_neurocore/hdl_gen/openroad_flow/run_asic_flow.sh
(229 LOC).
- Matching Lean proofs: Formal Proofs.
sc_neurocore.hdl_gen
sc_neurocore.hdl_gen -- Tier: research (experimental / research).
VerilogGenerator
Generates Top-Level Verilog for a defined SC Network.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337 | class VerilogGenerator:
"""
Generates Top-Level Verilog for a defined SC Network.
"""
def __init__(self, module_name: str = "sc_network_top", bus_width: int = 8) -> None:
"""Initialise with a top-level module name."""
self.module_name = sanitize_ident(module_name, context="module name")
self.bus_width = self._require_positive_int(bus_width, "bus_width")
self.layers = [] # type: ignore[var-annotated]
self.wires = [] # type: ignore[var-annotated]
self.instances = [] # type: ignore[var-annotated]
def add_layer(self, layer_type: str, name: str, params: Dict[str, Any]) -> None:
"""Add a layer definition to the network."""
self.layers.append(
{
"type": layer_type,
"name": sanitize_ident(name, context="layer name"),
"params": params,
}
)
def generate(self, mode: str = "sync") -> str:
"""
Emits Verilog code.
"""
if mode == "async_aer":
emitter = AEREmitter(module_name=self.module_name, bus_width=self.bus_width)
for layer in self.layers:
emitter.add_layer(layer["type"], layer["name"], layer["params"])
return emitter.generate()
if mode != "sync":
raise ValueError("mode must be 'sync' or 'async_aer'")
self._validate_sync_layers()
layer_widths = self._sync_layer_widths()
input_width = layer_widths[0][0] if layer_widths else self.bus_width
output_width = layer_widths[-1][1] if layer_widths else self.bus_width
code = f"module {self.module_name} (\n"
code += " input wire clk,\n"
code += " input wire rst_n,\n"
code += f" input wire [{input_width - 1}:0] input_bus,\n"
code += f" output wire [{output_width - 1}:0] output_bus\n"
code += ");\n\n"
code += " // Internal Signals\n"
# Generate wires for connections
for i in range(len(layer_widths) - 1):
code += f" wire [{layer_widths[i][1] - 1}:0] layer_{i}_to_{i + 1};\n"
code += "\n"
# Instantiate Layers
dense_idx = 0
for i, layer in enumerate(self.layers):
l_type = layer["type"]
l_name = layer["name"]
if l_type == "Dense":
code += f" // Layer {i}: {l_name}\n"
code += " sc_dense_layer_core #(\n"
code += f" .NUM_NEURONS({layer['params']['n_neurons']})\n"
code += f" ) {l_name}_inst (\n"
code += " .clk(clk),\n"
code += " .rst_n(rst_n),\n"
# Connect Input
if dense_idx == 0:
code += " .input_bus(input_bus),\n"
else:
code += f" .input_bus(layer_{dense_idx - 1}_to_{dense_idx}),\n"
# Connect Output
if dense_idx == len(layer_widths) - 1:
code += " .output_bus(output_bus)\n"
else:
code += f" .output_bus(layer_{dense_idx}_to_{dense_idx + 1})\n"
code += " );\n\n"
dense_idx += 1
code += "endmodule\n"
source_modules = emit_sources_from_ir({"nodes": self.layers})
if source_modules:
code += f"\n\n{source_modules}\n"
return code
def _validate_sync_layers(self) -> None:
"""Reject sync RTL configurations that cannot be emitted faithfully."""
for layer in self.layers:
layer_type = layer["type"]
layer_name = layer["name"]
params = layer["params"]
if layer_type == "Dense":
if "n_neurons" not in params:
raise ValueError(f"Dense layer '{layer_name}' requires n_neurons")
self._require_positive_int(
params["n_neurons"],
f"Dense layer '{layer_name}' n_neurons",
)
for width_name in ("input_width", "output_width"):
if width_name in params:
self._require_positive_int(
params[width_name],
f"Dense layer '{layer_name}' {width_name}",
)
continue
if layer_type in _SYNC_AUXILIARY_LAYER_TYPES:
continue
raise ValueError(f"unsupported sync layer type '{layer_type}' for layer '{layer_name}'")
@staticmethod
def _require_positive_int(value: Any, name: str) -> int:
"""Return value as int after rejecting booleans and non-positive values."""
if isinstance(value, bool) or not isinstance(value, Integral) or int(value) <= 0:
raise ValueError(f"{name} must be a positive integer")
return int(value)
def _dense_input_width(self, params: Mapping[str, Any], previous_width: int | None) -> int:
if "input_width" in params:
return self._require_positive_int(params["input_width"], "input_width")
return previous_width if previous_width is not None else self.bus_width
def _dense_output_width(self, params: Mapping[str, Any]) -> int:
if "output_width" in params:
return self._require_positive_int(params["output_width"], "output_width")
return self._require_positive_int(params["n_neurons"], "n_neurons")
def _sync_layer_widths(self) -> list[tuple[int, int]]:
"""Return per-layer ``(input_width, output_width)`` and reject mismatches."""
widths: list[tuple[int, int]] = []
previous_width: int | None = None
previous_name: str | None = None
for layer in self.layers:
if layer["type"] != "Dense":
continue
name = layer["name"]
params = layer["params"]
input_width = self._dense_input_width(params, previous_width)
output_width = self._dense_output_width(params)
if previous_width is not None and input_width != previous_width:
raise ValueError(
f"{previous_name} -> {name} width mismatch: "
f"{previous_width} output bits cannot drive {input_width} input bits"
)
widths.append((input_width, output_width))
previous_width = output_width
previous_name = name
return widths
def emit_lfsr16_source(self, module_name: str = "sc_lfsr16_source", seed: int = 0xACE1) -> str:
"""Emit a standalone LFSR-16 stochastic source module."""
return Lfsr16Emitter(module_name=module_name, seed=seed).generate()
def emit_sobol16_source(self, module_name: str = "sc_sobol16_source", seed: int = 0) -> str:
"""Emit a standalone Sobol-16 stochastic source module."""
return Sobol16Emitter(module_name=module_name, seed=seed).generate()
def emit_sources_from_ir(self, ir: Any) -> str:
"""Emit standalone stochastic source modules declared in an IR payload."""
return emit_sources_from_ir(ir)
def emit_async_aer(self, module_name: str | None = None) -> str:
"""Emit the research-stage async AER wrapper."""
emitter = AEREmitter(module_name=module_name or self.module_name)
for layer in self.layers:
emitter.add_layer(layer["type"], layer["name"], layer["params"])
return emitter.generate()
def emit_kuramoto_phase(
self,
module_name: str | None = None,
*,
n_oscillators: int = 4,
omegas: list[float] | tuple[float, ...] | None = None,
initial_phases: list[float] | tuple[float, ...] | None = None,
coupling: float = 0.1,
dt: float = 1e-2,
data_width: int = 24,
fraction: int = 16,
lut_size: int = 64,
) -> str:
"""Emit the bounded research Kuramoto phase core."""
emitter = KuramotoEmitter(
module_name=module_name or self.module_name,
n_oscillators=n_oscillators,
omegas=omegas,
initial_phases=initial_phases,
coupling=coupling,
dt=dt,
data_width=data_width,
fraction=fraction,
lut_size=lut_size,
)
return emitter.generate()
def emit_halton16_source(self, module_name: str = "sc_halton16_source") -> str:
"""Emit a standalone Halton-16 stochastic source module."""
return Halton16Emitter(module_name=module_name).generate()
def emit_quasirandom_source(
self,
method: str = "sobol",
module_name: str | None = None,
seed: int = 0,
) -> str:
"""Emit a quasi-random source via the unified factory.
Parameters
----------
method : str
``"sobol"`` or ``"halton"``.
module_name : str, optional
Override the default module name.
seed : int
Seed for Sobol (ignored for Halton).
"""
if method not in {"sobol", "halton"}:
raise ValueError("method must be 'sobol' or 'halton'")
if method == "sobol":
return QuasiRandomEmitter(
method="sobol",
module_name=module_name,
seed=seed,
).generate()
return QuasiRandomEmitter(
method="halton",
module_name=module_name,
seed=seed,
).generate()
def emit_decorrelator(
self,
*,
num_streams: int = 8,
stream_width: int = 16,
shift_seed: int = 0xA5A5_5A5A,
) -> str:
"""Return the path to the sc_decorrelator HDL module.
The decorrelator is a static Verilog module — this method provides
the instantiation template for integration into top-level designs.
"""
return (
f" sc_decorrelator #(\n"
f" .NUM_STREAMS({num_streams}),\n"
f" .STREAM_WIDTH({stream_width}),\n"
f" .SHIFT_SEED(32'h{shift_seed:08X})\n"
f" ) decorrelator_inst (\n"
f" .clk(clk),\n"
f" .rst_n(rst_n),\n"
f" .source_bits(source_bits),\n"
f" .decorrelated(decorrelated_bus)\n"
f" );\n"
)
def emit_edt_controller(
self,
*,
data_width: int = 16,
margin: int = 0x0040,
stable_cycles: int = 8,
) -> str:
"""Return an instantiation template for the EDT controller."""
return (
f" sc_edt_controller #(\n"
f" .DATA_WIDTH({data_width}),\n"
f" .MARGIN(16'h{margin:04X}),\n"
f" .STABLE_CYCLES({stable_cycles})\n"
f" ) edt_inst (\n"
f" .clk(clk),\n"
f" .rst_n(rst_n),\n"
f" .enable(edt_enable),\n"
f" .accumulator(accumulator),\n"
f" .threshold(threshold),\n"
f" .decision_ready(decision_ready),\n"
f" .decision_value(decision_value),\n"
f" .freeze(freeze)\n"
f" );\n"
)
def emit_tmr_wrapper(
self,
module_name: str,
inputs: list[tuple[str, int]],
outputs: list[tuple[str, int]],
) -> str:
"""Generate a TMR wrapper for the given module."""
from .tmr_wrapper import generate_tmr_wrapper
return generate_tmr_wrapper(module_name=module_name, inputs=inputs, outputs=outputs)
def save_to_file(self, path: str) -> None:
"""Write generated Verilog to a file."""
try:
with open(path, "w") as f:
f.write(self.generate())
except OSError as exc:
logger.error("Failed to write Verilog to %s: %s", path, exc)
raise
|
__init__(module_name='sc_network_top', bus_width=8)
Initialise with a top-level module name.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
| def __init__(self, module_name: str = "sc_network_top", bus_width: int = 8) -> None:
"""Initialise with a top-level module name."""
self.module_name = sanitize_ident(module_name, context="module name")
self.bus_width = self._require_positive_int(bus_width, "bus_width")
self.layers = [] # type: ignore[var-annotated]
self.wires = [] # type: ignore[var-annotated]
self.instances = [] # type: ignore[var-annotated]
|
add_layer(layer_type, name, params)
Add a layer definition to the network.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
49
50
51
52
53
54
55
56
57 | def add_layer(self, layer_type: str, name: str, params: Dict[str, Any]) -> None:
"""Add a layer definition to the network."""
self.layers.append(
{
"type": layer_type,
"name": sanitize_ident(name, context="layer name"),
"params": params,
}
)
|
generate(mode='sync')
Emits Verilog code.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
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 | def generate(self, mode: str = "sync") -> str:
"""
Emits Verilog code.
"""
if mode == "async_aer":
emitter = AEREmitter(module_name=self.module_name, bus_width=self.bus_width)
for layer in self.layers:
emitter.add_layer(layer["type"], layer["name"], layer["params"])
return emitter.generate()
if mode != "sync":
raise ValueError("mode must be 'sync' or 'async_aer'")
self._validate_sync_layers()
layer_widths = self._sync_layer_widths()
input_width = layer_widths[0][0] if layer_widths else self.bus_width
output_width = layer_widths[-1][1] if layer_widths else self.bus_width
code = f"module {self.module_name} (\n"
code += " input wire clk,\n"
code += " input wire rst_n,\n"
code += f" input wire [{input_width - 1}:0] input_bus,\n"
code += f" output wire [{output_width - 1}:0] output_bus\n"
code += ");\n\n"
code += " // Internal Signals\n"
# Generate wires for connections
for i in range(len(layer_widths) - 1):
code += f" wire [{layer_widths[i][1] - 1}:0] layer_{i}_to_{i + 1};\n"
code += "\n"
# Instantiate Layers
dense_idx = 0
for i, layer in enumerate(self.layers):
l_type = layer["type"]
l_name = layer["name"]
if l_type == "Dense":
code += f" // Layer {i}: {l_name}\n"
code += " sc_dense_layer_core #(\n"
code += f" .NUM_NEURONS({layer['params']['n_neurons']})\n"
code += f" ) {l_name}_inst (\n"
code += " .clk(clk),\n"
code += " .rst_n(rst_n),\n"
# Connect Input
if dense_idx == 0:
code += " .input_bus(input_bus),\n"
else:
code += f" .input_bus(layer_{dense_idx - 1}_to_{dense_idx}),\n"
# Connect Output
if dense_idx == len(layer_widths) - 1:
code += " .output_bus(output_bus)\n"
else:
code += f" .output_bus(layer_{dense_idx}_to_{dense_idx + 1})\n"
code += " );\n\n"
dense_idx += 1
code += "endmodule\n"
source_modules = emit_sources_from_ir({"nodes": self.layers})
if source_modules:
code += f"\n\n{source_modules}\n"
return code
|
emit_lfsr16_source(module_name='sc_lfsr16_source', seed=44257)
Emit a standalone LFSR-16 stochastic source module.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
| def emit_lfsr16_source(self, module_name: str = "sc_lfsr16_source", seed: int = 0xACE1) -> str:
"""Emit a standalone LFSR-16 stochastic source module."""
return Lfsr16Emitter(module_name=module_name, seed=seed).generate()
|
emit_sobol16_source(module_name='sc_sobol16_source', seed=0)
Emit a standalone Sobol-16 stochastic source module.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
| def emit_sobol16_source(self, module_name: str = "sc_sobol16_source", seed: int = 0) -> str:
"""Emit a standalone Sobol-16 stochastic source module."""
return Sobol16Emitter(module_name=module_name, seed=seed).generate()
|
emit_sources_from_ir(ir)
Emit standalone stochastic source modules declared in an IR payload.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
| def emit_sources_from_ir(self, ir: Any) -> str:
"""Emit standalone stochastic source modules declared in an IR payload."""
return emit_sources_from_ir(ir)
|
emit_async_aer(module_name=None)
Emit the research-stage async AER wrapper.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
| def emit_async_aer(self, module_name: str | None = None) -> str:
"""Emit the research-stage async AER wrapper."""
emitter = AEREmitter(module_name=module_name or self.module_name)
for layer in self.layers:
emitter.add_layer(layer["type"], layer["name"], layer["params"])
return emitter.generate()
|
emit_kuramoto_phase(module_name=None, *, n_oscillators=4, omegas=None, initial_phases=None, coupling=0.1, dt=0.01, data_width=24, fraction=16, lut_size=64)
Emit the bounded research Kuramoto phase core.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
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 | def emit_kuramoto_phase(
self,
module_name: str | None = None,
*,
n_oscillators: int = 4,
omegas: list[float] | tuple[float, ...] | None = None,
initial_phases: list[float] | tuple[float, ...] | None = None,
coupling: float = 0.1,
dt: float = 1e-2,
data_width: int = 24,
fraction: int = 16,
lut_size: int = 64,
) -> str:
"""Emit the bounded research Kuramoto phase core."""
emitter = KuramotoEmitter(
module_name=module_name or self.module_name,
n_oscillators=n_oscillators,
omegas=omegas,
initial_phases=initial_phases,
coupling=coupling,
dt=dt,
data_width=data_width,
fraction=fraction,
lut_size=lut_size,
)
return emitter.generate()
|
emit_halton16_source(module_name='sc_halton16_source')
Emit a standalone Halton-16 stochastic source module.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
| def emit_halton16_source(self, module_name: str = "sc_halton16_source") -> str:
"""Emit a standalone Halton-16 stochastic source module."""
return Halton16Emitter(module_name=module_name).generate()
|
emit_quasirandom_source(method='sobol', module_name=None, seed=0)
Emit a quasi-random source via the unified factory.
Parameters
method : str
"sobol" or "halton".
module_name : str, optional
Override the default module name.
seed : int
Seed for Sobol (ignored for Halton).
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
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 | def emit_quasirandom_source(
self,
method: str = "sobol",
module_name: str | None = None,
seed: int = 0,
) -> str:
"""Emit a quasi-random source via the unified factory.
Parameters
----------
method : str
``"sobol"`` or ``"halton"``.
module_name : str, optional
Override the default module name.
seed : int
Seed for Sobol (ignored for Halton).
"""
if method not in {"sobol", "halton"}:
raise ValueError("method must be 'sobol' or 'halton'")
if method == "sobol":
return QuasiRandomEmitter(
method="sobol",
module_name=module_name,
seed=seed,
).generate()
return QuasiRandomEmitter(
method="halton",
module_name=module_name,
seed=seed,
).generate()
|
emit_decorrelator(*, num_streams=8, stream_width=16, shift_seed=2779077210)
Return the path to the sc_decorrelator HDL module.
The decorrelator is a static Verilog module — this method provides
the instantiation template for integration into top-level designs.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292 | def emit_decorrelator(
self,
*,
num_streams: int = 8,
stream_width: int = 16,
shift_seed: int = 0xA5A5_5A5A,
) -> str:
"""Return the path to the sc_decorrelator HDL module.
The decorrelator is a static Verilog module — this method provides
the instantiation template for integration into top-level designs.
"""
return (
f" sc_decorrelator #(\n"
f" .NUM_STREAMS({num_streams}),\n"
f" .STREAM_WIDTH({stream_width}),\n"
f" .SHIFT_SEED(32'h{shift_seed:08X})\n"
f" ) decorrelator_inst (\n"
f" .clk(clk),\n"
f" .rst_n(rst_n),\n"
f" .source_bits(source_bits),\n"
f" .decorrelated(decorrelated_bus)\n"
f" );\n"
)
|
emit_edt_controller(*, data_width=16, margin=64, stable_cycles=8)
Return an instantiation template for the EDT controller.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317 | def emit_edt_controller(
self,
*,
data_width: int = 16,
margin: int = 0x0040,
stable_cycles: int = 8,
) -> str:
"""Return an instantiation template for the EDT controller."""
return (
f" sc_edt_controller #(\n"
f" .DATA_WIDTH({data_width}),\n"
f" .MARGIN(16'h{margin:04X}),\n"
f" .STABLE_CYCLES({stable_cycles})\n"
f" ) edt_inst (\n"
f" .clk(clk),\n"
f" .rst_n(rst_n),\n"
f" .enable(edt_enable),\n"
f" .accumulator(accumulator),\n"
f" .threshold(threshold),\n"
f" .decision_ready(decision_ready),\n"
f" .decision_value(decision_value),\n"
f" .freeze(freeze)\n"
f" );\n"
)
|
emit_tmr_wrapper(module_name, inputs, outputs)
Generate a TMR wrapper for the given module.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
319
320
321
322
323
324
325
326
327
328 | def emit_tmr_wrapper(
self,
module_name: str,
inputs: list[tuple[str, int]],
outputs: list[tuple[str, int]],
) -> str:
"""Generate a TMR wrapper for the given module."""
from .tmr_wrapper import generate_tmr_wrapper
return generate_tmr_wrapper(module_name=module_name, inputs=inputs, outputs=outputs)
|
save_to_file(path)
Write generated Verilog to a file.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
330
331
332
333
334
335
336
337 | def save_to_file(self, path: str) -> None:
"""Write generated Verilog to a file."""
try:
with open(path, "w") as f:
f.write(self.generate())
except OSError as exc:
logger.error("Failed to write Verilog to %s: %s", path, exc)
raise
|
SpiceGenerator
Generates SPICE netlists for Memristive Crossbars.
Source code in src/sc_neurocore/hdl_gen/spice_generator.py
| Python |
|---|
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 | class SpiceGenerator:
"""
Generates SPICE netlists for Memristive Crossbars.
"""
@staticmethod
def generate_crossbar(weights: np.ndarray[Any, Any], filename: str) -> None:
"""
weights: (Rows, Cols) - Conductance values [0, 1] mapped to [G_off, G_on].
"""
rows, cols = weights.shape
g_on = 100e-6 # 100 uS (10 kOhm)
g_off = 1e-6 # 1 uS (1 MOhm)
netlist = f"* Memristor Crossbar {rows}x{cols}\n"
netlist += ".PARAM VDD=1.0\n\n"
# Inputs
for r in range(rows):
netlist += f"Vin_{r} in_{r} 0 DC 0.0\n"
# Memristors
for r in range(rows):
for c in range(cols):
w = weights[r, c]
g = g_off + w * (g_on - g_off)
r_val = 1.0 / g
netlist += f"R_{r}_{c} in_{r} out_{c} {r_val:.2f}\n"
# Outputs (current sensing ideally, here just nodes)
# Add load resistors
for c in range(cols):
netlist += f"Rload_{c} out_{c} 0 1k\n"
netlist += "\n.END\n"
with open(filename, "w") as f:
f.write(netlist)
logger.info("SPICE Netlist saved to %s", filename)
|
generate_crossbar(weights, filename)
staticmethod
weights: (Rows, Cols) - Conductance values [0, 1] mapped to [G_off, G_on].
Source code in src/sc_neurocore/hdl_gen/spice_generator.py
| Python |
|---|
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 | @staticmethod
def generate_crossbar(weights: np.ndarray[Any, Any], filename: str) -> None:
"""
weights: (Rows, Cols) - Conductance values [0, 1] mapped to [G_off, G_on].
"""
rows, cols = weights.shape
g_on = 100e-6 # 100 uS (10 kOhm)
g_off = 1e-6 # 1 uS (1 MOhm)
netlist = f"* Memristor Crossbar {rows}x{cols}\n"
netlist += ".PARAM VDD=1.0\n\n"
# Inputs
for r in range(rows):
netlist += f"Vin_{r} in_{r} 0 DC 0.0\n"
# Memristors
for r in range(rows):
for c in range(cols):
w = weights[r, c]
g = g_off + w * (g_on - g_off)
r_val = 1.0 / g
netlist += f"R_{r}_{c} in_{r} out_{c} {r_val:.2f}\n"
# Outputs (current sensing ideally, here just nodes)
# Add load resistors
for c in range(cols):
netlist += f"Rload_{c} out_{c} 0 1k\n"
netlist += "\n.END\n"
with open(filename, "w") as f:
f.write(netlist)
logger.info("SPICE Netlist saved to %s", filename)
|
AEREmitter
Emit a research-stage AER wrapper around the existing sync HDL path.
This is intentionally conservative: the compute pipeline remains clocked
and the output is wrapped in a 4-phase AER-style request/acknowledge
interface. It is not a QDI async network replacement.
Source code in src/sc_neurocore/hdl_gen/aer_emitter.py
| Python |
|---|
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 | class AEREmitter:
"""Emit a research-stage AER wrapper around the existing sync HDL path.
This is intentionally conservative: the compute pipeline remains clocked
and the output is wrapped in a 4-phase AER-style request/acknowledge
interface. It is not a QDI async network replacement.
"""
def __init__(self, module_name: str = "sc_network_async_aer", bus_width: int = 8) -> None:
self.module_name = sanitize_ident(module_name, context="module name")
self.bus_width = self._require_positive_int(bus_width, "bus_width")
self.layers: list[dict[str, Any]] = []
def add_layer(self, layer_type: str, name: str, params: dict[str, Any]) -> None:
self.layers.append(
{
"type": layer_type,
"name": sanitize_ident(name, context="layer name"),
"params": params,
}
)
def generate(self) -> str:
self._validate_layers()
layer_widths = self._dense_layer_widths()
input_width = layer_widths[0][0] if layer_widths else self.bus_width
spike_width = layer_widths[-1][1] if layer_widths else self.bus_width
addr_width = max(1, ceil(log2(spike_width)))
code = f"module {self.module_name} (\n"
code += " input wire clk,\n"
code += " input wire rst_n,\n"
code += f" input wire [{input_width - 1}:0] input_bus,\n"
code += " input wire aer_ack,\n"
code += " output reg aer_req,\n"
code += f" output reg [{addr_width - 1}:0] aer_addr,\n"
code += f" output wire [{spike_width - 1}:0] output_bus\n"
code += ");\n\n"
code += " // Research boundary: sync compute path with AER output wrapper.\n"
code += " // This is not a full asynchronous micropipeline implementation.\n"
code += f" wire [{spike_width - 1}:0] spike_vector;\n"
for i in range(len(layer_widths) - 1):
code += f" wire [{layer_widths[i][1] - 1}:0] layer_{i}_to_{i + 1};\n"
code += "\n"
dense_idx = 0
for i, layer in enumerate(self.layers):
if layer["type"] != "Dense":
continue
output_bus = (
"spike_vector"
if dense_idx == len(layer_widths) - 1
else f"layer_{dense_idx}_to_{dense_idx + 1}"
)
input_bus = "input_bus" if dense_idx == 0 else f"layer_{dense_idx - 1}_to_{dense_idx}"
code += f" // Sync layer {i}: {layer['name']}\n"
code += " sc_dense_layer_core #(\n"
code += f" .NUM_NEURONS({layer['params']['n_neurons']})\n"
code += f" ) {layer['name']}_inst (\n"
code += " .clk(clk),\n"
code += " .rst_n(rst_n),\n"
code += f" .input_bus({input_bus}),\n"
code += f" .output_bus({output_bus})\n"
code += " );\n\n"
dense_idx += 1
if not self.layers:
code += f" assign spike_vector = {spike_width}'b0;\n\n"
code += " assign output_bus = spike_vector;\n"
code += " wire spike_valid = |spike_vector;\n\n"
code += f" function [{addr_width - 1}:0] first_hot_index;\n"
code += f" input [{spike_width - 1}:0] vector;\n"
code += " integer k;\n"
code += " reg found;\n"
code += " begin\n"
code += f" first_hot_index = {addr_width}'d0;\n"
code += " found = 1'b0;\n"
code += f" for (k = 0; k < {spike_width}; k = k + 1) begin\n"
code += " if (!found && vector[k]) begin\n"
code += f" first_hot_index = k[{addr_width - 1}:0];\n"
code += " found = 1'b1;\n"
code += " end\n"
code += " end\n"
code += " end\n"
code += " endfunction\n\n"
code += f" wire [{addr_width - 1}:0] encoded_addr = first_hot_index(spike_vector);\n\n"
code += " always @(posedge clk or negedge rst_n) begin\n"
code += " if (!rst_n) begin\n"
code += " aer_req <= 1'b0;\n"
code += f" aer_addr <= {addr_width}'d0;\n"
code += " end else begin\n"
code += " if (!aer_req && spike_valid) begin\n"
code += " aer_req <= 1'b1;\n"
code += " aer_addr <= encoded_addr;\n"
code += " end else if (aer_req && aer_ack) begin\n"
code += " aer_req <= 1'b0;\n"
code += " end\n"
code += " end\n"
code += " end\n\n"
code += "endmodule\n"
return code
@staticmethod
def _require_positive_int(value: Any, name: str) -> int:
if isinstance(value, bool) or not isinstance(value, Integral) or int(value) <= 0:
raise ValueError(f"{name} must be a positive integer")
return int(value)
def _validate_layers(self) -> None:
for layer in self.layers:
if layer["type"] != "Dense":
raise ValueError(
f"unsupported async AER layer type '{layer['type']}' for layer '{layer['name']}'"
)
params = layer["params"]
if "n_neurons" not in params:
raise ValueError(f"Dense layer '{layer['name']}' requires n_neurons")
self._require_positive_int(
params["n_neurons"], f"Dense layer '{layer['name']}' n_neurons"
)
for width_name in ("input_width", "output_width"):
if width_name in params:
self._require_positive_int(
params[width_name],
f"Dense layer '{layer['name']}' {width_name}",
)
def _dense_input_width(self, params: Mapping[str, Any], previous_width: int | None) -> int:
if "input_width" in params:
return self._require_positive_int(params["input_width"], "input_width")
return previous_width if previous_width is not None else self.bus_width
def _dense_output_width(self, params: Mapping[str, Any]) -> int:
if "output_width" in params:
return self._require_positive_int(params["output_width"], "output_width")
return self._require_positive_int(params["n_neurons"], "n_neurons")
def _dense_layer_widths(self) -> list[tuple[int, int]]:
widths: list[tuple[int, int]] = []
previous_width: int | None = None
previous_name: str | None = None
for layer in self.layers:
params = layer["params"]
input_width = self._dense_input_width(params, previous_width)
output_width = self._dense_output_width(params)
if previous_width is not None and input_width != previous_width:
raise ValueError(
f"{previous_name} -> {layer['name']} width mismatch: "
f"{previous_width} output bits cannot drive {input_width} input bits"
)
widths.append((input_width, output_width))
previous_width = output_width
previous_name = layer["name"]
return widths
|
KuramotoEmitter
Emit a bounded fixed-point Kuramoto phase core for HDL experiments.
The generated RTL is intentionally narrow in scope:
- noiseless only
- all-to-all scalar coupling only
- fixed-point phase state
- LUT-based sine approximation
This is a synthesis exploration scaffold, not a drop-in replacement for
the production Kuramoto solvers.
Source code in src/sc_neurocore/hdl_gen/kuramoto_emitter.py
| Python |
|---|
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 | class KuramotoEmitter:
"""Emit a bounded fixed-point Kuramoto phase core for HDL experiments.
The generated RTL is intentionally narrow in scope:
- noiseless only
- all-to-all scalar coupling only
- fixed-point phase state
- LUT-based sine approximation
This is a synthesis exploration scaffold, not a drop-in replacement for
the production Kuramoto solvers.
"""
def __init__(
self,
module_name: str = "sc_kuramoto_phase_core",
*,
n_oscillators: int = 4,
omegas: list[float] | tuple[float, ...] | None = None,
initial_phases: list[float] | tuple[float, ...] | None = None,
coupling: float = 0.1,
dt: float = 1e-2,
data_width: int = 24,
fraction: int = 16,
lut_size: int = 64,
) -> None:
if n_oscillators < 1:
raise ValueError("n_oscillators must be >= 1")
if data_width < 16:
raise ValueError("data_width must be >= 16")
if not 0 < fraction < data_width:
raise ValueError("fraction must satisfy 0 < fraction < data_width")
if lut_size < 16 or lut_size & (lut_size - 1):
raise ValueError("lut_size must be a power of two >= 16")
self.module_name = sanitize_ident(module_name, context="module name")
self.n_oscillators = n_oscillators
self.data_width = data_width
self.fraction = fraction
self.lut_size = lut_size
self.dt = dt
self.coupling = coupling
self.omegas = list(omegas) if omegas is not None else [1.0] * n_oscillators
self.initial_phases = (
list(initial_phases) if initial_phases is not None else [0.0] * n_oscillators
)
if len(self.omegas) != n_oscillators:
raise ValueError("omegas length must equal n_oscillators")
if len(self.initial_phases) != n_oscillators:
raise ValueError("initial_phases length must equal n_oscillators")
def _fixed_int(self, value: float) -> int:
return int(round(value * (1 << self.fraction)))
def _signed_literal(self, value: int) -> str:
magnitude = abs(value)
if value < 0:
return f"-{self.data_width}'sd{magnitude}"
return f"{self.data_width}'sd{magnitude}"
def _lut_lines(self) -> list[str]:
lines = [
" function automatic signed [DATA_WIDTH-1:0] sin_lut;",
" input signed [DATA_WIDTH-1:0] phase_value;",
" reg signed [DATA_WIDTH-1:0] wrapped_phase;",
" reg [7:0] lut_index;",
" begin",
" wrapped_phase = wrap_phase(phase_value);",
" lut_index = (wrapped_phase * LUT_SIZE) / PHASE_MODULUS;",
" case (lut_index)",
]
for idx in range(self.lut_size):
value = self._fixed_int(math.sin((2.0 * math.pi * idx) / self.lut_size))
lines.append(f" 8'd{idx}: sin_lut = {self._signed_literal(value)};")
lines.extend(
[
" default: sin_lut = 0;",
" endcase",
" end",
" endfunction",
]
)
return lines
def generate(self) -> str:
phase_modulus = self._fixed_int(2.0 * math.pi)
half_phase_modulus = self._fixed_int(math.pi)
dt_fixed = self._fixed_int(self.dt)
coupling_fixed = self._fixed_int(self.coupling / self.n_oscillators)
acc_width = self.data_width + max(4, math.ceil(math.log2(max(2, self.n_oscillators))) + 2)
lines = [
f"module {self.module_name} (",
" input wire clk,",
" input wire rst_n,",
" input wire step_en,",
" output reg update_done,",
f" output wire [{self.n_oscillators * self.data_width - 1}:0] phase_bus",
");",
"",
" // Research boundary: fixed-point noiseless Kuramoto phase core.",
" // This module keeps only the all-to-all scalar-coupling regime and",
" // does not attempt to cover the production Rust solver extensions.",
f" localparam integer N_OSC = {self.n_oscillators};",
f" localparam integer DATA_WIDTH = {self.data_width};",
f" localparam integer FRACTION = {self.fraction};",
f" localparam integer LUT_SIZE = {self.lut_size};",
f" localparam integer ACC_WIDTH = {acc_width};",
f" localparam signed [DATA_WIDTH-1:0] PHASE_MODULUS = {self._signed_literal(phase_modulus)};",
f" localparam signed [DATA_WIDTH-1:0] HALF_PHASE_MODULUS = {self._signed_literal(half_phase_modulus)};",
f" localparam signed [DATA_WIDTH-1:0] DT = {self._signed_literal(dt_fixed)};",
f" localparam signed [DATA_WIDTH-1:0] K_OVER_N = {self._signed_literal(coupling_fixed)};",
"",
" function automatic signed [DATA_WIDTH-1:0] wrap_phase;",
" input signed [DATA_WIDTH-1:0] phase_value;",
" reg signed [DATA_WIDTH-1:0] wrapped;",
" begin",
" wrapped = phase_value;",
" if (wrapped >= PHASE_MODULUS) begin",
" wrapped = wrapped - PHASE_MODULUS;",
" end else if (wrapped < 0) begin",
" wrapped = wrapped + PHASE_MODULUS;",
" end",
" wrap_phase = wrapped;",
" end",
" endfunction",
"",
" function automatic signed [DATA_WIDTH-1:0] wrap_delta;",
" input signed [DATA_WIDTH-1:0] delta_value;",
" reg signed [DATA_WIDTH-1:0] wrapped;",
" begin",
" wrapped = delta_value;",
" if (wrapped > HALF_PHASE_MODULUS) begin",
" wrapped = wrapped - PHASE_MODULUS;",
" end else if (wrapped < -HALF_PHASE_MODULUS) begin",
" wrapped = wrapped + PHASE_MODULUS;",
" end",
" wrap_delta = wrapped;",
" end",
" endfunction",
"",
]
lines.extend(self._lut_lines())
lines.append("")
for idx, omega in enumerate(self.omegas):
lines.append(
f" localparam signed [DATA_WIDTH-1:0] OMEGA_{idx} = "
f"{self._signed_literal(self._fixed_int(omega))};"
)
for idx, phase in enumerate(self.initial_phases):
lines.append(
f" localparam signed [DATA_WIDTH-1:0] INIT_PHASE_{idx} = "
f"{self._signed_literal(self._fixed_int(phase % (2.0 * math.pi)))};"
)
lines.append("")
for idx in range(self.n_oscillators):
lines.append(f" reg signed [DATA_WIDTH-1:0] phase_reg_{idx};")
lines.append("")
for row in range(self.n_oscillators):
terms: list[str] = []
for col in range(self.n_oscillators):
lines.append(
f" wire signed [DATA_WIDTH-1:0] phase_diff_{row}_{col} = "
f"wrap_delta(phase_reg_{col} - phase_reg_{row});"
)
lines.append(
f" wire signed [DATA_WIDTH-1:0] sin_term_{row}_{col} = "
f"sin_lut(phase_diff_{row}_{col});"
)
if col != row:
terms.append(f"sin_term_{row}_{col}")
sum_expr = " + ".join(terms) if terms else f"{acc_width}'sd0"
lines.extend(
[
f" wire signed [ACC_WIDTH-1:0] coupling_sum_{row} = {sum_expr};",
f" wire signed [DATA_WIDTH+ACC_WIDTH-1:0] coupling_mult_{row} = coupling_sum_{row} * K_OVER_N;",
f" wire signed [DATA_WIDTH-1:0] coupling_term_{row} = coupling_mult_{row} >>> FRACTION;",
f" wire signed [DATA_WIDTH-1:0] phase_velocity_{row} = OMEGA_{row} + coupling_term_{row};",
f" wire signed [2*DATA_WIDTH-1:0] delta_mult_{row} = phase_velocity_{row} * DT;",
f" wire signed [DATA_WIDTH-1:0] phase_delta_{row} = delta_mult_{row} >>> FRACTION;",
f" wire signed [DATA_WIDTH-1:0] next_phase_{row} = "
f"wrap_phase(phase_reg_{row} + phase_delta_{row});",
"",
]
)
for idx in range(self.n_oscillators):
lines.append(
f" assign phase_bus[{(idx + 1) * self.data_width - 1}:{idx * self.data_width}] = phase_reg_{idx};"
)
lines.extend(
[
"",
" always @(posedge clk or negedge rst_n) begin",
" if (!rst_n) begin",
]
)
for idx in range(self.n_oscillators):
lines.append(f" phase_reg_{idx} <= INIT_PHASE_{idx};")
lines.extend(
[
" update_done <= 1'b0;",
" end else begin",
" update_done <= 1'b0;",
" if (step_en) begin",
]
)
for idx in range(self.n_oscillators):
lines.append(f" phase_reg_{idx} <= next_phase_{idx};")
lines.extend(
[
" update_done <= 1'b1;",
" end",
" end",
" end",
"endmodule",
]
)
return "\n".join(lines) + "\n"
|
Lfsr16Emitter
Emit a synthesisable standalone LFSR-16 Verilog module.
Source code in src/sc_neurocore/hdl_gen/lfsr16_emitter.py
| Python |
|---|
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 | class Lfsr16Emitter:
"""Emit a synthesisable standalone LFSR-16 Verilog module."""
def __init__(self, module_name: str = "sc_lfsr16_source", seed: int = 0xACE1) -> None:
self.module_name = sanitize_ident(module_name, context="module name")
self.seed = seed & 0xFFFF
if self.seed == 0:
self.seed = 0xACE1
def generate(self) -> str:
"""Return the standalone LFSR-16 Verilog module."""
seed_hex = f"16'h{self.seed:04X}"
first_sample_hex = f"16'h{self._advance(self.seed):04X}"
lines = [
f"module {self.module_name} (",
" input wire clk,",
" input wire rst_n,",
" input wire [15:0] threshold,",
" output wire bit_out,",
" output reg [15:0] state",
");",
"",
f" localparam [15:0] SEED = {seed_hex};",
f" localparam [15:0] FIRST_SAMPLE = {first_sample_hex};",
" wire feedback;",
"",
" // Compare the current generated sample before the next advance.",
" assign bit_out = (state < threshold);",
" assign feedback = state[0] ^ state[2] ^ state[3] ^ state[5];",
"",
" always @(posedge clk or negedge rst_n) begin",
" if (!rst_n) begin",
" state <= FIRST_SAMPLE;",
" end else begin",
" state <= {feedback, state[15:1]};",
" end",
" end",
"endmodule",
]
return "\n".join(lines)
@staticmethod
def _advance(state: int) -> int:
feedback = ((state >> 0) ^ (state >> 2) ^ (state >> 3) ^ (state >> 5)) & 1
return ((state >> 1) | (feedback << 15)) & 0xFFFF
|
generate()
Return the standalone LFSR-16 Verilog module.
Source code in src/sc_neurocore/hdl_gen/lfsr16_emitter.py
| Python |
|---|
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 | def generate(self) -> str:
"""Return the standalone LFSR-16 Verilog module."""
seed_hex = f"16'h{self.seed:04X}"
first_sample_hex = f"16'h{self._advance(self.seed):04X}"
lines = [
f"module {self.module_name} (",
" input wire clk,",
" input wire rst_n,",
" input wire [15:0] threshold,",
" output wire bit_out,",
" output reg [15:0] state",
");",
"",
f" localparam [15:0] SEED = {seed_hex};",
f" localparam [15:0] FIRST_SAMPLE = {first_sample_hex};",
" wire feedback;",
"",
" // Compare the current generated sample before the next advance.",
" assign bit_out = (state < threshold);",
" assign feedback = state[0] ^ state[2] ^ state[3] ^ state[5];",
"",
" always @(posedge clk or negedge rst_n) begin",
" if (!rst_n) begin",
" state <= FIRST_SAMPLE;",
" end else begin",
" state <= {feedback, state[15:1]};",
" end",
" end",
"endmodule",
]
return "\n".join(lines)
|
Sobol16Emitter
Emit a synthesisable standalone Sobol-16 Verilog module.
Source code in src/sc_neurocore/hdl_gen/sobol16_emitter.py
| Python |
|---|
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 | class Sobol16Emitter:
"""Emit a synthesisable standalone Sobol-16 Verilog module."""
def __init__(self, module_name: str = "sc_sobol16_source", seed: int = 0) -> None:
self.module_name = sanitize_ident(module_name, context="module name")
self.seed = seed & 0xFFFF
def generate(self) -> str:
"""Return the standalone Sobol-16 Verilog module."""
seed_hex = f"16'h{self.seed:04X}"
first_sample_hex = f"16'h{(self.seed ^ 0x8000) & 0xFFFF:04X}"
lines = [
f"module {self.module_name} (",
" input wire clk,",
" input wire rst_n,",
" input wire [15:0] threshold,",
" output wire bit_out,",
" output reg [15:0] value,",
" output reg [15:0] index",
");",
"",
f" localparam [15:0] SEED = {seed_hex};",
f" localparam [15:0] FIRST_SAMPLE = {first_sample_hex};",
"",
" reg [15:0] direction;",
"",
" // Compare the current generated sample before the next advance.",
" assign bit_out = (value < threshold);",
"",
" always @(*) begin",
" casez (index)",
" 16'b???????????????1: direction = 16'h8000;",
" 16'b??????????????10: direction = 16'h4000;",
" 16'b?????????????100: direction = 16'h2000;",
" 16'b????????????1000: direction = 16'h1000;",
" 16'b???????????10000: direction = 16'h0800;",
" 16'b??????????100000: direction = 16'h0400;",
" 16'b?????????1000000: direction = 16'h0200;",
" 16'b????????10000000: direction = 16'h0100;",
" 16'b???????100000000: direction = 16'h0080;",
" 16'b??????1000000000: direction = 16'h0040;",
" 16'b?????10000000000: direction = 16'h0020;",
" 16'b????100000000000: direction = 16'h0010;",
" 16'b???1000000000000: direction = 16'h0008;",
" 16'b??10000000000000: direction = 16'h0004;",
" 16'b?100000000000000: direction = 16'h0002;",
" 16'b1000000000000000: direction = 16'h0001;",
" default: direction = 16'h8000;",
" endcase",
" end",
"",
" always @(posedge clk or negedge rst_n) begin",
" if (!rst_n) begin",
" value <= FIRST_SAMPLE;",
" index <= 16'd1;",
" end else begin",
" value <= value ^ direction;",
" index <= index + 16'd1;",
" end",
" end",
"endmodule",
]
return "\n".join(lines)
|
generate()
Return the standalone Sobol-16 Verilog module.
Source code in src/sc_neurocore/hdl_gen/sobol16_emitter.py
| Python |
|---|
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 | def generate(self) -> str:
"""Return the standalone Sobol-16 Verilog module."""
seed_hex = f"16'h{self.seed:04X}"
first_sample_hex = f"16'h{(self.seed ^ 0x8000) & 0xFFFF:04X}"
lines = [
f"module {self.module_name} (",
" input wire clk,",
" input wire rst_n,",
" input wire [15:0] threshold,",
" output wire bit_out,",
" output reg [15:0] value,",
" output reg [15:0] index",
");",
"",
f" localparam [15:0] SEED = {seed_hex};",
f" localparam [15:0] FIRST_SAMPLE = {first_sample_hex};",
"",
" reg [15:0] direction;",
"",
" // Compare the current generated sample before the next advance.",
" assign bit_out = (value < threshold);",
"",
" always @(*) begin",
" casez (index)",
" 16'b???????????????1: direction = 16'h8000;",
" 16'b??????????????10: direction = 16'h4000;",
" 16'b?????????????100: direction = 16'h2000;",
" 16'b????????????1000: direction = 16'h1000;",
" 16'b???????????10000: direction = 16'h0800;",
" 16'b??????????100000: direction = 16'h0400;",
" 16'b?????????1000000: direction = 16'h0200;",
" 16'b????????10000000: direction = 16'h0100;",
" 16'b???????100000000: direction = 16'h0080;",
" 16'b??????1000000000: direction = 16'h0040;",
" 16'b?????10000000000: direction = 16'h0020;",
" 16'b????100000000000: direction = 16'h0010;",
" 16'b???1000000000000: direction = 16'h0008;",
" 16'b??10000000000000: direction = 16'h0004;",
" 16'b?100000000000000: direction = 16'h0002;",
" 16'b1000000000000000: direction = 16'h0001;",
" default: direction = 16'h8000;",
" endcase",
" end",
"",
" always @(posedge clk or negedge rst_n) begin",
" if (!rst_n) begin",
" value <= FIRST_SAMPLE;",
" index <= 16'd1;",
" end else begin",
" value <= value ^ direction;",
" index <= index + 16'd1;",
" end",
" end",
"endmodule",
]
return "\n".join(lines)
|
QuasiRandomEmitter
Unified factory for quasi-random SNG emitters.
Parameters
method : str
"sobol" or "halton".
module_name : str, optional
Override the default module name.
seed : int, optional
Seed for Sobol (ignored for Halton).
Source code in src/sc_neurocore/hdl_gen/quasirandom_emitter.py
| Python |
|---|
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 | class QuasiRandomEmitter:
"""Unified factory for quasi-random SNG emitters.
Parameters
----------
method : str
``"sobol"`` or ``"halton"``.
module_name : str, optional
Override the default module name.
seed : int, optional
Seed for Sobol (ignored for Halton).
"""
METHODS = {"sobol", "halton"}
def __init__(
self,
method: Literal["sobol", "halton"] = "sobol",
module_name: str | None = None,
seed: int = 0,
) -> None:
if method not in self.METHODS:
raise ValueError(
f"Unknown quasi-random method {method!r}. Supported: {sorted(self.METHODS)}"
)
self.method = method
self._seed = seed
self._emitter: Sobol16Emitter | Halton16Emitter
if method == "sobol":
name = module_name or "sc_sobol16_source"
self._emitter = Sobol16Emitter(module_name=name, seed=seed)
else:
name = module_name or "sc_halton16_source"
self._emitter = Halton16Emitter(module_name=name)
logger.debug("QuasiRandomEmitter: method=%s, module=%s", method, name)
def generate(self) -> str:
"""Generate the Verilog source for the selected method."""
return self._emitter.generate()
@property
def module_name(self) -> str:
"""Return the sanitised module name."""
return self._emitter.module_name
|
module_name
property
Return the sanitised module name.
generate()
Generate the Verilog source for the selected method.
Source code in src/sc_neurocore/hdl_gen/quasirandom_emitter.py
| Python |
|---|
| def generate(self) -> str:
"""Generate the Verilog source for the selected method."""
return self._emitter.generate()
|
Halton16Emitter
Emit a synthesisable standalone Halton-16 (Van der Corput base-2) module.
Architecture: pure counter + bit-reversal wiring.
Zero multipliers, zero LUTs for core logic.
Source code in src/sc_neurocore/hdl_gen/quasirandom_emitter.py
| Python |
|---|
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 | class Halton16Emitter:
"""Emit a synthesisable standalone Halton-16 (Van der Corput base-2) module.
Architecture: pure counter + bit-reversal wiring.
Zero multipliers, zero LUTs for core logic.
"""
def __init__(
self,
module_name: str = "sc_halton16_source",
) -> None:
self.module_name = sanitize_ident(module_name, context="module name")
def generate(self) -> str:
"""Return the standalone Halton-16 Verilog module."""
lines = [
f"module {self.module_name} (",
" input wire clk,",
" input wire rst_n,",
" input wire enable,",
" output reg [15:0] quasi_random,",
" output reg valid",
");",
"",
" reg [15:0] counter;",
"",
" // Bit-reversal = Van der Corput base-2 radical inverse",
" // Pure routing — zero LUT cost",
" wire [15:0] reversed;",
"",
]
# Generate bit-reversal wiring
for i in range(16):
lines.append(f" assign reversed[{i}] = counter[{15 - i}];")
lines.extend(
[
"",
" always @(posedge clk or negedge rst_n) begin",
" if (!rst_n) begin",
" counter <= 16'd0;",
" quasi_random <= 16'd0;",
" valid <= 1'b0;",
" end else if (enable) begin",
" quasi_random <= reversed;",
" valid <= 1'b1;",
" counter <= counter + 16'd1;",
" end else begin",
" valid <= 1'b0;",
" end",
" end",
"endmodule",
]
)
return "\n".join(lines)
|
generate()
Return the standalone Halton-16 Verilog module.
Source code in src/sc_neurocore/hdl_gen/quasirandom_emitter.py
| Python |
|---|
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 | def generate(self) -> str:
"""Return the standalone Halton-16 Verilog module."""
lines = [
f"module {self.module_name} (",
" input wire clk,",
" input wire rst_n,",
" input wire enable,",
" output reg [15:0] quasi_random,",
" output reg valid",
");",
"",
" reg [15:0] counter;",
"",
" // Bit-reversal = Van der Corput base-2 radical inverse",
" // Pure routing — zero LUT cost",
" wire [15:0] reversed;",
"",
]
# Generate bit-reversal wiring
for i in range(16):
lines.append(f" assign reversed[{i}] = counter[{15 - i}];")
lines.extend(
[
"",
" always @(posedge clk or negedge rst_n) begin",
" if (!rst_n) begin",
" counter <= 16'd0;",
" quasi_random <= 16'd0;",
" valid <= 1'b0;",
" end else if (enable) begin",
" quasi_random <= reversed;",
" valid <= 1'b1;",
" counter <= counter + 16'd1;",
" end else begin",
" valid <= 1'b0;",
" end",
" end",
"endmodule",
]
)
return "\n".join(lines)
|
emit_sources_from_ir(ir)
Emit LFSR-16 and Sobol-16 source modules from a lightweight IR payload.
The helper accepts the mapping shapes already used by documentation,
tests, and compiler-service payloads: {"nodes": [...]},
{"nodes": {"node_id": {...}}}, or a direct iterable of node mappings.
Non-source nodes are ignored. Source nodes must identify their generator
through source_type, decorrelator, generator, strategy, or
the node type/node_type itself.
Source code in src/sc_neurocore/hdl_gen/verilog_generator.py
| Python |
|---|
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369 | def emit_sources_from_ir(ir: Any) -> str:
"""Emit LFSR-16 and Sobol-16 source modules from a lightweight IR payload.
The helper accepts the mapping shapes already used by documentation,
tests, and compiler-service payloads: ``{"nodes": [...]}``,
``{"nodes": {"node_id": {...}}}``, or a direct iterable of node mappings.
Non-source nodes are ignored. Source nodes must identify their generator
through ``source_type``, ``decorrelator``, ``generator``, ``strategy``, or
the node ``type``/``node_type`` itself.
"""
emitted = []
seen_names: set[str] = set()
for index, (node_id, node) in enumerate(_iter_ir_nodes(ir)):
kind = _source_kind(node)
if kind is None:
continue
module_name = _source_module_name(node, node_id=node_id, index=index)
if module_name in seen_names:
raise ValueError(f"duplicate stochastic source module name {module_name!r}")
seen_names.add(module_name)
seed = _source_seed(node, default=0xACE1 if kind == "lfsr16" else 0)
if kind == "lfsr16":
emitted.append(Lfsr16Emitter(module_name=module_name, seed=seed).generate())
elif kind == "sobol16":
emitted.append(Sobol16Emitter(module_name=module_name, seed=seed).generate())
elif kind == "halton16":
emitted.append(Halton16Emitter(module_name=module_name).generate())
else:
raise ValueError(f"unsupported stochastic source type {kind!r}")
return "\n\n".join(emitted)
|
generate_tmr_wrapper(module_name, inputs, outputs, *, voter_module='sc_tmr_voter')
Generate a TMR wrapper for a given Verilog module.
Parameters
module_name : str
Name of the target module to triplicate.
inputs : list of (name, width) tuples
Input ports of the target module.
outputs : list of (name, width) tuples
Output ports to protect with majority voting.
voter_module : str
Name of the voter module (default: sc_tmr_voter).
Returns
str
Generated Verilog source for the TMR wrapper.
Source code in src/sc_neurocore/hdl_gen/tmr_wrapper.py
| Python |
|---|
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 | def generate_tmr_wrapper(
module_name: str,
inputs: list[tuple[str, int]],
outputs: list[tuple[str, int]],
*,
voter_module: str = "sc_tmr_voter",
) -> str:
"""Generate a TMR wrapper for a given Verilog module.
Parameters
----------
module_name : str
Name of the target module to triplicate.
inputs : list of (name, width) tuples
Input ports of the target module.
outputs : list of (name, width) tuples
Output ports to protect with majority voting.
voter_module : str
Name of the voter module (default: ``sc_tmr_voter``).
Returns
-------
str
Generated Verilog source for the TMR wrapper.
"""
safe_name = sanitize_ident(module_name, context="TMR target module")
wrapper_name = f"{safe_name}_tmr"
lines: list[str] = []
lines.append(f"// Auto-generated TMR wrapper for {safe_name}")
lines.append("// Generated by SC-NeuroCore tmr_wrapper.py")
lines.append("")
lines.append(f"module {wrapper_name} (")
# Port declarations
port_lines: list[str] = []
for name, width in inputs:
safe = sanitize_ident(name, context="input port")
if width == 1:
port_lines.append(f" input wire {safe}")
else:
port_lines.append(f" input wire [{width - 1}:0] {safe}")
for name, width in outputs:
safe = sanitize_ident(name, context="output port")
if width == 1:
port_lines.append(f" output wire {safe}")
else:
port_lines.append(f" output wire [{width - 1}:0] {safe}")
# Error flags
for name, _width in outputs:
safe = sanitize_ident(name, context="output port")
port_lines.append(f" output wire {safe}_tmr_error")
lines.append(",\n".join(port_lines))
lines.append(");")
lines.append("")
# Declare internal wires for each replica
for replica in range(3):
for name, width in outputs:
safe = sanitize_ident(name, context="output port")
if width == 1:
lines.append(f" wire rep{replica}_{safe};")
else:
lines.append(f" wire [{width - 1}:0] rep{replica}_{safe};")
lines.append("")
# Instantiate three replicas
for replica in range(3):
lines.append(f" {safe_name} replica_{replica} (")
inst_ports: list[str] = []
for name, _width in inputs:
safe = sanitize_ident(name, context="input port")
inst_ports.append(f" .{safe}({safe})")
for name, _width in outputs:
safe = sanitize_ident(name, context="output port")
inst_ports.append(f" .{safe}(rep{replica}_{safe})")
lines.append(",\n".join(inst_ports))
lines.append(" );")
lines.append("")
# Instantiate voters for each output
for name, width in outputs:
safe = sanitize_ident(name, context="output port")
lines.append(f" {voter_module} #(.DATA_WIDTH({width})) voter_{safe} (")
lines.append(f" .a(rep0_{safe}),")
lines.append(f" .b(rep1_{safe}),")
lines.append(f" .c(rep2_{safe}),")
lines.append(f" .voted({safe}),")
lines.append(f" .error({safe}_tmr_error)")
lines.append(" );")
lines.append("")
lines.append("endmodule")
lines.append("")
result = "\n".join(lines)
logger.info(
"Generated TMR wrapper for %s (%d inputs, %d voted outputs)",
safe_name,
len(inputs),
len(outputs),
)
return result
|