gsimplex 0.0.2__tar.gz → 0.0.3__tar.gz

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.
Files changed (54) hide show
  1. gsimplex-0.0.3/LICENSE +21 -0
  2. gsimplex-0.0.3/PKG-INFO +121 -0
  3. gsimplex-0.0.3/README.md +101 -0
  4. {gsimplex-0.0.2 → gsimplex-0.0.3}/pyproject.toml +4 -6
  5. gsimplex-0.0.3/src/gsimplex/__init__.py +3 -0
  6. gsimplex-0.0.2/src/gsimplex/main.py → gsimplex-0.0.3/src/gsimplex/__main__.py +17 -8
  7. gsimplex-0.0.3/src/gsimplex/basis.py +316 -0
  8. gsimplex-0.0.3/src/gsimplex/benchmarks/__main__.py +90 -0
  9. {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex/benchmarks/downloader.py +34 -0
  10. gsimplex-0.0.3/src/gsimplex/benchmarks/netlib.py +33 -0
  11. {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex/benchmarks/netlib_emps.py +2 -0
  12. gsimplex-0.0.3/src/gsimplex/benchmarks/plato.py +15 -0
  13. gsimplex-0.0.3/src/gsimplex/constants.py +16 -0
  14. gsimplex-0.0.3/src/gsimplex/exception.py +34 -0
  15. gsimplex-0.0.3/src/gsimplex/solvers/__init__.py +4 -0
  16. gsimplex-0.0.3/src/gsimplex/solvers/dual_simplex.py +330 -0
  17. gsimplex-0.0.3/src/gsimplex/solvers/gap_simplex.py +97 -0
  18. gsimplex-0.0.3/src/gsimplex/solvers/primal_simplex.py +384 -0
  19. gsimplex-0.0.3/src/gsimplex/solvers/simplex_interface.py +100 -0
  20. gsimplex-0.0.3/src/gsimplex/solvers/solver_interface.py +56 -0
  21. {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex/tools/extractor.py +16 -0
  22. gsimplex-0.0.3/src/gsimplex/tools/parser.py +76 -0
  23. gsimplex-0.0.3/src/gsimplex/vertex.py +312 -0
  24. gsimplex-0.0.3/src/gsimplex.egg-info/PKG-INFO +121 -0
  25. {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex.egg-info/SOURCES.txt +7 -7
  26. gsimplex-0.0.3/src/gsimplex.egg-info/entry_points.txt +4 -0
  27. gsimplex-0.0.3/tests/test_pulp.py +48 -0
  28. gsimplex-0.0.3/tests/test_simple.py +132 -0
  29. gsimplex-0.0.2/PKG-INFO +0 -25
  30. gsimplex-0.0.2/README.md +0 -7
  31. gsimplex-0.0.2/src/gsimplex/__init__.py +0 -1
  32. gsimplex-0.0.2/src/gsimplex/benchmarks/netlib.py +0 -78
  33. gsimplex-0.0.2/src/gsimplex/benchmarks/plato.py +0 -55
  34. gsimplex-0.0.2/src/gsimplex/demo.py +0 -54
  35. gsimplex-0.0.2/src/gsimplex/problem.py +0 -46
  36. gsimplex-0.0.2/src/gsimplex/solution.py +0 -25
  37. gsimplex-0.0.2/src/gsimplex/solvers/__init__.py +0 -1
  38. gsimplex-0.0.2/src/gsimplex/solvers/criss_cross.py +0 -13
  39. gsimplex-0.0.2/src/gsimplex/solvers/dual_simplex.py +0 -104
  40. gsimplex-0.0.2/src/gsimplex/solvers/gap_simplex.py +0 -74
  41. gsimplex-0.0.2/src/gsimplex/solvers/iterative_solver.py +0 -20
  42. gsimplex-0.0.2/src/gsimplex/solvers/primal_simplex.py +0 -136
  43. gsimplex-0.0.2/src/gsimplex/solvers/simplex_interface.py +0 -15
  44. gsimplex-0.0.2/src/gsimplex/solvers/solver_interface.py +0 -27
  45. gsimplex-0.0.2/src/gsimplex/tools/parser.py +0 -45
  46. gsimplex-0.0.2/src/gsimplex/vertex.py +0 -96
  47. gsimplex-0.0.2/src/gsimplex.egg-info/PKG-INFO +0 -25
  48. gsimplex-0.0.2/src/gsimplex.egg-info/entry_points.txt +0 -6
  49. gsimplex-0.0.2/tests/test_linear_programming.py +0 -46
  50. gsimplex-0.0.2/tests/test_simple.py +0 -88
  51. {gsimplex-0.0.2 → gsimplex-0.0.3}/setup.cfg +0 -0
  52. {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex.egg-info/dependency_links.txt +0 -0
  53. {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex.egg-info/requires.txt +0 -0
  54. {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex.egg-info/top_level.txt +0 -0
gsimplex-0.0.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riccardo Ciucci
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: gsimplex
3
+ Version: 0.0.3
4
+ Summary: Implementation of simplex algorithm controlled 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
+ License-File: LICENSE
14
+ Requires-Dist: numpy>=2.2.0
15
+ Requires-Dist: aiohttp>=3.9.0
16
+ Requires-Dist: pulp>=3.1.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Dynamic: license-file
20
+
21
+ # Gap controlled Simplex
22
+
23
+ [![Test and publish package](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml/badge.svg)](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml)
24
+ [![PyPI - Version](https://img.shields.io/pypi/v/gsimplex)](https://pypi.org/project/gsimplex/)
25
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/gsimplex)](https://pypi.org/project/gsimplex/)
26
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/gsimplex)](https://pypi.org/project/gsimplex/)
27
+ [![License](https://img.shields.io/pypi/l/gsimplex)](https://github.com/Richie314/GapControlledSimplex/blob/main/LICENSE)
28
+
29
+ `gsimplex` is a lightweight Python package that implements a simplex solver governed by the primal-dual gap.
30
+ It integrates directly with [pulp](https://coin-or.github.io/pulp/) and uses [numpy](https://numpy.org/) for its linear algebra routines.
31
+ The current release supports continuous linear programming problems; mixed-integer support may be added in a future version.
32
+
33
+ ## Features
34
+
35
+ - `pulp` compatible solver backend
36
+ - Gap-controlled primal/dual simplex algorithms
37
+ - Easy installation from PyPI or source
38
+ - Built-in benchmark downloader for Plato and Netlib test sets
39
+
40
+ ## Installation
41
+
42
+ Install from PyPI:
43
+
44
+ ```bash
45
+ python -m pip install gsimplex
46
+ ```
47
+
48
+ Install from source for local development:
49
+
50
+ ```bash
51
+ git clone https://github.com/Richie314/GapControlledSimplex.git
52
+ cd GapControlledSimplex
53
+ python -m pip install -e .
54
+ python -m pip install -e .[dev]
55
+ ```
56
+
57
+ Run the test suite with:
58
+
59
+ ```bash
60
+ python -m pytest
61
+ ```
62
+
63
+ ## Usage
64
+ ```python
65
+ from pulp import LpVariable, LpProblem, LpMaximize
66
+ from gsimplex.solvers import PrimalSimplex
67
+
68
+ x1 = LpVariable("x1", lowBound=0, upBound=1)
69
+ x2 = LpVariable("x2", lowBound=0, upBound=3)
70
+
71
+ problem = LpProblem("Problem", LpMaximize)
72
+ problem += x1 + x2
73
+ problem += x1 + x2 <= 2
74
+ problem += x1 <= 1
75
+ problem += x2 <= 3
76
+ problem += x1 >= 0
77
+ problem += x2 >= 0
78
+
79
+ solver = PrimalSimplex()
80
+ problem.solve(solver)
81
+
82
+ print("Optimal value:", problem.objective.value()) # 2.0
83
+ print("Solution:", [var.varValue for var in problem.variables()]) # [1.0, 1.0]
84
+ ```
85
+
86
+ ## Download benchmark problems
87
+
88
+ This package installs a command-line helper called `gsimplex-download-benchmarks`.
89
+ It downloads the Plato and Netlib benchmark sets into a local directory, so you can test the solver on real LP problems.
90
+
91
+ By default, benchmark files are saved under the `benchmark/` directory in the current working directory. Plato files are stored in `benchmark/plato/`, and Netlib files are stored in `benchmark/netlib/`.
92
+
93
+ ### Download all benchmarks
94
+
95
+ ```bash
96
+ gsimplex-download-benchmarks
97
+ ```
98
+
99
+ ### Download only one benchmark set
100
+
101
+ ```bash
102
+ gsimplex-download-benchmarks --plato True --netlib False
103
+ # or
104
+ gsimplex-download-benchmarks --plato False --netlib True
105
+ ```
106
+
107
+ > Note: the CLI uses boolean flags for `--plato` and `--netlib`, so set the one you want to disable to `False`.
108
+
109
+ ### Change the destination directory
110
+
111
+ ```bash
112
+ gsimplex-download-benchmarks --dir benchmark
113
+ ```
114
+
115
+ ### Quiet mode
116
+
117
+ ```bash
118
+ gsimplex-download-benchmarks --quiet
119
+ ```
120
+
121
+ If you installed the package editable with `pip install -e .`, the command will be available immediately.
@@ -0,0 +1,101 @@
1
+ # Gap controlled Simplex
2
+
3
+ [![Test and publish package](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml/badge.svg)](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml)
4
+ [![PyPI - Version](https://img.shields.io/pypi/v/gsimplex)](https://pypi.org/project/gsimplex/)
5
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/gsimplex)](https://pypi.org/project/gsimplex/)
6
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/gsimplex)](https://pypi.org/project/gsimplex/)
7
+ [![License](https://img.shields.io/pypi/l/gsimplex)](https://github.com/Richie314/GapControlledSimplex/blob/main/LICENSE)
8
+
9
+ `gsimplex` is a lightweight Python package that implements a simplex solver governed by the primal-dual gap.
10
+ It integrates directly with [pulp](https://coin-or.github.io/pulp/) and uses [numpy](https://numpy.org/) for its linear algebra routines.
11
+ The current release supports continuous linear programming problems; mixed-integer support may be added in a future version.
12
+
13
+ ## Features
14
+
15
+ - `pulp` compatible solver backend
16
+ - Gap-controlled primal/dual simplex algorithms
17
+ - Easy installation from PyPI or source
18
+ - Built-in benchmark downloader for Plato and Netlib test sets
19
+
20
+ ## Installation
21
+
22
+ Install from PyPI:
23
+
24
+ ```bash
25
+ python -m pip install gsimplex
26
+ ```
27
+
28
+ Install from source for local development:
29
+
30
+ ```bash
31
+ git clone https://github.com/Richie314/GapControlledSimplex.git
32
+ cd GapControlledSimplex
33
+ python -m pip install -e .
34
+ python -m pip install -e .[dev]
35
+ ```
36
+
37
+ Run the test suite with:
38
+
39
+ ```bash
40
+ python -m pytest
41
+ ```
42
+
43
+ ## Usage
44
+ ```python
45
+ from pulp import LpVariable, LpProblem, LpMaximize
46
+ from gsimplex.solvers import PrimalSimplex
47
+
48
+ x1 = LpVariable("x1", lowBound=0, upBound=1)
49
+ x2 = LpVariable("x2", lowBound=0, upBound=3)
50
+
51
+ problem = LpProblem("Problem", LpMaximize)
52
+ problem += x1 + x2
53
+ problem += x1 + x2 <= 2
54
+ problem += x1 <= 1
55
+ problem += x2 <= 3
56
+ problem += x1 >= 0
57
+ problem += x2 >= 0
58
+
59
+ solver = PrimalSimplex()
60
+ problem.solve(solver)
61
+
62
+ print("Optimal value:", problem.objective.value()) # 2.0
63
+ print("Solution:", [var.varValue for var in problem.variables()]) # [1.0, 1.0]
64
+ ```
65
+
66
+ ## Download benchmark problems
67
+
68
+ This package installs a command-line helper called `gsimplex-download-benchmarks`.
69
+ It downloads the Plato and Netlib benchmark sets into a local directory, so you can test the solver on real LP problems.
70
+
71
+ By default, benchmark files are saved under the `benchmark/` directory in the current working directory. Plato files are stored in `benchmark/plato/`, and Netlib files are stored in `benchmark/netlib/`.
72
+
73
+ ### Download all benchmarks
74
+
75
+ ```bash
76
+ gsimplex-download-benchmarks
77
+ ```
78
+
79
+ ### Download only one benchmark set
80
+
81
+ ```bash
82
+ gsimplex-download-benchmarks --plato True --netlib False
83
+ # or
84
+ gsimplex-download-benchmarks --plato False --netlib True
85
+ ```
86
+
87
+ > Note: the CLI uses boolean flags for `--plato` and `--netlib`, so set the one you want to disable to `False`.
88
+
89
+ ### Change the destination directory
90
+
91
+ ```bash
92
+ gsimplex-download-benchmarks --dir benchmark
93
+ ```
94
+
95
+ ### Quiet mode
96
+
97
+ ```bash
98
+ gsimplex-download-benchmarks --quiet
99
+ ```
100
+
101
+ If you installed the package editable with `pip install -e .`, the command will be available immediately.
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gsimplex"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  authors = [
9
9
  { name="Riccardo Ciucci", email="riccardo@ciucci.dev" },
10
10
  ]
11
- description = "Implementation of simplex algorithm contrelled by the primal-dual gap"
11
+ description = "Implementation of simplex algorithm controlled by the primal-dual gap"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.12"
14
14
  classifiers = [
@@ -24,11 +24,9 @@ dependencies = [
24
24
  ]
25
25
 
26
26
  [project.scripts]
27
- gsimplex = "gsimplex.main:__main"
28
- gsimplex-demo = "gsimplex.demo:demo"
29
- gsimplex-download-netlib = "gsimplex.benchmarks.netlib:main"
27
+ gsimplex = "gsimplex.__main__:main"
30
28
  gsimplex-emps = "gsimplex.benchmarks.netlib_emps:__main"
31
- gsimplex-download-plato = "gsimplex.benchmarks.plato:main"
29
+ gsimplex-download-benchmarks = "gsimplex.benchmarks.__main__:main"
32
30
 
33
31
  [project.urls]
34
32
  Homepage = "https://github.com/Richie314/GapControlledSimplex"
@@ -0,0 +1,3 @@
1
+ from gsimplex.exception import GsimplexException
2
+ from gsimplex.basis import Basis
3
+ from gsimplex.vertex import Vertex
@@ -1,16 +1,25 @@
1
+ #!/usr/bin/env python3
2
+
1
3
  import argparse
2
4
  import sys
3
5
 
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
6
+ from gsimplex.solvers import (
7
+ ISolver,
8
+ PrimalSimplex,
9
+ DualSimplex,
10
+ GapDoubleSimplex,
11
+ )
9
12
  from gsimplex.tools.parser import ProblemParser
10
13
 
11
- def __main():
14
+ def main():
15
+ """
16
+ Command-line entry point for the gsimplex solver.
17
+
18
+ :return: Exit code for the program.
19
+ :rtype: int
20
+ """
12
21
  solvers = {
13
- 'gsimplex' : GapSimplex,
22
+ 'gsimplex' : GapDoubleSimplex,
14
23
  'psimplex': PrimalSimplex,
15
24
  'dsimplex': DualSimplex,
16
25
  }
@@ -34,4 +43,4 @@ def __main():
34
43
  return 0
35
44
 
36
45
  if __name__ == "__main__":
37
- sys.exit(__main())
46
+ sys.exit(main())
@@ -0,0 +1,316 @@
1
+ from pulp import LpProblem, LpConstraint, LpVariable
2
+ from pulp.constants import LpConstraintEQ, LpConstraintLE, LpConstraintGE
3
+ from typing import List, Tuple, Optional, Union
4
+ import numpy as np
5
+
6
+ class ConstraintSet(List[LpConstraint]):
7
+ """
8
+ A set of linear constraints for a linear programming problem.
9
+ """
10
+
11
+ def __init__(self, *constraints: LpConstraint):
12
+ super().__init__(constraints)
13
+
14
+ def _compute_system(self, problem: LpProblem) -> Tuple[np.ndarray, np.ndarray]:
15
+ """
16
+ Compute the system of equations defined by the constraints in this set.
17
+
18
+ :param problem: The linear programming problem that defines the variables.
19
+ :type problem: LpProblem
20
+ :return: A tuple containing the constraint coefficient matrix and right-hand side vector.
21
+ :rtype: Tuple[np.ndarray, np.ndarray]
22
+ """
23
+
24
+ a_B = np.array([ConstraintSet.constraint_to_row(constraint, problem) for constraint in self])
25
+ b_B = np.array([ConstraintSet.constraint_to_linear_term(constraint) for constraint in self])
26
+ return a_B, b_B
27
+
28
+ def _compute_primal_point(self, problem: LpProblem) -> np.ndarray:
29
+ """
30
+ Compute the primal point *x* corresponding to this basis by solving the basis system (A_B x = b_B).
31
+
32
+ :param problem: The linear programming problem used to determine the active variables.
33
+ :type problem: LpProblem
34
+ :return: The primal solution vector associated with the current basis.
35
+ :rtype: np.ndarray
36
+ """
37
+
38
+ n = problem.numVariables()
39
+ assert len(self) >= n, "Not enough constraints to form a square matrix"
40
+
41
+ a_B, b_B = self._compute_system(problem)
42
+ return np.linalg.solve(a_B, b_B)
43
+
44
+ def _compute_dual_point(self, problem: LpProblem) -> np.ndarray:
45
+ """
46
+ Compute the dual point *y* corresponding to this basis by solving the dual system.
47
+
48
+ :param problem: The linear programming problem whose objective determines the dual solution.
49
+ :type problem: LpProblem
50
+ :return: The dual solution values for the basic constraints.
51
+ :rtype: np.ndarray
52
+ """
53
+
54
+ n = problem.numVariables()
55
+ assert len(self) == n, f"Constraint number mismatch: {len(self)} != {n}"
56
+
57
+ m = problem.numConstraints()
58
+ assert len(self) <= m, "Too many constraints in basis"
59
+
60
+ c = Basis.get_objective_function(problem)
61
+ assert len(c) == n
62
+
63
+ a_B, _ = self._compute_system(problem)
64
+ y_B = np.linalg.solve(a_B.T, c)
65
+
66
+ return y_B
67
+
68
+ @staticmethod
69
+ def __get_constraint_sense(constraint: LpConstraint,
70
+ convert_eq_to: int = LpConstraintLE,
71
+ ) -> int:
72
+ """
73
+ Extracts the sense from a `LpConstraint`.
74
+
75
+ :param constraint: The constraint to extract the sense from.
76
+ :type constraint: LpConstraint
77
+ :param convert_eq_to: How to treat equality constraints when converting them.
78
+ :type convert_eq_to: int
79
+ :return: Either `pulp.LpConstraintGE` or `pulp.LpConstraintLE`.
80
+ :rtype: int
81
+ """
82
+
83
+ sense = constraint.sense
84
+ if sense == LpConstraintEQ:
85
+ sense = convert_eq_to
86
+
87
+ if sense != LpConstraintLE and sense != LpConstraintGE:
88
+ raise ValueError(f"Unsupported constraint sense: {constraint.sense}")
89
+
90
+ return sense
91
+
92
+ @staticmethod
93
+ def constraint_to_row(constraint: LpConstraint,
94
+ problem: LpProblem,
95
+ convert_eq_to: int = LpConstraintLE,
96
+ ) -> np.ndarray:
97
+ """
98
+ Convert a constraint to a numpy array of coefficients corresponding to the given variables.
99
+
100
+ :param constraint: The constraint to convert.
101
+ :type constraint: LpConstraint
102
+ :param problem: The linear programming problem containing the variables.
103
+ :type problem: LpProblem
104
+ :param convert_eq_to: How to treat equality constraints when converting them.
105
+ :type convert_eq_to: int
106
+ :return: A numpy vector of coefficients corresponding to problem variables.
107
+ :rtype: np.ndarray
108
+ """
109
+
110
+ sense = ConstraintSet.__get_constraint_sense(constraint, convert_eq_to)
111
+ return -sense * np.array([constraint.get(var, 0) for var in problem.variables()])
112
+
113
+ @staticmethod
114
+ def constraint_to_linear_term(constraint: LpConstraint,
115
+ convert_eq_to: int = LpConstraintLE,
116
+ ) -> float:
117
+ """
118
+ Extract the linear term from a constraint in the form Ax <= b.
119
+
120
+ :param constraint: The constraint to convert.
121
+ :type constraint: LpConstraint
122
+ :param convert_eq_to: How to treat equality constraints when converting them.
123
+ :type convert_eq_to: int
124
+ :return: The right-hand side constant term for the converted constraint.
125
+ :rtype: float
126
+ """
127
+
128
+ """
129
+ Pulp memorizes data in the form Ax + constant <=> 0
130
+ Hence b is
131
+ * -constant if <=
132
+ * constant if >=
133
+ * any of the above if == (treated as convert_eq_to says)
134
+ """
135
+ sense = ConstraintSet.__get_constraint_sense(constraint, convert_eq_to)
136
+
137
+ """
138
+ if sense == LpConstraintLE:
139
+ return -constraint.constant
140
+ else:
141
+ return constraint.constant
142
+ """
143
+ return sense * constraint.constant
144
+
145
+ def has_named_constraints(self, names: List[str]) -> bool:
146
+ """
147
+ Check whether this vertex contains any of the requested constraint names.
148
+
149
+ :param names: A list of constraint names to search for.
150
+ :type names: List[str]
151
+ :return: True if the vertex includes at least one named constraint.
152
+ :rtype: bool
153
+ """
154
+ for c in self:
155
+ if c.name is not None and c.name in names:
156
+ return True
157
+
158
+ return False
159
+
160
+ @staticmethod
161
+ def get_objective_function(problem: LpProblem) -> np.ndarray:
162
+ """
163
+ Extract the objective function coefficients from the problem.
164
+
165
+ :param problem: The linear programming problem containing the objective.
166
+ :type problem: LpProblem
167
+ :return: The objective coefficients vector aligned with problem variables.
168
+ :rtype: np.ndarray
169
+ """
170
+
171
+ assert problem.objective, "Problem must have an objective function"
172
+ return np.array([problem.objective.get(var, 0) for var in problem.variables()])
173
+
174
+
175
+ class Basis(ConstraintSet):
176
+ """
177
+ A basis for a linear programming problem, represented as a set of constraints.
178
+ """
179
+
180
+ def __init__(self, problem: LpProblem, *constraints: LpConstraint):
181
+ """
182
+ Initialize a basis with the specified constraints for a linear problem.
183
+
184
+ :param problem: The linear programming problem for this basis.
185
+ :type problem: LpProblem
186
+ :param constraints: The active basis constraints.
187
+ :type constraints: LpConstraint
188
+ """
189
+
190
+ super().__init__(*constraints)
191
+
192
+ assert problem.objective, "Problem must have an objective function"
193
+
194
+ self.n = problem.numVariables()
195
+ assert self.n > 0, "Problem must have at least one variable"
196
+ assert len(self) == self.n, f"Basis must have same number of constraints as problem dimension: {len(self)} ≠ {self.n}"
197
+
198
+ self.problem = problem
199
+
200
+ self.__x: Optional[np.ndarray] = None
201
+ self.__y: Optional[np.ndarray] = None
202
+
203
+ def _set_primal_vars(self, x: Union[np.ndarray, List[float]]) -> np.ndarray:
204
+ """
205
+ Store and assign the primal solution values to problem variables.
206
+
207
+ :param x: The primal solution values for all problem variables.
208
+ :type x: Union[np.ndarray, List[float]]
209
+ :return: The stored primal solution as a NumPy array.
210
+ :rtype: np.ndarray
211
+ """
212
+
213
+ if not isinstance(x, np.ndarray):
214
+ x = np.array(x)
215
+
216
+ assert len(x) == self.problem.numVariables()
217
+
218
+ self.__x = x
219
+ for i, var in enumerate(self.variables):
220
+ var.varValue = self.__x[i]
221
+
222
+ return self.__x
223
+
224
+ @property
225
+ def x(self) -> np.ndarray:
226
+ """
227
+ Get the primal point corresponding to this basis.
228
+
229
+ :return: The primal solution vector for the basis.
230
+ :rtype: np.ndarray
231
+ """
232
+
233
+ if self.__x is not None:
234
+ return self.__x
235
+
236
+ x = self._compute_primal_point(self.problem)
237
+ return self._set_primal_vars(x)
238
+
239
+
240
+ def _set_dual_vars(self, y_B: Union[np.ndarray, List[float]]) -> np.ndarray:
241
+ """
242
+ Store the dual solution values and map them into the full constraint vector.
243
+
244
+ :param y_B: The dual values for the basic constraints.
245
+ :type y_B: Union[np.ndarray, List[float]]
246
+ :return: The full dual solution vector mapped to all problem constraints.
247
+ :rtype: np.ndarray
248
+ """
249
+
250
+ assert len(y_B) == self.problem.numVariables()
251
+
252
+ self.__y = np.zeros(self.problem.numConstraints())
253
+ for i, y_i in enumerate(y_B):
254
+ constraint = self[i]
255
+ self.__y[self.global_index(constraint)] = y_i
256
+
257
+ return self.__y
258
+
259
+ @property
260
+ def y(self) -> np.ndarray:
261
+ """
262
+ Get the dual point corresponding to this basis.
263
+
264
+ :return: The dual solution vector for all problem constraints.
265
+ :rtype: np.ndarray
266
+ """
267
+
268
+ if self.__y is not None:
269
+ return self.__y
270
+
271
+ y_B = self._compute_dual_point(self.problem)
272
+ return self._set_dual_vars(y_B)
273
+
274
+ @property
275
+ def variables(self) -> List[LpVariable]:
276
+ return self.problem.variables()
277
+
278
+ @property
279
+ def indices(self) -> List[int]:
280
+ return [self.global_index(c) for c in self]
281
+
282
+ @property
283
+ def non_basis_indices(self) -> List[int]:
284
+ return [self.global_index(c) for c in self.non_basis]
285
+
286
+ @property
287
+ def all_constraints(self) -> List[LpConstraint]:
288
+ return list(self.problem.constraints.values())
289
+
290
+ def global_index(self, constraint: LpConstraint) -> int:
291
+ """
292
+ Return the global index of a constraint in the original problem ordering.
293
+
294
+ :param constraint: The constraint whose index is requested.
295
+ :type constraint: LpConstraint
296
+ :return: The zero-based index of the constraint in the problem.
297
+ :rtype: int
298
+ """
299
+
300
+ return self.all_constraints.index(constraint)
301
+
302
+ @property
303
+ def non_basis(self) -> ConstraintSet:
304
+ """
305
+ Return the set of constraints that are not in the basis.
306
+ """
307
+
308
+ return ConstraintSet(*[c for c in self.all_constraints if c not in self])
309
+
310
+
311
+ def __str__(self) -> str:
312
+ s = f'x = {self.x}\n'
313
+ s += f'y = {self.y}\n'
314
+ for c in self:
315
+ s += f'{c}\t→ {c.value():.4}\n'
316
+ return s