[Arjen Markus] (9 june 2004) ''Linear programming'' is the name for a class of optimisation problems. Such problems may arise in operations research, where you have limited resources and other constraints and want to find the parameters for which a gain function is maximum. Such problems are formulated like: Find the values of x and y for which f(x,y) = x + 2y is maximal, given that: x + y <= 10 and 2x - y <= 3 The script below implements the classic ''simplex method'' for solving such problems. Note that it is not as robust as should be - it does not use a two-phase solution method, necessary for real-world problems - at least, not yet. I intend to submit this to Tcllib. For some more background information, see for instance http://www-unix.mcs.anl.gov/otc/Guide/faq/linear-programming-faq.html [AM] (10 june 2004) The code below is flawed, as it does not work if one or more of the constraints are superfluous for pinning down the solution. I will have to look into this ... [AM] (25 august 2005) The improved code has been committed to Tcllib. Please use that code. ---- ====== # linprog.tcl -- # Script to solve linear programs using the Simplex method # # math::optimize -- # Namespace for the commands # namespace eval ::math::optimize { namespace export solveLinearProgram \ linearProgramMaximum } # solveLinearProgram # Solve a linear program in standard form # # Arguments: # objective Vector defining the objective function # constraints Matrix of constraints (as a list of lists) # # Return value: # Computed values for the coordinates or "unbounded" or "infeasible" # proc ::math::optimize::solveLinearProgram { objective constraints } { # # Check the arguments first and then put them in a more convenient # form # foreach {nconst nvars matrix} \ [SimplexPrepareMatrix $objective $constraints] {break} #SimplexPrintMatrix $matrix set solution [SimplexSolve $nconst nvars $matrix] if { [llength $solution] > 1 } { return [lrange $solution 0 [expr {$nvars-1}]] } else { return $solution } } # linearProgramMaximum -- # Compute the value attained at the optimum # # Arguments: # objective The coefficients of the objective function # result The coordinate values as obtained by solving the program # # Return value: # Value at the maximum point # proc ::math::optimize::linearProgramMaximum {objective result} { set value 0.0 foreach coeff $objective coord $result { set value [expr {$value+$coeff*$coord}] } return $value } # SimplexPrintMatrix # Debugging routine: print the matrix in easy to read form # # Arguments: # matrix Matrix to be printed # # Return value: # None # proc ::math::optimize::SimplexPrintMatrix {matrix} { puts "\nBasis:\t[join [lindex $matrix 0] \t]" foreach col [lrange $matrix 1 end] { puts " \t[join $col \t]" } } # SimplexPrepareMatrix # Prepare the standard tableau from all program data # # Arguments: # objective Vector defining the objective function # constraints Matrix of constraints (as a list of lists) # # Return value: # List of values as a standard tableau and two values # for the sizes # proc ::math::optimize::SimplexPrepareMatrix {objective constraints} { # # Check the arguments first # set nconst [llength $constraints] set ncols {} foreach row $constraints { if { $ncols == {} } { set ncols [llength $row] } else { if { $ncols != [llength $row] } { return -code error -errorcode ARGS "Incorrectly formed constraints matrix" } } } set nvars [expr {$ncols-1}] if { [llength $objective] != $nvars } { return -code error -errorcode ARGS "Incorrect length for objective vector" } # # Set up the tableau: # Easiest manipulations if we store the columns first # So: # - First column is the list of variable indices in the basis # - Second column is the list of maximum values # - "nvars" columns that follow: the coefficients for the actual # variables # - last "nconst" columns: the slack variables # set matrix [list] set lastrow [concat $objective [list 0.0]] set newcol [list] for {set idx 0} {$idx < $nconst} {incr idx} { lappend newcol [expr {$nvars+$idx}] } lappend newcol "?" lappend matrix $newcol set zvector [list] foreach row $constraints { lappend zvector [lindex $row end] } lappend zvector 0.0 lappend matrix $zvector for {set idx 0} {$idx < $nvars} {incr idx} { set newcol [list] foreach row $constraints { lappend newcol [expr {double([lindex $row $idx])}] } lappend newcol [expr {-double([lindex $lastrow $idx])}] lappend matrix $newcol } return [list $nconst $nvars $matrix] } # SimplexSolve -- # Solve the given linear program using the simplex method # # Arguments: # nconst Number of constraints # nvars Number of actual variables # tableau Standard tableau (as a list of columns) # # Return value: # List of values for the actual variables # proc ::math::optimize::SimplexSolve {nconst nvars tableau} { set end 0 while { !$end } { # # Find the new variable to put in the basis # set nextcol [SimplexFindNextColumn $tableau] if { $nextcol == -1 } { set end 1 continue } # # Now determine which one should leave # TODO: is a lack of a proper row indeed an # indication of the infeasibility? # set nextrow [SimplexFindNextRow $tableau $nextcol] if { $nextrow == -1 } { return "infeasible" } # # Make the vector for sweeping through the tableau # set vector [SimplexMakeVector $tableau $nextcol $nextrow] # # Sweep through the tableau # set tableau [SimplexNewTableau $tableau $nextcol $nextrow $vector] #SimplexPrintMatrix $tableau } # # Now we can return the result # SimplexResult $tableau } # SimplexResult -- # Reconstruct the result vector # # Arguments: # tableau Standard tableau (as a list of columns) # # Return value: # Vector of values representing the maximum point # proc ::math::optimize::SimplexResult {tableau} { set result {} set firstcol [lindex $tableau 0] set secondcol [lindex $tableau 1] set result {} set nvars [expr {[llength $tableau]-2}] for {set i 0} {$i < $nvars } { incr i } { lappend result 0.0 } set idx 0 foreach col [lrange $firstcol 0 end-1] { set result [lreplace $result $col $col [lindex $secondcol $idx]] incr idx } return $result } # SimplexFindNextColumn -- # Find the next column - the one with the largest negative # coefficient # # Arguments: # tableau Standard tableau (as a list of columns) # # Return value: # Index of the column # proc ::math::optimize::SimplexFindNextColumn {tableau} { set idx 0 set minidx -1 set mincoeff 0.0 foreach col [lrange $tableau 2 end] { set coeff [lindex $col end] if { $coeff < 0.0 } { if { $coeff < $mincoeff } { set minidx $idx set mincoeff $coeff } } incr idx } return $minidx } # SimplexFindNextRow -- # Find the next row - the one with the largest negative # coefficient # # Arguments: # tableau Standard tableau (as a list of columns) # nextcol Index of the variable that will replace this one # # Return value: # Index of the row # proc ::math::optimize::SimplexFindNextRow {tableau nextcol} { set idx 0 set minidx -1 set mincoeff {} set bvalues [lrange [lindex $tableau 1] 0 end-1] set yvalues [lrange [lindex $tableau [expr {2+$nextcol}]] 0 end-1] foreach rowcoeff $bvalues divcoeff $yvalues { if { $divcoeff > 0.0 } { set coeff [expr {$rowcoeff/$divcoeff}] if { $mincoeff == {} || $coeff < $mincoeff } { set minidx $idx set mincoeff $coeff } } incr idx } return $minidx } # SimplexMakeVector -- # Make the "sweep" vector # # Arguments: # tableau Standard tableau (as a list of columns) # nextcol Index of the variable that will replace this one # nextrow Index of the variable in the base that will be replaced # # Return value: # Vector to be used to update the coefficients of the tableau # proc ::math::optimize::SimplexMakeVector {tableau nextcol nextrow} { set idx 0 set vector {} set column [lindex $tableau [expr {2+$nextcol}]] set divcoeff [lindex $column $nextrow] foreach colcoeff $column { if { $idx != $nextrow } { set coeff [expr {-$colcoeff/$divcoeff}] } else { set coeff [expr {1.0/$divcoeff-1.0}] } lappend vector $coeff incr idx } return $vector } # SimplexNewTableau -- # Sweep through the tableau and create the new one # # Arguments: # tableau Standard tableau (as a list of columns) # nextcol Index of the variable that will replace this one # nextrow Index of the variable in the base that will be replaced # vector Vector to sweep with # # Return value: # New tableau # proc ::math::optimize::SimplexNewTableau {tableau nextcol nextrow vector} { # # The first column: replace the nextrow-th element # The second column: replace the value at the nextrow-th element # For all the others: the same receipe # set firstcol [lreplace [lindex $tableau 0] $nextrow $nextrow $nextcol] set newtableau [list $firstcol] # # The rest of the matrix # foreach column [lrange $tableau 1 end] { set yval [lindex $column $nextrow] set newcol {} foreach c $column vcoeff $vector { set newval [expr {$c+$yval*$vcoeff}] lappend newcol $newval } lappend newtableau $newcol } return $newtableau } # # Some simple tests # namespace import ::math::optimize::* puts "Linear program:" puts "Maximum of 1.7x+1.8y with:" puts " 1.1x + 2.2y <= 1.3" puts " 2.4x + 1.5y <= 1.6" set result [solveLinearProgram \ { 1.7 1.8 } \ {{ 1.1 2.2 1.3 } { 2.4 1.5 1.6 }}] puts "Solution: $result" puts "Maximum attained: [linearProgramMaximum {1.7 1.8} $result]" puts "Linear program:" puts "Maximum of 1.7x+1.8y with:" puts " x - 2y <= 5" puts " 2x - y <= 5" set result [solveLinearProgram \ { 1.7 1.8 } \ {{ 1.0 2.0 5.0 } { 2.0 1.0 5.0 }}] puts "Solution: $result" puts "Maximum attained: [linearProgramMaximum {1.7 1.8} $result]" puts "Linear program:" puts "Maximum of 1.7x+1.8y with:" puts " 2x - y <= 5" set result [solveLinearProgram \ { 1.7 1.8 } \ {{ 2.0 1.0 5.0 }}] puts "Solution: $result" puts "Maximum attained: [linearProgramMaximum {1.7 1.8} $result]" ====== ---- [Derek Peschel] May 22, 2005 -- George B. Dantzig, the inventor of the simplex method, died on May 13 at the age of 90. <> Mathematics | Numerical Analysis