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
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.4"

[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 = -1
favored_level = -1
favored_lb = Inf # we maximize level, then minimize lb

# For unfavored side
unfavored_id = -1
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 !== -1
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
87 changes: 87 additions & 0 deletions test/traverse_strategy_test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Boscia
using Bonobo
using FrankWolfe
using Test
using Random
using SCIP
using LinearAlgebra

import MathOptInterface
const MOI = MathOptInterface

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

@assert isposdef(A)

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] = false
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