diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 22e0cc2..e63eb37 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -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 diff --git a/Project.toml b/Project.toml index 4cefaf1..c492270 100644 --- a/Project.toml +++ b/Project.toml @@ -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] diff --git a/src/Bonobo.jl b/src/Bonobo.jl index b054e32..e98d3fc 100644 --- a/src/Bonobo.jl +++ b/src/Bonobo.jl @@ -2,6 +2,7 @@ module Bonobo using DataStructures using NamedTupleTools +using TableLogger """ AbstractNode @@ -117,6 +118,7 @@ mutable struct Options branch_strategy :: AbstractBranchStrategy atol :: Float64 rtol :: Float64 + log_table :: Bool end """ @@ -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). """ @@ -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, @@ -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 @@ -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 @@ -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) @@ -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) diff --git a/src/node.jl b/src/node.jl index 3d4f57d..5cce0d0 100644 --- a/src/node.jl +++ b/src/node.jl @@ -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 \ No newline at end of file diff --git a/test/end2end/dummy.jl b/test/end2end/dummy.jl index 0930be6..84a1234 100644 --- a/test/end2end/dummy.jl +++ b/test/end2end/dummy.jl @@ -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 @@ -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 diff --git a/test/end2end/mip.jl b/test/end2end/mip.jl index 2d0a872..bbba668 100644 --- a/test/end2end/mip.jl +++ b/test/end2end/mip.jl @@ -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)), @@ -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)),