Skip to article frontmatterSkip to article content

2.7 Case Study: Network Flows Revisited

linear algebra in electric circuits

Dept. of Electrical and Systems Engineering
University of Pennsylvania

Binder

Lecture notes

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

Network

We can associate an incidence matrix AA with this graph. Each row corresponds to a node, and each column to an edge, with

aij={1if edge j points to node i1if edge j points from node i.a_{ij} = \begin{cases} 1 & \textrm{if edge} \ j \ \textrm{points \textbf{to} node} \ i \\ -1 & \textrm{if edge} \ j \ \textrm{points \textbf{from} node} \ i \end{cases}.

For Figure 1, AR4×5A \in \mathbb{R}^{4 \times 5}, and

A=[11000101100110100011]\begin{align*} A &= \bm -1 & -1 & 0 & 0 & 0 \\ 1 & 0 & -1 & -1 & 0 \\ 0 & 1 & 1 & 0 & -1 \\ 0 & 0 & 0 & 1 & 1 \em \end{align*}

By considering the four fundamental subspaces (Row, Column, Null and Left Null Space) of AA, 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 s=[s00s]\vv s = \bm s \\ 0 \\ 0 \\ -s \em, 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 1s=0\vv 1^{\top} \vv s = 0.

The flow conservation equations say that flows entering a node must equal flows leaving a node, which can be written as

Af+s=0 or Af=s,A \vv f + \vv s = \vv 0 \ \textrm{or} \ A \vv f = - \vv s,

where f=[f1f2f3f4f5]R5\vv f = \bm f_1 \\ f_2 \\ f_3 \\ f_4 \\ f_5 \em \in \mathbb{R}^{5} is the vector of edge flows. The solution set to (3) characterizes all flows compatible with the network and the source vector s\vv s.

From this theorem, we see that Af=sA \vv f = - \vv s has a solution if and only if s\vv s \in Col(A)(A). Let’s try to understand when this might be true by computing a basis for Col(A)(A).

From (2), we notice that

  1. columns 1, 2 and 3 are not independent: column 3 = column 2 - column 1.
  2. 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(A)(A). We conclude that columns 1, 2 and 4 form a basis for Col(A)(A), and dim(Col(A))=3(A)) = 3.

Now, let’s take a closer look at Figure 1: edges 1,21, 2 and 3 form a loop in the graph, while edges 3,43, 4 and 5 form another loop. In contrast, edges 1,21, 2 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 s\vv s \inCol(A)(A) using columns 1,21, 2 and 4.

[110101010001][f1f2f4]=[s00s]f1f2=sf1f4=0f2=0f4=s\begin{align*} &\bm -1 & -1 & 0 \\ 1 & 0 & -1 \\ 0 & 1 & 0 \\ 0 & 0 & 1\em \bm f_1 \\ f_2 \\ f_4\em &= \bm -s \\ 0 \\ 0 \\ s\em \\ \Rightarrow &-f_1 - f_2 &= -s \\ & f_1 - f_4 &= 0 \\ & f_2 &= 0\\ & f_4 &= s \end{align*}

which has solution f1=f4=sf_1 = f_4 = s and f2=0f_2 = 0. Therefore, one possible network flow is given by

f=[s00s0].f^* = \bm s \\ 0 \\ 0 \\ s \\ 0\em.

This corresponds to putting the flow on edges 1 and 4 as shown below.

Network Flow

For any value of ss, 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 Af=sA \vv f = -\vv s. That’s where the null space of AA comes in!

Let’s look at NullAA. This is the solution set to Af=0A \vv f = \vv 0, 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 Af=0A \vv f = \vv 0 is f1=[11100]\vv f_1 = \bm -1 \\ 1 \\ -1 \\ 0 \\ 0\em (verify for yourself), which corresponds to going around the 1321 \to 3 \to 2 loop! Similarly, column 5 = column 4- column 3, giving f2=[00111]\vv f_2 = \bm 0 \\ 0 \\ -1 \\ 1 \\ -1\em, corresponding to the 3453 \to 4 \to 5 loop! f1\vv f_1 and f2\vv f_2 are linearly independent, and we know that

dim(Null(A))=5 (our variables f live in R5)dim(Col(A))=53=2 \textrm{dim(Null}(A)) = 5 \ (\textrm{our variables} \ \vv f \ \textrm{live in} \ \mathbb{R}^5) - \textrm{dim(Col}(A)) = 5 - 3 = 2

Hence, f1\vv f_1 and f2\vv f_2 form a basis!

From this theorem, we can therefore write the general solution to Af=sA \vv f = -\vv s as

f=f+c1f1+c2f2.\vv f = \vv f^* + c_1 \vv f_1 + c_2 \vv f_2.

The elements n\vv n \in Null(A)(A) 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 c1c_1 and c2c_2 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.

Network Voltage

Solving Ax=vA^{\top} \vv x = \vv v tells us what potentials we need to put on the nodes to achieve the desired voltages. For example, the first row of Ax=vA^{\top} \vv x = \vv v is x1+x2=v1-x_1 + x_2 = v_1. This is Kirchoff’s Voltage Law!

Let’s start with Null(A)(A^{\top}), which we find by setting v=0\vv v = \vv 0.

Ax=0x1=x2 (first row), x1=x3 (second row), x2=x4 (fourth row)x1=x2=x3=x4=c.A^{\top} \vv x = \vv 0 \Rightarrow x_1 = x_2 \ (\textrm{first row}), \ x_1 = x_3 \ (\textrm{second row}), \ x_2 = x_4 \ (\textrm{fourth row}) \Rightarrow x_1 = x_2 = x_3 = x_4 = c.

From (8), Null(A)(A^{\top}) is a line in R4\mathbb{R}^4 spanned by 1=[1111]\vv 1 = \bm 1 \\ 1 \\ 1 \\ 1\em. The rank of AA must be 41=34 - 1 = 3, 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 AA is the column space of AA^{\top}. There must be 3 independent columns of AA^{\top} (since rank = 3). So, let’s try to find them by inspection. The first three columns of AA^{\top} are

v1=[11000], v2=[10110], v3=[01101]\vv v_1 = \bm -1 \\ -1 \\ 0 \\ 0 \\ 0 \em, \ \vv v_2 = \bm 1 \\ 0 \\ -1 \\ -1 \\ 0 \em, \ \vv v_3 = \bm 0 \\ 1 \\ 1 \\ 0 \\ -1 \em

which can be verified to to be linearly independent. Therefore, only voltage configurations v\vv v lying in span{v1,v2,v3}\{\vv v_1, \vv v_2, \vv v_3\} can be encoded on this graph. Therefore, for any x1x_1, x2x_2, and x3x_3, voltage configurations x1v1+x2v2+x3v3x_1 \vv v_1 + x_2 \vv v_2 + x_3 \vv v_3 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!

Binder

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:

circuit_diagram

We can verify that the above circuit conforms to the network flow depicted in Figure 1 by letting IS=sI_S = s, and I1,I2,I3,I4,I5=f1,f2,f3,f4,f5I_1, I_2, I_3, I_4, I_5 = f_1, f_2, f_3, f_4, f_5. By the current law, we know that the currents must satisfy

i=[I1I2I3I4I5]=f+c1f1+c2f2, \mathbf{i} = \bm I_1 \\ I_2 \\ I_3 \\ I_4 \\ I_5 \em = \mathbf{f}_\star + c_1 \mathbf{f}_1 + c_2 \mathbf{f}_2,

for some numbers c1c_1 and c2c_2. Ohm’s law allows us to relate the current between two nodes and the voltage drop between two nodes as Vi=IiRiV_i = I_i R_i , where RiR_i is the resistance of edge ii. We may therefore write

v=[V1V2V3V4V5]=[R1I1R2I2R3I3R4I4R5I5]=Ri, \mathbf{v} = \bm V_1 \\ V_2 \\ V_3 \\ V_4 \\ V_5 \em = \bm R_1 I_1 \\ R_2 I_2 \\ R_3 I_3 \\ R_4 I_4 \\ R_5 I_5 \em = R \mathbf{i},

where R=diag(R1,R2,R3,R4,R5)R = \mathsf{diag}(R_1, R_2, R_3, R_4, R_5).

By the voltage law, V1,V2,V3,V4,V5V_1, V_2, V_3, V_4, V_5 is a consistent profile only if there exists some x1,x2,x3x_1, x_2, x_3 such that

x1v1+x2v2+x3v3=v=Ri=R(f+c1f1+c2f2). x_1 \mathbf{v}_1 + x_2 \mathbf{v}_2 + x_3 \mathbf{v}_3 = \mathbf{v} = R \mathbf{i} = R (\mathbf{f}_\star + c_1 \mathbf{f}_1 + c_2 \mathbf{f}_2).

Moving the terms involving c1c_1 and c2c_2 to the left side, we find that we must have

x1v1+x2v2+x3v3c1Rf1c2Rf2=Ri=Rf x_1 \mathbf{v}_1 + x_2 \mathbf{v}_2 + x_3 \mathbf{v}_3 - c_1 R \mathbf{f}_1 - c_2 R \mathbf{f}_2 = R \mathbf{i} = R \mathbf{f}_\star

The left hand side may be expressed as a matrix, leading to the equation

[v1v2v3Rf1Rf2][x1x2x3c1c2]=Rf. \bm \vv v_1 & \vv v_2 & \vv v_3 & -R \vv f_1 & -R \vv f_2 \em \bm x_1 \\ x_2 \\ x_3 \\ c_1 \\ c_2 \em = R \mathbf{f}_\star.

We may verify that for positive R1,R2,R3,R4,R5R_1, R_2, R_3, R_4, R_5, the vectors v1\vv v_1, v2\vv v_2, v3\vv v_3, Rf1R \vv f_1, Rf2R \vv f_2 are linearly independent. Therefore, we may invert the matrix [v1v2v3Rf1Rf2]\bm \vv v_1 & \vv v_2 & \vv v_3 & -R \vv f_1 & -R \vv f_2 \em to solve the above equation, from which we find that

[x1x2x3c1c2]=[v1v2v3Rf1Rf2]1Rf. \bm x_1 \\ x_2 \\ x_3 \\ c_1 \\ c_2 \em = \bm \vv v_1 & \vv v_2 & \vv v_3 & R \vv f_1 & R \vv f_2 \em^{-1} R \mathbf{f}_\star.

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 R1R_1 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.