Skip to content

Commit fb2b846

Browse files
committed
Update
1 parent 6b86bd0 commit fb2b846

3 files changed

Lines changed: 85 additions & 96 deletions

File tree

ext/Polyhedra/GeneralDichotomy.jl

Lines changed: 77 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -12,86 +12,81 @@ mutable struct Weight
1212
removed::Bool # weights that are no longer part of the decomposition
1313
end
1414

15-
struct CustomVec
15+
struct _ApproxVector
1616
value::Vector{Float64}
1717
value_int::Vector{Int}
1818

19-
function CustomVec(vec::Vector{Float64}, scaling::Real)
20-
return new(vec, round.(Int, scaling .* vec))
19+
function _ApproxVector(x::Vector{Float64}; atol::Float64 = 1e-6)
20+
return new(x, round.(Int, x ./ atol))
2121
end
2222
end
2323

24-
Base.:(==)(a::CustomVec, b::CustomVec) = a.value_int == b.value_int
24+
Base.:(==)(a::_ApproxVector, b::_ApproxVector) = a.value_int == b.value_int
2525

26-
Base.hash(a::CustomVec, h::UInt64) = hash(a.value_int, h)
26+
Base.hash(a::_ApproxVector, h::UInt64) = hash(a.value_int, h)
2727

2828
function MOA.minimize_multiobjective!(
2929
alg::MOA.GeneralDichotomy,
3030
model::MOA.Optimizer,
3131
)
32+
# Some constants. These could be converted into algorithm options.
33+
# - atol: the absolute tolerance used to compare solutions in objective space
34+
# - wnorm: ???
35+
atol, wnorm = 1e-6, 1e2
36+
# Storage we need for the algorithm.
37+
weights, solutions = Weight[], MOA.SolutionPoint[]
3238
n_obj = MOI.output_dimension(model.f)
33-
wnorm = 100.0
34-
start_time = time()
35-
weights = Weight[]
36-
# Initial extreme weights.
39+
# First, minimize the first objective to obtain a primal feasible point.
40+
w = zeros(Float64, n_obj)
41+
w[1] = 1.0
42+
status, solution = MOA._solve_weighted_sum(model, alg, w)
43+
if solution === nothing
44+
return status, nothing
45+
end
46+
push!(solutions, solution)
47+
# Initialize the weights. There is one weight vector for each objective, and
48+
# the weight is set to wnorm for each objective. We use the current solution
49+
# obtained by minimizing the 1st objective as the reference.
3750
for i in 1:n_obj
3851
w = zeros(Float64, n_obj)
3952
w[i] = wnorm
53+
z = w' * solution.y
4054
adj_bnd = Int[-j for j in 1:n_obj if j != i]
41-
push!(weights, Weight(w, NaN, adj_bnd, [1], false, false))
42-
end
43-
status, solution = MOA._solve_weighted_sum(model, alg, weights[1].w)
44-
if !MOA._is_scalar_status_optimal(status)
45-
# Return immediately if no solution nor unbounded.
46-
return status, nothing
55+
push!(weights, Weight(w, z, adj_bnd, [1], i == 1, false))
4756
end
48-
solutions = MOA.SolutionPoint[solution]
49-
# Weight update for the new solution
50-
for weight in weights
51-
weight.z = sum(weight.w .* solutions[1].y)
52-
end
53-
weights[1].tested = true
54-
# Prevent solution duplicates.
55-
# existing_sol maps a CustomVec of the solution to the solution index.
56-
existing_sol = Dict(CustomVec(solution.y, alg.scaling) => 1)
57+
# Prevent solution duplicates: existing_sol maps an _ApproxVector of the
58+
# solution to the index in `solutions::Vector{MOA.SolutionPoint}`.
59+
existing_sol = Dict(_ApproxVector(solution.y) => 1)
5760
n_removed = 0
58-
solution_limit = MOI.get(alg, MOA.SolutionLimit())
59-
iteration = 0
60-
while !(iteration >= alg.max_iter > 0) && length(solutions) < solution_limit
61-
iteration += 1
61+
while length(solutions) < MOI.get(alg, MOA.SolutionLimit())
6262
# Look for a new solution by testing the extreme weights.
63-
found, new_sol_ind, wind, target_weight = false, 0, 1, 0
64-
while wind <= length(weights) && !found
65-
if weights[wind].tested || weights[wind].removed
66-
wind += 1
63+
improving_solution = false
64+
for (i, weight) in enumerate(weights)
65+
if weight.tested || weight.removed
6766
continue
6867
end
69-
status, sol = MOA._solve_weighted_sum(model, alg, weights[wind].w)
68+
status, sol = MOA._solve_weighted_sum(model, alg, weight.w)
7069
# TODO(odow): what if this solve fails?
71-
weights[wind].tested = true
72-
sol_z = sum(sol.y .* weights[wind].w)
73-
if !haskey(existing_sol, CustomVec(sol.y, alg.scaling))
70+
weight.tested = true
71+
if !haskey(existing_sol, _ApproxVector(sol.y))
7472
push!(solutions, sol)
7573
# Prepare new weight index set for the new solution's adjacency.
76-
new_sol_ind = length(solutions)
77-
existing_sol[CustomVec(sol.y, alg.scaling)] = new_sol_ind
78-
if sol_z < weights[wind].z
79-
# Triggers weight set decomp. update.
80-
found = true
81-
target_weight = wind
74+
existing_sol[_ApproxVector(sol.y)] = length(solutions)
75+
if weight.w' * sol.y < weight.z
76+
improving_solution = true
77+
break
8278
end
8379
end
84-
wind += 1
8580
end
86-
if !found
87-
break # Terminate the search when no solution can be found.
81+
if !improving_solution
82+
break # Terminate the search when no new solution can be found.
8883
end
89-
polytope_sol = Set{Int}()
90-
equal_weights = Dict{CustomVec,Int}()
91-
for (wind, weight) in enumerate(weights)
92-
sol_z = sum(solutions[new_sol_ind].y .* weight.w)
93-
if sol_z < weight.z - alg.epsilon
94-
# Improved weighted value.
84+
new_sol, new_sol_ind = last(solutions), length(solutions)
85+
polytope_sol, equal_weights = Set{Int}(), Dict{_ApproxVector,Int}()
86+
for (i, weight) in enumerate(weights)
87+
sol_z = weight.w' * new_sol.y
88+
if sol_z < weight.z - atol
89+
# The new solution is strictly better than the previous.
9590
if length(weight.adj_bnd) < n_obj
9691
weight.removed = true
9792
n_removed += 1
@@ -100,62 +95,61 @@ function MOA.minimize_multiobjective!(
10095
weight.z = sol_z
10196
end
10297
union!(polytope_sol, weight.adj_sol)
103-
elseif sol_z <= weight.z + alg.epsilon
104-
# Equal weighted value.
98+
elseif sol_z <= weight.z + atol
99+
# The new solution is equal in value to the previous.
105100
push!(weight.adj_sol, new_sol_ind)
106101
union!(polytope_sol, weight.adj_sol)
107-
equal_weights[CustomVec(weight.w, alg.scaling)] = wind
102+
equal_weights[_ApproxVector(weight.w)] = i
108103
end
109104
end
110105
# Construction of the weight polytope for the new solution.
111106
h = Polyhedra.HyperPlane(ones(n_obj), wnorm)
112107
for i in 1:n_obj
113-
vec = zeros(n_obj)
114-
vec[i] = -1
108+
vec = zeros(Float64, n_obj)
109+
vec[i] = -1.0
115110
h = intersect(h, Polyhedra.HalfSpace(vec, 0))
116111
end
112+
# Convert the set of polytope solutions into a vector. It's important
113+
# that iteration is ordered because we're going to rely on this later.
117114
polytope_sol_vec = collect(polytope_sol)
118-
for other_sol_ind in polytope_sol_vec
119-
vec = solutions[new_sol_ind].y - solutions[other_sol_ind].y
120-
h = intersect(h, Polyhedra.HalfSpace(vec, 0))
115+
for i in polytope_sol_vec
116+
h = intersect(h, Polyhedra.HalfSpace(new_sol.y - solutions[i].y, 0))
121117
end
122118
poly = Polyhedra.polyhedron(h)
123119
# Update of the extreme weights from the new polytope vertices.
124120
for idx in eachindex(Polyhedra.points(poly))
125121
w = get(poly, idx)
126-
weight_ind = get(equal_weights, CustomVec(w, alg.scaling), nothing)
127-
if weight_ind !== nothing
122+
z = w' * new_sol.y
123+
if (i = get(equal_weights, _ApproxVector(w), nothing)) !== nothing
128124
# Update an existing extreme weight.
129-
weights[weight_ind].z = sum(w .* solutions[new_sol_ind].y)
130-
weights[weight_ind].tested = true
131-
weights[weight_ind].adj_sol = Int[new_sol_ind]
132-
if length(weights[weight_ind].adj_bnd) < n_obj-1
133-
if !weights[weight_ind].removed
134-
weights[weight_ind].removed = true
125+
weights[i].z = z
126+
weights[i].tested = true
127+
weights[i].adj_sol = Int[new_sol_ind]
128+
if length(weights[i].adj_bnd) < n_obj - 1
129+
if !weights[i].removed
130+
weights[i].removed = true
135131
n_removed += 1
136132
end
137133
else
138-
weights[weight_ind].removed = false
134+
weights[i].removed = false
139135
end
140136
else
141137
# Insert a new extreme weight.
142-
incidence = Polyhedra.incidenthalfspaceindices(poly, idx)
143-
new_weight = Weight(
144-
w,
145-
sum(w .* solutions[new_sol_ind].y),
146-
Int[-elt.value for elt in incidence if elt.value <= n_obj],
147-
Int[
148-
polytope_sol_vec[elt.value-n_obj] for
149-
elt in incidence if elt.value > n_obj
150-
],
151-
false,
152-
false,
153-
)
154-
push!(new_weight.adj_sol, new_sol_ind)
155-
push!(weights, new_weight)
138+
adj_bnd, adj_sol = Int[], Int[]
139+
for elt in Polyhedra.incidenthalfspaceindices(poly, idx)
140+
if elt.value <= n_obj
141+
push!(adj_bnd, -elt.value)
142+
else
143+
push!(adj_sol, polytope_sol_vec[elt.value-n_obj])
144+
end
145+
end
146+
push!(adj_sol, new_sol_ind)
147+
push!(weights, Weight(w, z, adj_bnd, adj_sol, false, false))
156148
end
157149
end
158-
if 3*n_removed >= length(weights)
150+
# This is a heuristic: filter the weights if approximately 1/3 of them
151+
# have been removed.
152+
if n_removed >= length(weights) / 3
159153
filter!(w -> !w.removed, weights)
160154
n_removed = 0
161155
end

src/algorithms/GeneralDichotomy.jl

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# obtain one at http://mozilla.org/MPL/2.0/.
55

66
"""
7-
GeneralDichotomy(precision::Int = 3)
7+
GeneralDichotomy()
88
99
`GeneralDichotomy` implements the algorithm from Buchet, S. and Defresne, M.
1010
(2026). Efficient Enumeration of Supported Solutions for General Multi-Objective
@@ -24,7 +24,7 @@ To use this algorithm you MUST first load the Polyhedra.jl Julia package:
2424
```julia
2525
import MultiObjectiveAlgorithms as MOA
2626
import Polyhedra
27-
algorithm = MOA.GeneralDichotomy(3)
27+
algorithm = MOA.GeneralDichotomy()
2828
```
2929
3030
## Supported optimizer attributes
@@ -36,13 +36,8 @@ algorithm = MOA.GeneralDichotomy(3)
3636
"""
3737
mutable struct GeneralDichotomy <: AbstractAlgorithm
3838
solution_limit::Union{Nothing,Int}
39-
max_iter::Int
40-
epsilon::Float64
41-
scaling::Float64
4239

43-
function GeneralDichotomy(precision::Int = 3)
44-
return new(nothing, 0, 10.0^-precision, 10^precision)
45-
end
40+
GeneralDichotomy() = new(nothing)
4641
end
4742

4843
MOI.supports(::GeneralDichotomy, ::SolutionLimit) = true

test/algorithms/test_GeneralDichotomy.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function test_lap()
5757
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
5858
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
5959
MOI.set(model, MOI.Silent(), true)
60-
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy(3))
60+
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy())
6161
MOI.optimize!(model)
6262
@test MOI.get(model, MOI.ResultCount()) == 4
6363
return
@@ -134,7 +134,7 @@ function test_vlp()
134134
f = MOI.Utilities.vectorize(P' * x)
135135
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
136136
MOI.set(model, MOI.Silent(), true)
137-
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy(3))
137+
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy())
138138
MOI.optimize!(model)
139139
return
140140
end
@@ -144,7 +144,7 @@ function test_biobjective_knapsack()
144144
p2 = [65, 90, 90, 77, 95, 84, 70, 94, 66, 92, 74, 97, 60, 60, 65, 97, 93]
145145
w = [80, 87, 68, 72, 66, 77, 99, 85, 70, 93, 98, 72, 100, 89, 67, 86, 91]
146146
model = MOA.Optimizer(HiGHS.Optimizer)
147-
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy(3))
147+
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy())
148148
MOI.set(model, MOI.Silent(), true)
149149
x = MOI.add_variables(model, length(w))
150150
MOI.add_constraint.(model, x, MOI.ZeroOne())
@@ -178,7 +178,7 @@ end
178178

179179
function test_infeasible()
180180
model = MOA.Optimizer(HiGHS.Optimizer)
181-
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy(3))
181+
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy())
182182
MOI.set(model, MOI.Silent(), true)
183183
x = MOI.add_variables(model, 6)
184184
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
@@ -195,7 +195,7 @@ end
195195

196196
function test_unbounded()
197197
model = MOA.Optimizer(HiGHS.Optimizer)
198-
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy(3))
198+
MOI.set(model, MOA.Algorithm(), MOA.GeneralDichotomy())
199199
MOI.set(model, MOI.Silent(), true)
200200
x = MOI.add_variables(model, 3)
201201
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))

0 commit comments

Comments
 (0)