Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
# Since JuMP doesn't have binary dependencies, only test on a subset of
# Since MOO doesn't have binary dependencies, only test on a subset of
# possible platforms.
include:
- version: '1' # The latest point-release (Linux)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ MOO is a collection of algorithms for multi-objective optimization.
This package is currently under development. You can install it as follows:

```julia
] add MathOptInterface#od/vector-optimization
] add MathOptInterface#master
] add JuMP#od/vector-optimization
] add https://github.com/odow/MOO.jl
```
Expand Down Expand Up @@ -36,8 +36,9 @@ the choice of solution algorithm.

There are a number of algorithms supported by the algorithms in MOO.

* `MOO.NISE()`
* `MOO.Lexicographic()` [default]
* `MOO.Hierarchical()`
* `MOO.NISE()`

Consult their docstrings for details.

Expand Down
43 changes: 32 additions & 11 deletions src/MOO.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ function MOI.set(model::Optimizer, ::Algorithm, alg::AbstractAlgorithm)
return
end

default(::Algorithm) = Lexicographic()

### AbstractAlgorithmAttribute

"""
Expand Down Expand Up @@ -182,13 +184,20 @@ Assign a `Float64` tolerance to objective number `index`. This is most commonly
used to constrain an objective to a range relative to the optimal objective
value of that objective.

Defaults to `0.01` (1%).
Defaults to `0.0`.
"""
struct ObjectiveRelativeTolerance <: AbstractAlgorithmAttribute
index::Int
end

default(::ObjectiveRelativeTolerance) = 0.01
default(::ObjectiveRelativeTolerance) = 0.0

function _append_default(attr::AbstractAlgorithmAttribute, x::Vector)
for _ in (1+length(x)):attr.index
push!(x, default(attr))
end
return
end

### RawOptimizerAttribute

Expand Down Expand Up @@ -220,6 +229,12 @@ function MOI.get(model::Optimizer, attr::MOI.AbstractOptimizerAttribute)
return MOI.get(model.inner, attr)
end

function MOI.get(model::Optimizer, ::MOI.SolverName)
alg = typeof(something(model.algorithm, default(Algorithm())))
inner = MOI.get(model.inner, MOI.SolverName())
return "MOO[algorithm=$alg, optimizer=$inner]"
end

### AbstractModelAttribute

function MOI.supports(model::Optimizer, arg::MOI.AbstractModelAttribute)
Expand Down Expand Up @@ -300,14 +315,20 @@ MOI.delete(model::Optimizer, i::MOI.Index) = MOI.delete(model.inner, i)
function MOI.optimize!(model::Optimizer)
model.solutions = nothing
model.termination_status = MOI.OPTIMIZE_NOT_CALLED
status, solutions = optimize_multiobjective!(model.algorithm, model)
algorithm = something(model.algorithm, default(Algorithm()))
status, solutions = optimize_multiobjective!(algorithm, model)
model.termination_status = status
model.solutions = solutions
return
end

MOI.get(model::Optimizer, ::MOI.ResultCount) = length(model.solutions)

function MOI.get(model::Optimizer, ::MOI.RawStatusString)
n = MOI.get(model, MOI.ResultCount())
return "Solve complete. Found $n solution(s)"
end

function MOI.get(
model::Optimizer,
attr::MOI.VariablePrimal,
Expand All @@ -322,16 +343,16 @@ function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue)
end

function MOI.get(model::Optimizer, attr::MOI.ObjectiveBound)
bound = zeros(length(model.solutions[1].y))
sense = MOI.get(model, MOI.ObjectiveSense())
for i in 1:length(bound)
if sense == MOI.MIN_SENSE
bound[i] = minimum([sol.y[i] for sol in model.solutions])
else
bound[i] = maximum([sol.y[i] for sol in model.solutions])
objectives = MOI.Utilities.eachscalar(model.f)
utopia = fill(NaN, length(objectives))
for (i, f) in enumerate(objectives)
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model.inner)
if MOI.get(model.inner, MOI.TerminationStatus()) == MOI.OPTIMAL
utopia[i] = MOI.get(model.inner, MOI.ObjectiveValue())
end
end
return bound
return utopia
end

MOI.get(model::Optimizer, ::MOI.TerminationStatus) = model.termination_status
Expand Down
15 changes: 5 additions & 10 deletions src/algorithms/Hierarchical.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ mutable struct Hierarchical <: AbstractAlgorithm
Hierarchical() = new(Int[], Float64[], Float64[])
end

function _append_default(attr, x)
for _ in (1+length(x)):attr.index
push!(x, default(attr))
end
return
end

MOI.supports(::Hierarchical, ::ObjectivePriority) = true

function MOI.get(alg::Hierarchical, attr::ObjectivePriority)
Expand Down Expand Up @@ -87,7 +80,9 @@ function optimize_multiobjective!(algorithm::Hierarchical, model::Optimizer)
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
# Find list of objectives with same priority
constraints = Any[]
objective_subsets = _sorted_priorities(algorithm.priorities)
objective_subsets = _sorted_priorities([
MOI.get(algorithm, ObjectivePriority(i)) for i in 1:N
])
for (round, indices) in enumerate(objective_subsets)
# Solve weighted sum
new_vector_f = objectives[indices]
Expand All @@ -110,9 +105,9 @@ function optimize_multiobjective!(algorithm::Hierarchical, model::Optimizer)
for (i, fi) in enumerate(MOI.Utilities.eachscalar(new_vector_f))
rtol = MOI.get(algorithm, ObjectiveRelativeTolerance(i))
set = if sense == MOI.MIN_SENSE
MOI.LessThan(Y[i] * (1 + rtol))
MOI.LessThan(Y[i] + rtol * abs(Y[i]))
else
MOI.GreaterThan(Y[i] * (1 - rtol))
MOI.GreaterThan(Y[i] - rtol * abs(Y[i]))
end
push!(constraints, MOI.add_constraint(model, fi, set))
end
Expand Down
68 changes: 68 additions & 0 deletions src/algorithms/Lexicographic.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright 2019, Oscar Dowson and contributors
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v.2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at http://mozilla.org/MPL/2.0/.

"""
Lexicographic()

`Lexicographic` implements a lexigographic algorithm that returns a single point
on the frontier, corresponding to solving each objective in order.

## Supported optimizer attributes

* `MOO.ObjectiveRelativeTolerance`
"""
mutable struct Lexicographic <: AbstractAlgorithm
rtol::Vector{Float64}

Lexicographic() = new(Float64[])
end

MOI.supports(::Lexicographic, ::ObjectiveRelativeTolerance) = true

function MOI.get(alg::Lexicographic, attr::ObjectiveRelativeTolerance)
return get(alg.rtol, attr.index, default(attr))
end

function MOI.set(alg::Lexicographic, attr::ObjectiveRelativeTolerance, value)
_append_default(attr, alg.rtol)
alg.rtol[attr.index] = value
return
end

function optimize_multiobjective!(algorithm::Lexicographic, model::Optimizer)
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
# Find list of objectives with same priority
constraints = Any[]
for (i, f) in enumerate(MOI.Utilities.eachscalar(model.f))
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model.inner)
if MOI.get(model.inner, MOI.TerminationStatus()) != MOI.OPTIMAL
return MOI.OTHER_ERROR, nothing
end
# Add tolerance constraints
X = Dict{MOI.VariableIndex,Float64}(
x => MOI.get(model.inner, MOI.VariablePrimal(), x) for
x in variables
)
Y = MOI.Utilities.eval_variables(x -> X[x], f)
sense = MOI.get(model.inner, MOI.ObjectiveSense())
rtol = MOI.get(algorithm, ObjectiveRelativeTolerance(i))
set = if sense == MOI.MIN_SENSE
MOI.LessThan(Y + rtol * abs(Y))
else
MOI.GreaterThan(Y - rtol * abs(Y))
end
push!(constraints, MOI.add_constraint(model, f, set))
end
X = Dict{MOI.VariableIndex,Float64}(
x => MOI.get(model.inner, MOI.VariablePrimal(), x) for x in variables
)
Y = MOI.Utilities.eval_variables(x -> X[x], model.f)
# Remove tolerance constraints
for c in constraints
MOI.delete(model, c)
end
return MOI.OPTIMAL, [ParetoSolution(X, Y)]
end
18 changes: 17 additions & 1 deletion test/algorithms/Hierarchical.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,23 @@ function test_knapsack()
@objective(model, Max, P * x)
@constraint(model, sum(x) <= 2)
optimize!(model)
@test ≈(value.(x), [81 / 90, 1 / 90, 80 / 90, 18 / 90]; atol = 1e-3)
@test ≈(value.(x), [0.9, 0, 0.9, 0.2]; atol = 1e-3)
return
end

function test_knapsack_min()
P = [1 0 0 0; 0 1 1 0; 0 0 1 1; 0 1 0 0]
model = Model(() -> MOO.Optimizer(HiGHS.Optimizer))
set_optimizer_attribute(model, MOO.Algorithm(), MOO.Hierarchical())
set_optimizer_attribute.(model, MOO.ObjectivePriority.(1:4), [2, 1, 1, 0])
set_optimizer_attribute.(model, MOO.ObjectiveWeight.(1:4), [1, 0.5, 0.5, 1])
set_optimizer_attribute(model, MOO.ObjectiveRelativeTolerance(1), 0.1)
set_silent(model)
@variable(model, 0 <= x[1:4] <= 1)
@objective(model, Min, -P * x)
@constraint(model, sum(x) <= 2)
optimize!(model)
@test ≈(value.(x), [0.9, 0, 0.9, 0.2]; atol = 1e-3)
return
end

Expand Down
68 changes: 68 additions & 0 deletions test/algorithms/Lexicographic.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright 2019, Oscar Dowson and contributors
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v.2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at http://mozilla.org/MPL/2.0/.

module TestLexicographic

using Test
using JuMP

import HiGHS
import MOO

function run_tests()
for name in names(@__MODULE__; all = true)
if startswith("$name", "test_")
@testset "$name" begin
getfield(@__MODULE__, name)()
end
end
end
return
end

function test_knapsack()
P = [1 0 0 0; 0 1 0 0; 0 0 0 1; 0 0 1 0]
model = Model(() -> MOO.Optimizer(HiGHS.Optimizer))
set_optimizer_attribute(model, MOO.Algorithm(), MOO.Lexicographic())
set_optimizer_attribute(model, MOO.ObjectiveRelativeTolerance(1), 0.1)
set_silent(model)
@variable(model, 0 <= x[1:4] <= 1)
@objective(model, Max, P * x)
@constraint(model, sum(x) <= 2)
optimize!(model)
@test ≈(value.(x), [0.9, 1, 0, 0.1]; atol = 1e-3)
return
end

function test_knapsack_min()
P = [1 0 0 0; 0 1 0 0; 0 0 0 1; 0 0 1 0]
model = Model(() -> MOO.Optimizer(HiGHS.Optimizer))
set_optimizer_attribute(model, MOO.Algorithm(), MOO.Lexicographic())
set_optimizer_attribute(model, MOO.ObjectiveRelativeTolerance(1), 0.1)
set_silent(model)
@variable(model, 0 <= x[1:4] <= 1)
@objective(model, Min, -P * x)
@constraint(model, sum(x) <= 2)
optimize!(model)
@test ≈(value.(x), [0.9, 1, 0, 0.1]; atol = 1e-3)
return
end

function test_knapsack_default()
P = [1 0 0 0; 0 1 0 0; 0 0 0 1; 0 0 1 0]
model = Model(() -> MOO.Optimizer(HiGHS.Optimizer))
set_silent(model)
@variable(model, 0 <= x[1:4] <= 1)
@objective(model, Max, P * x)
@constraint(model, sum(x) <= 2)
optimize!(model)
@test raw_status(model) == "Solve complete. Found 1 solution(s)"
@test ≈(value.(x), [1, 1, 0, 0]; atol = 1e-3)
return
end

end

TestLexicographic.run_tests()
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import Pkg

if get(ENV, "CI", "false") == "true"
Pkg.pkg"add MathOptInterface#od/vector-optimization"
Pkg.pkg"add MathOptInterface#master"
Pkg.pkg"add JuMP#od/vector-optimization"
end

Expand Down