2 Commits
6eeaaa7ca7
...
fa3fb1d8d3
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
fa3fb1d8d3 |
Add XSPICE code model build system and pre-built .cm plugins
- Add build_cmpp.py: builds cmpp preprocessor, preprocesses .ifs/.mod files, compiles .cm DLL plugins for runtime loading via codemodel command - Ship 7 pre-built .cm plugins in pyngspice/codemodels/: analog (23 models), digital (32 models incl. d_cosim for Verilog co-simulation), spice2poly, table, tlines (5 transmission line models), xtradev (13 models incl. magnetic core, zener), xtraevt (4 models + UDNs) - Add XSPICE_CODE_MODELS.md developer guide for writing custom components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
2 weeks ago |
|
|
218953aa5a |
Add incremental data streaming, GIL release, GPL-3.0 license
- Add IncrementalBuffer with thread-safe SendData/SendInitData callback handling for live waveform plotting during transient simulations - Expose get_incremental_data(), get_incremental_vector_names(), get_incremental_count(), clear_incremental_buffer() to Python - Add py::call_guard<py::gil_scoped_release>() on run(), run_async(), SimRunner::run_now(), SimRunner::run() so Python UI stays responsive - Update license from BSD-3-Clause to GPL-3.0-or-later (required by GPLv3 components: admst, xspice/icm/table) - Fix netlist preprocessor robustness when .end line is missing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
2 weeks ago |
16 changed files with 869 additions and 10 deletions
-
221INCREMENTAL_DATA_SPEC.md
-
44LICENSE
-
193XSPICE_CODE_MODELS.md
-
242build_cmpp.py
-
BINpyngspice/codemodels/analog.cm
-
BINpyngspice/codemodels/digital.cm
-
BINpyngspice/codemodels/spice2poly.cm
-
BINpyngspice/codemodels/table.cm
-
BINpyngspice/codemodels/tlines.cm
-
BINpyngspice/codemodels/xtradev.cm
-
BINpyngspice/codemodels/xtraevt.cm
-
20pyngspice/netlist.py
-
4pyproject.toml
-
24src/bindings/module.cpp
-
96src/cpp/simulator.cpp
-
35src/cpp/simulator.h
@ -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") |
|||
``` |
|||
@ -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. |
|||
@ -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.) |
|||
@ -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} ===") |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue