Electronics Overview

The electronics area provides a rich canvas for one piece flow production techniques.

Circuit analysis is often performed on graph based representations of a circuit which are easily modelled and then rendered as circuit diagrams or treated analytically.

This notebook demonstrates how we can use a range of techniques to script the creation of electrical circuit diagrams, as well as creating models of circuits that can be rendered as a schematic circuit diagram and analysed as a computational model. This means we can:

  • create a model of a circuit as a computational object through a simple description language;

  • render a schematic diagram of the circuit from the model;

  • display analytic equations describing the model that represent particular quantities such as currents and voltages as a function of component variables;

  • automatically calculate the values of voltages and currents from the model based on provided component values.

The resulting document is self-standing in terms of creating the media assets that are displayed from within the document itself. In addition, analytic treatments and exact calculations can be performed on the same model, which means that diagrams, analyses and calculations will always be consistent, automatically derived as they are from the same source. This compares to a traditional production route where the different components of the document may be created independently of each other.

See also the Diagramming section for a discussion of netlistsvg, a node package that can create SVG schematic diagrams from a netlist.

lcapy

As described elsewhere, lcapy is a linear circuit analysis package that can be used to describe, display and analyse the behaviour of a wide range of linear analogue electrical circuits. lcapy supports numerical analysis of described circuits in terms of response in time and frequency domains, the charting of the results of the analysis and various forms of symbolic analysis of circuit descriptions in various domains.

lcapy use a circuit description that can be used to generate an circuit diagram as the basis for a wide range of analyses. For example, lcapy can be used to describe equivalent circuits (such as Thevenin or Norton equivalent circuits), or generate Bode plots.

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')
../_images/overview_2_0.png

The lcapy/schematic.py package describes the various stylings 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)

cct2.draw()
../_images/overview_5_0.png

We can lookup a netlist description from the circuit object directly:

cct2.sch()

# Or as a string: cct2.netlist()
W 1 2; right=0.5
W 2 4; up=0.4
W 3 5; up=0.4
R1 4 6 1000000.0; right
W 6 7; right=0.5
L1 7 5 0.002; right
W 2 8; down=0.4
W 3 9; down=0.4
C1 8 9 3e-06; right
W 3 0; right=0.5

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.

cct2.netlist()
'W 1 2; right=0.5\nW 2 4; up=0.4\nW 3 5; up=0.4\nR1 4 6 1000000.0; right\nW 6 7; right=0.5\nL1 7 5 0.002; right\nW 2 8; down=0.4\nW 3 9; down=0.4\nC1 8 9 3e-06; right\nW 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

from lcapy import Circuit

# 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
netlist_cct = Circuit(fn)

netlist_cct.draw()
../_images/overview_11_0.png

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 = netlist_cct.R2.v.evaluate(t)

Let’s see what voltage response looks like:

from matplotlib.pyplot import figure, savefig

fig = figure()
ax = fig.add_subplot(111, title='Resistor R2 voltage')

# The response voltage across R2
ax.plot(t, vr, linewidth=2)

# The input voltage, Vi
ax.plot(t, netlist_cct.Vi.v.evaluate(t), linewidth=2, color='red')

# The voltage aceoss R1
ax.plot(t, netlist_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! Being a matplotlib chart, we can style it as we would any matplotlb chart. Or use a different plotting library altogether, such as plotly, to create interactive HTML charts.

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:

from ipywidgets import interact

@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')

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

    from matplotlib.pyplot import figure, savefig
    fig = figure()
    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()
../_images/overview_18_0.png

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 a step function for it:

from lcapy import Vstep, R, C, L
from numpy import linspace

underDampedRLC = Vstep(10) + R(0.1) + L(0.2, 0)+ C(0.4, 0)
underDampedRLC.draw();

t = linspace(0, 10, 1000)

underDampedRLC.Isc.transient_response().plot(t);
../_images/overview_20_0.png

Or a frequency response:

n = Vstep(20) + R(5) + C(10)
n.draw()

vf = linspace(0, 1, 4000)
n.Isc.frequency_response().plot(vf, log_scale=True);
../_images/overview_22_0.png
Should convert current expression to time-domain first

It’s trivial to make an end user application around a function that lets us select component values and explore the effect they have on the damping.

Note

This will only work interactively within a notebook or via a Thebe enabled code cell hooked up to a Jupyter kernel somewhere.

@interact(R1=(0.1, 10),L1=(0.01, 1),C1=(0.01,0.5))
def damping(R1=0.1,L1=0.2,C1=0.4):
    underDampedRLC = Vstep(10) + R(R1) + L(L1)+ C(C1)
    underDampedRLC.draw();

    t = linspace(0, 10, 1000)

    underDampedRLC.Isc.transient_response().plot(t);
../_images/overview_24_0.png ../_images/overview_24_1.png

As well as the numerical analysis, lcapy also supports a range of symbolic analysis functions. For example, consider a parallel resistor circuit, defined using a network description:

parallelR = R('R_1') | R('R_2')
parallelR.draw()
../_images/overview_26_0.png

We can find the overall resistance in simplest terms:

parallelR.simplify().R
../_images/overview_28_0.png

Note

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.

We can do a similar thing for parallel capacitors:

parallelC = C('C_1') | C('C_2')
parallelC.draw()

parallelC.simplify().C
../_images/overview_31_0.png ../_images/overview_31_1.png

We can fudge the creation of text around this representation:

from IPython.display import Latex
# Hmmm, could we make some cell block magic for this

txt = f"The overall resistance value simplifies to: {parallelR.simplify().R._repr_latex_()}"

Latex( txt)
\[The overall resistance value simplifies to: $$\frac{R_{1} R_{2}}{R_{1} + R_{2}}$$\]

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

Domain Transformations and Transfer Functions

We can represent voltages across components in out circuit in the time domain:

from lcapy import Vac, t, s, pi

#Representation of AC voltage source in time domain
#Vac(amplitude, phase)
Vac(20, pi/2).Voc(t)
../_images/overview_36_0.png

We can also view s-domain representations of components in a circuit:

#Representation of AC voltage source in s domain
Vac(20).Voc(s)
../_images/overview_38_0.png

For example, for a simple circuit:

cct.draw()

f'{cct[0].V.s}, {cct[1].V.s}, {cct[2].V.s}'
../_images/overview_40_0.png
'0, 20/s, 20/(s + 1/(C*R))'
cct[1].name
'1'

Pole-Zero Plots

Pole-zero plots can be created quite straightforwardly, directly from an expression in the s-domain:

#pole-zero plot
from lcapy import s, j, transfer
from matplotlib.pyplot import savefig, show

H = transfer((s - 2) * (s + 3) / (s * (s - 2 * j) * (s + 2 * j)))
H.plot();

View the transfer function:

H
../_images/overview_45_0.png

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.

Bode Plots

We can generate Bode plots given a particular transfer function. For example:

from lcapy import s, j, pi, f, transfer

from numpy import logspace
import matplotlib.pyplot as plt

H = transfer((s - 2) * (s + 3) / (s * (s - 2 * j) * (s + 2 * j)))

We can transform representations:

H(s), H(f), H(j * 2 * pi * f)
../_images/overview_50_0.png

Now generate the Bode plot:

fv = logspace(-1, 3, 400)

# db vs frequency
H(f).dB.plot(fv, log_scale=True)

# Phase
H(f).phase_degrees.plot(fv,log_scale=True);

The control Package

The control package provides a range of tools to support the analysis and design of feedback control system. The API provides compatibility with the MATLAB Control Systems Toolbox.

#%pip install control slycot
import control
import slycot # Makes for more efficient computation

A transfer function can be created by passing in the numerator and denominator values for the polynomial coefficients of the transfer function:

control.tf([1,2,3], [4,5,6])
\[\frac{s^2 + 2 s + 3}{4 s^2 + 5 s + 6}\]

We can define a system based on its transfer function from matrices defining the transfer functions between inputs and outputs.

For example, suppose the transfer function from the 2nd input to the 1st output is \(\frac{3s + 4}{6s^2 + 5s + 4}\).

num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]]
sys1 = control.tf(num, den)

sys1
\[\begin{split}\begin{bmatrix}\frac{s + 2}{9 s^2 + 8 s + 7}&\frac{3 s + 4}{6 s^2 + 5 s + 4}\\\frac{5 s + 6}{3 s^2 + 2 s + 1}&\frac{7 s + 8}{-s^2 - 2 s - 3}\\ \end{bmatrix}\end{split}\]

The StateSpace class is used to represent state-space realizations of linear time-invariant (LTI) systems

A simple system defined as ss(A, B, C, D) provides a state space definition using a matrix to represent its state and output equations:

\[\begin{split} \dot x = A.x + B.u \\ y = C.x + D.u \end{split}\]

for input \(u\), output \(y\) and state \(x\).

#State space system definition
sys = control.ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")

sys
\[\begin{split} \left( \begin{array}{rllrll|rll} 1\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&-2\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&5\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\ 3\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&-4\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&7\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\ \hline 6\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&8\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}&9\phantom{.}&\hspace{-1em}&\hspace{-1em}\phantom{\cdot}\\ \end{array}\right) \end{split}\]

We can access this directly as a numpy array if required:

sys.A
array([[ 1., -2.],
       [ 3., -4.]])

Generate a Nyquist diagram for the system:

control.nyquist_plot(sys, omega=None, plot=True, color='b');

Generate a Bode plot for the system:

mag, phase, omega = control.bode(sys)