diff --git a/resources/simplex/simplex.py b/resources/simplex/simplex.py index c2620ee7b8a1e44ab917259ee8e9ad2785bc235a..3ff0e608b8bd49caf2036843538afd87c7f797d2 100644 --- a/resources/simplex/simplex.py +++ b/resources/simplex/simplex.py @@ -52,6 +52,8 @@ class Simplex(): self._constants = None self._obj_func = None self._obj_constant_index = None + self._basic_var_indexes = None + self._nonbasic_var_indexes = None def read_data_from_json(self, json_file_location): logger.info('') @@ -98,16 +100,42 @@ class Simplex(): else: raise KeyError('Could not find key "objective" or "objective_function" in JSON data.') + # Check if "basic_vars" key exists. + if 'basic_vars' in json_data: + basic_vars = json_data['basic_vars'] + elif 'basic_variables' in json_data: + basic_vars = json_data['basic_variables'] + else: + basic_vars = None + + # Check if "non_basic_vars" key exists. + if 'non_basic_vars' in json_data: + non_basic_vars = json_data['non_basic_vars'] + elif 'non_basic_variables' in json_data: + non_basic_vars = json_data['non_basic_variables'] + else: + non_basic_vars = None + # Data has been parsed. Now attempt to set it. - self.set_simplex_values(matrix_a, vector_b, vector_c) + self.set_simplex_values(matrix_a, vector_b, vector_c, basic_vars=basic_vars, non_basic_vars=non_basic_vars) - def set_simplex_values(self, matrix_a, vector_b, vector_c): + def set_simplex_values(self, matrix_a, vector_b, vector_c, basic_vars=None, non_basic_vars=None): """ Sets values for simplex. :param matrix_a: Matrix of constraint equation coefficients. :param vector_b: Vector of constraint constants. :param vector_c: Vector of objective function coefficients. + :param basic_vars: Optional array of basic variable column indexes. + :param non_basic_vars: Optional array of non-basic variable column indexes. """ + # Reset all values to None. + self._matrix_a = None + self._constants = None + self._obj_func = None + self._obj_constant_index = None + self._basic_var_indexes = None + self._nonbasic_var_indexes = None + # Check that values are of expected types. if not isinstance(matrix_a, list) or not isinstance(vector_b, list) or not isinstance(vector_c, list): raise TypeError('Expected Constraint Equations, Constraint Constants, and Objective function to be lists.') @@ -134,7 +162,7 @@ class Simplex(): # Check that vector_c length is equal to (or one greater than) matrix_a row length. if len(vector_c) == matrix_row_length: # vector_c is exactly equal to row length. - logger.info('Assuming constant for objective function is currently 0.') + logger.info('Assuming constant for objective function is initially 0.') self._obj_func = vector_c self._obj_func.append(0) elif len(vector_c) - 1 == matrix_row_length: @@ -150,6 +178,125 @@ class Simplex(): self._constants = vector_b self._obj_constant_index = len(self._obj_func) - 1 + # Check if optional basic var arguments were provided. + if basic_vars is not None: + if isinstance(basic_vars, list): + self._basic_var_indexes = basic_vars + else: + raise TypeError('The "basic_vars" argument is expected to be of type list.') + + # Check if optional non-basic var arguments were provided. + if non_basic_vars is not None: + if isinstance(non_basic_vars, list): + self._nonbasic_var_indexes = non_basic_vars + else: + raise TypeError('The "non_basic_vars" argument is expected to be of type list.') + + # Finally, determine/set basic and nonbasic variables. + self._determine_basic_variables() + self._determine_nonbasic_variables() + + def _determine_basic_variables(self): + """ + Determines (and if needed, sets) all basic variables for given constraints. + """ + # Check that values exist. If they do, we will assume that this was called from "set_simplex_values" and we + # don't need to re-validate. + if self._matrix_a is None or self._constants is None or self._obj_func is None: + raise ValueError('Values are not yet set. This function should only be called from "set_simplex_values".') + + # Check if basic variables are already defined. + if isinstance(self._basic_var_indexes, list) and len(self._basic_var_indexes) > 0: + # Basic variables are already defined. No reason to proceed. + return None + + rows_with_slack = [] + self._basic_var_indexes = [] + expected_basic_var_count = len(self._matrix_a) + + # Loop through one column at a time of all rows, checking for potential instances of a basic variable. + for col_index in range(len(self._matrix_a[0])): + col_valid_for_slack = True + found_col_non_zero = False + non_zero_index = 0 + for row_index in range(len(self._matrix_a)): + + # Only continue if col values so far meet criteria for a basic variable. + if col_valid_for_slack: + + # If this col has a basic var, then all coefficients will be either 0 or 1. + cur_coefficient = self._matrix_a[row_index][col_index] + if cur_coefficient != 0 and cur_coefficient != 1: + # Found a non-0 and non-1 value. Col no longer valid for slack. + col_valid_for_slack = False + + # Col may still be valid for slack variable. Check if non-zero value, and handle accordingly. + elif cur_coefficient == 1: + + # Current coefficient is non-zero value. Either this is a basic var or col is not slack. + if found_col_non_zero: + # Another value of 1 was already found. + # This means we have two values of 1 and this col is no longer valid for slack. + col_valid_for_slack = False + + else: + # This is the first value of 1 found in column. May be slack. + found_col_non_zero = True + non_zero_index = row_index + + # Now that we checked all rows in current column, determine if we have a slack col. + if col_valid_for_slack and found_col_non_zero: + # We found exactly one non-0 value of 1 in the column. Our saved index was a slack variable. + rows_with_slack.append(non_zero_index) + self._basic_var_indexes.append(col_index) + + # We've checked all rows and columns. Check our number of found basic variables. + if len(self._basic_var_indexes) > expected_basic_var_count: + # We found more possible basic variables than exist rows. + # While this is technically a valid matrix format, it so gives us no way to determine which columns contain + # the initial basic variables. User needs to define it themselves to proceed. + raise ValueError('Only detected {0} constraint equations, yet found {1} possible basic variables. If the ' + 'provided equations are correct, then please manually define "basic_variables" value.') + elif len(self._basic_var_indexes) < expected_basic_var_count: + # Found less basic variables than exist rows. We need to add additional columns for the missing variables. + for row_index in range(len(self._matrix_a)): + # Find all rows without a slack variable. + if row_index not in rows_with_slack: + # Given row does not have a slack variable. Add a new column to the end of all rows. + self._matrix_a[row_index].append(1) + for new_row_index in range(len(self._matrix_a)): + if new_row_index != row_index: + self._matrix_a[new_row_index].append(0) + + # Add new column to objective function too. But make sure to keep objective constant at end of list. + self._obj_func.insert((len(self._obj_func) - 1), 0) + self._obj_constant_index += 1 + + # Save column location of basic var. + self._basic_var_indexes.append(len(self._matrix_a[0]) - 1) + + def _determine_nonbasic_variables(self): + """ + Determines all non-basic variables for given constraints. + """ + # Check that values exist. If they do, we will assume that this was called from "set_simplex_values" and we + # don't need to re-validate. + if self._matrix_a is None or self._constants is None or self._obj_func is None: + raise ValueError('Values are not yet set. This function should only be called from "set_simplex_values".') + + # Check if nonbasic variables are already defined. + if isinstance(self._nonbasic_var_indexes, list) and len(self._nonbasic_var_indexes) > 0: + # Nonbasic variables are already defined. No reason to proceed. + return None + + if self._basic_var_indexes is None or len(self._basic_var_indexes) == 0: + raise ValueError('Values are not yet set. This function should only be called from "set_simplex_values".') + + # If we got this far, assume we have a proper count of basic variables and record all nonbasic columns. + self._nonbasic_var_indexes = [] + for col_index in range(len(self._matrix_a[0])): + if col_index not in self._basic_var_indexes: + self._nonbasic_var_indexes.append(col_index) def display_tableau(self): logger.info('') diff --git a/tests/resources/simplex/simplex.py b/tests/resources/simplex/simplex.py index 6ee3bd85b5fa37f57e13cb949677352858015fdb..cefe8a71fbebe6f360149db224cc94284044bcc2 100644 --- a/tests/resources/simplex/simplex.py +++ b/tests/resources/simplex/simplex.py @@ -21,52 +21,78 @@ class TestSimplex(unittest.TestCase): def test__parse_from_json__success(self): with self.subTest('Using first set of keys.'): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [ [1], ], 'constants': [2], 'objective': [3], + 'basic_vars': [4], + 'non_basic_vars': [5], }) self.assertEqual(self.simplex._matrix_a, [[1]]) self.assertEqual(self.simplex._constants, [2]) self.assertEqual(self.simplex._obj_func, [3, 0]) self.assertEqual(self.simplex._obj_constant_index, 1) + self.assertEqual(self.simplex._basic_var_indexes, [4]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [5]) with self.subTest('Using second set of keys.'): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraint_matrix': [ [1], ], 'constraint_constants': [2], 'objective_function': [3], + 'basic_variables': [4], + 'non_basic_variables': [5], }) self.assertEqual(self.simplex._matrix_a, [[1]]) self.assertEqual(self.simplex._constants, [2]) self.assertEqual(self.simplex._obj_func, [3, 0]) self.assertEqual(self.simplex._obj_constant_index, 1) + self.assertEqual(self.simplex._basic_var_indexes, [4]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [5]) + + with self.subTest('Objective constant provided.'): + self.simplex._parse_json_data({ + 'constraint_matrix': [ + [1], + ], + 'constraint_constants': [2], + 'objective_function': [3, 5], + 'basic_variables': [4], + 'non_basic_variables': [5], + }) + self.assertEqual(self.simplex._matrix_a, [[1]]) + self.assertEqual(self.simplex._constants, [2]) + self.assertEqual(self.simplex._obj_func, [3, 5]) + self.assertEqual(self.simplex._obj_constant_index, 1) + self.assertEqual(self.simplex._basic_var_indexes, [4]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [5]) + def test__parse_from_json__failure(self): with self.subTest('No valid keys.'): with self.assertRaises(KeyError): - self.simplex.parse_json_data({}) + self.simplex._parse_json_data({}) with self.subTest('Only "constants" key.'): with self.assertRaises(KeyError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [], }) with self.subTest('No "objective" key.'): with self.assertRaises(KeyError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [], 'constants': [], }) with self.subTest('Key "constraints" is not list.'): with self.assertRaises(TypeError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': '', 'constants': [], 'objective': [], @@ -74,7 +100,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Key "constants" is not list.'): with self.assertRaises(TypeError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [], 'constants': '', 'objective': [], @@ -82,7 +108,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Key "objective" is not list.'): with self.assertRaises(TypeError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [], 'constants': [], 'objective': '', @@ -90,7 +116,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Key "constraints" is empty list.'): with self.assertRaises(ValueError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [], 'constants': [1], 'objective': [1], @@ -98,7 +124,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Key "constants" is empty list.'): with self.assertRaises(ValueError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [1], 'constants': [], 'objective': [1], @@ -106,7 +132,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Key "objective" is empty list.'): with self.assertRaises(ValueError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [1], 'constants': [1], 'objective': [], @@ -114,7 +140,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Constraint matrix is not list of lists.'): with self.assertRaises(TypeError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [1], 'constants': [1], 'objective': [1], @@ -122,7 +148,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Constraint matrix rows do not match up.'): with self.assertRaises(ValueError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [ [1], [1,2], @@ -133,7 +159,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Constraint matrix and constraint constants do not match up.'): with self.assertRaises(ValueError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [ [1], [1], @@ -144,7 +170,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Objective function less than length of constraint matrix rows.'): with self.assertRaises(ValueError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [ [1, 2], [1, 2], @@ -155,7 +181,7 @@ class TestSimplex(unittest.TestCase): with self.subTest('Objective function greater than length of constraint matrix rows.'): with self.assertRaises(ValueError): - self.simplex.parse_json_data({ + self.simplex._parse_json_data({ 'constraints': [ [1], [1], @@ -163,3 +189,212 @@ class TestSimplex(unittest.TestCase): 'constants': [1, 1], 'objective': [1, 2, 3], }) + + with self.subTest('Variable "basic_vars" is provided, but not list.'): + with self.assertRaises(TypeError): + self.simplex._parse_json_data({ + 'constraints': [ + [1], + [1], + ], + 'constants': [1, 1], + 'objective': [1], + 'basic_vars': '', + }) + + with self.subTest('Variable "non_basic_vars" is provided, but not list.'): + with self.assertRaises(TypeError): + self.simplex._parse_json_data({ + 'constraints': [ + [1], + [1], + ], + 'constants': [1, 1], + 'objective': [1], + 'non_basic_vars': '', + }) + + def test__set_simplex_values(self): + # Note that type validation is already partially tested in above "parse_from_json" tests. + # Thus, here we more specifically test logic for setting and accessing of basic/nonbasic variables. + with self.subTest('No basic vars provided.'): + self.simplex.set_simplex_values([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ], + [1, 1, 1], + [1, 1, 1], + ) + self.assertEqual(self.simplex._matrix_a, [ + [1, 1, 1, 1, 0, 0], + [1, 1, 1, 0, 1, 0], + [1, 1, 1, 0, 0, 1], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [1, 1, 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]) + + with self.subTest('Some basic vars implicitly provided.'): + self.simplex.set_simplex_values([ + [1, 0, 1, 0, 1], + [1, 1, 1, 0, 1], + [1, 0, 1, 1, 1], + ], + [1, 1, 1], + [1, 0, 1, 0, 1], + ) + self.assertEqual(self.simplex._matrix_a, [ + [1, 0, 1, 0, 1, 1], + [1, 1, 1, 0, 1, 0], + [1, 0, 1, 1, 1, 0], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [1, 0, 1, 0, 1, 0, 0]) + self.assertEqual(self.simplex._obj_constant_index, 6) + self.assertEqual(self.simplex._basic_var_indexes, [1, 3, 5]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [0, 2, 4]) + + with self.subTest('Some basic vars implicitly provided - With obj constant.'): + self.simplex.set_simplex_values([ + [1, 0, 1, 0, 1], + [1, 1, 1, 0, 1], + [1, 0, 1, 1, 1], + ], + [1, 1, 1], + [1, 0, 1, 0, 1, 5], + ) + self.assertEqual(self.simplex._matrix_a, [ + [1, 0, 1, 0, 1, 1], + [1, 1, 1, 0, 1, 0], + [1, 0, 1, 1, 1, 0], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [1, 0, 1, 0, 1, 0, 5]) + self.assertEqual(self.simplex._obj_constant_index, 6) + self.assertEqual(self.simplex._basic_var_indexes, [1, 3, 5]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [0, 2, 4]) + + with self.subTest('All basic vars implicitly provided at start.'): + self.simplex.set_simplex_values([ + [0, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + ], + [1, 1, 1], + [0, 0, 0, 1, 1, 1], + ) + self.assertEqual(self.simplex._matrix_a, [ + [0, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [0, 0, 0, 1, 1, 1, 0]) + self.assertEqual(self.simplex._obj_constant_index, 6) + self.assertEqual(self.simplex._basic_var_indexes, [0, 1, 2]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [3, 4, 5]) + + with self.subTest('All basic vars implicitly provided at start - With obj constant.'): + self.simplex.set_simplex_values([ + [0, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + ], + [1, 1, 1], + [0, 0, 0, 1, 1, 1, 5], + ) + self.assertEqual(self.simplex._matrix_a, [ + [0, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [0, 0, 0, 1, 1, 1, 5]) + self.assertEqual(self.simplex._obj_constant_index, 6) + self.assertEqual(self.simplex._basic_var_indexes, [0, 1, 2]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [3, 4, 5]) + + with self.subTest('All basic vars implicitly provided at end.'): + self.simplex.set_simplex_values([ + [1, 1, 1, 0, 1, 0], + [1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 0, 0], + ], + [1, 1, 1], + [1, 1, 1, 0, 0, 0], + ) + self.assertEqual(self.simplex._matrix_a, [ + [1, 1, 1, 0, 1, 0], + [1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 0, 0], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [1, 1, 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]) + + with self.subTest('All basic vars implicitly provided at end - With objective constant.'): + self.simplex.set_simplex_values([ + [1, 1, 1, 0, 1, 0], + [1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 0, 0], + ], + [1, 1, 1], + [1, 1, 1, 0, 0, 0, 5], + ) + self.assertEqual(self.simplex._matrix_a, [ + [1, 1, 1, 0, 1, 0], + [1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 0, 0], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [1, 1, 1, 0, 0, 0, 5]) + 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]) + + with self.subTest('All basic vars explicitly provided at start.'): + self.simplex.set_simplex_values([ + [0, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + ], + [1, 1, 1], + [0, 0, 0, 1, 1, 1], + basic_vars=[0, 1, 2], + ) + self.assertEqual(self.simplex._matrix_a, [ + [0, 1, 0, 1, 1, 1], + [1, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [0, 0, 0, 1, 1, 1, 0]) + self.assertEqual(self.simplex._obj_constant_index, 6) + self.assertEqual(self.simplex._basic_var_indexes, [0, 1, 2]) + self.assertEqual(self.simplex._nonbasic_var_indexes, [3, 4, 5]) + + with self.subTest('All basic vars explicitly provided at end.'): + self.simplex.set_simplex_values([ + [1, 1, 1, 0, 1, 0], + [1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 0, 0], + ], + [1, 1, 1], + [1, 1, 1, 0, 0, 0], + basic_vars=[3, 4, 5], + ) + self.assertEqual(self.simplex._matrix_a, [ + [1, 1, 1, 0, 1, 0], + [1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 0, 0], + ]) + self.assertEqual(self.simplex._constants, [1, 1, 1]) + self.assertEqual(self.simplex._obj_func, [1, 1, 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])