# 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<params.length
			puts i
			#puts params[i]
			case params[i]
				
				when "-pivf", "-pivm", "-piva", "-pivr", "-pivll", "-pivla", "-pivh", "-pivt", "-sp", "-si", "-se", "-presolvel", "-presolves", "-presolver", "-presolvek", "-presolveq", "-presolvem", "-presolvefd", "-presolvebnd", "-presolved", "-presolvef", "-presolveslk", "-presolveg", "-presolveb", "-presolvec", "-presolverowd", "-presolvecold", "-improve1", "-improve2", "-improve4", "-improve8", "-Bw", "-Bb", "-Bg", "-Bp", "-Bf", "-Br", "-BG", "-Bd", "-Bs", "-BB", "-Bo", "-Bc", "-Bi"
					#=== Standard 1: boolean parameters. -param_name if they're 1, nothing otherwise.
					if params[i+1] == "1"
						paramstring += " #{params[i]}"
						if params[i] =~ /-improve\d$/
							p params[i]
							set_improve_zero = false
							#puts "Should not set -improve0"
						end
					end
					i=i+1
					
				when "-piv", "-o", "-s", "-C", "-B"
					if params[i] == "-s" and params[i+1] == "1"
						paramstring  = paramstring # special case: -s1 is the default, so it shouldn't have an effect. But it changes the scale limit from 5 to 5.03. Now, I exactly replicate the same paramsout.txt file (assuming that -wpar captures all parameters...)
					else 
						#=== Standard 2: -<param_name><value>
						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 <instance_relname> <instance_specifics> <cutoff_time> <cutoff_length> <seed> <params to be passed on>."
    exit -1
end

wrap_lpsolve(ARGV)