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,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
+ [![Test and publish package](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml/badge.svg)](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml)
22
+
23
+ ![Algorithm diagram](./assets/AlgorithmDiagram.drawio.svg)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ gsimplex = gsimplex.main:__main
3
+ gsimplex-demo = gsimplex.demo:demo
4
+ gsimplex-download-netlib = gsimplex.benchmarks.netlib:main
5
+ gsimplex-download-plato = gsimplex.benchmarks.plato:main
6
+ gsimplex-emps = gsimplex.benchmarks.netlib_emps:__main
@@ -0,0 +1 @@
1
+ gsimplex