# ParamILS wrapper for lp_solve. # In order to catch lpsolve parameter bugs, you can create a file "instance_list_obj.txt" (which in each line lists an instance and its optimal solution quality). # This wrapper will read that file, and if lpsolve (with the current parameter setting) returns a different solution quality and "proves" it to be correct, then # the result of that run is "WRONG ANSWER", and the configuration procedure (e.g. ParamILS) can penalize it. # # For use in Windows: replace ./lp_solve with your lp_solve binary, e.g. lp_solve.exe def get_optimal_solution_qual(input_file) instance_list_obj_filename = "instance_list_obj.txt" #=== That file has two columns: first column is the instance filename, second column the optimal solution quality. Columns are separated by space, no comma. If instance is not found in the file or the file doesn't exist, the verification step is skipped. if File.file?(instance_list_obj_filename) File.open(instance_list_obj_filename){|file| while line = file.gets filename, opt = line.split if filename == input_file return obj.to_f end end } end #raise "need to include objective for input file #{input_file} in file /ubc/cs/project/arrow/hutter/instance_list_obj.txt" return 0 end def float_regexp() return '[+-]?\d+(?:\.\d+)?(?:[eE][+-]\d+)?'; end def parse_lpsolve_output(output_file, quality_for_verification, allowed_mipgap, timeout, seed) #=== Parse algorithm output to extract relevant information for ParamILS. solved = nil runtime = timeout + 0.001 best_sol = 1e100 obj = nil solved = "CRASHED" timed_out = false wrong_bit = false crash_bit = false File.open(output_file){|file| while line = file.gets # puts "out: #{line}" if line =~ /Optimal solution\s+(#{float_regexp})/ obj = $1.to_f end if line =~ /solution.*\(gap (#{float_regexp})\%\)/ # Optimal or Feasible best_sol = $1.to_f elsif line =~ /^Optimal solution$/ solved = "SAT" # Nothing else in this line -> optimal best_sol = 0 # no more MIP gap end if line =~ /^Value of objective function:\s*(#{float_regexp})$/ obj = $1.to_f solved = "SAT" # this does not always mean we're optimal; if some of the timeout detectors fire, it can still be a timeout. end if line =~ /This problem is infeasible/ #solved = "UNSAT" solved = "WRONG ANSWER" # we KNOW that all input models are feasible. #raise "numerical issues with these params---lpsolve thinks the instance is infeasible" end if line =~ /lp_solve optimization was stopped due to time-out./ timed_out = true end if line =~ /Timeout/ timed_out = true end if line =~ /The model FAILED/ timed_out = true end if line =~/lp_solve failed/ timed_out = true end if line =~ /Suboptimal solution/ timed_out = true end if line =~ /alloc of .* failed/ timed_out = true # out of memory end if line =~ /in total (#{float_regexp}) seconds./ runtime = $1.to_f end if line =~ /In the total iteration count 0, 0 \(100.0%\) were bound flips./ wrong_bit = true; # didn't do anything. end if line =~/This problem is unbounded/ wrong_bit = true; # we know it's *not* unbounded end if line =~ /Value of objective function: nan/ wrong_bit = true end if line =~ /Error, Unable to open input file/ crash_bit = true end if line =~/Usage of .\/lp_solve version 5.5.0.15/ crash_bit = true end end if timed_out # and runtime >= timeout - 1e-4 solved = "TIMEOUT" # It actually happens that lpsolve reports gap=0.0% (on the line after "lp_solve optimization was stopped due to time-out."), so this is correct: very small MIP gap, but timeout. # raise "Probably parsing error: timeout but MIPgap=0" if best_sol < 1e-4 else #=== Check correctness. if solved == "SAT" raise "It looks like lpsolve has solved the instance, but I didn't see an output of the solution quality. Probably parsing error." unless obj unless quality_for_verification.to_f == 0 || quality_for_verification == "instance_specific" # for backwards compatibility with my previous runs maxi = [obj.abs, quality_for_verification.to_f.abs].max if (obj.abs - quality_for_verification.to_f.abs).abs/maxi > slack_in_my_assertions*allowed_mipgap && (obj.abs - quality_for_verification.to_f.abs).abs > 1e-8 solved = "WRONG ANSWER" #raise "lpsolve claims to have solved the instance, but its result (#{obj.abs}) differs from the actual one (#{quality_for_verification.to_f.abs}) by more than a relative error of 0.01%." if obj.abs > 1e-9 end end end end if wrong_bit solved = "WRONG ANSWER" end if crash_bit solved = "CRASHED" end if (solved == "SAT" or solved =="UNSAT") and runtime.to_f > timeout.to_f solved = "TIMEOUT" end } puts "Result for ParamILS: #{solved}, #{runtime}, 0, #{best_sol}, #{seed}" return [solved, runtime, 0, best_sol, seed] end def wrap_lpsolve(argv) #tmpdir = "/tmp" input_file = argv[0] #=== Here instance_specifics are used to verify the result Gurobi computes. instance_specifics = argv[1] #===Ignore that input; rather check instance specifics myself. optimal_qual = get_optimal_solution_qual(input_file) timeout = argv[2].to_f cutoff_length = argv[3].to_i seed = argv[4].to_i allowed_mipgap = 0.0001 slack_in_my_assertions = 1.1 #=== Go through all the parameters, and transform them to the appropriate lpsolve input command. params = argv[5...argv.length] paramstring = "" set_improve_zero = true i=0 unless params[0] == "-param_string" and params[1] == "default-params" while i paramstring += " #{params[i]}#{params[i+1]}" end i=i+1 when "-presolverowcol" case params[i+1] when "0" paramstring = paramstring # do nothing when "1" paramstring += " -presolve" when "2" paramstring += " -presolverow" when "3" paramstring += " -presolvecol" else raise "unknown value #{params[i+1]} for -presolverowcol" end i=i+1 when "-simplex_type" case params[i+1] # 5 and 9 don't work in the version I'm using: the first phase is always fixed to dual. # when "5" # paramstring += " -prim" #paramstring += " -simplexpp" when "6" paramstring += " -simplexdp" # when "9" # paramstring += " -simplexpd" when "10" paramstring += " -simplexdd" else raise "unknown value #{params[i+1]} for -simplex_type" end i=i+1 # -bfp is not working for me---doesn't find directory with the implementation. # when "-bfp" # unless params[i+1] == "NULL" # paramstring += " -bfp #{params[i+1]}" # end # i=i+1 when "-bfirst" case params[i+1] when "0" paramstring += " -cc" when "1" paramstring += " -cf" when "2" paramstring += " -ca" else raise "unknown value #{params[i+1]} for -bfirst" end i=i+1 else raise "Unknown parameter #{params[i]}. Context: #{[params[i-1], params[i], params[i+1]].join(" ")}" end i=i+1 end paramstring = paramstring + " -improve0" if set_improve_zero end p paramstring #exit # Build algorithm command and execute it. cmd = "./lp_solve #{paramstring} -timeout #{timeout.ceil} -R -v4 -S1 -depth 0" # -wpar paramsout.txt" #if input_file =~ /CATSmps/ # cmd += " -fmps #{input_file} " #else # cmd += " -mps #{input_file} " #end =begin path, filename = File.split input_file badfiles = ["conic.sch", "neos808444", "neos823206", "neos648910", "neos6.mps", "neos897005", "neos-799716", "neos-799711"] badfile = false for pattern in badfiles if input_file =~ /#{pattern}/ badfile = true end end if input_file =~ /\.lp$/ input_file = path + "/FIXED-" + filename.sub(/.lp/, ".mps") unless File.file?(input_file) input_file = input_file = path + "/CATSmps-" + filename.sub(/.lp/, ".mps") end cmd += " -fmps #{input_file} " elsif badfile input_file = path + "/FIXED-" + filename cmd += " -fmps #{input_file} " else cmd += " -mps #{input_file} " end =end if input_file =~ /\.mps$/ cmd += " -mps #{input_file} " elsif input_file =~ /\.lp$/ cmd += " #{input_file} " else raise "Unknown extension of input file: #{input_file}" end # outfile = "#{tmpdir}/lpsolve-out-#{rand}.txt" outfile = "lpsolve-out-#{rand}.txt" numtry = 1 begin puts "Calling: #{cmd} > #{outfile}" system("#{cmd} > #{outfile}") inner_exit = $? puts "inner exit: #{inner_exit}" result = parse_lpsolve_output(outfile, optimal_qual, allowed_mipgap, timeout, seed) raise "run crashed: #{cmd}" if result[0] == "CRASHED" File.delete(outfile) rescue puts $! sleep(10) numtry = numtry + 1 retry if numtry <= 5 # to safeguard against temporary problems with the fily system etc end end ################# MAIN ################# # Deal with inputs. if ARGV.length < 5 puts "lpsolve_wrapper.rb is a wrapper for lpsolve." puts "Usage: ruby lpsolve_wrapper.rb ." exit -1 end wrap_lpsolve(ARGV)