Skip to content

Commit 3afcebc

Browse files
authored
Add RandomWeighting algorithm (#127)
1 parent 21b42d7 commit 3afcebc

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ The value must be one of the algorithms supported by MOA:
5959
* `MOA.Hierarchical()`
6060
* `MOA.KirlikSayin()`
6161
* `MOA.Lexicographic()` [default]
62+
* `MOA.RandomWeighting()`
6263
* `MOA.TambyVanderpooten()`
6364

6465
Consult their docstrings for details.

src/algorithms/RandomWeighting.jl

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2019, Oscar Dowson and contributors
2+
# This Source Code Form is subject to the terms of the Mozilla Public License,
3+
# v.2.0. If a copy of the MPL was not distributed with this file, You can
4+
# obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
"""
7+
RandomWeighting()
8+
9+
A heuristic solver that works by repeatedly solving a weighted sum problem with
10+
random weights.
11+
12+
## Supported optimizer attributes
13+
14+
* `MOI.TimeLimitSec()`: terminate if the time limit is exceeded and return the
15+
list of current solutions.
16+
17+
* `MOA.SolutionLimit()`: terminate once this many solutions have been found.
18+
19+
At least one of these two limits must be set.
20+
"""
21+
mutable struct RandomWeighting <: AbstractAlgorithm
22+
solution_limit::Union{Nothing,Int}
23+
RandomWeighting() = new(nothing)
24+
end
25+
26+
MOI.supports(::RandomWeighting, ::SolutionLimit) = true
27+
28+
function MOI.set(alg::RandomWeighting, ::SolutionLimit, value)
29+
alg.solution_limit = value
30+
return
31+
end
32+
33+
function MOI.get(alg::RandomWeighting, attr::SolutionLimit)
34+
return something(alg.solution_limit, default(alg, attr))
35+
end
36+
37+
function optimize_multiobjective!(algorithm::RandomWeighting, model::Optimizer)
38+
if MOI.get(model, MOI.TimeLimitSec()) === nothing &&
39+
algorithm.solution_limit === nothing
40+
error("At least `MOI.TimeLimitSec` or `MOA.SolutionLimit` must be set")
41+
end
42+
start_time = time()
43+
solutions = SolutionPoint[]
44+
sense = MOI.get(model, MOI.ObjectiveSense())
45+
P = MOI.output_dimension(model.f)
46+
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
47+
f = _scalarise(model.f, ones(P))
48+
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f)
49+
MOI.optimize!(model.inner)
50+
status = MOI.get(model.inner, MOI.TerminationStatus())
51+
if _is_scalar_status_optimal(status)
52+
X, Y = _compute_point(model, variables, model.f)
53+
push!(solutions, SolutionPoint(X, Y))
54+
else
55+
return status, nothing
56+
end
57+
# This double loop is a bit weird:
58+
# * the inner loop fills up SolutionLimit number of solutions. Then we cut
59+
# it back to nondominated.
60+
# * then the outer loop goes again
61+
while length(solutions) < MOI.get(algorithm, SolutionLimit())
62+
while length(solutions) < MOI.get(algorithm, SolutionLimit())
63+
if _time_limit_exceeded(model, start_time)
64+
return MOI.TIME_LIMIT, filter_nondominated(sense, solutions)
65+
end
66+
weights = rand(P)
67+
f = _scalarise(model.f, weights)
68+
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f)
69+
MOI.optimize!(model.inner)
70+
status = MOI.get(model.inner, MOI.TerminationStatus())
71+
if _is_scalar_status_optimal(status)
72+
X, Y = _compute_point(model, variables, model.f)
73+
push!(solutions, SolutionPoint(X, Y))
74+
end
75+
end
76+
solutions = filter_nondominated(sense, solutions)
77+
end
78+
return MOI.OPTIMAL, filter_nondominated(sense, solutions)
79+
end

test/algorithms/RandomWeighting.jl

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Copyright 2019, Oscar Dowson and contributors
2+
# This Source Code Form is subject to the terms of the Mozilla Public License,
3+
# v.2.0. If a copy of the MPL was not distributed with this file, You can
4+
# obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
module TestRandomWeighting
7+
8+
using Test
9+
10+
import HiGHS
11+
import MultiObjectiveAlgorithms as MOA
12+
import MultiObjectiveAlgorithms: MOI
13+
14+
include(joinpath(dirname(@__DIR__), "mock_optimizer.jl"))
15+
16+
function run_tests()
17+
for name in names(@__MODULE__; all = true)
18+
if startswith("$name", "test_")
19+
@testset "$name" begin
20+
getfield(@__MODULE__, name)()
21+
end
22+
end
23+
end
24+
return
25+
end
26+
27+
function test_error_attribute()
28+
model = MOA.Optimizer(HiGHS.Optimizer)
29+
MOI.set(model, MOA.Algorithm(), MOA.RandomWeighting())
30+
x = MOI.add_variables(model, 2)
31+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
32+
f = MOI.VectorOfVariables(x)
33+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
34+
@test_throws(
35+
ErrorException(
36+
"At least `MOI.TimeLimitSec` or `MOA.SolutionLimit` must be set",
37+
),
38+
MOI.optimize!(model),
39+
)
40+
return
41+
end
42+
43+
function test_knapsack_min()
44+
n = 10
45+
W = 2137.0
46+
C = Float64[
47+
566 611 506 180 817 184 585 423 26 317
48+
62 84 977 979 874 54 269 93 881 563
49+
]
50+
w = Float64[557, 898, 148, 63, 78, 964, 246, 662, 386, 272]
51+
model = MOA.Optimizer(HiGHS.Optimizer)
52+
MOI.set(model, MOA.Algorithm(), MOA.RandomWeighting())
53+
MOI.set(model, MOA.SolutionLimit(), 3)
54+
MOI.set(model, MOI.Silent(), true)
55+
x = MOI.add_variables(model, n)
56+
MOI.add_constraint.(model, x, MOI.ZeroOne())
57+
MOI.add_constraint(
58+
model,
59+
MOI.ScalarAffineFunction(
60+
[MOI.ScalarAffineTerm(w[j], x[j]) for j in 1:n],
61+
0.0,
62+
),
63+
MOI.LessThan(W),
64+
)
65+
f = MOI.VectorAffineFunction(
66+
[
67+
MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(-C[i, j], x[j]))
68+
for i in 1:2 for j in 1:n
69+
],
70+
[0.0, 0.0],
71+
)
72+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
73+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
74+
MOI.optimize!(model)
75+
results = [
76+
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1] => [-3394, -3817],
77+
[0, 1, 1, 1, 1, 0, 1, 0, 1, 1] => [-3042, -4627],
78+
[0, 0, 1, 1, 1, 0, 1, 1, 1, 1] => [-2854, -4636],
79+
]
80+
@test MOI.get(model, MOI.ResultCount()) == length(results)
81+
for (i, (x_sol, y_sol)) in enumerate(results)
82+
@test (x_sol, MOI.get(model, MOI.VariablePrimal(i), x); atol = 1e-6)
83+
@test (y_sol, MOI.get(model, MOI.ObjectiveValue(i)); atol = 1e-6)
84+
end
85+
@test MOI.get(model, MOI.ObjectiveBound()) [-3394, -4636]
86+
return
87+
end
88+
89+
function test_knapsack_max()
90+
n = 10
91+
W = 2137.0
92+
C = Float64[
93+
566 611 506 180 817 184 585 423 26 317
94+
62 84 977 979 874 54 269 93 881 563
95+
]
96+
w = Float64[557, 898, 148, 63, 78, 964, 246, 662, 386, 272]
97+
model = MOA.Optimizer(HiGHS.Optimizer)
98+
MOI.set(model, MOA.Algorithm(), MOA.RandomWeighting())
99+
MOI.set(model, MOA.SolutionLimit(), 3)
100+
MOI.set(model, MOI.Silent(), true)
101+
x = MOI.add_variables(model, n)
102+
MOI.add_constraint.(model, x, MOI.ZeroOne())
103+
MOI.add_constraint(
104+
model,
105+
MOI.ScalarAffineFunction(
106+
[MOI.ScalarAffineTerm(w[j], x[j]) for j in 1:n],
107+
0.0,
108+
),
109+
MOI.LessThan(W),
110+
)
111+
f = MOI.VectorAffineFunction(
112+
[
113+
MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(C[i, j], x[j])) for
114+
i in 1:2 for j in 1:n
115+
],
116+
[1.0, 0.0],
117+
)
118+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
119+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
120+
MOI.optimize!(model)
121+
results = [
122+
[0, 0, 1, 1, 1, 0, 1, 1, 1, 1] => [2855, 4636],
123+
[0, 1, 1, 1, 1, 0, 1, 0, 1, 1] => [3043, 4627],
124+
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1] => [3395, 3817],
125+
]
126+
@test MOI.get(model, MOI.ResultCount()) == length(results)
127+
for (i, (x_sol, y_sol)) in enumerate(results)
128+
@test (x_sol, MOI.get(model, MOI.VariablePrimal(i), x); atol = 1e-6)
129+
@test (y_sol, MOI.get(model, MOI.ObjectiveValue(i)); atol = 1e-6)
130+
end
131+
@test MOI.get(model, MOI.ObjectiveBound()) [3395, 4636]
132+
return
133+
end
134+
135+
function test_time_limit()
136+
n = 10
137+
W = 2137.0
138+
C = Float64[
139+
566 611 506 180 817 184 585 423 26 317
140+
62 84 977 979 874 54 269 93 881 563
141+
]
142+
w = Float64[557, 898, 148, 63, 78, 964, 246, 662, 386, 272]
143+
model = MOA.Optimizer(HiGHS.Optimizer)
144+
MOI.set(model, MOA.Algorithm(), MOA.RandomWeighting())
145+
MOI.set(model, MOI.Silent(), true)
146+
MOI.set(model, MOI.TimeLimitSec(), 0.0)
147+
x = MOI.add_variables(model, n)
148+
MOI.add_constraint.(model, x, MOI.ZeroOne())
149+
MOI.add_constraint(
150+
model,
151+
MOI.ScalarAffineFunction(
152+
[MOI.ScalarAffineTerm(w[j], x[j]) for j in 1:n],
153+
0.0,
154+
),
155+
MOI.LessThan(W),
156+
)
157+
f = MOI.VectorAffineFunction(
158+
[
159+
MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(C[i, j], x[j])) for
160+
i in 1:2 for j in 1:n
161+
],
162+
[0.0, 0.0],
163+
)
164+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
165+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
166+
MOI.optimize!(model)
167+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.TIME_LIMIT
168+
@test MOI.get(model, MOI.ResultCount()) >= 1
169+
return
170+
end
171+
172+
function test_unbounded()
173+
model = MOA.Optimizer(HiGHS.Optimizer)
174+
MOI.set(model, MOA.Algorithm(), MOA.RandomWeighting())
175+
MOI.set(model, MOI.Silent(), true)
176+
@test MOI.supports(model, MOA.SolutionLimit())
177+
MOI.set(model, MOA.SolutionLimit(), 10)
178+
x = MOI.add_variables(model, 2)
179+
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
180+
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
181+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
182+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
183+
MOI.optimize!(model)
184+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
185+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
186+
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
187+
return
188+
end
189+
190+
end # module TestRandomWeighting
191+
192+
TestRandomWeighting.run_tests()

0 commit comments

Comments
 (0)