diff --git a/lippy/__init__.py b/lippy/__init__.py deleted file mode 100755 index 10e9e5f..0000000 --- a/lippy/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -from .simplex_method import SimplexMethod -from .branch_and_bound import BranchAndBound -from .brute_force import BruteForce -from .cutting_plane_method import CuttingPlaneMethod -from .zero_sum_game import ZeroSumGame -from ._log_modes import FULL_LOG, MEDIUM_LOG, LOG_OFF -from .dual_lp import primal_to_dual_lp - - -__version__ = "0.0.1" -__doc__ = """ -LipPy -===== - -Provides - 1. Solving primal linear programming problems - 2. Solving dual linear programming problems - 3. Solving integer linear programming problems - 4. Solving zero-sum games ----------------------------- - -@Medvate was developed. -GitHub repository: -Materials written by N.S.Konnova used in the development. -""" - - - diff --git a/lippy/_log_modes.py b/lippy/_log_modes.py deleted file mode 100755 index 9b000e8..0000000 --- a/lippy/_log_modes.py +++ /dev/null @@ -1,3 +0,0 @@ -FULL_LOG = 2 -MEDIUM_LOG = 1 -LOG_OFF = 0 diff --git a/lippy/dual_lp.py b/lippy/dual_lp.py deleted file mode 100755 index b68d617..0000000 --- a/lippy/dual_lp.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List, Tuple - -import numpy as np - - -def primal_to_dual_lp(func_vec: List[List[int or float]] or np.ndarray, - conditions_matrix: List[List[int or float]] or np.ndarray, - constraints_vec: List[List[int or float]] or np.ndarray) -> Tuple[np.ndarray, np.ndarray, - np.ndarray]: - """ - Converts a primal linear programming problem into a dual. - - :param func_vec: Coefficients of the equation. - :param conditions_matrix: The left part of the restriction system. - :param constraints_vec: The right part of the restriction system. - :return: Formulation of a dual linear programming problem. - """ - func_vec = np.array(func_vec, dtype=np.float64) - conditions_matrix = np.array(conditions_matrix, dtype=np.float64) - constraints_vec = np.array(constraints_vec, dtype=np.float64) - - return -constraints_vec, -conditions_matrix.T, -func_vec diff --git a/lippy/zero_sum_game.py b/lippy/zero_sum_game.py deleted file mode 100755 index b2d1754..0000000 --- a/lippy/zero_sum_game.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import List - -import numpy as np - -from .simplex_method import SimplexMethod -from ._log_modes import FULL_LOG, MEDIUM_LOG, LOG_OFF - - -class ZeroSumGame: - """ - In game theory and economic theory, a zero-sum game is a mathematical representation - of a situation in which an advantage that is won by one of two sides is lost by the other. - If the total gains of the participants are added up, and the total losses are subtracted, they will sum to zero. - """ - def __init__(self, matrix: List[List[int or float]] or np.ndarray, var_tag: str = "x", - func_tag: str = "F", log_mode: int = LOG_OFF): - """ - Initialization of an object of the "Zero-sum game" class. - - :param matrix: Game's payoff matrix. - :param var_tag: The name of the variables, default is "x". - :param func_tag: The name of the function, default is "F". - :param log_mode: So much information about the solution to write to the console. - """ - self.matrix = np.array(matrix, dtype=np.float64) - self.optimal_strategies = list() - - self.var_tag = var_tag - self.func_tag = func_tag - self.log_mode = log_mode - - def solve(self) -> [np.ndarray, np.ndarray]: - """ - Finds optimal strategies for both players. - - :return: Optimal strategies. - """ - self._solve_for_player(-np.ones(self.matrix.shape[0]), -self.matrix.T, -np.ones(self.matrix.shape[1])) - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print(f"Winning strategy for the first player: {np.around(self.optimal_strategies[0], 3)}\n\n") - - self._solve_for_player(np.ones(self.matrix.shape[1]), self.matrix, np.ones(self.matrix.shape[0])) - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print(f"Winning strategy for the second player: {np.around(self.optimal_strategies[1], 3)}\n\n") - - return self.optimal_strategies - - def _solve_for_player(self, c_vec: np.ndarray, matrix: np.ndarray, b_vec: np.ndarray) -> None: - """ - Finds optimal strategies for a player. - - :param c_vec: Coefficients of the equation. - :param matrix: Game's payoff matrix. - :param b_vec: The right part of the restriction system. - :return: None - """ - simplex = SimplexMethod(c_vec, matrix, b_vec, var_tag=self.var_tag, - func_tag=self.func_tag, log_mode=self.log_mode) - simplex.solve() - - h = abs(1 / simplex.get_func_value()) - if self.log_mode == FULL_LOG: - print(f"h = {round(h, 3)}") - solution = simplex.get_solution() * h - self.optimal_strategies.append(solution) diff --git a/src/lippy/__init__.py b/src/lippy/__init__.py new file mode 100755 index 0000000..bce1617 --- /dev/null +++ b/src/lippy/__init__.py @@ -0,0 +1,38 @@ +from .bnb import BranchAndBound +from .brute_force import BruteForce +from .dual import primal_to_dual_lp +from .game import ZeroSumGame +from .gomory import CuttingPlaneMethod +from .simplex import SimplexMethod + +from .enums import LogMode + + +__author__ = 'ispaneli' +__email__ = 'ispanelki@gmail.com' + +__version__ = '0.0.5' + +__doc__ = """ +Lippy +===== + +Provides: + 1. Simplex method in primal linear programming + 2. Simplex method in dual linear programming + 3. Branch and bound in integer linear programming + 4. Brute force method in integer linear programming + 5. Cutting-plane method in integer linear programming + 6. Zero-sum game in game theory (using Simplex method) +---------------------------- + +Author: @ispaneli +E-mail: ispanelki@gmail.com +GitHub repository: + +p.s. During the development, the materials of N.S.Konnova (BMSTU, IU-8) were used. +""" + + + + diff --git a/src/lippy/bnb/__init__.py b/src/lippy/bnb/__init__.py new file mode 100644 index 0000000..8a937ae --- /dev/null +++ b/src/lippy/bnb/__init__.py @@ -0,0 +1 @@ +from .branch_and_bound import BranchAndBound diff --git a/lippy/branch_and_bound.py b/src/lippy/bnb/branch_and_bound.py similarity index 56% rename from lippy/branch_and_bound.py rename to src/lippy/bnb/branch_and_bound.py index 9334100..79d064b 100755 --- a/lippy/branch_and_bound.py +++ b/src/lippy/bnb/branch_and_bound.py @@ -1,123 +1,151 @@ -from typing import List - -import numpy as np - -from .simplex_method import SimplexMethod -from ._log_modes import FULL_LOG, MEDIUM_LOG, LOG_OFF - - -class BranchAndBound: - """ - Branch and bound is an algorithm design paradigm for discrete and combinatorial optimization problems, - as well as mathematical optimization. - A branch-and-bound algorithm consists of a systematic enumeration of candidate solutions by means of state - space search: the set of candidate solutions is thought of as forming a rooted tree with the full set at the root. - The algorithm explores branches of this tree, which represent subsets of the solution set. - """ - def __init__(self, func_vec: List[int or float] or np.ndarray, - conditions_matrix: List[List[int or float]] or np.ndarray, - constraints_vec: List[int or float] or np.ndarray, - var_tag: str = "x", func_tag: str = "F", log_mode: int = LOG_OFF): - """ - Initialization of an object of the "Branch and Bound" class. - - :param func_vec: Coefficients of the equation. - :param conditions_matrix: The left part of the restriction system. - :param constraints_vec: The right part of the restriction system. - :param var_tag: The name of the variables, default is "x". - :param func_tag: The name of the function, default is "F". - :param log_mode: So much information about the solution to write to the console. - """ - self.c_vec = np.array(func_vec, dtype=np.float64) - self.a_matrix = np.array(conditions_matrix, dtype=np.float64) - self.b_vec = np.array(constraints_vec, dtype=np.float64) - self.num_of_vars = self.c_vec.shape[0] - - self.var_tag = var_tag - self.func_tag = func_tag - self.log_mode = log_mode - - self._final_values = list() - self._solution = NotImplemented - self._func_value = NotImplemented - - def solve(self) -> (np.ndarray, np.float64): - """ - Solve the integer problem of linear programming by the Branch and bound method. - - :return: The solution and the value of the function. - """ - self._node_iteration(self.c_vec, self.a_matrix, self.b_vec) - - func_results = [d['F'] for d in self._final_values] - index_of_solution = func_results.index(max(func_results)) - self._solution = np.array([self._final_values[index_of_solution][i] for i in range(self.num_of_vars)]) - self._func_value = self._final_values[index_of_solution]['F'] - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print("\nSolution of the integer problem of linear programming:") - print(np.around(self._solution, 3)) - print("The value of the function:") - print(np.around(self._func_value, 3), "\n") - - return self._solution, self._func_value - - def _node_iteration(self, c_vec: np.ndarray, a_matrix: np.ndarray, b_vec: np.ndarray) -> None: - """ - Performs calculations to the node, then starts branching left and right. - - :param c_vec: Coefficients of the equation. - :param a_matrix: The left part of the restriction system. - :param b_vec: The right part of the restriction system. - :return: None. - """ - simplex_method = SimplexMethod(c_vec, a_matrix, b_vec, var_tag=self.var_tag, - func_tag=self.func_tag, log_mode=self.log_mode) - simplex_method.solve() - - if not simplex_method.table.was_solved() and not simplex_method.table.has_optimal_solution(): - return - - is_integer_solution = True - solution = simplex_method.get_solution(self.num_of_vars) - for index, value in enumerate(solution): - if round(value, 5) % 1 != 0: - is_integer_solution = False - - new_a_row = np.zeros(self.c_vec.shape[0], dtype=np.float64) - new_a_row[index] = 1 - a_matrix = np.vstack((a_matrix, new_a_row)) - - b_vec = np.append(b_vec, int(value)) - if int(value) >= 0: - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print(f"\nNew branch: x{index + 1} <= {int(value)}") - self._node_iteration(c_vec, a_matrix, b_vec) - - a_matrix[-1, index] = -1 - b_vec[-1] = -b_vec[-1] - 1 - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print(f"New branch: x{index + 1} >= {int(value) + 1}") - self._node_iteration(c_vec, a_matrix, b_vec) - - if is_integer_solution: - values = {i: np.rint(v) for i, v in enumerate(solution)} - values['F'] = simplex_method.get_func_value() - self._final_values.append(values) - - def get_solution(self) -> np.ndarray: - """ - Return solution of problem of linear programming. - - :return: Solution of problem of linear programming - """ - return self._solution - - def get_func_value(self) -> np.float64: - """ - Return the value of the function. - - :return: The value of the function. - """ - return self._func_value +import numpy as np + +from ..simplex import SimplexMethod +from ..enums import LogMode +from ..types import Vector, Matrix + + +class BranchAndBound: + """ + Branch and bound is an algorithm design paradigm + for discrete and combinatorial optimization problems, + as well as mathematical optimization. + + A branch-and-bound algorithm consists of a systematic + enumeration of candidate solutions by means of state + space search: the set of candidate solutions is thought + of as forming a rooted tree with the full set at the root. + + The algorithm explores branches of this tree, + which represent subsets of the solution set. + """ + def __init__( + self, + func_vec: Vector, + conditions_matrix: Matrix, + constraints_vec: Vector, + var_tag: str = 'x', + func_tag: str = 'F', + log_mode: int = LogMode.LOG_OFF + ): + """ + Initialization of an object of the "Branch and Bound" class. + + :param Vector func_vec: Coefficients of the equation. + :param Matrix conditions_matrix: The left part of the restriction system. + :param Vector constraints_vec: The right part of the restriction system. + :param str var_tag: The name of the variables (default: 'x'). + :param str func_tag: The name of the function (default: 'F'). + :param int log_mode: So much information about the solution to write + to the console (default: LogMode.LOG_OFF). + """ + self.c_vec = np.array(func_vec, dtype=np.float64) + self.a_matrix = np.array(conditions_matrix, dtype=np.float64) + self.b_vec = np.array(constraints_vec, dtype=np.float64) + self.num_of_vars = self.c_vec.shape[0] + + self.var_tag = var_tag + self.func_tag = func_tag + self.log_mode = log_mode + + self._final_values = list() + self._solution = NotImplemented + self._func_value = NotImplemented + + def solve(self) -> (np.ndarray, np.float64): + """ + Solve the integer problem of linear programming by the Branch and bound method. + + :return: The solution and the value of the function. + :rtype: (np.ndarray, np.float64) + """ + self._node_iteration(self.c_vec, self.a_matrix, self.b_vec) + + func_results = [d['F'] for d in self._final_values] + index_of_solution = func_results.index(max(func_results)) + self._solution = np.array([self._final_values[index_of_solution][i] for i in range(self.num_of_vars)]) + self._func_value = self._final_values[index_of_solution]['F'] + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print("\nSolution of the integer problem of linear programming:") + print(np.around(self._solution, 3)) + print("The value of the function:") + print(np.around(self._func_value, 3), "\n") + + return self._solution, self._func_value + + def _node_iteration( + self, + c_vec: np.ndarray, + a_matrix: np.ndarray, + b_vec: np.ndarray + ) -> None: + """ + Performs calculations to the node, then starts branching left and right. + + :param np.ndarray c_vec: Coefficients of the equation. + :param np.ndarray a_matrix: The left part of the restriction system. + :param np.ndarray b_vec: The right part of the restriction system. + :return: None + """ + simplex_method = SimplexMethod( + c_vec, + a_matrix, + b_vec, + var_tag=self.var_tag, + func_tag=self.func_tag, + log_mode=self.log_mode + ) + simplex_method.solve() + + if all(( + not simplex_method.table.was_solved(), + not simplex_method.table.has_optimal_solution() + )): + return + + is_integer_solution = True + solution = simplex_method.get_solution(self.num_of_vars) + for index, value in enumerate(solution): + if round(value, 5) % 1 != 0: + is_integer_solution = False + + new_a_row = np.zeros(self.c_vec.shape[0], dtype=np.float64) + new_a_row[index] = 1 + a_matrix = np.vstack((a_matrix, new_a_row)) + + b_vec = np.append(b_vec, int(value)) + if int(value) >= 0: + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print(f"\nNew branch: x{index + 1} <= {int(value)}") + self._node_iteration(c_vec, a_matrix, b_vec) + + a_matrix[-1, index] = -1 + b_vec[-1] = -b_vec[-1] - 1 + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print(f"New branch: x{index + 1} >= {int(value) + 1}") + self._node_iteration(c_vec, a_matrix, b_vec) + + if is_integer_solution: + values = {i: np.rint(v) for i, v in enumerate(solution)} + values['F'] = simplex_method.get_func_value() + self._final_values.append(values) + + def get_solution(self) -> np.ndarray: + """ + Return solution of problem of linear programming. + + :return: Solution of problem of linear programming. + :rtype: np.ndarray + """ + return self._solution + + def get_func_value(self) -> np.float64: + """ + Return the value of the function. + + :return: The value of the function. + :rtype: np.float64 + """ + return self._func_value diff --git a/src/lippy/brute_force/__init__.py b/src/lippy/brute_force/__init__.py new file mode 100644 index 0000000..a755bfe --- /dev/null +++ b/src/lippy/brute_force/__init__.py @@ -0,0 +1 @@ +from .brute_force import BruteForce diff --git a/lippy/brute_force.py b/src/lippy/brute_force/brute_force.py similarity index 61% rename from lippy/brute_force.py rename to src/lippy/brute_force/brute_force.py index 5e7d13a..6881b6f 100755 --- a/lippy/brute_force.py +++ b/src/lippy/brute_force/brute_force.py @@ -1,110 +1,135 @@ -from typing import List - -import itertools -import numpy as np -import prettytable - -from .simplex_method import SimplexMethod -from ._log_modes import FULL_LOG, MEDIUM_LOG, LOG_OFF - - -class BruteForce: - """ - In computer science, brute-force search or exhaustive search, also known as generate and test, - is a very general problem-solving technique and algorithmic paradigm that consists of systematically enumerating - all possible candidates for the solution and checking whether each candidate satisfies the problem's statement. - It is used to solve integer linear programming problems. - """ - def __init__(self, func_vec: List[int or float] or np.ndarray, - conditions_matrix: List[List[int or float]] or np.ndarray, - constraints_vec: List[int or float] or np.ndarray, - var_tag: str = "x", func_tag: str = "F", log_mode: int = LOG_OFF): - """ - Initialization of an object of the "Brute Force" class. - :param func_vec: Coefficients of the equation. - :param conditions_matrix: The left part of the restriction system. - :param constraints_vec: The right part of the restriction system. - :param var_tag: The name of the variables, default is "x". - :param func_tag: The name of the function, default is "F". - :param log_mode: So much information about the solution to write to the console. - """ - self.c_vec = np.array(func_vec, dtype=np.float64) - self.a_matrix = np.array(conditions_matrix, dtype=np.float64) - self.b_vec = np.array(constraints_vec, dtype=np.float64) - self.num_of_vars = self.c_vec.shape[0] - - self.var_tag = var_tag - self.func_tag = func_tag - self.log_mode = log_mode - - self._nodes = list() - self._solution = NotImplemented - self._func_value = NotImplemented - - def solve(self) -> (np.ndarray, np.float64): - """ - Solves the problem of integer linear programming by the method of full iteration. - :return: The solution and the value of the function. - """ - simplex = SimplexMethod(self.c_vec, self.a_matrix, self.b_vec, var_tag=self.var_tag, - func_tag=self.func_tag, log_mode=self.log_mode) - simplex.solve() - - simplex_func_value = simplex.get_func_value() - self._func_value = -np.inf if simplex_func_value > 0 else np.inf - limit_value = np.ceil(simplex_func_value / self.c_vec.min()) - if self.log_mode == FULL_LOG: - print(f"The upper bound of values for all variables {self.var_tag}: {limit_value}.\n") - - for test_solution in itertools.product(np.arange(limit_value + 1), repeat=self.num_of_vars): - test_solution_np = np.array(test_solution) - - if self._check_solution(test_solution_np): - func_value = sum(test_solution_np * self.c_vec) - new_node = [int(s) for s in test_solution] + [func_value] - self._nodes.append(new_node) - - if (simplex_func_value > 0 and func_value > self._func_value)\ - or (simplex_func_value <= 0 and func_value < self._func_value): - self._func_value = func_value - self._solution = test_solution_np - - if self.log_mode == FULL_LOG: - print("Table of solutions:") - field_names = [f"{simplex.table.var_tag}{i + 1}" for i in range(self.c_vec.shape[0])] - field_names.append(simplex.table.func_tag) - solutions_table = prettytable.PrettyTable(field_names=field_names) - solutions_table.add_rows(self._nodes) - print(solutions_table, "\n") - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print("\nSolution of the integer problem of linear programming:") - print(np.around(self._solution, 3)) - print("The value of the function:") - print(np.around(self._func_value, 3), "\n") - - return self._solution, self._func_value - - def _check_solution(self, solution: np.ndarray) -> bool: - """ - Checks the solution for compliance with a system of equations with constraints. - :param solution: Integer values of variables. - :return: True - if the solution satisfies the system of constraints; False - else. - """ - b_test = np.sum(self.a_matrix * solution, axis=1) - vec_of_comparisons = (b_test <= self.b_vec) - return vec_of_comparisons.sum() == vec_of_comparisons.shape[0] - - def get_solution(self) -> np.ndarray: - """ - Return solution of problem of linear programming. - :return: Solution of problem of linear programming - """ - return self._solution - - def get_func_value(self) -> np.float64: - """ - Return the value of the function. - :return: The value of the function. - """ - return self._func_value +import itertools +import numpy as np +import prettytable + +from ..simplex import SimplexMethod +from ..enums import LogMode +from ..types import Vector, Matrix + + +class BruteForce: + """ + In computer science, brute-force search or exhaustive search, + also known as generate and test, is a very general problem-solving + technique and algorithmic paradigm that consists of systematically + enumerating all possible candidates for the solution and checking + whether each candidate satisfies the problem's statement. + + It's used to solve integer linear programming problems. + """ + def __init__( + self, + func_vec: Vector, + conditions_matrix: Matrix, + constraints_vec: Vector, + var_tag: str = 'x', + func_tag: str = 'F', + log_mode: int = LogMode.LOG_OFF + ): + """ + Initialization of an object of the "Brute Force" class. + + :param Vector func_vec: Coefficients of the equation. + :param Matrix conditions_matrix: The left part of the restriction system. + :param Vector constraints_vec: The right part of the restriction system. + :param str var_tag: The name of the variables (default: 'x'). + :param str func_tag: The name of the function (default: 'F'). + :param int log_mode: So much information about the solution to write + to the console (default: LogMode.LOG_OFF). + """ + self.c_vec = np.array(func_vec, dtype=np.float64) + self.a_matrix = np.array(conditions_matrix, dtype=np.float64) + self.b_vec = np.array(constraints_vec, dtype=np.float64) + self.num_of_vars = self.c_vec.shape[0] + + self.var_tag = var_tag + self.func_tag = func_tag + self.log_mode = log_mode + + self._nodes = list() + self._solution = NotImplemented + self._func_value = NotImplemented + + def solve(self) -> (np.ndarray, np.float64): + """ + Solves the problem of integer linear programming by the method of full iteration. + + :return: The solution and the value of the function. + :rtype: (np.ndarray, np.float64) + """ + simplex = SimplexMethod( + self.c_vec, + self.a_matrix, + self.b_vec, + var_tag=self.var_tag, + func_tag=self.func_tag, + log_mode=self.log_mode + ) + simplex.solve() + + simplex_func_value = simplex.get_func_value() + self._func_value = -np.inf if simplex_func_value > 0 else np.inf + limit_value = np.ceil(simplex_func_value / self.c_vec.min()) + if self.log_mode is LogMode.FULL_LOG: + print( + f"The upper bound of values for all variables {self.var_tag}: {limit_value}.\n" + ) + + for test_solution in itertools.product(np.arange(limit_value + 1), repeat=self.num_of_vars): + test_solution_np = np.array(test_solution) + + if self._check_solution(test_solution_np): + func_value = sum(test_solution_np * self.c_vec) + new_node = [int(s) for s in test_solution] + [func_value] + self._nodes.append(new_node) + + if (simplex_func_value > 0 and func_value > self._func_value)\ + or (simplex_func_value <= 0 and func_value < self._func_value): + self._func_value = func_value + self._solution = test_solution_np + + if self.log_mode is LogMode.FULL_LOG: + print("Table of solutions:") + field_names = [f"{simplex.table.var_tag}{i + 1}" for i in range(self.c_vec.shape[0])] + field_names.append(simplex.table.func_tag) + solutions_table = prettytable.PrettyTable(field_names=field_names) + solutions_table.add_rows(self._nodes) + print(solutions_table, "\n") + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print("\nSolution of the integer problem of linear programming:") + print(np.around(self._solution, 3)) + print("The value of the function:") + print(np.around(self._func_value, 3), "\n") + + return self._solution, self._func_value + + def _check_solution(self, solution: np.ndarray) -> bool: + """ + Checks the solution for compliance with a system of equations with constraints. + + :param np.ndarray solution: Integer values of variables. + :return: Does the solution satisfy the constraint system? + :rtype: bool + """ + b_test = np.sum(self.a_matrix * solution, axis=1) + vec_of_comparisons = (b_test <= self.b_vec) + return vec_of_comparisons.sum() == vec_of_comparisons.shape[0] + + def get_solution(self) -> np.ndarray: + """ + Return solution of problem of linear programming. + + :return: Solution of problem of linear programming. + :rtype: np.ndarray + """ + return self._solution + + def get_func_value(self) -> np.float64: + """ + Return the value of the function. + + :return: The value of the function. + :rtype: np.float64 + """ + return self._func_value diff --git a/src/lippy/dual/__init__.py b/src/lippy/dual/__init__.py new file mode 100644 index 0000000..9835708 --- /dev/null +++ b/src/lippy/dual/__init__.py @@ -0,0 +1 @@ +from .dual_lp import primal_to_dual_lp diff --git a/src/lippy/dual/dual_lp.py b/src/lippy/dual/dual_lp.py new file mode 100755 index 0000000..50cdeab --- /dev/null +++ b/src/lippy/dual/dual_lp.py @@ -0,0 +1,24 @@ +import numpy as np + +from ..types import Vector, Matrix + + +def primal_to_dual_lp( + func_vec: Vector, + conditions_matrix: Matrix, + constraints_vec: Vector +) -> (np.ndarray, np.ndarray, np.ndarray): + """ + Converts a primal linear programming problem into a dual. + + :param Vector func_vec: Coefficients of the equation. + :param Matrix conditions_matrix: The left part of the restriction system. + :param Vector constraints_vec: The right part of the restriction system. + :return: Formulation of a dual linear programming problem. + :rtype: (np.ndarray, np.ndarray, np.ndarray) + """ + func_vec = np.array(func_vec, dtype=np.float64) + conditions_matrix = np.array(conditions_matrix, dtype=np.float64) + constraints_vec = np.array(constraints_vec, dtype=np.float64) + + return -constraints_vec, -conditions_matrix.T, -func_vec diff --git a/src/lippy/enums.py b/src/lippy/enums.py new file mode 100644 index 0000000..f68910f --- /dev/null +++ b/src/lippy/enums.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class LogMode(Enum): + """ + Enum of logging modes for solving linear programming problems. + """ + LOG_OFF = 0 + MEDIUM_LOG = 1 + FULL_LOG = 2 diff --git a/src/lippy/exceptions.py b/src/lippy/exceptions.py new file mode 100644 index 0000000..c3b9886 --- /dev/null +++ b/src/lippy/exceptions.py @@ -0,0 +1,10 @@ +TABLE_CONST_VEC_ERROR = ValueError("The shape of constraints-vector must be of the form (m, ).") +TABLE_FUNC_VEC_ERROR = ValueError("The shape of function-vector must be of the form (n, ).") +TABLE_CONST_MATRIX_ERROR = ValueError( + "The shape of constraints-vector must be of the form (m, ); " + "the shape of conditions-matrix must be of the form (m, n)." +) +TABLE_FUNC_MATRIX_ERROR = ValueError( + "The shape of function-vector must be of the form (n, ); " + "the shape of conditions-matrix must be of the form (m, n)." +) diff --git a/src/lippy/game/__init__.py b/src/lippy/game/__init__.py new file mode 100644 index 0000000..8efe801 --- /dev/null +++ b/src/lippy/game/__init__.py @@ -0,0 +1 @@ +from .zero_sum_game import ZeroSumGame diff --git a/src/lippy/game/zero_sum_game.py b/src/lippy/game/zero_sum_game.py new file mode 100755 index 0000000..1dbcc0a --- /dev/null +++ b/src/lippy/game/zero_sum_game.py @@ -0,0 +1,93 @@ +import numpy as np + +from ..simplex import SimplexMethod +from ..enums import LogMode +from ..types import Matrix + + +class ZeroSumGame: + """ + In game theory and economic theory, a zero-sum game + is a mathematical representation of a situation in which + an advantage that is won by one of two sides is lost by the other. + + If the total gains of the participants are added up, + and the total losses are subtracted, they will sum to zero. + """ + def __init__( + self, + matrix: Matrix, + var_tag: str = 'x', + func_tag: str = 'F', + log_mode: int = LogMode.LOG_OFF + ): + """ + Initialization of an object of the "Zero-sum game" class. + + :param Matrix matrix: Game's payoff matrix. + :param str var_tag: The name of the variables (default: 'x'). + :param str func_tag: The name of the function (default: 'F'). + :param int log_mode: So much information about the solution to write + to the console (default: LogMode.LOG_OFF). + """ + self.matrix = np.array(matrix, dtype=np.float64) + self.optimal_strategies = list() + + self.var_tag = var_tag + self.func_tag = func_tag + self.log_mode = log_mode + + def solve(self) -> [np.ndarray, np.ndarray]: + """ + Finds optimal strategies for both players. + + :return: Optimal strategies. + :rtype: [np.ndarray, np.ndarray] + """ + self._solve_for_player( + -np.ones(self.matrix.shape[0]), + -self.matrix.T, + -np.ones(self.matrix.shape[1]) + ) + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print( + f"Winning strategy for the first player: {np.around(self.optimal_strategies[0], 3)}\n\n" + ) + + self._solve_for_player(np.ones(self.matrix.shape[1]), self.matrix, np.ones(self.matrix.shape[0])) + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print( + f"Winning strategy for the second player: {np.around(self.optimal_strategies[1], 3)}\n\n" + ) + + return self.optimal_strategies + + def _solve_for_player( + self, + c_vec: np.ndarray, + matrix: np.ndarray, + b_vec: np.ndarray + ) -> None: + """ + Finds optimal strategies for a player. + + :param np.ndarray c_vec: Coefficients of the equation. + :param np.ndarray matrix: Game's payoff matrix. + :param np.ndarray b_vec: The right part of the restriction system. + :return: None + """ + simplex = SimplexMethod( + c_vec, + matrix, + b_vec, + var_tag=self.var_tag, + func_tag=self.func_tag, + log_mode=self.log_mode + ) + simplex.solve() + + h = abs(1 / simplex.get_func_value()) + if self.log_mode is LogMode.FULL_LOG: + print(f"h = {round(h, 3)}") + solution = simplex.get_solution() * h + self.optimal_strategies.append(solution) diff --git a/src/lippy/gomory/__init__.py b/src/lippy/gomory/__init__.py new file mode 100644 index 0000000..aff41ba --- /dev/null +++ b/src/lippy/gomory/__init__.py @@ -0,0 +1 @@ +from .cutting_plane_method import CuttingPlaneMethod diff --git a/lippy/cutting_plane_method.py b/src/lippy/gomory/cutting_plane_method.py similarity index 60% rename from lippy/cutting_plane_method.py rename to src/lippy/gomory/cutting_plane_method.py index 44a651b..8eea7b3 100755 --- a/lippy/cutting_plane_method.py +++ b/src/lippy/gomory/cutting_plane_method.py @@ -1,124 +1,154 @@ -from typing import List - -import numpy as np - -from .simplex_method import SimplexMethod -from ._log_modes import FULL_LOG, MEDIUM_LOG, LOG_OFF - - -class CuttingPlaneMethod: - """ - In mathematical optimization, the cutting-plane method is any of a variety of optimization methods that - iteratively refine a feasible set or objective function by means of linear inequalities, termed cuts. - Such procedures are commonly used to find integer solutions to mixed integer linear programming (MILP) problems, - as well as to solve general, not necessarily differentiable convex optimization problems. - The use of cutting planes to solve MILP was introduced by Ralph E. Gomory. - """ - def __init__(self, func_vec: List[int or float] or np.ndarray, - conditions_matrix: List[List[int or float]] or np.ndarray, - constraints_vec: List[int or float] or np.ndarray, - var_tag: str = "x", func_tag: str = "F", log_mode: int = LOG_OFF): - """ - Initialization of an object of the "Cutting Plane method" class. - - :param func_vec: Coefficients of the equation. - :param conditions_matrix: The left part of the restriction system. - :param constraints_vec: The right part of the restriction system. - :param var_tag: The name of the variables, default is "x". - :param func_tag: The name of the function, default is "F". - :param log_mode: So much information about the solution to write to the console. - """ - self.c_vec = np.array(func_vec) - self.a_matrix = np.array(conditions_matrix) - self.b_vec = np.array(constraints_vec) - - self._num_of_vars = self.c_vec.shape[0] - self.var_tag = var_tag - self.func_tag = func_tag - self.log_mode = log_mode - - self._simplex: SimplexMethod = NotImplemented - - def solve(self) -> (np.ndarray, np.float64): - """ - Solve the integer problem of linear programming by the Cutting Plane method. - - :return: The solution and the value of the function. - """ - self._simplex = SimplexMethod(self.c_vec, self.a_matrix, self.b_vec, var_tag=self.var_tag, - func_tag=self.func_tag, log_mode=self.log_mode) - start_table = np.copy(self._simplex.table.table) - self._simplex.solve() - - while self._check_solution(): - table = self._simplex.table.table - - fractional_parts = np.modf(table[:-1, 0])[0] - row_index = fractional_parts.argmax() - - coefficients = table[row_index] - coefficients -= np.floor(coefficients) - coefficients[0] *= -1 - - new_equation = np.zeros(self.c_vec.shape[0] + 1) - for i_1, x_i in enumerate(self._simplex.table.column_indices): - if x_i >= self.c_vec.shape[0]: - for i_2, y_i in enumerate(np.arange(table.shape[0] - 1) + table.shape[1]): - if x_i == y_i: - for i_3 in range(new_equation.shape[0]): - new_equation[i_3] += coefficients[i_1] * start_table[i_2, i_3] - break - else: - for column_index in range(self.c_vec.shape[0]): - if column_index == x_i: - new_equation[column_index] += coefficients[i_1] - break - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - var_tags = [f"{self.var_tag}{i + 1}" for i in range(self._num_of_vars)] - equation = " + ".join([f"{c}*{v}" for c, v in zip(np.around(new_equation[1:], 3), var_tags)]) - print(f"New equation: {equation} <= {new_equation[0]}") - - self.a_matrix = np.vstack((self.a_matrix, new_equation[1:])) - self.b_vec = np.append(self.b_vec, new_equation[0]) - - self._simplex = SimplexMethod(self.c_vec, self.a_matrix, self.b_vec, var_tag=self.var_tag, - func_tag=self.func_tag, log_mode=self.log_mode) - start_table = np.copy(self._simplex.table.table) - self._simplex.solve() - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print("\nSolution of the integer problem of linear programming:") - print(np.around(self._simplex.get_solution(), 3)) - print("The value of the function:") - print(np.around(self._simplex.get_func_value(), 3), "\n") - - return self._simplex.get_solution(), self._simplex.get_func_value() - - def _check_solution(self) -> bool: - """ - Checks the solution for integers. - - :return: True - solution is integer; False - is not. - """ - solution = self._simplex.get_solution(self._num_of_vars) - for value in solution: - if round(value, 5) % 1 != 0: - return True - return False - - def get_solution(self) -> np.ndarray: - """ - Return solution of problem of linear programming. - - :return: Solution of problem of linear programming - """ - return self._simplex.get_solution() - - def get_func_value(self) -> np.float64: - """ - Return the value of the function. - - :return: The value of the function. - """ - return self._simplex.get_func_value() \ No newline at end of file +import numpy as np + +from ..simplex import SimplexMethod +from ..enums import LogMode +from ..types import Vector, Matrix + + +class CuttingPlaneMethod: + """ + In mathematical optimization, the cutting-plane method is any + of a variety of optimization methods that iteratively refine a feasible set + or objective function by means of linear inequalities, termed cuts. + + Such procedures are commonly used to find integer solutions to + mixed integer linear programming (MILP) problems, as well as to solve + general, not necessarily differentiable convex optimization problems. + + The use of cutting planes to solve MILP was introduced by Ralph E. Gomory. + """ + def __init__( + self, + func_vec: Vector, + conditions_matrix: Matrix, + constraints_vec: Vector, + var_tag: str = 'x', + func_tag: str = 'F', + log_mode: int = LogMode.LOG_OFF + ): + """ + Initialization of an object of the "Cutting Plane method" class. + + :param Vector func_vec: Coefficients of the equation. + :param Matrix conditions_matrix: The left part of the restriction system. + :param Vector constraints_vec: The right part of the restriction system. + :param str var_tag: The name of the variables (default: 'x'). + :param str func_tag: The name of the function (default: 'F'). + :param int log_mode: So much information about the solution to write + to the console (default: LogMode.LOG_OFF). + """ + self.c_vec = np.array(func_vec) + self.a_matrix = np.array(conditions_matrix) + self.b_vec = np.array(constraints_vec) + + self._num_of_vars = self.c_vec.shape[0] + self.var_tag = var_tag + self.func_tag = func_tag + self.log_mode = log_mode + + self._simplex: SimplexMethod = NotImplemented + + def solve(self) -> (np.ndarray, np.float64): + """ + Solve the integer problem of linear programming by the Cutting Plane method. + + :return: The solution and the value of the function. + :rtype: (np.ndarray, np.float64) + """ + self._simplex = SimplexMethod( + self.c_vec, + self.a_matrix, + self.b_vec, + var_tag=self.var_tag, + func_tag=self.func_tag, + log_mode=self.log_mode + ) + start_table = np.copy(self._simplex.table.table) + self._simplex.solve() + + while self._check_solution(): + table = self._simplex.table.table + + fractional_parts = np.modf(table[:-1, 0])[0] + row_index = fractional_parts.argmax() + + coefficients = table[row_index] + coefficients -= np.floor(coefficients) + coefficients[0] *= -1 + + new_equation = np.zeros(self.c_vec.shape[0] + 1) + for i_1, x_i in enumerate(self._simplex.table.column_indices): + if x_i >= self.c_vec.shape[0]: + for i_2, y_i in enumerate(np.arange(table.shape[0] - 1) + table.shape[1]): + if x_i == y_i: + for i_3 in range(new_equation.shape[0]): + new_equation[i_3] += coefficients[i_1] * start_table[i_2, i_3] + break + else: + for column_index in range(self.c_vec.shape[0]): + if column_index == x_i: + new_equation[column_index] += coefficients[i_1] + break + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + var_tags = [f"{self.var_tag}{i + 1}" for i in range(self._num_of_vars)] + equation = " + ".join( + [ + f"{c}*{v}" + for c, v in zip(np.around(new_equation[1:], 3), var_tags) + ] + ) + print(f"New equation: {equation} <= {new_equation[0]}") + + self.a_matrix = np.vstack((self.a_matrix, new_equation[1:])) + self.b_vec = np.append(self.b_vec, new_equation[0]) + + self._simplex = SimplexMethod( + self.c_vec, + self.a_matrix, + self.b_vec, + var_tag=self.var_tag, + func_tag=self.func_tag, + log_mode=self.log_mode + ) + start_table = np.copy(self._simplex.table.table) + self._simplex.solve() + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print("\nSolution of the integer problem of linear programming:") + print(np.around(self._simplex.get_solution(), 3)) + print("The value of the function:") + print(np.around(self._simplex.get_func_value(), 3), "\n") + + return self._simplex.get_solution(), self._simplex.get_func_value() + + def _check_solution(self) -> bool: + """ + Checks the solution for integers. + + :return: Is the solution an integer number? + :rtype: bool + """ + solution = self._simplex.get_solution(self._num_of_vars) + for value in solution: + if round(value, 5) % 1 != 0: + return True + return False + + def get_solution(self) -> np.ndarray: + """ + Return solution of problem of linear programming. + + :return: Solution of problem of linear programming. + :rtype: np.ndarray + """ + return self._simplex.get_solution() + + def get_func_value(self) -> np.float64: + """ + Return the value of the function. + + :return: The value of the function. + :rtype: np.float64 + """ + return self._simplex.get_func_value() diff --git a/src/lippy/simplex/__init__.py b/src/lippy/simplex/__init__.py new file mode 100644 index 0000000..5395ade --- /dev/null +++ b/src/lippy/simplex/__init__.py @@ -0,0 +1 @@ +from .simplex_method import SimplexMethod diff --git a/lippy/_simplex_table.py b/src/lippy/simplex/_simplex_table.py similarity index 69% rename from lippy/_simplex_table.py rename to src/lippy/simplex/_simplex_table.py index ac0e547..afbb52d 100755 --- a/lippy/_simplex_table.py +++ b/src/lippy/simplex/_simplex_table.py @@ -1,228 +1,261 @@ -import random -from typing import List - -import numpy as np -import prettytable - - -random.seed(43) - - -class _SimplexTable: - """ - A table that provides a convenient interface for the operation of the Simplex method. - """ - DECIMALS_FOR_AROUND = 3 - - def __init__(self, func_vec: np.ndarray, conditions_matrix: np.ndarray, - constraints_vec: np.ndarray, var_tag: str, func_tag: str): - """ - Initialization of an object of the "Simplex table" class. - - :param func_vec: Coefficients of the equation. - :param conditions_matrix: The left part of the restriction system. - :param constraints_vec: The right part of the restriction system. - :param var_tag: The name of the variables, default is "x". - :param func_tag: The name of the function, default is "F". - """ - self.table: np.ndarray = NotImplemented - - self.var_tag = var_tag - self.func_tag = func_tag - - self.column_tags: List[str] = NotImplemented - self.row_tags: List[str] = NotImplemented - - self.column_indices: List[int] = NotImplemented - self.row_indices: List[int] = NotImplemented - - self._has_optimal_solution = True - self._was_solved = False - - self._set_table(func_vec, conditions_matrix, constraints_vec) - - def _set_table(self, c_vec: np.ndarray, a_matrix: np.ndarray, b_vec: np.ndarray) -> None: - """ - Filling the class object with data. - - :param c_vec: Coefficients of the equation. - :param a_matrix: The left part of the restriction system. - :param b_vec: The right part of the restriction system. - :return: None. - """ - # 1. Check shapes of data. - self._check_input_data(c_vec, a_matrix, b_vec) - - # 2. Set simplex-table. - self.table = np.zeros([size + 1 for size in a_matrix.shape], dtype=np.float64) - self.table[:-1, 1:] = a_matrix - self.table[:-1, 0] = b_vec - self.table[-1, 1:] = c_vec - - # 3. Set tags for print-ability. - self.column_indices = [i for i in range(self.table.shape[1])] - self.column_tags = [f"{self.var_tag}{i}" for i in self.column_indices] - self.column_tags[0] = "S0" - - self.row_indices = [i + self.table.shape[1] for i in range(self.table.shape[0])] - self.row_tags = [f"{self.var_tag}{i}" for i in self.row_indices] - self.row_tags[-1] = self.func_tag - - @staticmethod - def _check_input_data(c_vec: np.ndarray, a_matrix: np.ndarray, b_vec: np.ndarray) -> None: - """ - Checking the dimensions of the input data for errors. - - :param c_vec: Coefficients of the equation. - :param a_matrix: The left part of the restriction system. - :param b_vec: The right part of the restriction system. - :return: None. - """ - if len(b_vec.shape) > 1: - raise ValueError("The shape of constraints-vector must be of the form (m,).") - if len(c_vec.shape) > 1: - raise ValueError("The shape of function-vector must be of the form (n,).") - if b_vec.shape[0] != a_matrix.shape[0]: - raise ValueError("The shape of constraints-vector must be of the form (m,); " - "the shape of conditions-matrix must be of the form (m, n).") - if c_vec.shape[0] != a_matrix.shape[1]: - raise ValueError("The shape of function-vector must be of the form (n,); " - "the shape of conditions-matrix must be of the form (m, n).") - - def get_index_of_pivot_column(self) -> int: - """ - Searches for a pivot column in the simplex table. - - :return: Index of a pivot column. - """ - index_of_pivot_column = NotImplemented - - # 1. Поиск индекса разрешающего столбца в столбце пересечений S0 со строками с переменными. - column_s0 = self.table[:-1, 0] - for row_index, item_s0 in enumerate(column_s0): - if item_s0 < 0: - row_without_s0 = self.table[row_index, 1:] - if row_without_s0.min() >= 0: - # System hasn't optimal solution. - self._has_optimal_solution = False - return -1 - if item_s0 == column_s0.min(): - index_of_pivot_column = row_without_s0.argmin() + 1 - - if index_of_pivot_column is not NotImplemented: - return index_of_pivot_column - - # 2. Поиск индекса разрешающего столбца в строке пересечений F со столбцами с переменными. - row_func = self.table[-1, 1:] - if row_func.max() > 0: - # find index of minimal positive value in row_func. - index_of_min: np.ndarray = np.where(row_func == [min(row_func[row_func > 0])])[0] - return int(index_of_min[0]) + 1 - - # Optimal solution was found. - self._was_solved = True - return -1 - - def get_index_of_pivot_row(self, i_pc: int) -> int: - """ - Searches for a pivot row in the simplex table. - - :param i_pc: Index of a pivot column. - :return: Index of a pivot row. - """ - targets_of_rows = np.zeros((self.table.shape[0] - 1,), dtype=np.float64) - - for index, row in enumerate(self.table[:-1]): - if row[i_pc] and row[0] / row[i_pc] > 0: - targets_of_rows[index] = row[0] / row[i_pc] - else: - targets_of_rows[index] = np.inf - - indices_of_mins = np.where(targets_of_rows == np.amin(targets_of_rows))[0] - return random.choice(indices_of_mins) - - def recalculate(self, i_pr: int, i_pc: int) -> None: - """ - Performs one iteration of the recalculation of the simplex table for a pivot element. - - :param i_pr: Index of a pivot row. - :param i_pc: Index of a pivot column. - :return: None. - """ - self.column_tags[i_pc], self.row_tags[i_pr] = self.row_tags[i_pr], self.column_tags[i_pc] - self.column_indices[i_pc], self.row_indices[i_pr] = self.row_indices[i_pr], self.column_indices[i_pc] - - central_item = self.table[i_pr, i_pc] - new_table = np.copy(self.table) - for i_r, column in enumerate(self.table): - for i_c, item in enumerate(column): - if i_r == i_pr and i_c == i_pc: - new_table[i_r, i_c] = 1 / central_item - elif i_r == i_pr: - new_table[i_r, i_c] = item / central_item - elif i_c == i_pc: - new_table[i_r, i_c] = -item / central_item - else: - new_table[i_r, i_c] = item - self.table[i_pr, i_c] * self.table[i_r, i_pc] / central_item - - self.table = new_table - - def __str__(self) -> str: - """ - Returns a beautiful representation of a simplex table for printing. - - :return: Simplex table for print. - """ - table_for_print = prettytable.PrettyTable(field_names=["", *self.column_tags]) - - row_tags = np.array(self.row_tags).reshape((len(self.row_tags), 1)) - data = np.around(self.table, decimals=_SimplexTable.DECIMALS_FOR_AROUND) - data = np.concatenate((row_tags, data), axis=1) - table_for_print.add_rows(data) - - return table_for_print.__str__() - - def __repr__(self) -> str: - """ - Returns representation of a simplex table for printing. - - :return: Simplex table as string. - """ - return self.__str__() - - def has_optimal_solution(self) -> bool: - """ - Answers the question: "Has the optimal solution been found?" - - :return: True or False. - """ - return self._has_optimal_solution - - def was_solved(self) -> bool: - """ - Answers the question: "Has the linear programming problem been solved?" - - :return: True or False. - """ - return self._was_solved - - def get_solution(self, num_of_vars: int) -> np.ndarray: - """ - Return solution of problem of linear programming. - - :return: Solution of problem of linear programming - """ - solution = np.zeros(num_of_vars, dtype=np.float64) - for var_index in range(1, num_of_vars + 1): - if var_index in self.row_indices: - solution[var_index - 1] = self.table[self.row_indices.index(var_index), 0] - - return solution - - def get_func_value(self) -> np.float64: - """ - Return the value of the function. - - :return: The value of the function. - """ - return -self.table[-1, 0] +import random +from typing import List + +import numpy as np +import prettytable + +from ..exceptions import ( + TABLE_CONST_VEC_ERROR, + TABLE_FUNC_VEC_ERROR, + TABLE_CONST_MATRIX_ERROR, + TABLE_FUNC_MATRIX_ERROR +) + + +random.seed(43) + + +class _SimplexTable: + """ + A table that provides a convenient interface + for the operation of the Simplex method. + """ + DECIMALS_FOR_AROUND = 3 + + def __init__( + self, + func_vec: np.ndarray, + conditions_matrix: np.ndarray, + constraints_vec: np.ndarray, + var_tag: str = 'x', + func_tag: str = 'F' + ): + """ + Initialization of an object of the "Simplex table" class. + + :param np.ndarray func_vec: Coefficients of the equation. + :param np.ndarray conditions_matrix: The left part of the restriction system. + :param np.ndarray constraints_vec: The right part of the restriction system. + :param str var_tag: The name of the variables (default: 'x'). + :param str func_tag: The name of the function (default: 'F'). + """ + self.table: np.ndarray = NotImplemented + + self.var_tag = var_tag + self.func_tag = func_tag + + self.column_tags: List[str] = NotImplemented + self.row_tags: List[str] = NotImplemented + + self.column_indices: List[int] = NotImplemented + self.row_indices: List[int] = NotImplemented + + self._has_optimal_solution = True + self._was_solved = False + + self._set_table(func_vec, conditions_matrix, constraints_vec) + + def _set_table( + self, + c_vec: np.ndarray, + a_matrix: np.ndarray, + b_vec: np.ndarray + ) -> None: + """ + Filling the class object with data. + + :param np.ndarray c_vec: Coefficients of the equation. + :param np.ndarray a_matrix: The left part of the restriction system. + :param np.ndarray b_vec: The right part of the restriction system. + :return: None + """ + # 1. Check shapes of data. + self._check_input_data(c_vec, a_matrix, b_vec) + + # 2. Set simplex-table. + self.table = np.zeros([size + 1 for size in a_matrix.shape], dtype=np.float64) + self.table[:-1, 1:] = a_matrix + self.table[:-1, 0] = b_vec + self.table[-1, 1:] = c_vec + + # 3. Set tags for print-ability. + self.column_indices = [i for i in range(self.table.shape[1])] + self.column_tags = [f"{self.var_tag}{i}" for i in self.column_indices] + self.column_tags[0] = "S0" + + self.row_indices = [i + self.table.shape[1] for i in range(self.table.shape[0])] + self.row_tags = [f"{self.var_tag}{i}" for i in self.row_indices] + self.row_tags[-1] = self.func_tag + + @staticmethod + def _check_input_data( + c_vec: np.ndarray, + a_matrix: np.ndarray, + b_vec: np.ndarray + ) -> None: + """ + Checking the dimensions of the input data for errors. + + :param np.ndarray c_vec: Coefficients of the equation. + :param np.ndarray a_matrix: The left part of the restriction system. + :param np.ndarray b_vec: The right part of the restriction system. + :return: None + """ + if len(b_vec.shape) > 1: + raise TABLE_CONST_VEC_ERROR + if len(c_vec.shape) > 1: + raise TABLE_FUNC_VEC_ERROR + if b_vec.shape[0] != a_matrix.shape[0]: + raise TABLE_CONST_MATRIX_ERROR + if c_vec.shape[0] != a_matrix.shape[1]: + raise TABLE_FUNC_MATRIX_ERROR + + def get_index_of_pivot_column(self) -> int: + """ + Searches for a pivot column in the simplex table. + + :return: Index of a pivot column. + :rtype: int + """ + index_of_pivot_column = NotImplemented + + # 1. Поиск индекса разрешающего столбца в столбце + # пересечений S0 со строками с переменными. + column_s0 = self.table[:-1, 0] + for row_index, item_s0 in enumerate(column_s0): + if item_s0 < 0: + row_without_s0 = self.table[row_index, 1:] + if row_without_s0.min() >= 0: + # 1.1. Оптимальное решение не найдено. + self._has_optimal_solution = False + return -1 + if item_s0 == column_s0.min(): + index_of_pivot_column = row_without_s0.argmin() + 1 + + if index_of_pivot_column is not NotImplemented: + return index_of_pivot_column + + # 2. Поиск индекса разрешающего столбца в строке + # пересечений F со столбцами с переменными. + row_func = self.table[-1, 1:] + if row_func.max() > 0: + # 2.1. Поиск индекса минимального положительного значения в `row_func`. + index_of_min: np.ndarray = np.where(row_func == [min(row_func[row_func > 0])])[0] + return int(index_of_min[0]) + 1 + + # 3. Оптимальное решение найдено. + self._was_solved = True + return -1 + + def get_index_of_pivot_row(self, i_pc: int) -> int: + """ + Searches for a pivot row in the simplex table. + + :param int i_pc: Index of a pivot column. + :return: Index of a pivot row. + :rtype: int + """ + targets_of_rows = np.zeros((self.table.shape[0] - 1,), dtype=np.float64) + + for index, row in enumerate(self.table[:-1]): + if row[i_pc] and row[0] / row[i_pc] > 0: + targets_of_rows[index] = row[0] / row[i_pc] + else: + targets_of_rows[index] = np.inf + + indices_of_mins = np.where(targets_of_rows == np.amin(targets_of_rows))[0] + return random.choice(indices_of_mins) + + def recalculate(self, i_pr: int, i_pc: int) -> None: + """ + Performs one iteration of the recalculation of the simplex table for a pivot element. + + :param int i_pr: Index of a pivot row. + :param int i_pc: Index of a pivot column. + :return: None + """ + self.column_tags[i_pc], self.row_tags[i_pr] = self.row_tags[i_pr], self.column_tags[i_pc] + self.column_indices[i_pc], self.row_indices[i_pr] = self.row_indices[i_pr], self.column_indices[i_pc] + + central_item = self.table[i_pr, i_pc] + new_table = np.copy(self.table) + for i_r, column in enumerate(self.table): + for i_c, item in enumerate(column): + if i_r == i_pr and i_c == i_pc: + new_table[i_r, i_c] = 1 / central_item + elif i_r == i_pr: + new_table[i_r, i_c] = item / central_item + elif i_c == i_pc: + new_table[i_r, i_c] = -item / central_item + else: + new_table[i_r, i_c] = item - self.table[i_pr, i_c] * self.table[i_r, i_pc] / central_item + + self.table = new_table + + def __str__(self) -> str: + """ + Returns a beautiful representation of a simplex table for printing. + + :return: Simplex table for print. + :rtype: str + """ + table_for_print = prettytable.PrettyTable(field_names=["", *self.column_tags]) + + row_tags = np.array(self.row_tags).reshape((len(self.row_tags), 1)) + data = np.around(self.table, decimals=_SimplexTable.DECIMALS_FOR_AROUND) + data = np.concatenate((row_tags, data), axis=1) + table_for_print.add_rows(data) + + return table_for_print.__str__() + + def __repr__(self) -> str: + """ + Returns representation of a simplex table for printing. + + :return: Simplex table as string. + :rtype: str + """ + return self.__str__() + + def has_optimal_solution(self) -> bool: + """ + Answers the question: "Has the optimal solution been found?" + + :return: Optimal solution status. + :rtype: bool + """ + return self._has_optimal_solution + + def was_solved(self) -> bool: + """ + Answers the question: "Has the linear programming problem been solved?" + + :return: Solving status. + :rtype: bool + """ + return self._was_solved + + def get_solution(self, num_of_vars: int) -> np.ndarray: + """ + Return solution of problem of linear programming. + + :param int num_of_vars: Number of variables. + :return: Solution of problem of linear programming. + :rtype: np.ndarray + """ + solution = np.zeros(num_of_vars, dtype=np.float64) + + for var_index in range(1, num_of_vars + 1): + if var_index in self.row_indices: + solution[var_index - 1] = self.table[self.row_indices.index(var_index), 0] + + return solution + + def get_func_value(self) -> np.float64: + """ + Return the value of the function. + + :return: The value of the function. + :rtype: np.float64 + """ + return -self.table[-1, 0] diff --git a/lippy/simplex_method.py b/src/lippy/simplex/simplex_method.py similarity index 64% rename from lippy/simplex_method.py rename to src/lippy/simplex/simplex_method.py index ad4d715..464c899 100755 --- a/lippy/simplex_method.py +++ b/src/lippy/simplex/simplex_method.py @@ -1,143 +1,164 @@ -import numpy as np -from typing import List - -from ._simplex_table import _SimplexTable -from ._log_modes import FULL_LOG, MEDIUM_LOG, LOG_OFF - - -class SimplexMethod: - """ - In mathematical optimization, Dantzig's simplex algorithm - (or simplex method) is a popular algorithm for linear programming. - """ - def __init__(self, func_vec: List[int or float] or np.ndarray, - conditions_matrix: List[List[int or float]] or np.ndarray, - constraints_vec: List[int or float] or np.ndarray, - var_tag: str = "x", func_tag: str = "F", log_mode: int = LOG_OFF): - """ - Initialization of an object of the SimplexMethod-class. - - :param func_vec: Coefficients of the equation. - :param conditions_matrix: The left part of the restriction system. - :param constraints_vec: The right part of the restriction system. - :param var_tag: The name of the variables, default is "x". - :param func_tag: The name of the function, default is "F". - :param log_mode: So much information about the solution to write to the console. - """ - self.c_vec = np.array(func_vec, np.float64) - self.a_matrix = np.array(conditions_matrix, np.float64) - self.b_vec = np.array(constraints_vec, np.float64) - - self.log_mode = log_mode - - self.table = _SimplexTable(self.c_vec, self.a_matrix, self.b_vec, var_tag, func_tag) - - def solve(self) -> (np.ndarray, np.float64): - """ - Solve the problem of linear programming by the simplex method. - - :return: The solution and the value of the function. - """ - if not self.table.has_optimal_solution(): - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print("Linear program has not optimal solution.") - return - if self.table.was_solved(): - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print("Linear program was solved.") - return - - if self.log_mode == FULL_LOG: - print(f"Input data:\nc = {self.c_vec}\nA = {self.a_matrix}\nb = {self.b_vec}\n") - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print("Start table:") - print(self.table, "\n") - while self.one_iteration(): - if self.log_mode == FULL_LOG: - print(self.table, "\n") - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print("\nSolution of the problem of linear programming:") - print(np.around(self.get_solution(), 3)) - print("The value of the function:") - print(np.around(self.get_func_value(), 3), "\n") - - return self.get_solution(), self.get_func_value() - - def one_iteration(self) -> bool: - """ - Performs one iteration of the simplex method. - - :return: If the problem of linear programming was solved - False; else - True. - """ - index_of_pivot_column = self.table.get_index_of_pivot_column() - if index_of_pivot_column == -1: - return False - - index_of_pivot_row = self.table.get_index_of_pivot_row(index_of_pivot_column) - if index_of_pivot_row == -1: - return False - - if self.log_mode in [FULL_LOG, MEDIUM_LOG]: - print(f"The pivot column: {self.table.column_tags[index_of_pivot_column]}; " - f"the pivot row: {self.table.row_tags[index_of_pivot_row]}:") - - self.table.recalculate(index_of_pivot_row, index_of_pivot_column) - return True - - def get_solution(self, _num_of_vars: int = None) -> np.ndarray: - """ - Return solution of problem of linear programming. - - :param _num_of_vars: The number of variables in the equation. - :return: Solution of problem of linear programming - """ - if _num_of_vars is None: - _num_of_vars = self.c_vec.shape[0] - return self.table.get_solution(_num_of_vars) - - def get_func_value(self) -> np.float64: - """ - Return the value of the function. - - :return: The value of the function. - """ - return self.table.get_func_value() - - def print_solution_check(self, _num_of_vars: int = None) -> None: - """ - Arithmetically checks the solution that was obtained by the simplex method. - - :param _num_of_vars: The number of variables in the equation. - :return: None. - """ - if _num_of_vars is None: - _num_of_vars = self.c_vec.shape[0] - solution = self.get_solution(_num_of_vars) - - # 1. Checking the equation. - around_solution = np.around(solution, 3) - var_tags = [f"{self.table.var_tag}{i + 1}" for i in range(_num_of_vars)] - - equation_1 = " + ".join([f"{c}*{v}" for c, v in zip(self.c_vec, var_tags)]) - equation_2 = " + ".join([f"{c}*{v}" for c, v in zip(self.c_vec, around_solution)]) - func_value = round(sum([c * v for c, v in zip(self.c_vec, around_solution)]), 3) - - print("Solution check:\n1. Function:") - print(f" • {self.table.func_tag} = {equation_1} = {equation_2} = {func_value}") - - # 2. Checking the restriction system. - print("2. Restriction system:") - - b_test = np.sum(self.a_matrix * solution, axis=1) - vec_of_comparisons = (b_test <= self.b_vec) - - for a_row_i, b_i, answer in zip(self.a_matrix, self.b_vec, vec_of_comparisons): - equation_i = list() - for x_i, a_i in zip(around_solution, a_row_i): - if x_i != 0 and a_i != 0: - equation_i.append(f"{a_i}*{x_i}") - - print(" • " + " + ".join(equation_i) + f" <= {b_i}, <-- {bool(answer)}") - +import numpy as np + +from ._simplex_table import _SimplexTable +from ..enums import LogMode +from ..types import Vector, Matrix + + +class SimplexMethod: + """ + In mathematical optimization, Dantzig's simplex algorithm + (or simplex method) is a popular algorithm for linear programming. + """ + def __init__( + self, + func_vec: Vector, + conditions_matrix: Matrix, + constraints_vec: Vector, + var_tag: str = 'x', + func_tag: str = 'F', + log_mode: int = LogMode.LOG_OFF + ): + """ + Initialization of an object of the SimplexMethod-class. + + :param Vector func_vec: Coefficients of the equation. + :param Matrix conditions_matrix: The left part of the restriction system. + :param Vector constraints_vec: The right part of the restriction system. + :param str var_tag: The name of the variables (default: 'x'). + :param str func_tag: The name of the function (default: 'F'). + :param int log_mode: So much information about the solution to write + to the console (default: LogMode.LOG_OFF). + """ + self.c_vec = np.array(func_vec, np.float64) + self.a_matrix = np.array(conditions_matrix, np.float64) + self.b_vec = np.array(constraints_vec, np.float64) + + self.log_mode = log_mode + + self.table = _SimplexTable( + self.c_vec, + self.a_matrix, + self.b_vec, + var_tag, + func_tag + ) + + def solve(self) -> (np.ndarray, np.float64): + """ + Solve the problem of linear programming by the simplex method. + + :return: The solution and the value of the function. + :rtype: (np.ndarray, np.float64) + """ + if not self.table.has_optimal_solution(): + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print("Linear program has not optimal solution.") + return + if self.table.was_solved(): + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print("Linear program was solved.") + return + + if self.log_mode is LogMode.FULL_LOG: + print(f"Input data:\nc = {self.c_vec}\nA = {self.a_matrix}\nb = {self.b_vec}\n") + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print("Start table:") + print(self.table, "\n") + while self.one_iteration(): + if self.log_mode == LogMode.FULL_LOG: + print(self.table, "\n") + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print("\nSolution of the problem of linear programming:") + print(np.around(self.get_solution(), 3)) + print("The value of the function:") + print(np.around(self.get_func_value(), 3), "\n") + + return self.get_solution(), self.get_func_value() + + def one_iteration(self) -> bool: + """ + Performs one iteration of the simplex method. + + :return: Solving status. + :rtype: bool + """ + index_of_pivot_column = self.table.get_index_of_pivot_column() + if index_of_pivot_column == -1: + return False + + index_of_pivot_row = self.table.get_index_of_pivot_row(index_of_pivot_column) + if index_of_pivot_row == -1: + return False + + if self.log_mode in (LogMode.MEDIUM_LOG, LogMode.FULL_LOG): + print( + f"The pivot column: {self.table.column_tags[index_of_pivot_column]}; " + f"the pivot row: {self.table.row_tags[index_of_pivot_row]}:" + ) + + self.table.recalculate( + index_of_pivot_row, + index_of_pivot_column + ) + return True + + def get_solution(self, _num_of_vars: int = None) -> np.ndarray: + """ + Return solution of problem of linear programming. + + :param int _num_of_vars: The number of variables in the equation. + :return: Solution of problem of linear programming. + :rtype: np.ndarray + """ + if _num_of_vars is None: + _num_of_vars = self.c_vec.shape[0] + return self.table.get_solution(_num_of_vars) + + def get_func_value(self) -> np.float64: + """ + Return the value of the function. + + :return: The value of the function. + :rtype: np.float64 + """ + return self.table.get_func_value() + + def print_solution_check(self, _num_of_vars: int = None) -> None: + """ + Arithmetically checks the solution that was obtained by the simplex method. + + :param int _num_of_vars: The number of variables in the equation. + :return: None + """ + if _num_of_vars is None: + _num_of_vars = self.c_vec.shape[0] + solution = self.get_solution(_num_of_vars) + + # 1. Checking the equation. + around_solution = np.around(solution, 3) + var_tags = [f"{self.table.var_tag}{i + 1}" for i in range(_num_of_vars)] + + equation_1 = " + ".join([f"{c}*{v}" for c, v in zip(self.c_vec, var_tags)]) + equation_2 = " + ".join([f"{c}*{v}" for c, v in zip(self.c_vec, around_solution)]) + func_value = round(sum([c * v for c, v in zip(self.c_vec, around_solution)]), 3) + + print("Solution check:\n1. Function:") + print(f" • {self.table.func_tag} = {equation_1} = {equation_2} = {func_value}") + + # 2. Checking the restriction system. + print("2. Restriction system:") + + b_test = np.sum(self.a_matrix * solution, axis=1) + vec_of_comparisons = (b_test <= self.b_vec) + + for a_row_i, b_i, answer in zip(self.a_matrix, self.b_vec, vec_of_comparisons): + equation_i = list() + for x_i, a_i in zip(around_solution, a_row_i): + if x_i != 0 and a_i != 0: + equation_i.append(f"{a_i}*{x_i}") + + print(" • " + " + ".join(equation_i) + f" <= {b_i}, <-- {bool(answer)}") + diff --git a/src/lippy/types.py b/src/lippy/types.py new file mode 100644 index 0000000..759a8e5 --- /dev/null +++ b/src/lippy/types.py @@ -0,0 +1,11 @@ +from typing import ( + List, + NewType, + Union +) + +import numpy as np + + +Vector = NewType('Matrix', Union[List[int or float], np.ndarray]) +Matrix = NewType('Matrix', Union[List[List[int or float]], np.ndarray])