# 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 ```bash 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 ```bash 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) ```python 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` - `.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 ```python 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`: ```cmake 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 ```bash 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: ```bash 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: ```bash python build_mingw.py --sync ``` This writes a `.pth` file into that venv's site-packages so `import pyngspice` works there.