SLiCAP state-space representation
The instruction doStateSpace() returns the state-space representation of a circuit:
in which \(\mathbf{x}\) is the vector with the state variables, \(\mathbf{u}\) the vector with the inputs, and \(\mathbf{y}\) the vector with the outputs. The representation is the full one (MIMO): every independent source is an input and every nodal voltage and branch current is an output. The state variables are linear combinations of the network variables, the nodal voltages and branch currents that make up the vector \(\mathbf{y}\); conversely, every network variable, also a capacitor voltage or an inductor current that is not a state, follows from the states and the inputs through \(\mathbf{C}\) and \(\mathbf{D}\). A transfer from one source to one detector is read from the representation as a column of \(\mathbf{B}\) and \(\mathbf{D}\) and a row of \(\mathbf{C}\) and \(\mathbf{D}\):
The representation is obtained from the first-order MNA matrix \(\mathbf{M} = \mathbf{G} + s\mathbf{C}\) by an exact reduction:
The number of state variables equals the number of finite poles of the circuit, i.e. the degree of \(\det(\mathbf{M})\). It is not the number of capacitors and inductors: a capacitor across a voltage source, an inductor in series with a current source, a capacitor loop, or a pair of ideally coupled inductors contribute no state.
The reduction works with rational numbers and symbols, so the decision which directions are states is exact; no tolerance is involved. Circuits with symbolic element values give a symbolic representation.
An output that follows the derivative of a source (an improper output, for example the current through a voltage source with a capacitor across it) shows up as a term with the Laplace variable in \(\mathbf{D}\).
The state variables are named after physical quantities wherever the network allows it (the voltage across a capacitor, the current through an inductor); a state that is necessarily a combination of such quantities keeps the name \(x_k\).
Symbolic entries of the matrices are returned in the canonical form of a ratio of polynomials.
Controlled sources with a Laplace-rational transfer (including the operational amplifier models) are expanded into first-order form before the reduction, so their internal states are part of \(\mathbf{x}\).
SLiCAP output displayed on this manual page, is generated with the script: statespace.py, imported by Manual.py.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5statespace.py: SLiCAP scripts for the HTML help file
6"""
7import SLiCAP as sl
A passive network
10pn = sl.makeCircuit("sch/myPassiveNetwork.slicap_sch")
doStateSpace() takes only the circuit object and, optionally, the conversion type, the parameter definitions and the numeric flag (see General instruction format). The transfer type, the source, the detector, the loop gain reference and the step dictionary do not apply. listStateSpace() prints the result in the console:
12ssResult = sl.doStateSpace(pn)
13sl.listStateSpace(ssResult)
This yields:
State-space realization: dx/dt = A x + B u, y = C x + D u
x^T = [V_C2 V_C1 I_L1]
u^T = [V1]
y^T = [I_L1 I_V1 V_1 V_2 V_out]
A =
[-R_ell - R_s -1 ]
[------------- ------- 0 ]
[C_a*R_ell*R_s C_a*R_s ]
[ ]
[ -1 -1 -1 ]
[ ------- ------- ---]
[ C_b*R_s C_b*R_s C_b]
[ ]
[ 1 ]
[ 0 - 0 ]
[ L ]
B =
[ 1 ]
[-------]
[C_a*R_s]
[ ]
[ 1 ]
[-------]
[C_b*R_s]
[ ]
[ 0 ]
C =
[ 0 0 1]
[ ]
[ 1 1 ]
[--- --- 0]
[R_s R_s ]
[ ]
[ 0 0 0]
[ ]
[ 1 1 0]
[ ]
[ 1 0 0]
D =
[ 0 ]
[ ]
[-1 ]
[---]
[R_s]
[ ]
[ 1 ]
[ ]
[ 0 ]
[ ]
[ 0 ]
Two capacitors and one inductor, three states, and each state is a physical quantity: the voltages across the capacitors, \(V_{C2}\) and \(V_{C1}\), and the current through the inductor, \(I_{L1}\). The first row of \(\mathbf{A}\) is the node equation of out: the current that arrives through \(R_s\) and the parallel pair \(C_1 \parallel L_1\), \((V_1 - V_2)/R_s\), charges \(C_a\) and flows through \(R_{ell}\); the second row is the node equation of node 2, where the same current splits between \(C_b\) and \(L\); the third row is the branch equation of the inductor, \(L\,\mathrm{d}I_{L1}/\mathrm{d}t = V_{C1}\). The one input is the source voltage \(V_1\), and the five outputs are the network variables: the node voltage \(V_1\) has a direct feedthrough only (\(\mathbf{D}\)), \(V_{out}\) is a state, and \(V_2 = V_{C1} + V_{C2}\) and the source current \(I_{V1} = (V_{C1} + V_{C2} - V_1)/R_s\) are combinations.
The realization is available as the attribute .stateSpace of the result, a named tuple with the fields A, B, C, D, x, u and y:
15ss = ssResult.stateSpace
16print(ss.x, ss.u, ss.y)
17print(ss.A, ss.B, ss.C, ss.D)
This yields:
Matrix([[V_C2], [V_C1], [I_L1]]) Matrix([[V1]]) Matrix([[I_L1], [I_V1], [V_1], [V_2], [V_out]])
Matrix([[(-R_ell - R_s)/(C_a*R_ell*R_s), -1/(C_a*R_s), 0], [-1/(C_b*R_s), -1/(C_b*R_s), -1/C_b], [0, 1/L, 0]]) Matrix([[1/(C_a*R_s)], [1/(C_b*R_s)], [0]]) Matrix([[0, 0, 1], [1/R_s, 1/R_s, 0], [0, 0, 0], [1, 1, 0], [1, 0, 0]]) Matrix([[0], [-1/R_s], [1], [0], [0]])
Rendered with the formatter (see Formatters below):
A capacitor across a voltage source
21cv = sl.makeCircuit("cir/CacrossV.cir")
22
23cvResult = sl.doStateSpace(cv)
24sl.listStateSpace(cvResult)
1"Capacitor across a voltage source"
2V1 1 0 V value=0
3C1 1 0 C value={C_1}
4R1 1 2 R value={R_1}
5C2 2 0 C value={C_2}
6.end
This yields:
State-space realization: dx/dt = A x + B u, y = C x + D u
x^T = [V_C2]
u^T = [V1]
y^T = [I_V1 V_1 V_2]
A =
[ -1 ]
[-------]
[C_2*R_1]
B =
[ 1 ]
[-------]
[C_2*R_1]
C =
[ 1 ]
[---]
[R_1]
[ ]
[ 0 ]
[ ]
[ 1 ]
D =
[ 1 ]
[-C_1*s - ---]
[ R_1]
[ ]
[ 1 ]
[ ]
[ 0 ]
Two capacitors, one state, and it is the voltage across \(C_2\): the voltage across \(C_1\) is dictated by the source, so \(C_1\) stores nothing of its own, it is rejected as a state variable, and the only time constant is \(R_1C_2\). The source, however, has to deliver the current \(C_1\,\mathrm{d}V_1/\mathrm{d}t\), which is why the row of \(I_{V1}\) in \(\mathbf{D}\) contains the Laplace variable: this output is improper. Rendered with the formatter, showing only the outputs and the feedthrough:
Poles and zeros with the state-space engine
The state-space representation is meant for computing transfers: one realization holds every transfer of the circuit. For poles and zeros it offers no accuracy advantage in practice and it is considerably slower than the determinant, so the determinant remains the default engine. SLiCAP has two engines for the poles and zeros of a transfer, selected with the keyword method of doPoles(), doZeros() and doPZ():
method='det', the determinant engine. The numerator and the denominator of the transfer are determinants of the MNA matrix, computed exactly (ini.numer,ini.denom: minor expansion in Python or in the C++ engineMECPP). The poles and zeros are the roots of these polynomials, found numerically as the eigenvalues of the companion matrix (numpy). Fast. The number of roots is the degree of the polynomial. A coinciding pair of poles is a double root of the polynomial and comes out as two nearby roots.method='state', the state-space engine. The circuit is written as a first-order MNA matrix and reduced exactly to a state matrix \(\mathbf{A}\) as described above (for the zeros: the state matrix of the numerator system). The number of poles is the size of \(\mathbf{A}\), exact by construction. The poles are the eigenvalues of the exact matrix, computed after an exact balancing with the QR algorithm at 30 digits (mpmath), accurate to about \(10^{-12}\); coinciding poles are resolved as eigenvalues, not split. Slower than the determinant engine on numeric circuits of the size of the examples (a few tenths of a second for 14 states), because the exact reduction and the high-precision eigenvalues cost more than one determinant.method=None(the default):ini.pz_methoddecides for poles, zeros and pz analyses (default'det'); all other analyses use the determinant. Stepped analyses and circuits with symbolic element values always use the determinant.
Both engines give the same number of poles and zeros, because both start from an exact description: the degree of the exact determinant and the size of the exact state matrix are the same number, also for capacitor loops, inductor cut sets, ideal coupling and nullors. Both give the same values, and the test suite holds them to each other. The difference lies in coinciding poles or zeros. The state matrix is exact, the QR algorithm that computes its eigenvalues is not: a double eigenvalue splits at the level of the working precision, 30 digits, and coincides in every displayed digit. A double root of the polynomial splits at the level of the double precision of the root finder, typically in the eighth digit, and shows as two nearby roots. Use 'state' when poles or zeros coincide; use 'det' for speed, root loci and stepped analyses.
27pzResult = sl.doPZ(pn, pardefs="circuit", numeric=True, method="state")
28sl.listPZ(pzResult)
This yields:
DC value of gain: 9.00e-01
Poles of gain:
n Real part [Hz] Imag part [Hz] Frequency [Hz] Q [-]
-- -------------- -------------- -------------- --------
0 -1.57e+06 4.70e+06 4.96e+06 1.58e+00
1 -1.57e+06 -4.70e+06 4.96e+06 1.58e+00
2 -1.44e+05 0.00e+00 1.44e+05
Zeros of gain:
n Real part [Hz] Imag part [Hz] Frequency [Hz] Q [-]
-- -------------- -------------- -------------- --------
0 0.00e+00 -5.00e+06 5.00e+06 inf
1 0.00e+00 5.00e+06 5.00e+06 inf
Three poles, the size of \(\mathbf{A}\), and the two zeros of the parallel resonance \(C_b L\) at \(f_s = 5\,\mathrm{MHz}\), where the network transmits nothing.
The project setting ini.pz_method (pz_method in the [math] section of the project SLiCAP.ini) selects the engine for instructions that do not pass method; its default is 'det'. With doMatrix(cir, method='state') the first-order (expanded) MNA matrix itself is returned.
Formatters
The LaTeX and RST formatters have the method stateSpace(resultObject, label="", parts=None). It creates one aligned display: the vectors \(\mathbf{x}\), \(\mathbf{u}\) and \(\mathbf{y}\) as transposed rows, then the matrices \(\mathbf{A}\), \(\mathbf{B}\), \(\mathbf{C}\) and \(\mathbf{D}\), one object per line and aligned on the equal sign, so that large matrices never have to share a line. The keyword parts selects a subset, for example parts=("x", "A"); in LaTeX every line carries the label <label>-<name>. The TXT formatter has stateSpace(resultObject) with the console listing, and stateSpace2html() displays the realization on the active HTML page.
31rst = sl.RSTformatter()
32rst.stateSpace(ssResult, label="eqn-ss-PN").save("eqn-ss-PN")
33rst.stateSpace(ssResult, label="eqn-ss-PN-Ax", parts=("x", "A")).save("eqn-ss-PN-Ax")
34rst.stateSpace(cvResult, label="eqn-ss-CV").save("eqn-ss-CV")
35rst.stateSpace(cvResult, label="eqn-ss-CV-D", parts=("y", "D")).save("eqn-ss-CV-D")
36
37# Generate LaTeX snippets
38ltx = sl.LaTeXformatter()
39ltx.stateSpace(ssResult, label="eqn-ss-PN").save("eqn-ss-PN")
40ltx.stateSpace(cvResult, label="eqn-ss-CV").save("eqn-ss-CV")
The second snippet renders as:
Conversion type
With convtype='dd' or convtype='cc' the outputs are the differential-mode or common-mode variables of a balanced circuit and the inputs remain the independent sources; the state matrix is that of the dd or cc block. See The conversion type.