From 06b0d602439d3f200d7ac8dfd7e7cccac7dd5d2c Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 29 Sep 2023 11:21:47 +1300 Subject: [PATCH 1/8] Add := operator for Boolean satisfiability problems --- docs/src/developers/extensions.md | 8 ++-- src/constraints.jl | 28 ++++++++++++ src/macros.jl | 4 ++ test/test_constraint.jl | 76 +++++++++++++++++++++++++++++++ test/test_macros.jl | 4 +- 5 files changed, 114 insertions(+), 6 deletions(-) diff --git a/docs/src/developers/extensions.md b/docs/src/developers/extensions.md index 4d7a19f561d..cf65e9a8a92 100644 --- a/docs/src/developers/extensions.md +++ b/docs/src/developers/extensions.md @@ -200,11 +200,11 @@ julia> model = Model(); @variable(model, x); julia> function JuMP.parse_constraint_head( _error::Function, - ::Val{:(:=)}, + ::Val{:≡}, lhs, rhs, ) - println("Rewriting := as ==") + println("Rewriting ≡ as ==") new_lhs, parse_code = MutableArithmetics.rewrite(lhs) build_code = :( build_constraint($(_error), $(new_lhs), MOI.EqualTo($(rhs))) @@ -212,8 +212,8 @@ julia> function JuMP.parse_constraint_head( return false, parse_code, build_code end -julia> @constraint(model, x + x := 1.0) -Rewriting := as == +julia> @constraint(model, x + x ≡ 1.0) +Rewriting ≡ as == 2 x = 1 ``` diff --git a/src/constraints.jl b/src/constraints.jl index 75b2e027910..aaca4009e38 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -1542,3 +1542,31 @@ function relax_with_penalty!( ) where {T} return relax_with_penalty!(model, Dict(); default = default) end + + +function parse_constraint_head( + error_fn::Function, + ::Val{:(:=)}, + lhs, + rhs, +) + new_lhs, parse_code_lhs = _rewrite_expression(lhs) + new_rhs, parse_code_rhs = _rewrite_expression(rhs) + parse_code = quote + $parse_code_lhs + $parse_code_rhs + end + build_code = quote + if $new_rhs isa Bool + ScalarConstraint($new_lhs, MOI.EqualTo($new_rhs)) + elseif $new_lhs isa Bool + ScalarConstraint($new_rhs, MOI.EqualTo($new_lhs)) + else + ScalarConstraint( + op_equal_to($new_lhs, $new_rhs), + MOI.EqualTo(true), + ) + end + end + return false, parse_code, build_code +end diff --git a/src/macros.jl b/src/macros.jl index 9cb281d0315..2a5127bfc34 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -1160,6 +1160,10 @@ function model_convert(model::AbstractModel, set::MOI.AbstractScalarSet) return set end +function model_convert(model::GenericModel{Bool}, set::MOI.EqualTo{Bool}) + return set +end + function model_convert(model::AbstractModel, α::Number) T = value_type(typeof(model)) V = variable_ref_type(model) diff --git a/test/test_constraint.jl b/test/test_constraint.jl index eaed17cbb43..98a0ce4e3b0 100644 --- a/test/test_constraint.jl +++ b/test/test_constraint.jl @@ -1721,4 +1721,80 @@ function test_triangle_vec() return end +function test_def_equal_to_operator() + model = GenericModel{Bool}() + @variable(model, x[1:3]) + # x[1] := x[2] + c = @constraint(model, x[1] := x[2]) + o = constraint_object(c) + @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) + @test o.set == MOI.EqualTo(true) + # x[1] == x[2] := false + c = @constraint(model, x[1] == x[2] := false) + o = constraint_object(c) + @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) + @test o.set == MOI.EqualTo(false) + # x[1] && x[2] := false + c = @constraint(model, x[1] && x[2] := false) + o = constraint_object(c) + @test isequal_canonical(o.func, op_and(x[1], x[2])) + @test o.set == MOI.EqualTo(false) + # x[1] && x[2] := true + c = @constraint(model, x[1] && x[2] := true) + o = constraint_object(c) + @test isequal_canonical(o.func, op_and(x[1], x[2])) + @test o.set == MOI.EqualTo(true) + # x[1] || x[2] := y + y = true + c = @constraint(model, x[1] || x[2] := y) + o = constraint_object(c) + @test isequal_canonical(o.func, op_or(x[1], x[2])) + @test o.set == MOI.EqualTo(y) + # y := x[1] || x[2] + y = true + c = @constraint(model, y := x[1] || x[2]) + o = constraint_object(c) + @test isequal_canonical(o.func, op_or(x[1], x[2])) + @test o.set == MOI.EqualTo(y) + return +end + +function test_def_equal_to_operator_float() + model = Model() + @variable(model, x[1:3]) + # x[1] := x[2] + c = @constraint(model, x[1] := x[2]) + o = constraint_object(c) + @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) + @test o.set == MOI.EqualTo(1.0) + # x[1] == x[2] := false + c = @constraint(model, x[1] == x[2] := false) + o = constraint_object(c) + @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) + @test o.set == MOI.EqualTo(0.0) + # x[1] && x[2] := false + c = @constraint(model, x[1] && x[2] := false) + o = constraint_object(c) + @test isequal_canonical(o.func, op_and(x[1], x[2])) + @test o.set == MOI.EqualTo(0.0) + # x[1] && x[2] := true + c = @constraint(model, x[1] && x[2] := true) + o = constraint_object(c) + @test isequal_canonical(o.func, op_and(x[1], x[2])) + @test o.set == MOI.EqualTo(1.0) + # x[1] || x[2] := y + y = true + c = @constraint(model, x[1] || x[2] := y) + o = constraint_object(c) + @test isequal_canonical(o.func, op_or(x[1], x[2])) + @test o.set == MOI.EqualTo(1.0) + # y := x[1] || x[2] + y = true + c = @constraint(model, y := x[1] || x[2]) + o = constraint_object(c) + @test isequal_canonical(o.func, op_or(x[1], x[2])) + @test o.set == MOI.EqualTo(1.0) + return +end + end diff --git a/test/test_macros.jl b/test/test_macros.jl index 76db0db35a7..ee665eaf346 100644 --- a/test/test_macros.jl +++ b/test/test_macros.jl @@ -89,7 +89,7 @@ end struct CustomType end -function JuMP.parse_constraint_head(_error::Function, ::Val{:(:=)}, lhs, rhs) +function JuMP.parse_constraint_head(_error::Function, ::Val{:≡}, lhs, rhs) return false, :(), :(build_constraint($_error, $(esc(lhs)), $(esc(rhs)))) end @@ -326,7 +326,7 @@ function test_extension_custom_expression_test( ) model = ModelType() @variable(model, x) - @constraint(model, con_ref, x := CustomType()) + @constraint(model, con_ref, x ≡ CustomType()) con = constraint_object(con_ref) @test jump_function(con) == x @test moi_set(con) isa CustomSet From 892bc1f5ffa8fd5e1845cda4a299632f7f0ae80e Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 29 Sep 2023 12:09:37 +1300 Subject: [PATCH 2/8] Update --- docs/src/developers/extensions.md | 8 ++++---- src/constraints.jl | 13 ++----------- test/test_macros.jl | 4 ++-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/src/developers/extensions.md b/docs/src/developers/extensions.md index cf65e9a8a92..d638b59d29b 100644 --- a/docs/src/developers/extensions.md +++ b/docs/src/developers/extensions.md @@ -200,11 +200,11 @@ julia> model = Model(); @variable(model, x); julia> function JuMP.parse_constraint_head( _error::Function, - ::Val{:≡}, + ::Val{:≝}, lhs, rhs, ) - println("Rewriting ≡ as ==") + println("Rewriting ≝ as ==") new_lhs, parse_code = MutableArithmetics.rewrite(lhs) build_code = :( build_constraint($(_error), $(new_lhs), MOI.EqualTo($(rhs))) @@ -212,8 +212,8 @@ julia> function JuMP.parse_constraint_head( return false, parse_code, build_code end -julia> @constraint(model, x + x ≡ 1.0) -Rewriting ≡ as == +julia> @constraint(model, x + x ≝ 1.0) +Rewriting ≝ as == 2 x = 1 ``` diff --git a/src/constraints.jl b/src/constraints.jl index aaca4009e38..6d4cb9e6c2e 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -1543,13 +1543,7 @@ function relax_with_penalty!( return relax_with_penalty!(model, Dict(); default = default) end - -function parse_constraint_head( - error_fn::Function, - ::Val{:(:=)}, - lhs, - rhs, -) +function parse_constraint_head(error_fn::Function, ::Val{:(:=)}, lhs, rhs) new_lhs, parse_code_lhs = _rewrite_expression(lhs) new_rhs, parse_code_rhs = _rewrite_expression(rhs) parse_code = quote @@ -1562,10 +1556,7 @@ function parse_constraint_head( elseif $new_lhs isa Bool ScalarConstraint($new_rhs, MOI.EqualTo($new_lhs)) else - ScalarConstraint( - op_equal_to($new_lhs, $new_rhs), - MOI.EqualTo(true), - ) + ScalarConstraint(op_equal_to($new_lhs, $new_rhs), MOI.EqualTo(true)) end end return false, parse_code, build_code diff --git a/test/test_macros.jl b/test/test_macros.jl index ee665eaf346..f2df0c24ed2 100644 --- a/test/test_macros.jl +++ b/test/test_macros.jl @@ -89,7 +89,7 @@ end struct CustomType end -function JuMP.parse_constraint_head(_error::Function, ::Val{:≡}, lhs, rhs) +function JuMP.parse_constraint_head(_error::Function, ::Val{:≝}, lhs, rhs) return false, :(), :(build_constraint($_error, $(esc(lhs)), $(esc(rhs)))) end @@ -326,7 +326,7 @@ function test_extension_custom_expression_test( ) model = ModelType() @variable(model, x) - @constraint(model, con_ref, x ≡ CustomType()) + @constraint(model, con_ref, x ≝ CustomType()) con = constraint_object(con_ref) @test jump_function(con) == x @test moi_set(con) isa CustomSet From ad43e49014b4eaebf8716bd2ea0d9b8725184819 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 29 Sep 2023 12:29:02 +1300 Subject: [PATCH 3/8] Apply suggestions from code review --- docs/src/developers/extensions.md | 8 ++++---- test/test_macros.jl | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/developers/extensions.md b/docs/src/developers/extensions.md index d638b59d29b..e2fbcd3fc26 100644 --- a/docs/src/developers/extensions.md +++ b/docs/src/developers/extensions.md @@ -200,11 +200,11 @@ julia> model = Model(); @variable(model, x); julia> function JuMP.parse_constraint_head( _error::Function, - ::Val{:≝}, + ::Val{:≔}, lhs, rhs, ) - println("Rewriting ≝ as ==") + println("Rewriting ≔ as ==") new_lhs, parse_code = MutableArithmetics.rewrite(lhs) build_code = :( build_constraint($(_error), $(new_lhs), MOI.EqualTo($(rhs))) @@ -212,8 +212,8 @@ julia> function JuMP.parse_constraint_head( return false, parse_code, build_code end -julia> @constraint(model, x + x ≝ 1.0) -Rewriting ≝ as == +julia> @constraint(model, x + x ≔ 1.0) +Rewriting ≔ as == 2 x = 1 ``` diff --git a/test/test_macros.jl b/test/test_macros.jl index f2df0c24ed2..49f2583bcd7 100644 --- a/test/test_macros.jl +++ b/test/test_macros.jl @@ -89,7 +89,7 @@ end struct CustomType end -function JuMP.parse_constraint_head(_error::Function, ::Val{:≝}, lhs, rhs) +function JuMP.parse_constraint_head(_error::Function, ::Val{:≔}, lhs, rhs) return false, :(), :(build_constraint($_error, $(esc(lhs)), $(esc(rhs)))) end @@ -326,7 +326,7 @@ function test_extension_custom_expression_test( ) model = ModelType() @variable(model, x) - @constraint(model, con_ref, x ≝ CustomType()) + @constraint(model, con_ref, x ≔ CustomType()) con = constraint_object(con_ref) @test jump_function(con) == x @test moi_set(con) isa CustomSet From 0bc09cfa195b31a82e89ef539cb64c6f20758476 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 11 Oct 2023 17:01:12 +0200 Subject: [PATCH 4/8] Replace with dispatch --- src/constraints.jl | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index 6d4cb9e6c2e..b42ea4495ca 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -1543,6 +1543,20 @@ function relax_with_penalty!( return relax_with_penalty!(model, Dict(); default = default) end +struct _BooleanEqualTo end + +function build_constraint(::Function, lhs, rhs, ::_BooleanEqualTo) + return ScalarConstraint(op_equal_to(lhs, rhs), MOI.EqualTo(true)) +end + +function build_constraint(::Function, lhs::Bool, rhs, ::_BooleanEqualTo) + return ScalarConstraint(rhs, MOI.EqualTo(lhs)) +end + +function build_constraint(::Function, lhs, rhs::Bool, ::_BooleanEqualTo) + return ScalarConstraint(lhs, MOI.EqualTo(rhs)) +end + function parse_constraint_head(error_fn::Function, ::Val{:(:=)}, lhs, rhs) new_lhs, parse_code_lhs = _rewrite_expression(lhs) new_rhs, parse_code_rhs = _rewrite_expression(rhs) @@ -1551,13 +1565,7 @@ function parse_constraint_head(error_fn::Function, ::Val{:(:=)}, lhs, rhs) $parse_code_rhs end build_code = quote - if $new_rhs isa Bool - ScalarConstraint($new_lhs, MOI.EqualTo($new_rhs)) - elseif $new_lhs isa Bool - ScalarConstraint($new_rhs, MOI.EqualTo($new_lhs)) - else - ScalarConstraint(op_equal_to($new_lhs, $new_rhs), MOI.EqualTo(true)) - end + build_constraint($error_fn, $new_lhs, $new_rhs, _BooleanEqualTo()) end return false, parse_code, build_code end From 41990246400361ccb3ffa9661a19d30031900a20 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 12 Oct 2023 12:06:09 +0200 Subject: [PATCH 5/8] Refactor --- src/constraints.jl | 31 +++++++++++++++++++++---------- src/macros.jl | 4 ---- test/test_constraint.jl | 12 ++++++------ 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index b42ea4495ca..5db80f7ca12 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -1543,18 +1543,31 @@ function relax_with_penalty!( return relax_with_penalty!(model, Dict(); default = default) end -struct _BooleanEqualTo end +struct _DoNotConvertSet{S} <: MOI.AbstractScalarSet + set::S +end + +model_convert(::AbstractModel, set::_DoNotConvertSet) = set + +function moi_set(constraint::ScalarConstraint{F,<:_DoNotConvertSet}) where {F} + return constraint.set.set +end + +function _build_boolean_equal_to(::Function, lhs, rhs) + set = _DoNotConvertSet(MOI.EqualTo(true)) + return ScalarConstraint(op_equal_to(lhs, rhs), set) +end -function build_constraint(::Function, lhs, rhs, ::_BooleanEqualTo) - return ScalarConstraint(op_equal_to(lhs, rhs), MOI.EqualTo(true)) +function _build_boolean_equal_to(::Function, lhs::Bool, rhs) + return ScalarConstraint(rhs, _DoNotConvertSet(MOI.EqualTo(lhs))) end -function build_constraint(::Function, lhs::Bool, rhs, ::_BooleanEqualTo) - return ScalarConstraint(rhs, MOI.EqualTo(lhs)) +function _build_boolean_equal_to(::Function, lhs, rhs::Bool) + return ScalarConstraint(lhs, _DoNotConvertSet(MOI.EqualTo(rhs))) end -function build_constraint(::Function, lhs, rhs::Bool, ::_BooleanEqualTo) - return ScalarConstraint(lhs, MOI.EqualTo(rhs)) +function _build_boolean_equal_to(error_fn::Function, lhs::Bool, rhs::Bool) + return error_fn("cannot add the trivial constraint `$lhs := $rhs`") end function parse_constraint_head(error_fn::Function, ::Val{:(:=)}, lhs, rhs) @@ -1564,8 +1577,6 @@ function parse_constraint_head(error_fn::Function, ::Val{:(:=)}, lhs, rhs) $parse_code_lhs $parse_code_rhs end - build_code = quote - build_constraint($error_fn, $new_lhs, $new_rhs, _BooleanEqualTo()) - end + build_code = :(_build_boolean_equal_to($error_fn, $new_lhs, $new_rhs)) return false, parse_code, build_code end diff --git a/src/macros.jl b/src/macros.jl index 2a5127bfc34..9cb281d0315 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -1160,10 +1160,6 @@ function model_convert(model::AbstractModel, set::MOI.AbstractScalarSet) return set end -function model_convert(model::GenericModel{Bool}, set::MOI.EqualTo{Bool}) - return set -end - function model_convert(model::AbstractModel, α::Number) T = value_type(typeof(model)) V = variable_ref_type(model) diff --git a/test/test_constraint.jl b/test/test_constraint.jl index 98a0ce4e3b0..02e5dd009e8 100644 --- a/test/test_constraint.jl +++ b/test/test_constraint.jl @@ -1766,34 +1766,34 @@ function test_def_equal_to_operator_float() c = @constraint(model, x[1] := x[2]) o = constraint_object(c) @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) - @test o.set == MOI.EqualTo(1.0) + @test o.set == MOI.EqualTo(true) # x[1] == x[2] := false c = @constraint(model, x[1] == x[2] := false) o = constraint_object(c) @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) - @test o.set == MOI.EqualTo(0.0) + @test o.set == MOI.EqualTo(false) # x[1] && x[2] := false c = @constraint(model, x[1] && x[2] := false) o = constraint_object(c) @test isequal_canonical(o.func, op_and(x[1], x[2])) - @test o.set == MOI.EqualTo(0.0) + @test o.set == MOI.EqualTo(false) # x[1] && x[2] := true c = @constraint(model, x[1] && x[2] := true) o = constraint_object(c) @test isequal_canonical(o.func, op_and(x[1], x[2])) - @test o.set == MOI.EqualTo(1.0) + @test o.set == MOI.EqualTo(true) # x[1] || x[2] := y y = true c = @constraint(model, x[1] || x[2] := y) o = constraint_object(c) @test isequal_canonical(o.func, op_or(x[1], x[2])) - @test o.set == MOI.EqualTo(1.0) + @test o.set == MOI.EqualTo(true) # y := x[1] || x[2] y = true c = @constraint(model, y := x[1] || x[2]) o = constraint_object(c) @test isequal_canonical(o.func, op_or(x[1], x[2])) - @test o.set == MOI.EqualTo(1.0) + @test o.set == MOI.EqualTo(true) return end From 9a93f0e9d789ce49d29cdd5b05284a0db8f2ba50 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 12 Oct 2023 15:15:03 +0200 Subject: [PATCH 6/8] Restrict rhs to Bool --- src/constraints.jl | 25 +++++++++---------- test/test_constraint.jl | 54 ++++++++--------------------------------- 2 files changed, 21 insertions(+), 58 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index 5db80f7ca12..dcb76d0d1c1 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -1549,25 +1549,22 @@ end model_convert(::AbstractModel, set::_DoNotConvertSet) = set -function moi_set(constraint::ScalarConstraint{F,<:_DoNotConvertSet}) where {F} - return constraint.set.set -end - -function _build_boolean_equal_to(::Function, lhs, rhs) - set = _DoNotConvertSet(MOI.EqualTo(true)) - return ScalarConstraint(op_equal_to(lhs, rhs), set) -end +moi_set(c::ScalarConstraint{F,<:_DoNotConvertSet}) where {F} = c.set.set -function _build_boolean_equal_to(::Function, lhs::Bool, rhs) - return ScalarConstraint(rhs, _DoNotConvertSet(MOI.EqualTo(lhs))) +function _build_boolean_equal_to(::Function, lhs::AbstractJuMPScalar, rhs::Bool) + return ScalarConstraint(lhs, _DoNotConvertSet(MOI.EqualTo(rhs))) end -function _build_boolean_equal_to(::Function, lhs, rhs::Bool) - return ScalarConstraint(lhs, _DoNotConvertSet(MOI.EqualTo(rhs))) +function _build_boolean_equal_to(error_fn::Function, ::AbstractJuMPScalar, rhs) + return error_fn( + "cannot add the `:=` constraint. The right-hand side must be a `Bool`", + ) end -function _build_boolean_equal_to(error_fn::Function, lhs::Bool, rhs::Bool) - return error_fn("cannot add the trivial constraint `$lhs := $rhs`") +function _build_boolean_equal_to(error_fn::Function, lhs, ::Any) + return error_fn( + "cannot add the `:=` constraint with left-hand side of type `::$(typeof(lhs))`", + ) end function parse_constraint_head(error_fn::Function, ::Val{:(:=)}, lhs, rhs) diff --git a/test/test_constraint.jl b/test/test_constraint.jl index 02e5dd009e8..de98fd8b331 100644 --- a/test/test_constraint.jl +++ b/test/test_constraint.jl @@ -1721,14 +1721,11 @@ function test_triangle_vec() return end -function test_def_equal_to_operator() - model = GenericModel{Bool}() +function _test_def_equal_to_operator_T(::Type{T}) where {T} + model = GenericModel{T}() @variable(model, x[1:3]) # x[1] := x[2] - c = @constraint(model, x[1] := x[2]) - o = constraint_object(c) - @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) - @test o.set == MOI.EqualTo(true) + @test_throws ErrorException @constraint(model, x[1] := x[2]) # x[1] == x[2] := false c = @constraint(model, x[1] == x[2] := false) o = constraint_object(c) @@ -1752,48 +1749,17 @@ function test_def_equal_to_operator() @test o.set == MOI.EqualTo(y) # y := x[1] || x[2] y = true - c = @constraint(model, y := x[1] || x[2]) - o = constraint_object(c) - @test isequal_canonical(o.func, op_or(x[1], x[2])) - @test o.set == MOI.EqualTo(y) + @test_throws ErrorException @constraint(model, y := x[1] || x[2]) return end function test_def_equal_to_operator_float() - model = Model() - @variable(model, x[1:3]) - # x[1] := x[2] - c = @constraint(model, x[1] := x[2]) - o = constraint_object(c) - @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) - @test o.set == MOI.EqualTo(true) - # x[1] == x[2] := false - c = @constraint(model, x[1] == x[2] := false) - o = constraint_object(c) - @test isequal_canonical(o.func, op_equal_to(x[1], x[2])) - @test o.set == MOI.EqualTo(false) - # x[1] && x[2] := false - c = @constraint(model, x[1] && x[2] := false) - o = constraint_object(c) - @test isequal_canonical(o.func, op_and(x[1], x[2])) - @test o.set == MOI.EqualTo(false) - # x[1] && x[2] := true - c = @constraint(model, x[1] && x[2] := true) - o = constraint_object(c) - @test isequal_canonical(o.func, op_and(x[1], x[2])) - @test o.set == MOI.EqualTo(true) - # x[1] || x[2] := y - y = true - c = @constraint(model, x[1] || x[2] := y) - o = constraint_object(c) - @test isequal_canonical(o.func, op_or(x[1], x[2])) - @test o.set == MOI.EqualTo(true) - # y := x[1] || x[2] - y = true - c = @constraint(model, y := x[1] || x[2]) - o = constraint_object(c) - @test isequal_canonical(o.func, op_or(x[1], x[2])) - @test o.set == MOI.EqualTo(true) + _test_def_equal_to_operator_T(Float64) + return +end + +function test_def_equal_to_operator_bool() + _test_def_equal_to_operator_T(Bool) return end From 0f485162ab57416dcbb1201cf6423c7d62c89aa3 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 12 Oct 2023 17:08:56 +0200 Subject: [PATCH 7/8] Add docs --- docs/src/manual/constraints.md | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/src/manual/constraints.md b/docs/src/manual/constraints.md index 10f6add36a1..68fdb960345 100644 --- a/docs/src/manual/constraints.md +++ b/docs/src/manual/constraints.md @@ -1534,3 +1534,50 @@ julia> q = [5, 6] julia> @constraint(model, M * y + q ⟂ y) [y[1] + 2 y[2] + 5, 3 y[1] + 4 y[2] + 6, y[1], y[2]] ∈ MathOptInterface.Complements(4) ``` + +## Boolean constraints + +Add a Boolean constraint (a [`MOI.EqualTo{Bool}`](@ref) set) using the `:=` +operator with a `Bool` right-hand side term: + +```jldoctest +julia> model = GenericModel{Bool}(); + +julia> @variable(model, x[1:2]); + +julia> @constraint(model, x[1] || x[2] := true) +x[1] || x[2] = true + +julia> @constraint(model, x[1] && x[2] := false) +x[1] && x[2] = false + +julia> model +A JuMP Model +Feasibility problem with: +Variables: 2 +`GenericNonlinearExpr{GenericVariableRef{Bool}}`-in-`MathOptInterface.EqualTo{Bool}`: 2 constraints +Model mode: AUTOMATIC +CachingOptimizer state: NO_OPTIMIZER +Solver name: No optimizer attached. +Names registered in the model: x +``` + +Boolean constraints should not be added using the `==` operator because JuMP +will rewrite the constraint as `lhs - rhs = 0`, and because constraints like +`a == b == c` require parentheses to diambiguate between `(a == b) == c` and +`a == (b == c)`. In constrast, `a == b := c` is equivalent to `(a == b) := c`: + +```jldoctest +julia> model = Model(); + +julia> @variable(model, x[1:2]); + +julia> rhs = false +false + +julia> @constraint(model, (x[1] == x[2]) == rhs) +(x[1] == x[2]) - 0.0 = 0 + +julia> @constraint(model, x[1] == x[2] := rhs) +x[1] == x[2] = false +``` From fa07a0f3f6ca83a5adfd8f3ccbb70c8c2a24266d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 13 Oct 2023 09:20:12 +0200 Subject: [PATCH 8/8] Apply suggestions from code review --- docs/src/manual/constraints.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/manual/constraints.md b/docs/src/manual/constraints.md index 68fdb960345..40df7c8dc1e 100644 --- a/docs/src/manual/constraints.md +++ b/docs/src/manual/constraints.md @@ -1564,8 +1564,8 @@ Names registered in the model: x Boolean constraints should not be added using the `==` operator because JuMP will rewrite the constraint as `lhs - rhs = 0`, and because constraints like -`a == b == c` require parentheses to diambiguate between `(a == b) == c` and -`a == (b == c)`. In constrast, `a == b := c` is equivalent to `(a == b) := c`: +`a == b == c` require parentheses to disambiguate between `(a == b) == c` and +`a == (b == c)`. In contrast, `a == b := c` is equivalent to `(a == b) := c`: ```jldoctest julia> model = Model();