diff --git a/src/julia/Manifest.toml b/src/julia/Manifest.toml index 21fc833..3d079cb 100644 --- a/src/julia/Manifest.toml +++ b/src/julia/Manifest.toml @@ -26,6 +26,12 @@ git-tree-sha1 = "62847acab40e6855a9b5905ccb99c2b5cf6b3ebb" uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" version = "0.2.0" +[[CPLEX]] +deps = ["Libdl", "LinearAlgebra", "MathOptInterface", "MathProgBase", "SparseArrays"] +git-tree-sha1 = "f8dac98fbff2f7d7fe58fa1fcdbefaaaf29a1f59" +uuid = "a076750e-1247-5638-91d2-ce28b192dca0" +version = "0.6.5" + [[CPLEXW]] deps = ["CEnum", "Libdl"] git-tree-sha1 = "ebad297748ee2a12cc13b5fb07f9bbfa8a900494" diff --git a/src/julia/Project.toml b/src/julia/Project.toml index 7d6d77c..f42b7b9 100644 --- a/src/julia/Project.toml +++ b/src/julia/Project.toml @@ -4,6 +4,7 @@ authors = ["Alinson S Xavier "] version = "0.1.0" [deps] +CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" CPLEXW = "cfecb002-79c2-11e9-35be-cb59aa640f85" Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" diff --git a/src/julia/src/MIPLearn.jl b/src/julia/src/MIPLearn.jl index d0fe206..264d6a5 100644 --- a/src/julia/src/MIPLearn.jl +++ b/src/julia/src/MIPLearn.jl @@ -6,7 +6,6 @@ __precompile__(false) module MIPLearn using JuMP -using Gurobi using PyCall using MathOptInterface const MOI = MathOptInterface @@ -17,26 +16,44 @@ LearningSolver = miplearn.LearningSolver InternalSolver = miplearn.solvers.internal.InternalSolver @pydef mutable struct JuMPSolver <: InternalSolver + function __init__(self; optimizer=CPLEX.Optimizer) + self.optimizer = optimizer + end + function add_constraint(self, constraint) + @error "Not implemented" end function set_warm_start(self, solution) + for (varname, value) in solution + var = JuMP.variable_by_name(self.model, varname) + JuMP.set_start_value(var, value) + end end function clear_warm_start(self) + @error "Not implemented" end function fix(self, solution) + for (varname, value) in solution + var = JuMP.variable_by_name(self.model, varname) + JuMP.fix(var, value, force=true) + end end function set_instance(self, instance, model) self.instance = instance self.model = model + self.bin_vars = [var + for var in JuMP.all_variables(self.model) + if JuMP.is_binary(var)] + JuMP.set_optimizer(self.model, self.optimizer) end function solve(self; tee=false) - JuMP.set_optimizer(self.model, Gurobi.Optimizer) JuMP.optimize!(self.model) + self._update_solution() primal_bound = JuMP.objective_value(self.model) dual_bound = JuMP.objective_bound(self.model) @@ -55,27 +72,54 @@ InternalSolver = miplearn.solvers.internal.InternalSolver return Dict("Lower bound" => lower_bound, "Upper bound" => upper_bound, - "Sense" => sense) + "Sense" => sense, + "Wallclock time" => JuMP.solve_time(self.model), + "Nodes" => 1, + "Log" => nothing, + "Warm start value" => nothing) end function solve_lp(self; tee=false) + for var in self.bin_vars + JuMP.unset_binary(var) + JuMP.set_upper_bound(var, 1.0) + JuMP.set_lower_bound(var, 0.0) + end + + JuMP.optimize!(self.model) + obj_value = JuMP.objective_value(self.model) + self._update_solution() + + for var in self.bin_vars + JuMP.set_binary(var) + end + + return Dict("Optimal value" => obj_value) end function get_solution(self) - return Dict(JuMP.name(var) => JuMP.value(var) - for var in JuMP.all_variables(self.model)) + return self.solution + end + + function _update_solution(self) + self.solution = Dict(JuMP.name(var) => JuMP.value(var) + for var in JuMP.all_variables(self.model)) end function set_gap_tolerance(self, gap_tolerance) + @error "Not implemented" end function set_node_limit(self) + @error "Not implemented" end function set_threads(self, threads) + @error "Not implemented" end function set_time_limit(self, time_limit) + JuMP.set_time_limit_sec(self.model, time_limit) end end diff --git a/src/julia/test/jump_solver.jl b/src/julia/test/jump_solver.jl new file mode 100644 index 0000000..ac53a78 --- /dev/null +++ b/src/julia/test/jump_solver.jl @@ -0,0 +1,58 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using Test +using MIPLearn +using CPLEX +using Gurobi + +@testset "JuMPSolver" begin + for optimizer in [CPLEX.Optimizer, Gurobi.Optimizer] + instance = KnapsackInstance([23., 26., 20., 18.], + [505., 352., 458., 220.], + 67.0) + model = instance.to_model() + + solver = JuMPSolver(optimizer=optimizer) + solver.set_instance(instance, model) + solver.set_time_limit(30) + solver.set_warm_start(Dict( + "x[1]" => 1.0, + "x[2]" => 0.0, + "x[3]" => 0.0, + "x[4]" => 1.0, + )) + stats = solver.solve() + + @test stats["Lower bound"] == 1183.0 + @test stats["Upper bound"] == 1183.0 + @test stats["Sense"] == "max" + @test stats["Wallclock time"] > 0 + + solution = solver.get_solution() + @test solution["x[1]"] == 1.0 + @test solution["x[2]"] == 0.0 + @test solution["x[3]"] == 1.0 + @test solution["x[4]"] == 1.0 + + stats = solver.solve_lp() + @test round(stats["Optimal value"], digits=3) == 1287.923 + + solution = solver.get_solution() + @test round(solution["x[1]"], digits=3) == 1.000 + @test round(solution["x[2]"], digits=3) == 0.923 + @test round(solution["x[3]"], digits=3) == 1.000 + @test round(solution["x[4]"], digits=3) == 0.000 + + solver.fix(Dict( + "x[1]" => 1.0, + "x[2]" => 0.0, + "x[3]" => 0.0, + "x[4]" => 1.0, + )) + stats = solver.solve() + @test stats["Lower bound"] == 725.0 + @test stats["Upper bound"] == 725.0 + end +end \ No newline at end of file diff --git a/src/julia/test/runtests.jl b/src/julia/test/runtests.jl index 2ac8082..7d262dd 100644 --- a/src/julia/test/runtests.jl +++ b/src/julia/test/runtests.jl @@ -4,36 +4,9 @@ using Test using MIPLearn +using CPLEX +using Gurobi @testset "MIPLearn" begin - instance = KnapsackInstance([23., 26., 20., 18.], - [505., 352., 458., 220.], - 67.0) - model = instance.to_model() - - solver = JuMPSolver() - solver.set_instance(instance, model) - stats = solver.solve() - - # assert len(stats["Log"]) > 100 - @test stats["Lower bound"] == 1183.0 - @test stats["Upper bound"] == 1183.0 - @test stats["Sense"] == "max" - # @test isinstance(stats["Wallclock time"], float) - # @test isinstance(stats["Nodes"], int) - - solution = solver.get_solution() - @test solution["x[1]"] == 1.0 - @test solution["x[2]"] == 0.0 - @test solution["x[3]"] == 1.0 - @test solution["x[4]"] == 1.0 - - # stats = solver.solve_lp() - # @test round(stats["Optimal value"], 3) == 1287.923 - # - # solution = solver.get_solution() - # @test round(solution["x"][0], 3) == 1.000 - # @test round(solution["x"][1], 3) == 0.923 - # @test round(solution["x"][2], 3) == 1.000 - # @test round(solution["x"][3], 3) == 0.000 + include("jump_solver.jl") end \ No newline at end of file