Interactive Simplex Tableau

Try me

Open In ColabBinder

Introduction

Interactive Simplex Tableau Activity

In this activity, we will walk through the Simplex algorithm step by step, exactly as it is done on the board, but using an interactive computational notebook to make each transformation explicit and traceable.

Rather than treating Simplex as a “black box” that jumps directly to the optimal solution, this notebook lets you control the algorithm iteration by iteration: selecting the entering variable from the z-row, applying the ratio test (with ratios shown explicitly), performing the pivot operation, and observing how the tableau evolves at each step.

At every iteration, the notebook:

  • displays the current tableau in a readable form,

  • briefly explains why each decision is made,

  • and pauses execution so you can reflect before moving on.

The goal is not speed, but understanding. By interacting with the tableau and seeing how algebraic operations translate into algorithmic steps, you will build an intuition for:

  • why the Simplex method works,

  • how feasibility and optimality are maintained,

  • and how local decisions in the tableau drive the global optimization process.

Think of this notebook as a guided conversation with the Simplex algorithm: you ask it to take the next step, and it shows you exactly what it is doing and why.

How to Use This Notebook

Instructions

Please follow this workflow:

  1. Run cells from top to bottom Make sure you execute each cell in order so that the Simplex functions and the initial tableau are properly defined, it is important that you run the first code cell that defines the SimplesStepper class we will use to run the Simplex.

  2. Read before running the next step After each Simplex iteration, the notebook explains what decision is being made (entering variable, ratio test, pivot). Take a moment to understand why that step is valid.

  3. Advance one iteration at a time The algorithm pauses after each iteration. Press Enter only when you are ready to move on.

  4. Focus on the logic, not the arithmetic The goal is not to memorize row operations, but to understand:

    • why a variable enters the basis,

    • why another one leaves,

    • and how these choices move the solution toward optimality. Try to predict the understand the decisions.

  5. Ask yourself at each step

    • Why is this variable entering?

    • What does the ratio test guarantee?

    • How is feasibility preserved after the pivot?

    • Remember what happens with the Simplex, the analogies with the graphical method.

If something feels unclear, stop and discuss it — this notebook is meant to support reasoning, not to rush to the final answer. ### Pre-requirements #### Google Colabs You do not need to install anything, everything works off-the-shelf in Google Colabs #### Binder In a fresh Jupyter Notebook environment, you need to install Numpy and Pandas. Create a new cell and run this code:

!pip install pandas
!pip install numpy

Simplex Stepper Initialization

Run this cell to initialize the Simplex Stepper class that we will use throughout the activity.

[15]:
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from IPython.display import display, Markdown

@dataclass
class SimplexStepper:
    """
    Tableau conventions:
    - Row 0 = objective (z) row
    - Column z_col = z column (typically 0), with tableau[0, z_col] = 1
    - Last column = RHS
    - Constraint rows = 1..(m)
    - Pivoting is standard Gauss-Jordan on (leaving_row, entering_col)

    Default pivot rule (common in many OR courses):
    - Entering variable: most negative coefficient in the objective row
      (excluding z column and RHS)
    - Optimal if all objective-row coefficients (excluding z and RHS) >= 0
    """
    tableau: np.ndarray
    row_labels: list = field(default_factory=list)
    col_labels: list = field(default_factory=list)

    z_col: int = 0
    rhs_col: int = None  # if None => last column
    iteration_count: int = 0
    history: list = field(default_factory=list)

    @classmethod
    def from_numpy(cls, arr, row_labels=None, col_labels=None, z_col=0, rhs_col=None):
        T = np.array(arr, dtype=float)
        return cls(T, row_labels or [], col_labels or [], z_col=z_col, rhs_col=rhs_col)

    @classmethod
    def from_dataframe(cls, df: pd.DataFrame, z_col=0, rhs_col=None):
        T = df.to_numpy(dtype=float)
        return cls(T, list(df.index), list(df.columns), z_col=z_col, rhs_col=rhs_col)

    def _rhs(self):
        return (self.tableau.shape[1] - 1) if self.rhs_col is None else self.rhs_col

    def as_dataframe(self) -> pd.DataFrame:
        df = pd.DataFrame(self.tableau.copy())
        if self.row_labels:
            df.index = self.row_labels
        if self.col_labels:
            df.columns = self.col_labels
        return df

    def show(self, title=None, note=None, highlight=None, ratio=None):
        if title:
            display(Markdown(f"### {title}"))
        if note:
            display(Markdown(note))

        df = self.as_dataframe().round(4)
        if ratio is not None:
            df['Ratio'] = ratio
        display(df)

        if highlight is not None:
            r, c = highlight
            r_name = self.row_labels[r] if self.row_labels else str(r)
            c_name = self.col_labels[c] if self.col_labels else str(c)
            display(Markdown(f"**Pivot position:** row `{r_name}`, column `{c_name}`"))

    # ---------------- simplex logic ----------------
    def _objective_coeffs_view(self):
        """
        Return objective row coefficients excluding z column and RHS.
        """
        rhs = self._rhs()
        cols = [j for j in range(self.tableau.shape[1]) if j not in (self.z_col, rhs)]
        return self.tableau[0, cols], cols

    def is_optimal(self, tol=1e-9):
        coeffs, _ = self._objective_coeffs_view()
        return np.all(coeffs >= -tol)

    def choose_entering_variable(self, tol=1e-9):
        """
        Most negative in objective row (excluding z and RHS).
        Returns column index or None if optimal.
        """
        coeffs, cols = self._objective_coeffs_view()
        min_val = np.min(coeffs)
        if min_val >= -tol:
            return None
        return cols[int(np.argmin(coeffs))]

    def choose_leaving_variable(self, entering_col, tol=1e-9):
        """
        Min ratio test over constraint rows (rows 1..end).
        Only rows with positive entry in entering column are candidates.
        """
        rhs = self._rhs()
        col = self.tableau[1:, entering_col]
        b = self.tableau[1:, rhs]

        valid = col > tol
        if not np.any(valid):
            return None, None  # unbounded

        ratios = np.full_like(b, np.inf, dtype=float)
        ratios[valid] = b[valid] / col[valid]
        leaving_in_constraints = int(np.argmin(ratios))
        print(ratios)
        # add a zero in first row of ratio to match index size
        ratios = np.insert(ratios, 0, 0, axis=0)
        print(ratios)
        return leaving_in_constraints + 1, ratios  # shift because constraints start at row 1

    def pivot(self, pivot_row, pivot_col, tol=1e-12):
        T = self.tableau
        p = T[pivot_row, pivot_col]
        if abs(p) < tol:
            raise ValueError("Pivot element too close to zero.")

        T[pivot_row, :] = T[pivot_row, :] / p
        for r in range(T.shape[0]):
            if r == pivot_row:
                continue
            factor = T[r, pivot_col]
            if abs(factor) > tol:
                T[r, :] = T[r, :] - factor * T[pivot_row, :]

    def step(self, tol=1e-9, pause=True):
        self.iteration_count += 1
        self.history.append(self.tableau.copy())

        # 1) optimality
        if self.is_optimal(tol=tol):
            self.show(
                title=f"Iteration {self.iteration_count} - Step 0: Optimality check",
                note="All objective-row coefficients (excluding **z** and **RHS**) are **non-negative** → ✅ **optimal**."
            )
            return "optimal"

        # 2) entering
        entering = self.choose_entering_variable(tol=tol)
        enter_name = self.col_labels[entering] if self.col_labels else f"col {entering}"
        self.show(
            title=f"Iteration {self.iteration_count} - Step 1: Choose entering variable",
            note=f"Pick the **most negative** coefficient in the objective row → entering variable is **{enter_name}**.",
        )

        # 3) leaving
        leaving, ratios = self.choose_leaving_variable(entering, tol=tol)
        if leaving is None:
            self.show(
                title=f"Iteration {self.iteration_count} - Step 2: Ratio test",
                note=f"No positive entries in column **{enter_name}** among constraints → ⚠️ **unbounded**."
            )
            return "unbounded"

        leave_name = self.row_labels[leaving] if self.row_labels else f"row {leaving}"
        self.show(
            title=f"Iteration {self.iteration_count} - Step 2: Choose leaving row (ratio test)",
            note=f"Apply minimum ratio test (RHS / positive pivot-column entry) → leaving row is **{leave_name}**.",
            highlight=(leaving, entering),
            ratio=ratios
        )

        # 4) pivot
        self.pivot(leaving, entering)
        self.show(
            title=f"Iteration {self.iteration_count} - Step 3: Pivot",
            note="Normalize pivot row, eliminate pivot column from all other rows. Tableau updated.",
            highlight=(leaving, entering),
        )

        if pause:
            input("Press Enter to continue...")

        return "continue"

    def run(self, max_steps=50, tol=1e-9, pause=True):
        for _ in range(max_steps):
            status = self.step(tol=tol, pause=pause)
            if status in ("optimal", "unbounded"):
                return status
        return "max_steps"

Tableau Initialization

The Simplex algorithm works on a tableau representation of the CLP problem. In this notebook, the tableau is stored as a NumPy array called T0.

Structure of the Tableau

The Tableau follows the same conventions as the Tableaus in other tutorials of this interactive book:

  • Rows represent equations of the problem model:

    • Row 0: is the objective function (or z-row)

    • Rows 1..m: represent the constraints

  • Columns contain the coefficients for each problem variable (\(z\), \(x_1\), \(x_2\), …, \(s_1\), \(s_2\), …):

    • The first column contains the coefficients for the objective variable \(z\) (1 in the objective function, and 0 for the rest of equations)

    • Next we add the columns corresponding to decision variables

    • And finally, coefficients for slacks or artificial variables

    • The last column contains the RHS coefficients

Sign convention

The problem needs to be converted to a standard maximization problem:

  • Objective coefficients appear negated in the z-row (e.g. maximize 3x₁ + 2x₂ → z-row contains -3, -2)

  • Constraint right-hand sides must be non-negative

Example

For instance, the following problem:

\(\max z = 3*x_1 + 2*x_2\)

s.t. \(x_1 + x_2 \leq 4\)

\(2*x_1 + x_2 \leq 5\)

results in the following Numpy array:

T0 = np.array([
    #  z   x1   x2   s1   s2   RHS
    [ 1,  -3,  -2,   0,   0,    0],   # objective (z-row)
    [ 0,   1,   1,   1,   0,    4],   # constraint 1
    [ 0,   2,   1,   0,   1,    5],   # constraint 2
], dtype=float)

Labels

We use labels to make the Tableau easier to interpret.

  • Row labels: label each equation. Normally we label the objective function with "z". You can use representative labels for the constraints

  • Column labels: Label each variable, for instance, "x1" for \(x_1\), and so forth. Remember that the first column is reserved for \(z\) and the last one for the \(RHS\), so label accordingly!

For instance, for the same example above, we would define the following labels:

row_labels = ["z", "c1", "c2"]
col_labels = ["z", "x1", "x2", "s1", "s2", "RHS"]

Simplex Stepper Initialization

Once you have created your Numpy model and your labels, you just need to instantiate the stepper. To check everything worked, use the method show() to display the initial Tableau (should be the same you created!)

simp = SimplexStepper.from_numpy(T0, row_labels=row_labels, col_labels=col_labels, z_col=0)
simp.show(title="Initial tableau (z row first)")

Example

Initialization

This is the same example in previous tutorials, you can edit it replace the initial Tableau with a different problem instance.

[20]:
M = 1e6 # Large number just in case it is needed for constraints of type equal

T0 = np.array([
    #  z   x1      x2   s1   s2  s3  RHS
    [ 1,  -300,  -250,   0,   0,  0,   0],  # objective row (maximize 3x1+2x2)
    [ 0,   2,       1,   1,   0,  0,  40],  # constraints
    [ 0,   1,       3,   0,   1,  0,  45],
    [0,    1,       0,   0,   0,  1,  12]
], dtype=float)

row_labels = ["z", "c1", "c2", "c3"]
col_labels = ["z", "x1", "x2", "s1", "s2", "s3", "RHS"]

simp = SimplexStepper.from_numpy(T0, row_labels=row_labels, col_labels=col_labels, z_col=0)
simp.show(title="Initial tableau (z row first)")

Initial tableau (z row first)

z x1 x2 s1 s2 s3 RHS
z 1.0 -300.0 -250.0 0.0 0.0 0.0 0.0
c1 0.0 2.0 1.0 1.0 0.0 0.0 40.0
c2 0.0 1.0 3.0 0.0 1.0 0.0 45.0
c3 0.0 1.0 0.0 0.0 0.0 1.0 12.0
[21]:
while True:
    status = simp.step(pause=True)
    if status != "continue":
        print("Status:", status)
        break

Iteration 1 - Step 1: Choose entering variable

Pick the most negative coefficient in the objective row → entering variable is x1.

z x1 x2 s1 s2 s3 RHS
z 1.0 -300.0 -250.0 0.0 0.0 0.0 0.0
c1 0.0 2.0 1.0 1.0 0.0 0.0 40.0
c2 0.0 1.0 3.0 0.0 1.0 0.0 45.0
c3 0.0 1.0 0.0 0.0 0.0 1.0 12.0
[20. 45. 12.]
[ 0. 20. 45. 12.]

Iteration 1 - Step 2: Choose leaving row (ratio test)

Apply minimum ratio test (RHS / positive pivot-column entry) → leaving row is c3.

z x1 x2 s1 s2 s3 RHS Ratio
z 1.0 -300.0 -250.0 0.0 0.0 0.0 0.0 0.0
c1 0.0 2.0 1.0 1.0 0.0 0.0 40.0 20.0
c2 0.0 1.0 3.0 0.0 1.0 0.0 45.0 45.0
c3 0.0 1.0 0.0 0.0 0.0 1.0 12.0 12.0

Pivot position: row c3, column x1

Iteration 1 - Step 3: Pivot

Normalize pivot row, eliminate pivot column from all other rows. Tableau updated.

z x1 x2 s1 s2 s3 RHS
z 1.0 0.0 -250.0 0.0 0.0 300.0 3600.0
c1 0.0 0.0 1.0 1.0 0.0 -2.0 16.0
c2 0.0 0.0 3.0 0.0 1.0 -1.0 33.0
c3 0.0 1.0 0.0 0.0 0.0 1.0 12.0

Pivot position: row c3, column x1

Iteration 2 - Step 1: Choose entering variable

Pick the most negative coefficient in the objective row → entering variable is x2.

z x1 x2 s1 s2 s3 RHS
z 1.0 0.0 -250.0 0.0 0.0 300.0 3600.0
c1 0.0 0.0 1.0 1.0 0.0 -2.0 16.0
c2 0.0 0.0 3.0 0.0 1.0 -1.0 33.0
c3 0.0 1.0 0.0 0.0 0.0 1.0 12.0
[16. 11. inf]
[ 0. 16. 11. inf]

Iteration 2 - Step 2: Choose leaving row (ratio test)

Apply minimum ratio test (RHS / positive pivot-column entry) → leaving row is c2.

z x1 x2 s1 s2 s3 RHS Ratio
z 1.0 0.0 -250.0 0.0 0.0 300.0 3600.0 0.0
c1 0.0 0.0 1.0 1.0 0.0 -2.0 16.0 16.0
c2 0.0 0.0 3.0 0.0 1.0 -1.0 33.0 11.0
c3 0.0 1.0 0.0 0.0 0.0 1.0 12.0 inf

Pivot position: row c2, column x2

Iteration 2 - Step 3: Pivot

Normalize pivot row, eliminate pivot column from all other rows. Tableau updated.

z x1 x2 s1 s2 s3 RHS
z 1.0 0.0 0.0 0.0 83.3333 216.6667 6350.0
c1 0.0 0.0 0.0 1.0 -0.3333 -1.6667 5.0
c2 0.0 0.0 1.0 0.0 0.3333 -0.3333 11.0
c3 0.0 1.0 0.0 0.0 0.0000 1.0000 12.0

Pivot position: row c2, column x2

Iteration 3 - Step 0: Optimality check

All objective-row coefficients (excluding z and RHS) are non-negative → ✅ optimal.

z x1 x2 s1 s2 s3 RHS
z 1.0 0.0 0.0 0.0 83.3333 216.6667 6350.0
c1 0.0 0.0 0.0 1.0 -0.3333 -1.6667 5.0
c2 0.0 0.0 1.0 0.0 0.3333 -0.3333 11.0
c3 0.0 1.0 0.0 0.0 0.0000 1.0000 12.0
Status: optimal

Guided Activity

Choose Problem

Choose a problem with 2 decision variables that is solved with the graphical method. You can use a problem you have defined and solved or alternatively select one instance from the problems solved with the graphical method in the interactive book tutorial Graphical Method.

Once you have chosen your problem, complete the following questions.

  1. Convert the problem into the standard form. Remember the steps:

  • Convert all the constraints to equalities introducing slack variables (or artificial variables)

  • Ensure RHS are greater than zero and that problem is of type maximize.

The following cell provides a Markdown template you can use to edit your model, just double-click and update the coefficients in the objective function and constraints.

Objective function (substitute the coefficients \(c_1\) and \(c_2\) with your coefficients).

\(z = c_1*x_1 + c_2*x_2\)

subject to: (substitute the LHS coefficients and RHS coefficients with your own)

\(a_{11}*x_1 + a_{12}*x_2 + s_1 = b_1\)

\(a_{21}*x_1 + a_{22}*x_2 + s_2 = b_2\)

\(a_{31}*x_1 + a_{32}*x_2 + s_3 = b_3\)

(copy and paste if you need more)

  1. Define the Tableau Check the example and fill in and run the following cell to define the Tableau for your model.

[ ]:
M = 1e6 # Large number just in case it is needed for constraints of type equal

T0 = np.array([
    # TODO: Add the objective row (e.g. maximize 3x1+2x2)
    # TODO: Add the constraints and RHS
], dtype=float)

# TODO: Edit your labels
row_labels = []
col_labels = []


Once you have defined your Tableau. Instantiate the simplex:

[ ]:
simp = SimplexStepper.from_numpy(T0, row_labels=row_labels, col_labels=col_labels, z_col=0)
simp.show(title="Initial tableau (z row first)")
  1. Run the Simplex. Use the following cell to run the Simplex: For each iteration, identify the vertex of the feasibility region visited by the algorithm in the graph. Use the last Markdown cell to write down the values of the decision variables and objective variables at each vertex visited. At every iteration, answer the following questions:

  • What is the entering variable? Why is that entering variable improving the objective in your sign convention?

  • Which edge of the feasible region are you moving along?

  • What does the ratio test guarantee geometrically (in terms of feasibility in that specific direction)?

  • Which constraint becomes active when the leaving variable leaves the basis?

[ ]:
while True:
    status = simp.step(pause=True)
    if status != "continue":
        print("Status:", status)
        break

Initial Tableau z: 0, \(x_1\): 0, \(x_2\): 0, \(s_1\): ?, \(s_2\): ?, \(s_3\): ? Iteration 1 Complete!