Skip to content

Commit

Permalink
Support MOI.TimeLimitSec (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Mar 22, 2023
1 parent d3f45d9 commit 1e3032c
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ the solution process.
* `MOA.ObjectiveRelativeTolerance(index::Int)`
* `MOA.ObjectiveWeight(index::Int)`
* `MOA.SolutionLimit()`
* `MOI.TimeLimitSec()`
34 changes: 31 additions & 3 deletions src/MultiObjectiveAlgorithms.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
module MultiObjectiveAlgorithms

import Combinatorics
import MathOptInterface

const MOI = MathOptInterface
import MathOptInterface as MOI

struct SolutionPoint
x::Dict{MOI.VariableIndex,Float64}
Expand Down Expand Up @@ -110,6 +108,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
f::Union{Nothing,MOI.AbstractVectorFunction}
solutions::Vector{SolutionPoint}
termination_status::MOI.TerminationStatusCode
time_limit_sec::Union{Nothing,Float64}

function Optimizer(optimizer_factory)
return new(
Expand All @@ -118,6 +117,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
nothing,
SolutionPoint[],
MOI.OPTIMIZE_NOT_CALLED,
nothing,
)
end
end
Expand All @@ -143,6 +143,34 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike)
return MOI.Utilities.default_copy_to(dest, src)
end

### TimeLimitSec

function MOI.supports(model::Optimizer, attr::MOI.TimeLimitSec)
return MOI.supports(model.inner, attr)
end

MOI.get(model::Optimizer, ::MOI.TimeLimitSec) = model.time_limit_sec

function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value::Real)
model.time_limit_sec = Float64(value)
return
end

function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, ::Nothing)
model.time_limit_sec = nothing
return
end

function _time_limit_exceeded(model::Optimizer, start_time::Float64)
time_limit = MOI.get(model, MOI.TimeLimitSec())
if time_limit === nothing
return false
end
return time() - start_time >= time_limit
end

### ObjectiveFunction

function MOI.supports(
::Optimizer,
::MOI.ObjectiveFunction{<:MOI.AbstractScalarFunction},
Expand Down
9 changes: 9 additions & 0 deletions src/algorithms/Chalmet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
Chalmet, L.G., and Lemonidis, L., and Elzinga, D.J. (1986). An algorithm for the
bi-criterion integer programming problem. European Journal of Operational
Research. 25(2), 292-300
## Supported optimizer attributes
* `MOI.TimeLimitSec()`: terminate if the time limit is exceeded and return the
list of current solutions.
"""
mutable struct Chalmet <: AbstractAlgorithm end

Expand All @@ -37,6 +42,7 @@ function _solve_constrained_model(
end

function optimize_multiobjective!(algorithm::Chalmet, model::Optimizer)
start_time = time()
if MOI.output_dimension(model.f) != 2
error("Chalmet requires exactly two objectives")
end
Expand Down Expand Up @@ -91,6 +97,9 @@ function optimize_multiobjective!(algorithm::Chalmet, model::Optimizer)
push!(Q, (1, 2))
t = 3
while !isempty(Q)
if _time_limit_exceeded(model, start_time)
return MOI.TIME_LIMIT, solutions
end
r, s = pop!(Q)
yr, ys = solutions[r].y, solutions[s].y
rhs = [max(yr[1], ys[1]), max(yr[2], ys[2])]
Expand Down
13 changes: 11 additions & 2 deletions src/algorithms/Dichotomy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ Science 25(1), 73-78.
## Supported optimizer attributes
* `MOA.SolutionLimit()`
* `MOI.TimeLimitSec()`: terminate if the time limit is exceeded and return the
list of current solutions.
* `MOA.SolutionLimit()`: terminate once this many solutions have been found.
"""
mutable struct Dichotomy <: AbstractAlgorithm
solution_limit::Union{Nothing,Int}
Expand Down Expand Up @@ -73,6 +76,7 @@ function _solve_weighted_sum(
end

function optimize_multiobjective!(algorithm::Dichotomy, model::Optimizer)
start_time = time()
if MOI.output_dimension(model.f) > 2
error("Only scalar or bi-objective problems supported.")
end
Expand All @@ -93,7 +97,12 @@ function optimize_multiobjective!(algorithm::Dichotomy, model::Optimizer)
push!(queue, (0.0, 1.0))
end
limit = MOI.get(algorithm, SolutionLimit())
status = MOI.OPTIMAL
while length(queue) > 0 && length(solutions) < limit
if _time_limit_exceeded(model, start_time)
status = MOI.TIME_LIMIT
break
end
(a, b) = popfirst!(queue)
y_d = solutions[a].y .- solutions[b].y
w = y_d[2] / (y_d[2] - y_d[1])
Expand All @@ -114,5 +123,5 @@ function optimize_multiobjective!(algorithm::Dichotomy, model::Optimizer)
end
solution_list =
[solutions[w] for w in sort(collect(keys(solutions)); rev = true)]
return MOI.OPTIMAL, solution_list
return status, solution_list
end
13 changes: 12 additions & 1 deletion src/algorithms/DominguezRios.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
Dominguez-Rios, M.A. & Chicano, F., & Alba, E. (2021). Effective anytime
algorithm for multiobjective combinatorial optimization problems. Information
Sciences, 565(7), 210-228.
## Supported optimizer attributes
* `MOI.TimeLimitSec()`: terminate if the time limit is exceeded and return the
list of current solutions.
"""
mutable struct DominguezRios <: AbstractAlgorithm end

Expand Down Expand Up @@ -138,6 +143,7 @@ function _update!(
end

function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
start_time = time()
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.MAX_SENSE
old_obj, neg_obj = copy(model.f), -model.f
Expand Down Expand Up @@ -187,7 +193,12 @@ function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
t_max = MOI.add_variable(model.inner)
solutions = SolutionPoint[]
k = 0
status = MOI.OPTIMAL
while any(!isempty(l) for l in L)
if _time_limit_exceeded(model, start_time)
status = MOI.TIME_LIMIT
break
end
i, k = _select_next_box(L, k)
B = L[k][i]
w = 1 ./ max.(1, B.u - yI)
Expand All @@ -214,5 +225,5 @@ function optimize_multiobjective!(algorithm::DominguezRios, model::Optimizer)
MOI.delete.(model.inner, constraints)
end
MOI.delete(model.inner, t_max)
return MOI.OPTIMAL, solutions
return status, solutions
end
8 changes: 7 additions & 1 deletion src/algorithms/EpsilonConstraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function optimize_multiobjective!(
algorithm::EpsilonConstraint,
model::Optimizer,
)
start_time = time()
if MOI.output_dimension(model.f) != 2
error("EpsilonConstraint requires exactly two objectives")
end
Expand Down Expand Up @@ -104,7 +105,12 @@ function optimize_multiobjective!(
MOI.GreaterThan{Float64}, left
end
ci = MOI.add_constraint(model, f1, SetType(bound))
status = MOI.OPTIMAL
while true
if _time_limit_exceeded(model, start_time)
status = MOI.TIME_LIMIT
break
end
MOI.set(model, MOI.ConstraintSet(), ci, SetType(bound))
MOI.optimize!(model.inner)
if !_is_scalar_status_optimal(model)
Expand All @@ -121,5 +127,5 @@ function optimize_multiobjective!(
end
end
MOI.delete(model, ci)
return MOI.OPTIMAL, filter_nondominated(sense, solutions)
return status, filter_nondominated(sense, solutions)
end
13 changes: 12 additions & 1 deletion src/algorithms/KirlikSayin.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ This is an algorithm to generate all nondominated solutions for multi-objective
discrete optimization problems. The algorithm maintains `(p-1)`-dimensional
rectangle regions in the solution space, and a two-stage optimization problem
is solved for each rectangle.
## Supported optimizer attributes
* `MOI.TimeLimitSec()`: terminate if the time limit is exceeded and return the
list of current solutions.
"""
mutable struct KirlikSayin <: AbstractAlgorithm end

Expand Down Expand Up @@ -74,6 +79,7 @@ end
_volume(r::_Rectangle, l::Vector{Float64}) = prod(r.u - l)

function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
start_time = time()
sense = MOI.get(model.inner, MOI.ObjectiveSense())
if sense == MOI.MAX_SENSE
old_obj, neg_obj = copy(model.f), -model.f
Expand Down Expand Up @@ -134,7 +140,12 @@ function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
MOI.LessThan{Float64},
MOI.GreaterThan{Float64},
)
status = MOI.OPTIMAL
while !isempty(L)
if _time_limit_exceeded(model, start_time)
status = MOI.TIME_LIMIT
break
end
Rᵢ = L[argmax([_volume(Rᵢ, _project(yI, k)) for Rᵢ in L])]
lᵢ, uᵢ = Rᵢ.l, Rᵢ.u
# Solving the first stage model: P_k(ε)
Expand Down Expand Up @@ -182,5 +193,5 @@ function optimize_multiobjective!(algorithm::KirlikSayin, model::Optimizer)
end
_remove_rectangle(L, _Rectangle(Y_proj, uᵢ))
end
return MOI.OPTIMAL, solutions
return status, solutions
end
37 changes: 37 additions & 0 deletions test/algorithms/Chalmet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,43 @@ function test_knapsack_max()
return
end

function test_time_limit()
n = 10
W = 2137.0
C = Float64[
566 611 506 180 817 184 585 423 26 317
62 84 977 979 874 54 269 93 881 563
]
w = Float64[557, 898, 148, 63, 78, 964, 246, 662, 386, 272]
model = MOA.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOA.Algorithm(), MOA.Chalmet())
MOI.set(model, MOI.Silent(), true)
MOI.set(model, MOI.TimeLimitSec(), 0.0)
x = MOI.add_variables(model, n)
MOI.add_constraint.(model, x, MOI.ZeroOne())
MOI.add_constraint(
model,
MOI.ScalarAffineFunction(
[MOI.ScalarAffineTerm(w[j], x[j]) for j in 1:n],
0.0,
),
MOI.LessThan(W),
)
f = MOI.VectorAffineFunction(
[
MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(C[i, j], x[j])) for
i in 1:2 for j in 1:n
],
[0.0, 0.0],
)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.TIME_LIMIT
@test MOI.get(model, MOI.ResultCount()) > 0
return
end

function test_unbounded()
model = MOA.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOA.Algorithm(), MOA.Chalmet())
Expand Down
31 changes: 31 additions & 0 deletions test/algorithms/Dichotomy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,37 @@ function test_biobjective_knapsack()
return
end

function test_time_limit()
p1 = [77, 94, 71, 63, 96, 82, 85, 75, 72, 91, 99, 63, 84, 87, 79, 94, 90]
p2 = [65, 90, 90, 77, 95, 84, 70, 94, 66, 92, 74, 97, 60, 60, 65, 97, 93]
w = [80, 87, 68, 72, 66, 77, 99, 85, 70, 93, 98, 72, 100, 89, 67, 86, 91]
f = MOI.OptimizerWithAttributes(
() -> MOA.Optimizer(HiGHS.Optimizer),
MOA.Algorithm() => MOA.Dichotomy(),
)
model = MOI.instantiate(f)
MOI.set(model, MOI.Silent(), true)
MOI.set(model, MOI.TimeLimitSec(), 0.0)
x = MOI.add_variables(model, length(w))
MOI.add_constraint.(model, x, MOI.ZeroOne())
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
f = MOI.Utilities.operate(
vcat,
Float64,
[sum(1.0 * p[i] * x[i] for i in 1:length(w)) for p in [p1, p2]]...,
)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.add_constraint(
model,
sum(1.0 * w[i] * x[i] for i in 1:length(w)),
MOI.LessThan(900.0),
)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.TIME_LIMIT
@test MOI.get(model, MOI.ResultCount()) > 0
return
end

function test_infeasible()
model = MOA.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOA.Algorithm(), MOA.Dichotomy())
Expand Down
39 changes: 39 additions & 0 deletions test/algorithms/DominguezRios.jl
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,45 @@ function test_no_bounding_box()
return
end

function test_time_limit()
p = 3
n = 10
W = 2137.0
C = Float64[
566 611 506 180 817 184 585 423 26 317
62 84 977 979 874 54 269 93 881 563
664 982 962 140 224 215 12 869 332 537
]
w = Float64[557, 898, 148, 63, 78, 964, 246, 662, 386, 272]
model = MOA.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOA.Algorithm(), MOA.DominguezRios())
MOI.set(model, MOI.TimeLimitSec(), 0.0)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variables(model, n)
MOI.add_constraint.(model, x, MOI.ZeroOne())
MOI.add_constraint(
model,
MOI.ScalarAffineFunction(
[MOI.ScalarAffineTerm(w[j], x[j]) for j in 1:n],
0.0,
),
MOI.LessThan(W),
)
f = MOI.VectorAffineFunction(
[
MOI.VectorAffineTerm(i, MOI.ScalarAffineTerm(-C[i, j], x[j]))
for i in 1:p for j in 1:n
],
fill(0.0, p),
)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.TIME_LIMIT
@test MOI.get(model, MOI.ResultCount()) == 0
return
end

end

TestDominguezRios.run_tests()
Loading

0 comments on commit 1e3032c

Please sign in to comment.