Skip to content

Commit

Permalink
Merge pull request #121 from SCIP-Interfaces/indicator-moi
Browse files Browse the repository at this point in the history
Indicator moi
  • Loading branch information
rschwarz authored Aug 20, 2019
2 parents 824e3ea + b8c612c commit 6ce01a6
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 133 deletions.
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
3 changes: 3 additions & 0 deletions test/MOI_wrapper_direct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,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

0 comments on commit 6ce01a6

Please sign in to comment.