diff --git a/docs/src/manual/models.md b/docs/src/manual/models.md index ba5444910a..1a4e5ba3bb 100644 --- a/docs/src/manual/models.md +++ b/docs/src/manual/models.md @@ -100,3 +100,4 @@ The following attributes are available: * [`SolverVersion`](@ref) * [`SolveTimeSec`](@ref) * [`TimeLimitSec`](@ref) + * [`ObjectiveLimit`](@ref) diff --git a/docs/src/reference/models.md b/docs/src/reference/models.md index 86b5778551..c36eafebfd 100644 --- a/docs/src/reference/models.md +++ b/docs/src/reference/models.md @@ -77,6 +77,7 @@ SolverName SolverVersion Silent TimeLimitSec +ObjectiveLimit RawOptimizerAttribute NumberOfThreads RawSolver diff --git a/docs/src/tutorials/implementing.md b/docs/src/tutorials/implementing.md index 20902815e3..49976d119d 100644 --- a/docs/src/tutorials/implementing.md +++ b/docs/src/tutorials/implementing.md @@ -333,6 +333,7 @@ method for each attribute. | [`Name`](@ref) | Yes | Yes | Yes | | [`Silent`](@ref) | Yes | Yes | Yes | | [`TimeLimitSec`](@ref) | Yes | Yes | Yes | +| [`ObjectiveLimit`](@ref) | Yes | Yes | Yes | | [`RawOptimizerAttribute`](@ref) | Yes | Yes | Yes | | [`NumberOfThreads`](@ref) | Yes | Yes | Yes | | [`AbsoluteGapTolerance`](@ref) | Yes | Yes | Yes | diff --git a/src/Test/test_attribute.jl b/src/Test/test_attribute.jl index 473deb2f8f..300020492d 100644 --- a/src/Test/test_attribute.jl +++ b/src/Test/test_attribute.jl @@ -190,6 +190,32 @@ function setup_test( return end +function test_attribute_ObjectiveLimit(model::MOI.AbstractOptimizer, ::Config) + @requires MOI.supports(model, MOI.ObjectiveLimit()) + # Get the current value to restore it at the end of the test + value = MOI.get(model, MOI.ObjectiveLimit()) + MOI.set(model, MOI.ObjectiveLimit(), 0.0) + @test MOI.get(model, MOI.ObjectiveLimit()) == 0.0 + MOI.set(model, MOI.ObjectiveLimit(), nothing) + @test MOI.get(model, MOI.ObjectiveLimit()) === nothing + MOI.set(model, MOI.ObjectiveLimit(), 1.0) + @test MOI.get(model, MOI.ObjectiveLimit()) == 1.0 + MOI.set(model, MOI.ObjectiveLimit(), value) + @test value == MOI.get(model, MOI.ObjectiveLimit()) # Equality should hold + _test_attribute_value_type(model, MOI.ObjectiveLimit()) + return +end +test_attribute_ObjectiveLimit(::MOI.ModelLike, ::Config) = nothing + +function setup_test( + ::typeof(test_attribute_ObjectiveLimit), + model::MOIU.MockOptimizer, + ::Config, +) + MOI.set(model, MOI.ObjectiveLimit(), nothing) + return +end + """ test_attribute_AbsoluteGapTolerance(model::MOI.AbstractOptimizer, config::Config) diff --git a/src/attributes.jl b/src/attributes.jl index 7e83041236..454164a9ac 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -850,6 +850,23 @@ struct TimeLimitSec <: AbstractOptimizerAttribute end attribute_value_type(::TimeLimitSec) = Union{Nothing,Float64} +""" + ObjectiveLimit() + +An optimizer attribute for setting a limit on the objective value. + +The provided limit must be a `Union{Real,Nothing}`. + +When `set` to `nothing`, the limit reverts to the solver's default. + +The default value is `nothing`. + +The solver may stop when the [`ObjectiveValue`](@ref) is better (lower for +minimization, higher for maximization) than the `ObjectiveLimit`. If stopped, +the [`TerminationStatus`](@ref) should be `OBJECTIVE_LIMIT`. +""" +struct ObjectiveLimit <: AbstractOptimizerAttribute end + """ RawOptimizerAttribute(name::String) diff --git a/test/Utilities/cachingoptimizer.jl b/test/Utilities/cachingoptimizer.jl index d562c79351..f37871867a 100644 --- a/test/Utilities/cachingoptimizer.jl +++ b/test/Utilities/cachingoptimizer.jl @@ -190,6 +190,7 @@ function test_default_attributes() MOI.SolverName(), MOI.Silent(), MOI.TimeLimitSec(), + MOI.ObjectiveLimit(), MOI.NumberOfThreads(), MOI.ResultCount(), ) @@ -209,6 +210,7 @@ function test_copyable_solver_attributes() cached = MOIU.CachingOptimizer(cache, MOIU.MANUAL) MOI.set(cached, MOI.Silent(), true) MOI.set(cached, MOI.TimeLimitSec(), 0.0) + MOI.set(cached, MOI.ObjectiveLimit(), 42.0) MOI.set(cached, MOI.NumberOfThreads(), 1) mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}())) MOIU.reset_optimizer(cached, mock) @@ -216,15 +218,20 @@ function test_copyable_solver_attributes() @test MOI.get(cached, MOI.Silent()) @test MOI.get(mock, MOI.TimeLimitSec()) == 0.0 @test MOI.get(cached, MOI.TimeLimitSec()) == 0.0 + @test MOI.get(mock, MOI.ObjectiveLimit()) == 42.0 + @test MOI.get(cached, MOI.ObjectiveLimit()) == 42.0 @test MOI.get(mock, MOI.NumberOfThreads()) == 1 @test MOI.get(cached, MOI.NumberOfThreads()) == 1 MOI.set(cached, MOI.Silent(), false) MOI.set(cached, MOI.TimeLimitSec(), 1.0) + MOI.set(cached, MOI.ObjectiveLimit(), 1.0) MOI.set(cached, MOI.NumberOfThreads(), 2) @test !MOI.get(mock, MOI.Silent()) @test !MOI.get(cached, MOI.Silent()) @test MOI.get(mock, MOI.TimeLimitSec()) ≈ 1.0 + @test MOI.get(mock, MOI.ObjectiveLimit()) ≈ 1.0 @test MOI.get(cached, MOI.TimeLimitSec()) ≈ 1.0 + @test MOI.get(cached, MOI.ObjectiveLimit()) ≈ 1.0 @test MOI.get(mock, MOI.NumberOfThreads()) == 2 @test MOI.get(cached, MOI.NumberOfThreads()) == 2 mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}())) @@ -233,6 +240,8 @@ function test_copyable_solver_attributes() @test !MOI.get(cached, MOI.Silent()) @test MOI.get(mock, MOI.TimeLimitSec()) ≈ 1.0 @test MOI.get(cached, MOI.TimeLimitSec()) ≈ 1.0 + @test MOI.get(mock, MOI.ObjectiveLimit()) ≈ 1.0 + @test MOI.get(cached, MOI.ObjectiveLimit()) ≈ 1.0 @test MOI.get(mock, MOI.NumberOfThreads()) == 2 @test MOI.get(cached, MOI.NumberOfThreads()) == 2 MOI.set(cached, MOI.Silent(), true) diff --git a/test/Utilities/universalfallback.jl b/test/Utilities/universalfallback.jl index aa15019a1c..ea57a1771f 100644 --- a/test/Utilities/universalfallback.jl +++ b/test/Utilities/universalfallback.jl @@ -382,6 +382,7 @@ function test_missing_attribute() model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) @test MOI.get(model, MOI.Test.UnknownModelAttribute()) === nothing @test MOI.get(model, MOI.TimeLimitSec()) === nothing + @test MOI.get(model, MOI.ObjectiveLimit()) === nothing return end