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

Branching with non-trivial domain oracle #207

Merged
merged 39 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
33d954e
Add the strong convexity and sharpness tests to the test suite.
Oct 25, 2024
e883734
Split active set with respect to the domain oracle.
Oct 25, 2024
936e9b0
Let user specify the n.
Oct 25, 2024
168d292
Track issue on Mac.
Oct 25, 2024
5e527f0
minor change.
Oct 25, 2024
31f9890
Ease tolerance.
Oct 25, 2024
b7d3272
First split active set. Only clean up via domain oracle if the new st…
Oct 25, 2024
f8f8ec4
Check that the heuristics points are domain feasible.
Oct 25, 2024
ab2e408
Define defaults for the domain oracle and domain point function (new …
Oct 28, 2024
6e60faf
Add domain point function to the tree dictionary.
Oct 28, 2024
b847b85
Build new start point in the domain infeasbility case.
Oct 28, 2024
9d25ae1
Test implement domain point function.
Oct 28, 2024
9a8a71f
Build the node LMO.
Oct 28, 2024
30da897
Add LinearAlgebra has an active dependency.
Oct 28, 2024
09706ea
Syntax fixes.
Oct 28, 2024
7370a20
Check if child node is even feasible before runing the projection.
Oct 28, 2024
d0ed985
logic fix.
Oct 28, 2024
ab59f66
Logic was correct before.
Oct 28, 2024
e6ea0dc
minor fix.§
Oct 28, 2024
284b9fa
Minor change
Oct 28, 2024
0de4c79
Push new parameter down to the actual solve function.
Oct 29, 2024
b1a7a48
Update examples/optimal_experiment_design.jl
matbesancon Oct 29, 2024
fe08f3b
Update examples/optimal_experiment_design.jl
matbesancon Oct 29, 2024
8d3a858
Update examples/optimal_experiment_design.jl
matbesancon Oct 29, 2024
a9d1070
Update src/node.jl
matbesancon Oct 29, 2024
a4e4e3a
Update src/utilities.jl
matbesancon Oct 29, 2024
b37fff4
Add description to add_min2 to differentiate it from add_min.
Oct 29, 2024
a7c1997
Continue key word not necessary.
Oct 29, 2024
b3ed059
Add better description to the domain point function.
Oct 29, 2024
d84a590
Delete blank space.
Oct 29, 2024
62a80e9
Delete blank spaces.
Oct 29, 2024
6466d06
Rename the function to find_domain_point.
Oct 29, 2024
a92ae12
Resolve merge conflict.
Oct 29, 2024
79ece44
Logic fix.
Oct 29, 2024
9b89cad
Make docstring clearer.
Oct 29, 2024
754e770
Replace SCIP by HiGHS.
Oct 29, 2024
d87942a
Update examples/oed_utils.jl
dhendryc Nov 7, 2024
57dadcc
Update examples/optimal_experiment_design.jl
dhendryc Nov 7, 2024
4839750
Update src/utilities.jl
dhendryc Nov 7, 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
72 changes: 64 additions & 8 deletions examples/oed_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ m - number of experiments.
fusion - boolean deiciding whether we build the fusion or standard problem.
corr - boolean deciding whether we build the independent or correlated data.
"""
function build_data(m)
function build_data(m, n)
# set up
n = Int(floor(m/10))

B = rand(m,n)
B = B'*B
@assert isposdef(B)
Expand All @@ -22,7 +20,7 @@ function build_data(m)
u = floor(N/3)
ub = rand(1.0:u, m)

return A, n, N, ub
return A, N, ub
end

"""
Expand Down Expand Up @@ -178,8 +176,22 @@ function add_to_min(x, u)
if x[perm[i]] < u[perm[i]]
x[perm[i]] += 1
break
else
continue
end
end
return x
end

"""
We want to add to the smallest value of x while respecting the upper bounds u.
In constrast to the function above, we do not require x to have zero entries.
dhendryc marked this conversation as resolved.
Show resolved Hide resolved
"""
function add_to_min2(x,u)
dhendryc marked this conversation as resolved.
Show resolved Hide resolved
perm = sortperm(x)

for i in perm
if x[i] < u[i]
x[i] += 1
break
end
end
return x
Expand All @@ -193,8 +205,6 @@ function remove_from_max(x)
if x[perm[i]] > 1
x[perm[i]] -= 1
break
else
continue
end
end
return x
Expand Down Expand Up @@ -237,6 +247,52 @@ function build_start_point(A, N, ub)
return x, active_set, S
end

"""
Build domain feasible point for any node.
The point has to be domain feasible as well as respect the
node bounds. If no such point can be constructed, return nothing.
"""
dhendryc marked this conversation as resolved.
Show resolved Hide resolved
function build_domain_point_function(domain_oracle, A, N, int_vars, initial_lb, initial_ub)
return function domain_point(local_bounds)
dhendryc marked this conversation as resolved.
Show resolved Hide resolved
lb = initial_lb
ub = initial_ub
for idx in int_vars
if haskey(local_bounds.lower_bounds, idx)
lb[idx] = max(initial_lb[idx], local_bounds.lower_bounds[idx])
end
if haskey(local_bounds.upper_bounds, idx)
ub[idx] = min(initial_ub[idx], local_bounds.upper_bounds[idx])
end
end
# Node itself infeasible
if sum(lb) > N
return nothing
end
# No intersection between node and domain
if !domain_oracle(ub)
return nothing
end
x = lb

S = linearly_independent_rows(A)
while sum(x) <= N
if sum(x) == N
if domain_oracle(x)
return x
else
return nothing
end
end
if iszero(x[S]-ub[S])
y = add_to_min2(x[S], ub[S])
x[S] = y
else
x = add_to_min2(x, ub)
end
end
end
end

"""
Create first incumbent for Boscia and custom BB in a greedy fashion.
"""
Expand Down
21 changes: 14 additions & 7 deletions examples/optimal_experiment_design.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,36 +42,39 @@ include("oed_utils.jl")


m = 50
n = Int(floor(m / 10))
verbose = true

## A-Optimal Design Problem
@testset "A-Optimal Design" begin

Ex_mat, n, N, ub = build_data(m)
Ex_mat, N, ub = build_data(m, n)

g, grad! = build_a_criterion(Ex_mat, build_safe=false)
blmo = build_blmo(m, N, ub)
heu = Boscia.Heuristic(Boscia.rounding_hyperplane_heuristic, 0.7, :hyperplane_aware_rounding)
domain_oracle = build_domain_oracle(Ex_mat, n)
domain_point = build_domain_point_function(domain_oracle, Ex_mat, N, collect(1:m), fill(0.0, m), ub)

# precompile
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
x0, active_set = build_start_point(Ex_mat, N, ub)
z = greedy_incumbent(Ex_mat, N, ub)
x, _, _ = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, time_limit=10, verbose=false, domain_oracle=domain_oracle, custom_heuristics=[heu], line_search=line_search)
x, _, _ = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, time_limit=10, verbose=false, domain_oracle=domain_oracle, find_domain_point=domain_point, custom_heuristics=[heu], line_search=line_search)

# proper run with MGLS and Adaptive
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
x0, active_set = build_start_point(Ex_mat, N, ub)
z = greedy_incumbent(Ex_mat, N, ub)
x, _, result = Boscia.solve(
x, _, result = Boscia.solve(
dhendryc marked this conversation as resolved.
Show resolved Hide resolved
g,
grad!,
blmo,
active_set=active_set,
start_solution=z,
verbose=verbose,
domain_oracle=domain_oracle,
find_domain_point=domain_point,
custom_heuristics=[heu],
line_search=line_search,
)
Expand All @@ -89,29 +92,31 @@ verbose = true
start_solution=z,
verbose=verbose,
domain_oracle=domain_oracle,
find_domain_point=domain_point,
custom_heuristics=[heu],
line_search=line_search,
)

@test result_s[:dual_bound] <= g(x) + 1e-4
@test result[:dual_bound] <= g(x_s) + 1e-4
@test result_s[:dual_bound] <= g(x) + 1e-3
@test result[:dual_bound] <= g(x_s) + 1e-3
@test isapprox(g(x), g(x_s), atol=1e-3)
end

## D-Optimal Design Problem
@testset "D-optimal Design" begin
Ex_mat, n, N, ub = build_data(m)
Ex_mat, N, ub = build_data(m, n)

g, grad! = build_d_criterion(Ex_mat, build_safe=false)
blmo = build_blmo(m, N, ub)
heu = Boscia.Heuristic(Boscia.rounding_hyperplane_heuristic, 0.7, :hyperplane_aware_rounding)
domain_oracle = build_domain_oracle(Ex_mat, n)
domain_point = build_domain_point_function(domain_oracle, Ex_mat, N, collect(1:m), fill(0.0, m), ub)

# precompile
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
x0, active_set = build_start_point(Ex_mat, N, ub)
z = greedy_incumbent(Ex_mat, N, ub)
x, _, _ = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, time_limit=10, verbose=false, domain_oracle=domain_oracle, custom_heuristics=[heu], line_search=line_search)
x, _, _ = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, time_limit=10, verbose=false, domain_oracle=domain_oracle, find_domain_point=domain_point, custom_heuristics=[heu], line_search=line_search)

# proper run with MGLS and Adaptive
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
Expand All @@ -125,6 +130,7 @@ end
start_solution=z,
verbose=verbose,
domain_oracle=domain_oracle,
find_domain_point=domain_point,
custom_heuristics=[heu],
line_search=line_search,
)
Expand All @@ -142,6 +148,7 @@ end
start_solution=z,
verbose=verbose,
domain_oracle=domain_oracle,
find_domain_point=domain_point,
custom_heuristics=[heu],
line_search=line_search,
)
Expand Down
1 change: 1 addition & 0 deletions src/Boscia.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ using FrankWolfe
import FrankWolfe: compute_extreme_point
export compute_extreme_point
using Random
using LinearAlgebra
import Bonobo
using Printf
using Dates
Expand Down
4 changes: 3 additions & 1 deletion src/MOI_bounded_oracle.jl
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,8 @@ function solve(
strong_convexity=0.0,
sharpness_constant = 0.0,
sharpness_exponent = Inf,
domain_oracle=x -> true,
domain_oracle=_trivial_domain,
find_domain_point= _trivial_domain_point,
start_solution=nothing,
fw_verbose=false,
use_shadow_set=true,
Expand Down Expand Up @@ -671,6 +672,7 @@ function solve(
sharpness_constant=sharpness_constant,
sharpness_exponent=sharpness_exponent,
domain_oracle=domain_oracle,
find_domain_point=find_domain_point,
start_solution=start_solution,
fw_verbose=fw_verbose,
use_shadow_set=use_shadow_set,
Expand Down
2 changes: 1 addition & 1 deletion src/heuristics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function run_heuristics(tree, x, heuristic_list; rng=Random.GLOBAL_RNG)
min_val = Inf
min_idx = -1
for (i, x_heu) in enumerate(list_x_heu)
feasible = check_feasibility ? is_linear_feasible(tree.root.problem.tlmo, x_heu) && is_integer_feasible(tree, x_heu) : false
feasible = check_feasibility ? is_linear_feasible(tree.root.problem.tlmo, x_heu) && is_integer_feasible(tree, x_heu) && tree.root.options[:domain_oracle](x_heu) : false
if feasible
val = tree.root.problem.f(x_heu)
if val < min_val
Expand Down
13 changes: 11 additions & 2 deletions src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ sharpness_constant - the constant M > 0 for (θ, M)-sharpness.
where X* is the set minimizer of f.
sharpness_exponent - the exponent θ ∈ [0, 1/2] for (θ, M)-sharpness.
domain_oracle - For a point x: returns true if x is in the domain of f, else false. Per default is true.
In case of the non trivial domain oracle, the starting point has to be feasible for f. Also, depending
In case of the non trivial domain oracle, the starting point has to be feasible for f. Additionally,
the user has to provide a function `domain_point`, see below. Also, depending
on the Line Search method, you might have to provide the domain oracle to it, too.
find_domain_point - Given the current node bounds return a domain feasible point respecting the bounds.
If no such point can be found, return nothing.
start_solution - initial solution to start with an incumbent
fw_verbose - if true, FrankWolfe logs are printed
use_shadow_set - The shadow set is the set of discarded vertices which is inherited by the children nodes.
Expand Down Expand Up @@ -98,7 +101,8 @@ function solve(
strong_convexity=0.0,
sharpness_constant = 0.0,
sharpness_exponent = Inf,
domain_oracle=x -> true,
domain_oracle=_trivial_domain,
find_domain_point= _trivial_domain_point,
start_solution=nothing,
fw_verbose=false,
use_shadow_set=true,
Expand Down Expand Up @@ -146,6 +150,10 @@ function solve(

global_bounds = build_global_bounds(blmo, integer_variables)

if typeof(domain_oracle) != typeof(_trivial_domain) && typeof(find_domain_point) == typeof(_trivial_domain_point)
@warn "For a non trivial domain oracle, please provide the DOMAIN POINT function. Otherwise, Boscia might not converge."
end

v = []
if active_set === nothing
direction = collect(1.0:n)
Expand Down Expand Up @@ -201,6 +209,7 @@ function solve(
global_tightenings=IntegerBounds(),
options=Dict{Symbol,Any}(
:domain_oracle => domain_oracle,
:find_domain_point => find_domain_point,
:dual_gap => dual_gap,
:dual_gap_decay_factor => dual_gap_decay_factor,
:dual_tightening => dual_tightening,
Expand Down
4 changes: 3 additions & 1 deletion src/managed_blmo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ function solve(
strong_convexity=0.0,
sharpness_constant = 0.0,
sharpness_exponent = Inf,
domain_oracle=x -> true,
domain_oracle=_trivial_domain,
find_domain_point= _trivial_domain_point,
start_solution=nothing,
fw_verbose=false,
use_shadow_set=true,
Expand Down Expand Up @@ -313,6 +314,7 @@ function solve(
sharpness_constant=sharpness_constant,
sharpness_exponent=sharpness_exponent,
domain_oracle=domain_oracle,
find_domain_point=find_domain_point,
start_solution=start_solution,
fw_verbose=fw_verbose,
use_shadow_set=use_shadow_set,
Expand Down
60 changes: 32 additions & 28 deletions src/node.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,30 @@ function Bonobo.get_branching_nodes_info(tree::Bonobo.BnBTree, node::FrankWolfeN
split_vertices_set!(node.discarded_vertices, tree, vidx, x, node.local_bounds)

# Sanity check
@assert isapprox(sum(active_set_left.weights), 1.0)
@assert isapprox(sum(active_set_left.weights), 1.0) "sum weights left: $(sum(active_set_left.weights))"
@assert sum(active_set_left.weights .< 0) == 0
@assert isapprox(sum(active_set_right.weights), 1.0)
for v in active_set_left.atoms
if !(v[vidx] <= floor(x[vidx]) + tree.options.atol)
error("active_set_left\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
end
@assert isapprox(sum(active_set_right.weights), 1.0) "sum weights right: $(sum(active_set_right.weights))"
@assert sum(active_set_right.weights .< 0) == 0
for v in active_set_right.atoms
if !(v[vidx] >= ceil(x[vidx]) - tree.options.atol)
error("active_set_right\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
end
for v in discarded_set_left.storage
if !(v[vidx] <= floor(x[vidx]) + tree.options.atol)
error("storage left\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
end
for v in discarded_set_right.storage
if !(v[vidx] >= ceil(x[vidx]) - tree.options.atol)
error("storage right\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
end

# add new bounds to the feasible region left and right
# copy bounds from parent
Expand All @@ -108,26 +128,15 @@ function Bonobo.get_branching_nodes_info(tree::Bonobo.BnBTree, node::FrankWolfeN
fw_dual_gap_limit = tree.root.options[:dual_gap_decay_factor] * node.fw_dual_gap_limit
fw_dual_gap_limit = max(fw_dual_gap_limit, tree.root.options[:min_node_fw_epsilon])

# Sanity check
for v in active_set_left.atoms
if !(v[vidx] <= floor(x[vidx]) + tree.options.atol)
error("active_set_left\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
end
for v in discarded_set_left.storage
if !(v[vidx] <= floor(x[vidx]) + tree.options.atol)
error("storage left\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
end
for v in active_set_right.atoms
if !(v[vidx] >= ceil(x[vidx]) - tree.options.atol)
error("active_set_right\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
# in case of non trivial domain oracle: Only split if the iterate is still domain feasible
x_left = FrankWolfe.compute_active_set_iterate!(active_set_left)
x_right = FrankWolfe.compute_active_set_iterate!(active_set_right)

if !tree.root.options[:domain_oracle](x_left)
active_set_left = build_active_set_by_domain_oracle(active_set_left, tree, varbounds_left, node)
end
for v in discarded_set_right.storage
if !(v[vidx] >= ceil(x[vidx]) - tree.options.atol)
error("storage right\n$(v)\n$vidx, $(x[vidx]), $(v[vidx])")
end
if !tree.root.options[:domain_oracle](x_right)
active_set_right = build_active_set_by_domain_oracle(active_set_right, tree, varbounds_right, node)
end

# update the LMO
Expand Down Expand Up @@ -156,13 +165,8 @@ function Bonobo.get_branching_nodes_info(tree::Bonobo.BnBTree, node::FrankWolfeN
dual_gap=NaN,
)

# in case of non trivial domain oracle: Only split if the iterate is still domain feasible
x_left = FrankWolfe.compute_active_set_iterate!(active_set_left)
x_right = FrankWolfe.compute_active_set_iterate!(active_set_right)
domain_oracle = tree.root.options[:domain_oracle]

domain_right = domain_oracle(x_right)
domain_left = domain_oracle(x_left)
domain_right = !isempty(active_set_right)
domain_left = !isempty(active_set_left)

nodes = if !prune_left && !prune_right && domain_right && domain_left
[node_info_left, node_info_right]
Expand Down
10 changes: 10 additions & 0 deletions src/problem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ Enum for the solving stage
TIME_LIMIT_REACHED = 3
end

"""
Trivial domain function.
"""
_trivial_domain(x) = true

"""
Trivial domain point function.
"""
_trivial_domain_point(local_bounds::IntegerBounds) = nothing

abstract type AbstractSimpleOptimizationProblem end

"""
Expand Down
Loading
Loading