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

Lower bound improvement based on sharpness of the objective #195

Merged
merged 52 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
367e73f
Move tightening functions into a separate file.
Aug 30, 2024
4b55f60
Tighten bounds using sharpness.
Aug 30, 2024
7f9b4c7
Add sharpness constant and exponent to the interface.
Sep 2, 2024
5819c74
Minor fixes.
Sep 2, 2024
81000b6
Tests for strong convexity and sharpness on the Normbox example.
Sep 2, 2024
df13669
Use the node dual gap instead of root dual gap.
Sep 2, 2024
a17f993
Make sure that we collect the statistics, even if in case of timeout.
Sep 3, 2024
06a0e25
We only need this if the key is not in the result dictionary.
Sep 3, 2024
c8add28
Move the BnB Callback into the appriopiate file.
Sep 3, 2024
0fad6d2
We don't need the root dual gap.
Sep 3, 2024
48ccb45
Update src/custom_bonobo.jl
dhendryc Sep 3, 2024
5650287
Add a bit of give to the sharpness constant to avoid cutting of optim…
Sep 4, 2024
f1b676f
Merge changes from repository
Sep 4, 2024
26b52a0
Added small definition of sharpness.
Sep 6, 2024
6cd74a7
Update src/callbacks.jl
matbesancon Sep 10, 2024
7aab85b
Update src/callbacks.jl
matbesancon Sep 10, 2024
32627ee
Don't create child node if the domain oracle is infeasible.
Sep 11, 2024
414a3f9
Set version up after bug fix.
Sep 11, 2024
056e088
Merge branch 'main' into sharpness
Sep 12, 2024
d1b0441
Merge changes from the remote repository.
Sep 12, 2024
53b2c52
Merge branch 'domain-branch' into sharpness
Sep 12, 2024
d79263c
Optimal Experiment Design example.
Sep 12, 2024
802f3b8
Fix syntax issues.
Sep 12, 2024
c993562
Apply suggestions from code review
matbesancon Sep 13, 2024
a068fc8
Add corrected sharpness constants.
Sep 13, 2024
b1b5273
In case the vertex is domain infeasible, use x to get the data type o…
Sep 13, 2024
b676ef6
Syntax issue fix.
Sep 13, 2024
1f26759
Corrections.
Sep 13, 2024
98783d5
Print statement in case assert fails.
Sep 13, 2024
8dd9ea4
Merge changes from github repository
Sep 13, 2024
c55223f
Print logs for better bug hunting.
Sep 16, 2024
b5e7e24
Another try to reproduce error.
Sep 16, 2024
722cb2d
Test lower bound against tree.incumbent plus current fw_epsilon.
Sep 16, 2024
c6aaf84
Have the same seed as in runtests.jl.
Sep 16, 2024
dc65962
Rename A to Ex_mat to avoid constant redefinition.
Sep 16, 2024
4c16b12
Clean up another constant redefinition.
Sep 16, 2024
3a4da67
Add test wrapper.
Sep 16, 2024
015b4a7
Debug statements.
Sep 17, 2024
9030728
More debug statements.
Sep 17, 2024
ee85ecd
Merge branch 'domain-branch' into sharpness
Sep 17, 2024
1cc7254
If heuristic return infeasible solution, ignore it.
Sep 17, 2024
2936851
Minor change.
Sep 17, 2024
961c1a7
Clean up example.
Sep 17, 2024
e1d298b
Show solutions.
Sep 17, 2024
4e42b57
Don't use vertex storage for the heuristics.
Sep 17, 2024
9837858
Final clean up.
Sep 18, 2024
c0c822f
Run tests with one specific seed.
Sep 18, 2024
f0dedc0
Merge branch 'main' into sharpness
Sep 18, 2024
7d93f84
Added time limit.
Sep 18, 2024
c5b0019
Corrected sharpness constant.
Sep 19, 2024
069c4d8
Corrected sharpness constant and slight relaxation of test for A-Opti…
Sep 19, 2024
db20f7e
Update src/tightenings.jl
matbesancon Sep 20, 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
1 change: 1 addition & 0 deletions src/Boscia.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ include("blmo_interface.jl")
include("time_tracking_lmo.jl")
include("frank_wolfe_variants.jl")
include("build_lmo.jl")
include("tightenings.jl")
include("node.jl")
include("custom_bonobo.jl")
include("callbacks.jl")
Expand Down
4 changes: 4 additions & 0 deletions src/MOI_bounded_oracle.jl
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,8 @@ function solve(
global_dual_tightening=true,
bnb_callback=nothing,
strong_convexity=0.0,
sharpness_constant = 0.0,
sharpness_exponent = Inf,
domain_oracle=x -> true,
start_solution=nothing,
fw_verbose=false,
Expand Down Expand Up @@ -664,6 +666,8 @@ function solve(
global_dual_tightening=global_dual_tightening,
bnb_callback=bnb_callback,
strong_convexity=strong_convexity,
sharpness_constant=sharpness_constant,
sharpness_exponent=sharpness_exponent,
domain_oracle=domain_oracle,
start_solution=start_solution,
fw_verbose=fw_verbose,
Expand Down
260 changes: 259 additions & 1 deletion src/callbacks.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# FW callback
"""
Frank-Wolfe Callback.

Is called in every Frank-Wolfe iteration.
Node evaluation can be dymamically stopped here.
Time limit is checked.
matbesancon marked this conversation as resolved.
Show resolved Hide resolved
If the vertex is providing a better incumbent, it is added as solution.
"""
function build_FW_callback(
tree,
min_number_lower,
Expand Down Expand Up @@ -94,3 +101,254 @@ function build_FW_callback(
return true
end
end

"""
Branch-and-Bound Callback.
Collects statistics and prints logs if verbose is turned on.

Output of Boscia

iter : current iteration of Boscia
matbesancon marked this conversation as resolved.
Show resolved Hide resolved
node id : current node id
lower bound : tree_lb(tree)
incumbent : tree.incumbent
gap : tree.incumbent-tree_lb(tree)
rel. gap : dual_gap/tree.incumbent
time : total time of Boscia
time/nodes : average time per node
FW time : time spent in FW
LMO time : time used by LMO
LMO calls : number of compute_extreme_point calls in FW
FW iterations : number of iterations in FW
"""
function build_bnb_callback(
tree,
time_ref,
list_lb_cb,
list_ub_cb,
list_time_cb,
list_num_nodes_cb,
list_lmo_calls_cb,
verbose,
fw_iterations,
list_active_set_size_cb,
list_discarded_set_size_cb,
result,
lmo_calls_per_layer,
active_set_size_per_layer,
discarded_set_size_per_layer,
node_level,
baseline_callback,
local_tightenings,
global_tightenings,
local_potential_tightenings,
num_bin,
num_int,
)
iteration = 0

headers = [
" ",
"Iter",
"Open",
"Bound",
"Incumbent",
"Gap (abs)",
"Gap (rel)",
"Time (s)",
"Nodes/sec",
"FW (ms)",
"LMO (ms)",
"LMO (calls c)",
"FW (its)",
"#activeset",
"#shadow",
]
format_string = "%1s %5i %5i %14e %14e %14e %14e %14e %14e %12i %10i %14i %10i %8i %8i\n"
print_iter = get(tree.root.options, :print_iter, 100)

if verbose
FrankWolfe.print_callback(headers, format_string, print_header=true)
end
return function callback(
tree,
node;
worse_than_incumbent=false,
node_infeasible=false,
lb_update=false,
)
if baseline_callback !== nothing
baseline_callback(
tree,
node,
worse_than_incumbent=worse_than_incumbent,
node_infeasible=node_infeasible,
lb_update=lb_update,
)
end
if !node_infeasible
#update lower bound
if lb_update == true
tree.node_queue[node.id] = (node.lb, node.id)
_, prio = peek(tree.node_queue)
@assert tree.lb <= prio[1]
tree.lb = min(minimum([prio[2][1] for prio in tree.node_queue]), tree.incumbent)
end
push!(list_ub_cb, tree.incumbent)
push!(list_num_nodes_cb, tree.num_nodes)
push!(node_level, node.level)
iteration += 1
if tree.lb == -Inf && isempty(tree.nodes)
tree.lb = node.lb
end

time = float(Dates.value(Dates.now() - time_ref))
push!(list_time_cb, time)

if tree.root.options[:time_limit] < Inf
if time / 1000.0 ≥ tree.root.options[:time_limit]
@assert tree.root.problem.solving_stage == SOLVING
tree.root.problem.solving_stage = TIME_LIMIT_REACHED
end
end

fw_time = Dates.value(node.fw_time)
fw_iter = if !isempty(fw_iterations)
fw_iterations[end]
else
0
end
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.tlmo.ncalls
push!(list_lmo_calls_cb, copy(LMO_calls_c))

if !isempty(tree.node_queue)
p_lb = tree.lb
tree.lb = min(minimum([prio[2][1] for prio in tree.node_queue]), tree.incumbent)
@assert p_lb <= tree.lb + tree.root.options[:dual_gap] "p_lb <= tree.lb + tree.root.options[:dual_gap] $(p_lb) <= $(tree.lb + tree.root.options[:dual_gap])"
end
# correct lower bound if necessary
tree.lb = tree_lb(tree)
dual_gap = tree.incumbent - tree_lb(tree)
push!(list_lb_cb, tree_lb(tree))
active_set_size = length(node.active_set)
discarded_set_size = length(node.discarded_vertices.storage)
push!(list_active_set_size_cb, active_set_size)
push!(list_discarded_set_size_cb, discarded_set_size)
nodes_left = length(tree.nodes)
if tree.root.updated_incumbent[]
incumbent_updated = "*"
else
incumbent_updated = " "
end
if verbose && (
mod(iteration, print_iter) == 0 ||
iteration == 1 ||
Bonobo.terminated(tree) ||
tree.root.updated_incumbent[]
)
if (mod(iteration, print_iter * 40) == 0)
FrankWolfe.print_callback(headers, format_string, print_header=true)
end
FrankWolfe.print_callback(
(
incumbent_updated,
iteration,
nodes_left,
tree_lb(tree),
tree.incumbent,
dual_gap,
relative_gap(tree.incumbent, tree_lb(tree)),
time / 1000.0,
tree.num_nodes / time * 1000.0,
fw_time,
LMO_time,
tree.root.problem.tlmo.ncalls,
fw_iter,
active_set_size,
discarded_set_size,
),
format_string,
print_header=false,
)
tree.root.updated_incumbent[] = false
end
# lmo calls per layer
if length(list_lmo_calls_cb) > 1
LMO_calls = list_lmo_calls_cb[end] - list_lmo_calls_cb[end-1]
else
LMO_calls = list_lmo_calls_cb[end]
end
if length(lmo_calls_per_layer) < node.level
push!(lmo_calls_per_layer, [LMO_calls])
push!(active_set_size_per_layer, [active_set_size])
push!(discarded_set_size_per_layer, [discarded_set_size])
else
push!(lmo_calls_per_layer[node.level], LMO_calls)
push!(active_set_size_per_layer[node.level], active_set_size)
push!(discarded_set_size_per_layer[node.level], discarded_set_size)
end

# add tightenings
push!(global_tightenings, node.global_tightenings)
push!(local_tightenings, node.local_tightenings)
push!(local_potential_tightenings, node.local_potential_tightenings)
@assert node.local_potential_tightenings <= num_bin + num_int
@assert node.local_tightenings <= num_bin + num_int
@assert node.global_tightenings <= num_bin + num_int
end
# update current_node_id
if !Bonobo.terminated(tree)
tree.root.current_node_id[] =
Bonobo.get_next_node(tree, tree.options.traverse_strategy).id
end

if Bonobo.terminated(tree)
Bonobo.sort_solutions!(tree.solutions, tree.sense)
x = Bonobo.get_solution(tree)
# x can be nothing if the user supplied a custom domain oracle and the time limit is reached
if x === nothing
@assert tree.root.problem.solving_stage == TIME_LIMIT_REACHED
end
primal_value = x !== nothing ? tree.root.problem.f(x) : Inf
# deactivate postsolve if there is no solution
tree.root.options[:usePostsolve] =
x === nothing ? false : tree.root.options[:usePostsolve]

# TODO: here we need to calculate the actual state

# If the tree is empty, incumbent and solution should be the same!
if isempty(tree.nodes)
@assert isapprox(tree.incumbent, primal_value)
end

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
result[:list_discarded_set_size] = list_discarded_set_size_cb
result[:list_lb] = list_lb_cb
result[:list_ub] = list_ub_cb
result[:list_time] = list_time_cb
result[:lmo_calls_per_layer] = lmo_calls_per_layer
result[:active_set_size_per_layer] = active_set_size_per_layer
result[:discarded_set_size_per_layer] = discarded_set_size_per_layer
result[:node_level] = node_level
result[:global_tightenings] = global_tightenings
result[:local_tightenings] = local_tightenings
result[:local_potential_tightenings] = local_potential_tightenings

if verbose
FrankWolfe.print_callback(headers, format_string, print_footer=true)
println()
end
end
end
end
19 changes: 19 additions & 0 deletions src/custom_bonobo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ function Bonobo.optimize!(
Bonobo.branch!(tree, node)
callback(tree, node)
end
# To make sure that we collect the statistics in case the time limit is reached.
if !haskey(tree.root.result, :global_tightenings)
y = Bonobo.get_solution(tree)
vertex_storage = FrankWolfe.DeletedVertexStorage(typeof(y)[], 1)
dummy_node = FrankWolfeNode(
NodeInfo(-1, Inf, Inf),
FrankWolfe.ActiveSet([(1.0, y)]),
vertex_storage,
IntegerBounds(),
1,
1e-3,
Millisecond(0),
0,
0,
0,
0.0,
)
callback(tree, dummy_node, node_infeasible=true)
end
return Bonobo.sort_solutions!(tree.solutions, tree.sense)
end

Expand Down
Loading
Loading