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
gsimplex/tools/parser.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pulp as pl
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import BinaryIO
|
|
5
|
+
|
|
6
|
+
from gsimplex.tools.extractor import Extractor
|
|
7
|
+
|
|
8
|
+
class ProblemParser:
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def __load_mps_file(file_path: str|Path) -> pl.LpProblem:
|
|
12
|
+
_, problem = pl.LpProblem.fromMPS(str(file_path))
|
|
13
|
+
return problem
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def load_mps_from_file(file_path: str|Path) -> pl.LpProblem:
|
|
17
|
+
|
|
18
|
+
# Raise exception if file does not exist
|
|
19
|
+
if not Path(file_path).exists():
|
|
20
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
21
|
+
|
|
22
|
+
# If it's not compressed, we can load it directly
|
|
23
|
+
if not Extractor.is_compressed(file_path):
|
|
24
|
+
return ProblemParser.__load_mps_file(file_path)
|
|
25
|
+
|
|
26
|
+
# File must be uncompressed first
|
|
27
|
+
with Extractor.extract_to_stream(file_path) as file_stream:
|
|
28
|
+
return ProblemParser.load_mps_from_stream(file_stream)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def load_mps_from_stream(file_stream: BinaryIO) -> pl.LpProblem:
|
|
32
|
+
|
|
33
|
+
# Save stream to a temporary file
|
|
34
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mps") as tmp_file:
|
|
35
|
+
tmp_file.write(file_stream.read())
|
|
36
|
+
tmp_file_path = tmp_file.name
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Load the problem from the temporary file
|
|
40
|
+
problem = ProblemParser.__load_mps_file(tmp_file_path)
|
|
41
|
+
finally:
|
|
42
|
+
Path(tmp_file_path).unlink() # Clean up the temporary file
|
|
43
|
+
|
|
44
|
+
return problem
|
|
45
|
+
|
gsimplex/vertex.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Iterable, Tuple, Optional
|
|
3
|
+
from gsimplex.problem import Problem
|
|
4
|
+
|
|
5
|
+
class Vertex:
|
|
6
|
+
"""
|
|
7
|
+
Vertex (basis solution) for a primal or dual linear programming polyhedron.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ABSOLUTE_TOLERANCE = 1e-10
|
|
12
|
+
RELATIVE_TOLERANCE = 1e-9
|
|
13
|
+
|
|
14
|
+
def __init__(self, problem: Problem, basis: Iterable[int]):
|
|
15
|
+
self.problem = problem
|
|
16
|
+
self.basis = np.array(sorted(set(basis)), dtype=int)
|
|
17
|
+
|
|
18
|
+
if len(self.basis) != problem.dimension:
|
|
19
|
+
raise ValueError("Basis must have same size as problem dimension")
|
|
20
|
+
|
|
21
|
+
A_B = problem.A[self.basis, :]
|
|
22
|
+
b_B = problem.b[self.basis]
|
|
23
|
+
|
|
24
|
+
self.W = -np.linalg.inv(A_B)
|
|
25
|
+
|
|
26
|
+
# A_B x = b_B
|
|
27
|
+
self.x = np.linalg.solve(A_B, b_B)
|
|
28
|
+
|
|
29
|
+
# y_B = c^T A_B^-1
|
|
30
|
+
self.y_B = np.linalg.solve(A_B.T, problem.c)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def non_basis(self) -> np.ndarray:
|
|
34
|
+
return np.setdiff1d(np.arange(self.problem.constraints), self.basis)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def y(self) -> np.ndarray:
|
|
38
|
+
y_full = np.zeros(self.problem.constraints)
|
|
39
|
+
y_full[self.basis] = self.y_B
|
|
40
|
+
return y_full
|
|
41
|
+
|
|
42
|
+
def primal_residuals(self) -> np.ndarray:
|
|
43
|
+
r = self.problem.b - self.problem.A @ self.x
|
|
44
|
+
return np.minimum(r, 0)
|
|
45
|
+
|
|
46
|
+
def is_primal_feasible(self, abs_tol: float = ABSOLUTE_TOLERANCE, rel_tol: float = RELATIVE_TOLERANCE) -> bool:
|
|
47
|
+
return len(self.primal_infeasible_rows(abs_tol, rel_tol)) == 0
|
|
48
|
+
|
|
49
|
+
def primal_infeasible_rows(self, abs_tol: float = ABSOLUTE_TOLERANCE, rel_tol: float = RELATIVE_TOLERANCE) -> List[Tuple[int, float]]:
|
|
50
|
+
scale = np.max(np.abs(self.problem.b))
|
|
51
|
+
slack = self.primal_residuals()
|
|
52
|
+
return [(i, slack[i]) for i in range(self.problem.constraints)
|
|
53
|
+
if abs(slack[i]) > abs_tol + rel_tol * scale]
|
|
54
|
+
|
|
55
|
+
def is_primal_degenerate(self, abs_tol: float = ABSOLUTE_TOLERANCE, rel_tol: float = RELATIVE_TOLERANCE) -> bool:
|
|
56
|
+
scale = np.max(np.abs(self.problem.b))
|
|
57
|
+
residuals = self.primal_residuals()
|
|
58
|
+
return np.sum(np.abs(residuals) <= abs_tol + rel_tol * scale) > self.problem.dimension
|
|
59
|
+
|
|
60
|
+
def dual_infeasible_values(self, abs_tol: float = ABSOLUTE_TOLERANCE) -> List[Tuple[int, float]]:
|
|
61
|
+
return [(self.basis[i], abs(self.y_B[i])) for i in range(len(self.y_B)) if self.y_B[i] < -abs_tol]
|
|
62
|
+
|
|
63
|
+
def is_dual_feasible(self, abs_tol: float = ABSOLUTE_TOLERANCE) -> bool:
|
|
64
|
+
return len(self.dual_infeasible_values(abs_tol)) == 0
|
|
65
|
+
|
|
66
|
+
def is_dual_degenerate(self, abs_tol: float = ABSOLUTE_TOLERANCE) -> np.bool:
|
|
67
|
+
return np.any(np.abs(self.y_B) < abs_tol)
|
|
68
|
+
|
|
69
|
+
def is_optimal_point(self, abs_tol: float = ABSOLUTE_TOLERANCE, rel_tol: float = RELATIVE_TOLERANCE) -> bool:
|
|
70
|
+
return self.is_primal_feasible(abs_tol, rel_tol) and self.is_dual_feasible(abs_tol)
|
|
71
|
+
|
|
72
|
+
def primal_value(self, abs_tol: float = ABSOLUTE_TOLERANCE, rel_tol: float = RELATIVE_TOLERANCE) -> float:
|
|
73
|
+
if not self.is_primal_feasible(abs_tol, rel_tol):
|
|
74
|
+
raise ValueError("Point is not primal feasible")
|
|
75
|
+
|
|
76
|
+
return self.problem.c @ self.x
|
|
77
|
+
|
|
78
|
+
def dual_value(self, abs_tol: float = ABSOLUTE_TOLERANCE) -> float:
|
|
79
|
+
if not self.is_dual_feasible(abs_tol):
|
|
80
|
+
raise ValueError("Point is not dual feasible")
|
|
81
|
+
|
|
82
|
+
return self.y_B @ self.problem.b[self.basis]
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def gap(dual_vertex: 'Vertex', primal_vertex: 'Vertex',
|
|
86
|
+
abs_tol: float = ABSOLUTE_TOLERANCE, rel_tol: float = RELATIVE_TOLERANCE) -> Tuple[float, Optional[float], float, float]:
|
|
87
|
+
if dual_vertex.problem is not primal_vertex.problem:
|
|
88
|
+
raise ValueError("Vertices from different problems")
|
|
89
|
+
|
|
90
|
+
dual_val = dual_vertex.dual_value(abs_tol)
|
|
91
|
+
primal_val = primal_vertex.primal_value(abs_tol, rel_tol)
|
|
92
|
+
|
|
93
|
+
gap = dual_val - primal_val
|
|
94
|
+
rel_gap = gap / primal_val if abs(primal_val) > abs_tol else None
|
|
95
|
+
|
|
96
|
+
return gap, rel_gap, dual_val, primal_val
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gsimplex
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Implementation of simplex algorithm contrelled by the primal-dual gap
|
|
5
|
+
Author-email: Riccardo Ciucci <riccardo@ciucci.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Richie314/GapControlledSimplex
|
|
8
|
+
Project-URL: Issues, https://github.com/Richie314/GapControlledSimplex/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy>=2.2.0
|
|
14
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
15
|
+
Requires-Dist: pulp>=3.1.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# Gap controlled Simplex
|
|
20
|
+
|
|
21
|
+
[](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml)
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Download test problems
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
gsimplex/__init__.py,sha256=7ClinUCqGdgz_LWsrTf6gKvVXP37wps3KD9s2cdMbeM,17
|
|
2
|
+
gsimplex/demo.py,sha256=B5aPvlT4g25M34MoWKuWqNpq6vKRwuCFesUHPkQLtEo,1490
|
|
3
|
+
gsimplex/main.py,sha256=RmiR41qfkCwXg2UeWWI9fzcg5UOiGHOBJgwqHu9EZpY,1182
|
|
4
|
+
gsimplex/problem.py,sha256=YLizccYcQ4nYnCjF1hMOD00FLOB0S9z-ZrVuk8lDtJU,1467
|
|
5
|
+
gsimplex/solution.py,sha256=EntWtN2Ayc-jYT6NiSldR6IBFtWa2syN63QpJfEejM8,432
|
|
6
|
+
gsimplex/vertex.py,sha256=cPVVicCePY89uqDnle-oMi4hsK1nvzGvTsbBLVUCZ9c,3930
|
|
7
|
+
gsimplex/benchmarks/downloader.py,sha256=k6aPa65dOAy5p5cnY7CAvvZLzqzh4mPLdQliHIEDG7I,2891
|
|
8
|
+
gsimplex/benchmarks/netlib.py,sha256=zXMpdvbjFeGM8cB60-_GtsROAu6eg8-QV6OHq4RSmyc,3390
|
|
9
|
+
gsimplex/benchmarks/netlib_emps.py,sha256=Y6fRz-TaVtwBnqCqv3FAsBlbq0tasq8DtVJwfpGg_ZM,26260
|
|
10
|
+
gsimplex/benchmarks/plato.py,sha256=SlOzAWZeOnEcUs-MqMxfN5VKfhUjoX0ScY6gDH6AXiQ,2136
|
|
11
|
+
gsimplex/solvers/__init__.py,sha256=3gI174qJwg9MpsT2DBKLcwNgRrA49dqE1EIp-9Dc0aA,17
|
|
12
|
+
gsimplex/solvers/criss_cross.py,sha256=vjo_IT68u6VWjeLCMISSnXvRZe-aIn2h_zt1oPHiiqQ,553
|
|
13
|
+
gsimplex/solvers/dual_simplex.py,sha256=TPWEZzU-BzjMbsLQp1DErY_S9Pj-KhetFxK2qGnrawk,3463
|
|
14
|
+
gsimplex/solvers/gap_simplex.py,sha256=mfWngK06Aq2gUUAPx7sv8Wehl7IZgLrzRNaKvxlZJ0g,3249
|
|
15
|
+
gsimplex/solvers/iterative_solver.py,sha256=304Mdxi5ES0rG_dV15-wjSlWbGueCrFHlzFZWOmuUgw,696
|
|
16
|
+
gsimplex/solvers/primal_simplex.py,sha256=ESKv4Ba_cPx300FZsObT38H7lskB8DVkD5jChlDzZw8,4557
|
|
17
|
+
gsimplex/solvers/simplex_interface.py,sha256=Q_PBJzDDkzjoMdVoCrrFf1d_R6IwKpGWYRsR-wI4yeQ,445
|
|
18
|
+
gsimplex/solvers/solver_interface.py,sha256=rO4kTPH8b5Psz-4HfzwFCs-aDW5t_DzYS40rFRYNoVc,871
|
|
19
|
+
gsimplex/tools/extractor.py,sha256=B3WE1I4GHsLEHr2EUkh-09_FnCj3KYx-Pcw_99oyOSE,1225
|
|
20
|
+
gsimplex/tools/parser.py,sha256=jnOJqw4tWIVHoL5kJMDFrnvb_hC8lAF33Ecj1QpzYfo,1495
|
|
21
|
+
gsimplex-0.0.2.dist-info/METADATA,sha256=E6SIGye9IEENLX3E9vh9zwd-lb23oQP1NPC612WcItM,970
|
|
22
|
+
gsimplex-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
gsimplex-0.0.2.dist-info/entry_points.txt,sha256=5dA7QYXYzzydAqZgJ927MPaLTpdmzk2dGxBBlx4wXGc,256
|
|
24
|
+
gsimplex-0.0.2.dist-info/top_level.txt,sha256=SEkvaxvhQLqFFKojZfyGLFHzBgHZxtFOeWwAuOiB3g4,9
|
|
25
|
+
gsimplex-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gsimplex
|