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

Fix querying dual of symmetric equality constraints #3797

Merged
merged 1 commit into from
Aug 5, 2024
Merged
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
160 changes: 153 additions & 7 deletions src/sd.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,50 @@ MathOptInterface.PositiveSemidefiniteConeTriangle(2)
struct PSDCone end

"""
SymmetricMatrixShape
SymmetricMatrixShape(
side_dimension::Int;
needs_adjoint_dual::Bool = false,
)

The shape object for a symmetric square matrix of `side_dimension` rows and
columns.

Shape object for a symmetric square matrix of `side_dimension` rows and columns.
The vectorized form contains the entries of the upper-right triangular part of
the matrix given column by column (or equivalently, the entries of the
lower-left triangular part given row by row).

## `needs_adjoint_dual`

By default, the [`dual_shape`](@ref) of [`SymmetricMatrixShape`](@ref) is also
[`SymmetricMatrixShape`](@ref). This is true for cases such as a
`LinearAlgebra.Symmetric` matrix in [`PSDCone`](@ref).

However, JuMP also supports `LinearAlgebra.Symmetric` matrix in `Zeros`, which
is interpreted as an element-wise equality constraint. By exploiting symmetry,
we pass only the upper triangle of the equality constraints. This works for the
primal, but it leads to a factor of 2 difference in the off-diagonal dual
elements. (The dual value of the `(i, j)` element in the triangle formulation
should be divided by 2 when spread across the `(i, j)` and `(j, i)` elements in
the square matrix formulation.) If the constraint has this dual inconsistency,
set `needs_adjoint_dual = true`.
"""
struct SymmetricMatrixShape <: AbstractShape
side_dimension::Int
needs_adjoint_dual::Bool

function SymmetricMatrixShape(
side_dimension::Int;
needs_adjoint_dual::Bool = false,
)
return new(side_dimension, needs_adjoint_dual)
end
end

function dual_shape(s::SymmetricMatrixShape)
if s.needs_adjoint_dual
return SymmetricMatrixAdjointShape(s.side_dimension)
end
return s
end

function reshape_vector(
Expand All @@ -174,6 +209,39 @@ function reshape_set(
return PSDCone()
end

"""
SymmetricMatrixAdjointShape(side_dimension)

The [`dual_shape`](@ref) of [`SymmetricMatrixShape`](@ref).

This shape is not intended for regular use.
"""
struct SymmetricMatrixAdjointShape <: AbstractShape
side_dimension::Int
end

function vectorize(matrix::AbstractMatrix, s::SymmetricMatrixAdjointShape)
n = LinearAlgebra.checksquare(matrix)
return [((i == j) ? 1 : 2) * matrix[i, j] for j in 1:n for i in 1:j]
end

function reshape_vector(
v::Vector{T},
shape::SymmetricMatrixAdjointShape,
) where {T}
matrix = Matrix{T}(undef, shape.side_dimension, shape.side_dimension)
k = 0
for j in 1:shape.side_dimension
for i in 1:j-1
k += 1
matrix[j, i] = matrix[i, j] = 0.5 * v[k]
end
k += 1
matrix[j, j] = v[k]
end
return LinearAlgebra.Symmetric(matrix)
end

"""
triangle_vec(matrix::Matrix)

Expand Down Expand Up @@ -432,14 +500,49 @@ parametrize a Hermitian matrix.
struct HermitianPSDCone end

"""
HermitianMatrixShape
HermitianMatrixShape(
side_dimension::Int;
needs_adjoint_dual::Bool = false,
)

The shape object for a Hermitian square matrix of `side_dimension` rows and
columns.

Shape object for a Hermitian square matrix of `side_dimension` rows and
columns. The vectorized form corresponds to
The vectorized form corresponds to
[`MOI.HermitianPositiveSemidefiniteConeTriangle`](@ref).

## `needs_adjoint_dual`

By default, the [`dual_shape`](@ref) of [`HermitianMatrixShape`](@ref) is also
[`HermitianMatrixShape`](@ref). This is true for cases such as a
`LinearAlgebra.Hermitian` matrix in [`HermitianPSDCone`](@ref).

However, JuMP also supports `LinearAlgebra.Hermitian` matrix in `Zeros`, which
is interpreted as an element-wise equality constraint. By exploiting symmetry,
we pass only the upper triangle of the equality constraints. This works for the
primal, but it leads to a factor of 2 difference in the off-diagonal dual
elements. (The dual value of the `(i, j)` element in the triangle formulation
should be divided by 2 when spread across the `(i, j)` and `(j, i)` elements in
the square matrix formulation.) If the constraint has this dual inconsistency,
set `needs_adjoint_dual = true`.
"""
struct HermitianMatrixShape <: AbstractShape
side_dimension::Int
needs_adjoint_dual::Bool

function HermitianMatrixShape(
side_dimension::Int;
needs_adjoint_dual::Bool = false,
)
return new(side_dimension, needs_adjoint_dual)
end
end

function dual_shape(s::HermitianMatrixShape)
if s.needs_adjoint_dual
return HermitianMatrixAdjointShape(s.side_dimension)
end
return s
end

function vectorize(matrix, ::HermitianMatrixShape)
Expand Down Expand Up @@ -479,6 +582,49 @@ function reshape_vector(v::Vector{T}, shape::HermitianMatrixShape) where {T}
return LinearAlgebra.Hermitian(matrix)
end

"""
HermitianMatrixAdjointShape(side_dimension)

The [`dual_shape`](@ref) of [`HermitianMatrixShape`](@ref).

This shape is not intended for regular use.
"""
struct HermitianMatrixAdjointShape <: AbstractShape
side_dimension::Int
end

function vectorize(matrix, ::HermitianMatrixAdjointShape)
n = LinearAlgebra.checksquare(matrix)
real_shape = SymmetricMatrixAdjointShape(n)
imag_shape = SymmetricMatrixShape(n - 1)
return vcat(
vectorize(_real.(matrix), real_shape),
vectorize(2 * _imag.(matrix[1:(end-1), 2:end]), imag_shape),
)
end

function reshape_vector(
v::Vector{T},
shape::HermitianMatrixAdjointShape,
) where {T}
NewType = _MA.promote_operation(_MA.add_mul, T, Complex{Bool}, T)
n = shape.side_dimension
matrix = Matrix{NewType}(undef, n, n)
real_k = 0
imag_k = MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(n))
for j in 1:n
for i in 1:(j-1)
real_k += 1
imag_k += 1
matrix[i, j] = (v[real_k] + im * v[imag_k]) / 2
matrix[j, i] = (v[real_k] - im * v[imag_k]) / 2
end
real_k += 1
matrix[j, j] = v[real_k]
end
return LinearAlgebra.Hermitian(matrix)
end

function _vectorize_complex_variables(error_fn::Function, matrix::Matrix)
if any(_is_binary, matrix) || any(_is_integer, matrix)
# We would then need to fix the imaginary value to zero. Let's wait to
Expand Down Expand Up @@ -552,7 +698,7 @@ function build_constraint(
::Zeros,
)
n = LinearAlgebra.checksquare(H)
shape = HermitianMatrixShape(n)
shape = HermitianMatrixShape(n; needs_adjoint_dual = true)
x = vectorize(H, shape)
return VectorConstraint(x, MOI.Zeros(length(x)), shape)
end
Expand All @@ -565,7 +711,7 @@ function build_constraint(
::Zeros,
)
n = LinearAlgebra.checksquare(f)
shape = SymmetricMatrixShape(n)
shape = SymmetricMatrixShape(n; needs_adjoint_dual = true)
x = vectorize(f, shape)
return VectorConstraint(x, MOI.Zeros(length(x)), shape)
end
Expand Down
4 changes: 2 additions & 2 deletions src/variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2304,10 +2304,10 @@ function _imag(s::String)
end

_real(v::ScalarVariable) = _mapinfo(real, v)
_real(scalar::AbstractJuMPScalar) = real(scalar)
_real(scalar) = real(scalar)

_imag(v::ScalarVariable) = _mapinfo(imag, v)
_imag(scalar::AbstractJuMPScalar) = imag(scalar)
_imag(scalar) = imag(scalar)

_conj(v::ScalarVariable) = _mapinfo(conj, v)

Expand Down
11 changes: 11 additions & 0 deletions test/test_constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,12 @@ function test_hermitian_in_zeros()
model = Model()
@variable(model, H[1:2, 1:2] in HermitianPSDCone())
c = @constraint(model, H == LinearAlgebra.I)
@test c.shape == HermitianMatrixShape(2; needs_adjoint_dual = true)
set_dual_start_value(c, [1 2+1im; 2-1im 3])
@test dual_start_value(c) ==
LinearAlgebra.Hermitian([1.0 2.0+1.0im; 2.0-1.0im 3.0])
@test MOI.get(backend(model), MOI.ConstraintDualStart(), index(c)) ==
[1.0, 4.0, 3.0, 2.0]
con = constraint_object(c)
@test isequal_canonical(con.func[1], real(H[1, 1]) - 1)
@test isequal_canonical(con.func[2], real(H[1, 2]))
Expand All @@ -1650,6 +1656,11 @@ function test_symmetric_in_zeros()
model = Model()
@variable(model, H[1:2, 1:2], Symmetric)
c = @constraint(model, H == LinearAlgebra.I)
@test c.shape == SymmetricMatrixShape(2; needs_adjoint_dual = true)
set_dual_start_value(c, [1 2; 2 3])
@test dual_start_value(c) == LinearAlgebra.Symmetric([1.0 2.0; 2.0 3.0])
@test MOI.get(backend(model), MOI.ConstraintDualStart(), index(c)) ==
[1.0, 4.0, 3.0]
con = constraint_object(c)
@test isequal_canonical(con.func[1], H[1, 1] - 1)
@test isequal_canonical(con.func[2], H[1, 2] - 0)
Expand Down
Loading