Quarantine Logs 2020-04-13: Circuit-Sim Progress!


Been a week! This is by far the most frequently I’ve ever posted. I’m hoping to keep it up.

I’m happy to update that I made a little progress on the circuit simulator I’ve been working on. Here I’ll get into some of what that’s all about in a little bit more detail. All the relevant code is up on Github, though it’s really bare-bones and without documentation as of the time of writing.

Learning about circuit analysis introduced me to the concept of nodes. A node is a point in a circuit where two or more components meet. This concept is important because of Kirchhoff’s Current Law, which states that the sum of currents leaving a node is 0.

Current leaving node i=jN(i)Ii,j=0

Here’s an example:

Note that this system can also be solved by the matrix equation

[10001/R11/R1+1/R3+1/R21/R31/R201/R31/R3+1/R41/R40001][VsV1V2VGND]=[Vs000]

There’s a bit of abuse of notation here with Vs referring both to the variable corresponding to the voltage level at the voltage source, and the value of the voltage it adds.

When we consider a circuit with only resistors, the current leaving the node from each branch is equal to the voltage drop along the resistor divided by its resistance. (Ohm’s Law states that V=IR). Thus, if N(i) refers to the set of nodes that are adjacent to node i, we have

Current leaving node i=jN(i)ViVjRi,j=0

At this point I practiced with a couple of circuits, calculating the voltage level at each node by hand, eventually moving onto just solving for the voltage level at each point by solving a linear system via matrices. After a couple of these I started to notice that all of this started to look a little familiar. That’s right—this looks a lot like a “graph kind of computation” would fit perfectly. The “nodes” in the circuit are just the same as “nodes” in a graph, and the resistors are just like a graph’s edges, storing information. Given the properties of circuits, I determined that the best thing to work with here are undirected graphs, with the possibility of having multiple edges between any two nodes; this is certainly possible in a circuit.

It turns out that that’s almost enough to start implementing a circuit simulator! You take your graph representation of your circuit, and run it through some function that will generate a linear system for that graph. Once you have that you can automatically solve the linear system to find the voltages at every single node. It’s still pretty early so I currently support circuits that have exactly one voltage source and where the only type of circuit component is a resistor.

I chose Julia to implement it, mostly because its matrix solving syntax is nice.

Here’s my circuit representation. A Node (which I’ve left out the implementation of) can either be a VoltageSource (adding a set amount of voltage) or a BaseNode, which is just a wire junction.

mutable struct Circuit
    ground::Node
    nodes::Dict{String, Node}
    edges::Array{Edge}

    function Circuit()
        ground = BaseNode()
        nodes = Dict()
        get!(nodes, "GND", ground)
        edges = []
        return new(ground, nodes, edges)
    end
end

And here’s my current version of solve_voltages. To generate the linear system I follow a couple of basic rules:

using LinearAlgebra

# ...

function update_soln_matx!(S::Array{T, 2}, c, i, j, node1, node2, R) where {T <: Real}
    if !isa(node1, VoltageSource) && node1 != c.ground
        S[i, i] += 1 / R
        S[i, j] += -1 / R
    end
end

function solve_voltages(c::Circuit)
    names = [n[1] for n in collect(c.nodes)]
    node_to_idx = Dict([name => i for (i, name) in enumerate(names)])
    N = length(c.nodes)
    S = zeros(N, N)
    b = zeros(N, 1)

    z = node_to_idx["GND"]
    S[z,z] = 1

    for (name, node) in c.nodes
        if node isa VoltageSource
            i = node_to_idx[name]
            S[i, i] = 1
            b[i] = node.added_voltage
        end
    end

    for (name1, name2, con) in c.edges
        node1 = c.nodes[name1]
        node2 = c.nodes[name2]
        i = node_to_idx[name1]
        j = node_to_idx[name2]
        if con isa Resistor
            R = con.resistance
            update_soln_matx!(S, c, i, j, node1, node2, R)
            update_soln_matx!(S, c, j, i, node2, node1, R)
        else
            throw(MethodError("Only resistors supported at this time."))
        end
    end

    if DEBUG
        println(S)
        println(b)
    end
    return inv(S) * b
end

Here’s a couple of examples:

# Really basic case with two resistors.
c = Circuit()
register_node!(c, "n1", VoltageSource(5.0))
register_node!(c, "n2", BaseNode())
connect_nodes!(c, "n1", "n2", Resistor(5.0))
connect_nodes!(c, "n2", "GND", Resistor(10.0))
println(solve_voltages(c))
# output: [5; 3.3333; 0]

# This one is equivalent to the diagram at the top of the post with some
# arbitrary values chosen for resistances.
c2 = Circuit()
register_node!(c2, "n1", VoltageSource(12.0))
register_node!(c2, "n2", BaseNode())
register_node!(c2, "n3", BaseNode())
connect_nodes!(c2, "n1", "n2", Resistor(4000.))
connect_nodes!(c2, "n2", "n3", Resistor(2000.))
connect_nodes!(c2, "n2", "GND", Resistor(1000.))
connect_nodes!(c2, "n3", "GND", Resistor(2000.))
println(solve_voltages(c2))
#output: [12; 2.0; 1.0; 0]

So that’s where I’m at so far! There are some pretty natural extensions to what I’ve done so far:

If you’re interested in learning more about circuit analysis, the book I’ve been going through is Electric Circuits by James S. Kang. It offers a really concise introduction to circuit analysis and has tons of exercises to practice with.