Interface with NGspice
SLiCAP runs NGspice simulations from a Python script and turns the results into traces, measurements and plots. It requires:
NGspice for circuit simulation (NGspice manual); its location is set in the
[commands]section of the SLiCAP configuration file (see Installation).An NGspice schematic (
.spice_sch), drawn with the SLiCAP schematic editor and its NGspice symbol library, or a hand-written netlistcir/<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:
op(): operating point analysis
dc(): DC sweep of a source, or of the temperature (
"TEMP")ac(): small-signal frequency-domain analysis
tran(): time-domain analysis, with FOURIER or FFT post-processing
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 (), 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
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.
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:
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.
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")
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")
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")
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")
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")
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)")
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")
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")
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])
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