Skip to content

Commit

Permalink
Improve matrix inequality support (#3778)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Aug 6, 2024
1 parent 95da051 commit 88738c4
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 61 deletions.
33 changes: 26 additions & 7 deletions docs/src/manual/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ julia> b = [5, 6]
6
julia> @constraint(model, con_vector, A * x == b)
con_vector : [x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ MathOptInterface.Zeros(2)
con_vector : [x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Zeros()
julia> @constraint(model, con_scalar, A * x .== b)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.EqualTo{Float64}}, ScalarShape}}:
Expand Down Expand Up @@ -148,15 +148,15 @@ constraint.

```jldoctest con_vector
julia> @constraint(model, A * x <= b)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ MathOptInterface.Nonpositives(2)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Nonpositives()
julia> @constraint(model, A * x .<= b)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
x[1] + 2 x[2] ≤ 5
3 x[1] + 4 x[2] ≤ 6
julia> @constraint(model, A * x >= b)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ MathOptInterface.Nonnegatives(2)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Nonnegatives()
julia> @constraint(model, A * x .>= b)
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.GreaterThan{Float64}}, ScalarShape}}:
Expand All @@ -169,18 +169,37 @@ julia> @constraint(model, A * x .>= b)
Inequalities between matrices are not supported, due to the common ambiguity
between elementwise inequalities and a [`PSDCone`](@ref) constraint.

```jldoctest symmetric_matrix
````jldoctest symmetric_matrix
julia> model = Model();
julia> @variable(model, x[1:2, 1:2], Symmetric);
julia> @variable(model, y[1:2, 1:2], Symmetric);
julia> @constraint(model, x >= y)
ERROR: At none:1: `@constraint(model, x >= y)`: Unsupported matrix in vector-valued set. Did you mean to use the broadcasting syntax `.>=` instead? Alternatively, perhaps you are missing a set argument like `@constraint(model, X >= 0, PSDCone())` or `@constraint(model, X >= 0, HermitianPSDCone())`.
ERROR: At none:1: `@constraint(model, x >= y)`:
The syntax `x >= y` is ambiguous for matrices because we cannot tell if
you intend a positive semidefinite constraint or an elementwise
inequality.
To create a positive semidefinite constraint, pass `PSDCone()` or
`HermitianPSDCone()`:
```julia
@constraint(model, x >= y, PSDCone())
```
To create an element-wise inequality, pass `Nonnegatives()`, or use
broadcasting:
```julia
@constraint(model, x >= y, Nonnegatives())
# or
@constraint(model, x .>= y)
```
Stacktrace:
[...]
```
````

Instead, use the [Set inequality syntax](@ref) to specify a set like
[`PSDCone`](@ref) or [`Nonnegatives`](@ref):
Expand Down Expand Up @@ -1257,7 +1276,7 @@ julia> @constraint(model, x in MOI.ExponentialCone())
## Set inequality syntax

For modeling convenience, the syntax `@constraint(model, x >= y, Set())` is
short-hand for `@constraint(model, x - y in Set())`.
short-hand for `@constraint(model, x - y in Set())`.

Therefore, the following calls are equivalent:
```jldoctest set_inequality
Expand Down
14 changes: 7 additions & 7 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ julia> model = Model();
julia> @variable(model, x, start = 2.0);
julia> @constraint(model, c, [2x] in Nonnegatives())
c : [2 x] ∈ MathOptInterface.Nonnegatives(1)
c : [2 x] ∈ Nonnegatives()
julia> set_dual_start_value(c, [0.0])
Expand Down Expand Up @@ -174,7 +174,7 @@ julia> model = Model();
julia> @variable(model, x, start = 2.0);
julia> @constraint(model, c, [2x] in Nonnegatives())
c : [2 x] ∈ MathOptInterface.Nonnegatives(1)
c : [2 x] ∈ Nonnegatives()
julia> set_dual_start_value(c, [0.0])
Expand Down Expand Up @@ -253,7 +253,7 @@ julia> model = Model();
julia> @variable(model, x, start = 2.0);
julia> @constraint(model, c, [2x] in Nonnegatives())
c : [2 x] ∈ MathOptInterface.Nonnegatives(1)
c : [2 x] ∈ Nonnegatives()
julia> set_start_value(c, [4.0])
Expand Down Expand Up @@ -333,7 +333,7 @@ julia> model = Model();
julia> @variable(model, x, start = 2.0);
julia> @constraint(model, c, [2x] in Nonnegatives())
c : [2 x] ∈ MathOptInterface.Nonnegatives(1)
c : [2 x] ∈ Nonnegatives()
julia> set_start_value(c, [4.0])
Expand Down Expand Up @@ -368,7 +368,7 @@ julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, c, [2x] in Nonnegatives())
c : [2 x] ∈ MathOptInterface.Nonnegatives(1)
c : [2 x] ∈ Nonnegatives()
julia> name(c)
"c"
Expand Down Expand Up @@ -404,15 +404,15 @@ julia> model = Model();
julia> @variable(model, x);
julia> @constraint(model, c, [2x] in Nonnegatives())
c : [2 x] ∈ MathOptInterface.Nonnegatives(1)
c : [2 x] ∈ Nonnegatives()
julia> set_name(c, "my_constraint")
julia> name(c)
"my_constraint"
julia> c
my_constraint : [2 x] ∈ MathOptInterface.Nonnegatives(1)
my_constraint : [2 x] ∈ Nonnegatives()
```
"""
function set_name(
Expand Down
138 changes: 130 additions & 8 deletions src/macros/@constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -587,19 +587,40 @@ julia> @variable(model, x[1:2])
x[2]
julia> @constraint(model, x in Nonnegatives())
[x[1], x[2]] ∈ MathOptInterface.Nonnegatives(2)
[x[1], x[2]] ∈ Nonnegatives()
julia> A = [1 2; 3 4];
julia> b = [5, 6];
julia> @constraint(model, A * x >= b)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ MathOptInterface.Nonnegatives(2)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Nonnegatives()
```
"""
struct Nonnegatives end

operator_to_set(::Function, ::Union{Val{:(>=)},Val{:(≥)}}) = Nonnegatives()
"""
GreaterThanZero()
A struct used to intercept when `>=` or `≥` is used in a macro via
[`operator_to_set`](@ref).
This struct is not the same as [`Nonnegatives`](@ref) so that we can disambiguate
`x >= y` and `x - y in Nonnegatives()`.
This struct is not intended for general usage, but it may be useful to some
JuMP extensions.
## Example
```jldoctest
julia> operator_to_set(error, Val(:>=))
GreaterThanZero()
```
"""
struct GreaterThanZero end

operator_to_set(::Function, ::Union{Val{:(>=)},Val{:(≥)}}) = GreaterThanZero()

"""
Nonpositives()
Expand All @@ -618,19 +639,40 @@ julia> @variable(model, x[1:2])
x[2]
julia> @constraint(model, x in Nonpositives())
[x[1], x[2]] ∈ MathOptInterface.Nonpositives(2)
[x[1], x[2]] ∈ Nonpositives()
julia> A = [1 2; 3 4];
julia> b = [5, 6];
julia> @constraint(model, A * x <= b)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ MathOptInterface.Nonpositives(2)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Nonpositives()
```
"""
struct Nonpositives end

operator_to_set(::Function, ::Union{Val{:(<=)},Val{:(≤)}}) = Nonpositives()
"""
GreaterThanZero()
A struct used to intercept when `<=` or `≤` is used in a macro via
[`operator_to_set`](@ref).
This struct is not the same as [`Nonpositives`](@ref) so that we can disambiguate
`x <= y` and `x - y in Nonpositives()`.
This struct is not intended for general usage, but it may be useful to some
JuMP extensions.
## Example
```jldoctest
julia> operator_to_set(error, Val(:<=))
LessThanZero()
```
"""
struct LessThanZero end

operator_to_set(::Function, ::Union{Val{:(<=)},Val{:(≤)}}) = LessThanZero()

"""
Zeros()
Expand All @@ -649,14 +691,14 @@ julia> @variable(model, x[1:2])
x[2]
julia> @constraint(model, x in Zeros())
[x[1], x[2]] ∈ MathOptInterface.Zeros(2)
[x[1], x[2]] ∈ Zeros()
julia> A = [1 2; 3 4];
julia> b = [5, 6];
julia> @constraint(model, A * x == b)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ MathOptInterface.Zeros(2)
[x[1] + 2 x[2] - 5, 3 x[1] + 4 x[2] - 6] ∈ Zeros()
```
"""
struct Zeros end
Expand Down Expand Up @@ -792,6 +834,86 @@ function parse_constraint_call(
return parse_code, build_call
end

function build_constraint(
error_fn::Function,
f,
::GreaterThanZero,
args...;
kwargs...,
)
return build_constraint(error_fn, f, Nonnegatives(), args...; kwargs...)
end

function build_constraint(
error_fn::Function,
::Union{Matrix,LinearAlgebra.Symmetric,LinearAlgebra.Hermitian},
::GreaterThanZero,
)
return error_fn(
"""
The syntax `x >= y` is ambiguous for matrices because we cannot tell if
you intend a positive semidefinite constraint or an elementwise
inequality.
To create a positive semidefinite constraint, pass `PSDCone()` or
`HermitianPSDCone()`:
```julia
@constraint(model, x >= y, PSDCone())
```
To create an element-wise inequality, pass `Nonnegatives()`, or use
broadcasting:
```julia
@constraint(model, x >= y, Nonnegatives())
# or
@constraint(model, x .>= y)
```""",
)
end

function build_constraint(
error_fn::Function,
f,
::LessThanZero,
args...;
kwargs...,
)
return build_constraint(error_fn, f, Nonpositives(), args...; kwargs...)
end

function build_constraint(
error_fn::Function,
::Union{Matrix,LinearAlgebra.Symmetric,LinearAlgebra.Hermitian},
::LessThanZero,
)
return error_fn(
"""
The syntax `x <= y` is ambiguous for matrices because we cannot tell if
you intend a positive semidefinite constraint or an elementwise
inequality.
To create a positive semidefinite constraint, reverse the sense of the
inequality and pass `PSDCone()` or `HermitianPSDCone()`:
```julia
@constraint(model, y >= x, PSDCone())
```
To create an element-wise inequality, reverse the sense of the
inequality and pass `Nonnegatives()`, or use broadcasting:
```julia
@constraint(model, y >= x, Nonnegatives())
# or
@constraint(model, x .<= y)
```""",
)
end

function build_constraint(
error_fn::Function,
f,
Expand Down
Loading

0 comments on commit 88738c4

Please sign in to comment.