Skip to content

Commit

Permalink
[WIP] Handle != constraint via MOI.AllDifferent(2)
Browse files Browse the repository at this point in the history
```
(@v1.10) pkg> activate /repositories/JuMP.jl
  Activating project at `/repositories/JuMP.jl`

julia> using JuMP
Precompiling JuMP
  1 dependency successfully precompiled in 10 seconds. 37 already precompiled.

julia> model = Model();

julia> @variable(model, x[1:3])
3-element Vector{VariableRef}:
 x[1]
 x[2]
 x[3]

julia> @variable(model, y[1:3])
3-element Vector{VariableRef}:
 y[1]
 y[2]
 y[3]

julia> @constraint(model, x[1] != y[1])
x[1] != y[1]

julia> @constraint(model, x .!= y)
3-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.VectorOfVariables, MathOptInterface.AllDifferent}, VectorShape}}:
 x[1] != y[1]
 x[2] != y[2]
 x[3] != y[3]

julia> @constraint(model, x != y)
ERROR: At REPL[8]:1: `@constraint(model, x != y)`: Ineqality operator with vector operands must be explicitly vectorized, use `.!=` instead of `!=`.
Stacktrace:
 [1] error(::String, ::String)
   @ Base ./error.jl:44
 [2] (::JuMP.Containers.var"#error_fn#98"{String})(str::String)
   @ JuMP.Containers ~/.julia/compiled/v1.10/JuMP/DmXqY_F8XkK.so:-1
 [3] macro expansion
   @ /repositories/JuMP.jl/src/macros/@constraint.jl:132 [inlined]
 [4] macro expansion
   @ /repositories/JuMP.jl/src/macros.jl:393 [inlined]
 [5] top-level scope
   @ REPL[8]:1
```

I'm not yet sure how to support the not-explicitly vectorized case.
We'd need to somehow deduce (in `parse_constraint_call()`)
that our arguments are vectors, and extend `parse_constraint_call()`
to return `vectorized` itself. I'm not convinced this is even possible.

Otherwise, we get
```
julia> @constraint(model, x != y)
vectorized = false
ERROR: MethodError: no method matching _build_inequality_constraint(::Bool, ::JuMP.Containers.var"#error_fn#98"{String}, ::Vector{VariableRef}, ::Vector{VariableRef})

Closest candidates are:
  _build_inequality_constraint(::Function, ::Bool, ::Vector{VariableRef}, ::Vector{VariableRef})
   @ JuMP /repositories/JuMP.jl/src/inequality.jl:14

Stacktrace:
 [1] macro expansion
   @ /repositories/JuMP.jl/src/macros/@constraint.jl:132 [inlined]
 [2] macro expansion
   @ /repositories/JuMP.jl/src/macros.jl:393 [inlined]
 [3] top-level scope
   @ REPL[8]:1
```
(because we should have called `@constraint.jl:123`)

Missing tests, docs.

As discussed in jump-dev/MathOptInterface.jl#2405
  • Loading branch information
LebedevRI committed Jan 19, 2024
1 parent a55edea commit 73f0c9c
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/JuMP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,7 @@ include("operators.jl")
include("sd.jl")
include("sets.jl")
include("solution_summary.jl")
include("inequality.jl")

# print.jl must come last, because it uses types defined in earlier files.
include("print.jl")
Expand Down
78 changes: 78 additions & 0 deletions src/inequality.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

function _build_inequality_constraint(
error_fn::Function,
vectorized::Bool,
lhs::VariableRef,
rhs::VariableRef,
)
@assert !vectorized
set = MOI.AllDifferent(2)
return VectorConstraint([lhs; rhs], set)
end

function _build_inequality_constraint(
error_fn::Function,
vectorized::Bool,
lhs::Vector{VariableRef},
rhs::Vector{VariableRef},
)
if !vectorized
error_fn(
"Ineqality operator with vector operands must be explicitly " *
"vectorized, use `.!=` instead of `!=`.",
)
end
if length(lhs) != length(rhs)
error_fn("Operand length mismatch, $(length(lhs)) vs $(length(rhs)).")
end
lhs = _desparsify(lhs)
rhs = _desparsify(rhs)
return _build_inequality_constraint.(error_fn, false, lhs, rhs)
end

function _build_inequality_constraint(error_fn::Function, ::Bool, lhs, rhs)
return error_fn(
"Unsupported form of inequality constraint. The left- and right-hand " *
"sides must both be decision variables.",
)
end

function parse_constraint_call(
error_fn::Function,
vectorized::Bool,
::Val{:(!=)},
lhs,
rhs,
)
build_call = Expr(
:call,
:_build_inequality_constraint,
error_fn,
vectorized,
esc(lhs),
esc(rhs),
)
return nothing, build_call
end

function constraint_string(
print_mode,
constraint::VectorConstraint{F,<:MOI.AllDifferent},
) where {F}
set = constraint.set
if set.dimension == 2
ineq_sym = JuMP._math_symbol(print_mode, :(!=))
lhs = function_string(print_mode, constraint.func[1])
rhs = function_string(print_mode, constraint.func[2])
return string(lhs, " $ineq_sym ", rhs)
end

# FIXME: can we just fallback to the generic handling here?
ops = [function_string(print_mode, op) for op in constraint.func[1:end]]
in_sym = JuMP._math_symbol(print_mode, :in)
return string("[", join(ops, ", "), "] $in_sym $set")
end
7 changes: 4 additions & 3 deletions src/macros/@constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The expression `expr` may be one of following forms:
which is either a [`MOI.AbstractSet`](@ref) or one of the JuMP shortcuts like
[`SecondOrderCone`](@ref) or [`PSDCone`](@ref)
* `a <op> b`, where `<op>` is one of `==`, `≥`, `>=`, `≤`, `<=`
* `a <op> b`, where `<op>` is one of `==`, `!=`, `≥`, `>=`, `≤`, `<=`
* `l <= f <= u` or `u >= f >= l`, constraining the expression `f` to lie
between `l` and `u`
Expand Down Expand Up @@ -233,6 +233,7 @@ The entry-point for all constraint-related parsing.
JuMP currently supports the following `expr` objects:
* `lhs <= rhs`
* `lhs == rhs`
* `lhs != rhs`
* `lhs >= rhs`
* `l <= body <= u`
* `u >= body >= l`
Expand All @@ -259,7 +260,7 @@ end
function parse_constraint(error_fn::Function, arg)
return error_fn(
"Incomplete constraint specification $arg. Are you missing a " *
"comparison (<=, >=, or ==)?",
"comparison (<=, >=, == or !=)?",
)
end

Expand Down Expand Up @@ -591,7 +592,7 @@ julia> @constraint(model, A * x == b)
"""
struct Zeros end

operator_to_set(::Function, ::Val{:(==)}) = Zeros()
operator_to_set(::Function, ::Union{Val{:(==)},Val{:(!=)}}) = Zeros()

"""
parse_constraint_call(
Expand Down
4 changes: 4 additions & 0 deletions src/print.jl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ function _math_symbol(::MIME"text/plain", name::Symbol)
return Sys.iswindows() ? ">=" : ""
elseif name == :eq
return Sys.iswindows() ? "==" : "="
elseif name == :(!=)
return Sys.iswindows() ? "!=" : ""
elseif name == :sq
return "²"
else
Expand All @@ -160,6 +162,8 @@ function _math_symbol(::MIME"text/latex", name::Symbol)
return "\\geq"
elseif name == :eq
return "="
elseif name == :(!=)
return "\\neq"
else
@assert name == :sq
return "^2"
Expand Down
212 changes: 212 additions & 0 deletions test/test_inequality.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

module TestInequality

using JuMP
using Test

include(joinpath(@__DIR__, "utilities.jl"))

function test_inequality_two_int_scalars()
model = Model()
@variable(model, -4 <= x <= 4, Int)
@variable(model, -4 <= y <= 4, Int)
c = @constraint(model, x != y)
ineq_sym = JuMP._math_symbol(MIME("text/plain"), :(!=))
set = MOI.AllDifferent(2)
@test sprint(show, c) == "x $ineq_sym y"
obj = constraint_object(c)
@test obj.func == [x; y]
@test obj.set == set
return
end

function test_inequality_latex()
model = Model()
@variable(model, -4 <= x <= 4, Int)
@variable(model, -4 <= y <= 4, Int)
c = @constraint(model, x != y)
set = MOI.AllDifferent(2)
@test sprint(io -> show(io, MIME("text/latex"), c)) == "\$\$ x \\neq y \$\$"
obj = constraint_object(c)
@test obj.func == [x; y]
@test obj.set == set
return
end

function test_inequality_two_bin_scalars()
model = Model()
@variable(model, x, Bin)
@variable(model, y, Bin)
c = @constraint(model, x != y)
ineq_sym = JuMP._math_symbol(MIME("text/plain"), :(!=))
set = MOI.AllDifferent(2)
@test sprint(show, c) == "x $ineq_sym y"
obj = constraint_object(c)
@test obj.func == [x; y]
@test obj.set == set
return
end

# FIXME: should this fail?
function test_inequality_two_scalars_only_one_being_int()
model = Model()
@variable(model, -4 <= x <= 4)
@variable(model, -4 <= y <= 4, Int)
c = @constraint(model, x != y)
ineq_sym = JuMP._math_symbol(MIME("text/plain"), :(!=))
set = MOI.AllDifferent(2)
@test sprint(show, c) == "x $ineq_sym y"
obj = constraint_object(c)
@test obj.func == [x; y]
@test obj.set == set
return
end

# FIXME: should this fail?
function test_inequality_two_scalars_only_one_being_bin()
model = Model()
@variable(model, -4 <= x <= 4)
@variable(model, y, Bin)
c = @constraint(model, x != y)
ineq_sym = JuMP._math_symbol(MIME("text/plain"), :(!=))
set = MOI.AllDifferent(2)
@test sprint(show, c) == "x $ineq_sym y"
obj = constraint_object(c)
@test obj.func == [x; y]
@test obj.set == set
return
end

# FIXME: should this fail?
function test_inequality_two_scalars_int_vs_bin()
model = Model()
@variable(model, -4 <= x <= 4, Int)
@variable(model, y, Bin)
c = @constraint(model, x != y)
ineq_sym = JuMP._math_symbol(MIME("text/plain"), :(!=))
set = MOI.AllDifferent(2)
@test sprint(show, c) == "x $ineq_sym y"
obj = constraint_object(c)
@test obj.func == [x; y]
@test obj.set == set
return
end

# FIXME: should this fail?
function test_inequality_two_scalars_real_scalars()
model = Model()
@variable(model, -4 <= x <= 4)
@variable(model, -4 <= y <= 4)
c = @constraint(model, x != y)
ineq_sym = JuMP._math_symbol(MIME("text/plain"), :(!=))
set = MOI.AllDifferent(2)
@test sprint(show, c) == "x $ineq_sym y"
obj = constraint_object(c)
@test obj.func == [x; y]
@test obj.set == set
return
end

function test_inequality_two_vectors_vectorized()
model = Model()
@variable(model, -4 <= x[1:3] <= 4, Int)
@variable(model, -4 <= y[1:3] <= 4, Int)
c = @constraint(model, x .!= y)
ineq_sym = JuMP._math_symbol(MIME("text/plain"), :(!=))
set = MOI.AllDifferent(2)
for (i, ci) in enumerate(c)
@test sprint(show, ci) == "x[$i] $ineq_sym y[$i]"
obj = constraint_object(ci)
@test obj.func == [x[i]; y[i]]
@test obj.set == set
end
return
end

function test_inequality_two_vectors_nonvectorized()
model = Model()
@variable(model, -4 <= x[1:3] <= 4, Int)
@variable(model, -4 <= y[1:3] <= 4, Int)
@test_throws_runtime(
ErrorException(
"In `@constraint(model, x != y)`: Ineqality operator with " *
"vector operands must be explicitly vectorized, " *
"use `.!=` instead of `!=`.",
),
@constraint(model, x != y)
)
return
end

function test_inequality_two_vectors_nonvectorized_len_mismatch()
model = Model()
@variable(model, -4 <= x[1:3] <= 4, Int)
@variable(model, -4 <= y[1:2] <= 4, Int)
@test_throws_runtime(
ErrorException(
"In `@constraint(model, x != y)`: Ineqality operator with " *
"vector operands must be explicitly vectorized, " *
"use `.!=` instead of `!=`.",
),
@constraint(model, x != y)
)
return
end

function test_inequality_two_vectors_vectorized_len_mismatch()
model = Model()
@variable(model, -4 <= x[1:3] <= 4, Int)
@variable(model, -4 <= y[1:2] <= 4, Int)
@test_throws_runtime(
ErrorException(
"In `@constraint(model, x .!= y)`: " *
"Operand length mismatch, 3 vs 2.",
),
@constraint(model, x .!= y)
)
return
end

function test_inequality_non_variables()
model = Model()
@variable(model, -4 <= x <= 4, Int)
@test_throws_runtime(
ErrorException(
"In `@constraint(model, x != 0)`: Unsupported form of " *
"inequality constraint. The left- and right-hand sides must both " *
"be decision variables.",
),
@constraint(model, x != 0)
)
@test_throws_runtime(
ErrorException(
"In `@constraint(model, 0 != x)`: Unsupported form of " *
"inequality constraint. The left- and right-hand sides must both " *
"be decision variables.",
),
@constraint(model, 0 != x)
)
@test_throws_runtime(
ErrorException(
"In `@constraint(model, 2x != 0)`: Unsupported form of " *
"inequality constraint. The left- and right-hand sides must both " *
"be decision variables.",
),
@constraint(model, 2 * x != 0)
)
@test_throws_runtime(
ErrorException(
"In `@constraint(model, x != 2x)`: Unsupported form of " *
"inequality constraint. The left- and right-hand sides must both " *
"be decision variables.",
),
@constraint(model, x != 2 * x)
)
return
end

end # module
2 changes: 1 addition & 1 deletion test/test_macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1731,7 +1731,7 @@ function test_build_constraint_invalid()
@test_throws_parsetime(
ErrorException(
"In `@build_constraint(x)`: Incomplete constraint specification " *
"x. Are you missing a comparison (<=, >=, or ==)?",
"x. Are you missing a comparison (<=, >=, == or !=)?",
),
@build_constraint(x),
)
Expand Down

0 comments on commit 73f0c9c

Please sign in to comment.