Lcapy: symbolic linear circuit analysis with Python

Lcapy is an open-source Python package for solving linear circuits symbolically. It uses a superposition of DC analysis, AC (phasor) analysis, transient (Laplace) analysis, and noise analysis. Expressions are evaluated using the computer algebra system SymPy. Lcapy can model circuits comprised of combinations of one-port and two-port networks or circuits specified using a netlist with a Spice-like notation. Lcapy can present the system of equations produced from nodal analysis, modified nodal analysis, loop analysis, and state-space analysis. Expressions can be formatted into many representations, parameterized, and transformed to other domains. Dimensional analysis is performed to reduce user errors and to present results with units. Both continuous and discrete signals are supported. Lcapy produces high-quality output. Textbook quality schematics in a number of different formats can be generated from netlists and customized for different conventions. Expressions can be formatted into LaTeX format for inclusion into a document or numerically evaluated and plotted. An overview of the features and capabilities of Lcapy is presented, along with implementation details and performance considerations.


INTRODUCTION
Lcapy is an open-source Python tool to symbolically generate and solve the system of integro-differential equations for a linear electrical, mechanical, or electro-mechanical circuit.
Unlike most other circuit analysis programs, Lcapy stores all values and expressions symbolically. It utilizes the powerful Python symbolic mathematics package, Sympy (Joyner et al., 2012;Meurer et al., 2017). The primary advantage of symbolic analysis is that it provides an exact solution and avoids the trial-and-error nature of system analysis using numerical simulation. Moreover, it avoids the instability and inaccuracy associated with numerical integration required by time-stepping simulation. The symbolic solution provides insight into system behavior, for example, how a component influences the poles of a power-electronics controller (Heffernan, Mitchell & Hayes, 2020). Using symbolic mathematical tools allows expressions to be easily converted to standard forms, such a pole-zero-gain or partial fraction, to gain further insight into system behaviour.
Lcapy started in 2013 as a collection of Python classes for handling rational functions for modelling piezo-electric transducers. This was augmented with a collection of component classes that could be organised into networks using parallel and series operators (Hayes, 2014). As components were added, the network was represented either as a Thévenin or Norton model. This was efficient since the network could be represented as a Laplace-domain generalized impedance with either a current source or a voltage source. Later, networks were stored as an abstract syntax tree to provide a more general representation. Netlists were then added to describe the component connectivity but analysis could only be performed in the Laplace domain. Analysis of networks was performed by converting them to netlists. Lcapy was then largely rewritten to represent voltages and currents as a superposition of DC, AC, transient, and noise signals. Thus, the response for arbitrary sources could be analysed. Many other features have since been added; such as semi-automated textbook-quality schematic generation (Hayes, 2016), discrete-time analysis, plotting, expression manipulation, network synthesis, and unit tracking.
Open-source tools are available for numerical circuit simulation, including SPICE (Nagel & Pederson, 1973) and its many derivatives, such as PySpice (https://pypi.org/ project/PySpice/), and Qucs (Brinson & Jahn, 2009), an enhancement to SPICE. These tools are excellent for the generation of numerical solutions to general, non-linear circuits. However, they do not provide the insight of symbolic analysis.
There is also an unnamed MATLAB toolbox that uses transfer function approximation (Vazzana, Grasso & Pennisi, 2017) and another unnamed tool that uses a graph reduction decision diagram (GRDD) algorithm (Chen & Shi, 2006). In addition, a summary of some older programs is given by Fernández & Rodríguez-Vázquez (1996).
Most of these tools are are designed specifically for integrated circuit analysis (Hsu & Sechen, 1993), are tied to a particular operating system, and are no longer available. A possible reason is that most are not open-source (Huelsman, 1996) and thus have not been maintained over time.
A novel aspect of Lcapy is that it decomposes signals into a number of transform domains (DC, phasor, Laplace), performs modified nodal analysis for each domain, and superimposes the result. A rich variety of transformations is provided to convert between the various domains and to present results in different formats. Lcapy uses an object-oriented approach and provides a comprehensive selection of expression classes to specify the transform domain and data quantity.
This paper is not a comprehensive description of the capabilities of Lcapy (this can be found in the supporting documentation (https://lcapy.readthedocs.io) (Hayes, 2020)). Instead, the focus is on the philosophy, use cases, and implementation. The paper starts with a brief overview in "Overview", the major use cases are described with some examples in "Basic usage", circuit analysis techniques are described in "Circuit Analysis", additional features are presented in "Additional Features", details of the implementation are discussed in "Implementation Details", and a performance analysis is shown in "Performance". The paper concludes with a discussion and conclusions in "Discussion" and "Conclusions and Future Work".

OVERVIEW
Lcapy is an open-source Python package for symbolic linear circuit analysis. Python was chosen since it is platform independent (van Rossum & Drake, 1995), can be used with a command-line interface or an integrated development environment, and has excellent packages for plotting-Matplotlib, symbolic analysis-Sympy (Joyner et al., 2012;Meurer et al., 2017), and numerical analysis-NumPy (Oliphant, 2006;van der Walt, Colbert & Varoquaux, 2011) and SciPy (Virtanen et al., 2020). It is also well documented with online support.
The author has used Lcapy for modelling low-noise operational amplifier circuits, designing analog and digital filters, simulating polyphase power systems, fitting models to electrode impedance data for electromagnetic flowmeters, designing controllers for power-electronics, modelling impedance compensation systems for biomedical instrumentation, and modelling piezoelectric transducers. He and others have also used Lcapy for producing textbook quality schematics, generating exam questions, checking exam and homework answers, and teaching students circuit theory.
An object-oriented design was chosen to simplify both the implementation and user interface. The classes provided by Lcapy range from simple expressions, to one-port and two-port networks, to complete netlist models. The object oriented approach makes available methods for a given object clear to the user. For example, time-domain objects have a Laplace transform method but Laplace-domain objects have an inverse Laplace transform method.
Lcapy uses an instance-based netlist similar to SPICE (Nagel & Pederson, 1973) to describe how components are connected. It is simply a list of named components, each with named nodes, see Listing 1. Circuit analysis is performed by converting the netlist into sub-netlists; one for each of the DC, AC, transient, and noise domains identified from the independent sources. Each of the sub-netlists are solved independently using modified nodal analysis and the results are stored as a superposition of DC, AC, transient, and noise signals. These superpositions can then be transformed to the time domain or Laplace domain as required by the user.
As well as electrical components, Lcapy netlists can contain masses, springs, and dampers for modelling mechanical or electro-mechanical networks. The mechanical analogue II (impedance analogue) is employed where voltage is equivalent to force and current is equivalent to speed. Thus a mass is analogous to an inductor, a spring is analogous to a capacitor, and a damper is analogous to a resistor.

BASIC USAGE
Lcapy has three major use cases: network and circuit analysis using combinations of one-port and two-port models; network and circuit analysis using netlists; and symbolic expression manipulation for interpretation of results. These are discussed in the following sections.

Expressions
Lcapy has a number of pre-defined variables associated with different domains: f for the Fourier (frequency) domain, F for the normalized Fourier domain, s for the Laplace (s) domain, t for the time domain, omega for the angular Fourier domain, and Omega for the normalized angular Fourier domain. These variables can be utilised to create expressions or to specify the result domain for transformations, for example 1 : >>> from lcapy import s, t, exp >>> H = s * (s + 2)/ (s + 3) * exp(-2 * s) >>> H(t) Here δ(t) is the Dirac delta and δ (1) (t) is the first derivative of the Dirac delta. As a consequence of the unilateral Laplace transform, the result is only known for t ≥ 0. However, if the result is known to be zero for t < 0, this can be specified by asserting the causal argument, for example: >>> from lcapy import s, t, exp >>> H = s * (s + 2) / (s + 3) * exp(-2 * s) >>> H(t, causal = True) 3e 6À3t u t À 2 ð ÞÀd t À 2 ð Þþd 1 ð Þ t À 2 ð Þ: Lcapy can display expressions in a number of forms. In each case the expressions are factored into a rational function and an optional exponential function (the latter is used to support Laplace-domain delays): HðsÞ ¼ NðsÞ DðsÞ expðÀsTÞ: Rational functions can be represented in many forms. Table 1 shows the representations that Lcapy supports.

Discrete-time expressions
Lcapy also has a number of pre-defined variables associated with discrete-time signals: k for the discrete-frequency sample index, n for the discrete-time sample index, and z Table 1 Rational function representation methods.

Sequences
Discrete-time, discrete-Fourier, and Z-domain expressions can be converted to and from sequences. These can be convolved, shifted, transformed, etc. For example, >>> from lcapy import symbols, delta, n where the underscore marks the origin. Similarly, sequences can be converted to expressions: >>> seq((1, 2, 3, 'a')).expr One-port networks Networks can be constructed by connecting one-port (two-terminal) and two-port components. Basic one-port components include: Vdc (DC voltage source), Idc (DC current source), Vac (AC voltage source), Iac (AC current source), R, G, C, and L. These are augmented by V (generic voltage source), I (generic current source), Y (generic admittance), and Z (generic impedance). More esoteric components include gyrators and constant phase elements.
Here is an example that combines an 8 V source in series with a parallel combination of 6 ohm and 3 ohm resistors: >>> from lcapy import V, R >>> net = V(8) + (R(6) | R(3)) >>> net Vð8Þ þ ðRð6ÞjRð3ÞÞ: Note, Lcapy overloads the Python | operator to denote a parallel operation. Also note that when the net object is printed, the underlying components forming the network are shown. This is achieved using an abstract syntax tree to represent the component connections.

Netlists
More complex circuit topologies are best described using netlists. The Lcapy netlist syntax is similar to that of SPICE. Usually nodes are numbered, but they can have symbolic names. The netlist can be loaded from a file or generated incrementally using the add method of the Circuit class. For example, Listing 1 describes the circuit shown in Fig. 2.
A Circuit object is collection of components and nodes. The nodes are accessed using Python's array-indexing notation, for example, Nodes have a number of attributes including: V-the voltage between the node and ground; dpY-the driving-point generalized admittance between the node and ground; dpZ-the driving-point generalized impedance between the node and ground. Thus the potential of node 2 with respect to ground can be found using: >>> cct[2].V Components are dynamically added as attributes of the Circuit class. Thus components can be accessed using the Python's attribute notation. For example: >>> cct.C1 Each component has several attributes as shown in Table 2. For example, the current through the inductor L1 in the netlist described by Listing 1 is found using: >>> from lcapy import Circuit The output is a dictionary showing the superposition of a DC component and a transient-component described in the Laplace domain.
The time-domain response can be found using Python's call notation: >>> cct.L1.I(t) This transforms each component of the superposition into the time domain and sums the result to give: Similarly, the total Laplace-domain response can be found using: A common circuit analysis problem is to find the transfer function between two pairs of nodes. There are several ways this can be achieved with Lcapy. For example, consider the filter network shown in Fig. 3. First, the independent sources need to be killed and an arbitrary Laplace-domain voltage source connected across the input nodes. The transfer function is then found from the ratio of the voltage measured across the output nodes to the input voltage. An alternative approach is to use the transfer() method which implicitly kills any independent sources. For example: >>> H = cct.transfer(1, 0, 2, 0) >>> H.canonical() A more general approach is to generate a two-port model. For example, the Zparameters can be found using: >>> tp = cct.twoport(1, 0, 2, 0) >>> tp.Zparams The common two-port models are supported, including the A, B, G, H, S, T, Y, and Z-parameter models (Hayt, Kemmerly & Durbin, 2006). They can be converted interchangeably provided the matrices are not singular.
Other methods of the Circuit class are shown in Table 3. These are augmented with methods for manipulating a netlist as listed in Table 4.

CIRCUIT ANALYSIS
As well as calculating node voltages and branch currents, Lcapy can also display the equations for nodal analysis, modified nodal analysis, mesh analysis, and state-space analysis. For the following, consider the circuit shown in Fig. 4 defined by the netlist in Listing 3. Isc() Returns the short-circuit current between two specified nodes Y() Returns the generalized admittance between specified nodes (with the independent sources killed) Z () Returns the generalized impedance between specified nodes (with the independent sources killed)  2 : This is a dictionary keyed by the circuit node names. The nodal equations can be formulated in the Laplace-domain using the laplace() method to convert the circuit. For example: >>> na = cct.laplace().nodal_analysis() >>> na.nodal_equations() f1 : V 1 ðsÞ ¼ VðsÞ; 2 : The matrix formulation can also be generated for the Laplace-domain, DC, and phasors.
This is a dictionary indexed by the mesh current.

State-space analysis
State-space analysis is performed by assigning capacitor voltages and inductor currents as state variables. The state equations are found using: >>> from lcapy import Circuit >>> cct = Circuit('example1.sch'). >>> ss = cct.state_space() >>> ss.state_equations() The state-space analyser can generate state-transition matrices, system transfer functions, system impulse responses, and the characteristic polynomial. For example, the characteristic polynomial for the circuit in Fig. 4 is generated by: >>> ss.P

Initial value problems
If any capacitor or inductor in a netlist has an initial value, the transient response is calculated as an initial value problem using Laplace transforms. The result is only valid for t ≥ 0.

ADDITIONAL FEATURES Network synthesis
Networks can be created using network synthesis techniques given an impedance or admittance expression. For example: >>> from lcapy import s, Impedance >>> Z = Impedance((4 * s**2 + 3 * s + '1 / 6') / (s**2 + 2 * s / 3)) >>> Z.network() ððCð1Þ þ Rð2ÞÞjCð3ÞÞ þ Rð4Þ: Note, in this example the ratio 1/6 is placed in quotes to prevent Python converting the result to a floating point number before Lcapy converts it to a rational number. This overcomes an unfortunate side-effect of the Python parser. Lcapy provides many network synthesis methods, including recognising the common parallel/serial combinations of resistors, inductors, and capacitors plus more generic methods such as Foster I, II and Cauer I, II (Bakshi & Bakshi, 2005).

Random networks
Networks with a random topology can generated with the random_network() function. This is useful for automated exam question generation. For example: >>> from lcapy import random_network >>> net = random_network(num_resistors = 4, num_voltage_sources = 1, kind = 'dc') This example generates a DC network with four resistors and one voltage source. The kind argument can be ac, dc, or transient. The number of parallel connections can be specified with the num_parallel argument.

Polyphase systems
The polyphase module provides classes for handling polyphase signals. This handles conversions between line voltages and currents to phase voltages and currents. In addition, it can decompose line and phase signals into symmetrical components and vice-versa. In each case, the polyphase signals are stored as vectors. An arbitrary number of phases can be supported, although there are many attributes specifically for three-phase systems, such as Vab that returns the phase-phase voltage between the a and b phases.

Printing
Expressions have printing methods similar to Sympy. These are summarised in Table 5 2 .
There are other methods for automatic formatting of the expression for a Jupyter notebook (Kluyver et al., 2016) or an IPython interpreter (Pérez & Granger, 2007). These include the use of MathJax (Cervone (2012)) for mathematics formatting in web browsers.

Evaluation
Expressions have an evaluate() method which accepts a NumPy array of values at which to evaluate the expression. Internally, Lcapy uses Sympy's lambdify() function to convert the expression to executable Python code that can be efficiently evaluated.

Plotting
Expressions have a plot() method. For Laplace-domain expressions, this is a pole-zero plot. For time-domain expressions, this shows the waveform. In addition, there are specific methods for Bode and Nyquist plots. The plot() methods provide automatic labelling and use the Matplotlib library for high-quality output in a number of formats (Hunter (2007)).

Schematics
Lcapy can draw circuits specified as networks or netlists. Networks are drawn automatically using a number of layout forms, including as a ladder network. Netlists require drawing hints to describe the component orientation, size, color, etc. These hints are used to automatically position the components. LaTeX CircuiTikz (https://www.ctan. org/pkg/circuitikz) macros are output producing text-book quality schematics. Lcapy provides a script, schtex, that can convert a netlist into an image, with a number of formats such as pdf or png. This script can also manipulate the netlist, such as renumbering the nodes.

Customisation
Lcapy has a config module that can be used for customisation. For example, the printing of ffiffiffiffiffiffi À1 p as i or j, aliases for functions such as the Dirac delta or Heaviside unit step, an extensible list of words used for formatting in a Roman font in LaTeX expressions, and which method Sympy uses for matrix inversion.

IMPLEMENTATION DETAILS
Lcapy is implemented using 393 classes within 115 modules 3 . Most modules have a single class but a number, such as Oneport, represent many types of similar one-port component. Lcapy has an additional 170 expression classes that are lazily defined as required.

Expressions
Expressions are represented using one of the 170 dynamic expression classes 4 . Each of these inherit from a quantity class (voltage, current, impedance, etc.) and from a domain Lcapy expressions are created from factory functions, including: expr() for general expressions, voltage(), current(), impedance(), admittance(), and transfer() (for transfer functions). For example, the expr() factory function chooses one of the expression classes listed in Table 6. Similarly, the voltage() factory function chooses one of the voltage classes, subclassed from a corresponding expression class.
There are two special superposition classes: SuperpositionCurrent and SuperpositionVoltage. These represent signals as a superposition of DC, phasor, transient, and noise signals. They collate the results from the different circuit analysis methods before conversion to the time domain, Laplace domain, etc.
All Lcapy expression classes sub-class the Expr class. This facade class provides a common interface to the myriad of Sympy expression classes. Note, Sympy represents an expression using an abstract syntax tree of expression classes (Meurer et al., 2017). For example, it includes simple singleton classes to represent common constants such as 0 or 1, the Mul class to represent the product of two sub-expressions, the Add class to represent the sum of two sub-expressions, etc.
The Expr class also tracks the units of an expression using the Sympy units sub-system with an Lcapy extension that simplifies products of units into canonical forms. Units can be printed after expressions in both standard and abbreviated forms.
Lcapy has a Matrix class to extend the Sympy Matrix class. It also has classes to extend list, tuple, and dict. These provide a unified set of methods for printing of expressions.

Numbers
Lcapy converts floating point numbers to rational numbers of arbitrary precision. This improves detection of equality between expressions and reduces the order of some rational functions.

Noise signals
Lcapy assumes noise signals are zero-mean, wide-sense stationary, Gaussian random processes. Correlations are specified by a one-sided amplitude spectral density. Each noise source is assigned a noise identifier (nid) attribute. Noise expressions with different nids are assumed to be independent and thus represent different noise realisations. Lcapy analyses the circuit for each noise realisation independently and stores the result for each realisation separately, using a SuperpositionVoltage or SuperpositionCurrent object. For example: >>> from lcapy import Circuit >>> cct = Circuit(" " " … Vn1 1 0 noise 3 … Vn2 2 1 noise 4" " ") >>> cct[2].V n1 : 3; n2 : 4 f g : In this example Vn1 defines a white Gaussian noise source with an amplitude spectral density (ASD) of 3 V= ffiffiffiffiffiffi Hz p . It has a noise identifier n1. Similarly, Vn2 defines an independent white Gaussian noise source with an ASD of 4 V= ffiffiffiffiffiffi Hz p . It has a noise identifier n2. The voltage at node 2 can be seen to be a superposition of the two noise signals. The total noise ASD at node 2 is found using the n attribute. This sums the noise components in quadrature, i.e., ffiffiffiffiffiffiffiffiffiffiffiffiffiffi ffi 3 2 þ 4 2 p , since the noise components have different nids and are thus considered independent.

Netlists
Netlists have a Spice-like syntax and are parsed with a custom parser. A parser based on lex-yacc (Johnson, 1975) was tried but found to give poor messages for syntax errors. Each netlist component has an optional orientation hint and drawing attributes. These are used to semi-automatically draw a schematic, using a graph-based approach to define the component positions.
Netlists are stored as ordered dictionaries. This simplifies the lookup of components by name and preserves the component order whenever a netlist is converted into another form.

Modified nodal analysis
Circuit analysis is performed using modified nodal analysis (MNA) (Ho, Ruehli & Brennan, 1975). Essentially, this analyses the circuit by solving a set of simultaneous linear equations, (2) where x is a vector of unknown nodal voltages and unknown currents through dependent voltage sources, b is a vector of the known voltage and current sources, and A is the system matrix. For example, the system of equations for the circuit shown in Fig. 2 and described by the netlist shown in Listing 1 is: Here V 1 and V 2 are the unknown nodal voltages and I Vs is the unknown current through the independent voltage source V s . Note, since the voltage source is DC, its Laplace-domain voltage is V s =s.
Lcapy uses a custom implementation of modified nodal analysis and supports controlled sources (VCCS, CCVS, etc.), opamps, gyrators, mutual inductances, transformers, and two-ports. Each component has a stamp() method that fills in the system matrices.

Transforms
Lcapy supports the Laplace and the Fourier transform, the discrete-time equivalents (the Z-transform and the discrete-time Fourier transform (DTFT)), plus the discrete Fourier transform (DFT). The inverse transforms are also supported. Where possible, Lcapy breaks an expression into terms and evaluates each term. All results are cached.

Laplace transform
Lcapy uses the L À form of the Laplace transform 5 , Lcapy can evaluate most common Laplace transforms. To improve performance, it caches results. Expressions that Lcapy cannot recognise are passed to Sympy. Unfortunately, Sympy uses the L form of the Laplace transform, However, this only affects Dirac deltas (and their derivatives) and these are handled by Lcapy.
Time-domain expressions are determined using an inverse Laplace transform of the corresponding Laplace-domain expression. The Sympy inverse Laplace transform routines were found to be slow and unreliable and so were augmented with a custom method. This was applied when the expression could be factored into the product of a rational function and an exponential, as described by (1). After finding the poles of the rational function, the residues of the partial fraction expansion are determined using a custom algorithm. When pairs of unrepeated conjugate poles are detected, sines and cosines are generated, otherwise exponential functions (and their derivatives for repeated poles) are generated.

Fourier transform
The Fourier transform computed by Lcapy is Both the forward and inverse Fourier transforms are performed using the same algorithm with a change of variable. Here is an example of usage: >>> from lcapy import expr, f >>> v = expr('sin(2 * pi * f0 * t)') >>> The Fourier transform can also be expressed in terms of angular frequency using a change of variable. For example: 2 :

Z-transform
The unilateral Z-transform is computed as v½nz Àn : Lcapy recognises many common discrete-time signals and returns the appropriate Z-transform. For inverse Z-transforms, Lcapy uses partial fraction expansion to simplify expressions.

Discrete-time Fourier transform
The DTFT is computed as The normalized linear and angular frequency forms are handled with a change of variable. For example, the DTFT of the rect function in terms of normalized angular frequency, Ω: >>> from lcapy import symbol, rect, n, Omega >>> N = symbol('N', even = True) >>> x = rect(n/N) >>> x(Omega) e j 2 sin N 2 sin 2 ; and the DTFT of the normalized sinc function in terms of the normalized frequency, F: >>> from lcapy import symbol, sincn, n, F >>> x = sincn(n * 'K') >>> x(F) One-port components The one-port components are sub-classed from the OnePort class. Parallel and series combinations are represented by the Par and Ser classes. Both of these combinations are also OnePort objects.

Two-port components
Two-port components are based on the TwoPortModel class. This class contains a 2 × 2 matrix of expressions for the two-port parameters. This is augmented with a vector of two voltages, two currents, or a voltage/current pair to describe the effects of independent sources within the circuit. For example, the equations describing a B-parameter two-port model are: Here V 2b and I 2b model the independent sources within the circuit. Similarly, the equations used to describe a Z-parameter two-port model are: In this case, a pair of voltages (one for each port) are required to model the independent sources within the circuit, see Figs. 6 and 7.

Nodal and mesh analysis
Nodal and mesh analysis use the CircuitGraph class to convert a netlist into a graph. The Python package Networkx (Hagberg, Swart & Chult, 2008) is employed to determine the loops in the graph. Unfortunately, Networkx cannot find loops when there are parallel edges in a graph, and so Lcapy adds dummy nodes and dummy wires to avoid parallel edges (see Fig. 8).

PERFORMANCE
The performance of Lcapy is strongly dependent on the matrix inversion algorithm used by Sympy. This constrains the complexity of the circuit that can be solved interactively. Figure 9 shows the time taken to determine the open-circuit voltage for twenty randomly-generated networks with a specified number of components. Each network has a single voltage source and a number of resistors, and its open-circuit voltage was computed using: >>> net = random_network(num_resistors = NR, num_voltage_sources = 1, kind = 'dc') >>> net.Voc The timing tests were performed on an HP Elitebook 850 G6 using a single core of an Intel i5-8365U CPU running at 1.60 GHz. It is apparent that the maximum computation time increases exponentially with the number of components. The computation time is also highly variable for a given number of components, with over three orders of magnitude variation for 11 components. This is partly due to the MNA matrix size, but it also depends on the number of terms that each matrix element has. The variation of time with the MNA matrix size is not obvious, as shown in Fig. 10.
Sympy has a number of matrix inversion algorithms. Historically, Lcapy used Gaussian elimination since it was fastest, however, the latest version of Sympy has disappointing performance for some of the MNA matrices generated by Lcapy. The other methods produce a similar maximum time, as shown in Fig. 11. The exception is the new domain matrix (DM) algorithm, currently under development, which shows promising results. These algorithms are O(N 3 ) as expected (Krishnamoorthy & Menon, 2013) but only for small N × N matrix sizes. For larger N, the computation time is slower due to the additional arithmetic required to maintain all the terms.

Documentation
Documentation is stored as Restructured Text and converted to HTML/PDF using Sphinx (http://sphinx-doc.org/sphinx.pdf). On-line documentation can be found at

DISCUSSION
Lcapy leverages from the strengths of Python as an interpreted programming language and Sympy as a computer algebra system. Its advantage over other circuit analysis programs is that it can generate and solve systems of integro-differential equations. The symbolic solutions explicitly shows how component values affects circuit behaviour and allows an exact solution to be calculated. A unique feature of Lcapy is that it can perform noise analysis symbolically. Lcapy can present the circuit equations for nodal analysis, modified nodal analysis, loop analysis, and state-space analysis. Expressions can be represented in many different formats, parameterized, and transformed between domains, such as Laplace and Fourier. Expressions can be associated with a quantity to ensure correct dimensional calculations and to print expressions with units.
Textbook-quality schematics can be generated in a number of formats from a network or netlist description. These can be customised for different conventions.
Symbolic analysis provides an exact solution and avoids numerical inaccuracies and instabilities. However, it is much slower for large problems. The time-critical functions are matrix inversion for modified nodal analysis and pole/zero finding. The computation time could be improved using the Simplification Before Generation (SBG) approach (Kolka, Biolek & Biolkova, 2008;Kolka, Vlk & Horak, 2011). To mitigate slow computation, wherever possible Lcapy uses lazy evaluation and caches results.
Sympy is an excellent tool but can be confusing for novices since it allows symbols of the same name but with different assumptions. Thus the symbols are not equivalent. Lcapy tries to resolve this problem by keeping a registry of symbols and looking them up by name. The disadvantage of this approach is that symbol assumptions cannot be changed.
Lcapy has a number of circuit transformations but further work could add more advanced circuit transformations, similar to programs like SAPWIN (Luchetta, Manetti & Reatti, 2001). Another improvement for circuit analysis teaching would be to incorporate a trainer program (Weyten, Rombouts & De Maeyer, 2009).

CONCLUSIONS AND FUTURE WORK
Lcapy is an open-source Python package, designed to generate and solve systems of equations for linear circuit analysis. It supports many aspects of circuit theory, including two-port networks, network synthesis, and polyphase systems. The author is aware of its use for generating exam questions, checking exam and homework answers, teaching students circuit theory and signal processing, simulating polyphase power systems, designing controllers for power-electronics, modelling low-noise operational amplifier circuits, and modelling electromechanical systems. There are many other potential applications.
Planned work is to improve compatibility with PySpice and to add a graphical user interface (GUI) to enter and display circuits schematically. This could be achieved by importing and exporting netlists or implementing the GUI in Python and using Lcapy as a module. A GUI is useful from a circuit teaching perspective since components could be selected, the current/voltage interrogated and specific transforms applied.