2020class Tableau :
2121 """Operate on simplex tableaus
2222
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)
2429 Traceback (most recent call last):
2530 ...
2631 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
2737 """
2838
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+
3048 # Check if RHS is negative
31- if np . any (tableau [:, - 1 ], where = tableau [:, - 1 ] < 0 ):
49+ if not (tableau [:, - 1 ] >= 0 ). all ( ):
3250 raise ValueError ("RHS must be > 0" )
3351
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+
3457 self .tableau = tableau
35- self .n_rows , _ = tableau .shape
58+ self .n_rows , n_cols = tableau .shape
3659
3760 # 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
4262
4363 # 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
4565
4666 # 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
4868
4969 # Objectives for each stage
5070 self .objectives = ["max" ]
5171
5272 # In two stage simplex, first minimise then maximise
53- if self .n_art_vars :
73+ if self .n_artificial_vars :
5474 self .objectives .append ("min" )
5575
56- self .col_titles = [ "" ]
76+ self .col_titles = self . generate_col_titles ()
5777
5878 # Index of current pivot row and column
5979 self .row_idx = None
@@ -62,48 +82,39 @@ def __init__(self, tableau: np.ndarray, n_vars: int) -> None:
6282 # Does objective row only contain (non)-negative values?
6383 self .stop_iter = False
6484
65- @staticmethod
66- def generate_col_titles (* args : int ) -> list [str ]:
85+ def generate_col_titles (self ) -> list [str ]:
6786 """Generate column titles for tableau of specific dimensions
6887
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']
8391
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 )
8697
87- # decision | slack | artificial
88- string_starts = ["x" , "s" , "a" ]
98+ # decision | slack
99+ string_starts = ["x" , "s" ]
89100 titles = []
90- for i in range (3 ):
101+ for i in range (2 ):
91102 for j in range (args [i ]):
92103 titles .append (string_starts [i ] + str (j + 1 ))
93104 titles .append ("RHS" )
94105 return titles
95106
96- def find_pivot (self , tableau : np . ndarray ) -> tuple [Any , Any ]:
107+ def find_pivot (self ) -> tuple [Any , Any ]:
97108 """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()
100111 (1, 0)
101112 """
102113 objective = self .objectives [- 1 ]
103114
104115 # Find entries of highest magnitude in objective rows
105116 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 ])
107118
108119 # Choice is only valid if below 0 for maximise, and above for minimise
109120 if sign * self .tableau [0 , col_idx ] <= 0 :
@@ -117,15 +128,15 @@ def find_pivot(self, tableau: np.ndarray) -> tuple[Any, Any]:
117128 s = slice (self .n_stages , self .n_rows )
118129
119130 # RHS
120- dividend = tableau [s , - 1 ]
131+ dividend = self . tableau [s , - 1 ]
121132
122133 # Elements of pivot column within slice
123- divisor = tableau [s , col_idx ]
134+ divisor = self . tableau [s , col_idx ]
124135
125136 # Array filled with nans
126137 nans = np .full (self .n_rows - self .n_stages , np .nan )
127138
128- # If element in pivot column is greater than zeron_stages , return
139+ # If element in pivot column is greater than zero , return
129140 # quotient or nan otherwise
130141 quotients = np .divide (dividend , divisor , out = nans , where = divisor > 0 )
131142
@@ -134,67 +145,66 @@ def find_pivot(self, tableau: np.ndarray) -> tuple[Any, Any]:
134145 row_idx = np .nanargmin (quotients ) + self .n_stages
135146 return row_idx , col_idx
136147
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 :
138149 """Pivots on value on the intersection of pivot row and column.
139150
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()
142153 ... # doctest: +NORMALIZE_WHITESPACE
143154 [[0.0, 3.0, 2.0, 0.0, 8.0],
144155 [1.0, 3.0, 1.0, 0.0, 4.0],
145156 [0.0, -8.0, -3.0, 1.0, -8.0]]
146157 """
147158 # Avoid changes to original tableau
148- piv_row = tableau [row_idx ].copy ()
159+ piv_row = self . tableau [row_idx ].copy ()
149160
150161 piv_val = piv_row [col_idx ]
151162
152163 # Entry becomes 1
153164 piv_row *= 1 / piv_val
154165
155166 # 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
160171
161- def change_stage (self , tableau : np . ndarray ) -> np .ndarray :
172+ def change_stage (self ) -> np .ndarray :
162173 """Exits first phase of the two-stage method by deleting artificial
163174 rows and columns, or completes the algorithm if exiting the standard
164175 case.
165176
166- >>> t = Tableau(np.array([
177+ >>> Tableau(np.array([
167178 ... [3, 3, -1, -1, 0, 0, 4],
168179 ... [2, 1, 0, 0, 0, 0, 0.],
169180 ... [1, 2, -1, 0, 1, 0, 2],
170181 ... [2, 1, 0, -1, 0, 1, 2]
171- ... ]), 2)
172- >>> t.change_stage(t.tableau).tolist()
182+ ... ]), 2, 2).change_stage().tolist()
173183 ... # 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]]
177187 """
178188 # Objective of original objective row remains
179189 self .objectives .pop ()
180190
181191 if not self .objectives :
182- return tableau
192+ return self . tableau
183193
184194 # Slice containing ids for artificial columns
185- s = slice (- self .n_art_vars - 1 , - 1 )
195+ s = slice (- self .n_artificial_vars - 1 , - 1 )
186196
187197 # Delete the artificial variable columns
188- tableau = np .delete (tableau , s , axis = 1 )
198+ self . tableau = np .delete (self . tableau , s , axis = 1 )
189199
190200 # 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 )
192202
193203 self .n_stages = 1
194204 self .n_rows -= 1
195- self .n_art_vars = 0
205+ self .n_artificial_vars = 0
196206 self .stop_iter = False
197- return tableau
207+ return self . tableau
198208
199209 def run_simplex (self ) -> dict [Any , Any ]:
200210 """Operate on tableau until objective function cannot be
@@ -205,15 +215,29 @@ def run_simplex(self) -> dict[Any, Any]:
205215 ST: x1 + 3x2 <= 4
206216 3x1 + x2 <= 4
207217 >>> 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()
209219 {'P': 2.0, 'x1': 1.0, 'x2': 1.0}
210220
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+
211235 # Optimal tableau input:
212236 >>> Tableau(np.array([
213237 ... [0, 0, 0.25, 0.25, 2],
214238 ... [0, 1, 0.375, -0.125, 1],
215239 ... [1, 0, -0.125, 0.375, 1]
216- ... ]), 2).run_simplex()
240+ ... ]), 2, 0 ).run_simplex()
217241 {'P': 2.0, 'x1': 1.0, 'x2': 1.0}
218242
219243 # Non-standard: >= constraints
@@ -227,81 +251,84 @@ def run_simplex(self) -> dict[Any, Any]:
227251 ... [1, 1, 1, 1, 0, 0, 0, 0, 40],
228252 ... [2, 1, -1, 0, -1, 0, 1, 0, 10],
229253 ... [0, -1, 1, 0, 0, -1, 0, 1, 10.]
230- ... ]), 3).run_simplex()
254+ ... ]), 3, 2 ).run_simplex()
231255 {'P': 70.0, 'x1': 10.0, 'x2': 10.0, 'x3': 20.0}
232256
233257 # Non standard: minimisation and equalities
234258 Min: x1 + x2
235259 ST: 2x1 + x2 = 12
236260 6x1 + 5x2 = 40
237261 >>> 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()
245267 {'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...}
246287 """
247288 # Stop simplex algorithm from cycling.
248- for _ in range (100 ):
289+ for _ in range (Tableau . maxiter ):
249290 # Completion of each stage removes an objective. If both stages
250291 # are complete, then no objectives are left
251292 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-
256293 # Find the values of each variable at optimal solution
257- return self .interpret_tableau (self . tableau , self . col_titles )
294+ return self .interpret_tableau ()
258295
259- row_idx , col_idx = self .find_pivot (self . tableau )
296+ row_idx , col_idx = self .find_pivot ()
260297
261298 # If there are no more negative values in objective row
262299 if self .stop_iter :
263300 # Delete artificial variable columns and rows. Update attributes
264- self .tableau = self .change_stage (self . tableau )
301+ self .tableau = self .change_stage ()
265302 else :
266- self .tableau = self .pivot (self . tableau , row_idx , col_idx )
303+ self .tableau = self .pivot (row_idx , col_idx )
267304 return {}
268305
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 ]:
272307 """Given the final tableau, add the corresponding values of the basic
273308 decision variables to the `output_dict`
274- >>> tableau = np.array([
309+ >>> Tableau( np.array([
275310 ... [0,0,0.875,0.375,5],
276311 ... [0,1,0.375,-0.125,1],
277312 ... [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()
281314 {'P': 5.0, 'x1': 1.0, 'x2': 1.0}
282315 """
283316 # P = RHS of final tableau
284- output_dict = {"P" : abs (tableau [0 , - 1 ])}
317+ output_dict = {"P" : abs (self . tableau [0 , - 1 ])}
285318
286319 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 ])
289322 n_nonzero = len (nonzero [0 ])
290323
291- # First entry in the nonzero ids
324+ # First entry in the nonzero indices
292325 nonzero_rowidx = nonzero [0 ][0 ]
293- nonzero_val = tableau [nonzero_rowidx , i ]
326+ nonzero_val = self . tableau [nonzero_rowidx , i ]
294327
295328 # 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
305332 return output_dict
306333
307334
0 commit comments