diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 176e10c..4fe2c2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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) diff --git a/README.md b/README.md index 373b1e1..b860fde 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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. diff --git a/src/MOO.jl b/src/MOO.jl index 1dce900..8abe67d 100644 --- a/src/MOO.jl +++ b/src/MOO.jl @@ -113,6 +113,8 @@ function MOI.set(model::Optimizer, ::Algorithm, alg::AbstractAlgorithm) return end +default(::Algorithm) = Lexicographic() + ### AbstractAlgorithmAttribute """ @@ -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 @@ -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) @@ -300,7 +315,8 @@ 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 @@ -308,6 +324,11 @@ 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, @@ -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 diff --git a/src/algorithms/Hierarchical.jl b/src/algorithms/Hierarchical.jl index 75fc8ed..a9d0869 100644 --- a/src/algorithms/Hierarchical.jl +++ b/src/algorithms/Hierarchical.jl @@ -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) @@ -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] @@ -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 diff --git a/src/algorithms/Lexicographic.jl b/src/algorithms/Lexicographic.jl new file mode 100644 index 0000000..4c61b52 --- /dev/null +++ b/src/algorithms/Lexicographic.jl @@ -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 diff --git a/test/algorithms/Hierarchical.jl b/test/algorithms/Hierarchical.jl index 6c6741f..19c193b 100644 --- a/test/algorithms/Hierarchical.jl +++ b/test/algorithms/Hierarchical.jl @@ -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 diff --git a/test/algorithms/Lexicographic.jl b/test/algorithms/Lexicographic.jl new file mode 100644 index 0000000..c42e2d7 --- /dev/null +++ b/test/algorithms/Lexicographic.jl @@ -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() diff --git a/test/runtests.jl b/test/runtests.jl index 8028bd6..4c117b7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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