Skip to content

Commit c3b5602

Browse files
committed
Add Sandwiching
1 parent e5ebe9b commit c3b5602

File tree

4 files changed

+249
-1
lines changed

4 files changed

+249
-1
lines changed

Project.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ version = "1.4.3"
77
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
88
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
99

10+
[weakdeps]
11+
Polyhedra = "67491407-f73d-577b-9b50-8179a7c68029"
12+
13+
[extensions]
14+
PolyhedraExt = "Polyhedra"
15+
1016
[compat]
1117
Combinatorics = "1"
1218
HiGHS = "1"
@@ -21,6 +27,7 @@ HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
2127
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
2228
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
2329
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
30+
Polyhedra = "67491407-f73d-577b-9b50-8179a7c68029"
2431

2532
[targets]
26-
test = ["HiGHS", "Ipopt", "JSON", "Test"]
33+
test = ["HiGHS", "Ipopt", "JSON", "Test", "Polyhedra"]

ext/PolyhedraExt.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module PolyhedraExt
2+
3+
using MultiObjectiveAlgorithms
4+
using Polyhedra
5+
6+
function MultiObjectiveAlgorithms._halfspaces(IPS::Vector{Vector{Float64}})
7+
V = vrep(IPS)
8+
H = halfspaces(doubledescription(V))
9+
return [(-H_i.a, -H_i.β) for H_i in H]
10+
end
11+
12+
end

src/algorithms/Sandwiching.jl

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
function _halfspaces(IPS::Vector{Vector{Float64}})
2+
error("MOA.Sandwiching requires Polyhedra.jl to be loaded")
3+
end
4+
5+
mutable struct Sandwiching <: AbstractAlgorithm
6+
precision::Float64
7+
end
8+
9+
tol = 1e-3
10+
11+
function _compute_anchors(model::Optimizer)
12+
anchors = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}()
13+
n = MOI.output_dimension(model.f)
14+
scalars = MOI.Utilities.scalarize(model.f)
15+
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
16+
yI, yUB = zeros(n), zeros(n)
17+
for (i, f_i) in enumerate(scalars)
18+
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i) # ρ * sum(f_j for (j, f_j) in enumerate(model.f) if j != i)
19+
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
20+
MOI.optimize!(model.inner)
21+
# status check
22+
X, Y = _compute_point(model, variables, model.f)
23+
model.ideal_point[i] = Y[i]
24+
yI[i] = Y[i]
25+
anchors[Y] = X
26+
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
27+
MOI.optimize!(model.inner)
28+
# status check
29+
_, Y = _compute_point(model, variables, f_i)
30+
yUB[i] = Y
31+
end
32+
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
33+
return yI, yUB, anchors
34+
end
35+
36+
@enum DistanceMeasure SUB CUR SOL
37+
38+
function _distance(w̄, b̄, OPS, model)
39+
n = MOI.output_dimension(model.f)
40+
optimizer = typeof(model.inner.optimizer)
41+
δ_optimizer = optimizer()
42+
MOI.set(δ_optimizer, MOI.Silent(), true)
43+
x = MOI.add_variables(δ_optimizer, n)
44+
for (w, b) in OPS
45+
MOI.add_constraint(
46+
δ_optimizer,
47+
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0),
48+
MOI.GreaterThan(b),
49+
)
50+
end
51+
MOI.set(
52+
δ_optimizer,
53+
MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
54+
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w̄, x), 0.0),
55+
)
56+
MOI.set(δ_optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE)
57+
MOI.optimize!(δ_optimizer)
58+
δ =- MOI.get(δ_optimizer, MOI.ObjectiveValue())
59+
return δ
60+
end
61+
62+
function _select_next_halfspace(H, OPS, model)
63+
distances = [_distance(w, b, OPS, model) for (w, b) in H]
64+
@info "Distances: $(Dict(zip(H, distances)))"
65+
index = argmax(distances)
66+
w, b = H[index]
67+
return distances[index], w, b
68+
end
69+
70+
function optimize_multiobjective!(algorithm::Sandwiching, model::Optimizer)
71+
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
72+
ε = algorithm.precision
73+
start_time = time()
74+
solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}()
75+
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
76+
n = MOI.output_dimension(model.f)
77+
scalars = MOI.Utilities.scalarize(model.f)
78+
yI, yUB, anchors = _compute_anchors(model)
79+
merge!(solutions, anchors)
80+
@info "yI: $(yI)"
81+
@info "yUB: $(yUB)"
82+
IPS = [yUB, keys(anchors)...]
83+
OPS = Tuple{Vector{Float64}, Float64}[]
84+
for i in 1:n
85+
e_i = Float64.(1:n .== i)
86+
push!(OPS, (e_i, yI[i])) # e_i' * y >= yI_i
87+
push!(OPS, (-e_i, -yUB[i])) # -e_i' * y >= -yUB_i ⟹ e_i' * y <= yUB_i
88+
end
89+
@info "IPS: $(IPS)"
90+
@info "OPS: $(OPS)"
91+
u = MOI.add_variables(model.inner, n)
92+
u_constraints = [ # u_i >= 0 for all i = 1:n
93+
MOI.add_constraint(
94+
model.inner,
95+
u_i,
96+
MOI.GreaterThan{Float64}(0),
97+
)
98+
for u_i in u
99+
]
100+
f_constraints = [ # f_i + u_i <= yUB_i for all i = 1:n
101+
MOI.Utilities.normalize_and_add_constraint(
102+
model.inner,
103+
scalars[i] + u[i],
104+
MOI.LessThan(yUB[i]),
105+
) for i in 1:n
106+
]
107+
H = _halfspaces(IPS)
108+
109+
count = 0
110+
111+
while !isempty(H)
112+
count += 1
113+
@info "-- Iteration #$(count) --"
114+
@info "HalfSpaces: $(H)"
115+
δ, w, b = _select_next_halfspace(H, OPS, model)
116+
@info "Selected halfspace: w: $(w), b: $(b)"
117+
@info "δ: $(δ)"
118+
if δ - tol > ε # added some convergence tolerance
119+
# would not terminate when precision is set to 0
120+
new_f = sum(w[i] * (scalars[i] + u[i]) for i in 1:n) # w' * (f(x) + u)
121+
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(new_f)}(), new_f)
122+
MOI.optimize!(model.inner)
123+
β̄ = MOI.get(model.inner, MOI.ObjectiveValue())
124+
@info "β̄: $(β̄)"
125+
X, Y = _compute_point(model, variables, model.f)
126+
@info "Y: $(Y)"
127+
solutions[Y] = X
128+
push!(OPS, (w, β̄))
129+
@info "Added halfspace w: $(w), b: $(β̄) to OPS"
130+
IPS = push!(IPS, Y)
131+
else
132+
break
133+
end
134+
@info "IPS: $(IPS)"
135+
@info "OPS: $(OPS)"
136+
H = _halfspaces(IPS)
137+
if count == 10
138+
break
139+
end
140+
end
141+
MOI.delete.(model.inner, f_constraints)
142+
MOI.delete.(model.inner, u_constraints)
143+
MOI.delete.(model.inner, u)
144+
return MOI.OPTIMAL, [SolutionPoint(X, Y) for (Y, X) in solutions]
145+
end

test/algorithms/Sandwiching.jl

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
module TestSandwiching
2+
3+
using Test
4+
5+
import HiGHS
6+
using Polyhedra
7+
import MultiObjectiveAlgorithms as MOA
8+
import MultiObjectiveAlgorithms: MOI
9+
10+
function run_tests()
11+
for name in names(@__MODULE__; all = true)
12+
if startswith("$name", "test_")
13+
@testset "$name" begin
14+
getfield(@__MODULE__, name)()
15+
end
16+
end
17+
end
18+
return
19+
end
20+
21+
22+
# From International Doctoral School Algorithmic Decision Theory: MCDA and MOO
23+
# Lecture 2: Multiobjective Linear Programming
24+
# Matthias Ehrgott
25+
# Department of Engineering Science, The University of Auckland, New Zealand
26+
# Laboratoire d’Informatique de Nantes Atlantique, CNRS, Universit´e de Nantes, France
27+
function test_molp()
28+
C = Float64[
29+
3 1
30+
-1 -2
31+
]
32+
p = size(C, 1)
33+
A = Float64[
34+
0 1
35+
3 -1
36+
]
37+
m, n = size(A)
38+
b = Float64[3, 6]
39+
model = MOI.instantiate(; with_bridge_type = Float64) do
40+
return MOA.Optimizer(HiGHS.Optimizer)
41+
end
42+
MOI.set(model, MOA.Algorithm(), MOA.Sandwiching(0.0))
43+
MOI.set(model, MOI.Silent(), true)
44+
x = MOI.add_variables(model, n)
45+
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
46+
for i in 1:m
47+
MOI.add_constraint(
48+
model,
49+
MOI.ScalarAffineFunction(
50+
[MOI.ScalarAffineTerm(A[i, j], x[j]) for j in 1:n],
51+
0.0,
52+
),
53+
MOI.LessThan(b[i]),
54+
)
55+
end
56+
f = MOI.VectorAffineFunction(
57+
[
58+
MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(C[i, j], x[j]))
59+
for i in 1:p for j in 1:n
60+
],
61+
zeros(p),
62+
)
63+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
64+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
65+
MOI.optimize!(model)
66+
N = MOI.get(model, MOI.ResultCount())
67+
solutions = reverse([MOI.get(model, MOI.VariablePrimal(i), x) => MOI.get(model, MOI.ObjectiveValue(i)) for i in 1:N])
68+
results = reverse([
69+
[0., 0.] => [0., 0.],
70+
[0., 3.] => [3., -6.],
71+
[3., 3.] => [12., -9.],
72+
])
73+
@test length(solutions) == length(results)
74+
for (sol, res) in zip(solutions, results)
75+
x_sol, y_sol = sol
76+
x_res, y_res = res
77+
@test (x_sol, x_res; atol = 1e-6)
78+
@test (y_sol, y_res; atol = 1e-6)
79+
end
80+
end
81+
82+
end
83+
84+
TestSandwiching.run_tests()

0 commit comments

Comments
 (0)