From 62974e2438deeeb26dd34c97cc29db2bb9647d5a Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Mon, 24 May 2021 15:26:41 -0500 Subject: [PATCH] Implement save, load_jump_instance --- Manifest.toml | 23 +++++ Project.toml | 1 + README.md | 42 ++++++++- src/modeling/jump_instance.jl | 124 ++++++++++++++++++++++++-- test/modeling/learning_solver_test.jl | 32 ++++++- 5 files changed, 212 insertions(+), 10 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index c1f3ca8..6b961e3 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -101,6 +101,12 @@ uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" deps = ["ArgTools", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +[[FileIO]] +deps = ["Pkg", "Requires", "UUIDs"] +git-tree-sha1 = "cfb694feaddf4f0381ef3cc9d4c0d8fc6b7e2ea7" +uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +version = "1.9.0" + [[ForwardDiff]] deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "NaNMath", "Printf", "Random", "SpecialFunctions", "StaticArrays"] git-tree-sha1 = "e2af66012e08966366a43251e1fd421522908be6" @@ -123,6 +129,12 @@ version = "0.5.0" deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +[[JLD2]] +deps = ["DataStructures", "FileIO", "MacroTools", "Mmap", "Pkg", "Printf", "Reexport", "TranscodingStreams", "UUIDs"] +git-tree-sha1 = "236b8ca4b8f01ebc6f2fceedf344a077f0e69e79" +uuid = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +version = "0.4.7" + [[JLLWrappers]] deps = ["Preferences"] git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e" @@ -264,6 +276,17 @@ uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" deps = ["Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +[[Reexport]] +git-tree-sha1 = "57d8440b0c7d98fc4f889e478e80f268d534c9d5" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.0.0" + +[[Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "4036a3bd08ac7e968e27c203d45f5fff15020621" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.1.3" + [[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" diff --git a/Project.toml b/Project.toml index 904dd0b..5b7e658 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.2.0" [deps] Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" diff --git a/README.md b/README.md index 6b6197b..34d5193 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,12 @@ end fit!(solver, training_instances) # Save trained solver to disk -save!(solver, "solver.bin") +save("solver.mls", solver) # Application restarts... # Load trained solver from disk -solver = LearningSolver(Cbc.Optimizer) -load!(solver, "solver.bin") +solver = load("solver.mls") # Solve additional instances test_instances = [...] @@ -140,6 +139,43 @@ test_instances = [...] parallel_solve!(solver, test_instances) ``` +### 1.6 Solving instances from disk + +```julia +using MIPLearn +using JuMP +using Cbc + +# Create 600 problem instances and save them to files +for i in 1:600 + m = Model() + @variable(m, x, Bin) + @objective(m, Min, x) + @feature(x, [1.0]) + + instance = JuMPInstance(m) + save("instance-$i.bin", instance) +end + +# Initialize instances and solver +training_instances = [FileInstance("instance-$i.bin") for i in 1:500] +test_instances = [FileInstance("instance-$i.bin") for i in 501:600] +solver = LearningSolver(Cbc.Optimizer) + +# Solve training instances +for instance in training_instances + solve!(solver, instance) +end + +# Train ML models +fit!(solver, training_instances) + +# Solve test instances +for instance in test_instances + solve!(solver, instance) +end +``` + ## 2. Customization ### 2.1 Selecting solver components diff --git a/src/modeling/jump_instance.jl b/src/modeling/jump_instance.jl index 8519f8f..55c173d 100644 --- a/src/modeling/jump_instance.jl +++ b/src/modeling/jump_instance.jl @@ -3,6 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. using JuMP +using JLD2 @pydef mutable struct PyJuMPInstance <: miplearn.Instance @@ -23,38 +24,149 @@ using JuMP function get_variable_features(self, var_name) model = self.model v = variable_by_name(model, var_name) - return get(model.ext[:miplearn][:variable_features], v, [0.0]) + return get(model.ext[:miplearn][:variable_features], v, nothing) end function get_variable_category(self, var_name) model = self.model v = variable_by_name(model, var_name) - return get(model.ext[:miplearn][:variable_categories], v, "default") + return get(model.ext[:miplearn][:variable_categories], v, nothing) end function get_constraint_features(self, cname) model = self.model c = constraint_by_name(model, cname) - return get(model.ext[:miplearn][:constraint_features], c, [0.0]) + return get(model.ext[:miplearn][:constraint_features], c, nothing) end function get_constraint_category(self, cname) model = self.model c = constraint_by_name(model, cname) - return get(model.ext[:miplearn][:constraint_categories], c, "default") + return get(model.ext[:miplearn][:constraint_categories], c, nothing) end end struct JuMPInstance py::PyCall.PyObject + model::Model end function JuMPInstance(model) model isa Model || error("model should be a JuMP.Model. Found $(typeof(model)) instead.") - return JuMPInstance(PyJuMPInstance(model)) + return JuMPInstance( + PyJuMPInstance(model), + model, + ) end -export JuMPInstance +function save(filename::AbstractString, instance::JuMPInstance)::Nothing + # Convert JuMP model to MPS + mps_filename = "$(tempname()).mps.gz" + write_to_file(instance.model, mps_filename) + mps = read(mps_filename) + + # Pickle instance.py.samples. Ideally, we would use dumps and loads, but this + # causes some issues with PyCall, probably due to automatic type conversions. + py_samples_filename = tempname() + miplearn.write_pickle_gz(instance.py.samples, py_samples_filename) + py_samples = read(py_samples_filename) + + # Replace variable/constraint refs by names + _to_names(d) = Dict(name(var) => value for (var, value) in d) + ext_original = instance.model.ext[:miplearn] + ext_names = Dict( + :variable_features => _to_names(ext_original[:variable_features]), + :variable_categories => _to_names(ext_original[:variable_categories]), + :constraint_features => _to_names(ext_original[:constraint_features]), + :constraint_categories => _to_names(ext_original[:constraint_categories]), + :instance_features => ext_original[:instance_features], + ) + + # Generate JLD2 file + jldsave( + filename; + miplearn_version=0.2, + mps=mps, + ext=ext_names, + py_samples=py_samples, + ) + return +end + + +function load_jump_instance(filename::AbstractString)::JuMPInstance + jldopen(filename, "r") do file + file["miplearn_version"] == 0.2 || error( + "MIPLearn version 0.2 cannot read instance files generated by " * + "version $(file["miplearn_version"])." + ) + + # Convert MPS to JuMP + mps_filename = "$(tempname()).mps.gz" + write(mps_filename, file["mps"]) + model = read_from_file(mps_filename) + + # Unpickle instance.py.samples + py_samples_filename = tempname() + write(py_samples_filename, file["py_samples"]) + py_samples = miplearn.read_pickle_gz(py_samples_filename) + + # Replace variable/constraint names by refs + _to_var(model, d) = Dict( + variable_by_name(model, varname) => value + for (varname, value) in d + ) + _to_constr(model, d) = Dict( + constraint_by_name(model, cname) => value + for (cname, value) in d + ) + ext = file["ext"] + model.ext[:miplearn] = Dict( + :variable_features => _to_var(model, ext[:variable_features]), + :variable_categories => _to_var(model, ext[:variable_categories]), + :constraint_features => _to_constr(model, ext[:constraint_features]), + :constraint_categories => _to_constr(model, ext[:constraint_categories]), + :instance_features => ext[:instance_features], + ) + + instance = JuMPInstance(model) + instance.py.samples = py_samples + + return instance + end +end + + +struct FileInstance + filename::AbstractString + loaded::Union{Nothing,JuMPInstance} +end + + +function FileInstance(filename::AbstractString)::FileInstance + return FileInstance( + filename, + nothing, + ) +end + + +function load!(instance::FileInstance) + instance.loaded = load_jump_instance(instance.filename) +end + + +function free!(instance::FileInstance) + instance.loaded = nothing +end + + +function flush!(instance::FileInstance) + save(instance.filename, instance.loaded) +end + + +export JuMPInstance, save, load_jump_instance diff --git a/test/modeling/learning_solver_test.jl b/test/modeling/learning_solver_test.jl index b2f79a3..37c21f6 100644 --- a/test/modeling/learning_solver_test.jl +++ b/test/modeling/learning_solver_test.jl @@ -48,7 +48,7 @@ using Gurobi solve!(solver, instance) end - @testset "plain model" begin + @testset "model without annotations" begin model = Model() @variable(model, x, Bin) @variable(model, y, Bin) @@ -57,4 +57,34 @@ using Gurobi instance = JuMPInstance(model) stats = solve!(solver, instance) end + + @testset "file model" begin + # Create basic model + model = Model() + @variable(model, x, Bin) + @variable(model, y, Bin) + @objective(model, Max, x + y) + @feature(x, [1.0]) + @category(x, "cat1") + @feature(model, [5.0]) + + # Solve + instance = JuMPInstance(model) + solver = LearningSolver(Gurobi.Optimizer) + stats = solve!(solver, instance) + @test length(instance.py.samples) == 1 + + # Save model to file + filename = tempname() + save(filename, instance) + @test isfile(filename) + + # Read model from file + loaded = load_jump_instance(filename) + x = variable_by_name(loaded.model, "x") + @test loaded.model.ext[:miplearn][:variable_features][x] == [1.0] + @test loaded.model.ext[:miplearn][:variable_categories][x] == "cat1" + @test loaded.model.ext[:miplearn][:instance_features] == [5.0] + @test length(loaded.py.samples) == 1 + end end