From 9b9f4d968dbd9c3aa0aa2f7d311e97e18e3a7aea Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 29 Aug 2020 12:47:46 -0500 Subject: [PATCH] Initial version --- .gitignore | 10 ++ LICENSE | 25 +++ Manifest.toml | 339 +++++++++++++++++++++++++++++++++++ Project.toml | 22 +++ src/MIPLearn.jl | 41 +++++ src/instance.jl | 61 +++++++ src/jump_solver.jl | 254 ++++++++++++++++++++++++++ src/learning_solver.jl | 28 +++ src/log.jl | 62 +++++++ src/sysimage.jl | 22 +++ test/jump_solver_test.jl | 65 +++++++ test/knapsack.jl | 34 ++++ test/learning_solver_test.jl | 50 ++++++ test/runtests.jl | 14 ++ 14 files changed, 1027 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Manifest.toml create mode 100644 Project.toml create mode 100644 src/MIPLearn.jl create mode 100644 src/instance.jl create mode 100644 src/jump_solver.jl create mode 100644 src/learning_solver.jl create mode 100644 src/log.jl create mode 100644 src/sysimage.jl create mode 100644 test/jump_solver_test.jl create mode 100644 test/knapsack.jl create mode 100644 test/learning_solver_test.jl create mode 100644 test/runtests.jl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd393ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.jl.*.cov +*.jl.cov +*.jl.mem +deps/build.log +deps/deps.jl +deps/downloads/ +deps/src/ +deps/usr/ +docs/build/ +docs/site/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..497106c --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright © 2020, UChicago Argonne, LLC + +All Rights Reserved + +Software Name: MIPLearn + +By: Argonne National Laboratory + +OPEN SOURCE LICENSE +------------------- + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +******************************************************************************** + +DISCLAIMER +---------- + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +******************************************************************************** diff --git a/Manifest.toml b/Manifest.toml new file mode 100644 index 0000000..e536c33 --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,339 @@ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[BenchmarkTools]] +deps = ["JSON", "Logging", "Printf", "Statistics", "UUIDs"] +git-tree-sha1 = "9e62e66db34540a0c919d72172cc2f642ac71260" +uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +version = "0.5.0" + +[[Bzip2_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "03a44490020826950c68005cafb336e5ba08b7e8" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.6+4" + +[[CPLEX]] +deps = ["Libdl", "LinearAlgebra", "MathOptInterface", "MathProgBase", "SparseArrays"] +git-tree-sha1 = "c3d7c4c3e4d4bd01c5ac89dee420be93ef7ef20b" +uuid = "a076750e-1247-5638-91d2-ce28b192dca0" +version = "0.6.6" + +[[Calculus]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad" +uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +version = "0.5.1" + +[[CodeTracking]] +deps = ["InteractiveUtils", "UUIDs"] +git-tree-sha1 = "ccc043a0df446cac279dca29d13e2827b40aceb5" +uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" +version = "0.5.12" + +[[CodecBzip2]] +deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"] +git-tree-sha1 = "2e62a725210ce3c3c2e1a3080190e7ca491f18d7" +uuid = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" +version = "0.7.2" + +[[CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.0" + +[[CommonSubexpressions]] +deps = ["MacroTools", "Test"] +git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7" +uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" +version = "0.3.0" + +[[CompilerSupportLibraries_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "7c4f882c41faa72118841185afc58a2eb00ef612" +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "0.3.3+0" + +[[Conda]] +deps = ["JSON", "VersionParsing"] +git-tree-sha1 = "7a58bb32ce5d85f8bf7559aa7c2842f9aecf52fc" +uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d" +version = "1.4.1" + +[[DataStructures]] +deps = ["InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "88d48e133e6d3dd68183309877eac74393daa7eb" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.17.20" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DiffResults]] +deps = ["StaticArrays"] +git-tree-sha1 = "da24935df8e0c6cf28de340b958f6aac88eaa0cc" +uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" +version = "1.0.2" + +[[DiffRules]] +deps = ["NaNMath", "Random", "SpecialFunctions"] +git-tree-sha1 = "eb0c34204c8410888844ada5359ac8b96292cfd1" +uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" +version = "1.0.1" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[ForwardDiff]] +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "NaNMath", "Random", "SpecialFunctions", "StaticArrays"] +git-tree-sha1 = "1d090099fb82223abc48f7ce176d3f7696ede36d" +uuid = "f6369f11-7733-5829-9624-2563aa707210" +version = "0.10.12" + +[[Gurobi]] +deps = ["Libdl", "LinearAlgebra", "MathOptInterface", "MathProgBase", "SparseArrays"] +git-tree-sha1 = "f36a2fa62909675681aec582ccfc4a4a629406e4" +uuid = "2e9cd046-0924-5485-92f1-d5272153d98b" +version = "0.8.1" + +[[HTTP]] +deps = ["Base64", "Dates", "IniFile", "MbedTLS", "Sockets"] +git-tree-sha1 = "2ac03263ce44be4222342bca1c51c36ce7566161" +uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" +version = "0.8.17" + +[[IniFile]] +deps = ["Test"] +git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8" +uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +version = "0.5.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.0" + +[[JSON2]] +deps = ["Dates", "Parsers", "Test"] +git-tree-sha1 = "66397cc6c08922f98a28ab05a8d3002f9853b129" +uuid = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3" +version = "0.3.2" + +[[JSONSchema]] +deps = ["HTTP", "JSON", "ZipFile"] +git-tree-sha1 = "a9ecdbc90be216912a2e3e8a8e38dc4c93f0d065" +uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +version = "0.3.2" + +[[JuMP]] +deps = ["Calculus", "DataStructures", "ForwardDiff", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Random", "SparseArrays", "Statistics"] +git-tree-sha1 = "cbab42e2e912109d27046aa88f02a283a9abac7c" +uuid = "4076af6c-e467-56ae-b986-b466b2749572" +version = "0.21.3" + +[[JuliaInterpreter]] +deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] +git-tree-sha1 = "7b2a1b650cec61a7d8cd8ee9ee7a818b5764d502" +uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" +version = "0.7.26" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[LoweredCodeUtils]] +deps = ["JuliaInterpreter"] +git-tree-sha1 = "dbd9336b43c2d6fa492efa09ba3bb10fbdbeeb64" +uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" +version = "0.4.9" + +[[MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "f7d2e3f654af75f01ec49be82c231c382214223a" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.5" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MathOptInterface]] +deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "JSON", "JSONSchema", "LinearAlgebra", "MutableArithmetics", "OrderedCollections", "SparseArrays", "Test", "Unicode"] +git-tree-sha1 = "cd2049c055c7d192a235670d50faa375361624ba" +uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +version = "0.9.14" + +[[MathProgBase]] +deps = ["LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "9abbe463a1e9fc507f12a69e7f29346c2cdc472c" +uuid = "fdba3010-5040-5b88-9595-932c9decdf73" +version = "0.7.8" + +[[MbedTLS]] +deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] +git-tree-sha1 = "426a6978b03a97ceb7ead77775a1da066343ec6e" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "1.0.2" + +[[MbedTLS_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "a0cb0d489819fa7ea5f9fa84c7e7eba19d8073af" +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.16.6+1" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[MutableArithmetics]] +deps = ["LinearAlgebra", "SparseArrays", "Test"] +git-tree-sha1 = "6cf09794783b9de2e662c4e8b60d743021e338d0" +uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" +version = "0.2.10" + +[[NaNMath]] +git-tree-sha1 = "c84c576296d0e2fbb3fc134d3e09086b3ea617cd" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "0.3.4" + +[[OpenSpecFun_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl", "Pkg"] +git-tree-sha1 = "d51c416559217d974a1113522d5919235ae67a87" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.3+3" + +[[OrderedCollections]] +git-tree-sha1 = "293b70ac1780f9584c89268a6e2a560d938a7065" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.3.0" + +[[PackageCompiler]] +deps = ["Libdl", "Pkg", "UUIDs"] +git-tree-sha1 = "98aa9c653e1dc3473bb5050caf8501293db9eee1" +uuid = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +version = "1.2.1" + +[[Parsers]] +deps = ["Dates", "Test"] +git-tree-sha1 = "8077624b3c450b15c087944363606a6ba12f925e" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "1.0.10" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[PyCall]] +deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"] +git-tree-sha1 = "3a3fdb9000d35958c9ba2323ca7c4958901f115d" +uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" +version = "1.91.4" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[Revise]] +deps = ["CodeTracking", "Distributed", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Pkg", "REPL", "UUIDs", "Unicode"] +git-tree-sha1 = "db20b9938ed44ea2f5b48f92a9b4e0a0afe37823" +uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" +version = "2.7.4" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[SpecialFunctions]] +deps = ["OpenSpecFun_jll"] +git-tree-sha1 = "d8d8b8a9f4119829410ecd706da4cc8594a1e020" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "0.10.3" + +[[StaticArrays]] +deps = ["LinearAlgebra", "Random", "Statistics"] +git-tree-sha1 = "016d1e1a00fabc556473b07161da3d39726ded35" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "0.12.4" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[TimerOutputs]] +deps = ["Printf"] +git-tree-sha1 = "f458ca23ff80e46a630922c555d838303e4b9603" +uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" +version = "0.5.6" + +[[TranscodingStreams]] +deps = ["Random", "Test"] +git-tree-sha1 = "7c53c35547de1c5b9d46a4797cf6d8253807108c" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.9.5" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[VersionParsing]] +git-tree-sha1 = "80229be1f670524750d905f8fc8148e5a8c4537f" +uuid = "81def892-9a0e-5fdd-b105-ffc91e053289" +version = "1.2.0" + +[[ZipFile]] +deps = ["Libdl", "Printf", "Zlib_jll"] +git-tree-sha1 = "254975fef2fc526583bb9b7c9420fe66ffe09f2f" +uuid = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" +version = "0.9.2" + +[[Zlib_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "fdd89e5ab270ea0f2a0174bd9093e557d06d4bfa" +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.11+16" diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..bc25b13 --- /dev/null +++ b/Project.toml @@ -0,0 +1,22 @@ +name = "MIPLearn" +uuid = "2b1277c3-b477-4c49-a15e-7ba350325c68" +authors = ["Alinson S Xavier "] +version = "0.1.0" + +[deps] +CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" +Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" +JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" + +[compat] +CPLEX = "0.6" +JuMP = "0.21" diff --git a/src/MIPLearn.jl b/src/MIPLearn.jl new file mode 100644 index 0000000..8e1cb2c --- /dev/null +++ b/src/MIPLearn.jl @@ -0,0 +1,41 @@ +# 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. + +__precompile__(false) +module MIPLearn + +using PyCall +miplearn = pyimport("miplearn") +Instance = miplearn.Instance +BenchmarkRunner = miplearn.BenchmarkRunner + +macro pycall(expr) + quote + err_msg = nothing + result = nothing + try + result = $(esc(expr)) + catch err + args = err.val.args[1] + if (err isa PyCall.PyError) && (args isa String) && startswith(args, "Julia") + err_msg = replace(args, r"Stacktrace.*" => "") + else + rethrow(err) + end + end + if err_msg != nothing + error(err_msg) + end + result + end +end + +include("log.jl") +include("jump_solver.jl") +include("learning_solver.jl") +include("instance.jl") + +export Instance, BenchmarkRunner + +end # module diff --git a/src/instance.jl b/src/instance.jl new file mode 100644 index 0000000..718cccf --- /dev/null +++ b/src/instance.jl @@ -0,0 +1,61 @@ +# 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 JSON2 +import Base: dump + +get_instance_features(instance) = [0.] +get_variable_features(instance, var, index) = [0.] +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))) + end + + to_model(self) = + $(esc(:to_model))(self.data) + + get_instance_features(self) = + get_instance_features(self.data) + + get_variable_features(self, var, index) = + get_variable_features(self.data, var, index) + + function 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.data, model, v) + end + end + end +end + +export get_instance_features, + get_variable_features, + find_violated_lazy_constraints, + build_lazy_constraint, + dump, + load!, + @Instance \ No newline at end of file diff --git a/src/jump_solver.jl b/src/jump_solver.jl new file mode 100644 index 0000000..759818a --- /dev/null +++ b/src/jump_solver.jl @@ -0,0 +1,254 @@ +# 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 JuMP +using CPLEX +using MathOptInterface +const MOI = MathOptInterface +using TimerOutputs + + +mutable struct JuMPSolverData + basename_idx_to_var + var_to_basename_idx + 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] +end + + +""" + optimize_and_capture_output!(model; tee=tee) + +Optimizes a given JuMP model while capturing the solver log, then returns that log. +If tee=true, prints the solver log to the standard output as the optimization takes place. +""" +function optimize_and_capture_output!(model; tee::Bool=false) + original_stdout = stdout + rd, wr = redirect_stdout() + task = @async begin + log = "" + while true + line = String(readavailable(rd)) + isopen(rd) || break + log *= String(line) + if tee + print(original_stdout, line) + flush(original_stdout) + end + end + return log + end + JuMP.optimize!(model) + sleep(1) + redirect_stdout(original_stdout) + close(rd) + return fetch(task) +end + + +function solve(data::JuMPSolverData; tee::Bool=false) + 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 + 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 + sense = "min" + lower_bound = dual_bound + upper_bound = primal_bound + else + sense = "max" + 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) +end + + +function solve_lp(data::JuMPSolverData; tee::Bool=false) + model, bin_vars = data.model, data.bin_vars + for var in bin_vars + JuMP.unset_binary(var) + JuMP.set_upper_bound(var, 1.0) + JuMP.set_lower_bound(var, 0.0) + end + log = optimize_and_capture_output!(model, tee=tee) + update_solution!(data) + obj_value = JuMP.objective_value(model) + for var in bin_vars + JuMP.set_binary(var) + end + return Dict("Optimal value" => obj_value, + "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 +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 + JuMP.set_optimizer(model, data.optimizer) + 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 + 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 + end + @info "Setting warm start values for $count variables" +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 + ) + end + + set_warm_start(self, solution) = + set_warm_start!(self.data, solution) + + fix(self, solution) = + fix!(self.data, solution) + + set_instance(self, instance, model) = + set_instance!(self.data, instance, model) + + solve(self; tee=false) = + solve(self.data, tee=tee) + + solve_lp(self; tee=false) = + solve_lp(self.data, tee=tee) + + get_solution(self) = + self.data.solution + + 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 + + clear_warm_start(self) = + error("JuMPSolver.clear_warm_start should never be called") + +end + +export JuMPSolver, solve!, fit!, add! \ No newline at end of file diff --git a/src/learning_solver.jl b/src/learning_solver.jl new file mode 100644 index 0000000..9b6e3e0 --- /dev/null +++ b/src/learning_solver.jl @@ -0,0 +1,28 @@ +# 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. + +struct LearningSolver + py::PyCall.PyObject +end + +function LearningSolver(; + optimizer, + kwargs..., + )::LearningSolver + py = @pycall miplearn.LearningSolver(; + kwargs..., + solver=JuMPSolver(optimizer=optimizer)) + return LearningSolver(py) +end + +solve!(solver::LearningSolver, instance; kwargs...) = + @pycall 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...) + +export LearningSolver \ No newline at end of file diff --git a/src/log.jl b/src/log.jl new file mode 100644 index 0000000..d497cd6 --- /dev/null +++ b/src/log.jl @@ -0,0 +1,62 @@ +import Logging: min_enabled_level, shouldlog, handle_message +using Base.CoreLogging, Logging, Printf + +struct TimeLogger <: AbstractLogger + initial_time::Float64 + file::Union{Nothing, IOStream} + screen_log_level + io_log_level +end + +function TimeLogger(; + initial_time::Float64, + file::Union{Nothing, IOStream} = nothing, + screen_log_level = CoreLogging.Info, + io_log_level = CoreLogging.Info, + ) :: TimeLogger + return TimeLogger(initial_time, file, screen_log_level, io_log_level) +end + +min_enabled_level(logger::TimeLogger) = logger.io_log_level +shouldlog(logger::TimeLogger, level, _module, group, id) = true + +function handle_message(logger::TimeLogger, + level, + message, + _module, + group, + id, + filepath, + line; + kwargs...) + elapsed_time = time() - logger.initial_time + time_string = @sprintf("[%12.3f] ", elapsed_time) + + if level >= Logging.Error + color = :light_red + elseif level >= Logging.Warn + color = :light_yellow + else + color = :light_green + end + + if level >= logger.screen_log_level + printstyled(time_string, color=color) + println(message) + end + if logger.file != nothing && level >= logger.io_log_level + write(logger.file, time_string) + write(logger.file, message) + write(logger.file, "\n") + flush(logger.file) + end +end + +function setup_logger() + initial_time = time() + global_logger(TimeLogger(initial_time=initial_time)) + miplearn = pyimport("miplearn") + miplearn.setup_logger(initial_time) +end + +export TimeLogger \ No newline at end of file diff --git a/src/sysimage.jl b/src/sysimage.jl new file mode 100644 index 0000000..b5def82 --- /dev/null +++ b/src/sysimage.jl @@ -0,0 +1,22 @@ +using PackageCompiler + +using CPLEX +using CPLEXW +using Gurobi +using JuMP +using MathOptInterface +using PyCall +using TimerOutputs +using TinyBnB + +pkg = [:CPLEX + :CPLEXW + :Gurobi + :JuMP + :MathOptInterface + :PyCall + :TimerOutputs + :TinyBnB] + +@info "Building system image..." +create_sysimage(pkg, sysimage_path="build/sysimage.so") \ No newline at end of file diff --git a/test/jump_solver_test.jl b/test/jump_solver_test.jl new file mode 100644 index 0000000..a61b37a --- /dev/null +++ b/test/jump_solver_test.jl @@ -0,0 +1,65 @@ +# 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 "varname_split" begin + @test MIPLearn.varname_split("x[1]") == ("x", "1") +end + + +@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" => 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 + end +end \ No newline at end of file diff --git a/test/knapsack.jl b/test/knapsack.jl new file mode 100644 index 0000000..95f4c02 --- /dev/null +++ b/test/knapsack.jl @@ -0,0 +1,34 @@ +# 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. + +import MIPLearn: get_instance_features, + get_variable_features + find_violated_lazy_constraints +using JuMP + +struct KnapsackData + weights + prices + capacity +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) + return model +end + +function get_instance_features(data::KnapsackData) + return [0.] +end + + +function get_variable_features(data::KnapsackData, var, index) + return [0.] +end + +KnapsackInstance = @Instance(KnapsackData) diff --git a/test/learning_solver_test.jl b/test/learning_solver_test.jl new file mode 100644 index 0000000..5e1ce29 --- /dev/null +++ b/test/learning_solver_test.jl @@ -0,0 +1,50 @@ +# 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 "Instance" begin + weights = [23., 26., 20., 18.] + prices = [505., 352., 458., 220.] + capacity = 67.0 + + instance = KnapsackInstance(weights, prices, capacity) + dump(instance, "tmp/instance.json.gz") + + instance = KnapsackInstance([0.0], [0.0], 0.0) + load!(instance, "tmp/instance.json.gz") + @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) + 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 diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..6630ccf --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,14 @@ +# 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 + +MIPLearn.setup_logger() + +@testset "MIPLearn" begin + include("knapsack.jl") + include("jump_solver_test.jl") + include("learning_solver_test.jl") +end \ No newline at end of file