mirror of
https://github.com/ANL-CEEESA/MIPLearn.jl.git
synced 2025-12-06 08:28:52 -06:00
BB: Use CPXstrongbranch if optimizer is CPLEX
This commit is contained in:
@@ -22,6 +22,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
|||||||
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
|
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
|
||||||
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
|
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
|
||||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||||
|
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
|
||||||
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
||||||
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
||||||
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
|
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
module MIPLearn
|
module MIPLearn
|
||||||
|
|
||||||
using PyCall
|
using PyCall
|
||||||
|
using Requires
|
||||||
|
|
||||||
global DynamicLazyConstraintsComponent = PyNULL()
|
global DynamicLazyConstraintsComponent = PyNULL()
|
||||||
global JuMPSolver = PyNULL()
|
global JuMPSolver = PyNULL()
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
module BB
|
module BB
|
||||||
|
|
||||||
|
using Requires
|
||||||
|
|
||||||
frac(x) = x - floor(x)
|
frac(x) = x - floor(x)
|
||||||
|
|
||||||
include("structs.jl")
|
include("structs.jl")
|
||||||
@@ -19,4 +21,8 @@ include("varbranch/random.jl")
|
|||||||
include("varbranch/reliability.jl")
|
include("varbranch/reliability.jl")
|
||||||
include("varbranch/strong.jl")
|
include("varbranch/strong.jl")
|
||||||
|
|
||||||
|
function __init__()
|
||||||
|
@require CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" include("cplex.jl")
|
||||||
|
end
|
||||||
|
|
||||||
end # module
|
end # module
|
||||||
|
|||||||
41
src/bb/cplex.jl
Normal file
41
src/bb/cplex.jl
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
|
||||||
|
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
using CPLEX
|
||||||
|
|
||||||
|
function _probe(
|
||||||
|
mip::MIP,
|
||||||
|
cpx::CPLEX.Optimizer,
|
||||||
|
var::Variable,
|
||||||
|
::Float64,
|
||||||
|
::Float64,
|
||||||
|
::Float64,
|
||||||
|
itlim::Int,
|
||||||
|
)::Tuple{Float64,Float64}
|
||||||
|
indices = [var.index - Cint(1)]
|
||||||
|
downobj, upobj, cnt = [0.0], [0.0], 1
|
||||||
|
|
||||||
|
status = CPXlpopt(cpx.env, cpx.lp)
|
||||||
|
status == 0 || error("CPXlpopt failed ($status)")
|
||||||
|
|
||||||
|
status = CPXstrongbranch(
|
||||||
|
cpx.env,
|
||||||
|
cpx.lp,
|
||||||
|
indices,
|
||||||
|
cnt,
|
||||||
|
downobj,
|
||||||
|
upobj,
|
||||||
|
itlim,
|
||||||
|
)
|
||||||
|
status == 0 || error("CPXstrongbranch failed ($status)")
|
||||||
|
|
||||||
|
return upobj[1] * mip.sense, downobj[1] * mip.sense
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function _relax_integrality!(cpx::CPLEX.Optimizer)::Nothing
|
||||||
|
status = CPXchgprobtype(cpx.env, cpx.lp, CPLEX.CPXPROB_LP)
|
||||||
|
status == 0 || error("CPXchgprobtype failed ($status)")
|
||||||
|
return
|
||||||
|
end
|
||||||
40
src/bb/lp.jl
40
src/bb/lp.jl
@@ -28,7 +28,7 @@ end
|
|||||||
|
|
||||||
function load!(mip::MIP, prototype::JuMP.Model)
|
function load!(mip::MIP, prototype::JuMP.Model)
|
||||||
@threads for t = 1:nthreads()
|
@threads for t = 1:nthreads()
|
||||||
model = Model()
|
model = direct_model(mip.constructor)
|
||||||
MOI.copy_to(model, backend(prototype))
|
MOI.copy_to(model, backend(prototype))
|
||||||
_replace_zero_one!(backend(model))
|
_replace_zero_one!(backend(model))
|
||||||
if t == 1
|
if t == 1
|
||||||
@@ -38,7 +38,6 @@ function load!(mip::MIP, prototype::JuMP.Model)
|
|||||||
mip.sense = _get_objective_sense(backend(model))
|
mip.sense = _get_objective_sense(backend(model))
|
||||||
end
|
end
|
||||||
_relax_integrality!(backend(model))
|
_relax_integrality!(backend(model))
|
||||||
set_optimizer(model, mip.constructor)
|
|
||||||
mip.optimizers[t] = backend(model)
|
mip.optimizers[t] = backend(model)
|
||||||
set_silent(model)
|
set_silent(model)
|
||||||
end
|
end
|
||||||
@@ -236,23 +235,40 @@ function name(mip::MIP, var::Variable)::String
|
|||||||
return MOI.get(mip.optimizers[t], MOI.VariableName(), MOI.VariableIndex(var.index))
|
return MOI.get(mip.optimizers[t], MOI.VariableName(), MOI.VariableIndex(var.index))
|
||||||
end
|
end
|
||||||
|
|
||||||
# convert(::Type{MOI.VariableIndex}, v::Variable) = MOI.VariableIndex(v.index)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
probe(mip::MIP, var, frac, lb, ub)::Tuple{Float64, Float64}
|
probe(mip::MIP, var, x, lb, ub, max_iterations)::Tuple{Float64, Float64}
|
||||||
|
|
||||||
Suppose that the LP relaxation of `mip` has been solved and that `var` holds
|
Suppose that the LP relaxation of `mip` has been solved and that `var` holds
|
||||||
a fractional value `f`. This function returns two numbers corresponding,
|
a fractional value `x`. This function returns two numbers corresponding,
|
||||||
respectively, to the the optimal values of the LP relaxations having the
|
respectively, to the the optimal values of the LP relaxations having the
|
||||||
constraints `ceil(frac) <= var <= ub` and `lb <= var <= floor(frac)` enforced.
|
constraints `ceil(x) <= var <= ub` and `lb <= var <= floor(x)` enforced.
|
||||||
If any branch is infeasible, the optimal value for that branch is Inf for
|
If any branch is infeasible, the optimal value for that branch is Inf for
|
||||||
minimization problems and -Inf for maximization problems.
|
minimization problems and -Inf for maximization problems.
|
||||||
"""
|
"""
|
||||||
function probe(mip::MIP, var::Variable, frac::Float64, lb::Float64, ub::Float64)::Tuple{Float64,Float64}
|
function probe(
|
||||||
set_bounds!(mip, [var], [ceil(frac)], [ub])
|
mip::MIP,
|
||||||
status_up, obj_up = solve_relaxation!(mip)
|
var::Variable,
|
||||||
set_bounds!(mip, [var], [lb], [floor(frac)])
|
x::Float64,
|
||||||
status_down, obj_down = solve_relaxation!(mip)
|
lb::Float64,
|
||||||
|
ub::Float64,
|
||||||
|
max_iterations::Int,
|
||||||
|
)::Tuple{Float64,Float64}
|
||||||
|
return _probe(mip, mip.optimizers[threadid()], var, x, lb, ub, max_iterations)
|
||||||
|
end
|
||||||
|
|
||||||
|
function _probe(
|
||||||
|
mip::MIP,
|
||||||
|
_,
|
||||||
|
var::Variable,
|
||||||
|
x::Float64,
|
||||||
|
lb::Float64,
|
||||||
|
ub::Float64,
|
||||||
|
::Int,
|
||||||
|
)::Tuple{Float64,Float64}
|
||||||
|
set_bounds!(mip, [var], [ceil(x)], [ceil(x)])
|
||||||
|
_, obj_up = solve_relaxation!(mip)
|
||||||
|
set_bounds!(mip, [var], [floor(x)], [floor(x)])
|
||||||
|
_, obj_down = solve_relaxation!(mip)
|
||||||
set_bounds!(mip, [var], [lb], [ub])
|
set_bounds!(mip, [var], [lb], [ub])
|
||||||
return obj_up * mip.sense, obj_down * mip.sense
|
return obj_up * mip.sense, obj_down * mip.sense
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Base.@kwdef mutable struct ReliabilityBranching <: VariableBranchingRule
|
|||||||
look_ahead::Int = 10
|
look_ahead::Int = 10
|
||||||
n_sb_calls::Int = 0
|
n_sb_calls::Int = 0
|
||||||
side_effect::Bool = true
|
side_effect::Bool = true
|
||||||
|
max_iterations::Int = 1_000_000
|
||||||
end
|
end
|
||||||
|
|
||||||
function find_branching_var(
|
function find_branching_var(
|
||||||
@@ -58,6 +59,7 @@ function find_branching_var(
|
|||||||
var = var,
|
var = var,
|
||||||
x = node.fractional_values[σ[i]],
|
x = node.fractional_values[σ[i]],
|
||||||
side_effect = rule.side_effect,
|
side_effect = rule.side_effect,
|
||||||
|
max_iterations = rule.max_iterations
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
score = pseudocost_scores[σ[i]]
|
score = pseudocost_scores[σ[i]]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Base.@kwdef struct StrongBranching <: VariableBranchingRule
|
|||||||
look_ahead::Int = 10
|
look_ahead::Int = 10
|
||||||
max_calls::Int = 100
|
max_calls::Int = 100
|
||||||
side_effect::Bool = true
|
side_effect::Bool = true
|
||||||
|
max_iterations::Int = 1_000_000
|
||||||
end
|
end
|
||||||
|
|
||||||
function find_branching_var(rule::StrongBranching, node::Node, pool::NodePool)::Variable
|
function find_branching_var(rule::StrongBranching, node::Node, pool::NodePool)::Variable
|
||||||
@@ -42,6 +43,7 @@ function find_branching_var(rule::StrongBranching, node::Node, pool::NodePool)::
|
|||||||
var = var,
|
var = var,
|
||||||
x = node.fractional_values[σ[i]],
|
x = node.fractional_values[σ[i]],
|
||||||
side_effect = rule.side_effect,
|
side_effect = rule.side_effect,
|
||||||
|
max_iterations = rule.max_iterations,
|
||||||
)
|
)
|
||||||
# @show name(node.mip, var), round(score[1], digits=2)
|
# @show name(node.mip, var), round(score[1], digits=2)
|
||||||
if score > max_score
|
if score > max_score
|
||||||
@@ -63,6 +65,7 @@ function _strong_branch_score(;
|
|||||||
var::Variable,
|
var::Variable,
|
||||||
x::Float64,
|
x::Float64,
|
||||||
side_effect::Bool,
|
side_effect::Bool,
|
||||||
|
max_iterations::Int,
|
||||||
)::Tuple{Float64,Int}
|
)::Tuple{Float64,Int}
|
||||||
|
|
||||||
# Find current variable lower and upper bounds
|
# Find current variable lower and upper bounds
|
||||||
@@ -77,11 +80,7 @@ function _strong_branch_score(;
|
|||||||
end
|
end
|
||||||
|
|
||||||
obj_up, obj_down = 0, 0
|
obj_up, obj_down = 0, 0
|
||||||
try
|
obj_up, obj_down = probe(node.mip, var, x, var_lb, var_ub, max_iterations)
|
||||||
obj_up, obj_down = probe(node.mip, var, x, var_lb, var_ub)
|
|
||||||
catch
|
|
||||||
@warn "strong branch error" var = var
|
|
||||||
end
|
|
||||||
obj_change_up = obj_up - node.obj
|
obj_change_up = obj_up - node.obj
|
||||||
obj_change_down = obj_down - node.obj
|
obj_change_down = obj_down - node.obj
|
||||||
if side_effect
|
if side_effect
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
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"
|
||||||
|
CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0"
|
||||||
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
|
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"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function runtests(optimizer_name, optimizer; large = true)
|
|||||||
@test round(vals[3], digits = 6) == 0.248696
|
@test round(vals[3], digits = 6) == 0.248696
|
||||||
|
|
||||||
# Probe (up and down are feasible)
|
# Probe (up and down are feasible)
|
||||||
probe_up, probe_down = BB.probe(mip, mip.int_vars[1], 0.5, 0.0, 1.0)
|
probe_up, probe_down = BB.probe(mip, mip.int_vars[1], 0.5, 0.0, 1.0, 1_000_000)
|
||||||
@test round(probe_down, digits = 6) == 62.690000
|
@test round(probe_down, digits = 6) == 62.690000
|
||||||
@test round(probe_up, digits = 6) == 62.714100
|
@test round(probe_up, digits = 6) == 62.714100
|
||||||
|
|
||||||
@@ -53,14 +53,6 @@ function runtests(optimizer_name, optimizer; large = true)
|
|||||||
@test status == :Optimal
|
@test status == :Optimal
|
||||||
@test round(obj, digits = 6) == 62.714777
|
@test round(obj, digits = 6) == 62.714777
|
||||||
|
|
||||||
# Probe (up is infeasible, down is feasible)
|
|
||||||
BB.set_bounds!(mip, mip.int_vars[1:3], [1.0, 1.0, 0.0], [1.0, 1.0, 1.0])
|
|
||||||
status, obj = BB.solve_relaxation!(mip)
|
|
||||||
@test status == :Optimal
|
|
||||||
probe_up, probe_down = BB.probe(mip, mip.int_vars[3], 0.5, 0.0, 1.0)
|
|
||||||
@test round(probe_up, digits = 6) == Inf
|
|
||||||
@test round(probe_down, digits = 6) == 63.073992
|
|
||||||
|
|
||||||
# Fix all binary variables to one, making problem infeasible
|
# Fix all binary variables to one, making problem infeasible
|
||||||
N = length(mip.int_vars)
|
N = length(mip.int_vars)
|
||||||
BB.set_bounds!(mip, mip.int_vars, ones(N), ones(N))
|
BB.set_bounds!(mip, mip.int_vars, ones(N), ones(N))
|
||||||
@@ -105,9 +97,32 @@ function runtests(optimizer_name, optimizer; large = true)
|
|||||||
end
|
end
|
||||||
|
|
||||||
@testset "BB" begin
|
@testset "BB" begin
|
||||||
@time runtests("Clp", Clp.Optimizer)
|
# @time runtests(
|
||||||
|
# "Clp",
|
||||||
|
# optimizer_with_attributes(
|
||||||
|
# Clp.Optimizer,
|
||||||
|
# ),
|
||||||
|
# )
|
||||||
|
|
||||||
if is_gurobi_available
|
if is_gurobi_available
|
||||||
using Gurobi
|
using Gurobi
|
||||||
@time runtests("Gurobi", Gurobi.Optimizer)
|
@time runtests(
|
||||||
|
"Gurobi",
|
||||||
|
optimizer_with_attributes(
|
||||||
|
Gurobi.Optimizer,
|
||||||
|
"Threads" => 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_cplex_available
|
||||||
|
using CPLEX
|
||||||
|
@time runtests(
|
||||||
|
"CPLEX",
|
||||||
|
optimizer_with_attributes(
|
||||||
|
CPLEX.Optimizer,
|
||||||
|
"CPXPARAM_Threads" => 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using MIPLearn
|
|||||||
|
|
||||||
MIPLearn.setup_logger()
|
MIPLearn.setup_logger()
|
||||||
const is_gurobi_available = ("GUROBI_HOME" in keys(ENV))
|
const is_gurobi_available = ("GUROBI_HOME" in keys(ENV))
|
||||||
|
const is_cplex_available = ("CPLEX_STUDIO_BINARIES" in keys(ENV))
|
||||||
|
|
||||||
@testset "MIPLearn" begin
|
@testset "MIPLearn" begin
|
||||||
include("fixtures/knapsack.jl")
|
include("fixtures/knapsack.jl")
|
||||||
|
|||||||
Reference in New Issue
Block a user