Skip to content

HDL Generation + Hardware Safety

Two cooperating paths out of the Python design space and into silicon:

  1. Verilog / SystemVerilog emissionVerilogGenerator 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.
  2. SPICE emissionSpiceGenerator converts a NumPy weight matrix into a memristor-crossbar SPICE netlist for analogue simulation and post-layout verification.
  3. 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.
Python
from sc_neurocore.hdl_gen import VerilogGenerator, SpiceGenerator

1. Mathematical formalism

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.

2.2 Formal-spec ↔ RTL 1:1 correspondence

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.

2.5 OpenROAD vs commercial tools

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

Python
from 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

Bash
cd 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 Only
yosys -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

Python
import 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

  1. Rushby J. (1993). Formal methods and digital systems validation for airborne systems. NASA Contractor Report 4551. (Sticky-violation rationale.)
  2. Wolf C. et al. (2012–present). Yosys Open Synthesis Suite. https://yosyshq.net/yosys/
  3. Ajayi T. et al. (2019). OpenROAD: Toward a Self-Driving, Open-Source Digital Layout Implementation Tool Chain. GOMAC Tech.
  4. Strukov D.B., Snider G.S., Stewart D.R., Williams R.S. (2008). The missing memristor found. Nature 453:80–83. (Memristor model basis.)
  5. Nagel L.W., Pederson D.O. (1973). SPICE (Simulation Program with Integrated Circuit Emphasis). UC Berkeley ERL Memo ERL-M382.
  6. Chakrabarti C. et al. (2018). Designing for reliability in stochastic computing. ACM TRETS 11(3), Article 21. (Safety-monitor background.)
  7. Š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:

Bash
python 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
41
42
43
44
45
46
47
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
187
188
189
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
191
192
193
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
195
196
197
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
199
200
201
202
203
204
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
233
234
235
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
133
134
135
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