20
20
class Tableau :
21
21
"""Operate on simplex tableaus
22
22
23
- >>> t = Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2)
23
+ >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4]]), 2, 2)
24
+ Traceback (most recent call last):
25
+ ...
26
+ TypeError: Tableau must have type float64
27
+
28
+ >>> Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2, 2)
24
29
Traceback (most recent call last):
25
30
...
26
31
ValueError: RHS must be > 0
32
+
33
+ >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), -2, 2)
34
+ Traceback (most recent call last):
35
+ ...
36
+ ValueError: number of (artificial) variables must be a natural number
27
37
"""
28
38
29
- def __init__ (self , tableau : np .ndarray , n_vars : int ) -> None :
39
+ # Max iteration number to prevent cycling
40
+ maxiter = 100
41
+
42
+ def __init__ (
43
+ self , tableau : np .ndarray , n_vars : int , n_artificial_vars : int
44
+ ) -> None :
45
+ if tableau .dtype != "float64" :
46
+ raise TypeError ("Tableau must have type float64" )
47
+
30
48
# Check if RHS is negative
31
- if np . any (tableau [:, - 1 ], where = tableau [:, - 1 ] < 0 ):
49
+ if not (tableau [:, - 1 ] >= 0 ). all ( ):
32
50
raise ValueError ("RHS must be > 0" )
33
51
52
+ if n_vars < 2 or n_artificial_vars < 0 :
53
+ raise ValueError (
54
+ "number of (artificial) variables must be a natural number"
55
+ )
56
+
34
57
self .tableau = tableau
35
- self .n_rows , _ = tableau .shape
58
+ self .n_rows , n_cols = tableau .shape
36
59
37
60
# Number of decision variables x1, x2, x3...
38
- self .n_vars = n_vars
39
-
40
- # Number of artificial variables to be minimised
41
- self .n_art_vars = len (np .where (tableau [self .n_vars : - 1 ] == - 1 )[0 ])
61
+ self .n_vars , self .n_artificial_vars = n_vars , n_artificial_vars
42
62
43
63
# 2 if there are >= or == constraints (nonstandard), 1 otherwise (std)
44
- self .n_stages = (self .n_art_vars > 0 ) + 1
64
+ self .n_stages = (self .n_artificial_vars > 0 ) + 1
45
65
46
66
# Number of slack variables added to make inequalities into equalities
47
- self .n_slack = self .n_rows - self .n_stages
67
+ self .n_slack = n_cols - self .n_vars - self .n_artificial_vars - 1
48
68
49
69
# Objectives for each stage
50
70
self .objectives = ["max" ]
51
71
52
72
# In two stage simplex, first minimise then maximise
53
- if self .n_art_vars :
73
+ if self .n_artificial_vars :
54
74
self .objectives .append ("min" )
55
75
56
- self .col_titles = [ "" ]
76
+ self .col_titles = self . generate_col_titles ()
57
77
58
78
# Index of current pivot row and column
59
79
self .row_idx = None
@@ -62,48 +82,39 @@ def __init__(self, tableau: np.ndarray, n_vars: int) -> None:
62
82
# Does objective row only contain (non)-negative values?
63
83
self .stop_iter = False
64
84
65
- @staticmethod
66
- def generate_col_titles (* args : int ) -> list [str ]:
85
+ def generate_col_titles (self ) -> list [str ]:
67
86
"""Generate column titles for tableau of specific dimensions
68
87
69
- >>> Tableau.generate_col_titles(2, 3, 1)
70
- ['x1', 'x2', 's1', 's2', 's3', 'a1', 'RHS']
71
-
72
- >>> Tableau.generate_col_titles()
73
- Traceback (most recent call last):
74
- ...
75
- ValueError: Must provide n_vars, n_slack, and n_art_vars
76
- >>> Tableau.generate_col_titles(-2, 3, 1)
77
- Traceback (most recent call last):
78
- ...
79
- ValueError: All arguments must be non-negative integers
80
- """
81
- if len (args ) != 3 :
82
- raise ValueError ("Must provide n_vars, n_slack, and n_art_vars" )
88
+ >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]),
89
+ ... 2, 0).generate_col_titles()
90
+ ['x1', 'x2', 's1', 's2', 'RHS']
83
91
84
- if not all (x >= 0 and isinstance (x , int ) for x in args ):
85
- raise ValueError ("All arguments must be non-negative integers" )
92
+ >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]),
93
+ ... 2, 2).generate_col_titles()
94
+ ['x1', 'x2', 'RHS']
95
+ """
96
+ args = (self .n_vars , self .n_slack )
86
97
87
- # decision | slack | artificial
88
- string_starts = ["x" , "s" , "a" ]
98
+ # decision | slack
99
+ string_starts = ["x" , "s" ]
89
100
titles = []
90
- for i in range (3 ):
101
+ for i in range (2 ):
91
102
for j in range (args [i ]):
92
103
titles .append (string_starts [i ] + str (j + 1 ))
93
104
titles .append ("RHS" )
94
105
return titles
95
106
96
- def find_pivot (self , tableau : np . ndarray ) -> tuple [Any , Any ]:
107
+ def find_pivot (self ) -> tuple [Any , Any ]:
97
108
"""Finds the pivot row and column.
98
- >>> t = Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6], [1,2,0,1,7.]]), 2)
99
- >>> t .find_pivot(t.tableau )
109
+ >>> Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6], [1,2,0,1,7.]]),
110
+ ... 2, 0) .find_pivot()
100
111
(1, 0)
101
112
"""
102
113
objective = self .objectives [- 1 ]
103
114
104
115
# Find entries of highest magnitude in objective rows
105
116
sign = (objective == "min" ) - (objective == "max" )
106
- col_idx = np .argmax (sign * tableau [0 , : self . n_vars ])
117
+ col_idx = np .argmax (sign * self . tableau [0 , :- 1 ])
107
118
108
119
# Choice is only valid if below 0 for maximise, and above for minimise
109
120
if sign * self .tableau [0 , col_idx ] <= 0 :
@@ -117,15 +128,15 @@ def find_pivot(self, tableau: np.ndarray) -> tuple[Any, Any]:
117
128
s = slice (self .n_stages , self .n_rows )
118
129
119
130
# RHS
120
- dividend = tableau [s , - 1 ]
131
+ dividend = self . tableau [s , - 1 ]
121
132
122
133
# Elements of pivot column within slice
123
- divisor = tableau [s , col_idx ]
134
+ divisor = self . tableau [s , col_idx ]
124
135
125
136
# Array filled with nans
126
137
nans = np .full (self .n_rows - self .n_stages , np .nan )
127
138
128
- # If element in pivot column is greater than zeron_stages , return
139
+ # If element in pivot column is greater than zero , return
129
140
# quotient or nan otherwise
130
141
quotients = np .divide (dividend , divisor , out = nans , where = divisor > 0 )
131
142
@@ -134,67 +145,66 @@ def find_pivot(self, tableau: np.ndarray) -> tuple[Any, Any]:
134
145
row_idx = np .nanargmin (quotients ) + self .n_stages
135
146
return row_idx , col_idx
136
147
137
- def pivot (self , tableau : np . ndarray , row_idx : int , col_idx : int ) -> np .ndarray :
148
+ def pivot (self , row_idx : int , col_idx : int ) -> np .ndarray :
138
149
"""Pivots on value on the intersection of pivot row and column.
139
150
140
- >>> t = Tableau(np.array([[-2,-3,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]), 2)
141
- >>> t.pivot(t.tableau, 1, 0).tolist()
151
+ >>> Tableau(np.array([[-2,-3,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]),
152
+ ... 2, 2).pivot( 1, 0).tolist()
142
153
... # doctest: +NORMALIZE_WHITESPACE
143
154
[[0.0, 3.0, 2.0, 0.0, 8.0],
144
155
[1.0, 3.0, 1.0, 0.0, 4.0],
145
156
[0.0, -8.0, -3.0, 1.0, -8.0]]
146
157
"""
147
158
# Avoid changes to original tableau
148
- piv_row = tableau [row_idx ].copy ()
159
+ piv_row = self . tableau [row_idx ].copy ()
149
160
150
161
piv_val = piv_row [col_idx ]
151
162
152
163
# Entry becomes 1
153
164
piv_row *= 1 / piv_val
154
165
155
166
# Variable in pivot column becomes basic, ie the only non-zero entry
156
- for idx , coeff in enumerate (tableau [:, col_idx ]):
157
- tableau [idx ] += - coeff * piv_row
158
- tableau [row_idx ] = piv_row
159
- return tableau
167
+ for idx , coeff in enumerate (self . tableau [:, col_idx ]):
168
+ self . tableau [idx ] += - coeff * piv_row
169
+ self . tableau [row_idx ] = piv_row
170
+ return self . tableau
160
171
161
- def change_stage (self , tableau : np . ndarray ) -> np .ndarray :
172
+ def change_stage (self ) -> np .ndarray :
162
173
"""Exits first phase of the two-stage method by deleting artificial
163
174
rows and columns, or completes the algorithm if exiting the standard
164
175
case.
165
176
166
- >>> t = Tableau(np.array([
177
+ >>> Tableau(np.array([
167
178
... [3, 3, -1, -1, 0, 0, 4],
168
179
... [2, 1, 0, 0, 0, 0, 0.],
169
180
... [1, 2, -1, 0, 1, 0, 2],
170
181
... [2, 1, 0, -1, 0, 1, 2]
171
- ... ]), 2)
172
- >>> t.change_stage(t.tableau).tolist()
182
+ ... ]), 2, 2).change_stage().tolist()
173
183
... # doctest: +NORMALIZE_WHITESPACE
174
- [[2.0, 1.0, 0.0, 0.0, 0.0, 0.0 ],
175
- [1.0, 2.0, -1.0, 0.0, 1.0, 2.0],
176
- [2.0, 1.0, 0.0, -1.0, 0.0, 2.0]]
184
+ [[2.0, 1.0, 0.0, 0.0, 0.0],
185
+ [1.0, 2.0, -1.0, 0.0, 2.0],
186
+ [2.0, 1.0, 0.0, -1.0, 2.0]]
177
187
"""
178
188
# Objective of original objective row remains
179
189
self .objectives .pop ()
180
190
181
191
if not self .objectives :
182
- return tableau
192
+ return self . tableau
183
193
184
194
# Slice containing ids for artificial columns
185
- s = slice (- self .n_art_vars - 1 , - 1 )
195
+ s = slice (- self .n_artificial_vars - 1 , - 1 )
186
196
187
197
# Delete the artificial variable columns
188
- tableau = np .delete (tableau , s , axis = 1 )
198
+ self . tableau = np .delete (self . tableau , s , axis = 1 )
189
199
190
200
# Delete the objective row of the first stage
191
- tableau = np .delete (tableau , 0 , axis = 0 )
201
+ self . tableau = np .delete (self . tableau , 0 , axis = 0 )
192
202
193
203
self .n_stages = 1
194
204
self .n_rows -= 1
195
- self .n_art_vars = 0
205
+ self .n_artificial_vars = 0
196
206
self .stop_iter = False
197
- return tableau
207
+ return self . tableau
198
208
199
209
def run_simplex (self ) -> dict [Any , Any ]:
200
210
"""Operate on tableau until objective function cannot be
@@ -205,15 +215,29 @@ def run_simplex(self) -> dict[Any, Any]:
205
215
ST: x1 + 3x2 <= 4
206
216
3x1 + x2 <= 4
207
217
>>> Tableau(np.array([[-1,-1,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]),
208
- ... 2).run_simplex()
218
+ ... 2, 0 ).run_simplex()
209
219
{'P': 2.0, 'x1': 1.0, 'x2': 1.0}
210
220
221
+ # Standard linear program with 3 variables:
222
+ Max: 3x1 + x2 + 3x3
223
+ ST: 2x1 + x2 + x3 ≤ 2
224
+ x1 + 2x2 + 3x3 ≤ 5
225
+ 2x1 + 2x2 + x3 ≤ 6
226
+ >>> Tableau(np.array([
227
+ ... [-3,-1,-3,0,0,0,0],
228
+ ... [2,1,1,1,0,0,2],
229
+ ... [1,2,3,0,1,0,5],
230
+ ... [2,2,1,0,0,1,6.]
231
+ ... ]),3,0).run_simplex() # doctest: +ELLIPSIS
232
+ {'P': 5.4, 'x1': 0.199..., 'x3': 1.6}
233
+
234
+
211
235
# Optimal tableau input:
212
236
>>> Tableau(np.array([
213
237
... [0, 0, 0.25, 0.25, 2],
214
238
... [0, 1, 0.375, -0.125, 1],
215
239
... [1, 0, -0.125, 0.375, 1]
216
- ... ]), 2).run_simplex()
240
+ ... ]), 2, 0 ).run_simplex()
217
241
{'P': 2.0, 'x1': 1.0, 'x2': 1.0}
218
242
219
243
# Non-standard: >= constraints
@@ -227,81 +251,84 @@ def run_simplex(self) -> dict[Any, Any]:
227
251
... [1, 1, 1, 1, 0, 0, 0, 0, 40],
228
252
... [2, 1, -1, 0, -1, 0, 1, 0, 10],
229
253
... [0, -1, 1, 0, 0, -1, 0, 1, 10.]
230
- ... ]), 3).run_simplex()
254
+ ... ]), 3, 2 ).run_simplex()
231
255
{'P': 70.0, 'x1': 10.0, 'x2': 10.0, 'x3': 20.0}
232
256
233
257
# Non standard: minimisation and equalities
234
258
Min: x1 + x2
235
259
ST: 2x1 + x2 = 12
236
260
6x1 + 5x2 = 40
237
261
>>> Tableau(np.array([
238
- ... [8, 6, 0, -1, 0, -1, 0, 0, 52],
239
- ... [1, 1, 0, 0, 0, 0, 0, 0, 0],
240
- ... [2, 1, 1, 0, 0, 0, 0, 0, 12],
241
- ... [2, 1, 0, -1, 0, 0, 1, 0, 12],
242
- ... [6, 5, 0, 0, 1, 0, 0, 0, 40],
243
- ... [6, 5, 0, 0, 0, -1, 0, 1, 40.]
244
- ... ]), 2).run_simplex()
262
+ ... [8, 6, 0, 0, 52],
263
+ ... [1, 1, 0, 0, 0],
264
+ ... [2, 1, 1, 0, 12],
265
+ ... [6, 5, 0, 1, 40.],
266
+ ... ]), 2, 2).run_simplex()
245
267
{'P': 7.0, 'x1': 5.0, 'x2': 2.0}
268
+
269
+
270
+ # Pivot on slack variables
271
+ Max: 8x1 + 6x2
272
+ ST: x1 + 3x2 <= 33
273
+ 4x1 + 2x2 <= 48
274
+ 2x1 + 4x2 <= 48
275
+ x1 + x2 >= 10
276
+ x1 >= 2
277
+ >>> Tableau(np.array([
278
+ ... [2, 1, 0, 0, 0, -1, -1, 0, 0, 12.0],
279
+ ... [-8, -6, 0, 0, 0, 0, 0, 0, 0, 0.0],
280
+ ... [1, 3, 1, 0, 0, 0, 0, 0, 0, 33.0],
281
+ ... [4, 2, 0, 1, 0, 0, 0, 0, 0, 60.0],
282
+ ... [2, 4, 0, 0, 1, 0, 0, 0, 0, 48.0],
283
+ ... [1, 1, 0, 0, 0, -1, 0, 1, 0, 10.0],
284
+ ... [1, 0, 0, 0, 0, 0, -1, 0, 1, 2.0]
285
+ ... ]), 2, 2).run_simplex() # doctest: +ELLIPSIS
286
+ {'P': 132.0, 'x1': 12.000... 'x2': 5.999...}
246
287
"""
247
288
# Stop simplex algorithm from cycling.
248
- for _ in range (100 ):
289
+ for _ in range (Tableau . maxiter ):
249
290
# Completion of each stage removes an objective. If both stages
250
291
# are complete, then no objectives are left
251
292
if not self .objectives :
252
- self .col_titles = self .generate_col_titles (
253
- self .n_vars , self .n_slack , self .n_art_vars
254
- )
255
-
256
293
# Find the values of each variable at optimal solution
257
- return self .interpret_tableau (self . tableau , self . col_titles )
294
+ return self .interpret_tableau ()
258
295
259
- row_idx , col_idx = self .find_pivot (self . tableau )
296
+ row_idx , col_idx = self .find_pivot ()
260
297
261
298
# If there are no more negative values in objective row
262
299
if self .stop_iter :
263
300
# Delete artificial variable columns and rows. Update attributes
264
- self .tableau = self .change_stage (self . tableau )
301
+ self .tableau = self .change_stage ()
265
302
else :
266
- self .tableau = self .pivot (self . tableau , row_idx , col_idx )
303
+ self .tableau = self .pivot (row_idx , col_idx )
267
304
return {}
268
305
269
- def interpret_tableau (
270
- self , tableau : np .ndarray , col_titles : list [str ]
271
- ) -> dict [str , float ]:
306
+ def interpret_tableau (self ) -> dict [str , float ]:
272
307
"""Given the final tableau, add the corresponding values of the basic
273
308
decision variables to the `output_dict`
274
- >>> tableau = np.array([
309
+ >>> Tableau( np.array([
275
310
... [0,0,0.875,0.375,5],
276
311
... [0,1,0.375,-0.125,1],
277
312
... [1,0,-0.125,0.375,1]
278
- ... ])
279
- >>> t = Tableau(tableau, 2)
280
- >>> t.interpret_tableau(tableau, ["x1", "x2", "s1", "s2", "RHS"])
313
+ ... ]),2, 0).interpret_tableau()
281
314
{'P': 5.0, 'x1': 1.0, 'x2': 1.0}
282
315
"""
283
316
# P = RHS of final tableau
284
- output_dict = {"P" : abs (tableau [0 , - 1 ])}
317
+ output_dict = {"P" : abs (self . tableau [0 , - 1 ])}
285
318
286
319
for i in range (self .n_vars ):
287
- # Gives ids of nonzero entries in the ith column
288
- nonzero = np .nonzero (tableau [:, i ])
320
+ # Gives indices of nonzero entries in the ith column
321
+ nonzero = np .nonzero (self . tableau [:, i ])
289
322
n_nonzero = len (nonzero [0 ])
290
323
291
- # First entry in the nonzero ids
324
+ # First entry in the nonzero indices
292
325
nonzero_rowidx = nonzero [0 ][0 ]
293
- nonzero_val = tableau [nonzero_rowidx , i ]
326
+ nonzero_val = self . tableau [nonzero_rowidx , i ]
294
327
295
328
# If there is only one nonzero value in column, which is one
296
- if n_nonzero == nonzero_val == 1 :
297
- rhs_val = tableau [nonzero_rowidx , - 1 ]
298
- output_dict [col_titles [i ]] = rhs_val
299
-
300
- # Check for basic variables
301
- for title in col_titles :
302
- # Don't add RHS or slack variables to output dict
303
- if title [0 ] not in "R-s-a" :
304
- output_dict .setdefault (title , 0 )
329
+ if n_nonzero == 1 and nonzero_val == 1 :
330
+ rhs_val = self .tableau [nonzero_rowidx , - 1 ]
331
+ output_dict [self .col_titles [i ]] = rhs_val
305
332
return output_dict
306
333
307
334
0 commit comments