Skip to content
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Boscia"
uuid = "36b166db-dac5-4d05-b36a-e6c4cef071c9"
version = "0.2.4"
authors = ["ZIB IOL"]
version = "0.2.2"

[deps]
Bonobo = "f7b14807-3d4d-461a-888a-05dd4bca8bc3"
Expand Down
51 changes: 51 additions & 0 deletions src/custom_bonobo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,54 @@ function Bonobo.get_solution(
end
return tree.solutions[result].solution
end

struct DepthFirstSearch <: Bonobo.AbstractTraverseStrategy
favor_right::Bool
end

DepthFirstSearch() = DepthFirstSearch(true)

function Bonobo.get_next_node(tree::Bonobo.BnBTree, strategy::DepthFirstSearch)
node_queue = tree.node_queue
nodes = tree.nodes

# For favored branch side (e.g. right if strategy.favor_right == true)
favored_id = nothing
favored_level = -1
favored_lb = Inf # we maximize level, then minimize lb

# For unfavored side
unfavored_id = nothing
unfavored_lb = Inf # we minimize lb

for id in keys(node_queue)

# temporary fix: skip stale IDs
if !haskey(nodes, id)
continue
end

node = nodes[id]

if node.branched_right == strategy.favor_right
# Favored: maximize depth, tie-break by smaller lb
if node.level > favored_level || (node.level == favored_level && node.lb < favored_lb)
favored_level = node.level
favored_lb = node.lb
favored_id = id
end
else
# Unfavored: choose smallest lb
if node.lb < unfavored_lb
unfavored_lb = node.lb
unfavored_id = id
end
end
end

if favored_id !== nothing
return nodes[favored_id]
end

return nodes[unfavored_id]
end
2 changes: 1 addition & 1 deletion src/node.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ end
Holds the necessary information of every node.
This needs to be added by every `AbstractNode` as `std::NodeInfo`

This variant is more flexibel than Bonobo.BnBNodeInfo.
This variant is more flexible than Bonobo.BnBNodeInfo.
"""
mutable struct NodeInfo{T<:Real}
id::Int
Expand Down
1 change: 1 addition & 0 deletions src/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,4 @@ _value_to_print(::LargestGradient) = "Largest Gradient"
_value_to_print(::LargestMostInfeasibleGradient) = "Largest most infeasible gradient"
_value_to_print(::LargestIndex) = "Largest Index"
_value_to_print(::RandomBranching) = "Uniform Random Choice"
_value_to_print(::DepthFirstSearch) = "DepthFirstSearch"
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ using Aqua
include("time_limit.jl")
include("strong_convexity_and_sharpness.jl")
include("branching_strategy_test.jl")
include("traverse_strategy_test.jl")

# Files to exclude from testing (e.g., utilities that require extra dependencies)
excluded_files = ["plot_utilities.jl"]
Expand Down
92 changes: 92 additions & 0 deletions test/traverse_strategy_test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Boscia
using Bonobo
using FrankWolfe
using Test
using Random
using SCIP
using LinearAlgebra

import MathOptInterface
const MOI = MathOptInterface

println("\nTraverse Strategy Tests")

verbose = true


function build_examples(o, n, seed)
Random.seed!(seed)
A = let
A = randn(n, n)
A' * A
end

@assert isposdef(A) == true

y = Random.rand(Bool, n) * 0.6 .+ 0.3

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.ZeroOne())
MOI.add_constraint(o, xi, MOI.GreaterThan(0.0))
MOI.add_constraint(o, xi, MOI.LessThan(1.0))
end
lmo = Boscia.MathOptBLMO(o)

function f(x)
d = x - y
return dot(d, A, d)
end

function grad!(storage, x)
# storage = Ax
mul!(storage, A, x)
# storage = 2Ax - 2Ay
return mul!(storage, A, y, -2, 2)
end
return f, grad!, lmo
end


@testset "DepthFirstSearch Traverse Strategy" begin
dimension = 20
seed = 1
o = SCIP.Optimizer()
f, grad!, lmo = build_examples(o, dimension, seed)
time_limit = 60

settings = Boscia.create_default_settings()
settings.branch_and_bound[:verbose] = verbose
settings.branch_and_bound[:time_limit] = time_limit
x_mi, _, result_mi = Boscia.solve(f, grad!, lmo, settings=settings)

@testset "DepthFirstSearch favoring right" begin
o = SCIP.Optimizer()
f, grad!, lmo = build_examples(o, dimension, seed)

settings = Boscia.create_default_settings()
settings.branch_and_bound[:verbose] = verbose
settings.branch_and_bound[:time_limit] = time_limit
settings.branch_and_bound[:traverse_strategy] = Boscia.DepthFirstSearch(true)
x, _, result = Boscia.solve(f, grad!, lmo, settings=settings)

@test isapprox(f(x_mi), f(x), atol=1e-6, rtol=1e-3)
@test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3)
end

@testset "DepthFirstSearch favoring left" begin
o = SCIP.Optimizer()
f, grad!, lmo = build_examples(o, dimension, seed)

settings = Boscia.create_default_settings()
settings.branch_and_bound[:verbose] = verbose
settings.branch_and_bound[:time_limit] = time_limit
settings.branch_and_bound[:traverse_strategy] = Boscia.DepthFirstSearch(false)
x, _, result = Boscia.solve(f, grad!, lmo, settings=settings)

@test isapprox(f(x_mi), f(x), atol=1e-6, rtol=1e-3)
@test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3)
end
end
Loading