@@ -32,6 +32,7 @@ This algorithm is restricted to problems with:
3232"""
3333struct TambyVanderpooten <: AbstractAlgorithm end
3434
35+ # This is Algorithm 1 from the paper.
3536function _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
8586
8687function _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
8992end
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.
91106function _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]
130139end
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.
132146function _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
260292end
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