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.
@@ -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')