You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

10 KiB

CLAUDE.md - pyngspice Build Guide

pyngspice provides native Python bindings for the ngspice circuit simulator using pybind11. It serves as the ngspice backend for pyTesla (Tesla coil simulator), providing a SpiceRunner interface that's a drop-in alternative to LTspice.

Building and Installation

Quick Start

python build_mingw.py

What this does

  1. Sets MinGW environment (PATH, CC, CXX) to avoid Git MSYS conflicts
  2. Builds C++ extension via scikit-build-core + CMake + pybind11
  3. Installs in editable mode (Python changes = instant, C++ changes = rebuild)
  4. Syncs to configured venvs (if SYNC_VENVS is set in build_mingw.py)
  5. Verifies the build by importing pyngspice

Requirements

  • MinGW-w64 GCC 15.x at C:\mingw64
  • CMake 3.18+ (at C:\Program Files\CMake\bin)
  • Python 3.9+ with development headers
  • NumPy 1.20+

After C++ changes: python build_mingw.py

After Python changes: nothing (editable install)

Build Variants

python build_mingw.py              # Standard build
python build_mingw.py --clean      # Clean + rebuild from scratch
python build_mingw.py --debug      # Debug configuration
python build_mingw.py --sync       # Sync .pth files to other venvs
python build_mingw.py --verify     # Just check if install works

How It Compiles

Three layers turn C++ into Python:

pyproject.toml          scikit-build-core (PEP 517 build backend)
       ↓
CMakeLists.txt          CMake (finds Python, pybind11, compiles ~2000 ngspice C sources + wrapper C++)
       ↓
src/bindings/module.cpp pybind11 (generates _pyngspice.pyd that Python can import)

pip install -e . triggers the whole chain. The editable.mode = "inplace" setting in pyproject.toml is critical — it makes CMake build the .pyd directly into pyngspice/, next to __init__.py. Without this, scikit-build-core uses fragile import hooks.

Architecture

Directory Structure

pyngspice/                 # Python package (importable, no rebuild needed)
├── __init__.py            # Package entry point, imports _pyngspice C++ extension
├── runner.py              # SpiceRunner interface + NgspiceRunner + SubprocessRunner
├── netlist.py             # Netlist pre-processor (Rser= translation for ngspice)
└── _pyngspice.*.pyd       # Compiled C++ extension (built in-place, rebuild required)

src/cpp/                   # C++ wrapper code (rebuild required after changes)
├── simulator.cpp/h        # Low-level ngspice API wrapper (ngSpice_Init, etc.)
├── callbacks.cpp/h        # Callback routing for ngspice events
├── sim_runner.cpp/h       # PyLTSpice-compatible simulation runner
├── raw_read.cpp/h         # .raw file parser (binary + ASCII, complex data, step detection)
└── trace.cpp/h            # Vector/trace handling (numpy array conversion)

src/bindings/              # pybind11 bindings (rebuild required after changes)
└── module.cpp             # PYBIND11_MODULE(_pyngspice, m) — the C++/Python bridge

Key Files

File Purpose
CMakeLists.txt Main build: compiles ngspice_core static lib + _pyngspice extension
pyproject.toml scikit-build-core config, package metadata
build_mingw.py One-command build script (sets PATH, syncs venvs)
src/bindings/module.cpp pybind11 module definition — all C++/Python type bindings
src/cpp/sim_runner.cpp C++ SimRunner — core simulation engine that NgspiceRunner wraps
src/cpp/raw_read.cpp .raw file parser — handles both binary and ASCII formats
pyngspice/runner.py SpiceRunner interface — what pyTesla consumes
pyngspice/netlist.py Rser= pre-processor — critical for LTspice netlist compatibility
visualc/src/include/ngspice/config.h Build feature flags (XSPICE, OSDI, CIDER, KLU)

pyTesla Integration

SpiceRunner Interface (pyTesla's swap point)

from pyngspice import NgspiceRunner

# Create runner — replaces PyLTSpice's get_configured_sim_runner()
runner = NgspiceRunner(working_directory="./output")

# Run simulation — handles Rser= translation automatically
raw_file, log_file = runner.run("tesla_coil.net")

# Parse results — PyLTSpice's RawRead also works on ngspice .raw files
from pyngspice import RawRead
raw = RawRead(raw_file)
trace = raw.get_trace("V(out)")
data = trace.get_wave(0)  # numpy array

Netlist Pre-processing

pyngspice automatically translates LTspice syntax to ngspice before simulation:

  • L1 p1 0 8.5u Rser=0.012L1 p1 _rser_L1 8.5u + R_L1_ser _rser_L1 0 0.012
  • .backanno directives are stripped (LTspice-specific, benign)
  • Rpar= and Cpar= on inductors are stripped (could expand to parallel elements later)
  • Standard SPICE constructs (behavioral sources, subcircuits, etc.) pass through unchanged

Auto-detection Factory

from pyngspice.runner import get_runner

# "auto" tries embedded C++ first, falls back to subprocess
runner = get_runner("./output", backend="auto")  # or "embedded" or "subprocess"

Build Configuration

Enabled Features

  • XSPICE - Code model support (ON by default)
  • OSDI - Verilog-A support (ON by default)
  • HICUM2 - High-current BJT model with cppduals autodiff library

Disabled Features

  • CIDER - Numerical device models (requires complex KLU setup)
  • KLU - Sparse matrix solver (requires SuiteSparse build)

To re-enable, modify CMakeLists.txt:

option(ENABLE_CIDER "Enable CIDER numerical device models" ON)
option(ENABLE_KLU "Enable KLU sparse matrix solver" ON)

And ensure visualc/src/include/ngspice/config.h matches (e.g., #define CIDER).

Testing

pytest tests/ -v                                    # All tests
pytest tests/test_netlist.py -v                     # Netlist pre-processor only (no build needed)
pytest tests/test_runner.py tests/test_sim.py -v    # Integration tests (need built extension)

Requirements

  • Every new feature or bug fix must include tests. Add test methods to the relevant test class in tests/, or create a new class if the feature is distinct.
  • Always run the full test suite (pytest tests/test_netlist.py -v at minimum) after changes to verify nothing is broken (regression testing).
  • Do not consider a change complete until all tests pass.

Troubleshooting

"DLL load failed" or "ImportError: _pyngspice"

Re-run python build_mingw.py --clean. If that doesn't work, check that MinGW-w64 is at C:\mingw64\bin and that _pyngspice.*.pyd exists in pyngspice/.

"No module named pyngspice"

Run pip install -e . or python build_mingw.py from the repo root.

"gcc not found" during build

Install MinGW-w64 to C:\mingw64. The build script overrides PATH to avoid Git's built-in MSYS tools which conflict with MinGW. Run from cmd.exe, not Git Bash.

"undefined reference to SIMinfo"

ngspice.c was excluded from the build. It contains SIMinfo but has no main(). Ensure it's NOT in the exclusion list in CMakeLists.txt.

"undefined reference to get_nbjt_info, get_numd_info..."

CIDER is enabled in config.h but CIDER sources are not compiled. Ensure config.h has /* #undef CIDER */ when ENABLE_CIDER=OFF.

"undefined reference to HICUMload, HICUMtemp"

HICUM2 init is compiled but .cpp implementation files are excluded. Ensure HICUM2 .cpp files are in the source list and cppduals include path is set.

Module not found after editable install

The .pyd was not built into pyngspice/. Check that LIBRARY_OUTPUT_DIRECTORY is set in CMakeLists.txt. Rebuild or manually copy:

copy build\cp311-cp311-win_amd64\_pyngspice.cp311-win_amd64.pyd pyngspice\

DO NOT MODIFY (hard-won lessons)

ngspice.c must stay in the build

src/ngspice.c is NOT the main entry point — it contains SIMinfo (device info table). src/main.c is the actual entry point and IS excluded. Do not confuse the two.

config.h feature flags must match CMakeLists.txt

visualc/src/include/ngspice/config.h and CMakeLists.txt options must agree. If config.h has #define CIDER but ENABLE_CIDER=OFF, you get linker errors for get_nbjt_info etc. If config.h undefs CIDER but ENABLE_CIDER=ON, CIDER sources compile but the device models aren't registered.

The Rser= regex in netlist.py handles the common case

The inductor Rser= pattern handles: bare numbers, engineering suffixes (u, m, n, p, k), and scientific notation. It does NOT handle parameter references like Rser={Rprimary} (curly brace expressions). If pyTesla generates parameterized Rser values, the pre-processor will need to be extended to also expand .param definitions.

MinGW static linking is intentional

We statically link libgcc, libstdc++, and libwinpthread so the .pyd ships zero DLLs. Without this, users would need libgcc_s_seh-1.dll, libstdc++-6.dll, and libwinpthread-1.dll in their PATH. Do not remove the static link flags.

Raw file format differences

Both LTspice and ngspice produce .raw files, but there are subtle differences:

  • NGspice may use different variable naming conventions (lowercase vs mixed case)
  • Step detection works via scale vector value resets (handled in raw_read.cpp)
  • PyLTSpice's RawRead CAN parse ngspice .raw files (community-verified)

Reference Implementation

  • Authoritative ngspice source: This repo (C:\git\ngspice) tracks upstream ngspice
  • ngspice docs: https://ngspice.sourceforge.io/docs.html
  • Shared library API: src/include/ngspice/sharedspice.h — the C API we wrap
  • pyfemm-xplat (sister project): Follow its architecture patterns for build system, pybind11 bindings, and Python package structure

Development Notes

Adding New Python Bindings

  1. Add C++ wrapper function/class in src/cpp/
  2. Register with pybind11 in src/bindings/module.cpp
  3. Export from pyngspice/__init__.py if part of public API

Adding Netlist Translations

Add new regex patterns or transformations in pyngspice/netlist.py. Add corresponding tests in tests/test_netlist.py.

Syncing to pyTesla's venv

Edit SYNC_VENVS in build_mingw.py to include pyTesla's venv path, then:

python build_mingw.py --sync

This writes a .pth file into that venv's site-packages so import pyngspice works there.