Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- `log_table` option for printing a table containing useful information about the current status
- Store all solutions

## v0.1.1
Expand Down
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ version = "0.1.1"
[deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
NamedTupleTools = "d9ec5142-1e00-5aa0-9d6a-321866360f50"
TableLogger = "72b659bb-f61b-4d0d-9dbb-0f81f57d8545"

[compat]
DataStructures = "0.18"
NamedTupleTools = "0.13, 0.14"
TableLogger = "0.1"
julia = "1.6"

[extras]
Expand Down
57 changes: 56 additions & 1 deletion src/Bonobo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Bonobo

using DataStructures
using NamedTupleTools
using TableLogger

"""
AbstractNode
Expand Down Expand Up @@ -117,6 +118,7 @@ mutable struct Options
branch_strategy :: AbstractBranchStrategy
atol :: Float64
rtol :: Float64
log_table :: Bool
end

"""
Expand Down Expand Up @@ -176,6 +178,7 @@ Later it can be dispatched on `BnBTree{Node, Root, Solution}` for various method
- `root` [`nothing`] the information about the root problem. The type can be used for dispatching on types
- `sense` [`:Min`] can be `:Min` or `:Max` depending on the objective sense
- `Value` [`Vector{Float64}`] the type of a solution
- `log_table` [`true`] print a table about the current status including: incumbent, best bound and running time

Return a [`BnBTree`](@ref) object which is the input for [`optimize!`](@ref).
"""
Expand All @@ -189,6 +192,7 @@ function initialize(;
Solution = DefaultSolution{Node,Value},
root = nothing,
sense = :Min,
log_table = true,
)
return BnBTree{Node,typeof(root),Value,Solution}(
Inf,
Expand All @@ -201,7 +205,7 @@ function initialize(;
get_branching_indices(root),
0,
sense,
Options(traverse_strategy, branch_strategy, atol, rtol)
Options(traverse_strategy, branch_strategy, atol, rtol, log_table)
)
end

Expand Down Expand Up @@ -246,12 +250,27 @@ which are set in the following ways:
2. If the node has a higher lower bound than the incumbent the kwarg `worse_than_incumbent` is set to `true`.
"""
function optimize!(tree::BnBTree; callback=(args...; kwargs...)->())
table = init_log_table(
(id=:open_nodes, name="#Open"),
(id=:closed_nodes, name="#Closed"),
(id=:incumbent, name="Incumbent", width=20),
(id=:best_bound, name="Best Bound", width=20),
(id=:gap, name="Gap", width=12, alignment=:right),
(id=:time, name="Time [s]", width=10, alignment=:right);
width = 15,
alignment = :center
)
closed_nodes = 0
start_time = time()
tree.options.log_table && print_header(table)

while !terminated(tree)
node = get_next_node(tree, tree.options.traverse_strategy)
lb, ub = evaluate_node!(tree, node)
# if the problem was infeasible we simply close the node and continue
if isnan(lb) && isnan(ub)
close_node!(tree, node)
closed_nodes += 1
callback(tree, node; node_infeasible=true)
continue
end
Expand All @@ -266,11 +285,13 @@ function optimize!(tree::BnBTree; callback=(args...; kwargs...)->())
# if the evaluated lower bound is worse than the best incumbent -> close and continue
if node.lb >= tree.incumbent
close_node!(tree, node)
closed_nodes += 1
callback(tree, node; worse_than_incumbent=true)
continue
end

updated = update_best_solution!(tree, node)

if updated
bound!(tree, node.id)
if isapprox(tree.incumbent, tree.lb; atol=tree.options.atol, rtol=tree.options.rtol)
Expand All @@ -279,12 +300,46 @@ function optimize!(tree::BnBTree; callback=(args...; kwargs...)->())
end

close_node!(tree, node)
closed_nodes += 1
branch!(tree, node)
callback(tree, node)
tree.options.log_table && set_and_print_table_values!(table, tree, start_time, closed_nodes)
end
tree.options.log_table && set_and_print_table_values!(table, tree, start_time, closed_nodes)
sort_solutions!(tree.solutions, tree.sense)
end

"""
set_and_print_table_values!(table, tree, start_time, closed_nodes)

Set the values for the new table line and print it. Whether it's actually printed depends on whether it is changed compared to the previous line.
This is decided by TableLogger.jl
"""
function set_and_print_table_values!(table, tree, start_time, closed_nodes)
set_value!(table, :open_nodes, length(tree.nodes))
set_value!(table, :closed_nodes, closed_nodes)
table_incumbent = tree.sense == :Max ? -tree.incumbent : tree.incumbent
table_best_bound = tree.sense == :Max ? -tree.lb : tree.lb
if isinf(table_incumbent)
table_incumbent = "-"
end

set_value!(table, :incumbent, table_incumbent)
set_value!(table, :best_bound, table_best_bound)
gap = abs(tree.lb - tree.incumbent) / abs(tree.incumbent)
gap *= 100
gap = round(gap; digits=2)
if isnan(gap)
table_gap = "-"
else
table_gap = "$gap %"
end
set_value!(table, :gap, table_gap)
set_value!(table, :time, time()-start_time)

print_line(table)
end

"""
sort_solutions!(solutions::Vector{<:AbstractSolution}, sense::Symbol)

Expand Down
2 changes: 2 additions & 0 deletions src/node.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,7 @@ end
evaluate_node!(tree, node)

Evaluate the current node and return the lower and upper bound of that node.
Return `NaN` for the lower bound if the problem is infeasible
and `NaN` for the upper bound if no solution was found.
"""
function evaluate_node! end
8 changes: 8 additions & 0 deletions test/end2end/dummy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ function BB.evaluate_node!(tree::BnBTree{BB.DefaultNode, DummyRoot}, node::BB.De
lb = 1.0
ub = 1.0
end
if node.id == 1
ub = NaN
end
return lb, ub
end

Expand All @@ -24,6 +27,11 @@ function BB.get_branching_nodes_info(tree::BnBTree{BB.DefaultNode, DummyRoot}, n
end

function dummy_callback(tree, node; node_infeasible=false, worse_than_incumbent=false)
if node.id <= 2
@test !node_infeasible
@test !worse_than_incumbent
return
end
if node.id % 2 == 0
@test worse_than_incumbent
else
Expand Down
6 changes: 4 additions & 2 deletions test/end2end/mip.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ end
traverse_strategy = BB.BFS(),
Node = MIPNode,
root = m,
sense = objective_sense(m) == MOI.MAX_SENSE ? :Max : :Min
sense = objective_sense(m) == MOI.MAX_SENSE ? :Max : :Min,
log_table = false
)
BB.set_root!(bnb_model, (
lbs = zeros(length(x)),
Expand Down Expand Up @@ -128,7 +129,8 @@ end
branch_strategy = BB.MOST_INFEASIBLE(),
Node = MIPNode,
root = m,
sense = objective_sense(m) == MOI.MAX_SENSE ? :Max : :Min
sense = objective_sense(m) == MOI.MAX_SENSE ? :Max : :Min,
log_table = false
)
BB.set_root!(bnb_model, (
lbs = zeros(length(x)),
Expand Down