From db6456dbaa4ed3a906daf55fa823c83767fccf5b Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 22 Mar 2023 09:40:12 -0500 Subject: [PATCH] Re-add BB module --- Project.toml | 5 + src/BB/BB.jl | 29 ++++ src/BB/collect.jl | 63 +++++++ src/BB/cplex.jl | 33 ++++ src/BB/log.jl | 68 ++++++++ src/BB/lp.jl | 269 ++++++++++++++++++++++++++++++ src/BB/nodepool.jl | 186 +++++++++++++++++++++ src/BB/optimize.jl | 169 +++++++++++++++++++ src/BB/structs.jl | 74 ++++++++ src/BB/varbranch/hybrid.jl | 31 ++++ src/BB/varbranch/infeasibility.jl | 54 ++++++ src/BB/varbranch/pseudocost.jl | 45 +++++ src/BB/varbranch/random.jl | 17 ++ src/BB/varbranch/reliability.jl | 191 +++++++++++++++++++++ src/BB/varbranch/strong.jl | 32 ++++ src/MIPLearn.jl | 5 +- test/Project.toml | 2 + test/fixtures/bell5.h5 | Bin 80984 -> 82932 bytes test/src/BB/test_bb.jl | 134 +++++++++++++++ test/src/MIPLearnT.jl | 4 + 20 files changed, 1409 insertions(+), 2 deletions(-) create mode 100644 src/BB/BB.jl create mode 100644 src/BB/collect.jl create mode 100644 src/BB/cplex.jl create mode 100644 src/BB/log.jl create mode 100644 src/BB/lp.jl create mode 100644 src/BB/nodepool.jl create mode 100644 src/BB/optimize.jl create mode 100644 src/BB/structs.jl create mode 100644 src/BB/varbranch/hybrid.jl create mode 100644 src/BB/varbranch/infeasibility.jl create mode 100644 src/BB/varbranch/pseudocost.jl create mode 100644 src/BB/varbranch/random.jl create mode 100644 src/BB/varbranch/reliability.jl create mode 100644 src/BB/varbranch/strong.jl create mode 100644 test/src/BB/test_bb.jl diff --git a/Project.toml b/Project.toml index 236f600..3418f58 100644 --- a/Project.toml +++ b/Project.toml @@ -6,12 +6,17 @@ version = "0.3.0" [deps] CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] JuMP = "1" diff --git a/src/BB/BB.jl b/src/BB/BB.jl new file mode 100644 index 0000000..18fb8a9 --- /dev/null +++ b/src/BB/BB.jl @@ -0,0 +1,29 @@ +# 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. + +module BB + +using Requires + +frac(x) = x - floor(x) + +include("structs.jl") + +include("collect.jl") +include("nodepool.jl") +include("optimize.jl") +include("log.jl") +include("lp.jl") +include("varbranch/hybrid.jl") +include("varbranch/infeasibility.jl") +include("varbranch/pseudocost.jl") +include("varbranch/random.jl") +include("varbranch/reliability.jl") +include("varbranch/strong.jl") + +function __init__() + @require CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" include("cplex.jl") +end + +end # module diff --git a/src/BB/collect.jl b/src/BB/collect.jl new file mode 100644 index 0000000..a1dde95 --- /dev/null +++ b/src/BB/collect.jl @@ -0,0 +1,63 @@ +# 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 Printf +using Base.Threads +import Base.Threads: @threads, nthreads, threadid + +import ..H5File + +function collect!( + optimizer, + filename::String; + time_limit::Float64 = Inf, + node_limit::Int = typemax(Int), + gap_limit::Float64 = 1e-4, + print_interval::Int = 5, + branch_rule::VariableBranchingRule = ReliabilityBranching(collect = true), + enable_plunging = true, +)::NodePool + model = read_from_file(filename) + mip = init(optimizer) + load!(mip, model) + + h5 = H5File(replace(filename, ".mps.gz" => ".h5"), "r") + primal_bound = h5.get_scalar("mip_upper_bound") + if primal_bound === nothing + primal_bound = h5.get_scalar("mip_obj_value") + end + h5.file.close() + + pool = solve!( + mip; + initial_primal_bound = primal_bound, + time_limit, + node_limit, + gap_limit, + print_interval, + branch_rule, + enable_plunging, + ) + + h5 = H5File(replace(filename, ".mps.gz" => ".h5")) + pseudocost_up = [NaN for _ = 1:mip.nvars] + pseudocost_down = [NaN for _ = 1:mip.nvars] + priorities = [0.0 for _ = 1:mip.nvars] + for (var, var_hist) in pool.var_history + pseudocost_up[var.index] = var_hist.pseudocost_up + pseudocost_down[var.index] = var_hist.pseudocost_down + x = mean(var_hist.fractional_values) + f_up = x - floor(x) + f_down = ceil(x) - x + priorities[var.index] = + var_hist.pseudocost_up * f_up * var_hist.pseudocost_down * f_down + end + h5.put_array("bb_var_pseudocost_up", pseudocost_up) + h5.put_array("bb_var_pseudocost_down", pseudocost_down) + h5.put_array("bb_var_priority", priorities) + collect!(branch_rule, h5) + h5.file.close() + + return pool +end diff --git a/src/BB/cplex.jl b/src/BB/cplex.jl new file mode 100644 index 0000000..2463a45 --- /dev/null +++ b/src/BB/cplex.jl @@ -0,0 +1,33 @@ +# 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 diff --git a/src/BB/log.jl b/src/BB/log.jl new file mode 100644 index 0000000..5c7bbbf --- /dev/null +++ b/src/BB/log.jl @@ -0,0 +1,68 @@ +# 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 Printf + +function print_progress_header() + @printf( + "%8s %9s %9s %13s %13s %9s %6s %13s %6s %-24s %9s %9s %6s %6s", + "time", + "processed", + "pending", + "primal-bound", + "dual-bound", + "gap", + "node", + "obj", + "parent", + "branch-var", + "branch-lb", + "branch-ub", + "depth", + "iinfes" + ) + println() + flush(stdout) +end + +function print_progress( + pool::NodePool, + node::Node; + time_elapsed::Float64, + print_interval::Int, + primal_update::Bool, +)::Nothing + if (pool.processed % print_interval == 0) || isempty(pool.pending) || primal_update + if isempty(node.branch_vars) + branch_var_name = "---" + branch_lb = "---" + branch_ub = "---" + else + branch_var_name = name(node.mip, last(node.branch_vars)) + L = min(24, length(branch_var_name)) + branch_var_name = branch_var_name[1:L] + branch_lb = @sprintf("%9.2f", last(node.branch_lb)) + branch_ub = @sprintf("%9.2f", last(node.branch_ub)) + end + @printf( + "%8.2f %9d %9d %13.6e %13.6e %9.2e %6d %13.6e %6s %-24s %9s %9s %6d %6d", + time_elapsed, + pool.processed, + length(pool.processing) + length(pool.pending), + pool.primal_bound * node.mip.sense, + pool.dual_bound * node.mip.sense, + pool.gap, + node.index, + node.obj * node.mip.sense, + node.parent === nothing ? "---" : @sprintf("%d", node.parent.index), + branch_var_name, + branch_lb, + branch_ub, + length(node.branch_vars), + length(node.fractional_variables) + ) + println() + flush(stdout) + end +end diff --git a/src/BB/lp.jl b/src/BB/lp.jl new file mode 100644 index 0000000..76fa4ec --- /dev/null +++ b/src/BB/lp.jl @@ -0,0 +1,269 @@ +# 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 Base: values, convert +using Base.Threads +import Base.Threads: @threads, nthreads, threadid +using JuMP +using MathOptInterface +const MOI = MathOptInterface + +function init(constructor)::MIP + return MIP( + constructor = constructor, + optimizers = Any[nothing for t = 1:nthreads()], + int_vars = Variable[], + int_vars_lb = Float64[], + int_vars_ub = Float64[], + sense = 1.0, + lp_iterations = 0, + nvars = 0, + ) +end + +function read!(mip::MIP, filename::AbstractString)::Nothing + load!(mip, read_from_file(filename)) + return +end + +function load!(mip::MIP, prototype::JuMP.Model) + mip.nvars = num_variables(prototype) + _replace_zero_one!(backend(prototype)) + _assert_supported(backend(prototype)) + mip.int_vars, mip.int_vars_lb, mip.int_vars_ub = _get_int_variables(backend(prototype)) + mip.sense = _get_objective_sense(backend(prototype)) + _relax_integrality!(backend(prototype)) + @threads for t = 1:nthreads() + model = Model(mip.constructor) + MOI.copy_to(model, backend(prototype)) + mip.optimizers[t] = backend(model) + set_silent(model) + end + return +end + +function _assert_supported(optimizer::MOI.AbstractOptimizer)::Nothing + types = MOI.get(optimizer, MOI.ListOfConstraintTypesPresent()) + for (F, S) in types + _assert_supported(F, S) + end +end + +function _assert_supported(F::Type, S::Type)::Nothing + if F in [MOI.ScalarAffineFunction{Float64}, MOI.VariableIndex] && S in [ + MOI.LessThan{Float64}, + MOI.GreaterThan{Float64}, + MOI.EqualTo{Float64}, + MOI.Interval{Float64}, + ] + return + end + if F in [MOI.VariableIndex] && S in [MOI.Integer, MOI.ZeroOne] + return + end + error("MOI constraint not supported: $F in $S") +end + +function _get_objective_sense(optimizer::MOI.AbstractOptimizer)::Float64 + sense = MOI.get(optimizer, MOI.ObjectiveSense()) + if sense == MOI.MIN_SENSE + return 1.0 + elseif sense == MOI.MAX_SENSE + return -1.0 + else + error("objective sense not supported: $sense") + end +end + +_interval_index(v::Variable) = + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}(v.index) + +_lower_bound_index(v::Variable) = + MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(v.index) + +_upper_bound_index(v::Variable) = + MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(v.index) + + +function _replace_zero_one!(optimizer::MOI.AbstractOptimizer)::Nothing + constrs_to_delete = MOI.ConstraintIndex[] + funcs = MOI.VariableIndex[] + sets = Union{MOI.Interval,MOI.Integer}[] + for ci in + MOI.get(optimizer, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}()) + func = MOI.get(optimizer, MOI.ConstraintFunction(), ci) + var = func.value + push!(constrs_to_delete, ci) + push!(funcs, MOI.VariableIndex(var)) + push!(funcs, MOI.VariableIndex(var)) + push!(sets, MOI.Interval{Float64}(0.0, 1.0)) + push!(sets, MOI.Integer()) + end + MOI.delete(optimizer, constrs_to_delete) + MOI.add_constraints(optimizer, funcs, sets) + return +end + +function _get_int_variables( + optimizer::MOI.AbstractOptimizer, +)::Tuple{Vector{Variable},Vector{Float64},Vector{Float64}} + vars = Variable[] + lb = Float64[] + ub = Float64[] + for ci in + MOI.get(optimizer, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}()) + func = MOI.get(optimizer, MOI.ConstraintFunction(), ci) + var = Variable(func.value) + + var_lb, var_ub = -Inf, Inf + if MOI.is_valid(optimizer, _interval_index(var)) + constr = MOI.get(optimizer, MOI.ConstraintSet(), _interval_index(var)) + var_ub = constr.upper + var_lb = constr.lower + else + # If interval constraint is not found, query individual lower/upper bound + # constraints and replace them by an interval constraint. + if MOI.is_valid(optimizer, _lower_bound_index(var)) + constr = MOI.get(optimizer, MOI.ConstraintSet(), _lower_bound_index(var)) + var_lb = constr.lower + MOI.delete(optimizer, _lower_bound_index(var)) + end + if MOI.is_valid(optimizer, _upper_bound_index(var)) + constr = MOI.get(optimizer, MOI.ConstraintSet(), _upper_bound_index(var)) + var_ub = constr.upper + MOI.delete(optimizer, _upper_bound_index(var)) + end + MOI.add_constraint(optimizer, var, MOI.Interval(var_lb, var_ub)) + end + push!(vars, var) + push!(lb, var_lb) + push!(ub, var_ub) + end + return vars, lb, ub +end + +function _relax_integrality!(optimizer::MOI.AbstractOptimizer)::Nothing + indices = + MOI.get(optimizer, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}()) + MOI.delete(optimizer, indices) +end + +""" + solve_relaxation(mip::MIP)::Tuple{Symbol, Float64} + +Solve the linear relaxation of `mip` and returns a tuple containing the +solution status (either :Optimal or :Infeasible) and the optimal objective +value. If the problem is infeasible, the optimal value is Inf for minimization +problems and -Inf for maximization problems.. +""" +function solve_relaxation!(mip::MIP)::Tuple{Symbol,Float64} + t = threadid() + MOI.optimize!(mip.optimizers[t]) + try + mip.lp_iterations += MOI.get(mip.optimizers[t], MOI.SimplexIterations()) + catch + # ignore + end + status = MOI.get(mip.optimizers[t], MOI.TerminationStatus()) + if status == MOI.OPTIMAL + obj = MOI.get(mip.optimizers[t], MOI.ObjectiveValue()) + return :Optimal, obj * mip.sense + elseif status in [MOI.INFEASIBLE, MOI.INFEASIBLE_OR_UNBOUNDED] + return :Infeasible, Inf * mip.sense + end + error("unknown status: $status") +end + +""" + values(mip::MIP, vars::Vector{Variable})::Array{Float64} + +Returns a vector `vals` which describes the current primal values for the +decision variables `vars`. More specifically, `vals[j]` is the current +primal value of `vars[j]`. +""" +function values(mip::MIP, vars::Vector{Variable})::Array{Float64} + return MOI.get( + mip.optimizers[threadid()], + MOI.VariablePrimal(), + [MOI.VariableIndex(v.index) for v in vars], + ) +end + +values(mip::MIP) = + values(mip, MOI.get(mip.optimizers[threadid()], MOI.ListOfVariableIndices())) + +""" + set_bounds!(mip::MIP, + vars::Vector{Variable}, + lb::Array{Float64}, + ub::Array{Float64}) + +Modify the bounds of the given variables. More specifically, sets +upper and lower bounds of `vars[j]` to `ub[j]` and `lb[j]`, respectively. +""" +function set_bounds!( + mip::MIP, + vars::Vector{Variable}, + lb::Array{Float64}, + ub::Array{Float64}, +)::Nothing + t = threadid() + for j = 1:length(vars) + MOI.delete(mip.optimizers[t], _interval_index(vars[j])) + MOI.add_constraint( + mip.optimizers[t], + MOI.VariableIndex(vars[j].index), + MOI.Interval(lb[j], ub[j]), + ) + end + return +end + +""" + name(mip::MIP, var::Variable)::String + +Return the name of the decision variable `var`. +""" +function name(mip::MIP, var::Variable)::String + t = threadid() + return MOI.get(mip.optimizers[t], MOI.VariableName(), MOI.VariableIndex(var.index)) +end + +""" + 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 +a fractional value `x`. This function returns two numbers corresponding, +respectively, to the the optimal values of the LP relaxations having the +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 +minimization problems and -Inf for maximization problems. +""" +function probe( + mip::MIP, + var::Variable, + x::Float64, + 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]) + return obj_up * mip.sense, obj_down * mip.sense +end diff --git a/src/BB/nodepool.jl b/src/BB/nodepool.jl new file mode 100644 index 0000000..be1fa9a --- /dev/null +++ b/src/BB/nodepool.jl @@ -0,0 +1,186 @@ +# 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 Statistics +using DataStructures +import Base.Threads: threadid + +function take( + pool::NodePool; + suggestions::Array{Node} = [], + time_remaining::Float64, + gap_limit::Float64, + node_limit::Int, +)::Union{Symbol,Node} + t = threadid() + lock(pool.lock) do + n_processing = length(pool.processing) + if ( + (pool.gap < gap_limit) || + (n_processing + pool.processed >= node_limit) || + (time_remaining < 0) + ) + return :END + end + if isempty(pool.pending) + if isempty(pool.processing) + return :END + else + return :WAIT + end + else + # If one of the suggested nodes is still pending, return it. + # This is known in the literature as plunging. + for s in suggestions + if s in keys(pool.pending) + delete!(pool.pending, s) + pool.processing[s] = s.obj + return s + end + end + + # If all suggestions have already been processed + # or pruned, find another node based on best bound. + node = dequeue!(pool.pending) + pool.processing[node] = node.obj + return node + end + end +end + +function offer( + pool::NodePool; + parent_node::Union{Nothing,Node}, + child_nodes::Vector{Node}, + time_elapsed::Float64 = 0.0, + print_interval::Int = 100, +)::Nothing + lock(pool.lock) do + primal_update = false + + # Update node.processing and node.processed + if parent_node !== nothing + pool.processed += 1 + delete!(pool.processing, parent_node) + end + + # Queue child nodes + for node in child_nodes + if node.status == :Infeasible + continue + end + if node.obj >= pool.primal_bound - 1e-6 + continue + end + if isempty(node.fractional_variables) + pool.primal_bound = min(pool.primal_bound, node.obj) + primal_update = true + continue + end + pool.pending[node] = node.obj + end + + # Update dual bound + pool.dual_bound = pool.primal_bound + if !isempty(pool.pending) + pool.dual_bound = min(pool.dual_bound, peek(pool.pending)[2]) + end + if !isempty(pool.processing) + pool.dual_bound = min(pool.dual_bound, peek(pool.processing)[2]) + end + + # Update gap + if pool.primal_bound == pool.dual_bound + pool.gap = 0 + else + pool.gap = abs((pool.primal_bound - pool.dual_bound) / pool.primal_bound) + end + + if parent_node !== nothing + # Update branching variable history + branch_var = child_nodes[1].branch_vars[end] + offset = findfirst(isequal(branch_var), parent_node.fractional_variables) + x = parent_node.fractional_values[offset] + obj_change_up = child_nodes[1].obj - parent_node.obj + obj_change_down = child_nodes[2].obj - parent_node.obj + _update_var_history( + pool = pool, + var = branch_var, + x = x, + obj_change_down = obj_change_down, + obj_change_up = obj_change_up, + ) + # Update global history + pool.history.avg_pseudocost_up = + mean(vh.pseudocost_up for vh in values(pool.var_history)) + pool.history.avg_pseudocost_down = + mean(vh.pseudocost_down for vh in values(pool.var_history)) + end + + for node in child_nodes + print_progress( + pool, + node, + time_elapsed = time_elapsed, + print_interval = print_interval, + primal_update = isfinite(node.obj) && isempty(node.fractional_variables), + ) + end + end + return +end + +function _update_var_history(; + pool::NodePool, + var::Variable, + x::Float64, + obj_change_down::Float64, + obj_change_up::Float64, +)::Nothing + # Create new history entry + if var ∉ keys(pool.var_history) + pool.var_history[var] = VariableHistory() + end + varhist = pool.var_history[var] + + # Push fractional value + push!(varhist.fractional_values, x) + + # Push objective value changes + push!(varhist.obj_change_up, obj_change_up) + push!(varhist.obj_change_down, obj_change_down) + + # Push objective change ratios + f_up = x - floor(x) + f_down = ceil(x) - x + if isfinite(obj_change_up) + push!(varhist.obj_ratio_up, obj_change_up / f_up) + end + if isfinite(obj_change_down) + push!(varhist.obj_ratio_down, obj_change_down / f_down) + end + + # Update variable pseudocosts + varhist.pseudocost_up = 0.0 + varhist.pseudocost_down = 0.0 + if !isempty(varhist.obj_ratio_up) + varhist.pseudocost_up = sum(varhist.obj_ratio_up) / length(varhist.obj_ratio_up) + end + if !isempty(varhist.obj_ratio_down) + varhist.pseudocost_down = + sum(varhist.obj_ratio_down) / length(varhist.obj_ratio_down) + end + return +end + +function generate_indices(pool::NodePool, n::Int)::Vector{Int} + lock(pool.lock) do + result = Int[] + for i = 1:n + push!(result, pool.next_index) + pool.next_index += 1 + end + return result + end +end diff --git a/src/BB/optimize.jl b/src/BB/optimize.jl new file mode 100644 index 0000000..bb0fb06 --- /dev/null +++ b/src/BB/optimize.jl @@ -0,0 +1,169 @@ +# 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 Printf +using Base.Threads +import Base.Threads: @threads, nthreads, threadid + +import ..H5File + +function solve!( + mip::MIP; + time_limit::Float64 = Inf, + node_limit::Int = typemax(Int), + gap_limit::Float64 = 1e-4, + print_interval::Int = 5, + initial_primal_bound::Float64 = Inf, + branch_rule::VariableBranchingRule = ReliabilityBranching(), + enable_plunging = true, +)::NodePool + time_initial = time() + pool = NodePool(mip = mip) + pool.primal_bound = initial_primal_bound + + root_node = _create_node(mip) + if isempty(root_node.fractional_variables) + println("root relaxation is integer feasible") + pool.dual_bound = root_node.obj + pool.primal_bound = root_node.obj + return pool + else + print_progress_header() + end + + offer( + pool, + parent_node = nothing, + child_nodes = [root_node], + print_interval = print_interval, + ) + @threads for t = 1:nthreads() + child_one, child_zero, suggestions = nothing, nothing, Node[] + while true + time_elapsed = time() - time_initial + if enable_plunging && (child_one !== nothing) + suggestions = Node[child_one, child_zero] + end + node = take( + pool, + suggestions = suggestions, + time_remaining = time_limit - time_elapsed, + node_limit = node_limit, + gap_limit = gap_limit, + ) + if node == :END + break + elseif node == :WAIT + sleep(0.1) + continue + else + # Assert node is feasible + _set_node_bounds(node) + status, _ = solve_relaxation!(mip) + @assert status == :Optimal + _unset_node_bounds(node) + + # Find branching variable + ids = generate_indices(pool, 2) + branch_var = find_branching_var(branch_rule, node, pool) + + # Find current variable lower and upper bounds + offset = findfirst(isequal(branch_var), mip.int_vars) + var_lb = mip.int_vars_lb[offset] + var_ub = mip.int_vars_ub[offset] + for (offset, v) in enumerate(node.branch_vars) + if v == branch_var + var_lb = max(var_lb, node.branch_lb[offset]) + var_ub = min(var_ub, node.branch_ub[offset]) + end + end + + # Query current fractional value + offset = findfirst(isequal(branch_var), node.fractional_variables) + var_value = node.fractional_values[offset] + + child_zero = _create_node( + mip, + index = ids[2], + parent = node, + branch_var = branch_var, + branch_var_lb = var_lb, + branch_var_ub = floor(var_value), + ) + child_one = _create_node( + mip, + index = ids[1], + parent = node, + branch_var = branch_var, + branch_var_lb = ceil(var_value), + branch_var_ub = var_ub, + ) + offer( + pool, + parent_node = node, + child_nodes = [child_one, child_zero], + time_elapsed = time_elapsed, + print_interval = print_interval, + ) + end + end + end + return pool +end + +function _create_node( + mip; + index::Int = 0, + parent::Union{Nothing,Node} = nothing, + branch_var::Union{Nothing,Variable} = nothing, + branch_var_lb::Union{Nothing,Float64} = nothing, + branch_var_ub::Union{Nothing,Float64} = nothing, +)::Node + if parent === nothing + branch_vars = Variable[] + branch_lb = Float64[] + branch_ub = Float64[] + depth = 1 + else + branch_vars = [parent.branch_vars; branch_var] + branch_lb = [parent.branch_lb; branch_var_lb] + branch_ub = [parent.branch_ub; branch_var_ub] + depth = parent.depth + 1 + end + set_bounds!(mip, branch_vars, branch_lb, branch_ub) + status, obj = solve_relaxation!(mip) + if status == :Optimal + vals = values(mip, mip.int_vars) + fractional_indices = [ + j for j in 1:length(mip.int_vars) if 1e-6 < vals[j] - floor(vals[j]) < 1 - 1e-6 + ] + fractional_values = vals[fractional_indices] + fractional_variables = mip.int_vars[fractional_indices] + else + fractional_variables = Variable[] + fractional_values = Float64[] + end + set_bounds!(mip, mip.int_vars, mip.int_vars_lb, mip.int_vars_ub) + return Node( + mip, + index, + depth, + obj, + status, + branch_vars, + branch_lb, + branch_ub, + fractional_variables, + fractional_values, + parent, + ) +end + +function _set_node_bounds(node::Node) + set_bounds!(node.mip, node.branch_vars, node.branch_lb, node.branch_ub) +end + +function _unset_node_bounds(node::Node) + set_bounds!(node.mip, node.mip.int_vars, node.mip.int_vars_lb, node.mip.int_vars_ub) +end diff --git a/src/BB/structs.jl b/src/BB/structs.jl new file mode 100644 index 0000000..fb99f17 --- /dev/null +++ b/src/BB/structs.jl @@ -0,0 +1,74 @@ +# 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 DataStructures +abstract type VariableBranchingRule end + +struct Variable + index::Any +end + +Base.@kwdef mutable struct MIP + constructor::Any + optimizers::Vector + int_vars::Vector{Variable} + int_vars_lb::Vector{Float64} + int_vars_ub::Vector{Float64} + sense::Float64 + lp_iterations::Int64 + nvars::Int +end + +struct Node + mip::MIP + index::Int + depth::Int + obj::Float64 + status::Symbol + branch_vars::Array{Variable} + branch_lb::Array{Float64} + branch_ub::Array{Float64} + fractional_variables::Array{Variable} + fractional_values::Array{Float64} + parent::Union{Nothing,Node} +end + +Base.@kwdef mutable struct History + avg_pseudocost_up::Float64 = 1.0 + avg_pseudocost_down::Float64 = 1.0 +end + +mutable struct VariableHistory + fractional_values::Array{Float64} + obj_change_up::Array{Float64} + obj_change_down::Array{Float64} + obj_ratio_up::Array{Float64} + obj_ratio_down::Array{Float64} + pseudocost_up::Float64 + pseudocost_down::Float64 + + VariableHistory() = new( + Float64[], # fractional_values + Float64[], # obj_change_up + Float64[], # obj_change_down + Float64[], # obj_ratio_up + Float64[], # obj_ratio_up + 0.0, # pseudocost_up + 0.0, # pseudocost_down + ) +end + +Base.@kwdef mutable struct NodePool + mip::MIP + pending::PriorityQueue{Node,Float64} = PriorityQueue{Node,Float64}() + processing::PriorityQueue{Node,Float64} = PriorityQueue{Node,Float64}() + processed::Int = 0 + next_index::Int = 1 + lock::ReentrantLock = ReentrantLock() + primal_bound::Float64 = Inf + dual_bound::Float64 = Inf + gap::Float64 = Inf + history::History = History() + var_history::Dict{Variable,VariableHistory} = Dict() +end diff --git a/src/BB/varbranch/hybrid.jl b/src/BB/varbranch/hybrid.jl new file mode 100644 index 0000000..fa5d959 --- /dev/null +++ b/src/BB/varbranch/hybrid.jl @@ -0,0 +1,31 @@ +# 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. + +""" + HybridBranching(depth_cutoff::Int, + shallow_rule::VariableBranchingRule, + deep_rule::::VariableBranchingRule) + +Branching strategy that switches between two variable branching strategies, +according to the depth of the node. + +More specifically, if `node.depth <= depth_cutoff`, then `shallow_rule` is +applied. Otherwise, `deep_rule` is applied. +""" +mutable struct HybridBranching <: VariableBranchingRule + depth_cutoff::Int + shallow_rule::VariableBranchingRule + deep_rule::VariableBranchingRule +end + +HybridBranching(depth_cutoff::Int = 10) = + HybridBranching(depth_cutoff, StrongBranching(), PseudocostBranching()) + +function find_branching_var(rule::HybridBranching, node::Node, pool::NodePool)::Variable + if node.depth <= rule.depth_cutoff + return find_branching_var(rule.shallow_rule, node, pool) + else + return find_branching_var(rule.deep_rule, node, pool) + end +end diff --git a/src/BB/varbranch/infeasibility.jl b/src/BB/varbranch/infeasibility.jl new file mode 100644 index 0000000..2c99ed8 --- /dev/null +++ b/src/BB/varbranch/infeasibility.jl @@ -0,0 +1,54 @@ +# 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. + +""" + FirstInfeasibleBranching() + +Branching rule that always selects the first fractional variable. +""" +struct FirstInfeasibleBranching <: VariableBranchingRule end + +function find_branching_var( + rule::FirstInfeasibleBranching, + node::Node, + pool::NodePool, +)::Variable + return node.fractional_variables[1] +end + +""" + LeastInfeasibleBranching() + +Branching strategy that select the fractional variable whose value is the closest +to an integral value. +""" +struct LeastInfeasibleBranching <: VariableBranchingRule end + +function find_branching_var( + rule::LeastInfeasibleBranching, + node::Node, + pool::NodePool, +)::Variable + scores = [max(v - floor(v), ceil(v) - v) for v in node.fractional_values] + _, max_offset = findmax(scores) + return node.fractional_variables[max_offset] +end + +""" + MostInfeasibleBranching() + +Branching strategy that selects the fractional variable whose value is closest +to 1/2. +""" +struct MostInfeasibleBranching <: VariableBranchingRule end + +function find_branching_var( + rule::MostInfeasibleBranching, + node::Node, + pool::NodePool, +)::Variable + scores = [min(v - floor(v), ceil(v) - v) for v in node.fractional_values] + _, max_offset = findmax(scores) + return node.fractional_variables[max_offset] +end diff --git a/src/BB/varbranch/pseudocost.jl b/src/BB/varbranch/pseudocost.jl new file mode 100644 index 0000000..8b3cff0 --- /dev/null +++ b/src/BB/varbranch/pseudocost.jl @@ -0,0 +1,45 @@ +# 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. + +""" + PseudocostBranching() + +Branching strategy that uses historical changes in objective value to estimate +strong branching scores at lower computational cost. +""" +struct PseudocostBranching <: VariableBranchingRule end + +function find_branching_var(rule::PseudocostBranching, node::Node, pool::NodePool)::Variable + scores = [ + _pseudocost_score( + node, + pool, + node.fractional_variables[j], + node.fractional_values[j], + ) for j = 1:length(node.fractional_variables) + ] + _, max_offset = findmax(scores) + return node.fractional_variables[max_offset] +end + +function _pseudocost_score( + node::Node, + pool::NodePool, + var::Variable, + x::Float64, +)::Tuple{Float64,Int} + f_up = x - floor(x) + f_down = ceil(x) - x + pseudo_up = pool.history.avg_pseudocost_up * f_up + pseudo_down = pool.history.avg_pseudocost_down * f_down + if var in keys(pool.var_history) + if isfinite(pool.var_history[var].pseudocost_up) + pseudo_up = pool.var_history[var].pseudocost_up * f_up + end + if isfinite(pool.var_history[var].pseudocost_down) + pseudo_down = pool.var_history[var].pseudocost_down * f_down + end + end + return (pseudo_up * f_up * pseudo_down * f_down, var.index) +end diff --git a/src/BB/varbranch/random.jl b/src/BB/varbranch/random.jl new file mode 100644 index 0000000..fd09c30 --- /dev/null +++ b/src/BB/varbranch/random.jl @@ -0,0 +1,17 @@ +# 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 Random + + +""" + RandomBranching() + +Branching strategy that picks a fractional variable randomly. +""" +struct RandomBranching <: VariableBranchingRule end + +function find_branching_var(rule::RandomBranching, node::Node, pool::NodePool)::Variable + return shuffle(node.fractional_variables)[1] +end diff --git a/src/BB/varbranch/reliability.jl b/src/BB/varbranch/reliability.jl new file mode 100644 index 0000000..e3bc29a --- /dev/null +++ b/src/BB/varbranch/reliability.jl @@ -0,0 +1,191 @@ +# 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 ..to_str_array + +Base.@kwdef mutable struct ReliabilityBranchingStats + branched_count::Vector{Int} = [] + num_strong_branch_calls = 0 + score_var_names::Vector{String} = [] + score_features::Vector{Vector{Float32}} = [] + score_targets::Vector{Float32} = [] +end + +""" + ReliabilityBranching + +Branching strategy that uses pseudocosts if there are enough observations +to make an accurate prediction of strong branching scores, or runs the +actual strong branching routine if not enough observations are available. +""" +Base.@kwdef mutable struct ReliabilityBranching <: VariableBranchingRule + min_samples::Int = 8 + max_sb_calls::Int = 100 + look_ahead::Int = 10 + side_effect::Bool = true + max_iterations::Int = 1_000_000 + aggregation::Symbol = :prod + stats::ReliabilityBranchingStats = ReliabilityBranchingStats() + collect::Bool = false +end + + +function _strong_branch_score(; + node::Node, + pool::NodePool, + var::Variable, + x::Float64, + side_effect::Bool, + max_iterations::Int, + aggregation::Symbol, +)::Tuple{Float64,Int} + # Find current variable lower and upper bounds + offset = findfirst(isequal(var), node.mip.int_vars) + var_lb = node.mip.int_vars_lb[offset] + var_ub = node.mip.int_vars_ub[offset] + for (offset, v) in enumerate(node.branch_vars) + if v == var + var_lb = max(var_lb, node.branch_lb[offset]) + var_ub = min(var_ub, node.branch_ub[offset]) + end + end + + obj_up, obj_down = 0, 0 + obj_up, obj_down = probe(node.mip, var, x, var_lb, var_ub, max_iterations) + obj_change_up = obj_up - node.obj + obj_change_down = obj_down - node.obj + if side_effect + _update_var_history( + pool = pool, + var = var, + x = x, + obj_change_down = obj_change_down, + obj_change_up = obj_change_up, + ) + end + if aggregation == :prod + return (obj_change_up * obj_change_down, var.index) + elseif aggregation == :min + sense = node.mip.sense + return (min(sense * obj_up, sense * obj_down), var.index) + else + error("Unknown aggregation: $aggregation") + end +end + +function find_branching_var( + rule::ReliabilityBranching, + node::Node, + pool::NodePool, +)::Variable + stats = rule.stats + + # Initialize statistics + if isempty(stats.branched_count) + stats.branched_count = zeros(node.mip.nvars) + end + + # Sort variables by pseudocost score + nfrac = length(node.fractional_variables) + pseudocost_scores = [ + _pseudocost_score( + node, + pool, + node.fractional_variables[j], + node.fractional_values[j], + ) for j = 1:nfrac + ] + σ = sortperm(pseudocost_scores, rev = true) + sorted_vars = node.fractional_variables[σ] + + if rule.collect + # Compute dynamic features for all fractional variables + features = [] + for (i, var) in enumerate(sorted_vars) + branched_count = stats.branched_count[var.index] + branched_count_rel = 0.0 + branched_count_sum = sum(stats.branched_count[var.index]) + if branched_count_sum > 0 + branched_count_rel = branched_count / branched_count_sum + end + push!( + features, + Float32[ + nfrac, + node.fractional_values[σ[i]], + node.depth, + pseudocost_scores[σ[i]][1], + branched_count, + branched_count_rel, + ], + ) + end + end + + _set_node_bounds(node) + no_improv_count, n_sb_calls = 0, 0 + max_score, max_var = (-Inf, -Inf), sorted_vars[1] + for (i, var) in enumerate(sorted_vars) + + # Decide whether to use strong branching + use_strong_branch = true + if n_sb_calls >= rule.max_sb_calls + use_strong_branch = false + else + if var in keys(pool.var_history) + varhist = pool.var_history[var] + hlength = min(length(varhist.obj_ratio_up), length(varhist.obj_ratio_down)) + if hlength >= rule.min_samples + use_strong_branch = false + end + end + end + + if use_strong_branch + # Compute strong branching score + n_sb_calls += 1 + score = _strong_branch_score( + node = node, + pool = pool, + var = var, + x = node.fractional_values[σ[i]], + side_effect = rule.side_effect, + max_iterations = rule.max_iterations, + aggregation = rule.aggregation, + ) + + if rule.collect + # Store training data + push!(stats.score_var_names, name(node.mip, var)) + push!(stats.score_features, features[i]) + push!(stats.score_targets, score[1]) + end + else + score = pseudocost_scores[σ[i]] + end + if score > max_score + max_score, max_var = score, var + no_improv_count = 0 + else + no_improv_count += 1 + end + no_improv_count <= rule.look_ahead || break + end + _unset_node_bounds(node) + + # Update statistics + stats.branched_count[max_var.index] += 1 + stats.num_strong_branch_calls += n_sb_calls + + return max_var +end + +function collect!(rule::ReliabilityBranching, h5) + if rule.stats.num_strong_branch_calls == 0 + return + end + h5.put_array("bb_score_var_names", to_str_array(rule.stats.score_var_names)) + h5.put_array("bb_score_features", vcat(rule.stats.score_features'...)) + h5.put_array("bb_score_targets", rule.stats.score_targets) +end diff --git a/src/BB/varbranch/strong.jl b/src/BB/varbranch/strong.jl new file mode 100644 index 0000000..9cb36a5 --- /dev/null +++ b/src/BB/varbranch/strong.jl @@ -0,0 +1,32 @@ +# 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 Random + +""" + StrongBranching(look_ahead::Int, max_calls::Int) + +Branching strategy that selects a subset of fractional variables +as candidates (according to pseudocosts) the solves two linear +programming problems for each candidate. +""" +Base.@kwdef struct StrongBranching <: VariableBranchingRule + look_ahead::Int = 10 + max_calls::Int = 100 + side_effect::Bool = true + max_iterations::Int = 1_000_000 + aggregation::Symbol = :prod +end + +function find_branching_var(rule::StrongBranching, node::Node, pool::NodePool)::Variable + rb_rule = ReliabilityBranching( + min_samples = typemax(Int), + max_sb_calls = rule.max_calls, + look_ahead = rule.look_ahead, + side_effect = rule.side_effect, + max_iterations = rule.max_iterations, + aggregation = rule.aggregation, + ) + return find_branching_var(rb_rule, node, pool) +end diff --git a/src/MIPLearn.jl b/src/MIPLearn.jl index d12e6bc..c21cb1d 100644 --- a/src/MIPLearn.jl +++ b/src/MIPLearn.jl @@ -7,8 +7,6 @@ module MIPLearn using PyCall using SparseArrays -include("Cuts/BlackBox/cplex.jl") - include("collectors.jl") include("components.jl") include("extractors.jl") @@ -27,4 +25,7 @@ function __init__() __init_solvers_learning__() end +include("BB/BB.jl") +include("Cuts/BlackBox/cplex.jl") + end # module diff --git a/test/Project.toml b/test/Project.toml index 3fb6a3d..f0fecf8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -4,6 +4,8 @@ authors = ["Alinson S. Xavier "] version = "0.1.0" [deps] +CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" +Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d" Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" diff --git a/test/fixtures/bell5.h5 b/test/fixtures/bell5.h5 index 8e7d0779eb5e6cf6dec1088691b2f3a1f95eb865..a27e74b95d83ce28fe960b283694656b256dd2bf 100644 GIT binary patch delta 3704 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 1)) +end diff --git a/test/src/MIPLearnT.jl b/test/src/MIPLearnT.jl index 1209478..9db1830 100644 --- a/test/src/MIPLearnT.jl +++ b/test/src/MIPLearnT.jl @@ -11,6 +11,7 @@ FIXTURES = "$BASEDIR/../fixtures" include("fixtures.jl") +include("BB/test_bb.jl") include("Cuts/BlackBox/test_cplex.jl") include("problems/test_setcover.jl") include("test_io.jl") @@ -19,6 +20,9 @@ include("test_usage.jl") function runtests() @testset "MIPLearn" begin + @testset "BB" begin + test_bb() + end test_cuts_blackbox_cplex() test_io() test_problems_setcover()