Skip to content

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:

  1. Compiles a model schema to Verilog via the equation compiler
  2. Generates a testbench that drives constant current for N clock cycles
  3. Compiles and runs the simulation via Icarus Verilog (iverilog + vvp)
  4. Parses the spike count from simulation output
  5. 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_neurocore package installed
  • Icarus Verilog (iverilog, vvp) — install via apt install iverilog
  • Tests skip gracefully if iverilog is not available

Running Co-Simulation Tests

Bash
# 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

  1. Ensure the model schema exists in src/sc_neurocore/neurons/model_schemas/
  2. Add the model name to _COSIM_MODELS in tests/test_cosimulation.py
  3. Run the test — if it fails:
  4. Check if the model uses transcendental functions (not yet supported in co-sim)
  5. Check if parameters overflow the chosen precision mode
  6. Use python -m sc_neurocore.neurons precision <model> for diagnostics
Python
# 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:

Verilog
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 #1 delay were added in 2026-05-01 to eliminate a 0.5% residual gap caused by sampling spike_out before 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:

Verilog
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:

Verilog
// 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

  1. Check dt encoding: python -m sc_neurocore.neurons precision <model>
  2. If dt underflows (Q-value = 0), the Euler update is zero
  3. Solution: increase dt or use wider precision mode

  4. Check parameter range: Look for ⚠ Overflow or ⚠ Underflow warnings

  5. Solution: use Q16.16 for wide-range parameters

  6. Check division: Parameters used as divisors should be > 0.004 (Q8.8 minimum representable value)

Verilog produces more spikes than Python

  1. Check for Q-format overflow: Large v² products may wrap in 16-bit
  2. Solution: use Q16.16 for nonlinear models
  3. Example: Izhikevich at zero current — 0.04*v² overflows Q8.8

Compilation fails with iverilog

  • Ensure -g2012 flag is passed (SystemVerilog 2012 features required)
  • Check for bit-select on complex expressions (should be caught by the compiler)

Further Reading