Interface with NGspice

../../_images/colorCode.svg

SLiCAP runs NGspice simulations from a Python script and turns the results into traces, measurements and plots. It requires:

  1. NGspice for circuit simulation (NGspice manual); its location is set in the [commands] section of the SLiCAP configuration file (see Installation).

  2. An NGspice schematic (.spice_sch), drawn with the SLiCAP schematic editor and its NGspice symbol library, or a hand-written netlist cir/<name>.cir. See NGspice schematics in the GUI manual.

Supported analysis

One function per NGspice analysis runs the simulation and returns a result object with the simulated vectors under their NGspice names:

  1. op(): operating point analysis

  2. dc(): DC sweep of a source, or of the temperature ("TEMP")

  3. ac(): small-signal frequency-domain analysis

  4. tran(): time-domain analysis, with FOURIER or FFT post-processing

  5. noise(): small-signal frequency-domain noise analysis

All of them accept:

  • step: a parameter step dictionary, {"param": "C_c", "method": "lin", "start": "2p", "stop": "20p", "num": 10}, with method "lin", "log" or "list" ("values": [...]); the temperature is stepped with "param": "TEMP".

  • params: an ordered list of (name, value) parameter definitions for this run only, overriding the values on the schematic.

  • stimuli: another stimulus for an independent source for this run only, e.g. {"V1": ["SIN", 0, "{V_p}", "100k"]}.

Numbers are written in SLiCAP notation ("10M", "2p"), not in NGspice notation (10MEG).

Compatibility mode

Netlists and device libraries written for other simulators use syntax that NGspice reads only in a compatibility mode. Every analysis function has the keyword behavior, which is passed to NGspice as its variable ngbehavior (NGspice manual, section 12.11.1). NGspice reads the keyword by scanning it for the following two-letter flags, so a combination is written as one word: "ltpsa" is lt + ps + a.

Flag

Meaning

ps

PSPICE syntax

lt

LTSPICE syntax

hs

HSPICE syntax (ps and hs are mutually exclusive; NGspice switches to ps)

spe

Spectre syntax

s3

Spice3 behaviour, disables some NGspice extensions

ki

KiCad vector names that contain a slash

eg

EAGLE compatible voltage vector output

a

transform the whole netlist; without it the selected syntax applies to libraries added with .include only

The last flag is the one that matters in practice: a device model written in PSPICE syntax inside the circuit itself needs behavior="psa", and behavior="ps" leaves it untouched. In the schematic editor the flags are check boxes on the NGspice instruction dialog.

Simulator options

Every analysis function has the keyword options: a dictionary of NGspice simulator options (NGspice manual, chapter 11) that holds for this run only. Each entry is written as an option name = value command before the analysis; a value of None writes a flag without a value. Numbers are written in SLiCAP notation.

OP1 = sl.op("myAmp", behavior="psa", options={"rshunt": "1e12"})
TR1 = sl.tran("myAmp", "1n", "1u", options={"method": "gear", "reltol": "1e-4"})

The first line is the usual cure for a vendor macro-model that does not converge. Such models often have internal nodes without a DC path to ground (a node between two zener diodes, or between a current source and an inductor); NGspice then reports singular matrix and gmin stepping, source stepping and the transient operating point all fail. rshunt adds a resistance from every node to ground, gmin ("1e-10") adds a conductance across every pn junction; both give the model an operating point without affecting the results. Loosening reltol or abstol does not help in this case. In the schematic editor the options are a table on the NGspice instruction dialog.

The result objects are post-processed with the functions of the trace model: make_traces() builds traces from expressions over the simulated vectors, measure() reduces them to numbers with goal functions, and plot() plots the traces. NGspice’s own vector names, v(out), i(v2), onoise_spectrum, are not Python identifiers; the keyword variables of these functions maps them onto names of your own.

Important

The schematic editor writes the same function calls into the project’s instruction file (Instruction ‣ Create / edit NGspice instruction…), so everything on this page can be composed in the GUI as well.

Example

The SLiCAP output displayed on this manual page, is generated with the script: ngspice.py, imported by Manual.py.

1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5ngspice.py: SLiCAP scripts for the HTML help file
6"""
7import SLiCAP as sl
../../_images/colorCode.svg

Schematic capture, operating point and netlist generation

The circuit is a two-stage transistor amplifier drawn with the NGspice symbols of the schematic editor. The independent sources carry their stimuli (a dc value, an ac value and a transient waveform), the parameters C_c and V_S are defined in a parameter block, and the transistor model is included from a library file.

 9fileName = "VampQspice"
10
11# Operating point: NGspice writes cir/VampQspice_op.raw, which the schematic
12# export reads for the bias annotations on the schematic
13OP = sl.op(fileName)
14
15# Netlist, schematic image with the bias annotations, and HTML circuit page
16netlist = sl.makeCircuit("sch/" + fileName + ".spice_sch")

op() takes the circuit name, exports the netlist from the schematic when it is newer than the netlist, and writes the operating point to cir/VampQspice_op.raw. makeCircuit() recognizes the NGspice schematic by its extension, exports the netlist, the schematic image and an HTML page with the circuit data, and returns the netlist text. The image carries the operating point annotations: the DC voltages of the nets and the DC currents of the sources for which they were switched on in the schematic editor (see Component properties), read from the most recent unstepped operating-point run. Whenever a new operating point is simulated, the image is exported again.

../../_images/VampQspice.svg

Netlist

 1VampQspice
 2
 3.include "lib/BC847.lib"
 4
 5.param
 6+ C_c={1.8e-11}
 7+ V_S={12}
 8
 9C1 3 inDC 1e-06
10C2 outDC 4 {C_c}
11C3 2 0 0.0002
12C4 outDC out 1e-07
13C5 out 0 1e-10
14Q1 1 inDC 4 BC847
15Q2 outDC 1 2 BC847
16R1 3 5 R=2000 noisy=1
17R2 Supply 1 R=82000 noisy=1
18R3 outDC 4 R=3000 noisy=1
19R4 4 0 R=1000 noisy=1
20R5 Supply outDC R=2700 noisy=1
21R6 2 0 R=1000 noisy=1
22R7 out 0 R=10000 noisy=1
23R8 inDC 2 R=100000 noisy=1
24V1 5 0 dc 0 ac 1 0 PULSE(0 0.3 0 1e-09 1e-09 4.99e-07 1e-06)
25V2 Supply 0 dc {V_S}
26
27.end

Operating point information

Without parameter stepping the result of op() holds one number per NGspice vector, under the NGspice name; with parameter stepping it holds one array per vector.

18# Operating point information: the result holds every saved NGspice vector
19# under its NGspice name; pick the ones of interest under names of your own
20opNames = {"V_c1": "v(1)", "V_b1": "v(indc)", "V_e1": "v(4)",
21           "V_c2": "v(outdc)", "V_e2": "v(2)", "I_V2": "i(v2)"}
22OPinfo  = {name: OP.op[vector] for name, vector in opNames.items()}
23for name in OPinfo.keys():
24    print(name, ":", OPinfo[name])
25
26rst  = sl.RSTformatter()
27head = ["Name", "Value"]
28rst.dictTable(OPinfo, head=head,
29              caption="Bias voltages and currents").save("table-VampQ-opinfo")

This yields:

V_c1 : 2.491854967999738
V_b1 : 1.7636793328341864
V_e1 : 1.1567937843803244
V_c2 : 4.2957257123120804
V_e2 : 1.8125941183950072
I_V2 : -0.0029693879095610215

Typesetted:

Table 21 Bias voltages and currents

Name

Value

V_c1

\(2.492\)

V_b1

\(1.764\)

V_e1

\(1.157\)

V_c2

\(4.296\)

V_e2

\(1.813\)

I_V2

\(-0.002969\)

Currents follow the NGspice sign convention: the current through a voltage source is measured into its positive terminal, so a source that delivers current reads negative.

DC sweep

31# DC sweep of the supply voltage
32DC = sl.dc(fileName, "V2", 6, 12, 1)
33DCtraces = sl.make_traces(DC, [{"y": "V_c2", "label": "$V_{c2}$"},
34                               {"y": "V_e2", "label": "$V_{e2}$"}],
35                          variables={"V_c2": "v(outdc)", "V_e2": "v(2)"})
36sl.plot("VampQspiceDC", "DC voltages $V_{c2}$, $V_{e2}$ versus power supply",
37        "lin", DCtraces, xName="$V_S$", xUnits="V", yUnits="V")

The trace specifications are dictionaries: "y" is an expression over the simulated vectors, named through variables, and "label" the legend entry. The sweep variable is the abscissa; its name and units are given with the plot.

../../_images/VampQspiceDC.svg

AC analysis

With parameter stepping every signal holds one row per run, and make_traces() returns one trace per run, labelled with the step value. The expressions dB() and phase() are evaluated on the complex vectors.

39# AC analysis with parameter stepping
40AC = sl.ac(fileName, "dec", 50, 5, "10M",
41           step={"param": "C_c", "method": "lin",
42                 "start": "2p", "stop": "20p", "num": 10})
43mag = sl.make_traces(AC, [{"y": "dB(V_out)", "label": "$V_{out}$"}],
44                     variables={"V_out": "v(out)"})
45sl.plot("VampQspiceM", "dBmag($V_{out}$)", "semilogx", mag,
46        xName="frequency", xUnits="Hz", yUnits="dB")
47phs = sl.make_traces(AC, [{"y": "phase(V_out)", "label": "$V_{out}$"}],
48                     variables={"V_out": "v(out)"})
49sl.plot("VampQspiceP", "arg($V_{out}$)", "semilogx", phs,
50        xName="frequency", xUnits="Hz", yUnits="deg")
../../_images/VampQspiceM.svg ../../_images/VampQspiceP.svg

Transient analysis

52# Transient analysis with parameter stepping
53TR = sl.tran(fileName, "1n", "1u",
54             step={"param": "C_c", "method": "lin",
55                   "start": "2p", "stop": "20p", "num": 10})
56tran = sl.make_traces(TR, [{"y": "V_out", "label": "$V_{out}$"}],
57                      variables={"V_out": "v(out)"})
58sl.plot("VampQspiceT1", "Pulse $V_{out}$, stepped $C_c$", "lin", tran,
59        xName="time", xUnits="s", xScale="u", yUnits="V")
../../_images/VampQspiceT1.svg
61# Transient analysis, several signals
62TR = sl.tran(fileName, "1n", "1u")
63tran = sl.make_traces(TR, [{"y": "V_out", "label": "$V_{out}$"},
64                           {"y": "V_in", "label": "$V_{in}$"},
65                           {"y": "V_c2", "label": "$V_{c2}$"}],
66                      variables={"V_out": "v(out)", "V_in": "v(5)",
67                                 "V_c2": "v(outdc)"})
68sl.plot("VampQspiceT2", "Pulse $V_{out}$, $C_c$=18pF", "lin", tran,
69        xName="time", xUnits="s", xScale="u", yUnits="V")
../../_images/VampQspiceT2.svg

Change the stimulus

The stimulus of an independent source can be changed for one run with the keyword stimuli; the schematic and its netlist are left as they are. Here the pulse source becomes a sine with a stepped amplitude V_p, a parameter that is defined for this run with params.

71# Change the stimulus of a source for one run: a sine with a stepped
72# amplitude instead of the pulse of the schematic
73TR = sl.tran(fileName, "10n", "20u",
74             stimuli={"V1": ["SIN", 0, "{V_p}", "100k"]},
75             params=[("V_p", 1)],
76             step={"param": "V_p", "method": "lin",
77                   "start": 0.5, "stop": 1, "num": 2})
78sine = sl.make_traces(TR, [{"y": "V_out", "label": "$V_{out}$"},
79                           {"y": "V_in", "label": "$V_{in}$"},
80                           {"y": "V_c2", "label": "$V_{c2}$"}],
81                      variables={"V_out": "v(out)", "V_in": "v(5)",
82                                 "V_c2": "v(outdc)"})
83sl.plot("VampQspiceS", "Sine overdrive $V_{out}$, $C_c$=18pF", "lin", sine,
84        xName="time", xUnits="s", xScale="u", yUnits="V")
../../_images/VampQspiceS.svg

DC TEMP sweep

86# DC temperature sweep
87TMP = sl.dc(fileName, "TEMP", -55, 125, 5)
88tmp = sl.make_traces(TMP, [{"y": "V_c2", "label": "$V_{c2}$"},
89                           {"y": "V_e2", "label": "$V_{e2}$"}],
90                     variables={"V_c2": "v(outdc)", "V_e2": "v(2)"})
91sl.plot("VampQspiceTMP", "DC voltages $V_{c2}$, $V_{e2}$ versus temperature",
92        "lin", tmp, xName="temperature", xUnits="Celsius", yUnits="V")
../../_images/VampQspiceTMP.svg

NOISE analysis

NGspice returns the spectral densities of the output noise and of the source-referred noise in \(\mathrm{V^2/Hz}\); their square roots are plotted here.

 94# Noise analysis: NGspice returns the spectral densities in V^2/Hz
 95NOISE = sl.noise(fileName, "V(out)", "V1", "dec", 50, 5, "10M")
 96noise = sl.make_traces(NOISE, [{"y": "sqrt(S_vo)", "label": "$S_{vo}$"},
 97                               {"y": "sqrt(S_vi)", "label": "$S_{vi}$"}],
 98                       variables={"S_vo": "onoise_spectrum",
 99                                  "S_vi": "inoise_spectrum"})
100sl.plot("VampQspiceNOISE", "Noise input and output spectrum", "log", noise,
101        xName="frequency", xUnits="Hz", yUnits="V/sqrt(Hz)")
../../_images/VampQspiceNOISE.svg

The total noise follows from the goal function RMS_NOISE, which integrates a spectral density over the simulated frequency range and takes the square root:

103# Total output noise: the RMS_NOISE goal function integrates the spectrum
104v_no = sl.measure(NOISE, "RMS_NOISE(onoise_spectrum)", units="V")
105print("Total output noise:", v_no)

This yields:

Total output noise: 6.86794e-05 V

A goal function applied to a stepped result gives one value per run, and make_traces() then returns one trace whose points are the runs: the total output noise versus the temperature.

107# Total output noise versus temperature: a stepped noise analysis reduced
108# to one value per run gives a trace over the step values
109NOISE = sl.noise(fileName, "V(out)", "V1", "dec", 50, 5, "10M",
110                 step={"param": "TEMP", "method": "lin",
111                       "start": -55, "stop": 125, "num": 19})
112noisetot = sl.make_traces(NOISE, [{"y": "RMS_NOISE(S_vo)", "label": "$v_{no}$"}],
113                          variables={"S_vo": "onoise_spectrum"})
114sl.plot("VampQspiceNOISETOT", "Total output noise versus temperature", "lin",
115        noisetot, xName="temperature", xUnits="Celsius", yUnits="V", yScale="u")
../../_images/VampQspiceNOISETOT.svg

Transient analysis with parameter substitution

117# Transient analysis with parameter substitution
118TR = sl.tran(fileName, "0.1u", "20u",
119             stimuli={"V1": ["SIN", 0, "{V_p}", "100k"]},
120             params=[("V_p", 0.5)])
121tran = sl.make_traces(TR, [{"y": "V_out", "label": "$V_{out}$"}],
122                      variables={"V_out": "v(out)"})
123sl.plot("VampQspiceSIN", "$V_{out}$", "lin", tran,
124        xName="time", xUnits="s", xScale="u", yUnits="V")
../../_images/VampQspiceSIN.svg

Fourier and FFT post processing

Both post-processing options of tran() require the analysed vectors to be listed with save. An entry "name = expression" defines a derived vector with NGspice let after the transient; here the operating-point voltage of the collector, taken from the op() result at the top of the script, is subtracted, so that the DC component does not leak into the spectrum through the window. The keyword tmax limits the internal time step of the integration; a small value keeps the numerical noise floor of the spectrum low. With fft the transient is linearized on the grid of the time step, which sets the highest frequency of the spectrum, and transformed; the result is in the frequency domain (dataType 'fft', complex vectors and frequency) and plots like an AC result. The window follows NGspice specwindow (default hanning).

126# FFT of the collector voltage of Q2 with its operating point removed
127FFT = sl.tran(fileName, "0.5u", "512u", tstart="64u", tmax="10n",
128              stimuli={"V1": ["SIN", 0, "{V_p}", "100k"]}, params=[("V_p", 0.5)],
129              save=["v_ac = v(outdc) - {}".format(OPinfo["V_c2"])],
130              fft={"window": "gaussian", "order": 8}, options={"RELTOL": 1e-6})
131spectrum = sl.make_traces(FFT, [{"y": "V_c2", "label": "$V_{c2}$"}],
132                          variables={"V_c2": "v_ac"})
133sl.plot("VampQspiceFFT", "Spectrum of $V_{c2}$", "log", spectrum,
134        xName="frequency", xUnits="Hz", yUnits="V",
135        xLim=[10e3, 1e6], yLim=[2e-7, 2])
../../_images/VampQspiceFFT.svg

With fourier="<fundamental>" (or {"freq": "100k", "nfreqs": 10}) the result keeps the time-domain traces, and the harmonics are attached as the dictionary fourier of the result: the magnitude, phase and normalized values per harmonic, the total harmonic distortion thd(<vector>) in percent, and NGspice’s own table as text. fourier is not available for stepped runs, fft is.

137# Fourier analysis of the collector voltage of Q2
138FOURIER = sl.tran(fileName, "1u", "512u", tstart="64u", tmax="10n",
139                  stimuli={"V1": ["SIN", 0, "{V_p}", "100k"]}, params=[("V_p", 0.5)],
140                  save=["v_ac = v(outdc) - {}".format(OPinfo["V_c2"])],
141                  fourier="100k", options={"RELTOL": 1e-6})
142print(FOURIER.fourier["table"])

This yields:

Fourier analysis for v_ac:
  No. Harmonics: 10, THD: 0.0956276 %, Gridsize: 200, Interpolation Degree: 1
Harmonic Frequency   Magnitude   Phase       Norm. Mag   Norm. Phase
-------- ---------   ---------   -----       ---------   -----------
 0       0           0.000174029 0           0           0
 1       100000      1.93937     69.4189     1           0
 2       200000      0.000321188 98.5215     0.000165615 29.1026
 3       300000      0.00179453  -63.719     0.000925317 -133.14
 4       400000      8.47422e-05 -9.3169     4.36958e-05 -78.736
 5       500000      0.000320489 -101.63     0.000165254 -171.05
 6       600000      3.58591e-05 -53.403     1.84901e-05 -122.82
 7       700000      6.65262e-05 -141.36     3.4303e-05  -210.78
 8       800000      1.09453e-05 -92.258     5.64372e-06 -161.68
 9       900000      1.44341e-05 177.589     7.4427e-06  108.17