2 Commits

Author SHA1 Message Date
Joe DiPrima fa3fb1d8d3 Add XSPICE code model build system and pre-built .cm plugins 2 weeks ago
Joe DiPrima 218953aa5a Add incremental data streaming, GIL release, GPL-3.0 license 2 weeks ago
  1. 221
      INCREMENTAL_DATA_SPEC.md
  2. 44
      LICENSE
  3. 193
      XSPICE_CODE_MODELS.md
  4. 242
      build_cmpp.py
  5. BIN
      pyngspice/codemodels/analog.cm
  6. BIN
      pyngspice/codemodels/digital.cm
  7. BIN
      pyngspice/codemodels/spice2poly.cm
  8. BIN
      pyngspice/codemodels/table.cm
  9. BIN
      pyngspice/codemodels/tlines.cm
  10. BIN
      pyngspice/codemodels/xtradev.cm
  11. BIN
      pyngspice/codemodels/xtraevt.cm
  12. 20
      pyngspice/netlist.py
  13. 4
      pyproject.toml
  14. 24
      src/bindings/module.cpp
  15. 96
      src/cpp/simulator.cpp
  16. 35
      src/cpp/simulator.h

221
INCREMENTAL_DATA_SPEC.md

@ -0,0 +1,221 @@
# Incremental Simulation Data Feature - Implementation Spec
## Goal
Enable live waveform plotting in pyTesla by exposing ngspice's `SendData` callback data to Python. During a transient simulation, pyTesla needs to periodically read partial results and update the plot while the simulation is still running.
## Current State
The `send_data_callback` is already registered with ngspice in `simulator.cpp` line 71, but the handler `handle_data()` is empty - it discards the data. The `pvecvaluesall` struct that ngspice passes to this callback contains ALL vector values at each simulation timestep.
The relevant data structures from `sharedspice.h`:
```cpp
typedef struct vecvalues {
char* name; // Vector name (e.g., "v(top)", "i(l1)")
double creal; // Real value at this timestep
double cimag; // Imaginary value
NG_BOOL is_scale; // True if this is the sweep/time variable
NG_BOOL is_complex;
} vecvalues, *pvecvalues;
typedef struct vecvaluesall {
int veccount; // Number of vectors
int vecindex; // Current step index
pvecvalues *vecsa; // Array of vector value pointers
} vecvaluesall, *pvecvaluesall;
```
## Required Changes
### 1. Add incremental data buffer to Simulator class (`simulator.h`)
Add these members to the `Simulator` class:
```cpp
private:
// Incremental data buffer - written by SendData callback, read by Python
struct IncrementalBuffer {
std::vector<std::string> vector_names; // Set once by SendInitData
std::vector<std::vector<double>> data; // [vector_index][step] = value
size_t read_cursor = 0; // How far Python has read
bool initialized = false; // True after SendInitData sets names
std::mutex mutex; // Separate mutex (callback is hot path)
};
IncrementalBuffer incr_buffer_;
```
### 2. Implement handle_init_data() (`simulator.cpp`)
When ngspice calls `SendInitData` before simulation starts, capture the vector names:
```cpp
void Simulator::handle_init_data(pvecinfoall data) {
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
incr_buffer_.vector_names.clear();
incr_buffer_.data.clear();
incr_buffer_.read_cursor = 0;
incr_buffer_.initialized = false;
if (data && data->veccount > 0) {
for (int i = 0; i < data->veccount; i++) {
if (data->vecs[i] && data->vecs[i]->vecname) {
incr_buffer_.vector_names.push_back(data->vecs[i]->vecname);
}
}
incr_buffer_.data.resize(incr_buffer_.vector_names.size());
incr_buffer_.initialized = true;
}
}
```
### 3. Implement handle_data() (`simulator.cpp`)
When ngspice calls `SendData` during simulation, append the values:
```cpp
void Simulator::handle_data(pvecvaluesall data, int count) {
if (!data || !incr_buffer_.initialized) return;
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
for (int i = 0; i < data->veccount && i < (int)incr_buffer_.data.size(); i++) {
if (data->vecsa[i]) {
incr_buffer_.data[i].push_back(data->vecsa[i]->creal);
}
}
}
```
### 4. Add Python-accessible methods to Simulator (`simulator.h` / `simulator.cpp`)
```cpp
// Returns vector names from the incremental buffer
std::vector<std::string> get_incremental_vector_names() const;
// Returns new data since last call.
// Returns map: vector_name -> vector of new values since last read.
// Advances the read cursor.
std::map<std::string, std::vector<double>> get_incremental_data();
// Returns total number of data points buffered so far
size_t get_incremental_count() const;
// Clears the incremental buffer (call before starting a new simulation)
void clear_incremental_buffer();
```
Implementation of `get_incremental_data()`:
```cpp
std::map<std::string, std::vector<double>> Simulator::get_incremental_data() {
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
std::map<std::string, std::vector<double>> result;
if (!incr_buffer_.initialized || incr_buffer_.data.empty()) {
return result;
}
size_t total = incr_buffer_.data[0].size();
if (incr_buffer_.read_cursor >= total) {
return result; // No new data
}
for (size_t i = 0; i < incr_buffer_.vector_names.size(); i++) {
auto begin = incr_buffer_.data[i].begin() + incr_buffer_.read_cursor;
auto end = incr_buffer_.data[i].end();
result[incr_buffer_.vector_names[i]] = std::vector<double>(begin, end);
}
incr_buffer_.read_cursor = total;
return result;
}
```
### 5. Expose in pybind11 bindings (`module.cpp`)
Add to the Simulator class bindings:
```cpp
.def("get_incremental_vector_names", &ngspice::Simulator::get_incremental_vector_names,
"Get vector names available in the incremental buffer")
.def("get_incremental_data", &ngspice::Simulator::get_incremental_data,
"Get new simulation data since last call. Returns dict of vector_name -> list of new values.")
.def("get_incremental_count", &ngspice::Simulator::get_incremental_count,
"Get total number of timesteps buffered so far")
.def("clear_incremental_buffer", &ngspice::Simulator::clear_incremental_buffer,
"Clear the incremental data buffer")
```
### 6. Clear buffer on reset (`simulator.cpp`)
In the existing `reset()` method, add:
```cpp
void Simulator::reset() {
// ... existing reset code ...
clear_incremental_buffer();
}
```
## Usage Pattern (Python side - for reference only, don't implement)
```python
sim = Simulator()
sim.initialize()
sim.load_netlist("circuit.net")
sim.command("set filetype=binary")
sim.clear_incremental_buffer()
sim.run_async() # Non-blocking
while sim.is_running():
new_data = sim.get_incremental_data()
if new_data:
# new_data is dict: {"time": [0.1, 0.2, ...], "v(top)": [1.5, 2.3, ...], ...}
update_plot(new_data)
time.sleep(0.1)
# Final read to get any remaining data
final_data = sim.get_incremental_data()
```
## Important Notes
- The `SendData` callback fires on ngspice's simulation thread, so the buffer mutex must be lightweight (no Python GIL interaction)
- `get_incremental_data()` is called from the Python main thread via pybind11, which handles GIL automatically
- The read cursor pattern avoids copying the entire buffer each poll - only new data is returned
- Don't forget to release the GIL when calling `run_async()` so the background thread can actually run (use `py::call_guard<py::gil_scoped_release>()` in the binding if not already done)
- After implementation, rebuild with: `pip install -e .` from the ngspice repo root
## Files to Modify
1. `C:\git\ngspice\src\cpp\simulator.h` - Add IncrementalBuffer struct and new methods
2. `C:\git\ngspice\src\cpp\simulator.cpp` - Implement handle_data(), handle_init_data(), and new methods
3. `C:\git\ngspice\src\bindings\module.cpp` - Add pybind11 bindings for new methods
## Testing
After building, verify with:
```python
from pyngspice._pyngspice import Simulator
sim = Simulator()
sim.initialize()
# Load a simple transient netlist
sim.load_netlist("test.net")
sim.clear_incremental_buffer()
sim.run_async()
import time
while sim.is_running():
data = sim.get_incremental_data()
if data:
for name, values in data.items():
print(f" {name}: {len(values)} new points")
time.sleep(0.1)
# Final read
data = sim.get_incremental_data()
print(f"Final: {sum(len(v) for v in data.values()) // max(len(data),1)} total points per vector")
```

44
LICENSE

@ -0,0 +1,44 @@
pyngspice - Python bindings for ngspice circuit simulator
=========================================================
Copyright (C) 2025 pyngspice contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Third-Party Licenses
====================
pyngspice statically links the ngspice circuit simulator, which contains
code under several licenses. See COPYING for the full ngspice license
details. Summary:
- ngspice core: Modified BSD (3-clause)
Copyright 1985-2018, Regents of the University of California and others
- XSPICE: Public domain (Georgia Tech Research Corporation)
Except src/xspice/icm/table: GPLv2 or later
- numparam (src/frontend/numparam): LGPLv2 or later
- KLU (src/maths/KLU): LGPLv2
- OSDI (src/osdi): Mozilla Public License 2.0
- cppduals (src/include/cppduals): Mozilla Public License 2.0
- admst (src/spicelib/devices/adms/admst): GPLv3
GPLv3 was chosen for pyngspice to ensure compatibility with all of
the above component licenses.

193
XSPICE_CODE_MODELS.md

@ -0,0 +1,193 @@
# Writing XSPICE Code Models for pyTesla
## What This Is
XSPICE code models let you write circuit components in C. During simulation,
ngspice calls your C function at each timestep with port voltages/states and
you compute the outputs. This is how we'll add custom parts (gate drivers,
controllers, digital logic) to the pyTesla part library.
## How a Code Model Looks in a Netlist
```spice
* Analog component using a code model
A1 in out mymodel
.model mymodel custom_model(gain=2.0 offset=0.1)
* Digital component with bridge to analog world
A2 [digital_in] [digital_out] my_gate
.model my_gate d_and(rise_delay=1n fall_delay=1n)
* Analog-to-digital bridge (connects analog node to digital code model)
A3 [analog_node] [digital_node] adc1
.model adc1 adc_bridge(in_low=0.8 in_high=2.0)
```
The `A` device letter tells ngspice this is an XSPICE code model instance.
## File Structure for a New Code Model
Each code model needs two files in `src/xspice/icm/<category>/<model_name>/`:
### 1. `ifspec.ifs` — Interface Specification
Declares the model's name, ports, and parameters. Not C — it's a simple
declarative format that the `cmpp` preprocessor converts to C.
```
NAME_TABLE:
C_Function_Name: cm_my_driver
Spice_Model_Name: my_driver
Description: "Half-bridge gate driver model"
PORT_TABLE:
Port_Name: hin lin ho lo
Description: "high in" "low in" "high out" "low out"
Direction: in in out out
Default_Type: d d v v
Allowed_Types: [d] [d] [v] [v]
Vector: no no no no
Vector_Bounds: - - - -
Null_Allowed: no no no no
PARAMETER_TABLE:
Parameter_Name: vcc dead_time
Description: "supply volts" "dead time seconds"
Data_Type: real real
Default_Value: 12.0 100e-9
Limits: [0.1 1000] [0 -]
Vector: no no
Vector_Bounds: - -
Null_Allowed: yes yes
```
### 2. `cfunc.mod` — C Implementation
Plain C with XSPICE macros for accessing ports/parameters. This is where
your component's behavior goes.
```c
#include <math.h>
void cm_my_driver(ARGS)
{
/* Read parameters */
double vcc = PARAM(vcc);
double dead_time = PARAM(dead_time);
/* Read digital input states */
Digital_State_t hin = INPUT_STATE(hin);
Digital_State_t lin = INPUT_STATE(lin);
/* Compute outputs */
if (INIT) {
/* First call — initialize state */
OUTPUT(ho) = 0.0;
OUTPUT(lo) = 0.0;
} else {
/* Normal operation */
if (hin == ONE)
OUTPUT(ho) = vcc;
else
OUTPUT(ho) = 0.0;
if (lin == ONE)
OUTPUT(lo) = vcc;
else
OUTPUT(lo) = 0.0;
}
/* Set output delay */
OUTPUT_DELAY(ho) = dead_time;
OUTPUT_DELAY(lo) = dead_time;
}
```
## Key XSPICE Macros
| Macro | Purpose |
|-------|---------|
| `ARGS` | Function signature placeholder (always use in the function declaration) |
| `INIT` | True on first call — use for initialization |
| `PARAM(name)` | Read a model parameter value |
| `INPUT(name)` | Read analog input voltage/current |
| `INPUT_STATE(name)` | Read digital input (ONE, ZERO, UNKNOWN) |
| `OUTPUT(name)` | Set analog output value |
| `OUTPUT_STATE(name)` | Set digital output state |
| `OUTPUT_DELAY(name)` | Set propagation delay for output |
| `OUTPUT_STRENGTH(name)` | Set digital output drive strength |
| `PORT_SIZE(name)` | Number of elements in a vector port |
| `PARTIAL(out, in)` | Set partial derivative (for analog convergence) |
| `STATIC_VAR(name)` | Access persistent state between timesteps |
| `T(n)` | Current time at call (n=0) or previous calls (n=1,2) |
| `cm_event_alloc(tag, size)` | Allocate event-driven state memory |
| `cm_event_get_ptr(tag, n)` | Get state pointer (n=0 current, n=1 previous) |
## Port Types
- **v** — analog voltage
- **i** — analog current
- **vd** — analog voltage differential
- **id** — analog current differential
- **d** — digital (ONE, ZERO, UNKNOWN)
## Analog-Digital Bridges
To connect analog and digital ports, use bridge models in the netlist:
```spice
* Analog voltage -> Digital state
A_bridge1 [v_node] [d_node] adc1
.model adc1 adc_bridge(in_low=0.8 in_high=2.0)
* Digital state -> Analog voltage
A_bridge2 [d_node] [v_node] dac1
.model dac1 dac_bridge(out_low=0.0 out_high=3.3)
```
## Verilog Co-Simulation (Future)
Once code models work, Verilog support follows:
1. Write your FPGA design in Verilog
2. Compile with Verilator: `verilator --cc design.v --exe`
3. Build the output into a shared library (.dll)
4. Use `d_cosim` in the netlist:
```spice
A1 [clk din] [dout pwm] cosim1
.model cosim1 d_cosim(simulation="my_fpga_design.dll")
```
ngspice loads the DLL and runs the compiled Verilog alongside the analog sim.
## Directory Layout for pyTesla Parts
```
src/xspice/icm/
├── analog/ # Built-in analog models (gain, limit, etc.)
├── digital/ # Built-in digital models (d_and, d_cosim, etc.)
├── pytesla/ # Our custom part library (new category)
│ ├── modpath.lst # List of model directories
│ ├── gate_driver/
│ │ ├── cfunc.mod
│ │ └── ifspec.ifs
│ ├── spark_gap/
│ │ ├── cfunc.mod
│ │ └── ifspec.ifs
│ └── ...
```
## Build Process
After adding a new code model:
1. Place `cfunc.mod` and `ifspec.ifs` in the appropriate directory
2. Add the model name to `modpath.lst`
3. Rebuild: `python build_mingw.py`
The CMake build will run `cmpp` to preprocess the files, compile
the generated C, and register the model with ngspice automatically.
(Note: the ICM build step in CMakeLists.txt is not yet implemented —
this document describes the target workflow once it is.)

242
build_cmpp.py

@ -0,0 +1,242 @@
"""Build the cmpp preprocessor and compile XSPICE .cm code model libraries."""
import subprocess
import os
import sys
import shutil
MINGW_BIN = r"C:\mingw64\bin"
NGSPICE_ROOT = r"C:\git\ngspice"
CMPP_SRC = os.path.join(NGSPICE_ROOT, "src", "xspice", "cmpp")
ICM_DIR = os.path.join(NGSPICE_ROOT, "src", "xspice", "icm")
INCLUDE_DIR = os.path.join(NGSPICE_ROOT, "src", "include")
CONFIG_H_DIR = os.path.join(NGSPICE_ROOT, "visualc", "src", "include")
BUILD_DIR = os.path.join(NGSPICE_ROOT, "build", "cmpp_build")
CM_OUTPUT_DIR = os.path.join(NGSPICE_ROOT, "pyngspice", "codemodels")
CMPP_EXE = os.path.join(BUILD_DIR, "cmpp.exe")
def get_env():
env = os.environ.copy()
env["PATH"] = MINGW_BIN + ";" + env["PATH"]
return env
def run(cmd, cwd=None):
print(f" > {' '.join(cmd)}")
r = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd, env=get_env())
if r.stdout.strip():
for line in r.stdout.strip().split('\n')[:20]:
print(f" {line}")
if r.stderr.strip():
lines = r.stderr.strip().split('\n')
# Show first 10 and last 30 lines of errors
if len(lines) > 40:
for line in lines[:5]:
print(f" {line}")
print(f" ... ({len(lines) - 35} lines omitted) ...")
for line in lines[-30:]:
print(f" {line}")
else:
for line in lines:
print(f" {line}")
if r.returncode != 0:
print(f" FAILED (rc={r.returncode})")
return False
return True
def build_cmpp():
"""Build the cmpp preprocessor tool."""
print("=== Step 1: Building cmpp preprocessor ===")
os.makedirs(BUILD_DIR, exist_ok=True)
if os.path.exists(CMPP_EXE):
print(f" cmpp.exe already exists, skipping")
return True
cmpp_sources = [
"main.c", "file_buffer.c", "pp_ifs.c", "pp_lst.c", "pp_mod.c",
"read_ifs.c", "util.c", "writ_ifs.c", "ifs_yacc.c", "mod_yacc.c",
"ifs_lex.c", "mod_lex.c"
]
cmd = [
"gcc", "-o", CMPP_EXE,
] + [os.path.join(CMPP_SRC, f) for f in cmpp_sources] + [
f"-I{CMPP_SRC}", f"-I{INCLUDE_DIR}",
"-lshlwapi"
]
if not run(cmd):
return False
print(f" OK: {CMPP_EXE}")
return True
def preprocess_category(category):
"""Run cmpp on a code model category (analog, digital, etc.)."""
print(f"\n=== Step 2: Preprocessing '{category}' code models ===")
src_dir = os.path.join(ICM_DIR, category)
out_dir = os.path.join(BUILD_DIR, category)
# Copy the category directory to build dir for cmpp to work in
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
shutil.copytree(src_dir, out_dir)
# Step 2a: Generate registration headers from modpath.lst
print(f" Generating registration headers...")
env = get_env()
env["CMPP_IDIR"] = src_dir
env["CMPP_ODIR"] = out_dir
r = subprocess.run([CMPP_EXE, "-lst"], capture_output=True, text=True,
cwd=out_dir, env=env)
if r.returncode != 0:
print(f" cmpp -lst FAILED: {r.stderr}")
return None
print(f" OK: cminfo.h, cmextrn.h generated")
# Step 2b: Read modpath.lst to get model names
modpath = os.path.join(src_dir, "modpath.lst")
with open(modpath) as f:
models = [line.strip() for line in f if line.strip()]
# Step 2c: Preprocess each model's .ifs and .mod files
for model in models:
model_src = os.path.join(src_dir, model)
model_out = os.path.join(out_dir, model)
if not os.path.isdir(model_src):
print(f" SKIP: {model} (directory not found)")
continue
# Preprocess ifspec.ifs -> ifspec.c
env_mod = get_env()
env_mod["CMPP_IDIR"] = model_src
env_mod["CMPP_ODIR"] = model_out
r = subprocess.run([CMPP_EXE, "-ifs"], capture_output=True, text=True,
cwd=model_out, env=env_mod)
if r.returncode != 0:
print(f" FAIL: {model}/ifspec.ifs: {r.stderr}")
return None
# Preprocess cfunc.mod -> cfunc.c
r = subprocess.run([CMPP_EXE, "-mod"], capture_output=True, text=True,
cwd=model_out, env=env_mod)
if r.returncode != 0:
print(f" FAIL: {model}/cfunc.mod: {r.stderr}")
return None
print(f" OK: {len(models)} models preprocessed")
return models
# Category-specific extra source files and include paths
CATEGORY_EXTRAS = {
"tlines": {
"extra_sources": [
os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines", "tline_common.c"),
os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines", "msline_common.c"),
],
"extra_includes": [
os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines"),
],
},
}
def compile_cm(category, models):
"""Compile preprocessed code models into a .cm DLL."""
print(f"\n=== Step 3: Compiling '{category}.cm' DLL ===")
src_dir = os.path.join(ICM_DIR, category)
out_dir = os.path.join(BUILD_DIR, category)
os.makedirs(CM_OUTPUT_DIR, exist_ok=True)
# Collect all .c files to compile
c_files = []
# dlmain.c (the DLL entry point with exports)
dlmain = os.path.join(ICM_DIR, "dlmain.c")
c_files.append(dlmain)
# Each model's cfunc.c and ifspec.c
for model in models:
model_dir = os.path.join(out_dir, model)
cfunc_c = os.path.join(model_dir, "cfunc.c")
ifspec_c = os.path.join(model_dir, "ifspec.c")
if os.path.exists(cfunc_c) and os.path.exists(ifspec_c):
c_files.append(cfunc_c)
c_files.append(ifspec_c)
else:
print(f" SKIP: {model} (generated .c files missing)")
# UDN (user-defined node) types — udnfunc.c files from udnpath.lst
udnpath = os.path.join(src_dir, "udnpath.lst")
if os.path.exists(udnpath):
with open(udnpath) as f:
udns = [line.strip() for line in f if line.strip()]
for udn in udns:
udnfunc = os.path.join(src_dir, udn, "udnfunc.c")
if os.path.exists(udnfunc):
c_files.append(udnfunc)
print(f" UDN: {udn}/udnfunc.c")
# Also need dstring.c for some models that use DS_CREATE
dstring_c = os.path.join(NGSPICE_ROOT, "src", "misc", "dstring.c")
if os.path.exists(dstring_c):
c_files.append(dstring_c)
# Category-specific extra sources
extras = CATEGORY_EXTRAS.get(category, {})
for src in extras.get("extra_sources", []):
if os.path.exists(src):
c_files.append(src)
print(f" EXTRA: {os.path.basename(src)}")
cm_file = os.path.join(CM_OUTPUT_DIR, f"{category}.cm")
cmd = [
"gcc", "-shared", "-o", cm_file,
] + c_files + [
f"-I{INCLUDE_DIR}",
f"-I{CONFIG_H_DIR}",
f"-I{out_dir}", # For cminfo.h, cmextrn.h
f"-I{ICM_DIR}", # For dlmain.c includes
]
# Category-specific extra include paths
for inc in extras.get("extra_includes", []):
cmd.append(f"-I{inc}")
cmd += [
"-DHAS_PROGREP",
"-DXSPICE",
"-DSIMULATOR",
"-DNG_SHARED_BUILD",
"-DNGSPICEDLL",
"-Wall", "-Wno-unused-variable", "-Wno-unused-function",
"-Wno-sign-compare", "-Wno-maybe-uninitialized",
"-Wno-implicit-function-declaration", # GCC 15 makes this an error; some models missing stdlib.h
]
if not run(cmd):
return False
size_kb = os.path.getsize(cm_file) // 1024
print(f" OK: {cm_file} ({size_kb} KB)")
return True
def build_category(category):
"""Full pipeline: preprocess + compile a code model category."""
models = preprocess_category(category)
if models is None:
return False
return compile_cm(category, models)
if __name__ == "__main__":
categories = sys.argv[1:] if len(sys.argv) > 1 else ["analog"]
if not build_cmpp():
sys.exit(1)
for cat in categories:
if not build_category(cat):
print(f"\nFailed to build {cat}.cm")
sys.exit(1)
print(f"\n=== Done! Code models in: {CM_OUTPUT_DIR} ===")

BIN
pyngspice/codemodels/analog.cm

BIN
pyngspice/codemodels/digital.cm

BIN
pyngspice/codemodels/spice2poly.cm

BIN
pyngspice/codemodels/table.cm

BIN
pyngspice/codemodels/tlines.cm

BIN
pyngspice/codemodels/xtradev.cm

BIN
pyngspice/codemodels/xtraevt.cm

20
pyngspice/netlist.py

@ -83,11 +83,17 @@ def preprocess_netlist(content: str) -> str:
# Insert extra components (series resistors) just before .end
if extra_components:
result = []
found_end = False
for line in output_lines:
if line.strip().lower() == '.end':
found_end = True
result.append('* --- pyngspice: inductor series resistance expansion ---')
result.extend(extra_components)
result.append(line)
if not found_end:
# No .end found - append at end to avoid dangling nodes
result.append('* --- pyngspice: inductor series resistance expansion ---')
result.extend(extra_components)
output_lines = result
# Insert 0V voltage source probes for capacitor current measurement
@ -249,10 +255,14 @@ def _expand_savecurrents(lines: List[str]) -> List[str]:
# Insert before .end
result = []
found_end = False
for line in lines:
if line.strip().lower() == '.end':
found_end = True
result.extend(save_lines)
result.append(line)
if not found_end:
result.extend(save_lines)
return result
@ -343,11 +353,16 @@ def _insert_capacitor_probes(lines: List[str]) -> List[str]:
# Insert probe V sources before .end
if probe_lines:
result = []
found_end = False
for line in modified_lines:
if line.strip().lower() == '.end':
found_end = True
result.append('* --- pyngspice: capacitor current probes ---')
result.extend(probe_lines)
result.append(line)
if not found_end:
result.append('* --- pyngspice: capacitor current probes ---')
result.extend(probe_lines)
return result
return modified_lines
@ -399,11 +414,16 @@ def _insert_targeted_capacitor_probes(lines: List[str], target_caps: set) -> Lis
# Insert probe V sources before .end
if probe_lines:
result = []
found_end = False
for line in modified_lines:
if line.strip().lower() == '.end':
found_end = True
result.append('* --- pyngspice: targeted capacitor current probes ---')
result.extend(probe_lines)
result.append(line)
if not found_end:
result.append('* --- pyngspice: targeted capacitor current probes ---')
result.extend(probe_lines)
return result
return modified_lines

4
pyproject.toml

@ -10,7 +10,7 @@ build-backend = "scikit_build_core.build"
name = "pyngspice"
version = "43.0.0"
description = "Python bindings for ngspice circuit simulator (pyTesla backend)"
license = {text = "BSD-3-Clause"}
license = {text = "GPL-3.0-or-later"}
requires-python = ">=3.9"
authors = [
{name = "ngspice team", email = "ngspice-devel@lists.sourceforge.net"},
@ -20,7 +20,7 @@ classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: BSD License",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",

24
src/bindings/module.cpp

@ -219,9 +219,10 @@ PYBIND11_MODULE(_pyngspice, m) {
)pbdoc")
.def("run_now", &ngspice::SimRunner::run_now,
py::call_guard<py::gil_scoped_release>(),
py::arg("netlist"),
R"pbdoc(
Run simulation synchronously (blocking).
Run simulation synchronously (blocking, releases GIL).
Args:
netlist: Path to the netlist file
@ -231,9 +232,10 @@ PYBIND11_MODULE(_pyngspice, m) {
)pbdoc")
.def("run", &ngspice::SimRunner::run,
py::call_guard<py::gil_scoped_release>(),
py::arg("netlist"),
R"pbdoc(
Queue a simulation for async execution.
Queue a simulation for async execution (releases GIL).
Args:
netlist: Path to the netlist file
@ -305,9 +307,11 @@ PYBIND11_MODULE(_pyngspice, m) {
"Execute a SPICE command")
.def("run", &ngspice::Simulator::run,
"Run the simulation (blocking)")
py::call_guard<py::gil_scoped_release>(),
"Run the simulation (blocking, releases GIL)")
.def("run_async", &ngspice::Simulator::run_async,
py::call_guard<py::gil_scoped_release>(),
"Run simulation in background thread")
.def("is_running", &ngspice::Simulator::is_running,
@ -336,5 +340,17 @@ PYBIND11_MODULE(_pyngspice, m) {
"Get accumulated output messages")
.def("clear_output", &ngspice::Simulator::clear_output,
"Clear accumulated output");
"Clear accumulated output")
.def("get_incremental_vector_names", &ngspice::Simulator::get_incremental_vector_names,
"Get vector names available in the incremental buffer")
.def("get_incremental_data", &ngspice::Simulator::get_incremental_data,
"Get new simulation data since last call. Returns dict of vector_name -> list of new values.")
.def("get_incremental_count", &ngspice::Simulator::get_incremental_count,
"Get total number of timesteps buffered so far")
.def("clear_incremental_buffer", &ngspice::Simulator::clear_incremental_buffer,
"Clear the incremental data buffer");
}

96
src/cpp/simulator.cpp

@ -128,13 +128,33 @@ void Simulator::handle_exit(int status, bool immediate, bool quit) {
}
void Simulator::handle_data(pvecvaluesall data, int count) {
// Called during simulation with vector values
// We don't store this - results are read from raw file
if (!data || !incr_buffer_.initialized) return;
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
for (int i = 0; i < data->veccount && i < (int)incr_buffer_.data.size(); i++) {
if (data->vecsa[i]) {
incr_buffer_.data[i].push_back(data->vecsa[i]->creal);
}
}
}
void Simulator::handle_init_data(pvecinfoall data) {
// Called before simulation with vector info
// We don't need this - info is in raw file
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
incr_buffer_.vector_names.clear();
incr_buffer_.data.clear();
incr_buffer_.read_cursor = 0;
incr_buffer_.initialized = false;
if (data && data->veccount > 0) {
for (int i = 0; i < data->veccount; i++) {
if (data->vecs[i] && data->vecs[i]->vecname) {
incr_buffer_.vector_names.push_back(data->vecs[i]->vecname);
}
}
incr_buffer_.data.resize(incr_buffer_.vector_names.size());
incr_buffer_.initialized = true;
}
}
void Simulator::handle_thread_status(bool running) {
@ -168,6 +188,16 @@ Simulator::Simulator(Simulator&& other) noexcept
, output_callback_(std::move(other.output_callback_))
, status_callback_(std::move(other.status_callback_))
{
// Transfer incremental buffer under lock
{
std::lock_guard<std::mutex> lock(other.incr_buffer_.mutex);
incr_buffer_.vector_names = std::move(other.incr_buffer_.vector_names);
incr_buffer_.data = std::move(other.incr_buffer_.data);
incr_buffer_.read_cursor = other.incr_buffer_.read_cursor;
incr_buffer_.initialized = other.incr_buffer_.initialized;
other.incr_buffer_.read_cursor = 0;
other.incr_buffer_.initialized = false;
}
other.initialized_ = false;
unregister_instance(&other);
register_instance(this);
@ -187,6 +217,17 @@ Simulator& Simulator::operator=(Simulator&& other) noexcept {
output_callback_ = std::move(other.output_callback_);
status_callback_ = std::move(other.status_callback_);
// Transfer incremental buffer under lock
{
std::lock_guard<std::mutex> lock(other.incr_buffer_.mutex);
incr_buffer_.vector_names = std::move(other.incr_buffer_.vector_names);
incr_buffer_.data = std::move(other.incr_buffer_.data);
incr_buffer_.read_cursor = other.incr_buffer_.read_cursor;
incr_buffer_.initialized = other.incr_buffer_.initialized;
other.incr_buffer_.read_cursor = 0;
other.incr_buffer_.initialized = false;
}
other.initialized_ = false;
unregister_instance(&other);
register_instance(this);
@ -332,6 +373,7 @@ void Simulator::reset() {
command("reset");
status_ = SimulationStatus{};
accumulated_output_.clear();
clear_incremental_buffer();
}
SimulationStatus Simulator::get_status() const {
@ -430,4 +472,50 @@ void Simulator::clear_output() {
accumulated_output_.clear();
}
// Incremental data methods
std::vector<std::string> Simulator::get_incremental_vector_names() {
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
return incr_buffer_.vector_names;
}
std::map<std::string, std::vector<double>> Simulator::get_incremental_data() {
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
std::map<std::string, std::vector<double>> result;
if (!incr_buffer_.initialized || incr_buffer_.data.empty()) {
return result;
}
size_t total = incr_buffer_.data[0].size();
if (incr_buffer_.read_cursor >= total) {
return result; // No new data
}
for (size_t i = 0; i < incr_buffer_.vector_names.size(); i++) {
auto begin = incr_buffer_.data[i].begin() + incr_buffer_.read_cursor;
auto end = incr_buffer_.data[i].end();
result[incr_buffer_.vector_names[i]] = std::vector<double>(begin, end);
}
incr_buffer_.read_cursor = total;
return result;
}
size_t Simulator::get_incremental_count() {
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
if (incr_buffer_.data.empty()) {
return 0;
}
return incr_buffer_.data[0].size();
}
void Simulator::clear_incremental_buffer() {
std::lock_guard<std::mutex> lock(incr_buffer_.mutex);
incr_buffer_.vector_names.clear();
incr_buffer_.data.clear();
incr_buffer_.read_cursor = 0;
incr_buffer_.initialized = false;
}
} // namespace ngspice

35
src/cpp/simulator.h

@ -11,6 +11,7 @@
#include <string>
#include <vector>
#include <map>
#include <functional>
#include <memory>
#include <mutex>
@ -245,6 +246,30 @@ public:
*/
void clear_output();
/**
* @brief Get vector names from the incremental data buffer
*/
std::vector<std::string> get_incremental_vector_names();
/**
* @brief Get new simulation data since last call
*
* Returns a map of vector_name -> list of new values since the
* last call. Advances an internal read cursor so each data point
* is returned exactly once.
*/
std::map<std::string, std::vector<double>> get_incremental_data();
/**
* @brief Get total number of timesteps buffered so far
*/
size_t get_incremental_count();
/**
* @brief Clear the incremental data buffer
*/
void clear_incremental_buffer();
/**
* @brief Get the output directory for raw files
*/
@ -285,6 +310,16 @@ private:
std::function<void(const std::string&)> output_callback_;
std::function<void(const std::string&)> status_callback_;
// Incremental data buffer - written by SendData callback, read by Python
struct IncrementalBuffer {
std::vector<std::string> vector_names; // Set once by SendInitData
std::vector<std::vector<double>> data; // [vector_index][step] = value
size_t read_cursor = 0; // How far Python has read
bool initialized = false; // True after SendInitData sets names
std::mutex mutex; // Separate mutex (callback is hot path)
};
IncrementalBuffer incr_buffer_;
// Global instance map for callback routing
static std::mutex instances_mutex_;
static std::vector<Simulator*> instances_;

Loading…
Cancel
Save