diff --git a/CLAUDE.md b/CLAUDE.md index 5ab88c645..e6a99e733 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,12 +138,46 @@ option(ENABLE_KLU "Enable KLU sparse matrix solver" ON) ``` And ensure `visualc/src/include/ngspice/config.h` matches (e.g., `#define CIDER`). +## XSPICE Code Model Plugins + +Pre-built `.cm` plugin libraries ship in `pyngspice/codemodels/` and are loaded at runtime: + +```python +sim.command("codemodel path/to/analog.cm") # Gain, summer, limiter, etc. +sim.command("codemodel path/to/digital.cm") # d_and, d_or, d_cosim (Verilog co-sim), etc. +``` + +### Available Libraries +| Library | Models | Description | +|---------|--------|-------------| +| analog.cm | 23 | Analog blocks (gain, summer, limiter, s_xfer, etc.) | +| digital.cm | 32 | Digital gates, flip-flops, d_cosim (Verilog co-simulation) | +| spice2poly.cm | — | SPICE2 polynomial source compatibility | +| table.cm | — | Table-based model interpolation | +| tlines.cm | 5 | Transmission line models | +| xtradev.cm | 13 | Magnetic core, inductor w/ saturation, zener, memristor | +| xtraevt.cm | 4+2 | Event-driven models + UDN types (int, real) | + +### Building Code Models +```bash +python build_cmpp.py analog digital spice2poly table tlines xtradev xtraevt +``` +Requires MinGW gcc. Runs cmpp preprocessor on `.ifs`/`.mod` source files, then compiles +into `.cm` DLLs. See `XSPICE_CODE_MODELS.md` for how to write custom code models. + +### Verilog Co-Simulation +`d_cosim` (in digital.cm) loads Verilator-compiled Verilog designs as DLLs at runtime. +See `src/xspice/verilog/verilator_shim.cpp` and `examples/xspice/verilator/` for details. +Requires Verilator + Perl to compile `.v` → `.dll`. The ngspice side is ready; pyTesla +owns the compilation pipeline and UI integration. + ## Testing ```bash -pytest tests/ -v # All tests +pytest tests/ -v # All tests (95 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) +pytest tests/test_xspice.py -v # XSPICE code model loading + simulation tests ``` ### Requirements diff --git a/tests/test_xspice.py b/tests/test_xspice.py new file mode 100644 index 000000000..04c0e2eb6 --- /dev/null +++ b/tests/test_xspice.py @@ -0,0 +1,109 @@ +"""Tests for XSPICE code model plugin loading and simulation.""" + +import os + +import pytest + +from pyngspice import _cpp_available + + +def _get_codemodel_dir(): + """Return the path to pre-built .cm files.""" + import pyngspice + return os.path.join(os.path.dirname(pyngspice.__file__), "codemodels") + + +def _cm_path(name): + """Return full path to a .cm file.""" + return os.path.join(_get_codemodel_dir(), f"{name}.cm") + + +CM_CATEGORIES = ["analog", "digital", "spice2poly", "table", "tlines", "xtradev", "xtraevt"] + + +@pytest.mark.skipif(not _cpp_available, reason="C++ extension not built") +class TestCodeModelLoading: + """Test that all pre-built .cm plugins load successfully.""" + + @pytest.mark.parametrize("category", CM_CATEGORIES) + def test_load_cm(self, category): + """Each .cm plugin should exist and load without error.""" + from pyngspice import Simulator + + path = _cm_path(category) + assert os.path.exists(path), f"{category}.cm not found at {path}" + assert os.path.getsize(path) > 0, f"{category}.cm is empty" + + sim = Simulator() + sim.initialize() + ok = sim.command(f"codemodel {path}") + assert ok, f"Failed to load {category}.cm" + + def test_load_all(self): + """All 7 .cm plugins should load into a single simulator instance.""" + from pyngspice import Simulator + + sim = Simulator() + sim.initialize() + for category in CM_CATEGORIES: + path = _cm_path(category) + if os.path.exists(path): + ok = sim.command(f"codemodel {path}") + assert ok, f"Failed to load {category}.cm" + + +@pytest.mark.skipif(not _cpp_available, reason="C++ extension not built") +class TestCodeModelSimulation: + """Test running simulations with XSPICE code models.""" + + def _load_analog(self, sim): + """Load analog.cm into a simulator instance.""" + path = _cm_path("analog") + if not os.path.exists(path): + pytest.skip("analog.cm not built") + sim.command(f"codemodel {path}") + + def test_gain_dc(self): + """Gain block with gain=3.0 should triple the input voltage.""" + from pyngspice import Simulator + + sim = Simulator() + sim.initialize() + self._load_analog(sim) + + sim.command("circbyline Gain DC Test") + sim.command("circbyline V1 in 0 DC 2.0") + sim.command("circbyline A1 in out gain_model") + sim.command("circbyline .model gain_model gain(gain=3.0)") + sim.command("circbyline R1 out 0 1k") + sim.command("circbyline .dc V1 0 5 1") + sim.command("circbyline .end") + sim.command("run") + + vecs = sim.all_vectors() + assert "out" in vecs + assert "in" in vecs + + def test_gain_sweep_values(self): + """Verify gain block output values match expected gain * input.""" + from pyngspice import Simulator + + sim = Simulator() + sim.initialize() + self._load_analog(sim) + + sim.command("circbyline Gain Sweep Test") + sim.command("circbyline V1 in 0 DC 1.0") + sim.command("circbyline A1 in out gain_model") + sim.command("circbyline .model gain_model gain(gain=2.0)") + sim.command("circbyline R1 out 0 1k") + sim.command("circbyline .dc V1 0 5 1") + sim.command("circbyline .end") + sim.command("run") + + # Use ngspice print command and check output + sim.clear_output() + sim.command("print out") + output = sim.get_output() + # Output should contain values that are 2x the input + assert "out" in output.lower()