diff --git a/examples/approx_planted_point.jl b/examples/approx_planted_point.jl index b0b46a59d..c9cb2091f 100644 --- a/examples/approx_planted_point.jl +++ b/examples/approx_planted_point.jl @@ -9,21 +9,10 @@ using Distributions import MathOptInterface const MOI = MathOptInterface - n = 20 diffi = Random.rand(Bool, n) * 0.6 .+ 0.3 -@testset "Approximate planted point" begin - o = SCIP.Optimizer() - MOI.set(o, MOI.Silent(), true) - MOI.empty!(o) - x = MOI.add_variables(o, n) - for xi in x - MOI.add_constraint(o, xi, MOI.GreaterThan(0.0)) - MOI.add_constraint(o, xi, MOI.LessThan(1.0)) - MOI.add_constraint(o, xi, MOI.ZeroOne()) # or MOI.Integer() - end - lmo = FrankWolfe.MathOptLMO(o) +@testset "Approximate planted point - Integer" begin function f(x) return 0.5 * sum((x[i] - diffi[i])^2 for i in eachindex(x)) @@ -32,8 +21,39 @@ diffi = Random.rand(Bool, n) * 0.6 .+ 0.3 @. storage = x - diffi end - x, _, result = Boscia.solve(f, grad!, lmo, verbose=true) + @testset "Using SCIP" begin + o = SCIP.Optimizer() + MOI.set(o, MOI.Silent(), true) + MOI.empty!(o) + x = MOI.add_variables(o, n) + for xi in x + MOI.add_constraint(o, xi, MOI.GreaterThan(0.0)) + MOI.add_constraint(o, xi, MOI.LessThan(1.0)) + MOI.add_constraint(o, xi, MOI.ZeroOne()) # or MOI.Integer() + end + lmo = FrankWolfe.MathOptLMO(o) + + x, _, result = Boscia.solve(f, grad!, lmo, verbose=true) + + @test x == round.(diffi) + @test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3) + end - @test x == round.(diffi) - @test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3) + @testset "Using Cube LMO" begin + int_vars = [] + bin_vars = collect(1:n) + + bounds = Boscia.IntegerBounds() + for i in 1:n + push!(bounds, (i, MOI.GreaterThan(0.0))) + push!(bounds, (i, MOI.LessThan(1.0))) + end + blmo = Boscia.CubeBLMO(n, int_vars, bin_vars, bounds) + + x, _, result = Boscia.solve(f, grad!, blmo, verbose =true) + + @test x == round.(diffi) + @test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3) + end end + diff --git a/examples/birkhoff.jl b/examples/birkhoff.jl index 5b5a07852..7c7025b48 100644 --- a/examples/birkhoff.jl +++ b/examples/birkhoff.jl @@ -118,8 +118,9 @@ x, _, _ = Boscia.solve(f, grad!, lmo, verbose=true) x, _, result_baseline = Boscia.solve(f, grad!, lmo, verbose=true) @test f(x) <= f(result_baseline[:raw_solution]) + 1e-6 lmo = build_birkhoff_lmo() - branching_strategy = Boscia.PartialStrongBranching(20, 1e-4, HiGHS.Optimizer()) - MOI.set(branching_strategy.optimizer, MOI.Silent(), true) + blmo = Boscia.MathOptBLMO(HiGHS.Optimizer()) + branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, blmo) + MOI.set(branching_strategy.bounded_lmo.o, MOI.Silent(), true) x_strong, _, result_strong = Boscia.solve(f, grad!, lmo, verbose=true, branching_strategy=branching_strategy) @test f(x) ≈ f(x_strong) diff --git a/examples/strong_branching_portfolio.jl b/examples/strong_branching_portfolio.jl index 3f0b72af6..90213a41c 100644 --- a/examples/strong_branching_portfolio.jl +++ b/examples/strong_branching_portfolio.jl @@ -69,8 +69,9 @@ end @test dot(ai, x) <= bi + 1e-6 @test f(x) <= f(result_baseline[:raw_solution]) + 1e-6 - branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, HiGHS.Optimizer()) - MOI.set(branching_strategy.optimizer, MOI.Silent(), true) + blmo = Boscia.MathOptBLMO(HiGHS.Optimizer()) + branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, blmo) + MOI.set(branching_strategy.bounded_lmo.o, MOI.Silent(), true) lmo = prepare_portfolio_lmo() x, _, result_strong_branching = diff --git a/src/Boscia.jl b/src/Boscia.jl index e73ee7b51..3068372d4 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -1,6 +1,8 @@ module Boscia using FrankWolfe +import FrankWolfe: compute_extreme_point +export compute_extreme_point using Random using SCIP import MathOptInterface @@ -12,17 +14,20 @@ const MOIU = MOI.Utilities import MathOptSetDistances as MOD +include("integer_bounds.jl") +include("blmo_interface.jl") include("time_tracking_lmo.jl") -include("bounds.jl") include("frank_wolfe_variants.jl") +include("build_lmo.jl") include("node.jl") include("custom_bonobo.jl") include("callbacks.jl") include("problem.jl") -include("infeasible_pairwise.jl") include("heuristics.jl") include("strong_branching.jl") include("utilities.jl") include("interface.jl") +include("MOI_bounded_oracle.jl") +include("cube_blmo.jl") end # module diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl new file mode 100644 index 000000000..3f801c0ae --- /dev/null +++ b/src/MOI_bounded_oracle.jl @@ -0,0 +1,574 @@ +""" + BoundedLinearMinimizationOracle for solvers supporting MathOptInterface. +""" +struct MathOptBLMO{OT<:MOI.AbstractOptimizer} <: BoundedLinearMinimizationOracle + o::OT + use_modify::Bool + function MathOptBLMO(o, use_modify=true) + MOI.set(o, MOI.ObjectiveSense(), MOI.MIN_SENSE) + return new{typeof(o)}(o, use_modify) + end +end + +""" +Build MathOptBLMO from FrankWolfe.MathOptLMO. +""" +function MathOptBLMO(lmo::FrankWolfe.MathOptLMO) + return MathOptBLMO(lmo.o, lmo.use_modfify) +end + +""" +Convert object of Type MathOptLMO into MathOptBLMO and viceversa. +""" +function Base.convert(::Type{MathOptBLMO}, lmo::FrankWolfe.MathOptLMO) + return MathOptBLMO(lmo.o, lmo.use_modify) +end +function Base.convert(::Type{FrankWolfe.MathOptLMO}, blmo::MathOptBLMO) + return FrankWolfe.MathOptLMO(blmo.o, blmo.use_modify) +end + + +################## Necessary to implement #################### +""" + compute_extreme_point + +Is implemented in the FrankWolfe package in file "moi_oracle.jl". +""" +function compute_extreme_point(blmo::MathOptBLMO, d; kwargs...) + lmo = convert(FrankWolfe.MathOptLMO, blmo) + v = FrankWolfe.compute_extreme_point(lmo, d; kwargs) + @assert blmo isa MathOptBLMO + return v +end + +""" +Get list of variables indices and the total number of variables. +If the problem has n variables, they are expected to contiguous and ordered from 1 to n. +""" +function get_list_of_variables(blmo::MathOptBLMO) + v_indices = MOI.get(blmo.o, MOI.ListOfVariableIndices()) + n = length(v_indices) + if v_indices != MOI.VariableIndex.(1:n) + error("Variables are expected to be contiguous and ordered from 1 to N") + end + return n, v_indices +end + +""" +Get list of binary and integer variables, respectively. +""" +function get_binary_variables(blmo::MathOptBLMO) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}()) +end +function get_integer_variables(blmo::MathOptBLMO) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}()) +end + +""" +Get the index of the integer variable the bound is working on. +""" +function get_int_var(blmo::MathOptBLMO, c_idx) + return c_idx.value +end + +""" +Get the list of lower bounds. +""" +function get_lower_bound_list(blmo::MathOptBLMO) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{Float64}}()) +end + +""" +Get the list of upper bounds. +""" +function get_upper_bound_list(blmo::MathOptBLMO) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{Float64}}()) +end + +""" +Change the value of the bound c_idx. +""" +function set_bound!(blmo::MathOptBLMO, c_idx, value) + MOI.set(blmo.o, MOI.ConstraintSet(), c_idx, value) +end + +""" +Read bound value for c_idx. +""" +function get_lower_bound(blmo, c_idx) + return MOI.get(blmo.o, MOI.ConstraintSet(), c_idx) +end +function get_upper_bound(blmo, c_idx) + return MOI.get(blmo.o, MOI.ConstraintSet(), c_idx) +end + +""" +Check if the subject of the bound c_idx is an integer variable (recorded in int_vars). +""" +function is_constraint_on_int_var(blmo::MathOptBLMO, c_idx, int_vars) + return c_idx.value in int_vars +end + +""" +To check if there is bound for the variable in the global or node bounds. +""" +function is_bound_in(blmo::MathOptBLMO, c_idx, bounds) + return haskey(bounds, c_idx.value) +end + +""" +Delete bounds. +""" +function delete_bounds!(blmo::MathOptBLMO, cons_delete) + for d_idx in cons_delete + MOI.delete(blmo.o, d_idx) + end +end + +""" +Add bound constraint. +""" +function add_bound_constraint!(blmo::MathOptBLMO, key, value) + MOI.add_constraint(blmo.o, MOI.VariableIndex(key), value) +end + +""" +Has variable a binary constraint? +""" +function has_binary_constraint(blmo::MathOptBLMO, idx::Int) + consB_list = MOI.get( + blmo.o, + MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}(), + ) + for c_idx in consB_list + if c_idx.value == idx + return true, c_idx + end + end + return false, -1 +end + +""" +Does the variable have an integer constraint? +""" +function has_integer_constraint(blmo::MathOptBLMO, idx::Int) + consB_list = MOI.get( + blmo.o, + MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}(), + ) + for c_idx in consB_list + if c_idx.value == idx + return true, c_idx + end + end + return false, -1 +end + +""" +Is a given point v linear feasible for the model? +""" +function is_linear_feasible(blmo::MathOptBLMO, v::AbstractVector) + return is_linear_feasible(blmo.o, v) +end +function is_linear_feasible(o::MOI.ModelLike, v::AbstractVector) + valvar(f) = v[f.value] + for (F, S) in MOI.get(o, MOI.ListOfConstraintTypesPresent()) + isfeasible = is_linear_feasible_subroutine(o, F, S, valvar) + if !isfeasible + return false + end + end + # satisfies all constraints + return true +end +# function barrier for performance +function is_linear_feasible_subroutine(o::MOI.ModelLike, ::Type{F}, ::Type{S}, valvar) where {F,S} + if S == MOI.ZeroOne || S <: MOI.Indicator || S == MOI.Integer + return true + end + cons_list = MOI.get(o, MOI.ListOfConstraintIndices{F,S}()) + for c_idx in cons_list + func = MOI.get(o, MOI.ConstraintFunction(), c_idx) + val = MOIU.eval_variables(valvar, func) + set = MOI.get(o, MOI.ConstraintSet(), c_idx) + # @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") + dist = MOD.distance_to_set(MOD.DefaultDistance(), val, set) + scip_tol = 1e-6 + if o isa SCIP.Optimizer + scip_tol = MOI.get(o, MOI.RawOptimizerAttribute("numerics/feastol")) + end + if dist > 5000.0 * scip_tol + @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") + @debug("Distance to set: $(dist)") + return false + end + end + return true +end + +""" +Read global bounds from the problem +""" +function build_global_bounds(blmo::MathOptBLMO, integer_variables) + global_bounds = Boscia.IntegerBounds() + for idx in integer_variables + for ST in (MOI.LessThan{Float64}, MOI.GreaterThan{Float64}) + cidx = MOI.ConstraintIndex{MOI.VariableIndex,ST}(idx) + # Variable constraints to not have to be explicitly given, see Buchheim example + if MOI.is_valid(blmo.o, cidx) + s = MOI.get(blmo.o, MOI.ConstraintSet(), cidx) + push!(global_bounds, (idx, s)) + end + end + cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}(idx) + if MOI.is_valid(blmo.o, cidx) + x = MOI.VariableIndex(idx) + s = MOI.get(blmo.o, MOI.ConstraintSet(), cidx) + MOI.delete(blmo.o, cidx) + MOI.add_constraint(blmo.o, x, MOI.GreaterThan(s.lower)) + MOI.add_constraint(blmo.o, x, MOI.LessThan(s.upper)) + push!(global_bounds, (idx, MOI.GreaterThan(s.lower))) + push!(global_bounds, (idx, MOI.LessThan(s.upper))) + end + @assert !MOI.is_valid(blmo.o, cidx) + end + return global_bounds +end + +""" +Add explicit bounds for binary variables. +""" +function explicit_bounds_binary_var(blmo::MathOptBLMO, global_bounds::IntegerBounds, binary_variables) + # adding binary bounds explicitly + for idx in binary_variables + cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(idx) + if !MOI.is_valid(blmo.o, cidx) + MOI.add_constraint(blmo.o, MOI.VariableIndex(idx), MOI.LessThan(1.0)) + end + @assert MOI.is_valid(blmo.o, cidx) + cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(idx) + if !MOI.is_valid(blmo.o, cidx) + MOI.add_constraint(blmo.o, MOI.VariableIndex(idx), MOI.GreaterThan(0.0)) + end + global_bounds[idx, :greaterthan] = MOI.GreaterThan(0.0) + global_bounds[idx, :lessthan] = MOI.LessThan(1.0) + end +end + + +##################### Optional to implement ################ + +""" +Check if the bounds were set correctly in build_LMO. +Safety check only. +""" +function build_LMO_correct(blmo, node_bounds) + for list in (node_bounds.lower_bounds, node_bounds.upper_bounds) + for (idx, set) in list + c_idx = MOI.ConstraintIndex{MOI.VariableIndex, typeof(set)}(idx) + @assert MOI.is_valid(blmo.o, c_idx) + set2 = MOI.get(blmo.o, MOI.ConstraintSet(), c_idx) + if !(set == set2) + MOI.set(blmo.o, MOI.ConstraintSet(), c_idx, set) + set3 = MOI.get(blmo.o, MOI.ConstraintSet(), c_idx) + @assert (set3 == set) "$((idx, set3, set))" + end + end + end + return true +end + +""" +Free model data from previous solve (if necessary). +""" +function free_model(blmo) + free_model(blmo.o) +end + +# cleanup internal SCIP model +function free_model(o::SCIP.Optimizer) + SCIP.SCIPfreeTransform(o) +end + +# no-op by default +function free_model(o::MOI.AbstractOptimizer) + return true +end + +""" +Check if problem is bounded and feasible, i.e. no contradicting constraints. +""" +function check_feasibility(blmo::MathOptBLMO) + MOI.set( + blmo.o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction{Float64}([], 0.0), + ) + MOI.optimize!(blmo.o) + status = MOI.get(blmo.o, MOI.TerminationStatus()) + return status +end + +""" +Check whether a split is valid, i.e. the upper and lower on variable vidx are not the same. +""" +function is_valid_split(tree::Bonobo.BnBTree, blmo::MathOptBLMO, vidx::Int) + bin_var, _ = has_binary_constraint(tree, vidx) + int_var, _ = has_integer_constraint(tree, vidx) + if int_var || bin_var + l_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(vidx) + u_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(vidx) + l_bound = + MOI.is_valid(blmo.o, l_idx) ? + MOI.get(blmo.o, MOI.ConstraintSet(), l_idx) : nothing + u_bound = + MOI.is_valid(blmo.o, u_idx) ? + MOI.get(blmo.o, MOI.ConstraintSet(), u_idx) : nothing + if (l_bound !== nothing && u_bound !== nothing && l_bound.lower === u_bound.upper) + @debug l_bound.lower, u_bound.upper + return false + else + return true + end + else #!bin_var && !int_var + @debug "No binary or integer constraint here." + return true + end +end + +""" +Get solve time, number of nodes and number of simplex iterations. +""" +function get_BLMO_solve_data(blmo::MathOptBLMO) + opt_times = MOI.get(blmo.o, MOI.SolveTimeSec()) + numberofnodes = MOI.get(blmo.o, MOI.NodeCount()) + simplex_iterations = MOI.get(blmo.o, MOI.SimplexIterations()) + return opt_times, numberofnodes, simplex_iterations +end + +""" +Is a given point v indicator feasible, i.e. meets the indicator constraints? If applicable. +""" +function is_indicator_feasible(blmo::MathOptBLMO, v; atol= 1e-6, rtol=1e-6) + return is_indicator_feasible(blmo.o, v; atol, rtol) +end +function is_indicator_feasible(o, x; atol = 1e-6, rtol=1e-6) + valvar(f) = x[f.value] + for (F, S) in MOI.get(o, MOI.ListOfConstraintTypesPresent()) + if S <: MOI.Indicator + cons_list = MOI.get(o, MOI.ListOfConstraintIndices{F,S}()) + for c_idx in cons_list + func = MOI.get(o, MOI.ConstraintFunction(), c_idx) + val = MOIU.eval_variables(valvar, func) + set = MOI.get(o, MOI.ConstraintSet(), c_idx) + # @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") + dist = MOD.distance_to_set(MOD.DefaultDistance(), val, set) + if dist > atol + @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") + @debug("Distance to set: $(dist)") + return false + end + end + end + end + return true +end + +""" +Are indicator constraints present? +""" +function indicator_present(blmo::MathOptBLMO) + for (_, S) in MOI.get(blmo.o, MOI.ListOfConstraintTypesPresent()) + if S <: MOI.Indicator + return true + end + end + return false +end + +""" +Get solving tolerance for the BLMO. +""" +function get_tol(blmo::MathOptBLMO) + return get_tol(blmo.o) +end +function get_tol(o::SCIP.Optimizer) + return MOI.get(o, MOI.RawOptimizerAttribute("numerics/feastol")) +end +function get_tol(o::MOI.AbstractOptimizer) + return 1e-06 +end + +""" +Find best solution from the solving process. +""" +function find_best_solution(f::Function, blmo::MathOptBLMO, vars, domain_oracle) + return find_best_solution(f, blmo.o, vars, domain_oracle) +end + +""" +List of all variable pointers. Depends on how you save your variables internally. + +Is used in `find_best_solution`. +""" +function get_variables_pointers(blmo::MathOptBLMO, tree) + return [MOI.VariableIndex(var) for var in 1:(tree.root.problem.nvars)] +end + +""" +Deal with infeasible vertex if necessary, e.g. check what caused it etc. +""" +function check_infeasible_vertex(blmo::MathOptBLMO, tree) + node = tree.nodes[tree.root.current_node_id[]] + node_bounds = node.local_bounds + for list in (node_bounds.lower_bounds, node_bounds.upper_bounds) + for (idx, set) in list + c_idx = MOI.ConstraintIndex{MOI.VariableIndex, typeof(set)}(idx) + @assert MOI.is_valid(state.tlmo.blmo.o, c_idx) + set2 = MOI.get(state.tlmo.blmo.o, MOI.ConstraintSet(), c_idx) + if !(set == set2) + MOI.set(tlmo.blmo.o, MOI.ConstraintSet(), c_idx, set) + set3 = MOI.get(tlmo.blmo.o, MOI.ConstraintSet(), c_idx) + @assert (set3 == set) "$((idx, set3, set))" + end + end + end +end + +""" +Behavior for strong branching. +""" +function Bonobo.get_branching_variable( + tree::Bonobo.BnBTree, + branching::PartialStrongBranching{MathOptBLMO{OT}}, + node::Bonobo.AbstractNode, +) where OT <: MOI.AbstractOptimizer + xrel = Bonobo.get_relaxed_values(tree, node) + max_lowerbound = -Inf + max_idx = -1 + # copy problem and remove integer constraints + filtered_src = MOI.Utilities.ModelFilter(tree.root.problem.tlmo.blmo.o) do item + if item isa Tuple + (_, S) = item + if S <: Union{MOI.Indicator,MOI.Integer,MOI.ZeroOne} + return false + end + end + return !(item isa MOI.ConstraintIndex{<:Any,<:Union{MOI.ZeroOne,MOI.Integer,MOI.Indicator}}) + end + index_map = MOI.copy_to(branching.bounded_lmo.o, filtered_src) + # sanity check, otherwise the functions need permuted indices + for (v1, v2) in index_map + if v1 isa MOI.VariableIndex + @assert v1 == v2 + end + end + relaxed_lmo = MathOptBLMO(branching.bounded_lmo.o) + @assert !isempty(node.active_set) + active_set = copy(node.active_set) + empty!(active_set) + num_frac = 0 + for idx in Bonobo.get_branching_indices(tree.root) + if !isapprox(xrel[idx], round(xrel[idx]), atol=tree.options.atol, rtol=tree.options.rtol) + # left node: x_i <= floor(̂x_i) + fxi = floor(xrel[idx]) + # create LMO + boundsLeft = copy(node.local_bounds) + if haskey(boundsLeft.upper_bounds, idx) + delete!(boundsLeft.upper_bounds, idx) + end + push!(boundsLeft.upper_bounds, (idx => MOI.LessThan(fxi))) + build_LMO( + relaxed_lmo, + tree.root.problem.integer_variable_bounds, + boundsLeft, + Bonobo.get_branching_indices(tree.root), + ) + MOI.optimize!(relaxed_lmo.o) + #MOI.set(relaxed_lmo.o, MOI.Silent(), false) + if MOI.get(relaxed_lmo.o, MOI.TerminationStatus()) == MOI.OPTIMAL + empty!(active_set) + for (λ, v) in node.active_set + if v[idx] <= xrel[idx] + push!(active_set, ((λ, v))) + end + end + @assert !isempty(active_set) + FrankWolfe.active_set_renormalize!(active_set) + _, _, primal_relaxed, dual_gap_relaxed, _ = + FrankWolfe.blended_pairwise_conditional_gradient( + tree.root.problem.f, + tree.root.problem.g, + relaxed_lmo, + active_set, + verbose=false, + epsilon=branching.solving_epsilon, + max_iteration=branching.max_iteration, + ) + left_relaxed = primal_relaxed - dual_gap_relaxed + else + @debug "Left non-optimal status $(MOI.get(relaxed_lmo.o, MOI.TerminationStatus()))" + left_relaxed = Inf + end + #right node: x_i >= floor(̂x_i) + cxi = ceil(xrel[idx]) + boundsRight = copy(node.local_bounds) + if haskey(boundsRight.lower_bounds, idx) + delete!(boundsRight.lower_bounds, idx) + end + push!(boundsRight.lower_bounds, (idx => MOI.GreaterThan(cxi))) + build_LMO( + relaxed_lmo, + tree.root.problem.integer_variable_bounds, + boundsRight, + Bonobo.get_branching_indices(tree.root), + ) + MOI.optimize!(relaxed_lmo.o) + if MOI.get(relaxed_lmo.o, MOI.TerminationStatus()) == MOI.OPTIMAL + empty!(active_set) + for (λ, v) in node.active_set + if v[idx] >= xrel[idx] + push!(active_set, (λ, v)) + end + end + if isempty(active_set) + @show xrel[idx] + @show length(active_set) + @info [active_set.atoms[idx] for idx in eachindex(active_set)] + error("Empty active set, unreachable") + end + FrankWolfe.active_set_renormalize!(active_set) + _, _, primal_relaxed, dual_gap_relaxed, _ = + FrankWolfe.blended_pairwise_conditional_gradient( + tree.root.problem.f, + tree.root.problem.g, + relaxed_lmo, + active_set, + verbose=false, + epsilon=branching.solving_epsilon, + max_iteration=branching.max_iteration, + ) + right_relaxed = primal_relaxed - dual_gap_relaxed + else + @debug "Right non-optimal status $(MOI.get(relaxed_lmo.o, MOI.TerminationStatus()))" + right_relaxed = Inf + end + # lowest lower bound on the two branches + lowerbound_increase = min(left_relaxed, right_relaxed) + if lowerbound_increase > max_lowerbound + max_lowerbound = lowerbound_increase + max_idx = idx + end + num_frac += 1 + end + end + @debug "strong branching: index $max_idx, lower bound $max_lowerbound" + if max_idx <= 0 && num_frac != 0 + error("Infeasible node! Please check constraints! node lb: $(node.lb)") + max_idx = -1 + end + if max_idx <= 0 + max_idx = -1 + end + return max_idx +end \ No newline at end of file diff --git a/src/blmo_interface.jl b/src/blmo_interface.jl new file mode 100644 index 000000000..7b8d7a3de --- /dev/null +++ b/src/blmo_interface.jl @@ -0,0 +1,203 @@ +""" + BLMO + +Supertype for the Bounded Linear Minimization Oracles +""" +abstract type BoundedLinearMinimizationOracle <: FrankWolfe.LinearMinimizationOracle end + +###################################### Necessary to implement #################################### + +""" +Implement `FrankWolfe.compute_extreme_point` + +Given a direction d solves the problem + min_x d^T x +where x has to be an integer feasible point +""" +function compute_extreme_point end + +""" +Read global bounds from the problem. +""" +function build_global_bounds end + +""" +Add explicit bounds for binary variables, if not already done from the get-go. +""" +function explicit_bounds_binary_var end + + +## Read information from problem + +""" +Get list of variables indices. +If the problem has n variables, they are expected to contiguous and ordered from 1 to n. +""" +function get_list_of_variables end + +""" +Get list of binary variables. +""" +function get_binary_variables end + +""" +Get list of integer variables. +""" +function get_integer_variables end + +""" +Get the index of the integer variable the bound is working on. +""" +function get_int_var end + +""" +Get the list of lower bounds. +""" +function get_lower_bound_list end + +""" +Get the list of upper bounds. +""" +function get_upper_bound_list end + +""" +Read bound value for c_idx. +""" +function get_bound end + + +## Changing the bounds constraints. +""" +Change the value of the bound c_idx. +""" +function set_bound! end + +""" +Delete bounds. +""" +function delete_bounds! end + +""" +Add bound constraint. +""" +function add_bound_constraint! end + + +## Checks +""" +Check if the subject of the bound c_idx is an integer variable (recorded in int_vars). +""" +function is_constraint_on_int_var end + +""" +To check if there is bound for the variable in the global or node bounds. +""" +function is_bound_in end + +""" +Is a given point v linear feasible for the model? +That means does v satisfy all bounds and other linear constraints? +""" +function is_linear_feasible end + +""" +Has variable a binary constraint? +""" +function has_binary_constraint end + +""" +Has variable an integer constraint? +""" +function has_integer_constraint end + + + +#################### Optional to implement #################### + +# These are safety check, utilities and log functions. +# They are not strictly necessary for Boscia to run but would be beneficial to add, especially in the case of the safety functions. + +## Safety Functions +""" +Check if the bounds were set correctly in build_LMO. +Safety check only. +""" +function build_LMO_correct(blmo::BoundedLinearMinimizationOracle, node_bounds) + return true +end + +""" +Check if problem is bounded and feasible, i.e. no contradicting constraints. +""" +function check_feasibility(blmo::BoundedLinearMinimizationOracle) + return MOI.OPTIMAL +end + +""" +Check whether a split is valid, i.e. the upper and lower on variable vidx are not the same. +""" +function is_valid_split(tree::Bonobo.BnBTree, blmo::BoundedLinearMinimizationOracle, vidx::Int) + return true +end + +""" +Is a given point v indicator feasible, i.e. meets the indicator constraints? If applicable. +""" +function is_indicator_feasible(blmo::BoundedLinearMinimizationOracle, v; atol= 1e-6, rtol=1e-6) + return true +end + +""" +Are indicator constraints present? +""" +function indicator_present(blmo::BoundedLinearMinimizationOracle) + return false +end + +""" +Deal with infeasible vertex if necessary, e.g. check what caused it etc. +""" +function check_infeasible_vertex(blmo::BoundedLinearMinimizationOracle, tree) +end + + +## Utility +""" +Free model data from previous solve (if necessary). +""" +function free_model(blmo::BoundedLinearMinimizationOracle) + return true +end + +""" +Get solving tolerance for the BLMO. +""" +function get_tol(blmo::BoundedLinearMinimizationOracle) + return 1e-6 +end + +""" +Find best solution from the solving process. +""" +function find_best_solution(f::Function, blmo::BoundedLinearMinimizationOracle, vars, domain_oracle) + return (nothing, Inf) +end + +""" +List of all variable pointers. Depends on how you save your variables internally. In the easy case, this is simply `collect(1:N)`. + +Is used in `find_best_solution`. +""" +function get_variables_pointers(blmo::BoundedLinearMinimizationOracle, tree) + N = tree.root.problem.nvars + return collect(1:N) +end + + +## Logs +""" +Get solve time, number of nodes and number of iterations, if applicable. +""" +function get_BLMO_solve_data(blmo::BoundedLinearMinimizationOracle) + return 0.0, 0.0, 0.0 +end diff --git a/src/bounds.jl b/src/bounds.jl deleted file mode 100644 index 733ad01b4..000000000 --- a/src/bounds.jl +++ /dev/null @@ -1,184 +0,0 @@ -""" -Constant to handle half open interval bounds on variables -""" -const inf_bound = 10.0^6 - -""" - IntegerBounds - -Keeps track of the bounds of the integer (binary) variables. - -`lower_bounds` dictionary of the MOI.GreaterThan, index is the key. -`upper_bounds` dictionary of the MOI.LessThan, index is the key. -""" -mutable struct IntegerBounds #<: AbstractVector{Tuple{Int,MOI.LessThan{Float64}, MOI.GreaterThan{Float64}}} - lower_bounds::Dict{Int,MOI.GreaterThan{Float64}} - upper_bounds::Dict{Int,MOI.LessThan{Float64}} -end - -IntegerBounds() = - IntegerBounds(Dict{Int,MOI.GreaterThan{Float64}}(), Dict{Int,MOI.LessThan{Float64}}()) - -function Base.push!(ib::IntegerBounds, (idx, bound)) - if bound isa MOI.GreaterThan{Float64} - ib.lower_bounds[idx] = bound - elseif bound isa MOI.LessThan{Float64} - ib.upper_bounds[idx] = bound - end - return ib -end - -#Base.get(ib::GlobalIntegerBounds, i) = (ib.indices[i], ib.lessthan[i], ib.greaterthan[i]) -#Base.size(ib::GlobalIntegerBounds) = size(ib.indices) - -function Base.isempty(ib::IntegerBounds) - return isempty(ib.lower_bounds) && isempty(ib.upper_bounds) -end - -Base.copy(ib::IntegerBounds) = IntegerBounds(copy(ib.lower_bounds), copy(ib.upper_bounds)) - -# convenient call -# ib[3, :lessthan] or ib[3, :greaterthan] -function Base.getindex(ib::IntegerBounds, idx::Int, sense::Symbol) - if sense == :lessthan - ib.upper_bounds[idx] - else - ib.lower_bounds[idx] - end -end - -function Base.get(ib::IntegerBounds, (idx, sense), default) - if sense == :lessthan - get(ib.upper_bounds, idx, default) - else - get(ib.lower_bounds, idx, default) - end -end - -function Base.setindex!(ib::IntegerBounds, val, idx::Int, sense::Symbol) - if sense == :lessthan - ib.upper_bounds[idx] = val - else - ib.lower_bounds[idx] = val - end -end - -function Base.haskey(ib::IntegerBounds, (idx, sense)) - if sense == :lessthan - haskey(ib.upper_bounds, idx) - else - haskey(ib.lower_bounds, idx) - end -end - -#=function find_bound(ib::GlobalIntegerBounds, vidx) - @inbounds for idx in eachindex(ib) - if ib.indices[idx] == vidx - return idx - end - end - return -1 -end =# - -""" -Build node LMO from global LMO - -Four action can be taken: -KEEP - constraint is as saved in the global bounds -CHANGE - lower/upper bound is changed to the node specific one -DELETE - custom bound from the previous node that is invalid at current node and has to be deleted -ADD - bound has to be added for this node because it does not exist in the global bounds (e.g. variable bound is a half open interval globally) -""" -function build_LMO( - lmo::FrankWolfe.LinearMinimizationOracle, - global_bounds::IntegerBounds, - node_bounds::IntegerBounds, - int_vars::Vector{Int}, -) - free_model(lmo.o) - consLT_list = - MOI.get(lmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{Float64}}()) - consGT_list = - MOI.get(lmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{Float64}}()) - cons_delete = [] - - # Lower bounds - for c_idx in consGT_list - if c_idx.value in int_vars - if haskey(global_bounds.lower_bounds, c_idx.value) - # change - if haskey(node_bounds.lower_bounds, c_idx.value) - MOI.set(lmo.o, MOI.ConstraintSet(), c_idx, node_bounds.lower_bounds[c_idx.value]) - else - # keep - MOI.set( - lmo.o, - MOI.ConstraintSet(), - c_idx, - global_bounds.lower_bounds[c_idx.value], - ) - end - else - # delete - push!(cons_delete, c_idx) - end - end - end - - # Upper bounds - for c_idx in consLT_list - if c_idx.value in int_vars - if haskey(global_bounds.upper_bounds, c_idx.value) - # change - if haskey(node_bounds.upper_bounds, c_idx.value) - MOI.set(lmo.o, MOI.ConstraintSet(), c_idx, node_bounds.upper_bounds[c_idx.value]) - else - # keep - MOI.set( - lmo.o, - MOI.ConstraintSet(), - c_idx, - global_bounds.upper_bounds[c_idx.value], - ) - end - else - # delete - push!(cons_delete, c_idx) - end - end - end - - # delete constraints - for d_idx in cons_delete - MOI.delete(lmo.o, d_idx) - end - - # add node specific constraints - for key in keys(node_bounds.lower_bounds) - if !haskey(global_bounds.lower_bounds, key) - MOI.add_constraint(lmo.o, MOI.VariableIndex(key), node_bounds.lower_bounds[key]) - end - end - for key in keys(node_bounds.upper_bounds) - if !haskey(global_bounds.upper_bounds, key) - MOI.add_constraint(lmo.o, MOI.VariableIndex(key), node_bounds.upper_bounds[key]) - end - end - - for list in (node_bounds.lower_bounds, node_bounds.upper_bounds) - for (idx, set) in list - c_idx = MOI.ConstraintIndex{MOI.VariableIndex, typeof(set)}(idx) - @assert MOI.is_valid(lmo.o, c_idx) - set2 = MOI.get(lmo.o, MOI.ConstraintSet(), c_idx) - if !(set == set2) - MOI.set(lmo.o, MOI.ConstraintSet(), c_idx, set) - set3 = MOI.get(lmo.o, MOI.ConstraintSet(), c_idx) - @assert (set3 == set) "$((idx, set3, set))" - end - end - end - -end - -build_LMO(lmo::TimeTrackingLMO, gb::IntegerBounds, nb::IntegerBounds, int_vars::Vector{Int64}) = - build_LMO(lmo.lmo, gb, nb, int_vars) diff --git a/src/build_lmo.jl b/src/build_lmo.jl new file mode 100644 index 000000000..16ddc3a17 --- /dev/null +++ b/src/build_lmo.jl @@ -0,0 +1,81 @@ + +""" +Build node LMO from global LMO + +Four action can be taken: +KEEP - constraint is as saved in the global bounds +CHANGE - lower/upper bound is changed to the node specific one +DELETE - custom bound from the previous node that is invalid at current node and has to be deleted +ADD - bound has to be added for this node because it does not exist in the global bounds (e.g. variable bound is a half open interval globally) +""" +function build_LMO( + blmo::BoundedLinearMinimizationOracle, + global_bounds::IntegerBounds, + node_bounds::IntegerBounds, + int_vars::Vector{Int}, +) + free_model(blmo) + + consLB_list = get_lower_bound_list(blmo) + consUB_list = get_upper_bound_list(blmo) + cons_delete = [] + + # Lower bounds + for c_idx in consLB_list + if is_constraint_on_int_var(blmo, c_idx, int_vars) + v_idx = get_int_var(blmo, c_idx) + if is_bound_in(blmo, c_idx, global_bounds.lower_bounds) + # change + if is_bound_in(blmo, c_idx, node_bounds.lower_bounds) + set_bound!(blmo, c_idx, node_bounds[v_idx, :greaterthan]) + # keep + else + set_bound!(blmo, c_idx, global_bounds[v_idx, :greaterthan]) + end + else + # Delete + push!(cons_delete, c_idx) + end + end + end + + # Upper bounds + for c_idx in consUB_list + if is_constraint_on_int_var(blmo, c_idx, int_vars) + v_idx = get_int_var(blmo, c_idx) + if is_bound_in(blmo, c_idx, global_bounds.upper_bounds) + # change + if is_bound_in(blmo, c_idx, node_bounds.upper_bounds) + set_bound!(blmo, c_idx, node_bounds[v_idx, :lessthan]) + # keep + else + set_bound!(blmo, c_idx, global_bounds[v_idx, :lessthan]) + end + else + # Delete + push!(cons_delete, c_idx) + end + end + end + + # delete constraints + delete_bounds!(blmo, cons_delete) + + # add node specific constraints + # These are bounds constraints where there is no corresponding global bound + for key in keys(node_bounds.lower_bounds) + if !haskey(global_bounds.lower_bounds, key) + add_bound_constraint!(blmo, key, node_bounds.lower_bounds[key]) + end + end + for key in keys(node_bounds.upper_bounds) + if !haskey(global_bounds.upper_bounds, key) + add_bound_constraint!(blmo, key, node_bounds.upper_bounds[key]) + end + end + + build_LMO_correct(blmo, node_bounds) +end + +build_LMO(tlmo::TimeTrackingLMO, gb::IntegerBounds, nb::IntegerBounds, int_vars::Vector{Int64}) = + build_LMO(tlmo.blmo, gb, nb, int_vars) diff --git a/src/callbacks.jl b/src/callbacks.jl index 08bddc6bf..a838d5c03 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -1,31 +1,16 @@ # FW callback function build_FW_callback(tree, min_number_lower, check_rounding_value::Bool, fw_iterations, min_fw_iterations) - vars = [MOI.VariableIndex(var) for var in 1:tree.root.problem.nvars] + vars = get_variables_pointers(tree.root.problem.tlmo.blmo, tree) # variable to only fetch heuristics when the counter increases ncalls = -1 return function fw_callback(state, active_set, args...) @assert isapprox(sum(active_set.weights), 1.0) @assert sum(active_set.weights .< 0) == 0 # TODO deal with vertices becoming infeasible with conflicts - @debug begin - if !is_linear_feasible(tree.root.problem.lmo, state.v) - @info "$(state.v)" - node = tree.nodes[tree.root.current_node_id[]] - node_bounds = node.local_bounds - for list in (node_bounds.lower_bounds, node_bounds.upper_bounds) - for (idx, set) in list - c_idx = MOI.ConstraintIndex{MOI.VariableIndex, typeof(set)}(idx) - @assert MOI.is_valid(state.lmo.lmo.o, c_idx) - set2 = MOI.get(state.lmo.lmo.o, MOI.ConstraintSet(), c_idx) - if !(set == set2) - MOI.set(lmo.lmo.o, MOI.ConstraintSet(), c_idx, set) - set3 = MOI.get(lmo.lmo.o, MOI.ConstraintSet(), c_idx) - @assert (set3 == set) "$((idx, set3, set))" - end - end - end - @assert is_linear_feasible(tree.root.problem.lmo, state.v) - end + if !is_linear_feasible(tree.root.problem.tlmo, state.v) + @info "$(state.v)" + check_infeasible_vertex(tree.root.problem.tlmo.blmo, tree) + @assert is_linear_feasible(tree.root.problem.tlmo, state.v) end push!(fw_iterations, state.t) @@ -33,7 +18,7 @@ function build_FW_callback(tree, min_number_lower, check_rounding_value::Bool, f if ncalls != state.lmo.ncalls ncalls = state.lmo.ncalls (best_v, best_val) = - find_best_solution(tree.root.problem.f, tree.root.problem.lmo.lmo.o, vars, tree.root.options[:domain_oracle]) + find_best_solution(tree.root.problem.f, tree.root.problem.tlmo.blmo, vars, tree.root.options[:domain_oracle]) if best_val < tree.incumbent tree.root.updated_incumbent[] = true node = tree.nodes[tree.root.current_node_id[]] @@ -92,7 +77,7 @@ function build_FW_callback(tree, min_number_lower, check_rounding_value::Bool, f x_rounded[idx] = round(state.x[idx]) end # check linear feasibility - if is_linear_feasible(tree.root.problem.lmo, x_rounded) && is_integer_feasible(tree, x_rounded) + if is_linear_feasible(tree.root.problem.tlmo, x_rounded) && is_integer_feasible(tree, x_rounded) # evaluate f(rounded) val = tree.root.problem.f(x_rounded) if val < tree.incumbent diff --git a/src/cube_blmo.jl b/src/cube_blmo.jl new file mode 100644 index 000000000..47a26372f --- /dev/null +++ b/src/cube_blmo.jl @@ -0,0 +1,167 @@ + +""" + CubeBLMO + +A Bounded Linear Minimization Oracle over a cube. +""" +mutable struct CubeBLMO <: BoundedLinearMinimizationOracle + n::Int + int_vars::Vector{Int} + bin_vars::Vector{Int} + bounds::IntegerBounds + solving_time::Float64 +end + +CubeBLMO(n, int_vars, bin_vars, bounds) = CubeBLMO(n, int_vars, bin_vars, bounds, 0.0) + +## Necessary + +# computing an extreme point for the cube amounts to checking the sign of the gradient +function compute_extreme_point(blmo::CubeBLMO, d; kwargs...) + time_ref = Dates.now() + v = zeros(length(d)) + for i in eachindex(d) + v[i] = d[i] > 0 ? blmo.bounds[i, :greaterthan].lower : blmo.bounds[i, :lessthan].upper + end + blmo.solving_time = float(Dates.value(Dates.now() - time_ref)) + return v +end + +## + +function build_global_bounds(blmo::CubeBLMO, integer_variables) + global_bounds = IntegerBounds() + for i in 1:blmo.n + if i in integer_variables + push!(global_bounds, (i, blmo.bounds[i, :lessthan])) + push!(global_bounds, (i, blmo.bounds[i, :greaterthan])) + end + end + return global_bounds +end + +function explicit_bounds_binary_var(blmo::CubeBLMO, gb::IntegerBounds, binary_vars) + nothing +end + +## Read information from problem +function get_list_of_variables(blmo::CubeBLMO) + return blmo.n, collect(1:blmo.n) + end + +# Get list of binary and integer variables, respectively. +function get_binary_variables(blmo::CubeBLMO) + return blmo.bin_vars +end + +function get_integer_variables(blmo::CubeBLMO) + return blmo.int_vars +end + +function get_int_var(blmo::CubeBLMO, cidx) + return cidx +end + +function get_lower_bound_list(blmo::CubeBLMO) + return keys(blmo.bounds.lower_bounds) +end + +function get_upper_bound_list(blmo::CubeBLMO) + return keys(blmo.bounds.upper_bounds) +end + +function get_lower_bound(blmo::CubeBLMO, c_idx) + return blmo.bounds[c_idx, :greaterthan] +end + +function get_upper_bound(blmo::CubeBLMO, c_idx) + return blmo.bounds[c_idx, :lessthan] +end + +## Changing the bounds constraints. +function set_bound!(blmo::CubeBLMO, c_idx, value) + if value isa MOI.GreaterThan{Float64} + blmo.bounds.lower_bounds[c_idx] = value + elseif value isa MOI.LessThan{Float64} + blmo.bounds.upper_bounds[c_idx] = value + else + error("We expect the value to be of type MOI.GreaterThan or Moi.LessThan!") + end +end + +function delete_bounds!(::CubeBLMO, cons_delete) + # For the cube this shouldn't happen! Otherwise we get unbounded! + if !isempty(cons_delete) + error("Trying to delete bounds of the cube!") + end +end + +function add_bound_constraint!(::CubeBLMO, key, value) + # Should not be necessary + error("Trying to add bound constraints of the cube!") +end + +## Checks + +function is_constraint_on_int_var(blmo::CubeBLMO, c_idx, int_vars) + return c_idx in int_vars +end + +function is_bound_in(blmo::CubeBLMO, c_idx, bounds) + return haskey(bounds, c_idx) +end + +function is_linear_feasible(blmo::CubeBLMO, v::AbstractVector) + for i in eachindex(v) + if !(blmo.bounds[i, :greaterthan].lower ≤ v[i] + 1e-6 || !(v[i] - 1e-6 ≤ blmo.bounds[i, :lessthan].upper)) + @debug("Vertex entry: $(v[i]) Lower bound: $(blmo.bounds[i, :greaterthan].lower) Upper bound: $(blmo.bounds[i, :lessthan].upper))") + return false + end + end + return true +end + +function has_binary_constraint(blmo::CubeBLMO, idx) + return idx in blmo.int_vars +end + +function has_integer_constraint(blmo::CubeBLMO, idx) + return idx in blmo.bin_vars +end + + + +###################### Optional +## Safety Functions + +function build_LMO_correct(blmo::CubeBLMO, node_bounds) + for key in keys(node_bounds.lower_bounds) + if !haskey(blmo.bounds, (key, :greaterthan)) || blmo.bounds[key, :greaterthan] != node_bounds[key, :greaterthan] + return false + end + end + for key in keys(node_bounds.upper_bounds) + if !haskey(blmo.bounds, (key, :lessthan)) || blmo.bounds[key, :lessthan] != node_bounds[key, :lessthan] + return false + end + end + return true +end + +function check_feasibility(blmo::CubeBLMO) + for i in 1:blmo.n + if !haskey(blmo.bounds, (i, :greaterthan)) || !haskey(blmo.bounds, (i, :lessthan)) + return MOI.DUAL_INFEASIBLE + end + end + return MOI.OPTIMAL +end + +function is_valid_split(tree::Bonobo.BnBTree, blmo::CubeBLMO, vidx::Int) + return blmo.bounds[vidx, :lessthan] != blmo.bounds[vidx, :greaterthan] +end + +## Logs +function get_BLMO_solve_data(blmo::CubeBLMO) + return blmo.solving_time, 0.0, 0.0 +end diff --git a/src/heuristics.jl b/src/heuristics.jl index ebbcd47a4..bc6719bd6 100644 --- a/src/heuristics.jl +++ b/src/heuristics.jl @@ -22,6 +22,10 @@ function find_best_solution(f::Function, o::SCIP.Optimizer, vars::Vector{MOI.Var return (best_v, best_val) end +""" +Finds the best solution in the Optimizer's solution storage, based on the objective function `f`. +Returns the solution vector and the corresponding best value. +""" function find_best_solution(f::Function, o::MOI.AbstractOptimizer, vars::Vector{MOI.VariableIndex}, domain_oracle) nsols = MOI.get(o, MOI.ResultCount()) @assert nsols > 0 diff --git a/src/infeasible_pairwise.jl b/src/infeasible_pairwise.jl deleted file mode 100644 index 5c900e6b4..000000000 --- a/src/infeasible_pairwise.jl +++ /dev/null @@ -1,509 +0,0 @@ -import FrankWolfe: fast_dot, compute_extreme_point, muladd_memory_mode, get_active_set_iterate - -""" -InfeasibleFrankWolfeNode functions - - InfeasibleFrankWolfeNode <: AbstractFrankWolfeNode - -A node in the branch-and-bound tree storing information for a Frank-Wolfe subproblem. - -`std` stores the id, lower and upper bound of the node. -`valid_active` vector of booleans indicating which vertices in the global active set are valid for the node. -`lmo` is the minimization oracle capturing the feasible region. -""" -mutable struct InfeasibleFrankWolfeNode{IB<:IntegerBounds} <: AbstractFrankWolfeNode - std::Bonobo.BnBNodeInfo - valid_active::Vector{Bool} - local_bounds::IB -end - -""" -InfeasibleFrankWolfeNode: Create the information of the new branching nodes -based on their parent and the index of the branching variable -""" -function Bonobo.get_branching_nodes_info( - tree::Bonobo.BnBTree, - node::InfeasibleFrankWolfeNode, - vidx::Int, -) - # get solution - x = Bonobo.get_relaxed_values(tree, node) - - # add new bounds to the feasible region left and right - # copy bounds from parent - varbounds_left = copy(node.local_bounds) - varbounds_right = copy(node.local_bounds) - - if haskey(varbounds_left.upper_bounds, vidx) - delete!(varbounds_left.upper_bounds, vidx) - end - if haskey(varbounds_right.lower_bounds, vidx) - delete!(varbounds_right.lower_bounds, vidx) - end - push!(varbounds_left.upper_bounds, (vidx => MOI.LessThan(floor(x[vidx])))) - push!(varbounds_right.lower_bounds, (vidx => MOI.GreaterThan(ceil(x[vidx])))) - - #valid_active is set at evaluation time - node_info_left = (valid_active=Bool[], local_bounds=varbounds_left) - node_info_right = (valid_active=Bool[], local_bounds=varbounds_right) - - return [node_info_left, node_info_right] - -end - - -""" -Build up valid_active; is called whenever the global active_set changes -""" -function populate_valid_active!( - active_set::FrankWolfe.ActiveSet, - node::InfeasibleFrankWolfeNode, - lmo::FrankWolfe.LinearMinimizationOracle, -) - empty!(node.valid_active) - for i in eachindex(active_set) - push!(node.valid_active, is_linear_feasible(lmo, active_set.atoms[i])) - end -end - -function Bonobo.get_relaxed_values(tree::Bonobo.BnBTree, node::InfeasibleFrankWolfeNode) - return copy(FrankWolfe.get_active_set_iterate(tree.root.problem.active_set)) -end - - - -""" -Blended pairwise CG coping with infeasible vertices in the active set. -Infeasible vertices are those for which the passed `filter_function(v)` is false. -They can be used only as away directions, not as forward ones. The LMO is always assumed to return feasible vertices. -""" -function infeasible_blended_pairwise( - f, - grad!, - lmo, - x0, - node::InfeasibleFrankWolfeNode; - line_search::FrankWolfe.LineSearchMethod=Adaptive(), - epsilon=1e-7, - max_iteration=10000, - print_iter=1000, - trajectory=false, - verbose=false, - memory_mode::FrankWolfe.MemoryEmphasis=InplaceEmphasis(), - gradient=nothing, - callback=nothing, - timeout=Inf, - print_callback=print_callback, - renorm_interval=1000, - lazy=false, - linesearch_workspace=nothing, - lazy_tolerance=2.0, - filter_function=(args...) -> true, - eager_filter=true, - coldStart=false, -) - # add the first vertex to active set from initialization - active_set = ActiveSet([(1.0, x0)]) - - return infeasible_blended_pairwise( - f, - grad!, - lmo, - active_set, - node, - line_search=line_search, - epsilon=epsilon, - max_iteration=max_iteration, - print_iter=print_iter, - trajectory=trajectory, - verbose=verbose, - memory_mode=memory_mode, - gradient=gradient, - callback=callback, - timeout=timeout, - print_callback=print_callback, - renorm_interval=renorm_interval, - lazy=lazy, - linesearch_workspace=linesearch_workspace, - lazy_tolerance=lazy_tolerance, - filter_function=filter_function, - eager_filter=eager_filter, - coldStart=coldStart, - ) -end - -function infeasible_blended_pairwise( - f, - grad!, - lmo, - active_set::FrankWolfe.ActiveSet, - node::InfeasibleFrankWolfeNode; - line_search::FrankWolfe.LineSearchMethod=FrankWolfe.Adaptive(), - epsilon=1e-7, - max_iteration=10000, - print_iter=1000, - trajectory=false, - verbose=false, - memory_mode::FrankWolfe.MemoryEmphasis=FrankWolfe.InplaceEmphasis(), - gradient=nothing, - callback=nothing, - timeout=Inf, - print_callback=FrankWolfe.print_callback, - renorm_interval=1000, - lazy=false, - linesearch_workspace=nothing, - lazy_tolerance=2.0, - filter_function=(args...) -> true, - eager_filter=true, # removes infeasible points from the get go - coldStart=false, # if the active set is completey infeasible, it will be cleaned up and restarted -) - # format string for output of the algorithm - format_string = "%6s %13s %14e %14e %14e %14e %14e %14i\n" - - # if true, all infeasible vertices will be deleted from the active set - if eager_filter - if count(node.valid_active) > 0 - for i in Iterators.reverse(eachindex(node.valid_active)) - if !node.valid_active[i] - deleteat!(active_set, i) - deleteat!(node.valid_active, i) - end - end - FrankWolfe.active_set_renormalize!(active_set) - else - v = compute_extreme_point(lmo, randn(length(active_set.atoms[1]))) - FrankWolfe.empty!(active_set) - empty!(node.valid_active) - FrankWolfe.push!(active_set, (1.0, v)) - push!(node.valid_active, true) - end - FrankWolfe.compute_active_set_iterate!(active_set) - end - - t = 0 - primal = Inf - x = get_active_set_iterate(active_set) - feasible_x = eager_filter ? x : calculate_feasible_x(active_set, node, coldStart) - tt = FrankWolfe.regular - traj_data = [] - if trajectory && callback === nothing - callback = FrankWolfe.trajectory_callback(traj_data) - end - time_start = time_ns() - - d = similar(x) - - if verbose - println("\nInfeasible Blended Pairwise Conditional Gradient Algorithm.") - NumType = eltype(x) - println( - "MEMORY_MODE: $memory_mode STEPSIZE: $line_search EPSILON: $epsilon MAXITERATION: $max_iteration TYPE: $NumType", - ) - grad_type = typeof(gradient) - println("GRADIENTTYPE: $grad_type LAZY: $lazy lazy_tolerance: $lazy_tolerance") - if memory_mode isa FrankWolfe.InplaceEmphasis - @info("In memory_mode memory iterates are written back into x0!") - end - headers = - ("Type", "Iteration", "Primal", "Dual", "Dual Gap", "Time", "It/sec", "#ActiveSet") - print_callback(headers, format_string, print_header=true) - end - - # likely not needed anymore as now the iterates are provided directly via the active set - if gradient === nothing - gradient = similar(x) - end - - grad!(gradient, x) - v = compute_extreme_point(lmo, gradient) - # if !lazy, phi is maintained as the global dual gap - phi = max(0, fast_dot(feasible_x, gradient) - fast_dot(v, gradient)) - local_gap = zero(phi) - gamma = 1.0 - - if linesearch_workspace === nothing - linesearch_workspace = FrankWolfe.build_linesearch_workspace(line_search, x, gradient) - end - - while t <= max_iteration && (phi >= max(epsilon, eps()) || !filter_function(lmo, x)) - - # managing time limit - time_at_loop = time_ns() - if t == 0 - time_start = time_at_loop - end - # time is measured at beginning of loop for consistency throughout all algorithms - tot_time = (time_at_loop - time_start) / 1e9 - - if timeout < Inf - if tot_time ≥ timeout - if verbose - @info "Time limit reached" - end - break - end - end - - ##################### - - # compute current iterate from active set - x = get_active_set_iterate(active_set) - grad!(gradient, x) - feasible_x = eager_filter ? x : calculate_feasible_x(active_set, node, coldStart) - - _, v_local, v_local_loc, v_val, a_val, a, a_loc, _, _ = - active_set_argminmax_filter(active_set, gradient, node, lmo, filter_function) - - local_gap = fast_dot(gradient, a) - fast_dot(gradient, v_local) - # if not finite, there is no feasible vertex to move towards, - # local_gap = -Inf to be sure to pick the FW vertex - if !isfinite(v_val) - local_gap -= Inf - end - if !lazy - v = compute_extreme_point(lmo, gradient) - dual_gap = fast_dot(gradient, feasible_x) - fast_dot(gradient, v) - phi = dual_gap - end - # minor modification from original paper for improved sparsity - # (proof follows with minor modification when estimating the step) - if local_gap ≥ phi / lazy_tolerance - if !is_linear_feasible(lmo, a) - deleteat!(active_set, a_loc) - FrankWolfe.active_set_renormalize!(active_set) - FrankWolfe.compute_active_set_iterate!(active_set) - else - d = muladd_memory_mode(memory_mode, d, a, v_local) - vertex_taken = v_local - gamma_max = a_val - gamma = FrankWolfe.perform_line_search( - line_search, - t, - f, - grad!, - gradient, - x, - d, - gamma_max, - linesearch_workspace, - memory_mode, - ) - # reached maximum of lambda -> dropping away vertex - if gamma ≈ gamma_max - tt = FrankWolfe.drop - active_set.weights[v_local_loc] += gamma - deleteat!(active_set, a_loc) - @assert FrankWolfe.active_set_validate(active_set) - else # transfer weight from away to local FW - tt = FrankWolfe.pairwise - active_set.weights[a_loc] -= gamma - active_set.weights[v_local_loc] += gamma - end - populate_valid_active!(active_set, node, lmo) - FrankWolfe.active_set_update_iterate_pairwise!(active_set, gamma, v_local, a) - end - else # add to active set - if lazy # otherwise, v computed above already - v = compute_extreme_point(lmo, gradient) - end - vertex_taken = v - dual_gap = fast_dot(gradient, feasible_x) - fast_dot(gradient, v) - if (!lazy || dual_gap ≥ phi / lazy_tolerance) - tt = FrankWolfe.regular - d = FrankWolfe.muladd_memory_mode(memory_mode, d, x, v) - - gamma = FrankWolfe.perform_line_search( - line_search, - t, - f, - grad!, - gradient, - x, - d, - one(eltype(x)), - linesearch_workspace, - memory_mode, - ) - # dropping active set and restarting from singleton - if gamma ≈ 1.0 - FrankWolfe.active_set_initialize!(active_set, v) - else - renorm = mod(t, renorm_interval) == 0 - FrankWolfe.active_set_update!(active_set, gamma, v) - end - @assert FrankWolfe.active_set_validate(active_set) - populate_valid_active!(active_set, node, lmo) - else # dual step - tt = FrankWolfe.dualstep - # set to computed dual_gap for consistency between the lazy and non-lazy run. - # that is ok as we scale with the K = 2.0 default anyways - phi = dual_gap - end - end - if ( - ((mod(t, print_iter) == 0 || tt == FrankWolfe.dualstep) == 0 && verbose) || - callback !== nothing || - !( - line_search isa FrankWolfe.Agnostic || - line_search isa FrankWolfe.Nonconvex || - line_search isa FrankWolfe.FixedStep - ) - ) - primal = f(x) - end - if callback !== nothing - state = ( - t=t, - primal=primal, - dual=primal - phi, - dual_gap=phi, - time=tot_time, - x=x, - v=vertex_taken, - gamma=gamma, - active_set=active_set, - gradient=gradient, - ) - callback(state) - end - - if verbose && (mod(t, print_iter) == 0 || tt == FrankWolfe.dualstep) - if t == 0 - tt = FrankWolfe.initial - end - rep = ( - st[Symbol(tt)], - string(t), - Float64(primal), - Float64(primal - dual_gap), - Float64(dual_gap), - tot_time, - t / tot_time, - length(active_set), - ) - print_callback(rep, format_string) - flush(stdout) - end - t += 1 - end - - # recompute everything once more for final verfication / do not record to trajectory though for now! - # this is important as some variants do not recompute f(x) and the dual_gap regularly but only when reporting - # hence the final computation. - # do also cleanup of active_set due to many operations on the same set - - if verbose - x = get_active_set_iterate(active_set) - grad!(gradient, x) - v = compute_extreme_point(lmo, gradient) - primal = f(x) - phi = fast_dot(x, gradient) - fast_dot(v, gradient) - tt = FrankWolfe.last - rep = ( - FrankWolfe.st[Symbol(tt)], - string(t - 1), - Float64(primal), - Float64(primal - phi), - Float64(phi), - (time_ns() - time_start) / 1.0e9, - t / ((time_ns() - time_start) / 1.0e9), - length(active_set), - ) - print_callback(rep, format_string) - flush(stdout) - end - FrankWolfe.active_set_renormalize!(active_set) - FrankWolfe.active_set_cleanup!(active_set) - populate_valid_active!(active_set, node, lmo) - x = FrankWolfe.get_active_set_iterate(active_set) - grad!(gradient, x) - v = compute_extreme_point(lmo, gradient) - primal = f(x) - dual_gap = fast_dot(x, gradient) - fast_dot(v, gradient) - if verbose - tt = FrankWolfe.pp - rep = ( - st[Symbol(tt)], - string(t - 1), - Float64(primal), - Float64(primal - dual_gap), - Float64(dual_gap), - (time_ns() - time_start) / 1.0e9, - t / ((time_ns() - time_start) / 1.0e9), - length(active_set), - ) - print_callback(rep, format_string) - print_callback(nothing, format_string, print_footer=true) - flush(stdout) - end - - return x, v, primal, dual_gap, traj_data, active_set -end - -function active_set_argminmax_filter( - active_set::FrankWolfe.ActiveSet, - direction, - node::InfeasibleFrankWolfeNode, - lmo::FrankWolfe.LinearMinimizationOracle, - filter_function::Function; - Φ=0.5, -) - val = Inf - valM = -Inf - idx = -1 - idxM = -1 - for i in eachindex(active_set) - temp_val = fast_dot(active_set.atoms[i], direction) - if !node.valid_active[i] - @debug "Hitting infeasible vertex" - end - if temp_val < val && node.valid_active[i] - val = temp_val - idx = i - end - if valM < temp_val && active_set.weights[i] != 0.0 # don't step away from "deleted" vertices - valM = temp_val - x = zeros(length(active_set.atoms[1])) - s = zero(eltype(x)) - # Are there any feasible vertices - idxM = i - end - end - return (active_set[idx]..., idx, val, active_set[idxM]..., idxM, valM, valM - val ≥ Φ) -end - -function calculate_feasible_x( - active_set::FrankWolfe.ActiveSet, - node::InfeasibleFrankWolfeNode, - coldStart::Bool, -) - x = zeros(length(active_set.atoms[1])) - s = zero(eltype(x)) - # Are there any feasible vertices - if count(node.valid_active) > 0 - for i in eachindex(active_set) - if node.valid_active[i] - x .+= active_set.weights[i] * active_set.atoms[i] - s += active_set.weights[i] - end - end - x ./= s - else - v = compute_extreme_point(node.lmo, randn(length(active_set.atoms[1]))) - λ = 0.0 - # If coldStart tre, completely restart the active set - if coldStart - FrankWolfe.empty!(active_set) - empty!(node.valid_active) - λ = 1.0 - else # if not, add v and asign greatest currently appearing weight - λ = maximum(active_set.weights) - end - FrankWolfe.push!(active_set, (λ, v)) - push!(node.valid_active, true) - FrankWolfe.active_set_renormalize!(active_set) - FrankWolfe.compute_active_set_iterate!(active_set) - x .= v - end - return x -end diff --git a/src/integer_bounds.jl b/src/integer_bounds.jl new file mode 100644 index 000000000..23f61d792 --- /dev/null +++ b/src/integer_bounds.jl @@ -0,0 +1,65 @@ + +""" + IntegerBounds + +Keeps track of the bounds of the integer (binary) variables. + +`lower_bounds` dictionary of the MOI.GreaterThan, index is the key. +`upper_bounds` dictionary of the MOI.LessThan, index is the key. +""" +mutable struct IntegerBounds + lower_bounds::Dict{Int,MOI.GreaterThan{Float64}} + upper_bounds::Dict{Int,MOI.LessThan{Float64}} +end + +IntegerBounds() = + IntegerBounds(Dict{Int,MOI.GreaterThan{Float64}}(), Dict{Int,MOI.LessThan{Float64}}()) + +function Base.push!(ib::IntegerBounds, (idx, bound)) + if bound isa MOI.GreaterThan{Float64} + ib.lower_bounds[idx] = bound + elseif bound isa MOI.LessThan{Float64} + ib.upper_bounds[idx] = bound + end + return ib +end + +function Base.isempty(ib::IntegerBounds) + return isempty(ib.lower_bounds) && isempty(ib.upper_bounds) +end + +Base.copy(ib::IntegerBounds) = IntegerBounds(copy(ib.lower_bounds), copy(ib.upper_bounds)) + +# convenient call +# ib[3, :lessthan] or ib[3, :greaterthan] +function Base.getindex(ib::IntegerBounds, idx::Int, sense::Symbol) + if sense == :lessthan + ib.upper_bounds[idx] + else + ib.lower_bounds[idx] + end +end + +function Base.get(ib::IntegerBounds, (idx, sense), default) + if sense == :lessthan + get(ib.upper_bounds, idx, default) + else + get(ib.lower_bounds, idx, default) + end +end + +function Base.setindex!(ib::IntegerBounds, val, idx::Int, sense::Symbol) + if sense == :lessthan + ib.upper_bounds[idx] = val + else + ib.lower_bounds[idx] = val + end +end + +function Base.haskey(ib::IntegerBounds, (idx, sense)) + if sense == :lessthan + return haskey(ib.upper_bounds, idx) + else + return haskey(ib.lower_bounds, idx) + end +end diff --git a/src/interface.jl b/src/interface.jl index 0d252d919..c7cba9686 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,9 +1,75 @@ +""" +Solve function in case of MathOptLMO. +Converts the lmo into a MathOptBLMO and calls the solve function below. +""" +function solve( + f, + g, + lmo::FrankWolfe.MathOptLMO; + traverse_strategy=Bonobo.BestFirstSearch(), + branching_strategy=Bonobo.MOST_INFEASIBLE(), + variant::FrankWolfeVariant=BPCG(), + line_search::FrankWolfe.LineSearchMethod=FrankWolfe.Adaptive(), + active_set::Union{Nothing, FrankWolfe.ActiveSet} = nothing, + fw_epsilon=1e-2, + verbose=false, + dual_gap=1e-6, + rel_dual_gap=1.0e-2, + time_limit=Inf, + print_iter=100, + dual_gap_decay_factor=0.8, + max_fw_iter=10000, + min_number_lower=Inf, + min_node_fw_epsilon=1e-6, + use_postsolve=true, + min_fw_iterations=5, + max_iteration_post=10000, + dual_tightening=true, + global_dual_tightening=true, + bnb_callback=nothing, + strong_convexity=0.0, + domain_oracle= x->true, + start_solution=nothing, + fw_verbose = false, + kwargs... +) + blmo = convert(MathOptBLMO, lmo) + return solve(f, g, blmo; + traverse_strategy=traverse_strategy, + branching_strategy=branching_strategy, + variant=variant, + line_search=line_search, + active_set=active_set, + fw_epsilon=fw_epsilon, + verbose=verbose, + dual_gap=dual_gap, + rel_dual_gap=rel_dual_gap, + time_limit=time_limit, + print_iter=print_iter, + dual_gap_decay_factor=dual_gap_decay_factor, + max_fw_iter=max_fw_iter, + min_number_lower=min_number_lower, + min_node_fw_epsilon=min_node_fw_epsilon, + use_postsolve=use_postsolve, + min_fw_iterations=min_fw_iterations, + max_iteration_post=max_iteration_post, + dual_tightening=dual_tightening, + global_dual_tightening=global_dual_tightening, + bnb_callback=bnb_callback, + strong_convexity=strong_convexity, + domain_oracle=domain_oracle, + start_solution=start_solution, + fw_verbose=fw_verbose, + kwargs... + ) +end + """ solve f - objective function oracle. g - oracle for the gradient of the objective. -lmo - a MIP solver instance (SCIP) encoding the feasible region. +blmo - a MIP solver instance (e.g., SCIP) encoding the feasible region. Has to be of type `BoundedLinearMinimizationOracle` (see `lmo_wrapper.jl`). traverse_strategy - encodes how to choose the next node for evaluation. By default the node with the best lower bound is picked. branching_strategy - by default we branch on the entry which is the farthest @@ -50,7 +116,7 @@ fw_verbose - if true, FrankWolfe logs are printed function solve( f, grad!, - lmo; + blmo::BoundedLinearMinimizationOracle; traverse_strategy=Bonobo.BestFirstSearch(), branching_strategy=Bonobo.MOST_INFEASIBLE(), variant::FrankWolfeVariant=BPCG(), @@ -96,26 +162,24 @@ function solve( println("\t Additional kwargs: ", join(keys(kwargs), ",")) end - v_indices = MOI.get(lmo.o, MOI.ListOfVariableIndices()) - n = length(v_indices) - if v_indices != MOI.VariableIndex.(1:n) - error("Variables are expected to be contiguous and ordered from 1 to N") - end + n, v_indices = get_list_of_variables(blmo) integer_variables = Vector{Int}() num_int = 0 num_bin = 0 - for cidx in MOI.get(lmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}()) - push!(integer_variables, cidx.value) + for c_idx in get_integer_variables(blmo) + v_idx = get_int_var(blmo, c_idx) + push!(integer_variables, v_idx) num_int += 1 end binary_variables = BitSet() - for cidx in MOI.get(lmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}()) - push!(integer_variables, cidx.value) - push!(binary_variables, cidx.value) + for c_idx in get_binary_variables(blmo) + v_idx = get_int_var(blmo, c_idx) + push!(integer_variables, v_idx) + push!(binary_variables, v_idx) num_bin += 1 end - time_lmo = Boscia.TimeTrackingLMO(lmo, integer_variables) + time_lmo = Boscia.TimeTrackingLMO(blmo, integer_variables) if num_bin == 0 && num_int == 0 error("No integer or binary variables detected! Please use an IP solver!") @@ -127,54 +191,20 @@ function solve( println("\t Number of binary variables: $(num_bin)\n") end - global_bounds = Boscia.IntegerBounds() - for idx in integer_variables - for ST in (MOI.LessThan{Float64}, MOI.GreaterThan{Float64}) - cidx = MOI.ConstraintIndex{MOI.VariableIndex,ST}(idx) - # Variable constraints to not have to be explicitly given, see Buchheim example - if MOI.is_valid(lmo.o, cidx) - s = MOI.get(lmo.o, MOI.ConstraintSet(), cidx) - push!(global_bounds, (idx, s)) - end - end - cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}(idx) - if MOI.is_valid(lmo.o, cidx) - x = MOI.VariableIndex(idx) - s = MOI.get(lmo.o, MOI.ConstraintSet(), cidx) - MOI.delete(lmo.o, cidx) - MOI.add_constraint(lmo.o, x, MOI.GreaterThan(s.lower)) - MOI.add_constraint(lmo.o, x, MOI.LessThan(s.upper)) - push!(global_bounds, (idx, MOI.GreaterThan(s.lower))) - push!(global_bounds, (idx, MOI.LessThan(s.upper))) - end - @assert !MOI.is_valid(lmo.o, cidx) - end - # adding binary bounds explicitly - for idx in binary_variables - cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(idx) - if !MOI.is_valid(lmo.o, cidx) - MOI.add_constraint(lmo.o, MOI.VariableIndex(idx), MOI.LessThan(1.0)) - end - @assert MOI.is_valid(lmo.o, cidx) - cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(idx) - if !MOI.is_valid(lmo.o, cidx) - MOI.add_constraint(lmo.o, MOI.VariableIndex(idx), MOI.GreaterThan(0.0)) - end - global_bounds[idx, :greaterthan] = MOI.GreaterThan(0.0) - global_bounds[idx, :lessthan] = MOI.LessThan(1.0) - end + global_bounds = build_global_bounds(blmo, integer_variables) + explicit_bounds_binary_var(blmo, global_bounds, binary_variables) v = [] if active_set === nothing direction = collect(1.0:n) - v = compute_extreme_point(lmo, direction) + v = compute_extreme_point(blmo, direction) v[integer_variables] = round.(v[integer_variables]) active_set = FrankWolfe.ActiveSet([(1.0, v)]) vertex_storage = FrankWolfe.DeletedVertexStorage(typeof(v)[], 1) else @assert FrankWolfe.active_set_validate(active_set) for a in active_set.atoms - @assert is_linear_feasible(lmo.o, a) + @assert is_linear_feasible(blmo.o, a) end v = active_set.atoms[1] end @@ -255,7 +285,7 @@ function solve( error("size of starting solution differs from vertices: $(size(start_solution)), $(size(v))") end # Sanity check that the provided solution is in fact feasible. - @assert is_linear_feasible(lmo, start_solution) && is_integer_feasible(tree, start_solution) + @assert is_linear_feasible(blmo, start_solution) && is_integer_feasible(tree, start_solution) node = tree.nodes[1] sol = FrankWolfeSolution(f(start_solution), start_solution, node, :start) push!(tree.solutions, sol) @@ -321,7 +351,7 @@ function solve( # Check solution and polish x_polished = x if x !== nothing - if !is_linear_feasible(tree.root.problem.lmo, x) + if !is_linear_feasible(tree.root.problem.tlmo, x) error("Reported solution not linear feasbile!") end if !is_integer_feasible(tree.root.problem.integer_variables, x, atol=1e-16, rtol=1e-16) && x !== nothing @@ -329,7 +359,7 @@ function solve( for i in tree.root.problem.integer_variables x_polished[i] = round(x_polished[i]) end - if !is_linear_feasible(tree.root.problem.lmo, x_polished) + if !is_linear_feasible(tree.root.problem.tlmo, x_polished) @warn "Polished solution not linear feasible" else x = x_polished @@ -338,7 +368,7 @@ function solve( end println() # cleaner output - return x, tree.root.problem.lmo, result + return x, tree.root.problem.tlmo, result end """ @@ -448,19 +478,19 @@ function build_bnb_callback( else 0 end - if !isempty(tree.root.problem.lmo.optimizing_times) - LMO_time = sum(1000 * tree.root.problem.lmo.optimizing_times) - empty!(tree.root.problem.lmo.optimizing_times) + if !isempty(tree.root.problem.tlmo.optimizing_times) + LMO_time = sum(1000 * tree.root.problem.tlmo.optimizing_times) + empty!(tree.root.problem.tlmo.optimizing_times) else LMO_time = 0 end - LMO_calls_c = tree.root.problem.lmo.ncalls + LMO_calls_c = tree.root.problem.tlmo.ncalls push!(list_lmo_calls_cb, copy(LMO_calls_c)) if !isempty(tree.node_queue) p_lb = tree.lb tree.lb = min(minimum([prio[2][1] for prio in tree.node_queue]), tree.incumbent) - @assert p_lb <= tree.lb + @assert p_lb <= tree.lb + tree.root.options[:dual_gap] end # correct lower bound if necessary tree.lb = tree_lb(tree) @@ -498,7 +528,7 @@ function build_bnb_callback( tree.num_nodes / time * 1000.0, fw_time, LMO_time, - tree.root.problem.lmo.ncalls, + tree.root.problem.tlmo.ncalls, fw_iter, active_set_size, discarded_set_size, @@ -557,7 +587,7 @@ function build_bnb_callback( end result[:number_nodes] = tree.num_nodes - result[:lmo_calls] = tree.root.problem.lmo.ncalls + result[:lmo_calls] = tree.root.problem.tlmo.ncalls result[:list_num_nodes] = list_num_nodes_cb result[:list_lmo_calls_acc] = list_lmo_calls_cb result[:list_active_set_size] = list_active_set_size_cb @@ -612,23 +642,23 @@ function postsolve(tree, result, time_ref, verbose, max_iteration_post) push!(fix_bounds, (i => MOI.GreaterThan(round(x[i])))) end - MOI.set(tree.root.problem.lmo.lmo.o, MOI.Silent(), true) - free_model(tree.root.problem.lmo.lmo.o) + MOI.set(tree.root.problem.tlmo.blmo.o, MOI.Silent(), true) + free_model(tree.root.problem.tlmo.blmo.o) build_LMO( - tree.root.problem.lmo, + tree.root.problem.tlmo, tree.root.problem.integer_variable_bounds, fix_bounds, tree.root.problem.integer_variables, ) # Postprocessing direction = ones(length(x)) - v = compute_extreme_point(tree.root.problem.lmo, direction) + v = compute_extreme_point(tree.root.problem.tlmo, direction) active_set = FrankWolfe.ActiveSet([(1.0, v)]) verbose && println("Postprocessing") x, _, primal, dual_gap, _, _ = FrankWolfe.blended_pairwise_conditional_gradient( tree.root.problem.f, tree.root.problem.g, - tree.root.problem.lmo, + tree.root.problem.tlmo, active_set, line_search=FrankWolfe.Adaptive(verbose=false), lazy=true, @@ -676,11 +706,11 @@ function postsolve(tree, result, time_ref, verbose, max_iteration_post) println("\t Dual Gap (relative): $(relative_gap(primal,tree_lb(tree)))\n") println("Search Statistics.") println("\t Total number of nodes processed: ", tree.num_nodes) - println("\t Total number of lmo calls: ", tree.root.problem.lmo.ncalls) + println("\t Total number of lmo calls: ", tree.root.problem.tlmo.ncalls) println("\t Total time (s): ", total_time_in_sec) - println("\t LMO calls / sec: ", tree.root.problem.lmo.ncalls / total_time_in_sec) + println("\t LMO calls / sec: ", tree.root.problem.tlmo.ncalls / total_time_in_sec) println("\t Nodes / sec: ", tree.num_nodes / total_time_in_sec) - println("\t LMO calls / node: $(tree.root.problem.lmo.ncalls / tree.num_nodes)\n") + println("\t LMO calls / node: $(tree.root.problem.tlmo.ncalls / tree.num_nodes)\n") if tree.root.options[:global_dual_tightening] println("\t Total number of global tightenings: ", sum(result[:global_tightenings])) println("\t Global tightenings / node: ", round(sum(result[:global_tightenings])/length(result[:global_tightenings]), digits=2)) @@ -695,7 +725,7 @@ function postsolve(tree, result, time_ref, verbose, max_iteration_post) # Reset LMO int_bounds = IntegerBounds() build_LMO( - tree.root.problem.lmo, + tree.root.problem.tlmo, tree.root.problem.integer_variable_bounds, int_bounds, tree.root.problem.integer_variables, @@ -704,11 +734,3 @@ function postsolve(tree, result, time_ref, verbose, max_iteration_post) return x end -# cleanup internal SCIP model -function free_model(o::SCIP.Optimizer) - SCIP.SCIPfreeTransform(o) -end - -# no-op by default -function free_model(o::MOI.AbstractOptimizer) -end diff --git a/src/node.jl b/src/node.jl index dce74d7a5..a9ed2fc12 100644 --- a/src/node.jl +++ b/src/node.jl @@ -184,9 +184,14 @@ function prune_children(tree, node, lower_bound_base, x, vidx) @debug "prune right, from $(node.lb) -> $new_bound_right, ub $(tree.incumbent), lb $(node.lb)" prune_right = true end + @assert !((new_bound_left > tree.incumbent + tree.root.options[:dual_gap]) && (new_bound_right > tree.incumbent + tree.root.options[:dual_gap])) "both sides should not be pruned" end - @assert !(prune_left && prune_right) "both sides should not be pruned" + # If both nodes are pruned, when one of them has to be equal to the incumbent. + # Thus, we have proof of optimality by strong convexity. + if prune_left && prune_right + tree.lb = min(new_bound_left, new_bound_right) + end return prune_left, prune_right end @@ -219,14 +224,14 @@ function Bonobo.evaluate_node!(tree::Bonobo.BnBTree, node::FrankWolfeNode) # build up node LMO build_LMO( - tree.root.problem.lmo, + tree.root.problem.tlmo, tree.root.problem.integer_variable_bounds, node.local_bounds, tree.root.problem.integer_variables, ) # check for feasibility and boundedness - status = check_feasibility(tree.root.problem.lmo) + status = check_feasibility(tree.root.problem.tlmo) if status == MOI.INFEASIBLE return NaN, NaN end @@ -235,19 +240,12 @@ function Bonobo.evaluate_node!(tree::Bonobo.BnBTree, node::FrankWolfeNode) return NaN, NaN end - # set relative accurary for the IP solver - # accurary = node.level >= 2 ? 0.1 / (floor(node.level / 2) * (3 / 4)) : 0.10 - # if MOI.get(tree.root.problem.lmo.lmo.o, MOI.SolverName()) == "SCIP" - # MOI.set(tree.root.problem.lmo.lmo.o, MOI.RawOptimizerAttribute("limits/gap"), accurary) - # end - - # Check feasibility of the iterate active_set = node.active_set x = FrankWolfe.compute_active_set_iterate!(node.active_set) - @assert is_linear_feasible(tree.root.problem.lmo, x) + @assert is_linear_feasible(tree.root.problem.tlmo, x) for (_,v) in node.active_set - @assert is_linear_feasible(tree.root.problem.lmo, v) + @assert is_linear_feasible(tree.root.problem.tlmo, v) end # time tracking FW @@ -258,7 +256,7 @@ function Bonobo.evaluate_node!(tree::Bonobo.BnBTree, node::FrankWolfeNode) tree.root.options[:variant], tree.root.problem.f, tree.root.problem.g, - tree.root.problem.lmo, + tree.root.problem.tlmo, node.active_set; epsilon=node.fw_dual_gap_limit, max_iteration=tree.root.options[:max_fw_iter], diff --git a/src/problem.jl b/src/problem.jl index 8b255e7a7..2a706ce7c 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -21,14 +21,14 @@ s.t. x ∈ X (given by the LMO) mutable struct SimpleOptimizationProblem{ F, G, - LMO<:FrankWolfe.LinearMinimizationOracle, + TLMO<:TimeTrackingLMO, IB<:IntegerBounds, } <: AbstractSimpleOptimizationProblem f::F g::G nvars::Int integer_variables::Vector{Int64} - lmo::LMO + tlmo::TLMO integer_variable_bounds::IB solving_stage::Solve_Stage #constraints_lessthan::Vector{Tuple{MOI.ScalarAffineFunction{T}, MOI.LessThan{T}}} @@ -36,34 +36,13 @@ mutable struct SimpleOptimizationProblem{ #constraints_equalto::Vector{Tuple{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}} end -SimpleOptimizationProblem(f, g, n, int_vars, lmo, int_bounds) = - SimpleOptimizationProblem(f, g, n, int_vars, lmo, int_bounds, SOLVING) - -mutable struct SimpleOptimizationProblemInfeasible{ - F, - G, - AT<:FrankWolfe.ActiveSet, - DVS<:FrankWolfe.DeletedVertexStorage, - LMO<:FrankWolfe.LinearMinimizationOracle, - IB<:IntegerBounds, -} <: AbstractSimpleOptimizationProblem - f::F - g::G - nvars::Int - integer_variables::Vector{Int64} - lmo::LMO - integer_variable_bounds::IB - active_set::AT - discarded_verices::DVS - #constraints_lessthan::Vector{Tuple{MOI.ScalarAffineFunction{T}, MOI.LessThan{T}}} - #constraints_greaterthan::Vector{Tuple{MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}}} - #constraints_equalto::Vector{Tuple{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}} -end +SimpleOptimizationProblem(f, g, n, int_vars, tlmo, int_bounds) = + SimpleOptimizationProblem(f, g, n, int_vars, tlmo, int_bounds, SOLVING) """ Returns the indices of the discrete variables for the branching in `Bonobo.BnBTree` """ -function Bonobo.get_branching_indices(problem) +function Bonobo.get_branching_indices(problem::SimpleOptimizationProblem) return problem.integer_variables end @@ -87,7 +66,7 @@ function is_integer_feasible( end function is_integer_feasible(tree::Bonobo.BnBTree, x::AbstractVector) - indicator_feasible = indicator_present(tree) ? is_indicator_feasible(tree.root.problem.lmo.lmo.o, x) : true + indicator_feasible = indicator_present(tree) ? is_indicator_feasible(tree.root.problem.tlmo.blmo.o, x) : true return is_integer_feasible( tree.root.problem.integer_variables, x; @@ -96,113 +75,13 @@ function is_integer_feasible(tree::Bonobo.BnBTree, x::AbstractVector) ) && indicator_feasible end -""" -Check if indicator constraints are being met -""" -function is_indicator_feasible(o, x, atol = 1e-6, rtol=1e-6) - valvar(f) = x[f.value] - for (F, S) in MOI.get(o, MOI.ListOfConstraintTypesPresent()) - if S <: MOI.Indicator - cons_list = MOI.get(o, MOI.ListOfConstraintIndices{F,S}()) - for c_idx in cons_list - func = MOI.get(o, MOI.ConstraintFunction(), c_idx) - val = MOIU.eval_variables(valvar, func) - set = MOI.get(o, MOI.ConstraintSet(), c_idx) - # @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") - dist = MOD.distance_to_set(MOD.DefaultDistance(), val, set) - if dist > atol - @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") - @debug("Distance to set: $(dist)") - return false - end - end - end - end - return true -end - -is_indicator_feasible(lmo::TimeTrackingLMO, v::AbstractVector) = is_indicator_feasible(lmo.lmo.o, v) -is_indicator_feasible(lmo::FrankWolfe.LinearMinimizationOracle, v::AbstractVector) = - is_indicator_feasible(lmo.o, v) - - -""" -Return the underlying optimizer -For better access and readability -""" -function get_optimizer(tree::Bonobo.BnBTree) - return tree.root.problem.lmo.lmo.o -end - - """ Checks if x is valid for all linear and variable bound constraints """ -function is_linear_feasible(o::MOI.ModelLike, v::AbstractVector) - valvar(f) = v[f.value] - for (F, S) in MOI.get(o, MOI.ListOfConstraintTypesPresent()) - isfeasible = is_linear_feasible_subroutine(o, F, S, valvar) - if !isfeasible - return false - end - end - # satisfies all constraints - return true -end - -# function barrier for performance -function is_linear_feasible_subroutine(o::MOI.ModelLike, ::Type{F}, ::Type{S}, valvar) where {F,S} - if S == MOI.ZeroOne || S <: MOI.Indicator || S == MOI.Integer - return true - end - cons_list = MOI.get(o, MOI.ListOfConstraintIndices{F,S}()) - for c_idx in cons_list - func = MOI.get(o, MOI.ConstraintFunction(), c_idx) - val = MOIU.eval_variables(valvar, func) - set = MOI.get(o, MOI.ConstraintSet(), c_idx) - # @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") - dist = MOD.distance_to_set(MOD.DefaultDistance(), val, set) - scip_tol = 1e-6 - if o isa SCIP.Optimizer - scip_tol = MOI.get(o, MOI.RawOptimizerAttribute("numerics/feastol")) - end - if dist > 100.0 * scip_tol - @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") - @debug("Distance to set: $(dist)") - @debug("SCIP tolerance: $(scip_tol)") - @debug("Loosened tolerance: $(100.0 * scip_tol)") - return false - end - end - return true -end - -function get_tol(o::SCIP.Optimizer) - return MOI.get(o, MOI.RawOptimizerAttribute("numerics/feastol")) -end - -function get_tol(o::MOI.AbstractOptimizer) - return 1e-06 -end - -is_linear_feasible(lmo::TimeTrackingLMO, v::AbstractVector) = is_linear_feasible(lmo.lmo.o, v) -is_linear_feasible(lmo::FrankWolfe.LinearMinimizationOracle, v::AbstractVector) = - is_linear_feasible(lmo.o, v) - +is_linear_feasible(lmo::TimeTrackingLMO, v::AbstractVector) = is_linear_feasible(lmo.blmo, v) """ Are indicator constraints present """ -function indicator_present(o) - for (_, S) in MOI.get(o, MOI.ListOfConstraintTypesPresent()) - if S <: MOI.Indicator - return true - end - end - return false -end - -indicator_present(time_lmo::TimeTrackingLMO) = indicator_present(time_lmo.lmo.o) -indicator_present(lmo::FrankWolfe.LinearMinimizationOracle) = indicator_present(lmo.o) -indicator_present(tree::Bonobo.BnBTree) = indicator_present(tree.root.problem.lmo.lmo.o) - +indicator_present(time_lmo::TimeTrackingLMO) = indicator_present(time_lmo.blmo) +indicator_present(tree::Bonobo.BnBTree) = indicator_present(tree.root.problem.tlmo.blmo) diff --git a/src/strong_branching.jl b/src/strong_branching.jl index a48d27908..d79075c2d 100644 --- a/src/strong_branching.jl +++ b/src/strong_branching.jl @@ -1,42 +1,29 @@ -struct PartialStrongBranching{O} <: Bonobo.AbstractBranchStrategy +struct PartialStrongBranching{BLMO<:BoundedLinearMinimizationOracle} <: Bonobo.AbstractBranchStrategy max_iteration::Int solving_epsilon::Float64 - optimizer::O + bounded_lmo::BLMO end +""" +Get branching variable using strong branching. +Create all possible subproblems, solve them and pick the one with the most progress. +""" function Bonobo.get_branching_variable( tree::Bonobo.BnBTree, - branching::PartialStrongBranching, + branching::PartialStrongBranching{BoundedLinearMinimizationOracle}, node::Bonobo.AbstractNode, ) xrel = Bonobo.get_relaxed_values(tree, node) max_lowerbound = -Inf max_idx = -1 - # copy problem and remove integer constraints - filtered_src = MOI.Utilities.ModelFilter(tree.root.problem.lmo.lmo.o) do item - if item isa Tuple - (_, S) = item - if S <: Union{MOI.Indicator,MOI.Integer,MOI.ZeroOne} - return false - end - end - return !(item isa MOI.ConstraintIndex{<:Any,<:Union{MOI.ZeroOne,MOI.Integer,MOI.Indicator}}) - end - index_map = MOI.copy_to(branching.optimizer, filtered_src) - # sanity check, otherwise the functions need permuted indices - for (v1, v2) in index_map - if v1 isa MOI.VariableIndex - @assert v1 == v2 - end - end - relaxed_lmo = FrankWolfe.MathOptLMO(branching.optimizer) @assert !isempty(node.active_set) active_set = copy(node.active_set) empty!(active_set) num_frac = 0 for idx in Bonobo.get_branching_indices(tree.root) if !isapprox(xrel[idx], round(xrel[idx]), atol=tree.options.atol, rtol=tree.options.rtol) + # left node: x_i <= floor(̂x_i) fxi = floor(xrel[idx]) # create LMO @@ -46,14 +33,13 @@ function Bonobo.get_branching_variable( end push!(boundsLeft.upper_bounds, (idx => MOI.LessThan(fxi))) build_LMO( - relaxed_lmo, + branching.bounded_lmo, tree.root.problem.integer_variable_bounds, boundsLeft, Bonobo.get_branching_indices(tree.root), ) - MOI.optimize!(relaxed_lmo.o) - #MOI.set(relaxed_lmo.o, MOI.Silent(), false) - if MOI.get(relaxed_lmo.o, MOI.TerminationStatus()) == MOI.OPTIMAL + status = check_feasibility(branching.bounded_lmo) + if status == MOI.OPTIMAL empty!(active_set) for (λ, v) in node.active_set if v[idx] <= xrel[idx] @@ -66,7 +52,7 @@ function Bonobo.get_branching_variable( FrankWolfe.blended_pairwise_conditional_gradient( tree.root.problem.f, tree.root.problem.g, - relaxed_lmo, + branching.bounded_lmo, active_set, verbose=false, epsilon=branching.solving_epsilon, @@ -74,9 +60,10 @@ function Bonobo.get_branching_variable( ) left_relaxed = primal_relaxed - dual_gap_relaxed else - @debug "Left non-optimal status $(MOI.get(relaxed_lmo.o, MOI.TerminationStatus()))" + @debug "Left non-optimal status $(status)" left_relaxed = Inf end + #right node: x_i >= floor(̂x_i) cxi = ceil(xrel[idx]) boundsRight = copy(node.local_bounds) @@ -85,13 +72,13 @@ function Bonobo.get_branching_variable( end push!(boundsRight.lower_bounds, (idx => MOI.GreaterThan(cxi))) build_LMO( - relaxed_lmo, + branching.bounded_lmo, tree.root.problem.integer_variable_bounds, boundsRight, Bonobo.get_branching_indices(tree.root), ) - MOI.optimize!(relaxed_lmo.o) - if MOI.get(relaxed_lmo.o, MOI.TerminationStatus()) == MOI.OPTIMAL + status = check_feasibility(branching.bounded_lmo) + if status == MOI.OPTIMAL empty!(active_set) for (λ, v) in node.active_set if v[idx] >= xrel[idx] @@ -109,7 +96,7 @@ function Bonobo.get_branching_variable( FrankWolfe.blended_pairwise_conditional_gradient( tree.root.problem.f, tree.root.problem.g, - relaxed_lmo, + branching.bounded_lmo, active_set, verbose=false, epsilon=branching.solving_epsilon, @@ -117,7 +104,7 @@ function Bonobo.get_branching_variable( ) right_relaxed = primal_relaxed - dual_gap_relaxed else - @debug "Right non-optimal status $(MOI.get(relaxed_lmo.o, MOI.TerminationStatus()))" + @debug "Right non-optimal status $(status)" right_relaxed = Inf end # lowest lower bound on the two branches @@ -144,9 +131,9 @@ end Hybrid between partial strong branching and another strategy. `perform_strong_branch(tree, node) -> Bool` decides whether to perform strong branching or not. """ -struct HybridStrongBranching{O,F<:Function,B<:Bonobo.AbstractBranchStrategy} <: +struct HybridStrongBranching{BLMO<:BoundedLinearMinimizationOracle,F<:Function,B<:Bonobo.AbstractBranchStrategy} <: Bonobo.AbstractBranchStrategy - pstrong::PartialStrongBranching{O} + pstrong::PartialStrongBranching{BLMO} perform_strong_branch::F alternative_branching::B end @@ -154,12 +141,12 @@ end function HybridStrongBranching( max_iteration::Int, solving_epsilon::Float64, - optimizer::O, + bounded_lmo::BoundedLinearMinimizationOracle, perform_strong_branch::Function, alternative=Bonobo.MOST_INFEASIBLE(), -) where {O} +) return HybridStrongBranching( - PartialStrongBranching(max_iteration, solving_epsilon, optimizer), + PartialStrongBranching(max_iteration, solving_epsilon, bounded_lmo), perform_strong_branch, alternative, ) @@ -184,13 +171,13 @@ strong_up_to_depth performs strong branching on nodes up to a predetermined dept function strong_up_to_depth( max_iteration::Int, solving_epsilon::Float64, - optimizer::O, + bounded_lmo::BoundedLinearMinimizationOracle, max_depth::Int, alternative=Bonobo.MOST_INFEASIBLE(), -) where {O} +) perform_strong_while_depth(_, node) = node.level <= max_depth return HybridStrongBranching( - PartialStrongBranching(max_iteration, solving_epsilon, optimizer), + PartialStrongBranching(max_iteration, solving_epsilon, bounded_lmo), perform_strong_while_depth, alternative, ) diff --git a/src/time_tracking_lmo.jl b/src/time_tracking_lmo.jl index 43457da5c..4e075e278 100644 --- a/src/time_tracking_lmo.jl +++ b/src/time_tracking_lmo.jl @@ -4,9 +4,9 @@ An LMO wrapping another one tracking the time, number of nodes and number of calls. """ -mutable struct TimeTrackingLMO{LMO<:FrankWolfe.LinearMinimizationOracle} <: +mutable struct TimeTrackingLMO{BLMO<:BoundedLinearMinimizationOracle} <: FrankWolfe.LinearMinimizationOracle - lmo::LMO + blmo::BLMO optimizing_times::Vector{Float64} optimizing_nodes::Vector{Int} simplex_iterations::Vector{Int} @@ -14,42 +14,37 @@ mutable struct TimeTrackingLMO{LMO<:FrankWolfe.LinearMinimizationOracle} <: int_vars::Vector{Int} end -TimeTrackingLMO(lmo::FrankWolfe.LinearMinimizationOracle) = - TimeTrackingLMO(lmo, Float64[], Int[], Int[], 0, Int[]) +TimeTrackingLMO(blmo::BoundedLinearMinimizationOracle) = + TimeTrackingLMO(blmo, Float64[], Int[], Int[], 0, Int[]) -TimeTrackingLMO(lmo::FrankWolfe.LinearMinimizationOracle, int_vars) = - TimeTrackingLMO(lmo, Float64[], Int[], Int[], 0, int_vars) +TimeTrackingLMO(blmo::BoundedLinearMinimizationOracle, int_vars) = + TimeTrackingLMO(blmo, Float64[], Int[], Int[], 0, int_vars) # if we want to reset the info between nodes in Bonobo -function reset!(lmo::TimeTrackingLMO) - empty!(lmo.optimizing_times) - empty!(lmo.optimizing_nodes) - empty!(lmo.simplex_iterations) - return lmo.ncalls = 0 +function reset!(tlmo::TimeTrackingLMO) + empty!(tlmo.optimizing_times) + empty!(tlmo.optimizing_nodes) + empty!(tlmo.simplex_iterations) + return tlmo.ncalls = 0 end -function FrankWolfe.compute_extreme_point(lmo::TimeTrackingLMO, d; kwargs...) - lmo.ncalls += 1 - cleanup_solver(lmo.lmo.o) - v = FrankWolfe.compute_extreme_point(lmo.lmo, d; kwargs) +function FrankWolfe.compute_extreme_point(tlmo::TimeTrackingLMO, d; kwargs...) + tlmo.ncalls += 1 + free_model(tlmo.blmo) + v = FrankWolfe.compute_extreme_point(tlmo.blmo, d; kwargs) - @debug begin - if !is_linear_feasible(lmo, v) - @debug "Vertex not linear feasible $(v)" - end + if !is_linear_feasible(tlmo, v) + @debug "Vertex not linear feasible $(v)" + @assert is_linear_feasible(tlmo, v) end - v[lmo.int_vars] = round.(v[lmo.int_vars]) + v[tlmo.int_vars] = round.(v[tlmo.int_vars]) - push!(lmo.optimizing_times, MOI.get(lmo.lmo.o, MOI.SolveTimeSec())) - numberofnodes = MOI.get(lmo.lmo.o, MOI.NodeCount()) - push!(lmo.optimizing_nodes, numberofnodes) - push!(lmo.simplex_iterations, MOI.get(lmo.lmo.o, MOI.SimplexIterations())) + opt_times, numberofnodes, simplex_iterations = get_BLMO_solve_data(tlmo.blmo) - cleanup_solver(lmo.lmo.o) + push!(tlmo.optimizing_times, opt_times) + push!(tlmo.optimizing_nodes, numberofnodes) + push!(tlmo.simplex_iterations, simplex_iterations) + + free_model(tlmo.blmo) return v end - -cleanup_solver(o) = nothing -cleanup_solver(o::SCIP.Optimizer) = SCIP.SCIPfreeTransform(o) - -MOI.optimize!(time_lmo::TimeTrackingLMO) = MOI.optimize!(time_lmo.lmo.o) diff --git a/src/utilities.jl b/src/utilities.jl index a63f0ed8d..b5263d46e 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -16,73 +16,28 @@ end """ Check feasibility and boundedness """ -function check_feasibility(lmo::TimeTrackingLMO) - MOI.set( - lmo.lmo.o, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction{Float64}([], 0.0), - ) - MOI.optimize!(lmo) - status = MOI.get(lmo.lmo.o, MOI.TerminationStatus()) - return status +function check_feasibility(tlmo::TimeTrackingLMO) + return check_feasibility(tlmo.blmo) end """ Check if at a given index we have a binary and integer constraint respectivily. """ -function is_binary_constraint(tree::Bonobo.BnBTree, idx::Int) - consB_list = MOI.get( - tree.root.problem.lmo.lmo.o, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}(), - ) - for c_idx in consB_list - if c_idx.value == idx - return true, c_idx - end - end - return false, -1 +function has_binary_constraint(tree::Bonobo.BnBTree, idx::Int) + return has_binary_constraint(tree.root.problem.tlmo.blmo, idx) end -function is_integer_constraint(tree::Bonobo.BnBTree, idx::Int) - consB_list = MOI.get( - tree.root.problem.lmo.lmo.o, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}(), - ) - for c_idx in consB_list - if c_idx.value == idx - return true, c_idx - end - end - return false, -1 +function has_integer_constraint(tree::Bonobo.BnBTree, idx::Int) + return has_integer_constraint(tree.root.problem.tlmo.blmo, idx) end """ -Check wether a split is valid. It is +Check wether a split is valid. """ function is_valid_split(tree::Bonobo.BnBTree, vidx::Int) - bin_var, _ = is_binary_constraint(tree, vidx) - int_var, _ = is_integer_constraint(tree, vidx) - if int_var || bin_var - l_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(vidx) - u_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(vidx) - l_bound = - MOI.is_valid(get_optimizer(tree), l_idx) ? - MOI.get(get_optimizer(tree), MOI.ConstraintSet(), l_idx) : nothing - u_bound = - MOI.is_valid(get_optimizer(tree), u_idx) ? - MOI.get(get_optimizer(tree), MOI.ConstraintSet(), u_idx) : nothing - if (l_bound !== nothing && u_bound !== nothing && l_bound.lower === u_bound.upper) - @debug l_bound.lower, u_bound.upper - return false - else - return true - end - else #!bin_var && !int_var - @debug "No binary or integer constraint here." - return true - end + return is_valid_split(tree, tree.root.problem.tlmo.blmo, vidx) end diff --git a/test/LMO_test.jl b/test/LMO_test.jl index 6a714a3ca..185a4b0fb 100644 --- a/test/LMO_test.jl +++ b/test/LMO_test.jl @@ -36,7 +36,7 @@ const MOD = MathOptSetDistances MOI.add_constraint(o, xi, MOI.LessThan(5.0)) end end - lmo = FrankWolfe.MathOptLMO(o) + lmo = Boscia.MathOptBLMO(o) global_bounds = Boscia.IntegerBounds() @test isempty(global_bounds) diff --git a/test/indicator_test.jl b/test/indicator_test.jl index 435266ab5..b207bfe80 100644 --- a/test/indicator_test.jl +++ b/test/indicator_test.jl @@ -26,9 +26,9 @@ const MOIU = MOI.Utilities MOI.add_constraint(o, z[i], MOI.LessThan(1.0)) MOI.add_constraint(o, z[i], MOI.ZeroOne()) end - lmo = FrankWolfe.MathOptLMO(o) + blmo = Boscia.MathOptBLMO(o) - @test Boscia.indicator_present(lmo.o) == false + @test Boscia.indicator_present(blmo) == false for i in 1:n gl = MOI.VectorAffineFunction( @@ -42,7 +42,7 @@ const MOIU = MOI.Utilities MOI.add_constraint(o, gl, MOI.Indicator{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(0.0))) MOI.add_constraint(o, gg, MOI.Indicator{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(0.0))) end - @test Boscia.indicator_present(lmo.o) == true + @test Boscia.indicator_present(blmo) == true function ind_rounding(x) round.(x[n+1:2n]) @@ -56,8 +56,8 @@ const MOIU = MOI.Utilities x = [0.5, 1.0, 0.75, 0.0, 0.9, 1.0, 1.0, 1.0, 0.0, 0.0] y = [0.0, 0.0, 0.5, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0] - @test Boscia.is_indicator_feasible(o, x) == false - @test Boscia.is_indicator_feasible(o, y) == true + @test Boscia.is_indicator_feasible(blmo, x) == false + @test Boscia.is_indicator_feasible(blmo, y) == true ind_rounding(x) - @test Boscia.is_indicator_feasible(o, x) == true + @test Boscia.is_indicator_feasible(blmo, x) == true end diff --git a/test/interface_test.jl b/test/interface_test.jl index b2c3b275b..1375f1e8f 100644 --- a/test/interface_test.jl +++ b/test/interface_test.jl @@ -63,8 +63,9 @@ end @. storage = x - diffi end - branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, HiGHS.Optimizer()) - MOI.set(branching_strategy.optimizer, MOI.Silent(), true) + blmo = Boscia.MathOptBLMO(HiGHS.Optimizer()) + branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, blmo) + MOI.set(branching_strategy.bounded_lmo.o, MOI.Silent(), true) x, _, result = Boscia.solve(f, grad!, lmo, verbose=false, branching_strategy = branching_strategy) diff --git a/test/poisson.jl b/test/poisson.jl index 9c1bd7422..ac80c79cb 100644 --- a/test/poisson.jl +++ b/test/poisson.jl @@ -154,8 +154,9 @@ end return storage end - branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, HiGHS.Optimizer()) - MOI.set(branching_strategy.optimizer, MOI.Silent(), true) + blmo = Boscia.MathOptBLMO(HiGHS.Optimizer()) + branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, blmo) + MOI.set(branching_strategy.bounded_lmo.o, MOI.Silent(), true) x, _, result = Boscia.solve(f, grad!, lmo, verbose = true, branching_strategy = branching_strategy) @test sum(x[p+1:2p]) <= k @@ -329,8 +330,9 @@ end return storage end - branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, HiGHS.Optimizer()) - MOI.set(branching_strategy.optimizer, MOI.Silent(), true) + blmo = Boscia.MathOptBLMO(HiGHS.Optimizer()) + branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, blmo) + MOI.set(branching_strategy.bounded_lmo.o, MOI.Silent(), true) x, _, result = Boscia.solve(f, grad!, lmo, verbose=true, branching_strategy=branching_strategy) diff --git a/test/runtests.jl b/test/runtests.jl index df0d92e79..2f4144d87 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -78,8 +78,9 @@ const diff1 = rand(Bool, n) * 0.8 .+ 1.1 ) lmo = FrankWolfe.MathOptLMO(o) - branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, HiGHS.Optimizer()) - MOI.set(branching_strategy.optimizer, MOI.Silent(), true) + blmo = Boscia.MathOptBLMO(HiGHS.Optimizer()) + branching_strategy = Boscia.PartialStrongBranching(10, 1e-3, blmo) + MOI.set(branching_strategy.bounded_lmo.o, MOI.Silent(), true) x, _, result_strong_branching = Boscia.solve(f, grad!, lmo, verbose=true, branching_strategy=branching_strategy) @@ -122,9 +123,9 @@ end function perform_strong_branch(tree, node) return node.level <= length(tree.root.problem.integer_variables) / 3 end - branching_strategy = - Boscia.HybridStrongBranching(10, 1e-3, HiGHS.Optimizer(), perform_strong_branch) - MOI.set(branching_strategy.pstrong.optimizer, MOI.Silent(), true) + blmo = Boscia.MathOptBLMO(HiGHS.Optimizer()) + branching_strategy = Boscia.HybridStrongBranching(10, 1e-3, blmo, perform_strong_branch) + MOI.set(branching_strategy.pstrong.bounded_lmo.o, MOI.Silent(), true) x,_,result = Boscia.solve(f, grad!, lmo, verbose = true, branching_strategy=branching_strategy)