From c87945988ba4adba19da284c755f9f09ad3039fd Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 5 Jul 2021 16:00:00 +1200 Subject: [PATCH] [Utilities] Migrate Utilities/cachingoptimizer to new MOI.Test and fix bugs --- src/Utilities/cachingoptimizer.jl | 26 +- test/Utilities/cachingoptimizer.jl | 390 ++++++++++++++++------------- 2 files changed, 232 insertions(+), 184 deletions(-) diff --git a/src/Utilities/cachingoptimizer.jl b/src/Utilities/cachingoptimizer.jl index 9e0738fa46..d7a26c01f3 100644 --- a/src/Utilities/cachingoptimizer.jl +++ b/src/Utilities/cachingoptimizer.jl @@ -545,14 +545,15 @@ end # ConstraintSet and ConstraintFunction methods, but allows us to strongly type # the third and fourth arguments of the set methods so that we only support # setting the same type of set or function. -function replace_constraint_function_or_set( +function _replace_constraint_function_or_set( m::CachingOptimizer, attr, cindex, replacement, ) - replacement_optimizer = map_indices(m.model_to_optimizer_map, replacement) if m.state == ATTACHED_OPTIMIZER + replacement_optimizer = + map_indices(m.model_to_optimizer_map, replacement) if m.mode == AUTOMATIC try MOI.set( @@ -587,18 +588,18 @@ function MOI.set( cindex::CI{F,S}, set::S, ) where {F,S} - replace_constraint_function_or_set(m, MOI.ConstraintSet(), cindex, set) + _replace_constraint_function_or_set(m, MOI.ConstraintSet(), cindex, set) return end function MOI.set( - m::CachingOptimizer, + model::CachingOptimizer, ::MOI.ConstraintFunction, - cindex::CI{F}, + cindex::MOI.ConstraintIndex{F,S}, func::F, -) where {F} - replace_constraint_function_or_set( - m, +) where {F,S} + _replace_constraint_function_or_set( + model, MOI.ConstraintFunction(), cindex, func, @@ -606,6 +607,15 @@ function MOI.set( return end +function MOI.set( + ::CachingOptimizer, + ::MOI.ConstraintFunction, + ::MOI.ConstraintIndex{MOI.SingleVariable,S}, + ::MOI.SingleVariable, +) where {S} + return throw(MOI.SettingSingleVariableFunctionNotAllowed()) +end + function MOI.modify( m::CachingOptimizer, obj::MOI.ObjectiveFunction, diff --git a/test/Utilities/cachingoptimizer.jl b/test/Utilities/cachingoptimizer.jl index 748febbc20..5c530c9b77 100644 --- a/test/Utilities/cachingoptimizer.jl +++ b/test/Utilities/cachingoptimizer.jl @@ -1,15 +1,141 @@ +module TestCachingOptimizer + using Test + import MathOptInterface + const MOI = MathOptInterface -const MOIT = MOI.DeprecatedTest const MOIU = MOI.Utilities -@testset "Test default attributes" begin - # Without an optimizer attached (i.e., `MOI.state(model) == NO_OPTIMIZER`) we - # need throw nice errors for attributes that are based on the optimizer. For - # `AbstractModelAttribute`s that `is_set_by_optimize` returns `true` for, we - # overload `TerminationStatus`, `PrimalStatus`, or `DualStatus` to return - # sane default values. Otherwise we throw a nice error. +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +### +### Helper structs +### + +struct DummyModelAttribute <: MOI.AbstractModelAttribute end + +struct DummyEvaluator <: MOI.AbstractNLPEvaluator end + +struct DummyVariableAttribute <: MOI.AbstractVariableAttribute end + +struct DummyConstraintAttribute <: MOI.AbstractConstraintAttribute end + +mutable struct NoFreeVariables <: MOI.AbstractOptimizer + inner::MOIU.Model{Float64} + NoFreeVariables() = new(MOIU.Model{Float64}()) +end + +MOI.is_empty(model::NoFreeVariables) = MOI.is_empty(model.inner) + +MOI.empty!(model::NoFreeVariables) = MOI.empty!(model.inner) + +function MOI.get(model::NoFreeVariables, attr::MOI.AnyAttribute, idx::Vector) + return MOI.get(model.inner, attr, idx) +end + +function MOI.get(model::NoFreeVariables, attr::MOI.AnyAttribute, args...) + return MOI.get(model.inner, attr, args...) +end + +function MOI.supports_add_constrained_variables( + ::NoFreeVariables, + ::Type{MOI.Reals}, +) + return false +end + +function MOI.supports_add_constrained_variable( + ::NoFreeVariables, + ::Type{<:MOI.AbstractScalarSet}, +) + return true +end + +function MOI.add_constrained_variable( + model::NoFreeVariables, + set::MOI.AbstractScalarSet, +) + return MOI.add_constrained_variable(model.inner, set) +end + +function MOI.supports_add_constrained_variables( + ::NoFreeVariables, + ::Type{<:MOI.AbstractVectorSet}, +) + return true +end + +function MOI.add_constrained_variables( + model::NoFreeVariables, + set::MOI.AbstractVectorSet, +) + return MOI.add_constrained_variables(model.inner, set) +end + +MOI.supports_incremental_interface(::NoFreeVariables, names::Bool) = !names + +function MOI.copy_to(dest::NoFreeVariables, src::MOI.ModelLike; kwargs...) + return MOI.Utilities.automatic_copy_to(dest, src; kwargs...) +end + +### +### The tests +### + +function test_MOI_Test() + # It seems like these loops might take a while. But the first one takes + # _forever_, and then the rest are really quick (like 140s vs 0.4s). + for state in + (MOIU.NO_OPTIMIZER, MOIU.EMPTY_OPTIMIZER, MOIU.ATTACHED_OPTIMIZER) + for mode in (MOIU.MANUAL, MOIU.AUTOMATIC) + model = MOIU.CachingOptimizer(MOIU.Model{Float64}(), mode) + if state != MOIU.NO_OPTIMIZER + optimizer = MOIU.MockOptimizer( + MOIU.Model{Float64}(), + supports_names = false, + ) + MOIU.reset_optimizer(model, optimizer) + if state == MOIU.ATTACHED_OPTIMIZER + MOIU.attach_optimizer(model) + end + end + @test MOIU.state(model) == state + @test MOIU.mode(model) == mode + MOI.Test.runtests( + model, + MOI.Test.Config(exclude = Any[MOI.optimize!]), + exclude = String[ + # This test is optional. + "test_model_ScalarFunctionConstantNotZero", + # MockOptimizer doesnt' support these + "test_attribute_SolverName", + "test_constraint_ConstraintDualStart", + "test_constraint_ConstraintPrimalStart", + ], + ) + end + end + return +end + +""" +Without an optimizer attached (i.e., `MOI.state(model) == NO_OPTIMIZER`) we +need throw nice errors for attributes that are based on the optimizer. For +`AbstractModelAttribute`s that `is_set_by_optimize` returns `true` for, we +overload `TerminationStatus`, `PrimalStatus`, or `DualStatus` to return +sane default values. Otherwise we throw a nice error. +""" +function test_default_attributes() model = MOIU.CachingOptimizer(MOIU.Model{Float64}(), MOIU.MANUAL) @test MOIU.state(model) == MOIU.NO_OPTIMIZER @@ -24,40 +150,25 @@ const MOIU = MOI.Utilities ) @test_throws exception MOI.get(model, MOI.VariablePrimal(), x[1]) @test_throws exception MOI.get(model, MOI.VariablePrimal(), x) - - attr = MOI.SolverName() - exception = ErrorException( - "Cannot query $(attr) from caching optimizer because no optimizer" * - " is attached.", - ) - @test_throws exception MOI.get(model, attr) - attr = MOI.Silent() - exception = ErrorException( - "Cannot query $(attr) from caching optimizer because no optimizer" * - " is attached.", - ) - @test_throws exception MOI.get(model, attr) - attr = MOI.TimeLimitSec() - exception = ErrorException( - "Cannot query $(attr) from caching optimizer because no optimizer" * - " is attached.", - ) - @test_throws exception MOI.get(model, attr) - attr = MOI.NumberOfThreads() - exception = ErrorException( - "Cannot query $(attr) from caching optimizer because no optimizer" * - " is attached.", + for attr in ( + MOI.SolverName(), + MOI.Silent(), + MOI.TimeLimitSec(), + MOI.NumberOfThreads(), + MOI.ResultCount(), ) - @test_throws exception MOI.get(model, attr) - attr = MOI.ResultCount() - exception = ErrorException( - "Cannot query $(attr) from caching optimizer because no optimizer" * - " is attached.", - ) - @test_throws exception MOI.get(model, attr) + @test_throws( + ErrorException( + "Cannot query $(attr) from caching optimizer because no " * + "optimizer is attached.", + ), + MOI.get(model, attr), + ) + end + return end -@testset "Copyable solver attributes" begin +function test_copyable_solver_attributes() cache = MOIU.UniversalFallback(MOIU.Model{Float64}()) cached = MOIU.CachingOptimizer(cache, MOIU.MANUAL) MOI.set(cached, MOI.Silent(), true) @@ -105,14 +216,10 @@ end @test MOI.get(cached, MOI.TimeLimitSec()) == 0.0 @test MOI.get(mock, MOI.NumberOfThreads()) == 1 @test MOI.get(cached, MOI.NumberOfThreads()) == 1 + return end -struct DummyModelAttribute <: MOI.AbstractModelAttribute end -struct DummyEvaluator <: MOI.AbstractNLPEvaluator end -struct DummyVariableAttribute <: MOI.AbstractVariableAttribute end -struct DummyConstraintAttribute <: MOI.AbstractConstraintAttribute end - -@testset "Mapping of variables" begin +function test_mapping_of_variables() mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}())) model = MOIU.CachingOptimizer( MOIU.UniversalFallback(MOIU.Model{Float64}()), @@ -245,9 +352,10 @@ struct DummyConstraintAttribute <: MOI.AbstractConstraintAttribute end @test nlp_data.evaluator isa DummyEvaluator @test nlp_data.has_objective == false end + return end -@testset "CachingOptimizer MANUAL mode" begin +function test_CachingOptimizer_MANUAL_mode() m = MOIU.CachingOptimizer(MOIU.Model{Float64}(), MOIU.MANUAL) @test MOIU.state(m) == MOIU.NO_OPTIMIZER @@ -359,7 +467,7 @@ end @test MOIU.state(m) == MOIU.EMPTY_OPTIMIZER end -@testset "CachingOptimizer AUTOMATIC mode" begin +function test_CachingOptimizer_AUTOMATIC_mode() m = MOIU.CachingOptimizer(MOIU.Model{Float64}(), MOIU.AUTOMATIC) @test MOIU.state(m) == MOIU.NO_OPTIMIZER @@ -497,136 +605,47 @@ end MOI.empty!(m) @test MOIU.state(m) == MOIU.EMPTY_OPTIMIZER + return end -@testset "Constructor with optimizer" begin - @testset "Empty model and optimizer" begin - s = MOIU.MockOptimizer(MOIU.Model{Float64}(), supports_names = false) - model = MOIU.Model{Float64}() - m = MOIU.CachingOptimizer(model, s) - @test m isa MOIU.CachingOptimizer{typeof(s),typeof(model)} - @test MOI.is_empty(m) - @test MOIU.state(m) == MOIU.EMPTY_OPTIMIZER - @test MOIU.mode(m) == MOIU.AUTOMATIC - @test MOI.get(m, MOI.SolverName()) == "Mock" - @test sprint(show, m) == MOI.Utilities.replace_acronym(""" - $(MOIU.CachingOptimizer{MOIU.MockOptimizer{MOIU.Model{Float64}},MOIU.Model{Float64}}) - in state EMPTY_OPTIMIZER - in mode AUTOMATIC - with model cache $(MOIU.Model{Float64}) - with optimizer $(MOIU.MockOptimizer{MOIU.Model{Float64}})""") - end - @testset "Non-empty optimizer" begin - s = MOIU.MockOptimizer(MOIU.Model{Float64}(), supports_names = false) - MOI.add_variable(s) - model = MOIU.Model{Float64}() - @test MOI.is_empty(model) - @test !MOI.is_empty(s) - @test_throws AssertionError MOIU.CachingOptimizer(model, s) - end - @testset "Non-empty model" begin - s = MOIU.MockOptimizer(MOIU.Model{Float64}(), supports_names = false) - model = MOIU.Model{Float64}() - MOI.add_variable(model) - @test !MOI.is_empty(model) - @test MOI.is_empty(s) - @test MOIU.CachingOptimizer(model, s) isa MOIU.CachingOptimizer - end -end - -for state in (MOIU.NO_OPTIMIZER, MOIU.EMPTY_OPTIMIZER, MOIU.ATTACHED_OPTIMIZER) - @testset "Optimization tests in state $state and mode $mode" for mode in ( - MOIU.MANUAL, - MOIU.AUTOMATIC, - ) - m = MOIU.CachingOptimizer(MOIU.Model{Float64}(), mode) - if state != MOIU.NO_OPTIMIZER - s = MOIU.MockOptimizer( - MOIU.Model{Float64}(), - supports_names = false, - ) - MOIU.reset_optimizer(m, s) - if state == MOIU.ATTACHED_OPTIMIZER - MOIU.attach_optimizer(m) - end - end - @test MOIU.state(m) == state - @test MOIU.mode(m) == mode - - @testset "Name test" begin - MOIT.nametest(m) - end - - @testset "Copy test" begin - MOIT.failcopytestc(m) - MOIT.failcopytestia(m) - MOIT.failcopytestva(m) - MOIT.failcopytestca(m) - MOIT.copytest(m, MOIU.Model{Float64}()) - end - - config = MOIT.Config(solve = false) - @testset "Unit" begin - MOIT.unittest(m, config) - end - @testset "Continuous Linear" begin - exclude = ["partial_start"] # VariablePrimalStart not supported. - MOIT.contlineartest(m, config, exclude) - end - end +function test_empty_model_and_optimizer() + s = MOIU.MockOptimizer(MOIU.Model{Float64}(), supports_names = false) + model = MOIU.Model{Float64}() + m = MOIU.CachingOptimizer(model, s) + @test m isa MOIU.CachingOptimizer{typeof(s),typeof(model)} + @test MOI.is_empty(m) + @test MOIU.state(m) == MOIU.EMPTY_OPTIMIZER + @test MOIU.mode(m) == MOIU.AUTOMATIC + @test MOI.get(m, MOI.SolverName()) == "Mock" + @test sprint(show, m) == MOI.Utilities.replace_acronym(""" + $(MOIU.CachingOptimizer{MOIU.MockOptimizer{MOIU.Model{Float64}},MOIU.Model{Float64}}) + in state EMPTY_OPTIMIZER + in mode AUTOMATIC + with model cache $(MOIU.Model{Float64}) + with optimizer $(MOIU.MockOptimizer{MOIU.Model{Float64}})""") end -mutable struct NoFreeVariables <: MOI.AbstractOptimizer - inner::MOIU.Model{Float64} - function NoFreeVariables() - return new(MOIU.Model{Float64}()) - end -end -MOI.is_empty(model::NoFreeVariables) = MOI.is_empty(model.inner) -MOI.empty!(model::NoFreeVariables) = MOI.empty!(model.inner) -function MOI.get(model::NoFreeVariables, attr::MOI.AnyAttribute, idx::Vector) - return MOI.get(model.inner, attr, idx) -end -function MOI.get(model::NoFreeVariables, attr::MOI.AnyAttribute, args...) - return MOI.get(model.inner, attr, args...) -end -function MOI.supports_add_constrained_variables( - ::NoFreeVariables, - ::Type{MOI.Reals}, -) - return false -end -function MOI.supports_add_constrained_variable( - ::NoFreeVariables, - ::Type{<:MOI.AbstractScalarSet}, -) - return true -end -function MOI.add_constrained_variable( - model::NoFreeVariables, - set::MOI.AbstractScalarSet, -) - return MOI.add_constrained_variable(model.inner, set) -end -function MOI.supports_add_constrained_variables( - ::NoFreeVariables, - ::Type{<:MOI.AbstractVectorSet}, -) - return true -end -function MOI.add_constrained_variables( - model::NoFreeVariables, - set::MOI.AbstractVectorSet, -) - return MOI.add_constrained_variables(model.inner, set) +function test_empty_model_nonempty_optimizer() + s = MOIU.MockOptimizer(MOIU.Model{Float64}(), supports_names = false) + MOI.add_variable(s) + model = MOIU.Model{Float64}() + @test MOI.is_empty(model) + @test !MOI.is_empty(s) + @test_throws AssertionError MOIU.CachingOptimizer(model, s) + return end -MOI.supports_incremental_interface(::NoFreeVariables, names::Bool) = !names -function MOI.copy_to(dest::NoFreeVariables, src::MOI.ModelLike; kwargs...) - return MOI.Utilities.automatic_copy_to(dest, src; kwargs...) +function test_nonempty_model() + s = MOIU.MockOptimizer(MOIU.Model{Float64}(), supports_names = false) + model = MOIU.Model{Float64}() + MOI.add_variable(model) + @test !MOI.is_empty(model) + @test MOI.is_empty(s) + @test MOIU.CachingOptimizer(model, s) isa MOIU.CachingOptimizer + return end -function constrained_variables_test(model) +function _constrained_variables_test(model) @test !MOI.supports_add_constrained_variables(model, MOI.Reals) @test MOI.supports_add_constrained_variable(model, MOI.ZeroOne) @test !MOI.supports_constraint(model, MOI.SingleVariable, MOI.ZeroOne) @@ -651,38 +670,51 @@ function constrained_variables_test(model) end @test Set(MOI.get(model.optimizer, MOI.ListOfConstraintTypesPresent())) == constraint_types + return end -@testset "Constrained Variables" begin +function test_constrained_variables() cache = NoFreeVariables() optimizer = NoFreeVariables() model = MOIU.CachingOptimizer(cache, optimizer) - constrained_variables_test(model) - MOI.empty!(cache) - MOI.empty!(optimizer) + _constrained_variables_test(model) + return +end + +function test_constrained_variables_AUTOMATIC() + cache = NoFreeVariables() + optimizer = NoFreeVariables() model = MOIU.CachingOptimizer(cache, MOIU.AUTOMATIC) MOIU.reset_optimizer(model, optimizer) - constrained_variables_test(model) + _constrained_variables_test(model) + return end struct Issue1220 <: MOI.AbstractOptimizer optimizer_attributes::Dict{Any,Any} Issue1220() = new(Dict{Any,Any}()) end + MOI.is_empty(model::Issue1220) = isempty(model.optimizer_attributes) + function MOI.get(model::Issue1220, ::MOI.ListOfOptimizerAttributesSet) return collect(keys(model.optimizer_attributes)) end + MOI.supports(::Issue1220, ::MOI.AbstractOptimizerAttribute) = true + MOI.supports(::Issue1220, ::MOI.NumberOfThreads) = false + function MOI.get(model::Issue1220, attr::MOI.AbstractOptimizerAttribute) return model.optimizer_attributes[attr] end + function MOI.set(model::Issue1220, attr::MOI.AbstractOptimizerAttribute, value) model.optimizer_attributes[attr] = value - return value + return end -@testset "Issue1220_dont_pass_raw_parameter" begin + +function test_issue1220_dont_pass_raw_parameter() model = MOIU.CachingOptimizer(Issue1220(), Issue1220()) MOI.set(model, MOI.Silent(), true) MOI.set(model, MOI.RawOptimizerAttribute("foo"), "bar") @@ -691,9 +723,10 @@ end @test MOI.get(model, MOI.Silent()) == true @test_throws KeyError MOI.get(model, MOI.RawOptimizerAttribute("foo")) @test_throws KeyError MOI.get(model, MOI.NumberOfThreads()) + return end -@testset "Status codes" begin +function test_status_codes() optimizer = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}()) model = MOI.Utilities.CachingOptimizer( MOI.Utilities.Model{Float64}(), @@ -704,4 +737,9 @@ end MOI.set(optimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + return end + +end # module + +TestCachingOptimizer.runtests()