Simulation

The same code that defines the board can SPICE it. simulate() builds a netlist from the schematic, hands it to ngspice headlessly, and hands you the numbers — operating point, transient, DC sweep, or AC.

method

simulate

Passives are netlisted automatically and power rails are discovered from the Power objects already attached to the circuit — pass Power arguments explicitly to force the sources. Returns a SimulationContext for running an analysis. Needs the ngspice binary in your PATH — one of the optional installs listed in Getting started.

Example first

a 5 V divider, simulated
import { PCB, Resistor, Power } from '@typecad/pcb';

let typecad = new PCB('typecad');

let r1 = new Resistor({ value: '10k', size: '0603' });
let r2 = new Resistor({ value: '10k', size: '0603' });
let vin = new Power({ power: r1.pin(1), gnd: r2.pin(2), voltage: 5 });

typecad.named('in').net(r1.pin(1));
typecad.named('vdiv').net(r1.pin(2), r2.pin(1));
typecad.named('gnd').net(r2.pin(2));
typecad.add(r1, r2);

const result = typecad.simulate().op();
if (result) {
  console.log(result.getVoltage('vdiv'));         // 2.5 — the divider midpoint
  console.log(result.getCurrent(r1.reference));   // 0.00025 A
  console.log(result.getPower(r1.reference));     // 0.000625 W
}

The netlist

PieceRole
Auto netlistingPassives (R/C/L/D references) join the netlist automatically — no per-component opt-ins
Rail discoveryPower objects already in the circuit stamp voltage ratings on their pins; those rails become the sources. Passing Power objects to simulate() explicitly still works and wins
simulation.excludeOpts a component out of the netlist
simulation.modelEmits a .model card, e.g. 'DLED D (IS=1a RS=3.3 N=1.8)'
simulation.subckt + libraryInstantiates .subckt parts from vendor-style .lib files — include paths are rewritten relative to the netlist
simulation.pinOrderMaps component pins onto model / subckt terminals
simulation.paramsInstance parameters ('m=2', area factors, W=/L=…) — named forms only: ngspice 44 rejects positional tokens
./build/<name>.cirThe generated SPICE netlist lands next to the other build output
reference

Analyses

Four analyses, all headless — results come back as a parsed NgspiceResult, not plot windows. Each writes the netlist to ./build/ first, so what ngspice ran is always inspectable.

op op() → NgspiceResult | null

Takes no arguments — it solves the DC operating point of the whole circuit, prints a formatted table (V / mA / mW) to the console, and the returned result answers questions programmatically:

AccessorReads
getVoltage(net)Voltage at a named net — getVoltage('vdiv')
getCurrent(ref)Current through a component, by reference — getCurrent('R1')
getPower(ref)Power dissipated in a component, by reference
get(variable)Any raw result variable by name
getWaveform(name)The full data column — every point of a tran/dc/ac sweep
getAt(name, index)One point of a sweep
getMagnitude / getDb / getPhaseDegComplex (AC) results as magnitude, dB, or phase in degrees

tran tran(tstep, tstop, tstart?, tmax?, uic?, openPlots?) → NgspiceResult | null

ParameterTypeUnits / valuesDescription
tstepstringtime, e.g. 1uThe time step
tstopstringtime, e.g. 10mWhen the analysis stops
tstartstringtimeOptional initial time
tmaxstringtimeOptional maximum internal step
uicbooleantrue / falseUse initial conditions
openPlotsbooleantrue / falsePass true for ngspice’s interactive plot windows — default false (headless)

Transient analysis in batch mode: the waveform comes back parsed, with a final-value summary printed instead of a per-point dump:

terminal
const result = typecad.simulate().tran('1u', '10m');
// headless: returns the parsed waveform — no plot windows, no blocking
const v = result?.getWaveform('v(vdiv)');

typecad.simulate().tran('1u', '10m', 0, undefined, false, true);  // openPlots = true

dc dc(source, start, stop, increment) → NgspiceResult | null

ParameterTypeUnits / valuesDescription
sourcestringsource nameA named Power source — new Power({ …, name: 'VS' }); auto sources are V1, V2, …
start / stopnumbervoltsThe sweep bounds
incrementnumbervoltsThe step

Sweeps a named source. The sweep axis is the v(v-sweep) variable in the result.

ac ac(sweep, points, fstart, fstop) → NgspiceResult | null

ParameterTypeUnits / valuesDescription
sweepstringdec | oct | linThe sweep type
pointsnumbercountPoints per decade / octave / line
fstart / fstopnumberHzThe frequency bounds

Small-signal frequency sweep — give a source a small-signal magnitude with ac:, read complex results back with getMagnitude / getDb / getPhaseDeg for Bode-style checks:

dc sweep + ac analysis
const led = new Power({ power: r1.pin(1), gnd: r2.pin(2), voltage: 5, name: 'VS' });

for (const r of typecad.simulate().dc('VS', 0, 5, 0.25)?.getWaveform('v(vdiv)') ?? []) {
  console.log(r);
}

const ac = typecad.simulate().ac('dec', 20, 10, 1e6);
ac?.getDb('v(vout)');      // Bode-style magnitude
ac?.getPhaseDeg('v(vout)');
workflow

The CLI loop

typecad-pcb simulate solves the operating point for the board viewer: it re-runs the project entry exactly like a build, writes build/<board>_op.json beside the netlist, and the viewer's ngspice view picks it up on the next refresh — trace hover then shows each net's voltage and the connected devices' current and power. Operating-point only; for other analyses use the API.

Example first

terminal
typecad-pcb simulate   # re-runs the entry, solves the OP, writes build/<board>_op.json
# the board viewer's ngspice view reads it — hover a trace for volts / amps / watts
workflow

When it pays off

Simulation runs in your build code, so the checks travel with the design. Assert the divider really divides, the LED current really limits — and fail the build when physics disagrees with intent.

Example first

src/index.ts
const vdiv = typecad.simulate().op()?.getVoltage('vdiv');
if (vdiv !== undefined && Math.abs(vdiv - 2.5) > 0.01) {
  typecad.error(`divider midpoint is ${vdiv} V, expected 2.5 V`);
}
© 2026 typeCAD — code-defined electronics github