velxio/docs/wiki/circuit-emulation-ngspice.md

303 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ngspice-WASM Integration
Location: [`test/test_circuit/src/spice/`](../../test/test_circuit/src/spice/)
## Why `eecircuit-engine`
When the user pushed back on "no quiero inventar nada raro", we surveyed the landscape:
| Package | Status 2026-04 | Decision |
|---|---|---|
| [`eecircuit-engine`](https://www.npmjs.com/package/eecircuit-engine) 1.7.0 | Active, published npm | **Chosen** |
| [`tscircuit/ngspice`](https://github.com/tscircuit/ngspice) | GitHub only, not on npm | Deferred (requires fork+publish) |
| [ngspice.js](https://ngspice.js.org/) | Demo site, no npm API | Declined |
| [SpiceJS](https://www.spicejs.org/) | Web UI, no programmable API | Declined |
| `webspice` on npm | Unrelated (NASA observation geometry) | Declined |
| `vollgas` | Pure-digital logic-gate sim | Declined |
`eecircuit-engine` is ngspice compiled with Emscripten to WebAssembly + a thin TypeScript wrapper. It is published by `eelab-dev`, used as the engine behind [eecircuit.com](https://eecircuit.com/). MIT licensed, has TypeScript types.
## Installation
```bash
cd test/test_circuit
npm install eecircuit-engine
```
Resulting size on disk:
```
node_modules/eecircuit-engine/
├── LICENSE
├── README.md
├── dist/
│ ├── eecircuit-engine.mjs ← ESM entry
│ ├── eecircuit-engine.umd.js ← UMD bundle
│ ├── main.d.ts ← TypeScript types
│ └── (WASM binary embedded as base64 inside the JS bundle)
└── package.json
Total: 39 MB
```
Yes, 39 MB is large. In Velxio production this must be **lazy-loaded**: the user hits a "⚡ Electrical simulation" toggle, and only then is the bundle fetched. The size trade-off is documented in [performance](circuit-emulation-performance.md).
## API surface (from `dist/main.d.ts`)
```typescript
export declare class Simulation {
start: () => Promise<void>;
setNetList: (input: string) => void;
runSim: () => Promise<ResultType>;
getInfo: () => string;
getInitInfo: () => string;
getError: () => string[];
isInitialized: () => boolean;
}
export declare type ResultType = {
header: string;
numVariables: number;
variableNames: string[];
numPoints: number;
dataType: "real";
data: RealDataType[]; // data[i].values: number[]
} | {
header: string;
numVariables: number;
variableNames: string[];
numPoints: number;
dataType: "complex";
data: ComplexDataType[]; // data[i].values: {real, img}[]
};
```
That's the entire public API. Everything else — analyses, models, components — is expressed through the netlist string.
## The wrapper
[`src/spice/SpiceEngine.js`](../../test/test_circuit/src/spice/SpiceEngine.js) wraps this with conveniences:
```javascript
import { runNetlist, NL } from '../src/spice/SpiceEngine.js';
// Simple DC
const { dcValue } = await runNetlist(`VDIV
V1 vcc 0 DC 9
R1 vcc out 1k
R2 out 0 2k
.op
.end`);
console.log(dcValue('v(out)')); // 6
// Transient: vec(name) returns array
const { vec } = await runNetlist(`
V1 in 0 PULSE(0 5 0 1n 1n 10m 20m)
R1 in out 10k
C1 out 0 100u IC=0
.tran 100u 30m
.end`);
const time = vec('time');
const vout = vec('v(out)');
// AC sweep: vec returns complex
const { vec: vecAC } = await runNetlist(`
V1 in 0 AC 1
R1 in out 1k
C1 out 0 1u
.ac dec 20 10 1Meg
.end`);
const freq = vecAC('frequency'); // [{real, img}] — img is 0 for freq
const vout = vecAC('v(out)'); // [{real, img}]
const mag_dB = vout.map(c => 20*Math.log10(Math.sqrt(c.real**2 + c.img**2)));
```
Key design decisions:
- **Singleton engine**. Booting takes 400 ms; we boot once per test process.
- **`findVar(name)`** accepts either `"v(node)"` or `"node"` — ngspice typically uses the former for voltages and `time` / `frequency` for the sweep variables.
- **Case-insensitive**. SPICE is historically case-preserving-but-insensitive; ngspice tends to lowercase node names.
- **`NL.pulse / NL.sin / NL.pwl`** helpers format the common source cards correctly.
## Netlist syntax (ngspice)
This section summarizes the SPICE cards we validated. For the full reference, see the [ngspice manual](https://ngspice.sourceforge.io/docs.html).
### Passive
```spice
R1 node1 node2 1k ; resistor in Ω (k=1e3, Meg=1e6, u=1e-6, n=1e-9, p=1e-12)
C1 node1 node2 100u IC=0 ; capacitor, optional initial condition
L1 node1 node2 10m ; inductor
V1 plus minus DC 5 ; voltage source
I1 from to DC 1m ; current source
```
### Sources
```spice
* Pulse: V1 P N PULSE(V_init V_final T_delay T_rise T_fall T_pulse_width T_period)
Vclk clk 0 PULSE(0 5 0 10n 10n 1u 2u)
* Sinusoidal: V1 P N SIN(V_offset V_amp freq)
Vosc osc 0 SIN(0 1 1k)
* Piecewise linear: V1 P N PWL(t0 v0 t1 v1 t2 v2 ...)
Vsig sig 0 PWL(0 0 1m 5 2m 5 3m 0)
* For AC analysis: add "AC amp" on the source card
V1 in 0 AC 1 ; 1V AC small-signal
V1 in 0 DC 0 AC 1 ; DC bias + AC probe
```
### Active devices
```spice
* Diode
D1 anode cathode DMOD
.model DMOD D(Is=1e-14 N=1)
* ngspice ships common models; for 1N4148:
.model D1N4148 D(Is=2.52n N=1.752 Rs=0.568 Ibv=0.1u Bv=100)
* BJT (npn/pnp)
Q1 collector base emitter Q2N2222
.model Q2N2222 NPN(Is=1e-14 Bf=200 Vaf=75)
* MOSFET (L1 Shichman-Hodges)
M1 drain gate source bulk NMOS_L1 L=1u W=100u
.model NMOS_L1 NMOS(Level=1 Vto=1.0 Kp=50u Lambda=0.01)
```
### Controlled sources (ideal op-amps etc.)
```spice
* Voltage-controlled voltage source (VCVS)
Eopa out 0 inp_plus inp_minus 1e6 ; V_out = 1e6·(V(inp_plus) - V(inp_minus))
* Voltage-controlled current source (VCCS)
Gx out 0 inp_plus inp_minus 1m ; I_out = 1m·ΔV
* Current-controlled voltage source (CCVS): needs a voltage source to sense current
* H1 out 0 V_sense 100
* Current-controlled current source (CCCS): F1 out 0 V_sense 100
```
### Behavioral (B-source) — the mixed-signal key
`B` sources compute a voltage (or current) from an arbitrary expression of node voltages and other quantities:
```spice
B1 out 0 V = 5 * u(V(a) - 2.5) ; simple inverter
B2 and 0 V = 5 * u(V(a)-2.5) * u(V(b)-2.5) ; AND
B3 xor 0 V = 5 * (u(V(a)-2.5) + u(V(b)-2.5) - 2*u(V(a)-2.5)*u(V(b)-2.5))
```
Supported functions include `u(x)` (Heaviside step), `sin`, `cos`, `exp`, `log`, `sqrt`, `abs`, `min`, `max`, `limit(x, lo, hi)`, ternary `a?b:c` — see the ngspice manual for the full list.
**Important gotchas** (documented separately in [gotchas](circuit-emulation-gotchas.md)):
- Use `&&` / `||` for logical (not `&` / `|`, which are bitwise and behave differently in B-sources).
- `u(x-threshold)` is the most portable way to get digital-like behaviour.
### Voltage-controlled switches (stateful)
```spice
S1 out 0 ctrl 0 SMOD
.model SMOD SW(Vt=2.5 Vh=0.833 Ron=100 Roff=1G)
```
`Vt` is the threshold, `Vh` is the hysteresis half-width: switch turns ON when `V(ctrl) > Vt+Vh`, OFF when `V(ctrl) < VtVh`. **Between** the thresholds the switch retains its state. This is the only stateful element we found in the current `eecircuit-engine` build that does not require XSPICE. Essential for building relaxation oscillators / latches behaviorally.
### Analyses
```spice
.op ; DC operating point
.tran tstep tstop ; transient; e.g. .tran 10u 5m
.tran tstep tstop tstart ; transient with start time
.tran tstep tstop tstart tmax ; with max step
.ac DEC ppd fstart fstop ; AC sweep (decade, points-per-decade)
.dc Vsrc vstart vstop vstep ; DC sweep
.noise v(n1,n2) Vsrc DEC ...; noise analysis (not validated here)
```
Every netlist must end with `.end`.
## Result parsing
```javascript
const result = await sim.runSim();
// result.dataType: "real" | "complex"
// result.variableNames: string[] e.g. ["time", "v(in)", "v(out)"]
// result.data: { name, type, values }[]
```
For `.op`: `numPoints=1`, `values[0]` is the single operating-point value.
For `.tran`: `values` is the time series, and `variableNames[0]` is usually `"time"`.
For `.ac`: `dataType='complex'`, `variableNames[0]` is `"frequency"`, and voltage values are `{real, img}`.
Our wrapper's `dcValue(name)` just returns `vec(name)[0]`; for `.op` that is the operating point.
## Lessons learned (ngspice warnings)
ngspice emits informational and warning messages on stderr. We see them in test output:
```
Note: v1: has no value, DC 0 assumed ; harmless — you declared V1 with only AC amp
Warning: singular matrix: check node n ; FIX: add a pull to ground on the floating node
Note: Starting dynamic gmin stepping ; recovery strategy kicking in
Warning: Dynamic gmin stepping failed ; still singular
Note: Starting source stepping ; last-ditch recovery
Warning: source stepping failed ; now we're in trouble
Note: Transient op started ; gave up on DC, jumping to t=0
Note: Transient op finished successfully ; got it eventually
```
The `Warning: singular matrix` path can cost 60+ seconds. Always ensure every node has a DC path to ground — add a `R_pull node 0 1Meg` if necessary (documented in [gotchas](circuit-emulation-gotchas.md)).
## Performance
Benchmark numbers on WSL2 / Node 22 / Core i7-12700:
| Task | Time |
|---|---|
| First-time engine boot | ~400 ms |
| `.op` with 3 passive components | 510 ms |
| `.op` with diode | 2030 ms |
| `.op` with BJT | 5080 ms |
| `.tran 3m 1u` (10 passive components) | 100300 ms |
| `.tran 30m 10u` with switches (555-core) | ~400 ms |
| `.tran 40m 0.1m` with 4 diodes (bridge rectifier) | ~300 ms |
| `.ac dec 30 10 1Meg` (RC low-pass) | ~10 ms |
For the sandbox the engine is a singleton, so the 400 ms boot is paid only once per `vitest` process.
## Code example — every analysis in one file
The smoke test is the simplest possible demonstration:
```javascript
import { Simulation } from 'eecircuit-engine';
const sim = new Simulation();
await sim.start();
sim.setNetList(`Voltage divider
V1 vcc 0 DC 9
R1 vcc out 1k
R2 out 0 2k
.op
.end`);
const result = await sim.runSim();
console.log(result.variableNames); // ["v(vcc)", "v(out)", "i(v1)"]
const idx = result.variableNames.indexOf("v(out)");
console.log(result.data[idx].values[0]); // 6
```
That is all.
## Code reviewed here
- [`src/spice/SpiceEngine.js`](../../test/test_circuit/src/spice/SpiceEngine.js) — wrapper + helpers.
- [`src/spice/AVRSpiceBridge.js`](../../test/test_circuit/src/spice/AVRSpiceBridge.js) — quasi-static co-sim (see [AVR bridge](circuit-emulation-avr-bridge.md)).
- [`test/ngspice_smoke.test.js`](../../test/test_circuit/test/ngspice_smoke.test.js) — first working simulation.
- [`test/spice_*.test.js`](../../test/test_circuit/test/) — all 22 ngspice-backed tests.