Skip to content

Commit

Permalink
Add indicator syntax to @constraint
Browse files Browse the repository at this point in the history
  • Loading branch information
blegat committed Nov 1, 2019
1 parent ab8eda3 commit e0320db
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 2 deletions.
26 changes: 26 additions & 0 deletions docs/src/constraints.md
Expand Up @@ -497,6 +497,32 @@ julia> @constraint(model, x in MOI.SOS2([3.0, 1.0, 2.0]))
[x[1], x[2], x[3]] in MathOptInterface.SOS2{Float64}([3.0, 1.0, 2.0])
```

## Indicator constraints

JuMP provides a special syntax for creating indicator constraints, that is,
enforce a constraint to hold depending on the value of a binary variable.
In order to constrain the constraint `x + y <= 1` to hold when a binary
variable `a` is one, use the following syntax:
```jldoctest indicator; setup=:(model = Model())
julia> @variable(model, x)
x
julia> @variable(model, y)
y
julia> @variable(model, a, Bin)
a
julia> @constraint(model, a => x + y <= 1)
[a, x + y] ∈ MathOptInterface.IndicatorSet{MathOptInterface.ACTIVATE_ON_ONE,MathOptInterface.LessThan{Float64}}(MathOptInterface.LessThan{Float64}(1.0))
```
If instead the constraint should hold when `a` is zero, simply add a `!` before
the binary variable.
```jldoctest indicator
julia> @constraint(model, !a => x + y <= 1)
[a, x + y] ∈ MathOptInterface.IndicatorSet{MathOptInterface.ACTIVATE_ON_ZERO,MathOptInterface.LessThan{Float64}}(MathOptInterface.LessThan{Float64}(1.0))
```

## Semidefinite constraints

JuMP provides a special syntax for constraining a matrix to be symmetric
Expand Down
2 changes: 2 additions & 0 deletions src/JuMP.jl
Expand Up @@ -696,6 +696,8 @@ include("quad_expr.jl")

include("sets.jl")

# Indicator constraint
include("indicator.jl")
# SDConstraint
include("sd.jl")

Expand Down
37 changes: 37 additions & 0 deletions src/indicator.jl
@@ -0,0 +1,37 @@
function build_indicator_constraint(
_error::Function, variable::JuMP.AbstractVariableRef, constraint::JuMP.ScalarConstraint, ::Type{MOI.IndicatorSet{A}}) where A

set = MOI.IndicatorSet{A}(moi_set(constraint))
return VectorConstraint([variable, jump_function(constraint)], set)
end
function _indicator_variable_set(::Function, variable::Symbol)
return variable, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}
end
function _indicator_variable_set(_error::Function, expr::Expr)
if expr.args[1] == :¬ || expr.args[1] == :!
if length(expr.args) != 2
_error("Invalid binary variable expression `$(expr)` for indicator constraint.")
end
return expr.args[2], MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO}
else
return expr, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}
end
end
function parse_one_operator_constraint(
_error::Function, vectorized::Bool, ::Union{Val{:(=>)}, Val{:⇒}}, lhs, rhs)

variable, S = _indicator_variable_set(_error, lhs)
if !(rhs isa Expr)
_error("Invalid right-hand side `$(rhs)`.")
end
rhs_vectorized, rhs_parsecode, rhs_buildcall = parse_constraint(_error, rhs.args...)
if vectorized != rhs_vectorized
_error("Inconsistent use of `.` in symbols to indicate vectorization.")
end
if vectorized
buildcall = :(build_indicator_constraint.($_error, $(esc(variable)), $rhs_buildcall, $S))
else
buildcall = :(build_indicator_constraint($_error, $(esc(variable)), $rhs_buildcall, $S))
end
return rhs_parsecode, buildcall
end
31 changes: 31 additions & 0 deletions test/constraint.jl
Expand Up @@ -239,6 +239,37 @@ function constraints_test(ModelType::Type{<:JuMP.AbstractModel},
@test_throws err @constraint(model, [3, x] in SecondOrderCone())
end

@testset "Indicator constraint" begin
model = ModelType()
@variable(model, a, Bin)
@variable(model, b, Bin)
@variable(model, x)
@variable(model, y)
for cref in [
@constraint(model, a => x + 2y <= 1),
@constraint(model, a x + 2y 1)
]
c = JuMP.constraint_object(cref)
@test c.func == [a, x + 2y]
@test c.set == MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.LessThan(1.0))
end
for cref in [
@constraint(model, !b => 2x + y <= 1);
@constraint(model, ¬b 2x + y 1);
@constraint(model, ![b, b] .=> [2x + y, 2x + y] .≤ 1);
]
c = JuMP.constraint_object(cref)
@test c.func == [b, 2x + y]
@test c.set == MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO}(MOI.LessThan(1.0))
end
err = ErrorException("In `@constraint(model, !(a, b) => x <= 1)`: Invalid binary variable expression `!(a, b)` for indicator constraint.")
@test_macro_throws err @constraint(model, !(a, b) => x <= 1)
err = ErrorException("In `@constraint(model, a => x)`: Invalid right-hand side `x`.")
@test_macro_throws err @constraint(model, a => x)
err = ErrorException("In `@constraint(model, [a, b] .=> x + y <= 1)`: Inconsistent use of `.` in symbols to indicate vectorization.")
@test_macro_throws err @constraint(model, [a, b] .=> x + y <= 1)
end

@testset "SDP constraint" begin
m = ModelType()
@variable(m, x)
Expand Down
4 changes: 2 additions & 2 deletions test/utilities.jl
Expand Up @@ -27,5 +27,5 @@ end
# Test that the macro call `m` throws an error exception during pre-compilation
macro test_macro_throws(errortype, m)
# See https://discourse.julialang.org/t/test-throws-with-macros-after-pr-23533/5878
:(@test_throws $errortype try @eval $m catch err; throw(err.error) end)
end
:(@test_throws $(esc(errortype)) try @eval $m catch err; throw(err.error) end)
end

0 comments on commit e0320db

Please sign in to comment.