Skip to content

Commit a40f626

Browse files
imenguspre-commit-ci[bot]
authored andcommitted
Fix simplex.py (TheAlgorithms#8843)
* changes to accommodate special case * changed n_slack calculation method * fix precommit typehints * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * n_art_vars inputs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: docstrings and typehints * fix: doctest issues when running code * additional check and doctests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix whitespace --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 6a6fde0 commit a40f626

File tree

1 file changed

+128
-101
lines changed

1 file changed

+128
-101
lines changed

Diff for: linear_programming/simplex.py

+128-101
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,60 @@
2020
class 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

Comments
 (0)