-
Notifications
You must be signed in to change notification settings - Fork 17
/
simplex.py
274 lines (218 loc) · 10 KB
/
simplex.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
from fractions import Fraction
from warnings import warn
class Simplex(object):
def __init__(self, num_vars, constraints, objective_function):
"""
num_vars: Number of variables
equations: A list of strings representing constraints
each variable should be start with x followed by a underscore
and a number
eg of constraints
['1x_1 + 2x_2 >= 4', '2x_3 + 3x_1 <= 5', 'x_3 + 3x_2 = 6']
Note that in x_num, num should not be more than num_vars.
Also single spaces should be used in expressions.
objective_function: should be a tuple with first element
either 'min' or 'max', and second element be the equation
eg
('min', '2x_1 + 4x_3 + 5x_2')
For solution finding algorithm uses two-phase simplex method
"""
self.num_vars = num_vars
self.constraints = constraints
self.objective = objective_function[0]
self.objective_function = objective_function[1]
self.coeff_matrix, self.r_rows, self.num_s_vars, self.num_r_vars = self.construct_matrix_from_constraints()
del self.constraints
self.basic_vars = [0 for i in range(len(self.coeff_matrix))]
self.phase1()
r_index = self.num_r_vars + self.num_s_vars
for i in self.basic_vars:
if i > r_index:
raise ValueError("Infeasible solution")
self.delete_r_vars()
if 'min' in self.objective.lower():
self.solution = self.objective_minimize()
else:
self.solution = self.objective_maximize()
self.optimize_val = self.coeff_matrix[0][-1]
def construct_matrix_from_constraints(self):
num_s_vars = 0 # number of slack and surplus variables
num_r_vars = 0 # number of additional variables to balance equality and less than equal to
for expression in self.constraints:
if '>=' in expression:
num_s_vars += 1
elif '<=' in expression:
num_s_vars += 1
num_r_vars += 1
elif '=' in expression:
num_r_vars += 1
total_vars = self.num_vars + num_s_vars + num_r_vars
coeff_matrix = [[Fraction("0/1") for i in range(total_vars+1)] for j in range(len(self.constraints)+1)]
s_index = self.num_vars
r_index = self.num_vars + num_s_vars
r_rows = [] # stores the non -zero index of r
for i in range(1, len(self.constraints)+1):
constraint = self.constraints[i-1].split(' ')
for j in range(len(constraint)):
if '_' in constraint[j]:
coeff, index = constraint[j].split('_')
if constraint[j-1] is '-':
coeff_matrix[i][int(index)-1] = Fraction("-" + coeff[:-1] + "/1")
else:
coeff_matrix[i][int(index)-1] = Fraction(coeff[:-1] + "/1")
elif constraint[j] == '<=':
coeff_matrix[i][s_index] = Fraction("1/1") # add surplus variable
s_index += 1
elif constraint[j] == '>=':
coeff_matrix[i][s_index] = Fraction("-1/1") # slack variable
coeff_matrix[i][r_index] = Fraction("1/1") # r variable
s_index += 1
r_index += 1
r_rows.append(i)
elif constraint[j] == '=':
coeff_matrix[i][r_index] = Fraction("1/1") # r variable
r_index += 1
r_rows.append(i)
coeff_matrix[i][-1] = Fraction(constraint[-1] + "/1")
return coeff_matrix, r_rows, num_s_vars, num_r_vars
def phase1(self):
# Objective function here is minimize r1+ r2 + r3 + ... + rn
r_index = self.num_vars + self.num_s_vars
for i in range(r_index, len(self.coeff_matrix[0])-1):
self.coeff_matrix[0][i] = Fraction("-1/1")
coeff_0 = 0
for i in self.r_rows:
self.coeff_matrix[0] = add_row(self.coeff_matrix[0], self.coeff_matrix[i])
self.basic_vars[i] = r_index
r_index += 1
s_index = self.num_vars
for i in range(1, len(self.basic_vars)):
if self.basic_vars[i] == 0:
self.basic_vars[i] = s_index
s_index += 1
# Run the simplex iterations
key_column = max_index(self.coeff_matrix[0])
condition = self.coeff_matrix[0][key_column] > 0
while condition is True:
key_row = self.find_key_row(key_column = key_column)
self.basic_vars[key_row] = key_column
pivot = self.coeff_matrix[key_row][key_column]
self.normalize_to_pivot(key_row, pivot)
self.make_key_column_zero(key_column, key_row)
key_column = max_index(self.coeff_matrix[0])
condition = self.coeff_matrix[0][key_column] > 0
def find_key_row(self, key_column):
min_val = float("inf")
min_i = 0
for i in range(1, len(self.coeff_matrix)):
if self.coeff_matrix[i][key_column] > 0:
val = self.coeff_matrix[i][-1] / self.coeff_matrix[i][key_column]
if val < min_val:
min_val = val
min_i = i
if min_val == float("inf"):
raise ValueError("Unbounded solution")
if min_val == 0:
warn("Dengeneracy")
return min_i
def normalize_to_pivot(self, key_row, pivot):
for i in range(len(self.coeff_matrix[0])):
self.coeff_matrix[key_row][i] /= pivot
def make_key_column_zero(self, key_column, key_row):
num_columns = len(self.coeff_matrix[0])
for i in range(len(self.coeff_matrix)):
if i != key_row:
factor = self.coeff_matrix[i][key_column]
for j in range(num_columns):
self.coeff_matrix[i][j] -= self.coeff_matrix[key_row][j] * factor
def delete_r_vars(self):
for i in range(len(self.coeff_matrix)):
non_r_length = self.num_vars + self.num_s_vars + 1
length = len(self.coeff_matrix[i])
while length != non_r_length:
del self.coeff_matrix[i][non_r_length-1]
length -= 1
def update_objective_function(self):
objective_function_coeffs = self.objective_function.split()
for i in range(len(objective_function_coeffs)):
if '_' in objective_function_coeffs[i]:
coeff, index = objective_function_coeffs[i].split('_')
if objective_function_coeffs[i-1] is '-':
self.coeff_matrix[0][int(index)-1] = Fraction(coeff[:-1] + "/1")
else:
self.coeff_matrix[0][int(index)-1] = Fraction("-" + coeff[:-1] + "/1")
def check_alternate_solution(self):
for i in range(len(self.coeff_matrix[0])):
if self.coeff_matrix[0][i] and i not in self.basic_vars[1:]:
warn("Alternate Solution exists")
break
def objective_minimize(self):
self.update_objective_function()
for row, column in enumerate(self.basic_vars[1:]):
if self.coeff_matrix[0][column] != 0:
self.coeff_matrix[0] = add_row(self.coeff_matrix[0], multiply_const_row(-self.coeff_matrix[0][column], self.coeff_matrix[row+1]))
key_column = max_index(self.coeff_matrix[0])
condition = self.coeff_matrix[0][key_column] > 0
while condition is True:
key_row = self.find_key_row(key_column = key_column)
self.basic_vars[key_row] = key_column
pivot = self.coeff_matrix[key_row][key_column]
self.normalize_to_pivot(key_row, pivot)
self.make_key_column_zero(key_column, key_row)
key_column = max_index(self.coeff_matrix[0])
condition = self.coeff_matrix[0][key_column] > 0
solution = {}
for i, var in enumerate(self.basic_vars[1:]):
if var < self.num_vars:
solution['x_'+str(var+1)] = self.coeff_matrix[i+1][-1]
for i in range(0, self.num_vars):
if i not in self.basic_vars[1:]:
solution['x_'+str(i+1)] = Fraction("0/1")
self.check_alternate_solution()
return solution
def objective_maximize(self):
self.update_objective_function()
for row, column in enumerate(self.basic_vars[1:]):
if self.coeff_matrix[0][column] != 0:
self.coeff_matrix[0] = add_row(self.coeff_matrix[0], multiply_const_row(-self.coeff_matrix[0][column], self.coeff_matrix[row+1]))
key_column = min_index(self.coeff_matrix[0])
condition = self.coeff_matrix[0][key_column] < 0
while condition is True:
key_row = self.find_key_row(key_column = key_column)
self.basic_vars[key_row] = key_column
pivot = self.coeff_matrix[key_row][key_column]
self.normalize_to_pivot(key_row, pivot)
self.make_key_column_zero(key_column, key_row)
key_column = min_index(self.coeff_matrix[0])
condition = self.coeff_matrix[0][key_column] < 0
solution = {}
for i, var in enumerate(self.basic_vars[1:]):
if var < self.num_vars:
solution['x_'+str(var+1)] = self.coeff_matrix[i+1][-1]
for i in range(0, self.num_vars):
if i not in self.basic_vars[1:]:
solution['x_'+str(i+1)] = Fraction("0/1")
self.check_alternate_solution()
return solution
def add_row(row1, row2):
row_sum = [0 for i in range(len(row1))]
for i in range(len(row1)):
row_sum[i] = row1[i] + row2[i]
return row_sum
def max_index(row):
max_i = 0
for i in range(0, len(row)-1):
if row[i] > row[max_i]:
max_i = i
return max_i
def multiply_const_row(const, row):
mul_row = []
for i in row:
mul_row.append(const*i)
return mul_row
def min_index(row):
min_i = 0
for i in range(0, len(row)):
if row[min_i] > row[i]:
min_i = i
return min_i