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 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
+ ```
@@ -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,8 @@
1
+ README.md
2
+ linprog.py
3
+ pyproject.toml
4
+ linprog.egg-info/PKG-INFO
5
+ linprog.egg-info/SOURCES.txt
6
+ linprog.egg-info/dependency_links.txt
7
+ linprog.egg-info/requires.txt
8
+ linprog.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ numpy>=1.24
2
+ pandas>=2.0
3
+ scipy>=1.11
@@ -0,0 +1 @@
1
+ linprog
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+