2.7 Case Study: Network Flows Revisited
linear algebra in electric circuits
1Reading¶
Material related to this page, as well as additional exercises, can be found in ALA Ch. 1.6.
2Learning Objectives¶
By the end of this page, you should know:
- how to represent flows in networks as matrices
- the sources and sinks in a network
- the relationship of flow conservation to linear systems
3Incidence Matrix¶
Consider the directed graph below with 4 nodes and 5 edges
We can associate an incidence matrix with this graph. Each row corresponds to a node, and each column to an edge, with
For Figure 1, , and
By considering the four fundamental subspaces (Row, Column, Null and Left Null Space) of , we can completely understand the properties of our network flow problem. The incidence flow can be represented in code using numpy as follows:
import numpy as np
A = np.array([
[-1, -1, 0, 0, 0],
[1, 0, -1, -1, 0],
[0, 1, 1, 0, -1],
[0, 0, 0, 1, 1]
])
4The Current Law¶
First, we define the source vector , which captures external flows entering (positive entries) the network referred to as sources, and flows leaving (negative entries) the network known as sinks. We make sure .
The flow conservation equations say that flows entering a node must equal flows leaving a node, which can be written as
where is the vector of edge flows. The solution set to (3) characterizes all flows compatible with the network and the source vector .
From this theorem, we see that has a solution if and only if Col. Let’s try to understand when this might be true by computing a basis for Col.
From (2), we notice that
- columns 1, 2 and 3 are not independent: column 3 = column 2 - column 1.
- columns 3, 4 and 5 are not independent: column 5 = column 4 - column 3 = column 4 - column2 + column 1
However, we have that columns 1, 2 and 4 are independent! Since we can represent columns 3 and 5 in terms of columns 1, 2 and 4; columns 1, 2 and 4 span Col. We conclude that columns 1, 2 and 4 form a basis for Col, and dim(Col.
Now, let’s take a closer look at Figure 1: edges and 3 form a loop in the graph, while edges and 5 form another loop. In contrast, edges and 4 form a tree, which has no loops! This tells us that the edges of any tree in our graph gives us independent columns!
So we now check if Col using columns and 4.
which has solution and . Therefore, one possible network flow is given by
This corresponds to putting the flow on edges 1 and 4 as shown below.
For any value of , this solution can be represented in code using the following vector. Let’s verify that it is a solution.
s = 4
f_star = np.array([s, 0, 0, s, 0])
print(A@f_star)
[-4 0 0 4]
There are other ways to distribute the flow to satisfy . That’s where the null space of comes in!
Let’s look at Null. This is the solution set to , which captures flow conservation in the absence of external sources. This corresponds to (flow in) - (flow out) = 0 at each node: this is called Kirchoff’s current law in electric circuits.
We already noticed that column 3= column 2 - column 1. Hence, one solution to is (verify for yourself), which corresponds to going around the loop! Similarly, column 5 = column 4- column 3, giving , corresponding to the loop! and are linearly independent, and we know that
Hence, and form a basis!
From this theorem, we can therefore write the general solution to as
The elements Null are called circulations (why?).
Using this expression for the null space, any network flow for the network in Figure 1 may be represented in code for arbitrary and as follows.
f_1 = np.array([-1, 1, -1, 0, 0])
f_2 = np.array([0, 0, -1, 1, -1])
c = np.array([1, 4])
f = f_star + c[0]*f_1 + c[1]*f_2
print(f)
[ 3 1 -5 8 -4]
5The Voltage Law¶
Now, instead of flows, let’s discuss about potential differences, or voltages, across nodes.
Solving tells us what potentials we need to put on the nodes to achieve the desired voltages. For example, the first row of is . This is Kirchoff’s Voltage Law!
Let’s start with Null, which we find by setting .
From (8), Null is a line in spanned by . The rank of must be , which we saw was true above! This implies that we can increase the voltage across all nodes by the same amount, and achieve the same voltage drop between nodes. Makes sense!
The row space of is the column space of . There must be 3 independent columns of (since rank = 3). So, let’s try to find them by inspection. The first three columns of are
which can be verified to to be linearly independent. Therefore, only voltage configurations lying in span can be encoded on this graph. Therefore, for any , , and , voltage configurations are valid. Such configurations can be represented in code as follows.
v_1 = np.array([-1, -1, 0, 0 ,0])
v_2 = np.array([1, 0, -1, -1, 0])
v_3 = np.array([0, 1, 1, 0, -1])
x = np.array([.5, 3, 1])
v = x[0]*v_1 + x[1]*v_2 + x[2]*v_3
print(v)
[ 2.5 0.5 -2. -3. -1. ]
We will understand where the statement in Exercise 1 comes from in the next couple of lectures!
Using the current and voltage laws above, let’s try to compute the currents in a circuit consisting of a current source and several resistors. In particular, we will consider the below circuit:
We can verify that the above circuit conforms to the network flow depicted in Figure 1 by letting , and . By the current law, we know that the currents must satisfy
for some numbers and . Ohm’s law allows us to relate the current between two nodes and the voltage drop between two nodes as , where is the resistance of edge . We may therefore write
where .
By the voltage law, is a consistent profile only if there exists some such that
Moving the terms involving and to the left side, we find that we must have
The left hand side may be expressed as a matrix, leading to the equation
We may verify that for positive , the vectors , , , , are linearly independent. Therefore, we may invert the matrix to solve the above equation, from which we find that
Let’s try out some different resistances and check that the resulting current profiles make sense.
R = np.diag([10, 10, 10, 10, 10])
stacked_matrix = np.stack([v_1, v_2, v_3, -R@f_1, -R@f_2], axis=1)
solution = np.linalg.inv(stacked_matrix)@R@f_star
x_1 = solution[0]; x_2 = solution[1]; x_3 = solution[2]; c_1 = solution[3]; c_2 = solution[4]
currents = f_star + c_1*f_1 + c_2*f_2
voltages = R@currents
print('currents: ', currents)
print('voltages:' , voltages)
currents: [2.0000000e+00 2.0000000e+00 4.4408921e-16 2.0000000e+00 2.0000000e+00]
voltages: [2.0000000e+01 2.0000000e+01 4.4408921e-15 2.0000000e+01 2.0000000e+01]
Does the above solution make sense? When all resistances are equal, the two branches of the circuit are equally appealing, so the current is split evenly. Let’s try a different resistance profile.
R = np.diag([100, 10, 10, 10, 10])
stacked_matrix = np.stack([v_1, v_2, v_3, -R@f_1, -R@f_2], axis=1)
solution = np.linalg.inv(stacked_matrix)@R@f_star
x_1 = solution[0]; x_2 = solution[1]; x_3 = solution[2]; c_1 = solution[3]; c_2 = solution[4]
currents = f_star + c_1*f_1 + c_2*f_2
voltages = R@currents
print('currents: ', currents)
print('voltages:' , voltages)
currents: [ 0.45714286 3.54285714 -1.02857143 1.48571429 2.51428571]
voltages: [ 45.71428571 35.42857143 -10.28571429 14.85714286 25.14285714]
Now that the resistance is higher, the second path is prefereable. Therefore, a higher portion of the current flows through the second path. After bypassing the high resistance, some of the current reverts to the first path by flowing from node three to node two.