Software

pingspice

Object-oriented, asynchronous SPICE circuit simulation

The pingspice package provides you with the object-oriented power of Python integrated with the free Ngspice circuit simulator. You use Python to build your circuit netlist, you use Python to run simulations of it, and you use Python-based plotting and data analysis tools to study the results. It’s all Python, except for the underlying C code running the SPICE simulations.

MORE THAN forty years ago, Laurence W. Nagel and D.O. Pederson presented to the 16th Midwest Symposium on Circuit Theory a “Simulation Program with Integrated Circuit Emphasis” (SPICE).1 In this new mainframe FORTRAN program, the “simulation capabilities of nonlinear dc analysis, small signal analysis, and nonlinear transient analysis” were combined “to yield a reasonably general purpose electronic circuit simulation program.”

In the years that followed, electronics and computer technology went exponential. SPICE saw some of its own important improvements–including a 1989 re-write in C by Thomas Quarles–though nothing on the scale of what was happening to the integrated circuits it was designed to simulate.

A switchmode DC-DC converter simulated with pingspice

The fundamental work done by Nagel, Pederson, and Quarles still lives on today in computers whose speed and complexity would have blown those guys away. In 1994-95, I went to a computer lab at the University of Washington to do SPICE simulations on a UNIX workstation for my EE classes. Now I run simulations for my circuit designs on my kid’s gaming CPU that has more processing power than everything in that computer lab put together. And yet the underlying simulation program still includes quite a lot of the C code that Thomas Quarles wrote thirty years ago.

The pingspice package adds the ease and flexibility of object-oriented Python programming, the computing power of asynchronous multi-core processing, and some sophisticated semiconductor modeling to the open-source Ngspice SPICE circuit simulator. It has a small library of components that try to model real-world behavior, including some power MOSFETs (really!) and diodes, big lossy capacitors and inductors, wires and wire pairs with resistance and parasitic reactance, and a PWM signal generator with realistic rise and fall transitions.

And what you use to work with all that is Python. Using pingspice, it’s easy to forget that everything’s being run on a much-updated (but, alas, still very user-unfriendly) 40-year old simulation engine buried in the background. You’ll only need to work directly with Ngspice and its arcane netlist format when there’s a problem with your simulation. With pingspice’s simulation-friendly circuit components, that should happen less than ever.

This UML diagram shows the main objects of pingspice hard at work, identifying component parameters using differential evolution. Black lines represent one object or method constructing an instance of another object. Red lines represent one object or method calling a method of another object. The blue box represents a separate package of mine, ade, that does differential evolution asynchronously.

The first building blocks for using pingspice are enclosed in the dashed boxes. Once you go beyond simple circuit simulations, you’ll start using the AC, DC, and TRAN subclasses of Analyzer to simplify running and fetching results from Ngspice. Then, when you want to run simulations repeatedly to do cool stuff like exploring the behavior of your circuit under different conditions, evolving parameters for a MOSFET using info you’ve entered in from its datasheet, or optimizing component values for a high-order active filter, you’ll get acquainted with MultiRunner and ParameterFinder.

Building the Netlist

You define your circuit by constructing a Netlist object and calling it in a with . . . as block to have it pass you the only tool you’ll need for creating as elaborate of a netlist as you want, an instance of Elements. Give this tool a short and convenient name like f and call it over and over again to add lines to your netlist, like this:

with Netlist(FILEPATH)() as f:
    f.V(1, 0, 'DC', 1, 'AC', 1)
    f.R(1, 2, R)
    f.L(2, 3, L)
    f.C(3, 0, C)

The actual method of f that you call determines the component’s type, f.R for a resistor, f.D for a diode, etc. The first two arguments of the call specify the circuit nodes that the component is connected to. The argument (or arguments) that follow the first two define the component’s value. The resistor defined with f.R(1, 2, R) is assigned the value that is currently in the Python variable R. The call to f.V has several arguments following the circuit nodes because voltage sources are defined by several items and not just a single value.

You don’t have to worry about keeping track of reference designators like R1, R2, etc. for your various components, unless you want to.2 You don’t have to mess around with creating strings of text in a format that Ngspice will accept; everything is done with calls to that little tool f, with whatever arguments are appropriate for specifying the kind of component that you want. And when your code drops off the end of the with . . . as block and leaves the context of the Netlist, the netlist text that Ngspice will be sourcing gets automatically generated with a sensible set of simulation options.3

SPICE subcircuits get an intuitive object-oriented pingspice building block of their own. Just subclass Subcircuit, define your subcircuit’s external nodes, and write a setup method that takes a reference to the netlist-building tool as its argument. Here’s an example4 from pingspice’s modest but hopefully growing component library:

class OpAmp(Subcircuit):
    """
    I am a behaviorally modeled op amp with offset voltage, bias
    current (opposite and equal amounts for each input), common and
    differential mode input resistance and capacitance, a large but
    finite DC open-loop gain, gain-bandwith product, and output slew
    rate. I also have a second 1/RC pole that gives me a 45-degree
    phase margin frequency.

    My internal voltages never go within I{hr0} volts of I{vcc} and
    ground. I have a low but finite output impedance that increases
    that headroom with higher output source or sink current.

    My default attributes are aimed at matching the Microchip
    MCP6281. Subclass me with your own attributes, or set different
    ones with constructor keywords, for a different op amp.
    """
    vcc =    5.0   # Supply voltage
    voff = +1.2e-3 # input offset voltage
    ioff = +1e-12  # input offset current
    ib =   2e-12   # input bias current (worst case: opposite for inp, inn)

    rdm =  10e13   # input resistance, differential mode
    cdm =  3e-12   # input capacitance, differential mode
    rcm =  10e13   # input resistance, common mode
    cdm =  3e-12   # input resistance, common mode

    gdb =    110   # db open-loop gain at dc
    gbp =    5e6   # gain-bandwidth product (hz)
    p45 =   12E6   # 45-degree phase margin frequency

    hr0 =  0.012   # Output voltage headroom from +/- rail
    rout =  19.4   # Est. from output headroom reduction w/ higher current
    sr =   2.5E6   # Output slew rate, V/sec

    nodes = ['inp', 'inn', 'out']

    def setup(self, f):
        # ESD diodes
        f.D(0, 'inp', 'esd')
        f.D(0, 'inn', 'esd')
        f.MODEL('esd', 'D', IS=5E-12, BV=6, IBV=1E-3)
        # Input impedance and offset...
        f.V('inp', 1, -self.voff)
        # ... common mode
        for sign, pin in ((+1, 1), (-1, 'inn')):
            f.R(pin, 0, 10E13)
            f.C(pin, 0, 6E-12)
            f.I(pin, 0, sign*self.ib)
        # ... differential mode
        f.R(1, 'inn', 10E13)
        f.C(1, 'inn', 3E-12)
        f.I(1, 0, self.ioff)
        # Open-loop gain amplifier with realistic rail-to-rail limits
        # and an RC pole for the gain-bandwidth product
        g = 10**(0.05*self.gdb)
        f.E(2, 0, 1, 'inn', g)
        f.A(2, 3, 'rails')
        f.model('rails', Paren(
            'limit', fraction=False, gain=1.0,
            out_lower_limit=0, out_upper_limit=self.vcc,
            limit_range=self.hr0))
        C = capValueUnity(1.0, self.gbp, g)
        f.R(3, 0, 1.0)
        f.C(3, 0, C)
        # Unity-gain buffer with a second pole at 45-degree phase
        # margin frequency
        f.G(0, 4, 3, 0, 1.0)
        f.R(4, 0, 1.0)
        C = capValue45deg(1.0, self.p45)
        f.C(4, 0, C)
        # Unity-gain output buffer with low impedance and limited slew
        # rate (using default soft transition of 0.1V)
        f.A(4, 5, 'slew')
        f.model('slew', Paren(
            'slew', rise_slope=self.sr, fall_slope= self.sr))
        f.R(5, 'out', self.rout)

There’s a lot going on in that code block, and all of it is Python. What it produces is an incomprehensible little chunk of stone-age coding:

.SUBCKT OpAmp inp inn out
*--------------------------------------------------
D1000 0 inp esd
D1001 0 inn esd
.MODEL esd D (is=5e-12 bv=6 ibv=0.001)
V1000 inp 1 -0.0012
R1000 1 0 1e+14
C1000 1 0 6e-12
I1000 1 0 2e-12
R1001 inn 0 1e+14
C1001 inn 0 6e-12
I1001 inn 0 -2e-12
R1002 1 inn 1e+14
C1002 1 inn 3e-12
I1002 1 0 1e-12
E1000 2 0 1 inn 316227.766017
A1000 2 3 rails
.MODEL rails limit(limit_range=0.012 out_upper_limit=5.0 out_lower_limit=0 fraction=0 gain=1.0)
R1003 3 0 1.0
C1003 3 0 0.0632453532034
G1000 0 4 3 0 1.0
R1004 4 0 1.0
C1004 4 0 1.32629119243e-08
A1001 4 5 slew
.MODEL slew slew(rise_slope=2500000.0 fall_slope=2500000.0)
R1005 5 out 19.4
.ENDS OpAmp

You’ll be glad to never actually have to see this.5 Let Ngspice chew on its netlists behind the scenes without you. Focus on what your circuit actually does rather than on arranging some arcane combination of symbols that date back to the era of paper punch cards.

And yes, in case you were wondering, you can nest subcircuits. Just define each one in its own class with its own setup method, and construct one inside the setup method of the other. Here’s an example from the library, a subcircuit that tries to model the on-off transition behavior of a set of ganged circuit breakers. It uses an instance of the Switch subcircuit to do so–see the highlighted line:6

class GangedBreakers(Subcircuit):
    """
    Two 80A DC circuit breakers ganged together with each other and a
    0.5 A DC breaker, originally intended for PV ground-fault
    protection.

    If I{t0} is C{None}, the breakers never get set or trip. Rather,
    they are just modeled as the on or off resistance.
    """
    # Estimated resistances of circuit breakers, Ohms
    R = [5.0, 2E-3]     # 0.5A, 80A

    t0 = None
    tr = 20E-6
    td = 150E-6
    initially_on = False

    nodes = ['a0', 'a1', 'b0', 'b1', 'c0', 'c1']

    def setup(self, f):
        if self.t0 is None:
            for a, b, k in (
                    ('a0', 'a1', 0), ('b0', 'b1', 1), ('c0', 'c1', 1)):
                resistance = self.R[k] if self.initially_on else 1E9
                f.R(a, b, resistance)
            return
        Switch(                                                        
            f, N=3, t0=self.t0, tr=self.tr, td=self.td,
            log=True, initially_on=self.initially_on, R_on=self.R[1])(
                11, 'a1', 'b0', 'b1', 'c0', 'c1')
        f.R(11, 'a0', self.R[0]-self.R[1])

Running Simulations

When it comes time to use the circuit you’ve built, pingspice spawns one or more Ngspice processes and communicates with them in the background,7 providing you with a clean, convenient API for initiating and updating simulations and accessing the results. It’s as easy as this:

@defer.inlineCallbacks
def run():
    # Construct an NgspiceRunner, the fundamental object
    r = NgspiceRunner()
    # Have it source the netlist file
    yield r.source(FILEPATH)
    # Do an AC analysis
    N, T = yield r.ac('lin', 11, 1, 101)
    print(sub("Simulated {:d} frequencies at {:+.1f} degrees C", N, T))
    # Get the frequency and output vectors and print them
    F, V3 = yield r.get('frequency', 'V(3)')
    for f, dB in zip(F, 20*np.log10(np.abs(V3))):
        print(sub("{:5.1f} Hz: {:+.2f} dB", f, dB))

That highlighted line is where you construct the engine for all your simulation work. An NgspiceRunner spawns an Ngspice process and talks to it for you. No more typing lines of text to a creaky old console program and wondering how to do something useful with the results. The next thing is to tell your NgspiceRunner to source the netlist file you’ve constructed, and yield the Twisted framework’s Deferred object that comes back.8 When Ngspice is done sourcing the netlist, the Deferred fires and your method resumes. Meanwhile, your Python program can do other stuff like network communication or running a user interface.

Same thing again with telling it to do an AC analysis–yield the Deferred that comes back and your method will resume when Ngspice is done with the analysis. Then you fetch some result vectors with a call to the get method of your NgspiceRunner, yield that Deferred, and come back to do Python stuff with the Numpy vectors that appear a few milliseconds later. All of Ngspice’s commands are run asynchronously, and your code doesn’t just sit there blocking other activity while it works.

The basics are really that simple. But, with asynchronous programming, evolutionary parameter finding using ade, and easy integration with numerical analysis and plotting tools like Numpy, Scipy, Matplotlib (or my yampex Matplotlib extension), you can do a whole lot more.

Interested? Then head over to the tutorial I’ve written to get you started, with tons of code examples that progress from the simplest single-stage RLC circuit to algorithmic multi-stage circuit construction and evolutionary identification of component parameters.

License

Copyright (C) 2017-2018 by Edwin A. Suominen, http://edsuom.com/:

See edsuom.com for API documentation as well as information about
Ed's background and other projects, software and otherwise.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the
License. You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an "AS
IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the specific language
governing permissions and limitations under the License.

Notes


  1. Technical Report No. UCB/​ERL M382, April 1973. http://www2.eecs.berkeley.edu/​Pubs/TechRpts/​1973/ERL-382.pdf 

  2. Specify the reference designator by including its suffix after the component symbol, like this: f.R1(1, 2, R)

  3. The Sourcer object takes care of this. Naturally, you can change any of the default simulation options as desired. 

  4. Although I attempted to make this subcircuit act sort of like a commonly used op-amp available from Microchip, there are no guarantees, and neither this code nor pingspice is affiliated with Microchip in any way. I seriously doubt anyone there has ever heard of it, or me. An example of a circuit that uses the OpAmp subcircuit subclass is opamp-01.py

  5. Unless there is a circuit problem such as the dreaded “timestep too small” error. In that case, NgspiceRunner offers a debug keyword that you can set True in its constructor to drop the console into an actual Ngspice session with the offending netlist loaded. It’s not perfect, but it helps. 

  6. The  Switch subcircuit class uses as many instances of the XSPICE analog switch model as needed to represent the desired number of ganged switches, three in this case. 

  7. Via standard input and output, using a carefully developed subclass of Twisted’s protocol.ProcessProtocol. The same Ngspice process remains running for repeated simulations, in server mode. Ngspice’s old-fashioned TTY-oriented protocol with all its text-mode back-and-forth was quite a pain to set up for robust interprocess communication, as you can probably guess from looking at the code. It took a lot of unit testing and dealing with seemingly endless nuances with every command. But it’s done, and is a more efficient way to do things than spawning a new batch-mode instance of the simulator program every time you want to alter some component values and do another simulation. 

  8. The old-fashioned way of using Twisted was to add a callback function to the Deferred and then return it. This becomes unwieldy when you are doing one deferred thing after another, and the easier and newer way of doing things is to use the inlineCallbacks decorator and the yield statement.