From 7a4bf490b6f7d6459fcbb8b27435433dd087e159 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 25 Jul 2023 12:11:30 +0200 Subject: [PATCH 01/81] Supertype for the Mixed Integer Linear Oracles. --- src/lmo_wrapper.jl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/lmo_wrapper.jl diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl new file mode 100644 index 000000000..c6fea650f --- /dev/null +++ b/src/lmo_wrapper.jl @@ -0,0 +1,24 @@ +""" + MILMO + +Supertype for the Mixed Integer Linear Minimization Oracles +""" +abstract type MixedIntegerLinearMinimizationOracle end + +""" +Multiple dispatch of FrankWolfe.compute_extreme_point +""" +function compute_extreme_point end + +""" +""" +function optimize! end + +function get_lower_bounds end + +function get_upper_bounds end + +function set_lower_bounds end + +function set_upper_bounds end + From 19dde3e1ddcfea226a4a7cf0edeb6ce1dcaed787 Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Fri, 13 Oct 2023 21:58:17 +0200 Subject: [PATCH 02/81] BLMO wrapper and functions that have to be implemented. --- src/lmo_wrapper.jl | 75 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index c6fea650f..82a05cadc 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -1,9 +1,9 @@ """ MILMO -Supertype for the Mixed Integer Linear Minimization Oracles +Supertype for the Bounded Integer Linear Minimization Oracles """ -abstract type MixedIntegerLinearMinimizationOracle end +abstract type BoundedLinearMinimizationOracle end """ Multiple dispatch of FrankWolfe.compute_extreme_point @@ -14,11 +14,74 @@ function compute_extreme_point end """ function optimize! end -function get_lower_bounds end +""" +Get list of lower bounds. +""" +function get_lower_bounds_list end -function get_upper_bounds end +""" +Get list of upper bounds. +""" +function get_upper_bounds_list end -function set_lower_bounds end +""" +Get lower bound of variable. +""" +function get_lower_bound end + +""" +Get upper bound of variable. +""" +function get_upper_bound end + +""" +Set new lower bound. +""" +function set_lower_bound! end + +""" +Set new upper bound. +""" +function set_upper_bound! end + +""" +Check if the bound constraint is on an integral variable. +""" +function is_constraint_on_int_var end + +""" +Is the constraint part of this contraints set. Used to check membership to the global and node bounds. +""" +function is_constraint_in end + +""" +Delete constraints that are not needed anymore. +""" +function delete_constraints! end + +""" +Add new bound constraint. +""" +function add_constraint! end + +""" +Get the variable of the bound constraint. +""" +function get_variable_index(blmo::BoundedLinearMinimizationOracle, c_idx) + return c_idx +end + +""" +Check that all node bounds were set correctly. Not necessary +""" +function check_bounds(blmo::BoundedLinearMinimizationOracle, node_bounds) + return true + end -function set_upper_bounds end + """ + Check that the problem is feasible, i.e. there are no contradicting constraints, and that it is bounded. + """ +function check_feasibility(blmo::BoundedLinearMinimizationOracle) + return +end From b00426d954be17087cfd7f36ea5932cf25dd5ed9 Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Fri, 13 Oct 2023 22:03:15 +0200 Subject: [PATCH 03/81] BLMO implemented for solvers supporting MathOptInterface. --- src/MOI_bounded_oracle.jl | 254 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 src/MOI_bounded_oracle.jl diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl new file mode 100644 index 000000000..dcf833a72 --- /dev/null +++ b/src/MOI_bounded_oracle.jl @@ -0,0 +1,254 @@ +""" + +The Bounded Linear Minimization Oracle for solvers supporting MathOptInterface. +""" +struct MathOptBLMO{OT<:MOI.AbstractOptimizer} <: BoundedLinearMinimizationOracle + o::OT + use_modify::Bool + function MathOptLMO(o, use_modify=true) + MOI.set(o, MOI.ObjectiveSense(), MOI.MIN_SENSE) + return new{typeof(o)}(o, use_modify) + end +end + +""" +Get list of lower bounds. +""" +function get_lower_bounds_list(blmo::MathOptBLMO) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{Float64}}()) +end + +""" +Get list of upper bounds. +""" +function get_upper_bounds_list(blmo::MathOptBLMO) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{Float64}}()) +end + +""" +Get lower bound of variable. +""" +function get_lower_bound(blmo, c_idx) + return MOI.get(blmo.o, MOI.ConstraintSet(), c_idx) +end + +""" +Get upper bound of variable. +""" +function get_upper_bound(blmo, c_idx) + return MOI.get(blmo.o, MOI.ConstraintSet(), c_idx) +end + +""" +Set new lower bound. +""" +function set_lower_bound!(blmo, c_idx, value) + MOI.set(blmo.o, MOI.ConstraintSet(), c_idx, value) +end + +""" +Set new upper bound. +""" +function set_upper_bound!(blmo, c_idx, value) + MOI.set(blmo.o, MOI.ConstraintSet(), c_idx, value) +end + +""" +Delete constraints that are not needed anymore. +""" +function delete_constraints!(blmo, cons_delete) + for d_idx in cons_delete + MOI.delete(blmo, d_idx) + end +end + +""" +Add new bound constraint. +""" +function add_constraint!(blmo, key, value) + MOI.add_constraint(blmo, MOI.VariableIndex(key), value) +end + +""" +Check if the bound constraint is on an integral variable. +""" +function is_constraint_on_int_var(c_idx, int_vars) + return c_idx.value in int_vars +end + +""" +Is the constraint part of this contraints set. Used to check membership to the global and node bounds. +""" +function is_constraint_in(set, c_idx) + return haskey(set, c_idx.value) +end + +""" +Function for MOI.optimize! +""" +MOI.optimize!(blmo::MathOptBLMO) = MOI.optimize!(blmo.o) + +""" +Get the variable of the bound constraint. +""" +function get_variable_index(blmo::MathOptBLMO, c_idx) + return c_idx.value +end + +""" +Check that all node bounds were set correctly. Not necessary +""" +function check_bounds(blmo::MathOptBLMO, 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 + + """ + Check that the problem is feasible, i.e. there are no contradicting constraints, and that it is bounded. + """ +function check_feasibility(blmo::BoundedLinearMinimizationOracle) + MOI.set( + blmo.o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction{Float64}([], 0.0), + ) + MOI.optimize!(blmo) + status = MOI.get(blmo.o, MOI.TerminationStatus()) + return status +end + + """ + Compute the extreme point given a direction. + """ + function compute_extreme_point( + blmo::MathOptBLMO{OT}, + direction::AbstractVector{T}; + kwargs..., +) where {OT,T<:Real} + variables = MOI.get(blmo.o, MOI.ListOfVariableIndices()) + if blmo.use_modify + for i in eachindex(variables) + MOI.modify( + blmo.o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), + MOI.ScalarCoefficientChange(variables[i], direction[i]), + ) + end + else + terms = [MOI.ScalarAffineTerm(d, v) for (d, v) in zip(direction, variables)] + obj = MOI.ScalarAffineFunction(terms, zero(T)) + MOI.set(blmo.o, MOI.ObjectiveFunction{typeof(obj)}(), obj) + end + return _optimize_and_return(blmo, variables) +end + +""" +Compute extreme point when the given direction is a matrix. +""" +function compute_extreme_point( + blmo::MathOptBLMO{OT}, + direction::AbstractMatrix{T}; + kwargs..., +) where {OT,T<:Real} + n = size(direction, 1) + v = compute_extreme_point(blmo, vec(direction)) + return reshape(v, n, n) +end + +""" +Copy BLMO. +""" +function Base.copy(blmo::MathOptBLMO{OT}; ensure_identity=true) where {OT} + opt = OT() # creates the empty optimizer + index_map = MOI.copy_to(opt, blmo.o) + if ensure_identity + for (src_idx, des_idx) in index_map.var_map + if src_idx != des_idx + error("Mapping of variables is not identity") + end + end + end + return MathOptBLMO(opt) +end + # is this necessary for us? +function Base.copy( + blmo::MathOptBLMO{OT}; + ensure_identity=true, +) where {OTI,OT<:MOIU.CachingOptimizer{OTI}} + opt = MOIU.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + OTI(), + ) + index_map = MOI.copy_to(opt, blmo.o) + if ensure_identity + for (src_idx, des_idx) in index_map.var_map + if src_idx != des_idx + error("Mapping of variables is not identity") + end + end + end + return MathOptBLMO(opt) +end + + +# Necessary? +function compute_extreme_point( + blmo::MathOptBLMO{OT}, + direction::AbstractVector{MOI.ScalarAffineTerm{T}}; + kwargs..., +) where {OT,T} + if blmo.use_modify + for d in direction + MOI.modify( + blmo.o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), + MOI.ScalarCoefficientChange(d.variable, d.coefficient), + ) + end + + variables = MOI.get(blmo.o, MOI.ListOfVariableIndices()) + variables_to_zero = setdiff(variables, [dir.variable for dir in direction]) + + terms = [ + MOI.ScalarAffineTerm(d, v) for + (d, v) in zip(zeros(length(variables_to_zero)), variables_to_zero) + ] + + for t in terms + MOI.modify( + blmo.o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), + MOI.ScalarCoefficientChange(t.variable, t.coefficient), + ) + end + else + variables = [d.variable for d in direction] + obj = MOI.ScalarAffineFunction(direction, zero(T)) + MOI.set(blmo.o, MOI.ObjectiveFunction{typeof(obj)}(), obj) + end + return _optimize_and_return(blmo, variables) +end + +""" +Optimize the given problem. +""" +function _optimize_and_return(blmo, variables) + MOI.optimize!(blmo.o) + term_st = MOI.get(blmo.o, MOI.TerminationStatus()) + if term_st ∉ (MOI.OPTIMAL, MOI.ALMOST_OPTIMAL, MOI.SLOW_PROGRESS) + @error "Unexpected termination: $term_st" + return MOI.get.(blmo.o, MOI.VariablePrimal(), variables) + end + return MOI.get.(blmo.o, MOI.VariablePrimal(), variables) +end \ No newline at end of file From 5f09349ca5a8b952cb582c7e361559c27cfa74bb Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Fri, 13 Oct 2023 22:03:42 +0200 Subject: [PATCH 04/81] Rework build_LMO for the BLMO wrapper. --- src/bounds.jl | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/bounds.jl b/src/bounds.jl index 733ad01b4..fc179cc33 100644 --- a/src/bounds.jl +++ b/src/bounds.jl @@ -80,6 +80,7 @@ end return -1 end =# +#= """ Build node LMO from global LMO @@ -181,4 +182,86 @@ function build_LMO( end build_LMO(lmo::TimeTrackingLMO, gb::IntegerBounds, nb::IntegerBounds, int_vars::Vector{Int64}) = - build_LMO(lmo.lmo, gb, nb, int_vars) + build_LMO(lmo.lmo, gb, nb, int_vars)=# + + + """ +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 data from previous nodes + free_model(lmo.o) + + consLB_list = get_lower_bounds_list(blmo) + consUB_list = get_upper_bounds_list(blmo) + cons_delete = [] + + # Lower bounds + for c_idx in consLB_list + v_idx = get_variable_index(blmo, c_idx) + if is_constraint_on_int_var(c_idx, int_vars) + if is_constraint_in(global_bounds.lower_bounds, c_idx) + # change + if is_constraint_in(node.lower_bounds, c_idx) + set_lower_bound(blmo, c_idx, node_bounds.lower_bounds[v_idx]) + # keep + else + set_lower_bound(blmo, c_idx, global_bounds.lower_bounds[v_idx]) + end + else + # delete + push!(cons_delete, c_idx) + end + end + end + + # Upper bounds + for c_idx in consUB_list + v_idx = get_variable_index(blmo, c_idx) + if is_constraint_on_int_var(c_idx, int_vars) + if is_constraint_in(global_bounds.uppers_bounds, c_idx) + # change + if is_constraint_in(node.uppers_bounds, c_idx) + set_upper_bound(blmo, c_idx, node_bounds.upper_bounds[v_idx]) + # keep + else + set_upper_bound(blmo, c_idx, global_bounds.upper_bounds[v_idx]) + end + else + # delete + push!(cons_delete, c_idx) + end + end + end + + # delete constraints + delete_constraints!(blmo, cons_delete) + + # add node specific constraints + for key in keys(node_bounds.lower_bounds) + if !is_constraint_in(global_bounds.lower_bounds, key) + add_constraint!(blmo, key, node_bounds.lower_bounds[key]) + end + end + for key in keys(node_bounds.upper_bounds) + if !is_constraint_in(global_bounds.upper_bounds, key) + add_constraint!(blmo, key, node_bounds.upper_bounds[key]) + end + end + + @assert check_bounds(lmo, node_bounds) +end + +build_LMO(tt_lmo::TimeTrackingLMO, gb::IntegerBounds, nb::IntegerBounds, int_vars::Vector{Int64}) = + build_LMO(tt_lmo.blmo, gb, nb, int_vars) From 59bb4c9e7f09da85e75321133485242de1cc82c6 Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Fri, 13 Oct 2023 22:04:01 +0200 Subject: [PATCH 05/81] Add the new files. --- src/Boscia.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Boscia.jl b/src/Boscia.jl index e73ee7b51..d27a7fee6 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -14,6 +14,8 @@ import MathOptSetDistances as MOD include("time_tracking_lmo.jl") include("bounds.jl") +include("MOI_bounded_oracle.jl") +include("lmo_wrapper.jl") include("frank_wolfe_variants.jl") include("node.jl") include("custom_bonobo.jl") From 6f2dbd7f0cbacd2742de2830ce040e2419e33a2f Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Fri, 13 Oct 2023 22:04:27 +0200 Subject: [PATCH 06/81] Started on adapting function for BLMO. --- src/utilities.jl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index a63f0ed8d..6632822ef 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -17,14 +17,7 @@ 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 + return check_feasibility(lmo.blmo) end From ccea3d998abf9d38416e296ddda3f6787fd7eb90 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 10:43:12 +0200 Subject: [PATCH 07/81] Bounded Linear Minimzation Oracle as subtype of Linear Minimization Oracle. --- src/lmo_wrapper.jl | 71 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index c6fea650f..f584438f9 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -1,24 +1,79 @@ """ - MILMO + BLMO -Supertype for the Mixed Integer Linear Minimization Oracles +Supertype for the Bounded Linear Minimization Oracles """ -abstract type MixedIntegerLinearMinimizationOracle end +abstract type BoundedLinearMinimizationOracle <: FrankWolfe.LinearMinimizationOracle end """ -Multiple dispatch of 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 """ +CHECK IF NECESSARY """ function optimize! end -function get_lower_bounds 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 -function get_upper_bounds end +""" +Change the value of the bound c_idx. +""" +function set_bound! end + +""" +Read bound value for c_idx. +""" +function get_bound end + +""" +Check if the subject of the bound c_idx is an integer variable (recorded in int_vars). +""" +function is_constraint_on_int_var end -function set_lower_bounds end +""" +To check if there is bound for the variable in the global or node bounds. +""" +function is_bound_in end + +""" +Delete bounds. +""" +function delete_bounds! end + +""" +Add bound constraint. +""" +function add_bound_constraint! end + +""" +Check if the bounds were set correctly in build_LMO. +Safety check only. +""" +function build_LMO_correct(blmo, node_bounds) + return true +end + +""" +Free model data from previous solve (if necessary). +""" +function free_model(blmo) + return true +end -function set_upper_bounds end From 8191716c1f0dc66a284e9b5edb42fd6f0c663729 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 10:43:35 +0200 Subject: [PATCH 08/81] Adapted build_LMO for BLMO. --- src/bounds.jl | 86 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/bounds.jl b/src/bounds.jl index 733ad01b4..3c6d9cdad 100644 --- a/src/bounds.jl +++ b/src/bounds.jl @@ -79,7 +79,7 @@ end end return -1 end =# - +#= """ Build node LMO from global LMO @@ -178,7 +178,89 @@ function build_LMO( end end -end +end build_LMO(lmo::TimeTrackingLMO, gb::IntegerBounds, nb::IntegerBounds, int_vars::Vector{Int64}) = build_LMO(lmo.lmo, gb, nb, int_vars) +=# + + """ +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.lower_bounds[v_idx]) + # keep + else + set_bound(blmo, c_idx, global_bounds.lower_bounds[v_idx]) + 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.upper_bounds[v_idx]) + # keep + else + set_bound(blmo, c_idx, global_bounds.upper_bounds[v_idx]) + 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) From cbc81661e65c518297b25d6c4e9046e6fbd88ece Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 10:43:55 +0200 Subject: [PATCH 09/81] BLMO for all solvers supporting MOI. --- src/MOI_bounded_oracle.jl | 118 ++++++++++++++++++++++++++++++++++++++ src/interface.jl | 8 --- 2 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 src/MOI_bounded_oracle.jl diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl new file mode 100644 index 000000000..392fd9e68 --- /dev/null +++ b/src/MOI_bounded_oracle.jl @@ -0,0 +1,118 @@ +""" + 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 + +""" + compute_extreme_point + +Is implemented in the FrankWolfe package in file "moi_oracle.jl". +""" + +""" +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.LessThan{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.GreaterThan{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_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(lmo.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 + +""" +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 \ No newline at end of file diff --git a/src/interface.jl b/src/interface.jl index 07bab846e..5d9d3791b 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -689,11 +689,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 From fa434fcf66b2a561ba39b51dfbf6258028cab8d3 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 10:45:14 +0200 Subject: [PATCH 10/81] Add BLMO wrapper and MOI BLMO to source. --- src/Boscia.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Boscia.jl b/src/Boscia.jl index e73ee7b51..e98395d83 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -13,6 +13,8 @@ const MOIU = MOI.Utilities import MathOptSetDistances as MOD include("time_tracking_lmo.jl") +include("lmo_wrapper.jl") +include("MOI_bounded_oracle.jl") include("bounds.jl") include("frank_wolfe_variants.jl") include("node.jl") From ed5aa04447804bde79cf8b256343f13a5b9d6b6a Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 11:10:00 +0200 Subject: [PATCH 11/81] Adapting file ultilities.jl. --- src/MOI_bounded_oracle.jl | 77 +++++++++++++++++++++++++++++++++++++++ src/lmo_wrapper.jl | 32 +++++++++++++++- src/utilities.jl | 57 +++-------------------------- 3 files changed, 113 insertions(+), 53 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 392fd9e68..46b937a56 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -10,6 +10,7 @@ struct MathOptBLMO{OT<:MOI.AbstractOptimizer} <: BoundedLinearMinimizationOracle end end +################## Necessary to implement #################### """ compute_extreme_point @@ -80,6 +81,41 @@ function add_bound_constraint!(blmo::MathOptBLMO, key, value) MOI.add_constraint(blmo.o, MOI.VariableIndex(key), value) end +""" +Has variable a binary constraint? +""" +function is_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 + +""" +Has variable an integer constraint? +""" +function is_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 + + +##################### Optional to implement ################ + """ Check if the bounds were set correctly in build_LMO. Safety check only. @@ -115,4 +151,45 @@ 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::BoundedLinearMinimizationOracle) + MOI.set( + blmo.o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction{Float64}([], 0.0), + ) + MOI.optimize!(blmo) + 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, _ = 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(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 \ No newline at end of file diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index f584438f9..8434a6126 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -5,6 +5,8 @@ Supertype for the Bounded Linear Minimization Oracles """ abstract type BoundedLinearMinimizationOracle <: FrankWolfe.LinearMinimizationOracle end +################## Necessary to implement ################## + """ Given a direction d solves the problem min_x d^T x @@ -61,19 +63,45 @@ Add bound constraint. """ function add_bound_constraint! end +""" +Has variable a binary constraint? +""" +function is_binary_constraint end + +""" +Has variable an integer constraint? +""" +function is_integer_constraint end + + +#################### Optional to implement #################### + """ Check if the bounds were set correctly in build_LMO. Safety check only. """ -function build_LMO_correct(blmo, node_bounds) +function build_LMO_correct(blmo::BoundedLinearMinimizationOracle, node_bounds) return true end """ Free model data from previous solve (if necessary). """ -function free_model(blmo) +function free_model(blmo::BoundedLinearMinimizationOracle) 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 diff --git a/src/utilities.jl b/src/utilities.jl index a63f0ed8d..a6b5376d0 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -16,15 +16,8 @@ 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 @@ -32,57 +25,19 @@ 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 + return is_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 + return is_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 From 8174e2d0c727a1157140efd40ab74930b8d4867d Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 11:35:50 +0200 Subject: [PATCH 12/81] Boscia.compute_extreme_point is the same function as FrankWolfe.compute_extreme_point. --- src/Boscia.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Boscia.jl b/src/Boscia.jl index e98395d83..2ff671388 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 From cea0ff98f50f3bfe183cc0fcbfc121c666975b11 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 11:36:08 +0200 Subject: [PATCH 13/81] Adapt time_tracking_lmo.jl. --- src/MOI_bounded_oracle.jl | 19 ++++++++++++++- src/lmo_wrapper.jl | 14 +++++++---- src/time_tracking_lmo.jl | 50 +++++++++++++++++++-------------------- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 46b937a56..3a57bb024 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -10,6 +10,13 @@ struct MathOptBLMO{OT<:MOI.AbstractOptimizer} <: BoundedLinearMinimizationOracle end end +""" +Build MathOptBLMO from FrankWolfe.MathOptLMO. +""" +function MathOptBLMO(lmo::FrankWolfe.MathOptLMO) + return MathOptBLMO(lmo.o, lmo.use_modfify) +end + ################## Necessary to implement #################### """ compute_extreme_point @@ -162,7 +169,7 @@ function check_feasibility(blmo::BoundedLinearMinimizationOracle) MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction{Float64}([], 0.0), ) - MOI.optimize!(blmo) + MOI.optimize!(blmo.o) status = MOI.get(blmo.o, MOI.TerminationStatus()) return status end @@ -192,4 +199,14 @@ function is_valid_split(tree::Bonobo.BnBTree, blmo::MathOptBLMO, vidx::Int) @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 \ No newline at end of file diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 8434a6126..389d3bf7f 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -8,16 +8,14 @@ abstract type BoundedLinearMinimizationOracle <: FrankWolfe.LinearMinimizationOr ################## 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 +function compute_extreme_point end -""" -CHECK IF NECESSARY -""" -function optimize! end """ Get the index of the integer variable the bound is working on. @@ -105,3 +103,9 @@ function is_valid_split(tree::Bonobo.BnBTree, blmo::BoundedLinearMinimizationOra return true end +""" +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/time_tracking_lmo.jl b/src/time_tracking_lmo.jl index dbabc8743..b6cd1769b 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,41 +14,39 @@ 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) - if !is_linear_feasible(lmo, v) + if !is_linear_feasible(tlmo, v) @debug "Vertex not linear feasible $(v)" - @assert is_linear_feasible(lmo, 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()) + opt_times, numberofnodes, simplex_iterations = get_BLMO_solve_data(tlmo.blmo) + + push!(tlmo.optimizing_times, opt_times) push!(lmo.optimizing_nodes, numberofnodes) - push!(lmo.simplex_iterations, MOI.get(lmo.lmo.o, MOI.SimplexIterations())) + push!(lmo.simplex_iterations, simplex_iterations) - cleanup_solver(lmo.lmo.o) + 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) +#MOI.optimize!(time_lmo::TimeTrackingLMO) = MOI.optimize!(time_lmo.lmo.o) From c553a746ee93a1ad69e58dd473ff7d3c4e864f9a Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 12:13:39 +0200 Subject: [PATCH 14/81] Adapt problem.jl. --- src/MOI_bounded_oracle.jl | 76 +++++++++++++++++++++- src/lmo_wrapper.jl | 32 ++++++++++ src/problem.jl | 129 ++++---------------------------------- 3 files changed, 118 insertions(+), 119 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 3a57bb024..b985d0d6b 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -209,4 +209,78 @@ function get_BLMO_solve_data(blmo::MathOptBLMO) numberofnodes = MOI.get(blmo.o, MOI.NodeCount()) simplex_iterations = MOI.get(blmo.o, MOI.SimplexIterations()) return opt_times, numberofnodes, simplex_iterations -end \ No newline at end of file +end + +""" +Is a given point v linear feasible for the model? +""" +function is_linear_feasible(blmo::MathOptBLMO) + return is_linear_feasible(blmo.o) +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 + +""" +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 + +""" +Are indicator constraints present? +""" +function indicator_present(blmo::BoundedLinearMinimizationOracle) + 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::BoundedLinearMinimizationOracle) + 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 diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 389d3bf7f..9c72152b5 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -74,6 +74,9 @@ function is_integer_constraint end #################### Optional to implement #################### +# These are safety check function, performance 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. + """ Check if the bounds were set correctly in build_LMO. Safety check only. @@ -109,3 +112,32 @@ 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 + +""" +Is a given point v linear feasible for the model? +""" +function is_linear_feasible(blmo::BoundedLinearMinimizationOracle) + 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 + + +""" +Get solving tolerance for the BLMO. +""" +function get_tol(blmo::BoundedLinearMinimizationOracle) + return 1e-6 +end diff --git a/src/problem.jl b/src/problem.jl index 968c4e206..ced252ae1 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 @@ -96,107 +75,21 @@ 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 - - """ Return the underlying optimizer For better access and readability """ -function get_optimizer(tree::Bonobo.BnBTree) - return tree.root.problem.lmo.lmo.o -end - +#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 > 5000.0 * scip_tol - @debug("Constraint: $(F)-$(S) $(func) = $(val) in $(set)") - @debug("Distance to set: $(dist)") - 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) From c5df4527dcfe3f3251688ffd6eaf6ab5d6e8106b Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 12:14:01 +0200 Subject: [PATCH 15/81] Adapt problem.jl II. --- src/MOI_bounded_oracle.jl | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index b985d0d6b..94935dc2d 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -257,7 +257,28 @@ 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) + 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 """ From a619cd27a7258ba3c3692771154f6f6b51c76344 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 12:42:07 +0200 Subject: [PATCH 16/81] Adapt node.jl --- src/node.jl | 58 +++++++++-------------------------------------------- 1 file changed, 9 insertions(+), 49 deletions(-) diff --git a/src/node.jl b/src/node.jl index 817200a92..dd0f9cafb 100644 --- a/src/node.jl +++ b/src/node.jl @@ -205,14 +205,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 @@ -221,25 +221,19 @@ 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 - - if isempty(node.active_set) + #= if isempty(node.active_set) consI_list = MOI.get( - tree.root.problem.lmo.lmo.o, + tree.root.problem.tlmo.blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}(), ) + MOI.get( - tree.root.problem.lmo.lmo.o, + tree.root.problem.tlmo.blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}(), ) if !isempty(consI_list) @error "Unreachable node! Active set is empty!" end - restart_active_set(node, tree.root.problem.lmo.lmo, tree.root.problem.nvars) - end + restart_active_set(node, tree.root.problem.tlmo.blmo, tree.root.problem.nvars) + end =# # time tracking FW active_set = node.active_set @@ -282,7 +276,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], @@ -294,41 +288,7 @@ function Bonobo.evaluate_node!(tree::Bonobo.BnBTree, node::FrankWolfeNode) extra_vertex_storage=node.discarded_vertices, callback=tree.root.options[:callback], verbose=tree.root.options[:fwVerbose], - )#= - if tree.root.options[:variant] == BPCG - # call blended_pairwise_conditional_gradient - x, _, primal, dual_gap, _, active_set = FrankWolfe.blended_pairwise_conditional_gradient( - tree.root.problem.f, - tree.root.problem.g, - tree.root.problem.lmo, - node.active_set, - epsilon=node.fw_dual_gap_limit, - max_iteration=tree.root.options[:max_fw_iter], - line_search=tree.root.options[:lineSearch], - add_dropped_vertices=true, - use_extra_vertex_storage=true, - extra_vertex_storage=node.discarded_vertices, - callback=tree.root.options[:callback], - lazy=true, - timeout=tree.root.options[:time_limit], - verbose=tree.root.options[:fwVerbose], - ) - elseif tree.root.options[:variant] == AFW - # call away_frank_wolfe - x, _, primal, dual_gap, _, active_set = FrankWolfe.away_frank_wolfe( - tree.root.problem.f, - tree.root.problem.g, - tree.root.problem.lmo, - node.active_set, - epsilon=node.fw_dual_gap_limit, - max_iteration=tree.root.options[:max_fw_iter], - line_search=tree.root.options[:lineSearch], - callback=tree.root.options[:callback], - lazy=true, - timeout=tree.root.options[:time_limit], - verbose=tree.root.options[:fwVerbose], - ) - end=# + ) node.fw_time = Dates.now() - time_ref node.dual_gap = dual_gap From 7b757bb0b31525844c7b617763c57ec834b285b1 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 14:04:48 +0200 Subject: [PATCH 17/81] Is not needed any longer. --- src/infeasible_pairwise.jl | 509 ------------------------------------- 1 file changed, 509 deletions(-) delete mode 100644 src/infeasible_pairwise.jl 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 From ba01559814befe16e8744823b0e4b0ddd5d9279e Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 14:12:38 +0200 Subject: [PATCH 18/81] Convert between FW.MathOptLMO and MathOptBLMO. --- src/Boscia.jl | 1 + src/MOI_bounded_oracle.jl | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/Boscia.jl b/src/Boscia.jl index 2ff671388..02d91b3ec 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -12,6 +12,7 @@ using Dates const MOI = MathOptInterface const MOIU = MOI.Utilities +import Base: convert import MathOptSetDistances as MOD include("time_tracking_lmo.jl") diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 94935dc2d..aac86b4e8 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -17,6 +17,13 @@ function MathOptBLMO(lmo::FrankWolfe.MathOptLMO) return MathOptBLMO(lmo.o, lmo.use_modfify) end +""" +Convert object of Type MathOptLMO into MathOptBLMO +""" +function convert(::Type{MathOptBLMO}, lmo::FrankWolfe.MathOptLMO) + return MathOptBLMO(lmo.o, lmo.use_modify) +end + ################## Necessary to implement #################### """ compute_extreme_point From e47d6d361c124d0c7c789c0a55a8fe0e5c63a22c Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 14:37:42 +0200 Subject: [PATCH 19/81] Convert MathOptBLMO into MathOptLMO. --- src/MOI_bounded_oracle.jl | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index aac86b4e8..e48eb1fc5 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -18,11 +18,14 @@ function MathOptBLMO(lmo::FrankWolfe.MathOptLMO) end """ -Convert object of Type MathOptLMO into MathOptBLMO +Convert object of Type MathOptLMO into MathOptBLMO and viceversa. """ function convert(::Type{MathOptBLMO}, lmo::FrankWolfe.MathOptLMO) return MathOptBLMO(lmo.o, lmo.use_modify) end +function convert(::Type{FrankWolfe.MathOptLMO}, nlmo::MathOptBLMO) + return FrankWolfe.MathOptLMO(blmo.o, blmo.use_modfify) +end ################## Necessary to implement #################### """ @@ -31,6 +34,28 @@ end Is implemented in the FrankWolfe package in file "moi_oracle.jl". """ +""" +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(blmo::MathOptBLMO) + v_indices = MOI.get(blmo.o, MOI.ListOfVariableIndices()) + if v_indices != MOI.VariableIndex.(1:n) + error("Variables are expected to be contiguous and ordered from 1 to N") + end + return 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. """ From a85593e1351ffc7625f96bf5f121c55209790d2d Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 14:38:20 +0200 Subject: [PATCH 20/81] Get list of all variables, binaries and integers respectivily. --- src/lmo_wrapper.jl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 9c72152b5..af784c79d 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -16,6 +16,17 @@ where x has to be an integer feasible point """ function compute_extreme_point end +""" +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 and integer variables, respectively. +""" +function get_binary_variables end +function get_integer_variables end """ Get the index of the integer variable the bound is working on. From bae27adbe0e9b7f9bced761fe249521c8189d5d1 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 14:46:34 +0200 Subject: [PATCH 21/81] Deleted infeasible_pairwise.jl. --- src/Boscia.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Boscia.jl b/src/Boscia.jl index 02d91b3ec..bfb0b707f 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -24,7 +24,6 @@ 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") From 4c2482e8de1fdb716f9d62c33aee04bc327ce525 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 14:57:46 +0200 Subject: [PATCH 22/81] Adapt heuristics.jl. --- src/MOI_bounded_oracle.jl | 7 +++++++ src/heuristics.jl | 5 +++++ src/lmo_wrapper.jl | 8 +++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index e48eb1fc5..a507d16ad 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -337,3 +337,10 @@ 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 diff --git a/src/heuristics.jl b/src/heuristics.jl index ebbcd47a4..8e464a9f0 100644 --- a/src/heuristics.jl +++ b/src/heuristics.jl @@ -1,3 +1,8 @@ +""" + Heuristics +""" + +#### TO DO: Implement Feasibility Pump? """ Finds the best solution in the SCIP solution storage, based on the objective function `f`. diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index af784c79d..31210cb49 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -145,10 +145,16 @@ function indicator_present(blmo::BoundedLinearMinimizationOracle) return false 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 From dd90fbd8f8ed79dedf30725751880f6578ddd6fb Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 15:50:01 +0200 Subject: [PATCH 23/81] Adapt callback.jl --- src/MOI_bounded_oracle.jl | 113 ++++++++++++++++++++++++-------------- src/callbacks.jl | 25 ++------- src/lmo_wrapper.jl | 29 +++++++--- 3 files changed, 99 insertions(+), 68 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index a507d16ad..284ab751e 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -152,6 +152,48 @@ function is_integer_constraint(blmo::MathOptBLMO, idx::Int) 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 + ##################### Optional to implement ################ @@ -243,48 +285,6 @@ function get_BLMO_solve_data(blmo::MathOptBLMO) return opt_times, numberofnodes, simplex_iterations end -""" -Is a given point v linear feasible for the model? -""" -function is_linear_feasible(blmo::MathOptBLMO) - return is_linear_feasible(blmo.o) -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 - """ Is a given point v indicator feasible, i.e. meets the indicator constraints? If applicable. """ @@ -344,3 +344,32 @@ 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::BoundedLinearMinimizationOracle, 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 \ No newline at end of file diff --git a/src/callbacks.jl b/src/callbacks.jl index 03b1d297b..e974d62d7 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -1,29 +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 - if !is_linear_feasible(tree.root.problem.lmo, state.v) + if !is_linear_feasible(tree.root.problem.tlmo, 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) + check_infeasible_vertex(tree.root.problem.tlmo.blmo, tree) + @assert is_linear_feasible(tree.root.problem.tlmo, state.v) end # @assert is_linear_feasible(tree.root.problem.lmo, state.x) push!(fw_iterations, state.t) @@ -32,7 +19,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.o, vars, tree.root.options[:domain_oracle]) if best_val < tree.incumbent tree.root.updated_incumbent[] = true node = tree.nodes[tree.root.current_node_id[]] @@ -91,7 +78,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/lmo_wrapper.jl b/src/lmo_wrapper.jl index 31210cb49..e82f20903 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -82,6 +82,12 @@ Has variable an integer constraint? """ function is_integer_constraint 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 + #################### Optional to implement #################### @@ -124,13 +130,6 @@ function get_BLMO_solve_data(blmo::BoundedLinearMinimizationOracle) return 0.0, 0.0, 0.0 end -""" -Is a given point v linear feasible for the model? -""" -function is_linear_feasible(blmo::BoundedLinearMinimizationOracle) - return true -end - """ Is a given point v indicator feasible, i.e. meets the indicator constraints? If applicable. """ @@ -158,3 +157,19 @@ 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 + +""" +Deal with infeasible vertex if necessary, e.g. check what caused it etc. +""" +function check_infeasible_vertex(blmo::BoundedLinearMinimizationOracle, tree) +end \ No newline at end of file From 75d6bb5736ee7b32ac167ba9aa632c92c73e76f6 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 16:32:58 +0200 Subject: [PATCH 24/81] Adapt strong branching. --- src/MOI_bounded_oracle.jl | 138 ++++++++++++++++++++++++++++++++++++++ src/lmo_wrapper.jl | 5 ++ src/strong_branching.jl | 71 +++++++++----------- 3 files changed, 173 insertions(+), 41 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 284ab751e..405098e67 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -372,4 +372,142 @@ function check_infeasible_vertex(blmo::MathOptBLMO, tree) end end end +end + +""" +Behavior for strong branching. +""" +function Bonobo.get_branching_variable( + tree::Bonobo.BnBTree, + branching::PartialStrongBranching{MathOptBLMO}, + 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 + 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/lmo_wrapper.jl b/src/lmo_wrapper.jl index e82f20903..f603ea087 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -88,6 +88,11 @@ That means does v satisfy all bounds and other linear constraints? """ function is_linear_feasible end +""" +Define a copy function for strong branching +""" +function copy end + #################### Optional to implement #################### diff --git a/src/strong_branching.jl b/src/strong_branching.jl index a48d27908..fade6456c 100644 --- a/src/strong_branching.jl +++ b/src/strong_branching.jl @@ -1,42 +1,31 @@ -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, - node::Bonobo.AbstractNode, + branching::PartialStrongBranching{BoundedLinearMinimizationOracle}, + node::Bonbo.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) + # copy problem + relaxed_blmo = copy(branching.bounded_lmo) @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 +35,13 @@ function Bonobo.get_branching_variable( end push!(boundsLeft.upper_bounds, (idx => MOI.LessThan(fxi))) build_LMO( - relaxed_lmo, + relaxed_blmo, 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(relaxed_blmo) + if status == MOI.OPTIMAL empty!(active_set) for (λ, v) in node.active_set if v[idx] <= xrel[idx] @@ -66,7 +54,7 @@ function Bonobo.get_branching_variable( FrankWolfe.blended_pairwise_conditional_gradient( tree.root.problem.f, tree.root.problem.g, - relaxed_lmo, + relaxed_blmo, active_set, verbose=false, epsilon=branching.solving_epsilon, @@ -74,9 +62,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 +74,13 @@ function Bonobo.get_branching_variable( end push!(boundsRight.lower_bounds, (idx => MOI.GreaterThan(cxi))) build_LMO( - relaxed_lmo, + relaxed_blmo, 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(relaxed_blmo) + if status == MOI.OPTIMAL empty!(active_set) for (λ, v) in node.active_set if v[idx] >= xrel[idx] @@ -109,7 +98,7 @@ function Bonobo.get_branching_variable( FrankWolfe.blended_pairwise_conditional_gradient( tree.root.problem.f, tree.root.problem.g, - relaxed_lmo, + relaxed_blmo, active_set, verbose=false, epsilon=branching.solving_epsilon, @@ -117,7 +106,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 +133,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 +143,12 @@ end function HybridStrongBranching( max_iteration::Int, solving_epsilon::Float64, - optimizer::O, + bounded_lmo::BLMO, perform_strong_branch::Function, alternative=Bonobo.MOST_INFEASIBLE(), -) where {O} +) where {BLMO<:BoundedLinearMinimizationOracle} return HybridStrongBranching( - PartialStrongBranching(max_iteration, solving_epsilon, optimizer), + PartialStrongBranching(max_iteration, solving_epsilon, bounded_lmo), perform_strong_branch, alternative, ) @@ -184,13 +173,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::BLMO, max_depth::Int, alternative=Bonobo.MOST_INFEASIBLE(), -) where {O} +) where {BLMO<:BoundedLinearMinimizationOracle} 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, ) From a1b04d898c9750aecb416db69aacd24d0fe0b91f Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 16:56:47 +0200 Subject: [PATCH 25/81] Adapt interface.jl. --- src/MOI_bounded_oracle.jl | 57 ++++++++++++++++++++++++ src/interface.jl | 92 ++++++++++++--------------------------- src/lmo_wrapper.jl | 12 ++++- 3 files changed, 96 insertions(+), 65 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 405098e67..11f93a38c 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -27,6 +27,7 @@ function convert(::Type{FrankWolfe.MathOptLMO}, nlmo::MathOptBLMO) return FrankWolfe.MathOptLMO(blmo.o, blmo.use_modfify) end + ################## Necessary to implement #################### """ compute_extreme_point @@ -194,6 +195,62 @@ function is_linear_feasible_subroutine(o::MOI.ModelLike, ::Type{F}, ::Type{S}, v return true end +""" +Define a copy function for strong branching +""" +function Base.copy(blmo::MathOptBLMO) + return MathOptBLMO(blmo.o, blmo.use_modify) +end + +""" +Read global bounds from the problem +""" +function build_global_bounds(blmo::MathOptBLMO) + 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(lmo.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 ################ diff --git a/src/interface.jl b/src/interface.jl index 5d9d3791b..090e85f17 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -3,7 +3,7 @@ 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). 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 @@ -43,7 +43,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(), @@ -85,26 +85,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 + 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!") @@ -116,54 +114,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) + 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 @@ -306,7 +270,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 @@ -314,7 +278,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 @@ -323,7 +287,7 @@ function solve( end println() # cleaner output - return x, tree.root.problem.lmo, result + return x, tree.root.problem.tlmo, result end """ @@ -433,13 +397,13 @@ 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) @@ -597,23 +561,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, @@ -680,7 +644,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, diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index f603ea087..85f2705a4 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -89,10 +89,20 @@ That means does v satisfy all bounds and other linear constraints? function is_linear_feasible end """ -Define a copy function for strong branching +Define a copy function for strong branching. Originally from Base. """ function copy end +""" +Read global bounds from the problem. +""" +function build_global_bounds end + +""" +Add explicit bounds for binary variables. +""" +function explicit_bounds_binary_var end + #################### Optional to implement #################### From f6295df2a65a66fbcc81b3242441fc9567713903 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 17:11:04 +0200 Subject: [PATCH 26/81] Rename files for more clearity. --- src/{bounds.jl => integer_bounds.jl} | 81 ---------------------------- 1 file changed, 81 deletions(-) rename src/{bounds.jl => integer_bounds.jl} (67%) diff --git a/src/bounds.jl b/src/integer_bounds.jl similarity index 67% rename from src/bounds.jl rename to src/integer_bounds.jl index 3c6d9cdad..a74ced770 100644 --- a/src/bounds.jl +++ b/src/integer_bounds.jl @@ -183,84 +183,3 @@ end build_LMO(lmo::TimeTrackingLMO, gb::IntegerBounds, nb::IntegerBounds, int_vars::Vector{Int64}) = build_LMO(lmo.lmo, gb, nb, int_vars) =# - - """ -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.lower_bounds[v_idx]) - # keep - else - set_bound(blmo, c_idx, global_bounds.lower_bounds[v_idx]) - 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.upper_bounds[v_idx]) - # keep - else - set_bound(blmo, c_idx, global_bounds.upper_bounds[v_idx]) - 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) From f3918d8fa83afe58db522b5bf6ca11a813e0ccdc Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 17:11:30 +0200 Subject: [PATCH 27/81] Separate file for the build_LMO function. --- src/Boscia.jl | 7 +++-- src/build_lmo.jl | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 src/build_lmo.jl diff --git a/src/Boscia.jl b/src/Boscia.jl index bfb0b707f..1119f09a5 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -15,10 +15,10 @@ const MOIU = MOI.Utilities import Base: convert import MathOptSetDistances as MOD -include("time_tracking_lmo.jl") +include("integer_bounds.jl") include("lmo_wrapper.jl") -include("MOI_bounded_oracle.jl") -include("bounds.jl") +include("time_tracking_lmo.jl") +include("build_lmo.jl") include("frank_wolfe_variants.jl") include("node.jl") include("custom_bonobo.jl") @@ -28,5 +28,6 @@ include("heuristics.jl") include("strong_branching.jl") include("utilities.jl") include("interface.jl") +include("MOI_bounded_oracle.jl") end # module diff --git a/src/build_lmo.jl b/src/build_lmo.jl new file mode 100644 index 000000000..d20fc6ade --- /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.lower_bounds[v_idx]) + # keep + else + set_bound(blmo, c_idx, global_bounds.lower_bounds[v_idx]) + 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.upper_bounds[v_idx]) + # keep + else + set_bound(blmo, c_idx, global_bounds.upper_bounds[v_idx]) + 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) From a93213c20e9250d997bb4c626cd12bed2cce15ab Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 17:15:08 +0200 Subject: [PATCH 28/81] Fix syntax issues. --- src/strong_branching.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/strong_branching.jl b/src/strong_branching.jl index fade6456c..06a35edb3 100644 --- a/src/strong_branching.jl +++ b/src/strong_branching.jl @@ -12,7 +12,7 @@ Create all possible subproblems, solve them and pick the one with the most progr function Bonobo.get_branching_variable( tree::Bonobo.BnBTree, branching::PartialStrongBranching{BoundedLinearMinimizationOracle}, - node::Bonbo.AbstractNode, + node::Bonobo.AbstractNode, ) xrel = Bonobo.get_relaxed_values(tree, node) max_lowerbound = -Inf @@ -133,7 +133,7 @@ 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{BLMO::BoundedLinearMinimizationOracle,F<:Function,B<:Bonobo.AbstractBranchStrategy} <: +struct HybridStrongBranching{BLMO<:BoundedLinearMinimizationOracle,F<:Function,B<:Bonobo.AbstractBranchStrategy} <: Bonobo.AbstractBranchStrategy pstrong::PartialStrongBranching{BLMO} perform_strong_branch::F @@ -143,10 +143,10 @@ end function HybridStrongBranching( max_iteration::Int, solving_epsilon::Float64, - bounded_lmo::BLMO, + bounded_lmo::BoundedLinearMinimizationOracle, perform_strong_branch::Function, alternative=Bonobo.MOST_INFEASIBLE(), -) where {BLMO<:BoundedLinearMinimizationOracle} +) return HybridStrongBranching( PartialStrongBranching(max_iteration, solving_epsilon, bounded_lmo), perform_strong_branch, @@ -173,10 +173,10 @@ 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, - bounded_lmo::BLMO, + bounded_lmo::BoundedLinearMinimizationOracle, max_depth::Int, alternative=Bonobo.MOST_INFEASIBLE(), -) where {BLMO<:BoundedLinearMinimizationOracle} +) perform_strong_while_depth(_, node) = node.level <= max_depth return HybridStrongBranching( PartialStrongBranching(max_iteration, solving_epsilon, bounded_lmo), From c5c03dc5ff51edc188668b3b195fdb0d204d2675 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 17:20:37 +0200 Subject: [PATCH 29/81] Rather write Base.convert. --- src/Boscia.jl | 1 - src/MOI_bounded_oracle.jl | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Boscia.jl b/src/Boscia.jl index 1119f09a5..96f32d4f3 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -12,7 +12,6 @@ using Dates const MOI = MathOptInterface const MOIU = MOI.Utilities -import Base: convert import MathOptSetDistances as MOD include("integer_bounds.jl") diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 11f93a38c..657823daa 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -20,10 +20,10 @@ end """ Convert object of Type MathOptLMO into MathOptBLMO and viceversa. """ -function convert(::Type{MathOptBLMO}, lmo::FrankWolfe.MathOptLMO) +function Base.convert(::Type{MathOptBLMO}, lmo::FrankWolfe.MathOptLMO) return MathOptBLMO(lmo.o, lmo.use_modify) end -function convert(::Type{FrankWolfe.MathOptLMO}, nlmo::MathOptBLMO) +function Base.convert(::Type{FrankWolfe.MathOptLMO}, nlmo::MathOptBLMO) return FrankWolfe.MathOptLMO(blmo.o, blmo.use_modfify) end From 5441367a74a22412f410b61f78432ab86ae062e1 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 17:25:07 +0200 Subject: [PATCH 30/81] Have a solve function for MathOptLMO. Converts internally and calls original solve functions. This way the examples and results don't have to be changed. --- src/interface.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/interface.jl b/src/interface.jl index 090e85f17..0a0cb6f1b 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,3 +1,17 @@ +""" +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; + kwargs... +) + blmo = convert(MathOptBLMO, lmo) + solve(f, g, blmo; kwargs) +end + """ solve From 32e2f45ba24c5f97c44f3d5462eb029c03d51a0a Mon Sep 17 00:00:00 2001 From: Hendrych Date: Mon, 16 Oct 2023 17:29:33 +0200 Subject: [PATCH 31/81] Minor change. --- src/heuristics.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/heuristics.jl b/src/heuristics.jl index 8e464a9f0..816c3d9cf 100644 --- a/src/heuristics.jl +++ b/src/heuristics.jl @@ -1,7 +1,3 @@ -""" - Heuristics -""" - #### TO DO: Implement Feasibility Pump? """ @@ -27,6 +23,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 From b2c0d4a47eaea949e7bb9941025be8cd046c4a4d Mon Sep 17 00:00:00 2001 From: dhendryc <92737336+dhendryc@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:30:27 +0200 Subject: [PATCH 32/81] Better explanation of blmo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathieu Besançon --- src/interface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.jl b/src/interface.jl index 0a0cb6f1b..824f39aa4 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -17,7 +17,7 @@ end f - objective function oracle. g - oracle for the gradient of the objective. -blmo - a MIP solver instance (e.g.SCIP) encoding the feasible region. Has to be of type BoundedLinearMinimizationOracle (see lmo_wrapper). +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 From 0d8d0bc3ee5eea2e18dea6bbfe7d184ec56be37e Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 13:48:07 +0200 Subject: [PATCH 33/81] Fix incremental compilation warning. --- src/MOI_bounded_oracle.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 657823daa..9d1b2ddd6 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -294,7 +294,7 @@ end """ Check if problem is bounded and feasible, i.e. no contradicting constraints. """ -function check_feasibility(blmo::BoundedLinearMinimizationOracle) +function check_feasibility(blmo::MathOptBLMO) MOI.set( blmo.o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), @@ -373,7 +373,7 @@ end """ Are indicator constraints present? """ -function indicator_present(blmo::BoundedLinearMinimizationOracle) +function indicator_present(blmo::MathOptBLMO) for (_, S) in MOI.get(blmo.o, MOI.ListOfConstraintTypesPresent()) if S <: MOI.Indicator return true @@ -385,7 +385,7 @@ end """ Get solving tolerance for the BLMO. """ -function get_tol(blmo::BoundedLinearMinimizationOracle) +function get_tol(blmo::MathOptBLMO) return get_tol(blmo.o) end function get_tol(o::SCIP.Optimizer) @@ -407,7 +407,7 @@ List of all variable pointers. Depends on how you save your variables internally Is used in `find_best_solution`. """ -function get_variables_pointers(blmo::BoundedLinearMinimizationOracle, tree) +function get_variables_pointers(blmo::MathOptBLMO, tree) return [MOI.VariableIndex(var) for var in 1:tree.root.problem.nvars] end From 223794ee56a425542c1b08ab5effa12bf3d383cf Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 13:53:23 +0200 Subject: [PATCH 34/81] Signuature fix. --- src/MOI_bounded_oracle.jl | 5 +++-- src/interface.jl | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 9d1b2ddd6..fd07e3013 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -41,6 +41,7 @@ If the problem has n variables, they are expected to contiguous and ordered from """ 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 @@ -205,7 +206,7 @@ end """ Read global bounds from the problem """ -function build_global_bounds(blmo::MathOptBLMO) +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}) @@ -408,7 +409,7 @@ 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] + return [MOI.VariableIndex(var) for var in 1:(tree.root.problem.nvars)] end """ diff --git a/src/interface.jl b/src/interface.jl index 0a0cb6f1b..6faf191e2 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -128,7 +128,7 @@ function solve( println("\t Number of binary variables: $(num_bin)\n") end - global_bounds = build_global_bounds(blmo) + global_bounds = build_global_bounds(blmo, integer_variables) explicit_bounds_binary_var(blmo, global_bounds, binary_variables) v = [] From edf9b1c58138bc807d93fd261c0fb36d1c3ee633 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:03:14 +0200 Subject: [PATCH 35/81] minor change. --- src/MOI_bounded_oracle.jl | 6 +++--- src/interface.jl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index fd07e3013..ffe12714e 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -36,7 +36,7 @@ Is implemented in the FrankWolfe package in file "moi_oracle.jl". """ """ -Get list of variables indices. +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) @@ -45,7 +45,7 @@ function get_list_of_variables(blmo::MathOptBLMO) if v_indices != MOI.VariableIndex.(1:n) error("Variables are expected to be contiguous and ordered from 1 to N") end - return v_indices + return n, v_indices end """ @@ -227,7 +227,7 @@ function build_global_bounds(blmo::MathOptBLMO, integer_variables) push!(global_bounds, (idx, MOI.GreaterThan(s.lower))) push!(global_bounds, (idx, MOI.LessThan(s.upper))) end - @assert !MOI.is_valid(lmo.o, cidx) + @assert !MOI.is_valid(blmo.o, cidx) end return global_bounds end diff --git a/src/interface.jl b/src/interface.jl index 6faf191e2..25c607897 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -99,7 +99,7 @@ function solve( println("\t Additional kwargs: ", join(keys(kwargs), ",")) end - v_indices = get_list_of_variables(blmo) + n, v_indices = get_list_of_variables(blmo) integer_variables = Vector{Int}() num_int = 0 From 1733515b1d546b685a00673018148cc7f33a4259 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:19:19 +0200 Subject: [PATCH 36/81] Redirect to FrankWolfe.compute_extreme_point for MathOptLMO. --- src/MOI_bounded_oracle.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index ffe12714e..bae2f3a3d 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -23,8 +23,8 @@ 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}, nlmo::MathOptBLMO) - return FrankWolfe.MathOptLMO(blmo.o, blmo.use_modfify) +function Base.convert(::Type{FrankWolfe.MathOptLMO}, blmo::MathOptBLMO) + return FrankWolfe.MathOptLMO(blmo.o, blmo.use_modify) end @@ -34,6 +34,12 @@ end 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. From 4130c16f7e878ec01920f91cc408324198a6c84d Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:20:40 +0200 Subject: [PATCH 37/81] Syntax fix. --- src/build_lmo.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/build_lmo.jl b/src/build_lmo.jl index d20fc6ade..83a9d517f 100644 --- a/src/build_lmo.jl +++ b/src/build_lmo.jl @@ -27,10 +27,10 @@ function build_LMO( 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.lower_bounds[v_idx]) + set_bound!(blmo, c_idx, node_bounds.lower_bounds[v_idx]) # keep else - set_bound(blmo, c_idx, global_bounds.lower_bounds[v_idx]) + set_bound!(blmo, c_idx, global_bounds.lower_bounds[v_idx]) end else # Delete @@ -46,10 +46,10 @@ function build_LMO( 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.upper_bounds[v_idx]) + set_bound!(blmo, c_idx, node_bounds.upper_bounds[v_idx]) # keep else - set_bound(blmo, c_idx, global_bounds.upper_bounds[v_idx]) + set_bound!(blmo, c_idx, global_bounds.upper_bounds[v_idx]) end else # Delete From 7090af6110ce20d4eab16fa7b882196ebdfd0ba3 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:25:15 +0200 Subject: [PATCH 38/81] Mix up between lower and upper bounds. --- src/MOI_bounded_oracle.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index bae2f3a3d..7cb12ad37 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -75,13 +75,13 @@ end Get the list of lower bounds. """ function get_lower_bound_list(blmo::MathOptBLMO) - return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{Float64}}()) + 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.GreaterThan{Float64}}()) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{Float64}}()) end """ From db6e3c32ea4c4ffb343498df9bbdcedfb618c859 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:26:51 +0200 Subject: [PATCH 39/81] Syntax fix. --- src/build_lmo.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/build_lmo.jl b/src/build_lmo.jl index 83a9d517f..9ca04aa6e 100644 --- a/src/build_lmo.jl +++ b/src/build_lmo.jl @@ -59,18 +59,18 @@ function build_LMO( end # delete constraints - delete_bounds(blmo, cons_delete) + 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]) + 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]) + add_bound_constraint!(blmo, key, node_bounds.upper_bounds[key]) end end From c5677801b451e65594d72491373ab8368893f4a7 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:28:52 +0200 Subject: [PATCH 40/81] Syntax fix. --- src/time_tracking_lmo.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/time_tracking_lmo.jl b/src/time_tracking_lmo.jl index b6cd1769b..5c9876afb 100644 --- a/src/time_tracking_lmo.jl +++ b/src/time_tracking_lmo.jl @@ -42,8 +42,8 @@ function FrankWolfe.compute_extreme_point(tlmo::TimeTrackingLMO, d; kwargs...) opt_times, numberofnodes, simplex_iterations = get_BLMO_solve_data(tlmo.blmo) push!(tlmo.optimizing_times, opt_times) - push!(lmo.optimizing_nodes, numberofnodes) - push!(lmo.simplex_iterations, simplex_iterations) + push!(tlmo.optimizing_nodes, numberofnodes) + push!(tlmo.simplex_iterations, simplex_iterations) free_model(tlmo.blmo) return v From f2a386ee27ac389710fb8a65b918cdcefde87761 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:37:12 +0200 Subject: [PATCH 41/81] Export Base.copy. --- src/Boscia.jl | 2 ++ src/lmo_wrapper.jl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Boscia.jl b/src/Boscia.jl index 96f32d4f3..b79d1b43d 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -3,6 +3,8 @@ module Boscia using FrankWolfe import FrankWolfe: compute_extreme_point export compute_extreme_point +import Base: copy +export copy using Random using SCIP import MathOptInterface diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 85f2705a4..153000033 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -90,6 +90,8 @@ function is_linear_feasible end """ Define a copy function for strong branching. Originally from Base. + +Hence, implement as `Base.copy(..)`. """ function copy end From 9d7c507c6d73a197f8b22f1204e9ac07aed49a7a Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 14:45:28 +0200 Subject: [PATCH 42/81] Syntax fix. --- src/interface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.jl b/src/interface.jl index bddd6c251..c48db3538 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -520,7 +520,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 From 064d74c14b1ae0c5a94e0f840febf843df8acaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Tue, 17 Oct 2023 14:52:45 +0200 Subject: [PATCH 43/81] Update src/MOI_bounded_oracle.jl --- src/MOI_bounded_oracle.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 7cb12ad37..576199cd1 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -75,7 +75,7 @@ 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}}()) + return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{Float64}}()) end """ Get the list of upper bounds. From 9064c2ccf7e1d103b513698e6e8acc9fbdfb76b6 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 15:13:21 +0200 Subject: [PATCH 44/81] No extra copy function needed. --- src/Boscia.jl | 2 -- src/MOI_bounded_oracle.jl | 7 ------- src/lmo_wrapper.jl | 7 ------- src/strong_branching.jl | 14 ++++++-------- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/Boscia.jl b/src/Boscia.jl index b79d1b43d..96f32d4f3 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -3,8 +3,6 @@ module Boscia using FrankWolfe import FrankWolfe: compute_extreme_point export compute_extreme_point -import Base: copy -export copy using Random using SCIP import MathOptInterface diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 7cb12ad37..c107fe6fa 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -202,13 +202,6 @@ function is_linear_feasible_subroutine(o::MOI.ModelLike, ::Type{F}, ::Type{S}, v return true end -""" -Define a copy function for strong branching -""" -function Base.copy(blmo::MathOptBLMO) - return MathOptBLMO(blmo.o, blmo.use_modify) -end - """ Read global bounds from the problem """ diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 153000033..170b4ccce 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -88,13 +88,6 @@ That means does v satisfy all bounds and other linear constraints? """ function is_linear_feasible end -""" -Define a copy function for strong branching. Originally from Base. - -Hence, implement as `Base.copy(..)`. -""" -function copy end - """ Read global bounds from the problem. """ diff --git a/src/strong_branching.jl b/src/strong_branching.jl index 06a35edb3..d79075c2d 100644 --- a/src/strong_branching.jl +++ b/src/strong_branching.jl @@ -17,8 +17,6 @@ function Bonobo.get_branching_variable( xrel = Bonobo.get_relaxed_values(tree, node) max_lowerbound = -Inf max_idx = -1 - # copy problem - relaxed_blmo = copy(branching.bounded_lmo) @assert !isempty(node.active_set) active_set = copy(node.active_set) empty!(active_set) @@ -35,12 +33,12 @@ function Bonobo.get_branching_variable( end push!(boundsLeft.upper_bounds, (idx => MOI.LessThan(fxi))) build_LMO( - relaxed_blmo, + branching.bounded_lmo, tree.root.problem.integer_variable_bounds, boundsLeft, Bonobo.get_branching_indices(tree.root), ) - status = check_feasibility(relaxed_blmo) + status = check_feasibility(branching.bounded_lmo) if status == MOI.OPTIMAL empty!(active_set) for (λ, v) in node.active_set @@ -54,7 +52,7 @@ function Bonobo.get_branching_variable( FrankWolfe.blended_pairwise_conditional_gradient( tree.root.problem.f, tree.root.problem.g, - relaxed_blmo, + branching.bounded_lmo, active_set, verbose=false, epsilon=branching.solving_epsilon, @@ -74,12 +72,12 @@ function Bonobo.get_branching_variable( end push!(boundsRight.lower_bounds, (idx => MOI.GreaterThan(cxi))) build_LMO( - relaxed_blmo, + branching.bounded_lmo, tree.root.problem.integer_variable_bounds, boundsRight, Bonobo.get_branching_indices(tree.root), ) - status = check_feasibility(relaxed_blmo) + status = check_feasibility(branching.bounded_lmo) if status == MOI.OPTIMAL empty!(active_set) for (λ, v) in node.active_set @@ -98,7 +96,7 @@ function Bonobo.get_branching_variable( FrankWolfe.blended_pairwise_conditional_gradient( tree.root.problem.f, tree.root.problem.g, - relaxed_blmo, + branching.bounded_lmo, active_set, verbose=false, epsilon=branching.solving_epsilon, From 60d1b443e1377623e51a85b793fe2f21dc4084da Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 15:25:16 +0200 Subject: [PATCH 45/81] Changes to the Strong Branching struct. --- examples/birkhoff.jl | 5 +++-- examples/strong_branching_portfolio.jl | 5 +++-- src/MOI_bounded_oracle.jl | 2 +- test/interface_test.jl | 5 +++-- test/poisson.jl | 10 ++++++---- test/runtests.jl | 11 ++++++----- 6 files changed, 22 insertions(+), 16 deletions(-) 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/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 1fa2c5f30..dbf20b079 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -459,7 +459,7 @@ function Bonobo.get_branching_variable( @assert v1 == v2 end end - relaxed_lmo = FrankWolfe.MathOptLMO(branching.optimizer) + relaxed_lmo = FrankWolfe.MathOptLMO(branching.bounded_lmo.optimizer) @assert !isempty(node.active_set) active_set = copy(node.active_set) empty!(active_set) diff --git a/test/interface_test.jl b/test/interface_test.jl index d3b5039f0..bf51f9178 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) From 701140a3de1061c1995f97d28773752c626d3243 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 15:26:57 +0200 Subject: [PATCH 46/81] Syntax fix. --- src/problem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/problem.jl b/src/problem.jl index ced252ae1..e2447c445 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -66,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; From 2682cd851de8df8261173960473c51e8acdd8542 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 15:29:23 +0200 Subject: [PATCH 47/81] Change to MathOptBLMO. --- test/LMO_test.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 1b0790433dcf32679c9f66db7d174e5ae669dc08 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 15:56:15 +0200 Subject: [PATCH 48/81] Syntax change. --- test/indicator_test.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From b56ae3c1f1d16fdce3e335bfad17a862d560cfdc Mon Sep 17 00:00:00 2001 From: Hendrych Date: Tue, 17 Oct 2023 16:06:37 +0200 Subject: [PATCH 49/81] Signature change. --- src/MOI_bounded_oracle.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index dbf20b079..6f09956bb 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -117,7 +117,7 @@ Delete bounds. """ function delete_bounds!(blmo::MathOptBLMO, cons_delete) for d_idx in cons_delete - MOI.delete(lmo.o, d_idx) + MOI.delete(blmo.o, d_idx) end end From edda0142105e6530d3c39cd4a3ca804f27239045 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 10:27:22 +0200 Subject: [PATCH 50/81] Delete unnecessary code. --- src/integer_bounds.jl | 104 ------------------------------------------ 1 file changed, 104 deletions(-) diff --git a/src/integer_bounds.jl b/src/integer_bounds.jl index a74ced770..1283ffbc9 100644 --- a/src/integer_bounds.jl +++ b/src/integer_bounds.jl @@ -79,107 +79,3 @@ 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) -=# From 0deade1389912584384b7809a413822eac9c8b35 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 10:27:57 +0200 Subject: [PATCH 51/81] Clean up. --- src/time_tracking_lmo.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/time_tracking_lmo.jl b/src/time_tracking_lmo.jl index 5c9876afb..4e075e278 100644 --- a/src/time_tracking_lmo.jl +++ b/src/time_tracking_lmo.jl @@ -48,5 +48,3 @@ function FrankWolfe.compute_extreme_point(tlmo::TimeTrackingLMO, d; kwargs...) free_model(tlmo.blmo) return v end - -#MOI.optimize!(time_lmo::TimeTrackingLMO) = MOI.optimize!(time_lmo.lmo.o) From 5748d5eb6aee79a0b47c8dac158be4014a945f62 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 15:09:15 +0200 Subject: [PATCH 52/81] Some rewriting. --- src/MOI_bounded_oracle.jl | 8 ++-- src/lmo_wrapper.jl | 97 +++++++++++++++++---------------------- src/utilities.jl | 8 ++-- 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 6f09956bb..7ecb56309 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -131,7 +131,7 @@ end """ Has variable a binary constraint? """ -function is_binary_constraint(blmo::MathOptBLMO, idx::Int) +function has_binary_constraint(blmo::MathOptBLMO, idx::Int) consB_list = MOI.get( blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}(), @@ -147,7 +147,7 @@ end """ Has variable an integer constraint? """ -function is_integer_constraint(blmo::MathOptBLMO, idx::Int) +function has_integer_constraint(blmo::MathOptBLMO, idx::Int) consB_list = MOI.get( blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}(), @@ -309,8 +309,8 @@ 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, _ = is_binary_constraint(tree, vidx) - int_var, _ = is_integer_constraint(tree, vidx) + 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) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 170b4ccce..97a4d296a 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -5,7 +5,7 @@ Supertype for the Bounded Linear Minimization Oracles """ abstract type BoundedLinearMinimizationOracle <: FrankWolfe.LinearMinimizationOracle end -################## Necessary to implement ################## +###################################### Necessary to implement #################################### """ Implement `FrankWolfe.compute_extreme_point` @@ -16,23 +16,31 @@ 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 and integer variables, respectively. """ function get_binary_variables end 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. """ @@ -41,69 +49,55 @@ function get_lower_bound_list end Get the list of upper bounds. """ function get_upper_bound_list end - -""" -Change the value of the bound c_idx. -""" -function set_bound! end - """ Read bound value for c_idx. """ function get_bound end +## Changing the bounds constraints. """ -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. +Change the value of the bound c_idx. """ -function is_bound_in end - +function set_bound! end """ Delete bounds. """ function delete_bounds! end - """ Add bound constraint. """ function add_bound_constraint! end +## Checks """ -Has variable a binary constraint? +Check if the subject of the bound c_idx is an integer variable (recorded in int_vars). """ -function is_binary_constraint end - +function is_constraint_on_int_var end """ -Has variable an integer constraint? +To check if there is bound for the variable in the global or node bounds. """ -function is_integer_constraint end - +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 - """ -Read global bounds from the problem. +Has variable a binary constraint? """ -function build_global_bounds end - +function has_binary_constraint end """ -Add explicit bounds for binary variables. +Has variable an integer constraint? """ -function explicit_bounds_binary_var end +function has_integer_constraint end #################### Optional to implement #################### -# These are safety check function, performance and log functions, .. +# 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. @@ -111,35 +105,18 @@ Safety check only. function build_LMO_correct(blmo::BoundedLinearMinimizationOracle, node_bounds) return true end - -""" -Free model data from previous solve (if necessary). -""" -function free_model(blmo::BoundedLinearMinimizationOracle) - 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 - -""" -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 - """ Is a given point v indicator feasible, i.e. meets the indicator constraints? If applicable. """ @@ -153,21 +130,31 @@ 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)`. @@ -178,8 +165,10 @@ function get_variables_pointers(blmo::BoundedLinearMinimizationOracle, tree) return collect(1:N) end +## Logs """ -Deal with infeasible vertex if necessary, e.g. check what caused it etc. +Get solve time, number of nodes and number of iterations, if applicable. """ -function check_infeasible_vertex(blmo::BoundedLinearMinimizationOracle, tree) -end \ No newline at end of file +function get_BLMO_solve_data(blmo::BoundedLinearMinimizationOracle) + return 0.0, 0.0, 0.0 +end diff --git a/src/utilities.jl b/src/utilities.jl index a6b5376d0..b5263d46e 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -24,12 +24,12 @@ end """ Check if at a given index we have a binary and integer constraint respectivily. """ -function is_binary_constraint(tree::Bonobo.BnBTree, idx::Int) - return is_binary_constraint(tree.root.problem.tlmo.blmo, idx) +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) - return is_integer_constraint(tree.root.problem.tlmo.blmo, idx) +function has_integer_constraint(tree::Bonobo.BnBTree, idx::Int) + return has_integer_constraint(tree.root.problem.tlmo.blmo, idx) end From f5142811da7865d7e9c6fb39178ff1518d0c7972 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 15:40:19 +0200 Subject: [PATCH 53/81] Explicitly hand down all the arguments. --- src/interface.jl | 62 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index c48db3538..0005e175d 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -6,10 +6,62 @@ 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) - solve(f, g, blmo; kwargs) + 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 """ @@ -461,7 +513,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, @@ -639,11 +691,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)) From cda0fc3cd027a099322c17811d3f78f5dc8fea4f Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 16:49:24 +0200 Subject: [PATCH 54/81] Syntax fix. --- src/MOI_bounded_oracle.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 7ecb56309..155735463 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -436,14 +436,14 @@ Behavior for strong branching. """ function Bonobo.get_branching_variable( tree::Bonobo.BnBTree, - branching::PartialStrongBranching{MathOptBLMO}, + 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.lmo.lmo.o) do item + 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} @@ -452,14 +452,14 @@ function Bonobo.get_branching_variable( end return !(item isa MOI.ConstraintIndex{<:Any,<:Union{MOI.ZeroOne,MOI.Integer,MOI.Indicator}}) end - index_map = MOI.copy_to(branching.optimizer, filtered_src) + 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 = FrankWolfe.MathOptLMO(branching.bounded_lmo.optimizer) + relaxed_lmo = MathOptBLMO(branching.bounded_lmo.o) @assert !isempty(node.active_set) active_set = copy(node.active_set) empty!(active_set) From 23f760c2d54180b483beae440512f5e004e370b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Wed, 18 Oct 2023 17:11:36 +0200 Subject: [PATCH 55/81] Update src/lmo_wrapper.jl --- src/lmo_wrapper.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 97a4d296a..c2c601005 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -32,10 +32,15 @@ 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 and integer variables, respectively. +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. From 3bf1ad4023101d38622a383b250af88a0dc5887a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Wed, 18 Oct 2023 17:13:35 +0200 Subject: [PATCH 56/81] Update src/lmo_wrapper.jl --- src/lmo_wrapper.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index c2c601005..fcf8911df 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -16,7 +16,6 @@ where x has to be an integer feasible point """ function compute_extreme_point end -## """ Read global bounds from the problem. """ From e31022360bd7c3fd63565b649f123af528735a72 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 18:46:02 +0200 Subject: [PATCH 57/81] LMO over the cube. --- examples/cube_blmo.jl | 218 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 examples/cube_blmo.jl diff --git a/examples/cube_blmo.jl b/examples/cube_blmo.jl new file mode 100644 index 000000000..c125ed0e1 --- /dev/null +++ b/examples/cube_blmo.jl @@ -0,0 +1,218 @@ +using Dates + +""" + CubeBLMO + +A Bounded Linear Minimization Oracle over a cube. +""" +mutable struct CubeBLMO <: Boscia.BoundedLinearMinimizationOracle + n::Int + int_vars::Vector{Int} + bin_vars::Vector{Int} + bounds::Boscia.IntegerBounds + solving_time +end + +CubeBLMO(n, int_vars, bin_vars, bounds) = CubeBLMO(n, int_vars, bin_vars, bounds, 0.0) + +## Necessary +""" +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 Boscia.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 + +## +""" +Read global bounds from the problem. +""" +function Boscia.build_global_bounds(blmo::CubeBLMO, integer_variables) + global_bounds = Boscia.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 +""" +Add explicit bounds for binary variables, if not already done from the get-go. +""" +function Boscia.explicit_bounds_binary_var(blmo::CubeBLMO, gb::Boscia.IntegerBounds, binary_vars) + nothing +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 Boscia.get_list_of_variables(blmo::CubeBLMO) + return blmo.n, collect(1:blmo.n) + end +""" +Get list of binary and integer variables, respectively. +""" +function Boscia.get_binary_variables(blmo::CubeBLMO) + return blmo.bin_vars +end +function Boscia.get_integer_variables(blmo::CubeBLMO) + return blmo.int_vars +end +""" +Get the index of the integer variable the bound is working on. +""" +function Boscia.get_int_var(blmo::CubeBLMO, cidx) + return cidx +end +""" +Get the list of lower bounds. +""" +function Boscia.get_lower_bound_list(blmo::CubeBLMO) + return keys(blmo.bounds.lower_bounds) +end +""" +Get the list of upper bounds. +""" +function Boscia.get_upper_bound_list(blmo::CubeBLMO) + return keys(blmo.bounds.upper_bounds) +end +""" +Read bound value for c_idx. +""" +function Boscia.get_lower_bound(blmo::CubeBLMO, c_idx) + return blmo.bounds[c_idx, :greaterthan] +end +function Boscia.get_upper_bound(blmo::CubeBLMO, c_idx) + return blmo.bounds[c_idx, :lessthan] +end + +## Changing the bounds constraints. +""" +Change the value of the bound c_idx. +""" +function Boscia.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 +""" +Delete bounds. +""" +function Boscia.delete_bounds!(blmo::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 +""" +Add bound constraint. +""" +function Boscia.add_bound_constraint!(blmo::CubeBLMO, key, value) + # Should not be necessary + error("Trying to add bound constraints of the cube!") +end + +## Checks +""" +Check if the subject of the bound c_idx is an integer variable (recorded in int_vars). +""" +function Boscia.is_constraint_on_int_var(blmo::CubeBLMO, c_idx, int_vars) + return c_idx in int_vars +end +""" +To check if there is bound for the variable in the global or node bounds. +""" +function Boscia.is_bound_in(blmo::CubeBLMO, c_idx, bounds) + return haskey(bounds, c_idx) +end +""" +Is a given point v linear feasible for the model? +That means does v satisfy all bounds and other linear constraints? +""" +function Boscia.is_linear_feasible(blmo::CubeBLMO, v::AbstractVector) + for i in eachindex(v) + if !(blmo.bounds[i, :greaterthan].lower ≤ v[i] ≤ 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 +""" +Has variable a binary constraint? +""" +function Boscia.has_binary_constraint(blmo::CubeBLMO, idx) + return idx in blmo.int_vars +end +""" +Has variable an integer constraint? +""" +function Boscia.has_integer_constraint(blmo::CubeBLMO, idx) + return idx in blmo.bin_vars +end + + + +###################### Optional +## Safety Functions +""" +Check if the bounds were set correctly in build_LMO. +Safety check only. +""" +function Boscia.build_LMO_correct(blmo::CubeBLMO, node_bounds) + for key in keys(node_bounds.lower_bounds) + if !haskey(blmo.bounds, (i, :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 + +""" +Check if problem is bounded and feasible, i.e. no contradicting constraints. +""" +function Boscia.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 + +""" +Check whether a split is valid, i.e. the upper and lower on variable vidx are not the same. +""" +function Boscia.is_valid_split(tree, blmo::CubeBLMO, vidx::Int) + return blmo.bounds[vidx, :lessthan] != blmo.bounds[vidx, :greaterthan] +end + +## Logs +""" +Get solve time, number of nodes and number of iterations, if applicable. +""" +function Boscia.get_BLMO_solve_data(blmo::CubeBLMO) + return blmo.solving_time, 0.0, 0.0 +end From 297178b1cd341c4ccc9129b4345e833bd59f7f10 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 18:46:40 +0200 Subject: [PATCH 58/81] Approx planted point with LMO over cube. --- examples/approx_planted_point.jl | 52 +++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/examples/approx_planted_point.jl b/examples/approx_planted_point.jl index b0b46a59d..92e5d393a 100644 --- a/examples/approx_planted_point.jl +++ b/examples/approx_planted_point.jl @@ -9,21 +9,12 @@ using Distributions import MathOptInterface const MOI = MathOptInterface +include("cube_blmo.jl") 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 +23,41 @@ 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) + + x_scip = x + 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(0.0))) + end + blmo = 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 + From 70674ee93ab3f1280d6e781e460526e703a31fe6 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 18:47:15 +0200 Subject: [PATCH 59/81] Split up get_bound. --- src/MOI_bounded_oracle.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 155735463..78764ef94 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -94,7 +94,10 @@ end """ Read bound value for c_idx. """ -function get_bound(blmo, 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 From 2f58f3c66d40f2ab24756713260800904e165fb4 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 18:47:28 +0200 Subject: [PATCH 60/81] Minor change. --- src/build_lmo.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/build_lmo.jl b/src/build_lmo.jl index 9ca04aa6e..16ddc3a17 100644 --- a/src/build_lmo.jl +++ b/src/build_lmo.jl @@ -27,10 +27,10 @@ function build_LMO( 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.lower_bounds[v_idx]) + set_bound!(blmo, c_idx, node_bounds[v_idx, :greaterthan]) # keep else - set_bound!(blmo, c_idx, global_bounds.lower_bounds[v_idx]) + set_bound!(blmo, c_idx, global_bounds[v_idx, :greaterthan]) end else # Delete @@ -46,10 +46,10 @@ function build_LMO( 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.upper_bounds[v_idx]) + set_bound!(blmo, c_idx, node_bounds[v_idx, :lessthan]) # keep else - set_bound!(blmo, c_idx, global_bounds.upper_bounds[v_idx]) + set_bound!(blmo, c_idx, global_bounds[v_idx, :lessthan]) end else # Delete From 098e7a78b238d27a1ab46fa12caeb4eaa517d7eb Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 18:47:40 +0200 Subject: [PATCH 61/81] Fomratting change. --- src/lmo_wrapper.jl | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl index 97a4d296a..f33910ac1 100644 --- a/src/lmo_wrapper.jl +++ b/src/lmo_wrapper.jl @@ -16,82 +16,101 @@ 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 and integer variables, respectively. +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. @@ -105,18 +124,21 @@ 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. """ @@ -130,12 +152,14 @@ 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). @@ -143,18 +167,21 @@ 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)`. @@ -165,6 +192,7 @@ function get_variables_pointers(blmo::BoundedLinearMinimizationOracle, tree) return collect(1:N) end + ## Logs """ Get solve time, number of nodes and number of iterations, if applicable. From f6fe5f98612606dcb53e8e0eaa4d681062747531 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 18:47:53 +0200 Subject: [PATCH 62/81] Minor fix. --- src/callbacks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/callbacks.jl b/src/callbacks.jl index e974d62d7..7b29a90be 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -19,7 +19,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.tlmo.blmo.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[]] From 13d8e93e6363eacb54b44a5683a4f110e70ff316 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Wed, 18 Oct 2023 18:48:03 +0200 Subject: [PATCH 63/81] minor fix. --- src/integer_bounds.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integer_bounds.jl b/src/integer_bounds.jl index 1283ffbc9..931836106 100644 --- a/src/integer_bounds.jl +++ b/src/integer_bounds.jl @@ -65,9 +65,9 @@ end function Base.haskey(ib::IntegerBounds, (idx, sense)) if sense == :lessthan - haskey(ib.upper_bounds, idx) + return haskey(ib.upper_bounds, idx) else - haskey(ib.lower_bounds, idx) + return haskey(ib.lower_bounds, idx) end end From e32de1ba5a98db5eecafbff2fb3dd7049fcaefa2 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 10:45:35 +0200 Subject: [PATCH 64/81] Minor fix. --- examples/cube_blmo.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/cube_blmo.jl b/examples/cube_blmo.jl index c125ed0e1..2ebcba30b 100644 --- a/examples/cube_blmo.jl +++ b/examples/cube_blmo.jl @@ -1,4 +1,5 @@ using Dates +using Bonobo """ CubeBLMO @@ -178,7 +179,7 @@ Safety check only. """ function Boscia.build_LMO_correct(blmo::CubeBLMO, node_bounds) for key in keys(node_bounds.lower_bounds) - if !haskey(blmo.bounds, (i, :greaterthan)) || blmo.bounds[key, :greaterthan] != node_bounds[key, :greaterthan] + if !haskey(blmo.bounds, (key, :greaterthan)) || blmo.bounds[key, :greaterthan] != node_bounds[key, :greaterthan] return false end end @@ -205,7 +206,7 @@ end """ Check whether a split is valid, i.e. the upper and lower on variable vidx are not the same. """ -function Boscia.is_valid_split(tree, blmo::CubeBLMO, vidx::Int) +function Boscia.is_valid_split(tree::Bonobo.BnBTree, blmo::CubeBLMO, vidx::Int) return blmo.bounds[vidx, :lessthan] != blmo.bounds[vidx, :greaterthan] end From b79f45a8142765cfeceaabf1348fc74b6711f454 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 10:46:37 +0200 Subject: [PATCH 65/81] Move the cube LMO into the source files. --- {examples => src}/cube_blmo.jl | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {examples => src}/cube_blmo.jl (100%) diff --git a/examples/cube_blmo.jl b/src/cube_blmo.jl similarity index 100% rename from examples/cube_blmo.jl rename to src/cube_blmo.jl From 9057ee74289d86e6ba74d8f642a0ce01817cdd96 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 10:48:12 +0200 Subject: [PATCH 66/81] Cube BLMO now part of source, sees all dependencies. --- src/Boscia.jl | 1 + src/cube_blmo.jl | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Boscia.jl b/src/Boscia.jl index 96f32d4f3..aac7ba34b 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -28,5 +28,6 @@ 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/cube_blmo.jl b/src/cube_blmo.jl index 2ebcba30b..39307a99b 100644 --- a/src/cube_blmo.jl +++ b/src/cube_blmo.jl @@ -1,6 +1,3 @@ -using Dates -using Bonobo - """ CubeBLMO From 005db869ccc73226661fec5138db0a77daa6b862 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 11:22:39 +0200 Subject: [PATCH 67/81] Minor change. --- examples/approx_planted_point.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/approx_planted_point.jl b/examples/approx_planted_point.jl index 92e5d393a..24ed4a441 100644 --- a/examples/approx_planted_point.jl +++ b/examples/approx_planted_point.jl @@ -9,8 +9,6 @@ using Distributions import MathOptInterface const MOI = MathOptInterface -include("cube_blmo.jl") - n = 20 diffi = Random.rand(Bool, n) * 0.6 .+ 0.3 @@ -50,9 +48,9 @@ diffi = Random.rand(Bool, n) * 0.6 .+ 0.3 bounds = Boscia.IntegerBounds() for i in 1:n push!(bounds, (i, MOI.GreaterThan(0.0))) - push!(bounds, (i, MOI.LessThan(0.0))) + push!(bounds, (i, MOI.LessThan(1.0))) end - blmo = CubeBLMO(n, int_vars, bin_vars, bounds) + blmo = Boscia.CubeBLMO(n, int_vars, bin_vars, bounds) x, _, result = Boscia.solve(f, grad!, blmo, verbose =true) From 5e293ce67a5b21ba74309f9735fa30f1497ecb94 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 12:27:30 +0200 Subject: [PATCH 68/81] Account for numerical inaccuracies. --- src/cube_blmo.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cube_blmo.jl b/src/cube_blmo.jl index 39307a99b..56f4c1549 100644 --- a/src/cube_blmo.jl +++ b/src/cube_blmo.jl @@ -146,7 +146,7 @@ That means does v satisfy all bounds and other linear constraints? """ function Boscia.is_linear_feasible(blmo::CubeBLMO, v::AbstractVector) for i in eachindex(v) - if !(blmo.bounds[i, :greaterthan].lower ≤ v[i] ≤ blmo.bounds[i, :lessthan].upper) + 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 From f6f1fc2b051887c483d9e4e9fa38e26c44ee74c0 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 12:27:52 +0200 Subject: [PATCH 69/81] Renaming.2 --- src/interface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.jl b/src/interface.jl index bfa861805..1cece5890 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -285,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) From 7321fdebce1d1fc86bb8e66ee0bb5fba5a1e3779 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 12:28:13 +0200 Subject: [PATCH 70/81] Renaming of fields. --- src/node.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node.jl b/src/node.jl index 2ebb14c25..d28baeee6 100644 --- a/src/node.jl +++ b/src/node.jl @@ -252,9 +252,9 @@ function Bonobo.evaluate_node!(tree::Bonobo.BnBTree, node::FrankWolfeNode) # 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 From b6d9fa518881a6b2aad0fa33ccf786e9d2fd5004 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 13:23:48 +0200 Subject: [PATCH 71/81] Beware of numerical rounding issues. --- src/interface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.jl b/src/interface.jl index 1cece5890..baa166f04 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -490,7 +490,7 @@ function build_bnb_callback( 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) From 32bd9f1e7801f437bebf23c6c49aa43858379da2 Mon Sep 17 00:00:00 2001 From: Hendrych Date: Thu, 19 Oct 2023 13:24:33 +0200 Subject: [PATCH 72/81] Both nodes can be pruned if one of them actually reaches the incumbent. Then we have proof of optimality by strong convexity. --- src/node.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node.jl b/src/node.jl index d28baeee6..c90705f27 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 From b039daffa9e3cc1b11cc1580a76a3ae219710ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 19 Oct 2023 14:59:14 +0200 Subject: [PATCH 73/81] Update src/MOI_bounded_oracle.jl --- src/MOI_bounded_oracle.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index 78764ef94..aa440177c 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -148,7 +148,7 @@ function has_binary_constraint(blmo::MathOptBLMO, idx::Int) end """ -Has variable an integer constraint? +Does the variable have an integer constraint? """ function has_integer_constraint(blmo::MathOptBLMO, idx::Int) consB_list = MOI.get( From b91d306372899f2e69ff64ef24d36ae227feadd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 19 Oct 2023 14:59:40 +0200 Subject: [PATCH 74/81] Update src/cube_blmo.jl --- src/cube_blmo.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cube_blmo.jl b/src/cube_blmo.jl index 56f4c1549..1faaa76a1 100644 --- a/src/cube_blmo.jl +++ b/src/cube_blmo.jl @@ -154,7 +154,7 @@ function Boscia.is_linear_feasible(blmo::CubeBLMO, v::AbstractVector) return true end """ -Has variable a binary constraint? +Does the variable have a binary constraint? """ function Boscia.has_binary_constraint(blmo::CubeBLMO, idx) return idx in blmo.int_vars From 1a5ead0e123cbc65675a4144436706a84380b1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 19 Oct 2023 15:00:14 +0200 Subject: [PATCH 75/81] Update src/MOI_bounded_oracle.jl --- src/MOI_bounded_oracle.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MOI_bounded_oracle.jl b/src/MOI_bounded_oracle.jl index aa440177c..3f801c0ae 100644 --- a/src/MOI_bounded_oracle.jl +++ b/src/MOI_bounded_oracle.jl @@ -77,6 +77,7 @@ 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. """ From 231319e33ed3383c05449a9085e47b2aacb7f4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 19 Oct 2023 15:00:38 +0200 Subject: [PATCH 76/81] Update src/cube_blmo.jl --- src/cube_blmo.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cube_blmo.jl b/src/cube_blmo.jl index 1faaa76a1..8e8961340 100644 --- a/src/cube_blmo.jl +++ b/src/cube_blmo.jl @@ -60,6 +60,7 @@ If the problem has n variables, they are expected to contiguous and ordered from function Boscia.get_list_of_variables(blmo::CubeBLMO) return blmo.n, collect(1:blmo.n) end + """ Get list of binary and integer variables, respectively. """ From 670bce845bf06cc9bf06c127d3bbab8b4216013f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 19 Oct 2023 15:14:26 +0200 Subject: [PATCH 77/81] cleanup --- src/Boscia.jl | 2 +- src/cube_blmo.jl | 160 ++++++++++++--------------------- src/heuristics.jl | 1 - src/integer_bounds.jl | 18 +--- src/lmo_wrapper.jl | 202 ------------------------------------------ src/node.jl | 14 --- 6 files changed, 57 insertions(+), 340 deletions(-) delete mode 100644 src/lmo_wrapper.jl diff --git a/src/Boscia.jl b/src/Boscia.jl index aac7ba34b..f247b77de 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -15,7 +15,7 @@ const MOIU = MOI.Utilities import MathOptSetDistances as MOD include("integer_bounds.jl") -include("lmo_wrapper.jl") +include("blmo_interface.jl") include("time_tracking_lmo.jl") include("build_lmo.jl") include("frank_wolfe_variants.jl") diff --git a/src/cube_blmo.jl b/src/cube_blmo.jl index 8e8961340..47a26372f 100644 --- a/src/cube_blmo.jl +++ b/src/cube_blmo.jl @@ -1,42 +1,36 @@ + """ CubeBLMO - -A Bounded Linear Minimization Oracle over a cube. + +A Bounded Linear Minimization Oracle over a cube. """ -mutable struct CubeBLMO <: Boscia.BoundedLinearMinimizationOracle +mutable struct CubeBLMO <: BoundedLinearMinimizationOracle n::Int int_vars::Vector{Int} bin_vars::Vector{Int} - bounds::Boscia.IntegerBounds - solving_time + bounds::IntegerBounds + solving_time::Float64 end CubeBLMO(n, int_vars, bin_vars, bounds) = CubeBLMO(n, int_vars, bin_vars, bounds, 0.0) ## Necessary -""" -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 Boscia.compute_extreme_point(blmo::CubeBLMO, d; kwargs...) +# 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 + end blmo.solving_time = float(Dates.value(Dates.now() - time_ref)) return v end -## -""" -Read global bounds from the problem. -""" -function Boscia.build_global_bounds(blmo::CubeBLMO, integer_variables) - global_bounds = Boscia.IntegerBounds() +## + +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])) @@ -45,64 +39,47 @@ function Boscia.build_global_bounds(blmo::CubeBLMO, integer_variables) end return global_bounds end -""" -Add explicit bounds for binary variables, if not already done from the get-go. -""" -function Boscia.explicit_bounds_binary_var(blmo::CubeBLMO, gb::Boscia.IntegerBounds, binary_vars) + +function explicit_bounds_binary_var(blmo::CubeBLMO, gb::IntegerBounds, binary_vars) nothing 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 Boscia.get_list_of_variables(blmo::CubeBLMO) +function get_list_of_variables(blmo::CubeBLMO) return blmo.n, collect(1:blmo.n) end -""" -Get list of binary and integer variables, respectively. -""" -function Boscia.get_binary_variables(blmo::CubeBLMO) +# Get list of binary and integer variables, respectively. +function get_binary_variables(blmo::CubeBLMO) return blmo.bin_vars end -function Boscia.get_integer_variables(blmo::CubeBLMO) + +function get_integer_variables(blmo::CubeBLMO) return blmo.int_vars -end -""" -Get the index of the integer variable the bound is working on. -""" -function Boscia.get_int_var(blmo::CubeBLMO, cidx) +end + +function get_int_var(blmo::CubeBLMO, cidx) return cidx end -""" -Get the list of lower bounds. -""" -function Boscia.get_lower_bound_list(blmo::CubeBLMO) + +function get_lower_bound_list(blmo::CubeBLMO) return keys(blmo.bounds.lower_bounds) end -""" -Get the list of upper bounds. -""" -function Boscia.get_upper_bound_list(blmo::CubeBLMO) + +function get_upper_bound_list(blmo::CubeBLMO) return keys(blmo.bounds.upper_bounds) -end -""" -Read bound value for c_idx. -""" -function Boscia.get_lower_bound(blmo::CubeBLMO, c_idx) +end + +function get_lower_bound(blmo::CubeBLMO, c_idx) return blmo.bounds[c_idx, :greaterthan] end -function Boscia.get_upper_bound(blmo::CubeBLMO, c_idx) + +function get_upper_bound(blmo::CubeBLMO, c_idx) return blmo.bounds[c_idx, :lessthan] end ## Changing the bounds constraints. -""" -Change the value of the bound c_idx. -""" -function Boscia.set_bound!(blmo::CubeBLMO, c_idx, value) +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} @@ -111,41 +88,30 @@ function Boscia.set_bound!(blmo::CubeBLMO, c_idx, value) error("We expect the value to be of type MOI.GreaterThan or Moi.LessThan!") end end -""" -Delete bounds. -""" -function Boscia.delete_bounds!(blmo::CubeBLMO, cons_delete) + +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 -""" -Add bound constraint. -""" -function Boscia.add_bound_constraint!(blmo::CubeBLMO, key, value) + +function add_bound_constraint!(::CubeBLMO, key, value) # Should not be necessary error("Trying to add bound constraints of the cube!") end -## Checks -""" -Check if the subject of the bound c_idx is an integer variable (recorded in int_vars). -""" -function Boscia.is_constraint_on_int_var(blmo::CubeBLMO, c_idx, int_vars) +## Checks + +function is_constraint_on_int_var(blmo::CubeBLMO, c_idx, int_vars) return c_idx in int_vars end -""" -To check if there is bound for the variable in the global or node bounds. -""" -function Boscia.is_bound_in(blmo::CubeBLMO, c_idx, bounds) + +function is_bound_in(blmo::CubeBLMO, c_idx, bounds) return haskey(bounds, c_idx) end -""" -Is a given point v linear feasible for the model? -That means does v satisfy all bounds and other linear constraints? -""" -function Boscia.is_linear_feasible(blmo::CubeBLMO, v::AbstractVector) + +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))") @@ -154,16 +120,12 @@ function Boscia.is_linear_feasible(blmo::CubeBLMO, v::AbstractVector) end return true end -""" -Does the variable have a binary constraint? -""" -function Boscia.has_binary_constraint(blmo::CubeBLMO, idx) + +function has_binary_constraint(blmo::CubeBLMO, idx) return idx in blmo.int_vars end -""" -Has variable an integer constraint? -""" -function Boscia.has_integer_constraint(blmo::CubeBLMO, idx) + +function has_integer_constraint(blmo::CubeBLMO, idx) return idx in blmo.bin_vars end @@ -171,11 +133,8 @@ end ###################### Optional ## Safety Functions -""" -Check if the bounds were set correctly in build_LMO. -Safety check only. -""" -function Boscia.build_LMO_correct(blmo::CubeBLMO, node_bounds) + +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 @@ -185,15 +144,12 @@ function Boscia.build_LMO_correct(blmo::CubeBLMO, node_bounds) if !haskey(blmo.bounds, (key, :lessthan)) || blmo.bounds[key, :lessthan] != node_bounds[key, :lessthan] return false end - end + end return true end -""" -Check if problem is bounded and feasible, i.e. no contradicting constraints. -""" -function Boscia.check_feasibility(blmo::CubeBLMO) - for i in 1:blmo.n +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 @@ -201,17 +157,11 @@ function Boscia.check_feasibility(blmo::CubeBLMO) return MOI.OPTIMAL end -""" -Check whether a split is valid, i.e. the upper and lower on variable vidx are not the same. -""" -function Boscia.is_valid_split(tree::Bonobo.BnBTree, blmo::CubeBLMO, vidx::Int) +function is_valid_split(tree::Bonobo.BnBTree, blmo::CubeBLMO, vidx::Int) return blmo.bounds[vidx, :lessthan] != blmo.bounds[vidx, :greaterthan] end ## Logs -""" -Get solve time, number of nodes and number of iterations, if applicable. -""" -function Boscia.get_BLMO_solve_data(blmo::CubeBLMO) +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 816c3d9cf..bc6719bd6 100644 --- a/src/heuristics.jl +++ b/src/heuristics.jl @@ -1,4 +1,3 @@ -#### TO DO: Implement Feasibility Pump? """ Finds the best solution in the SCIP solution storage, based on the objective function `f`. diff --git a/src/integer_bounds.jl b/src/integer_bounds.jl index 931836106..23f61d792 100644 --- a/src/integer_bounds.jl +++ b/src/integer_bounds.jl @@ -1,7 +1,3 @@ -""" -Constant to handle half open interval bounds on variables -""" -const inf_bound = 10.0^6 """ IntegerBounds @@ -11,7 +7,7 @@ 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}}} +mutable struct IntegerBounds lower_bounds::Dict{Int,MOI.GreaterThan{Float64}} upper_bounds::Dict{Int,MOI.LessThan{Float64}} end @@ -28,9 +24,6 @@ function Base.push!(ib::IntegerBounds, (idx, bound)) 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 @@ -70,12 +63,3 @@ function Base.haskey(ib::IntegerBounds, (idx, sense)) return 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 =# diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl deleted file mode 100644 index f33910ac1..000000000 --- a/src/lmo_wrapper.jl +++ /dev/null @@ -1,202 +0,0 @@ -""" - 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/node.jl b/src/node.jl index c90705f27..a9ed2fc12 100644 --- a/src/node.jl +++ b/src/node.jl @@ -240,20 +240,6 @@ function Bonobo.evaluate_node!(tree::Bonobo.BnBTree, node::FrankWolfeNode) return NaN, NaN end - #= if isempty(node.active_set) - consI_list = MOI.get( - tree.root.problem.tlmo.blmo.o, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}(), - ) + MOI.get( - tree.root.problem.tlmo.blmo.o, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}(), - ) - if !isempty(consI_list) - @error "Unreachable node! Active set is empty!" - end - restart_active_set(node, tree.root.problem.tlmo.blmo, tree.root.problem.nvars) - end =# - # Check feasibility of the iterate active_set = node.active_set x = FrankWolfe.compute_active_set_iterate!(node.active_set) From 854dc7b7f506f9b14fe02d00a9fcab899f7eb581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 19 Oct 2023 15:14:46 +0200 Subject: [PATCH 78/81] readd file --- src/blmo_interface.jl | 203 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/blmo_interface.jl 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 From 6e0f597abb991fcd4fabd7df485cd19bb0485b56 Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Thu, 19 Oct 2023 18:51:40 +0200 Subject: [PATCH 79/81] Fix merge conflicts. --- src/Boscia.jl | 3 +- src/bounds.jl | 267 --------------------------------------------- src/lmo_wrapper.jl | 87 --------------- 3 files changed, 1 insertion(+), 356 deletions(-) delete mode 100644 src/bounds.jl delete mode 100644 src/lmo_wrapper.jl diff --git a/src/Boscia.jl b/src/Boscia.jl index a48938598..3068372d4 100644 --- a/src/Boscia.jl +++ b/src/Boscia.jl @@ -17,9 +17,8 @@ import MathOptSetDistances as MOD include("integer_bounds.jl") include("blmo_interface.jl") include("time_tracking_lmo.jl") -include("bounds.jl") -include("lmo_wrapper.jl") include("frank_wolfe_variants.jl") +include("build_lmo.jl") include("node.jl") include("custom_bonobo.jl") include("callbacks.jl") diff --git a/src/bounds.jl b/src/bounds.jl deleted file mode 100644 index fc179cc33..000000000 --- a/src/bounds.jl +++ /dev/null @@ -1,267 +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)=# - - - """ -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 data from previous nodes - free_model(lmo.o) - - consLB_list = get_lower_bounds_list(blmo) - consUB_list = get_upper_bounds_list(blmo) - cons_delete = [] - - # Lower bounds - for c_idx in consLB_list - v_idx = get_variable_index(blmo, c_idx) - if is_constraint_on_int_var(c_idx, int_vars) - if is_constraint_in(global_bounds.lower_bounds, c_idx) - # change - if is_constraint_in(node.lower_bounds, c_idx) - set_lower_bound(blmo, c_idx, node_bounds.lower_bounds[v_idx]) - # keep - else - set_lower_bound(blmo, c_idx, global_bounds.lower_bounds[v_idx]) - end - else - # delete - push!(cons_delete, c_idx) - end - end - end - - # Upper bounds - for c_idx in consUB_list - v_idx = get_variable_index(blmo, c_idx) - if is_constraint_on_int_var(c_idx, int_vars) - if is_constraint_in(global_bounds.uppers_bounds, c_idx) - # change - if is_constraint_in(node.uppers_bounds, c_idx) - set_upper_bound(blmo, c_idx, node_bounds.upper_bounds[v_idx]) - # keep - else - set_upper_bound(blmo, c_idx, global_bounds.upper_bounds[v_idx]) - end - else - # delete - push!(cons_delete, c_idx) - end - end - end - - # delete constraints - delete_constraints!(blmo, cons_delete) - - # add node specific constraints - for key in keys(node_bounds.lower_bounds) - if !is_constraint_in(global_bounds.lower_bounds, key) - add_constraint!(blmo, key, node_bounds.lower_bounds[key]) - end - end - for key in keys(node_bounds.upper_bounds) - if !is_constraint_in(global_bounds.upper_bounds, key) - add_constraint!(blmo, key, node_bounds.upper_bounds[key]) - end - end - - @assert check_bounds(lmo, node_bounds) -end - -build_LMO(tt_lmo::TimeTrackingLMO, gb::IntegerBounds, nb::IntegerBounds, int_vars::Vector{Int64}) = - build_LMO(tt_lmo.blmo, gb, nb, int_vars) diff --git a/src/lmo_wrapper.jl b/src/lmo_wrapper.jl deleted file mode 100644 index 82a05cadc..000000000 --- a/src/lmo_wrapper.jl +++ /dev/null @@ -1,87 +0,0 @@ -""" - MILMO - -Supertype for the Bounded Integer Linear Minimization Oracles -""" -abstract type BoundedLinearMinimizationOracle end - -""" -Multiple dispatch of FrankWolfe.compute_extreme_point -""" -function compute_extreme_point end - -""" -""" -function optimize! end - -""" -Get list of lower bounds. -""" -function get_lower_bounds_list end - -""" -Get list of upper bounds. -""" -function get_upper_bounds_list end - -""" -Get lower bound of variable. -""" -function get_lower_bound end - -""" -Get upper bound of variable. -""" -function get_upper_bound end - -""" -Set new lower bound. -""" -function set_lower_bound! end - -""" -Set new upper bound. -""" -function set_upper_bound! end - -""" -Check if the bound constraint is on an integral variable. -""" -function is_constraint_on_int_var end - -""" -Is the constraint part of this contraints set. Used to check membership to the global and node bounds. -""" -function is_constraint_in end - -""" -Delete constraints that are not needed anymore. -""" -function delete_constraints! end - -""" -Add new bound constraint. -""" -function add_constraint! end - -""" -Get the variable of the bound constraint. -""" -function get_variable_index(blmo::BoundedLinearMinimizationOracle, c_idx) - return c_idx -end - -""" -Check that all node bounds were set correctly. Not necessary -""" -function check_bounds(blmo::BoundedLinearMinimizationOracle, node_bounds) - return true - end - - """ - Check that the problem is feasible, i.e. there are no contradicting constraints, and that it is bounded. - """ -function check_feasibility(blmo::BoundedLinearMinimizationOracle) - return -end - From ca50fc3ea786442ead79d596404e91dd245ae70c Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Thu, 19 Oct 2023 18:52:57 +0200 Subject: [PATCH 80/81] Minor change. --- examples/approx_planted_point.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/approx_planted_point.jl b/examples/approx_planted_point.jl index 24ed4a441..c9cb2091f 100644 --- a/examples/approx_planted_point.jl +++ b/examples/approx_planted_point.jl @@ -37,8 +37,6 @@ diffi = Random.rand(Bool, n) * 0.6 .+ 0.3 @test x == round.(diffi) @test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3) - - x_scip = x end @testset "Using Cube LMO" begin From 1a9ef2897fff110f000bf443b518de0e2993bd70 Mon Sep 17 00:00:00 2001 From: Deborah Hendrych Date: Thu, 19 Oct 2023 18:53:23 +0200 Subject: [PATCH 81/81] Hand down kwargs... --- src/interface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.jl b/src/interface.jl index baa166f04..c7cba9686 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -60,7 +60,7 @@ function solve( domain_oracle=domain_oracle, start_solution=start_solution, fw_verbose=fw_verbose, - kwargs + kwargs... ) end