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 54 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_aer_priority_queue.v, sc_lif_neuron.v, sc_firing_rate_bank.v, plus 11 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.

The precision-overflow trap follows the same fail-observable rule for mixed-precision datapaths. sc_precision_overflow_trap exposes both trap_event_vector/trap_event, which mirror accepted overflow lanes in the same cycle, and trap_vector/trap_latched, which retain every lane until the host asserts clear_trap or reset. Clear and reset dominate concurrent overflow pulses, so host intervention cannot accidentally re-latch stale saturation telemetry. Optional SC_NEUROCORE_ASSERTIONS properties bind the no-silent-overflow and sticky-latch contracts for formal or simulation audit runs.

Live-control parameter banks are generated from MMIOUpdateSpec with generate_live_parameter_bank(...). The emitted AXI4-Lite RTL uses BRAM/distributed RAM style hints per bank, fixed control/status register addresses, staged low/high write-data registers, CRC32-guarded shadow loads, explicit apply and rollback pulses, checksum-mismatch pulses, flattened active-only parameter_words output, and host-visible trap clear/status signals. It also derives sticky staged-overflow, staged-underflow, and CRC32-mismatch traps before shadow loading, and latches invalid bank/entry selection as trap bit 0x8 plus read-only bank writes as trap bit 0x10, so malformed MMIO payloads cannot truncate into active coefficients, bypass the update guard, mutate calibration/read-only constants, or masquerade as a loaded shadow update. This lets a deployed design hot-swap weights or phase-coupling coefficients while keeping the precision and trap contracts auditable. The same generated core rejects partial write strobes as trap bit 0x20 and returns a write error before any control or staged-data register is updated, matching the default supports_partial_write=False schema contract. Invalid active-readback bank or entry selections return a bus error and latch the sticky invalid_selection trap, preventing host verification code from confusing an invalid readback with a committed zero-valued coefficient. Successful shadow loads also latch the accepted bank and entry index. Apply and rollback use that latched identity, preventing post-load writes to bank_select or entry_index from redirecting an in-flight coefficient update. The local regression artefact benchmarks/results/local_python_2026-06-04_live_control_updates.json records the generated update-sequence timing, static RTL regeneration timing, and overflow/underflow trap-capture simulation under recorded process affinity; it is not a production throughput claim.

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
179
180
181
182
183
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"

        # _validate_layers() above rejects any non-Dense layer, so every layer is
        # Dense and its position ``i`` doubles as the dense-layer index.
        for i, layer in enumerate(self.layers):
            output_bus = "spike_vector" if i == len(layer_widths) - 1 else f"layer_{i}_to_{i + 1}"
            input_bus = "input_bus" if i == 0 else f"layer_{i - 1}_to_{i}"
            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"

        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"
        code += f"    reg [{spike_width - 1}:0] event_vector;\n"
        code += f"    reg [{spike_width - 1}:0] acknowledged_vector;\n"
        code += (
            "    wire new_spike_vector = spike_valid && (spike_vector != acknowledged_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 += f"            event_vector <= {spike_width}'d0;\n"
        code += f"            acknowledged_vector <= {spike_width}'d0;\n"
        code += "        end else begin\n"
        code += "            if (!spike_valid) begin\n"
        code += f"                acknowledged_vector <= {spike_width}'d0;\n"
        code += "            end\n"
        code += "            if (!aer_req && new_spike_vector) begin\n"
        code += "                aer_req <= 1'b1;\n"
        code += "                aer_addr <= encoded_addr;\n"
        code += "                event_vector <= spike_vector;\n"
        code += "            end else if (aer_req && aer_ack) begin\n"
        code += "                aer_req <= 1'b0;\n"
        code += "                acknowledged_vector <= event_vector;\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
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
338
339
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
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
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")
        if not math.isfinite(dt) or dt <= 0.0:
            raise ValueError("dt must be finite and positive")
        if not math.isfinite(coupling):
            raise ValueError("coupling must be finite")

        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")
        if not all(math.isfinite(omega) for omega in self.omegas):
            raise ValueError("omegas must contain only finite values")
        if not all(math.isfinite(phase) for phase in self.initial_phases):
            raise ValueError("initial_phases must contain only finite values")
        self._validate_fixed_point_format()
        self._validate_single_step_wrap_bound()

    def _fixed_int(self, value: float) -> int:
        return int(round(value * (1 << self.fraction)))

    def _require_representable_fixed(self, value: float, name: str) -> None:
        fixed = self._fixed_int(value)
        min_signed = -(1 << (self.data_width - 1))
        max_signed = (1 << (self.data_width - 1)) - 1
        if fixed < min_signed or fixed > max_signed:
            raise ValueError(
                f"{name} fixed-point value {fixed} exceeds signed Q{self.data_width - self.fraction}."
                f"{self.fraction} range [{min_signed}, {max_signed}]"
            )

    def _validate_fixed_point_format(self) -> None:
        try:
            self._require_representable_fixed(2.0 * math.pi, "phase modulus")
        except ValueError as exc:
            raise ValueError("fixed-point format cannot represent 2pi") from exc
        self._require_representable_fixed(math.pi, "half phase modulus")
        self._require_representable_fixed(self.dt, "dt")
        self._require_representable_fixed(self.coupling / self.n_oscillators, "coupling / N")
        for idx, omega in enumerate(self.omegas):
            self._require_representable_fixed(omega, f"omega[{idx}]")
        for idx, phase in enumerate(self.initial_phases):
            self._require_representable_fixed(phase % (2.0 * math.pi), f"initial_phases[{idx}]")

    def _validate_single_step_wrap_bound(self) -> None:
        max_omega = max(abs(omega) for omega in self.omegas)
        max_coupling_term = abs(self.coupling) * max(0, self.n_oscillators - 1) / self.n_oscillators
        max_phase_advance = self.dt * (max_omega + max_coupling_term)
        if max_phase_advance >= 2.0 * math.pi:
            raise ValueError("single-step phase advance must stay below 2pi")

    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 initial_phase_state_fixed(self) -> list[int]:
        """Return the emitted fixed-point reset state for each oscillator."""
        return [self._fixed_int(phase % (2.0 * math.pi)) for phase in self.initial_phases]

    def fixed_point_step(self, phase_state: list[int] | tuple[int, ...]) -> list[int]:
        """Mirror one generated RTL phase step in integer fixed-point arithmetic."""
        if len(phase_state) != self.n_oscillators:
            raise ValueError("phase_state length must equal n_oscillators")
        phases = [int(phase) for phase in phase_state]
        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)
        omega_fixed = [self._fixed_int(omega) for omega in self.omegas]
        next_phases: list[int] = []

        for row, row_phase in enumerate(phases):
            coupling_sum = 0
            for col, col_phase in enumerate(phases):
                if col == row:
                    continue
                phase_diff = self._wrap_delta_fixed(
                    col_phase - row_phase,
                    phase_modulus=phase_modulus,
                    half_phase_modulus=half_phase_modulus,
                )
                coupling_sum += self._sin_lut_fixed(phase_diff, phase_modulus=phase_modulus)
            coupling_term = (coupling_sum * coupling_fixed) >> self.fraction
            phase_velocity = omega_fixed[row] + coupling_term
            phase_delta = (phase_velocity * dt_fixed) >> self.fraction
            next_phases.append(self._wrap_phase_fixed(row_phase + phase_delta, phase_modulus))

        return next_phases

    def fixed_point_error_summary(self, *, steps: int) -> dict[str, int | float | list[float]]:
        """Characterise fixed-point drift against the float Kuramoto Euler step."""
        if steps < 1:
            raise ValueError("steps must be >= 1")

        fixed_state = self.initial_phase_state_fixed()
        float_state = [phase % (2.0 * math.pi) for phase in self.initial_phases]
        max_abs_error = 0.0
        sum_sq_error = 0.0
        sample_count = 0

        for _ in range(steps):
            fixed_state = self.fixed_point_step(fixed_state)
            float_state = self._float_step(float_state)
            fixed_float = self.fixed_state_to_float(fixed_state)
            errors = [
                abs(self._circular_phase_error(fixed_phase, float_phase))
                for fixed_phase, float_phase in zip(fixed_float, float_state)
            ]
            max_abs_error = max(max_abs_error, *errors)
            sum_sq_error += sum(error * error for error in errors)
            sample_count += len(errors)

        rms_error = math.sqrt(sum_sq_error / sample_count)
        return {
            "steps": steps,
            "oscillator_count": self.n_oscillators,
            "data_width": self.data_width,
            "fraction": self.fraction,
            "lut_size": self.lut_size,
            "dt": self.dt,
            "coupling": self.coupling,
            "max_abs_phase_error_rad": max_abs_error,
            "rms_phase_error_rad": rms_error,
            "final_fixed_phases_rad": self.fixed_state_to_float(fixed_state),
            "final_float_phases_rad": float_state,
        }

    def fixed_state_to_float(self, phase_state: list[int] | tuple[int, ...]) -> list[float]:
        """Convert integer fixed-point phase state to radians."""
        if len(phase_state) != self.n_oscillators:
            raise ValueError("phase_state length must equal n_oscillators")
        scale = float(1 << self.fraction)
        return [int(phase) / scale for phase in phase_state]

    def _float_step(self, phases: list[float]) -> list[float]:
        next_phases: list[float] = []
        for row, row_phase in enumerate(phases):
            coupling_sum = 0.0
            for col, col_phase in enumerate(phases):
                if col == row:
                    continue
                coupling_sum += math.sin(col_phase - row_phase)
            velocity = self.omegas[row] + (self.coupling * coupling_sum / self.n_oscillators)
            next_phases.append((row_phase + self.dt * velocity) % (2.0 * math.pi))
        return next_phases

    @staticmethod
    def _circular_phase_error(actual: float, expected: float) -> float:
        return ((actual - expected + math.pi) % (2.0 * math.pi)) - math.pi

    @staticmethod
    def _wrap_phase_fixed(phase_value: int, phase_modulus: int) -> int:
        if phase_value >= phase_modulus:
            return phase_value - phase_modulus
        if phase_value < 0:
            return phase_value + phase_modulus
        return phase_value

    @staticmethod
    def _wrap_delta_fixed(delta_value: int, *, phase_modulus: int, half_phase_modulus: int) -> int:
        if delta_value > half_phase_modulus:
            return delta_value - phase_modulus
        if delta_value < -half_phase_modulus:
            return delta_value + phase_modulus
        return delta_value

    def _sin_lut_fixed(self, phase_value: int, *, phase_modulus: int) -> int:
        wrapped_phase = self._wrap_phase_fixed(phase_value, phase_modulus)
        lut_index = (wrapped_phase * self.lut_size) // phase_modulus
        if lut_index < 0 or lut_index >= self.lut_size:
            return 0
        return self._fixed_int(math.sin((2.0 * math.pi * lut_index) / self.lut_size))

    def _lut_lines(self) -> list[str]:
        index_width = max(1, math.ceil(math.log2(self.lut_size)))
        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;",
            f"        reg [{index_width - 1}: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"                {index_width}'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"

initial_phase_state_fixed()

Return the emitted fixed-point reset state for each oscillator.

Source code in src/sc_neurocore/hdl_gen/kuramoto_emitter.py
Python
118
119
120
def initial_phase_state_fixed(self) -> list[int]:
    """Return the emitted fixed-point reset state for each oscillator."""
    return [self._fixed_int(phase % (2.0 * math.pi)) for phase in self.initial_phases]

fixed_point_step(phase_state)

Mirror one generated RTL phase step in integer fixed-point arithmetic.

Source code in src/sc_neurocore/hdl_gen/kuramoto_emitter.py
Python
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
def fixed_point_step(self, phase_state: list[int] | tuple[int, ...]) -> list[int]:
    """Mirror one generated RTL phase step in integer fixed-point arithmetic."""
    if len(phase_state) != self.n_oscillators:
        raise ValueError("phase_state length must equal n_oscillators")
    phases = [int(phase) for phase in phase_state]
    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)
    omega_fixed = [self._fixed_int(omega) for omega in self.omegas]
    next_phases: list[int] = []

    for row, row_phase in enumerate(phases):
        coupling_sum = 0
        for col, col_phase in enumerate(phases):
            if col == row:
                continue
            phase_diff = self._wrap_delta_fixed(
                col_phase - row_phase,
                phase_modulus=phase_modulus,
                half_phase_modulus=half_phase_modulus,
            )
            coupling_sum += self._sin_lut_fixed(phase_diff, phase_modulus=phase_modulus)
        coupling_term = (coupling_sum * coupling_fixed) >> self.fraction
        phase_velocity = omega_fixed[row] + coupling_term
        phase_delta = (phase_velocity * dt_fixed) >> self.fraction
        next_phases.append(self._wrap_phase_fixed(row_phase + phase_delta, phase_modulus))

    return next_phases

fixed_point_error_summary(*, steps)

Characterise fixed-point drift against the float Kuramoto Euler step.

Source code in src/sc_neurocore/hdl_gen/kuramoto_emitter.py
Python
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
def fixed_point_error_summary(self, *, steps: int) -> dict[str, int | float | list[float]]:
    """Characterise fixed-point drift against the float Kuramoto Euler step."""
    if steps < 1:
        raise ValueError("steps must be >= 1")

    fixed_state = self.initial_phase_state_fixed()
    float_state = [phase % (2.0 * math.pi) for phase in self.initial_phases]
    max_abs_error = 0.0
    sum_sq_error = 0.0
    sample_count = 0

    for _ in range(steps):
        fixed_state = self.fixed_point_step(fixed_state)
        float_state = self._float_step(float_state)
        fixed_float = self.fixed_state_to_float(fixed_state)
        errors = [
            abs(self._circular_phase_error(fixed_phase, float_phase))
            for fixed_phase, float_phase in zip(fixed_float, float_state)
        ]
        max_abs_error = max(max_abs_error, *errors)
        sum_sq_error += sum(error * error for error in errors)
        sample_count += len(errors)

    rms_error = math.sqrt(sum_sq_error / sample_count)
    return {
        "steps": steps,
        "oscillator_count": self.n_oscillators,
        "data_width": self.data_width,
        "fraction": self.fraction,
        "lut_size": self.lut_size,
        "dt": self.dt,
        "coupling": self.coupling,
        "max_abs_phase_error_rad": max_abs_error,
        "rms_phase_error_rad": rms_error,
        "final_fixed_phases_rad": self.fixed_state_to_float(fixed_state),
        "final_float_phases_rad": float_state,
    }

fixed_state_to_float(phase_state)

Convert integer fixed-point phase state to radians.

Source code in src/sc_neurocore/hdl_gen/kuramoto_emitter.py
Python
190
191
192
193
194
195
def fixed_state_to_float(self, phase_state: list[int] | tuple[int, ...]) -> list[float]:
    """Convert integer fixed-point phase state to radians."""
    if len(phase_state) != self.n_oscillators:
        raise ValueError("phase_state length must equal n_oscillators")
    scale = float(1 << self.fraction)
    return [int(phase) / scale for phase in phase_state]

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)

SideChannelEncodingEmitter

Emit a synthesisable ROM-style wrapper for one protected encoding record.

Source code in src/sc_neurocore/hdl_gen/side_channel_encoding_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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class SideChannelEncodingEmitter:
    """Emit a synthesisable ROM-style wrapper for one protected encoding record."""

    def __init__(
        self,
        *,
        module_name: str = "sc_side_channel_encoding_source",
        encoding: ActivityBalancedEncoding,
    ) -> None:
        self.module_name = sanitize_ident(module_name, context="module name")
        self.encoding = encoding

    def generate(self) -> str:
        """Return a Verilog module exposing payload and dummy stream bits."""

        bitstream_length = len(self.encoding.bitstream)
        dummy_streams = len(self.encoding.dummy_bitstreams)
        payload_bits = _bits_literal(self.encoding.bitstream)
        dummy_bits = _bits_literal(
            tuple(bit for stream in self.encoding.dummy_bitstreams for bit in stream)
        )
        dummy_width = max(dummy_streams, 1)
        sample_width = max((bitstream_length - 1).bit_length(), 1)
        lines = [
            f"module {self.module_name} (",
            f"    input wire [{sample_width - 1}:0] sample_index,",
            "    output wire payload_bit,",
            f"    output wire [{dummy_width - 1}:0] dummy_bits",
            ");",
            "",
            "    // Evidence boundary: analytic_simulation_only.",
            f"    localparam integer BITSTREAM_LENGTH = {bitstream_length};",
            f"    localparam integer DUMMY_STREAMS = {dummy_streams};",
            f"    localparam [{bitstream_length - 1}:0] PAYLOAD_BITS = "
            f"{bitstream_length}'b{payload_bits};",
            f"    localparam [{max(bitstream_length * dummy_streams, 1) - 1}:0] "
            f"DUMMY_BITS = {max(bitstream_length * dummy_streams, 1)}'b{dummy_bits};",
            "",
            "    assign payload_bit = PAYLOAD_BITS[sample_index];",
        ]
        if dummy_streams == 0:
            lines.append("    assign dummy_bits = 1'b0;")
        else:
            for index in range(dummy_streams):
                if index == 0:
                    offset = "sample_index"
                elif index == 1:
                    offset = "BITSTREAM_LENGTH + sample_index"
                else:
                    offset = f"BITSTREAM_LENGTH * {index} + sample_index"
                lines.append(f"    assign dummy_bits[{index}] = DUMMY_BITS[{offset}];")
        lines.append("endmodule")
        return "\n".join(lines)

    def manifest(self, *, verilog_path: str) -> dict[str, Any]:
        """Return transport metadata linking the HDL hook to analytic evidence."""

        return {
            "schema_version": SIDE_CHANNEL_HDL_HOOK_SCHEMA_VERSION,
            "module_name": self.module_name,
            "verilog_path": verilog_path,
            "evidence_boundary": self.encoding.evidence_boundary,
            "bitstream_length": len(self.encoding.bitstream),
            "dummy_streams": len(self.encoding.dummy_bitstreams),
            "payload_transitions": self.encoding.activity_summary.per_stream_transition_counts[0],
            "dummy_transitions": list(
                self.encoding.activity_summary.per_stream_transition_counts[1:]
            ),
        }

generate()

Return a Verilog module exposing payload and dummy stream bits.

Source code in src/sc_neurocore/hdl_gen/side_channel_encoding_emitter.py
Python
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
def generate(self) -> str:
    """Return a Verilog module exposing payload and dummy stream bits."""

    bitstream_length = len(self.encoding.bitstream)
    dummy_streams = len(self.encoding.dummy_bitstreams)
    payload_bits = _bits_literal(self.encoding.bitstream)
    dummy_bits = _bits_literal(
        tuple(bit for stream in self.encoding.dummy_bitstreams for bit in stream)
    )
    dummy_width = max(dummy_streams, 1)
    sample_width = max((bitstream_length - 1).bit_length(), 1)
    lines = [
        f"module {self.module_name} (",
        f"    input wire [{sample_width - 1}:0] sample_index,",
        "    output wire payload_bit,",
        f"    output wire [{dummy_width - 1}:0] dummy_bits",
        ");",
        "",
        "    // Evidence boundary: analytic_simulation_only.",
        f"    localparam integer BITSTREAM_LENGTH = {bitstream_length};",
        f"    localparam integer DUMMY_STREAMS = {dummy_streams};",
        f"    localparam [{bitstream_length - 1}:0] PAYLOAD_BITS = "
        f"{bitstream_length}'b{payload_bits};",
        f"    localparam [{max(bitstream_length * dummy_streams, 1) - 1}:0] "
        f"DUMMY_BITS = {max(bitstream_length * dummy_streams, 1)}'b{dummy_bits};",
        "",
        "    assign payload_bit = PAYLOAD_BITS[sample_index];",
    ]
    if dummy_streams == 0:
        lines.append("    assign dummy_bits = 1'b0;")
    else:
        for index in range(dummy_streams):
            if index == 0:
                offset = "sample_index"
            elif index == 1:
                offset = "BITSTREAM_LENGTH + sample_index"
            else:
                offset = f"BITSTREAM_LENGTH * {index} + sample_index"
            lines.append(f"    assign dummy_bits[{index}] = DUMMY_BITS[{offset}];")
    lines.append("endmodule")
    return "\n".join(lines)

manifest(*, verilog_path)

Return transport metadata linking the HDL hook to analytic evidence.

Source code in src/sc_neurocore/hdl_gen/side_channel_encoding_emitter.py
Python
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def manifest(self, *, verilog_path: str) -> dict[str, Any]:
    """Return transport metadata linking the HDL hook to analytic evidence."""

    return {
        "schema_version": SIDE_CHANNEL_HDL_HOOK_SCHEMA_VERSION,
        "module_name": self.module_name,
        "verilog_path": verilog_path,
        "evidence_boundary": self.encoding.evidence_boundary,
        "bitstream_length": len(self.encoding.bitstream),
        "dummy_streams": len(self.encoding.dummy_bitstreams),
        "payload_transitions": self.encoding.activity_summary.per_stream_transition_counts[0],
        "dummy_transitions": list(
            self.encoding.activity_summary.per_stream_transition_counts[1:]
        ),
    }

OnlineO1LearningEmitter

Emit a synthesisable reward-modulated STDP state machine.

Source code in src/sc_neurocore/hdl_gen/online_learning_emitter.py
Python
 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
class OnlineO1LearningEmitter:
    """Emit a synthesisable reward-modulated STDP state machine."""

    def __init__(
        self,
        *,
        module_name: str = "sc_online_o1_reward_stdp",
        config: OnlineO1Config | None = None,
    ) -> None:
        self.module_name = sanitize_ident(module_name, context="module name")
        self.config = config if config is not None else OnlineO1Config()

    def generate(self) -> str:
        """Return Verilog for one bounded online-learning synapse lane."""

        cfg = self.config
        return "\n".join(
            [
                f"module {self.module_name} #(",
                f"    parameter integer WEIGHT_BITS = {cfg.weight_bits},",
                f"    parameter integer TRACE_BITS = {cfg.trace_bits},",
                f"    parameter integer REWARD_BITS = {cfg.reward_bits},",
                f"    parameter integer LEARNING_SHIFT = {cfg.learning_shift},",
                f"    parameter integer TRACE_DECAY_SHIFT = {cfg.trace_decay_shift}",
                ") (",
                "    input wire clk,",
                "    input wire rst_n,",
                "    input wire step_en,",
                "    input wire pre_spike,",
                "    input wire post_spike,",
                "    input wire signed [REWARD_BITS-1:0] reward,",
                "    output wire [WEIGHT_BITS-1:0] current_weight,",
                "    output reg update_done",
                ");",
                "",
                f"    localparam integer PER_SYNAPSE_STATE_BITS = {cfg.per_synapse_state_bits};",
                "",
                "    reg [WEIGHT_BITS-1:0] weight;",
                "    reg [TRACE_BITS-1:0] pre_trace;",
                "    reg [TRACE_BITS-1:0] post_trace;",
                "    reg signed [TRACE_BITS-1:0] eligibility;",
                "",
                "    reg signed [TRACE_BITS:0] eligibility_delta;",
                "    reg signed [TRACE_BITS:0] decayed_eligibility;",
                "    reg signed [TRACE_BITS:0] potentiation_trace;",
                "    reg signed [WEIGHT_BITS+TRACE_BITS+REWARD_BITS-1:0] weight_update_acc;",
                "",
                "    function automatic [TRACE_BITS-1:0] decay_trace;",
                "        input [TRACE_BITS-1:0] value;",
                "        begin",
                "            if (TRACE_DECAY_SHIFT == 0)",
                "                decay_trace = value;",
                "            else",
                "                decay_trace = value - (value >> TRACE_DECAY_SHIFT);",
                "        end",
                "    endfunction",
                "",
                "    function automatic signed [TRACE_BITS-1:0] decay_signed_trace;",
                "        input signed [TRACE_BITS-1:0] value;",
                "        reg signed [TRACE_BITS-1:0] magnitude;",
                "        begin",
                "            if (TRACE_DECAY_SHIFT == 0) begin",
                "                decay_signed_trace = value;",
                "            end else if (value >= 0) begin",
                "                decay_signed_trace = value - (value >>> TRACE_DECAY_SHIFT);",
                "            end else begin",
                "                magnitude = -value;",
                "                decay_signed_trace = -(magnitude - (magnitude >>> TRACE_DECAY_SHIFT));",
                "            end",
                "        end",
                "    endfunction",
                "",
                "    function automatic signed [TRACE_BITS-1:0] sat_eligibility;",
                "        input signed [TRACE_BITS:0] value;",
                "        begin",
                "            if (value > $signed({1'b0, {TRACE_BITS-1{1'b1}}}))",
                "                sat_eligibility = $signed({1'b0, {TRACE_BITS-1{1'b1}}});",
                "            else if (value < $signed({1'b1, {TRACE_BITS-1{1'b0}}}))",
                "                sat_eligibility = $signed({1'b1, {TRACE_BITS-1{1'b0}}});",
                "            else",
                "                sat_eligibility = value[TRACE_BITS-1:0];",
                "        end",
                "    endfunction",
                "",
                "    function automatic [WEIGHT_BITS-1:0] sat_weight;",
                "        input signed [WEIGHT_BITS+TRACE_BITS+REWARD_BITS-1:0] value;",
                "        begin",
                "            if (value < 0)",
                "                sat_weight = {WEIGHT_BITS{1'b0}};",
                "            else if (value > $signed({1'b0, {WEIGHT_BITS{1'b1}}}))",
                "                sat_weight = {WEIGHT_BITS{1'b1}};",
                "            else",
                "                sat_weight = value[WEIGHT_BITS-1:0];",
                "        end",
                "    endfunction",
                "",
                "    assign current_weight = weight;",
                "",
                "    always @(*) begin",
                "        decayed_eligibility = decay_signed_trace(eligibility);",
                "        potentiation_trace = pre_spike ? {1'b0, {TRACE_BITS{1'b1}}} : $signed({1'b0, pre_trace});",
                "        eligibility_delta = post_spike ? potentiation_trace : '0;",
                "        eligibility_delta = eligibility_delta - (pre_spike ? $signed({1'b0, post_trace}) : '0);",
                "        weight_update_acc = $signed({1'b0, weight}) + (($signed(reward) * $signed(sat_eligibility(decayed_eligibility + eligibility_delta))) >>> LEARNING_SHIFT);",
                "    end",
                "",
                "    always @(posedge clk or negedge rst_n) begin",
                "        if (!rst_n) begin",
                "            weight <= {WEIGHT_BITS{1'b0}};",
                "            pre_trace <= {TRACE_BITS{1'b0}};",
                "            post_trace <= {TRACE_BITS{1'b0}};",
                "            eligibility <= {TRACE_BITS{1'b0}};",
                "            update_done <= 1'b0;",
                "        end else begin",
                "            update_done <= 1'b0;",
                "            if (step_en) begin",
                "                pre_trace <= pre_spike ? {TRACE_BITS{1'b1}} : decay_trace(pre_trace);",
                "                post_trace <= post_spike ? {TRACE_BITS{1'b1}} : decay_trace(post_trace);",
                "                eligibility <= sat_eligibility(decayed_eligibility + eligibility_delta);",
                "                weight <= sat_weight(weight_update_acc);",
                "                update_done <= 1'b1;",
                "            end",
                "        end",
                "    end",
                "endmodule",
            ]
        )

    def estimate_resources(
        self, *, n_synapses: int, target: str = "generic"
    ) -> OnlineO1ResourceEstimate:
        """Return a conservative pre-synthesis resource estimate.

        This is a deterministic planning estimate derived from the emitted state
        fields and one update lane. It deliberately does not claim synthesis,
        place-and-route, timing, power, or board measurement evidence.
        """

        if not isinstance(n_synapses, int) or isinstance(n_synapses, bool) or n_synapses <= 0:
            raise ValueError("n_synapses must be a positive integer")
        if not target:
            raise ValueError("target must be non-empty")

        cfg = self.config
        total_state_bits = n_synapses * cfg.per_synapse_state_bits
        lane_ff_bits = (
            cfg.per_synapse_state_bits
            + 3 * (cfg.trace_bits + 1)
            + cfg.weight_bits
            + cfg.trace_bits
            + cfg.reward_bits
            + 1
        )
        uses_dsp = target.lower() not in {"ice40", "gowin_littlebee", "generic_lut_only"}
        multiplier_luts = 0 if uses_dsp else math.ceil((cfg.reward_bits * cfg.trace_bits) / 4)
        estimated_luts = (
            24 + cfg.weight_bits + 2 * cfg.trace_bits + cfg.reward_bits + multiplier_luts
        )
        return OnlineO1ResourceEstimate(
            target=target,
            n_synapses=n_synapses,
            per_synapse_state_bits=cfg.per_synapse_state_bits,
            total_state_bits=total_state_bits,
            bram18_tiles=math.ceil(total_state_bits / 18_432),
            bram36_tiles=math.ceil(total_state_bits / 36_864),
            lane_ff_bits=lane_ff_bits,
            estimated_luts=estimated_luts,
            estimated_dsps=1 if uses_dsp else 0,
        )

    def manifest(self, *, verilog_path: str, n_synapses: int | None = None) -> dict[str, Any]:
        """Return deterministic metadata for the generated learning lane."""

        annotation = self.config.to_scnir_annotation(rule_id=self.module_name)
        resource_estimate = (
            self.estimate_resources(n_synapses=n_synapses, target="artix7").as_dict()
            if n_synapses is not None
            else None
        )
        return {
            "schema_version": ONLINE_O1_HDL_MANIFEST_SCHEMA_VERSION,
            "module_name": self.module_name,
            "verilog_path": verilog_path,
            "rule_family": self.config.rule_family,
            "per_synapse_state_bits": self.config.per_synapse_state_bits,
            "state_fields": annotation["state_fields"],
            "sequence_length_independent": True,
            "hidden_history_fields": [],
            "scnir_annotation": annotation,
            "resource_estimate": resource_estimate,
        }

generate()

Return Verilog for one bounded online-learning synapse lane.

Source code in src/sc_neurocore/hdl_gen/online_learning_emitter.py
Python
 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
def generate(self) -> str:
    """Return Verilog for one bounded online-learning synapse lane."""

    cfg = self.config
    return "\n".join(
        [
            f"module {self.module_name} #(",
            f"    parameter integer WEIGHT_BITS = {cfg.weight_bits},",
            f"    parameter integer TRACE_BITS = {cfg.trace_bits},",
            f"    parameter integer REWARD_BITS = {cfg.reward_bits},",
            f"    parameter integer LEARNING_SHIFT = {cfg.learning_shift},",
            f"    parameter integer TRACE_DECAY_SHIFT = {cfg.trace_decay_shift}",
            ") (",
            "    input wire clk,",
            "    input wire rst_n,",
            "    input wire step_en,",
            "    input wire pre_spike,",
            "    input wire post_spike,",
            "    input wire signed [REWARD_BITS-1:0] reward,",
            "    output wire [WEIGHT_BITS-1:0] current_weight,",
            "    output reg update_done",
            ");",
            "",
            f"    localparam integer PER_SYNAPSE_STATE_BITS = {cfg.per_synapse_state_bits};",
            "",
            "    reg [WEIGHT_BITS-1:0] weight;",
            "    reg [TRACE_BITS-1:0] pre_trace;",
            "    reg [TRACE_BITS-1:0] post_trace;",
            "    reg signed [TRACE_BITS-1:0] eligibility;",
            "",
            "    reg signed [TRACE_BITS:0] eligibility_delta;",
            "    reg signed [TRACE_BITS:0] decayed_eligibility;",
            "    reg signed [TRACE_BITS:0] potentiation_trace;",
            "    reg signed [WEIGHT_BITS+TRACE_BITS+REWARD_BITS-1:0] weight_update_acc;",
            "",
            "    function automatic [TRACE_BITS-1:0] decay_trace;",
            "        input [TRACE_BITS-1:0] value;",
            "        begin",
            "            if (TRACE_DECAY_SHIFT == 0)",
            "                decay_trace = value;",
            "            else",
            "                decay_trace = value - (value >> TRACE_DECAY_SHIFT);",
            "        end",
            "    endfunction",
            "",
            "    function automatic signed [TRACE_BITS-1:0] decay_signed_trace;",
            "        input signed [TRACE_BITS-1:0] value;",
            "        reg signed [TRACE_BITS-1:0] magnitude;",
            "        begin",
            "            if (TRACE_DECAY_SHIFT == 0) begin",
            "                decay_signed_trace = value;",
            "            end else if (value >= 0) begin",
            "                decay_signed_trace = value - (value >>> TRACE_DECAY_SHIFT);",
            "            end else begin",
            "                magnitude = -value;",
            "                decay_signed_trace = -(magnitude - (magnitude >>> TRACE_DECAY_SHIFT));",
            "            end",
            "        end",
            "    endfunction",
            "",
            "    function automatic signed [TRACE_BITS-1:0] sat_eligibility;",
            "        input signed [TRACE_BITS:0] value;",
            "        begin",
            "            if (value > $signed({1'b0, {TRACE_BITS-1{1'b1}}}))",
            "                sat_eligibility = $signed({1'b0, {TRACE_BITS-1{1'b1}}});",
            "            else if (value < $signed({1'b1, {TRACE_BITS-1{1'b0}}}))",
            "                sat_eligibility = $signed({1'b1, {TRACE_BITS-1{1'b0}}});",
            "            else",
            "                sat_eligibility = value[TRACE_BITS-1:0];",
            "        end",
            "    endfunction",
            "",
            "    function automatic [WEIGHT_BITS-1:0] sat_weight;",
            "        input signed [WEIGHT_BITS+TRACE_BITS+REWARD_BITS-1:0] value;",
            "        begin",
            "            if (value < 0)",
            "                sat_weight = {WEIGHT_BITS{1'b0}};",
            "            else if (value > $signed({1'b0, {WEIGHT_BITS{1'b1}}}))",
            "                sat_weight = {WEIGHT_BITS{1'b1}};",
            "            else",
            "                sat_weight = value[WEIGHT_BITS-1:0];",
            "        end",
            "    endfunction",
            "",
            "    assign current_weight = weight;",
            "",
            "    always @(*) begin",
            "        decayed_eligibility = decay_signed_trace(eligibility);",
            "        potentiation_trace = pre_spike ? {1'b0, {TRACE_BITS{1'b1}}} : $signed({1'b0, pre_trace});",
            "        eligibility_delta = post_spike ? potentiation_trace : '0;",
            "        eligibility_delta = eligibility_delta - (pre_spike ? $signed({1'b0, post_trace}) : '0);",
            "        weight_update_acc = $signed({1'b0, weight}) + (($signed(reward) * $signed(sat_eligibility(decayed_eligibility + eligibility_delta))) >>> LEARNING_SHIFT);",
            "    end",
            "",
            "    always @(posedge clk or negedge rst_n) begin",
            "        if (!rst_n) begin",
            "            weight <= {WEIGHT_BITS{1'b0}};",
            "            pre_trace <= {TRACE_BITS{1'b0}};",
            "            post_trace <= {TRACE_BITS{1'b0}};",
            "            eligibility <= {TRACE_BITS{1'b0}};",
            "            update_done <= 1'b0;",
            "        end else begin",
            "            update_done <= 1'b0;",
            "            if (step_en) begin",
            "                pre_trace <= pre_spike ? {TRACE_BITS{1'b1}} : decay_trace(pre_trace);",
            "                post_trace <= post_spike ? {TRACE_BITS{1'b1}} : decay_trace(post_trace);",
            "                eligibility <= sat_eligibility(decayed_eligibility + eligibility_delta);",
            "                weight <= sat_weight(weight_update_acc);",
            "                update_done <= 1'b1;",
            "            end",
            "        end",
            "    end",
            "endmodule",
        ]
    )

estimate_resources(*, n_synapses, target='generic')

Return a conservative pre-synthesis resource estimate.

This is a deterministic planning estimate derived from the emitted state fields and one update lane. It deliberately does not claim synthesis, place-and-route, timing, power, or board measurement evidence.

Source code in src/sc_neurocore/hdl_gen/online_learning_emitter.py
Python
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
def estimate_resources(
    self, *, n_synapses: int, target: str = "generic"
) -> OnlineO1ResourceEstimate:
    """Return a conservative pre-synthesis resource estimate.

    This is a deterministic planning estimate derived from the emitted state
    fields and one update lane. It deliberately does not claim synthesis,
    place-and-route, timing, power, or board measurement evidence.
    """

    if not isinstance(n_synapses, int) or isinstance(n_synapses, bool) or n_synapses <= 0:
        raise ValueError("n_synapses must be a positive integer")
    if not target:
        raise ValueError("target must be non-empty")

    cfg = self.config
    total_state_bits = n_synapses * cfg.per_synapse_state_bits
    lane_ff_bits = (
        cfg.per_synapse_state_bits
        + 3 * (cfg.trace_bits + 1)
        + cfg.weight_bits
        + cfg.trace_bits
        + cfg.reward_bits
        + 1
    )
    uses_dsp = target.lower() not in {"ice40", "gowin_littlebee", "generic_lut_only"}
    multiplier_luts = 0 if uses_dsp else math.ceil((cfg.reward_bits * cfg.trace_bits) / 4)
    estimated_luts = (
        24 + cfg.weight_bits + 2 * cfg.trace_bits + cfg.reward_bits + multiplier_luts
    )
    return OnlineO1ResourceEstimate(
        target=target,
        n_synapses=n_synapses,
        per_synapse_state_bits=cfg.per_synapse_state_bits,
        total_state_bits=total_state_bits,
        bram18_tiles=math.ceil(total_state_bits / 18_432),
        bram36_tiles=math.ceil(total_state_bits / 36_864),
        lane_ff_bits=lane_ff_bits,
        estimated_luts=estimated_luts,
        estimated_dsps=1 if uses_dsp else 0,
    )

manifest(*, verilog_path, n_synapses=None)

Return deterministic metadata for the generated learning lane.

Source code in src/sc_neurocore/hdl_gen/online_learning_emitter.py
Python
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def manifest(self, *, verilog_path: str, n_synapses: int | None = None) -> dict[str, Any]:
    """Return deterministic metadata for the generated learning lane."""

    annotation = self.config.to_scnir_annotation(rule_id=self.module_name)
    resource_estimate = (
        self.estimate_resources(n_synapses=n_synapses, target="artix7").as_dict()
        if n_synapses is not None
        else None
    )
    return {
        "schema_version": ONLINE_O1_HDL_MANIFEST_SCHEMA_VERSION,
        "module_name": self.module_name,
        "verilog_path": verilog_path,
        "rule_family": self.config.rule_family,
        "per_synapse_state_bits": self.config.per_synapse_state_bits,
        "state_fields": annotation["state_fields"],
        "sequence_length_independent": True,
        "hidden_history_fields": [],
        "scnir_annotation": annotation,
        "resource_estimate": resource_estimate,
    }

OnlineO1ResourceEstimate dataclass

Deterministic pre-synthesis resource estimate for one online-learning block.

Source code in src/sc_neurocore/hdl_gen/online_learning_emitter.py
Python
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
@dataclass(frozen=True, slots=True)
class OnlineO1ResourceEstimate:
    """Deterministic pre-synthesis resource estimate for one online-learning block."""

    target: str
    n_synapses: int
    per_synapse_state_bits: int
    total_state_bits: int
    bram18_tiles: int
    bram36_tiles: int
    lane_ff_bits: int
    estimated_luts: int
    estimated_dsps: int
    max_update_latency_cycles: int = 1
    evidence_class: str = "pre_synthesis_estimate"
    hardware_measurement_claimed: bool = False

    def as_dict(self) -> dict[str, Any]:
        """Return a deterministic JSON-ready estimate payload."""

        return {
            "schema_version": ONLINE_O1_RESOURCE_ESTIMATE_SCHEMA_VERSION,
            "target": self.target,
            "evidence_class": self.evidence_class,
            "n_synapses": self.n_synapses,
            "per_synapse_state_bits": self.per_synapse_state_bits,
            "total_state_bits": self.total_state_bits,
            "bram18_tiles": self.bram18_tiles,
            "bram36_tiles": self.bram36_tiles,
            "lane_ff_bits": self.lane_ff_bits,
            "estimated_luts": self.estimated_luts,
            "estimated_dsps": self.estimated_dsps,
            "max_update_latency_cycles": self.max_update_latency_cycles,
            "hardware_measurement_claimed": self.hardware_measurement_claimed,
        }

as_dict()

Return a deterministic JSON-ready estimate payload.

Source code in src/sc_neurocore/hdl_gen/online_learning_emitter.py
Python
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def as_dict(self) -> dict[str, Any]:
    """Return a deterministic JSON-ready estimate payload."""

    return {
        "schema_version": ONLINE_O1_RESOURCE_ESTIMATE_SCHEMA_VERSION,
        "target": self.target,
        "evidence_class": self.evidence_class,
        "n_synapses": self.n_synapses,
        "per_synapse_state_bits": self.per_synapse_state_bits,
        "total_state_bits": self.total_state_bits,
        "bram18_tiles": self.bram18_tiles,
        "bram36_tiles": self.bram36_tiles,
        "lane_ff_bits": self.lane_ff_bits,
        "estimated_luts": self.estimated_luts,
        "estimated_dsps": self.estimated_dsps,
        "max_update_latency_cycles": self.max_update_latency_cycles,
        "hardware_measurement_claimed": self.hardware_measurement_claimed,
    }

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
370
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())
        else:
            # _source_kind() only ever returns lfsr16/sobol16/halton16 (None is
            # filtered above; an unknown candidate raises inside it), so the
            # remaining case is always halton16.
            emitted.append(Halton16Emitter(module_name=module_name).generate())
    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

generate_live_parameter_bank(spec, *, module_name='sc_live_parameter_bank', addr_width=None, bus_data_width=32, block_ram_threshold_bits=1024)

Generate a live-parameter bank from an MMIO update spec.

The emitted RTL stores each parameter bank in distributed RAM or BRAM and exposes the fixed live-control register map through either AXI4-Lite or a PCIe MMIO register-window adapter. The PCIe path intentionally models the endpoint-adapter contract only: upstream PCIe hard IP must decode posted writes and reads into the single-clock MMIO strobes exposed here.

Both protocol paths stage low/high data words, commit updates only after a checksum-valid update/apply handshake, and export a flattened parameter_words bus for downstream dense, BFP, or phase-coupling RTL.

Source code in src/sc_neurocore/hdl_gen/bus_interface.py
Python
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
338
339
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
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
def generate_live_parameter_bank(
    spec: MMIOUpdateSpec,
    *,
    module_name: str = "sc_live_parameter_bank",
    addr_width: int | None = None,
    bus_data_width: int = 32,
    block_ram_threshold_bits: int = 1024,
) -> str:
    """Generate a live-parameter bank from an MMIO update spec.

    The emitted RTL stores each parameter bank in distributed RAM or BRAM and
    exposes the fixed live-control register map through either AXI4-Lite or a
    PCIe MMIO register-window adapter.  The PCIe path intentionally models the
    endpoint-adapter contract only: upstream PCIe hard IP must decode posted
    writes and reads into the single-clock MMIO strobes exposed here.

    Both protocol paths stage low/high data words, commit updates only after a
    checksum-valid update/apply handshake, and export a flattened
    ``parameter_words`` bus for downstream dense, BFP, or phase-coupling RTL.
    """
    if spec.bus_protocol == "pcie":
        return _generate_pcie_live_parameter_bank(
            spec,
            module_name=module_name,
            addr_width=addr_width,
            bus_data_width=bus_data_width,
            block_ram_threshold_bits=block_ram_threshold_bits,
        )
    # ``MMIOUpdateSpec`` normalises ``bus_protocol`` into ``_VALID_PROTOCOLS`` =
    # {"axi4_lite", "pcie"} at construction, and the PCIe case has already
    # returned above, so the only protocol reaching this point is axi4_lite — no
    # further protocol guard is reachable here.
    if bus_data_width != 32:
        raise ValueError("live parameter-bank RTL currently requires a 32-bit AXI data bus")
    if spec.supports_partial_write:
        raise ValueError("live parameter-bank RTL requires full-word writes")
    if spec.trap.max_flags > bus_data_width:
        raise ValueError("trap flag count must fit in one AXI read word for RTL emission")
    module = _validate_sv_identifier(module_name, "module_name")
    adw = addr_width or spec.address_width_bits
    if adw < 8 or adw > 64:
        raise ValueError("addr_width must be between 8 and 64")
    if block_ram_threshold_bits <= 0:
        raise ValueError("block_ram_threshold_bits must be positive")

    bank_names = [_validate_sv_identifier(bank.bank_name, "bank_name") for bank in spec.banks]
    total_parameter_bits = sum(bank.entry_width_bits * bank.parameter_count for bank in spec.banks)
    trap_width = spec.effective_trap_width
    ctrl = spec.control_register_addresses
    status_trap_expr = (
        f"{{{{(DATA_WIDTH-4){{1'b0}}}}, 4'h{STATUS_TRAP_LATCHED:X}}}"
        if STATUS_TRAP_LATCHED < 16
        else f"{bus_data_width}'h{STATUS_TRAP_LATCHED:X}"
    )

    lines = [
        f"// Auto-generated live parameter bank for {module}",
        "// SC-NeuroCore bus interface generator",
        "// Updates are staged and committed through AXI4-Lite control registers.",
        "",
        f"module {module} #(",
        f"    parameter integer ADDR_WIDTH = {adw},",
        f"    parameter integer DATA_WIDTH = {bus_data_width},",
        f"    parameter integer PARAMETER_WORDS_WIDTH = {total_parameter_bits},",
        f"    parameter integer TRAP_WIDTH = {trap_width}",
        ") (",
        "    input  wire                         S_AXI_ACLK,",
        "    input  wire                         S_AXI_ARESETN,",
        "    input  wire [ADDR_WIDTH-1:0]        S_AXI_AWADDR,",
        "    input  wire                         S_AXI_AWVALID,",
        "    output reg                          S_AXI_AWREADY,",
        "    input  wire [DATA_WIDTH-1:0]        S_AXI_WDATA,",
        "    input  wire [DATA_WIDTH/8-1:0]      S_AXI_WSTRB,",
        "    input  wire                         S_AXI_WVALID,",
        "    output reg                          S_AXI_WREADY,",
        "    output reg  [1:0]                   S_AXI_BRESP,",
        "    output reg                          S_AXI_BVALID,",
        "    input  wire                         S_AXI_BREADY,",
        "    input  wire [ADDR_WIDTH-1:0]        S_AXI_ARADDR,",
        "    input  wire                         S_AXI_ARVALID,",
        "    output reg                          S_AXI_ARREADY,",
        "    output reg  [DATA_WIDTH-1:0]        S_AXI_RDATA,",
        "    output reg  [1:0]                   S_AXI_RRESP,",
        "    output reg                          S_AXI_RVALID,",
        "    input  wire                         S_AXI_RREADY,",
        "    input  wire [TRAP_WIDTH-1:0]        trap_vector,",
        "    output wire                         trap_latched,",
        "    output wire [TRAP_WIDTH-1:0]        trap_status_vector,",
        "    output wire                         staged_overflow,",
        "    output wire                         staged_underflow,",
        "    output reg                          update_pulse,",
        "    output reg                          apply_pulse,",
        "    output reg                          rollback_pulse,",
        "    output reg                          trap_clear_pulse,",
        "    output reg                          checksum_mismatch_pulse,",
        "    output reg                          invalid_selection_pulse,",
        "    output reg                          read_only_bank_pulse,",
        "    output reg                          partial_write_pulse,",
        "    output wire                         shadow_loaded,",
        "    output wire [PARAMETER_WORDS_WIDTH-1:0] parameter_words",
        ");",
        "",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_CONTROL    = {adw}'h{ctrl['control']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_STATUS     = {adw}'h{ctrl['status']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_BANK_SEL   = {adw}'h{ctrl['bank_select']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_ENTRY_IDX  = {adw}'h{ctrl['entry_index']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_DATA_LO    = {adw}'h{ctrl['write_data_lo']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_DATA_HI    = {adw}'h{ctrl['write_data_hi']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_TRAP_STAT  = {adw}'h{ctrl['trap_status']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_TRAP_CLEAR = {adw}'h{ctrl['trap_clear']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_CHECKSUM   = {adw}'h{ctrl['write_checksum']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_READ_LO    = {adw}'h{ctrl['read_data_lo']:X};",
        f"    localparam [ADDR_WIDTH-1:0] ADDR_READ_HI    = {adw}'h{ctrl['read_data_hi']:X};",
        f"    localparam [DATA_WIDTH-1:0] CTRL_UPDATE_VALID = {bus_data_width}'h{CONTROL_UPDATE_VALID:X};",
        f"    localparam [DATA_WIDTH-1:0] CTRL_COMMIT       = {bus_data_width}'h{CONTROL_COMMIT:X};",
        f"    localparam [DATA_WIDTH-1:0] CTRL_ROLLBACK     = {bus_data_width}'h{CONTROL_ROLLBACK:X};",
        f"    localparam [DATA_WIDTH-1:0] STATUS_READY      = {bus_data_width}'h{STATUS_READY:X};",
        f"    localparam [DATA_WIDTH-1:0] STATUS_TRAP_LATCHED = {bus_data_width}'h{STATUS_TRAP_LATCHED:X};",
        f"    localparam [DATA_WIDTH-1:0] STATUS_UPDATE_ACK = {bus_data_width}'h{STATUS_UPDATE_ACK:X};",
        f"    localparam [DATA_WIDTH-1:0] STATUS_SHADOW_LOADED = {bus_data_width}'h{STATUS_SHADOW_LOADED:X};",
        f"    localparam [DATA_WIDTH-1:0] STATUS_APPLIED    = {bus_data_width}'h{STATUS_APPLIED:X};",
        f"    localparam [DATA_WIDTH-1:0] STATUS_ROLLBACK_ACK = {bus_data_width}'h{STATUS_ROLLBACK_ACK:X};",
        f"    localparam [DATA_WIDTH-1:0] STATUS_CHECKSUM_VALID = {bus_data_width}'h{STATUS_CHECKSUM_VALID:X};",
        f"    localparam [TRAP_WIDTH-1:0] TRAP_STAGED_OVERFLOW_VECTOR = {trap_width}'h{TRAP_STAGED_OVERFLOW:X};",
        f"    localparam [TRAP_WIDTH-1:0] TRAP_STAGED_UNDERFLOW_VECTOR = {trap_width}'h{TRAP_STAGED_UNDERFLOW:X};",
        f"    localparam [TRAP_WIDTH-1:0] TRAP_CHECKSUM_MISMATCH_VECTOR = {trap_width}'h{TRAP_CHECKSUM_MISMATCH:X};",
        f"    localparam [TRAP_WIDTH-1:0] TRAP_INVALID_SELECTION_VECTOR = {trap_width}'h{TRAP_INVALID_SELECTION:X};",
        f"    localparam [TRAP_WIDTH-1:0] TRAP_READ_ONLY_BANK_VECTOR = {trap_width}'h{TRAP_READ_ONLY_BANK:X};",
        f"    localparam [TRAP_WIDTH-1:0] TRAP_PARTIAL_WRITE_VECTOR = {trap_width}'h{TRAP_PARTIAL_WRITE:X};",
        "    localparam [DATA_WIDTH/8-1:0] FULL_WRITE_STROBE = {(DATA_WIDTH/8){1'b1}};",
        "    localparam [31:0] UPDATE_CRC32_POLY_REFLECTED = 32'hEDB88320;",
        "",
        "    reg [DATA_WIDTH-1:0] reg_control;",
        "    reg [DATA_WIDTH-1:0] reg_status;",
        "    reg [DATA_WIDTH-1:0] reg_bank_select;",
        "    reg [DATA_WIDTH-1:0] reg_entry_index;",
        "    reg [DATA_WIDTH-1:0] reg_write_data_lo;",
        "    reg [DATA_WIDTH-1:0] reg_write_data_hi;",
        "    reg [DATA_WIDTH-1:0] reg_write_checksum;",
        "    reg reg_shadow_loaded;",
        "    reg [DATA_WIDTH-1:0] reg_shadow_bank_select;",
        "    reg [DATA_WIDTH-1:0] reg_shadow_entry_index;",
        "    reg [TRAP_WIDTH-1:0] reg_trap_vector;",
        "    reg [DATA_WIDTH-1:0] active_read_data_lo;",
        "    reg [DATA_WIDTH-1:0] active_read_data_hi;",
        "    wire [63:0] staged_word = {reg_write_data_hi, reg_write_data_lo};",
        "    wire write_strobe_accepted = (S_AXI_WSTRB == FULL_WRITE_STROBE);",
        "",
        "    function automatic [31:0] crc32_update_word;",
        "        input [31:0] crc_in;",
        "        input [31:0] data_word;",
        "        integer bit_idx;",
        "        reg [31:0] next_crc;",
        "        begin",
        "            next_crc = crc_in;",
        "            for (bit_idx = 0; bit_idx < 32; bit_idx = bit_idx + 1) begin",
        "                if (next_crc[0] ^ data_word[bit_idx]) begin",
        "                    next_crc = {1'b0, next_crc[31:1]} ^ UPDATE_CRC32_POLY_REFLECTED;",
        "                end else begin",
        "                    next_crc = {1'b0, next_crc[31:1]};",
        "                end",
        "            end",
        "            crc32_update_word = next_crc;",
        "        end",
        "    endfunction",
        "",
        "    function automatic [31:0] live_update_crc32;",
        "        input [31:0] bank_select;",
        "        input [31:0] entry_index;",
        "        input [31:0] data_lo;",
        "        input [31:0] data_hi;",
        "        reg [31:0] crc_state;",
        "        begin",
        "            crc_state = 32'hFFFFFFFF;",
        "            crc_state = crc32_update_word(crc_state, bank_select);",
        "            crc_state = crc32_update_word(crc_state, entry_index);",
        "            crc_state = crc32_update_word(crc_state, data_lo);",
        "            crc_state = crc32_update_word(crc_state, data_hi);",
        "            live_update_crc32 = crc_state ^ 32'hFFFFFFFF;",
        "        end",
        "    endfunction",
        "",
        "    wire [DATA_WIDTH-1:0] observed_checksum = live_update_crc32(reg_bank_select, reg_entry_index, reg_write_data_lo, reg_write_data_hi);",
        "    wire checksum_valid = (reg_write_checksum == observed_checksum);",
        f"    wire [DATA_WIDTH-1:0] trap_status_bit = trap_latched ? {status_trap_expr} : {{DATA_WIDTH{{1'b0}}}};",
        "",
    ]

    flat_offset = 0
    overflow_terms: list[str] = []
    underflow_terms: list[str] = []
    writable_terms: list[str] = []
    for bank_index, (bank, bank_name) in enumerate(zip(spec.banks, bank_names)):
        style = _ram_style_for_bank(
            bank.parameter_count * bank.entry_width_bits, block_ram_threshold_bits
        )
        width = bank.entry_width_bits
        count = bank.parameter_count
        reset_word = bank.normalise_encoded_word(bank.reset_value)
        overflow_name = f"{bank_name}_staged_overflow"
        underflow_name = f"{bank_name}_staged_underflow"
        overflow_terms.append(overflow_name)
        underflow_terms.append(underflow_name)
        writable_terms.append(f"{bank_name}_writable_for_update")
        lines.extend(
            [
                f'    (* ram_style = "{style}" *) reg [{width - 1}:0] {bank_name} [0:{count - 1}];',
                f'    (* ram_style = "{style}" *) reg [{width - 1}:0] shadow_{bank_name} [0:{count - 1}];',
                f"    localparam [{width - 1}:0] RESET_{bank_name.upper()} = {width}'h{reset_word:X};",
                f"    wire {bank_name}_selected_for_update = (reg_bank_select == 32'd{bank_index}) && (reg_entry_index < 32'd{count});",
                f"    wire {bank_name}_writable_for_update = {bank_name}_selected_for_update && 1'b{int(bank.writable)};",
            ]
        )
        if width < 64:
            extension_width = 64 - width
            lines.extend(
                [
                    f"    wire {bank_name}_zero_extension_valid = (staged_word[63:{width}] == {extension_width}'d0);",
                    "    wire %s_sign_extension_valid = (staged_word[63:%d] == {%d{staged_word[%d]}});"
                    % (bank_name, width, extension_width, width - 1),
                    f"    wire {bank_name}_staged_range_valid = {bank_name}_zero_extension_valid || {bank_name}_sign_extension_valid;",
                    f"    wire {overflow_name} = {bank_name}_selected_for_update && !{bank_name}_staged_range_valid && !staged_word[63];",
                    f"    wire {underflow_name} = {bank_name}_selected_for_update && !{bank_name}_staged_range_valid && staged_word[63];",
                ]
            )
        else:
            lines.extend(
                [
                    f"    wire {overflow_name} = 1'b0;",
                    f"    wire {underflow_name} = 1'b0;",
                ]
            )
        for index in range(count):
            lines.append(
                f"    assign parameter_words[{flat_offset} +: {width}] = {bank_name}[{index}];"
            )
            flat_offset += width

    lines.extend(
        [
            f"    wire staged_overflow_fault = {' | '.join(overflow_terms)};",
            f"    wire staged_underflow_fault = {' | '.join(underflow_terms)};",
            "    wire staged_update_fault = staged_overflow_fault | staged_underflow_fault;",
            f"    wire bank_entry_selection_valid = {' | '.join(f'{name}_selected_for_update' for name in bank_names)};",
            f"    wire bank_update_writable = {' | '.join(writable_terms)};",
            "    wire [DATA_WIDTH-1:0] rollback_bank_select = reg_shadow_loaded ? reg_shadow_bank_select : reg_bank_select;",
            "    wire [DATA_WIDTH-1:0] rollback_entry_index = reg_shadow_loaded ? reg_shadow_entry_index : reg_entry_index;",
            "    wire [TRAP_WIDTH-1:0] generated_trap_vector =",
            "        (staged_overflow_fault ? TRAP_STAGED_OVERFLOW_VECTOR : {TRAP_WIDTH{1'b0}}) |",
            "        (staged_underflow_fault ? TRAP_STAGED_UNDERFLOW_VECTOR : {TRAP_WIDTH{1'b0}});",
            "    wire [TRAP_WIDTH-1:0] observed_trap_vector = trap_vector | generated_trap_vector;",
            "",
            "    assign trap_latched = |reg_trap_vector;",
            "    assign trap_status_vector = reg_trap_vector;",
            "    assign staged_overflow = staged_overflow_fault;",
            "    assign staged_underflow = staged_underflow_fault;",
            "    assign shadow_loaded = reg_shadow_loaded;",
        ]
    )

    lines.extend(
        [
            "",
            "    always @* begin",
            "        active_read_data_lo = {DATA_WIDTH{1'b0}};",
            "        active_read_data_hi = {DATA_WIDTH{1'b0}};",
            "        case (reg_bank_select)",
        ]
    )
    for bank_index, (bank, bank_name) in enumerate(zip(spec.banks, bank_names)):
        width = bank.entry_width_bits
        count = bank.parameter_count
        lines.extend(
            [
                f"            32'd{bank_index}: begin",
                f"                if (reg_entry_index < 32'd{count}) begin",
            ]
        )
        if width <= bus_data_width:
            pad_width = bus_data_width - width
            if pad_width:
                lines.append(
                    f"                    active_read_data_lo = {{{{{pad_width}{{1'b0}}}}, {bank_name}[reg_entry_index]}};"
                )
            else:
                lines.append(
                    f"                    active_read_data_lo = {bank_name}[reg_entry_index];"
                )
            lines.append("                    active_read_data_hi = {DATA_WIDTH{1'b0}};")
        else:
            high_width = width - bus_data_width
            high_pad_width = bus_data_width - high_width
            lines.append(
                f"                    active_read_data_lo = {bank_name}[reg_entry_index][DATA_WIDTH-1:0];"
            )
            if high_pad_width:
                lines.append(
                    "                    active_read_data_hi = "
                    f"{{{{{high_pad_width}{{1'b0}}}}, {bank_name}[reg_entry_index][{width - 1}:DATA_WIDTH]}};"
                )
            else:
                lines.append(
                    f"                    active_read_data_hi = {bank_name}[reg_entry_index][{width - 1}:DATA_WIDTH];"
                )
        lines.extend(
            [
                "                end",
                "            end",
            ]
        )
    lines.extend(
        [
            "            default: begin end",
            "        endcase",
            "    end",
        ]
    )

    lines.extend(
        [
            "",
            "    integer init_idx;",
            "",
            "    always @(posedge S_AXI_ACLK or negedge S_AXI_ARESETN) begin",
            "        if (!S_AXI_ARESETN) begin",
            "            S_AXI_AWREADY <= 1'b0;",
            "            S_AXI_WREADY <= 1'b0;",
            "            S_AXI_BRESP <= 2'b00;",
            "            S_AXI_BVALID <= 1'b0;",
            "            S_AXI_ARREADY <= 1'b0;",
            "            S_AXI_RDATA <= {DATA_WIDTH{1'b0}};",
            "            S_AXI_RRESP <= 2'b00;",
            "            S_AXI_RVALID <= 1'b0;",
            "            reg_control <= {DATA_WIDTH{1'b0}};",
            "            reg_status <= STATUS_READY;",
            "            reg_bank_select <= {DATA_WIDTH{1'b0}};",
            "            reg_entry_index <= {DATA_WIDTH{1'b0}};",
            "            reg_write_data_lo <= {DATA_WIDTH{1'b0}};",
            "            reg_write_data_hi <= {DATA_WIDTH{1'b0}};",
            "            reg_write_checksum <= {DATA_WIDTH{1'b0}};",
            "            reg_shadow_loaded <= 1'b0;",
            "            reg_shadow_bank_select <= {DATA_WIDTH{1'b0}};",
            "            reg_shadow_entry_index <= {DATA_WIDTH{1'b0}};",
            "            reg_trap_vector <= {TRAP_WIDTH{1'b0}};",
            "            update_pulse <= 1'b0;",
            "            apply_pulse <= 1'b0;",
            "            rollback_pulse <= 1'b0;",
            "            trap_clear_pulse <= 1'b0;",
            "            checksum_mismatch_pulse <= 1'b0;",
            "            invalid_selection_pulse <= 1'b0;",
            "            read_only_bank_pulse <= 1'b0;",
            "            partial_write_pulse <= 1'b0;",
        ]
    )

    for bank, bank_name in zip(spec.banks, bank_names):
        lines.extend(
            [
                f"            for (init_idx = 0; init_idx < {bank.parameter_count}; init_idx = init_idx + 1) begin",
                f"                {bank_name}[init_idx] <= RESET_{bank_name.upper()};",
                f"                shadow_{bank_name}[init_idx] <= RESET_{bank_name.upper()};",
                "            end",
            ]
        )

    lines.extend(
        [
            "        end else begin",
            "            S_AXI_AWREADY <= 1'b0;",
            "            S_AXI_WREADY <= 1'b0;",
            "            S_AXI_ARREADY <= 1'b0;",
            "            update_pulse <= 1'b0;",
            "            apply_pulse <= 1'b0;",
            "            rollback_pulse <= 1'b0;",
            "            trap_clear_pulse <= 1'b0;",
            "            checksum_mismatch_pulse <= 1'b0;",
            "            invalid_selection_pulse <= 1'b0;",
            "            read_only_bank_pulse <= 1'b0;",
            "            partial_write_pulse <= 1'b0;",
            "            reg_trap_vector <= reg_trap_vector | observed_trap_vector;",
            "            reg_status <= STATUS_READY | trap_status_bit | (reg_shadow_loaded ? STATUS_SHADOW_LOADED : {DATA_WIDTH{1'b0}}) | (checksum_valid ? STATUS_CHECKSUM_VALID : {DATA_WIDTH{1'b0}});",
            "",
            "            if (S_AXI_BREADY && S_AXI_BVALID) begin",
            "                S_AXI_BVALID <= 1'b0;",
            "            end",
            "            if (S_AXI_RREADY && S_AXI_RVALID) begin",
            "                S_AXI_RVALID <= 1'b0;",
            "            end",
            "",
            "            if (S_AXI_AWVALID && S_AXI_WVALID && !S_AXI_BVALID) begin",
            "                S_AXI_AWREADY <= 1'b1;",
            "                S_AXI_WREADY <= 1'b1;",
            "                S_AXI_BVALID <= 1'b1;",
            "                S_AXI_BRESP <= 2'b00;",
            "                if (!write_strobe_accepted) begin",
            "                    S_AXI_BRESP <= 2'b10;",
            "                    partial_write_pulse <= 1'b1;",
            "                    reg_trap_vector <= reg_trap_vector | observed_trap_vector | TRAP_PARTIAL_WRITE_VECTOR;",
            "                    reg_status <= STATUS_READY | STATUS_TRAP_LATCHED;",
            "                end else begin",
            "                case (S_AXI_AWADDR)",
            "                    ADDR_CONTROL: begin",
            "                        reg_control <= S_AXI_WDATA;",
            "                        if ((S_AXI_WDATA & CTRL_UPDATE_VALID) != {DATA_WIDTH{1'b0}}) begin",
            "                            if (checksum_valid && !staged_update_fault && bank_entry_selection_valid && bank_update_writable) begin",
            "                            update_pulse <= 1'b1;",
            "                            reg_shadow_loaded <= 1'b1;",
            "                            reg_shadow_bank_select <= reg_bank_select;",
            "                            reg_shadow_entry_index <= reg_entry_index;",
            "                            reg_status <= STATUS_READY | STATUS_SHADOW_LOADED | STATUS_CHECKSUM_VALID | trap_status_bit;",
            "                            case (reg_bank_select)",
        ]
    )

    for bank_index, (bank, bank_name) in enumerate(zip(spec.banks, bank_names)):
        lines.extend(
            [
                f"                                32'd{bank_index}: begin",
                f"                                    if (reg_entry_index < 32'd{bank.parameter_count}) begin",
                f"                                        shadow_{bank_name}[reg_entry_index] <= staged_word[{bank.entry_width_bits - 1}:0];",
                "                                    end",
                "                                end",
            ]
        )

    lines.extend(
        [
            "                                default: begin",
            "                                    reg_status <= STATUS_READY | trap_status_bit;",
            "                                end",
            "                            endcase",
            "                            end",
            "                            else if (!checksum_valid) begin",
            "                                checksum_mismatch_pulse <= 1'b1;",
            "                                reg_trap_vector <= reg_trap_vector | observed_trap_vector | TRAP_CHECKSUM_MISMATCH_VECTOR;",
            "                                reg_status <= STATUS_READY | STATUS_TRAP_LATCHED;",
            "                            end",
            "                            else if (!bank_entry_selection_valid) begin",
            "                                invalid_selection_pulse <= 1'b1;",
            "                                reg_trap_vector <= reg_trap_vector | observed_trap_vector | TRAP_INVALID_SELECTION_VECTOR;",
            "                                reg_status <= STATUS_READY | STATUS_TRAP_LATCHED;",
            "                            end",
            "                            else if (!bank_update_writable) begin",
            "                                read_only_bank_pulse <= 1'b1;",
            "                                reg_trap_vector <= reg_trap_vector | observed_trap_vector | TRAP_READ_ONLY_BANK_VECTOR;",
            "                                reg_status <= STATUS_READY | STATUS_TRAP_LATCHED;",
            "                            end",
            "                        end else if ((S_AXI_WDATA & CTRL_COMMIT) != {DATA_WIDTH{1'b0}}) begin",
            "                            if (reg_shadow_loaded) begin",
            "                                apply_pulse <= 1'b1;",
            "                                reg_shadow_loaded <= 1'b0;",
            "                                reg_status <= STATUS_READY | STATUS_UPDATE_ACK | STATUS_APPLIED | trap_status_bit;",
            "                                case (reg_shadow_bank_select)",
        ]
    )

    for bank_index, (bank, bank_name) in enumerate(zip(spec.banks, bank_names)):
        lines.extend(
            [
                f"                                    32'd{bank_index}: begin",
                f"                                        if (reg_shadow_entry_index < 32'd{bank.parameter_count}) begin",
                f"                                            {bank_name}[reg_shadow_entry_index] <= shadow_{bank_name}[reg_shadow_entry_index];",
                "                                        end",
                "                                    end",
            ]
        )

    lines.extend(
        [
            "                                    default: begin",
            "                                        reg_status <= STATUS_READY | trap_status_bit;",
            "                                    end",
            "                                endcase",
            "                            end",
            "                        end else if ((S_AXI_WDATA & CTRL_ROLLBACK) != {DATA_WIDTH{1'b0}}) begin",
            "                            rollback_pulse <= 1'b1;",
            "                            reg_shadow_loaded <= 1'b0;",
            "                            reg_status <= STATUS_READY | STATUS_ROLLBACK_ACK | trap_status_bit;",
            "                            case (rollback_bank_select)",
        ]
    )

    for bank_index, (bank, bank_name) in enumerate(zip(spec.banks, bank_names)):
        lines.extend(
            [
                f"                                32'd{bank_index}: begin",
                f"                                    if (rollback_entry_index < 32'd{bank.parameter_count}) begin",
                f"                                        shadow_{bank_name}[rollback_entry_index] <= {bank_name}[rollback_entry_index];",
                "                                    end",
                "                                end",
            ]
        )

    lines.extend(
        [
            "                                default: begin",
            "                                    reg_status <= STATUS_READY | trap_status_bit;",
            "                                end",
            "                            endcase",
            "                        end",
            "                        if (S_AXI_WDATA[2]) begin",
            "                            trap_clear_pulse <= 1'b1;",
            "                        end",
            "                    end",
            "                    ADDR_BANK_SEL: reg_bank_select <= S_AXI_WDATA;",
            "                    ADDR_ENTRY_IDX: reg_entry_index <= S_AXI_WDATA;",
            "                    ADDR_DATA_LO: reg_write_data_lo <= S_AXI_WDATA;",
            "                    ADDR_DATA_HI: reg_write_data_hi <= S_AXI_WDATA;",
            "                    ADDR_CHECKSUM: reg_write_checksum <= S_AXI_WDATA;",
            "                    ADDR_TRAP_CLEAR: begin",
            "                        trap_clear_pulse <= 1'b1;",
            "                        reg_trap_vector <= (reg_trap_vector | observed_trap_vector) & ~S_AXI_WDATA[TRAP_WIDTH-1:0];",
            "                    end",
            "                    default: begin end",
            "                endcase",
            "                end",
            "            end",
            "",
            "            if (S_AXI_ARVALID && !S_AXI_RVALID) begin",
            "                S_AXI_ARREADY <= 1'b1;",
            "                S_AXI_RVALID <= 1'b1;",
            "                S_AXI_RRESP <= 2'b00;",
            "                case (S_AXI_ARADDR)",
            "                    ADDR_CONTROL: S_AXI_RDATA <= reg_control;",
            "                    ADDR_STATUS: S_AXI_RDATA <= reg_status | trap_status_bit;",
            "                    ADDR_BANK_SEL: S_AXI_RDATA <= reg_bank_select;",
            "                    ADDR_ENTRY_IDX: S_AXI_RDATA <= reg_entry_index;",
            "                    ADDR_DATA_LO: S_AXI_RDATA <= reg_write_data_lo;",
            "                    ADDR_DATA_HI: S_AXI_RDATA <= reg_write_data_hi;",
            "                    ADDR_CHECKSUM: S_AXI_RDATA <= observed_checksum;",
            "                    ADDR_TRAP_STAT: S_AXI_RDATA <= {{(DATA_WIDTH-TRAP_WIDTH){1'b0}}, reg_trap_vector};",
            "                    ADDR_READ_LO: begin",
            "                        S_AXI_RDATA <= active_read_data_lo;",
            "                        if (!bank_entry_selection_valid) begin",
            "                            S_AXI_RRESP <= 2'b10;",
            "                            invalid_selection_pulse <= 1'b1;",
            "                            reg_trap_vector <= reg_trap_vector | observed_trap_vector | TRAP_INVALID_SELECTION_VECTOR;",
            "                            reg_status <= STATUS_READY | STATUS_TRAP_LATCHED;",
            "                        end",
            "                    end",
            "                    ADDR_READ_HI: begin",
            "                        S_AXI_RDATA <= active_read_data_hi;",
            "                        if (!bank_entry_selection_valid) begin",
            "                            S_AXI_RRESP <= 2'b10;",
            "                            invalid_selection_pulse <= 1'b1;",
            "                            reg_trap_vector <= reg_trap_vector | observed_trap_vector | TRAP_INVALID_SELECTION_VECTOR;",
            "                            reg_status <= STATUS_READY | STATUS_TRAP_LATCHED;",
            "                        end",
            "                    end",
            "                    default: S_AXI_RDATA <= {DATA_WIDTH{1'b0}};",
            "                endcase",
            "            end",
            "        end",
            "    end",
            "",
            "endmodule",
            "",
        ]
    )
    return "\n".join(lines)