Implement lazy callbacks

master
Alinson S. Xavier 4 years ago
parent 39072a6290
commit af3a5a69f0

@ -9,9 +9,13 @@ mutable struct FileInstance <: Instance
loaded::Union{Nothing,JuMPInstance} loaded::Union{Nothing,JuMPInstance}
filename::AbstractString filename::AbstractString
h5::PyCall.PyObject h5::PyCall.PyObject
lazycb::Union{Nothing,Tuple{Function,Function}}
function FileInstance(filename::AbstractString)::FileInstance function FileInstance(
instance = new(nothing, nothing, filename) filename::AbstractString;
lazycb::Union{Nothing,Tuple{Function,Function}} = nothing,
)::FileInstance
instance = new(nothing, nothing, filename, nothing, lazycb)
instance.py = PyFileInstance(instance) instance.py = PyFileInstance(instance)
instance.h5 = Hdf5Sample(filename) instance.h5 = Hdf5Sample(filename)
instance.filename = filename instance.filename = filename
@ -20,16 +24,27 @@ mutable struct FileInstance <: Instance
end end
to_model(instance::FileInstance) = to_model(instance.loaded) to_model(instance::FileInstance) = to_model(instance.loaded)
get_instance_features(instance::FileInstance) = get_instance_features(instance.loaded) get_instance_features(instance::FileInstance) = get_instance_features(instance.loaded)
get_variable_features(instance::FileInstance, names) = get_variable_features(instance::FileInstance, names) =
get_variable_features(instance.loaded, names) get_variable_features(instance.loaded, names)
get_variable_categories(instance::FileInstance, names) = get_variable_categories(instance::FileInstance, names) =
get_variable_categories(instance.loaded, names) get_variable_categories(instance.loaded, names)
get_constraint_features(instance::FileInstance, names) = get_constraint_features(instance::FileInstance, names) =
get_constraint_features(instance.loaded, names) get_constraint_features(instance.loaded, names)
get_constraint_categories(instance::FileInstance, names) = get_constraint_categories(instance::FileInstance, names) =
get_constraint_categories(instance.loaded, names) get_constraint_categories(instance.loaded, names)
find_violated_lazy_constraints(instance::FileInstance, solver) =
find_violated_lazy_constraints(instance.loaded, solver)
enforce_lazy_constraint(instance::FileInstance, solver, violation) =
enforce_lazy_constraint(instance.loaded, solver, violation)
function get_samples(instance::FileInstance) function get_samples(instance::FileInstance)
return [instance.h5] return [instance.h5]
end end
@ -40,7 +55,7 @@ end
function load(instance::FileInstance) function load(instance::FileInstance)
if instance.loaded === nothing if instance.loaded === nothing
instance.loaded = load_instance(instance.filename) instance.loaded = load_instance(instance.filename, lazycb = instance.lazycb)
end end
end end
@ -72,6 +87,10 @@ function __init_PyFileInstance__()
load(self) = load(self.jl) load(self) = load(self.jl)
free(self) = free(self.jl) free(self) = free(self.jl)
flush(self) = flush(self.jl) flush(self) = flush(self.jl)
find_violated_lazy_constraints(self, solver, _) =
find_violated_lazy_constraints(self.jl, solver)
enforce_lazy_constraint(self, solver, _, violation) =
enforce_lazy_constraint(self.jl, solver, violation)
end end
copy!(PyFileInstance, Class) copy!(PyFileInstance, Class)
end end

@ -12,7 +12,7 @@ mutable struct JuMPInstance <: Instance
ext::AbstractDict ext::AbstractDict
samples::Vector{PyCall.PyObject} samples::Vector{PyCall.PyObject}
function JuMPInstance(model::JuMP.Model) function JuMPInstance(model::JuMP.Model)::JuMPInstance
init_miplearn_ext(model) init_miplearn_ext(model)
instance = new(nothing, model, nothing, model.ext[:miplearn], []) instance = new(nothing, model, nothing, model.ext[:miplearn], [])
py = PyJuMPInstance(instance) py = PyJuMPInstance(instance)
@ -84,6 +84,18 @@ function create_sample!(instance::JuMPInstance)
return sample return sample
end end
function find_violated_lazy_constraints(instance::JuMPInstance, solver)::Vector{String}
if "lazy_find_cb" keys(instance.model.ext[:miplearn])
return instance.model.ext[:miplearn]["lazy_find_cb"](instance.model, solver.data)
else
return []
end
end
function enforce_lazy_constraint(instance::JuMPInstance, solver, violation::String)::Nothing
instance.model.ext[:miplearn]["lazy_enforce_cb"](instance.model, solver.data, violation)
end
function __init_PyJuMPInstance__() function __init_PyJuMPInstance__()
@pydef mutable struct Class <: miplearn.Instance @pydef mutable struct Class <: miplearn.Instance
function __init__(self, jl) function __init__(self, jl)
@ -101,6 +113,10 @@ function __init_PyJuMPInstance__()
to_str_array(get_constraint_categories(self.jl, from_str_array(names))) to_str_array(get_constraint_categories(self.jl, from_str_array(names)))
get_samples(self) = get_samples(self.jl) get_samples(self) = get_samples(self.jl)
create_sample(self) = create_sample!(self.jl) create_sample(self) = create_sample!(self.jl)
find_violated_lazy_constraints(self, solver, _) =
find_violated_lazy_constraints(self.jl, solver)
enforce_lazy_constraint(self, solver, _, violation) =
enforce_lazy_constraint(self.jl, solver, violation)
end end
copy!(PyJuMPInstance, Class) copy!(PyJuMPInstance, Class)
end end
@ -116,7 +132,11 @@ function save(filename::AbstractString, instance::JuMPInstance)::Nothing
h5 = Hdf5Sample(filename, mode = "w") h5 = Hdf5Sample(filename, mode = "w")
h5.put_scalar("miplearn_version", "0002") h5.put_scalar("miplearn_version", "0002")
h5.put_bytes("mps", mps) h5.put_bytes("mps", mps)
h5.put_scalar("jump_ext", JSON.json(model.ext[:miplearn]))
ext = copy(model.ext[:miplearn])
delete!(ext, "lazy_find_cb")
delete!(ext, "lazy_enforce_cb")
h5.put_scalar("jump_ext", JSON.json(ext))
return return
end end
@ -130,12 +150,19 @@ function _check_miplearn_version(h5)
) )
end end
function load_instance(filename::AbstractString)::JuMPInstance function load_instance(
filename::AbstractString;
lazycb::Union{Nothing,Tuple{Function,Function}} = nothing,
)::JuMPInstance
h5 = Hdf5Sample(filename) h5 = Hdf5Sample(filename)
_check_miplearn_version(h5) _check_miplearn_version(h5)
mps = h5.get_bytes("mps") mps = h5.get_bytes("mps")
ext = h5.get_scalar("jump_ext") ext = JSON.parse(h5.get_scalar("jump_ext"))
instance = JuMPInstance(Vector{UInt8}(mps), JSON.parse(ext)) if lazycb !== nothing
ext["lazy_find_cb"] = lazycb[1]
ext["lazy_enforce_cb"] = lazycb[2]
end
instance = JuMPInstance(Vector{UInt8}(mps), ext)
return instance return instance
end end

@ -9,6 +9,8 @@ using MathOptInterface
using TimerOutputs using TimerOutputs
const MOI = MathOptInterface const MOI = MathOptInterface
import JuMP: value
mutable struct JuMPSolverData mutable struct JuMPSolverData
optimizer_factory::Any optimizer_factory::Any
varname_to_var::Dict{String,VariableRef} varname_to_var::Dict{String,VariableRef}
@ -19,6 +21,7 @@ mutable struct JuMPSolverData
solution::Dict{JuMP.VariableRef,Float64} solution::Dict{JuMP.VariableRef,Float64}
reduced_costs::Vector{Float64} reduced_costs::Vector{Float64}
dual_values::Dict{JuMP.ConstraintRef,Float64} dual_values::Dict{JuMP.ConstraintRef,Float64}
cb_data::Any
end end
@ -175,10 +178,25 @@ function remove_constraints(data::JuMPSolverData, names::Vector{String})::Nothin
end end
function solve(data::JuMPSolverData; tee::Bool = false, iteration_cb = nothing) function solve(
data::JuMPSolverData;
tee::Bool = false,
iteration_cb = nothing,
lazy_cb = nothing,
)
model = data.model model = data.model
wallclock_time = 0 wallclock_time = 0
log = "" log = ""
if lazy_cb !== nothing
function lazy_cb_wrapper(cb_data)
data.cb_data = cb_data
lazy_cb(nothing, nothing)
data.cb_data = nothing
end
MOI.set(model, MOI.LazyConstraintCallback(), lazy_cb_wrapper)
end
while true while true
wallclock_time += @elapsed begin wallclock_time += @elapsed begin
log *= _optimize_and_capture_output!(model, tee = tee) log *= _optimize_and_capture_output!(model, tee = tee)
@ -189,6 +207,7 @@ function solve(data::JuMPSolverData; tee::Bool = false, iteration_cb = nothing)
break break
end end
end end
if is_infeasible(data) if is_infeasible(data)
data.solution = Dict() data.solution = Dict()
primal_bound = nothing primal_bound = nothing
@ -452,6 +471,7 @@ function __init_JuMPSolver__()
Dict(), # solution Dict(), # solution
[], # reduced_costs [], # reduced_costs
Dict(), # dual_values Dict(), # dual_values
nothing, # cb_data
) )
end end
@ -555,12 +575,27 @@ function __init_JuMPSolver__()
iteration_cb = nothing, iteration_cb = nothing,
lazy_cb = nothing, lazy_cb = nothing,
user_cut_cb = nothing, user_cut_cb = nothing,
) = solve(self.data, tee = tee, iteration_cb = iteration_cb) ) = solve(self.data, tee = tee, iteration_cb = iteration_cb, lazy_cb = lazy_cb)
solve_lp(self; tee = false) = solve_lp(self.data, tee = tee) solve_lp(self; tee = false) = solve_lp(self.data, tee = tee)
end end
copy!(JuMPSolver, Class) copy!(JuMPSolver, Class)
end end
function value(solver::JuMPSolverData, var::VariableRef)
if solver.cb_data !== nothing
return JuMP.callback_value(solver.cb_data, var)
else
return JuMP.value(var)
end
end
function submit(solver::JuMPSolverData, con::AbstractConstraint, name::String = "")
if solver.cb_data !== nothing
MOI.submit(solver.model, MOI.LazyConstraint(solver.cb_data), con)
else
JuMP.add_constraint(solver.model, con, name)
end
end
export JuMPSolver export JuMPSolver, submit

@ -53,6 +53,13 @@ function set_category!(c::ConstraintRef, category::String)::Nothing
return return
end end
function set_lazy_callback!(model::Model, find_cb::Function, enforce_cb::Function)::Nothing
ext = init_miplearn_ext(model)
ext["lazy_find_cb"] = find_cb
ext["lazy_enforce_cb"] = enforce_cb
return
end
macro feature(obj, features) macro feature(obj, features)
quote quote
@ -67,6 +74,12 @@ macro category(obj, category)
end end
end end
macro lazycb(obj, find_cb, enforce_cb)
quote
set_lazy_callback!($(esc(obj)), $(esc(find_cb)), $(esc(enforce_cb)))
end
end
function _get_and_check_name(obj) function _get_and_check_name(obj)
n = name(obj) n = name(obj)
length(n) > 0 || error( length(n) > 0 || error(
@ -77,4 +90,4 @@ function _get_and_check_name(obj)
end end
export @feature, @category export @feature, @category, @lazycb

@ -4,10 +4,10 @@
using PackageCompiler using PackageCompiler
using CSV
using Cbc using Cbc
using Clp using Clp
using Conda using Conda
using CSV
using DataFrames using DataFrames
using Distributed using Distributed
using JLD2 using JLD2
@ -20,10 +20,10 @@ using PyCall
using TimerOutputs using TimerOutputs
pkg = [ pkg = [
:CSV
:Cbc :Cbc
:Clp :Clp
:Conda :Conda
:CSV
:DataFrames :DataFrames
:Distributed :Distributed
:JLD2 :JLD2

@ -1,14 +1,14 @@
[deps] [deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d" Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d"
Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3" JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572" JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
MIPLearn = "2b1277c3-b477-4c49-a15e-7ba350325c68"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
MIPLearn = "2b1277c3-b477-4c49-a15e-7ba350325c68"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"

@ -2,29 +2,62 @@
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
using Cbc
using JuMP
using MathOptInterface
using MIPLearn using MIPLearn
const MOI = MathOptInterface
function find_lazy(model::Model, cb_data)::Vector{String}
x = variable_by_name(model, "x")
y = variable_by_name(model, "y")
x_val = value(cb_data, x)
y_val = value(cb_data, y)
if x_val + y_val > 1 + 1e-6
return ["con"]
end
return []
end
function enforce_lazy(model::Model, cb_data, violation::String)::Nothing
if violation == "con"
x = variable_by_name(model, "x")
y = variable_by_name(model, "y")
con = @build_constraint(x + y <= 1)
submit(cb_data, con)
end
return
end
function build_model()
model = Model()
@variable(model, x, Bin)
@variable(model, y, Bin)
@objective(model, Max, 2 * x + y)
@constraint(model, c1, x + y <= 2)
@lazycb(model, find_lazy, enforce_lazy)
return model
end
@testset "JuMPInstance" begin @testset "Lazy callback" begin
@testset "Save and load" begin @testset "JuMPInstance" begin
# Build instance and solve model = build_model()
model = model = build_knapsack_model()
instance = JuMPInstance(model) instance = JuMPInstance(model)
solver = LearningSolver(Gurobi.Optimizer) solver = LearningSolver(Cbc.Optimizer)
stats = solve!(solver, instance) solve!(solver, instance)
@test length(instance.py.samples) == 1 @test value(model[:x]) == 1.0
@test value(model[:y]) == 0.0
end
# Save model to file @testset "FileInstance" begin
model = build_model()
instance = JuMPInstance(model)
filename = tempname() filename = tempname()
save(filename, instance) save(filename, instance)
@test isfile(filename) file_instance = FileInstance(filename, lazycb = (find_lazy, enforce_lazy))
solver = LearningSolver(Cbc.Optimizer)
# Read model from file solve!(solver, file_instance)
loaded = load_instance(filename) h5 = MIPLearn.Hdf5Sample(filename)
x = variable_by_name(loaded.model, "x") @test h5.get_array("mip_var_values") == [1.0, 0.0]
@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
end end

@ -10,6 +10,7 @@ MIPLearn.setup_logger()
@testset "MIPLearn" begin @testset "MIPLearn" begin
include("fixtures/knapsack.jl") include("fixtures/knapsack.jl")
include("instance/file_test.jl") include("instance/file_test.jl")
include("instance/jump_test.jl")
include("solvers/jump_test.jl") include("solvers/jump_test.jl")
include("solvers/learning_test.jl") include("solvers/learning_test.jl")
include("utils/benchmark_test.jl") include("utils/benchmark_test.jl")

Loading…
Cancel
Save