diff --git a/Manifest.toml b/Manifest.toml index ab005eb..fa6f596 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,5 +1,11 @@ # This file is machine-generated - editing it directly is not advised +[[Artifacts]] +deps = ["Pkg"] +git-tree-sha1 = "c30985d8821e0cd73870b17b0ed0ce6dc44cb744" +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.3.0" + [[Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -10,10 +16,10 @@ uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" version = "0.5.0" [[Bzip2_jll]] -deps = ["Libdl", "Pkg"] -git-tree-sha1 = "03a44490020826950c68005cafb336e5ba08b7e8" +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "c3598e525718abcc440f69cc6d5f60dda0a1b61e" uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" -version = "1.0.6+4" +version = "1.0.6+5" [[Calculus]] deps = ["LinearAlgebra"] @@ -21,6 +27,12 @@ git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad" uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" version = "0.5.1" +[[ChainRulesCore]] +deps = ["Compat", "LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "44e9f638aa9ed1ad58885defc568c133010140aa" +uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +version = "0.9.37" + [[CodecBzip2]] deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"] git-tree-sha1 = "2e62a725210ce3c3c2e1a3080190e7ca491f18d7" @@ -41,27 +53,27 @@ version = "0.3.0" [[Compat]] deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] -git-tree-sha1 = "7c7f4cda0d58ec999189d70f5ee500348c4b4df1" +git-tree-sha1 = "4fecfd5485d3c5de4003e19f00c6898cccd40667" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "3.16.0" +version = "3.26.0" [[CompilerSupportLibraries_jll]] -deps = ["Libdl", "Pkg"] -git-tree-sha1 = "7c4f882c41faa72118841185afc58a2eb00ef612" +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "8e695f735fca77e9708e795eda62afdb869cbb70" uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "0.3.3+0" +version = "0.3.4+0" [[Conda]] deps = ["JSON", "VersionParsing"] -git-tree-sha1 = "7a58bb32ce5d85f8bf7559aa7c2842f9aecf52fc" +git-tree-sha1 = "6231e40619c15148bcb80aa19d731e629877d762" uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d" -version = "1.4.1" +version = "1.5.1" [[DataStructures]] deps = ["Compat", "InteractiveUtils", "OrderedCollections"] -git-tree-sha1 = "0347f23484a96d56e7096eb1f55c6975be34b11a" +git-tree-sha1 = "4437b64df1e0adccc3e5d1adbc3ac741095e4677" uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.18.6" +version = "0.18.9" [[Dates]] deps = ["Printf"] @@ -73,31 +85,31 @@ uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" [[DiffResults]] deps = ["StaticArrays"] -git-tree-sha1 = "da24935df8e0c6cf28de340b958f6aac88eaa0cc" +git-tree-sha1 = "c18e98cba888c6c25d1c3b048e4b3380ca956805" uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" -version = "1.0.2" +version = "1.0.3" [[DiffRules]] deps = ["NaNMath", "Random", "SpecialFunctions"] -git-tree-sha1 = "eb0c34204c8410888844ada5359ac8b96292cfd1" +git-tree-sha1 = "214c3fcac57755cfda163d91c58893a8723f93e9" uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" -version = "1.0.1" +version = "1.0.2" [[Distributed]] deps = ["Random", "Serialization", "Sockets"] uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" [[ForwardDiff]] -deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "NaNMath", "Random", "SpecialFunctions", "StaticArrays"] -git-tree-sha1 = "1d090099fb82223abc48f7ce176d3f7696ede36d" +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "NaNMath", "Printf", "Random", "SpecialFunctions", "StaticArrays"] +git-tree-sha1 = "e2af66012e08966366a43251e1fd421522908be6" uuid = "f6369f11-7733-5829-9624-2563aa707210" -version = "0.10.12" +version = "0.10.18" [[HTTP]] -deps = ["Base64", "Dates", "IniFile", "MbedTLS", "Sockets"] -git-tree-sha1 = "c7ec02c4c6a039a98a15f955462cd7aea5df4508" +deps = ["Base64", "Dates", "IniFile", "MbedTLS", "NetworkOptions", "Sockets", "URIs"] +git-tree-sha1 = "c9f380c76d8aaa1fa7ea9cf97bddbc0d5b15adc2" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "0.8.19" +version = "0.9.5" [[IniFile]] deps = ["Test"] @@ -109,6 +121,11 @@ version = "0.5.0" deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +[[JLLWrappers]] +git-tree-sha1 = "a431f5f2ca3f4feef3bd7a5e94b8b8d4f2f647a0" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.2.0" + [[JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4" @@ -123,15 +140,15 @@ version = "0.3.2" [[JSONSchema]] deps = ["HTTP", "JSON", "ZipFile"] -git-tree-sha1 = "a9ecdbc90be216912a2e3e8a8e38dc4c93f0d065" +git-tree-sha1 = "b84ab8139afde82c7c65ba2b792fe12e01dd7307" uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" -version = "0.3.2" +version = "0.3.3" [[JuMP]] -deps = ["Calculus", "DataStructures", "ForwardDiff", "JSON", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Random", "SparseArrays", "Statistics"] -git-tree-sha1 = "766014f271bd33b7f9d9bdc4847e214ee20ae84d" +deps = ["Calculus", "DataStructures", "ForwardDiff", "JSON", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Random", "SparseArrays", "SpecialFunctions", "Statistics"] +git-tree-sha1 = "e952f49e2242fa21edcf27bbd6c67041685bee5d" uuid = "4076af6c-e467-56ae-b986-b466b2749572" -version = "0.21.4" +version = "0.21.6" [[LibGit2]] deps = ["Printf"] @@ -149,9 +166,9 @@ uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" [[MacroTools]] deps = ["Markdown", "Random"] -git-tree-sha1 = "f7d2e3f654af75f01ec49be82c231c382214223a" +git-tree-sha1 = "6a8a2a625ab0dea913aba95c11370589e0239ff0" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -version = "0.5.5" +version = "0.5.6" [[Markdown]] deps = ["Base64"] @@ -159,58 +176,63 @@ uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" [[MathOptInterface]] deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "JSON", "JSONSchema", "LinearAlgebra", "MutableArithmetics", "OrderedCollections", "SparseArrays", "Test", "Unicode"] -git-tree-sha1 = "cee244578983f9c9eb09278ef54981209b09d9cb" +git-tree-sha1 = "606efe4246da5407d7505265a1ead72467528996" uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -version = "0.9.16" +version = "0.9.20" [[MbedTLS]] deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] -git-tree-sha1 = "426a6978b03a97ceb7ead77775a1da066343ec6e" +git-tree-sha1 = "1c38e51c3d08ef2278062ebceade0e46cefc96fe" uuid = "739be429-bea8-5141-9913-cc70e7f3736d" -version = "1.0.2" +version = "1.0.3" [[MbedTLS_jll]] -deps = ["Libdl", "Pkg"] -git-tree-sha1 = "c0b1286883cac4e2b617539de41111e0776d02e8" +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "0eef589dd1c26a3ac9d753fe1a8bcad63f956fa6" uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.16.8+0" +version = "2.16.8+1" [[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" [[MutableArithmetics]] deps = ["LinearAlgebra", "SparseArrays", "Test"] -git-tree-sha1 = "6cf09794783b9de2e662c4e8b60d743021e338d0" +git-tree-sha1 = "6b6bb8f550dc38310afd4a0af0786dc3222459e2" uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" -version = "0.2.10" +version = "0.2.14" [[NaNMath]] -git-tree-sha1 = "c84c576296d0e2fbb3fc134d3e09086b3ea617cd" +git-tree-sha1 = "bfe47e760d60b82b66b61d2d44128b62e3a369fb" uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" -version = "0.3.4" +version = "0.3.5" + +[[NetworkOptions]] +git-tree-sha1 = "ed3157f48a05543cce9b241e1f2815f7e843d96e" +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.2.0" [[OpenSpecFun_jll]] -deps = ["CompilerSupportLibraries_jll", "Libdl", "Pkg"] -git-tree-sha1 = "d51c416559217d974a1113522d5919235ae67a87" +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "9db77584158d0ab52307f8c04f8e7c08ca76b5b3" uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" -version = "0.5.3+3" +version = "0.5.3+4" [[OrderedCollections]] -git-tree-sha1 = "16c08bf5dba06609fe45e30860092d6fa41fde7b" +git-tree-sha1 = "4fa2ba51070ec13fcc7517db714445b4ab986bdf" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.3.1" +version = "1.4.0" [[PackageCompiler]] deps = ["Libdl", "Pkg", "UUIDs"] -git-tree-sha1 = "98aa9c653e1dc3473bb5050caf8501293db9eee1" +git-tree-sha1 = "d448727c4b86be81b225b738c88d30334fda6779" uuid = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" -version = "1.2.1" +version = "1.2.5" [[Parsers]] -deps = ["Dates", "Test"] -git-tree-sha1 = "8077624b3c450b15c087944363606a6ba12f925e" +deps = ["Dates"] +git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "1.0.10" +version = "1.1.0" [[Pkg]] deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] @@ -222,9 +244,9 @@ uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" [[PyCall]] deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"] -git-tree-sha1 = "3a3fdb9000d35958c9ba2323ca7c4958901f115d" +git-tree-sha1 = "dd1a970b543bd02efce2984582e996af28cab27f" uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" -version = "1.91.4" +version = "1.92.2" [[REPL]] deps = ["InteractiveUtils", "Markdown", "Sockets"] @@ -252,16 +274,16 @@ deps = ["LinearAlgebra", "Random"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [[SpecialFunctions]] -deps = ["OpenSpecFun_jll"] -git-tree-sha1 = "d8d8b8a9f4119829410ecd706da4cc8594a1e020" +deps = ["ChainRulesCore", "OpenSpecFun_jll"] +git-tree-sha1 = "5919936c0e92cff40e57d0ddf0ceb667d42e5902" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "0.10.3" +version = "1.3.0" [[StaticArrays]] deps = ["LinearAlgebra", "Random", "Statistics"] -git-tree-sha1 = "016d1e1a00fabc556473b07161da3d39726ded35" +git-tree-sha1 = "2f01a51c23eed210ff4a1be102c4cc8236b66e5b" uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "0.12.4" +version = "1.1.0" [[Statistics]] deps = ["LinearAlgebra", "SparseArrays"] @@ -273,9 +295,9 @@ uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[TimerOutputs]] deps = ["Printf"] -git-tree-sha1 = "f458ca23ff80e46a630922c555d838303e4b9603" +git-tree-sha1 = "32cdbe6cd2d214c25a0b88f985c9e0092877c236" uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" -version = "0.5.6" +version = "0.5.8" [[TranscodingStreams]] deps = ["Random", "Test"] @@ -283,6 +305,11 @@ git-tree-sha1 = "7c53c35547de1c5b9d46a4797cf6d8253807108c" uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" version = "0.9.5" +[[URIs]] +git-tree-sha1 = "7855809b88d7b16e9b029afd17880930626f54a2" +uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +version = "1.2.0" + [[UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" @@ -297,12 +324,12 @@ version = "1.2.0" [[ZipFile]] deps = ["Libdl", "Printf", "Zlib_jll"] -git-tree-sha1 = "254975fef2fc526583bb9b7c9420fe66ffe09f2f" +git-tree-sha1 = "c3a5637e27e914a7a445b8d0ad063d701931e9f7" uuid = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" -version = "0.9.2" +version = "0.9.3" [[Zlib_jll]] -deps = ["Libdl", "Pkg"] -git-tree-sha1 = "fdd89e5ab270ea0f2a0174bd9093e557d06d4bfa" +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "320228915c8debb12cb434c59057290f0834dbf6" uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.11+16" +version = "1.2.11+18" diff --git a/Project.toml b/Project.toml index 3c0c2ef..b1937c6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "MIPLearn" uuid = "2b1277c3-b477-4c49-a15e-7ba350325c68" authors = ["Alinson S Xavier "] -version = "0.1.0" +version = "0.2.0" [deps] Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" diff --git a/deps/build.jl b/deps/build.jl index 07fd56a..487f21f 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -5,7 +5,7 @@ function install_miplearn() Conda.update() pip = joinpath(dirname(pyimport("sys").executable), "pip") isfile(pip) || error("$pip: invalid path") - run(`$pip install miplearn==0.1.0`) + run(`$pip install miplearn==0.2.0`) end install_miplearn() diff --git a/src/instance.jl b/src/instance.jl index f2c6b6c..3917ef9 100644 --- a/src/instance.jl +++ b/src/instance.jl @@ -8,67 +8,56 @@ import Base: dump to_model(instance) = error("not implemented: to_model") -get_instance_features(instance) = - error("not implemented: get_instance_features") +get_instance_features(instance) = [0.0] -get_variable_features(instance, var, index) = - error("not implemented: get_variable_features") +get_variable_features(instance, varname) = [0.0] -get_variable_category(instance, var, index) = "default" +get_variable_category(instance, varname) = "default" find_violated_lazy_constraints(instance, model) = [] build_lazy_constraint(instance, model, v) = nothing -dump(instance::PyCall.PyObject, filename) = @pycall instance.dump(filename) -load!(instance::PyCall.PyObject, filename) = @pycall instance.load(filename) - macro Instance(klass) quote @pydef mutable struct Wrapper <: Instance function __init__(self, args...; kwargs...) self.data = $(esc(klass))(args...; kwargs...) - end - - function dump(self, filename) - prev_data = self.data - self.data = JSON2.write(prev_data) - Instance.dump(self, filename) - self.data = prev_data - end - - function load(self, filename) - Instance.load(self, filename) - self.data = JSON2.read(self.data, $(esc(klass))) + self.training_data = [] + self.features = miplearn.Features() end to_model(self) = $(esc(:to_model))(self.data) get_instance_features(self) = - get_instance_features(self.data) + $(esc(:get_instance_features))(self.data) - get_variable_features(self, var, index) = - get_variable_features(self.data, var, index) + get_variable_features(self, varname) = + $(esc(:get_variable_features))(self.data, varname) - get_variable_category(self, var, index) = - get_variable_category(self.data, var, index) + get_variable_category(self, varname) = + $(esc(:get_variable_category))(self.data, varname) - function find_violated_lazy_constraints(self, model) + find_violated_lazy_constraints(self, model) = find_violated_lazy_constraints(self.data, model) - end - function build_lazy_constraint(self, model, v) + build_lazy_constraint(self, model, v) = build_lazy_constraint(self.data, model, v) - end + + load(self) = nothing + + flush(self) = nothing end end end export get_instance_features, get_variable_features, + get_variable_category, find_violated_lazy_constraints, build_lazy_constraint, + to_model, dump, load!, @Instance \ No newline at end of file diff --git a/src/jump_solver.jl b/src/jump_solver.jl index 6e7fa97..fcb3590 100644 --- a/src/jump_solver.jl +++ b/src/jump_solver.jl @@ -9,23 +9,13 @@ using TimerOutputs mutable struct JuMPSolverData - basename_idx_to_var - var_to_basename_idx + varname_to_var optimizer instance model bin_vars - solution::Union{Nothing,Dict{String,Dict{String,Float64}}} - time_limit::Union{Nothing, Float64} -end - - -function varname_split(varname::String) - m = match(r"([^[]*)\[(.*)\]", varname) - if m == nothing - return varname, "" - end - return m.captures[1], m.captures[2] + solution::Union{Nothing, Dict{String,Float64}} + cname_to_constr end @@ -59,29 +49,24 @@ function optimize_and_capture_output!(model; tee::Bool=false) end -function solve(data::JuMPSolverData; tee::Bool=false) +function solve( + data::JuMPSolverData; + tee::Bool=false, + iteration_cb, +)::Dict instance, model = data.instance, data.model - if data.time_limit != nothing - JuMP.set_time_limit_sec(model, data.time_limit) - end wallclock_time = 0 - found_lazy = [] log = "" while true log *= optimize_and_capture_output!(model, tee=tee) wallclock_time += JuMP.solve_time(model) - violations = instance.find_violated_lazy_constraints(model) - if length(violations) == 0 + if iteration_cb !== nothing + iteration_cb() || break + else break end - append!(found_lazy, violations) - for v in violations - instance.build_lazy_constraint(data.model, v) - end end update_solution!(data) - instance.found_violated_lazy_constraints = found_lazy - instance.found_violated_user_cuts = [] primal_bound = JuMP.objective_value(model) dual_bound = JuMP.objective_bound(model) if JuMP.objective_sense(model) == MOI.MIN_SENSE @@ -93,13 +78,15 @@ function solve(data::JuMPSolverData; tee::Bool=false) lower_bound = primal_bound upper_bound = dual_bound end - return Dict("Lower bound" => lower_bound, - "Upper bound" => upper_bound, - "Sense" => sense, - "Wallclock time" => wallclock_time, - "Nodes" => 1, - "Log" => log, - "Warm start value" => nothing) + return Dict( + "Lower bound" => lower_bound, + "Upper bound" => upper_bound, + "Sense" => sense, + "Wallclock time" => wallclock_time, + "Nodes" => 1, + "MIP log" => log, + "Warm start value" => nothing, + ) end @@ -116,95 +103,270 @@ function solve_lp(data::JuMPSolverData; tee::Bool=false) for var in bin_vars JuMP.set_binary(var) end - return Dict("Optimal value" => obj_value, - "Log" => log) + return Dict( + "LP value" => obj_value, + "LP log" => log, + ) end function update_solution!(data::JuMPSolverData) - var_to_basename_idx, model = data.var_to_basename_idx, data.model - solution = Dict{String,Dict{String,Float64}}() - for var in JuMP.all_variables(model) - var in keys(var_to_basename_idx) || continue - basename, idx = var_to_basename_idx[var] - if !haskey(solution, basename) - solution[basename] = Dict{String,Float64}() - end - solution[basename][idx] = JuMP.value(var) - end - data.solution = solution -end - - -function get_variables(data::JuMPSolverData) - var_to_basename_idx, model = data.var_to_basename_idx, data.model - variables = Dict() - for var in JuMP.all_variables(model) - var in keys(var_to_basename_idx) || continue - basename, idx = var_to_basename_idx[var] - if !haskey(variables, basename) - variables[basename] = [] - end - push!(variables[basename], idx) - end - return variables + data.solution = Dict( + JuMP.name(var) => JuMP.value(var) + for var in JuMP.all_variables(data.model) + ) end function set_instance!(data::JuMPSolverData, instance, model) data.instance = instance data.model = model - data.var_to_basename_idx = Dict(var => varname_split(JuMP.name(var)) - for var in JuMP.all_variables(model)) - data.basename_idx_to_var = Dict(varname_split(JuMP.name(var)) => var - for var in JuMP.all_variables(model)) - data.bin_vars = [var - for var in JuMP.all_variables(model) - if JuMP.is_binary(var)] - if data.optimizer != nothing + data.bin_vars = [ + var + for var in JuMP.all_variables(data.model) + if JuMP.is_binary(var) + ] + data.varname_to_var = Dict( + JuMP.name(var) => var + for var in JuMP.all_variables(data.model) + ) + if data.optimizer !== nothing JuMP.set_optimizer(model, data.optimizer) end -end + data.cname_to_constr = Dict() + for (ftype, stype) in JuMP.list_of_constraint_types(model) + for constr in JuMP.all_constraints(model, ftype, stype) + name = JuMP.name(constr) + length(name) > 0 || continue + data.cname_to_constr[name] = constr + end + end +end function fix!(data::JuMPSolverData, solution) - count = 0 - for (basename, subsolution) in solution - for (idx, value) in subsolution - value != nothing || continue - var = data.basename_idx_to_var[basename, idx] - JuMP.fix(var, value, force=true) - count += 1 - end + for (varname, value) in solution + value !== nothing || continue + var = data.varname_to_var[varname] + JuMP.fix(var, value, force=true) end - @info "Fixing $count variables" end function set_warm_start!(data::JuMPSolverData, solution) - count = 0 - for (basename, subsolution) in solution - for (idx, value) in subsolution - value != nothing || continue - var = data.basename_idx_to_var[basename, idx] - JuMP.set_start_value(var, value) - count += 1 - end + for (varname, value) in solution + value !== nothing || continue + var = data.varname_to_var[varname] + JuMP.set_start_value(var, value) end - @info "Setting warm start values for $count variables" -end +end + + +function get_variable_names(data::JuMPSolverData) + return [JuMP.name(var) for var in JuMP.all_variables(data.model)] +end + + +function is_infeasible(data::JuMPSolverData) + return JuMP.termination_status(data.model) == MOI.INFEASIBLE +end + + +function get_constraint_ids(data::JuMPSolverData) + return [cname for cname in keys(data.cname_to_constr)] +end + + +function get_constraint_rhs(data::JuMPSolverData, cname) + constr = data.cname_to_constr[cname] + return get_constraint_rhs(constr) +end + + +function get_constraint_lhs(data::JuMPSolverData, cname) + constr = data.cname_to_constr[cname] + return get_constraint_lhs(constr) +end + + +function get_constraint_sense(data::JuMPSolverData, cname) + constr = data.cname_to_constr[cname] + return get_constraint_sense(constr) +end + + +# Constraints: ScalarAffineFunction, LessThan +# ------------------------------------------------------------------------- +function get_constraint_rhs( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.LessThan{T}, + }, + ScalarShape, + }, +)::T where T + return MOI.get( + constr.model.moi_backend, + MOI.ConstraintSet(), + constr.index, + ).upper +end + + +function _terms_dict(constr) + terms = MOI.get( + constr.model.moi_backend, + MOI.ConstraintFunction(), + constr.index, + ).terms + return Dict( + MOI.get( + constr.model.moi_backend, + MOI.VariableName(), + term.variable_index + ) => term.coefficient + for term in terms + ) +end + + +function get_constraint_lhs( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.LessThan{T}, + }, + ScalarShape, + }, +)::Dict{String, T} where T + return _terms_dict(constr) +end + + +function get_constraint_sense( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.LessThan{T}, + }, + ScalarShape, + }, +)::String where T + return "<" +end + + +# Constraints: ScalarAffineFunction, GreaterThan +# ------------------------------------------------------------------------- +function get_constraint_rhs( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.GreaterThan{T}, + }, + ScalarShape, + }, +)::T where T + return MOI.get( + constr.model.moi_backend, + MOI.ConstraintSet(), + constr.index, + ).lower +end + + +function get_constraint_lhs( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.GreaterThan{T}, + }, + ScalarShape, + }, +)::Dict{String, T} where T + return _terms_dict(constr) +end + + +function get_constraint_sense( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.GreaterThan{T}, + }, + ScalarShape, + }, +)::String where T + return ">" +end + + +# Constraints: ScalarAffineFunction, EqualTo +# ------------------------------------------------------------------------- +function get_constraint_rhs( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.EqualTo{T}, + }, + ScalarShape, + }, +)::T where T + return MOI.get( + constr.model.moi_backend, + MOI.ConstraintSet(), + constr.index, + ).value +end + + +function get_constraint_lhs( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.EqualTo{T}, + }, + ScalarShape, + }, +)::Dict{String, T} where T + return _terms_dict(constr) +end + + +function get_constraint_sense( + constr::ConstraintRef{ + Model, + MathOptInterface.ConstraintIndex{ + MathOptInterface.ScalarAffineFunction{T}, + MathOptInterface.EqualTo{T}, + }, + ScalarShape, + }, +)::String where T + return "=" +end + @pydef mutable struct JuMPSolver <: miplearn.solvers.internal.InternalSolver function __init__(self; optimizer) - self.data = JuMPSolverData(nothing, # basename_idx_to_var - nothing, # var_to_basename_idx - optimizer, - nothing, # instance - nothing, # model - nothing, # bin_vars - nothing, # solution - nothing, # time limit - ) + self.data = JuMPSolverData( + nothing, # varname_to_var + optimizer, + nothing, # instance + nothing, # model + nothing, # bin_vars + nothing, # solution + nothing, # cname_to_constr + ) end set_warm_start(self, solution) = @@ -216,8 +378,17 @@ end set_instance(self, instance, model) = set_instance!(self.data, instance, model) - solve(self; tee=false) = - solve(self.data, tee=tee) + solve( + self; + tee=false, + iteration_cb, + lazy_cb, + user_cut_cb, + ) = solve( + self.data, + tee=tee, + iteration_cb=iteration_cb, + ) solve_lp(self; tee=false) = solve_lp(self.data, tee=tee) @@ -228,26 +399,40 @@ end get_variables(self) = get_variables(self.data) - set_time_limit(self, time_limit) = - self.data.time_limit = time_limit - - set_gap_tolerance(self, gap_tolerance) = - @warn "JuMPSolver: set_gap_tolerance not implemented" - - set_node_limit(self) = - @warn "JuMPSolver: set_node_limit not implemented" - - set_threads(self, threads) = - @warn "JuMPSolver: set_threads not implemented" - set_branching_priorities(self, priorities) = @warn "JuMPSolver: set_branching_priorities not implemented" - add_constraint(self, constraint) = nothing + add_constraint(self, constraint) = + nothing + + get_variable_names(self) = + get_variable_names(self.data) + + is_infeasible(self) = + is_infeasible(self.data) + + get_constraint_ids(self) = + get_constraint_ids(self.data) + + get_constraint_rhs(self, cname) = + get_constraint_rhs(self.data, cname) + + get_constraint_lhs(self, cname) = + get_constraint_lhs(self.data, cname) + + get_constraint_sense(self, cname) = + get_constraint_sense(self.data, cname) - clear_warm_start(self) = - error("JuMPSolver.clear_warm_start should never be called") + clone(self) = self + add_cut(self) = error("not implemented") + extract_constraint(self) = error("not implemented") + is_constraint_satisfied(self) = error("not implemented") + set_constraint_sense(self) = error("not implemented") + relax(self) = error("not implemented") + get_inequality_slacks(self) = error("not implemented") + get_dual(self) = error("not implemented") + get_sense(self) = error("not implemented") end -export JuMPSolver, solve!, fit!, add! \ No newline at end of file +export JuMPSolver, solve!, fit!, add! diff --git a/src/learning_solver.jl b/src/learning_solver.jl index 9b6e3e0..d419238 100644 --- a/src/learning_solver.jl +++ b/src/learning_solver.jl @@ -10,19 +10,18 @@ function LearningSolver(; optimizer, kwargs..., )::LearningSolver - py = @pycall miplearn.LearningSolver(; - kwargs..., - solver=JuMPSolver(optimizer=optimizer)) + py = miplearn.LearningSolver( + ; + kwargs..., + solver=JuMPSolver(optimizer=optimizer), + ) return LearningSolver(py) end solve!(solver::LearningSolver, instance; kwargs...) = - @pycall solver.py.solve(instance; kwargs...) + solver.py.solve(instance; kwargs...) fit!(solver::LearningSolver, instances; kwargs...) = - @pycall solver.py.fit(instances; kwargs...) - -add!(solver::LearningSolver, component; kwargs...) = - @pycall solver.py.add(component; kwargs...) + solver.py.fit(instances; kwargs...) export LearningSolver \ No newline at end of file diff --git a/test/Project.toml b/test/Project.toml index 4c6d44e..9ec49cc 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,4 @@ [deps] -CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3" diff --git a/test/jump_solver_test.jl b/test/jump_solver_test.jl index a61b37a..ec5d330 100644 --- a/test/jump_solver_test.jl +++ b/test/jump_solver_test.jl @@ -4,62 +4,20 @@ using Test using MIPLearn -using CPLEX using Gurobi +using PyCall -@testset "varname_split" begin - @test MIPLearn.varname_split("x[1]") == ("x", "1") -end - +miplearn_tests = pyimport("miplearn.solvers.tests") @testset "JuMPSolver" begin - for optimizer in [CPLEX.Optimizer, Gurobi.Optimizer] - instance = KnapsackInstance([23., 26., 20., 18.], - [505., 352., 458., 220.], - 67.0) + for optimizer in [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" => Dict( - "1" => 1.0, - "2" => 0.0, - "3" => 0.0, - "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 - @test length(stats["Log"]) > 100 - - 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 - @test length(stats["Log"]) > 100 - - 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" => Dict( - "1" => 1.0, - "2" => 0.0, - "3" => 0.0, - "4" => 1.0, - ))) - stats = solver.solve() - @test stats["Lower bound"] == 725.0 - @test stats["Upper bound"] == 725.0 + miplearn_tests.test_internal_solver(solver, instance, model) end end \ No newline at end of file diff --git a/test/knapsack.jl b/test/knapsack.jl index 2b45b92..13535c2 100644 --- a/test/knapsack.jl +++ b/test/knapsack.jl @@ -17,9 +17,16 @@ end function to_model(data::KnapsackData) model = Model() n = length(data.weights) - @variable(model, x[1:n], Bin) - @objective(model, Max, sum(x[i] * data.prices[i] for i in 1:n)) - @constraint(model, sum(x[i] * data.weights[i] for i in 1:n) <= data.capacity) + @variable(model, x[0:(n-1)], Bin) + @objective(model, Max, sum(x[i] * data.prices[i+1] for i in 0:(n-1))) + @constraint( + model, + eq_capacity, + sum( + x[i] * data.weights[i+1] + for i in 0:(n-1) + ) <= data.capacity, + ) return model end diff --git a/test/learning_solver_test.jl b/test/learning_solver_test.jl index fae3737..6091ba0 100644 --- a/test/learning_solver_test.jl +++ b/test/learning_solver_test.jl @@ -4,48 +4,22 @@ using Test using MIPLearn -using CPLEX using Gurobi -@testset "Instance" begin - weights = [23., 26., 20., 18.] - prices = [505., 352., 458., 220.] - capacity = 67.0 - - instance = KnapsackInstance(weights, prices, capacity) - filename = tempname() - dump(instance, filename) - - instance = KnapsackInstance([0.0], [0.0], 0.0) - load!(instance, filename) - @test instance.data.weights == weights - @test instance.data.prices == prices - @test instance.data.capacity == capacity -end - - @testset "LearningSolver" begin - for optimizer in [CPLEX.Optimizer, Gurobi.Optimizer] - instance = KnapsackInstance([23., 26., 20., 18.], - [505., 352., 458., 220.], - 67.0) - solver = LearningSolver(optimizer=optimizer, - mode="heuristic", - time_limit=90) + for optimizer in [Gurobi.Optimizer] + instance = KnapsackInstance( + [23., 26., 20., 18.], + [505., 352., 458., 220.], + 67.0, + ) + solver = LearningSolver( + optimizer=optimizer, + mode="heuristic", + ) stats = solve!(solver, instance) - @test instance.solution["x"]["1"] == 1.0 - @test instance.solution["x"]["2"] == 0.0 - @test instance.solution["x"]["3"] == 1.0 - @test instance.solution["x"]["4"] == 1.0 - @test instance.lower_bound == 1183.0 - @test instance.upper_bound == 1183.0 - @test round(instance.lp_solution["x"]["1"], digits=3) == 1.000 - @test round(instance.lp_solution["x"]["2"], digits=3) == 0.923 - @test round(instance.lp_solution["x"]["3"], digits=3) == 1.000 - @test round(instance.lp_solution["x"]["4"], digits=3) == 0.000 - @test round(instance.lp_value, digits=3) == 1287.923 fit!(solver, [instance]) solve!(solver, instance) end -end \ No newline at end of file +end