The symbolic_poly toolkit¶
pycircuit.circuit selects its math backend through a toolkit: the same
analysis code runs numerically or symbolically depending on the toolkit passed
to it. symbolic_poly is an experimental symbolic toolkit that works in
sympy’s polynomial/rational domains instead of on free-form expressions.
It behaves like the stock symbolic toolkit but
solves the MNA system fraction-free (
DomainMatrix.solve_den) so intermediate results stay polynomials instead of the nested fractionsMatrix.LUsolveproduces — which lets it handle larger circuits, andexposes the result as a rational transfer function
N(s)/D(s)with poles, zeros, gain and a fast numeric evaluator.
It is opt-in; the symbolic toolkit is unchanged.
Note
Declare the frequency variable as sympy.Symbol('s', imaginary=True) (it
represents \(j\omega\)). This lets conjugation resolve cleanly in the
noise analysis and keeps results expressed in s.
Note
A fully symbolic circuit (every component a distinct symbol) has a network
determinant with exponentially many terms — no method yields a compact
closed form. symbolic_poly wins when there are few symbolic
generators; numeric component values with only s symbolic is the sweet
spot and scales to circuits with many nodes.
Quick start¶
import sympy
from pycircuit.circuit import symbolic_poly, use_toolkit, SubCircuit, R, C, VS, gnd
from pycircuit.circuit.analysis_ss import AC
s = sympy.Symbol('s', imaginary=True)
R1, C1 = sympy.symbols('R C', positive=True)
with use_toolkit(symbolic_poly):
cir = SubCircuit()
cir['VS'] = VS('in', gnd, vac=1)
cir['R'] = R('in', 'out', r=R1)
cir['C'] = C('out', gnd, c=C1)
res = AC(cir, toolkit=symbolic_poly).solve(s, complexfreq=True)
H = res.tf('out', gnd) # the transfer function to v(out)
H.canonical() # -> 1/(C*R*s + 1)
H.poles() # -> {-1/(C*R): 1}
H.dcgain() # -> 1
Transfer functions¶
An AC solve with symbolic_poly returns a CircuitResultACPoly that keeps
the numerator vector N and the shared denominator D (the network
determinant). From it you get transfer functions without a swelling
cancel:
res.tf(plus, minus=None)— the voltage transfer function tov(plus[, minus]).res.tf_i(branch_or_term)— the current transfer function into a terminal or branch (transimpedance / current gain).res.poles()— the circuit poles, computed once from the shared denominator for the whole circuit.
Each res.tf* returns a TransferFunction:
H = res.tf('out', gnd)
H.canonical() # cancelled num/den expression
H.num, H.den # raw numerator / denominator
H.poles() # {root: multiplicity}
H.zeros()
H.dcgain() # H(s=0)
H.bode() # a fast lambdified callable f(s)
H.frequencyresponse([1e3, 1e6, 1e9]) # complex response at those Hz
Current transfer functions and input impedance:
# source current of the RC low-pass above
res.tf_i('R.plus').canonical() # -> C*s/(C*R*s + 1)
res.tf_i('R.plus').zeros() # -> {0: 1} (no current at DC)
# input impedance: drive with a unit current source, then Zin = v(node)
from pycircuit.circuit import IS
with use_toolkit(symbolic_poly):
cir = SubCircuit()
cir['IS'] = IS(gnd, 'in', iac=1)
cir['R'] = R('in', gnd, r=R1)
cir['C'] = C('in', gnd, c=C1) # R || C
res = AC(cir, toolkit=symbolic_poly).solve(s, complexfreq=True)
res.tf('in', gnd).canonical() # -> R/(C*R*s + 1)
Poles and zeros at scale¶
Symbolic root finding has no closed form for degree \(\ge 5\). When the
transfer function has numeric coefficients, pass numeric=True to find
the roots with numpy.roots — fast and reliable regardless of degree:
with use_toolkit(symbolic_poly):
cir = SubCircuit()
cir['VS'] = VS('in', gnd, vac=1)
prev = 'in'
for i in range(6): # a 6-section RC ladder
cir['R%d' % i] = R(prev, 'n%d' % i, r=1e3)
cir['C%d' % i] = C('n%d' % i, gnd, c=1e-9)
prev = 'n%d' % i
res = AC(cir, toolkit=symbolic_poly).solve(s, complexfreq=True)
res.poles(numeric=True) # 6 complex poles, all on the negative real axis
numeric=True raises ValueError if any coefficient is still symbolic —
substitute parameter values first.
Noise¶
The noise analysis works with symbolic_poly unchanged. Internally the
output noise power zm**T CY conj(zm) is kept as a single shared-denominator
rational N**T CY conj(N) / (D conj(D)) instead of an
\(O(n^2)\) sum of divided rationals — the same value, a far smaller
expression:
from pycircuit.circuit import Noise
with use_toolkit(symbolic_poly):
cir = SubCircuit()
cir['vs'] = VS(1, gnd, vac=1)
cir['R'] = R(1, 2, r=R1)
cir['C1'] = C(2, gnd, c=C1)
noise = Noise(cir, inputsrc='vs', outputnodes=('2', gnd), toolkit=symbolic_poly)
res = noise.solve(s, complexfreq=True)
sympy.simplify(res['Svnout']) # -> -4*R*T*k/(C**2*R**2*s**2 - 1)
Selecting the toolkit¶
Pass toolkit= to an analysis, or scope the construction-time default with
the use_toolkit context manager (the recommended replacement for assigning
the deprecated circuit.default_toolkit global):
from pycircuit.circuit import use_toolkit, symbolic_poly
with use_toolkit(symbolic_poly):
cir = SubCircuit()
... # circuits built here use symbolic_poly
# default restored on exit