Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heuristics Interface #162

Merged
merged 34 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
99429a0
Add heuristic interface.
Nov 15, 2023
dac2663
Max probility for predefined heuristics.
Nov 15, 2023
de62fe4
Call heuristics at node level.
Nov 15, 2023
606b211
Rounding is moving to the heuristics.
Nov 15, 2023
77f89f8
Add rounding as a standard heuristic.
Nov 15, 2023
c8b2ab5
Add test with a start active set.
Nov 15, 2023
94d73d4
Add rounding as first standard heuristic.
Nov 15, 2023
0f98532
Round most of the time.
Nov 15, 2023
a73e336
Heuristic test.
Nov 15, 2023
a3048b6
Merge changes from main
Nov 15, 2023
39aad6b
More documenation and checking feasibility is optional.
Nov 15, 2023
72f4b22
Custom list of heuristics.
Nov 15, 2023
f1edab0
Predefined heuristics for Probability and Unit simplex.
Nov 15, 2023
72d7a72
Heuristics can return list of possible solutions.
Nov 16, 2023
b8791c9
The floor function can be told to return an integer.
Nov 16, 2023
8580546
minor corrections.
Nov 16, 2023
d556004
Renaming.
Nov 16, 2023
ab41c79
bumped standard rounding to prob 1.0
pokutta Nov 19, 2023
c65876d
Minor fixes.
Jan 9, 2024
7b06216
Standard rounding might not always be cheap to check for feasibility.
Jan 9, 2024
022136d
Track LMO calls in the heuristics.
Jan 9, 2024
8572767
Merge changes.
Jan 9, 2024
204e2b9
Minor change.
Jan 9, 2024
4cd3979
minor change in printing of the statistics.
Jan 10, 2024
21ef4b2
minor change.
Jan 10, 2024
4bd316e
Probability rounding.
Jan 10, 2024
52ccfd9
minor fix.
Jan 10, 2024
75c6425
Error hunt.
Jan 10, 2024
4c39f09
Reset LMO also then the heuristic LMO is infeasible.
Jan 11, 2024
6b80b94
Added debug statement.
Jan 11, 2024
f37c1f8
Correction.
Jan 11, 2024
a30327f
Corrections.
Jan 11, 2024
76943ad
Add heuristics test suite.
Jan 11, 2024
1b554c9
Verbose off.
Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion examples/nonlinear.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ using LinearAlgebra
import MathOptInterface
using Random
using Boscia
using Bonobo
import Bonobo
using Printf
using Dates
Expand Down Expand Up @@ -66,6 +67,52 @@ end
# benchmarking Oracles
FrankWolfe.benchmark_oracles(f, grad!, () -> rand(n), lmo; k=100)

x, _, _ = Boscia.solve(f, grad!, lmo, verbose=true, print_iter=500)
#################


# are these lmo calls counted as well?

# #####
# # follow the gradient for a fixed number of steps and collect solutions on the way
# #####

# function follow_gradient_heuristic(tree::Bonobo.BnBTree, blmo::Boscia.BoundedLinearMinimizationOracle, x, k)
# nabla = similar(x)
# x_new = copy(x)
# sols = []
# for i in 1:k
# tree.root.problem.g(nabla,x_new)
# x_new = Boscia.compute_extreme_point(blmo, nabla)
# push!(sols, x_new)
# end
# return sols, false
# end

# #####
# # rounding respecting the hidden feasible region structure
# #####

# function rounding_lmo_01_heuristic(tree::Bonobo.BnBTree, blmo::Boscia.BoundedLinearMinimizationOracle, x)
# nabla = zeros(length(x))
# for idx in tree.branching_indices
# nabla[idx] = 1 - 2*round(x[idx]) # (0.7, 0.3) -> (1, 0) -> (-1, 1) -> min -> (1,0)
# end
# x_rounded = Boscia.compute_extreme_point(blmo, nabla)
# return [x_rounded], false
# end

#####
# geometric scaling like for a couple of steps
#####


depth = 5
heu = Boscia.Heuristic((tree, blmo, x) -> Boscia.follow_gradient_heuristic(tree,blmo,x, depth), 0.8, :follow_gradient)
heu2 = Boscia.Heuristic(Boscia.rounding_lmo_01_heuristic, 0.8, :lmo_rounding)

heuristics = [heu, heu2]
# heuristics = []

x, _, _ = Boscia.solve(f, grad!, lmo, verbose=true, print_iter=500, custom_heuristics=heuristics)

@show x
14 changes: 9 additions & 5 deletions examples/portfolio.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ using Test
using Random
using SCIP
using LinearAlgebra
using Distributions
import MathOptInterface
const MOI = MathOptInterface

# seed = 0x946d4b7835e92ffa takes 90 minutes to solve! -> not anymore
seed = 0x946d4b7835e92ffa
Random.seed!(seed)

n = 30
const ri = rand(n)
Expand Down Expand Up @@ -53,10 +55,12 @@ const Mi = (Ai + Ai') / 2
return storage
end

x, _, result = Boscia.solve(f, grad!, lmo, verbose=true, time_limit=600)
depth = 5
heu = Boscia.Heuristic((tree, blmo, x) -> Boscia.follow_gradient_heuristic(tree,blmo,x, depth), 0.2, :follow_gradient)
heuristics = [heu]
# heuristics = []

x, _, result = Boscia.solve(f, grad!, lmo, verbose=true, time_limit=600, custom_heuristics=heuristics)
@test dot(ai, x) <= bi + 1e-2
@test f(x) <= f(result[:raw_solution]) + 1e-6
end


# seed = 0x946d4b7835e92ffa takes 90 minutes to solve!
4 changes: 4 additions & 0 deletions src/MOI_bounded_oracle.jl
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"""
Read bound value for c_idx.
"""
function get_bound(blmo::MathOptBLMO, c_idx, sense::Symbol)

Check warning on line 109 in src/MOI_bounded_oracle.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_bounded_oracle.jl#L109

Added line #L109 was not covered by tests
return MOI.get(blmo.o, MOI.ConstraintSet(), c_idx)
end

Expand Down Expand Up @@ -271,7 +271,7 @@
Check if the bounds were set correctly in build_LMO.
Safety check only.
"""
function build_LMO_correct(blmo, node_bounds)

Check warning on line 274 in src/MOI_bounded_oracle.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_bounded_oracle.jl#L274

Added line #L274 was not covered by tests
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)
Expand Down Expand Up @@ -391,7 +391,7 @@
"""
Get solving tolerance for the BLMO.
"""
function get_tol(blmo::MathOptBLMO)

Check warning on line 394 in src/MOI_bounded_oracle.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_bounded_oracle.jl#L394

Added line #L394 was not covered by tests
return get_tol(blmo.o)
end
function get_tol(o::MOI.AbstractOptimizer)
Expand Down Expand Up @@ -444,7 +444,7 @@
"""
Deal with infeasible vertex if necessary, e.g. check what caused it etc.
"""
function check_infeasible_vertex(blmo::MathOptBLMO, tree)

Check warning on line 447 in src/MOI_bounded_oracle.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_bounded_oracle.jl#L447

Added line #L447 was not covered by tests
node = tree.nodes[tree.root.current_node_id[]]
node_bounds = node.local_bounds
for list in (node_bounds.lower_bounds, node_bounds.upper_bounds)
Expand Down Expand Up @@ -633,6 +633,8 @@
start_solution=nothing,
fw_verbose=false,
use_shadow_set=true,
custom_heuristics=[Heuristic()],
rounding_prob=1.0,
kwargs...,
)
blmo = convert(MathOptBLMO, lmo)
Expand Down Expand Up @@ -666,6 +668,8 @@
start_solution=start_solution,
fw_verbose=fw_verbose,
use_shadow_set=use_shadow_set,
custom_heuristics=custom_heuristics,
rounding_prob=rounding_prob,
kwargs...,
)
end
26 changes: 0 additions & 26 deletions src/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
@assert sum(active_set.weights .< 0) == 0
# TODO deal with vertices becoming infeasible with conflicts
@debug begin
if !is_linear_feasible(tree.root.problem.tlmo, state.v)
@info "$(state.v)"
check_infeasible_vertex(tree.root.problem.tlmo.blmo, tree)
@assert is_linear_feasible(tree.root.problem.tlmo, state.v)

Check warning on line 22 in src/callbacks.jl

View check run for this annotation

Codecov / codecov/patch

src/callbacks.jl#L19-L22

Added lines #L19 - L22 were not covered by tests
end
end
push!(fw_iterations, state.t)
Expand Down Expand Up @@ -86,32 +86,6 @@
end
end

if check_rounding_value && state.tt == FrankWolfe.pp
# round values
x_rounded = copy(state.x)
for idx in tree.branching_indices
x_rounded[idx] = round(state.x[idx])
end
# check linear feasibility
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
tree.root.updated_incumbent[] = true
node = tree.nodes[tree.root.current_node_id[]]
sol = FrankWolfeSolution(val, x_rounded, node, :rounded)
push!(tree.solutions, sol)
if tree.incumbent_solution === nothing ||
sol.objective < tree.incumbent_solution.objective
tree.incumbent_solution = sol
end
tree.incumbent = val
Bonobo.bound!(tree, node.id)
end
end
end

# check for time limit
if isfinite(time_limit) && Dates.now() >= time_ref + Dates.Second(time_limit)
return false
Expand Down
177 changes: 177 additions & 0 deletions src/heuristics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,180 @@

## Have a general interface for heuristics
## so that the user can link in custom heuristics.
"""
Boscia Heuristic

Interface for heuristics in Boscia.
`h` is the heuristic function receiving as input the tree, the bounded LMO and a point x (the current node solution).
It returns the heuristic solution (can be nothing, we check for that) and whether feasibility still has to be check.
`prob` is the probability with which it will be called.
"""
# Would 'Heuristic' also suffice? Or might we run into Identifer conflicts with other packages?
struct Heuristic{F<:Function}
run_heuristic::F
prob::Float64
identifer::Symbol
end

# Create default heuristic. Doesn't do anything and should be called.
Heuristic() = Heuristic(x -> nothing, -0.1, :default)

"""
Flip coin.
"""
function flip_coin(w=0.5, rng=Random.GLOBAL_RNG)
return rand(rng) ≤ w
end

"""
Add a new solution found from the heuristic to the tree.
"""
function add_heuristic_solution(tree, x, val, heuristic_name::Symbol)
tree.root.updated_incumbent[] = true
node = tree.nodes[tree.root.current_node_id[]]
sol = FrankWolfeSolution(val, x, node, heuristic_name)
push!(tree.solutions, sol)
if tree.incumbent_solution === nothing ||
sol.objective < tree.incumbent_solution.objective
tree.incumbent_solution = sol
end
tree.incumbent = val
Bonobo.bound!(tree, node.id)
end

# TO DO: We might want to change the probability depending on the depth of the tree
# or have other additional criteria on whether to run a heuristic
"""
Choose which heuristics to run by rolling a dice.
"""
function run_heuristics(tree, x, heuristic_list)
inner_lmo = tree.root.problem.tlmo.blmo
heuristic_lmo = TimeTrackingLMO(inner_lmo, tree.root.problem.integer_variables)

for heuristic in heuristic_list
if flip_coin(heuristic.prob)
list_x_heu, check_feasibility = heuristic.run_heuristic(tree, heuristic_lmo, x)

# check feasibility
if !isempty(list_x_heu)
min_val = Inf
min_idx = -1
for (i, x_heu) in enumerate(list_x_heu)
feasible = check_feasibility ? is_linear_feasible(tree.root.problem.tlmo, x_heu) && is_integer_feasible(tree, x_heu) : true
if feasible
val = tree.root.problem.f(x_heu)
if val < min_val
min_val = val
min_idx = i
end
end
end

if min_val < tree.incumbent # Inf < Inf = false
add_heuristic_solution(tree, list_x_heu[min_idx],min_val, heuristic.identifer)
end
end
end
end

# collect statistics from heuristic lmo
tree.root.options[:heu_ncalls] += heuristic_lmo.ncalls
return true
end

"""
Simple rounding heuristic.
"""
function rounding_heuristic(tree::Bonobo.BnBTree, blmo::Boscia.TimeTrackingLMO, x)
x_rounded = copy(x)
for idx in tree.branching_indices
x_rounded[idx] = round(x[idx])
end
return [x_rounded], true
end


"""
follow-the-gradient
Follow the gradient for a fixed number of steps and collect solutions on the way
"""
dhendryc marked this conversation as resolved.
Show resolved Hide resolved
function follow_gradient_heuristic(tree::Bonobo.BnBTree, tlmo::Boscia.TimeTrackingLMO, x, k)
nabla = similar(x)
x_new = copy(x)
sols = []
for i in 1:k
tree.root.problem.g(nabla,x_new)
x_new = Boscia.compute_extreme_point(tlmo, nabla)
push!(sols, x_new)
end
return sols, false
end


"""
Advanced lmo-aware rounding for binary vars. Rounding respecting the hidden feasible region structure.
"""
dhendryc marked this conversation as resolved.
Show resolved Hide resolved
function rounding_lmo_01_heuristic(tree::Bonobo.BnBTree, tlmo::Boscia.TimeTrackingLMO, x)
nabla = zeros(length(x))
for idx in tree.branching_indices
nabla[idx] = 1 - 2*round(x[idx]) # (0.7, 0.3) -> (1, 0) -> (-1, 1) -> min -> (1,0)
end
x_rounded = Boscia.compute_extreme_point(tlmo, nabla)
return [x_rounded], false
end

"""
Probability rounding for 0/1 problems.
It decides based on the fractional value whether to ceil or floor the variable value.
Afterward, one call to Frank-Wolfe is performed to optimize the continuous variables.
"""
function probability_rounding(tree::Bonobo.BnBTree, tlmo::Boscia.TimeTrackingLMO, x)
# save original bounds
node = tree.nodes[tree.root.current_node_id[]]
original_bounds = copy(node.local_bounds)

bounds = IntegerBounds()
for (i,x_i) in zip(tlmo.blmo.int_vars, x[tlmo.blmo.int_vars])
x_rounded = flip_coin(x_i) ? ceil(x_i) : floor(x_i)
push!(bounds, (i, x_rounded), :lessthan)
push!(bounds, (i, x_rounded), :greaterthan)
end

build_LMO(tlmo, tree.root.problem.integer_variable_bounds, bounds, tlmo.blmo.int_vars)

# check for feasibility and boundedness
status = check_feasibility(tlmo)
if status == INFEASIBLE || status == UNBOUNDED
@debug "LMO state in the probability rounding heuristic: $(status)"

Check warning on line 149 in src/heuristics.jl

View check run for this annotation

Codecov / codecov/patch

src/heuristics.jl#L149

Added line #L149 was not covered by tests
# reset LMO to node state
build_LMO(tlmo, tree.root.problem.integer_variable_bounds, original_bounds, tlmo.blmo.int_vars)

Check warning on line 151 in src/heuristics.jl

View check run for this annotation

Codecov / codecov/patch

src/heuristics.jl#L151

Added line #L151 was not covered by tests
# just return the point
return [x], false

Check warning on line 153 in src/heuristics.jl

View check run for this annotation

Codecov / codecov/patch

src/heuristics.jl#L153

Added line #L153 was not covered by tests
end

v = compute_extreme_point(tlmo, rand(length(x)))
active_set = FrankWolfe.ActiveSet([(1.0, v)])

x_rounded, _, _, _ = solve_frank_wolfe(
tree.root.options[:variant],
tree.root.problem.f,
tree.root.problem.g,
tree.root.problem.tlmo,
active_set;
epsilon=node.fw_dual_gap_limit,
max_iteration=tree.root.options[:max_fw_iter],
line_search=tree.root.options[:lineSearch],
lazy=tree.root.options[:lazy],
lazy_tolerance=tree.root.options[:lazy_tolerance],
add_dropped_vertices=tree.root.options[:use_shadow_set],
use_extra_vertex_storage=tree.root.options[:use_shadow_set],
extra_vertex_storage=node.discarded_vertices,
callback=tree.root.options[:callback],
verbose=tree.root.options[:fwVerbose],
)

# reset LMO to node state
build_LMO(tlmo, tree.root.problem.integer_variable_bounds, original_bounds, tlmo.blmo.int_vars)

return [x_rounded], true
end
19 changes: 18 additions & 1 deletion src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ fw_verbose - if true, FrankWolfe logs are printed
use_shadow_set - The shadow set is the set of discarded vertices which is inherited by the children nodes.
It is used to avoid recomputing of vertices in case the LMO is expensive. In case of a cheap LMO,
performance might improve by disabling this option.
custom_heuristics - List of custom heuristic from the user.
prob_rounding - The probability for calling the rounding heuristics. Since the feasibility has to be checked, it might
expensive to do this for every node.
"""
function solve(
f,
Expand Down Expand Up @@ -84,6 +87,8 @@ function solve(
start_solution=nothing,
fw_verbose=false,
use_shadow_set=true,
custom_heuristics=[Heuristic()],
rounding_prob=1.0,
kwargs...,
)
if verbose
Expand Down Expand Up @@ -155,6 +160,9 @@ function solve(
0.0,
)

# Create standard heuristics
heuristics = vcat([Heuristic(rounding_heuristic, rounding_prob, :rounding)], custom_heuristics)

Node = typeof(nodeEx)
Value = Vector{Float64}
tree = Bonobo.initialize(;
Expand Down Expand Up @@ -189,6 +197,8 @@ function solve(
:usePostsolve => use_postsolve,
:variant => variant,
:use_shadow_set => use_shadow_set,
:heuristics => heuristics,
:heu_ncalls => 0,
),
),
branch_strategy=branching_strategy,
Expand Down Expand Up @@ -530,6 +540,7 @@ function build_bnb_callback(

result[:number_nodes] = tree.num_nodes
result[:lmo_calls] = tree.root.problem.tlmo.ncalls
result[:heu_lmo_calls] = tree.root.options[:heu_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
Expand Down Expand Up @@ -649,7 +660,13 @@ 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.tlmo.ncalls)
if tree.root.options[:heu_ncalls] != 0
println("\t LMO calls over all nodes: ", tree.root.problem.tlmo.ncalls)
println("\t LMO calls in the heuristics: ", tree.root.options[:heu_ncalls])
println("\t Total number of lmo calls: ", tree.root.problem.tlmo.ncalls + tree.root.options[:heu_ncalls])
else
println("\t Total number of lmo calls: ", tree.root.problem.tlmo.ncalls)
end
println("\t Total time (s): ", 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)
Expand Down
Loading
Loading