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).

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