Skip to content

Add ComputeIdealPoint attribute #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
57 changes: 48 additions & 9 deletions src/MultiObjectiveAlgorithms.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -119,6 +120,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
nothing,
NaN,
Float64[],
default(ComputeIdealPoint()),
)
end
end
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions test/test_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading