# ACTIVESTATE TEAPOT-PKG BEGIN TM -*- tcl -*- # -- Tcl Module # @@ Meta Begin # Package math::optimize 1.0 # Meta as::build::date 2014-09-02 # Meta as::origin http://sourceforge.net/projects/tcllib # Meta category Tcl Math Library # Meta description Optimisation routines # Meta license BSD # Meta platform tcl # Meta require {Tcl 8.4} # Meta subject minimum maximum math optimization {linear program} # Meta summary math::optimize # @@ Meta End # ACTIVESTATE TEAPOT-PKG BEGIN REQUIREMENTS package require Tcl 8.4 # ACTIVESTATE TEAPOT-PKG END REQUIREMENTS # ACTIVESTATE TEAPOT-PKG BEGIN DECLARE package provide math::optimize 1.0 # ACTIVESTATE TEAPOT-PKG END DECLARE # ACTIVESTATE TEAPOT-PKG END TM #---------------------------------------------------------------------- # # math/optimize.tcl -- # # This file contains functions for optimization of a function # or expression. # # Copyright (c) 2004, by Arjen Markus. # Copyright (c) 2004, 2005 by Kevin B. Kenny. All rights reserved. # # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # # RCS: @(#) $Id: optimize.tcl,v 1.12 2011/01/18 07:49:53 arjenmarkus Exp $ # #---------------------------------------------------------------------- package require Tcl 8.4 # math::optimize -- # Namespace for the commands # namespace eval ::math::optimize { namespace export minimum maximum solveLinearProgram linearProgramMaximum namespace export min_bound_1d min_unbound_1d # Possible extension: minimumExpr, maximumExpr } # minimum -- # Minimize a given function over a given interval # # Arguments: # begin Start of the interval # end End of the interval # func Name of the function to be minimized (takes one # argument) # maxerr Maximum relative error (defaults to 1.0e-4) # Return value: # Computed value for which the function is minimal # Notes: # The function needs not to be differentiable, but it is supposed # to be continuous. There is no provision for sub-intervals where # the function is constant (this might happen when the maximum # error is very small, < 1.0e-15) # # Warning: # This procedure is deprecated - use min_bound_1d instead # proc ::math::optimize::minimum { begin end func {maxerr 1.0e-4} } { set nosteps [expr {3+int(-log($maxerr)/log(2.0))}] set delta [expr {0.5*($end-$begin)*$maxerr}] for { set step 0 } { $step < $nosteps } { incr step } { set x1 [expr {($end+$begin)/2.0}] set x2 [expr {$x1+$delta}] set fx1 [uplevel 1 $func $x1] set fx2 [uplevel 1 $func $x2] if {$fx1 < $fx2} { set end $x1 } else { set begin $x1 } } return $x1 } # maximum -- # Maximize a given function over a given interval # # Arguments: # begin Start of the interval # end End of the interval # func Name of the function to be maximized (takes one # argument) # maxerr Maximum relative error (defaults to 1.0e-4) # Return value: # Computed value for which the function is maximal # Notes: # The function needs not to be differentiable, but it is supposed # to be continuous. There is no provision for sub-intervals where # the function is constant (this might happen when the maximum # error is very small, < 1.0e-15) # # Warning: # This procedure is deprecated - use max_bound_1d instead # proc ::math::optimize::maximum { begin end func {maxerr 1.0e-4} } { set nosteps [expr {3+int(-log($maxerr)/log(2.0))}] set delta [expr {0.5*($end-$begin)*$maxerr}] for { set step 0 } { $step < $nosteps } { incr step } { set x1 [expr {($end+$begin)/2.0}] set x2 [expr {$x1+$delta}] set fx1 [uplevel 1 $func $x1] set fx2 [uplevel 1 $func $x2] if {$fx1 > $fx2} { set end $x1 } else { set begin $x1 } } return $x1 } #---------------------------------------------------------------------- # # min_bound_1d -- # # Find a local minimum of a function between two given # abscissae. Derivative of f is not required. # # Usage: # min_bound_1d f x1 x2 ?-option value?,,, # # Parameters: # f - Function to minimize. Must be expressed as a Tcl # command, to which will be appended the value at which # to evaluate the function. # x1 - Lower bound of the interval in which to search for a # minimum # x2 - Upper bound of the interval in which to search for a minimum # # Options: # -relerror value # Gives the tolerance desired for the returned # abscissa. Default is 1.0e-7. Should never be less # than the square root of the machine precision. # -maxiter n # Constrains minimize_bound_1d to evaluate the function # no more than n times. Default is 100. If convergence # is not achieved after the specified number of iterations, # an error is thrown. # -guess value # Gives a point between x1 and x2 that is an initial guess # for the minimum. f(guess) must be at most f(x1) or # f(x2). # -fguess value # Gives the value of the ordinate at the value of '-guess' # if known. Default is to evaluate the function # -abserror value # Gives the desired absolute error for the returned # abscissa. Default is 1.0e-10. # -trace boolean # A true value causes a trace to the standard output # of the function evaluations. Default is 0. # # Results: # Returns a two-element list comprising the abscissa at which # the function reaches a local minimum within the interval, # and the value of the function at that point. # # Side effects: # Whatever side effects arise from evaluating the given function. # #---------------------------------------------------------------------- proc ::math::optimize::min_bound_1d { f x1 x2 args } { set f [lreplace $f 0 0 [uplevel 1 [list namespace which [lindex $f 0]]]] set phim1 0.6180339887498949 set twomphi 0.3819660112501051 array set params { -relerror 1.0e-7 -abserror 1.0e-10 -maxiter 100 -trace 0 -fguess {} } set params(-guess) [expr { $phim1 * $x1 + $twomphi * $x2 }] if { ( [llength $args] % 2 ) != 0 } { return -code error -errorcode [list min_bound_1d wrongNumArgs] \ "wrong \# args, should be\ \"[lreplace [info level 0] 1 end f x1 x2 ?-option value?...]\"" } foreach { key value } $args { if { ![info exists params($key)] } { return -code error -errorcode [list min_bound_1d badoption $key] \ "unknown option \"$key\",\ should be -abserror,\ -fguess, -guess, -initial, -maxiter, -relerror,\ or -trace" } set params($key) $value } # a and b presumably bracket the minimum of the function. Make sure # they're in ascending order. if { $x1 < $x2 } { set a $x1; set b $x2 } else { set b $x1; set a $x2 } set x $params(-guess); # Best abscissa found so far set w $x; # Second best abscissa found so far set v $x; # Most recent earlier value of w set e 0.0; # Distance moved on the step before # last. # Evaluate the function at the initial guess if { $params(-fguess) ne {} } { set fx $params(-fguess) } else { set s $f; lappend s $x; set fx [eval $s] if { $params(-trace) } { puts stdout "f($x) = $fx (initialisation)" } } set fw $fx set fv $fx for { set iter 0 } { $iter < $params(-maxiter) } { incr iter } { # Find the midpoint of the current interval set xm [expr { 0.5 * ( $a + $b ) }] # Compute the current tolerance for x, and twice its value set tol [expr { $params(-relerror) * abs($x) + $params(-abserror) }] set tol2 [expr { $tol + $tol }] if { abs( $x - $xm ) <= $tol2 - 0.5 * ($b - $a) } { return [list $x $fx] } set golden 1 if { abs($e) > $tol } { # Use parabolic interpolation to find a minimum determined # by the evaluations at x, v, and w. The size of the step # to take will be $p/$q. set r [expr { ( $x - $w ) * ( $fx - $fv ) }] set q [expr { ( $x - $v ) * ( $fx - $fw ) }] set p [expr { ( $x - $v ) * $q - ( $x - $w ) * $r }] set q [expr { 2. * ( $q - $r ) }] if { $q > 0 } { set p [expr { - $p }] } else { set q [expr { - $q }] } set olde $e set e $d # Test if parabolic interpolation results in less than half # the movement of the step two steps ago. if { abs($p) < abs( .5 * $q * $olde ) && $p > $q * ( $a - $x ) && $p < $q * ( $b - $x ) } { set d [expr { $p / $q }] set u [expr { $x + $d }] if { ( $u - $a ) < $tol2 || ( $b - $u ) < $tol2 } { if { $xm-$x < 0 } { set d [expr { - $tol }] } else { set d $tol } } set golden 0 } } # If parabolic interpolation didn't come up with an acceptable # result, use Golden Section instead. if { $golden } { if { $x >= $xm } { set e [expr { $a - $x }] } else { set e [expr { $b - $x }] } set d [expr { $twomphi * $e }] } # At this point, d is the size of the step to take. Make sure # that it's at least $tol. if { abs($d) >= $tol } { set u [expr { $x + $d }] } elseif { $d < 0 } { set u [expr { $x - $tol }] } else { set u [expr { $x + $tol }] } # Evaluate the function set s $f; lappend s $u; set fu [eval $s] if { $params(-trace) } { if { $golden } { puts stdout "f($u)=$fu (golden section)" } else { puts stdout "f($u)=$fu (parabolic interpolation)" } } if { $fu <= $fx } { # We've the best abscissa so far. if { $u >= $x } { set a $x } else { set b $x } set v $w set fv $fw set w $x set fw $fx set x $u set fx $fu } else { if { $u < $x } { set a $u } else { set b $u } if { $fu <= $fw || $w == $x } { # We've the second-best abscissa so far set v $w set fv $fw set w $u set fw $fu } elseif { $fu <= $fv || $v == $x || $v == $w } { # We've the third-best so far set v $u set fv $fu } } } return -code error -errorcode [list min_bound_1d noconverge $iter] \ "[lindex [info level 0] 0] failed to converge after $iter steps." } #---------------------------------------------------------------------- # # brackmin -- # # Find a place along the number line where a given function has # a local minimum. # # Usage: # brackmin f x1 x2 ?trace? # # Parameters: # f - Function to minimize # x1 - Abscissa thought to be near the minimum # x2 - Additional abscissa thought to be near the minimum # trace - Boolean variable that, if true, # causes 'brackmin' to print a trace of its function # evaluations to the standard output. Default is 0. # # Results: # Returns a three element list {x1 y1 x2 y2 x3 y3} where # y1=f(x1), y2=f(x2), y3=f(x3). x2 lies between x1 and x3, and # y1>y2, y3>y2, proving that there is a local minimum somewhere # in the interval (x1,x3). # # Side effects: # Whatever effects the evaluation of f has. # #---------------------------------------------------------------------- proc ::math::optimize::brackmin { f x1 x2 {trace 0} } { set f [lreplace $f 0 0 [uplevel 1 [list namespace which [lindex $f 0]]]] set phi 1.6180339887498949 set epsilon 1.0e-20 set limit 50. # Choose a and b so that f(a) < f(b) set cmd $f; lappend cmd $x1; set fx1 [eval $cmd] if { $trace } { puts "f($x1) = $fx1 (initialisation)" } set cmd $f; lappend cmd $x2; set fx2 [eval $cmd] if { $trace } { puts "f($x2) = $fx2 (initialisation)" } if { $fx1 > $fx2 } { set a $x1; set fa $fx1 set b $x2; set fb $fx2 } else { set a $x2; set fa $fx2 set b $x1; set fb $fx1 } # Choose a c in the downhill direction set c [expr { $b + $phi * ($b - $a) }] set cmd $f; lappend cmd $c; set fc [eval $cmd] if { $trace } { puts "f($c) = $fc (initial dilatation by phi)" } while { $fb >= $fc } { # Try to do parabolic extrapolation to the minimum set r [expr { ($b - $a) * ($fb - $fc) }] set q [expr { ($b - $c) * ($fb - $fa) }] if { abs( $q - $r ) > $epsilon } { set denom [expr { $q - $r }] } elseif { $q > $r } { set denom $epsilon } else { set denom -$epsilon } set u [expr { $b - ( (($b - $c) * $q - ($b - $a) * $r) / (2. * $denom) ) }] set ulimit [expr { $b + $limit * ( $c - $b ) }] # Test the extrapolated abscissa if { ($b - $u) * ($u - $c) > 0 } { # u lies between b and c. Try to interpolate set cmd $f; lappend cmd $u; set fu [eval $cmd] if { $trace } { puts "f($u) = $fu (parabolic interpolation)" } if { $fu < $fc } { # fb > fu and fc > fu, so there is a minimum between b and c # with u as a starting guess. return [list $b $fb $u $fu $c $fc] } if { $fu > $fb } { # fb < fu, fb < fa, and u cannot lie between a and b # (because it lies between a and c). There is a minimum # somewhere between a and u, with b a starting guess. return [list $a $fa $b $fb $u $fu] } # Parabolic interpolation was useless. Expand the # distance by a factor of phi and try again. set u [expr { $c + $phi * ($c - $b) }] set cmd $f; lappend cmd $u; set fu [eval $cmd] if { $trace } { puts "f($u) = $fu (parabolic interpolation failed)" } } elseif { ( $c - $u ) * ( $u - $ulimit ) > 0 } { # u lies between $c and $ulimit. set cmd $f; lappend cmd $u; set fu [eval $cmd] if { $trace } { puts "f($u) = $fu (parabolic extrapolation)" } if { $fu > $fc } { # minimum lies between b and u, with c an initial guess. return [list $b $fb $c $fc $u $fu] } # function is still decreasing fa > fb > fc > fu. Take # another factor-of-phi step. set b $c; set fb $fc set c $u; set fc $fu set u [expr { $c + $phi * ( $c - $b ) }] set cmd $f; lappend cmd $u; set fu [eval $cmd] if { $trace } { puts "f($u) = $fu (parabolic extrapolation ok)" } } elseif { ($u - $ulimit) * ( $ulimit - $c ) >= 0 } { # u went past ulimit. Pull in to ulimit and evaluate there. set u $ulimit set cmd $f; lappend cmd $u; set fu [eval $cmd] if { $trace } { puts "f($u) = $fu (limited step)" } } else { # parabolic extrapolation gave a useless value. set u [expr { $c + $phi * ( $c - $b ) }] set cmd $f; lappend cmd $u; set fu [eval $cmd] if { $trace } { puts "f($u) = $fu (parabolic extrapolation failed)" } } set a $b; set fa $fb set b $c; set fb $fc set c $u; set fc $fu } return [list $a $fa $b $fb $c $fc] } #---------------------------------------------------------------------- # # min_unbound_1d -- # # Minimize a function of one variable, unconstrained, derivatives # not required. # # Usage: # min_bound_1d f x1 x2 ?-option value?,,, # # Parameters: # f - Function to minimize. Must be expressed as a Tcl # command, to which will be appended the value at which # to evaluate the function. # x1 - Initial guess at the minimum # x2 - Second initial guess at the minimum, used to set the # initial length scale for the search. # # Options: # -relerror value # Gives the tolerance desired for the returned # abscissa. Default is 1.0e-7. Should never be less # than the square root of the machine precision. # -maxiter n # Constrains min_bound_1d to evaluate the function # no more than n times. Default is 100. If convergence # is not achieved after the specified number of iterations, # an error is thrown. # -abserror value # Gives the desired absolute error for the returned # abscissa. Default is 1.0e-10. # -trace boolean # A true value causes a trace to the standard output # of the function evaluations. Default is 0. # #---------------------------------------------------------------------- proc ::math::optimize::min_unbound_1d { f x1 x2 args } { set f [lreplace $f 0 0 [uplevel 1 [list namespace which [lindex $f 0]]]] array set params { -relerror 1.0e-7 -abserror 1.0e-10 -maxiter 100 -trace 0 } if { ( [llength $args] % 2 ) != 0 } { return -code error -errorcode [list min_unbound_1d wrongNumArgs] \ "wrong \# args, should be\ \"[lreplace [info level 0] 1 end \ f x1 x2 ?-option value?...]\"" } foreach { key value } $args { if { ![info exists params($key)] } { return -code error -errorcode [list min_unbound_1d badoption $key] \ "unknown option \"$key\",\ should be -trace" } set params($key) $value } foreach { a fa b fb c fc } [brackmin $f $x1 $x2 $params(-trace)] { break } return [eval [linsert [array get params] 0 \ min_bound_1d $f $a $c -guess $b -fguess $fb]] } #---------------------------------------------------------------------- # # nelderMead -- # # Attempt to minimize/maximize a function using the downhill # simplex method of Nelder and Mead. # # Usage: # nelderMead f x ?-keyword value? # # Parameters: # f - The function to minimize. The function must be an incomplete # Tcl command, to which will be appended N parameters. # x - The starting guess for the minimum; a vector of N parameters # to be passed to the function f. # # Options: # -scale xscale # Initial guess as to the problem scale. If '-scale' is # supplied, then the parameters will be varied by the # specified amounts. The '-scale' parameter must of the # same dimension as the 'x' vector, and all elements must # be nonzero. Default is 0.0001 times the 'x' vector, # or 0.0001 for zero elements in the 'x' vector. # # -ftol epsilon # Requested tolerance in the function value; nelderMead # returns if N+1 consecutive iterates all differ by less # than the -ftol value. Default is 1.0e-7 # # -maxiter N # Maximum number of iterations to attempt. Default is # 500. # # -trace flag # If '-trace 1' is supplied, nelderMead writes a record # of function evaluations to the standard output as it # goes. Default is 0. # #---------------------------------------------------------------------- proc ::math::optimize::nelderMead { f startx args } { array set params { -ftol 1.e-7 -maxiter 500 -scale {} -trace 0 } # Check arguments if { ( [llength $args] % 2 ) != 0 } { return -code error -errorcode [list nelderMead wrongNumArgs] \ "wrong \# args, should be\ \"[lreplace [info level 0] 1 end \ f x1 x2 ?-option value?...]\"" } foreach { key value } $args { if { ![info exists params($key)] } { return -code error -errorcode [list nelderMead badoption $key] \ "unknown option \"$key\",\ should be -ftol, -maxiter, -scale or -trace" } set params($key) $value } # Construct the initial simplex set vertices [list $startx] if { [llength $params(-scale)] == 0 } { set i 0 foreach x0 $startx { if { $x0 == 0 } { set x1 0.0001 } else { set x1 [expr {1.0001 * $x0}] } lappend vertices [lreplace $startx $i $i $x1] incr i } } elseif { [llength $params(-scale)] != [llength $startx] } { return -code error -errorcode [list nelderMead badOption -scale] \ "-scale vector must be of same size as starting x vector" } else { set i 0 foreach x0 $startx s $params(-scale) { lappend vertices [lreplace $startx $i $i [expr { $x0 + $s }]] incr i } } # Evaluate at the initial points set n [llength $startx] foreach x $vertices { set cmd $f foreach xx $x { lappend cmd $xx } set y [uplevel 1 $cmd] if {$params(-trace)} { puts "nelderMead: evaluating initial point: x=[list $x] y=$y" } lappend yvec $y } # Loop adjusting the simplex in the 'vertices' array. set nIter 0 while { 1 } { # Find the highest, next highest, and lowest value in y, # and save the indices. set iBot 0 set yBot [lindex $yvec 0] set iTop -1 set yTop [lindex $yvec 0] set iNext -1 set i 0 foreach y $yvec { if { $y <= $yBot } { set yBot $y set iBot $i } if { $iTop < 0 || $y >= $yTop } { set iNext $iTop set yNext $yTop set iTop $i set yTop $y } elseif { $iNext < 0 || $y >= $yNext } { set iNext $i set yNext $y } incr i } # Return if the relative error is within an acceptable range set rerror [expr { 2. * abs( $yTop - $yBot ) / ( abs( $yTop ) + abs( $yBot ) ) }] if { $rerror < $params(-ftol) } { set status ok break } # Count iterations if { [incr nIter] > $params(-maxiter) } { set status too-many-iterations break } incr nIter # Find the centroid of the face opposite the vertex that # maximizes the function value. set centroid {} for { set i 0 } { $i < $n } { incr i } { lappend centroid 0.0 } set i 0 foreach v $vertices { if { $i != $iTop } { set newCentroid {} foreach x0 $centroid x1 $v { lappend newCentroid [expr { $x0 + $x1 }] } set centroid $newCentroid } incr i } set newCentroid {} foreach x $centroid { lappend newCentroid [expr { $x / $n }] } set centroid $newCentroid # The first trial point is a reflection of the high point # around the centroid set trial {} foreach x0 [lindex $vertices $iTop] x1 $centroid { lappend trial [expr {$x1 + ($x1 - $x0)}] } set cmd $f foreach xx $trial { lappend cmd $xx } set yTrial [uplevel 1 $cmd] if { $params(-trace) } { puts "nelderMead: trying reflection: x=[list $trial] y=$yTrial" } # If that reflection yields a new minimum, replace the high point, # and additionally try dilating in the same direction. if { $yTrial < $yBot } { set trial2 {} foreach x0 $centroid x1 $trial { lappend trial2 [expr { $x1 + ($x1 - $x0) }] } set cmd $f foreach xx $trial2 { lappend cmd $xx } set yTrial2 [uplevel 1 $cmd] if { $params(-trace) } { puts "nelderMead: trying dilated reflection:\ x=[list $trial2] y=$y" } if { $yTrial2 < $yBot } { # Additional dilation yields a new minimum lset vertices $iTop $trial2 lset yvec $iTop $yTrial2 } else { # Additional dilation failed, but we can still use # the first trial point. lset vertices $iTop $trial lset yvec $iTop $yTrial } } elseif { $yTrial < $yNext } { # The reflected point isn't a new minimum, but it's # better than the second-highest. Replace the old high # point and try again. lset vertices $iTop $trial lset yvec $iTop $yTrial } else { # The reflected point is worse than the second-highest point. # If it's better than the highest, keep it... but in any case, # we want to try contracting the simplex, because a further # reflection will simply bring us back to the starting point. if { $yTrial < $yTop } { lset vertices $iTop $trial lset yvec $iTop $yTrial set yTop $yTrial } set trial {} foreach x0 [lindex $vertices $iTop] x1 $centroid { lappend trial [expr { ( $x0 + $x1 ) / 2. }] } set cmd $f foreach xx $trial { lappend cmd $xx } set yTrial [uplevel 1 $cmd] if { $params(-trace) } { puts "nelderMead: contracting from high point:\ x=[list $trial] y=$y" } if { $yTrial < $yTop } { # Contraction gave an improvement, so continue with # the smaller simplex lset vertices $iTop $trial lset yvec $iTop $yTrial } else { # Contraction gave no improvement either; we seem to # be in a valley of peculiar topology. Contract the # simplex about the low point and try again. set newVertices {} set newYvec {} set i 0 foreach v $vertices y $yvec { if { $i == $iBot } { lappend newVertices $v lappend newYvec $y } else { set newv {} foreach x0 $v x1 [lindex $vertices $iBot] { lappend newv [expr { ($x0 + $x1) / 2. }] } lappend newVertices $newv set cmd $f foreach xx $newv { lappend cmd $xx } lappend newYvec [uplevel 1 $cmd] if { $params(-trace) } { puts "nelderMead: contracting about low point:\ x=[list $newv] y=$y" } } incr i } set vertices $newVertices set yvec $newYvec } } } return [list y $yBot x [lindex $vertices $iBot] vertices $vertices yvec $yvec nIter $nIter status $status] } # 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} 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 # # Note: # The tableau should be transposed ... # 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 } # # Add the columns for the slack variables # set zeros {} for {set idx 0} {$idx <= $nconst} {incr idx} { lappend zeros 0.0 } for {set idx 0} {$idx < $nconst} {incr idx} { lappend matrix [lreplace $zeros $idx $idx 1.0] } 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] } # # 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 } # Now we can announce our presence package provide math::optimize 1.0 if { ![info exists ::argv0] || [string compare $::argv0 [info script]] } { return } namespace import math::optimize::min_bound_1d namespace import math::optimize::maximum namespace import math::optimize::nelderMead proc f {x y} { set xx [expr { $x - 3.1415926535897932 / 2. }] set v1 [expr { 0.3 * exp( -$xx*$xx / 2. ) }] set d [expr { 10. * $y - sin(9. * $x) }] set v2 [expr { exp(-10.*$d*$d)}] set rv [expr { -$v1 - $v2 }] return $rv } proc g {a b} { set x1 [expr {0.1 - $a + $b}] set x2 [expr {$a + $b - 1.}] set x3 [expr {3.-8.*$a+8.*$a*$a-8.*$b+8.*$b*$b}] set x4 [expr {$a/10. + $b/10. + $x1*$x1/3. + $x2*$x2 - $x2 * exp(1-$x3*$x3)}] return $x4 } set prec $::tcl_precision if {![package vsatisfies [package provide Tcl] 8.5]} { set ::tcl_precision 17 } else { set ::tcl_precision 0 } puts "f" puts [math::optimize::nelderMead f {1. 0.} -scale {0.1 0.01} -trace 1] puts "g" puts [math::optimize::nelderMead g {0. 0.} -scale {1. 1.} -trace 1] set ::tcl_precision $prec