From b5d94908f4a31a151fd959c6d4606cdf199b87e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 17 Aug 2023 08:33:04 +0200 Subject: [PATCH 1/6] adding objective limit --- docs/src/manual/models.md | 1 + docs/src/reference/models.md | 1 + docs/src/tutorials/implementing.md | 1 + src/Test/test_attribute.jl | 24 ++++++++++++++++++++++++ src/attributes.jl | 12 ++++++++++++ src/precompile.jl | 1 + test/Utilities/cachingoptimizer.jl | 9 +++++++++ test/Utilities/universalfallback.jl | 1 + 8 files changed, 50 insertions(+) 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..e32e23ab0b 100644 --- a/src/Test/test_attribute.jl +++ b/src/Test/test_attribute.jl @@ -190,6 +190,30 @@ 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(), 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 e16059e97b..2b7a77c8ea 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -850,6 +850,18 @@ struct TimeLimitSec <: AbstractOptimizerAttribute end attribute_value_type(::TimeLimitSec) = Union{Nothing,Float64} +""" + ObjectiveLimit() + + +An optimizer attribute for setting a limit on the objective value. +The solver may stop when the `ObjectiveValue` is better (lower for minimization, higher for maximization) than the `ObjectiveLimit`. +When `set` to `nothing`, it removes the solver limit. The default value is `nothing`. +""" +struct ObjectiveLimit <: AbstractOptimizerAttribute end + +attribute_value_type(::ObjectiveLimit) = Union{Nothing,Float64} + """ RawOptimizerAttribute(name::String) diff --git a/src/precompile.jl b/src/precompile.jl index 59240b1dfc..31c3e85fcc 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -79,6 +79,7 @@ function precompile_model(model, constraints) ObjectiveValue, Silent, TimeLimitSec, + ObjectiveBound, NumberOfVariables, ) Base.precompile(get, (model, attr)) 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 From d1154d2d58f7770df8945f9d8c70f9e9bf357cda Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 18 Aug 2023 11:01:11 +1200 Subject: [PATCH 2/6] Apply suggestions from code review --- src/Test/test_attribute.jl | 2 ++ src/attributes.jl | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Test/test_attribute.jl b/src/Test/test_attribute.jl index e32e23ab0b..300020492d 100644 --- a/src/Test/test_attribute.jl +++ b/src/Test/test_attribute.jl @@ -196,6 +196,8 @@ function test_attribute_ObjectiveLimit(model::MOI.AbstractOptimizer, ::Config) 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) diff --git a/src/attributes.jl b/src/attributes.jl index 2b7a77c8ea..c4df61aa0b 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -853,10 +853,17 @@ attribute_value_type(::TimeLimitSec) = Union{Nothing,Float64} """ ObjectiveLimit() - An optimizer attribute for setting a limit on the objective value. -The solver may stop when the `ObjectiveValue` is better (lower for minimization, higher for maximization) than the `ObjectiveLimit`. -When `set` to `nothing`, it removes the solver limit. The default value is `nothing`. + +The provided limit must be a `Union{Float64,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 From 02e1c614fea8f269a033143ddd6aeeab3c4d3b4f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 18 Aug 2023 12:41:31 +1200 Subject: [PATCH 3/6] Apply suggestions from code review --- src/attributes.jl | 2 +- src/precompile.jl | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/attributes.jl b/src/attributes.jl index c4df61aa0b..339e943f53 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -860,10 +860,10 @@ The provided limit must be a `Union{Float64,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 diff --git a/src/precompile.jl b/src/precompile.jl index 31c3e85fcc..59240b1dfc 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -79,7 +79,6 @@ function precompile_model(model, constraints) ObjectiveValue, Silent, TimeLimitSec, - ObjectiveBound, NumberOfVariables, ) Base.precompile(get, (model, attr)) From 7fb9572603af8cbb2b47c50997ba9c6637210821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Sun, 20 Aug 2023 22:04:51 +0200 Subject: [PATCH 4/6] Update src/attributes.jl --- src/attributes.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/attributes.jl b/src/attributes.jl index 339e943f53..0402f00963 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -867,7 +867,6 @@ the [`TerminationStatus`](@ref) should be `OBJECTIVE_LIMIT`. """ struct ObjectiveLimit <: AbstractOptimizerAttribute end -attribute_value_type(::ObjectiveLimit) = Union{Nothing,Float64} """ RawOptimizerAttribute(name::String) From 2436c14ab5b1c63de013de5c41bd6d90c04f61a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Sun, 20 Aug 2023 22:17:20 +0200 Subject: [PATCH 5/6] Update src/attributes.jl --- src/attributes.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/attributes.jl b/src/attributes.jl index 0402f00963..9dfa2a095b 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -867,7 +867,6 @@ the [`TerminationStatus`](@ref) should be `OBJECTIVE_LIMIT`. """ struct ObjectiveLimit <: AbstractOptimizerAttribute end - """ RawOptimizerAttribute(name::String) From 79786a6f805cccf43ca1afcd8a20b1dc8c87dab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Tue, 5 Sep 2023 13:52:59 +0200 Subject: [PATCH 6/6] objective type --- src/attributes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attributes.jl b/src/attributes.jl index 9dfa2a095b..5b67e49396 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -855,7 +855,7 @@ attribute_value_type(::TimeLimitSec) = Union{Nothing,Float64} An optimizer attribute for setting a limit on the objective value. -The provided limit must be a `Union{Float64,Nothing}`. +The provided limit must be a `Union{Real,Nothing}`. When `set` to `nothing`, the limit reverts to the solver's default.