learn-prescriptive-analytics-with-phoebe / Practitioner session 3 of 10
Learn Prescriptive Analytics with Phoebe · Practitioner session 3 of 10

Simplex, mechanically

Session 2 said the best plan is at a corner and left the finding of it unexplained. The method that finds it is about thirty lines long and every one of them is arithmetic you already know. Turn the inequalities into equations by naming the unused part of each. Write the system as a grid. Then repeat one move: pick the column that improves the objective fastest, pick the row that runs out first, and pivot. Stop when no column improves anything, because that means no neighbouring corner is better.

🟠 Working depth Anyone who will debug a model Comfortable with row arithmetic 45 min
0-3 · Setup 3-17 · Slack and the grid 17-32 · Two pivots by hand 32-45 · Build-along and Q&A
Part 0

A corner walk you can do on paper

The solver running in this page implements the simplex method directly, and the whole of it fits on one screen. That is worth knowing for two reasons. It means nothing here has to be taken on faith, because the walk can be done by hand on a small problem and compared against the machine. And it means that when a model misbehaves, the internals are inspectable rather than mysterious, which matters a great deal in session 9, where this exact solver turns out to have been wrong twice while passing all of its tests.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. A tableau you can read without flinching: which columns are variables, which are slacks, where the objective lives and why it is stored negated. Two pivots done by hand, on a problem small enough to check, ending at 36 at (2, 6). And the moment the shadow prices appear: they are not computed at the end, they have been sitting in the objective row under the slack columns the whole time.

LiveThe one-paragraph version4 min▶

Start at the corner where you make nothing at all, which is always feasible and always terrible. Look at the objective and ask which variable, if it were allowed to grow, would raise the margin fastest per unit. Grow it. Something will stop you, because some constraint runs out first, and the amount you can grow by is set by whichever row runs out soonest. At that moment you are at a new corner, one wall further in, earning more. Repeat. When no variable improves the objective any more, you are done, and you are done for a reason rather than because you ran out of patience.

Everything else is bookkeeping, and the tableau is the bookkeeping. It keeps the whole system of equations in a form where "which variable grows fastest" and "which row runs out first" are both readable off the grid rather than worked out from scratch each time.

Real world

Tonight's walk is on the solver's own first test problem, not on Ravensworth: maximise 3x + 5y subject to x at most 4, 2y at most 12, and 3x + 2y at most 18, whose optimum is 36 at (2, 6). Two variables and three rows is small enough to finish by hand in ten minutes and check against the machine, which is the only way to learn a mechanism. Ravensworth has the same shape and more columns.

Self-studyThe words tonight adds4 min▶
  • Slack variable. The unused part of a constraint, given a name. It turns x <= 4 into x + s1 = 4 with s1 >= 0.
  • Standard form. The problem written entirely as equations, with all variables non-negative. Simplex needs this.
  • Tableau. That system as a grid: one row per constraint, one column per variable including slacks, one column for the right hand side, and one extra row for the objective.
  • Basis. The variables currently allowed to be nonzero, one per row. It starts as the slacks, which is the corner where nothing is made.
  • Entering column. The variable chosen to grow. Here, the most negative entry in the objective row.
  • Leaving row. The row that runs out first, found by the ratio test: smallest right hand side divided by entry in the entering column, over positive entries only.
  • Pivot. The row arithmetic that swaps those two, moving you to an adjacent corner.
  • Optimality test. No negative entry in the objective row. It means no single variable can be grown to improve things, so no adjacent corner is better.
  • Degeneracy. A pivot of length zero, where the corner does not actually move. Real, and a cause of cycling.
  • Unbounded. An entering column with no positive entry anywhere: nothing stops the variable growing, so there is no best plan.
Part 1 · covers slack variables and the tableau

Name the unused part, and the inequalities become equations

18 min live

Simplex cannot work with inequalities, and the fix is almost insultingly simple. An inequality says a quantity is at most a ceiling. The difference between them is a real, meaningful, non-negative quantity, so give it a name and write an equation instead. Nothing is lost, one new variable is gained per row, and the entire problem becomes a system of linear equations, which is territory with a well understood method.

An inequality becomes an equation by naming the part nobody uses x <= 4 2y <= 12 3x + 2y <= 18 x + s1 = 4 2y + s2 = 12 3x + 2y + s3 = 18 The third row has 18 units of capacity in it 3x + 2y, what the plan uses s3, what it does not Both are real quantities, and both must stay non-negative. The tableau is that system of equations, plus one more row for the objective STRUCTURAL SLACK RIGHT HAND SIDE x y s1 s2 s3 basis s1 1 0 1 0 0 4 basis s2 0 2 0 1 0 12 basis s3 3 2 0 0 1 18 objective -3 -5 0 0 0 0 x + s1 = 4, as a row of numbers 2y + s2 = 12 3x + 2y + s3 = 18 The objective 3x + 5y, stored negated, so that the most negative entry is the best column. Every column is a variable, every row is an equation, and the basis starts as the slacks. Which is the corner where x and y are both zero: the mill makes nothing, breaks no rule, and earns the 0 in the corner of the grid.
🔍 Click to zoom - the slack block starts as an identity matrix. That is not decoration, it is what makes the first corner free.
LiveWhy the objective row is stored negated4 min▶

The code writes obj[j] = -c[j] and it is worth being clear that this is a convention rather than a trick. Maximising 3x + 5y is the same as minimising -3x - 5y, and once the row holds the negated coefficients, "which variable would help most" becomes "which entry is most negative", which is one comparison rather than two.

It also makes the stopping rule a single test. When there is no negative entry left in the objective row, no variable can be increased to improve the objective, and the search is over. One sign convention buys you both the choice rule and the halt condition, which is why nearly every implementation does it, and why a tableau you find in the wild will usually look like this.

LiveThe first corner is free3 min▶

Notice what the starting tableau already is. The slack columns form an identity block, the basis is the three slacks, and the right hand side is the original ceilings. Read that as a plan and it says: make nothing, and all three capacities are entirely unused. That plan is feasible, it earns zero, and it required no work to find.

Having a feasible starting point for free is the reason this form is chosen, and it is also the reason this particular solver refuses any problem with a negative right hand side. A row like -x <= -5 would make the "make nothing" plan illegal, so the free starting corner disappears and a preliminary phase is needed to find any feasible point at all. The code returns "needs-phase-one" for that case rather than guessing, and the consequences of that refusal being silent elsewhere are the subject of session 9.

Part 2 · covers pivoting and the stopping rule

Two pivots, by hand, to 36 at (2, 6)

20 min live

The starting tableau has -5 under y and -3 under x, so y is the column that pays best per unit and y goes first. How far can y grow? Row 2 allows 12 divided by 2, which is 6. Row 3 allows 18 divided by 2, which is 9. Row 1 has a zero in the y column, so it does not restrict y at all. The smallest is 6, so row 2 is the first wall y hits, and row 2 leaves the basis. That is the entire decision, and it repeats.

Two pivots on max 3x + 5y, subject to x at most 4, 2y at most 12, 3x + 2y at most 18 After pivot 1: y entered, row 2 left on the ratio 12 / 2 = 6 x y s1 s2 s3 rhs s1 101004 y 0100.506 s3 300-116 obj -3002.5030 x enters next, -3 being the only negative left. Row 3 leaves: 6 / 3 = 2 beats 4 / 1 = 4. After pivot 2: x entered, row 3 left. No negative entry remains, so the walk stops. x y s1 s2 s3 rhs s1 0010.333-0.3332 y 0100.506 x 100-0.3330.3332 obj 0001.5136 (0, 6), margin 30 (2, 6), margin 36 start at (0, 0), margin 0 x y The ratio test, in one sentence In the entering column, divide each row's right hand side by its entry in that column. Ignore rows whose entry is zero or negative, because they do not stop the variable growing. Take the smallest ratio. That row is the first wall you would hit. Each pivot is one step from a corner to an adjacent corner that earns more. Two steps here, and then no better neighbour exists.
🔍 Click to zoom - the tableau and the plot are the same walk. The grid is what you can still do at forty variables.
Live Step the pivots yourself. Columns x1 and x2 are the x and y of the diagram. The page walks the tableau, then checks its answer against the solver.

...

...
LiveWhat a pivot actually does to the numbers4 min▶

Three lines of arithmetic. Divide the leaving row through by the entry sitting at the intersection of the entering column and the leaving row, so that entry becomes 1. Then subtract a multiple of that row from every other row, including the objective row, so that the rest of the entering column becomes 0. Then record that the entering variable now occupies that row in the basis.

That is ordinary elimination, the same operation used to solve simultaneous equations by hand, applied to a grid. Nothing clever happens. What makes it a method rather than a manipulation is the pair of rules for choosing where to pivot, and the guarantee those rules carry: the new corner is feasible, and it is at least as good as the old one.

Watch the right hand column while you step the live box. It holds the current values of the basic variables, and the bottom right cell holds the objective. It goes 0, then 30, then 36, and those three numbers are the three corners of the walk. Nothing about them is retrieved at the end; the tableau has been carrying the answer the whole time.

LiveWhere the shadow prices come from5 min▶

Look at the final objective row of the walk: 0 and 0 under x and y, then 0, 1.5 and 1 under s1, s2 and s3, and 36 in the right hand corner. The three entries under the slack columns are the shadow prices of the three constraints, and the code returns them directly with no extra computation: for (i = 0; i < m; i++) dual.push(T[m][n + i]);

The reason is readable from what a slack variable means. The slack in a row is the unused capacity of that row. Its entry in the objective row is the rate at which the objective would change if that slack were forced to be one unit larger, which is to say if one unit of capacity were taken away. Turn the sign around and it is what one more unit would be worth. So the first row here, which ends with slack left over, is worth 0, and the two rows the plan has used up are worth 1.5 and 1.

This is the point where the machinery starts paying rent beyond the plan itself. The prices arrive free with the answer, they say what each ceiling is worth at the margin, and every one of them is a rate with a validity range that nobody quotes. Session 4 is that range, measured, and it turns out to be shorter than anyone expects: at Ravensworth the milling price of 33.7931 stops holding at about +0.46 hours.

The last row of the walk is already a price list x y s1 s2 s3 rhs obj 0 0 0 1.5 1 36 the slack columns for (i = 0; i < m; i++) dual.push(T[m][n + i]); The whole of the shadow price computation, from the solver. s1 reads 0 x at most 4 still has slack, so it is worth nothing s2 reads 1.5 2y at most 12 is binding, and worth 1.5 s3 reads 1 3x + 2y at most 18 is binding, and worth 1 The same field, on the mill Ravensworth's r.shadow reads milling 33.7931, blending 0, premium protein 8.2759. Blending prices at zero because its row still has 0.2414 hours of slack. Each figure is a local rate, and how far it holds is session 4.
🔍 Click to zoom - the prices were never computed separately. The walk carried them in the bottom row the whole way.
The same walk, done by the solver
var r = OPT.solveLP([3, 5], [[1, 0], [0, 2], [3, 2]], [4, 12, 18]);

r.status;      // "optimal"
r.objective;   // 36
r.x;           // the corner the last pivot landed on
r.slack;       // what each row has left over at that corner
r.shadow;      // the final objective row, under the slack columns
r.binding;     // true where slack is zero

OPT.CASES;           // the three hand-checkable problems the solver ships
OPT.selfTest().ok;   // all three, run in this browser, right now
Self-studyDegeneracy, cycling and the guard in this code5 min▶

The ratio test can produce a tie, and a tie means more than two walls pass through the same corner. When that happens the next pivot can have a ratio of zero: the basis changes, the arithmetic all happens, and the corner does not move. The objective does not improve. That is degeneracy, and it is common in real models, especially ones with many similar rows.

A run of degenerate pivots can in principle return to a basis it has already visited, at which point the method cycles for ever. Anti-cycling rules exist and they work by changing the choice of entering column when ties appear. They also cost something: the rule that provably prevents cycling tends to choose poor columns and take many more steps.

What does this implementation do? It does not have an anti-cycling rule. It has if (++guard > 5000) return { status: "iteration-limit" };, which is a counter, not a proof. That is an honest engineering choice for a teaching solver and it is worth knowing about rather than discovering: a status of "iteration-limit" means the walk gave up, and the correct response is to look at the model rather than to raise the limit.

Self-studyWhere serious people disagree5 min▶

The rule used here for choosing the entering column, take the most negative objective entry, is the original one and it is not the only one. Whether it is the right default is a genuinely open argument.

One camp defends it on cost. The rule is a single scan of one row, it is almost free per step, and on the overwhelming majority of practical problems the number of steps stays modest. Spending more effort per step to choose a better column is paying a certain cost for an uncertain benefit, and a simple rule keeps the implementation small enough to audit, which has its own value.

The other camp points out that the rule measures the wrong thing. The most negative entry is the biggest improvement per unit of the entering variable, not the biggest improvement per step, and those come apart badly when columns are scaled differently. Rules that estimate the actual gain per step take far fewer steps on hard problems, and on large models the step count is what dominates. There are also problems constructed specifically to make the simple rule take an enormous number of steps.

Neither side claims a universal winner, because there is not one: the choice trades work per step against number of steps, and which dominates depends on the problem. What is not in dispute is that the choice is a choice. If a model is slow, the pivoting rule is one of the things to look at, and it is not a detail of the implementation so much as a lever on it.

Part 3 · build-along

Walk it on paper, then make the solver agree with you

18 min hands on

Six steps on the small problem. The aim is to be able to pick up any tableau, say which column enters and which row leaves, and know when to stop, without looking up a rule.

Write the starting tableau from scratch

On paper, turn the three inequalities into equations with s1, s2 and s3, and lay out the grid with the objective row negated. Check it against the first diagram cell by cell before you go on. Everything after this is arithmetic, so an error here is an error everywhere.

Choose the first pivot yourself

Name the entering column and say why. Do the ratio test on all three rows out loud, including the row where the entry is zero, and say why that row is skipped rather than treated as infinite improvement. Then name the leaving row.

Do the row arithmetic by hand

Divide the leaving row by the pivot entry, then clear the rest of the entering column, objective row included. Compare your result with the first tableau in the second diagram. If a number is off, the fault is almost always in the objective row, because it is the row people forget to update.

Step the live box and check yourself

Pivot once in the live box and compare every cell with your paper. Then do the second pivot on paper first, predict the stopping condition, and only then step the box. Read the objective as it goes 0, then 30, then 36.

Read the prices off the final row

Take the three entries under s1, s2 and s3 in your final tableau. Then call OPT.solveLP([3, 5], [[1, 0], [0, 2], [3, 2]], [4, 12, 18]) and compare with r.shadow. Say which constraint is worth nothing, and say why that one, using r.slack as the evidence.

Walk the deliberate tie

Switch the live box to the second problem, max x + y subject to x + y at most 10, and step it. One pivot ends it at 10. Then look at the objective row entry under the structural column that is not in the basis: it is zero, which is the tableau telling you that bringing that variable in would change nothing, because an entire edge is optimal.

The habit to keep. When a model behaves strangely, the tableau is inspectable and the status field is informative. An "unbounded" status means some column had no positive entry to stop it, which is a missing constraint. An "iteration-limit" means the walk did not terminate, which is usually degeneracy rather than size. And a shadow price of zero always has slack sitting next to it, so the two fields check each other.

Check yourself

Three questions

In the starting tableau the objective row reads -3 under x and -5 under y. Which column enters, and what does the choice mean?

The row holds the negated objective coefficients, so a more negative entry is a better per-unit improvement. Choosing the most negative is the original pivoting rule. It is a rule about improvement per unit of the variable, which is not the same as improvement per step, and that difference is where other rules come from.

The entering column is y. Row 1 has a 0 in that column, row 2 has 2 with a right hand side of 12, row 3 has 2 with a right hand side of 18. Which row leaves?

The ratio test asks how far the entering variable can grow before some row runs out. A zero entry means that row does not restrict y, so it is skipped rather than counted. Twelve over two is six, eighteen over two is nine, and six comes first, so row 2 is the binding wall and it leaves the basis.

The final objective row reads 0, 0, 0, 1.5, 1 with 36 in the corner. What are the 1.5 and the 1?

Each slack column's objective entry is the rate at which the objective changes as that row's capacity changes, which is exactly a shadow price, and the solver returns it without further work. The slack values themselves live in the right hand column, not the objective row, and a row with slack left over always prices at zero.

Before session 4

Homework

Thirty minutes, paper first and the console second.

Walk the third shipped problem by hand

Maximise 2x + y subject to x + y at most 10 and x at most 20, whose optimum is 20 at (10, 0). Do the tableau on paper, predict how many pivots it takes, then check against the live box. Note which constraint ends with slack and confirm its price is zero.

Predict Ravensworth's binding set

Before session 4, write down which of the mill's three ceilings you expect the optimal plan to use up completely, and which you expect to have room left over. Then solve it and check. Session 4 puts a price on each one and measures how far that price survives being multiplied.

Covered tonight

What this session covers

✓Slack variablesNaming the unused part turns every inequality into an equation.
✓The tableauStructural columns, slack columns, the right hand side, and a negated objective row.
✓The free starting cornerThe slack basis is the plan that makes nothing, and it is feasible for nothing.
✓The entering columnThe most negative objective entry, and what that rule actually measures.
✓The leaving rowThe ratio test over positive entries only, and why zero entries are skipped.
✓The pivot and the stopping ruleTwo pivots to 36 at (2, 6), stopping because no objective entry is negative.
✓Shadow prices in the final rowThe objective entries under the slack columns, returned as r.shadow.
✓Degeneracy and the iteration guardA pivot that moves nothing, and a counter where an anti-cycling rule would be.
◐Alternative optimaRead off the tie the solver ships, where a non-basic column prices at zero.
◐The dualMet tonight as a column of the tableau. Treated as an object of its own in session 4.
○Phase oneNamed as what a negative right hand side would need. This solver refuses instead.
○Whole tonnesBranch and bound sits on top of this walk and is session 5.
○Interior point methodsA different family that never visits a corner. Named in session 2, not taught.
Take this with you

Cheat sheet

Slack. x at most 4 becomes x + s1 = 4 with s1 at least 0. The unused part, named.
The grid. One row per constraint, one column per variable including slacks, plus the right hand side and an objective row.
Negated on purpose. The objective row holds -c, so the most negative entry is the best column and no negatives means stop.
Entering column. Most negative objective entry. Improvement per unit, not per step.
Leaving row. Smallest right hand side over entry, positive entries only. The first wall you hit.
A pivot. Divide the leaving row, clear the column everywhere else, update the basis. One corner to a better neighbour.
The walk. 0, then 30, then 36 at (2, 6). The bottom right cell is always the current objective.
Prices for free. The final objective row under the slack columns is r.shadow. A row with slack prices at zero.