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
- Sets MinGW environment (PATH, CC, CXX) to avoid Git MSYS conflicts
- Builds C++ extension via scikit-build-core + CMake + pybind11
- Installs in editable mode (Python changes = instant, C++ changes = rebuild)
- Syncs to configured venvs (if SYNC_VENVS is set in build_mingw.py)
- 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.012→L1 p1 _rser_L1 8.5u+R_L1_ser _rser_L1 0 0.012.backannodirectives are stripped (LTspice-specific, benign)Rpar=andCpar=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 -vat 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
- Add C++ wrapper function/class in
src/cpp/ - Register with pybind11 in
src/bindings/module.cpp - Export from
pyngspice/__init__.pyif 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.