Skip to content

Commit 067521e

Browse files
authored
[TambyVanderpooten] improve performance based on profiling (#170)
1 parent 6b7ae28 commit 067521e

File tree

1 file changed

+177
-105
lines changed

1 file changed

+177
-105
lines changed

src/algorithms/TambyVanderpooten.jl

Lines changed: 177 additions & 105 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,23 +41,23 @@ 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-
if all(y .< u)
44+
for (u, u_n) in U_N
45+
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
5455
end
5556
end
5657
else
5758
for k in 1:p
58-
if (y[k] == u[k]) && all(_project(y, k) .< _project(u, k))
59-
push!(U_N[u][k], y)
59+
if _is_less(y, u, k)
60+
push!(u_n[k], y)
6061
end
6162
end
6263
end
@@ -68,156 +69,227 @@ function _update_search_region(
6869
return
6970
end
7071

72+
function _is_less(y, u, k)
73+
for i in 1:length(y)
74+
if i == k
75+
if !(y[i] == u[i])
76+
return false
77+
end
78+
else
79+
if !(y[i] < u[i])
80+
return false
81+
end
82+
end
83+
end
84+
return true
85+
end
86+
7187
function _get_child(u::Vector{Float64}, y::Vector{Float64}, k::Int)
7288
@assert length(u) == length(y)
73-
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
92+
end
93+
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
74103
end
75104

105+
# This is Problem (6) in the paper. The upper bound M is our nadir point yN.
76106
function _select_search_zone(
77107
U_N::Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}},
78108
yI::Vector{Float64},
79109
yN::Vector{Float64},
80110
)
81-
upper_bounds = collect(keys(U_N))
82-
p = length(yI)
83-
hvs = [
84-
u[k] == yN[k] ? 0.0 : prod(_project(u, k) .- _project(yI, k)) for
85-
k in 1:p, u in upper_bounds
86-
]
87-
k_star, j_star = argmax(hvs).I
88-
return k_star, upper_bounds[j_star]
111+
k_star, u_star, v_star = 1, yN, -Inf
112+
for k in 1:length(yI), u in keys(U_N)
113+
if !isapprox(u[k], yN[k]; atol = 1e-6)
114+
v = _h(k, u, yI)
115+
if v > v_star
116+
k_star, u_star, v_star = k, u, v
117+
end
118+
end
119+
end
120+
return k_star, u_star
89121
end
90122

91123
function minimize_multiobjective!(
92124
algorithm::TambyVanderpooten,
93125
model::Optimizer,
94126
)
95-
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
96-
warm_start_supported = false
97-
if MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)
98-
warm_start_supported = true
99-
end
100127
solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}()
101-
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
102-
n = MOI.output_dimension(model.f)
128+
status = _minimize_multiobjective!(
129+
algorithm,
130+
model,
131+
model.inner,
132+
model.f,
133+
solutions,
134+
)::MOI.TerminationStatusCode
135+
return status, SolutionPoint[SolutionPoint(X, Y) for (Y, X) in solutions]
136+
end
137+
138+
# To ensure the type stability of `.inner` and `.f`, we use a function barrier.
139+
#
140+
# We also pass in `solutions` to simplify the function and to ensure that we can
141+
# return at any point from the this inner function with a partial list of primal
142+
# solutions.
143+
function _minimize_multiobjective!(
144+
algorithm::TambyVanderpooten,
145+
model::Optimizer,
146+
inner::MOI.ModelLike,
147+
f::MOI.AbstractVectorFunction,
148+
solutions::Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}},
149+
)
150+
@assert MOI.get(inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
151+
warm_start_supported =
152+
MOI.supports(inner, MOI.VariablePrimalStart(), MOI.VariableIndex)
153+
variables = MOI.get(inner, MOI.ListOfVariableIndices())
154+
n = MOI.output_dimension(f)
155+
# Compute the ideal (yI) and nadir (yN) points
103156
yI, yN = zeros(n), zeros(n)
104-
scalars = MOI.Utilities.scalarize(model.f)
157+
scalars = MOI.Utilities.scalarize(f)
105158
for (i, f_i) in enumerate(scalars)
106-
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i)
107-
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
159+
MOI.set(inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i)
160+
MOI.set(inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
108161
optimize_inner!(model)
109-
status = MOI.get(model.inner, MOI.TerminationStatus())
162+
status = MOI.get(inner, MOI.TerminationStatus())
110163
if !_is_scalar_status_optimal(status)
111-
return status, nothing
164+
return status
112165
end
113-
_, Y = _compute_point(model, variables, f_i)
166+
_, y_i = _compute_point(model, variables, f_i)
114167
_log_subproblem_solve(model, variables)
115-
yI[i] = Y
116-
model.ideal_point[i] = Y
117-
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
168+
model.ideal_point[i] = yI[i] = y_i
169+
MOI.set(inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
118170
optimize_inner!(model)
119-
status = MOI.get(model.inner, MOI.TerminationStatus())
171+
status = MOI.get(inner, MOI.TerminationStatus())
120172
if !_is_scalar_status_optimal(status)
121173
_warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i)
122-
return status, nothing
174+
return status
123175
end
124-
_, Y = _compute_point(model, variables, f_i)
176+
_, y_i = _compute_point(model, variables, f_i)
125177
_log_subproblem_solve(model, variables)
126-
yN[i] = Y + 1
178+
# This is not really the nadir point, it's an upper bound on the
179+
# nadir point because we later add epsilon constraints with a -1.
180+
yN[i] = y_i + 1
127181
end
128-
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
129-
U_N = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}()
182+
MOI.set(inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
183+
# Instead of adding and deleting constraints during the algorithm, we add
184+
# them all here at the beginning.
185+
ε_constraints = [
186+
MOI.Utilities.normalize_and_add_constraint(
187+
inner,
188+
f_i,
189+
MOI.LessThan(yN[i]),
190+
) for (i, f_i) in enumerate(scalars)
191+
]
192+
obj_constants = MOI.constant.(scalars)
193+
U_N = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}(
194+
# The nadir point, except for the ideal point in position k
195+
yN => [[_get_child(yN, yI, k)] for k in 1:n],
196+
)
130197
V = [Tuple{Vector{Float64},Vector{Float64}}[] for k in 1:n]
131-
U_N[yN] = [[_get_child(yN, yI, k)] for k in 1:n]
132198
status = MOI.OPTIMAL
199+
sum_f = MOI.Utilities.operate(+, Float64, scalars...)
133200
while !isempty(U_N)
134201
if (ret = _check_premature_termination(model)) !== nothing
135202
status = ret
136203
break
137204
end
138205
k, u = _select_search_zone(U_N, yI, yN)
139-
MOI.set(
140-
model.inner,
141-
MOI.ObjectiveFunction{typeof(scalars[k])}(),
142-
scalars[k],
143-
)
144-
ε_constraints = Any[]
145-
for (i, f_i) in enumerate(scalars)
146-
if i != k
147-
ci = MOI.Utilities.normalize_and_add_constraint(
148-
model.inner,
149-
f_i,
150-
MOI.LessThan{Float64}(u[i] - 1),
151-
)
152-
push!(ε_constraints, ci)
153-
end
206+
# Solve problem Π¹(k, u)
207+
MOI.set(inner, MOI.ObjectiveFunction{typeof(scalars[k])}(), scalars[k])
208+
# Update the constraints y_i < u_i. Note that this is a strict
209+
# equality. We use an ε of 1.0. This is also why we use yN+1 when
210+
# computing the nadir point.
211+
for i in 1:n
212+
u_i = ifelse(i == k, yN[i], u[i])
213+
set = MOI.LessThan(u_i - 1.0 - obj_constants[i])
214+
MOI.set(inner, MOI.ConstraintSet(), ε_constraints[i], set)
154215
end
155-
if u[k] yN[k]
156-
if warm_start_supported
157-
variables_start = solutions[first(U_N[u][k])]
158-
for x_i in variables
159-
MOI.set(
160-
model.inner,
161-
MOI.VariablePrimalStart(),
162-
x_i,
163-
variables_start[x_i],
164-
)
165-
end
216+
if warm_start_supported && !isempty(solutions)
217+
for (x_i, start_i) in solutions[last(U_N[u][k])]
218+
MOI.set(inner, MOI.VariablePrimalStart(), x_i, start_i)
166219
end
167220
end
168-
optimize_inner!(model)
169-
_log_subproblem_solve(model, "auxillary subproblem")
170-
status = MOI.get(model.inner, MOI.TerminationStatus())
221+
optimize_inner!(model) # We don't log this first-stage subproblem.
222+
status = MOI.get(inner, MOI.TerminationStatus())
171223
if !_is_scalar_status_optimal(status)
172-
MOI.delete.(model, ε_constraints)
173-
return status, nothing
174-
end
175-
y_k = MOI.get(model.inner, MOI.ObjectiveValue())
176-
sum_f = sum(1.0 * s for s in scalars)
177-
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(sum_f)}(), sum_f)
178-
y_k_constraint = MOI.Utilities.normalize_and_add_constraint(
179-
model.inner,
180-
scalars[k],
181-
MOI.EqualTo(y_k),
182-
)
224+
break
225+
end
226+
# Now solve problem Π²(k, u)
227+
y_k = MOI.get(inner, MOI.ObjectiveValue())::Float64
228+
set = MOI.LessThan(y_k - obj_constants[k])
229+
MOI.set(inner, MOI.ConstraintSet(), ε_constraints[k], set)
230+
MOI.set(inner, MOI.ObjectiveFunction{typeof(sum_f)}(), sum_f)
183231
optimize_inner!(model)
184-
status = MOI.get(model.inner, MOI.TerminationStatus())
232+
status = MOI.get(inner, MOI.TerminationStatus())
185233
if !_is_scalar_status_optimal(status)
186-
MOI.delete.(model, ε_constraints)
187-
MOI.delete(model, y_k_constraint)
188-
return status, nothing
234+
break
189235
end
190-
X, Y = _compute_point(model, variables, model.f)
236+
X, Y = _compute_point(model, variables, f)
191237
_log_subproblem_solve(model, Y)
192-
MOI.delete.(model, ε_constraints)
193-
MOI.delete(model, y_k_constraint)
194238
push!(V[k], (u, Y))
195-
# We want `if !(Y in U_N[u][k])` but this tests exact equality. We want
196-
# an approximate comparison.
239+
# We want `if !(Y in U_N[u][k])` but this tests exact equality. We
240+
# want an approximate comparison.
197241
if all(!isapprox(Y; atol = 1e-6), U_N[u][k])
198242
_update_search_region(U_N, Y, yN)
199243
solutions[Y] = X
200244
end
201-
bounds_to_remove = Vector{Float64}[]
202-
for u_i in keys(U_N)
203-
for k in 1:n
204-
if isapprox(u_i[k], yI[k]; atol = 1e-6)
205-
push!(bounds_to_remove, u_i)
206-
else
207-
for (u_j, y_j) in V[k]
208-
if all(_project(u_i, k) .<= _project(u_j, k)) &&
209-
isapprox(y_j[k], u_i[k]; atol = 1e-6)
210-
push!(bounds_to_remove, u_i)
211-
end
212-
end
213-
end
214-
end
245+
_clean_search_region(U_N, yI, V, k)
246+
end
247+
MOI.delete.(model, ε_constraints)
248+
return status
249+
end
250+
251+
# This function is lines 10-17 of the paper. We re-order things a bit. The outer
252+
# loop over u′ is the same, but we break the inner `foreach k` loop up into
253+
# separate loops so that we don't need to loop over all `V` if one of the u′ has
254+
# reached the ideal point.
255+
function _clean_search_region(U_N, yI, V, k)
256+
map_keys = collect(enumerate(keys(U_N)))
257+
to_delete = fill(false, length(map_keys))
258+
Threads.@threads for (i, u′) in map_keys
259+
to_delete[i] = _clean_search_region_inner(u′, U_N, yI, V, k)
260+
end # COV_EXCL_LINE
261+
for (i, u′) in map_keys
262+
if to_delete[i]
263+
delete!(U_N, u′)
215264
end
216-
if !isempty(bounds_to_remove)
217-
for bound_to_remove in bounds_to_remove
218-
delete!(U_N, bound_to_remove)
265+
end
266+
return
267+
end
268+
269+
function _clean_search_region_inner(u′, U_N, yI, V, k)
270+
for (u′_k, yI_k) in zip(u′, yI)
271+
if isapprox(u′_k, yI_k; atol = 1e-6)
272+
return true
273+
end
274+
end
275+
for (k, V_k) in enumerate(V)
276+
for (u, y_k) in V_k
277+
if _comparison_line_16(u′, u, y_k, k)
278+
return true
219279
end
220280
end
221281
end
222-
return status, [SolutionPoint(X, Y) for (Y, X) in solutions]
282+
return false
283+
end
284+
285+
function _comparison_line_16(u′, u, y_k, k)
286+
if !(y_k[k], u′[k]; atol = 1e-6)
287+
return false
288+
end
289+
for (i, (u′_i, u_i)) in enumerate(zip(u′, u))
290+
if i != k && !(u′_i <= u_i)
291+
return false
292+
end
293+
end
294+
return true
223295
end

0 commit comments

Comments
 (0)