simplex-trace
A simplex solver for linear programs that uses exact rational arithmetic and records every step — tableaus, ratio tests, pivots, phase changes — as plain data.
Production solvers (HiGHS, CBC, Gurobi...) give you the answer. simplex-trace gives you
the answer and how it was reached, exactly as it is done by hand in an operations research
course: no 0.9999999, no hidden iterations.
Features
- Methods: primal simplex, Big-M (with
Mkept symbolic) and two-phase. - Exact arithmetic: arbitrary-precision rationals (
num-rational), so tableaus show1/3, not0.333…. - Full trace: every tableau snapshot, the entering/leaving variables, the minimum-ratio test, phase 1 results, artificial variables driven out of the basis and redundant rows removed.
- Sensitivity analysis of every optimal solution: shadow prices, reduced costs, and the ranges of right-hand sides and costs that keep the basis optimal, each price linked to the tableau cell it is read from.
- Pivot rules: Dantzig (textbook) or Bland (anti-cycling).
- Always conclusive: every method reports optimal, infeasible or unbounded, and the trace says why.
- JSON (optional
serdefeature): problems in, solutions and traces out, with exact numbers. - Pure Rust, no I/O, no unsafe.
Example
use ;
// Wyndor Glass Co.: max 3x1 + 5x2
let problem = maximize
.subject_to
.subject_to
.subject_to;
let solution = solve.unwrap;
print!;
Output (cargo run --example wyndor):
== simplex, iteration 0 ==
basis | x1 | x2 | s1 | s2 | s3 | rhs
------+----+----+----+----+----+----
s1 | 1 | 0 | 1 | 0 | 0 | 4
s2 | 0 | 2 | 0 | 1 | 0 | 12
s3 | 3 | 2 | 0 | 0 | 1 | 18
z | -3 | -5 | 0 | 0 | 0 | 0
-> x2 enters, s2 leaves (row 2, pivot 2); ratios [row 1: -, row 2: 6, row 3: 9]
== simplex, iteration 1 ==
basis | x1 | x2 | s1 | s2 | s3 | rhs
------+----+----+----+-----+----+----
s1 | 1 | 0 | 1 | 0 | 0 | 4
x2 | 0 | 1 | 0 | 1/2 | 0 | 6
s3 | 3 | 0 | 0 | -1 | 1 | 6
z | -3 | 0 | 0 | 5/2 | 0 | 30
-> x1 enters, s3 leaves (row 3, pivot 3); ratios [row 1: 4, row 2: -, row 3: 2]
== simplex, iteration 2 ==
basis | x1 | x2 | s1 | s2 | s3 | rhs
------+----+----+----+------+------+----
s1 | 0 | 0 | 1 | 1/3 | -1/3 | 2
x2 | 0 | 1 | 0 | 1/2 | 0 | 6
x1 | 1 | 0 | 0 | -1/3 | 1/3 | 2
z | 0 | 0 | 0 | 3/2 | 1 | 36
== Optimal ==
The text output is only a convenience: solution.trace.steps is a Vec<Step> you can walk
to render the method in any UI.
Sensitivity analysis
Every optimal solution comes with solution.sensitivity, read from the final basis and
expressed in terms of the original problem. For Wyndor (print!("{sensitivity}")):
constraint | shadow price | slack | rhs range | read from
-----------+--------------+-------+-----------+----------
#1 | 0 | 2 | [2, +inf) | s1
#2 | 3/2 | 0 | [6, 18] | s2
#3 | 1 | 0 | [12, 24] | s3
variable | value | reduced cost | cost range
---------+-------+--------------+-----------
x1 | 2 | 0 | [0, 15/2]
x2 | 6 | 0 | [2, +inf)
- Shadow price
y_i = dz*/db_i: how much the optimal objective changes per unit increase ofb_i, whileb_istays within its rhs range. This is the rate of change ofzitself, as in Hillier & Lieberman and Excel, also when minimizing (LINDO's "dual price" is the improvement: the same when maximizing, negated when minimizing). - Cost range: the values of
c_jfor which the current solution stays optimal. - Reduced cost:
z_j - c_jin the final tableau. - Read from: the objective-row cell that holds the price: under the slack (
<=), the negated surplus (>=) or, with Big-M, the artificial variable (=, constant part only). The two-phase method drops artificial columns, so its equalities showcomputed. degenerateandalternative_optimaflag the cases where prices are one-sided or other optima exist. Redundant equalities get a zero price, and they and the equalities they depend on get a single-point rhs range (none can move alone).- With Big-M an artificial variable can be left in the basis at zero. The analysis then comes
from an equivalent optimal basis of the real problem (same solution, reached by degenerate
pivots), and
from_final_tableauisfalse.
JSON
Enable the serde feature to send problems from a front end and get the whole trace back:
= { = "0.1", = ["serde"] }
Numbers are always exact strings ("3", "-1/2"); JSON floats are rejected, so nothing
is ever rounded. A problem (var_names is optional):
A solution carries its status at the top level, next to the trace. Each step has a
"type"; values that may contain the symbolic M are {"m": "7", "c": "-4"} (that is,
7M - 4):
Infinite range ends are null. The last object is one entry of sensitivity.constraints.
Step types: tableau, pivot, phase_one_finished, artificial_pivoted_out,
redundant_row_removed, artificial_remains_positive, finished. New types may appear in
minor releases, so a UI should skip unknown ones. See cargo run --example json --features serde
for a full trace.
Conventions
Tableaus follow the convention of Taha's Operations Research: An Introduction, so they match the textbook line by line:
- All variables are non-negative.
- The problem is never silently converted: the objective row holds
z_j - c_jwith the real costs, and its right-hand side is the real objective value. - Maximize: the most negative
z_j - c_jenters; optimal when all are>= 0. Minimize: the most positive enters; optimal when all are<= 0.Snapshot::is_optimalapplies the right rule. - Big-M: artificial variables cost
-Mwhen maximizing and+Mwhen minimizing. TheMpart of the objective row is improved first (both pivot rules), so when the run stops — at an optimal tableau or at an unbounded direction — the penalty cannot improve any further. If an artificial variable is still positive at that point the problem is infeasible, and the trace records it asStep::ArtificialRemainsPositive. - Two-phase: phase 1 minimizes
r = a1 + a2 + ...and its row is labelledr; phase 2 drops the artificial columns and prices out the real objective. - Constraints with a negative right-hand side are multiplied by
-1(flipping<=/>=). - Column order: original variables, then slack/surplus (
s1,s2, ...), then artificial (a1,a2, ...), numbered by constraint.
Testing
- Golden tests pin every tableau of textbook examples (Taha's Big-M and two-phase example, Hillier & Lieberman's Wyndor Glass), so the presentation cannot change silently.
- Classic cases: infeasible, unbounded, redundant equalities, negative right-hand sides, Beale's cycling example.
- Oracle: plain simplex, Big-M and two-phase, under both pivot rules, are cross-checked against brute-force vertex enumeration on 600 random problems (optimal, infeasible, unbounded, degenerate and redundant). Every recorded step is checked too: non-negative right-hand sides, unit basic columns, the entering column chosen by the pivot rule, the minimum-ratio leaving row, and that the final tableau is optimal under its own sense and shows the real objective value.
- Sensitivity: textbook values (Wyndor, Taha), negative right-hand sides, redundant equalities, degenerate Big-M bases. In the oracle, every report is checked for strong duality, complementary slackness, dual feasibility and agreement with the final tableau, and every range is probed by brute force: at its ends the objective must follow the claimed slope, and just beyond them the basis must stop being optimal.
- JSON contract (
--features serde): exact round trips, and the shape of problems, statuses and steps is pinned by tests.
Roadmap
- Dual simplex, and with it adding a constraint or a variable after the optimum
- Free (unrestricted-sign) variables
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.