From 4bbae6bf7c1b23d1bf8aedc912f130d4351d95bf Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 27 Jun 2023 22:58:45 +0100 Subject: [PATCH 01/13] changes to accommodate special case --- linear_programming/simplex.py | 158 ++++++++++++++++------------------ 1 file changed, 75 insertions(+), 83 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index ba64add40b5f..79cf2130bb64 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -4,7 +4,7 @@ - `>=`, `<=`, and `=` constraints and - each variable `x1, x2, ...>= 0`. -See https://gist.github.com/imengus/f9619a568f7da5bc74eaf20169a24d98 for how to +See https://gist.github.com/imengus/f9619a568f8da5bc74eaf20169a24d98 for how to convert linear programs to simplex tableaus, and the steps taken in the simplex algorithm. @@ -20,13 +20,15 @@ class Tableau: """Operate on simplex tableaus - >>> t = Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2) + >>> Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2) Traceback (most recent call last): ... ValueError: RHS must be > 0 """ - def __init__(self, tableau: np.ndarray, n_vars: int) -> None: + def __init__( + self, tableau: np.ndarray, n_vars: int, n_art_vars: int | None = None + ) -> None: # Check if RHS is negative if np.any(tableau[:, -1], where=tableau[:, -1] < 0): raise ValueError("RHS must be > 0") @@ -35,10 +37,11 @@ def __init__(self, tableau: np.ndarray, n_vars: int) -> None: self.n_rows, _ = tableau.shape # Number of decision variables x1, x2, x3... - self.n_vars = n_vars + self.n_vars, self.n_art_vars = n_vars, n_art_vars # Number of artificial variables to be minimised - self.n_art_vars = len(np.where(tableau[self.n_vars : -1] == -1)[0]) + if self.n_art_vars is None: + self.n_art_vars = len(np.where(tableau[self.n_vars : -1] == -1)[0]) # 2 if there are >= or == constraints (nonstandard), 1 otherwise (std) self.n_stages = (self.n_art_vars > 0) + 1 @@ -53,7 +56,7 @@ def __init__(self, tableau: np.ndarray, n_vars: int) -> None: if self.n_art_vars: self.objectives.append("min") - self.col_titles = [""] + self.col_titles = self.generate_col_titles() # Index of current pivot row and column self.row_idx = None @@ -62,48 +65,35 @@ def __init__(self, tableau: np.ndarray, n_vars: int) -> None: # Does objective row only contain (non)-negative values? self.stop_iter = False - @staticmethod - def generate_col_titles(*args: int) -> list[str]: + def generate_col_titles(self) -> list[str]: """Generate column titles for tableau of specific dimensions - >>> Tableau.generate_col_titles(2, 3, 1) - ['x1', 'x2', 's1', 's2', 's3', 'a1', 'RHS'] - - >>> Tableau.generate_col_titles() - Traceback (most recent call last): - ... - ValueError: Must provide n_vars, n_slack, and n_art_vars - >>> Tableau.generate_col_titles(-2, 3, 1) - Traceback (most recent call last): - ... - ValueError: All arguments must be non-negative integers + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), + ... 2).generate_col_titles() + ['x1', 'x2', 's1', 's2', 'RHS'] """ - if len(args) != 3: - raise ValueError("Must provide n_vars, n_slack, and n_art_vars") + args = (self.n_vars, self.n_slack, self.n_art_vars) - if not all(x >= 0 and isinstance(x, int) for x in args): - raise ValueError("All arguments must be non-negative integers") - - # decision | slack | artificial - string_starts = ["x", "s", "a"] + # decision | slack + string_starts = ["x", "s"] titles = [] - for i in range(3): + for i in range(2): for j in range(args[i]): titles.append(string_starts[i] + str(j + 1)) titles.append("RHS") return titles - def find_pivot(self, tableau: np.ndarray) -> tuple[Any, Any]: + def find_pivot(self) -> tuple[Any, Any]: """Finds the pivot row and column. - >>> t = Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6], [1,2,0,1,7.]]), 2) - >>> t.find_pivot(t.tableau) + >>> Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6], [1,2,0,1,7.]]), + ... 2).find_pivot() (1, 0) """ objective = self.objectives[-1] # Find entries of highest magnitude in objective rows sign = (objective == "min") - (objective == "max") - col_idx = np.argmax(sign * tableau[0, : self.n_vars]) + col_idx = np.argmax(sign * self.tableau[0, :-1]) # Choice is only valid if below 0 for maximise, and above for minimise if sign * self.tableau[0, col_idx] <= 0: @@ -117,10 +107,10 @@ def find_pivot(self, tableau: np.ndarray) -> tuple[Any, Any]: s = slice(self.n_stages, self.n_rows) # RHS - dividend = tableau[s, -1] + dividend = self.tableau[s, -1] # Elements of pivot column within slice - divisor = tableau[s, col_idx] + divisor = self.tableau[s, col_idx] # Array filled with nans nans = np.full(self.n_rows - self.n_stages, np.nan) @@ -134,18 +124,18 @@ def find_pivot(self, tableau: np.ndarray) -> tuple[Any, Any]: row_idx = np.nanargmin(quotients) + self.n_stages return row_idx, col_idx - def pivot(self, tableau: np.ndarray, row_idx: int, col_idx: int) -> np.ndarray: + def pivot(self, row_idx: int, col_idx: int) -> np.ndarray: """Pivots on value on the intersection of pivot row and column. - >>> t = Tableau(np.array([[-2,-3,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]), 2) - >>> t.pivot(t.tableau, 1, 0).tolist() + >>> Tableau(np.array([[-2,-3,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]), + ... 2).pivot(1, 0).tolist() ... # doctest: +NORMALIZE_WHITESPACE [[0.0, 3.0, 2.0, 0.0, 8.0], [1.0, 3.0, 1.0, 0.0, 4.0], [0.0, -8.0, -3.0, 1.0, -8.0]] """ # Avoid changes to original tableau - piv_row = tableau[row_idx].copy() + piv_row = self.tableau[row_idx].copy() piv_val = piv_row[col_idx] @@ -153,23 +143,22 @@ def pivot(self, tableau: np.ndarray, row_idx: int, col_idx: int) -> np.ndarray: piv_row *= 1 / piv_val # Variable in pivot column becomes basic, ie the only non-zero entry - for idx, coeff in enumerate(tableau[:, col_idx]): - tableau[idx] += -coeff * piv_row - tableau[row_idx] = piv_row - return tableau + for idx, coeff in enumerate(self.tableau[:, col_idx]): + self.tableau[idx] += -coeff * piv_row + self.tableau[row_idx] = piv_row + return self.tableau - def change_stage(self, tableau: np.ndarray) -> np.ndarray: + def change_stage(self) -> np.ndarray: """Exits first phase of the two-stage method by deleting artificial rows and columns, or completes the algorithm if exiting the standard case. - >>> t = Tableau(np.array([ + >>> Tableau(np.array([ ... [3, 3, -1, -1, 0, 0, 4], ... [2, 1, 0, 0, 0, 0, 0.], ... [1, 2, -1, 0, 1, 0, 2], ... [2, 1, 0, -1, 0, 1, 2] - ... ]), 2) - >>> t.change_stage(t.tableau).tolist() + ... ]), 2).change_stage().tolist() ... # doctest: +NORMALIZE_WHITESPACE [[2.0, 1.0, 0.0, 0.0, 0.0, 0.0], [1.0, 2.0, -1.0, 0.0, 1.0, 2.0], @@ -179,22 +168,22 @@ def change_stage(self, tableau: np.ndarray) -> np.ndarray: self.objectives.pop() if not self.objectives: - return tableau + return self.tableau # Slice containing ids for artificial columns s = slice(-self.n_art_vars - 1, -1) # Delete the artificial variable columns - tableau = np.delete(tableau, s, axis=1) + self.tableau = np.delete(self.tableau, s, axis=1) # Delete the objective row of the first stage - tableau = np.delete(tableau, 0, axis=0) + self.tableau = np.delete(self.tableau, 0, axis=0) self.n_stages = 1 self.n_rows -= 1 self.n_art_vars = 0 self.stop_iter = False - return tableau + return self.tableau def run_simplex(self) -> dict[Any, Any]: """Operate on tableau until objective function cannot be @@ -227,7 +216,7 @@ def run_simplex(self) -> dict[Any, Any]: ... [1, 1, 1, 1, 0, 0, 0, 0, 40], ... [2, 1, -1, 0, -1, 0, 1, 0, 10], ... [0, -1, 1, 0, 0, -1, 0, 1, 10.] - ... ]), 3).run_simplex() + ... ]), 3, 2).run_simplex() {'P': 70.0, 'x1': 10.0, 'x2': 10.0, 'x3': 20.0} # Non standard: minimisation and equalities @@ -235,73 +224,76 @@ def run_simplex(self) -> dict[Any, Any]: ST: 2x1 + x2 = 12 6x1 + 5x2 = 40 >>> Tableau(np.array([ - ... [8, 6, 0, -1, 0, -1, 0, 0, 52], - ... [1, 1, 0, 0, 0, 0, 0, 0, 0], - ... [2, 1, 1, 0, 0, 0, 0, 0, 12], - ... [2, 1, 0, -1, 0, 0, 1, 0, 12], - ... [6, 5, 0, 0, 1, 0, 0, 0, 40], - ... [6, 5, 0, 0, 0, -1, 0, 1, 40.] - ... ]), 2).run_simplex() + ... [8, 6, 0, 0, 52], + ... [1, 1, 0, 0, 0], + ... [2, 1, 1, 0, 12], + ... [6, 5, 0, 1, 40.], + ... ]), 2, 2).run_simplex() {'P': 7.0, 'x1': 5.0, 'x2': 2.0} + + + # Pivot on slack variables + Max: 8x1 + 6x2 + ST: x1 + 3x2 <= 33 + 4x1 + 2x2 <= 48 + 2x1 + 4x2 <= 48 + x1 + x2 >= 10 + x1 >= 2 + >>> Tableau(np.array([ + ... [2, 1, 0, 0, 0, -1, -1, 0, 0, 12.0], + ... [-8, -6, 0, 0, 0, 0, 0, 0, 0, 0.0], + ... [1, 3, 1, 0, 0, 0, 0, 0, 0, 33.0], + ... [4, 2, 0, 1, 0, 0, 0, 0, 0, 60.0], + ... [2, 4, 0, 0, 1, 0, 0, 0, 0, 48.0], + ... [1, 1, 0, 0, 0, -1, 0, 1, 0, 10.0], + ... [1, 0, 0, 0, 0, 0, -1, 0, 1, 2.0] + ... ]), 2, 2).run_simplex() # doctest: +ELLIPSIS + {'P': 132.0, 'x1': 12.000... 'x2': 5.999...} """ # Stop simplex algorithm from cycling. for _ in range(100): # Completion of each stage removes an objective. If both stages # are complete, then no objectives are left if not self.objectives: - self.col_titles = self.generate_col_titles( - self.n_vars, self.n_slack, self.n_art_vars - ) - # Find the values of each variable at optimal solution - return self.interpret_tableau(self.tableau, self.col_titles) + return self.interpret_tableau() - row_idx, col_idx = self.find_pivot(self.tableau) + row_idx, col_idx = self.find_pivot() # If there are no more negative values in objective row if self.stop_iter: # Delete artificial variable columns and rows. Update attributes - self.tableau = self.change_stage(self.tableau) + self.tableau = self.change_stage() else: - self.tableau = self.pivot(self.tableau, row_idx, col_idx) + self.tableau = self.pivot(row_idx, col_idx) return {} - def interpret_tableau( - self, tableau: np.ndarray, col_titles: list[str] - ) -> dict[str, float]: + def interpret_tableau(self) -> dict[str, float]: """Given the final tableau, add the corresponding values of the basic decision variables to the `output_dict` - >>> tableau = np.array([ + >>> Tableau(np.array([ ... [0,0,0.875,0.375,5], ... [0,1,0.375,-0.125,1], ... [1,0,-0.125,0.375,1] - ... ]) - >>> t = Tableau(tableau, 2) - >>> t.interpret_tableau(tableau, ["x1", "x2", "s1", "s2", "RHS"]) + ... ]),2).interpret_tableau() {'P': 5.0, 'x1': 1.0, 'x2': 1.0} """ # P = RHS of final tableau - output_dict = {"P": abs(tableau[0, -1])} + output_dict = {"P": abs(self.tableau[0, -1])} for i in range(self.n_vars): # Gives ids of nonzero entries in the ith column - nonzero = np.nonzero(tableau[:, i]) + nonzero = np.nonzero(self.tableau[:, i]) n_nonzero = len(nonzero[0]) # First entry in the nonzero ids nonzero_rowidx = nonzero[0][0] - nonzero_val = tableau[nonzero_rowidx, i] + nonzero_val = self.tableau[nonzero_rowidx, i] # If there is only one nonzero value in column, which is one if n_nonzero == nonzero_val == 1: - rhs_val = tableau[nonzero_rowidx, -1] - output_dict[col_titles[i]] = rhs_val - - # Check for basic variables - for title in col_titles: - # Don't add RHS or slack variables to output dict - if title[0] not in "R-s-a": - output_dict.setdefault(title, 0) + rhs_val = self.tableau[nonzero_rowidx, -1] + output_dict[self.col_titles[i]] = rhs_val return output_dict From de6cb052a07159a5b231d0a2327fbe05c10077b8 Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 27 Jun 2023 23:12:11 +0100 Subject: [PATCH 02/13] changed n_slack calculation method --- linear_programming/simplex.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 79cf2130bb64..ecf7c596c76c 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -34,7 +34,7 @@ def __init__( raise ValueError("RHS must be > 0") self.tableau = tableau - self.n_rows, _ = tableau.shape + self.n_rows, n_cols = tableau.shape # Number of decision variables x1, x2, x3... self.n_vars, self.n_art_vars = n_vars, n_art_vars @@ -47,7 +47,7 @@ def __init__( self.n_stages = (self.n_art_vars > 0) + 1 # Number of slack variables added to make inequalities into equalities - self.n_slack = self.n_rows - self.n_stages + self.n_slack = n_cols - self.n_vars - self.n_art_vars - 1 # Objectives for each stage self.objectives = ["max"] @@ -115,7 +115,7 @@ def find_pivot(self) -> tuple[Any, Any]: # Array filled with nans nans = np.full(self.n_rows - self.n_stages, np.nan) - # If element in pivot column is greater than zeron_stages, return + # If element in pivot column is greater than zero, return # quotient or nan otherwise quotients = np.divide(dividend, divisor, out=nans, where=divisor > 0) From f40dad1c8b16638f90890a92b6510b055b3feb3f Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 27 Jun 2023 23:24:49 +0100 Subject: [PATCH 03/13] fix precommit typehints --- linear_programming/simplex.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index ecf7c596c76c..1c163c126245 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -27,8 +27,7 @@ class Tableau: """ def __init__( - self, tableau: np.ndarray, n_vars: int, n_art_vars: int | None = None - ) -> None: + self, tableau: np.ndarray, n_vars: int, n_art_vars: int) -> None: # Check if RHS is negative if np.any(tableau[:, -1], where=tableau[:, -1] < 0): raise ValueError("RHS must be > 0") @@ -39,10 +38,6 @@ def __init__( # Number of decision variables x1, x2, x3... self.n_vars, self.n_art_vars = n_vars, n_art_vars - # Number of artificial variables to be minimised - if self.n_art_vars is None: - self.n_art_vars = len(np.where(tableau[self.n_vars : -1] == -1)[0]) - # 2 if there are >= or == constraints (nonstandard), 1 otherwise (std) self.n_stages = (self.n_art_vars > 0) + 1 @@ -72,7 +67,7 @@ def generate_col_titles(self) -> list[str]: ... 2).generate_col_titles() ['x1', 'x2', 's1', 's2', 'RHS'] """ - args = (self.n_vars, self.n_slack, self.n_art_vars) + args = (self.n_vars, self.n_slack) # decision | slack string_starts = ["x", "s"] From c5fbf73153759fd7c1e9650bb700423f0cdaf1c2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 22:26:17 +0000 Subject: [PATCH 04/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linear_programming/simplex.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 1c163c126245..48dad7fd009e 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -26,8 +26,7 @@ class Tableau: ValueError: RHS must be > 0 """ - def __init__( - self, tableau: np.ndarray, n_vars: int, n_art_vars: int) -> None: + def __init__(self, tableau: np.ndarray, n_vars: int, n_art_vars: int) -> None: # Check if RHS is negative if np.any(tableau[:, -1], where=tableau[:, -1] < 0): raise ValueError("RHS must be > 0") From 0471792e42c5f524ddacaa591b60654cd239d2d1 Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 27 Jun 2023 23:50:42 +0100 Subject: [PATCH 05/13] n_art_vars inputs --- linear_programming/simplex.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 1c163c126245..895f29deb1ea 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -20,7 +20,7 @@ class Tableau: """Operate on simplex tableaus - >>> Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2) + >>> Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2, 2) Traceback (most recent call last): ... ValueError: RHS must be > 0 @@ -64,7 +64,7 @@ def generate_col_titles(self) -> list[str]: """Generate column titles for tableau of specific dimensions >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), - ... 2).generate_col_titles() + ... 2, 0).generate_col_titles() ['x1', 'x2', 's1', 's2', 'RHS'] """ args = (self.n_vars, self.n_slack) @@ -81,7 +81,7 @@ def generate_col_titles(self) -> list[str]: def find_pivot(self) -> tuple[Any, Any]: """Finds the pivot row and column. >>> Tableau(np.array([[-2,1,0,0,0], [3,1,1,0,6], [1,2,0,1,7.]]), - ... 2).find_pivot() + ... 2, 0).find_pivot() (1, 0) """ objective = self.objectives[-1] @@ -123,7 +123,7 @@ def pivot(self, row_idx: int, col_idx: int) -> np.ndarray: """Pivots on value on the intersection of pivot row and column. >>> Tableau(np.array([[-2,-3,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]), - ... 2).pivot(1, 0).tolist() + ... 2, 2).pivot(1, 0).tolist() ... # doctest: +NORMALIZE_WHITESPACE [[0.0, 3.0, 2.0, 0.0, 8.0], [1.0, 3.0, 1.0, 0.0, 4.0], @@ -153,11 +153,11 @@ def change_stage(self) -> np.ndarray: ... [2, 1, 0, 0, 0, 0, 0.], ... [1, 2, -1, 0, 1, 0, 2], ... [2, 1, 0, -1, 0, 1, 2] - ... ]), 2).change_stage().tolist() + ... ]), 2, 2).change_stage().tolist() ... # doctest: +NORMALIZE_WHITESPACE - [[2.0, 1.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 2.0, -1.0, 0.0, 1.0, 2.0], - [2.0, 1.0, 0.0, -1.0, 0.0, 2.0]] + [[2.0, 1.0, 0.0, 0.0, 0.0], + [1.0, 2.0, -1.0, 0.0, 2.0], + [2.0, 1.0, 0.0, -1.0, 2.0]] """ # Objective of original objective row remains self.objectives.pop() @@ -189,7 +189,7 @@ def run_simplex(self) -> dict[Any, Any]: ST: x1 + 3x2 <= 4 3x1 + x2 <= 4 >>> Tableau(np.array([[-1,-1,0,0,0],[1,3,1,0,4],[3,1,0,1,4.]]), - ... 2).run_simplex() + ... 2, 0).run_simplex() {'P': 2.0, 'x1': 1.0, 'x2': 1.0} # Optimal tableau input: @@ -197,7 +197,7 @@ def run_simplex(self) -> dict[Any, Any]: ... [0, 0, 0.25, 0.25, 2], ... [0, 1, 0.375, -0.125, 1], ... [1, 0, -0.125, 0.375, 1] - ... ]), 2).run_simplex() + ... ]), 2, 0).run_simplex() {'P': 2.0, 'x1': 1.0, 'x2': 1.0} # Non-standard: >= constraints @@ -270,7 +270,7 @@ def interpret_tableau(self) -> dict[str, float]: ... [0,0,0.875,0.375,5], ... [0,1,0.375,-0.125,1], ... [1,0,-0.125,0.375,1] - ... ]),2).interpret_tableau() + ... ]),2, 0).interpret_tableau() {'P': 5.0, 'x1': 1.0, 'x2': 1.0} """ # P = RHS of final tableau From 0594f33fd5345ba665e11bb3d4d11338f9ad950f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 22:55:46 +0000 Subject: [PATCH 06/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linear_programming/simplex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 6ab6035ac599..38f5cc096082 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -154,8 +154,8 @@ def change_stage(self) -> np.ndarray: ... [2, 1, 0, -1, 0, 1, 2] ... ]), 2, 2).change_stage().tolist() ... # doctest: +NORMALIZE_WHITESPACE - [[2.0, 1.0, 0.0, 0.0, 0.0], - [1.0, 2.0, -1.0, 0.0, 2.0], + [[2.0, 1.0, 0.0, 0.0, 0.0], + [1.0, 2.0, -1.0, 0.0, 2.0], [2.0, 1.0, 0.0, -1.0, 2.0]] """ # Objective of original objective row remains From db08b13d9730c0378f79d11215db5e2504f027f4 Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 25 Jul 2023 00:48:32 +0200 Subject: [PATCH 07/13] fix: docstrings and typehints --- linear_programming/simplex.py | 43 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 38f5cc096082..198dbef6a46b 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -4,7 +4,7 @@ - `>=`, `<=`, and `=` constraints and - each variable `x1, x2, ...>= 0`. -See https://gist.github.com/imengus/f9619a568f8da5bc74eaf20169a24d98 for how to +See https://gist.github.com/imengus/f9619a568f7da5bc74eaf20169a24d98 for how to convert linear programs to simplex tableaus, and the steps taken in the simplex algorithm. @@ -24,30 +24,47 @@ class Tableau: Traceback (most recent call last): ... ValueError: RHS must be > 0 + + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), -2, 2) + Traceback (most recent call last): + ... + ValueError: Number of decision and artificial variables must be greater or equal to + 2 or 0, respectively """ - def __init__(self, tableau: np.ndarray, n_vars: int, n_art_vars: int) -> None: + # Maximum number of iterations to prevent cycling + maxiter = 100 + + def __init__( + self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int + ) -> None: # Check if RHS is negative - if np.any(tableau[:, -1], where=tableau[:, -1] < 0): + if not (tableau[:, -1] >= 0).all(): raise ValueError("RHS must be > 0") + if n_vars < 2 or n_artificial_vars < 0: + raise ValueError( + "Number of decision and artificial variables must be greater or equal \ + to 2 or 0, respectively" + ) + self.tableau = tableau self.n_rows, n_cols = tableau.shape # Number of decision variables x1, x2, x3... - self.n_vars, self.n_art_vars = n_vars, n_art_vars + self.n_vars, self.n_artificial_vars = n_vars, n_artificial_vars # 2 if there are >= or == constraints (nonstandard), 1 otherwise (std) - self.n_stages = (self.n_art_vars > 0) + 1 + self.n_stages = (self.n_artificial_vars > 0) + 1 # Number of slack variables added to make inequalities into equalities - self.n_slack = n_cols - self.n_vars - self.n_art_vars - 1 + self.n_slack = n_cols - self.n_vars - self.n_artificial_vars - 1 # Objectives for each stage self.objectives = ["max"] # In two stage simplex, first minimise then maximise - if self.n_art_vars: + if self.n_artificial_vars: self.objectives.append("min") self.col_titles = self.generate_col_titles() @@ -165,7 +182,7 @@ def change_stage(self) -> np.ndarray: return self.tableau # Slice containing ids for artificial columns - s = slice(-self.n_art_vars - 1, -1) + s = slice(-self.n_artificial_vars - 1, -1) # Delete the artificial variable columns self.tableau = np.delete(self.tableau, s, axis=1) @@ -175,7 +192,7 @@ def change_stage(self) -> np.ndarray: self.n_stages = 1 self.n_rows -= 1 - self.n_art_vars = 0 + self.n_artificial_vars = 0 self.stop_iter = False return self.tableau @@ -245,7 +262,7 @@ def run_simplex(self) -> dict[Any, Any]: {'P': 132.0, 'x1': 12.000... 'x2': 5.999...} """ # Stop simplex algorithm from cycling. - for _ in range(100): + for _ in range(Tableau.maxiter): # Completion of each stage removes an objective. If both stages # are complete, then no objectives are left if not self.objectives: @@ -276,16 +293,16 @@ def interpret_tableau(self) -> dict[str, float]: output_dict = {"P": abs(self.tableau[0, -1])} for i in range(self.n_vars): - # Gives ids of nonzero entries in the ith column + # Gives indices of nonzero entries in the ith column nonzero = np.nonzero(self.tableau[:, i]) n_nonzero = len(nonzero[0]) - # First entry in the nonzero ids + # First entry in the nonzero indices nonzero_rowidx = nonzero[0][0] nonzero_val = self.tableau[nonzero_rowidx, i] # If there is only one nonzero value in column, which is one - if n_nonzero == nonzero_val == 1: + if n_nonzero == 1 and nonzero_val == 1: rhs_val = self.tableau[nonzero_rowidx, -1] output_dict[self.col_titles[i]] = rhs_val return output_dict From be44d46bd8456bb0ae5c1342662cd1a69a5e72e6 Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 25 Jul 2023 01:02:26 +0200 Subject: [PATCH 08/13] fix: doctest issues when running code --- linear_programming/simplex.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 198dbef6a46b..f46e1615c888 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -28,8 +28,7 @@ class Tableau: >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), -2, 2) Traceback (most recent call last): ... - ValueError: Number of decision and artificial variables must be greater or equal to - 2 or 0, respectively + ValueError: number of (artificial) variables must be a natural number """ # Maximum number of iterations to prevent cycling @@ -44,8 +43,7 @@ def __init__( if n_vars < 2 or n_artificial_vars < 0: raise ValueError( - "Number of decision and artificial variables must be greater or equal \ - to 2 or 0, respectively" + "number of (artificial) variables must be a natural number" ) self.tableau = tableau From b3481ff2885aaeed84f8bd4a0aa9e06381039ccf Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 15 Aug 2023 13:16:37 +0100 Subject: [PATCH 09/13] additional check and doctests --- linear_programming/simplex.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 6ab6035ac599..a2e354651e7d 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -20,6 +20,11 @@ class Tableau: """Operate on simplex tableaus + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4]]), 2, 2) + Traceback (most recent call last): + ... + TypeError: Tableau must have type float64 + >>> Tableau(np.array([[-1,-1,0,0,-1],[1,3,1,0,4],[3,1,0,1,4.]]), 2, 2) Traceback (most recent call last): ... @@ -27,6 +32,9 @@ class Tableau: """ def __init__(self, tableau: np.ndarray, n_vars: int, n_art_vars: int) -> None: + if tableau.dtype != 'float64': + raise TypeError('Tableau must have type float64') + # Check if RHS is negative if np.any(tableau[:, -1], where=tableau[:, -1] < 0): raise ValueError("RHS must be > 0") @@ -65,6 +73,10 @@ def generate_col_titles(self) -> list[str]: >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), ... 2, 0).generate_col_titles() ['x1', 'x2', 's1', 's2', 'RHS'] + + >>> Tableau(np.array([[-1,-1,0,0,1],[1,3,1,0,4],[3,1,0,1,4.]]), + ... 2, 2).generate_col_titles() + ['x1', 'x2', 'RHS'] """ args = (self.n_vars, self.n_slack) @@ -191,12 +203,26 @@ def run_simplex(self) -> dict[Any, Any]: ... 2, 0).run_simplex() {'P': 2.0, 'x1': 1.0, 'x2': 1.0} + # Standard linear program with 3 variables: + Max: 3x1 + x2 + 3x3 + ST: 2x1 + x2 + x3 ≤ 2 + x1 + 2x2 + 3x3 ≤ 5 + 2x1 + 2x2 + x3 ≤ 6 + >>> Tableau(np.array([ + ... [-3,-1,-3,0,0,0,0], + ... [2,1,1,1,0,0,2], + ... [1,2,3,0,1,0,5], + ... [2,2,1,0,0,1,6.] + ... ]),3,0).run_simplex() # doctest: +ELLIPSIS + {'P': 5.4, 'x1': 0.199..., 'x3': 1.6} + + # Optimal tableau input: >>> Tableau(np.array([ ... [0, 0, 0.25, 0.25, 2], ... [0, 1, 0.375, -0.125, 1], ... [1, 0, -0.125, 0.375, 1] - ... ]), 2, 0).run_simplex() + ... ]), 2, 0).run_simplex() {'P': 2.0, 'x1': 1.0, 'x2': 1.0} # Non-standard: >= constraints From ffce18c320d701d222fefe9eb31940aa5d76be8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:22:39 +0000 Subject: [PATCH 10/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linear_programming/simplex.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 314bcd327cf0..bbc97d8e22bf 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -35,11 +35,15 @@ class Tableau: ... ValueError: number of (artificial) variables must be a natural number """ + # Max iteration number to prevent cycling maxiter = 100 - def __init__(self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int) -> None: - if tableau.dtype != 'float64': - raise TypeError('Tableau must have type float64') + + def __init__( + self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int + ) -> None: + if tableau.dtype != "float64": + raise TypeError("Tableau must have type float64") # Check if RHS is negative if not (tableau[:, -1] >= 0).all(): @@ -233,7 +237,7 @@ def run_simplex(self) -> dict[Any, Any]: ... [0, 0, 0.25, 0.25, 2], ... [0, 1, 0.375, -0.125, 1], ... [1, 0, -0.125, 0.375, 1] - ... ]), 2, 0).run_simplex() + ... ]), 2, 0).run_simplex() {'P': 2.0, 'x1': 1.0, 'x2': 1.0} # Non-standard: >= constraints From da21cb4832a39b1c14c2c8ddac4568547f920c52 Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 15 Aug 2023 13:28:03 +0100 Subject: [PATCH 11/13] fix ruff --- linear_programming/simplex.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index 314bcd327cf0..c702bcc781b4 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -37,7 +37,9 @@ class Tableau: """ # Max iteration number to prevent cycling maxiter = 100 - def __init__(self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int) -> None: + def __init__( + self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int + ) -> None: if tableau.dtype != 'float64': raise TypeError('Tableau must have type float64') @@ -233,7 +235,7 @@ def run_simplex(self) -> dict[Any, Any]: ... [0, 0, 0.25, 0.25, 2], ... [0, 1, 0.375, -0.125, 1], ... [1, 0, -0.125, 0.375, 1] - ... ]), 2, 0).run_simplex() + ... ]), 2, 0).run_simplex() {'P': 2.0, 'x1': 1.0, 'x2': 1.0} # Non-standard: >= constraints From f12212ecf1bd9a9300180373bf807a59a45b2762 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:30:57 +0000 Subject: [PATCH 12/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linear_programming/simplex.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index c25feb77f9e1..bbc97d8e22bf 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -38,11 +38,12 @@ class Tableau: # Max iteration number to prevent cycling maxiter = 100 + def __init__( self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int - ) -> None: - if tableau.dtype != 'float64': - raise TypeError('Tableau must have type float64') + ) -> None: + if tableau.dtype != "float64": + raise TypeError("Tableau must have type float64") # Check if RHS is negative if not (tableau[:, -1] >= 0).all(): From 8bfb5912d354d25021dee096ecef5f38941350f7 Mon Sep 17 00:00:00 2001 From: imengus Date: Tue, 15 Aug 2023 13:48:12 +0100 Subject: [PATCH 13/13] fix whitespace --- linear_programming/simplex.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/linear_programming/simplex.py b/linear_programming/simplex.py index c25feb77f9e1..bbc97d8e22bf 100644 --- a/linear_programming/simplex.py +++ b/linear_programming/simplex.py @@ -38,11 +38,12 @@ class Tableau: # Max iteration number to prevent cycling maxiter = 100 + def __init__( self, tableau: np.ndarray, n_vars: int, n_artificial_vars: int - ) -> None: - if tableau.dtype != 'float64': - raise TypeError('Tableau must have type float64') + ) -> None: + if tableau.dtype != "float64": + raise TypeError("Tableau must have type float64") # Check if RHS is negative if not (tableau[:, -1] >= 0).all():