From 88738c441c0d405574301f0768eec25bdb5b0f18 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 6 Aug 2024 18:56:47 +1200 Subject: [PATCH] Improve matrix inequality support (#3778) --- docs/src/manual/constraints.md | 33 ++++++-- src/constraints.jl | 14 ++-- src/macros/@constraint.jl | 138 +++++++++++++++++++++++++++++++-- src/sd.jl | 69 +++++++++-------- src/shapes.jl | 1 + src/variables.jl | 4 +- test/test_constraint.jl | 138 ++++++++++++++++++++++++++++++++- test/test_macros.jl | 6 +- 8 files changed, 342 insertions(+), 61 deletions(-) diff --git a/docs/src/manual/constraints.md b/docs/src/manual/constraints.md index 69df26aeee9..b2db37737af 100644 --- a/docs/src/manual/constraints.md +++ b/docs/src/manual/constraints.md @@ -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}}: @@ -148,7 +148,7 @@ 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}}: @@ -156,7 +156,7 @@ julia> @constraint(model, A * x .<= b) 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}}: @@ -169,7 +169,7 @@ 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); @@ -177,10 +177,29 @@ 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): @@ -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 diff --git a/src/constraints.jl b/src/constraints.jl index 53e4a42c55a..240b295ecf5 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -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]) @@ -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]) @@ -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]) @@ -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]) @@ -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" @@ -404,7 +404,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> set_name(c, "my_constraint") @@ -412,7 +412,7 @@ julia> name(c) "my_constraint" julia> c -my_constraint : [2 x] ∈ MathOptInterface.Nonnegatives(1) +my_constraint : [2 x] ∈ Nonnegatives() ``` """ function set_name( diff --git a/src/macros/@constraint.jl b/src/macros/@constraint.jl index 9789971cbe6..f3dcfe27551 100644 --- a/src/macros/@constraint.jl +++ b/src/macros/@constraint.jl @@ -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() @@ -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() @@ -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 @@ -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, diff --git a/src/sd.jl b/src/sd.jl index 0dc684affa8..a00b85735ac 100644 --- a/src/sd.jl +++ b/src/sd.jl @@ -705,17 +705,6 @@ end reshape_set(s::MOI.Zeros, ::HermitianMatrixShape) = Zeros() -function build_constraint( - error_fn::Function, - f::LinearAlgebra.Symmetric, - ::Zeros, -) - n = LinearAlgebra.checksquare(f) - shape = SymmetricMatrixShape(n; needs_adjoint_dual = true) - x = vectorize(f, shape) - return VectorConstraint(x, MOI.Zeros(length(x)), shape) -end - function build_constraint(error_fn::Function, ::AbstractMatrix, ::Nonnegatives) return error_fn( "Unsupported matrix in vector-valued set. Did you mean to use the " * @@ -739,7 +728,7 @@ function build_constraint(error_fn::Function, ::AbstractMatrix, ::Zeros) "Unsupported matrix in vector-valued set. Did you mean to use the " * "broadcasting syntax `.==` for element-wise equality? Alternatively, " * "this syntax is supported in the special case that the matrices are " * - "`LinearAlgebra.Symmetric` or `LinearAlgebra.Hermitian`.", + "`Array`, `LinearAlgebra.Symmetric`, or `LinearAlgebra.Hermitian`.", ) end @@ -806,36 +795,50 @@ moi_set(::Nonnegatives, dim::Int) = MOI.Nonnegatives(dim) moi_set(::Nonpositives, dim::Int) = MOI.Nonpositives(dim) moi_set(::Zeros, dim::Int) = MOI.Zeros(dim) -shape(f::LinearAlgebra.Symmetric) = SymmetricMatrixShape(size(f, 1)) +function _shape_for_orthants(f::LinearAlgebra.Symmetric) + n = LinearAlgebra.checksquare(f) + return SymmetricMatrixShape(n; needs_adjoint_dual = true) +end reshape_set(::MOI.Nonnegatives, ::SymmetricMatrixShape) = Nonnegatives() reshape_set(::MOI.Nonpositives, ::SymmetricMatrixShape) = Nonpositives() reshape_set(::MOI.Zeros, ::SymmetricMatrixShape) = Zeros() -shape(f::Array) = ArrayShape(size(f)) +_shape_for_orthants(f::Array) = ArrayShape(size(f)) reshape_set(::MOI.Nonnegatives, ::ArrayShape) = Nonnegatives() reshape_set(::MOI.Nonpositives, ::ArrayShape) = Nonpositives() reshape_set(::MOI.Zeros, ::ArrayShape) = Zeros() -function build_constraint( - error_fn::Function, - f::Union{Array,LinearAlgebra.Symmetric}, - ::Nonnegatives, - set::Union{Nonnegatives,Nonpositives,Zeros}, -) - s = shape(f) - x = vectorize(f, s) - return VectorConstraint(x, moi_set(set, length(x)), s) -end +# We use an @eval loop because a `Union` introduces ambiguities. +for S in (Nonnegatives, Nonpositives, Zeros) + for F in (Array, LinearAlgebra.Symmetric) + @eval begin + function build_constraint(error_fn::Function, f::$F, set::$S) + s = _shape_for_orthants(f) + x = vectorize(f, s) + return VectorConstraint(x, moi_set(set, length(x)), s) + end -function build_constraint( - error_fn::Function, - ::Union{Array,LinearAlgebra.Symmetric}, - ::Nonpositives, - set::Union{Nonnegatives,Nonpositives,Zeros}, -) - return error_fn( - "The syntax `x <= y, $set` not supported. Use `y >= x, $set` instead.", - ) + function build_constraint( + error_fn::Function, + f::$F, + ::Nonnegatives, + set::$S, + ) + return build_constraint(error_fn, f, set) + end + + function build_constraint( + error_fn::Function, + ::$F, + ::Nonpositives, + set::$S, + ) + return error_fn( + "The syntax `x <= y, $set` not supported. Use `y >= x, $set` instead.", + ) + end + end + end end diff --git a/src/shapes.jl b/src/shapes.jl index 8c507786180..d10374d76c0 100644 --- a/src/shapes.jl +++ b/src/shapes.jl @@ -194,5 +194,6 @@ struct ArrayShape{N} <: AbstractShape end reshape_vector(x, shape::ArrayShape) = reshape(x, shape.dims) +reshape_vector(::Nothing, shape::ArrayShape) = nothing vectorize(x, ::ArrayShape) = vec(x) diff --git a/src/variables.jl b/src/variables.jl index 329e3573bd8..8237fcc280f 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -3000,7 +3000,7 @@ julia> normalized_coefficient(con, x) 5.0 julia> @constraint(model, con_vec, [x, 2x + 1, 3] >= 0) -con_vec : [x, 2 x + 1, 3] ∈ MathOptInterface.Nonnegatives(3) +con_vec : [x, 2 x + 1, 3] ∈ Nonnegatives() julia> normalized_coefficient(con_vec, x) 2-element Vector{Tuple{Int64, Float64}}: @@ -3052,7 +3052,7 @@ julia> normalized_coefficient(con, x[1], x[2]) 3.0 julia> @constraint(model, con_vec, x.^2 <= [1, 2]) -con_vec : [x[1]² - 1, x[2]² - 2] ∈ MathOptInterface.Nonpositives(2) +con_vec : [x[1]² - 1, x[2]² - 2] ∈ Nonpositives() julia> normalized_coefficient(con_vec, x[1], x[1]) 1-element Vector{Tuple{Int64, Float64}}: diff --git a/test/test_constraint.jl b/test/test_constraint.jl index d2b421e9a97..282d299de9c 100644 --- a/test/test_constraint.jl +++ b/test/test_constraint.jl @@ -1910,7 +1910,7 @@ function test_symmetric_matrix_inequality() o = constraint_object(c) @test isequal_canonical(o.func, g) @test o.set == moi_set(set, 3) - @test o.shape == SymmetricMatrixShape(2) + @test o.shape == SymmetricMatrixShape(2; needs_adjoint_dual = true) @test reshape_set(o.set, o.shape) == set primal = value(start_value, c) @test primal isa LinearAlgebra.Symmetric @@ -1952,4 +1952,140 @@ function test_matrix_inequality() return end +function test_symmetric_equality() + model = Model() + @variable(model, x[1:2, 1:2], Symmetric) + @variable(model, y[1:2, 1:2], Symmetric) + set_start_value.(x, [1 2; 2 3]) + set_start_value.(y, [6 4; 4 7]) + g = [x[1, 1] - y[1, 1], x[1, 2] - y[1, 2], x[2, 2] - y[2, 2]] + c = @constraint(model, x == y) + o = constraint_object(c) + @test isequal_canonical(o.func, g) + @test o.set == moi_set(Zeros(), 3) + @test o.shape == SymmetricMatrixShape(2; needs_adjoint_dual = true) + @test reshape_set(o.set, o.shape) == Zeros() + primal = value(start_value, c) + @test primal isa LinearAlgebra.Symmetric + @test primal == LinearAlgebra.Symmetric([-5.0 -2.0; -2.0 -4.0]) + return +end + +function test_matrix_equality() + model = Model() + @variable(model, x[1:2, 1:3]) + @variable(model, y[1:2, 1:3]) + set_start_value.(x, [1 2 3; 4 5 6]) + set_start_value.(y, [7 9 11; 8 12 13]) + g = vec(x .- y) + c = @constraint(model, x == y) + o = constraint_object(c) + @test isequal_canonical(o.func, g) + @test o.set == moi_set(Zeros(), 6) + @test o.shape == ArrayShape((2, 3)) + @test reshape_set(o.set, o.shape) == Zeros() + primal = value(start_value, c) + @test primal isa Matrix{Float64} + @test primal == [-6.0 -7.0 -8.0; -4.0 -7.0 -7.0] + @test dual_start_value(c) === nothing + dual_start = rand(2, 3) + set_dual_start_value(c, dual_start) + @test dual_start_value(c) == dual_start + return +end + +function test_matrix_ambiguous_greater_than_inequality() + model = Model() + @variable(model, x[1:2, 1:3]) + @variable(model, y[1:2, 1:3]) + set_start_value.(x, [1 2 3; 4 5 6]) + set_start_value.(y, [7 9 11; 8 12 13]) + err = ErrorException( + """ + In `@constraint(model, x >= y)`: \nThe 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) + ```""", + ) + @test_throws_runtime(err, @constraint(model, x >= y)) + c = @constraint(model, x - y in Nonnegatives()) + @test value(start_value, c) ≈ [-6 -7 -8; -4 -7 -7] + return +end + +function test_matrix_ambiguous_less_than_inequality() + model = Model() + @variable(model, x[1:2, 1:3]) + @variable(model, y[1:2, 1:3]) + set_start_value.(x, [1 2 3; 4 5 6]) + set_start_value.(y, [7 9 11; 8 12 13]) + err = ErrorException( + """ + In `@constraint(model, x <= y)`: \nThe 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) + ```""", + ) + @test_throws_runtime(err, @constraint(model, x <= y)) + c = @constraint(model, x - y in Nonpositives()) + @test value(start_value, c) ≈ [-6 -7 -8; -4 -7 -7] + return +end + +function test_abstract_vector_orthants() + model = Model() + @variable(model, x[i = 2:3], start = i) + y = Containers.DenseAxisArray([4, 6], 2:3) + g = [x[2] - 4, x[3] - 6] + for (c, set) in ( + @constraint(model, x >= y) => MOI.Nonnegatives(2), + @constraint(model, x <= y) => MOI.Nonpositives(2), + @constraint(model, x == y) => MOI.Zeros(2), + ) + o = constraint_object(c) + @test isequal_canonical(o.func, g) + @test o.set == set + @test o.shape == VectorShape() + @test reshape_set(o.set, o.shape) == set + primal = value(start_value, c) + @test primal isa Vector{Float64} + @test primal == [2 - 4, 3 - 6] + @test dual_start_value(c) === nothing + dual_start = rand(2) + set_dual_start_value(c, dual_start) + @test dual_start_value(c) == dual_start + end + return +end + end # module diff --git a/test/test_macros.jl b/test/test_macros.jl index bb80c711579..469cf4d173a 100644 --- a/test/test_macros.jl +++ b/test/test_macros.jl @@ -1955,8 +1955,8 @@ end function test_matrix_in_vector_set() model = Model() - @variable(model, X[1:2, 1:2]) - A = [1 2; 3 4] + @variable(model, X[2:3, 2:3]) + A = Containers.DenseAxisArray([1 2; 3 4], 2:3, 2:3) @test_throws_runtime( ErrorException( "In `@constraint(model, X >= A)`: " * @@ -1983,7 +1983,7 @@ function test_matrix_in_vector_set() "Unsupported matrix in vector-valued set. Did you mean to use the " * "broadcasting syntax `.==` for element-wise equality? Alternatively, " * "this syntax is supported in the special case that the matrices are " * - "`LinearAlgebra.Symmetric` or `LinearAlgebra.Hermitian`.", + "`Array`, `LinearAlgebra.Symmetric`, or `LinearAlgebra.Hermitian`.", ), @constraint(model, X == A), )