Skip to content

Commit c64ad5c

Browse files
committed
Add Xyce simulator support behind SimulatorInterface abstraction
Extract shared state and logic from NgspiceInterface into a SimulatorInterface ABC, add XyceInterface for explicit-stepping co-simulation via Xyce's C API, and make netlist generation simulator-aware (YDAC devices, .TRAN, .PRINT TRAN for Xyce). AnalogBlock gains a simulator="ngspice"|"xyce" parameter; MixedSignalBridge gains simulator_lib= (ngspice_lib deprecated). All existing tests pass unchanged. Remove deprecated sync_period_ns.
1 parent 3e5215e commit c64ad5c

10 files changed

Lines changed: 937 additions & 181 deletions

File tree

CLAUDE.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build & Test Commands
6+
7+
```bash
8+
pip install -e ".[dev]" # Install for development
9+
pytest tests/ -v # Run all unit tests
10+
pytest tests/test_pins.py -v # Run a single test file
11+
pytest tests/ -k "test_hysteresis" # Run tests matching pattern
12+
mypy src/cocotbext/ams/ --ignore-missing-imports # Type check
13+
ruff check src/ tests/ # Lint
14+
```
15+
16+
Integration examples (require ngspice + iverilog):
17+
```bash
18+
cd examples/sar_adc && make SIM=icarus
19+
cd examples/pll && make SIM=icarus
20+
```
21+
22+
## Architecture
23+
24+
cocotbext-ams bridges cocotb's digital simulation with an analog SPICE simulator (ngspice or Xyce) via shared library APIs (ctypes). It uses **event-driven synchronization** rather than fixed-interval lock-step.
25+
26+
### Supported Simulators
27+
28+
- **ngspice** (default): Callback-driven via libngspice. `GetSyncData` callback triggers sync points.
29+
- **Xyce**: Explicit-stepping via `xyce_simulateUntil()`. The stepping loop triggers sync points at each interval.
30+
31+
Both run inside a `@bridge` thread and periodically call `_on_sync_point()` (a `@resume` function). The difference is only what triggers that call — ngspice's internal callback vs Xyce's explicit loop.
32+
33+
### Module Dependency Flow
34+
35+
```
36+
_bridge.py (MixedSignalBridge, AnalogBlock)
37+
├── _simulator.py (SimulatorInterface — ABC with shared state)
38+
│ ├── _ngspice.py (NgspiceInterface — ctypes wrapper for libngspice)
39+
│ └── _xyce.py (XyceInterface — ctypes wrapper for Xyce C API)
40+
├── _pins.py (DigitalPin — D/A and A/D conversion with hysteresis)
41+
├── _netlist.py (generate_netlist — simulator-aware SPICE deck augmentation)
42+
└── _vcd.py (AnalogVcdWriter — real+digital VCD output)
43+
```
44+
45+
### Simulator Abstraction (`_simulator.py`)
46+
47+
`SimulatorInterface` ABC holds all shared state (`_vsrc_values`, `_node_voltages`, `_spice_time`, `_next_sync_time`, `_prev_digital_values`, `_output_pin_configs`, `_vcd_writer`, etc.) and implements `_check_crossings()` and `_write_vcd()`. Subclasses implement `load_circuit()`, `run_simulation()`, `get_node_voltage()`, `set_vsrc()`, `halt()`, `reset()`, `is_running()`.
48+
49+
### Two Asymmetric Data Paths
50+
51+
**Digital → Analog:** `ValueChange` monitor coroutines update `_vsrc_values` dict instantly. For ngspice, it reads these via `GetVSRCData` callback. For Xyce, they are pushed via `xyce_updateTimeVoltagePairs()` at each step.
52+
53+
**Analog → Digital:** Voltages are read from the simulator and `_check_crossings()` detects threshold crossings. When a crossing is detected, the sync mechanism forces new digital values onto Verilog and advances digital time.
54+
55+
A fallback `max_sync_interval_ns` (default 100ns) ensures periodic sync even without crossings.
56+
57+
### Thread Model
58+
59+
- The simulator runs a blocking simulation in a `@bridge` thread
60+
- **ngspice:** `GetVSRCData` / `SendData` / `GetSyncData` callbacks fire from the ngspice thread; `GetSyncData` calls `@resume` to sync
61+
- **Xyce:** The explicit stepping loop calls `@resume` at each sync interval
62+
- `_vsrc_values` dict is safe via GIL (cocotb writes, simulator reads)
63+
- `_node_voltages` is only read at sync points when the simulator is paused
64+
65+
### Netlist Augmentation
66+
67+
`_netlist.py` wraps the user's `.subckt` with simulator-specific syntax:
68+
69+
| Feature | ngspice | Xyce |
70+
|---------|---------|------|
71+
| Runtime sources | `v_name node 0 dc 0 external` | `YDAC v_name DAC node 0` |
72+
| Output save | `.save v(node)` | `.PRINT TRAN v(node)` |
73+
| Tran command | `.tran step stop uic` | `.TRAN step stop` |
74+
| Include | `.include path` | `.INCLUDE path` |
75+
| End marker | `.end` | `.END` |
76+
77+
The `simulator=` parameter on `generate_netlist()` dispatches to the right generator.
78+
79+
### Vector Name Normalization
80+
81+
ngspice reports names as `"tran1.v(d0)"`, `"v(d0)"`, or `"d0"`. All forms are stored in `_node_voltages` so lookups work with any variant. Xyce stores both the expression form (`v(d0)`) and bare name (`d0`).
82+
83+
## Testing Conventions
84+
85+
- Unit tests in `tests/` do NOT require ngspice or Xyce — they test pins, netlist generation, and VCD writing in isolation.
86+
- Tests that need `NgspiceInterface` create mock instances via `__new__` and manually set required attributes (`_node_voltages`, `_crossing_detected`, `_prev_digital_values`, `_output_pin_configs`, `_spice_time`). These attributes now come from `SimulatorInterface.__init__()`.
87+
- Xyce netlist generation is tested via `test_generate_xyce_netlist()` without requiring Xyce to be installed.
88+
- Integration tests live in `examples/` and require ngspice + iverilog. Xyce integration testing requires Xyce installed.
89+
90+
## Git Commit Rules
91+
92+
- Never add "Co-Authored-By: Claude" or any Claude attribution to commit messages.

README.md

Lines changed: 125 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<h1 align="center">cocotbext-ams</h1>
66

77
<p align="center">
8-
<strong>An ngspice bridge for <a href="https://github.com/cocotb/cocotb">cocotb</a> — open-source mixed-signal co-simulation</strong>
8+
<strong>An analog simulator bridge for <a href="https://github.com/cocotb/cocotb">cocotb</a> — open-source mixed-signal co-simulation</strong>
99
</p>
1010

1111
<p align="center">
@@ -23,15 +23,17 @@
2323
- [Prerequisites](#prerequisites)
2424
- [Installation](#installation)
2525
- [Quick Start](#quick-start)
26-
- [Tutorial: PWM DAC with SAR Controller](docs/tutorial/index.md)
2726
- [API Reference](#api-reference)
27+
- [Tutorial: PWM DAC with SAR Controller](docs/tutorial/index.md)
2828
- [Examples](#examples)
2929
- [Architecture Details](#architecture-details)
30+
- [Troubleshooting](#troubleshooting)
3031

3132
## Overview
3233

33-
cocotbext-ams synchronizes cocotb's digital simulation with ngspice's analog
34-
simulation via the libngspice shared library API. This allows you to co-simulate
34+
cocotbext-ams synchronizes cocotb's digital simulation with an analog SPICE
35+
simulator via shared library APIs. It supports **ngspice** (default) and
36+
**Xyce** (Sandia's open-source parallel SPICE), allowing you to co-simulate
3537
SPICE netlists alongside Verilog/VHDL testbenches using entirely open-source
3638
tools.
3739

@@ -44,27 +46,30 @@ cocotb testbench (Python async)
4446
MixedSignalBridge (orchestrator)
4547
|-- reads Verilog signals via cocotb handles
4648
|-- converts digital <-> analog (threshold-based)
47-
'-- drives ngspice via NgspiceInterface
48-
| |
49-
v v
50-
HDL Simulator libngspice.so
51-
(Icarus/Verilator) (ngspice 45+)
49+
'-- drives simulator via SimulatorInterface
50+
| |
51+
v v
52+
HDL Simulator libngspice.so or libxycecinterface.so
53+
(Icarus/Verilator) (ngspice 45+) (Xyce 7+)
5254
```
5355

5456
The bridge uses **event-driven synchronization**: instead of exchanging signals
5557
at a fixed interval, it reacts to actual signal changes:
5658

57-
- **Digital → Analog:** `ValueChange` monitor coroutines update ngspice voltage
58-
sources the instant a Verilog signal changes — no sync overhead needed since
59-
ngspice reads the values on every internal evaluation step.
60-
- **Analog → Digital:** Threshold-crossing detection in ngspice's `SendData`
61-
callback triggers an immediate sync when a SPICE output crosses a digital
62-
threshold, forcing the new value onto the Verilog signal.
59+
- **Digital → Analog:** `ValueChange` monitor coroutines update voltage source
60+
values the instant a Verilog signal changes — no sync overhead needed.
61+
- **Analog → Digital:** Threshold-crossing detection triggers an immediate sync
62+
when a SPICE output crosses a digital threshold, forcing the new value onto
63+
the Verilog signal.
6364
- A configurable **maximum sync interval** (default 100 ns) ensures periodic
6465
fallback synchronization even when no crossings occur.
6566

67+
**Supported simulators:**
68+
- **ngspice** (default) — callback-driven via libngspice's shared library API
69+
- **Xyce** — explicit stepping via Xyce's C interface (`xyce_simulateUntil()`)
70+
6671
**Signal bridging:**
67-
- **Digital → Analog:** Verilog 1/0 mapped to VDD/VSS via EXTERNAL voltage sources in SPICE
72+
- **Digital → Analog:** Verilog 1/0 mapped to VDD/VSS via voltage sources in SPICE
6873
- **Analog → Digital:** SPICE node voltage compared against a threshold (with optional hysteresis), result forced onto Verilog output
6974
- **Analog-only pins:** remain X in Verilog, fully simulated in SPICE
7075

@@ -84,7 +89,8 @@ comparator output q (second), SAR value register (third), and done signal
8489

8590
- **Python** >= 3.10
8691
- **cocotb** >= 2.0
87-
- **ngspice** shared library (`libngspice.so` / `libngspice.dylib`)
92+
- **ngspice** shared library (`libngspice.so` / `libngspice.dylib`) *or*
93+
**Xyce** shared library (`libxycecinterface.so`)
8894
- A Verilog simulator supported by cocotb (e.g., Icarus Verilog)
8995

9096
### Installing ngspice
@@ -109,7 +115,7 @@ brew install ngspice
109115
conda install -c conda-forge ngspice
110116
```
111117

112-
#### Building from source
118+
#### Building ngspice from source
113119

114120
If your distribution doesn't package the shared library, or you need a specific version:
115121

@@ -120,6 +126,25 @@ mkdir build && cd build
120126
make -j$(nproc) && sudo make install
121127
```
122128

129+
### Installing Xyce
130+
131+
Xyce is an open-source parallel SPICE simulator from Sandia National Laboratories.
132+
To use Xyce with cocotbext-ams, you need the shared library build
133+
(`libxycecinterface.so`).
134+
135+
See the [Xyce installation guide](https://xyce.sandia.gov/documentation-tutorials/building-guide/)
136+
for build instructions. When building, enable the shared library:
137+
138+
```bash
139+
cmake -DBUILD_SHARED_LIBS=ON ...
140+
```
141+
142+
If the library is installed in a non-standard location, pass the path explicitly:
143+
144+
```python
145+
bridge = MixedSignalBridge(dut, blocks, simulator_lib="/path/to/libxycecinterface.so")
146+
```
147+
123148
## Installation
124149

125150
```bash
@@ -202,10 +227,31 @@ async def test_my_block(dut):
202227
```
203228

204229
The `analog_vcd` parameter writes a VCD file with `real`-typed signals at
205-
full ngspice resolution. Load it alongside the HDL simulator's digital VCD
230+
full simulator resolution. Load it alongside the HDL simulator's digital VCD
206231
in Surfer, GTKWave, or any viewer that supports real-valued VCD signals to
207232
see analog and digital waveforms together.
208233

234+
#### Using Xyce instead of ngspice
235+
236+
```python
237+
block = AnalogBlock(
238+
name="dut",
239+
spice_file="my_block.sp",
240+
subcircuit="my_block",
241+
digital_pins={...},
242+
analog_inputs={"ain": 0.9},
243+
vdd=1.8,
244+
simulator="xyce", # use Xyce instead of ngspice
245+
)
246+
247+
bridge = MixedSignalBridge(dut, [block],
248+
simulator_lib="/path/to/libxycecinterface.so")
249+
await bridge.start(duration_ns=50_000)
250+
```
251+
252+
The bridge auto-generates a Xyce-compatible netlist (YDAC devices, `.TRAN`,
253+
`.PRINT TRAN`) and drives the simulation via Xyce's explicit stepping API.
254+
209255
## API Reference
210256

211257
### `DigitalPin(direction, width=1, vdd=1.8, vss=0.0, threshold=None, hysteresis=0.0)`
@@ -231,24 +277,32 @@ Describes an analog block (SPICE subcircuit) to be co-simulated.
231277
| `spice_file` | Path to the SPICE netlist |
232278
| `subcircuit` | Name of the `.subckt` |
233279
| `digital_pins` | `dict[str, DigitalPin]` — pin name to configuration |
234-
| `analog_inputs` | `dict[str, float]` — analog input name to initial voltage (EXTERNAL, changeable at runtime) |
280+
| `analog_inputs` | `dict[str, float]` — analog input name to initial voltage (changeable at runtime) |
235281
| `vdd` | Supply voltage (default 1.8) |
236282
| `vss` | Ground voltage (default 0.0) |
237283
| `tran_step` | SPICE transient step size (default `"0.1n"`) |
238284
| `extra_lines` | Additional SPICE lines for the generated netlist (e.g., `.include` directives for PDK libraries) |
285+
| `simulator` | `"ngspice"` (default) or `"xyce"` |
239286

240-
### `MixedSignalBridge(dut, analog_blocks, max_sync_interval_ns=100.0, ngspice_lib=None)`
287+
### `MixedSignalBridge(dut, analog_blocks, max_sync_interval_ns=100.0, simulator_lib=None)`
241288

242289
The main orchestrator.
243290

291+
| Parameter | Description |
292+
|-----------|-------------|
293+
| `dut` | cocotb DUT handle |
294+
| `analog_blocks` | List of `AnalogBlock` descriptions |
295+
| `max_sync_interval_ns` | Maximum time between sync points in nanoseconds (default 100.0) |
296+
| `simulator_lib` | Path to the simulator shared library (auto-detected if None) |
297+
244298
| Method | Description |
245299
|--------|-------------|
246300
| `await start(duration_ns, analog_vcd=None, vcd_nodes=None)` | Load circuit, start co-simulation. Pass `analog_vcd="file.vcd"` to record analog waveforms. `vcd_nodes` adds extra SPICE nodes beyond the auto-included output pins. |
247301
| `await stop()` | Halt simulation, release forced signals |
248302
| `set_analog_input(block, name, voltage)` | Change an analog input voltage at runtime |
249303
| `get_analog_voltage(block, node)` | Probe any SPICE node voltage |
250304

251-
> **Migration note:** The old `sync_period_ns` parameter still works but emits a `DeprecationWarning`. Rename it to `max_sync_interval_ns`.
305+
> **Migration note:** The old `ngspice_lib` parameter still works but emits a `DeprecationWarning`. Rename it to `simulator_lib`.
252306
253307
### Sync interval selection
254308

@@ -275,10 +329,21 @@ paths, runtime analog control, VCD export, and waveform viewing.
275329

276330
## Architecture Details
277331

332+
### Simulator abstraction
333+
334+
Both ngspice and Xyce inherit from `SimulatorInterface`, which holds all
335+
shared state (voltage source values, node voltages, crossing detection,
336+
VCD writer) and implements common logic (`_check_crossings()`,
337+
`_write_vcd()`). Subclasses implement the simulator-specific ctypes wrapper
338+
and control flow.
339+
278340
### Thread model
279341

280342
The bridge uses cocotb's `@bridge` / `@resume` mechanism for thread
281-
synchronization:
343+
synchronization. Both simulators run a blocking simulation in a
344+
`@bridge` thread and periodically call a `@resume` function at sync points:
345+
346+
**ngspice:**
282347

283348
1. `@bridge` runs ngspice's blocking `tran` command in a dedicated thread.
284349
2. `GetVSRCData` fires on every ngspice evaluation step, reading the
@@ -288,42 +353,63 @@ synchronization:
288353
4. `GetSyncData` fires at each internal timestep. If a crossing was detected
289354
(or the fallback interval elapsed), it calls a `@resume` function that
290355
blocks the ngspice thread and transfers control to the cocotb scheduler.
356+
357+
**Xyce:**
358+
359+
1. `@bridge` runs an explicit stepping loop in a dedicated thread.
360+
2. At each step: push VSRC values via `xyce_updateTimeVoltagePairs()`,
361+
advance via `xyce_simulateUntil()`, read voltages via
362+
`xyce_obtainResponse()`, check crossings.
363+
3. At sync intervals, calls the same `@resume` function as ngspice.
364+
365+
**Common to both:**
366+
291367
5. The cocotb scheduler forces new digital values onto Verilog and advances
292368
digital time by the actual elapsed SPICE time via `await Timer(...)`.
293-
6. When the `@resume` function returns, the ngspice thread resumes.
369+
6. When the `@resume` function returns, the simulator thread resumes.
294370

295371
This is event-driven: sync only happens when analog outputs actually cross
296372
a digital threshold, or at the fallback ceiling interval.
297373

298374
### Netlist augmentation
299375

300-
The bridge auto-generates a wrapper SPICE deck around the user's subcircuit:
301-
- Digital input pins become `EXTERNAL` voltage sources (ngspice calls
302-
`GetVSRCData` to read their values)
303-
- Analog inputs also use `EXTERNAL` sources so they can be changed at runtime
304-
- Output nodes are probed via `.save` directives
305-
- Power supplies are added automatically
376+
The bridge auto-generates a wrapper SPICE deck around the user's subcircuit,
377+
with simulator-specific syntax:
378+
379+
| Feature | ngspice | Xyce |
380+
|---------|---------|------|
381+
| Runtime sources | `v_name node 0 dc 0 external` | `YDAC v_name DAC node 0` |
382+
| Output probing | `.save v(node)` | `.PRINT TRAN v(node)` |
383+
| Transient analysis | `.tran step stop uic` | `.TRAN step stop` |
384+
| End marker | `.end` | `.END` |
385+
386+
Power supplies are standard DC sources in both formats.
306387

307388
### Vector name normalization
308389

309390
ngspice may report vector names with plot prefixes (e.g., `tran1.v(d0)`) or
310391
wrapped in `v()`. The bridge normalizes lookups so you can query by bare node
311-
name (`d0`), `v(d0)`, or the full qualified name.
392+
name (`d0`), `v(d0)`, or the full qualified name. Xyce stores both the
393+
expression form and bare name.
312394

313395
## Troubleshooting
314396

315-
### ngspice not found
397+
### Simulator library not found
316398

317399
```
318-
FileNotFoundError: Cannot find libngspice.so
400+
FileNotFoundError: Cannot find libngspice shared library.
319401
```
320402

321-
Install the ngspice shared library for your platform (see
322-
[Installing ngspice](#installing-ngspice) above). If the library is installed
403+
Install the ngspice or Xyce shared library for your platform (see
404+
[Prerequisites](#prerequisites) above). If the library is installed
323405
in a non-standard location, pass the path explicitly:
324406

325407
```python
326-
bridge = MixedSignalBridge(dut, blocks, ngspice_lib="/path/to/libngspice.so")
408+
# ngspice
409+
bridge = MixedSignalBridge(dut, blocks, simulator_lib="/path/to/libngspice.so")
410+
411+
# Xyce
412+
bridge = MixedSignalBridge(dut, blocks, simulator_lib="/path/to/libxycecinterface.so")
327413
```
328414

329415
### Signal not found
@@ -354,8 +440,9 @@ If the simulation appears to hang, check:
354440
updates. Check your cocotb log output.
355441
2. **Too-tight sync interval:** Very small `max_sync_interval_ns` values
356442
(< 1ns) can make the simulation extremely slow. Start with 50-100ns.
357-
3. **ngspice convergence:** Complex SPICE circuits may fail to converge.
358-
Check the cocotb log for `ngspice: stderr` warnings.
443+
3. **Simulator convergence:** Complex SPICE circuits may fail to converge.
444+
Check the cocotb log for `ngspice: stderr` warnings (ngspice) or
445+
Xyce error messages.
359446

360447
### Debugging sync behavior
361448

0 commit comments

Comments
 (0)