Simplex Tableau
Interactive visualizations for Chapter 4 of the Ricerca Operativa notes
The Furniture Workshop LP
We apply the Simplex method to the same LP studied graphically in Chapter 2:
\[ \begin{aligned} & \text{maximize} && 30\,x_1 + 50\,x_2 \\ & \text{subject to} && 2\,x_1 + 5\,x_2 \leq 40 && \text{(wood)} \\ & && 4\,x_1 + 2\,x_2 \leq 32 && \text{(labour)} \\ & && x_1, x_2 \geq 0. \end{aligned} \]
Introducing slack variables \(s_1, s_2 \geq 0\) converts the inequalities to equalities:
\[ \begin{aligned} 2x_1 + 5x_2 + s_1 &= 40 \\ 4x_1 + 2x_2 + s_2 &= 32 \end{aligned} \]
The Simplex method starts at a basic feasible solution (BFS) and moves along edges of the feasible polytope, improving the objective at each step, until no improving direction exists.
Iteration 0: Initial Tableau
The initial basis is \(\{s_1, s_2\}\). Setting the non-basic variables \(x_1 = x_2 = 0\) gives the BFS \((0, 0)\) with \(z = 0\).
**Pivot step:** divide row 1 by 5 (the pivot element), then eliminate $x_2$ from all other rows.
## Iteration 1: After First Pivot
Pivoting $x_2$ into the basis (replacing $s_1$) gives the updated tableau.
**Pivot step:** divide row 2 by $16/5$, then eliminate $x_1$ from all other rows.
## Iteration 2: Optimal Tableau
Pivoting $x_1$ into the basis (replacing $s_2$) gives the final tableau.
Both binding constraints ($2x_1+5x_2=40$ and $4x_1+2x_2=32$) are active at the optimum, confirming that all resources are fully utilised.
## Pivot Sequence Visualized
The three BFS vertices visited by the Simplex method, connected in order.
::: {.cell}
```{.pyodide .cell-code}
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon, FancyArrowPatch
import matplotlib.patheffects as pe
fig, ax = plt.subplots(figsize=(9, 8))
# Feasible region
vertices = np.array([[0, 0], [8, 0], [5, 6], [0, 8]])
poly = Polygon(vertices, closed=True, facecolor='#cce5ff',
edgecolor='#3366cc', linewidth=2.5, alpha=0.5,
label='Feasible region $P$')
ax.add_patch(poly)
# Constraint lines
x1_line = np.linspace(-0.5, 11, 300)
ax.plot(x1_line, (40 - 2*x1_line)/5, 'b-', lw=1.5, alpha=0.5,
label='$2x_1+5x_2=40$')
ax.plot(x1_line, (32 - 4*x1_line)/2, 'r-', lw=1.5, alpha=0.5,
label='$4x_1+2x_2=32$')
# Optimal isoline
ax.plot(x1_line, (450 - 30*x1_line)/50, '--', color='#cc5500', lw=2, alpha=0.7,
label='$z^*=450$ isoline')
# Non-path vertex
ax.plot(8, 0, 'o', color='#888888', markersize=9, zorder=4,
markeredgecolor='white', markeredgewidth=1.5)
ax.annotate('$(8,0)$\nnot visited', (8, 0), (9.2, -0.9),
fontsize=9, color='#888888',
arrowprops=dict(arrowstyle='->', color='#aaaaaa', lw=1.2))
# ── Simplex path ────────────────────────────────────────────────────────────
path_pts = [(0, 0), (0, 8), (5, 6)]
path_z = [0, 400, 450]
path_colors = ['#3366cc', '#ff9900', '#8b00ff']
path_labels = ['Start: $(0,\\ 0)$\n$z = 0$',
'Iter 1: $(0,\\ 8)$\n$z = 400$',
'Optimal: $(5,\\ 6)$\n$z^* = 450$']
path_label_offsets = [(-2.2, -1.2), (-2.8, 0.5), (1.0, 1.0)]
# Draw arrows between consecutive BFS vertices
arrow_style = dict(arrowstyle='->', lw=2.8, mutation_scale=20)
for k in range(len(path_pts) - 1):
x_s, y_s = path_pts[k]
x_e, y_e = path_pts[k+1]
ax.annotate('', xy=(x_e, y_e), xytext=(x_s, y_s),
arrowprops=dict(arrowstyle='->', color='#e67300', lw=3.0,
connectionstyle='arc3,rad=0.18'))
# Draw BFS vertices
sizes = [12, 12, 22]
shapes = ['o', 'o', '*']
for pt, z, col, lbl, off, sz, sh in zip(
path_pts, path_z, path_colors, path_labels,
path_label_offsets, sizes, shapes):
ax.plot(pt[0], pt[1], sh, color=col, markersize=sz, zorder=6,
markeredgecolor='white', markeredgewidth=2)
fc = '#f0e0ff' if col == '#8b00ff' else ('#fff3cd' if col == '#ff9900'
else '#e6eeff')
ax.annotate(lbl,
(pt[0], pt[1]),
(pt[0] + off[0], pt[1] + off[1]),
fontsize=10, fontweight='bold', color=col,
bbox=dict(boxstyle='round,pad=0.35', facecolor=fc,
edgecolor=col, alpha=0.95),
arrowprops=dict(arrowstyle='->', color=col, lw=1.6))
# Iteration labels along the arrows
ax.text(-1.3, 4.0, 'Pivot 1\n$x_2$ enters\n$s_1$ leaves', fontsize=9,
color='#e67300', ha='center', style='italic',
bbox=dict(boxstyle='round,pad=0.2', facecolor='#fff8ee',
edgecolor='#ffcc80', alpha=0.85))
ax.text(3.2, 8.2, 'Pivot 2\n$x_1$ enters\n$s_2$ leaves', fontsize=9,
color='#e67300', ha='center', style='italic',
bbox=dict(boxstyle='round,pad=0.2', facecolor='#fff8ee',
edgecolor='#ffcc80', alpha=0.85))
ax.set_xlim(-2.5, 11.5)
ax.set_ylim(-2.0, 10.8)
ax.set_xlabel('$x_1$', fontsize=13)
ax.set_ylabel('$x_2$', fontsize=13)
ax.set_title('Simplex Path: $(0,0) \\to (0,8) \\to (5,6)$', fontsize=14,
fontweight='bold')
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
ax.legend(fontsize=9, loc='upper right')
ax.axhline(0, color='black', lw=0.8)
ax.axvline(0, color='black', lw=0.8)
plt.tight_layout()
plt.show()
:::
The Simplex method traverses the edges of the feasible polytope, moving only to adjacent vertices with a strictly better objective value. It never revisits a vertex (assuming non-degeneracy) and terminates in at most finitely many steps.
Simplex Path Summary Table
Conclusion: The Simplex method reaches the optimum in just two pivot operations, visiting vertices \((0,0) \to (0,8) \to (5,6)\). The optimal solution is \(x_1^* = 5\) chairs, \(x_2^* = 6\) tables, with profit \(z^* = \mathbf{450}\).
TipSummary: Simplex Tableau
- Bases & Feasibility: The simplex algorithm moves from one Basic Feasible Solution (BFS) to an adjacent one.
- Pivoting: We identify the entering variable (column with positive reduced cost) and the leaving variable (minimum ratio test).
- Degeneracy: Can cause cycling (loops). Use Bland’s rule (smallest index) to guarantee convergence.