Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 177 additions & 105 deletions src/algorithms/TambyVanderpooten.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This algorithm is restricted to problems with:
"""
struct TambyVanderpooten <: AbstractAlgorithm end

# This is Algorithm 1 from the paper.
function _update_search_region(
U_N::Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}},
y::Vector{Float64},
Expand All @@ -40,23 +41,23 @@ function _update_search_region(
p = length(y)
bounds_to_remove = Vector{Float64}[]
bounds_to_add = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}()
for u in keys(U_N)
if all(y .< u)
for (u, u_n) in U_N
if _is_less(y, u, 0) # k=0 here because we want to check every element
push!(bounds_to_remove, u)
for l in 1:p
u_l = _get_child(u, y, l)
N = [
k == l ? [y] : [yi for yi in U_N[u][k] if yi[l] < y[l]]
for k in 1:p
k == l ? [y] : [yi for yi in u_n[k] if yi[l] < y[l]] for
k in 1:p
]
if all(!isempty(N[k]) for k in 1:p if k != l && u_l[k] != yN[k])
bounds_to_add[u_l] = N
end
end
else
for k in 1:p
if (y[k] == u[k]) && all(_project(y, k) .< _project(u, k))
push!(U_N[u][k], y)
if _is_less(y, u, k)
push!(u_n[k], y)
end
end
end
Expand All @@ -68,156 +69,227 @@ function _update_search_region(
return
end

function _is_less(y, u, k)
for i in 1:length(y)
if i == k
if !(y[i] == u[i])
return false
end
else
if !(y[i] < u[i])
return false
end
end
end
return true
end

function _get_child(u::Vector{Float64}, y::Vector{Float64}, k::Int)
@assert length(u) == length(y)
return vcat(u[1:(k-1)], y[k], u[(k+1):length(y)])
child = copy(u)
child[k] = y[k]
return child
end

# This is function h(u, k) in the paper.
function _h(k::Int, u::Vector{Float64}, yI::Vector{Float64})
h = 1.0
for (i, (u_i, yI_i)) in enumerate(zip(u, yI))
if i != k
h *= u_i - yI_i
end
end
return h
end

# This is Problem (6) in the paper. The upper bound M is our nadir point yN.
function _select_search_zone(
U_N::Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}},
yI::Vector{Float64},
yN::Vector{Float64},
)
upper_bounds = collect(keys(U_N))
p = length(yI)
hvs = [
u[k] == yN[k] ? 0.0 : prod(_project(u, k) .- _project(yI, k)) for
k in 1:p, u in upper_bounds
]
k_star, j_star = argmax(hvs).I
return k_star, upper_bounds[j_star]
k_star, u_star, v_star = 1, yN, -Inf
for k in 1:length(yI), u in keys(U_N)
if !isapprox(u[k], yN[k]; atol = 1e-6)
v = _h(k, u, yI)
if v > v_star
k_star, u_star, v_star = k, u, v
end
end
end
return k_star, u_star
end

function minimize_multiobjective!(
algorithm::TambyVanderpooten,
model::Optimizer,
)
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
warm_start_supported = false
if MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)
warm_start_supported = true
end
solutions = Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}}()
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
n = MOI.output_dimension(model.f)
status = _minimize_multiobjective!(
algorithm,
model,
model.inner,
model.f,
solutions,
)::MOI.TerminationStatusCode
return status, SolutionPoint[SolutionPoint(X, Y) for (Y, X) in solutions]
end

# To ensure the type stability of `.inner` and `.f`, we use a function barrier.
#
# We also pass in `solutions` to simplify the function and to ensure that we can
# return at any point from the this inner function with a partial list of primal
# solutions.
function _minimize_multiobjective!(
algorithm::TambyVanderpooten,
model::Optimizer,
inner::MOI.ModelLike,
f::MOI.AbstractVectorFunction,
solutions::Dict{Vector{Float64},Dict{MOI.VariableIndex,Float64}},
)
@assert MOI.get(inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
warm_start_supported =
MOI.supports(inner, MOI.VariablePrimalStart(), MOI.VariableIndex)
variables = MOI.get(inner, MOI.ListOfVariableIndices())
n = MOI.output_dimension(f)
# Compute the ideal (yI) and nadir (yN) points
yI, yN = zeros(n), zeros(n)
scalars = MOI.Utilities.scalarize(model.f)
scalars = MOI.Utilities.scalarize(f)
for (i, f_i) in enumerate(scalars)
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i)
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i)
MOI.set(inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
optimize_inner!(model)
status = MOI.get(model.inner, MOI.TerminationStatus())
status = MOI.get(inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
return status, nothing
return status
end
_, Y = _compute_point(model, variables, f_i)
_, y_i = _compute_point(model, variables, f_i)
_log_subproblem_solve(model, variables)
yI[i] = Y
model.ideal_point[i] = Y
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
model.ideal_point[i] = yI[i] = y_i
MOI.set(inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
optimize_inner!(model)
status = MOI.get(model.inner, MOI.TerminationStatus())
status = MOI.get(inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
_warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i)
return status, nothing
return status
end
_, Y = _compute_point(model, variables, f_i)
_, y_i = _compute_point(model, variables, f_i)
_log_subproblem_solve(model, variables)
yN[i] = Y + 1
# This is not really the nadir point, it's an upper bound on the
# nadir point because we later add epsilon constraints with a -1.
yN[i] = y_i + 1
end
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
U_N = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}()
MOI.set(inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
# Instead of adding and deleting constraints during the algorithm, we add
# them all here at the beginning.
ε_constraints = [
MOI.Utilities.normalize_and_add_constraint(
inner,
f_i,
MOI.LessThan(yN[i]),
) for (i, f_i) in enumerate(scalars)
]
obj_constants = MOI.constant.(scalars)
U_N = Dict{Vector{Float64},Vector{Vector{Vector{Float64}}}}(
# The nadir point, except for the ideal point in position k
yN => [[_get_child(yN, yI, k)] for k in 1:n],
)
V = [Tuple{Vector{Float64},Vector{Float64}}[] for k in 1:n]
U_N[yN] = [[_get_child(yN, yI, k)] for k in 1:n]
status = MOI.OPTIMAL
sum_f = MOI.Utilities.operate(+, Float64, scalars...)
while !isempty(U_N)
if (ret = _check_premature_termination(model)) !== nothing
status = ret
break
end
k, u = _select_search_zone(U_N, yI, yN)
MOI.set(
model.inner,
MOI.ObjectiveFunction{typeof(scalars[k])}(),
scalars[k],
)
ε_constraints = Any[]
for (i, f_i) in enumerate(scalars)
if i != k
ci = MOI.Utilities.normalize_and_add_constraint(
model.inner,
f_i,
MOI.LessThan{Float64}(u[i] - 1),
)
push!(ε_constraints, ci)
end
# Solve problem Π¹(k, u)
MOI.set(inner, MOI.ObjectiveFunction{typeof(scalars[k])}(), scalars[k])
# Update the constraints y_i < u_i. Note that this is a strict
# equality. We use an ε of 1.0. This is also why we use yN+1 when
# computing the nadir point.
for i in 1:n
u_i = ifelse(i == k, yN[i], u[i])
set = MOI.LessThan(u_i - 1.0 - obj_constants[i])
MOI.set(inner, MOI.ConstraintSet(), ε_constraints[i], set)
end
if u[k] ≠ yN[k]
if warm_start_supported
variables_start = solutions[first(U_N[u][k])]
for x_i in variables
MOI.set(
model.inner,
MOI.VariablePrimalStart(),
x_i,
variables_start[x_i],
)
end
if warm_start_supported && !isempty(solutions)
for (x_i, start_i) in solutions[last(U_N[u][k])]
MOI.set(inner, MOI.VariablePrimalStart(), x_i, start_i)
end
end
optimize_inner!(model)
_log_subproblem_solve(model, "auxillary subproblem")
status = MOI.get(model.inner, MOI.TerminationStatus())
optimize_inner!(model) # We don't log this first-stage subproblem.
status = MOI.get(inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
MOI.delete.(model, ε_constraints)
return status, nothing
end
y_k = MOI.get(model.inner, MOI.ObjectiveValue())
sum_f = sum(1.0 * s for s in scalars)
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(sum_f)}(), sum_f)
y_k_constraint = MOI.Utilities.normalize_and_add_constraint(
model.inner,
scalars[k],
MOI.EqualTo(y_k),
)
break
end
# Now solve problem Π²(k, u)
y_k = MOI.get(inner, MOI.ObjectiveValue())::Float64
set = MOI.LessThan(y_k - obj_constants[k])
MOI.set(inner, MOI.ConstraintSet(), ε_constraints[k], set)
MOI.set(inner, MOI.ObjectiveFunction{typeof(sum_f)}(), sum_f)
optimize_inner!(model)
status = MOI.get(model.inner, MOI.TerminationStatus())
status = MOI.get(inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
MOI.delete.(model, ε_constraints)
MOI.delete(model, y_k_constraint)
return status, nothing
break
end
X, Y = _compute_point(model, variables, model.f)
X, Y = _compute_point(model, variables, f)
_log_subproblem_solve(model, Y)
MOI.delete.(model, ε_constraints)
MOI.delete(model, y_k_constraint)
push!(V[k], (u, Y))
# We want `if !(Y in U_N[u][k])` but this tests exact equality. We want
# an approximate comparison.
# We want `if !(Y in U_N[u][k])` but this tests exact equality. We
# want an approximate comparison.
if all(!isapprox(Y; atol = 1e-6), U_N[u][k])
_update_search_region(U_N, Y, yN)
solutions[Y] = X
end
bounds_to_remove = Vector{Float64}[]
for u_i in keys(U_N)
for k in 1:n
if isapprox(u_i[k], yI[k]; atol = 1e-6)
push!(bounds_to_remove, u_i)
else
for (u_j, y_j) in V[k]
if all(_project(u_i, k) .<= _project(u_j, k)) &&
isapprox(y_j[k], u_i[k]; atol = 1e-6)
push!(bounds_to_remove, u_i)
end
end
end
end
_clean_search_region(U_N, yI, V, k)
end
MOI.delete.(model, ε_constraints)
return status
end

# This function is lines 10-17 of the paper. We re-order things a bit. The outer
# loop over u′ is the same, but we break the inner `foreach k` loop up into
# separate loops so that we don't need to loop over all `V` if one of the u′ has
# reached the ideal point.
function _clean_search_region(U_N, yI, V, k)
map_keys = collect(enumerate(keys(U_N)))
to_delete = fill(false, length(map_keys))
Threads.@threads for (i, u′) in map_keys
to_delete[i] = _clean_search_region_inner(u′, U_N, yI, V, k)
end # COV_EXCL_LINE
for (i, u′) in map_keys
if to_delete[i]
delete!(U_N, u′)
end
if !isempty(bounds_to_remove)
for bound_to_remove in bounds_to_remove
delete!(U_N, bound_to_remove)
end
return
end

function _clean_search_region_inner(u′, U_N, yI, V, k)
for (u′_k, yI_k) in zip(u′, yI)
if isapprox(u′_k, yI_k; atol = 1e-6)
return true
end
end
for (k, V_k) in enumerate(V)
for (u, y_k) in V_k
if _comparison_line_16(u′, u, y_k, k)
return true
end
end
end
return status, [SolutionPoint(X, Y) for (Y, X) in solutions]
return false
end

function _comparison_line_16(u′, u, y_k, k)
if !≈(y_k[k], u′[k]; atol = 1e-6)
return false
end
for (i, (u′_i, u_i)) in enumerate(zip(u′, u))
if i != k && !(u′_i <= u_i)
return false
end
end
return true
end
Loading