Skip to content

Commit 297ccfb

Browse files
authored
Fix in the lowerbound tightening (#204)
Sharpness cannot be used out of the box for the Optimal Design problems.
1 parent b42ec1a commit 297ccfb

7 files changed

+343
-66
lines changed

examples/birkhoff.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ function build_birkhoff_lmo()
7676
# doubly stochastic constraints
7777
MOI.add_constraint.(
7878
o,
79-
vec(sum(X[i], dims=1, init=MOI.ScalarAffineFunction{Float64}([], 0.0))),
79+
X[i] * ones(n),
8080
MOI.EqualTo(1.0),
8181
)
8282
MOI.add_constraint.(
8383
o,
84-
vec(sum(X[i], dims=2, init=MOI.ScalarAffineFunction{Float64}([], 0.0))),
84+
X[i]' * ones(n),
8585
MOI.EqualTo(1.0),
8686
)
8787
# 0 ≤ Y_i ≤ X_i

examples/optimal_experiment_design.jl

+84-58
Original file line numberDiff line numberDiff line change
@@ -40,88 +40,114 @@ include("oed_utils.jl")
4040
https://github.com/ZIB-IOL/FrankWolfe.jl/blob/master/examples/optimal_experiment_design.jl
4141
"""
4242

43-
m = 40
43+
44+
m = 50
4445
verbose = true
4546

4647
## A-Optimal Design Problem
47-
4848
@testset "A-Optimal Design" begin
4949

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

52-
# sharpness constants
53-
σ = minimum(Ex_mat' * Ex_mat)
54-
λ_max = maximum(ub) * maximum([norm(Ex_mat[i,:])^2 for i=1:size(Ex_mat,1)])
55-
θ = 1/2
56-
M = sqrt(λ_max^3/ n * σ^4)
57-
58-
59-
g, grad! = build_a_criterion(Ex_mat, build_safe=true)
52+
g, grad! = build_a_criterion(Ex_mat, build_safe=false)
6053
blmo = build_blmo(m, N, ub)
61-
x0, active_set = build_start_point(Ex_mat, N, ub)
62-
z = greedy_incumbent(Ex_mat, N, ub)
63-
domain_oracle = build_domain_oracle(Ex_mat, n)
64-
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
6554
heu = Boscia.Heuristic(Boscia.rounding_hyperplane_heuristic, 0.7, :hyperplane_aware_rounding)
55+
domain_oracle = build_domain_oracle(Ex_mat, n)
6656

67-
x, _, result = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, time_limit=10, verbose=false, domain_oracle=domain_oracle, custom_heuristics=[heu], sharpness_exponent=θ, sharpness_constant=M, line_search=line_search)
68-
69-
_, active_set = build_start_point(Ex_mat, N, ub)
57+
# precompile
58+
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
59+
x0, active_set = build_start_point(Ex_mat, N, ub)
7060
z = greedy_incumbent(Ex_mat, N, ub)
71-
x, _, result = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, verbose=verbose, domain_oracle=domain_oracle, custom_heuristics=[heu], sharpness_exponent=θ, sharpness_constant=M, line_search=line_search) #sharpness_exponent=θ, sharpness_constant=M,
72-
73-
gradient = similar(x)
74-
grad!(gradient, x)
75-
v = Boscia.compute_extreme_point(blmo, gradient)
76-
dual_gap = FrankWolfe.dot(gradient, x) - FrankWolfe.dot(gradient, v)
77-
@show dual_gap
61+
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)
7862

79-
blmo = build_blmo(m, N, ub)
80-
_, active_set = build_start_point(Ex_mat, N, ub)
63+
# proper run with MGLS and Adaptive
64+
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
65+
x0, active_set = build_start_point(Ex_mat, N, ub)
8166
z = greedy_incumbent(Ex_mat, N, ub)
82-
domain_oracle = build_domain_oracle(Ex_mat, n)
83-
heu = Boscia.Heuristic(Boscia.rounding_hyperplane_heuristic, 0.7, :hyperplane_aware_rounding)
84-
85-
x_s, _, result = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, verbose=verbose, line_search=FrankWolfe.Secant(domain_oracle=domain_oracle), domain_oracle=domain_oracle, sharpness_exponent=θ, sharpness_constant=M, custom_heuristics=[heu]) #sharpness_exponent=θ, sharpness_constant=M,
86-
87-
@show x
88-
@show x_s
89-
@show g(x), g(x_s)
90-
@test isapprox(g(x), g(x_s), atol=1e-3, rtol=5e-2)
67+
x, _, result = Boscia.solve(
68+
g,
69+
grad!,
70+
blmo,
71+
active_set=active_set,
72+
start_solution=z,
73+
verbose=verbose,
74+
domain_oracle=domain_oracle,
75+
custom_heuristics=[heu],
76+
line_search=line_search,
77+
)
78+
79+
# Run with Secant
80+
x0, active_set = build_start_point(Ex_mat, N, ub)
81+
z = greedy_incumbent(Ex_mat, N, ub)
82+
line_search = FrankWolfe.Secant(domain_oracle=domain_oracle)
83+
84+
x_s, _, result_s = Boscia.solve(
85+
g,
86+
grad!,
87+
blmo,
88+
active_set=active_set,
89+
start_solution=z,
90+
verbose=verbose,
91+
domain_oracle=domain_oracle,
92+
custom_heuristics=[heu],
93+
line_search=line_search,
94+
)
95+
96+
@test result_s[:dual_bound] <= g(x) + 1e-4
97+
@test result[:dual_bound] <= g(x_s) + 1e-4
98+
@test isapprox(g(x), g(x_s), atol=1e-3)
9199
end
92100

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

98-
# sharpness constants
99-
σ = minimum(Ex_mat' * Ex_mat)
100-
λ_max = maximum(ub) * maximum([norm(Ex_mat[i,:])^2 for i=1:size(Ex_mat,1)])
101-
θ = 1/2
102-
M = sqrt(2 * λ_max^2/ n * σ^4 )
103-
104-
105-
g, grad! = build_d_criterion(Ex_mat, build_safe=true)
105+
g, grad! = build_d_criterion(Ex_mat, build_safe=false)
106106
blmo = build_blmo(m, N, ub)
107-
x0, active_set = build_start_point(Ex_mat, N, ub)
108-
z = greedy_incumbent(Ex_mat, N, ub)
109-
domain_oracle = build_domain_oracle(Ex_mat, n)
110107
heu = Boscia.Heuristic(Boscia.rounding_hyperplane_heuristic, 0.7, :hyperplane_aware_rounding)
108+
domain_oracle = build_domain_oracle(Ex_mat, n)
111109

112-
x, _, result = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, verbose=verbose, domain_oracle=domain_oracle, sharpness_exponent=θ, sharpness_constant=M, custom_heuristics=[heu])
113-
114-
blmo = build_blmo(m, N, ub)
110+
# precompile
111+
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
115112
x0, active_set = build_start_point(Ex_mat, N, ub)
116113
z = greedy_incumbent(Ex_mat, N, ub)
117-
domain_oracle = build_domain_oracle(Ex_mat, n)
118-
heu = Boscia.Heuristic(Boscia.rounding_hyperplane_heuristic, 0.7, :hyperplane_aware_rounding)
114+
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)
119115

120-
x_s, _, result = Boscia.solve(g, grad!, blmo, active_set=active_set, start_solution=z, verbose=verbose, line_search=FrankWolfe.Secant(domain_oracle=domain_oracle), domain_oracle=domain_oracle, sharpness_exponent=θ, sharpness_constant=M, custom_heuristics=[heu])
121-
122-
@show x
123-
@show x_s
124-
@show g(x), g(x_s)
125-
@test isapprox(g(x), g(x_s), atol=1e-4, rtol=1e-2)
116+
# proper run with MGLS and Adaptive
117+
line_search = FrankWolfe.MonotonicGenericStepsize(FrankWolfe.Adaptive(), domain_oracle)
118+
x0, active_set = build_start_point(Ex_mat, N, ub)
119+
z = greedy_incumbent(Ex_mat, N, ub)
120+
x, _, result = Boscia.solve(
121+
g,
122+
grad!,
123+
blmo,
124+
active_set=active_set,
125+
start_solution=z,
126+
verbose=verbose,
127+
domain_oracle=domain_oracle,
128+
custom_heuristics=[heu],
129+
line_search=line_search,
130+
)
131+
132+
# Run with Secant
133+
x0, active_set = build_start_point(Ex_mat, N, ub)
134+
z = greedy_incumbent(Ex_mat, N, ub)
135+
line_search = FrankWolfe.Secant(domain_oracle=domain_oracle)
136+
137+
x_s, _, result_s = Boscia.solve(
138+
g,
139+
grad!,
140+
blmo,
141+
active_set=active_set,
142+
start_solution=z,
143+
verbose=verbose,
144+
domain_oracle=domain_oracle,
145+
custom_heuristics=[heu],
146+
line_search=line_search,
147+
)
148+
149+
@test result_s[:dual_bound] <= g(x)
150+
@test result[:dual_bound] <= g(x_s)
151+
@test isapprox(g(x), g(x_s), rtol=1e-2)
126152
end
127153

src/node.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ function Bonobo.evaluate_node!(tree::Bonobo.BnBTree, node::FrankWolfeNode)
283283
elseif node.id == 1
284284
@debug "Lower bound of root node: $(lower_bound)"
285285
@debug "Current incumbent: $(tree.incumbent)"
286-
@assert lower_bound <= tree.incumbent + node.fw_dual_gap_limit "lower_bound <= tree.incumbent + node.fw_dual_gap_limit : $(lower_bound) <= $(tree.incumbent + node.fw_dual_gap_limit)"
286+
@assert lower_bound <= tree.incumbent + dual_gap "lower_bound <= tree.incumbent + dual_gap : $(lower_bound) <= $(tree.incumbent + dual_gap)"
287287
end
288288

289289
# Call heuristic

src/tightenings.jl

+6-4
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,13 @@ function tightening_lowerbound(tree, node, x, lower_bound)
218218
@debug "Using sharpness θ= and M=$M"
219219
fx = tree.root.problem.f(x)
220220

221+
if node.dual_gap < 0.0
222+
@assert abs(node.dual_gap) > eps() "node dual gap is negative: $(node.dual_gap)"
223+
node.dual_gap = 0.0
224+
end
225+
221226
sharpness_bound = M^(- 1 / θ) * 1/ 2 * (sqrt(bound_improvement) - M / 2 * node.dual_gap^θ)^(1 / θ) + fx - node.dual_gap
227+
222228
@debug "Sharpness: $lower_bound -> $sharpness_bound"
223229
@assert num_fractional == 0 || sharpness_bound >= lower_bound "$(num_fractional) == 0 || $(sharpness_bound) > $(lower_bound)"
224230
end
@@ -283,10 +289,6 @@ function prune_children(tree, node, lower_bound_base, x, vidx)
283289
@debug "prune right, from $(node.lb) -> $new_bound_right, ub $(tree.incumbent), lb $(node.lb)"
284290
prune_right = true
285291
end
286-
@assert !(
287-
(new_bound_left > tree.incumbent + tree.root.options[:dual_gap]) &&
288-
(new_bound_right > tree.incumbent + tree.root.options[:dual_gap])
289-
)
290292
end
291293
end
292294

src/utilities.jl

+36
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,42 @@ function sparse_min_via_enum(f, n, k, values=fill(0:1, n))
229229
return best_val, best_sol
230230
end
231231

232+
function min_via_enum_simplex(f, n, N, values=fill(0:1,n))
233+
solutions = Iterators.product(values...)
234+
best_val = Inf
235+
best_sol = nothing
236+
for sol in solutions
237+
sol_vec = collect(sol)
238+
if sum(sol_vec) > N
239+
continue
240+
end
241+
val = f(sol_vec)
242+
if best_val > val
243+
best_val = val
244+
best_sol = sol_vec
245+
end
246+
end
247+
return best_val, best_sol
248+
end
249+
250+
function min_via_enum_prob_simplex(f, n, N, values=fill(0:1,n))
251+
solutions = Iterators.product(values...)
252+
best_val = Inf
253+
best_sol = nothing
254+
for sol in solutions
255+
sol_vec = collect(sol)
256+
if sum(sol_vec) != N
257+
continue
258+
end
259+
val = f(sol_vec)
260+
if best_val > val
261+
best_val = val
262+
best_sol = sol_vec
263+
end
264+
end
265+
return best_val, best_sol
266+
end
267+
232268

233269
# utility function to print the values of the parameters
234270
_value_to_print(::Bonobo.BestFirstSearch) = "Move best bound"

0 commit comments

Comments
 (0)