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.
- gsimplex-0.0.3/LICENSE +21 -0
- gsimplex-0.0.3/PKG-INFO +121 -0
- gsimplex-0.0.3/README.md +101 -0
- {gsimplex-0.0.2 → gsimplex-0.0.3}/pyproject.toml +4 -6
- gsimplex-0.0.3/src/gsimplex/__init__.py +3 -0
- gsimplex-0.0.2/src/gsimplex/main.py → gsimplex-0.0.3/src/gsimplex/__main__.py +17 -8
- gsimplex-0.0.3/src/gsimplex/basis.py +316 -0
- gsimplex-0.0.3/src/gsimplex/benchmarks/__main__.py +90 -0
- {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex/benchmarks/downloader.py +34 -0
- gsimplex-0.0.3/src/gsimplex/benchmarks/netlib.py +33 -0
- {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex/benchmarks/netlib_emps.py +2 -0
- gsimplex-0.0.3/src/gsimplex/benchmarks/plato.py +15 -0
- gsimplex-0.0.3/src/gsimplex/constants.py +16 -0
- gsimplex-0.0.3/src/gsimplex/exception.py +34 -0
- gsimplex-0.0.3/src/gsimplex/solvers/__init__.py +4 -0
- gsimplex-0.0.3/src/gsimplex/solvers/dual_simplex.py +330 -0
- gsimplex-0.0.3/src/gsimplex/solvers/gap_simplex.py +97 -0
- gsimplex-0.0.3/src/gsimplex/solvers/primal_simplex.py +384 -0
- gsimplex-0.0.3/src/gsimplex/solvers/simplex_interface.py +100 -0
- gsimplex-0.0.3/src/gsimplex/solvers/solver_interface.py +56 -0
- {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex/tools/extractor.py +16 -0
- gsimplex-0.0.3/src/gsimplex/tools/parser.py +76 -0
- gsimplex-0.0.3/src/gsimplex/vertex.py +312 -0
- gsimplex-0.0.3/src/gsimplex.egg-info/PKG-INFO +121 -0
- {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex.egg-info/SOURCES.txt +7 -7
- gsimplex-0.0.3/src/gsimplex.egg-info/entry_points.txt +4 -0
- gsimplex-0.0.3/tests/test_pulp.py +48 -0
- gsimplex-0.0.3/tests/test_simple.py +132 -0
- gsimplex-0.0.2/PKG-INFO +0 -25
- gsimplex-0.0.2/README.md +0 -7
- gsimplex-0.0.2/src/gsimplex/__init__.py +0 -1
- gsimplex-0.0.2/src/gsimplex/benchmarks/netlib.py +0 -78
- gsimplex-0.0.2/src/gsimplex/benchmarks/plato.py +0 -55
- gsimplex-0.0.2/src/gsimplex/demo.py +0 -54
- gsimplex-0.0.2/src/gsimplex/problem.py +0 -46
- gsimplex-0.0.2/src/gsimplex/solution.py +0 -25
- gsimplex-0.0.2/src/gsimplex/solvers/__init__.py +0 -1
- gsimplex-0.0.2/src/gsimplex/solvers/criss_cross.py +0 -13
- gsimplex-0.0.2/src/gsimplex/solvers/dual_simplex.py +0 -104
- gsimplex-0.0.2/src/gsimplex/solvers/gap_simplex.py +0 -74
- gsimplex-0.0.2/src/gsimplex/solvers/iterative_solver.py +0 -20
- gsimplex-0.0.2/src/gsimplex/solvers/primal_simplex.py +0 -136
- gsimplex-0.0.2/src/gsimplex/solvers/simplex_interface.py +0 -15
- gsimplex-0.0.2/src/gsimplex/solvers/solver_interface.py +0 -27
- gsimplex-0.0.2/src/gsimplex/tools/parser.py +0 -45
- gsimplex-0.0.2/src/gsimplex/vertex.py +0 -96
- gsimplex-0.0.2/src/gsimplex.egg-info/PKG-INFO +0 -25
- gsimplex-0.0.2/src/gsimplex.egg-info/entry_points.txt +0 -6
- gsimplex-0.0.2/tests/test_linear_programming.py +0 -46
- gsimplex-0.0.2/tests/test_simple.py +0 -88
- {gsimplex-0.0.2 → gsimplex-0.0.3}/setup.cfg +0 -0
- {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex.egg-info/dependency_links.txt +0 -0
- {gsimplex-0.0.2 → gsimplex-0.0.3}/src/gsimplex.egg-info/requires.txt +0 -0
- {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.
|
gsimplex-0.0.3/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml)
|
|
24
|
+
[](https://pypi.org/project/gsimplex/)
|
|
25
|
+
[](https://pypi.org/project/gsimplex/)
|
|
26
|
+
[](https://pypi.org/project/gsimplex/)
|
|
27
|
+
[](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.
|
gsimplex-0.0.3/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Gap controlled Simplex
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Richie314/GapControlledSimplex/actions/workflows/pypi.yml)
|
|
4
|
+
[](https://pypi.org/project/gsimplex/)
|
|
5
|
+
[](https://pypi.org/project/gsimplex/)
|
|
6
|
+
[](https://pypi.org/project/gsimplex/)
|
|
7
|
+
[](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.
|
|
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
|
|
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
|
|
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-
|
|
29
|
+
gsimplex-download-benchmarks = "gsimplex.benchmarks.__main__:main"
|
|
32
30
|
|
|
33
31
|
[project.urls]
|
|
34
32
|
Homepage = "https://github.com/Richie314/GapControlledSimplex"
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
2
4
|
import sys
|
|
3
5
|
|
|
4
|
-
from gsimplex.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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' :
|
|
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(
|
|
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
|