diff --git a/README.md b/README.md index 7212105..d8adeb5 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,13 @@ the solution process. * `MOA.ObjectiveWeight(index::Int)` * `MOA.SolutionLimit()` * `MOI.TimeLimitSec()` + +## Ideal point + +By default, MOA will compute the ideal point, which can be queried using the +`MOI.ObjectiveBound` attribute. + +Computing the ideal point requires as many solves as the dimension of the +objective function. Thus, if you do not need the ideal point information, you +can improve the performance of MOA by setting the `MOA.ComputeIdealPoint()` +attribute to `false`. diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index 32ca9fb..0a04a09 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -108,6 +108,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer time_limit_sec::Union{Nothing,Float64} solve_time::Float64 ideal_point::Vector{Float64} + compute_ideal_point::Bool function Optimizer(optimizer_factory) return new( @@ -119,6 +120,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer nothing, NaN, Float64[], + default(ComputeIdealPoint()), ) end end @@ -130,6 +132,7 @@ function MOI.empty!(model::Optimizer) model.termination_status = MOI.OPTIMIZE_NOT_CALLED model.solve_time = NaN empty!(model.ideal_point) + model.compute_ideal_point = default(ComputeIdealPoint()) return end @@ -139,7 +142,8 @@ function MOI.is_empty(model::Optimizer) isempty(model.solutions) && model.termination_status == MOI.OPTIMIZE_NOT_CALLED && isnan(model.solve_time) && - isempty(model.ideal_point) + isempty(model.ideal_point) && + model.compute_ideal_point == default(ComputeIdealPoint()) end MOI.supports_incremental_interface(::Optimizer) = true @@ -351,6 +355,33 @@ struct LexicographicAllPermutations <: AbstractAlgorithmAttribute end default(::LexicographicAllPermutations) = true +""" + ComputeIdealPoint <: AbstractOptimizerAttribute -> Bool + +Controls whether to compute the ideal point. + +Defaults to true`. + +If this attribute is set to `true`, the ideal point can be queried using the +`MOI.ObjectiveBound` attribute. + +Computing the ideal point requires as many solves as the dimension of the +objective function. Thus, if you do not need the ideal point information, you +can improve the performance of MOA by setting this attribute to `false`. +""" +struct ComputeIdealPoint <: MOI.AbstractOptimizerAttribute end + +default(::ComputeIdealPoint) = true + +MOI.supports(::Optimizer, ::ComputeIdealPoint) = true + +function MOI.set(model::Optimizer, ::ComputeIdealPoint, value::Bool) + model.compute_ideal_point = value + return +end + +MOI.get(model::Optimizer, ::ComputeIdealPoint) = model.compute_ideal_point + ### RawOptimizerAttribute function MOI.supports(model::Optimizer, attr::MOI.RawOptimizerAttribute) @@ -530,16 +561,12 @@ function MOI.delete(model::Optimizer, ci::MOI.ConstraintIndex) return end -function MOI.optimize!(model::Optimizer) - start_time = time() - empty!(model.solutions) - model.termination_status = MOI.OPTIMIZE_NOT_CALLED - if model.f === nothing - model.termination_status = MOI.INVALID_MODEL - return - end +function _compute_ideal_point(model::Optimizer) objectives = MOI.Utilities.eachscalar(model.f) model.ideal_point = fill(NaN, length(objectives)) + if !MOI.get(model, ComputeIdealPoint()) + return + end for (i, f) in enumerate(objectives) MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f)}(), f) MOI.optimize!(model.inner) @@ -548,6 +575,18 @@ function MOI.optimize!(model::Optimizer) model.ideal_point[i] = MOI.get(model.inner, MOI.ObjectiveValue()) end end + return +end + +function MOI.optimize!(model::Optimizer) + start_time = time() + empty!(model.solutions) + model.termination_status = MOI.OPTIMIZE_NOT_CALLED + if model.f === nothing + model.termination_status = MOI.INVALID_MODEL + return + end + _compute_ideal_point(model) algorithm = something(model.algorithm, default(Algorithm())) status, solutions = optimize_multiobjective!(algorithm, model) model.termination_status = status diff --git a/test/test_model.jl b/test/test_model.jl index 56cce2a..33cc01f 100644 --- a/test/test_model.jl +++ b/test/test_model.jl @@ -173,6 +173,36 @@ function test_scalarise() return end +function test_ideal_point() + for (flag, result) in (true => [0.0, -9.0], false => [NaN, NaN]) + model = MOA.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) + x = MOI.add_variables(model, 2) + MOI.add_constraint.(model, x, MOI.GreaterThan(0.0)) + MOI.add_constraint(model, x[2], MOI.LessThan(3.0)) + MOI.add_constraint(model, 3.0 * x[1] - 1.0 * x[2], MOI.LessThan(6.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = MOI.Utilities.vectorize([ + 3.0 * x[1] + x[2], + -1.0 * x[1] - 2.0 * x[2], + ]) + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + @test MOI.supports(model, MOA.ComputeIdealPoint()) + @test MOI.get(model, MOA.ComputeIdealPoint()) + @test MOI.set(model, MOA.ComputeIdealPoint(), flag) === nothing + @test MOI.get(model, MOA.ComputeIdealPoint()) == flag + MOI.optimize!(model) + point = MOI.get(model, MOI.ObjectiveBound()) + @test length(point) == 2 + if flag + @test point ≈ result + else + @test all(isnan, point) + end + end + return end +end # module + TestModel.run_tests()