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.
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
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
| Piece | Role |
|---|---|
| Auto netlisting | Passives (R/C/L/D references) join the netlist automatically — no per-component opt-ins |
| Rail discovery | Power 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.exclude | Opts a component out of the netlist |
| simulation.model | Emits a .model card, e.g. 'DLED D (IS=1a RS=3.3 N=1.8)' |
| simulation.subckt + library | Instantiates .subckt parts from vendor-style .lib files — include paths are rewritten relative to the netlist |
| simulation.pinOrder | Maps component pins onto model / subckt terminals |
| simulation.params | Instance parameters ('m=2', area factors, W=/L=…) — named forms only: ngspice 44 rejects positional tokens |
| ./build/<name>.cir | The generated SPICE netlist lands next to the other build output |
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:
| Accessor | Reads |
|---|---|
| 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 / getPhaseDeg | Complex (AC) results as magnitude, dB, or phase in degrees |
tran tran(tstep, tstop, tstart?, tmax?, uic?, openPlots?) → NgspiceResult | null
| Parameter | Type | Units / values | Description |
|---|---|---|---|
| tstep | string | time, e.g. 1u | The time step |
| tstop | string | time, e.g. 10m | When the analysis stops |
| tstart | string | time | Optional initial time |
| tmax | string | time | Optional maximum internal step |
| uic | boolean | true / false | Use initial conditions |
| openPlots | boolean | true / false | Pass 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:
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 = truedc dc(source, start, stop, increment) → NgspiceResult | null
| Parameter | Type | Units / values | Description |
|---|---|---|---|
| source | string | source name | A named Power source — new Power({ …, name: 'VS' }); auto sources are V1, V2, … |
| start / stop | number | volts | The sweep bounds |
| increment | number | volts | The 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
| Parameter | Type | Units / values | Description |
|---|---|---|---|
| sweep | string | dec | oct | lin | The sweep type |
| points | number | count | Points per decade / octave / line |
| fstart / fstop | number | Hz | The 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:
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)');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
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 / wattsWhen 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
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`);
}