From 1e3032c60a5220bec5cfc0823952d41b6a4bf303 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 22 Mar 2023 17:18:11 +1300 Subject: [PATCH] Support MOI.TimeLimitSec (#54) --- README.md | 1 + src/MultiObjectiveAlgorithms.jl | 34 +++++++++++++++++++++--- src/algorithms/Chalmet.jl | 9 +++++++ src/algorithms/Dichotomy.jl | 13 ++++++++-- src/algorithms/DominguezRios.jl | 13 +++++++++- src/algorithms/EpsilonConstraint.jl | 8 +++++- src/algorithms/KirlikSayin.jl | 13 +++++++++- test/algorithms/Chalmet.jl | 37 ++++++++++++++++++++++++++ test/algorithms/Dichotomy.jl | 31 ++++++++++++++++++++++ test/algorithms/DominguezRios.jl | 39 ++++++++++++++++++++++++++++ test/algorithms/EpsilonConstraint.jl | 28 ++++++++++++++++++++ test/algorithms/KirlikSayin.jl | 39 ++++++++++++++++++++++++++++ test/test_model.jl | 11 ++++++++ 13 files changed, 268 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index de8e78e..60224ca 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,4 @@ the solution process. * `MOA.ObjectiveRelativeTolerance(index::Int)` * `MOA.ObjectiveWeight(index::Int)` * `MOA.SolutionLimit()` + * `MOI.TimeLimitSec()` diff --git a/src/MultiObjectiveAlgorithms.jl b/src/MultiObjectiveAlgorithms.jl index 1318bde..db72333 100644 --- a/src/MultiObjectiveAlgorithms.jl +++ b/src/MultiObjectiveAlgorithms.jl @@ -6,9 +6,7 @@ module MultiObjectiveAlgorithms import Combinatorics -import MathOptInterface - -const MOI = MathOptInterface +import MathOptInterface as MOI struct SolutionPoint x::Dict{MOI.VariableIndex,Float64} @@ -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( @@ -118,6 +117,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer nothing, SolutionPoint[], MOI.OPTIMIZE_NOT_CALLED, + nothing, ) end end @@ -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}, diff --git a/src/algorithms/Chalmet.jl b/src/algorithms/Chalmet.jl index f183d0b..3714248 100644 --- a/src/algorithms/Chalmet.jl +++ b/src/algorithms/Chalmet.jl @@ -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 @@ -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 @@ -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])] diff --git a/src/algorithms/Dichotomy.jl b/src/algorithms/Dichotomy.jl index 4373dbd..6b66054 100644 --- a/src/algorithms/Dichotomy.jl +++ b/src/algorithms/Dichotomy.jl @@ -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} @@ -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 @@ -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]) @@ -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 diff --git a/src/algorithms/DominguezRios.jl b/src/algorithms/DominguezRios.jl index d7d5b60..1cbd936 100644 --- a/src/algorithms/DominguezRios.jl +++ b/src/algorithms/DominguezRios.jl @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/src/algorithms/EpsilonConstraint.jl b/src/algorithms/EpsilonConstraint.jl index e1ca343..aad156c 100644 --- a/src/algorithms/EpsilonConstraint.jl +++ b/src/algorithms/EpsilonConstraint.jl @@ -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 @@ -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) @@ -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 diff --git a/src/algorithms/KirlikSayin.jl b/src/algorithms/KirlikSayin.jl index 6707b7f..eea430d 100644 --- a/src/algorithms/KirlikSayin.jl +++ b/src/algorithms/KirlikSayin.jl @@ -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 @@ -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 @@ -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(ε) @@ -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 diff --git a/test/algorithms/Chalmet.jl b/test/algorithms/Chalmet.jl index 74039f0..f7ed540 100644 --- a/test/algorithms/Chalmet.jl +++ b/test/algorithms/Chalmet.jl @@ -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()) diff --git a/test/algorithms/Dichotomy.jl b/test/algorithms/Dichotomy.jl index 4c1ef69..f79c578 100644 --- a/test/algorithms/Dichotomy.jl +++ b/test/algorithms/Dichotomy.jl @@ -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()) diff --git a/test/algorithms/DominguezRios.jl b/test/algorithms/DominguezRios.jl index 8edce17..9704dc7 100644 --- a/test/algorithms/DominguezRios.jl +++ b/test/algorithms/DominguezRios.jl @@ -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() diff --git a/test/algorithms/EpsilonConstraint.jl b/test/algorithms/EpsilonConstraint.jl index 22e93d0..4c8bde0 100644 --- a/test/algorithms/EpsilonConstraint.jl +++ b/test/algorithms/EpsilonConstraint.jl @@ -362,6 +362,34 @@ function test_poor_numerics() 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] + model = MOA.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOA.Algorithm(), MOA.EpsilonConstraint()) + MOI.set(model, MOI.TimeLimitSec(), 0.0) + MOI.set(model, MOI.Silent(), true) + 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 + end TestEpsilonConstraint.run_tests() diff --git a/test/algorithms/KirlikSayin.jl b/test/algorithms/KirlikSayin.jl index b6f3851..17187db 100644 --- a/test/algorithms/KirlikSayin.jl +++ b/test/algorithms/KirlikSayin.jl @@ -547,6 +547,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.KirlikSayin()) + 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 TestKirlikSayin.run_tests() diff --git a/test/test_model.jl b/test/test_model.jl index d9d4d1f..55f9943 100644 --- a/test/test_model.jl +++ b/test/test_model.jl @@ -77,6 +77,17 @@ function test_unbounded() return end +function test_time_limit() + model = MOA.Optimizer(HiGHS.Optimizer) + @test MOI.supports(model, MOI.TimeLimitSec()) + @test MOI.get(model, MOI.TimeLimitSec()) === nothing + MOI.set(model, MOI.TimeLimitSec(), 2) + @test MOI.get(model, MOI.TimeLimitSec()) === 2.0 + MOI.set(model, MOI.TimeLimitSec(), nothing) + @test MOI.get(model, MOI.TimeLimitSec()) === nothing + return +end + end TestModel.run_tests()