From e0320db89d7c8a9b62db6946a4f180d4055de418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 1 Nov 2019 13:27:39 +0100 Subject: [PATCH] Add indicator syntax to @constraint --- docs/src/constraints.md | 26 ++++++++++++++++++++++++++ src/JuMP.jl | 2 ++ src/indicator.jl | 37 +++++++++++++++++++++++++++++++++++++ test/constraint.jl | 31 +++++++++++++++++++++++++++++++ test/utilities.jl | 4 ++-- 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/indicator.jl diff --git a/docs/src/constraints.md b/docs/src/constraints.md index 2dae857bc01..b2d8def0c98 100644 --- a/docs/src/constraints.md +++ b/docs/src/constraints.md @@ -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 diff --git a/src/JuMP.jl b/src/JuMP.jl index e227f2215a2..1e8e8b943d2 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -696,6 +696,8 @@ include("quad_expr.jl") include("sets.jl") +# Indicator constraint +include("indicator.jl") # SDConstraint include("sd.jl") diff --git a/src/indicator.jl b/src/indicator.jl new file mode 100644 index 00000000000..76abfc28261 --- /dev/null +++ b/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 diff --git a/test/constraint.jl b/test/constraint.jl index ed47c04e0e3..4b8d235d2bc 100644 --- a/test/constraint.jl +++ b/test/constraint.jl @@ -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) diff --git a/test/utilities.jl b/test/utilities.jl index f13914ba97c..de739ed867c 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -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 \ No newline at end of file + :(@test_throws $(esc(errortype)) try @eval $m catch err; throw(err.error) end) +end