gsimplex 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gsimplex/__init__.py +1 -0
- gsimplex/benchmarks/downloader.py +76 -0
- gsimplex/benchmarks/netlib.py +78 -0
- gsimplex/benchmarks/netlib_emps.py +805 -0
- gsimplex/benchmarks/plato.py +55 -0
- gsimplex/demo.py +54 -0
- gsimplex/main.py +37 -0
- gsimplex/problem.py +46 -0
- gsimplex/solution.py +25 -0
- gsimplex/solvers/__init__.py +1 -0
- gsimplex/solvers/criss_cross.py +13 -0
- gsimplex/solvers/dual_simplex.py +104 -0
- gsimplex/solvers/gap_simplex.py +74 -0
- gsimplex/solvers/iterative_solver.py +20 -0
- gsimplex/solvers/primal_simplex.py +136 -0
- gsimplex/solvers/simplex_interface.py +15 -0
- gsimplex/solvers/solver_interface.py +27 -0
- gsimplex/tools/extractor.py +37 -0
- gsimplex/tools/parser.py +45 -0
- gsimplex/vertex.py +96 -0
- gsimplex-0.0.2.dist-info/METADATA +25 -0
- gsimplex-0.0.2.dist-info/RECORD +25 -0
- gsimplex-0.0.2.dist-info/WHEEL +5 -0
- gsimplex-0.0.2.dist-info/entry_points.txt +6 -0
- gsimplex-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from gsimplex.benchmarks.downloader import Downloader
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlatoDownloader(Downloader):
|
|
12
|
+
BASE_URL = "https://plato.asu.edu/ftp/lptestset/"
|
|
13
|
+
|
|
14
|
+
async def download_plato_benchmarks_async(self, problem_names: List[str]) -> Dict[str, str]:
|
|
15
|
+
files: List[Tuple[str, str, str, Optional[str]]] = [
|
|
16
|
+
(f"{self.BASE_URL}{name}.mps.bz2", name, f"plato/{name}.mps.bz2", None)
|
|
17
|
+
for name in problem_names
|
|
18
|
+
if name.strip()
|
|
19
|
+
]
|
|
20
|
+
return await self.download_many_async(files)
|
|
21
|
+
|
|
22
|
+
async def download_plato_benchmarks(dir: Optional[str] = None, quiet: bool = False) -> bool:
|
|
23
|
+
downloader = PlatoDownloader(benchmark_dir=dir, quiet=quiet)
|
|
24
|
+
|
|
25
|
+
# All Plato problems (flattened)
|
|
26
|
+
problem_names = [
|
|
27
|
+
"Dual2_5000", "L2CTA3D", "Primal2_1000", "a2864", "bharat",
|
|
28
|
+
"brazil3", "chromaticindex1024-7", "datt256_lp", "dlr1", "dlr2",
|
|
29
|
+
"ex10", "fhnw-binschedule1", "graph40-40", "irish-electricity",
|
|
30
|
+
"neos-3025225", "neos-5052403-cygnet", "neos-5251015",
|
|
31
|
+
"physiciansched3-3", "qap15", "rmine15", "s82", "s100", "s250r10",
|
|
32
|
+
"savsched1", "scpm1", "set-cover-model", "square41",
|
|
33
|
+
"supportcase10", "supportcase19", "thk_48", "thk_63",
|
|
34
|
+
"tpl-tub-ws1617", "woodlands09"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
if not quiet:
|
|
38
|
+
print(f"Downloading {len(problem_names)} Plato problems...")
|
|
39
|
+
results = await downloader.download_plato_benchmarks_async(problem_names)
|
|
40
|
+
if not quiet:
|
|
41
|
+
print(f"Downloaded {len(results)} problems successfully")
|
|
42
|
+
|
|
43
|
+
return len(results) == len(problem_names)
|
|
44
|
+
|
|
45
|
+
def main():
|
|
46
|
+
parser = argparse.ArgumentParser(description="Download Plato benchmarks")
|
|
47
|
+
parser.add_argument('--quiet', action='store_true', help='Run in quiet mode')
|
|
48
|
+
parser.add_argument('--dir', type=str, default=None, help='Directory to save benchmarks')
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
esit = asyncio.run(download_plato_benchmarks(quiet=args.quiet, dir=args.dir))
|
|
52
|
+
return 0 if esit else 1
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
sys.exit(main())
|
gsimplex/demo.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from gsimplex.problem import Problem
|
|
2
|
+
from gsimplex.solvers.solver_interface import ISolver
|
|
3
|
+
from gsimplex.solvers.primal_simplex import PrimalSimplex
|
|
4
|
+
from gsimplex.solvers.dual_simplex import DualSimplex
|
|
5
|
+
from gsimplex.solvers.gap_simplex import GapSimplex
|
|
6
|
+
|
|
7
|
+
def __solve_and_print(solver: ISolver, problem: Problem, B: list[int]|None = None):
|
|
8
|
+
solution = None
|
|
9
|
+
try:
|
|
10
|
+
solution = solver.maximize(problem, B)
|
|
11
|
+
except Exception as e:
|
|
12
|
+
print(str(e))
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
if solution is None:
|
|
16
|
+
print("Problem is unbounded or infeasible")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
print(f"x = [{'; '.join(map(str, solution.x))}]")
|
|
20
|
+
print(f"y = [{'; '.join(map(str, solution.y))}]")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
print(f"c^T * x = {solution.point.primal_value()}")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(str(e))
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
print(f"y^T * b = {solution.point.dual_value()}")
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(str(e))
|
|
31
|
+
|
|
32
|
+
def demo():
|
|
33
|
+
problem = Problem.from_ab_rows(
|
|
34
|
+
[ 4.0, 5.0, 2.0],
|
|
35
|
+
[ 0.0, 0.6, 0.8, 500.0],
|
|
36
|
+
[-1.0, 2.0, 0.0, 0.0],
|
|
37
|
+
[ 1.0, 0.0, -1.0, 0.0]
|
|
38
|
+
).enforce_positivity()
|
|
39
|
+
|
|
40
|
+
print("=== Primal Simplex ===")
|
|
41
|
+
__solve_and_print(PrimalSimplex(), problem, B=[0, 3, 4])
|
|
42
|
+
print()
|
|
43
|
+
print()
|
|
44
|
+
|
|
45
|
+
print("=== Dual Simplex ===")
|
|
46
|
+
__solve_and_print(DualSimplex(), problem)
|
|
47
|
+
print()
|
|
48
|
+
print()
|
|
49
|
+
|
|
50
|
+
print("=== Gap-Controlled Simplex ===")
|
|
51
|
+
__solve_and_print(GapSimplex(), problem, B=[0, 3, 4])
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
demo()
|
gsimplex/main.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from gsimplex.problem import Problem
|
|
5
|
+
from gsimplex.solvers.solver_interface import ISolver
|
|
6
|
+
from gsimplex.solvers.primal_simplex import PrimalSimplex
|
|
7
|
+
from gsimplex.solvers.dual_simplex import DualSimplex
|
|
8
|
+
from gsimplex.solvers.gap_simplex import GapSimplex
|
|
9
|
+
from gsimplex.tools.parser import ProblemParser
|
|
10
|
+
|
|
11
|
+
def __main():
|
|
12
|
+
solvers = {
|
|
13
|
+
'gsimplex' : GapSimplex,
|
|
14
|
+
'psimplex': PrimalSimplex,
|
|
15
|
+
'dsimplex': DualSimplex,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
parser = argparse.ArgumentParser(description="")
|
|
19
|
+
parser.add_argument('--quiet', action='store_true',
|
|
20
|
+
help='Run in quiet mode')
|
|
21
|
+
parser.add_argument('--problem', type=str, required=True,
|
|
22
|
+
help='Name of the problem to solve or path to it')
|
|
23
|
+
parser.add_argument('--solver', default='gsimplex', type=str, choices=solvers.keys(),
|
|
24
|
+
help='Algorithm to use to solve the problem')
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
problem = ProblemParser.load_mps_from_file(args.problem)
|
|
29
|
+
print(f"{problem=}")
|
|
30
|
+
|
|
31
|
+
solver: ISolver = solvers[args.solver]()
|
|
32
|
+
print(f"{solver=}")
|
|
33
|
+
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
sys.exit(__main())
|
gsimplex/problem.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
class Problem:
|
|
5
|
+
"""
|
|
6
|
+
Linear programming problem in the form max { c^T x } subject to Ax <= b, x >= 0
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self,
|
|
10
|
+
c: Union[List[float], np.ndarray],
|
|
11
|
+
A: Union[List[List[float]], np.ndarray],
|
|
12
|
+
b: Union[List[float], np.ndarray]
|
|
13
|
+
):
|
|
14
|
+
self.c = np.array(c, dtype=float)
|
|
15
|
+
self.A = np.array(A, dtype=float)
|
|
16
|
+
self.b = np.array(b, dtype=float)
|
|
17
|
+
|
|
18
|
+
if self.c.shape[0] != self.A.shape[1]:
|
|
19
|
+
raise ValueError("c must have same length as A columns")
|
|
20
|
+
|
|
21
|
+
if self.A.shape[0] != self.b.shape[0]:
|
|
22
|
+
raise ValueError("A rows must match b length")
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def dimension(self) -> int:
|
|
26
|
+
return self.A.shape[1]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def constraints(self) -> int:
|
|
30
|
+
return self.A.shape[0]
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_arrays(cls, c: List[float], A: List[List[float]], b: List[float]) -> 'Problem':
|
|
34
|
+
return cls(c, A, b)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_ab_rows(cls, c: List[float], *ab_rows: List[float]) -> 'Problem':
|
|
38
|
+
A = [row[:-1] for row in ab_rows]
|
|
39
|
+
b = [row[-1] for row in ab_rows]
|
|
40
|
+
return cls(c, A, b)
|
|
41
|
+
|
|
42
|
+
def enforce_positivity(self) -> 'Problem':
|
|
43
|
+
I = np.eye(self.dimension)
|
|
44
|
+
new_b = np.concatenate([self.b, np.zeros(self.dimension)])
|
|
45
|
+
new_A = np.vstack([self.A, -I])
|
|
46
|
+
return Problem(self.c, new_A, new_b)
|
gsimplex/solution.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from gsimplex.vertex import Vertex
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Solution:
|
|
7
|
+
point: Vertex
|
|
8
|
+
iteration_count: int
|
|
9
|
+
initial_iterations: int = 0
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def problem(self):
|
|
13
|
+
return self.point.problem
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def basis(self):
|
|
17
|
+
return self.point.basis
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def x(self):
|
|
21
|
+
return self.point.x
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def y(self):
|
|
25
|
+
return self.point.y
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Solvers package
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional, Tuple, List
|
|
2
|
+
|
|
3
|
+
from gsimplex.solvers.iterative_solver import IterativeSolver
|
|
4
|
+
from gsimplex.problem import Problem
|
|
5
|
+
from gsimplex.vertex import Vertex
|
|
6
|
+
from gsimplex.solution import Solution
|
|
7
|
+
|
|
8
|
+
class CrissCross(IterativeSolver):
|
|
9
|
+
def get_starting_point(self, problem: Problem, given_basis: Optional[List[int]] = None) -> Tuple[Optional[Vertex], int]:
|
|
10
|
+
raise NotImplementedError()
|
|
11
|
+
|
|
12
|
+
def maximize(self, problem: Problem, start_basis: Optional[List[int]] = None) -> Optional[Solution]:
|
|
13
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Optional, Tuple, List
|
|
3
|
+
|
|
4
|
+
from gsimplex.solvers.iterative_solver import IterativeSolver
|
|
5
|
+
from gsimplex.solvers.simplex_interface import ISimplex
|
|
6
|
+
from gsimplex.problem import Problem
|
|
7
|
+
from gsimplex.vertex import Vertex
|
|
8
|
+
from gsimplex.solution import Solution
|
|
9
|
+
|
|
10
|
+
class DualSimplex(IterativeSolver, ISimplex):
|
|
11
|
+
@staticmethod
|
|
12
|
+
def iteration(vertex: Vertex) -> Optional[Vertex]:
|
|
13
|
+
if not vertex.is_dual_feasible():
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
if vertex.is_primal_feasible():
|
|
17
|
+
return vertex # Optimal
|
|
18
|
+
|
|
19
|
+
# Entering index (Bland rule)
|
|
20
|
+
infeas = vertex.primal_infeasible_rows()
|
|
21
|
+
if not infeas:
|
|
22
|
+
return vertex
|
|
23
|
+
k, _ = infeas[0]
|
|
24
|
+
|
|
25
|
+
Ak = vertex.problem.A[k]
|
|
26
|
+
|
|
27
|
+
# Leaving index (Minimum ratio + Bland rule)
|
|
28
|
+
ratios = []
|
|
29
|
+
for i in vertex.basis:
|
|
30
|
+
idx = np.where(vertex.basis == i)[0][0]
|
|
31
|
+
den = Ak @ vertex.W[:, idx]
|
|
32
|
+
if den < -Vertex.ABSOLUTE_TOLERANCE:
|
|
33
|
+
ratios.append((i, -vertex.y[i] / den))
|
|
34
|
+
|
|
35
|
+
if not ratios:
|
|
36
|
+
return None # Unbounded
|
|
37
|
+
|
|
38
|
+
ratios.sort(key=lambda x: x[1])
|
|
39
|
+
h, _ = ratios[0]
|
|
40
|
+
|
|
41
|
+
new_basis = np.setdiff1d(vertex.basis, [h])
|
|
42
|
+
new_basis = np.append(new_basis, k)
|
|
43
|
+
|
|
44
|
+
return Vertex(vertex.problem, new_basis)
|
|
45
|
+
|
|
46
|
+
def maximize(self, problem: Problem, start_basis: Optional[List[int]] = None) -> Optional[Solution]:
|
|
47
|
+
current, initial_iterations = self.get_starting_point(problem, start_basis)
|
|
48
|
+
|
|
49
|
+
iterations = 0
|
|
50
|
+
while current is not None and self._check_iteration_count(iterations):
|
|
51
|
+
if current.is_optimal_point():
|
|
52
|
+
return Solution(
|
|
53
|
+
point=current,
|
|
54
|
+
iteration_count=iterations,
|
|
55
|
+
initial_iterations=initial_iterations
|
|
56
|
+
)
|
|
57
|
+
current = self.iteration(current)
|
|
58
|
+
iterations += 1
|
|
59
|
+
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def make_feasible(self, vertex: Vertex) -> Optional[Vertex]:
|
|
63
|
+
v = vertex
|
|
64
|
+
while not v.is_dual_feasible():
|
|
65
|
+
dual_infeas = v.dual_infeasible_values()
|
|
66
|
+
if not dual_infeas:
|
|
67
|
+
break
|
|
68
|
+
p, _ = dual_infeas[0]
|
|
69
|
+
|
|
70
|
+
d = -v.W @ v.problem.A[p]
|
|
71
|
+
|
|
72
|
+
# Entering index
|
|
73
|
+
min_pivot = float('inf')
|
|
74
|
+
q = -1
|
|
75
|
+
for i in v.non_basis:
|
|
76
|
+
pivot = v.problem.A[i] @ d
|
|
77
|
+
if pivot <= -Vertex.ABSOLUTE_TOLERANCE and abs(pivot) < min_pivot:
|
|
78
|
+
min_pivot = abs(pivot)
|
|
79
|
+
q = i
|
|
80
|
+
|
|
81
|
+
if q == -1:
|
|
82
|
+
raise ValueError("Unbounded problem")
|
|
83
|
+
|
|
84
|
+
new_basis = np.setdiff1d(v.basis, [p])
|
|
85
|
+
new_basis = np.append(new_basis, q)
|
|
86
|
+
v = Vertex(v.problem, new_basis)
|
|
87
|
+
|
|
88
|
+
return v
|
|
89
|
+
|
|
90
|
+
def get_feasible_vertex(self, problem: Problem) -> Optional[Tuple[Vertex, int]]:
|
|
91
|
+
initial_point = Vertex(problem, np.arange(problem.dimension))
|
|
92
|
+
dual_feasible = self.make_feasible(initial_point)
|
|
93
|
+
if dual_feasible is None:
|
|
94
|
+
return None
|
|
95
|
+
return dual_feasible, 0
|
|
96
|
+
|
|
97
|
+
def get_starting_point(self, problem: Problem, given_basis: Optional[List[int]] = None) -> Tuple[Optional[Vertex], int]:
|
|
98
|
+
if given_basis is not None:
|
|
99
|
+
return Vertex(problem, given_basis), 0
|
|
100
|
+
|
|
101
|
+
phase_one = self.get_feasible_vertex(problem)
|
|
102
|
+
if phase_one is None:
|
|
103
|
+
return None, 0
|
|
104
|
+
return phase_one
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Optional, Tuple, List
|
|
2
|
+
|
|
3
|
+
from gsimplex.solvers.iterative_solver import IterativeSolver
|
|
4
|
+
from gsimplex.solvers.simplex_interface import ISimplex
|
|
5
|
+
from gsimplex.solvers.primal_simplex import PrimalSimplex
|
|
6
|
+
from gsimplex.solvers.dual_simplex import DualSimplex
|
|
7
|
+
from gsimplex.problem import Problem
|
|
8
|
+
from gsimplex.vertex import Vertex
|
|
9
|
+
from gsimplex.solution import Solution
|
|
10
|
+
|
|
11
|
+
class GapSimplex(IterativeSolver, ISimplex):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self._primal_simplex = PrimalSimplex()
|
|
15
|
+
self._dual_simplex = DualSimplex()
|
|
16
|
+
|
|
17
|
+
def maximize(self, problem: Problem, start_basis: Optional[List[int]] = None) -> Optional[Solution]:
|
|
18
|
+
primal_vertex, initial_primal = self.get_starting_point(problem, start_basis)
|
|
19
|
+
dual_vertex, initial_dual = self._dual_simplex.get_starting_point(problem)
|
|
20
|
+
|
|
21
|
+
if (primal_vertex is None or not primal_vertex.is_primal_feasible() or
|
|
22
|
+
dual_vertex is None or not dual_vertex.is_dual_feasible()):
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
primal_iterations = 0
|
|
26
|
+
dual_iterations = 0
|
|
27
|
+
|
|
28
|
+
while (not primal_vertex.is_optimal_point() and self._check_iteration_count(primal_iterations) and
|
|
29
|
+
not dual_vertex.is_optimal_point() and self._check_iteration_count(dual_iterations)):
|
|
30
|
+
|
|
31
|
+
gap, rel_gap, dual_val, primal_val = Vertex.gap(dual_vertex, primal_vertex)
|
|
32
|
+
|
|
33
|
+
print(f"Gap: {gap} = {dual_val} - {primal_val}")
|
|
34
|
+
if rel_gap is not None:
|
|
35
|
+
print(f"Relative gap: {rel_gap}")
|
|
36
|
+
|
|
37
|
+
if primal_vertex.is_primal_degenerate():
|
|
38
|
+
new_primal = self._primal_simplex.make_feasible(dual_vertex)
|
|
39
|
+
if new_primal is not None and new_primal.primal_value() > primal_val:
|
|
40
|
+
primal_vertex = new_primal
|
|
41
|
+
|
|
42
|
+
if dual_vertex.is_dual_degenerate():
|
|
43
|
+
new_dual = self._dual_simplex.make_feasible(primal_vertex)
|
|
44
|
+
if new_dual is not None and new_dual.dual_value() < dual_val:
|
|
45
|
+
dual_vertex = new_dual
|
|
46
|
+
|
|
47
|
+
primal_vertex = PrimalSimplex.iteration(primal_vertex)
|
|
48
|
+
if primal_vertex is None or primal_vertex.is_optimal_point():
|
|
49
|
+
break
|
|
50
|
+
primal_iterations += 1
|
|
51
|
+
|
|
52
|
+
dual_vertex = DualSimplex.iteration(dual_vertex)
|
|
53
|
+
if dual_vertex is None or dual_vertex.is_optimal_point():
|
|
54
|
+
break
|
|
55
|
+
dual_iterations += 1
|
|
56
|
+
|
|
57
|
+
if primal_vertex is None or dual_vertex is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
optimal_point = primal_vertex if primal_vertex.is_optimal_point() else dual_vertex
|
|
61
|
+
return Solution(
|
|
62
|
+
point=optimal_point,
|
|
63
|
+
iteration_count=max(primal_iterations, dual_iterations),
|
|
64
|
+
initial_iterations=initial_primal + initial_dual
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def get_feasible_vertex(self, problem: Problem) -> Optional[Tuple[Vertex, int]]:
|
|
68
|
+
return self._primal_simplex.get_feasible_vertex(problem)
|
|
69
|
+
|
|
70
|
+
def get_starting_point(self, problem: Problem, given_basis: Optional[List[int]] = None) -> Tuple[Optional[Vertex], int]:
|
|
71
|
+
return self._primal_simplex.get_starting_point(problem, given_basis)
|
|
72
|
+
|
|
73
|
+
def make_feasible(self, vertex: Vertex) -> Optional[Vertex]:
|
|
74
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from gsimplex.solvers.solver_interface import ISolver
|
|
5
|
+
from gsimplex.problem import Problem
|
|
6
|
+
from gsimplex.vertex import Vertex
|
|
7
|
+
|
|
8
|
+
class IterativeSolver(ISolver, ABC):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.max_iterations: Optional[int] = None
|
|
11
|
+
|
|
12
|
+
def _check_iteration_count(self, iterations: int) -> bool:
|
|
13
|
+
return self.max_iterations is None or iterations <= self.max_iterations
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_starting_point(self,
|
|
17
|
+
problem: Problem,
|
|
18
|
+
given_basis: Optional[list[int]] = None
|
|
19
|
+
) -> Tuple[Optional[Vertex], int]:
|
|
20
|
+
pass
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Optional, Tuple, List
|
|
3
|
+
|
|
4
|
+
from gsimplex.solvers.iterative_solver import IterativeSolver
|
|
5
|
+
from gsimplex.solvers.simplex_interface import ISimplex
|
|
6
|
+
from gsimplex.problem import Problem
|
|
7
|
+
from gsimplex.vertex import Vertex
|
|
8
|
+
from gsimplex.solution import Solution
|
|
9
|
+
|
|
10
|
+
class PrimalSimplex(IterativeSolver, ISimplex):
|
|
11
|
+
@staticmethod
|
|
12
|
+
def iteration(vertex: Vertex) -> Optional[Vertex]:
|
|
13
|
+
if not vertex.is_primal_feasible():
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
# Leaving index (Bland rule)
|
|
17
|
+
dual_infeas = vertex.dual_infeasible_values()
|
|
18
|
+
if not dual_infeas:
|
|
19
|
+
return vertex # Optimal
|
|
20
|
+
|
|
21
|
+
h, _ = dual_infeas[0]
|
|
22
|
+
|
|
23
|
+
# Wh is the h-th column of -A_B^-1
|
|
24
|
+
h_idx = np.where(vertex.basis == h)[0][0]
|
|
25
|
+
Wh = vertex.W[:, h_idx]
|
|
26
|
+
|
|
27
|
+
# Entering index (Bland rule)
|
|
28
|
+
non_basis = vertex.non_basis
|
|
29
|
+
ratios = []
|
|
30
|
+
for i in non_basis:
|
|
31
|
+
den = vertex.problem.A[i] @ Wh
|
|
32
|
+
if den > Vertex.ABSOLUTE_TOLERANCE:
|
|
33
|
+
num = vertex.problem.b[i] - vertex.problem.A[i] @ vertex.x
|
|
34
|
+
ratios.append((i, num / den))
|
|
35
|
+
|
|
36
|
+
if not ratios:
|
|
37
|
+
return None # Unbounded
|
|
38
|
+
|
|
39
|
+
ratios.sort(key=lambda x: x[1])
|
|
40
|
+
k, _ = ratios[0]
|
|
41
|
+
|
|
42
|
+
new_basis = np.setdiff1d(vertex.basis, [h])
|
|
43
|
+
new_basis = np.append(new_basis, k)
|
|
44
|
+
|
|
45
|
+
return Vertex(vertex.problem, new_basis)
|
|
46
|
+
|
|
47
|
+
def get_starting_point(self, problem: Problem, given_basis: Optional[List[int]] = None) -> Tuple[Optional[Vertex], int]:
|
|
48
|
+
if given_basis is not None:
|
|
49
|
+
return Vertex(problem, given_basis), 0
|
|
50
|
+
|
|
51
|
+
feasible = self.get_feasible_vertex(problem)
|
|
52
|
+
if feasible is None:
|
|
53
|
+
return None, 0
|
|
54
|
+
return feasible
|
|
55
|
+
|
|
56
|
+
def maximize(self, problem: Problem, start_basis: Optional[List[int]] = None) -> Optional[Solution]:
|
|
57
|
+
current, initial_iterations = self.get_starting_point(problem, start_basis)
|
|
58
|
+
|
|
59
|
+
iterations = 0
|
|
60
|
+
while current is not None and self._check_iteration_count(iterations):
|
|
61
|
+
if current.is_optimal_point():
|
|
62
|
+
return Solution(
|
|
63
|
+
point=current,
|
|
64
|
+
iteration_count=iterations,
|
|
65
|
+
initial_iterations=initial_iterations
|
|
66
|
+
)
|
|
67
|
+
current = self.iteration(current)
|
|
68
|
+
iterations += 1
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def make_feasible(self, vertex: Vertex) -> Optional[Vertex]:
|
|
73
|
+
v = vertex
|
|
74
|
+
while not v.is_primal_feasible():
|
|
75
|
+
infeas = v.primal_infeasible_rows()
|
|
76
|
+
if not infeas:
|
|
77
|
+
break
|
|
78
|
+
k, _ = min(infeas, key=lambda x: x[1])
|
|
79
|
+
|
|
80
|
+
Ak = v.problem.A[k]
|
|
81
|
+
|
|
82
|
+
# Leaving index (Bland rule)
|
|
83
|
+
h = -1
|
|
84
|
+
for i in v.basis:
|
|
85
|
+
idx = np.where(v.basis == i)[0][0]
|
|
86
|
+
if Ak @ v.W[:, idx] < 0:
|
|
87
|
+
h = i
|
|
88
|
+
break
|
|
89
|
+
if h == -1:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
new_basis = np.setdiff1d(v.basis, [h])
|
|
93
|
+
new_basis = np.append(new_basis, k)
|
|
94
|
+
v = Vertex(v.problem, new_basis)
|
|
95
|
+
|
|
96
|
+
return v
|
|
97
|
+
|
|
98
|
+
def get_feasible_vertex(self, problem: Problem) -> Optional[Tuple[Vertex, int]]:
|
|
99
|
+
n = problem.dimension
|
|
100
|
+
m = problem.constraints
|
|
101
|
+
|
|
102
|
+
initial_basis = np.arange(n)
|
|
103
|
+
initial_vertex = Vertex(problem, initial_basis)
|
|
104
|
+
|
|
105
|
+
if initial_vertex.is_primal_feasible():
|
|
106
|
+
return initial_vertex, 0
|
|
107
|
+
|
|
108
|
+
# Auxiliary problem
|
|
109
|
+
rp = initial_vertex.primal_residuals()
|
|
110
|
+
V = initial_vertex.non_basis[rp[initial_vertex.non_basis] < 0]
|
|
111
|
+
|
|
112
|
+
# Number of auxiliary variables
|
|
113
|
+
k = len(V)
|
|
114
|
+
|
|
115
|
+
# Build matrix of the auxiliary problem
|
|
116
|
+
aux_A = np.zeros((m + k, n + k))
|
|
117
|
+
aux_A[:m, :n] = problem.A
|
|
118
|
+
aux_A[m:, n:] = -np.eye(k)
|
|
119
|
+
for idx, i in enumerate(V):
|
|
120
|
+
aux_A[i, n + idx] = -1
|
|
121
|
+
|
|
122
|
+
aux_b = np.concatenate([problem.b, np.zeros(k)])
|
|
123
|
+
aux_c = np.concatenate([np.zeros(n), -np.ones(k)])
|
|
124
|
+
|
|
125
|
+
aux_problem = Problem(aux_c, aux_A, aux_b)
|
|
126
|
+
|
|
127
|
+
aux_solution = self.maximize(aux_problem, initial_vertex.basis.tolist() + V.tolist())
|
|
128
|
+
if aux_solution is None or not aux_solution.point.is_optimal_point() or aux_solution.point.primal_value() < 0:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
new_basis = aux_solution.basis[:n]
|
|
132
|
+
new_vertex = Vertex(problem, new_basis)
|
|
133
|
+
if not new_vertex.is_primal_feasible():
|
|
134
|
+
raise ValueError("Auxiliary problem failed")
|
|
135
|
+
|
|
136
|
+
return new_vertex, aux_solution.iteration_count
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from gsimplex.solvers.solver_interface import ISolver
|
|
5
|
+
from gsimplex.problem import Problem
|
|
6
|
+
from gsimplex.vertex import Vertex
|
|
7
|
+
|
|
8
|
+
class ISimplex(ISolver, ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def get_feasible_vertex(self, problem: Problem) -> Optional[Tuple[Vertex, int]]:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def make_feasible(self, vertex: Vertex) -> Optional[Vertex]:
|
|
15
|
+
pass
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from gsimplex.problem import Problem
|
|
5
|
+
from gsimplex.vertex import Vertex
|
|
6
|
+
from gsimplex.solution import Solution
|
|
7
|
+
|
|
8
|
+
class ISolver(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def maximize(self, problem: Problem, start_basis: Optional[list[int]] = None) -> Optional[Solution]:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def minimize(self,
|
|
14
|
+
problem: Problem,
|
|
15
|
+
start_basis: Optional[list[int]] = None
|
|
16
|
+
) -> Optional[Solution]:
|
|
17
|
+
|
|
18
|
+
inverted_problem = Problem(-problem.c, problem.A, problem.b)
|
|
19
|
+
result = self.maximize(inverted_problem, start_basis)
|
|
20
|
+
if result is None:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
# Change back to original problem
|
|
24
|
+
return Solution(
|
|
25
|
+
point=Vertex(problem, result.basis),
|
|
26
|
+
iteration_count=result.iteration_count
|
|
27
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import bz2
|
|
2
|
+
import gzip
|
|
3
|
+
import zipfile
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import BinaryIO
|
|
7
|
+
|
|
8
|
+
class Extractor:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def is_compressed(filepath: str|Path) -> bool:
|
|
11
|
+
path = Path(filepath)
|
|
12
|
+
return path.suffix.lower() in ['.bz2', '.gz', '.zip']
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def extract_to_stream(filepath: str|Path) -> BinaryIO:
|
|
16
|
+
path = Path(filepath)
|
|
17
|
+
if not path.exists():
|
|
18
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
19
|
+
|
|
20
|
+
suffix = path.suffix.lower()
|
|
21
|
+
|
|
22
|
+
with open(filepath, 'rb') as f:
|
|
23
|
+
if suffix == '.bz2':
|
|
24
|
+
return BytesIO(bz2.decompress(f.read()))
|
|
25
|
+
elif suffix == '.gz':
|
|
26
|
+
return BytesIO(gzip.decompress(f.read()))
|
|
27
|
+
elif suffix == '.zip':
|
|
28
|
+
# For simplicity, assume single file in zip
|
|
29
|
+
with zipfile.ZipFile(f) as zf:
|
|
30
|
+
names = zf.namelist()
|
|
31
|
+
if names:
|
|
32
|
+
return BytesIO(zf.read(names[0]))
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError("Empty zip file")
|
|
35
|
+
|
|
36
|
+
# Not compressed, return file stream
|
|
37
|
+
return open(filepath, 'rb')
|