I'll perform a few of the steps by hand, below. But the process you see here can be easily coded up in Python or any other language you prefer.
I'm going to use a circuit that is simple but also complicated enough to illustrate the details required. I don't want you to miss something important.
step 1
Use a schematic capture program (for example, LTspice performs that function) to draw out your schematic and get its netlist. Then remove any comment lines or Spice command lines from the list. This will provide a simple list of conductances of length \$N\$.
For example:
simulate this circuit– Schematic created using CircuitLab
When I put that into LTspice I get the following netlist:
R1 n1 in RR2 n2 n1 RR3 n3 n2 RR4 out n3 RC1 n1 0 CC2 n2 0 CC3 n3 0 CC4 out 0 C.backanno.end
The last two lines aren't needed (obviously.)
This leaves a conductance list that has these \$N=8\$ elements:
R1 n1 in RR2 n2 n1 RR3 n3 n2 RR4 out n3 RC1 n1 0 CC2 n2 0 CC3 n3 0 CC4 out 0 C
Should be very easy to code this up.
step 2
Scan the resulting list to create a unique list of nodes.
Do not include ground (0) in the list. It's not needed.
Also make sure that your input node is at the bottom of the list. The ordering of the rest isn't important. But that singular point is important.
I find:
n1n2n3outin
Note again that ground is not included in the list.
This list has \$M=5\$ elements.
This will also be easily coded up. (You will need to know your input source node name, of course.)
step 3
Create an \$N\times N\$ conductance matrix, \$C\$. Each conductance is listed just once along its diagonal. Everywhere else is zero.
I'll do this manually in SymPy:
C = Matrix( [ [1/R1,0,0,0,0,0,0,0], [0,1/R2,0,0,0,0,0,0], [0,0,1/R3,0,0,0,0,0], [0,0,0,1/R4,0,0,0,0], [0,0,0,0,s*C1,0,0,0], [0,0,0,0,0,s*C2,0,0], [0,0,0,0,0,0,s*C3,0], [0,0,0,0,0,0,0,s*C4] ] )
Obviously easy to code since I just coded it up above, by hand. You can do this automatically given the list from step 1 and knowledge of the conductance of resistors, inductors, and capacitors.
step 4
Using the unique node list and also the simple list of conductances, create an \$N\times M\$ incidence matrix, \$A\$. This matrix starts out as all zero but will have a -1 and +1 placed on the row for each conductance found in the length-\$N\$ conductance list located at the two columns associated with the two nodes it connects.
Looking over the list from step 1 above, I get the following code:
A = Matrix( [ [-1,0,0,0,1], # R1 n1 in R [1,-1,0,0,0], # R2 n2 n1 R [0,1,-1,0,0], # R3 n3 n2 R [0,0,1,-1,0], # R4 out n3 R [-1,0,0,0,0], # C1 n1 0 C [0,-1,0,0,0], # C2 n2 0 C [0,0,-1,0,0], # C3 n3 0 C [0,0,0,-1,0] ] ) # C4 out 0 C
That process will require re-parsing though the list from step 1. But you can see that the generation of the above isn't difficult to understand.
Note how the grounded capacitors only show one value, -1. It doesn't matter whether this is -1 or +1, as that only affects the assumed current (flux) direction. So just pick something in grounded device cases.
step 5
Now perform the following steps, using the Schur complement idea:
F = A.T * C * A # resulting F must be squareN,M = F.shape # so N == M.P = F.extract( [i for i in range(N-1)], [i for i in range(N-1)] )QT = F.extract( [i for i in range(N-1)], [N-1] )(P.inv()*QT)[N-2,0] # invert P, solve, and extract the desired transfer function.-1/(C1*C2*C3*C4*R1*R2*R3*R4*s**4 + C1*C2*C3*R1*R2*R3*s**3 + C1*C2*C4*R1*R2*R3*s**3 + C1*C2*C4*R1*R2*R4*s**3 + C1*C2*R1*R2*s**2 + C1*C3*C4*R1*R2*R4*s**3 + C1*C3*C4*R1*R3*R4*s**3 + C1*C3*R1*R2*s**2 + C1*C3*R1*R3*s**2 + C1*C4*R1*R2*s**2 + C1*C4*R1*R3*s**2 + C1*C4*R1*R4*s**2 + C1*R1*s + C2*C3*C4*R1*R3*R4*s**3 + C2*C3*C4*R2*R3*R4*s**3 + C2*C3*R1*R3*s**2 + C2*C3*R2*R3*s**2 + C2*C4*R1*R3*s**2 + C2*C4*R1*R4*s**2 + C2*C4*R2*R3*s**2 + C2*C4*R2*R4*s**2 + C2*R1*s + C2*R2*s + C3*C4*R1*R4*s**2 + C3*C4*R2*R4*s**2 + C3*C4*R3*R4*s**2 + C3*R1*s + C3*R2*s + C3*R3*s + C4*R1*s + C4*R2*s + C4*R3*s + C4*R4*s + 1)
That actually is correct, by the way. It's not in a form that will communicate well. But at least it is correct.
Note that I used \$N-2\$ as an index. This is because the output was at that position in the list from step 2. If you wanted a different transfer function, say one from \$n_2\$ for example, then all you would do is change that index.
All the answers are provided. So don't use any indexing and you will get all the transfer functions at once. Or, otherwise, just have to pick the one you want to see (as I do, above.)
summary
I've written most of the directed-graph algorithmic code for you. It's all done. All you need to do is fill in a few relatively simple blanks.
Also, you may wish to read through this EESE answer to fill out a few more details.
final addition
The parsing details were interesting enough that I went ahead and wrote a quick and dirty SymPy function to handle the question:
def transfer(netlist=None,innode=None,outnode=None): if netlist is None: netlist = sys.stdin.read() rawlist = [i.split() for i in netlist.splitlines()] if len(rawlist) < 1: return 'empty netlist' cookedlist = [] nodelist = [] for i in rawlist: if len(i) > 2 and len(i[0]) > 1 and i[0][0].isalpha(): device = i[0][0].upper() if not (device == 'R' or device == 'C' or device == 'L'): return 'netlist may only have R, L, or C devices' cookedlist.append(i[:3]) for e in range(1,3): if not (i[e] == '0'): nodelist.append(i[e].upper()) if len(cookedlist) < 1: return 'empty netlist' nodelist = list(set(nodelist)) if len(nodelist) < 1: return 'enpty list of nodes' if innode is None or outnode is None: print('List of available nodes: ', ','.join(map(str, nodelist))) exitlist = ['', 'Q', 'E', 'QUIT', 'EXIT'] if innode is None: while True: innode = input('Enter desired input node name: ').upper() if innode in nodelist: break if innode in exitlist: return 'user-requested termination' print(innode, ' not found in above list.') else: innode = innode.upper() if outnode is None: while True: outnode = input("Enter desired output node name (or '*' for all): ").upper() if outnode in nodelist: break if outnode == '*': break if outnode in exitlist: return 'user-requested termination' print(outnode, ' not found in above list.') else: outnode = outnode.upper() if not (outnode == '*'): temp = nodelist.pop(nodelist.index(outnode)) nodelist.append(temp) temp = nodelist.pop(nodelist.index(innode)) nodelist.append(temp) N = len(cookedlist) M = len(nodelist) C = zeros(N,N) A = zeros(N,M) s = symbols('s') for i in range(0,N): part = cookedlist[i][0].upper() device = part[0] partname = symbols(part, real=True, positive=True) if device == 'R': C[i,i] = 1/partname elif device == 'C': C[i,i] = s*partname elif device == 'L': C[i,i] = 1/(s*partname) else: return 'fatal internal error' for j in range(1,3): nodename = cookedlist[i][j].upper() if nodename == '0': continue if not (nodename in nodelist): return 'fatal internal error' nodeidx = nodelist.index(nodename) A[i,nodeidx] = 2*j-3 F = A.T * C * A FN,FM = F.shape P = F.extract( [i for i in range(FN-1)], [i for i in range(FN-1)] ) QT = F.extract( [i for i in range(FN-1)], [FN-1] ) Answer = P.inv()*QT Result = {} if not (outnode == '*'): Result[outnode] = Answer[nodelist.index(outnode),0] else: for i in range(0,M-1): Result[nodelist[i]] = Answer[i,0] return Result
It will produce just the one transfer function or all of them (when entering '*' for the output node.)
Using the above example, here's a run:
┌────────────────────────────────────────────────────────────────────┐│ SageMath version 9.5, Release Date: 2022-01-30 ││ Using Python 3.10.12. Type "help()" for help. │└────────────────────────────────────────────────────────────────────┘sage: transfer()R1 n1 vin RR2 n2 n1 RR3 n3 n2 RR4 vout n3 RC1 n1 0 CC2 n2 0 CC3 n3 0 CC4 vout 0 C.backanno.endList of available nodes: N2,N3,VIN,VOUT,N1Enter desired input node name: vinEnter desired output node name (or '*' for all): vout{'VOUT': -1/(C1*C2*C3*C4*R1*R2*R3*R4*s**4 + C1*C2*C3*R1*R2*R3*s**3 + C1*C2*C4*R1*R2*R3*s**3 + C1*C2*C4*R1*R2*R4*s**3 + C1*C2*R1*R2*s**2 + C1*C3*C4*R1*R2*R4*s**3 + C1*C3*C4*R1*R3*R4*s**3 + C1*C3*R1*R2*s**2 + C1*C3*R1*R3*s**2 + C1*C4*R1*R2*s**2 + C1*C4*R1*R3*s**2 + C1*C4*R1*R4*s**2 + C1*R1*s + C2*C3*C4*R1*R3*R4*s**3 + C2*C3*C4*R2*R3*R4*s**3 + C2*C3*R1*R3*s**2 + C2*C3*R2*R3*s**2 + C2*C4*R1*R3*s**2 + C2*C4*R1*R4*s**2 + C2*C4*R2*R3*s**2 + C2*C4*R2*R4*s**2 + C2*R1*s + C2*R2*s + C3*C4*R1*R4*s**2 + C3*C4*R2*R4*s**2 + C3*C4*R3*R4*s**2 + C3*R1*s + C3*R2*s + C3*R3*s + C4*R1*s + C4*R2*s + C4*R3*s + C4*R4*s + 1)}
So the idea does work and can be generalized enough to be slightly useful.