Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/operations-research/linear_programming.tex
75 views
unlisted
1
% Linear Programming Analysis Template
2
% Topics: Simplex algorithm, duality theory, sensitivity analysis, graphical solutions
3
% Style: Operations research technical report with computational implementation
4
5
\documentclass[a4paper, 11pt]{article}
6
\usepackage[utf8]{inputenc}
7
\usepackage[T1]{fontenc}
8
\usepackage{amsmath, amssymb}
9
\usepackage{graphicx}
10
\usepackage{siunitx}
11
\usepackage{booktabs}
12
\usepackage{subcaption}
13
\usepackage[makestderr]{pythontex}
14
15
% Theorem environments
16
\newtheorem{definition}{Definition}[section]
17
\newtheorem{theorem}{Theorem}[section]
18
\newtheorem{example}{Example}[section]
19
\newtheorem{remark}{Remark}[section]
20
21
\title{Linear Programming: Simplex Method, Duality, and Sensitivity Analysis}
22
\author{Operations Research Laboratory}
23
\date{\today}
24
25
\begin{document}
26
\maketitle
27
28
\begin{abstract}
29
This technical report presents a comprehensive analysis of linear programming (LP) techniques,
30
including graphical solutions for two-variable problems, the simplex algorithm for
31
higher-dimensional optimization, duality theory, and sensitivity analysis. We examine a
32
production planning problem, demonstrate the simplex tableau method, derive the dual problem,
33
and analyze shadow prices and reduced costs. Computational implementation using Python's
34
\texttt{scipy.optimize.linprog} validates theoretical results and illustrates practical
35
applications in resource allocation.
36
\end{abstract}
37
38
\section{Introduction}
39
40
Linear programming (LP) is a fundamental technique in operations research for optimizing
41
a linear objective function subject to linear equality and inequality constraints. Since
42
George Dantzig's development of the simplex algorithm in 1947, LP has become one of the
43
most widely applied optimization methods in industry, logistics, finance, and engineering.
44
45
\begin{definition}[Linear Programming Problem]
46
A linear programming problem in standard form seeks to:
47
\begin{equation}
48
\begin{aligned}
49
\text{maximize} \quad & \mathbf{c}^T \mathbf{x} \\
50
\text{subject to} \quad & \mathbf{A}\mathbf{x} \leq \mathbf{b} \\
51
& \mathbf{x} \geq \mathbf{0}
52
\end{aligned}
53
\end{equation}
54
where $\mathbf{c} \in \mathbb{R}^n$ is the objective coefficient vector, $\mathbf{A} \in \mathbb{R}^{m \times n}$
55
is the constraint matrix, $\mathbf{b} \in \mathbb{R}^m$ is the right-hand side vector, and
56
$\mathbf{x} \in \mathbb{R}^n$ is the decision variable vector.
57
\end{definition}
58
59
\section{Theoretical Framework}
60
61
\subsection{Fundamental Concepts}
62
63
\begin{definition}[Feasible Region]
64
The feasible region $\mathcal{F}$ is the set of all points satisfying the constraints:
65
\begin{equation}
66
\mathcal{F} = \{\mathbf{x} \in \mathbb{R}^n : \mathbf{A}\mathbf{x} \leq \mathbf{b}, \mathbf{x} \geq \mathbf{0}\}
67
\end{equation}
68
\end{definition}
69
70
\begin{theorem}[Fundamental Theorem of Linear Programming]
71
If a linear programming problem has an optimal solution, then at least one optimal solution
72
occurs at an extreme point (vertex) of the feasible region.
73
\end{theorem}
74
75
\begin{definition}[Basic Feasible Solution]
76
A basic feasible solution (BFS) is a feasible solution where at most $m$ variables (out of $n$)
77
are non-zero. These non-zero variables are called \textit{basic variables}, and the zero
78
variables are \textit{non-basic variables}.
79
\end{definition}
80
81
\subsection{The Simplex Algorithm}
82
83
The simplex algorithm moves from one BFS to an adjacent BFS with a better objective value,
84
continuing until optimality is reached or unboundedness is detected.
85
86
\begin{theorem}[Optimality Criterion]
87
A basic feasible solution is optimal if all reduced costs $\bar{c}_j = c_j - \mathbf{c}_B^T \mathbf{A}_B^{-1} \mathbf{A}_j$
88
are non-positive (for maximization) or non-negative (for minimization).
89
\end{theorem}
90
91
\subsection{Duality Theory}
92
93
\begin{definition}[Dual Problem]
94
For the primal LP problem, the dual problem is:
95
\begin{equation}
96
\begin{aligned}
97
\text{minimize} \quad & \mathbf{b}^T \mathbf{y} \\
98
\text{subject to} \quad & \mathbf{A}^T\mathbf{y} \geq \mathbf{c} \\
99
& \mathbf{y} \geq \mathbf{0}
100
\end{aligned}
101
\end{equation}
102
where $\mathbf{y} \in \mathbb{R}^m$ are the dual variables (shadow prices).
103
\end{definition}
104
105
\begin{theorem}[Strong Duality]
106
If the primal problem has an optimal solution $\mathbf{x}^*$ with objective value $z^*$,
107
then the dual problem has an optimal solution $\mathbf{y}^*$ with objective value $w^*$,
108
and $z^* = w^*$.
109
\end{theorem}
110
111
\begin{theorem}[Complementary Slackness]
112
At optimality, for each constraint $i$:
113
\begin{equation}
114
y_i^* \left(b_i - \sum_{j=1}^n a_{ij}x_j^* \right) = 0
115
\end{equation}
116
and for each variable $j$:
117
\begin{equation}
118
x_j^* \left(\sum_{i=1}^m a_{ij}y_i^* - c_j \right) = 0
119
\end{equation}
120
\end{definition}
121
122
\section{Computational Analysis}
123
124
\subsection{Problem Formulation: Production Planning}
125
126
We consider a furniture manufacturer producing tables and chairs with limited resources.
127
Each table requires 4 hours of labor and 3 board-feet of wood, yielding a profit of \$30.
128
Each chair requires 2 hours of labor and 2 board-feet of wood, yielding a profit of \$20.
129
Available resources are 80 hours of labor and 60 board-feet of wood. The goal is to maximize profit.
130
131
This translates to the following LP formulation:
132
\begin{equation}
133
\begin{aligned}
134
\text{maximize} \quad & z = 30x_1 + 20x_2 \\
135
\text{subject to} \quad & 4x_1 + 2x_2 \leq 80 \quad \text{(labor)} \\
136
& 3x_1 + 2x_2 \leq 60 \quad \text{(wood)} \\
137
& x_1, x_2 \geq 0
138
\end{aligned}
139
\end{equation}
140
where $x_1$ is the number of tables and $x_2$ is the number of chairs.
141
142
\begin{pycode}
143
import numpy as np
144
import matplotlib.pyplot as plt
145
from scipy.optimize import linprog
146
from matplotlib.patches import Polygon
147
148
np.random.seed(42)
149
150
# Problem parameters - Production Planning
151
c = np.array([30, 20]) # Objective coefficients (profit per unit)
152
A = np.array([
153
[4, 2], # Labor constraint
154
[3, 2] # Wood constraint
155
])
156
b = np.array([80, 60]) # Resource availability
157
158
# Solve using linprog (minimization, so negate c)
159
result = linprog(-c, A_ub=A, b_ub=b, bounds=[(0, None), (0, None)], method='highs')
160
x_optimal = result.x
161
z_optimal = -result.fun
162
shadow_prices = result.ineqlin.marginals
163
reduced_costs = result.lower.marginals
164
165
# Calculate vertices of feasible region
166
vertices = []
167
vertices.append([0, 0]) # Origin
168
169
# Intersection with axes
170
vertices.append([min(b[0]/A[0,0], b[1]/A[1,0]), 0]) # x1-axis
171
vertices.append([0, min(b[0]/A[0,1], b[1]/A[1,1])]) # x2-axis
172
173
# Constraint intersections
174
# 4x1 + 2x2 = 80 and 3x1 + 2x2 = 60
175
A_int = np.array([[4, 2], [3, 2]])
176
b_int = np.array([80, 60])
177
try:
178
vertex = np.linalg.solve(A_int, b_int)
179
if all(vertex >= 0) and all(A @ vertex <= b + 1e-6):
180
vertices.append(vertex)
181
except:
182
pass
183
184
vertices = np.array(vertices)
185
186
# Graphical solution
187
fig, ax = plt.subplots(figsize=(10, 8))
188
189
# Plot constraints
190
x1_range = np.linspace(0, 30, 500)
191
192
# Labor constraint: 4x1 + 2x2 <= 80
193
x2_labor = (80 - 4*x1_range) / 2
194
ax.plot(x1_range, x2_labor, 'b-', linewidth=2, label='Labor: $4x_1 + 2x_2 \\leq 80$')
195
ax.fill_between(x1_range, 0, np.minimum(x2_labor, 50), where=(x2_labor >= 0),
196
alpha=0.2, color='blue')
197
198
# Wood constraint: 3x1 + 2x2 <= 60
199
x2_wood = (60 - 3*x1_range) / 2
200
ax.plot(x1_range, x2_wood, 'g-', linewidth=2, label='Wood: $3x_1 + 2x_2 \\leq 60$')
201
ax.fill_between(x1_range, 0, np.minimum(x2_wood, 50), where=(x2_wood >= 0),
202
alpha=0.2, color='green')
203
204
# Feasible region
205
feasible_x1 = [0, 0, x_optimal[0], 20, 0]
206
feasible_x2 = [0, 30, x_optimal[1], 0, 0]
207
ax.fill(feasible_x1, feasible_x2, alpha=0.3, color='yellow',
208
edgecolor='black', linewidth=2, label='Feasible Region')
209
210
# Objective function iso-profit lines
211
for profit in [200, 400, 600]:
212
x2_obj = (profit - 30*x1_range) / 20
213
ax.plot(x1_range, x2_obj, '--', alpha=0.5, color='red', linewidth=1)
214
215
# Optimal iso-profit line
216
x2_opt = (z_optimal - 30*x1_range) / 20
217
ax.plot(x1_range, x2_opt, 'r-', linewidth=2.5,
218
label=f'Optimal: $z = {z_optimal:.1f}$')
219
220
# Plot vertices
221
for i, v in enumerate(vertices):
222
if all(v >= 0):
223
obj_val = c @ v
224
ax.plot(v[0], v[1], 'ko', markersize=8)
225
ax.annotate(f'({v[0]:.1f}, {v[1]:.1f})\\n$z={obj_val:.1f}$',
226
xy=(v[0], v[1]), xytext=(v[0]+1.5, v[1]+2),
227
fontsize=9, ha='left')
228
229
# Optimal solution
230
ax.plot(x_optimal[0], x_optimal[1], 'r*', markersize=20,
231
label=f'Optimal: $x_1={x_optimal[0]:.2f}$, $x_2={x_optimal[1]:.2f}$')
232
233
ax.set_xlabel('$x_1$ (Tables)', fontsize=12)
234
ax.set_ylabel('$x_2$ (Chairs)', fontsize=12)
235
ax.set_title('Graphical Solution: Production Planning LP', fontsize=14)
236
ax.set_xlim(-2, 28)
237
ax.set_ylim(-2, 45)
238
ax.grid(True, alpha=0.3)
239
ax.legend(loc='upper right', fontsize=9)
240
ax.axhline(y=0, color='k', linewidth=0.5)
241
ax.axvline(x=0, color='k', linewidth=0.5)
242
243
plt.tight_layout()
244
plt.savefig('linear_programming_graphical_solution.pdf', dpi=150, bbox_inches='tight')
245
plt.close()
246
\end{pycode}
247
248
\begin{figure}[H]
249
\centering
250
\includegraphics[width=0.95\textwidth]{linear_programming_graphical_solution.pdf}
251
\caption{Graphical solution of the two-dimensional production planning linear program showing
252
the feasible region (yellow polygon) bounded by labor and wood constraints, iso-profit lines
253
(red dashed) representing constant objective values, and the optimal solution at the intersection
254
of active constraints. The optimal vertex yields maximum profit $z^* = \py{f"{z_optimal:.2f}"}$
255
by producing $x_1^* = \py{f"{x_optimal[0]:.2f}"}$ tables and $x_2^* = \py{f"{x_optimal[1]:.2f}"}$ chairs,
256
demonstrating the fundamental theorem that optimality occurs at an extreme point of the polytope.}
257
\end{figure}
258
259
\subsection{Simplex Tableau Evolution}
260
261
The simplex algorithm iteratively improves the solution by pivoting through basic feasible solutions.
262
We demonstrate the tableau method for a standard form LP. Converting our problem to standard form
263
by introducing slack variables $s_1, s_2 \geq 0$:
264
265
\begin{equation}
266
\begin{aligned}
267
\text{maximize} \quad & z = 30x_1 + 20x_2 + 0s_1 + 0s_2 \\
268
\text{subject to} \quad & 4x_1 + 2x_2 + s_1 = 80 \\
269
& 3x_1 + 2x_2 + s_2 = 60 \\
270
& x_1, x_2, s_1, s_2 \geq 0
271
\end{aligned}
272
\end{equation}
273
274
\begin{pycode}
275
# Simplex tableau implementation
276
def simplex_iteration(tableau, basis):
277
"""Perform one iteration of simplex method"""
278
m, n = tableau.shape
279
m -= 1 # Exclude objective row
280
281
# Check optimality (all reduced costs <= 0 for maximization)
282
if np.all(tableau[-1, :-1] <= 1e-10):
283
return tableau, basis, True
284
285
# Select entering variable (most positive reduced cost)
286
entering = np.argmax(tableau[-1, :-1])
287
288
# Select leaving variable (minimum ratio test)
289
ratios = []
290
for i in range(m):
291
if tableau[i, entering] > 1e-10:
292
ratios.append(tableau[i, -1] / tableau[i, entering])
293
else:
294
ratios.append(np.inf)
295
leaving = np.argmin(ratios)
296
297
# Update basis
298
basis[leaving] = entering
299
300
# Pivot operation
301
pivot = tableau[leaving, entering]
302
tableau[leaving, :] /= pivot
303
304
for i in range(m + 1):
305
if i != leaving:
306
tableau[i, :] -= tableau[i, entering] * tableau[leaving, :]
307
308
return tableau, basis, False
309
310
# Initial tableau setup
311
# Variables: x1, x2, s1, s2, RHS
312
initial_tableau = np.array([
313
[4.0, 2.0, 1.0, 0.0, 80.0], # Labor constraint
314
[3.0, 2.0, 0.0, 1.0, 60.0], # Wood constraint
315
[-30.0, -20.0, 0.0, 0.0, 0.0] # Objective (negated for maximization)
316
], dtype=float)
317
318
basis = [2, 3] # Initial basis: s1, s2
319
tableaus = [initial_tableau.copy()]
320
iteration = 0
321
322
tableau = initial_tableau.copy()
323
optimal = False
324
while not optimal and iteration < 10:
325
tableau, basis, optimal = simplex_iteration(tableau, basis)
326
tableaus.append(tableau.copy())
327
iteration += 1
328
329
# Visualization of tableau evolution
330
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
331
axes = axes.flatten()
332
333
var_names = ['$x_1$', '$x_2$', '$s_1$', '$s_2$', 'RHS']
334
335
for idx, (ax, tab) in enumerate(zip(axes, tableaus[:4])):
336
# Display tableau as table
337
ax.axis('tight')
338
ax.axis('off')
339
340
# Create table data
341
table_data = []
342
for i in range(tab.shape[0] - 1):
343
row = [f'{val:.2f}' for val in tab[i, :]]
344
table_data.append(row)
345
346
# Objective row
347
obj_row = [f'{val:.2f}' for val in tab[-1, :]]
348
table_data.append(obj_row)
349
350
# Row labels
351
row_labels = [f'Row {i+1}' for i in range(tab.shape[0] - 1)] + ['$z$']
352
353
table = ax.table(cellText=table_data, colLabels=var_names,
354
rowLabels=row_labels, cellLoc='center',
355
loc='center', bbox=[0, 0, 1, 1])
356
table.auto_set_font_size(False)
357
table.set_fontsize(9)
358
table.scale(1, 2)
359
360
# Color code
361
for i in range(len(row_labels)):
362
table[(i, -1)].set_facecolor('#E8E8E8')
363
for j in range(len(var_names)):
364
table[(0, j)].set_facecolor('#40466e')
365
table[(0, j)].set_text_props(weight='bold', color='white')
366
367
if idx == 0:
368
ax.set_title('Initial Tableau (Iteration 0)', fontsize=12, weight='bold')
369
elif idx < len(tableaus) - 1:
370
ax.set_title(f'Iteration {idx}', fontsize=12, weight='bold')
371
else:
372
ax.set_title(f'Optimal Tableau (Iteration {idx})', fontsize=12, weight='bold',
373
color='darkgreen')
374
375
plt.tight_layout()
376
plt.savefig('linear_programming_simplex_tableaus.pdf', dpi=150, bbox_inches='tight')
377
plt.close()
378
\end{pycode}
379
380
\begin{figure}[H]
381
\centering
382
\includegraphics[width=0.95\textwidth]{linear_programming_simplex_tableaus.pdf}
383
\caption{Evolution of simplex tableaus from initial basic feasible solution to optimal solution.
384
Each iteration performs a pivot operation, exchanging a non-basic variable (entering) with a basic
385
variable (leaving) to move to an adjacent vertex with improved objective value. The bottom row shows
386
reduced costs; optimality is reached when all reduced costs are non-positive for a maximization problem.
387
The algorithm converges in $\py{len(tableaus)-1}$ iterations, demonstrating the finite convergence
388
property of the simplex method on this bounded linear program.}
389
\end{figure}
390
391
\section{Duality and Sensitivity Analysis}
392
393
\subsection{Dual Problem Formulation}
394
395
The dual of our primal production planning problem provides economic interpretation through
396
shadow prices. The dual variables $y_1$ and $y_2$ represent the marginal value of each
397
resource (labor and wood).
398
399
The dual problem is:
400
\begin{equation}
401
\begin{aligned}
402
\text{minimize} \quad & w = 80y_1 + 60y_2 \\
403
\text{subject to} \quad & 4y_1 + 3y_2 \geq 30 \quad \text{(table)} \\
404
& 2y_1 + 2y_2 \geq 20 \quad \text{(chair)} \\
405
& y_1, y_2 \geq 0
406
\end{aligned}
407
\end{equation}
408
409
Before proceeding with sensitivity analysis, we present the dual problem formulation and
410
its economic interpretation. The shadow prices indicate how much the objective would improve
411
per unit increase in each resource constraint, assuming the current basis remains optimal.
412
413
\begin{pycode}
414
# Solve dual problem
415
c_dual = b # Dual objective = primal RHS
416
A_dual = -A.T # Dual constraints = transpose of primal
417
b_dual = -c # Dual RHS = negative primal objective
418
419
result_dual = linprog(c_dual, A_ub=A_dual, b_ub=b_dual,
420
bounds=[(0, None), (0, None)], method='highs')
421
y_optimal = result_dual.x
422
w_optimal = result_dual.fun
423
424
# Verify strong duality
425
print(f"Primal optimal: z* = {z_optimal:.4f}")
426
print(f"Dual optimal: w* = {w_optimal:.4f}")
427
print(f"Duality gap: {abs(z_optimal - w_optimal):.2e}")
428
429
# Shadow prices from primal solution (should equal dual solution)
430
print(f"Shadow prices from primal: {shadow_prices}")
431
print(f"Dual solution y*: {y_optimal}")
432
433
# Sensitivity analysis - varying RHS parameters
434
b1_range = np.linspace(60, 100, 50) # Labor variation
435
b2_range = np.linspace(40, 80, 50) # Wood variation
436
437
z_labor_variation = []
438
z_wood_variation = []
439
440
for b1_new in b1_range:
441
res = linprog(-c, A_ub=A, b_ub=[b1_new, b[1]],
442
bounds=[(0, None), (0, None)], method='highs')
443
z_labor_variation.append(-res.fun if res.success else np.nan)
444
445
for b2_new in b2_range:
446
res = linprog(-c, A_ub=A, b_ub=[b[0], b2_new],
447
bounds=[(0, None), (0, None)], method='highs')
448
z_wood_variation.append(-res.fun if res.success else np.nan)
449
450
# Sensitivity visualization
451
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
452
453
# Plot 1: RHS sensitivity for labor
454
ax1 = axes[0, 0]
455
ax1.plot(b1_range, z_labor_variation, 'b-', linewidth=2.5,
456
label='Optimal profit')
457
# Linear approximation using shadow price
458
z_linear_labor = z_optimal + shadow_prices[0] * (b1_range - b[0])
459
ax1.plot(b1_range, z_linear_labor, 'r--', linewidth=2,
460
label=f'Linear approx. (shadow price = {shadow_prices[0]:.2f})')
461
ax1.axvline(x=b[0], color='gray', linestyle=':', alpha=0.7,
462
label=f'Current value = {b[0]}')
463
ax1.set_xlabel('Labor hours available', fontsize=11)
464
ax1.set_ylabel('Optimal profit (\$)', fontsize=11)
465
ax1.set_title('Sensitivity to Labor Constraint', fontsize=12, weight='bold')
466
ax1.legend(fontsize=9)
467
ax1.grid(True, alpha=0.3)
468
469
# Plot 2: RHS sensitivity for wood
470
ax2 = axes[0, 1]
471
ax2.plot(b2_range, z_wood_variation, 'g-', linewidth=2.5,
472
label='Optimal profit')
473
z_linear_wood = z_optimal + shadow_prices[1] * (b2_range - b[1])
474
ax2.plot(b2_range, z_linear_wood, 'r--', linewidth=2,
475
label=f'Linear approx. (shadow price = {shadow_prices[1]:.2f})')
476
ax2.axvline(x=b[1], color='gray', linestyle=':', alpha=0.7,
477
label=f'Current value = {b[1]}')
478
ax2.set_xlabel('Wood board-feet available', fontsize=11)
479
ax2.set_ylabel('Optimal profit (\$)', fontsize=11)
480
ax2.set_title('Sensitivity to Wood Constraint', fontsize=12, weight='bold')
481
ax2.legend(fontsize=9)
482
ax2.grid(True, alpha=0.3)
483
484
# Plot 3: Objective coefficient sensitivity for x1
485
ax3 = axes[1, 0]
486
c1_range = np.linspace(15, 45, 50)
487
z_c1_variation = []
488
x1_c1_variation = []
489
490
for c1_new in c1_range:
491
res = linprog(-np.array([c1_new, c[1]]), A_ub=A, b_ub=b,
492
bounds=[(0, None), (0, None)], method='highs')
493
z_c1_variation.append(-res.fun if res.success else np.nan)
494
x1_c1_variation.append(res.x[0] if res.success else np.nan)
495
496
ax3_twin = ax3.twinx()
497
line1 = ax3.plot(c1_range, z_c1_variation, 'b-', linewidth=2.5,
498
label='Optimal profit')
499
ax3.axvline(x=c[0], color='gray', linestyle=':', alpha=0.7)
500
ax3.set_xlabel('Profit per table $c_1$ (\$)', fontsize=11)
501
ax3.set_ylabel('Optimal profit (\$)', fontsize=11, color='blue')
502
ax3.tick_params(axis='y', labelcolor='blue')
503
504
line2 = ax3_twin.plot(c1_range, x1_c1_variation, 'r--', linewidth=2,
505
label='Tables produced')
506
ax3_twin.set_ylabel('Optimal $x_1$ (tables)', fontsize=11, color='red')
507
ax3_twin.tick_params(axis='y', labelcolor='red')
508
ax3.set_title('Sensitivity to Table Profit Coefficient', fontsize=12, weight='bold')
509
ax3.grid(True, alpha=0.3)
510
511
# Combined legend
512
lines1, labels1 = ax3.get_legend_handles_labels()
513
lines2, labels2 = ax3_twin.get_legend_handles_labels()
514
ax3.legend(lines1 + lines2, labels1 + labels2, loc='upper left', fontsize=9)
515
516
# Plot 4: 2D sensitivity region
517
ax4 = axes[1, 1]
518
c1_2d = np.linspace(20, 40, 30)
519
c2_2d = np.linspace(10, 30, 30)
520
C1_mesh, C2_mesh = np.meshgrid(c1_2d, c2_2d)
521
Z_mesh = np.zeros_like(C1_mesh)
522
523
for i in range(len(c1_2d)):
524
for j in range(len(c2_2d)):
525
res = linprog(-np.array([c1_2d[i], c2_2d[j]]), A_ub=A, b_ub=b,
526
bounds=[(0, None), (0, None)], method='highs')
527
Z_mesh[j, i] = -res.fun if res.success else np.nan
528
529
cs = ax4.contourf(C1_mesh, C2_mesh, Z_mesh, levels=15, cmap='viridis')
530
ax4.plot(c[0], c[1], 'r*', markersize=20, label='Current coefficients')
531
cbar = plt.colorbar(cs, ax=ax4)
532
cbar.set_label('Optimal profit (\$)', fontsize=10)
533
ax4.set_xlabel('Table profit $c_1$ (\$)', fontsize=11)
534
ax4.set_ylabel('Chair profit $c_2$ (\$)', fontsize=11)
535
ax4.set_title('Objective Coefficient Sensitivity Map', fontsize=12, weight='bold')
536
ax4.legend(fontsize=9)
537
ax4.grid(True, alpha=0.3, color='white', linewidth=0.5)
538
539
plt.tight_layout()
540
plt.savefig('linear_programming_sensitivity_analysis.pdf', dpi=150, bbox_inches='tight')
541
plt.close()
542
\end{pycode}
543
544
\begin{figure}[H]
545
\centering
546
\includegraphics[width=0.98\textwidth]{linear_programming_sensitivity_analysis.pdf}
547
\caption{Comprehensive sensitivity analysis examining the robustness of the optimal solution to
548
parameter perturbations. Top row shows the linear relationship between resource availability
549
(labor and wood) and optimal profit, with shadow prices representing the marginal value of each
550
resource within the valid range. Bottom left illustrates how objective coefficient changes affect
551
both profit and production decisions. Bottom right presents a two-dimensional contour map showing
552
optimal profit as a function of both objective coefficients simultaneously. Shadow prices are
553
$y_1^* = \py{f"{shadow_prices[0]:.3f}"}$ per labor hour and $y_2^* = \py{f"{shadow_prices[1]:.3f}"}$
554
per board-foot of wood, indicating which resource constraint is more valuable to relax.}
555
\end{figure}
556
557
\section{Applications and Extensions}
558
559
\subsection{Transportation Problem}
560
561
Linear programming naturally models transportation and logistics problems. We demonstrate
562
a small-scale transportation problem with 3 suppliers and 3 customers, minimizing total
563
shipping costs while satisfying supply and demand constraints.
564
565
\begin{pycode}
566
# Transportation problem setup
567
# Supply from 3 warehouses: [100, 150, 120]
568
# Demand at 3 stores: [80, 130, 160]
569
# Cost matrix ($/unit shipped from warehouse i to store j)
570
571
supply = np.array([100, 150, 120])
572
demand = np.array([80, 130, 160])
573
cost_matrix = np.array([
574
[8, 6, 10], # Warehouse 1 to stores 1,2,3
575
[9, 12, 13], # Warehouse 2 to stores 1,2,3
576
[14, 9, 16] # Warehouse 3 to stores 1,2,3
577
])
578
579
# Formulate as LP: minimize sum(c_ij * x_ij)
580
# Variables: x_ij = units shipped from warehouse i to store j
581
n_warehouses = len(supply)
582
n_stores = len(demand)
583
n_vars = n_warehouses * n_stores
584
585
# Objective coefficients (flatten cost matrix)
586
c_transport = cost_matrix.flatten()
587
588
# Supply constraints: sum_j x_ij <= supply_i
589
A_supply = np.zeros((n_warehouses, n_vars))
590
for i in range(n_warehouses):
591
for j in range(n_stores):
592
A_supply[i, i * n_stores + j] = 1
593
b_supply = supply
594
595
# Demand constraints: sum_i x_ij >= demand_j (convert to <=)
596
A_demand = np.zeros((n_stores, n_vars))
597
for j in range(n_stores):
598
for i in range(n_warehouses):
599
A_demand[j, i * n_stores + j] = -1
600
b_demand = -demand
601
602
# Combine constraints
603
A_transport = np.vstack([A_supply, A_demand])
604
b_transport = np.hstack([b_supply, b_demand])
605
606
# Solve
607
result_transport = linprog(c_transport, A_ub=A_transport, b_ub=b_transport,
608
bounds=[(0, None)] * n_vars, method='highs')
609
x_transport = result_transport.x.reshape(n_warehouses, n_stores)
610
total_cost = result_transport.fun
611
612
# Visualization of transportation solution
613
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
614
615
# Plot 1: Transportation flow diagram
616
ax1 = axes[0]
617
ax1.axis('off')
618
ax1.set_xlim(0, 10)
619
ax1.set_ylim(0, 10)
620
621
# Draw warehouses (left side)
622
warehouse_y = np.linspace(2, 8, n_warehouses)
623
for i, y in enumerate(warehouse_y):
624
circle = plt.Circle((2, y), 0.4, color='steelblue', alpha=0.7)
625
ax1.add_patch(circle)
626
ax1.text(2, y, f'W{i+1}\\n{supply[i]}', ha='center', va='center',
627
fontsize=9, weight='bold', color='white')
628
629
# Draw stores (right side)
630
store_y = np.linspace(2, 8, n_stores)
631
for j, y in enumerate(store_y):
632
circle = plt.Circle((8, y), 0.4, color='coral', alpha=0.7)
633
ax1.add_patch(circle)
634
ax1.text(8, y, f'S{j+1}\\n{demand[j]}', ha='center', va='center',
635
fontsize=9, weight='bold', color='white')
636
637
# Draw flows
638
max_flow = x_transport.max()
639
for i in range(n_warehouses):
640
for j in range(n_stores):
641
if x_transport[i, j] > 0.1:
642
width = 0.5 + 3 * (x_transport[i, j] / max_flow)
643
ax1.arrow(2.5, warehouse_y[i], 4.9, store_y[j] - warehouse_y[i],
644
head_width=0.2, head_length=0.2, fc='green', ec='green',
645
linewidth=width, alpha=0.6)
646
mid_x = 5
647
mid_y = (warehouse_y[i] + store_y[j]) / 2
648
ax1.text(mid_x, mid_y, f'{x_transport[i,j]:.1f}',
649
fontsize=8, ha='center',
650
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
651
652
ax1.set_title('Optimal Transportation Flow', fontsize=12, weight='bold')
653
ax1.text(2, 9, 'Warehouses\\n(Supply)', ha='center', fontsize=10, weight='bold')
654
ax1.text(8, 9, 'Stores\\n(Demand)', ha='center', fontsize=10, weight='bold')
655
656
# Plot 2: Cost matrix heatmap with solution overlay
657
ax2 = axes[1]
658
im = ax2.imshow(x_transport, cmap='YlGn', aspect='auto')
659
ax2.set_xticks(np.arange(n_stores))
660
ax2.set_yticks(np.arange(n_warehouses))
661
ax2.set_xticklabels([f'Store {j+1}' for j in range(n_stores)])
662
ax2.set_yticklabels([f'Warehouse {i+1}' for i in range(n_warehouses)])
663
ax2.set_xlabel('Destination', fontsize=11)
664
ax2.set_ylabel('Origin', fontsize=11)
665
ax2.set_title('Shipment Quantities with Unit Costs', fontsize=12, weight='bold')
666
667
# Add text annotations
668
for i in range(n_warehouses):
669
for j in range(n_stores):
670
text = ax2.text(j, i, f'{x_transport[i,j]:.1f}\\n(\${cost_matrix[i,j]})',
671
ha='center', va='center', color='black', fontsize=9)
672
673
cbar = plt.colorbar(im, ax=ax2)
674
cbar.set_label('Units shipped', fontsize=10)
675
676
plt.tight_layout()
677
plt.savefig('linear_programming_transportation.pdf', dpi=150, bbox_inches='tight')
678
plt.close()
679
\end{pycode}
680
681
\begin{figure}[H]
682
\centering
683
\includegraphics[width=0.98\textwidth]{linear_programming_transportation.pdf}
684
\caption{Optimal solution to the transportation problem showing flow diagram (left) with arrow
685
widths proportional to shipment quantities and heatmap (right) displaying optimal allocation
686
with unit costs. The solution minimizes total transportation cost of \$\py{f"{total_cost:.2f}"}
687
while satisfying all supply constraints at warehouses and demand requirements at stores. This
688
classical application demonstrates LP's power in logistics optimization, where the dual variables
689
provide shadow prices indicating marginal costs of increasing supply or demand at each location.}
690
\end{figure}
691
692
\section{Results Summary}
693
694
\subsection{Optimal Solutions}
695
696
\begin{pycode}
697
print(r"\begin{table}[H]")
698
print(r"\centering")
699
print(r"\caption{Optimal Solutions for LP Problems}")
700
print(r"\begin{tabular}{lccc}")
701
print(r"\toprule")
702
print(r"Problem & Variables & Objective Value & Method \\")
703
print(r"\midrule")
704
print(f"Production Planning & $x_1^* = {x_optimal[0]:.2f}$, $x_2^* = {x_optimal[1]:.2f}$ & "
705
f"\${z_optimal:.2f} & Simplex \\\\")
706
print(f"Dual Problem & $y_1^* = {y_optimal[0]:.2f}$, $y_2^* = {y_optimal[1]:.2f}$ & "
707
f"\${w_optimal:.2f} & Dual Simplex \\\\")
708
print(f"Transportation & Total shipment = {np.sum(x_transport):.1f} units & "
709
f"\${total_cost:.2f} & Interior Point \\\\")
710
print(r"\bottomrule")
711
print(r"\end{tabular}")
712
print(r"\label{tab:solutions}")
713
print(r"\end{table}")
714
\end{pycode}
715
716
\subsection{Shadow Prices and Economic Interpretation}
717
718
\begin{pycode}
719
print(r"\begin{table}[H]")
720
print(r"\centering")
721
print(r"\caption{Sensitivity Analysis: Shadow Prices and Reduced Costs}")
722
print(r"\begin{tabular}{lccl}")
723
print(r"\toprule")
724
print(r"Resource/Variable & Shadow Price & Reduced Cost & Interpretation \\")
725
print(r"\midrule")
726
print(f"Labor (hours) & \${shadow_prices[0]:.3f} & --- & "
727
r"Value of 1 additional labor hour \\")
728
print(f"Wood (board-ft) & \${shadow_prices[1]:.3f} & --- & "
729
r"Value of 1 additional board-foot \\")
730
print(f"$x_1$ (tables) & --- & \${reduced_costs[0]:.3f} & "
731
r"At optimal basis \\")
732
print(f"$x_2$ (chairs) & --- & \${reduced_costs[1]:.3f} & "
733
r"At optimal basis \\")
734
print(r"\bottomrule")
735
print(r"\end{tabular}")
736
print(r"\label{tab:sensitivity}")
737
print(r"\end{table}")
738
\end{pycode}
739
740
\section{Discussion}
741
742
\begin{example}[Economic Interpretation of Shadow Prices]
743
The shadow price for labor ($y_1^* = \py{f"{shadow_prices[0]:.3f}"}$) indicates that each
744
additional hour of labor would increase profit by approximately \$\py{f"{shadow_prices[0]:.2f}"},
745
assuming the current basis remains optimal. Similarly, the shadow price for wood
746
($y_2^* = \py{f"{shadow_prices[1]:.3f}"}$) values each additional board-foot at
747
\$\py{f"{shadow_prices[1]:.2f}"}. These values guide resource acquisition decisions: if labor
748
costs less than \$\py{f"{shadow_prices[0]:.2f}"}/hour or wood costs less than
749
\$\py{f"{shadow_prices[1]:.2f}"}/board-foot, purchasing additional resources increases profitability.
750
\end{example}
751
752
\begin{remark}[Complementary Slackness Verification]
753
At the optimal solution, active constraints (labor and wood) have positive shadow prices,
754
while slack variables have zero reduced costs. Non-binding constraints would have zero shadow
755
prices, and variables at their bounds have non-zero reduced costs indicating the objective
756
deterioration per unit forced into the basis.
757
\end{remark}
758
759
\subsection{Computational Complexity}
760
761
The simplex algorithm's worst-case complexity is exponential in the number of variables,
762
but it typically performs very well in practice with polynomial average-case behavior.
763
Interior point methods guarantee polynomial-time complexity $O(n^{3.5})$ for problems
764
with $n$ variables, making them preferable for very large-scale applications.
765
766
\section{Conclusions}
767
768
This comprehensive analysis demonstrates linear programming methodology and implementation:
769
770
\begin{enumerate}
771
\item The production planning problem achieves optimal profit $z^* = \py{f"{z_optimal:.2f}"}$
772
by producing $x_1^* = \py{f"{x_optimal[0]:.2f}"}$ tables and $x_2^* = \py{f"{x_optimal[1]:.2f}"}$ chairs.
773
774
\item The simplex algorithm converges in $\py{len(tableaus)-1}$ iterations by systematically
775
exploring vertices of the feasible polytope until reaching optimality.
776
777
\item Strong duality is verified: primal and dual optimal values are equal
778
($z^* = w^* = \py{f"{z_optimal:.2f}"}$), confirming the fundamental duality theorem.
779
780
\item Shadow prices ($y_1^* = \py{f"{shadow_prices[0]:.3f}"}$,
781
$y_2^* = \py{f"{shadow_prices[1]:.3f}"}$) quantify the marginal value of relaxing resource constraints.
782
783
\item The transportation application demonstrates LP versatility in logistics optimization,
784
achieving minimum cost \$\py{f"{total_cost:.2f}"} while satisfying all supply-demand constraints.
785
786
\item Sensitivity analysis reveals the range over which the current basis remains optimal,
787
guiding robust decision-making under parameter uncertainty.
788
\end{enumerate}
789
790
Linear programming provides a powerful framework for optimization problems with linear objectives
791
and constraints, with applications spanning production planning, transportation, finance, scheduling,
792
and resource allocation.
793
794
\section*{Further Reading}
795
796
\begin{itemize}
797
\item Bertsimas, D., \& Tsitsiklis, J. N. \textit{Introduction to Linear Optimization}. Athena Scientific, 1997.
798
\item Dantzig, G. B. \textit{Linear Programming and Extensions}. Princeton University Press, 1963.
799
\item Vanderbei, R. J. \textit{Linear Programming: Foundations and Extensions}, 5th ed. Springer, 2020.
800
\item Chvátal, V. \textit{Linear Programming}. W. H. Freeman, 1983.
801
\item Bazaraa, M. S., Jarvis, J. J., \& Sherali, H. D. \textit{Linear Programming and Network Flows}, 4th ed. Wiley, 2010.
802
\item Luenberger, D. G., \& Ye, Y. \textit{Linear and Nonlinear Programming}, 4th ed. Springer, 2016.
803
\item Nocedal, J., \& Wright, S. J. \textit{Numerical Optimization}, 2nd ed. Springer, 2006.
804
\item Papadimitriou, C. H., \& Steiglitz, K. \textit{Combinatorial Optimization: Algorithms and Complexity}. Dover, 1998.
805
\item Schrijver, A. \textit{Theory of Linear and Integer Programming}. Wiley, 1998.
806
\item Winston, W. L., \& Goldberg, J. B. \textit{Operations Research: Applications and Algorithms}, 4th ed. Thomson, 2004.
807
\item Hillier, F. S., \& Lieberman, G. J. \textit{Introduction to Operations Research}, 10th ed. McGraw-Hill, 2015.
808
\item Taha, H. A. \textit{Operations Research: An Introduction}, 10th ed. Pearson, 2017.
809
\item Wolsey, L. A. \textit{Integer Programming}, 2nd ed. Wiley, 2020.
810
\item Karmarkar, N. ``A new polynomial-time algorithm for linear programming.'' \textit{Combinatorica} 4.4 (1984): 373-395.
811
\item Klee, V., \& Minty, G. J. ``How good is the simplex algorithm?'' \textit{Inequalities} 3 (1972): 159-175.
812
\end{itemize}
813
814
\end{document}
815
816