Using Python with ngspice
We can automate this process by using ngspice in batch mode, i.e. running the simulator from the command line, and reading the output file using Python, and do the processing automatically. To run ngspice at the command line, you can use ngspice <circuit file>
.
One very good environment for Python3 is Spyder. You can download this for multiple platforms, and the easiest way to install Spyder is as part of the Anaconda distribution, also available for various operating systems.
Contents
Running ngspice from Python
Below is a simple Python script for running ngspice, reading its output. It uses the our custom ngspice_link module, that contains convenient classes and functions for running ngspice from within Python. In this tutorial, we are simulating the circuit example found in the ngspice tutorial.
1 import matplotlib.pyplot as plt
2 from si_prefix import si_format
3
4 # import our custom ngspice module
5 import ngspice_link as ngl
6
7 # setup the simulation configuration
8 cfg = {
9 'ngspice' : '/Applications/ngspice/bin/ngspice',
10 'cir_dir' : '/Users/louis/Documents/UPEEEI/Classes/EE 220/2020_1/Activities/',
11 'cir_file' : 'circuit1.sp',
12 }
13
14 # create the ngspice object
15 sim1 = ngl.ngspice(cfg)
16
17 # run ngspice with the configuration above
18 sim1.run_ngspice()
19
20 # read the simulation output produced by the 'wrdata' command
21 vbe, [ic] = sim1.read_dc_analysis('circuit1.dat', [1])
22
23 # define the plot parameters
24 plt_cfg = {
25 'grid_linestyle' : 'dotted',
26 'title' : r'2N2222a NPN BJT Transfer Characteristics',
27 'xlabel' : r'$V_{BE}$ [mV]',
28 'ylabel' : r'$I_C$ [mA]',
29 'legend_loc' : 'lower left',
30 'add_legend' : False,
31 'legend_title' : None
32 }
33
34 fig = plt.figure()
35 ax = fig.add_subplot(1, 1, 1)
36
37 # plot the collector current vs the base-emitter voltage
38 ax.plot(ngl.scale_vec(vbe, 1e-3), ngl.scale_vec(ic, 1e-3), '-')
39
40 # annotate the 1mA point (arbitrary)
41 ngl.add_hline_text(ax, 1, 550, \
42 r'{:.1f} mA'.format(1))
43
44 # find the vbe corresponding to 1mA
45 idx, icx = ngl.find_in_data(ic, 1e-3)
46
47 # annotate the vbe that corresponds to 1mA
48 ngl.add_vline_text(ax, vbe[idx]/1e-3, 3, r'$V_{BE}=$' + \
49 si_format(vbe[idx], precision=2) + 'V')
50
51 # label the plot
52 ngl.label_plot(plt_cfg, fig, ax)
53
54 # save the plot as an image
55 plt.savefig('BJT_2n2222a_transfer.png', dpi=600)
Let's go through the Python code one block at a time.
Importing Python libraries
One advantage of the Python language is its large collection of libraries, covering a vast number of topics. You can even build your own library, which is what we have done, to easily run ngspice from within Python.
1 import matplotlib.pyplot as plt
2 from si_prefix import si_format
3
4 # import our custom ngspice module
5 import ngspice_link as ngl
Here, we imported the following libraries:
- The matplotlib.pyplot library that contains plotting functions.
- From the si_prefix library, we import, si_format function. This function converts floating point numbers to numbers with proper SI prefixes, such as milli, centi, kilo, etc.
- Our own, user-defined library, the ngspice_link library. This library contains functions to setup the simulator, run the simulation, and extract data from the simulation output files. All functions and classes in this library are called with the prefix
ngl.
.
Creating the ngspice Object
We can the create an ngspice object, as defined in the ngspice_link library.
7 # setup the simulation configuration
8 cfg = {
9 'ngspice' : '/Applications/ngspice/bin/ngspice',
10 'cir_dir' : '/Users/louis/Documents/UPEEEI/Classes/EE 220/2020_1/Activities/',
11 'cir_file' : 'circuit1.sp',
12 }
13
14 # create the ngspice object
15 sim1 = ngl.ngspice(cfg)
Creating the ngspice object, in this case sim1
, requires a Python dictionary that contains the location of the ngspice executable, the directory of the ngspice circuit file, and the name of the circuit file itself.
Running the Simulation
We can then run a simulation attached to the sim1
object.
17 # run ngspice with the configuration above
18 sim1.run_ngspice()
This code assembles a string that can be run at the command line.
Reading the Simulation Results
Once the ngspice DC analysis finishes, we can extract the simulation data from the data file written by the ngspice wrdata
command, in this case it is circuit1.dat
.
20 # read the simulation output produced by the 'wrdata' command
21 vbe, [ic] = sim1.read_dc_analysis('circuit1.dat', [1])
The read_dc_analysis(filename, list_of_column_indices)
function takes as input:
- The simulation output data file name, and
- A list of column indices of the desired data. Note that if you are saving just one data item (either voltage or current), the first column (index 0) is always the sweep variable, and the index of the data column is 1. However, for multiple data items, the even columns contain the sweep variable, and the odd columns contain the data items, e.g.
[1, 3, 5, 7,...]
.
The function returns the sweep variable data, in this case, vbe
, and a multi-dimensional array, [ic]
, containing the data. Once we have extracted the data from the ngspice output file, we can now perform the various analysis and processing steps that we might require.
Plotting Simulation Data
To facilitate plotting, the ngspice_link module contains a function called label_plot()
that takes as input, a dictionary, plt_cfg
in this case, containing plot configurations, labels, and legends.
23 # define the plot parameters
24 plt_cfg = {
25 'grid_linestyle' : 'dotted',
26 'title' : r'2N2222a NPN BJT Transfer Characteristics',
27 'xlabel' : r'$V_{BE}$ [mV]',
28 'ylabel' : r'$I_C$ [mA]',
29 'legend_loc' : 'lower left',
30 'add_legend' : False,
31 'legend_title' : None
32 }
Note that matplotlib.pyplot can accept LaTeX math formatting in the axis labels, figure title, and annotations. We can then initialize the plot, and return the figure (fig
) and axes (ax
) handles.
34 fig = plt.figure()
35 ax = fig.add_subplot(1, 1, 1)
We can then call the axes' plot()
function to plot the result. Since we want to plot it on a mV and mA scale, we can use the scale_vec()
function to scale all the elements in the list by a constant.
37 # plot the collector current vs the base-emitter voltage
38 ax.plot(ngl.scale_vec(vbe, 1e-3), ngl.scale_vec(ic, 1e-3), '-')
The add_hline_text(ax, y_data_value, x_text_location, text)
function allows us to add horizontal lines and annotate these lines with text. In this case, we want to place a horizontal line at a collector current of 1mA.
40 # annotate the 1mA point (arbitrary)
41 ngl.add_hline_text(ax, 1, 550, \
42 r'{:.1f} mA'.format(1))
We can then find the corresponding base-emitter voltage by using the find_in_data(data, value)
function, which returns the index of the closest item in ic
, which we then use for the add_vline_text(ax, x_data_value, y_text_location, text)
function.
44 # find the vbe corresponding to 1mA
45 idx, icx = ngl.find_in_data(ic, 1e-3)
46
47 # annotate the vbe that corresponds to 1mA
48 ngl.add_vline_text(ax, vbe[idx]/1e-3, 3, r'$V_{BE}=$' + \
49 si_format(vbe[idx], precision=2) + 'V')
Finally, we can label and configure the plot using the label_plot()
function, and save it to an image using the savefig()
function.
51 # label the plot
52 ngl.label_plot(plt_cfg, fig, ax)
53
54 # save the plot as an image
55 plt.savefig('BJT_2n2222a_transfer.png', dpi=600)
We should get a plot similar to the one below: