From 9d82d070bb6e3a71930b9e177ffb142f4d776be0 Mon Sep 17 00:00:00 2001 From: Brandon Rodriguez <brodriguez8774@gmail.com> Date: Mon, 25 Nov 2019 03:27:40 -0500 Subject: [PATCH] Implement working simplex pivot --- documents/references.md | 9 ++ resources/simplex.py | 38 ------ resources/simplex/pivot.py | 214 +++++++++++++++++++++++++++++++ resources/simplex/simplex.py | 28 ++++ tests/resources/simplex/pivot.py | 59 +++++++++ 5 files changed, 310 insertions(+), 38 deletions(-) delete mode 100644 resources/simplex.py create mode 100644 resources/simplex/pivot.py create mode 100644 tests/resources/simplex/pivot.py diff --git a/documents/references.md b/documents/references.md index c51d9e8..bc58d0a 100644 --- a/documents/references.md +++ b/documents/references.md @@ -8,12 +8,21 @@ All references to external logic. Includes anything from stack overflow links to ## New References References new to this project. +### Class __call__ Function +Used to call a class instance as if it were a function. +* <https://stackoverflow.com/questions/9663562/what-is-the-difference-between-init-and-call> +* <https://docs.python.org/3/reference/datamodel.html#object.__call__> +* <http://hplgit.github.io/primer.html/doc/pub/class/._class-solarized003.html> + ### Using JSON * Comments in JSON: <https://stackoverflow.com/questions/244777/can-comments-be-used-in-json> * JSON Structures: <https://www.w3resource.com/JSON/structures.php> * Using JSON with Python: <https://docs.python.org/3.7/library/json.html> * Reading JSON with Python: <https://stackabuse.com/reading-and-writing-json-to-a-file-in-python/> +### Copying a List of Nested Lists +<https://stackoverflow.com/questions/28684154/python-copy-a-list-of-lists> + ## Older Relevant References References linked in previous assignments. diff --git a/resources/simplex.py b/resources/simplex.py deleted file mode 100644 index 702517f..0000000 --- a/resources/simplex.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Date: 11-22-19 -Class: CS5310 -Assignment: Linear Programming Simplex Algorithm -Author: Brandon Rodriguez - - -Implementation of the "Simplex" for Linear Programming problems. - - -General Terminology to Remember: -* Objective Function - The function to optimize (minimize/maximize). -* Basic Variable - The variables that define our current coordinates within our convex shape. These variables are -distinguishable by the fact that the coefficient is always exactly 1, and that each variable only shows up uniquely in a -single constraint equation. -* Non-Basic Variable - Any variable that is not a Basic Variable. Aka, a variable that will not necessarily have a -coefficient of exactly 1, and that can/will show up in multiple constraint equations. - - -Algorithm Variables (At least as far as I can tell): -* A - A m x n sized matrix, representing the set of equations in the linear program. -* b - A m sized vector, representing the set of constants for the constraint equations. -* c - A n sized vector, representing the set of coefficients for the objective function. - -* N - Indexes of all nonbasic variables for the given step. -* B - Indexes of all basic variables for the given step. -* n - The number of nonbasic variables. Aka n = |N|. -* m - The number of basic variables. Aka m = |B|. - -* v - Index of the constant term in the objective function. -* l - Index of basic variable that is turning nonbasic. Aka "leaving variable". -* e - Index of nonbasic variable that is turning basic. Aka "entering variable". -""" - -# System Imports. - - -# User Class Imports. diff --git a/resources/simplex/pivot.py b/resources/simplex/pivot.py new file mode 100644 index 0000000..6b79d55 --- /dev/null +++ b/resources/simplex/pivot.py @@ -0,0 +1,214 @@ +""" +Date: 11-22-19 +Class: CS5310 +Assignment: Linear Programming Simplex Algorithm +Author: Brandon Rodriguez + + +Implementation of the Pivot function for Linear Programming Simplex problems. +""" + +# System Imports. +import copy + +# User Class Imports. +from resources import logging as init_logging + + +# Initialize Logger. +logger = init_logging.get_logger(__name__) + + +class Pivot(): + def __init__(self, parent, *args, **kwargs): + # Get calling parent. We use this to pull parent data on __call__. + self._parent = parent + + # Parent data values to set later. + self._matrix_a = None + self._vector_b = None + self._vector_c = None + self._obj_constant_index = None + self._basic_var_indexes = None + self._nonbasic_var_indexes = None + + def __call__(self, old_basic_col, new_basic_col, *args, **kwargs): + # Get parent data at the point of call. + self._matrix_a = self._parent._matrix_a + self._vector_b = self._parent._constants + self._vector_c = self._parent._obj_func + self._obj_constant_index = self._parent._obj_constant_index + self._basic_var_indexes = self._parent._basic_var_indexes + self._nonbasic_var_indexes = self._parent._nonbasic_var_indexes + + return self.pivot(old_basic_col, new_basic_col) + + def pivot(self, old_basic_col, new_basic_col): + """ + Performs a pivot on parent class data. + :param old_basic_col: Column index of variable column turning nonbasic. + :param new_basic_col: Column index of variable column turning basic. + :return: Updated simplex values after pivot. + """ + # Get values pulled from parent. We also use typing to make sure we get a new copy and not a reference. + matrix_a = copy.deepcopy(self._matrix_a) + vector_b = copy.deepcopy(self._vector_b) + vector_c = copy.deepcopy(self._vector_c) + obj_const_index = self._obj_constant_index + b_array = copy.deepcopy(self._basic_var_indexes) + n_array = copy.deepcopy(self._nonbasic_var_indexes) + + # Determine exact locations of new basic variables. + old_basic = [-1, old_basic_col] + new_basic = [-1, new_basic_col] + + # Loop through all rows. + for row_index in range(len(matrix_a)): + if matrix_a[row_index][old_basic[1]] == 1: + old_basic[0] = row_index + new_basic[0] = row_index + + # Compute coefficients for the new basic variable x_e. + matrix_a, vector_b = self._calculate_new_basic( + n_array, + matrix_a, + vector_b, + old_basic, + new_basic, + ) + + # Compute coefficients of all remaining constraints. + matrix_a, vector_b = self._compute_coefficients( + n_array, + b_array, + matrix_a, + vector_b, + old_basic, + new_basic, + ) + + # Compute updated objective function. + vector_c, obj_const_index = self._update_objective( + n_array, + matrix_a, + vector_b, + vector_c, + obj_const_index, + old_basic, + new_basic, + ) + + # Compute the new sets of basic and nonbasic variables. + n_array, b_array = self._compute_basic_sets( + n_array, + b_array, + old_basic, + new_basic, + ) + + return (n_array, b_array, matrix_a, vector_b, vector_c, obj_const_index) + + def _calculate_new_basic(self, n_array, matrix_a, vector_b, old_basic, new_basic): + """ + Compute coefficients for the new basic variable x_e. + :param n_array: + :param matrix_a: + :param vector_b: + :param old_basic: + :param new_basic: + :return: + """ + # Calculate our new constraint constant. + vector_b[new_basic[0]] = self._vector_b[new_basic[0]] / self._matrix_a[old_basic[0]][new_basic[1]] + + # Loop through all nonbasic indexes (minus the one we're adding). Adjust coefficients in matrix. + for col_index in n_array: + + # Make sure to skip the one we're adding. + if col_index != new_basic[1]: + # Adjust given coefficient. + matrix_a[new_basic[0]][col_index] = self._matrix_a[old_basic[0]][col_index] / self._matrix_a[old_basic[0]][new_basic[1]] + + # Truncate if can be represented as int. + if matrix_a[new_basic[0]][col_index] % 1 == 0: + matrix_a[new_basic[0]][col_index] = int(matrix_a[new_basic[0]][col_index]) + + # Separately adjust the index of the newly nonbasic variable. + matrix_a[new_basic[0]][old_basic[1]] = self._matrix_a[old_basic[0]][old_basic[1]] / self._matrix_a[old_basic[0]][new_basic[1]] + + # Truncate if can be represented as int. + if matrix_a[new_basic[0]][old_basic[1]] % 1 == 0: + matrix_a[new_basic[0]][old_basic[1]] = int(matrix_a[new_basic[0]][old_basic[1]]) + + return (matrix_a, vector_b) + + def _compute_coefficients(self, n_array, b_array, matrix_a, vector_b, old_basic, new_basic): + """ + Compute coefficients of all remaining constraints. + :param n_array: + :param b_array: + :param matrix_a: + :param vector_b: + :param old_basic: + :param new_basic: + :return: + """ + # Loop through all basic indexes (minus the one we're removing). Adjust coefficients in matrix. + # logger.info('b_array: {0}'.format(b_array)) + for row_index in range(len(vector_b)): + # Make sure to skip the one we're removing. + if row_index != old_basic[0]: + # Adjust constraint constants. + vector_b[row_index] = self._vector_b[row_index] - (self._matrix_a[row_index][new_basic[1]] * self._vector_b[old_basic[0]]) + + # Loop through all nonbasic indexes (minus the one we're removing). + for col_index in range(len(self._vector_c) - 1): + # Adjust coefficient. + matrix_a[row_index][col_index] = self._matrix_a[row_index][col_index] - (self._matrix_a[new_basic[0]][col_index] * self._matrix_a[row_index][new_basic[1]]) + + return (matrix_a, vector_b) + + def _update_objective(self, n_array, matrix_a, vector_b, vector_c, obj_const_index, old_basic, new_basic): + """ + Compute updated objective function. + :param n_array: + :param matrix_a: + :param vector_b: + :param vector_c: + :param obj_const_index: + :param old_basic: + :param new_basic: + :return: + """ + # Update objective constant. + vector_c[self._obj_constant_index] = self._vector_c[self._obj_constant_index] - (self._vector_b[new_basic[0]] * self._vector_c[new_basic[1]]) + + # Loop through all nonbasic indexes (minus the one we're adding). + for col_index in range(len(self._vector_c) - 1): + # Adjust objective function value. + vector_c[col_index] = self._vector_c[col_index] - (self._matrix_a[new_basic[0]][col_index] * self._vector_c[new_basic[1]]) + + return (vector_c, obj_const_index) + + def _compute_basic_sets(self, n_array, b_array, old_basic, new_basic): + """ + Compute the new sets of basic and nonbasic variables. + :param n_array: + :param b_array: + :param old_basic: + :param new_basic: + :return: + """ + # Remove new basic variable from set of nonbasics. + n_array.remove(new_basic[1]) + + # Add new nonbasic variable to set of nonbasics. + n_array.append(old_basic[1]) + + # Remove new nonbasic variable from set of basics. + b_array.remove(old_basic[1]) + + # Add new basic variable to set of basics. + b_array.append(new_basic[1]) + + return (n_array, b_array) diff --git a/resources/simplex/simplex.py b/resources/simplex/simplex.py index 82e9ea8..24e282c 100644 --- a/resources/simplex/simplex.py +++ b/resources/simplex/simplex.py @@ -36,6 +36,7 @@ Algorithm Variables (At least as far as I can tell): import json # User Class Imports. +from .pivot import Pivot from resources import logging as init_logging @@ -56,6 +57,10 @@ class Simplex(): self._nonbasic_var_indexes = None self._description = None + self._pivot = Pivot(self) + + #region Simplex Read in and Setup + def read_data_from_json(self, json_file_location): logger.info('') logger.info('Attempting to read in simplex from file.') @@ -332,6 +337,8 @@ class Simplex(): if col_index not in self._basic_var_indexes: self._nonbasic_var_indexes.append(col_index) + #endregion Simplex Read in and Setup + def display_tableau(self): logger.info('') logger.info('Printing Simplex Tableau.') @@ -354,3 +361,24 @@ class Simplex(): logger.info('-' * tableau_length) logger.info('') + + def pivot(self, old_basic_index, new_basic_index): + """ + Pivots simplex around provided basic variable indexes. + :param old_basic_index: Index of variable column turning nonbasic. + :param new_basic_index: Index of variable column turning basic. + :return: + """ + if not isinstance(old_basic_index, int) or not isinstance(new_basic_index, int): + raise TypeError('Expected basic variable indexes to be of type int.') + + # Get new values from pivot. + n_array, b_array, matrix_a, vector_b, vector_c, obj_const_index = self._pivot(old_basic_index, new_basic_index) + + # Set new values to class. + self._nonbasic_var_indexes = n_array + self._basic_var_indexes = b_array + self._matrix_a = matrix_a + self._constants = vector_b + self._obj_func = vector_c + self._obj_constant_index = obj_const_index diff --git a/tests/resources/simplex/pivot.py b/tests/resources/simplex/pivot.py new file mode 100644 index 0000000..f4875fe --- /dev/null +++ b/tests/resources/simplex/pivot.py @@ -0,0 +1,59 @@ +""" +Date: 11-22-19 +Class: CS5310 +Assignment: Linear Programming Simplex Algorithm +Author: Brandon Rodriguez + + +Tests for Pivot implementation of the Simplex algorithm. +""" + +# System Imports. +import unittest + +# User Class Imports. +from resources.simplex.simplex import Simplex + + +class TestPivot(unittest.TestCase): + def setUp(self): + self.simplex = Simplex() + + def test__pivot(self): + # Setup initial simplex. + self.simplex.set_simplex_values( + [ + [1, 1, 1], + [2, -1, 2], + [2, 1, 0], + ], + [5, 3, 4], + [2, 4, 1], + ) + + # Test initial values after setup. + self.assertEqual(self.simplex._matrix_a, [ + [1, 1, 1, 1, 0, 0], + [2, -1, 2, 0, 1, 0], + [2, 1, 0, 0, 0, 1], + ]) + self.assertEqual(self.simplex._constants, [5, 3, 4]) + self.assertEqual(self.simplex._obj_func, [2, 4, 1, 0, 0, 0, 0]) + self.assertEqual(self.simplex._obj_constant_index, 6) + self.assertEqual(self.simplex._basic_var_indexes, [3, 4, 5]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [0, 1, 2]) + + # Pivot once, switching col x6 for col x2. + self.simplex.pivot(5, 1) + + # Test values after pivot. + self.assertEqual(self.simplex._matrix_a, [ + [-1, 0, 1, 1, 0, -1], + [4, 0, 2, 0, 1, 1], + [2, 1, 0, 0, 0, 1], + ]) + self.assertEqual(self.simplex._constants, [1, 7, 4]) + self.assertEqual(self.simplex._obj_func, [-6, 0, 1, 0, 0, -4, -16]) + self.assertEqual(self.simplex._obj_constant_index, 6) + self.assertEqual(self.simplex._basic_var_indexes, [3, 4, 1]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [0, 2, 5]) -- GitLab