diff --git a/docs/src/constraints.md b/docs/src/constraints.md index 328e9c3ef45..95dc121803d 100644 --- a/docs/src/constraints.md +++ b/docs/src/constraints.md @@ -492,7 +492,8 @@ julia> @variable(model, x) x julia> @SDconstraint(model, [x 2x; 3x 4x] >= ones(2, 2)) -[x - 1, 3 x - 1, 2 x - 1, 4 x - 1] ∈ MathOptInterface.PositiveSemidefiniteConeSquare(2) +[x - 1 2 x - 1; + 3 x - 1 4 x - 1] ∈ PSDCone() ``` Solvers supporting such constraints usually expect to be given a matrix that @@ -518,14 +519,25 @@ follows: julia> using LinearAlgebra julia> @constraint(model, Symmetric([x 2x; 2x 4x] - ones(2, 2)) in PSDCone()) -[x - 1, 2 x - 1, 4 x - 1] ∈ MathOptInterface.PositiveSemidefiniteConeTriangle(2) +[x - 1 2 x - 1; + 2 x - 1 4 x - 1] ∈ PSDCone() ``` Note that the lower triangular entries are silently ignored even if they are different so use it with caution: ```jldoctest con_psd -julia> @constraint(model, Symmetric([x 2x; 3x 4x]) in PSDCone()) -[x, 2 x, 4 x] ∈ MathOptInterface.PositiveSemidefiniteConeTriangle(2) +julia> cref = @constraint(model, Symmetric([x 2x; 3x 4x]) in PSDCone()) +[x 2 x; + 2 x 4 x] ∈ PSDCone() + +julia> jump_function(constraint_object(cref)) +3-element Array{GenericAffExpr{Float64,VariableRef},1}: + x + 2 x + 4 x + +julia> moi_set(constraint_object(cref)) +MathOptInterface.PositiveSemidefiniteConeTriangle(2) ``` ## Constraint modifications diff --git a/docs/src/extensions.md b/docs/src/extensions.md index a966cad183a..d4b92934926 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -92,7 +92,8 @@ used to reshape the result computed in [`value`](@ref) and [`dual`](@ref). ```@docs AbstractShape shape -reshape_result +reshape_vector +reshape_set dual_shape ScalarShape VectorShape diff --git a/src/constraints.jl b/src/constraints.jl index 3fba3df0b32..8c5415b325b 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -305,6 +305,7 @@ end jump_function(constraint::ScalarConstraint) = constraint.func moi_set(constraint::ScalarConstraint) = constraint.set +reshape_set(set::MOI.AbstractScalarSet, ::ScalarShape) = set shape(::ScalarConstraint) = ScalarShape() function constraint_object(ref::ConstraintRef{Model, _MOICON{FuncType, SetType}}) where @@ -342,6 +343,7 @@ end jump_function(constraint::VectorConstraint) = constraint.func moi_set(constraint::VectorConstraint) = constraint.set +reshape_set(set::MOI.AbstractVectorSet, ::VectorShape) = set shape(c::VectorConstraint) = c.shape function constraint_object(ref::ConstraintRef{Model, _MOICON{FuncType, SetType}}) where {FuncType <: MOI.AbstractVectorFunction, SetType <: MOI.AbstractVectorSet} @@ -442,7 +444,7 @@ evaluation of `2x + 3y`. ``` """ function value(cref::ConstraintRef{Model, <:_MOICON}) - return reshape_result(_constraint_primal(cref), cref.shape) + return reshape_vector(_constraint_primal(cref), cref.shape) end # Returns the value of MOI.ConstraintPrimal in a type-stable way @@ -475,7 +477,7 @@ Use `has_dual` to check if a result exists before asking for values. See also [`shadow_price`](@ref). """ function dual(cref::ConstraintRef{Model, <:_MOICON}) - return reshape_result(_constraint_dual(cref), dual_shape(cref.shape)) + return reshape_vector(_constraint_dual(cref), dual_shape(cref.shape)) end # Returns the value of MOI.ConstraintPrimal in a type-stable way diff --git a/src/macros.jl b/src/macros.jl index 41315149b65..bb7acb24735 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -759,8 +759,19 @@ julia> a = [x 2x julia> b = [1 2 3 4]; -julia> @SDconstraint(model, a ⪰ b) -[x - 1, -3, 2 x - 2, x - 4] ∈ MathOptInterface.PositiveSemidefiniteConeSquare(2) +julia> cref = @SDconstraint(model, a ⪰ b) +[x - 1 2 x - 2; + -3 x - 4 ] ∈ PSDCone() + +julia> jump_function(constraint_object(cref)) +4-element Array{GenericAffExpr{Float64,VariableRef},1}: + x - 1 + -3 + 2 x - 2 + x - 4 + +julia> moi_set(constraint_object(cref)) +MathOptInterface.PositiveSemidefiniteConeSquare(2) ``` In the set `PositiveSemidefiniteConeSquare(2)` in the last output, `Square` means that the matrix is passed as a square matrix as the corresponding diff --git a/src/print.jl b/src/print.jl index ac99dd44183..d04236c32c2 100644 --- a/src/print.jl +++ b/src/print.jl @@ -220,7 +220,17 @@ function model_string(print_mode, model::AbstractModel) end str *= eol str *= ijl ? "\\text{Subject to} \\quad" : "Subject to" * eol - str *= constraints_string(print_mode, model, sep, eol) + constraints = constraints_string(print_mode, model) + if print_mode == REPLMode + constraints = map(str -> replace(str, '\n' => eol * sep), constraints) + end + if !isempty(constraints) + str *= sep + end + str *= join(constraints, eol * sep) + if !isempty(constraints) + str *= eol + end if ijl str = "\\begin{alignat*}{1}" * str * "\\end{alignat*}\n" end @@ -259,12 +269,7 @@ end #------------------------------------------------------------------------ ## VariableRef #------------------------------------------------------------------------ -function Base.show(io::IO, v::AbstractVariableRef) - print(io, function_string(REPLMode, v)) -end -function Base.show(io::IO, ::MIME"text/latex", v::AbstractVariableRef) - print(io, _wrap_in_math_mode(function_string(IJuliaMode, v))) -end + function function_string(::Type{REPLMode}, v::AbstractVariableRef) var_name = name(v) if !isempty(var_name) @@ -283,10 +288,9 @@ function function_string(::Type{IJuliaMode}, v::AbstractVariableRef) end end -Base.show(io::IO, a::GenericAffExpr) = print(io, function_string(REPLMode, a)) -function Base.show(io::IO, ::MIME"text/latex", a::GenericAffExpr) - print(io, _wrap_in_math_mode(function_string(IJuliaMode, a))) -end +#------------------------------------------------------------------------ +## GenericAffExpr +#------------------------------------------------------------------------ function function_string(mode, a::GenericAffExpr, show_constant=true) # If the expression is empty, return the constant (or 0) @@ -326,10 +330,6 @@ end #------------------------------------------------------------------------ ## GenericQuadExpr #------------------------------------------------------------------------ -Base.show(io::IO, q::GenericQuadExpr) = print(io, function_string(REPLMode, q)) -function Base.show(io::IO, ::MIME"text/latex", q::GenericQuadExpr) - print(io, _wrap_in_math_mode(function_string(IJuliaMode, q))) -end function function_string(mode, q::GenericQuadExpr) length(quad_terms(q)) == 0 && return function_string(mode, q.aff) @@ -398,26 +398,25 @@ function show_constraints_summary(io::IO, model::Model) end """ - constraints_string(print_mode, model::AbstractModel, sep, eol)::String + constraints_string(print_mode, model::AbstractModel)::Vector{String} -Return a `String` describing the constraints of the model, each on a line -starting with `sep` and ending with `eol` (which already contains `\n`). +Return a list of `String`s describing each constraint of the model. """ -function constraints_string(print_mode, model::Model, sep, eol) - str = "" +function constraints_string(print_mode, model::Model) + strings = String[] for (F, S) in list_of_constraint_types(model) for cref in all_constraints(model, F, S) con = constraint_object(cref) - str *= sep * constraint_string(print_mode, con) * eol + push!(strings, constraint_string(print_mode, con)) end end if model.nlp_data !== nothing for nl_constraint in model.nlp_data.nlconstr - str *= sep * nl_constraint_string(model, print_mode, nl_constraint) - str *= eol + push!(strings, + nl_constraint_string(model, print_mode, nl_constraint)) end end - return str + return strings end ## Notes for extensions @@ -445,10 +444,52 @@ Return a `String` representing the function `func` using print mode """ function function_string end +function Base.show(io::IO, f::AbstractJuMPScalar) + print(io, function_string(REPLMode, f)) +end +function Base.show(io::IO, ::MIME"text/latex", f::AbstractJuMPScalar) + print(io, _wrap_in_math_mode(function_string(IJuliaMode, f))) +end + function function_string(print_mode, vector::Vector{<:AbstractJuMPScalar}) return "[" * join(function_string.(print_mode, vector), ", ") * "]" end +function function_string(::Type{REPLMode}, + A::AbstractMatrix{<:AbstractJuMPScalar}) + str = sprint(show, MIME"text/plain"(), A) + lines = split(str, '\n') + # We drop the first line with the signature "m×n Array{...}:" + lines = lines[2:end] + # We replace the first space by an opening `[` + lines[1] = '[' * lines[1][2:end] + for i in 1:length(lines) + lines[i] = lines[i] * (i == length(lines) ? ']' : ';') + end + return join(lines, '\n') +end + +function function_string(print_mode::Type{IJuliaMode}, + A::AbstractMatrix{<:AbstractJuMPScalar}) + str = sprint(show, MIME"text/plain"(), A) + str = "\\begin{bmatrix}\n" + for i in 1:size(A, 1) + line = "" + for j in 1:size(A, 2) + if j != 1 + line *= " & " + end + if A isa Symmetric && i > j + line *= "\\cdot" + else + line *= function_string(print_mode, A[i, j]) + end + end + str *= line * "\\\\\n" + end + return str * "\\end{bmatrix}" +end + """ function_string(print_mode::{<:JuMP.PrintMode}, constraint::JuMP.AbstractConstraint) @@ -457,7 +498,8 @@ Return a `String` representing the function of the constraint `constraint` using print mode `print_mode`. """ function function_string(print_mode, constraint::AbstractConstraint) - return function_string(print_mode, jump_function(constraint)) + f = reshape_vector(jump_function(constraint), shape(constraint)) + return function_string(print_mode, f) end function in_set_string(print_mode, set::MOI.LessThan) @@ -486,13 +528,12 @@ in_set_string(print_mode, ::MOI.Integer) = "integer" # regular text in math mode which looks a bit awkward. """ in_set_string(print_mode::Type{<:JuMP.PrintMode}, - set::Union{JuMP.AbstractJuMPScalar, - Vector{<:JuMP.AbstractJuMPScalar}}) + set::Union{PSDCone, MOI.AbstractSet}) Return a `String` representing the membership to the set `set` using print mode `print_mode`. """ -function in_set_string(print_mode, set::MOI.AbstractSet) +function in_set_string(print_mode, set::Union{PSDCone, MOI.AbstractSet}) return string(_math_symbol(print_mode, :in), " ", set) end @@ -504,13 +545,20 @@ Return a `String` representing the membership to the set of the constraint `constraint` using print mode `print_mode`. """ function in_set_string(print_mode, constraint::AbstractConstraint) - return in_set_string(print_mode, moi_set(constraint)) + set = reshape_set(moi_set(constraint), shape(constraint)) + return in_set_string(print_mode, set) end function constraint_string(print_mode, constraint_object::AbstractConstraint) func_str = function_string(print_mode, constraint_object) in_set_str = in_set_string(print_mode, constraint_object) - return func_str * " " * in_set_str + if print_mode == REPLMode + lines = split(func_str, '\n') + lines[1 + div(length(lines), 2)] *= " " * in_set_str + return join(lines, '\n') + else + return func_str * " " * in_set_str + end end function constraint_string(print_mode, constraint_name, constraint_object::AbstractConstraint) diff --git a/src/sd.jl b/src/sd.jl index 333344209e7..b5854471d64 100644 --- a/src/sd.jl +++ b/src/sd.jl @@ -24,8 +24,19 @@ julia> a = [ x 2x julia> b = [1 2 2 4]; -julia> @SDconstraint(model, a ⪰ b) -[x - 1, 2 x - 2, 2 x - 2, x - 4] ∈ MathOptInterface.PositiveSemidefiniteConeSquare(2) +julia> cref = @SDconstraint(model, a ⪰ b) +[x - 1 2 x - 2; + 2 x - 2 x - 4 ] ∈ PSDCone() + +julia> jump_function(constraint_object(cref)) +4-element Array{GenericAffExpr{Float64,VariableRef},1}: + x - 1 + 2 x - 2 + 2 x - 2 + x - 4 + +julia> moi_set(constraint_object(cref)) +MathOptInterface.PositiveSemidefiniteConeSquare(2) ``` We see in the output of the last command that the matrix the vectorization of the matrix is constrained to belong to the `PositiveSemidefiniteConeSquare`. @@ -33,8 +44,18 @@ matrix is constrained to belong to the `PositiveSemidefiniteConeSquare`. ```jldoctest PSDCone julia> using LinearAlgebra # For Symmetric -julia> @constraint(model, Symmetric(a - b) in PSDCone()) -[x - 1, 2 x - 2, x - 4] ∈ MathOptInterface.PositiveSemidefiniteConeTriangle(2) +julia> cref = @constraint(model, Symmetric(a - b) in PSDCone()) +[x - 1 2 x - 2; + 2 x - 2 x - 4 ] ∈ PSDCone() + +julia> jump_function(constraint_object(cref)) +3-element Array{GenericAffExpr{Float64,VariableRef},1}: + x - 1 + 2 x - 2 + x - 4 + +julia> moi_set(constraint_object(cref)) +MathOptInterface.PositiveSemidefiniteConeTriangle(2) ``` As we see in the output of the last command, the vectorization of only the upper triangular part of the matrix is constrained to belong to the @@ -53,7 +74,7 @@ lower-left triangular part given row by row). struct SymmetricMatrixShape <: AbstractShape side_dimension::Int end -function reshape_result(vectorized_form::Vector{T}, shape::SymmetricMatrixShape) where T +function reshape_vector(vectorized_form::Vector{T}, shape::SymmetricMatrixShape) where T matrix = Matrix{T}(undef, shape.side_dimension, shape.side_dimension) k = 0 for j in 1:shape.side_dimension @@ -64,6 +85,10 @@ function reshape_result(vectorized_form::Vector{T}, shape::SymmetricMatrixShape) end return Symmetric(matrix) end +function reshape_set(::MOI.PositiveSemidefiniteConeTriangle, + ::SymmetricMatrixShape) + return PSDCone() +end """ SquareMatrixShape @@ -76,9 +101,12 @@ row). struct SquareMatrixShape <: AbstractShape side_dimension::Int end -function reshape_result(vectorized_form::Vector{T}, shape::SquareMatrixShape) where T +function reshape_vector(vectorized_form::Vector{T}, shape::SquareMatrixShape) where T return reshape(vectorized_form, shape.side_dimension, shape.side_dimension) end +function reshape_set(::MOI.PositiveSemidefiniteConeSquare, ::SquareMatrixShape) + return PSDCone() +end """ function build_constraint(_error::Function, Q::Symmetric{V, M}, diff --git a/src/shapes.jl b/src/shapes.jl index 22e65def936..a5a756d26ea 100644 --- a/src/shapes.jl +++ b/src/shapes.jl @@ -12,7 +12,7 @@ AbstractShape Abstract vectorizable shape. Given a flat vector form of an object of shape -`shape`, the original object can be obtained by [`reshape_result`](@ref). +`shape`, the original object can be obtained by [`reshape_vector`](@ref). """ abstract type AbstractShape end @@ -36,7 +36,7 @@ end struct PolynomialShape <: AbstractShape monomials::Vector{Monomial} end -JuMP.reshape_result(x::Vector, shape::PolynomialShape) = Polynomial(x, shape.monomials) +JuMP.reshape_vector(x::Vector, shape::PolynomialShape) = Polynomial(x, shape.monomials) ``` and a shape for moments can be defined as follows: ```julia @@ -47,7 +47,7 @@ end struct MomentsShape <: AbstractShape monomials::Vector{Monomial} end -JuMP.reshape_result(x::Vector, shape::MomentsShape) = Moments(x, shape.monomials) +JuMP.reshape_vector(x::Vector, shape::MomentsShape) = Moments(x, shape.monomials) ``` The `dual_shape` allows to define the shape of the dual of polynomial and moment constraints: @@ -59,7 +59,26 @@ dual_shape(shape::MomentsShape) = PolynomialShape(shape.monomials) dual_shape(shape::AbstractShape) = shape """ - reshape_result(vectorized_form::Vector, shape::AbstractShape) + reshape_set(vectorized_set::MOI.AbstractSet, shape::AbstractShape) + +Return a set in its original shape `shape` given its vectorized form +`vectorized_form`. + +## Examples + +Given a [`SymmetricMatrixShape`](@ref) of vectorized form +`[1, 2, 3] in MOI.PositiveSemidefinieConeTriangle(2)`, the +following code returns the set of the original constraint +`Symmetric(Matrix[1 2; 2 3]) in PSDCone()`: +```jldoctest; setup = :(using JuMP) +julia> reshape_set(MOI.PositiveSemidefiniteConeTriangle(2), SymmetricMatrixShape(2)) +PSDCone() +``` +""" +function reshape_set end + +""" + reshape_vector(vectorized_form::Vector, shape::AbstractShape) Return an object in its original shape `shape` given its vectorized form `vectorized_form`. @@ -68,11 +87,14 @@ Return an object in its original shape `shape` given its vectorized form Given a [`SymmetricMatrixShape`](@ref) of vectorized form `[1, 2, 3]`, the following code returns the matrix `Symmetric(Matrix[1 2; 2 3])`: -```julia -reshape_result([1, 2, 3], SymmetricMatrixShape(2)) +```jldoctest; setup = :(using JuMP) +julia> reshape_vector([1, 2, 3], SymmetricMatrixShape(2)) +2×2 LinearAlgebra.Symmetric{Int64,Array{Int64,2}}: + 1 2 + 2 3 ``` """ -function reshape_result end +function reshape_vector end """ shape(c::AbstractConstraint)::AbstractShape @@ -87,7 +109,7 @@ function shape end Shape of scalar constraints. """ struct ScalarShape <: AbstractShape end -reshape_result(α, ::ScalarShape) = α +reshape_vector(α, ::ScalarShape) = α """ VectorShape @@ -95,4 +117,4 @@ reshape_result(α, ::ScalarShape) = α Vector for which the vectorized form corresponds exactly to the vector given. """ struct VectorShape <: AbstractShape end -reshape_result(vectorized_form, ::VectorShape) = vectorized_form +reshape_vector(vectorized_form, ::VectorShape) = vectorized_form diff --git a/test/JuMPExtension.jl b/test/JuMPExtension.jl index 1471adf5db3..4aee406fa4a 100644 --- a/test/JuMPExtension.jl +++ b/test/JuMPExtension.jl @@ -319,14 +319,14 @@ function JuMP.show_constraints_summary(io::IO, model::MyModel) n = length(model.constraints) print(io, "Constraint", _plural(n), ": ", n) end -function JuMP.constraints_string(print_mode, model::MyModel, sep, eol) - str = "" +function JuMP.constraints_string(print_mode, model::MyModel) + strings = String[] # Sort by creation order, i.e. ConstraintIndex value constraints = sort(collect(model.constraints), by = c -> c.first.value) for (index, constraint) in constraints - str *= sep * JuMP.constraint_string(print_mode, constraint) * eol + push!(strings, JuMP.constraint_string(print_mode, constraint)) end - return str + return strings end end diff --git a/test/print.jl b/test/print.jl index 48b63ed77a4..dfb008e427a 100644 --- a/test/print.jl +++ b/test/print.jl @@ -12,7 +12,7 @@ ############################################################################# using MathOptInterface using JuMP -using Test +using LinearAlgebra, Test import JuMP.REPLMode, JuMP.IJuliaMode # Helper function to test IO methods work correctly @@ -367,6 +367,10 @@ function model_printing_test(ModelType::Type{<:JuMP.AbstractModel}) @constraint(model_1, a + b - 10c - 2x + c1 <= 1) @constraint(model_1, a*b <= 2) @constraint(model_1, [1 - a; u] in SecondOrderCone()) + @constraint(model_1, [a b; c x] in PSDCone()) + @constraint(model_1, Symmetric([a b; b x]) in PSDCone()) + @constraint(model_1, [a, b, c] in MOI.PositiveSemidefiniteConeTriangle(2)) + @constraint(model_1, [a, b, c, x] in MOI.PositiveSemidefiniteConeSquare(2)) VariableType = typeof(a) @@ -392,6 +396,12 @@ function model_printing_test(ModelType::Type{<:JuMP.AbstractModel}) c1 $le 1.0 a + b - 10 c - 2 x + c1 $le 1.0 a*b $le 2.0 + [a b; + b x] $inset PSDCone() + [a, b, c] $inset MathOptInterface.PositiveSemidefiniteConeTriangle(2) + [a b; + c x] $inset PSDCone() + [a, b, c, x] $inset MathOptInterface.PositiveSemidefiniteConeSquare(2) [-a + 1, u[1], u[2], u[3]] $inset MathOptInterface.SecondOrderCone(4) """, repl=:print) @@ -407,6 +417,8 @@ function model_printing_test(ModelType::Type{<:JuMP.AbstractModel}) `$VariableType`-in-`MathOptInterface.LessThan{Float64}`: 4 constraints `GenericAffExpr{Float64,$VariableType}`-in-`MathOptInterface.LessThan{Float64}`: 1 constraint `GenericQuadExpr{Float64,$VariableType}`-in-`MathOptInterface.LessThan{Float64}`: 1 constraint + `Array{$VariableType,1}`-in-`MathOptInterface.PositiveSemidefiniteConeTriangle`: 2 constraints + `Array{$VariableType,1}`-in-`MathOptInterface.PositiveSemidefiniteConeSquare`: 2 constraints `Array{GenericAffExpr{Float64,$VariableType},1}`-in-`MathOptInterface.SecondOrderCone`: 1 constraint Model mode: AUTOMATIC CachingOptimizer state: NO_OPTIMIZER @@ -434,6 +446,16 @@ function model_printing_test(ModelType::Type{<:JuMP.AbstractModel}) & c1 \\leq 1.0\\\\ & a + b - 10 c - 2 x + c1 \\leq 1.0\\\\ & a\\times b \\leq 2.0\\\\ + & \\begin{bmatrix} + a & b\\\\ + \\cdot & x\\\\ + \\end{bmatrix} \\in PSDCone()\\\\ + & [a, b, c] \\in MathOptInterface.PositiveSemidefiniteConeTriangle(2)\\\\ + & \\begin{bmatrix} + a & b\\\\ + c & x\\\\ + \\end{bmatrix} \\in PSDCone()\\\\ + & [a, b, c, x] \\in MathOptInterface.PositiveSemidefiniteConeSquare(2)\\\\ & [-a + 1, u_{1}, u_{2}, u_{3}] \\in MathOptInterface.SecondOrderCone(4)\\\\ \\end{alignat*} """)