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

Indicator moi #121

Merged
merged 14 commits into from
Aug 20, 2019
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## v0.8.0

- SCIP-specific MOI indicator constraint replaced by the new MOI definition
- support Windows [#122](https://github.com/SCIP-Interfaces/SCIP.jl/pull/122)
- switch from REQUIRE to Project.toml [#119](https://github.com/SCIP-Interfaces/SCIP.jl/pull/119)

Expand Down
36 changes: 13 additions & 23 deletions src/MOI_wrapper/indicator_constraints.jl
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
# Indicator constraints

"""
Set of (x,y) that satisfy the indicator constraint:
MOI.supports_constraint(::Optimizer, ::Type{<:MOI.VectorAffineFunction}, ::Type{<:MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, <:MOI.LessThan}}) = true

y ∈ 𝔹, y = 1 ==> a^T x ≤ rhs
"""
struct IndicatorSet{T <: Real} <: MOI.AbstractVectorSet
a::Vector{T}
rhs::T
end

MOI.dimension(idset::IndicatorSet) = length(idset.a) + 1

MOI.supports_constraint(::Optimizer, ::Type{VECTOR}, ::Type{IS}) where {IS <: IndicatorSet{<:AbstractFloat}} = true

function MOI.add_constraint(o::Optimizer, func::VECTOR, set::IndicatorSet{Float64})
length(func.variables) == length(set.a) + 1 || throw(DimensionMismatch("Indicator set and VectorOfVariables length mismatch"))
function MOI.add_constraint(o::Optimizer, func::MOI.VectorAffineFunction{T}, set::MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, LT}) where {T<:Real, LT<:MOI.LessThan}
allow_modification(o)
y = VarRef(func.variables[1].value)
x = [VarRef(vi.value) for vi in func.variables[2:end]]

cr = add_indicator_constraint(o.mscip, y, x, set.a, set.rhs)
ci = CI{VECTOR, IndicatorSet{Float64}}(cr.val)
first_index_terms = [v.scalar_term for v in func.terms if v.output_index == 1]
scalar_index_terms = [v.scalar_term for v in func.terms if v.output_index != 1]
length(first_index_terms) == 1 || error("There should be exactly one term in output_index 1, found $(length(first_index_terms))")
y = VarRef(first_index_terms[1].variable_index.value)
x = [VarRef(vi.variable_index.value) for vi in scalar_index_terms]
a = [vi.coefficient for vi in scalar_index_terms]

cr = add_indicator_constraint(o.mscip, y, x, a, MOI.constant(set.set))
ci = CI{MOI.VectorAffineFunction{T}, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, LT}}(cr.val)
register!(o, ci)
register!(o, cons(o, ci), cr)
return ci
end

function MOI.delete(o::Optimizer, ci::CI{VECTOR, IndicatorSet{Float64}})
function MOI.delete(o::Optimizer, ci::CI{MOI.VectorAffineFunction{T}, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, LT}}) where {T<:Real, LT<:MOI.LessThan}
allow_modification(o)
delete!(o.constypes[VECTOR, IndicatorSet{Float64}], ConsRef(ci.value))
delete!(o.constypes[MOI.VectorAffineFunction{T}, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, LT}], ConsRef(ci.value))
delete!(o.reference, cons(o, ci))
delete(o.mscip, ConsRef(ci.value))
return nothing
Expand Down
130 changes: 25 additions & 105 deletions test/MOI_additional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,33 @@ end
x3 = MOI.add_variable(optimizer)
y = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, y, MOI.ZeroOne())
iset = SCIP.IndicatorSet{Float64}(ones(3), 1.0)
c = MOI.add_constraint(optimizer,
MOI.VectorOfVariables([y, x1, x2, x3]), iset
iset = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(1.0))
ind_func = MOI.VectorAffineFunction(
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, y)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x1)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x3)),
], [0.0, 0.0],
)

c = MOI.add_constraint(optimizer, ind_func, iset)
@test MOI.delete(optimizer, c) === nothing

# adding incorrect function throws
ind_func_wrong = MOI.VectorAffineFunction(
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, y)),
MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x1)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x3)),
], [0.0, 0.0],
)
@test_throws ErrorException MOI.add_constraint(optimizer, ind_func_wrong, iset)
ind_func_wrong2 = MOI.VectorAffineFunction(
[MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x2)),
MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x3)),
], [0.0, 0.0],
)
@test_throws ErrorException MOI.add_constraint(optimizer, ind_func_wrong2, iset)
end

@testset "Bound constraints for a general variable." begin
Expand Down Expand Up @@ -359,108 +381,6 @@ end
@test MOI.get(optimizer, MOI.VariablePrimal(), z2) -8.0 atol=atol rtol=rtol
end

@testset "indicator constraints" begin
# max 2x1 + 3x2
# s.t. x1 + x2 <= 10
# z1 ==> x2 <= 8
# z2 ==> x2 + x1/5 <= 9
# z1 + z2 >= 1

optimizer = SCIP.Optimizer(display_verblevel=0)

@test MOI.supports_constraint(optimizer, MOI.VectorOfVariables, SCIP.IndicatorSet{Float64})

x1 = MOI.add_variable(optimizer)
x2 = MOI.add_variable(optimizer)
x3 = MOI.add_variable(optimizer)
y = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, y, MOI.ZeroOne())
iset = SCIP.IndicatorSet{Float64}(ones(3), 1.0)
@test_throws DimensionMismatch MOI.add_constraint(optimizer,
MOI.VectorOfVariables([y, x1, x2]), iset
)
MOI.empty!(optimizer)

# non-binary y throws error
x1 = MOI.add_variable(optimizer)
x2 = MOI.add_variable(optimizer)
y = MOI.add_variable(optimizer)
iset = SCIP.IndicatorSet{Float64}(ones(2), 1.0)
@test_throws ErrorException MOI.add_constraint(optimizer,
MOI.VectorOfVariables([y, x1, x2]), iset
)

x1, x2, z1, z2 = MOI.add_variables(optimizer, 4)

MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2., 3.], [x1, x2]), 0.)
)
MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

MOI.add_constraint(optimizer, MOI.SingleVariable(z1), MOI.ZeroOne())
MOI.add_constraint(optimizer, MOI.SingleVariable(z2), MOI.ZeroOne())

MOI.add_constraint(optimizer,
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2)], 0.0),
MOI.LessThan(10.0),
)

MOI.add_constraint(optimizer,
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, z1), MOI.ScalarAffineTerm(1.0, z2)], 0.0),
MOI.GreaterThan(1.),
)

MOI.add_constraint(optimizer, MOI.VectorOfVariables([z1, x2]), SCIP.IndicatorSet{Float64}([1.], 8.))
MOI.add_constraint(optimizer, MOI.VectorOfVariables([z2, x1, x2]), SCIP.IndicatorSet{Float64}([0.2, 1.], 9.))

MOI.optimize!(optimizer)

@test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL
@test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT

atol, rtol = 1e-6, 1e-6
@test MOI.get(optimizer, MOI.ObjectiveValue()) 28.75 atol=atol rtol=rtol
@test MOI.get(optimizer, MOI.VariablePrimal(), x1) 1.25 atol=atol rtol=rtol
@test MOI.get(optimizer, MOI.VariablePrimal(), x2) 8.75 atol=atol rtol=rtol
@test MOI.get(optimizer, MOI.VariablePrimal(), z1) 0.0 atol=atol rtol=rtol
@test MOI.get(optimizer, MOI.VariablePrimal(), z2) 1.0 atol=atol rtol=rtol

# penalty on z2 must switch active constraint on z1
optimizer2 = SCIP.Optimizer(display_verblevel=0)

x1, x2, z1, z2 = MOI.add_variables(optimizer2, 4)

MOI.set(optimizer2, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2., 3., -30.], [x1, x2, z2]), 0.)
)
MOI.set(optimizer2, MOI.ObjectiveSense(), MOI.MAX_SENSE)

MOI.add_constraint(optimizer2, MOI.SingleVariable(z1), MOI.ZeroOne())
MOI.add_constraint(optimizer2, MOI.SingleVariable(z2), MOI.ZeroOne())

MOI.add_constraint(optimizer2,
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2)], 0.0),
MOI.LessThan(10.0),
)

MOI.add_constraint(optimizer2,
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, z1), MOI.ScalarAffineTerm(1.0, z2)], 0.0),
MOI.GreaterThan(1.),
)

MOI.add_constraint(optimizer2, MOI.VectorOfVariables([z1, x2]), SCIP.IndicatorSet{Float64}([1.], 8.))
MOI.add_constraint(optimizer2, MOI.VectorOfVariables([z2, x1, x2]), SCIP.IndicatorSet{Float64}([0.2, 1.], 9.))
MOI.optimize!(optimizer2)

atol, rtol = 1e-6, 1e-6
@test MOI.get(optimizer2, MOI.ObjectiveValue()) 28.0 atol=atol rtol=rtol
@test MOI.get(optimizer2, MOI.VariablePrimal(), x1) 2.0 atol=atol rtol=rtol
@test MOI.get(optimizer2, MOI.VariablePrimal(), x2) 8.0 atol=atol rtol=rtol
@test MOI.get(optimizer2, MOI.VariablePrimal(), z1) 1.0 atol=atol rtol=rtol
@test MOI.get(optimizer2, MOI.VariablePrimal(), z2) 0.0 atol=atol rtol=rtol

end

@testset "deleting variables" begin
optimizer = SCIP.Optimizer()

Expand Down
6 changes: 1 addition & 5 deletions test/MOI_wrapper_bridged.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ end
end

@testset "MOI Integer Linear" begin
excluded = [
"indicator1", # bridge is missing and
"indicator2", # our native implementation is not merged.
"indicator3",
]
excluded = String[]
MOIT.intlineartest(BRIDGED, CONFIG, excluded)
end

Expand Down
9 changes: 7 additions & 2 deletions test/MOI_wrapper_direct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ end
# call individual tests
MOIT.qcp2test(OPTIMIZER, CONFIG)
MOIT.qcp3test(OPTIMIZER, CONFIG)
# MOIT.qcp4test(OPTIMIZER, CONFIG) # not yet merged
# MOIT.qcp5test(OPTIMIZER, CONFIG) # not yet merged
MOIT.qcp4test(OPTIMIZER, CONFIG)

MOIT.ncqcp1test(OPTIMIZER, CONFIG)
MOIT.ncqcp2test(OPTIMIZER, CONFIG)
end

@testset "MOI Integer Linear" begin
Expand All @@ -56,4 +58,7 @@ end
MOIT.int1test(OPTIMIZER, CONFIG)
MOIT.int2test(OPTIMIZER, CONFIG)
MOIT.int3test(OPTIMIZER, CONFIG)
MOIT.indicator1_test(OPTIMIZER, CONFIG)
MOIT.indicator2_test(OPTIMIZER, CONFIG)
# MOIT.indicator3_test(OPTIMIZER, CONFIG) # no support for ACTIVATE_ON_ZERO
end