mfiumara/spice-ts

TypeScript-native SPICE circuit simulator

24

stars

326

commits

TypeScript

primary language

Jun 10, 2026

updated

spice-ts.vercel.app

README

spice-ts

CI codecov npm @spice-ts/core npm @spice-ts/ui GitHub stars

A TypeScript-native SPICE circuit simulator. Parse netlists and run DC, transient, and AC analysis directly in Node.js or the browser with the built-in solver, or swap to an optional ngspice WASM backend.

Live Showcase — interactive demos with streaming parametric sweeps running entirely in your browser.

import { simulate } from '@spice-ts/core';

const result = await simulate(`
  V1 1 0 DC 5
  R1 1 2 1k
  R2 2 0 1k
  .op
  .end
`);

console.log(result.dc?.voltage('2')); // 2.5

Install

npm install @spice-ts/core

Requires Node.js ≥ 20.

Features

  • DC operating point — Newton-Raphson with voltage limiting for convergence
  • DC sweep.dc transfer curves and I-V characteristics
  • Transient analysis — backward Euler and trapezoidal integration with adaptive timestep
  • AC small-signal — frequency sweep (dec/oct/lin) via complex LU solve
  • Device models — R, C, L, V, I, Diode (Shockley), BJT (Ebers-Moll NPN/PNP), MOSFET (Level 1 Shichman-Hodges NMOS/PMOS), BSIM3v3 (LEVEL=49)
  • Passive parasitics — capacitor ESR/ESL/leakage and inductor DCR/core-loss/winding-capacitance expansion for AC and transient analysis
  • Controlled sources — VCVS (E), VCCS (G), CCVS (H), CCCS (F) with DC, AC, and subcircuit support
  • Sparse solver — Gilbert-Peierls LU with symbolic/numeric split, typed-array stamping, batch MOSFET evaluation. Competitive with ngspice-WASM on DC, AC, and nonlinear circuits
  • Complex AC solver — native complex sparse LU (no 2n×2n real expansion)
  • Subcircuits.subckt/.ends definitions with X device instantiation, nested expansion, parameterized subcircuits
  • Library support.include file resolution, .lib/.endl section selection (process corners), .param expressions with SI suffixes
  • Async parsingparseAsync() with platform-agnostic IncludeResolver callback for loading external files
  • Streaming APIsimulateStream() yields results as an AsyncIterableIterator
  • Swappable simulator backend — use the default TypeScript solver or select optional ngspice-wasm via simulate(..., { simulator })
  • Programmatic API — build circuits in code with Circuit, or parse SPICE netlists with parse()
  • Typed errorsConvergenceError, SingularMatrixError, ParseError, CycleError, and more
  • Full TSDoc — every public API export has JSDoc comments for IDE hover-docs

Usage

Netlist (SPICE format)

import { simulate } from '@spice-ts/core';

// RC low-pass filter — transient step response
const result = await simulate(`
  V1 in 0 PULSE(0 5 0 1n 1n 500u 1m)
  R1 in out 1k
  C1 out 0 1u
  .tran 10u 5m
  .end
`);

const { time, voltage } = result.transient!;
// time: Float64Array of timepoints
// voltage('out'): Float64Array of node voltages

Programmatic (no netlist)

import { Circuit, simulate } from '@spice-ts/core';

const ckt = new Circuit();
ckt.addVoltageSource('V1', 'in', '0', { dc: 5 });
ckt.addResistor('R1', 'in', 'out', 1000);
ckt.addResistor('R2', 'out', '0', 1000);
ckt.addAnalysis('op');

const result = await simulate(ckt);
console.log(result.dc?.voltage('out')); // 2.5

Passive Parasitics

Capacitor and inductor instances can carry model or instance parameters. The native solver expands these into equivalent primitive networks before AC and transient analysis.

.model CLOSS C(CAP=1u ESR=250m ESL=10n RLEAK=1meg)
C1 in 0 CLOSS

.model LLOSS L(IND=10u RSER=400m RPAR=2k CPAR=3p)
L1 out 0 LLOSS

Capacitors support CAP, geometry/temperature parameters, ESR/RSER/RS, ESL/LSER/LS, RLEAK/RPAR, and loss-derived DF or Q with FREQ. Inductors support IND/L, geometry/temperature parameters, RSER/DCR/RDC, RPAR, CPAR, Q with FREQ, and SRF-derived parallel capacitance.

Swappable Backend

The TypeScript solver is used by default. To run through ngspice compiled to WASM, install eecircuit-engine and pass simulator: 'ngspice-wasm':

import { simulate } from '@spice-ts/core';

const result = await simulate(`
  V1 1 0 DC 5
  R1 1 2 1k
  R2 2 0 2k
  .op
`, { simulator: 'ngspice-wasm' });

You can also pass a custom simulator adapter implementing simulate(input, options).

Streaming

import { simulateStream } from '@spice-ts/core';

for await (const step of simulateStream(netlist)) {
  if ('time' in step) {
    console.log(`t=${step.time}  V(out)=${step.voltages.get('out')}`);
  }
}

AC analysis

const result = await simulate(`
  V1 in 0 AC 1
  R1 in out 1k
  C1 out 0 1u
  .ac dec 10 1 100k
  .end
`);

const freq = result.ac!.frequencies;          // Float64Array
const mag  = result.ac!.magnitude('out');     // Float64Array
const phase = result.ac!.phase('out');        // Float64Array (degrees)

API

simulate(input, options?)

simulate(input: string | Circuit, options?: SimulationOptions): Promise<SimulationResult>

Runs all analyses declared in the netlist/circuit. Returns a SimulationResult with .dc, .transient, and .ac fields (populated for each analysis present).

simulateStream(input, options?)

simulateStream(input: string | Circuit, options?: SimulationOptions): AsyncIterableIterator<TransientStep | ACPoint>

Yields each timestep/frequency point as it is computed.

parse(netlist)

parse(netlist: string): Circuit

Parses a SPICE netlist into a Circuit object without running any analysis. Throws ParseError if the netlist contains .include or .lib directives — use parseAsync() for those.

parseAsync(netlist, resolver?)

parseAsync(netlist: string, resolver?: IncludeResolver): Promise<Circuit>

Async variant of parse() that runs the preprocessor first, resolving .include, .lib/.endl, and .param directives. The optional resolver callback loads external files:

import { parseAsync } from '@spice-ts/core';
import { readFile } from 'fs/promises';

const ckt = await parseAsync(netlist, async (path) => {
  return readFile(path, 'utf-8');
});

The resolver is platform-agnostic — use fetch() in the browser, readFile() in Node, or return bundled strings for static assets.

SimulationOptions

OptionDefaultDescription
abstol1e-12Absolute current tolerance (A)
vntol1e-6Absolute voltage tolerance (V)
reltol1e-3Relative tolerance
maxIterations100Max Newton-Raphson iterations (DC)
maxTransientIterations50Max NR iterations per timestep
integrationMethod'trapezoidal''euler' or 'trapezoidal'
simulator'spice-ts'Built-in TypeScript solver, 'ngspice-wasm', or a custom simulator adapter
resolveIncludeAsync callback (path: string) => Promise<string> for .include/.lib

Benchmarks

Three-way comparison: spice-ts (pure TypeScript) vs eecircuit (ngspice compiled to WASM via EEcircuit-engine) vs ngspice (native C). Measured on Apple M4 Pro, Node.js v24. Run pnpm bench:compare to reproduce.

DC Operating Point

spice-ts uses a sparse LU solver (Gilbert-Peierls with symbolic/numeric split) and typed-array direct stamping. For DC analysis, it beats ngspice-WASM across all sizes:

CircuitSizespice-tseecircuit (WASM)ngspice (native)vs eecircuit
Resistor ladder100.16 ms0.9 ms0.9 ms5.5x faster
Resistor ladder1001.2 ms1.4 ms0.5 ms1.2x faster
Resistor ladder5002.2 ms4.0 ms0.7 ms1.8x faster

Transient

CircuitSizespice-tseecircuit (WASM)ngspice (native)vs eecircuit
RC chain105.1 ms4.2 ms1.6 ms1.2x slower
RC chain5014.7 ms17.1 ms4.0 ms1.2x faster
RC chain10025.9 ms21.0 ms7.0 ms1.2x slower
LC ladder1010.7 ms10.6 ms3.7 ms~parity
LC ladder5041.6 ms35.9 ms11.2 ms1.2x slower

AC Small-Signal

spice-ts uses a native complex sparse LU solver — no 2n×2n real expansion. Faster than WASM across all sizes:

CircuitSizespice-tseecircuit (WASM)ngspice (native)vs eecircuit
RC chain100.46 ms1.4 ms0.5 ms3.1x faster
RC chain501.7 ms4.1 ms0.7 ms2.4x faster
RC chain1003.3 ms5.8 ms1.1 ms1.8x faster

Nonlinear (CMOS / Ring Oscillators)

Batch MOSFET evaluation bypasses polymorphic dispatch — all transistors in a model group are evaluated in one tight loop with direct array writes:

Circuitspice-tseecircuit (WASM)ngspice (native)vs eecircuit
CMOS inv chain (5 stg)13.2 ms16.9 ms8.5 ms1.3x faster
CMOS inv chain (10 stg)21.7 ms28.4 ms13.3 ms1.3x faster
Ring oscillator (3 stg)25.7 ms29.8 ms14.5 ms1.2x faster
Ring oscillator (5 stg)37.6 ms40.5 ms20.8 ms1.1x faster
Ring oscillator (11 stg)84.3 ms69.8 ms37.4 ms1.2x slower

Where spice-ts shines: DC (up to 5x faster than WASM), AC (1.6-3x faster via native complex sparse solver), nonlinear CMOS (1.1-1.3x faster), and small-medium transient (parity). The default solver has no native binary, no WASM startup, and no process spawn. The optional ngspice-WASM backend is available when compatibility matters more than startup cost.

Accuracy

TestMetricspice-tsExpectedError
RC step responseV(out) at t=τ3.151 V3.161 V0.29%
BJT CE amplifierV(base) DC bias2.048 V2.105 V2.7%
RLC bandpasspeak frequency1585 Hz1592 Hz0.4%
RC ladder 5-stagef-3dB12.76 Hz12.73 Hz0.23%
RLC resonance (transient)oscillation frequency1579 Hz1592 Hz0.8%†

†RLC transient resonance error is due to numerical damping in the trapezoidal integrator with the default timestep. Use a finer timestep or integrationMethod: 'euler' to reduce it.

Run pnpm bench:accuracy to see the full accuracy report including SPICE3 Quarles reference circuits.

Limitations

  • Large ring oscillators: The 11-stage ring oscillator is 1.2x slower than ngspice-WASM. This is near the fundamental TypeScript-vs-compiled-C overhead floor.
  • BSIM3v3 MOSFET: Supported alongside Level 1 Shichman-Hodges. BSIM4, EKV not yet available.

Development

git clone https://github.com/mfiumara/spice-ts
cd spice-ts
pnpm install
pnpm test              # run all tests
pnpm build             # build @spice-ts/core
pnpm bench             # vitest bench (ops/sec, mean, p99 — no external deps)
pnpm bench:accuracy    # accuracy vs analytical + ngspice diff (requires ngspice)
pnpm bench:compare     # 3-way comparison: spice-ts vs eecircuit (WASM) vs ngspice

The bench script uses vitest bench (backed by tinybench) for statistically sound ops/sec and latency metrics. The bench:compare script runs 16 circuits through all three engines and outputs a markdown table. Pass --json for machine-readable output or --no-ngspice to skip native ngspice.

See ROADMAP.md for planned features.

License

MIT

Contributors

mfiumara

323 commits

claude

2 commits

mfiumara/spice-ts

TypeScript-native SPICE circuit simulator

24

stars

326

commits

TypeScript

primary language

Jun 10, 2026

updated

spice-ts.vercel.app

README

spice-ts

CI codecov npm @spice-ts/core npm @spice-ts/ui GitHub stars

A TypeScript-native SPICE circuit simulator. Parse netlists and run DC, transient, and AC analysis directly in Node.js or the browser with the built-in solver, or swap to an optional ngspice WASM backend.

Live Showcase — interactive demos with streaming parametric sweeps running entirely in your browser.

import { simulate } from '@spice-ts/core';

const result = await simulate(`
  V1 1 0 DC 5
  R1 1 2 1k
  R2 2 0 1k
  .op
  .end
`);

console.log(result.dc?.voltage('2')); // 2.5

Install

npm install @spice-ts/core

Requires Node.js ≥ 20.

Features

  • DC operating point — Newton-Raphson with voltage limiting for convergence
  • DC sweep.dc transfer curves and I-V characteristics
  • Transient analysis — backward Euler and trapezoidal integration with adaptive timestep
  • AC small-signal — frequency sweep (dec/oct/lin) via complex LU solve
  • Device models — R, C, L, V, I, Diode (Shockley), BJT (Ebers-Moll NPN/PNP), MOSFET (Level 1 Shichman-Hodges NMOS/PMOS), BSIM3v3 (LEVEL=49)
  • Passive parasitics — capacitor ESR/ESL/leakage and inductor DCR/core-loss/winding-capacitance expansion for AC and transient analysis
  • Controlled sources — VCVS (E), VCCS (G), CCVS (H), CCCS (F) with DC, AC, and subcircuit support
  • Sparse solver — Gilbert-Peierls LU with symbolic/numeric split, typed-array stamping, batch MOSFET evaluation. Competitive with ngspice-WASM on DC, AC, and nonlinear circuits
  • Complex AC solver — native complex sparse LU (no 2n×2n real expansion)
  • Subcircuits.subckt/.ends definitions with X device instantiation, nested expansion, parameterized subcircuits
  • Library support.include file resolution, .lib/.endl section selection (process corners), .param expressions with SI suffixes
  • Async parsingparseAsync() with platform-agnostic IncludeResolver callback for loading external files
  • Streaming APIsimulateStream() yields results as an AsyncIterableIterator
  • Swappable simulator backend — use the default TypeScript solver or select optional ngspice-wasm via simulate(..., { simulator })
  • Programmatic API — build circuits in code with Circuit, or parse SPICE netlists with parse()
  • Typed errorsConvergenceError, SingularMatrixError, ParseError, CycleError, and more
  • Full TSDoc — every public API export has JSDoc comments for IDE hover-docs

Usage

Netlist (SPICE format)

import { simulate } from '@spice-ts/core';

// RC low-pass filter — transient step response
const result = await simulate(`
  V1 in 0 PULSE(0 5 0 1n 1n 500u 1m)
  R1 in out 1k
  C1 out 0 1u
  .tran 10u 5m
  .end
`);

const { time, voltage } = result.transient!;
// time: Float64Array of timepoints
// voltage('out'): Float64Array of node voltages

Programmatic (no netlist)

import { Circuit, simulate } from '@spice-ts/core';

const ckt = new Circuit();
ckt.addVoltageSource('V1', 'in', '0', { dc: 5 });
ckt.addResistor('R1', 'in', 'out', 1000);
ckt.addResistor('R2', 'out', '0', 1000);
ckt.addAnalysis('op');

const result = await simulate(ckt);
console.log(result.dc?.voltage('out')); // 2.5

Passive Parasitics

Capacitor and inductor instances can carry model or instance parameters. The native solver expands these into equivalent primitive networks before AC and transient analysis.

.model CLOSS C(CAP=1u ESR=250m ESL=10n RLEAK=1meg)
C1 in 0 CLOSS

.model LLOSS L(IND=10u RSER=400m RPAR=2k CPAR=3p)
L1 out 0 LLOSS

Capacitors support CAP, geometry/temperature parameters, ESR/RSER/RS, ESL/LSER/LS, RLEAK/RPAR, and loss-derived DF or Q with FREQ. Inductors support IND/L, geometry/temperature parameters, RSER/DCR/RDC, RPAR, CPAR, Q with FREQ, and SRF-derived parallel capacitance.

Swappable Backend

The TypeScript solver is used by default. To run through ngspice compiled to WASM, install eecircuit-engine and pass simulator: 'ngspice-wasm':

import { simulate } from '@spice-ts/core';

const result = await simulate(`
  V1 1 0 DC 5
  R1 1 2 1k
  R2 2 0 2k
  .op
`, { simulator: 'ngspice-wasm' });

You can also pass a custom simulator adapter implementing simulate(input, options).

Streaming

import { simulateStream } from '@spice-ts/core';

for await (const step of simulateStream(netlist)) {
  if ('time' in step) {
    console.log(`t=${step.time}  V(out)=${step.voltages.get('out')}`);
  }
}

AC analysis

const result = await simulate(`
  V1 in 0 AC 1
  R1 in out 1k
  C1 out 0 1u
  .ac dec 10 1 100k
  .end
`);

const freq = result.ac!.frequencies;          // Float64Array
const mag  = result.ac!.magnitude('out');     // Float64Array
const phase = result.ac!.phase('out');        // Float64Array (degrees)

API

simulate(input, options?)

simulate(input: string | Circuit, options?: SimulationOptions): Promise<SimulationResult>

Runs all analyses declared in the netlist/circuit. Returns a SimulationResult with .dc, .transient, and .ac fields (populated for each analysis present).

simulateStream(input, options?)

simulateStream(input: string | Circuit, options?: SimulationOptions): AsyncIterableIterator<TransientStep | ACPoint>

Yields each timestep/frequency point as it is computed.

parse(netlist)

parse(netlist: string): Circuit

Parses a SPICE netlist into a Circuit object without running any analysis. Throws ParseError if the netlist contains .include or .lib directives — use parseAsync() for those.

parseAsync(netlist, resolver?)

parseAsync(netlist: string, resolver?: IncludeResolver): Promise<Circuit>

Async variant of parse() that runs the preprocessor first, resolving .include, .lib/.endl, and .param directives. The optional resolver callback loads external files:

import { parseAsync } from '@spice-ts/core';
import { readFile } from 'fs/promises';

const ckt = await parseAsync(netlist, async (path) => {
  return readFile(path, 'utf-8');
});

The resolver is platform-agnostic — use fetch() in the browser, readFile() in Node, or return bundled strings for static assets.

SimulationOptions

OptionDefaultDescription
abstol1e-12Absolute current tolerance (A)
vntol1e-6Absolute voltage tolerance (V)
reltol1e-3Relative tolerance
maxIterations100Max Newton-Raphson iterations (DC)
maxTransientIterations50Max NR iterations per timestep
integrationMethod'trapezoidal''euler' or 'trapezoidal'
simulator'spice-ts'Built-in TypeScript solver, 'ngspice-wasm', or a custom simulator adapter
resolveIncludeAsync callback (path: string) => Promise<string> for .include/.lib

Benchmarks

Three-way comparison: spice-ts (pure TypeScript) vs eecircuit (ngspice compiled to WASM via EEcircuit-engine) vs ngspice (native C). Measured on Apple M4 Pro, Node.js v24. Run pnpm bench:compare to reproduce.

DC Operating Point

spice-ts uses a sparse LU solver (Gilbert-Peierls with symbolic/numeric split) and typed-array direct stamping. For DC analysis, it beats ngspice-WASM across all sizes:

CircuitSizespice-tseecircuit (WASM)ngspice (native)vs eecircuit
Resistor ladder100.16 ms0.9 ms0.9 ms5.5x faster
Resistor ladder1001.2 ms1.4 ms0.5 ms1.2x faster
Resistor ladder5002.2 ms4.0 ms0.7 ms1.8x faster

Transient

CircuitSizespice-tseecircuit (WASM)ngspice (native)vs eecircuit
RC chain105.1 ms4.2 ms1.6 ms1.2x slower
RC chain5014.7 ms17.1 ms4.0 ms1.2x faster
RC chain10025.9 ms21.0 ms7.0 ms1.2x slower
LC ladder1010.7 ms10.6 ms3.7 ms~parity
LC ladder5041.6 ms35.9 ms11.2 ms1.2x slower

AC Small-Signal

spice-ts uses a native complex sparse LU solver — no 2n×2n real expansion. Faster than WASM across all sizes:

CircuitSizespice-tseecircuit (WASM)ngspice (native)vs eecircuit
RC chain100.46 ms1.4 ms0.5 ms3.1x faster
RC chain501.7 ms4.1 ms0.7 ms2.4x faster
RC chain1003.3 ms5.8 ms1.1 ms1.8x faster

Nonlinear (CMOS / Ring Oscillators)

Batch MOSFET evaluation bypasses polymorphic dispatch — all transistors in a model group are evaluated in one tight loop with direct array writes:

Circuitspice-tseecircuit (WASM)ngspice (native)vs eecircuit
CMOS inv chain (5 stg)13.2 ms16.9 ms8.5 ms1.3x faster
CMOS inv chain (10 stg)21.7 ms28.4 ms13.3 ms1.3x faster
Ring oscillator (3 stg)25.7 ms29.8 ms14.5 ms1.2x faster
Ring oscillator (5 stg)37.6 ms40.5 ms20.8 ms1.1x faster
Ring oscillator (11 stg)84.3 ms69.8 ms37.4 ms1.2x slower

Where spice-ts shines: DC (up to 5x faster than WASM), AC (1.6-3x faster via native complex sparse solver), nonlinear CMOS (1.1-1.3x faster), and small-medium transient (parity). The default solver has no native binary, no WASM startup, and no process spawn. The optional ngspice-WASM backend is available when compatibility matters more than startup cost.

Accuracy

TestMetricspice-tsExpectedError
RC step responseV(out) at t=τ3.151 V3.161 V0.29%
BJT CE amplifierV(base) DC bias2.048 V2.105 V2.7%
RLC bandpasspeak frequency1585 Hz1592 Hz0.4%
RC ladder 5-stagef-3dB12.76 Hz12.73 Hz0.23%
RLC resonance (transient)oscillation frequency1579 Hz1592 Hz0.8%†

†RLC transient resonance error is due to numerical damping in the trapezoidal integrator with the default timestep. Use a finer timestep or integrationMethod: 'euler' to reduce it.

Run pnpm bench:accuracy to see the full accuracy report including SPICE3 Quarles reference circuits.

Limitations

  • Large ring oscillators: The 11-stage ring oscillator is 1.2x slower than ngspice-WASM. This is near the fundamental TypeScript-vs-compiled-C overhead floor.
  • BSIM3v3 MOSFET: Supported alongside Level 1 Shichman-Hodges. BSIM4, EKV not yet available.

Development

git clone https://github.com/mfiumara/spice-ts
cd spice-ts
pnpm install
pnpm test              # run all tests
pnpm build             # build @spice-ts/core
pnpm bench             # vitest bench (ops/sec, mean, p99 — no external deps)
pnpm bench:accuracy    # accuracy vs analytical + ngspice diff (requires ngspice)
pnpm bench:compare     # 3-way comparison: spice-ts vs eecircuit (WASM) vs ngspice

The bench script uses vitest bench (backed by tinybench) for statistically sound ops/sec and latency metrics. The bench:compare script runs 16 circuits through all three engines and outputs a markdown table. Pass --json for machine-readable output or --no-ngspice to skip native ngspice.

See ROADMAP.md for planned features.

License

MIT

Contributors

mfiumara

323 commits

claude

2 commits

Languages

TypeScript

100.0%