Skip to content

Commit a028157

Browse files
committed
Update
1 parent 99c521b commit a028157

File tree

1 file changed

+120
-85
lines changed

1 file changed

+120
-85
lines changed

src/algorithms/TambyVanderpooten.jl

Lines changed: 120 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This algorithm is restricted to problems with:
3232
"""
3333
struct TambyVanderpooten <: AbstractAlgorithm end
3434

35+
# This is Algorithm 1 from the paper.
3536
function _update_search_region(
3637
U_N::Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}},
3738
y::Vector{Float64},
@@ -40,14 +41,14 @@ function _update_search_region(
4041
p = length(y)
4142
bounds_to_remove = Vector{Float64}[]
4243
bounds_to_add = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}()
43-
for u in keys(U_N)
44+
for (u, u_n) in U_N
4445
if _is_less(y, u, 0) # k=0 here because we want to check every element
4546
push!(bounds_to_remove, u)
4647
for l in 1:p
4748
u_l = _get_child(u, y, l)
4849
N = [
49-
k == l ? [y] : [yi for yi in U_N[u][k] if yi[l] < y[l]]
50-
for k in 1:p
50+
k == l ? [y] : [yi for yi in u_n[k] if yi[l] < y[l]] for
51+
k in 1:p
5152
]
5253
if all(!isempty(N[k]) for k in 1:p if k != l && u_l[k] != yN[k])
5354
bounds_to_add[u_l] = N
@@ -56,7 +57,7 @@ function _update_search_region(
5657
else
5758
for k in 1:p
5859
if _is_less(y, u, k)
59-
push!(U_N[u][k], y)
60+
push!(u_n[k], y)
6061
end
6162
end
6263
end
@@ -85,29 +86,37 @@ end
8586

8687
function _get_child(u::Vector{Float64}, y::Vector{Float64}, k::Int)
8788
@assert length(u) == length(y)
88-
return vcat(u[1:(k-1)], y[k], u[(k+1):length(y)])
89+
child = copy(u)
90+
child[k] = y[k]
91+
return child
8992
end
9093

94+
# This is function h(u, k) in the paper.
95+
function _h(k::Int, u::Vector{Float64}, yI::Vector{Float64})
96+
h = 1.0
97+
for (i, (u_i, yI_i)) in enumerate(zip(u, yI))
98+
if i != k
99+
h *= u_i - yI_i
100+
end
101+
end
102+
return h
103+
end
104+
105+
# This is Problem (6) in the paper. The upper bound M is our nadir point yN.
91106
function _select_search_zone(
92107
U_N::Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}},
93108
yI::Vector{Float64},
94109
yN::Vector{Float64},
95110
)
96-
upper_bounds = collect(keys(U_N))
97-
p = length(yI)
98-
k_star, u_star, v_star = 1, first(upper_bounds), 0.0
99-
for k in 1:p
100-
for u in upper_bounds
101-
if u[k] != yN[k]
102-
v = 1.0
103-
for i in 1:p
104-
if i != k
105-
v *= u[i] - yI[i]
106-
end
107-
end
108-
if v > v_star
109-
k_star, u_star, v_star = k, u, v
110-
end
111+
if isempty(U_N)
112+
return 1, yN
113+
end
114+
k_star, u_star, v_star = 1, yN, -Inf
115+
for k in 1:length(yI), u in keys(U_N)
116+
if !isapprox(u[k], yN[k]; atol = 1e-6)
117+
v = _h(k, u, yI)
118+
if v > v_star
119+
k_star, u_star, v_star = k, u, v
111120
end
112121
end
113122
end
@@ -129,6 +138,11 @@ function minimize_multiobjective!(
129138
return status, SolutionPoint[SolutionPoint(X, Y) for (Y, X) in solutions]
130139
end
131140

141+
# To ensure the type stability of `.inner` and `.f`, we use a function barrier.
142+
#
143+
# We also pass in `solutions` to simplify the function and to ensure that we can
144+
# return at any point from the this inner function with a partial list of primal
145+
# solutions.
132146
function _minimize_multiobjective!(
133147
algorithm::TambyVanderpooten,
134148
model::Optimizer,
@@ -141,6 +155,7 @@ function _minimize_multiobjective!(
141155
MOI.supports(inner, MOI.VariablePrimalStart(), MOI.VariableIndex)
142156
variables = MOI.get(inner, MOI.ListOfVariableIndices())
143157
n = MOI.output_dimension(f)
158+
# Compute the ideal (yI) and nadir (yN) points
144159
yI, yN = zeros(n), zeros(n)
145160
scalars = MOI.Utilities.scalarize(f)
146161
for (i, f_i) in enumerate(scalars)
@@ -151,117 +166,137 @@ function _minimize_multiobjective!(
151166
if !_is_scalar_status_optimal(status)
152167
return status
153168
end
154-
_, Y = _compute_point(model, variables, f_i)
169+
_, y_i = _compute_point(model, variables, f_i)
155170
_log_subproblem_solve(model, variables)
156-
yI[i] = Y
157-
model.ideal_point[i] = Y
171+
model.ideal_point[i] = yI[i] = y_i
158172
MOI.set(inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
159173
optimize_inner!(model)
160174
status = MOI.get(inner, MOI.TerminationStatus())
161175
if !_is_scalar_status_optimal(status)
162176
_warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i)
163177
return status
164178
end
165-
_, Y = _compute_point(model, variables, f_i)
179+
_, y_i = _compute_point(model, variables, f_i)
166180
_log_subproblem_solve(model, variables)
167-
yN[i] = Y + 1
181+
# This is not really the nadir point, it's an upper bound on the
182+
# nadir point because we later add epsilon constraints with a -1.
183+
yN[i] = y_i + 1
168184
end
169185
MOI.set(inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
170-
U_N = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}()
186+
# Instead of adding and deleting constraints during the algorithm, we add
187+
# them all here at the beginning.
188+
ε_constraints = [
189+
MOI.Utilities.normalize_and_add_constraint(
190+
inner,
191+
f_i,
192+
MOI.LessThan(yN[i]),
193+
) for (i, f_i) in enumerate(scalars)
194+
]
195+
obj_constants = MOI.constant.(scalars)
196+
# U_N:
197+
# keys: upper bound vectors
198+
# values: a vector with n elements, U_N[u][k] is a vector of y
199+
U_N = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}(
200+
# The nadir point, except for the ideal point in position k
201+
yN => [[_get_child(yN, yI, k)] for k in 1:n],
202+
)
171203
V = [Tuple{Vector{Float64},Vector{Float64}}[] for k in 1:n]
172-
U_N[yN] = [[_get_child(yN, yI, k)] for k in 1:n]
173204
status = MOI.OPTIMAL
205+
sum_f = sum(1.0 * s for s in scalars)
174206
while !isempty(U_N)
175207
if (ret = _check_premature_termination(model)) !== nothing
176208
status = ret
177209
break
178210
end
179211
k, u = _select_search_zone(U_N, yI, yN)
212+
# Solve problem Π¹(k, u)
180213
MOI.set(inner, MOI.ObjectiveFunction{typeof(scalars[k])}(), scalars[k])
181-
ε_constraints = Any[]
182-
for (i, f_i) in enumerate(scalars)
183-
if i != k
184-
ci = MOI.Utilities.normalize_and_add_constraint(
185-
inner,
186-
f_i,
187-
MOI.LessThan{Float64}(u[i] - 1),
188-
)
189-
push!(ε_constraints, ci)
190-
end
214+
# Update the constraints y_i < u_i. Note that this is a strict
215+
# equality. We use an ε of 1.0. This is also why we use yN+1 when
216+
# computing the nadir point.
217+
for i in 1:n
218+
u_i = ifelse(i == k, yN[i], u[i])
219+
set = MOI.LessThan(u_i - 1.0 - obj_constants[i])
220+
MOI.set(inner, MOI.ConstraintSet(), ε_constraints[i], set)
191221
end
192-
if u[k] yN[k]
193-
if warm_start_supported
194-
variables_start = solutions[first(U_N[u][k])]
195-
for x_i in variables
196-
MOI.set(
197-
inner,
198-
MOI.VariablePrimalStart(),
199-
x_i,
200-
variables_start[x_i],
201-
)
202-
end
222+
# The isapprox is another way of saying does U_N[u][k] exist
223+
if warm_start_supported && !isapprox(u[k], yN[k]; atol = 1e-6)
224+
for (x_i, start_i) in solutions[last(U_N[u][k])]
225+
MOI.set(inner, MOI.VariablePrimalStart(), x_i, start_i)
203226
end
204227
end
205228
optimize_inner!(model)
206-
_log_subproblem_solve(model, "auxillary subproblem")
229+
# We don't log this first-stage subproblem.
207230
status = MOI.get(inner, MOI.TerminationStatus())
208231
if !_is_scalar_status_optimal(status)
209-
MOI.delete.(model, ε_constraints)
210-
return status
232+
break
211233
end
234+
# Now solve problem Π²(k, u)
212235
y_k = MOI.get(inner, MOI.ObjectiveValue())::Float64
213-
sum_f = sum(1.0 * s for s in scalars)
236+
set = MOI.LessThan(y_k - obj_constants[k])
237+
MOI.set(inner, MOI.ConstraintSet(), ε_constraints[k], set)
214238
MOI.set(inner, MOI.ObjectiveFunction{typeof(sum_f)}(), sum_f)
215-
y_k_constraint = MOI.Utilities.normalize_and_add_constraint(
216-
inner,
217-
scalars[k],
218-
MOI.EqualTo(y_k),
219-
)
220239
optimize_inner!(model)
221240
status = MOI.get(inner, MOI.TerminationStatus())
222241
if !_is_scalar_status_optimal(status)
223-
MOI.delete.(model, ε_constraints)
224-
MOI.delete(model, y_k_constraint)
225-
return status
242+
break
226243
end
227244
X, Y = _compute_point(model, variables, f)
228245
_log_subproblem_solve(model, Y)
229-
MOI.delete.(model, ε_constraints)
230-
MOI.delete(model, y_k_constraint)
231246
push!(V[k], (u, Y))
232-
# We want `if !(Y in U_N[u][k])` but this tests exact equality. We want
233-
# an approximate comparison.
247+
# We want `if !(Y in U_N[u][k])` but this tests exact equality. We
248+
# want an approximate comparison.
234249
if all(!isapprox(Y; atol = 1e-6), U_N[u][k])
235250
_update_search_region(U_N, Y, yN)
236251
solutions[Y] = X
237252
end
238-
bounds_to_remove = Vector{Float64}[]
239-
for u_i in keys(U_N)
240-
for k in 1:n
241-
if isapprox(u_i[k], yI[k]; atol = 1e-6)
242-
push!(bounds_to_remove, u_i)
243-
else
244-
for (u_j, y_j) in V[k]
245-
if isapprox(y_j[k], u_i[k]; atol = 1e-6) &&
246-
_is_less_eq(u_i, u_j, k)
247-
push!(bounds_to_remove, u_i)
248-
end
249-
end
250-
end
251-
end
253+
_clean_search_region(U_N, yI, V, k)
254+
end
255+
MOI.delete.(model, ε_constraints)
256+
return status
257+
end
258+
259+
# This function is lines 10-17 of the paper. We re-order things a bit. The outer
260+
# loop over u′ is the same, but we break the inner `foreach k` loop up into
261+
# separate loops so that we don't need to loop over all `V` if one of the u′ has
262+
# reached the ideal point.
263+
#
264+
# TODO: this loop is a good candidate for parallelisation.
265+
#
266+
# TODO: we could probably also be cleverer here, and just do a partial update
267+
# based on the most recent changes to V. Do we need to keep re-checking
268+
# everything?
269+
function _clean_search_region(U_N, yI, V, k)
270+
for u′ in keys(U_N)
271+
if _clean_search_region_inner(u′, U_N, yI, V, k)
272+
delete!(U_N, u′)
252273
end
253-
if !isempty(bounds_to_remove)
254-
for bound_to_remove in bounds_to_remove
255-
delete!(U_N, bound_to_remove)
274+
end
275+
return
276+
end
277+
278+
function _clean_search_region_inner(u′, U_N, yI, V, k)
279+
for (u′_k, yI_k) in zip(u′, yI)
280+
if isapprox(u′_k, yI_k; atol = 1e-6)
281+
return true
282+
end
283+
end
284+
for (k, V_k) in enumerate(V)
285+
for (u, y_k) in V_k
286+
if _comparison_line_16(u′, u, y_k, k)
287+
return true
256288
end
257289
end
258290
end
259-
return status
291+
return false
260292
end
261293

262-
function _is_less_eq(y, u, k)
263-
for i in 1:length(y)
264-
if i != k && !(y[i] <= u[i])
294+
function _comparison_line_16(u′, u, y_k, k)
295+
if !(y_k[k], u′[k]; atol = 1e-6)
296+
return false
297+
end
298+
for (i, (u′_i, u_i)) in enumerate(zip(u′, u))
299+
if i != k && !(u′_i <= u_i)
265300
return false
266301
end
267302
end

0 commit comments

Comments
 (0)