├── README.md ├── tests ├── test.py └── standard-form-test.py ├── LICENSE └── simplex.py /README.md: -------------------------------------------------------------------------------- 1 | simplex-algorithm 2 | ================= 3 | 4 | Python source code for [Linear Programming and the Simplex Algorithm](http://jeremykun.com/2014/12/01/linear-programming-and-the-simplex-algorithm/) 5 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | def test(expected, actual): 2 | if expected != actual: 3 | import sys, traceback 4 | (filename, lineno, container, code) = traceback.extract_stack()[-2] 5 | print("Test: %r failed on line %d in file %r.\nExpected %r but got %r\n" % 6 | (code, lineno, filename, expected, actual)) 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jeremy Kun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/standard-form-test.py: -------------------------------------------------------------------------------- 1 | from test import test 2 | 3 | import simplex 4 | 5 | def testFromPost(): 6 | cost = [1,1,1] 7 | gts = [[0,1,4]] 8 | gtB = [10] 9 | lts = [[3,-2,0]] 10 | ltB = [7] 11 | eqs = [[1,1,0]] 12 | eqB = [2] 13 | 14 | expectedCost = [1,1,1,0,0] 15 | expectedConstraints = [[0,1,4,-1,0], [3,-2,0,0,1], [1,1,0,0,0]] 16 | expectedThresholds = [10,7,2] 17 | test((expectedCost, expectedConstraints, expectedThresholds), 18 | simplex.standardForm(cost, gts, gtB, lts, ltB, eqs, eqB)) 19 | 20 | 21 | def test2(): 22 | cost = [1,1,1] 23 | lts = [[3,-2,0]] 24 | ltB = [7] 25 | eqs = [[1,1,0]] 26 | eqB = [2] 27 | 28 | expectedCost = [1,1,1,0] 29 | expectedConstraints = [[3,-2,0,1], [1,1,0,0]] 30 | expectedThresholds = [7,2] 31 | test((expectedCost, expectedConstraints, expectedThresholds), 32 | simplex.standardForm(cost, lessThans=lts, ltThreshold=ltB, equalities=eqs, eqThreshold=eqB)) 33 | 34 | 35 | def test3(): 36 | cost = [1,1,1] 37 | eqs = [[1,1,0], [2,2,2]] 38 | eqB = [2, 5] 39 | 40 | expectedCost = [1,1,1] 41 | expectedConstraints = [[3,-2,0], [1,1,0]] 42 | expectedThresholds = [2, 5] 43 | test((expectedCost, expectedConstraints, expectedThresholds), 44 | simplex.standardForm(cost, equalities=eqs, eqThreshold=eqB)) 45 | 46 | 47 | if __name__ == "__main__": 48 | testFromPost() 49 | -------------------------------------------------------------------------------- /simplex.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | 3 | 4 | ''' 5 | Return a rectangular identity matrix with the specified diagonal entiries, possibly 6 | starting in the middle. 7 | ''' 8 | def identity(numRows, numCols, val=1, rowStart=0): 9 | return [[(val if i == j else 0) for j in range(numCols)] 10 | for i in range(rowStart, numRows)] 11 | 12 | 13 | ''' 14 | standardForm: [float], [[float]], [float], [[float]], [float], [[float]], [float] -> [float], [[float]], [float] 15 | Convert a linear program in general form to the standard form for the 16 | simplex algorithm. The inputs are assumed to have the correct dimensions: cost 17 | is a length n list, greaterThans is an n-by-m matrix, gtThreshold is a vector 18 | of length m, with the same pattern holding for the remaining inputs. No 19 | dimension errors are caught, and we assume there are no unrestricted variables. 20 | ''' 21 | def standardForm(cost, greaterThans=[], gtThreshold=[], lessThans=[], ltThreshold=[], 22 | equalities=[], eqThreshold=[], maximization=True): 23 | newVars = 0 24 | numRows = 0 25 | if gtThreshold != []: 26 | newVars += len(gtThreshold) 27 | numRows += len(gtThreshold) 28 | if ltThreshold != []: 29 | newVars += len(ltThreshold) 30 | numRows += len(ltThreshold) 31 | if eqThreshold != []: 32 | numRows += len(eqThreshold) 33 | 34 | if not maximization: 35 | cost = [-x for x in cost] 36 | 37 | if newVars == 0: 38 | return cost, equalities, eqThreshold 39 | 40 | newCost = list(cost) + [0] * newVars 41 | 42 | constraints = [] 43 | threshold = [] 44 | 45 | oldConstraints = [(greaterThans, gtThreshold, -1), (lessThans, ltThreshold, 1), 46 | (equalities, eqThreshold, 0)] 47 | 48 | offset = 0 49 | for constraintList, oldThreshold, coefficient in oldConstraints: 50 | constraints += [c + r for c, r in zip(constraintList, 51 | identity(numRows, newVars, coefficient, offset))] 52 | 53 | threshold += oldThreshold 54 | offset += len(oldThreshold) 55 | 56 | return newCost, constraints, threshold 57 | 58 | 59 | def dot(a,b): 60 | return sum(x*y for x,y in zip(a,b)) 61 | 62 | def column(A, j): 63 | return [row[j] for row in A] 64 | 65 | def transpose(A): 66 | return [column(A, j) for j in range(len(A[0]))] 67 | 68 | def isPivotCol(col): 69 | return (len([c for c in col if c == 0]) == len(col) - 1) and sum(col) == 1 70 | 71 | def variableValueForPivotColumn(tableau, column): 72 | pivotRow = [i for (i, x) in enumerate(column) if x == 1][0] 73 | return tableau[pivotRow][-1] 74 | 75 | # assume the last m columns of A are the slack variables; the initial basis is 76 | # the set of slack variables 77 | def initialTableau(c, A, b): 78 | tableau = [row[:] + [x] for row, x in zip(A, b)] 79 | tableau.append([ci for ci in c] + [0]) 80 | return tableau 81 | 82 | 83 | def primalSolution(tableau): 84 | # the pivot columns denote which variables are used 85 | columns = transpose(tableau) 86 | indices = [j for j, col in enumerate(columns[:-1]) if isPivotCol(col)] 87 | return [(colIndex, variableValueForPivotColumn(tableau, columns[colIndex])) 88 | for colIndex in indices] 89 | 90 | 91 | def objectiveValue(tableau): 92 | return -(tableau[-1][-1]) 93 | 94 | 95 | def canImprove(tableau): 96 | lastRow = tableau[-1] 97 | return any(x > 0 for x in lastRow[:-1]) 98 | 99 | 100 | # this can be slightly faster 101 | def moreThanOneMin(L): 102 | if len(L) <= 1: 103 | return False 104 | 105 | x,y = heapq.nsmallest(2, L, key=lambda x: x[1]) 106 | return x == y 107 | 108 | 109 | def findPivotIndex(tableau): 110 | # pick minimum positive index of the last row 111 | column_choices = [(i,x) for (i,x) in enumerate(tableau[-1][:-1]) if x > 0] 112 | column = min(column_choices, key=lambda a: a[1])[0] 113 | 114 | # check if unbounded 115 | if all(row[column] <= 0 for row in tableau): 116 | raise Exception('Linear program is unbounded.') 117 | 118 | # check for degeneracy: more than one minimizer of the quotient 119 | quotients = [(i, r[-1] / r[column]) 120 | for i,r in enumerate(tableau[:-1]) if r[column] > 0] 121 | 122 | if moreThanOneMin(quotients): 123 | raise Exception('Linear program is degenerate.') 124 | 125 | # pick row index minimizing the quotient 126 | row = min(quotients, key=lambda x: x[1])[0] 127 | 128 | return row, column 129 | 130 | 131 | def pivotAbout(tableau, pivot): 132 | i,j = pivot 133 | 134 | pivotDenom = tableau[i][j] 135 | tableau[i] = [x / pivotDenom for x in tableau[i]] 136 | 137 | for k,row in enumerate(tableau): 138 | if k != i: 139 | pivotRowMultiple = [y * tableau[k][j] for y in tableau[i]] 140 | tableau[k] = [x - y for x,y in zip(tableau[k], pivotRowMultiple)] 141 | 142 | 143 | ''' 144 | simplex: [float], [[float]], [float] -> [float], float 145 | Solve the given standard-form linear program: 146 | 147 | max 148 | s.t. Ax = b 149 | x >= 0 150 | 151 | providing the optimal solution x* and the value of the objective function 152 | ''' 153 | def simplex(c, A, b): 154 | tableau = initialTableau(c, A, b) 155 | print("Initial tableau:") 156 | for row in tableau: 157 | print(row) 158 | print() 159 | 160 | while canImprove(tableau): 161 | pivot = findPivotIndex(tableau) 162 | print("Next pivot index is=%d,%d \n" % pivot) 163 | pivotAbout(tableau, pivot) 164 | print("Tableau after pivot:") 165 | for row in tableau: 166 | print(row) 167 | print() 168 | 169 | return tableau, primalSolution(tableau), objectiveValue(tableau) 170 | 171 | 172 | if __name__ == "__main__": 173 | c = [300, 250, 450] 174 | A = [[15, 20, 25], [35, 60, 60], [20, 30, 25], [0, 250, 0]] 175 | b = [1200, 3000, 1500, 500] 176 | 177 | # add slack variables by hand 178 | A[0] += [1,0,0,0] 179 | A[1] += [0,1,0,0] 180 | A[2] += [0,0,1,0] 181 | A[3] += [0,0,0,-1] 182 | c += [0,0,0,0] 183 | 184 | t, s, v = simplex(c, A, b) 185 | print(s) 186 | print(v) 187 | --------------------------------------------------------------------------------