Python ↔ Verilog Co-Simulation Guide¶
This guide documents the SC-NeuroCore co-simulation framework for validating that the Verilog RTL generated by the equation compiler produces bit-true equivalent spike behaviour to the Python reference implementation.
Overview¶
The co-simulation framework:
- Compiles a model schema to Verilog via the equation compiler
- Generates a testbench that drives constant current for N clock cycles
- Compiles and runs the simulation via Icarus Verilog (
iverilog+vvp) - Parses the spike count from simulation output
- Compares against the Python
UniversalNeuron.step()reference
flowchart LR
A["Schema<br/>(TOML)"] --> B["UniversalNeuron<br/>.from_schema()"]
B --> C["Python<br/>simulation"]
B --> D["to_verilog()"]
D --> E["generate_testbench()"]
E --> F["iverilog<br/>+ vvp"]
C --> G{"Compare<br/>spike counts"}
F --> G
G -->|match| H["✓ Verified"]
G -->|mismatch| I["✗ Investigate"]
style H fill:#e8f5e9
style I fill:#ffcdd2
Prerequisites¶
- Python 3.10+ with the
sc_neurocorepackage installed - Icarus Verilog (
iverilog,vvp) — install viaapt install iverilog - Tests skip gracefully if iverilog is not available
Running Co-Simulation Tests¶
# Run all co-simulation tests
python -m pytest tests/test_cosimulation.py -v -s
# Run just the accuracy tests
python -m pytest tests/test_cosimulation.py -k "accuracy" -v -s
# Run Q4.12 precision tests
python -m pytest tests/test_cosimulation.py -k "Q412" -v -s
# Run Q16.16 precision tests
python -m pytest tests/test_cosimulation.py -k "Q1616" -v -s
Test Structure¶
The co-simulation suite is organized into four test classes:
TestCoSimulation — Q8.8 Baseline¶
| Test | Description | Assertion |
|---|---|---|
test_both_produce_spikes |
Both Python and Verilog spike (5 models) | spikes > 0 |
test_spike_count_accuracy |
Spike counts match within 1% (5 models) | gap < 1% |
test_no_current_no_spikes |
Zero current → zero spikes (4 models) | spikes == 0 |
test_python_sim_is_deterministic |
Python gives same result twice | a == b |
test_verilog_sim_is_deterministic |
Verilog gives same result twice | a == b |
TestQ412Precision — Q4.12 (16-bit, 12 fractional)¶
| Test | Description |
|---|---|
test_lif_q412_spikes |
Q4.12 LIF produces spikes |
test_lif_q412_near_python |
Q4.12 within 5% of Python |
test_q412_vs_q88_comparison |
Both Q4.12 and Q8.8 within 5% |
test_q412_zero_current_silence |
Zero current (xfail: range overflow) |
TestQ1616Precision — Q16.16 (32-bit, 16 fractional)¶
| Test | Description |
|---|---|
test_lif_q1616_spikes |
Q16.16 LIF produces spikes |
test_lif_q1616_near_python |
Q16.16 within 1% of Python |
test_q1616_zero_current_silence |
Zero current → silence ✓ |
Verified Models¶
Five models are verified across all precision modes:
| Model | State Variables | Complexity | Spikes (I=50, 200 steps) |
|---|---|---|---|
| LIF | v | Linear | 200 |
| Lapicque | v | Linear | 200 |
| Quadratic IF | v | Quadratic | 50 |
| Izhikevich | v, u | Quadratic (2-var) | 25 |
| Resonate-and-Fire | v, w | Linear (2-var) | 200 |
Adding a New Model to Co-Simulation¶
- Ensure the model schema exists in
src/sc_neurocore/neurons/model_schemas/ - Add the model name to
_COSIM_MODELSintests/test_cosimulation.py - Run the test — if it fails:
- Check if the model uses transcendental functions (not yet supported in co-sim)
- Check if parameters overflow the chosen precision mode
- Use
python -m sc_neurocore.neurons precision <model>for diagnostics
# Example: adding a new model to co-sim
_COSIM_MODELS = ["lif", "lapicque", "quadratic_if", "izhikevich",
"resonate_fire", "your_new_model"]
Testbench Architecture¶
The generated testbench follows this structure:
module tb_sc_lif;
reg clk;
reg rst_n;
wire spike_out;
wire signed [15:0] v_out;
sc_lif uut (
.clk(clk), .rst_n(rst_n),
.I_t(16'sd12800), // Q8.8 encoded input current
.spike_out(spike_out),
.v_out(v_out)
);
// 100 MHz clock
initial clk = 0;
always #5 clk = ~clk;
integer spike_count;
initial begin
$dumpfile("tb_sc_lif.vcd");
$dumpvars(0, tb_sc_lif);
spike_count = 0;
// Reset phase
rst_n = 0;
#20;
rst_n = 1;
@(posedge clk); // 1 settling cycle
// Measurement phase
repeat (200) begin
@(posedge clk);
#1; // combinational settling
if (spike_out)
spike_count = spike_count + 1;
end
$display("Simulation complete: %0d spikes in 200 cycles",
spike_count);
$finish;
end
endmodule
Key timing details:
| Phase | Purpose | Duration |
|---|---|---|
Reset (rst_n = 0) |
Initialise all registers | 20ns (2 clock periods) |
Settling (@(posedge clk)) |
Allow reset to propagate | 1 clock cycle |
#1 after posedge |
Let combinational outputs settle | 1ps (minimal) |
Historical note: The settling cycle and
#1delay were added in 2026-05-01 to eliminate a 0.5% residual gap caused by samplingspike_outbefore combinational logic had propagated after reset de-assertion.
Compiler Correctness: Six Critical Fixes¶
The following fixes were applied to achieve 0.0% co-simulation accuracy:
| # | Fix | Impact |
|---|---|---|
| 1 | Intermediate wire for bit-select | iverilog compilation |
| 2 | Persistent wire counters | Multi-variable models |
| 3 | Portable negative literals | Cross-tool compatibility |
| 4 | Q-format division | 99% → 50% gap |
| 5 | Look-ahead threshold | 50% → 0.5% gap |
| 6 | Testbench timing | 0.5% → 0.0% gap |
Fix #4: Q-Format Division¶
Bug: Division by parameters used bare Verilog integer division (a / b),
which divides by the Q-encoded raw value (e.g., 2560 for tau_m=10 in Q8.8).
Fix: Proper Q-format: (a << fraction) / b, with explicit sign extension:
wire signed [15:0] _dop0 = numerator; // named operand
wire signed [31:0] _dnum0 = $signed({...}) <<< 8; // shift up
wire signed [31:0] _div0 = _dnum0 / $signed({...}); // divide
wire signed [15:0] _dres0 = _div0[15:0]; // extract Q result
Fix #5: Look-Ahead Threshold¶
Bug: Threshold compared v_reg (current cycle) instead of v_next
(computed new voltage), creating a 1-cycle detection delay.
Fix: Threshold expression uses v_next wires:
// Before: if ((v_reg > threshold)) ← 1 cycle late
// After: if ((v_next > threshold)) ← matches Python semantics
Troubleshooting¶
Model produces spikes in Python but not in Verilog¶
- Check dt encoding:
python -m sc_neurocore.neurons precision <model> - If dt underflows (Q-value = 0), the Euler update is zero
-
Solution: increase dt or use wider precision mode
-
Check parameter range: Look for
⚠ Overflowor⚠ Underflowwarnings -
Solution: use Q16.16 for wide-range parameters
-
Check division: Parameters used as divisors should be > 0.004 (Q8.8 minimum representable value)
Verilog produces more spikes than Python¶
- Check for Q-format overflow: Large v² products may wrap in 16-bit
- Solution: use Q16.16 for nonlinear models
- Example: Izhikevich at zero current — 0.04*v² overflows Q8.8
Compilation fails with iverilog¶
- Ensure
-g2012flag is passed (SystemVerilog 2012 features required) - Check for bit-select on complex expressions (should be caught by the compiler)
Further Reading¶
- Precision Modes Guide — Q8.8, Q4.12, Q16.16 details
- Tutorial 33: Equation-to-Verilog
- Tutorial 09: Hardware Co-simulation
- Strategic Assessment Appendix A.4