core design
linear-program keeps variables, linear expressions, objectives, constraints and a two-phase simplex solver in one package at src. This page states the mathematics the solver implements, derives the tests it uses to stop, and explains how the code is organised around them.
Design goal
- Let a user write a linear program in the notation of a textbook: named variables, an objective with a sense, and equality and inequality constraints.
- Solve it with the textbook algorithm, the two-phase simplex method on a dense tableau, and expose every step (
Lp::pivot,Lp::simplex_iteration,Lp::phase_1,Lp::phase_2) so that the algorithm can be followed and taught. - Stay generic in the coefficient type through the traits of luna-generic, and store tableaux as matrices of linear-algebra.
Mathematical background
Linear programs and standard form
A linear program over variables optimises a linear objective subject to linear equalities and inequalities. The solver works on the standard form
Every program written with this package can be brought into this form without changing its optimal solutions. Lp::to_standard applies these rewritings:
Each equivalence holds because measures the slack of the inequality: for , set , which is non-negative exactly when the inequality holds. Multiplying an equation by changes nothing, and multiplying an inequality by reverses it, which is why the sign of the slack variable flips in the cases with and . In every case the new right-hand side is non-negative, which phase 1 needs.
The standard form assumes for every variable, including the variables of the original program. The bounds stored in Variable are not used.
Bases and basic solutions
Assume has rank . A basis is a set of column indices such that the submatrix is invertible; the remaining indices form . Setting the non-basic variables to zero determines the basic solution
which is feasible when . The fundamental theorem of linear programming states that if the program has an optimal solution, it has an optimal basic feasible solution, so the simplex method only visits bases.11 See for example Chvátal, Linear Programming (1983), chapter 3, or Bertsimas and Tsitsiklis, Introduction to Linear Optimization (1997), theorem 2.7.
The tableau and reduced costs
The solver stores the program as an tableau:
Row 0 is the objective row, rows are the constraints, and the last column holds right-hand sides. A pivot on position with is a Gauss–Jordan step: divide row by , then subtract times the new row from every other row , including row 0. Afterwards column is the unit vector . This is exactly Lp::pivot.
Suppose a sequence of pivots has made the columns of a basis into unit vectors. Every pivot is a left multiplication by an invertible matrix, and row 0 has only received multiples of constraint rows, so the tableau has the form
for some vector . The basic columns of row 0 are zero, so , that is
Substituting gives the canonical tableau
The entries are the reduced costs, is the basic solution, and the last entry of row 0 is the negated objective value . This is why Lp::phase_2 returns and why Lp::two_stage negates it again for a minimisation objective.
The reduced costs describe how the objective changes along the constraints. For any with , solve for the basic variables: . Then
Optimality
If , the basic feasible solution is optimal. Every feasible satisfies , so by the identity above , and the basic solution attains . Lp::simplex_iteration stops when no entry of row 0 is negative, which is this test ( holds by construction).
Choosing the entering column
If some , increasing from decreases the objective at rate . The solver chooses the most negative reduced cost,
Dantzig’s rule, and takes the leftmost column on ties.
The ratio test and unboundedness
Increase to and keep the other non-basic variables at zero. The constraints force , where is column of , and the objective becomes .
If , the program is unbounded. Every component of is non-decreasing in , so is feasible for all while . Lp::simplex_iteration aborts with Problem is unbounded in this case.
Otherwise feasibility requires for every row with , so the largest step is
the minimum ratio test. A row attaining the minimum leaves the basis: its basic variable reaches zero at . Pivoting on moves to the new basis , whose basic solution is , so feasibility is preserved, and whose objective value is
The solver takes the topmost row on ties.
Termination and degeneracy
If every pivot has (the program is non-degenerate), the objective strictly decreases, no basis repeats, and the method stops after at most pivots. When for the leaving row, : the basis changes but the point and the objective do not. A sequence of such degenerate pivots can return to an earlier basis and repeat forever. Dantzig’s rule with lowest-index tie-breaking, which is what this package implements, is known to cycle on small examples.22 E. M. L. Beale, “Cycling in the dual simplex algorithm”, Naval Research Logistics Quarterly 2 (1955), gives a cycling example with three constraints. R. G. Bland, “New finite pivoting rules for the simplex method”, Mathematics of Operations Research 2 (1977), proves that choosing, among the candidates, the entering and the leaving variable with the smallest index prevents cycling. The package has no anti-cycling rule; a run stops after max_iterations pivots (1000 by default) and prints a message, and the tableau it returns is then not optimal.
Design decisions
Two phases with artificial variables
Problem. The simplex method starts from a basic feasible solution, and a program in standard form does not come with one.
Options. (a) The big-M method: add artificial variables with a large cost to the original objective. (b) The two-phase method: first find a feasible basis with an auxiliary objective, then optimise. (c) Require the user to supply a feasible basis.
Choice. (b). Big-M needs a numeric value of that is large enough for the program but small enough not to swamp the other coefficients in floating point; the two-phase method needs no such constant, and its first phase also decides feasibility.
Phase 1. For every constraint row that has no usable unit column, add an artificial variable with coefficient in row . Let be the set of these rows. Phase 1 solves
Its initial basis consists of the artificial columns and the existing unit columns, with the basic solution , which is feasible because to_standard made non-negative.
The original program is feasible exactly when the optimum of phase 1 is . If is feasible for , then is feasible for phase 1 with , and always, so . Conversely, with forces , so the optimal satisfies .
Canonical form of the phase 1 tableau. Row 0 of the phase 1 objective starts as , which is not canonical because the basic artificial columns have cost . Applying the formula with gives , so the canonical row 0 is
obtained by subtracting each artificial row from row 0. Lp::phase_1 performs exactly these subtractions before calling simplex_iteration.
Phase 2. If (beyond the tolerance below), Lp::phase_2 aborts: the program is infeasible. Otherwise it drops the artificial columns, writes the original costs into row 0, and makes row 0 canonical again by subtracting times the row of each basic column , which is the same elimination as above. Then it runs simplex_iteration from the feasible basis found by phase 1.
Reusing unit columns as the initial basis
A slack variable of a <= row with has a column equal to a unit vector, so it can start in the basis without an artificial variable. The private helper that builds the phase 1 tableau recognises a column as a unit column when one entry equals and all other entries in the constraint rows are , and adds artificial variables only for the remaining rows. A program whose constraints are all <= with non-negative right-hand sides therefore needs no artificial variables at all, and phase 1 is skipped in effect: its tableau keeps the original objective, so phase 1 already optimises it.
Dense tableau
The tableau is a dense @mutable.Matrix. Each pivot costs multiply–subtract operations, . A revised simplex method with a factorised basis would cost less per iteration on sparse programs, but the dense tableau shows every quantity of the derivation above as an entry of one matrix, which serves the teaching goal of the package and keeps the code short.
Aborting instead of returning errors
Infeasible and unbounded programs, a zero pivot, an unknown objective sense and an unknown relation string end the program with abort. The package predates the Luna-Flow convention of returning Result with a structured error. Callers cannot recover from these outcomes; see the boundaries below.
Numeric tolerance
The simplex iterations compare entries with zero exactly: a reduced cost of produced by rounding counts as negative and triggers another pivot. Phase 2 uses the trait ApproximatelyZero in two places: to decide whether is zero, and to recognise unit columns. For Double the threshold is , an absolute bound.
Rounding errors in a pivot are relative to the size of the entries involved. One update in floating point satisfies
so its absolute error is bounded by about . After pivots on entries of magnitude around , the error in is of order . The threshold is therefore appropriate for well-scaled programs with entries near and few pivots. The tests show residuals such as . For coefficients in the thousands, the residual can exceed the threshold and a feasible program is reported as infeasible. A relative tolerance scaled by the size of would avoid this, but it is not implemented.
Correctness and invariants
The solver maintains these invariants between pivots:
- Canonical form. Every basic column of the constraint rows is a unit vector, and row 0 is zero in every basic column.
Lp::pivotpreserves this by construction,Lp::phase_1establishes it for the artificial basis, andLp::phase_2re-establishes it after replacing row 0. - Primal feasibility. The right-hand sides are non-negative.
to_standardmakes , and the ratio test keeps it, as derived above. - Objective in row 0. The last entry of row 0 is .
Under these invariants, the stopping tests are correct as derived above: no negative reduced cost means optimal, and a negative reduced cost with a non-positive column means unbounded.
The implementation has known defects that break these guarantees in specific cases. They are listed here so that users can avoid them; the code is unchanged.
- Objective row alignment.
Lpbuilds row 0 withObj_func::to_vector, which takes the stored coefficients in the order of the variable names and skips zero coefficients. Row 0 then disagrees with the column order whenever an objective coefficient is zero or the variables are not declared in name order, and the solver optimises a permuted objective. For example, minimising subject to , returns instead of . - Basis recognition. Phase 2 recognises basic variables as unit columns and takes the first such column in each row. When two columns are equal unit vectors, the reported solution can name the wrong variable even though the objective value is right.
- Degenerate artificial variables. If an artificial variable remains basic at value zero after phase 1, phase 2 drops its column without pivoting it out, and that row has no basic column during phase 2.
- Artificial index.
Lp::phase_1receives0as the artificial column of a row that has none, which it cannot distinguish from column 0. If the phase 1 objective coefficient of column 0 happens to be at that moment, a row is subtracted from row 0 that should not be.
Alternatives rejected
- Big-M method. Rejected for the numeric reasons given under “Two phases with artificial variables”.
- Bland’s rule. It guarantees termination, but it often needs many more pivots than Dantzig’s rule on non-degenerate programs. The package keeps Dantzig’s rule and bounds the number of iterations instead.
- Revised simplex and interior-point methods. They scale better to large sparse programs, but they hide the tableau, which the package exposes on purpose.
- Enforcing variable bounds by substitution. Bounds can be reduced to the standard form by and an extra constraint . The package stores bounds for display only and leaves such constraints to the user.
Boundaries
The package deliberately does not:
- return errors as values. Infeasible and unbounded programs, a zero pivot and invalid arguments abort the program;
- enforce variable bounds other than ;
- prevent cycling beyond the iteration limit of 1000 per simplex run;
- exploit sparsity, warm starts or a factorised basis;
- compute dual values or sensitivity ranges, although the vector of the final basis is implicit in the tableau;
- solve integer, mixed-integer or nonlinear programs.
Footnotes
-
See for example Chvátal, Linear Programming (1983), chapter 3, or Bertsimas and Tsitsiklis, Introduction to Linear Optimization (1997), theorem 2.7. ↩
-
E. M. L. Beale, “Cycling in the dual simplex algorithm”, Naval Research Logistics Quarterly 2 (1955), gives a cycling example with three constraints. R. G. Bland, “New finite pivoting rules for the simplex method”, Mathematics of Operations Research 2 (1977), proves that choosing, among the candidates, the entering and the leaving variable with the smallest index prevents cycling. ↩