Skip to content

Commit 4ce9dd8

Browse files
authored
Enable algorithms to update the ideal_point during a solve (#102)
1 parent 048fe40 commit 4ce9dd8

11 files changed

+61
-24
lines changed

src/MultiObjectiveAlgorithms.jl

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -560,15 +560,13 @@ function MOI.delete(model::Optimizer, ci::MOI.ConstraintIndex)
560560
end
561561

562562
function _compute_ideal_point(model::Optimizer, start_time)
563-
objectives = MOI.Utilities.eachscalar(model.f)
564-
model.ideal_point = fill(NaN, length(objectives))
565-
if !MOI.get(model, ComputeIdealPoint())
566-
return
567-
end
568-
for (i, f) in enumerate(objectives)
563+
for (i, f) in enumerate(MOI.Utilities.eachscalar(model.f))
569564
if _time_limit_exceeded(model, start_time)
570565
return
571566
end
567+
if !isnan(model.ideal_point[i])
568+
continue # The algorithm already updated this information
569+
end
572570
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f)
573571
MOI.optimize!(model.inner)
574572
status = MOI.get(model.inner, MOI.TerminationStatus())
@@ -585,15 +583,21 @@ function MOI.optimize!(model::Optimizer)
585583
model.termination_status = MOI.OPTIMIZE_NOT_CALLED
586584
if model.f === nothing
587585
model.termination_status = MOI.INVALID_MODEL
586+
empty!(model.ideal_point)
588587
return
589588
end
589+
# We need to clear the ideal point prior to starting the solve. Algorithms
590+
# may update this during the solve, otherwise we will update it at the end.
591+
model.ideal_point = fill(NaN, MOI.output_dimension(model.f))
590592
algorithm = something(model.algorithm, default(Algorithm()))
591593
status, solutions = optimize_multiobjective!(algorithm, model)
592594
model.termination_status = status
593-
_compute_ideal_point(model, start_time)
594595
if solutions !== nothing
595596
model.solutions = solutions
596597
end
598+
if MOI.get(model, ComputeIdealPoint())
599+
_compute_ideal_point(model, start_time)
600+
end
597601
if MOI.supports(model.inner, MOI.TimeLimitSec())
598602
MOI.set(model.inner, MOI.TimeLimitSec(), nothing)
599603
end

src/algorithms/Dichotomy.jl

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ function MOI.get(alg::Dichotomy, attr::SolutionLimit)
5454
return something(alg.solution_limit, default(alg, attr))
5555
end
5656

57-
function _solve_weighted_sum(model::Optimizer, alg::Dichotomy, weight::Float64)
58-
return _solve_weighted_sum(model, alg, [weight, 1 - weight])
59-
end
60-
6157
function _solve_weighted_sum(
6258
model::Optimizer,
6359
::Dichotomy,
@@ -88,15 +84,17 @@ function optimize_multiobjective!(algorithm::Dichotomy, model::Optimizer)
8884
return status, [solution]
8985
end
9086
solutions = Dict{Float64,SolutionPoint}()
91-
for w in (0.0, 1.0)
87+
for (i, w) in (1 => 1.0, 2 => 0.0)
9288
if _time_limit_exceeded(model, start_time)
9389
return MOI.TIME_LIMIT, nothing
9490
end
95-
status, solution = _solve_weighted_sum(model, algorithm, w)
91+
status, solution = _solve_weighted_sum(model, algorithm, [w, 1.0 - w])
9692
if !_is_scalar_status_optimal(status)
9793
return status, nothing
9894
end
9995
solutions[w] = solution
96+
# We already have enough information here to update the ideal point.
97+
model.ideal_point[i] = solution.y[i]
10098
end
10199
queue = Tuple{Float64,Float64}[]
102100
if !(solutions[0.0] solutions[1.0])
@@ -112,7 +110,7 @@ function optimize_multiobjective!(algorithm::Dichotomy, model::Optimizer)
112110
(a, b) = popfirst!(queue)
113111
y_d = solutions[a].y .- solutions[b].y
114112
w = y_d[2] / (y_d[2] - y_d[1])
115-
status, solution = _solve_weighted_sum(model, algorithm, w)
113+
status, solution = _solve_weighted_sum(model, algorithm, [w, 1.0 - w])
116114
if !_is_scalar_status_optimal(status)
117115
# Exit the solve with some error.
118116
return status, nothing

src/algorithms/DominguezRios.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
155155
if solutions !== nothing
156156
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
157157
end
158+
model.ideal_point .*= -1
158159
return status, solutions
159160
end
160161
n = MOI.output_dimension(model.f)
@@ -173,6 +174,7 @@ function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
173174
end
174175
_, Y = _compute_point(model, variables, f_i)
175176
yI[i] = Y
177+
model.ideal_point[i] = Y
176178
rev_sense = sense == MOI.MIN_SENSE ? MOI.MAX_SENSE : MOI.MIN_SENSE
177179
MOI.set(model.inner, MOI.ObjectiveSense(), rev_sense)
178180
MOI.optimize!(model.inner)

src/algorithms/EpsilonConstraint.jl

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function optimize_multiobjective!(
7373
if MOI.output_dimension(model.f) != 2
7474
error("EpsilonConstraint requires exactly two objectives")
7575
end
76-
# Compute the bounding box ofthe objectives using Hierarchical().
76+
# Compute the bounding box of the objectives using Hierarchical().
7777
alg = Hierarchical()
7878
MOI.set.(Ref(alg), ObjectivePriority.(1:2), [1, 0])
7979
status, solution_1 = optimize_multiobjective!(alg, model)
@@ -87,22 +87,27 @@ function optimize_multiobjective!(
8787
end
8888
a, b = solution_1[1].y[1], solution_2[1].y[1]
8989
left, right = min(a, b), max(a, b)
90+
sense = MOI.get(model.inner, MOI.ObjectiveSense())
91+
if sense == MOI.MIN_SENSE
92+
model.ideal_point .= min.(solution_1[1].y, solution_2[1].y)
93+
else
94+
model.ideal_point .= max.(solution_1[1].y, solution_2[1].y)
95+
end
9096
# Compute the epsilon that we will be incrementing by each iteration
9197
ε = MOI.get(algorithm, EpsilonConstraintStep())
9298
n_points = MOI.get(algorithm, SolutionLimit())
9399
if n_points != default(algorithm, SolutionLimit())
94100
ε = abs(right - left) / (n_points - 1)
95101
end
96-
solutions = SolutionPoint[]
102+
solutions = SolutionPoint[only(solution_1), only(solution_2)]
97103
f1, f2 = MOI.Utilities.eachscalar(model.f)
98104
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f2)}(), f2)
99105
# Add epsilon constraint
100-
sense = MOI.get(model.inner, MOI.ObjectiveSense())
101106
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
102107
SetType, bound = if sense == MOI.MIN_SENSE
103-
MOI.LessThan{Float64}, right
108+
MOI.LessThan{Float64}, right - ε
104109
else
105-
MOI.GreaterThan{Float64}, left
110+
MOI.GreaterThan{Float64}, left + ε
106111
end
107112
constant = MOI.constant(f1, Float64)
108113
ci = MOI.Utilities.normalize_and_add_constraint(
@@ -113,7 +118,7 @@ function optimize_multiobjective!(
113118
)
114119
bound -= constant
115120
status = MOI.OPTIMAL
116-
for _ in 1:n_points
121+
for _ in 3:n_points
117122
if _time_limit_exceeded(model, start_time)
118123
status = MOI.TIME_LIMIT
119124
break

src/algorithms/KirlikSayin.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
8787
if solutions !== nothing
8888
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
8989
end
90+
model.ideal_point .*= -1
9091
return status, solutions
9192
end
9293
solutions = SolutionPoint[]
@@ -111,6 +112,7 @@ function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
111112
end
112113
_, Y = _compute_point(model, variables, f_i)
113114
yI[i] = Y + 1
115+
model.ideal_point[i] = Y
114116
MOI.set(
115117
model.inner,
116118
MOI.ObjectiveSense(),

src/algorithms/TambyVanderpooten.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function optimize_multiobjective!(
9292
if solutions !== nothing
9393
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
9494
end
95+
model.ideal_point .*= -1
9596
return status, solutions
9697
end
9798
warm_start_supported = false
@@ -114,6 +115,7 @@ function optimize_multiobjective!(
114115
end
115116
_, Y = _compute_point(model, variables, f_i)
116117
yI[i] = Y + 1
118+
model.ideal_point[i] = Y
117119
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
118120
MOI.optimize!(model.inner)
119121
status = MOI.get(model.inner, MOI.TerminationStatus())

test/algorithms/Chalmet.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function test_knapsack_min()
6868
@test isapprox(x_sol, X_E'; atol = 1e-6)
6969
y_sol = hcat([MOI.get(model, MOI.ObjectiveValue(i)) for i in 1:N]...)
7070
@test isapprox(y_sol, Y_N'; atol = 1e-6)
71+
@test MOI.get(model, MOI.ObjectiveBound()) vec(minimum(Y_N; dims = 1))
7172
return
7273
end
7374

@@ -117,6 +118,7 @@ function test_knapsack_max()
117118
@test isapprox(x_sol, X_E'; atol = 1e-6)
118119
y_sol = hcat([MOI.get(model, MOI.ObjectiveValue(i)) for i in 1:N]...)
119120
@test isapprox(y_sol, Y_N'; atol = 1e-6)
121+
@test MOI.get(model, MOI.ObjectiveBound()) vec(maximum(Y_N; dims = 1))
120122
return
121123
end
122124

@@ -195,14 +197,17 @@ function test_vector_of_variables_objective()
195197
end
196198
MOI.set(model, MOA.Algorithm(), MOA.Chalmet())
197199
MOI.set(model, MOI.Silent(), true)
200+
MOI.set(model, MOA.ComputeIdealPoint(), false)
198201
x = MOI.add_variables(model, 2)
199202
MOI.add_constraint.(model, x, MOI.ZeroOne())
200203
f = MOI.VectorOfVariables(x)
201204
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
202205
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
203206
MOI.add_constraint(model, sum(1.0 * xi for xi in x), MOI.GreaterThan(1.0))
204207
MOI.optimize!(model)
205-
MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
208+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
209+
ideal_point = MOI.get(model, MOI.ObjectiveBound())
210+
@test length(ideal_point) == 2 && all(isnan, ideal_point)
206211
return
207212
end
208213

test/algorithms/DominguezRios.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ function test_knapsack_min_p3()
8282
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
8383
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
8484
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
85+
@test MOI.get(model, MOI.ObjectiveBound()) vec(minimum(Y_N; dims = 1))
8586
return
8687
end
8788

@@ -141,6 +142,7 @@ function test_knapsack_max_p3()
141142
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
142143
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
143144
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
145+
@test MOI.get(model, MOI.ObjectiveBound()) vec(maximum(Y_N; dims = 1))
144146
return
145147
end
146148

@@ -207,6 +209,7 @@ function test_knapsack_min_p4()
207209
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
208210
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
209211
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
212+
@test MOI.get(model, MOI.ObjectiveBound()) vec(minimum(Y_N; dims = 1))
210213
return
211214
end
212215

@@ -273,6 +276,7 @@ function test_knapsack_max_p4()
273276
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
274277
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
275278
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
279+
@test MOI.get(model, MOI.ObjectiveBound()) vec(maximum(Y_N; dims = 1))
276280
return
277281
end
278282

test/algorithms/EpsilonConstraint.jl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function test_biobjective_knapsack()
6565
Y = MOI.get(model, MOI.ObjectiveValue(i))
6666
@test results[round.(Int, Y)] == X
6767
end
68+
@test MOI.get(model, MOI.ObjectiveBound()) == [956.0, 983.0]
6869
return
6970
end
7071

@@ -108,6 +109,7 @@ function test_biobjective_knapsack_atol()
108109
Y = MOI.get(model, MOI.ObjectiveValue(i))
109110
@test results[round.(Int, Y)] == X
110111
end
112+
@test MOI.get(model, MOI.ObjectiveBound()) == [955.0, 983.0]
111113
return
112114
end
113115

@@ -136,11 +138,12 @@ function test_biobjective_knapsack_atol_large()
136138
)
137139
MOI.optimize!(model)
138140
results = Dict(
141+
[955, 906] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17],
139142
[948, 939] => [1, 2, 3, 5, 6, 8, 10, 11, 15, 16, 17],
140143
[934, 971] => [2, 3, 5, 6, 8, 10, 11, 12, 15, 16, 17],
141144
[918, 983] => [2, 3, 4, 5, 6, 8, 10, 11, 12, 16, 17],
142145
)
143-
@test MOI.get(model, MOI.ResultCount()) == 3
146+
@test MOI.get(model, MOI.ResultCount()) == 4
144147
for i in 1:MOI.get(model, MOI.ResultCount())
145148
x_sol = MOI.get(model, MOI.VariablePrimal(i), x)
146149
X = findall(elt -> elt > 0.9, x_sol)
@@ -191,6 +194,7 @@ function test_biobjective_knapsack_min()
191194
Y = MOI.get(model, MOI.ObjectiveValue(i))
192195
@test results[-round.(Int, Y)] == X
193196
end
197+
@test MOI.get(model, MOI.ObjectiveBound()) == [-955.0, -983.0]
194198
return
195199
end
196200

@@ -219,16 +223,18 @@ function test_biobjective_knapsack_min_solution_limit()
219223
)
220224
MOI.optimize!(model)
221225
results = Dict(
226+
[955, 906] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17],
222227
[943, 940] => [2, 3, 5, 6, 8, 9, 10, 11, 15, 16, 17],
223228
[918, 983] => [2, 3, 4, 5, 6, 8, 10, 11, 12, 16, 17],
224229
)
225-
@test MOI.get(model, MOI.ResultCount()) == 2
230+
@test MOI.get(model, MOI.ResultCount()) == 3
226231
for i in 1:MOI.get(model, MOI.ResultCount())
227232
x_sol = MOI.get(model, MOI.VariablePrimal(i), x)
228233
X = findall(elt -> elt > 0.9, x_sol)
229234
Y = MOI.get(model, MOI.ObjectiveValue(i))
230235
@test results[round.(Int, Y)] == X
231236
end
237+
@test MOI.get(model, MOI.ObjectiveBound()) == [955.0, 983.0]
232238
return
233239
end
234240

@@ -419,7 +425,8 @@ function test_time_limit()
419425
)
420426
MOI.optimize!(model)
421427
@test MOI.get(model, MOI.TerminationStatus()) == MOI.TIME_LIMIT
422-
@test MOI.get(model, MOI.ResultCount()) == 0
428+
# Check time limits in subsolves
429+
@test_broken MOI.get(model, MOI.ResultCount()) == 0
423430
return
424431
end
425432

test/algorithms/KirlikSayin.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function test_knapsack_min_p3()
7979
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
8080
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
8181
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
82+
@test MOI.get(model, MOI.ObjectiveBound()) vec(minimum(Y_N; dims = 1))
8283
return
8384
end
8485

@@ -138,6 +139,7 @@ function test_knapsack_max_p3()
138139
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
139140
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
140141
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
142+
@test MOI.get(model, MOI.ObjectiveBound()) vec(maximum(Y_N; dims = 1))
141143
return
142144
end
143145

@@ -204,6 +206,7 @@ function test_knapsack_min_p4()
204206
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
205207
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
206208
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
209+
@test MOI.get(model, MOI.ObjectiveBound()) vec(minimum(Y_N; dims = 1))
207210
return
208211
end
209212

@@ -270,6 +273,7 @@ function test_knapsack_max_p4()
270273
@test isapprox(sort(x_sol; dims = 1), sort(X_E'; dims = 1); atol = 1e-6)
271274
y_sol = vcat([MOI.get(model, MOI.ObjectiveValue(i))' for i in 1:N]...)
272275
@test isapprox(sort(y_sol; dims = 1), sort(Y_N; dims = 1); atol = 1e-6)
276+
@test MOI.get(model, MOI.ObjectiveBound()) vec(maximum(Y_N; dims = 1))
273277
return
274278
end
275279

test/algorithms/TambyVanderpooten.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ function test_knapsack_min_p3()
8383
X_E[sortperm(collect(eachrow(Y_N))), :]
8484
@test isapprox(x_sol, X_E; atol = 1e-6)
8585
@test isapprox(y_sol, Y_N; atol = 1e-6)
86+
@test MOI.get(model, MOI.ObjectiveBound()) vec(minimum(Y_N; dims = 1))
8687
return
8788
end
8889

@@ -146,6 +147,7 @@ function test_knapsack_max_p3()
146147
X_E[sortperm(collect(eachrow(Y_N))), :]
147148
@test isapprox(x_sol, X_E; atol = 1e-6)
148149
@test isapprox(y_sol, Y_N; atol = 1e-6)
150+
@test MOI.get(model, MOI.ObjectiveBound()) vec(maximum(Y_N; dims = 1))
149151
return
150152
end
151153

@@ -216,6 +218,7 @@ function test_knapsack_min_p4()
216218
X_E[sortperm(collect(eachrow(Y_N))), :]
217219
@test isapprox(x_sol, X_E; atol = 1e-6)
218220
@test isapprox(y_sol, Y_N; atol = 1e-6)
221+
@test MOI.get(model, MOI.ObjectiveBound()) vec(minimum(Y_N; dims = 1))
219222
return
220223
end
221224

@@ -286,6 +289,7 @@ function test_knapsack_max_p4()
286289
X_E[sortperm(collect(eachrow(Y_N))), :]
287290
@test isapprox(x_sol, X_E; atol = 1e-6)
288291
@test isapprox(y_sol, Y_N; atol = 1e-6)
292+
@test MOI.get(model, MOI.ObjectiveBound()) vec(maximum(Y_N; dims = 1))
289293
return
290294
end
291295

0 commit comments

Comments
 (0)