Skip to article frontmatterSkip to article content

1.9 Case Study: Modeling Network Flow using Systems of Linear Equations

Just go with the flow

Dept. of Electrical and Systems Engineering
University of Pennsylvania

Binder

1What is network flow?

Network flow models can be used to capture traffic flowing through a road network, water through a grid of pipes, or information flowing through the internet. In this section, we will use systems of linear equations to understand how to allocate flows (traffic, water, internet packets) across links (roads, pipes, network links) in order to meet demand (how much traffic must flow in and out of a network). We’ll also explore what happens when links are modified or deleted.

A key modeling assumption that we make is that flow is conserved, i.e., flows leaving a node have to equal flows leaving a node. Here we use the term node to describe where different links (roads, pipes, etc.) meet, i.e., nodes are where links intersect. In the figure below, flow conservation means that x1+x2=30x_1+x_2 = 30. This is a very reasonable assumption: for example, if we’re talking about road traffic, it means that all cars that enter a road network eventually leave it, and that no cars can spontaneously appear or disappear within the road network. Similarly, if we’re talking about internet traffic, it means that data can’t appear out of thin air, that all data entering the network eventually leaves it, and that packets can’t be dropped (this is not always true for internet traffic, but we definitely prefer it when no data is lost!).

Network Flow

2What Linear Algebra tools do we need to model network flow?

2.1Concepts

  1. Gaussian Elimination / Row Reduction

2.2Computation

  1. Python
  2. numpy.array
  3. sympy.matrices.matrices.MatrixReductions.echelon_form

3Small Example

Let’s consider a system of pipes.

Simple networks

Because of our assumption that the total inflows and outflows are conserved, we can say that for each intersection inflowoutflow=0\text{inflow} - \text{outflow} = 0. We make our first observation: each intersection, or node, in the graph above leads to an equation. We can rearrange terms, and get the following equation, that inflow=outflow\text{inflow} = \text{outflow}. This is fairly intuitive, and we’ll use this law to create our system of equations. We also know that the total outflows leaving the system must be equal to the total inflows entering the system: we’ll call this total flow conservation.

inflow=outflow10+40+30+50=60+30+x1[total conservation]x2+x3=x1+30[from i1]10+40=x2+x4[from i2]30+50=x3+x5[from i3]x5+x4=60[from i4]\begin{align} \text{inflow} &= \text{outflow} \\ 10 + 40 + 30 + 50 &= 60 + 30 + x_1 &[\text{total conservation}] \\ x_2 + x_3 &= x_1 + 30 &[\text{from } i_1] \\ 10 + 40 &= x_2 + x_4 &[\text{from } i_2] \\ 30 + 50 &= x_3 + x_5 &[\text{from } i_3] \\ x_5 + x_4 &= 60 &[\text{from } i_4] \\ \end{align}

Each equation above represents an equality that results from our original assumption. The first equation shows the total conservation, and the next 4 lines show the equations for each intersection. Rearranging to have the unknowns xix_i on the left, and the knowns on the right, we get the following system of linear equations:

inflow=outflow1x1+0x2+0x3+0x4+0x5=40[total conservation]1x1+1x2+1x3+0x4+0x5=30[from i1]0x1+1x2+0x3+1x4+0x5=50[from i2]0x1+0x2+1x3+0x4+1x5=80[from i3]0x1+0x2+0x3+1x4+1x5=60[from i4],\begin{align} \text{inflow} &= \text{outflow} \\ 1x_1 + 0x_2 + 0x_3 + 0x_4 + 0x_5 &= 40 &[\text{total conservation}] \\ -1x_1 + 1x_2 + 1x_3 + 0x_4 + 0x_5 &= 30 &[\text{from } i_1] \\ 0x_1 + 1x_2 + 0x_3 + 1x_4 + 0x_5 &= 50 &[\text{from } i_2] \\ 0x_1 + 0x_2 + 1x_3 + 0x_4 + 1x_5 &= 80 &[\text{from } i_3] \\ 0x_1 + 0x_2 + 0x_3 + 1x_4 + 1x_5 &= 60 &[\text{from } i_4], \\ \end{align}

which we can put in Ax=bA\vv x = \vv b form:

[1000011100010100010100011][x1x2x3x4x5]=[4030508060]\left[ \begin{array}{ccccc} %x_1 & x_2 &x_3 & x_4 &x_5\\\hline 1 & 0 & 0 & 0 & 0 \\ -1 & 1 & 1 & 0 & 0 \\ 0 & 1 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 1 \\ 0 & 0 & 0 & 1 & 1 \\ \end{array} \right] \left[ \begin{array}{c} x_1 \\ x_2 \\ x_3 \\ x_4 \\ x_5 \end{array} \right] = \left[ \begin{array}{cccccc} 40 \\ 30 \\ 50 \\ 80 \\ 60 \\ \end{array} \right]

We can construct the augmented matrix [Ab][A | \vv b] for this system below:

[Ab10000401110030010105000101800001160]\left[ \begin{array}{ccccc|c} & & A & & & \vv b\\\hline 1 & 0 & 0 & 0 & 0 & 40 \\ -1 & 1 & 1 & 0 & 0 & 30 \\ 0 & 1 & 0 & 1 & 0 & 50 \\ 0 & 0 & 1 & 0 & 1 & 80 \\ 0 & 0 & 0 & 1 & 1 & 60 \\ \end{array} \right]

4Python Implementation

We’ll work with SymPy to go through the steps of reducing the augmented matrix into row echelon form, and then solving for the general solutin.

import numpy as np
from numpy.linalg import solve
from sympy import Matrix, latex
augmented_matrix = np.array([
    [1 , 0 , 0 , 0,   0, 40],
    [-1 , 1 , 1 , 0 , 0, 30],
    [0 , 1 , 0 , 1, 0, 50],
    [0 , 0 , 1 , 0 , 1, 80],
    [0 , 0 , 0 , 1,   1, 60],
])
A = augmented_matrix[:,:-1]  # We take the first part of the augmented matrix, which represents A [: <- This means every row, :-1 <- This means all but the last column]
b = augmented_matrix[:, -1]  # Now the second part, which represents b [: <- This means every row, -1 <- This means only the last column]
matrix = Matrix(augmented_matrix)
matrix
Loading...
row_echelon_matrix = matrix.echelon_form(simplify=True)
row_echelon_matrix
Loading...

Placing this in equation form again, we get the below. There is no pivot in the 5th column, so x5x_5 is free.

1x1+0x2+0x3+0x4+0x5=400x1+1x2+1x3+0x4+0x5=700x1+0x2+1x3+1x4+0x5=200x1+0x2+0x3+1x4+1x5=60\begin{align} 1x_1 + 0x_2 + 0x_3 + 0x_4 + 0x_5 &= 40 \\ 0x_1 + 1x_2 + 1x_3 + 0x_4 + 0x_5 &= 70 \\ 0x_1 + 0x_2 + -1x_3 + 1x_4 + 0x_5 &= -20 \\ 0x_1 + 0x_2 + 0x_3 + -1x_4 + -1x_5 &= -60 \\ \end{align}

Simplifying, we get the following system of equations:

x1=40x2+x3=70x3+x4=20x4+x5=60\begin{align} x_1 &= 40 \\ x_2 + x_3 &= 70 \\ -x_3 + x_4 &= -20 \\ x_4 + x_5 &= 60 \\ \end{align}

We proceed from the bottom, and express the basic variables in terms of the free variable x5x_5. To get an expression to start, we can simplify the last equation to

x4=60x5x_4 = 60 -x_5

Using back substitution, we can first substitute x4x_4 into our third equation, yielding

x3x4=20x3(60x5)=20x3=20+60x5x3=80x5\begin{align*} x_3 - x_4 &= 20 \\ x_3 - (60 - x_5) &= 20 \\ x_3 &= 20 + 60 - x_5 \\ x_3 &= 80 - x_5 \end{align*}

Again substituting x3x_3 into the second equation, we get:

x2+x3=70x2+80x5=70x2=x510\begin{align*} x_2 + x_3 &= 70 \\ x_2 + 80 - x_5 &= 70 \\ x_2 &= x_5 - 10 \end{align*}

Gathering all of these equations, we get the general solution:

x1=40x2=x510x3=80x5x4=60x5x5=free\begin{align} x_1 &= 40 \\ x_2 &= x_5 - 10 \\ x_3 &= 80 - x_5 \\ x_4 &= 60 - x_5 \\ x_5 &= \text{free} \end{align}

Now, we need to apply some human reasoning, namely that none of the traffic flow can be negative! The equality x2=x510x_2 = x_5 - 10 tell us that x5x_5 must be at least 10 (otherwise x2x_2 would be negative!), and the equality x4=60x5 x_4 = 60 - x_5 tells us that x5x_5 can be at most 60 (otherwise x4x_4 would be negative!).

With that in mind, we pick x5=30x_5=30, which corresponds to putting 30 units of flow on x5x_5, and get the following specific solution:

x1=40x2=20x3=50x4=30x5=30\begin{align} x_1 &= 40 \\ x_2 &= 20 \\ x_3 &= 50 \\ x_4 &= 30 \\ x_5 &= 30 \end{align}

Let’s check our solution in the original equation:

x = np.array([40, 20, 50, 30, 30])
print(A@x)
print(b)
A@x == b
[40 30 50 80 60]
[40 30 50 80 60]
array([ True, True, True, True, True])

We got a solution!

5Penn Engineering Road Network

We can apply the same sort of thinking to roads!

Penn Road Network

The above figure shows a road network that looks similar to that around Penn Engineering. We model these road networks the same way, by balancing the inflows and outflows of each node, and by enforcing total flow conservation as well. This yields the following system of linear equations:

inflow=outflowx6+x2=x1[from i1]x3+x5=x2+50[from i2]x4+60=x3[from i3]x8=x4+x9[from i4]80+x7=x5+x8[from i5]100=x6+x7[from i6]100+80+60=x1+50+x9[total conservation]\begin{align} \text{inflow} &= \text{outflow} \\ x_6 + x_2 &= x_1 &[\text{from } i_1]\\ x_3 + x_5 &= x_2 + 50 &[\text{from } i_2]\\ x_4 + 60 &= x_3 &[\text{from } i_3]\\ x_8 &= x_4 + x_9 &[\text{from } i_4]\\ 80 + x_7 &= x_{5} +x_8 &[\text{from } i_5]\\ 100 &= x_{6} + x_7 &[\text{from } i_6]\\ 100 + 80 + 60 &= x_1 + 50 + x_9 &[\text{total conservation}]\\ \end{align}

To put this in matrix form, we need to do the same as before, and reorganize terms.

inflow=outflowx1+x2+x6=0[from i1]x2+x3+x5=50[from i2]x3+x4=60[from i3]x4x8+x9=0[from i4]x5x7+x8=80[from i5]x6+x7=100[from i6]x1+x9=190[total conservation]\begin{align} \text{inflow} &= \text{outflow} \\ -x_1 + x_2 + x_6 &= 0 &[\text{from } i_1]\\ - x_2 + x_3 + x_5 &= 50 &[\text{from } i_2]\\ -x_3 + x_4 &= -60 &[\text{from } i_3]\\ x_4 - x_8 + x_9 &= 0 &[\text{from } i_4]\\ x_{5} -x_7 + x_8 &= 80 &[\text{from } i_5]\\ x_{6} + x_7 &= 100 &[\text{from } i_6]\\ x_1 + x_9 &= 190 &[\text{total conservation}]\\ \end{align}

This gives us the following augmented matrix

[Ab11000100000110100005000110000060000100011000001011080000001100100100000001190][from i1][from i2][from i3][from i4][from i5][from i6][total conservation]\left[ \begin{array}{ccccccccc|c} & & & & A & & & & & \vv b \\ \hline -1 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & -1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 50 \\ 0 & 0 & -1 & 1 & 0 & 0 & 0 & 0 & 0 & -60 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & -1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & -1 & 1 & 0 & 80 \\ 0 & 0 & 0 & 0 & 0 & 1 & 1 & 0 & 0 & 100 \\ 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 190 \\ \end{array} \right] \begin{array}{l} \\ [\text{from } i_1]\\ [\text{from } i_2]\\ [\text{from } i_3]\\ [\text{from } i_4]\\ [\text{from } i_5]\\ [\text{from } i_6]\\ [\text{total conservation}]\\ \end{array}
augmented_matrix = np.array([
        [-1 , 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0],
        [0 , -1 , 1 , 0 , 1 , 0 , 0 , 0 , 0 , 50],
        [0 , 0 , -1 , 1 , 0 , 0 , 0 , 0 , 0 , -60],
        [0 , 0 , 0 , 1 , 0 , 0 , 0 , -1 , 1 , 0],
        [0 , 0 , 0 , 0 , 1 , 0 , -1 , 1 , 0 , 80],
        [0 , 0 , 0 , 0 , 0 , 1 , 1 , 0 , 0 , 100],
        [1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 190],
])
A = augmented_matrix[:,:-1]  # We take the first part of the augmented matrix, which represents A [: <- This means every row, :-1 <- This means all but the last column]
b = augmented_matrix[:, -1]  # Now the second part, which represents b [: <- This means every row, -1 <- This means only the last column]
matrix = Matrix(augmented_matrix)
echelon_form = matrix.echelon_form()
echelon_form
Loading...

Let’s print the row-echelon matrix in a nicer way:

[Uc110001000001101000050001100000600001000110000010110800000011001000000000000]\left[ \begin{array}{ccccccccc|c} & & & & U & & & & & \vv c \\ \hline -1 & 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0\\ 0 & -1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 50\\ 0 & 0 & -1 & 1 & 0 & 0 & 0 & 0 & 0 & -60\\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & -1 & 1 & 0\\ 0 & 0 & 0 & 0 & 1 & 0 & -1 & 1 & 0 & 80\\ 0 & 0 & 0 & 0 & 0 & 1 & 1 & 0 & 0 & 100\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \end{array} \right]

Looking at the row-echelon form, we should notice a few things. First, that the last row is empty. Second, that there are not pivots in every column. In the columns for x7,x8,x9x_7, x_{8}, x_{9}, there are no pivots. This means there are 3 free variables in this system. This makes sense, because we have only 6 intersections, giving 6 equations, but 9 flows, representing 9 variables. In order to find a solution, let’s set all of those free variables to be zero. We can think of this as closing all of the roads that are free variables.

Let’s solve for the free variables in terms of the basic variables:

First for x7x_7:

x6+x7=100x7=100x6\begin{align*} x_6 + x_7 &= 100 \\ x_7 &= 100 - x_6 \end{align*}

We know that if x7>100x_7>100, then x6x_6 will be negative.

Now for x8x_{8}:

x5x7+x8=80x8=80x5+x7\begin{align*} x_5 - x_7 + x_8 &= 80 \\ x_8 &= 80 - x_5 + x_7 \end{align*}

We also know that x8x_8 cannot be more than 80.

For x9x_{9}:

x4x8+x9=0x9=x4+x8\begin{align*} x_4 - x_8 + x_9 &= 0 \\ x_9 &= -x_4 + x_8 \end{align*}

Now let’s solve for the system, setting x7,x8,x9x_7, x_8, x_9 to be equal to 0. To do this, we first select all of the columns from the augmented matrix which correspond to basic variables.

smaller_augmented_matrix = np.array(echelon_form[:,[0,1,2,3,4,5,9]]).astype(np.float64)
"""
Above, we just selected all of the rows that correspond to the non-free variables. 
If the free variables are zero, then we can freely subtract them from the rest of the equations.
"""
# smaller_augmented_matrix = np.vstack([smaller_augmented_matrix, 
#                                       np.array([[0,0,0,0,0,0,0,0,1,0,80]]),
#                                      ])
smaller_augmented_matrix = smaller_augmented_matrix[:-1,:]
smaller_augmented_matrix
array([[ -1., 1., 0., 0., 0., 1., 0.], [ 0., -1., 1., 0., 1., 0., 50.], [ 0., 0., -1., 1., 0., 0., -60.], [ 0., 0., 0., 1., 0., 0., 0.], [ 0., 0., 0., 0., 1., 0., 80.], [ 0., 0., 0., 0., 0., 1., 100.]])

Now that there are pivots in all columns, we can try to use numpy’s solve method to get a solution.

sA = smaller_augmented_matrix[:,:-1]  # We take the first part of the augmented matrix, which represents A [: <- This means every row, :-1 <- This means all but the last column]
sb = smaller_augmented_matrix[:, -1]  # Now the second part, which represents b [: <- This means every row, -1 <- This means only the last column]
x = solve(sA,sb)
x
array([190., 90., 60., 0., 80., 100.])

Let’s write these solutions down first.

x1=190x2=90x3=60x4=0x5=80x6=100\begin{align} x_1 &= 190 \\ x_2 &= 90 \\ x_3 &= 60 \\ x_4 &= 0 \\ x_5 &= 80 \\ x_6 &= 100 \\ \end{align}

One key thing to remember is that these variables are currently unconstrained. In our road network, a negative number means that traffic is flowing the wrong way. Let’s verify that the free variables we specified are actually zero, and see what we can learn from each solution.

x7=100x6x7=0\begin{align*} x_7 &= 100 - x_6 \\ x_7 &= 0 \end{align*}
x8=80x5+x7x8=8080+0=0\begin{align*} x_8 &= 80 - x_5 + x_7 \\ x_8 &= 80 - 80 + 0 \\ &= 0 \end{align*}
x9=x4+x8x9=0+0=0\begin{align*} x_9 &= -x_4 + x_8 \\ x_9 &= 0 + 0 \\ &= 0 \end{align*}

Now let’s check our solution:

x=[x1x2x3x4x5x6x7x8x9]=[1909060080100000]x=\begin{bmatrix} x_1 \\ x_2 \\ x_3 \\ x_4 \\ x_5 \\ x_6 \\ x_7 \\ x_8 \\ x_9 \end{bmatrix} =\begin{bmatrix} 190 \\ 90 \\ 60 \\ 0 \\ 80 \\ 100 \\ 0 \\ 0 \\ 0 \end{bmatrix}
x = np.array([190,90,60,0,80,100,0,0,0])
print(A@x)
print(b)
print(A@x == b)
[  0  50 -60   0  80 100 190]
[  0  50 -60   0  80 100 190]
[ True  True  True  True  True  True  True]

As an extension, if you want to try to solve the same system, but fix the values for the free variables to different values. One way to do this would be to add equations to the system of equations that fix the values of specific variables. For example, you could add a row fixing x7=10x_7 = 10 like so:

[x1x2x3x4x5x6x7x8x9c00000010010] \left[ \begin{array}{ccccccccc|c} x_1 & x_2 & x_3 & x_4 & x_5 & x_6 & x_7 & x_8 & x_9 & \vv c \\ \hline 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 10\\ \end{array} \right]

If you’re interested in modeling traffic flow, check out the Cellular Transmission Model and Networked Macroscopic Fundamental Diagram models for more info on how traffic flow is modeled in practice, and some approaches to control traffic lights.

Binder