31  SIDO-low-pass-filter

Figure 31.1: SIDO low pass filter

31.1 Introduction

The circuit shown in Figure 31.1, is from an electronics stackexchange question, where user349644 (2023) asks for help in determining the transfer function of the circuit. The circuit appears to be a low pass filter for the differential input to a following stage. I’m calling this circuit a Single-In-Dual-Out (SIDO) filter, where the output is the voltage across C3. An interesting response to the question was provided by Franc (2023), who has provided some interesting solutions to other circuit analysis questions. Usually his solutions are in the from of an image captured from a MathCAD type program consisting of equations, diagrams and plots.

31.2 Circuit description

The circuit has nine branches, five nodes and was interned into LTSpice in order to capture the schematic and generate the netlist.

31.3 Circuit analysis

The following analysis was performed.

  • solve network equations for the transfer function
  • pole, zero plot
  • frequency response plot
  • step and impusle response
  • group delay

The net list for the filter is:

V1 1 0 1
C1 1 0 2.2n
C2 4 5 330p
C3 2 3 100p
R1 1 0 100k
R2 4 1 4.7k
R3 5 0 4.7k
R4 2 4 10k
R5 3 5 10k

The following Python modules are used.

from sympy import *
import numpy as np
from tabulate import tabulate
import pandas as pd
from scipy import signal
import matplotlib.pyplot as plt
import SymMNA
from IPython.display import display, Markdown, Math, Latex
init_printing()

31.3.1 Load the netlist

The netlist generated by LTSpice is pasted into the cell below and some edits were made to remove the inductor series resistance and the independent sources are set to their DC values.

net_list = '''
V1 1 0 1
C1 1 0 2.2e-9
C2 4 5 330e-12
C3 2 3 100e-12
R1 1 0 100e3
R2 4 1 4.7e3
R3 5 0 4.7e3
R4 2 4 10e3
R5 3 5 10e3
'''

Generate the network equations.

report, network_df, df2, A, X, Z = SymMNA.smna(net_list)

# Put matricies into SymPy 
X = Matrix(X)
Z = Matrix(Z)

NE_sym = Eq(A*X,Z)

Generate markdown text to display the network equations.

temp = ''
for i in range(len(X)):
    temp += '${:s}$<br>'.format(latex(Eq((A*X)[i:i+1][0],Z[i])))

Markdown(temp)

\(I_{V1} + v_{1} \left(C_{1} s + \frac{1}{R_{2}} + \frac{1}{R_{1}}\right) - \frac{v_{4}}{R_{2}} = 0\)
\(- C_{3} s v_{3} + v_{2} \left(C_{3} s + \frac{1}{R_{4}}\right) - \frac{v_{4}}{R_{4}} = 0\)
\(- C_{3} s v_{2} + v_{3} \left(C_{3} s + \frac{1}{R_{5}}\right) - \frac{v_{5}}{R_{5}} = 0\)
\(- C_{2} s v_{5} + v_{4} \left(C_{2} s + \frac{1}{R_{4}} + \frac{1}{R_{2}}\right) - \frac{v_{2}}{R_{4}} - \frac{v_{1}}{R_{2}} = 0\)
\(- C_{2} s v_{4} + v_{5} \left(C_{2} s + \frac{1}{R_{5}} + \frac{1}{R_{3}}\right) - \frac{v_{3}}{R_{5}} = 0\)
\(v_{1} = V_{1}\)

As shown above MNA generated many equations and these would be difficult to solve by hand and a symbolic soultion would take a lot of computing time. The equations are displace in matrix notation.

NE_sym

\(\displaystyle \left[\begin{matrix}I_{V1} + v_{1} \left(C_{1} s + \frac{1}{R_{2}} + \frac{1}{R_{1}}\right) - \frac{v_{4}}{R_{2}}\\- C_{3} s v_{3} + v_{2} \left(C_{3} s + \frac{1}{R_{4}}\right) - \frac{v_{4}}{R_{4}}\\- C_{3} s v_{2} + v_{3} \left(C_{3} s + \frac{1}{R_{5}}\right) - \frac{v_{5}}{R_{5}}\\- C_{2} s v_{5} + v_{4} \left(C_{2} s + \frac{1}{R_{4}} + \frac{1}{R_{2}}\right) - \frac{v_{2}}{R_{4}} - \frac{v_{1}}{R_{2}}\\- C_{2} s v_{4} + v_{5} \left(C_{2} s + \frac{1}{R_{5}} + \frac{1}{R_{3}}\right) - \frac{v_{3}}{R_{5}}\\v_{1}\end{matrix}\right] = \left[\begin{matrix}0\\0\\0\\0\\0\\V_{1}\end{matrix}\right]\)

The symbols generated by the Python code are extraced by the SymPy function free_symbols and then declared as SymPy variables.

# turn the free symbols into SymPy variables
var(str(NE_sym.free_symbols).replace('{','').replace('}',''))

\(\displaystyle \left( R_{3}, \ s, \ C_{2}, \ C_{1}, \ R_{5}, \ R_{2}, \ v_{2}, \ v_{5}, \ V_{1}, \ R_{1}, \ v_{1}, \ C_{3}, \ v_{3}, \ v_{4}, \ R_{4}, \ I_{V1}\right)\)

Built a dictionary of element values.

element_values = SymMNA.get_part_values(network_df)
element_values

\(\displaystyle \left\{ C_{1} : 2.2 \cdot 10^{-9}, \ C_{2} : 3.3 \cdot 10^{-10}, \ C_{3} : 1.0 \cdot 10^{-10}, \ R_{1} : 100000.0, \ R_{2} : 4700.0, \ R_{3} : 4700.0, \ R_{4} : 10000.0, \ R_{5} : 10000.0, \ V_{1} : 1.0\right\}\)

Generate the symbolic solution

U_sym = solve(NE_sym,X)

Display the symbolic solution

temp = ''
for i in U_sym.keys():
    temp += '${:s} = {:s}$<br>'.format(latex(i),latex(U_sym[i]))

Markdown(temp)

\(v_{1} = V_{1}\)
\(v_{2} = \frac{C_{2} C_{3} R_{3} R_{4} V_{1} s^{2} + C_{2} C_{3} R_{3} R_{5} V_{1} s^{2} + C_{2} R_{3} V_{1} s + C_{3} R_{3} V_{1} s + C_{3} R_{5} V_{1} s + V_{1}}{C_{2} C_{3} R_{2} R_{4} s^{2} + C_{2} C_{3} R_{2} R_{5} s^{2} + C_{2} C_{3} R_{3} R_{4} s^{2} + C_{2} C_{3} R_{3} R_{5} s^{2} + C_{2} R_{2} s + C_{2} R_{3} s + C_{3} R_{2} s + C_{3} R_{3} s + C_{3} R_{4} s + C_{3} R_{5} s + 1}\)
\(v_{3} = \frac{C_{2} C_{3} R_{3} R_{4} V_{1} s^{2} + C_{2} C_{3} R_{3} R_{5} V_{1} s^{2} + C_{2} R_{3} V_{1} s + C_{3} R_{3} V_{1} s + C_{3} R_{5} V_{1} s}{C_{2} C_{3} R_{2} R_{4} s^{2} + C_{2} C_{3} R_{2} R_{5} s^{2} + C_{2} C_{3} R_{3} R_{4} s^{2} + C_{2} C_{3} R_{3} R_{5} s^{2} + C_{2} R_{2} s + C_{2} R_{3} s + C_{3} R_{2} s + C_{3} R_{3} s + C_{3} R_{4} s + C_{3} R_{5} s + 1}\)
\(v_{4} = \frac{C_{2} C_{3} R_{3} R_{4} V_{1} s^{2} + C_{2} C_{3} R_{3} R_{5} V_{1} s^{2} + C_{2} R_{3} V_{1} s + C_{3} R_{3} V_{1} s + C_{3} R_{4} V_{1} s + C_{3} R_{5} V_{1} s + V_{1}}{C_{2} C_{3} R_{2} R_{4} s^{2} + C_{2} C_{3} R_{2} R_{5} s^{2} + C_{2} C_{3} R_{3} R_{4} s^{2} + C_{2} C_{3} R_{3} R_{5} s^{2} + C_{2} R_{2} s + C_{2} R_{3} s + C_{3} R_{2} s + C_{3} R_{3} s + C_{3} R_{4} s + C_{3} R_{5} s + 1}\)
\(v_{5} = \frac{C_{2} C_{3} R_{3} R_{4} V_{1} s^{2} + C_{2} C_{3} R_{3} R_{5} V_{1} s^{2} + C_{2} R_{3} V_{1} s + C_{3} R_{3} V_{1} s}{C_{2} C_{3} R_{2} R_{4} s^{2} + C_{2} C_{3} R_{2} R_{5} s^{2} + C_{2} C_{3} R_{3} R_{4} s^{2} + C_{2} C_{3} R_{3} R_{5} s^{2} + C_{2} R_{2} s + C_{2} R_{3} s + C_{3} R_{2} s + C_{3} R_{3} s + C_{3} R_{4} s + C_{3} R_{5} s + 1}\)
\(I_{V1} = \frac{- C_{1} C_{2} C_{3} R_{1} R_{2} R_{4} V_{1} s^{3} - C_{1} C_{2} C_{3} R_{1} R_{2} R_{5} V_{1} s^{3} - C_{1} C_{2} C_{3} R_{1} R_{3} R_{4} V_{1} s^{3} - C_{1} C_{2} C_{3} R_{1} R_{3} R_{5} V_{1} s^{3} - C_{1} C_{2} R_{1} R_{2} V_{1} s^{2} - C_{1} C_{2} R_{1} R_{3} V_{1} s^{2} - C_{1} C_{3} R_{1} R_{2} V_{1} s^{2} - C_{1} C_{3} R_{1} R_{3} V_{1} s^{2} - C_{1} C_{3} R_{1} R_{4} V_{1} s^{2} - C_{1} C_{3} R_{1} R_{5} V_{1} s^{2} - C_{1} R_{1} V_{1} s - C_{2} C_{3} R_{1} R_{4} V_{1} s^{2} - C_{2} C_{3} R_{1} R_{5} V_{1} s^{2} - C_{2} C_{3} R_{2} R_{4} V_{1} s^{2} - C_{2} C_{3} R_{2} R_{5} V_{1} s^{2} - C_{2} C_{3} R_{3} R_{4} V_{1} s^{2} - C_{2} C_{3} R_{3} R_{5} V_{1} s^{2} - C_{2} R_{1} V_{1} s - C_{2} R_{2} V_{1} s - C_{2} R_{3} V_{1} s - C_{3} R_{1} V_{1} s - C_{3} R_{2} V_{1} s - C_{3} R_{3} V_{1} s - C_{3} R_{4} V_{1} s - C_{3} R_{5} V_{1} s - V_{1}}{C_{2} C_{3} R_{1} R_{2} R_{4} s^{2} + C_{2} C_{3} R_{1} R_{2} R_{5} s^{2} + C_{2} C_{3} R_{1} R_{3} R_{4} s^{2} + C_{2} C_{3} R_{1} R_{3} R_{5} s^{2} + C_{2} R_{1} R_{2} s + C_{2} R_{1} R_{3} s + C_{3} R_{1} R_{2} s + C_{3} R_{1} R_{3} s + C_{3} R_{1} R_{4} s + C_{3} R_{1} R_{5} s + R_{1}}\)

Transfer function

H_sym = ((U_sym[v2]-U_sym[v3])/U_sym[v1]).nsimplify().simplify().expand().together()
H_sym

\(\displaystyle \frac{1}{C_{2} C_{3} R_{2} R_{4} s^{2} + C_{2} C_{3} R_{2} R_{5} s^{2} + C_{2} C_{3} R_{3} R_{4} s^{2} + C_{2} C_{3} R_{3} R_{5} s^{2} + C_{2} R_{2} s + C_{2} R_{3} s + C_{3} R_{2} s + C_{3} R_{3} s + C_{3} R_{4} s + C_{3} R_{5} s + 1}\)

cancel(H_sym,s)

\(\displaystyle \frac{1}{s^{2} \left(C_{2} C_{3} R_{2} R_{4} + C_{2} C_{3} R_{2} R_{5} + C_{2} C_{3} R_{3} R_{4} + C_{2} C_{3} R_{3} R_{5}\right) + s \left(C_{2} R_{2} + C_{2} R_{3} + C_{3} R_{2} + C_{3} R_{3} + C_{3} R_{4} + C_{3} R_{5}\right) + 1}\)

H_sym_num, H_sym_denom = fraction(H_sym) #returns numerator and denominator
solve(H_sym_denom,s)

\(\displaystyle \left[ \frac{- C_{2} R_{2} - C_{2} R_{3} - C_{3} R_{2} - C_{3} R_{3} - C_{3} R_{4} - C_{3} R_{5} - \sqrt{C_{2}^{2} R_{2}^{2} + 2 C_{2}^{2} R_{2} R_{3} + C_{2}^{2} R_{3}^{2} + 2 C_{2} C_{3} R_{2}^{2} + 4 C_{2} C_{3} R_{2} R_{3} - 2 C_{2} C_{3} R_{2} R_{4} - 2 C_{2} C_{3} R_{2} R_{5} + 2 C_{2} C_{3} R_{3}^{2} - 2 C_{2} C_{3} R_{3} R_{4} - 2 C_{2} C_{3} R_{3} R_{5} + C_{3}^{2} R_{2}^{2} + 2 C_{3}^{2} R_{2} R_{3} + 2 C_{3}^{2} R_{2} R_{4} + 2 C_{3}^{2} R_{2} R_{5} + C_{3}^{2} R_{3}^{2} + 2 C_{3}^{2} R_{3} R_{4} + 2 C_{3}^{2} R_{3} R_{5} + C_{3}^{2} R_{4}^{2} + 2 C_{3}^{2} R_{4} R_{5} + C_{3}^{2} R_{5}^{2}}}{2 C_{2} C_{3} \left(R_{2} R_{4} + R_{2} R_{5} + R_{3} R_{4} + R_{3} R_{5}\right)}, \ \frac{- C_{2} R_{2} - C_{2} R_{3} - C_{3} R_{2} - C_{3} R_{3} - C_{3} R_{4} - C_{3} R_{5} + \sqrt{C_{2}^{2} R_{2}^{2} + 2 C_{2}^{2} R_{2} R_{3} + C_{2}^{2} R_{3}^{2} + 2 C_{2} C_{3} R_{2}^{2} + 4 C_{2} C_{3} R_{2} R_{3} - 2 C_{2} C_{3} R_{2} R_{4} - 2 C_{2} C_{3} R_{2} R_{5} + 2 C_{2} C_{3} R_{3}^{2} - 2 C_{2} C_{3} R_{3} R_{4} - 2 C_{2} C_{3} R_{3} R_{5} + C_{3}^{2} R_{2}^{2} + 2 C_{3}^{2} R_{2} R_{3} + 2 C_{3}^{2} R_{2} R_{4} + 2 C_{3}^{2} R_{2} R_{5} + C_{3}^{2} R_{3}^{2} + 2 C_{3}^{2} R_{3} R_{4} + 2 C_{3}^{2} R_{3} R_{5} + C_{3}^{2} R_{4}^{2} + 2 C_{3}^{2} R_{4} R_{5} + C_{3}^{2} R_{5}^{2}}}{2 C_{2} C_{3} \left(R_{2} R_{4} + R_{2} R_{5} + R_{3} R_{4} + R_{3} R_{5}\right)}\right]\)

31.3.2 Numerical solution

NE = NE_sym.subs(element_values)
NE

\(\displaystyle \left[\begin{matrix}I_{V1} + v_{1} \cdot \left(2.2 \cdot 10^{-9} s + 0.000222765957446809\right) - 0.000212765957446809 v_{4}\\- 1.0 \cdot 10^{-10} s v_{3} + v_{2} \cdot \left(1.0 \cdot 10^{-10} s + 0.0001\right) - 0.0001 v_{4}\\- 1.0 \cdot 10^{-10} s v_{2} + v_{3} \cdot \left(1.0 \cdot 10^{-10} s + 0.0001\right) - 0.0001 v_{5}\\- 3.3 \cdot 10^{-10} s v_{5} - 0.000212765957446809 v_{1} - 0.0001 v_{2} + v_{4} \cdot \left(3.3 \cdot 10^{-10} s + 0.000312765957446809\right)\\- 3.3 \cdot 10^{-10} s v_{4} - 0.0001 v_{3} + v_{5} \cdot \left(3.3 \cdot 10^{-10} s + 0.000312765957446809\right)\\v_{1}\end{matrix}\right] = \left[\begin{matrix}0\\0\\0\\0\\0\\1.0\end{matrix}\right]\)

U = solve(NE,X)
U

\(\displaystyle \left\{ I_{V1} : \frac{- 1.452 \cdot 10^{18} s^{3} - 1.49089787234043 \cdot 10^{24} s^{2} - 2.86214893617021 \cdot 10^{29} s - 1.06382978723394 \cdot 10^{33}}{6.6 \cdot 10^{26} s^{2} + 6.42765957446809 \cdot 10^{32} s + 1.06382978723404 \cdot 10^{38}}, \ v_{1} : 1.0, \ v_{2} : \frac{660000000.0 s^{2} + 642765957446809.0 s + 2.12765957446809 \cdot 10^{20}}{1320000000.0 s^{2} + 1.28553191489362 \cdot 10^{15} s + 2.12765957446809 \cdot 10^{20}}, \ v_{3} : \frac{660000000.0 s^{2} + 642765957446809.0 s}{1320000000.0 s^{2} + 1.28553191489362 \cdot 10^{15} s + 2.12765957446809 \cdot 10^{20}}, \ v_{4} : \frac{330000000.0 s^{2} + 427765957446809.0 s + 1.06382978723404 \cdot 10^{20}}{660000000.0 s^{2} + 642765957446809.0 s + 1.06382978723404 \cdot 10^{20}}, \ v_{5} : \frac{330000000.0 s^{2} + 215000000000000.0 s}{660000000.0 s^{2} + 642765957446809.0 s + 1.06382978723404 \cdot 10^{20}}\right\}\)

temp = ''
for i in U.keys():
    temp += '${:s} = {:s}$<br>'.format(latex(i),latex(U[i]))

Markdown(temp)

\(v_{1} = 1.0\)
\(v_{2} = \frac{660000000.0 s^{2} + 642765957446809.0 s + 2.12765957446809 \cdot 10^{20}}{1320000000.0 s^{2} + 1.28553191489362 \cdot 10^{15} s + 2.12765957446809 \cdot 10^{20}}\)
\(v_{3} = \frac{660000000.0 s^{2} + 642765957446809.0 s}{1320000000.0 s^{2} + 1.28553191489362 \cdot 10^{15} s + 2.12765957446809 \cdot 10^{20}}\)
\(v_{4} = \frac{330000000.0 s^{2} + 427765957446809.0 s + 1.06382978723404 \cdot 10^{20}}{660000000.0 s^{2} + 642765957446809.0 s + 1.06382978723404 \cdot 10^{20}}\)
\(v_{5} = \frac{330000000.0 s^{2} + 215000000000000.0 s}{660000000.0 s^{2} + 642765957446809.0 s + 1.06382978723404 \cdot 10^{20}}\)
\(I_{V1} = \frac{- 1.452 \cdot 10^{18} s^{3} - 1.49089787234043 \cdot 10^{24} s^{2} - 2.86214893617021 \cdot 10^{29} s - 1.06382978723394 \cdot 10^{33}}{6.6 \cdot 10^{26} s^{2} + 6.42765957446809 \cdot 10^{32} s + 1.06382978723404 \cdot 10^{38}}\)

H = ((U[v2]-U[v3])/U[v1]).nsimplify().simplify().expand().together()
H

\(\displaystyle \frac{10638297872340450000}{66000000 s^{2} + 64276595744681 s + 10638297872340450000}\)

31.3.2.1 Convert transfer function to SciPy system

In this section we convert the SymPy equations into Numpy format.

Extract the numerator and denominator polynomials so that the system can be defined in SciPy.

H_num, H_denom = fraction(H) #returns numerator and denominator

The SciPy function, TransferFunction, represents the system as the continuous-time transfer function and takes as inputs the coeeficients of the numerator and denominator polynominals.

# convert symbolic to numpy polynomial
a = np.array(Poly(H_num, s).all_coeffs(), dtype=float)
b = np.array(Poly(H_denom, s).all_coeffs(), dtype=float)
sys = signal.TransferFunction(a,b)

31.3.3 Poles and zeros of the low pass transfer function

The poles and zeros of the transfer function can easly be obtained with the following code:

sys_zeros = np.roots(sys.num)
sys_poles = np.roots(sys.den)

31.3.3.1 Low pass filter pole zero plot

The poles and zeros of the preamp transfer function are plotted.

plt.plot(np.real(sys_zeros), np.imag(sys_zeros), 'ob', markerfacecolor='none')
plt.plot(np.real(sys_poles), np.imag(sys_poles), 'xr')
plt.legend(['Zeros', 'Poles'], loc=1)
plt.title('Pole / Zero Plot')
plt.xlabel('real part, \u03B1')
plt.ylabel('imaginary part, j\u03C9')
plt.grid()
plt.show()

Poles and zeros of the transfer function plotted on the complex plane. The units are in radian frequency.

Printing these values in Hz.

print('number of zeros: {:d}'.format(len(sys_zeros)))
for i in sys_zeros:
    print('{:,.2f} Hz'.format(i/(2*np.pi)))
number of zeros: 0
print('number of poles: {:d}'.format(len(sys_poles)))
for i in sys_poles:
    print('{:,.2f} Hz'.format(i/(2*np.pi)))
number of poles: 2
-121,354.74 Hz
-33,644.32 Hz

31.3.4 Bode plot

Use the SciPy function bode to plot the magnitude and phase of the filter. In electrical engineering, a Bode plot is a graph of the frequency response of a system. It is usually a combination of the magnitude (usually in decibels) of the frequency response and the phase shift. As originally conceived by Hendrik Wade Bode in the 1930s, the plot is an asymptotic approximation of the frequency response, using straight line segments. Bode plots are used to assess the stability of systems by finding the gain and phase margins.

x = np.logspace(3, 8, 200, endpoint=False)*2*np.pi
w, mag, phase = signal.bode(sys, w=x) # returns: rad/s, mag in dB, phase in deg

fig, ax1 = plt.subplots()
ax1.set_ylabel('magnitude, dB')
ax1.set_xlabel('frequency, Hz')

plt.semilogx(w/(2*np.pi), mag,'-b')    # Bode magnitude plot

ax1.tick_params(axis='y')
#ax1.set_ylim((-30,20))
plt.grid()

# instantiate a second y-axes that shares the same x-axis
ax2 = ax1.twinx()
color = 'tab:blue'

plt.semilogx(w/(2*np.pi), phase,':',color='tab:red')  # Bode phase plot

ax2.set_ylabel('phase, deg',color=color)
ax2.tick_params(axis='y', labelcolor=color)
#ax2.set_ylim((-5,25))

plt.title('Magnitude and phase response')
plt.show()

31.3.5 Filter impulse and step response

Use the SciPy functions impulse2 and step2 to plot the impulse and step response of the system.

The impulse and step response of the filter are plotted below. Any linear, time-invariant is completely characterized by its impulse response. The transfer function is the Laplace transform of the impulse response. The impulse response defines the response of a linear time-invariant system for all frequencies.

In electronic engineering and control theory, step response is the time behavior of the outputs of a general system when its inputs change from zero to one in a very short time.

plt.subplots(1,2,figsize=(15, 5))

# using subplot function and creating
# plot one
plt.subplot(1, 2, 1)

# impulse response
t, y = signal.impulse2(sys,N=500)
plt.plot(t/1e-3, y)
plt.title('Impulse response')
plt.ylabel('volts')
plt.xlabel('time, msec')
plt.grid()

# using subplot function and creating plot two
plt.subplot(1, 2, 2)

t, y = signal.step2(sys,N=500)
plt.plot(t/1e-3, y)
plt.title('Step response')
plt.ylabel('volts')
plt.xlabel('time, msec')
plt.grid()

# show plot
plt.show()

31.3.6 Low pass filter group delay

The following python code calculates and plots group delay. Frequency components of a signal are delayed when passed through a circuit and the signal delay will be different for the various frequencies unless the circuit has the property of being linear phase. The delay variation means that signals consisting of multiple frequency components will suffer distortion because these components are not delayed by the same amount of time at the output of the device.

Group delay: \(\tau _{g}(\omega )=-\frac {d\phi (\omega )}{d\omega }\)

#w_preamp, mag_preamp, phase_preamp = bp_sys.bode(w=x_axis_range)

plt.title('group delay')
plt.semilogx(w/(2*np.pi), -np.gradient(phase*np.pi/180)/np.gradient(w),'-',label='group delay')

plt.ylabel('Group delay, sec')
plt.xlabel('Frequency, Hz')
plt.legend()
plt.grid()
plt.show()