linprog 1.0.0__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.
- linprog-1.0.0/PKG-INFO +61 -0
- linprog-1.0.0/README.md +45 -0
- linprog-1.0.0/linprog.egg-info/PKG-INFO +61 -0
- linprog-1.0.0/linprog.egg-info/SOURCES.txt +8 -0
- linprog-1.0.0/linprog.egg-info/dependency_links.txt +1 -0
- linprog-1.0.0/linprog.egg-info/requires.txt +3 -0
- linprog-1.0.0/linprog.egg-info/top_level.txt +1 -0
- linprog-1.0.0/linprog.py +631 -0
- linprog-1.0.0/pyproject.toml +37 -0
- linprog-1.0.0/setup.cfg +4 -0
linprog-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linprog
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Linear Programming Solver and Sensitivity Analysis Toolkit
|
|
5
|
+
Author: Your Name
|
|
6
|
+
Project-URL: Homepage, https://github.com/martinWANG2014/ProgLin
|
|
7
|
+
Keywords: linear programming,optimization,operations research,lp,sensitivity analysis
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: pandas>=2.0
|
|
15
|
+
Requires-Dist: scipy>=1.11
|
|
16
|
+
|
|
17
|
+
# LinProg
|
|
18
|
+
|
|
19
|
+
Linear Programming Solver and Sensitivity Analysis Toolkit.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install linprog
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
# ============================================================
|
|
29
|
+
# Example
|
|
30
|
+
# max 3x1 + 5x2
|
|
31
|
+
#
|
|
32
|
+
# s.t.
|
|
33
|
+
# x1 <= 4
|
|
34
|
+
# 2x2 >= 8
|
|
35
|
+
# 3x1 + 2x2 <= 18
|
|
36
|
+
# x1 + x2 == 7
|
|
37
|
+
# x1, x2 >= 0
|
|
38
|
+
# ============================================================
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from linprog import LinearProgram
|
|
42
|
+
|
|
43
|
+
lp = LinearProgram(
|
|
44
|
+
c=[3,5],
|
|
45
|
+
A=[
|
|
46
|
+
[1,0],
|
|
47
|
+
[0,2],
|
|
48
|
+
[3,2],
|
|
49
|
+
[1,1]
|
|
50
|
+
],
|
|
51
|
+
senses=["<=",">=","<=","=="],
|
|
52
|
+
b=[4,8,18,7],
|
|
53
|
+
objective="max",
|
|
54
|
+
bounds=[(0, None), (0, None)],
|
|
55
|
+
var_names=["x1", "x2"],
|
|
56
|
+
con_names=["cte1", "cte3", "cte3", "cte4"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
lp.solve()
|
|
60
|
+
lp.report_sensitive_analysis()
|
|
61
|
+
```
|
linprog-1.0.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# LinProg
|
|
2
|
+
|
|
3
|
+
Linear Programming Solver and Sensitivity Analysis Toolkit.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install linprog
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
# ============================================================
|
|
13
|
+
# Example
|
|
14
|
+
# max 3x1 + 5x2
|
|
15
|
+
#
|
|
16
|
+
# s.t.
|
|
17
|
+
# x1 <= 4
|
|
18
|
+
# 2x2 >= 8
|
|
19
|
+
# 3x1 + 2x2 <= 18
|
|
20
|
+
# x1 + x2 == 7
|
|
21
|
+
# x1, x2 >= 0
|
|
22
|
+
# ============================================================
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from linprog import LinearProgram
|
|
26
|
+
|
|
27
|
+
lp = LinearProgram(
|
|
28
|
+
c=[3,5],
|
|
29
|
+
A=[
|
|
30
|
+
[1,0],
|
|
31
|
+
[0,2],
|
|
32
|
+
[3,2],
|
|
33
|
+
[1,1]
|
|
34
|
+
],
|
|
35
|
+
senses=["<=",">=","<=","=="],
|
|
36
|
+
b=[4,8,18,7],
|
|
37
|
+
objective="max",
|
|
38
|
+
bounds=[(0, None), (0, None)],
|
|
39
|
+
var_names=["x1", "x2"],
|
|
40
|
+
con_names=["cte1", "cte3", "cte3", "cte4"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
lp.solve()
|
|
44
|
+
lp.report_sensitive_analysis()
|
|
45
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linprog
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Linear Programming Solver and Sensitivity Analysis Toolkit
|
|
5
|
+
Author: Your Name
|
|
6
|
+
Project-URL: Homepage, https://github.com/martinWANG2014/ProgLin
|
|
7
|
+
Keywords: linear programming,optimization,operations research,lp,sensitivity analysis
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: pandas>=2.0
|
|
15
|
+
Requires-Dist: scipy>=1.11
|
|
16
|
+
|
|
17
|
+
# LinProg
|
|
18
|
+
|
|
19
|
+
Linear Programming Solver and Sensitivity Analysis Toolkit.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install linprog
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
# ============================================================
|
|
29
|
+
# Example
|
|
30
|
+
# max 3x1 + 5x2
|
|
31
|
+
#
|
|
32
|
+
# s.t.
|
|
33
|
+
# x1 <= 4
|
|
34
|
+
# 2x2 >= 8
|
|
35
|
+
# 3x1 + 2x2 <= 18
|
|
36
|
+
# x1 + x2 == 7
|
|
37
|
+
# x1, x2 >= 0
|
|
38
|
+
# ============================================================
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from linprog import LinearProgram
|
|
42
|
+
|
|
43
|
+
lp = LinearProgram(
|
|
44
|
+
c=[3,5],
|
|
45
|
+
A=[
|
|
46
|
+
[1,0],
|
|
47
|
+
[0,2],
|
|
48
|
+
[3,2],
|
|
49
|
+
[1,1]
|
|
50
|
+
],
|
|
51
|
+
senses=["<=",">=","<=","=="],
|
|
52
|
+
b=[4,8,18,7],
|
|
53
|
+
objective="max",
|
|
54
|
+
bounds=[(0, None), (0, None)],
|
|
55
|
+
var_names=["x1", "x2"],
|
|
56
|
+
con_names=["cte1", "cte3", "cte3", "cte4"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
lp.solve()
|
|
60
|
+
lp.report_sensitive_analysis()
|
|
61
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linprog
|
linprog-1.0.0/linprog.py
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from scipy.optimize import linprog
|
|
4
|
+
TOL = 1e-6
|
|
5
|
+
#####
|
|
6
|
+
## Contact: chenghao.wang@uphf.fr
|
|
7
|
+
#####
|
|
8
|
+
class LinearProgram:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
c,
|
|
12
|
+
A,
|
|
13
|
+
senses,
|
|
14
|
+
b,
|
|
15
|
+
objective="max",
|
|
16
|
+
bounds=None,
|
|
17
|
+
var_names=None,
|
|
18
|
+
con_names=None
|
|
19
|
+
):
|
|
20
|
+
self.c = np.array(c, dtype=float)
|
|
21
|
+
self.A = np.array(A, dtype=float)
|
|
22
|
+
self.senses = senses
|
|
23
|
+
self.b = np.array(b, dtype=float)
|
|
24
|
+
self.objective = objective.lower()
|
|
25
|
+
|
|
26
|
+
self.m, self.n = self.A.shape
|
|
27
|
+
|
|
28
|
+
self.var_names = var_names or [f"x{j+1}" for j in range(self.n)]
|
|
29
|
+
self.con_names = con_names or [f"Constraint {i+1}" for i in range(self.m)]
|
|
30
|
+
self.bounds = bounds or [(0, None)] * self.n
|
|
31
|
+
|
|
32
|
+
self.res = None
|
|
33
|
+
self.objective_value = None
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------
|
|
36
|
+
# Formatting helpers
|
|
37
|
+
# ------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def fmt(v):
|
|
41
|
+
if np.isneginf(v):
|
|
42
|
+
return "-INFINITY"
|
|
43
|
+
if np.isposinf(v):
|
|
44
|
+
return "INFINITY"
|
|
45
|
+
return round(float(v), 6)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def linear_expr(coeffs, names):
|
|
49
|
+
terms = []
|
|
50
|
+
for a, name in zip(coeffs, names):
|
|
51
|
+
if abs(a) < TOL:
|
|
52
|
+
continue
|
|
53
|
+
if abs(a - 1) < TOL:
|
|
54
|
+
terms.append(name)
|
|
55
|
+
elif abs(a + 1) < TOL:
|
|
56
|
+
terms.append(f"-{name}")
|
|
57
|
+
else:
|
|
58
|
+
terms.append(f"{a:g} {name}")
|
|
59
|
+
|
|
60
|
+
if not terms:
|
|
61
|
+
return "0"
|
|
62
|
+
|
|
63
|
+
return " + ".join(terms).replace("+ -", "- ")
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def interval_to_change(current, low, high):
|
|
67
|
+
dec = np.inf if np.isneginf(low) else current - low
|
|
68
|
+
inc = np.inf if np.isposinf(high) else high - current
|
|
69
|
+
return dec, inc
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------
|
|
72
|
+
# Model reports
|
|
73
|
+
# ------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def reportModel(self):
|
|
76
|
+
lines = []
|
|
77
|
+
lines.append("ORIGINAL LP MODEL")
|
|
78
|
+
lines.append("=" * 80)
|
|
79
|
+
|
|
80
|
+
obj = self.linear_expr(self.c, self.var_names)
|
|
81
|
+
lines.append(f"{self.objective.upper()} z = {obj}")
|
|
82
|
+
lines.append("")
|
|
83
|
+
lines.append("Subject to:")
|
|
84
|
+
|
|
85
|
+
for name, row, sense, rhs in zip(
|
|
86
|
+
self.con_names, self.A, self.senses, self.b
|
|
87
|
+
):
|
|
88
|
+
lhs = self.linear_expr(row, self.var_names)
|
|
89
|
+
lines.append(f" {name}: {lhs} {sense} {rhs:g}")
|
|
90
|
+
|
|
91
|
+
lines.append("")
|
|
92
|
+
lines.append("Bounds:")
|
|
93
|
+
for name, bound in zip(self.var_names, self.bounds):
|
|
94
|
+
lb, ub = bound
|
|
95
|
+
if lb is not None:
|
|
96
|
+
lines.append(f" {name} >= {lb:g}")
|
|
97
|
+
if ub is not None:
|
|
98
|
+
lines.append(f" {name} <= {ub:g}")
|
|
99
|
+
|
|
100
|
+
lines.append("-" * 80)
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
def _build_standard_form(self):
|
|
104
|
+
rows = []
|
|
105
|
+
rhs_std = []
|
|
106
|
+
slack_names = []
|
|
107
|
+
|
|
108
|
+
for i, (row, sense, rhs) in enumerate(
|
|
109
|
+
zip(self.A, self.senses, self.b)
|
|
110
|
+
):
|
|
111
|
+
if sense == "<=":
|
|
112
|
+
rows.append(row)
|
|
113
|
+
rhs_std.append(rhs)
|
|
114
|
+
|
|
115
|
+
elif sense == ">=":
|
|
116
|
+
rows.append(-row)
|
|
117
|
+
rhs_std.append(-rhs)
|
|
118
|
+
|
|
119
|
+
elif sense == "==":
|
|
120
|
+
rows.append(row)
|
|
121
|
+
rhs_std.append(rhs)
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError("Each sense must be '<=', '>=', or '=='")
|
|
125
|
+
|
|
126
|
+
A_base = np.array(rows, dtype=float)
|
|
127
|
+
b_std = np.array(rhs_std, dtype=float)
|
|
128
|
+
|
|
129
|
+
slack_cols = []
|
|
130
|
+
|
|
131
|
+
for i, sense in enumerate(self.senses):
|
|
132
|
+
if sense in ["<=", ">="]:
|
|
133
|
+
col = np.zeros(self.m)
|
|
134
|
+
col[i] = 1.0
|
|
135
|
+
slack_cols.append(col)
|
|
136
|
+
slack_names.append(f"s{i+1}")
|
|
137
|
+
|
|
138
|
+
if slack_cols:
|
|
139
|
+
S = np.column_stack(slack_cols)
|
|
140
|
+
Astd = np.hstack([A_base, S])
|
|
141
|
+
else:
|
|
142
|
+
Astd = A_base.copy()
|
|
143
|
+
|
|
144
|
+
cstd = np.concatenate([self.c, np.zeros(len(slack_names))])
|
|
145
|
+
std_names = self.var_names + slack_names
|
|
146
|
+
|
|
147
|
+
return Astd, b_std, cstd, std_names, slack_names
|
|
148
|
+
|
|
149
|
+
def reportStandardModelFormat(self):
|
|
150
|
+
Astd, b_std, cstd, std_names, _ = self._build_standard_form()
|
|
151
|
+
|
|
152
|
+
lines = []
|
|
153
|
+
lines.append("STANDARD LP FORMAT")
|
|
154
|
+
lines.append("=" * 80)
|
|
155
|
+
|
|
156
|
+
obj = self.linear_expr(cstd, std_names)
|
|
157
|
+
lines.append(f"{self.objective.upper()} z = {obj}")
|
|
158
|
+
lines.append("")
|
|
159
|
+
lines.append("Subject to:")
|
|
160
|
+
|
|
161
|
+
for row, rhs in zip(Astd, b_std):
|
|
162
|
+
lhs = self.linear_expr(row, std_names)
|
|
163
|
+
lines.append(f" {lhs} = {rhs:g}")
|
|
164
|
+
|
|
165
|
+
lines.append("")
|
|
166
|
+
lines.append("All variables >= 0")
|
|
167
|
+
lines.append("-" * 80)
|
|
168
|
+
return "\n".join(lines)
|
|
169
|
+
|
|
170
|
+
def reportMatrixFormat(self):
|
|
171
|
+
|
|
172
|
+
Astd, b_std, cstd, std_names, _ = self._build_standard_form()
|
|
173
|
+
|
|
174
|
+
def pretty_matrix(M):
|
|
175
|
+
rows = []
|
|
176
|
+
|
|
177
|
+
for i, row in enumerate(M):
|
|
178
|
+
body = " ".join(f"{x:>6g}" for x in row)
|
|
179
|
+
|
|
180
|
+
if i == 0:
|
|
181
|
+
rows.append(f"⎡ {body} ⎤")
|
|
182
|
+
elif i == len(M) - 1:
|
|
183
|
+
rows.append(f"⎣ {body} ⎦")
|
|
184
|
+
else:
|
|
185
|
+
rows.append(f"⎢ {body} ⎥")
|
|
186
|
+
|
|
187
|
+
return rows
|
|
188
|
+
|
|
189
|
+
def pretty_vector(v):
|
|
190
|
+
rows = []
|
|
191
|
+
|
|
192
|
+
for i, x in enumerate(v):
|
|
193
|
+
|
|
194
|
+
if i == 0:
|
|
195
|
+
rows.append(f"⎡ {x:g} ⎤")
|
|
196
|
+
elif i == len(v) - 1:
|
|
197
|
+
rows.append(f"⎣ {x:g} ⎦")
|
|
198
|
+
else:
|
|
199
|
+
rows.append(f"⎢ {x:g} ⎥")
|
|
200
|
+
|
|
201
|
+
return rows
|
|
202
|
+
|
|
203
|
+
def pretty_variable_vector(names):
|
|
204
|
+
rows = []
|
|
205
|
+
|
|
206
|
+
for i, n in enumerate(names):
|
|
207
|
+
|
|
208
|
+
if i == 0:
|
|
209
|
+
rows.append(f"⎡ {n} ⎤")
|
|
210
|
+
elif i == len(names) - 1:
|
|
211
|
+
rows.append(f"⎣ {n} ⎦")
|
|
212
|
+
else:
|
|
213
|
+
rows.append(f"⎢ {n} ⎥")
|
|
214
|
+
|
|
215
|
+
return rows
|
|
216
|
+
|
|
217
|
+
lines = []
|
|
218
|
+
|
|
219
|
+
lines.append("MATRIX FORMAT")
|
|
220
|
+
lines.append("=" * 80)
|
|
221
|
+
lines.append("")
|
|
222
|
+
|
|
223
|
+
lines.append(f"{self.objective.upper()} z = cᵀx")
|
|
224
|
+
lines.append("")
|
|
225
|
+
lines.append("Subject To")
|
|
226
|
+
lines.append("")
|
|
227
|
+
lines.append("Ax = b")
|
|
228
|
+
lines.append("x ≥ 0")
|
|
229
|
+
lines.append("")
|
|
230
|
+
|
|
231
|
+
# A matrix
|
|
232
|
+
A_lines = pretty_matrix(Astd)
|
|
233
|
+
|
|
234
|
+
lines.append("A =")
|
|
235
|
+
lines.extend(A_lines)
|
|
236
|
+
lines.append("")
|
|
237
|
+
|
|
238
|
+
# x vector
|
|
239
|
+
x_lines = pretty_variable_vector(std_names)
|
|
240
|
+
|
|
241
|
+
lines.append("x =")
|
|
242
|
+
lines.extend(x_lines)
|
|
243
|
+
lines.append("")
|
|
244
|
+
|
|
245
|
+
# b vector
|
|
246
|
+
b_lines = pretty_vector(b_std)
|
|
247
|
+
|
|
248
|
+
lines.append("b =")
|
|
249
|
+
lines.extend(b_lines)
|
|
250
|
+
lines.append("")
|
|
251
|
+
|
|
252
|
+
# c row vector
|
|
253
|
+
c_str = " ".join(f"{v:g}" for v in cstd)
|
|
254
|
+
|
|
255
|
+
lines.append(f"cᵀ = [ {c_str} ]")
|
|
256
|
+
lines.append("-" * 80)
|
|
257
|
+
|
|
258
|
+
return "\n".join(lines)
|
|
259
|
+
|
|
260
|
+
# ------------------------------------------------------------
|
|
261
|
+
# Solve
|
|
262
|
+
# ------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def solve(self):
|
|
265
|
+
A_ub, b_ub = [], []
|
|
266
|
+
A_eq, b_eq = [], []
|
|
267
|
+
|
|
268
|
+
for row, sense, rhs in zip(self.A, self.senses, self.b):
|
|
269
|
+
if sense == "<=":
|
|
270
|
+
A_ub.append(row)
|
|
271
|
+
b_ub.append(rhs)
|
|
272
|
+
elif sense == ">=":
|
|
273
|
+
A_ub.append(-row)
|
|
274
|
+
b_ub.append(-rhs)
|
|
275
|
+
elif sense == "==":
|
|
276
|
+
A_eq.append(row)
|
|
277
|
+
b_eq.append(rhs)
|
|
278
|
+
else:
|
|
279
|
+
raise ValueError("Each sense must be '<=', '>=', or '=='")
|
|
280
|
+
|
|
281
|
+
scipy_c = self.c.copy()
|
|
282
|
+
|
|
283
|
+
if self.objective == "max":
|
|
284
|
+
scipy_c = -scipy_c
|
|
285
|
+
elif self.objective != "min":
|
|
286
|
+
raise ValueError("objective must be 'max' or 'min'")
|
|
287
|
+
|
|
288
|
+
self.res = linprog(
|
|
289
|
+
scipy_c,
|
|
290
|
+
A_ub=np.array(A_ub) if A_ub else None,
|
|
291
|
+
b_ub=np.array(b_ub) if b_ub else None,
|
|
292
|
+
A_eq=np.array(A_eq) if A_eq else None,
|
|
293
|
+
b_eq=np.array(b_eq) if b_eq else None,
|
|
294
|
+
bounds=self.bounds,
|
|
295
|
+
method="highs-ds"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not self.res.success:
|
|
299
|
+
raise RuntimeError(self.res.message)
|
|
300
|
+
|
|
301
|
+
self.objective_value = float(self.c @ self.res.x)
|
|
302
|
+
|
|
303
|
+
return self.res
|
|
304
|
+
|
|
305
|
+
def reportSolution(self):
|
|
306
|
+
if self.res is None:
|
|
307
|
+
self.solve()
|
|
308
|
+
|
|
309
|
+
x = self.res.x
|
|
310
|
+
|
|
311
|
+
solution_table = pd.DataFrame({
|
|
312
|
+
"Variable": self.var_names,
|
|
313
|
+
"Value": x,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
lines = []
|
|
317
|
+
lines.append("OPTIMAL SOLUTION")
|
|
318
|
+
lines.append("=" * 80)
|
|
319
|
+
|
|
320
|
+
lines.append(f"Objective Type : {self.objective.upper()}")
|
|
321
|
+
lines.append(
|
|
322
|
+
f"Optimal Objective Value : {self.objective_value:.6f}"
|
|
323
|
+
)
|
|
324
|
+
lines.append("")
|
|
325
|
+
|
|
326
|
+
lines.append(solution_table.to_string(index=False))
|
|
327
|
+
lines.append("-" * 80)
|
|
328
|
+
report = "\n".join(lines)
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
"objective_type": self.objective,
|
|
332
|
+
"objective_value": self.objective_value,
|
|
333
|
+
"solution_table": solution_table,
|
|
334
|
+
"report_text": report
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# ------------------------------------------------------------
|
|
338
|
+
# Sensitivity analysis
|
|
339
|
+
# ------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
def report_sensitive_analysis(self):
|
|
342
|
+
if self.res is None:
|
|
343
|
+
self.solve()
|
|
344
|
+
|
|
345
|
+
x = self.res.x
|
|
346
|
+
true_obj = self.objective_value
|
|
347
|
+
|
|
348
|
+
Astd, b_std, cstd, std_names, slack_names = self._build_standard_form()
|
|
349
|
+
|
|
350
|
+
cstd_sens = -cstd if self.objective == "min" else cstd.copy()
|
|
351
|
+
|
|
352
|
+
std_values = list(x)
|
|
353
|
+
|
|
354
|
+
for i, sense in enumerate(self.senses):
|
|
355
|
+
activity = self.A[i] @ x
|
|
356
|
+
|
|
357
|
+
if sense == "<=":
|
|
358
|
+
std_values.append(self.b[i] - activity)
|
|
359
|
+
elif sense == ">=":
|
|
360
|
+
std_values.append(activity - self.b[i])
|
|
361
|
+
|
|
362
|
+
zstd = np.array(std_values, dtype=float)
|
|
363
|
+
|
|
364
|
+
total_vars = Astd.shape[1]
|
|
365
|
+
rank_needed = Astd.shape[0]
|
|
366
|
+
|
|
367
|
+
basic_idx = list(np.where(zstd > TOL)[0])
|
|
368
|
+
|
|
369
|
+
for j in range(total_vars):
|
|
370
|
+
if len(basic_idx) == rank_needed:
|
|
371
|
+
break
|
|
372
|
+
if j not in basic_idx:
|
|
373
|
+
trial = basic_idx + [j]
|
|
374
|
+
if np.linalg.matrix_rank(Astd[:, trial]) == len(trial):
|
|
375
|
+
basic_idx.append(j)
|
|
376
|
+
|
|
377
|
+
basic_idx = basic_idx[:rank_needed]
|
|
378
|
+
nonbasic_idx = [j for j in range(total_vars) if j not in basic_idx]
|
|
379
|
+
|
|
380
|
+
B = Astd[:, basic_idx]
|
|
381
|
+
N = Astd[:, nonbasic_idx]
|
|
382
|
+
|
|
383
|
+
if np.linalg.matrix_rank(B) < rank_needed:
|
|
384
|
+
raise RuntimeError("Could not identify nonsingular basis.")
|
|
385
|
+
|
|
386
|
+
B_inv = np.linalg.inv(B)
|
|
387
|
+
xB = B_inv @ b_std
|
|
388
|
+
|
|
389
|
+
cB = cstd_sens[basic_idx]
|
|
390
|
+
cN = cstd_sens[nonbasic_idx]
|
|
391
|
+
|
|
392
|
+
y = cB @ B_inv
|
|
393
|
+
reduced = cstd_sens - y @ Astd
|
|
394
|
+
|
|
395
|
+
# --------------------------------------------------------
|
|
396
|
+
# Solution table
|
|
397
|
+
# --------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
display_reduced = reduced[:self.n]
|
|
400
|
+
|
|
401
|
+
# if self.objective == "max":
|
|
402
|
+
display_reduced = -display_reduced
|
|
403
|
+
|
|
404
|
+
display_reduced[np.abs(display_reduced) < TOL] = 0.0
|
|
405
|
+
solution_table = pd.DataFrame({
|
|
406
|
+
"Variable": self.var_names,
|
|
407
|
+
"Value": x,
|
|
408
|
+
"Reduced Cost": display_reduced
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
# --------------------------------------------------------
|
|
412
|
+
# Constraint table
|
|
413
|
+
# --------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
con_rows = []
|
|
416
|
+
|
|
417
|
+
for i, (row, sense, rhs) in enumerate(
|
|
418
|
+
zip(self.A, self.senses, self.b)
|
|
419
|
+
):
|
|
420
|
+
activity = row @ x
|
|
421
|
+
|
|
422
|
+
if sense == "<=":
|
|
423
|
+
slack = rhs - activity
|
|
424
|
+
elif sense == ">=":
|
|
425
|
+
slack = activity - rhs
|
|
426
|
+
else:
|
|
427
|
+
slack = 0.0
|
|
428
|
+
|
|
429
|
+
shadow = y[i]
|
|
430
|
+
|
|
431
|
+
if sense == ">=":
|
|
432
|
+
shadow = -shadow
|
|
433
|
+
|
|
434
|
+
if abs(shadow) < TOL:
|
|
435
|
+
shadow = 0.0
|
|
436
|
+
con_rows.append({
|
|
437
|
+
"Constraint": self.con_names[i],
|
|
438
|
+
"Slack/Surplus": slack,
|
|
439
|
+
"Shadow/Dual Price": shadow
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
constraint_table = pd.DataFrame(con_rows)
|
|
443
|
+
|
|
444
|
+
# --------------------------------------------------------
|
|
445
|
+
# Objective coefficient sensitivity
|
|
446
|
+
# --------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
obj_rows = []
|
|
449
|
+
YN = B_inv @ N
|
|
450
|
+
reduced_N = cN - cB @ YN
|
|
451
|
+
|
|
452
|
+
for j in range(self.n):
|
|
453
|
+
current_coef_sens = cstd_sens[j]
|
|
454
|
+
|
|
455
|
+
if j in nonbasic_idx:
|
|
456
|
+
pos = nonbasic_idx.index(j)
|
|
457
|
+
r = reduced_N[pos]
|
|
458
|
+
|
|
459
|
+
lo_sens = -np.inf
|
|
460
|
+
hi_sens = current_coef_sens - r
|
|
461
|
+
|
|
462
|
+
else:
|
|
463
|
+
k = basic_idx.index(j)
|
|
464
|
+
row = YN[k, :]
|
|
465
|
+
|
|
466
|
+
low_delta, high_delta = -np.inf, np.inf
|
|
467
|
+
|
|
468
|
+
for r, a in zip(reduced_N, row):
|
|
469
|
+
if abs(a) < TOL:
|
|
470
|
+
continue
|
|
471
|
+
|
|
472
|
+
bound = r / a
|
|
473
|
+
|
|
474
|
+
if a > 0:
|
|
475
|
+
low_delta = max(low_delta, bound)
|
|
476
|
+
else:
|
|
477
|
+
high_delta = min(high_delta, bound)
|
|
478
|
+
|
|
479
|
+
lo_sens = current_coef_sens + low_delta
|
|
480
|
+
hi_sens = current_coef_sens + high_delta
|
|
481
|
+
|
|
482
|
+
if self.objective == "min":
|
|
483
|
+
lo = -hi_sens
|
|
484
|
+
hi = -lo_sens
|
|
485
|
+
else:
|
|
486
|
+
lo = lo_sens
|
|
487
|
+
hi = hi_sens
|
|
488
|
+
|
|
489
|
+
dec, inc = self.interval_to_change(self.c[j], lo, hi)
|
|
490
|
+
|
|
491
|
+
obj_rows.append({
|
|
492
|
+
"Variable": self.var_names[j],
|
|
493
|
+
"Current c_j": self.c[j],
|
|
494
|
+
"Allowable Increase": self.fmt(inc),
|
|
495
|
+
"Allowable Decrease": self.fmt(dec)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
obj_sensitivity = pd.DataFrame(obj_rows)
|
|
499
|
+
|
|
500
|
+
# --------------------------------------------------------
|
|
501
|
+
# RHS sensitivity
|
|
502
|
+
# --------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
rhs_rows = []
|
|
505
|
+
|
|
506
|
+
for i in range(self.m):
|
|
507
|
+
d = B_inv[:, i]
|
|
508
|
+
low_delta, high_delta = -np.inf, np.inf
|
|
509
|
+
|
|
510
|
+
for xb, di in zip(xB, d):
|
|
511
|
+
if abs(di) < TOL:
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
bound = -xb / di
|
|
515
|
+
|
|
516
|
+
if di > 0:
|
|
517
|
+
low_delta = max(low_delta, bound)
|
|
518
|
+
else:
|
|
519
|
+
high_delta = min(high_delta, bound)
|
|
520
|
+
|
|
521
|
+
std_lo = b_std[i] + low_delta
|
|
522
|
+
std_hi = b_std[i] + high_delta
|
|
523
|
+
|
|
524
|
+
if self.senses[i] == ">=":
|
|
525
|
+
orig_lo = -std_hi
|
|
526
|
+
orig_hi = -std_lo
|
|
527
|
+
else:
|
|
528
|
+
orig_lo = std_lo
|
|
529
|
+
orig_hi = std_hi
|
|
530
|
+
|
|
531
|
+
dec, inc = self.interval_to_change(
|
|
532
|
+
self.b[i], orig_lo, orig_hi
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
rhs_rows.append({
|
|
536
|
+
"Constraint": self.con_names[i],
|
|
537
|
+
"Current RHS": self.b[i],
|
|
538
|
+
"Allowable Increase": self.fmt(inc),
|
|
539
|
+
"Allowable Decrease": self.fmt(dec)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
rhs_sensitivity = pd.DataFrame(rhs_rows)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
basis_table = pd.DataFrame({
|
|
547
|
+
"Basic Variable": [std_names[i] for i in basic_idx],
|
|
548
|
+
"Value": xB
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
report_text = "\n\n".join([
|
|
552
|
+
self.reportModel(),
|
|
553
|
+
self.reportStandardModelFormat(),
|
|
554
|
+
self.reportMatrixFormat(),
|
|
555
|
+
"OPTIMAL SOLUTION\n" + "=" * 80 + "\n"
|
|
556
|
+
+ f"Objective value = {true_obj:.6f}\n\n"
|
|
557
|
+
+ solution_table.to_string(index=False)+ "\n"+"-" * 80,
|
|
558
|
+
"CONSTRAINT REPORT\n" + "=" * 80 + "\n"
|
|
559
|
+
+ constraint_table.to_string(index=False)+ "\n"+"-" * 80,
|
|
560
|
+
"OBJECTIVE COEFFICIENT SENSITIVITY\n" + "=" * 80 + "\n"
|
|
561
|
+
+ obj_sensitivity.to_string(index=False)+ "\n"+"-" * 80,
|
|
562
|
+
"RHS SENSITIVITY\n" + "=" * 80 + "\n"
|
|
563
|
+
+ rhs_sensitivity.to_string(index=False)+ "\n"+"-" * 80,
|
|
564
|
+
"FINAL BASIS\n" + "=" * 80 + "\n"
|
|
565
|
+
+ basis_table.to_string(index=False)+ "\n"+"-" * 80
|
|
566
|
+
])
|
|
567
|
+
|
|
568
|
+
print(report_text)
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
"result": self.res,
|
|
572
|
+
"objective_value": true_obj,
|
|
573
|
+
|
|
574
|
+
"lp_format": self.reportModel(),
|
|
575
|
+
"standard_lp_format": self.reportStandardModelFormat(),
|
|
576
|
+
"matrix_format": self.reportMatrixFormat(),
|
|
577
|
+
|
|
578
|
+
"solution": solution_table,
|
|
579
|
+
"constraints": constraint_table,
|
|
580
|
+
"obj_sensitivity": obj_sensitivity,
|
|
581
|
+
"rhs_sensitivity": rhs_sensitivity,
|
|
582
|
+
"basis": basis_table,
|
|
583
|
+
|
|
584
|
+
"A_standard": Astd,
|
|
585
|
+
"b_standard": b_std,
|
|
586
|
+
"c_standard": cstd,
|
|
587
|
+
"standard_variable_names": std_names,
|
|
588
|
+
|
|
589
|
+
"report_text": report_text
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if __name__ == '__main__':
|
|
593
|
+
# ============================================================
|
|
594
|
+
# Example
|
|
595
|
+
# max 3x1 + 5x2
|
|
596
|
+
#
|
|
597
|
+
# s.t.
|
|
598
|
+
# x1 <= 4
|
|
599
|
+
# 2x2 >= 8
|
|
600
|
+
# 3x1 + 2x2 <= 18
|
|
601
|
+
# x1 + x2 == 7
|
|
602
|
+
# x1, x2 >= 0
|
|
603
|
+
# ============================================================
|
|
604
|
+
|
|
605
|
+
c = [3, 5]
|
|
606
|
+
|
|
607
|
+
A = [
|
|
608
|
+
[1, 0],
|
|
609
|
+
[0, 2],
|
|
610
|
+
[3, 2],
|
|
611
|
+
[1, 1]
|
|
612
|
+
]
|
|
613
|
+
|
|
614
|
+
senses = ["<=", ">=", "<=", "=="]
|
|
615
|
+
|
|
616
|
+
b = [4, 8, 18, 7]
|
|
617
|
+
|
|
618
|
+
bounds = [(0, None), (0, None)]
|
|
619
|
+
|
|
620
|
+
lp = LinearProgram(
|
|
621
|
+
c=c,
|
|
622
|
+
A=A,
|
|
623
|
+
senses=senses,
|
|
624
|
+
b=b,
|
|
625
|
+
objective="max",
|
|
626
|
+
bounds=bounds,
|
|
627
|
+
var_names=["x1", "x2"],
|
|
628
|
+
con_names=["cte1", "cte3", "cte3", "cte4"]
|
|
629
|
+
)
|
|
630
|
+
lp.solve()
|
|
631
|
+
lp.report_sensitive_analysis()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "linprog"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Linear Programming Solver and Sensitivity Analysis Toolkit"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Your Name"}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"numpy>=1.24",
|
|
18
|
+
"pandas>=2.0",
|
|
19
|
+
"scipy>=1.11"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
keywords = [
|
|
23
|
+
"linear programming",
|
|
24
|
+
"optimization",
|
|
25
|
+
"operations research",
|
|
26
|
+
"lp",
|
|
27
|
+
"sensitivity analysis"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
classifiers = [
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"License :: OSI Approved :: MIT License",
|
|
33
|
+
"Operating System :: OS Independent"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/martinWANG2014/ProgLin"
|
linprog-1.0.0/setup.cfg
ADDED