Skip to content

Simplify algorithms by allowing them to assume minimization #116

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
Jun 13, 2025
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
24 changes: 24 additions & 0 deletions src/MultiObjectiveAlgorithms.jl
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,30 @@ function _compute_ideal_point(model::Optimizer, start_time)
return
end

function optimize_multiobjective!(
algorithm::AbstractAlgorithm,
model::Optimizer,
)
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.FEASIBILITY_SENSE
return MOI.INVALID_MODEL, nothing
elseif sense == MOI.MAX_SENSE
old_obj = copy(model.f)
neg_obj = MOI.Utilities.operate(-, Float64, model.f)
MOI.set(model, MOI.ObjectiveFunction{typeof(neg_obj)}(), neg_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
status, solutions = minimize_multiobjective!(algorithm, model)
MOI.set(model, MOI.ObjectiveFunction{typeof(old_obj)}(), old_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
if solutions !== nothing
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
end
model.ideal_point *= -1
return status, solutions
end
return minimize_multiobjective!(algorithm, model)
end

function MOI.optimize!(model::Optimizer)
start_time = time()
empty!(model.solutions)
Expand Down
16 changes: 2 additions & 14 deletions src/algorithms/Chalmet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,12 @@ function _solve_constrained_model(
return status, SolutionPoint(X, Y)
end

function optimize_multiobjective!(algorithm::Chalmet, model::Optimizer)
function minimize_multiobjective!(algorithm::Chalmet, model::Optimizer)
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
start_time = time()
if MOI.output_dimension(model.f) != 2
error("Chalmet requires exactly two objectives")
end
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.MAX_SENSE
old_obj, neg_obj = copy(model.f), -model.f
MOI.set(model, MOI.ObjectiveFunction{typeof(neg_obj)}(), neg_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
status, solutions = optimize_multiobjective!(algorithm, model)
MOI.set(model, MOI.ObjectiveFunction{typeof(old_obj)}(), old_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
if solutions !== nothing
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
end
return status, solutions
end
solutions = SolutionPoint[]
E = Tuple{Int,Int}[]
Q = Tuple{Int,Int}[]
Expand Down
26 changes: 6 additions & 20 deletions src/algorithms/DominguezRios.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,9 @@ function _update!(
return
end

function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
function minimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
start_time = time()
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.MAX_SENSE
old_obj, neg_obj = copy(model.f), -model.f
MOI.set(model, MOI.ObjectiveFunction{typeof(neg_obj)}(), neg_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
status, solutions = optimize_multiobjective!(algorithm, model)
MOI.set(model, MOI.ObjectiveFunction{typeof(old_obj)}(), old_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
if solutions !== nothing
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
end
model.ideal_point .*= -1
return status, solutions
end
n = MOI.output_dimension(model.f)
L = [_DominguezRiosBox[] for i in 1:n]
scalars = MOI.Utilities.scalarize(model.f)
Expand All @@ -166,7 +153,7 @@ function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
# Ideal and Nadir point estimation
for (i, f_i) in enumerate(scalars)
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i)
MOI.set(model.inner, MOI.ObjectiveSense(), sense)
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.optimize!(model.inner)
status = MOI.get(model.inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
Expand All @@ -175,18 +162,17 @@ function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
_, Y = _compute_point(model, variables, f_i)
yI[i] = Y
model.ideal_point[i] = Y
rev_sense = sense == MOI.MIN_SENSE ? MOI.MAX_SENSE : MOI.MIN_SENSE
MOI.set(model.inner, MOI.ObjectiveSense(), rev_sense)
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MAX_SENSE)
MOI.optimize!(model.inner)
status = MOI.get(model.inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
_warn_on_nonfinite_anti_ideal(algorithm, sense, i)
_warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i)
return status, nothing
end
_, Y = _compute_point(model, variables, f_i)
yN[i] = Y + 1
end
MOI.set(model.inner, MOI.ObjectiveSense(), sense)
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
ϵ = 1 / (2 * n * (maximum(yN - yI) - 1))
# If ϵ is small, then the scalar objectives can contain terms that fall
# below the tolerance level of the solver. To fix this, we rescale the
Expand Down
28 changes: 8 additions & 20 deletions src/algorithms/EpsilonConstraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ function MOI.get(alg::EpsilonConstraint, ::ObjectiveAbsoluteTolerance)
return MOI.get(alg, EpsilonConstraintStep())
end

function optimize_multiobjective!(
function minimize_multiobjective!(
algorithm::EpsilonConstraint,
model::Optimizer,
)
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
start_time = time()
if MOI.output_dimension(model.f) != 2
error("EpsilonConstraint requires exactly two objectives")
Expand All @@ -87,12 +88,7 @@ function optimize_multiobjective!(
end
a, b = solution_1[1].y[1], solution_2[1].y[1]
left, right = min(a, b), max(a, b)
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.MIN_SENSE
model.ideal_point .= min.(solution_1[1].y, solution_2[1].y)
else
model.ideal_point .= max.(solution_1[1].y, solution_2[1].y)
end
model.ideal_point .= min.(solution_1[1].y, solution_2[1].y)
# Compute the epsilon that we will be incrementing by each iteration
ε = MOI.get(algorithm, EpsilonConstraintStep())
n_points = MOI.get(algorithm, SolutionLimit())
Expand All @@ -104,16 +100,12 @@ function optimize_multiobjective!(
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f2)}(), f2)
# Add epsilon constraint
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
SetType, bound = if sense == MOI.MIN_SENSE
MOI.LessThan{Float64}, right - ε
else
MOI.GreaterThan{Float64}, left + ε
end
bound = right - ε
constant = MOI.constant(f1, Float64)
ci = MOI.Utilities.normalize_and_add_constraint(
model,
f1,
SetType(bound);
MOI.LessThan{Float64}(bound);
allow_modify_function = true,
)
bound -= constant
Expand All @@ -123,7 +115,7 @@ function optimize_multiobjective!(
status = MOI.TIME_LIMIT
break
end
MOI.set(model, MOI.ConstraintSet(), ci, SetType(bound))
MOI.set(model, MOI.ConstraintSet(), ci, MOI.LessThan{Float64}(bound))
MOI.optimize!(model.inner)
if !_is_scalar_status_optimal(model)
break
Expand All @@ -132,12 +124,8 @@ function optimize_multiobjective!(
if isempty(solutions) || !(Y ≈ solutions[end].y)
push!(solutions, SolutionPoint(X, Y))
end
if sense == MOI.MIN_SENSE
bound = min(Y[1] - constant - ε, bound - ε)
else
bound = max(Y[1] - constant + ε, bound + ε)
end
bound = min(Y[1] - constant - ε, bound - ε)
end
MOI.delete(model, ci)
return status, filter_nondominated(sense, solutions)
return status, filter_nondominated(MOI.MIN_SENSE, solutions)
end
10 changes: 3 additions & 7 deletions src/algorithms/Hierarchical.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ function _sorted_priorities(priorities::Vector{Int})
return [findall(isequal(u), priorities) for u in unique_priorities]
end

function optimize_multiobjective!(algorithm::Hierarchical, model::Optimizer)
function minimize_multiobjective!(algorithm::Hierarchical, model::Optimizer)
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
objectives = MOI.Utilities.eachscalar(model.f)
N = length(objectives)
variables = MOI.get(model.inner, MOI.ListOfVariableIndices())
Expand All @@ -109,14 +110,9 @@ function optimize_multiobjective!(algorithm::Hierarchical, model::Optimizer)
end
# Add tolerance constraints
X, Y = _compute_point(model, variables, new_vector_f)
sense = MOI.get(model.inner, MOI.ObjectiveSense())
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] + rtol * abs(Y[i]))
else
MOI.GreaterThan(Y[i] - rtol * abs(Y[i]))
end
set = MOI.LessThan(Y[i] + rtol * abs(Y[i]))
ci = MOI.Utilities.normalize_and_add_constraint(model, fi, set)
push!(constraints, ci)
end
Expand Down
18 changes: 2 additions & 16 deletions src/algorithms/KirlikSayin.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,9 @@ function _update_list(L::Vector{_Rectangle}, f::Vector{Float64})
return L_new
end

function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
function minimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
start_time = time()
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.MAX_SENSE
old_obj, neg_obj = copy(model.f), -model.f
MOI.set(model, MOI.ObjectiveFunction{typeof(neg_obj)}(), neg_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
status, solutions = optimize_multiobjective!(algorithm, model)
MOI.set(model, MOI.ObjectiveFunction{typeof(old_obj)}(), old_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
if solutions !== nothing
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
end
model.ideal_point .*= -1
return status, solutions
end
@assert sense == MOI.MIN_SENSE
solutions = SolutionPoint[]
# Problem with p objectives.
# Set k = 1, meaning the nondominated points will get projected
Expand Down
21 changes: 4 additions & 17 deletions src/algorithms/TambyVanderpooten.jl
Original file line number Diff line number Diff line change
Expand Up @@ -81,25 +81,12 @@ function _select_search_zone(
return k_star, upper_bounds[j_star]
end

function optimize_multiobjective!(
function minimize_multiobjective!(
algorithm::TambyVanderpooten,
model::Optimizer,
)
@assert MOI.get(model.inner, MOI.ObjectiveSense()) == MOI.MIN_SENSE
start_time = time()
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.MAX_SENSE
old_obj, neg_obj = copy(model.f), -model.f
MOI.set(model, MOI.ObjectiveFunction{typeof(neg_obj)}(), neg_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
status, solutions = optimize_multiobjective!(algorithm, model)
MOI.set(model, MOI.ObjectiveFunction{typeof(old_obj)}(), old_obj)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
if solutions !== nothing
solutions = [SolutionPoint(s.x, -s.y) for s in solutions]
end
model.ideal_point .*= -1
return status, solutions
end
warm_start_supported = false
if MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)
warm_start_supported = true
Expand All @@ -111,7 +98,7 @@ function optimize_multiobjective!(
scalars = MOI.Utilities.scalarize(model.f)
for (i, f_i) in enumerate(scalars)
MOI.set(model.inner, MOI.ObjectiveFunction{typeof(f_i)}(), f_i)
MOI.set(model.inner, MOI.ObjectiveSense(), sense)
MOI.set(model.inner, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.optimize!(model.inner)
status = MOI.get(model.inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
Expand All @@ -124,7 +111,7 @@ function optimize_multiobjective!(
MOI.optimize!(model.inner)
status = MOI.get(model.inner, MOI.TerminationStatus())
if !_is_scalar_status_optimal(status)
_warn_on_nonfinite_anti_ideal(algorithm, sense, i)
_warn_on_nonfinite_anti_ideal(algorithm, MOI.MIN_SENSE, i)
return status, nothing
end
_, Y = _compute_point(model, variables, f_i)
Expand Down
18 changes: 18 additions & 0 deletions test/algorithms/Chalmet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,31 @@ function test_unbounded()
return
end

function test_invalid_feasibility()
model = MOA.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOA.Algorithm(), MOA.Chalmet())
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, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE)
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INVALID_MODEL
@test MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
@test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
return
end

function test_infeasible()
model = MOA.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOA.Algorithm(), MOA.Chalmet())
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, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
Expand Down
1 change: 1 addition & 0 deletions test/algorithms/DominguezRios.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function test_infeasible()
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
Expand Down
1 change: 1 addition & 0 deletions test/algorithms/EpsilonConstraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ function test_infeasible()
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
Expand Down
1 change: 1 addition & 0 deletions test/algorithms/Hierarchical.jl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function test_infeasible()
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
Expand Down
1 change: 1 addition & 0 deletions test/algorithms/TambyVanderpooten.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function test_infeasible()
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
MOI.add_constraint(model, 1.0 * x[1] + 1.0 * x[2], MOI.LessThan(-1.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
f = MOI.Utilities.operate(vcat, Float64, 1.0 .* x...)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
Expand Down
Loading