# 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; setNetList: (input: string) => void; runSim: () => Promise; 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) < Vt−Vh`. **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 | 5–10 ms | | `.op` with diode | 20–30 ms | | `.op` with BJT | 50–80 ms | | `.tran 3m 1u` (10 passive components) | 100–300 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.