Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Handle != constraint via MOI.AllDifferent(2) #3656

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading