An Easier Approach to Electrical Circuit Diagram Generation – lcapy

Whilst I might succumb in my crazier evangelical moments to the idea that academic authors (other than those who speak LateX natively) and media developers might engage in the raw circuitikz authoring described Reproducible Diagram Generators – First Thoughts on Electrical Circuit Diagrams, the reality is that it’s probably just way too clunky, and a little bit too far removed from the everyday diagrams educators are likely to want to create, to get much, if any, take up.

However, understanding something of the capabilities of quite low level drawing packages, and reflecting (as in the last post) on some of the strategies we might adopt for creating reusable, maintainable, revisable with modification and extensible diagram scripts puts us in good stead for looking out for more usable approaches.

One such example is the Python lcapy package, a linear circuit analysis package that supports:

  • the description of simple electrical circuits at a sloghtly hoger level than the raw circuitikz circuit creation model;
  • the rendering of the circuits, with a few layout cues, using circuitikz;
  • numerical analysis of the circuits in terms of response in time and frequency domains, and the charting of the results of the analysis; and
  • various forms of symbolic analysis of circuit descriptions in various domains.

Here are some quick examples to give a taste of what’s possible.

You can run the notebook (albeit subject to significant changes) that contains the original working for examples used in this post on Binderhub: Binder

Here’s a simple circuit:

And here’s how we can create it in lcapy from a netlist, annotated with cues for the underlying circuitikz generator about how to lay out the diagram.

from lcapy import Circuit

cct = Circuit()
cct.add("""
Vi 1 0_1 step 20; down
C 1 2; right, size=1.5
R 2 0; down
W 0_1 0; right
W 0 0_2; right, size=0.5
P1 2_2 0_2; down
W 2 2_2;right, size=0.5""")

cct.draw(style='american')

The things to the right of the semicolon on each line are the optional layout elements – they’re not required when defining the actual circuit itself.

The display of nodes and numbered nodes are all controllable, and the symbol styles are selectable between american, british and european stylings.

The lcapy/schematic.py package describes the various stylings as composites of circuitikz regionalisations, and could be easily extended to support a named house style, or perhaps accommodate a regionalisation passed in as an explicit argument value.

if style == 'american':
    style_args = 'american currents, american voltages'
elif style == 'british':
    style_args = 'american currents, european voltages'
elif style == 'european':
    style_args = ('european currents, european voltages, european inductors, european resistors')

As well as constructing circuits from netlist descriptions, we can also create them from network style descriptions:

from lcapy import R, C, L

cct2= (R(1e6) + L(2e-3)) | C(3e-6)
#For some reason, the style= argument is not respected
cct2.draw()

The diagrams generated from networks are open linear circuits rather than loops, which may not be quite what we want. But these circuits are quicker to write, so we can use them to draft netlists for us that we may then want to tidy up a bit further.

print(cct2.netlist())

'''
W 1 2; right=0.5
W 2 4; up=0.4
W 3 5; up=0.4
R 4 6 1000000.0; right
W 6 7; right=0.5
L 7 5 0.002; right
W 2 8; down=0.4
W 3 9; down=0.4
C 8 9 3e-06; right
W 3 0; right=0.5
'''

Circuit descriptions can also be loaded in from a named text file, which is handy for course material maintenance as well as reuse of circuits across materials: it’s easy enough to imagine a library of circuit descriptions.

#Create a file containing a circuit netlist
sch='''
Vi 1 0_1 {sin(t)}; down
R1 1 2 22e3; right, size=1.5
R2 2 0 1e3; down
P1 2_2 0_2; down, v=V_{o}
W 2 2_2; right, size=1.5
W 0_1 0; right
W 0 0_2; right
'''

fn="voltageDivider.sch"
with open(fn, "w") as text_file:
    text_file.write(sch)

#Create a circuit from a netlist file
cct = Circuit(fn)

The ability to create – and share – circuit diagrams in a Python context that plays nicely with Jupyter notebooks is handy, but the lcapy approach becomes really useful if we want to produce other assets around the circuit we’ve just created.

For example, in the case of the above circuit, how do the various voltage levels across the resistors respond when we switch on the sinusoidal source?

import numpy as np
t = np.linspace(0, 5, 1000)
vr = cct.R2.v.evaluate(t)
from matplotlib.pyplot import figure, savefig
fig = figure()
ax = fig.add_subplot(111, title='Resistor R2 voltage')
ax.plot(t, vr, linewidth=2)
ax.plot(t, cct.Vi.v.evaluate(t), linewidth=2, color='red')
ax.plot(t, cct.R1.v.evaluate(t), linewidth=2, color='green')
ax.set_xlabel('Time (s)')
ax.set_ylabel('Resistor voltage (V)');

Not the best example, admittedly, but you get the idea!

Here’s another example, where I’ve created a simple interactive to let me see the effect of changing one of the component values on the response of a circuit to a step input:

(The nice plotting of the diagram gets messed up unfortunately, at least in the way I’ve set things up for this example…)

As the code below shows, the @interact decorator from ipywidgets makes it trivial to create a set of interactive controls based around the arguments passed into a function:

import numpy as np
from matplotlib.pyplot import figure, savefig

@interact(R=(1,10,1))
def response(R=1):
    cct = Circuit()

    cct.add('V 0_1 0 step 10;down')
    cct.add('L 0_1 0_2 1e-3;right')
    cct.add('C 0_2 1 1e-4;right')
    cct.add('R 1 0_4 {R};down'.format(R=R))
    cct.add('W 0_4 0; left')

    t = np.linspace(0, 0.01, 1000)
    vr = cct.R.v.evaluate(t)

    fig = figure()
    #Note that we can add Greek symbols from LaTex into the figure text
    ax = fig.add_subplot(111, title='Resistor voltage (R={}$\Omega$)'.format(R))
    ax.plot(t, vr, linewidth=2)
    ax.set_xlabel('Time (s)')
    ax.set_ylabel('Resistor voltage (V)')
    ax.grid(True)
    
    cct.draw()

Using the network description of a circuit, it only takes a couple of lines to define a circuit and then get the transient response to step function for it:

Again, it doesn’t take much more effort to create an interactive that lets us select component values and explore the effect they have on the damping:

As well as the numerical analysis, lcapy also supports a range of symbolic analysis functions. For example, given a parallel resistor circuit, defined using a network description, we can find the overall resistance in simplest terms:

Or for parallel capacitors:

Some other elementary transformations we can apply – providing expressions for the an input voltage in the time or Laplace/s domain:

We can also create pole-zero plots quite straightforwardly, directly from an expression in the s-domain:

This is just a quick skim through of some of what’s possible with lcapy. So how and why might it be useful as part of a reproducible educational resource production process?

One reason is that several of the functions can reduce the “production distance” between different likely components of a set of educational materials.

For example, given a particular circuit description as a netlist, we can annotate it with direction cribs in order to generate a visual circuit diagram, and we can use a circuit created from it directly (or from the direction annotated script) to generate time or frequency response charts. (We can also obtain symbolic transfer functions.)

When trying to plot things like pole zero charts, where it is important that the chart matches a particular s-domain expression, we can guarantee that the chart is correct by deriving it directly from the s-domain expression, and then rendering that expression in pretty LaTeX equation form in the materials.

The ability to simplify expressions  – as in the example of the simplified expressions for overall capacitance or resistance in the parallel circuit examples above – directly from a circuit description whilst at the same time using that circuit description to render the circuit diagram, also reduces the amount of separation between those two outputs to zero – they are both generated from the self-same source item.

You can run the notebook (albeit subject to significant changes) that contains the original working for examples used in this post on Binderhub: Binder

Reproducible Diagram Generators – First Thoughts on Electrical Circuit Diagrams

I had a quick tinker with one of the demo notebooks I’m putting together to try to work through what I think I mean by various takes on the phrase “reproducible educational materials” this morning, so here’s a quick note to keep track of my thinking.

The images are drawn in Jupyter notebooks using tikzmagic loaded as %load_ext tikz_magic. (The original notebook this post is based on can be found here, although it’s likely subject to significant change…)

The circuitikz LaTeX package (manual) supports the drawing of electrical circuit diagrams. Circuits are constructed by drawing symbols that connect pairs of Cartesian co-ordinates or that:

%%tikz -p circuitikz -s 0.3
    %Draw a resistor labelled R_1 connecting points (0,0) and (2,0)
    \draw (0,0) to[R, l=$R_1$] (2,0);


The requirement to specify co-ordinates means you need to think about the layout – drafting a circuit on graph paper can help with this.

But things may be simplified in maintenance terms if you label co-ordinates and join those.

For example, consider the following circuit:

This can be drawn according to the following script:

%%tikz -p circuitikz -s 0.3
    %Draw a resistor labelled R_1 connecting points (0,0) and (2,0)
    %Extend the circuit out with a wire to (4,0)
    \draw (0,0) to[R, l=$R_1$] (2,0) -- (4,0);

    %Add a capacitor labelled C_1 connecting points (2,0) and (2,-2)
    \draw (2,0) to[C, l=$C_1$] (2,-2);

    %Add a wire along the bottom
    \draw (0,-2) -- (4,-2);

There are a lot of explicitly set co-ordinate values in there, and it can be hard to see what they refer to. Even with such a simple diagram, making changes to it could become problematic.

In the same way that it is good practice to replace inline numerical values in computer programs with named constants or variables, we can start to make the figure more maintainable by naming nodal points and then connecting these named nodes:

%%tikz -p circuitikz -s 0.3

    %Define some base component size dimensions
    \def\componentSize{2}

    %Define the size of the diagram in terms of component width and height
    %That is, how many horizontally aligned components wide is the diagram
    % and how many vertically aligned components high
    \def\componentWidth{2}
    \def\componentHeight{1}

    %Define the y co-ordinate of the top and bottom rails
    \def\toprail{0}
    \def\height{\componentSize * \componentHeight}
    \def\bottomrail{\toprail - \height}

    %Define the right and left extent x coordinate values
    \def\leftside{0}
    \def\width{\componentSize * \componentWidth}
    \def\rightside{\leftside + \width}

    %Name the coordinate locations of particular nodes
    \coordinate (InTop) at (\leftside,\toprail);
    \coordinate (OutTop) at (\rightside,\toprail);
    \coordinate (InBottom) at (\leftside,\bottomrail);
    \coordinate (OutBottom) at (\rightside,\bottomrail);

    %Draw the top rail
    %Define a convenience x coordinate as the
    %  vertical aligned to the topmost component out
    %The number (1) in the product term below is based on
    %  how many components in from the left we are
    \def\R1outX{1 * \componentSize}

    %Add a resistor labelled R_1
    \coordinate (R1out) at (\R1outX,\toprail);
    \draw (InTop) to[R, l=$R_1$] (R1out) -- (OutTop);

    %Add a capacitor labelled C_1
    \coordinate (C1out) at (\R1outX,\bottomrail);
    \draw (R1out) to[C, l=$C_1$] (C1out);

    %Draw the bottom rail
    \draw (InBottom) -- (OutBottom);

Some reflections about possible best practice drawn (!) from this:

  • define named limits on x values to set the width of the diagram, such as \leftside and \rightside. This can be done by counting the number of components wide the diagram is (if we can assume components have width one).
  • name the maximum and minimum height (y) values such as \toprail and \bottomrail. Again, counting vertically place components may help.  Use relative definitions where possible to make the diagram easier to maintain.
  • define connections relative to each other to minimise the number of numerical values that need to be set explicitly;
  • name points sensibly; if we read the diagram from top left to bottom right, we can make use of easily recognised verticals by using named x coordinate values set relative to the topmost component out x co-ordinates (for example, \R1outX) and top leftmost component out y values; full cartesian co-ordinate pairs can then be named relative to nodes associated with top leftmost component outs (for example, \R1out);
  • the code to produce the diagram looks like overkill in its length, but lots of could quickly become boilerplate that could potentially be included in a slightly higher level TeX package that bakes more definitions in. Despite the added length, it also makes the script more readable and supports its self-documenting, literate programming style nature.

PS imagining feedback… “Ah yes, but we don’t draw resistors like that, so it’s no good…” ;-)

%%tikz -p circuitikz -s 0.3
%To ward off the "we don't draw resistors like that" cries...
\ctikzset{resistor = european}

%Draw a resistor labelled R_1 connecting points (0,0) and (2,0)
\draw (0,0) to[R, l=$R_1$] (2,0);

resistor2
And that’s another reason why this approach make sense…