Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Comprehensive testing of basic constraint functionality #354

Merged
merged 15 commits into from
May 16, 2018
237 changes: 237 additions & 0 deletions src/Test/UnitTests/basic_constraint_tests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# x
const dummy_single_variable = (x::Vector{MOI.VariableIndex}) -> MOI.SingleVariable(x[1])
# x₁, x₂
const dummy_vectorofvariables = (x::Vector{MOI.VariableIndex}) -> MOI.VectorOfVariables(x)
# 1.0 * x + 0.0
const dummy_scalar_affine = (x::Vector{MOI.VariableIndex}) -> MOI.ScalarAffineFunction(x, ones(Float64, length(x)), 0.0)
# 1.0 * x + 1.0 * x^2 + 0.0
const dummy_scalar_quadratic = (x::Vector{MOI.VariableIndex}) -> MOI.ScalarQuadraticFunction(x, ones(Float64, length(x)), x, x, ones(Float64, length(x)), 0.0)
# x₁ + + 0.0
# + x₂ + 0.0
const dummy_vector_affine = (x::Vector{MOI.VariableIndex}) -> MOI.VectorAffineFunction(collect(1:length(x)), x, ones(Float64, length(x)), zeros(Float64, length(x)))
# x₁ + + x₁^2 + 0.0
# + x₂ + + x₂^2 + 0.0
const dummy_vector_quadratic = (x::Vector{MOI.VariableIndex}) -> MOI.VectorQuadraticFunction(
collect(1:length(x)), x, ones(Float64, length(x)), # affine component
collect(1:length(x)), x, x, ones(Float64, length(x)), # quadratic component
zeros(Float64, length(x)) # constant term
)

const BasicConstraintTests = Dict(
(MOI.SingleVariable, MOI.LessThan{Float64}) => ( dummy_single_variable, 1, MOI.LessThan(1.0) ),
(MOI.SingleVariable, MOI.GreaterThan{Float64}) => ( dummy_single_variable, 1, MOI.GreaterThan(1.0) ),
(MOI.SingleVariable, MOI.EqualTo{Float64}) => ( dummy_single_variable, 1, MOI.EqualTo(1.0) ),
(MOI.SingleVariable, MOI.Interval{Float64}) => ( dummy_single_variable, 1, MOI.Interval(1.0, 2.0) ),

(MOI.SingleVariable, MOI.ZeroOne) => ( dummy_single_variable, 1, MOI.ZeroOne() ),
(MOI.SingleVariable, MOI.Integer) => ( dummy_single_variable, 1, MOI.Integer() ),
(MOI.SingleVariable, MOI.Semicontinuous{Float64}) => ( dummy_single_variable, 1, MOI.Semicontinuous(1.0, 2.0) ),
(MOI.SingleVariable, MOI.Semiinteger{Float64}) => ( dummy_single_variable, 1, MOI.Semiinteger(1.0, 2.0) ),

(MOI.VectorOfVariables, MOI.SOS1{Float64}) => ( dummy_vectorofvariables, 2, MOI.SOS1([1.0, 2.0]) ),
(MOI.VectorOfVariables, MOI.SOS2{Float64}) => ( dummy_vectorofvariables, 2, MOI.SOS2([1.0, 2.0]) ),
(MOI.VectorOfVariables, MOI.Reals) => ( dummy_vectorofvariables, 2, MOI.Reals(2) ),
(MOI.VectorOfVariables, MOI.Zeros) => ( dummy_vectorofvariables, 2, MOI.Zeros(2) ),
(MOI.VectorOfVariables, MOI.Nonpositives) => ( dummy_vectorofvariables, 2, MOI.Nonpositives(2) ),
(MOI.VectorOfVariables, MOI.Nonnegatives) => ( dummy_vectorofvariables, 2, MOI.Nonnegatives(2) ),

(MOI.VectorOfVariables, MOI.SecondOrderCone) => ( dummy_vectorofvariables, 3, MOI.SecondOrderCone(3) ),
(MOI.VectorOfVariables, MOI.RotatedSecondOrderCone) => ( dummy_vectorofvariables, 3, MOI.RotatedSecondOrderCone(3) ),
(MOI.VectorOfVariables, MOI.GeometricMeanCone) => ( dummy_vectorofvariables, 3, MOI.GeometricMeanCone(3) ),
(MOI.VectorOfVariables, MOI.ExponentialCone) => ( dummy_vectorofvariables, 3, MOI.ExponentialCone() ),
(MOI.VectorOfVariables, MOI.DualExponentialCone) => ( dummy_vectorofvariables, 3, MOI.DualExponentialCone() ),
(MOI.VectorOfVariables, MOI.PowerCone) => ( dummy_vectorofvariables, 3, MOI.PowerCone(2.0) ),
(MOI.VectorOfVariables, MOI.DualPowerCone) => ( dummy_vectorofvariables, 3, MOI.DualPowerCone(2.0) ),

(MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle) => ( dummy_vectorofvariables, 7, MOI.PositiveSemidefiniteConeTriangle(3) ),
(MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeSquare) => ( dummy_vectorofvariables, 10, MOI.PositiveSemidefiniteConeSquare(3) ),
(MOI.VectorOfVariables, MOI.LogDetConeTriangle) => ( dummy_vectorofvariables, 7, MOI.LogDetConeTriangle(3) ),
(MOI.VectorOfVariables, MOI.LogDetConeSquare) => ( dummy_vectorofvariables, 10, MOI.LogDetConeSquare(3) ),
(MOI.VectorOfVariables, MOI.RootDetConeTriangle) => ( dummy_vectorofvariables, 7, MOI.RootDetConeTriangle(3) ),
(MOI.VectorOfVariables, MOI.RootDetConeSquare) => ( dummy_vectorofvariables, 10, MOI.RootDetConeSquare(3) ),

(MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) => ( dummy_scalar_affine, 1, MOI.LessThan(1.0) ),
(MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) => ( dummy_scalar_affine, 1, MOI.GreaterThan(1.0) ),
(MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) => ( dummy_scalar_affine, 1, MOI.EqualTo(1.0) ),
(MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) => ( dummy_scalar_affine, 1, MOI.Interval(1.0, 2.0) ),

(MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) => ( dummy_scalar_quadratic, 1, MOI.LessThan(1.0) ),
(MOI.ScalarQuadraticFunction{Float64}, MOI.GreaterThan{Float64}) => ( dummy_scalar_quadratic, 1, MOI.GreaterThan(1.0) ),
(MOI.ScalarQuadraticFunction{Float64}, MOI.EqualTo{Float64}) => ( dummy_scalar_quadratic, 1, MOI.EqualTo(1.0) ),
(MOI.ScalarQuadraticFunction{Float64}, MOI.Interval{Float64}) => ( dummy_scalar_quadratic, 1, MOI.Interval(1.0, 2.0) ),

(MOI.VectorAffineFunction{Float64}, MOI.Reals) => ( dummy_vector_affine, 2, MOI.Reals(2) ),
(MOI.VectorAffineFunction{Float64}, MOI.Zeros) => ( dummy_vector_affine, 2, MOI.Zeros(2) ),
(MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => ( dummy_vector_affine, 2, MOI.Nonpositives(2) ),
(MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => ( dummy_vector_affine, 2, MOI.Nonnegatives(2) ),

(MOI.VectorQuadraticFunction{Float64}, MOI.Reals) => ( dummy_vector_quadratic, 2, MOI.Reals(2) ),
(MOI.VectorQuadraticFunction{Float64}, MOI.Zeros) => ( dummy_vector_quadratic, 2, MOI.Zeros(2) ),
(MOI.VectorQuadraticFunction{Float64}, MOI.Nonpositives) => ( dummy_vector_quadratic, 2, MOI.Nonpositives(2) ),
(MOI.VectorQuadraticFunction{Float64}, MOI.Nonnegatives) => ( dummy_vector_quadratic, 2, MOI.Nonnegatives(2) )
)
"""
basic_constraint_tests(model::MOI.ModelLike, config::TestConfig;
delete = true,
get_constraint_function = true,
get_constraint_set = true,
include = Any[],
exclude = Any[]
)

Test basic constraint functionality for all function-in-set combinations that
are supported by `model`.

See also `basic_constraint_test_helper`.

If `delete = true`, it will test the deletion of constraints.
If `get_constraint_function = true`, it will test the getting of `ConstraintFunction`.
If `get_constraint_set = true`, it will test the getting of `ConstraintSet`.

`include` and `exclude` can be used to run a subset of the tests, although only
one can be used in each call.

### Examples

basic_constraint_tests(model, config; exclude = [
(MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64})
])

basic_constraint_tests(model, config;
delete = false,
include = [
(MOI.VectorOfVariables, MOI.SOS2{Float64})
]
)
"""
function basic_constraint_tests(model::MOI.ModelLike, config::TestConfig;
delete = true,
get_constraint_function = true,
get_constraint_set = true,
include = Any[],
exclude = Any[]
)
if length(include) > 0 && length(exclude) > 0
error("Don't use `include` and `exclude` together.")
end
test_keys = length(include) > 0 ? include : Iterators.filter(x->!(x in exclude), keys(BasicConstraintTests))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you do test_keys = keys(BasicConstraintTests) in the case where include and exclude are both empty. Since you do if MOI.supportsconstraint later, this would test all supported constraints and remove the need for a ListOfSupportedConstraintTypes discussed in #357 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does test keys(BasicConstraintTests) if include and exclude are both empty.

If we had ListOfSupportedConstraintTypes, we could throw a warning that we didn't test SingleVariable-in-LessThan{Int} for example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does test keys(BasicConstraintTests) if include and exclude are both empty.

Indeed ^^

If we had ListOfSupportedConstraintTypes, we could throw a warning that we didn't test SingleVariable-in-LessThan{Int} for example.

The warning would be called when include is non-empty and it does not contain all supported sets ? This can already be done now by iterating through the keys of BasicConstraintTests

for (F,S) in test_keys
if MOI.supportsconstraint(model, F, S)
@testset "$(F)-$(S)" begin
(cf, N, set) = BasicConstraintTests[(F,S)]
basic_constraint_test_helper(model, config, cf, set, N;
delete = delete,
get_constraint_function = get_constraint_function,
get_constraint_set = get_constraint_set
)
end
end
end
end

"""
basic_constraint_test_helper(model::MOI.ModelLike, config::TestConfig, func::Function, set::MOI.AbstractSet, N::Int;
delete::Bool = true,
get_constraint_function::Bool = true,
get_constraint_set::Bool = true
)

A helper function for `basic_constraint_tests`.

This function tests the basic functionality of the constraint type `F`-in-`S`
where `S = typeof(set)`, and `func` is a function that takes a vector of `N`
variables and returns a `MOI.AbstractFunction` of type `F`.

If `delete = true`, it will test the deletion of constraints.
If `get_constraint_function = true`, it will test the getting of `ConstraintFunction`.
If `get_constraint_set = true`, it will test the getting of `ConstraintSet`.

### Example

basic_constraint_test_helper(model, config,
(x) -> MOI.ScalarAffineFunction(model, [x], [1.0], 0.0),
MOI.LessThan(1.0),
1;
delete=false
)

"""
function basic_constraint_test_helper(model::MOI.ModelLike, config::TestConfig, func::Function, set::MOI.AbstractSet, N::Int=1;
delete::Bool = true,
get_constraint_function::Bool = true,
get_constraint_set::Bool = true
)
MOI.empty!(model)
x = MOI.addvariables!(model, N)
constraint_function = func(x)
F, S = typeof(constraint_function), typeof(set)

@test MOI.supportsconstraint(model, F, S)
@test MOI.canaddconstraint(model, F, S)

@testset "NumberOfConstraints" begin
@test MOI.canget(model, MOI.NumberOfConstraints{F,S}())
end

@testset "addconstraint!" begin
n = MOI.get(model, MOI.NumberOfConstraints{F,S}())
c = MOI.addconstraint!(model, constraint_function, set)
@test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == n + 1

@testset "ConstraintName" begin
@test MOI.canget(model, MOI.ConstraintName(), typeof(c))
@test MOI.get(model, MOI.ConstraintName(), c) == ""
@test MOI.canset(model, MOI.ConstraintName(), typeof(c))
MOI.set!(model, MOI.ConstraintName(), c, "c")
@test MOI.get(model, MOI.ConstraintName(), c) == "c"
end

if get_constraint_function
@testset "ConstraintFunction" begin
@test MOI.canget(model, MOI.ConstraintFunction(), typeof(c))
@test MOI.get(model, MOI.ConstraintFunction(), c) ≈ constraint_function
end
end
if get_constraint_set
@testset "ConstraintSet" begin
@test MOI.canget(model, MOI.ConstraintSet(), typeof(c))
@test MOI.get(model, MOI.ConstraintSet(), c) == set
end
end
end

@testset "addconstraints!" begin
n = MOI.get(model, MOI.NumberOfConstraints{F,S}())
cc = MOI.addconstraints!(model,
[constraint_function, constraint_function],
[set, set]
)
@test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == n + 2
end

@testset "ListOfConstraintIndices" begin
@test MOI.canget(model, MOI.ListOfConstraintIndices{F,S}())
c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
# for sanity, check that we've added 3 constraints as expected.
@test length(c_indices) == MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 3
end

@testset "isvalid" begin
c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
# for sanity, check that we've added 3 constraints as expected.
@test length(c_indices) == 3
@test all(MOI.isvalid.(model, c_indices))
end

if delete
@testset "delete!" begin
c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
# for sanity, check that we've added 3 constraints as expected.
@test length(c_indices) == 3
@test MOI.candelete(model, c_indices[1])
MOI.delete!(model, c_indices[1])
@test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == length(c_indices)-1 == 2
@test !MOI.isvalid(model, c_indices[1])
end
end
end
2 changes: 2 additions & 0 deletions src/Test/UnitTests/unit_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,7 @@ end
include("variables.jl")
include("objectives.jl")
include("constraints.jl")
include("basic_constraint_tests.jl")


@moitestset unit
6 changes: 6 additions & 0 deletions src/sets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,9 @@ struct SOS1{T <: Real} <: AbstractVectorSet
weights::Vector{T}
end

Base.:(==)(a::SOS1{T}, b::SOS1{T}) where T = a.weights == b.weights
Base.isapprox(a::SOS1{T}, b::SOS1{T}; kwargs...) where T = isapprox(a.weights, b.weights; kwargs...)

"""
SOS2{T <: Real}(weights::Vector{T})

Expand All @@ -392,4 +395,7 @@ struct SOS2{T <: Real} <: AbstractVectorSet
weights::Vector{T}
end

Base.:(==)(a::SOS2{T}, b::SOS2{T}) where T = a.weights == b.weights
Base.isapprox(a::SOS2{T}, b::SOS2{T}; kwargs...) where T = isapprox(a.weights, b.weights; kwargs...)

dimension(s::Union{SOS1, SOS2}) = length(s.weights)
6 changes: 6 additions & 0 deletions test/Test/unit.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
@testset "Basic Constraint Tests" begin
mock = MOIU.MockOptimizer(Model{Float64}())
config = MOIT.TestConfig()
MOIT.basic_constraint_tests(mock, config)
end

@testset "Unit Tests" begin
mock = MOIU.MockOptimizer(Model{Float64}())
config = MOIT.TestConfig()
Expand Down
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ end
# Model supporting every MOI functions and sets
MOIU.@model(Model,
(ZeroOne, Integer),
(EqualTo, GreaterThan, LessThan, Interval),
(EqualTo, GreaterThan, LessThan, Interval, Semicontinuous, Semiinteger),
(Reals, Zeros, Nonnegatives, Nonpositives, SecondOrderCone, RotatedSecondOrderCone, GeometricMeanCone, ExponentialCone, DualExponentialCone, PositiveSemidefiniteConeTriangle, PositiveSemidefiniteConeSquare, RootDetConeTriangle, RootDetConeSquare, LogDetConeTriangle, LogDetConeSquare),
(PowerCone, DualPowerCone, SOS1, SOS2),
(SingleVariable,),
Expand Down