From 354b6336795807f098808e6af9419393930151b1 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 29 Jun 2021 13:02:33 +1200 Subject: [PATCH 1/2] [Test] add test_basic_constraint.jl --- src/Test/Test.jl | 31 ++-- src/Test/test_attribute.jl | 6 +- src/Test/test_basic_constraint.jl | 262 ++++++++++++++++++++++++++++++ src/Test/test_variable.jl | 4 +- 4 files changed, 284 insertions(+), 19 deletions(-) create mode 100644 src/Test/test_basic_constraint.jl diff --git a/src/Test/Test.jl b/src/Test/Test.jl index bdb8ea163e..baec91af36 100644 --- a/src/Test/Test.jl +++ b/src/Test/Test.jl @@ -13,7 +13,7 @@ mutable struct Config{T<:Real} rtol::T supports_optimize::Bool optimal_status::MOI.TerminationStatusCode - excluded_attributes::Vector{Any} + exclude::Vector{Any} end """ @@ -23,7 +23,7 @@ end rtol::Real = Base.rtoldefault(T), supports_optimize::Bool = true, optimal_status::MOI.TerminationStatusCode = MOI.OPTIMAL, - excluded_attributes::Vector{Any} = Any[], + exclude::Vector{Any} = Any[], ) where {T} Return an object that is used to configure various tests. @@ -38,6 +38,12 @@ Return an object that is used to configure various tests. call to [`MOI.optimize!`](@ref) * `optimal_status = MOI.OPTIMAL`: Set to `MOI.LOCALLY_SOLVED` if the solver cannot prove global optimality. + * `exclude = Vector{Any}`: Pass attributes or functions to `exclude` to skip + parts of tests that require certain functionality. Common arguments include: + - `MOI.delete` to skip deletion-related tests + - `MOI.ConstraintDual` to skip dual-related tests + - `MOI.VariableName` to skip setting variable names + - `MOI.ConstraintName` to skip setting constraint names ## Examples @@ -47,10 +53,11 @@ dual variables or constraint names: Config( Float64; optimal_status = MOI.LOCALLY_SOLVED, - excluded_attributes = Any[ - MOI.ConstraintDual(), - MOI.VariableName(), - MOI.ConstraintName(), + exclude = Any[ + MOI.ConstraintDual, + MOI.VariableName, + MOI.ConstraintName, + MOI.delete, ], ) ``` @@ -61,14 +68,14 @@ function Config( rtol::Real = Base.rtoldefault(T), supports_optimize::Bool = true, optimal_status::MOI.TerminationStatusCode = MOI.OPTIMAL, - excluded_attributes::Vector{Any} = Any[], + exclude::Vector{Any} = Any[], ) where {T<:Real} return Config{T}( atol, rtol, supports_optimize, optimal_status, - excluded_attributes, + exclude, ) end @@ -78,7 +85,7 @@ function Base.copy(config::Config{T}) where {T} config.rtol, config.supports_optimize, config.optimal_status, - copy(config.excluded_attributes), + copy(config.exclude), ) end @@ -274,14 +281,12 @@ This is helpful when writing tests. ## Example ```julia -if MOI.Test._supports(config, MOI.Silent()) +if MOI.Test._supports(config, MOI.Silent) @test MOI.get(model, MOI.Silent()) == true end ``` """ -function _supports(config::Config, attribute::MOI.AnyAttribute) - return !(attribute in config.excluded_attributes) -end +_supports(config::Config, T::Any)::Bool = !(T in config.exclude) """ _test_model_solution( diff --git a/src/Test/test_attribute.jl b/src/Test/test_attribute.jl index 6aa4c9615a..7c3241b4f4 100644 --- a/src/Test/test_attribute.jl +++ b/src/Test/test_attribute.jl @@ -33,7 +33,7 @@ Test that the [`MOI.RawStatusString`](@ref) attribute is implemented for `model`. """ function test_attribute_RawStatusString(model::MOI.ModelLike, config::Config) - if !config.supports_optimize || !_supports(config, MOI.RawStatusString()) + if !config.supports_optimize || !_supports(config, MOI.RawStatusString) return end MOI.add_variable(model) @@ -94,7 +94,7 @@ end Test that the [`MOI.SolverName`](@ref) attribute is implemented for `model`. """ function test_attribute_SolverName(model::MOI.ModelLike, config::Config) - if _supports(config, MOI.SolverName()) + if _supports(config, MOI.SolverName) @test MOI.get(model, MOI.SolverName()) isa AbstractString end return @@ -106,7 +106,7 @@ end Test that the [`MOI.SolveTimeSec`](@ref) attribute is implemented for `model`. """ function test_attribute_SolveTimeSec(model::MOI.ModelLike, config::Config) - if !config.supports_optimize || !_supports(config, MOI.SolveTimeSec()) + if !config.supports_optimize || !_supports(config, MOI.SolveTimeSec) return end MOI.add_variable(model) diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl new file mode 100644 index 0000000000..4f7fa182a2 --- /dev/null +++ b/src/Test/test_basic_constraint.jl @@ -0,0 +1,262 @@ +function _function( + ::Any, + ::Type{MOI.SingleVariable}, + x::Vector{MOI.VariableIndex}, +) + return MOI.SingleVariable(x[1]) +end + +function _function( + ::Any, + ::Type{MOI.VectorOfVariables}, + x::Vector{MOI.VariableIndex}, +) + return MOI.VectorOfVariables(x) +end + +function _function( + ::Type{T}, + ::Type{MOI.ScalarAffineFunction}, + x::Vector{MOI.VariableIndex} +) where {T} + return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) +end + +function _function( + ::Type{T}, + ::Type{MOI.ScalarQuadraticFunction}, + x::Vector{MOI.VariableIndex} +) where {T} + return MOI.ScalarQuadraticFunction( + MOI.ScalarAffineTerm.(one(T), x), + MOI.ScalarQuadraticTerm.(one(T), x, x), + zero(T), + ) +end + +function _function( + ::Type{T}, + ::Type{MOI.VectorAffineFunction}, + x::Vector{MOI.VariableIndex} +) where {T} + return MOI.VectorAffineFunction( + MOI.VectorAffineTerm.(1:length(x), MOI.ScalarAffineTerm.(one(T), x)), + zeros(T, length(x)), + ) +end + +function _function( + ::Type{T}, + ::Type{MOI.VectorQuadraticFunction}, + x::Vector{MOI.VariableIndex} +) where {T} + return MOI.VectorQuadraticFunction( + MOI.VectorAffineTerm.(1:length(x), MOI.ScalarAffineTerm.(one(T), x)), + MOI.VectorQuadraticTerm.( + 1:length(x), + MOI.ScalarQuadraticTerm.(one(T), x, x), + ), + zeros(T, length(x)), + ) +end + +# Default fallback. +_set(::Any, ::Type{S}) where {S} = _set(S) + +_set(::Type{T}, ::Type{MOI.LessThan}) where {T} = MOI.LessThan(one(T)) +_set(::Type{T}, ::Type{MOI.GreaterThan}) where {T} = MOI.GreaterThan(one(T)) +_set(::Type{T}, ::Type{MOI.EqualTo}) where {T} = MOI.EqualTo(one(T)) +_set(::Type{T}, ::Type{MOI.Interval}) where {T} = MOI.Interval(zero(T), one(T)) +_set(::Type{MOI.ZeroOne}) = MOI.ZeroOne() +_set(::Type{MOI.Integer}) = MOI.Integer() +function _set(::Type{T}, ::Type{MOI.Semicontinuous}) where {T} + return MOI.Semicontinuous(zero(T), one(T)) +end +function _set(::Type{T}, ::Type{MOI.Semiinteger}) where {T} + return MOI.Semiinteger(zero(T), one(T)) +end +_set(::Type{T}, ::Type{MOI.SOS1}) where {T} = MOI.SOS1(convert.(T, 1:2)) +_set(::Type{T}, ::Type{MOI.SOS2}) where {T} = MOI.SOS2(convert.(T, 1:2)) +_set(::Type{MOI.Zeros}) = MOI.Zeros(2) +_set(::Type{MOI.Nonpositives}) = MOI.Nonpositives(2) +_set(::Type{MOI.Nonnegatives}) = MOI.Nonnegatives(2) +_set(::Type{MOI.NormInfinityCone}) = MOI.NormInfinityCone(3) +_set(::Type{MOI.NormOneCone}) = MOI.NormOneCone(3) +_set(::Type{MOI.SecondOrderCone}) = MOI.SecondOrderCone(3) +_set(::Type{MOI.RotatedSecondOrderCone}) = MOI.RotatedSecondOrderCone(3) +_set(::Type{MOI.GeometricMeanCone}) = MOI.GeometricMeanCone(3) +_set(::Type{MOI.ExponentialCone}) = MOI.ExponentialCone() +_set(::Type{MOI.DualExponentialCone}) = MOI.DualExponentialCone() +_set(::Type{MOI.PowerCone}) = MOI.PowerCone(0.5) +_set(::Type{MOI.DualPowerCone}) = MOI.DualPowerCone(0.5) +_set(::Type{MOI.RelativeEntropyCone}) = MOI.RelativeEntropyCone(3) +_set(::Type{MOI.NormSpectralCone}) = MOI.NormSpectralCone(2, 3) +_set(::Type{MOI.NormNuclearCone}) = MOI.NormNuclearCone(2, 3) +_set(::Type{MOI.PositiveSemidefiniteConeTriangle}) = MOI.PositiveSemidefiniteConeTriangle(3) +_set(::Type{MOI.PositiveSemidefiniteConeSquare}) = MOI.PositiveSemidefiniteConeSquare(3) +_set(::Type{MOI.LogDetConeTriangle}) = MOI.LogDetConeTriangle(3) +_set(::Type{MOI.LogDetConeSquare}) = MOI.LogDetConeSquare(3) +_set(::Type{MOI.RootDetConeTriangle}) = MOI.RootDetConeTriangle(3) +_set(::Type{MOI.RootDetConeSquare}) = MOI.RootDetConeSquare(3) +_set(::Type{MOI.Complements}) = MOI.Complements(2) + +# MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(3.0) +# MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.GreaterThan(3.0) + +function _basic_constraint_test_helper( + model::MOI.ModelLike, + config::Config{T}, + ::Type{UntypedF}, + ::Type{UntypedS}, +) where {T,UntypedF,UntypedS} + set = _set(T, UntypedS) + N = MOI.dimension(set) + x = MOI.add_variables(model, N) + constraint_function = _function(T, UntypedF, x) + F, S = typeof(constraint_function), typeof(set) + ### + ### Test MOI.supports_constraint + ### + @requires MOI.supports_constraint(model, F, S) + ### + ### Test MOI.NumberOfConstraints + ### + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 0 + c = MOI.add_constraint(model, constraint_function, set) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + ### + ### Test MOI.is_valid + ### + @test MOI.is_valid(model, c) + ### + ### Test MOI.ConstraintName + ### + if _supports(config, MOI.ConstraintName) + if F == MOI.SingleVariable + @test_throws( + MOI.SingleVariableConstraintNameError(), + MOI.supports(model, MOI.ConstraintName(), typeof(c)), + ) + @test_throws( + MOI.SingleVariableConstraintNameError(), + MOI.set(model, MOI.ConstraintName(), c, "c"), + ) + else + @test MOI.get(model, MOI.ConstraintName(), c) == "" + @test MOI.supports(model, MOI.ConstraintName(), typeof(c)) + MOI.set(model, MOI.ConstraintName(), c, "c") + @test MOI.get(model, MOI.ConstraintName(), c) == "c" + end + end + ### + ### Test MOI.ConstraintFunction + ### + if _supports(config, MOI.ConstraintFunction) + @test MOI.get(model, MOI.ConstraintFunction(), c) ≈ constraint_function + end + ### + ### Test MOI.ConstraintSet + ### + if _supports(config, MOI.ConstraintSet) + @test MOI.get(model, MOI.ConstraintSet(), c) == set + end + ### + ### Test MOI.ListOfConstraintIndices + ### + c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + @test length(c_indices) == 1 + ### + ### Test MOI.add_constraints + ### + if F != MOI.SingleVariable && F != MOI.VectorOfVariables + # We can't add multiple variable constraints as these are + # interpreted as bounds etc. + MOI.add_constraints( + model, + [constraint_function, constraint_function], + [set, set], + ) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 3 + @test length(MOI.get(model, MOI.ListOfConstraintIndices{F,S}())) == 3 + c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + @test all(MOI.is_valid.(model, c_indices)) + end + ### + ### Test MOI.delete + ### + if _supports(config, MOI.delete) + MOI.delete(model, c_indices[1]) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == + length(c_indices) - 1 + @test !MOI.is_valid(model, c_indices[1]) + @test_throws( + MOI.InvalidIndex(c_indices[1]), + MOI.delete(model, c_indices[1]), + ) + if _supports(config, MOI.ConstraintFunction) + @test_throws( + MOI.InvalidIndex(c_indices[1]), + MOI.get(model, MOI.ConstraintFunction(), c_indices[1]), + ) + end + if _supports(config, MOI.ConstraintSet) + @test_throws( + MOI.InvalidIndex(c_indices[1]), + MOI.get(model, MOI.ConstraintSet(), c_indices[1]), + ) + end + end + return +end + +for s in [ + :GreaterThan, + :LessThan, + :EqualTo, + :Interval, + :ZeroOne, + :Semicontinuous, + :Semiinteger, + :SOS1, + :SOS2, + :Zeros, + :Nonpositives, + :Nonnegatives, + :NormInfinityCone, + :NormOneCone, + :SecondOrderCone, + :RotatedSecondOrderCone, + :GeometricMeanCone, + :ExponentialCone, + :DualExponentialCone, + :PowerCone, + :DualPowerCone, + :RelativeEntropyCone, + :NormSpectralCone, + :NormNuclearCone, + :PositiveSemidefiniteConeSquare, + :PositiveSemidefiniteConeTriangle, + :LogDetConeTriangle, + :LogDetConeSquare, + :RootDetConeTriangle, + :RootDetConeSquare, + :Complements, +] + S = getfield(MOI, s) + functions = if S <: MOI.AbstractScalarSet + (:SingleVariable, :ScalarAffineFunction, :ScalarQuadraticFunction) + else + (:VectorOfVariables, :VectorAffineFunction, :VectorQuadraticFunction) + end + for f in functions + func = Symbol("test_basic_$(f)_$(s)") + F = getfield(MOI, f) + @eval begin + function $(func)(model::MOI.ModelLike, config::Config) + _basic_constraint_test_helper(model, config, $F, $S) + return + end + end + end +end + diff --git a/src/Test/test_variable.jl b/src/Test/test_variable.jl index 99b3c6bae0..9d415fc4f3 100644 --- a/src/Test/test_variable.jl +++ b/src/Test/test_variable.jl @@ -143,9 +143,7 @@ end Test getting variables by name. """ function test_variable_get_VariableIndex(model::MOI.ModelLike, config::Config) - if !_supports(config, MOI.VariableName()) - return - end + @requires MOI.supports(model, MOI.VariableName(), MOI.VariableIndex) variable = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), variable, "x") x = MOI.get(model, MOI.VariableIndex, "x") From fc57d7a39fd6bf1e32cfdc61ab1911d7bd80d0dd Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 29 Jun 2021 13:22:24 +1200 Subject: [PATCH 2/2] fix formatting --- src/Test/Test.jl | 8 +------- src/Test/test_basic_constraint.jl | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Test/Test.jl b/src/Test/Test.jl index baec91af36..433aaf732a 100644 --- a/src/Test/Test.jl +++ b/src/Test/Test.jl @@ -70,13 +70,7 @@ function Config( optimal_status::MOI.TerminationStatusCode = MOI.OPTIMAL, exclude::Vector{Any} = Any[], ) where {T<:Real} - return Config{T}( - atol, - rtol, - supports_optimize, - optimal_status, - exclude, - ) + return Config{T}(atol, rtol, supports_optimize, optimal_status, exclude) end function Base.copy(config::Config{T}) where {T} diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index 4f7fa182a2..842c6da500 100644 --- a/src/Test/test_basic_constraint.jl +++ b/src/Test/test_basic_constraint.jl @@ -17,7 +17,7 @@ end function _function( ::Type{T}, ::Type{MOI.ScalarAffineFunction}, - x::Vector{MOI.VariableIndex} + x::Vector{MOI.VariableIndex}, ) where {T} return MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), x), zero(T)) end @@ -25,7 +25,7 @@ end function _function( ::Type{T}, ::Type{MOI.ScalarQuadraticFunction}, - x::Vector{MOI.VariableIndex} + x::Vector{MOI.VariableIndex}, ) where {T} return MOI.ScalarQuadraticFunction( MOI.ScalarAffineTerm.(one(T), x), @@ -37,7 +37,7 @@ end function _function( ::Type{T}, ::Type{MOI.VectorAffineFunction}, - x::Vector{MOI.VariableIndex} + x::Vector{MOI.VariableIndex}, ) where {T} return MOI.VectorAffineFunction( MOI.VectorAffineTerm.(1:length(x), MOI.ScalarAffineTerm.(one(T), x)), @@ -48,7 +48,7 @@ end function _function( ::Type{T}, ::Type{MOI.VectorQuadraticFunction}, - x::Vector{MOI.VariableIndex} + x::Vector{MOI.VariableIndex}, ) where {T} return MOI.VectorQuadraticFunction( MOI.VectorAffineTerm.(1:length(x), MOI.ScalarAffineTerm.(one(T), x)), @@ -92,8 +92,12 @@ _set(::Type{MOI.DualPowerCone}) = MOI.DualPowerCone(0.5) _set(::Type{MOI.RelativeEntropyCone}) = MOI.RelativeEntropyCone(3) _set(::Type{MOI.NormSpectralCone}) = MOI.NormSpectralCone(2, 3) _set(::Type{MOI.NormNuclearCone}) = MOI.NormNuclearCone(2, 3) -_set(::Type{MOI.PositiveSemidefiniteConeTriangle}) = MOI.PositiveSemidefiniteConeTriangle(3) -_set(::Type{MOI.PositiveSemidefiniteConeSquare}) = MOI.PositiveSemidefiniteConeSquare(3) +function _set(::Type{MOI.PositiveSemidefiniteConeTriangle}) + return MOI.PositiveSemidefiniteConeTriangle(3) +end +function _set(::Type{MOI.PositiveSemidefiniteConeSquare}) + return MOI.PositiveSemidefiniteConeSquare(3) +end _set(::Type{MOI.LogDetConeTriangle}) = MOI.LogDetConeTriangle(3) _set(::Type{MOI.LogDetConeSquare}) = MOI.LogDetConeSquare(3) _set(::Type{MOI.RootDetConeTriangle}) = MOI.RootDetConeTriangle(3) @@ -187,7 +191,7 @@ function _basic_constraint_test_helper( if _supports(config, MOI.delete) MOI.delete(model, c_indices[1]) @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == - length(c_indices) - 1 + length(c_indices) - 1 @test !MOI.is_valid(model, c_indices[1]) @test_throws( MOI.InvalidIndex(c_indices[1]), @@ -259,4 +263,3 @@ for s in [ end end end -