Skip to content

Commit ae0ad01

Browse files
authored
[Bridges] Add hermitian bridge for constraints (#1977)
1 parent 97405a7 commit ae0ad01

File tree

7 files changed

+364
-1
lines changed

7 files changed

+364
-1
lines changed

docs/src/submodules/Bridges/list_of_bridges.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Bridges.Constraint.RelativeEntropyBridge
4747
Bridges.Constraint.NormSpectralBridge
4848
Bridges.Constraint.NormNuclearBridge
4949
Bridges.Constraint.SquareBridge
50+
Bridges.Constraint.HermitianToSymmetricPSDBridge
5051
Bridges.Constraint.RootDetBridge
5152
Bridges.Constraint.LogDetBridge
5253
Bridges.Constraint.IndicatorActiveOnFalseBridge

src/Bridges/Constraint/Constraint.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ include("bridges/soc_to_nonconvex_quad.jl") # do not add these bridges by defaul
5050
include("bridges/soc_to_psd.jl")
5151
include("bridges/split_complex_equalto.jl")
5252
include("bridges/split_complex_zeros.jl")
53+
include("bridges/hermitian.jl")
5354
include("bridges/square.jl")
5455
include("bridges/table.jl")
5556
include("bridges/vectorize.jl")
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# Copyright (c) 2017: Miles Lubin and contributors
2+
# Copyright (c) 2017: Google Inc.
3+
#
4+
# Use of this source code is governed by an MIT-style license that can be found
5+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
6+
7+
"""
8+
HermitianToSymmetricPSDBridge{T,F,G} <: Bridges.Constraint.AbstractBridge
9+
10+
`HermitianToSymmetricPSDBridge` implements the following reformulation:
11+
12+
* Hermitian positive semidefinite `n x n` complex matrix to a symmetric
13+
positive semidefinite `2n x 2n` real matrix.
14+
15+
See also [`MOI.Bridges.Variable.HermitianToSymmetricPSDBridge`](@ref).
16+
17+
## Source node
18+
19+
`HermitianToSymmetricPSDBridge` supports:
20+
21+
* `G` in [`MOI.HermitianPositiveSemidefiniteConeTriangle`](@ref)
22+
23+
## Target node
24+
25+
`HermitianToSymmetricPSDBridge` creates:
26+
27+
* `F` in [`MOI.PositiveSemidefiniteConeTriangle`](@ref)
28+
29+
## Reformulation
30+
31+
The reformulation is best described by example.
32+
33+
The Hermitian matrix:
34+
```math
35+
\\begin{bmatrix}
36+
x_{11} & x_{12} + y_{12}im & x_{13} + y_{13}im\\\\
37+
x_{12} - y_{12}im & x_{22} & x_{23} + y_{23}im\\\\
38+
x_{13} - y_{13}im & x_{23} - y_{23}im & x_{33}
39+
\\end{bmatrix}
40+
```
41+
is positive semidefinite if and only if the symmetric matrix:
42+
```math
43+
\\begin{bmatrix}
44+
x_{11} & x_{12} & x_{13} & 0 & y_{12} & y_{13} \\\\
45+
& x_{22} & x_{23} & -y_{12} & 0 & y_{23} \\\\
46+
& & x_{33} & -y_{13} & -y_{23} & 0 \\\\
47+
& & & x_{11} & x_{12} & x_{13} \\\\
48+
& & & & x_{22} & x_{23} \\\\
49+
& & & & & x_{33}
50+
\\end{bmatrix}
51+
```
52+
is positive semidefinite.
53+
54+
The bridge achieves this reformulation by constraining the above matrix to
55+
belong to the `MOI.PositiveSemidefiniteConeTriangle(6)`.
56+
"""
57+
struct HermitianToSymmetricPSDBridge{T,F,G} <: SetMapBridge{
58+
T,
59+
MOI.PositiveSemidefiniteConeTriangle,
60+
MOI.HermitianPositiveSemidefiniteConeTriangle,
61+
F,
62+
G,
63+
}
64+
constraint::MOI.ConstraintIndex{F,MOI.PositiveSemidefiniteConeTriangle}
65+
end
66+
67+
const HermitianToSymmetricPSD{T,OT<:MOI.ModelLike} =
68+
SingleBridgeOptimizer{HermitianToSymmetricPSDBridge{T},OT}
69+
70+
function _promote_minus_vcat(::Type{T}, ::Type{G}) where {T,G}
71+
S = MOI.Utilities.scalar_type(G)
72+
M = MOI.Utilities.promote_operation(-, T, S)
73+
F = MOI.Utilities.promote_operation(vcat, T, S, M, T)
74+
return F
75+
end
76+
77+
function concrete_bridge_type(
78+
::Type{<:HermitianToSymmetricPSDBridge{T}},
79+
G::Type{<:MOI.AbstractVectorFunction},
80+
::Type{MOI.HermitianPositiveSemidefiniteConeTriangle},
81+
) where {T}
82+
F = _promote_minus_vcat(T, G)
83+
return HermitianToSymmetricPSDBridge{T,F,G}
84+
end
85+
86+
function MOI.Bridges.map_set(
87+
::Type{<:HermitianToSymmetricPSDBridge},
88+
set::MOI.HermitianPositiveSemidefiniteConeTriangle,
89+
)
90+
return MOI.PositiveSemidefiniteConeTriangle(2set.side_dimension)
91+
end
92+
93+
function MOI.Bridges.inverse_map_set(
94+
::Type{<:HermitianToSymmetricPSDBridge},
95+
set::MOI.PositiveSemidefiniteConeTriangle,
96+
)
97+
dim = set.side_dimension
98+
@assert iseven(dim)
99+
return MOI.HermitianPositiveSemidefiniteConeTriangle(div(dim, 2))
100+
end
101+
102+
function MOI.Bridges.map_function(
103+
::Type{<:HermitianToSymmetricPSDBridge{T}},
104+
func,
105+
) where {T}
106+
complex_scalars = MOI.Utilities.eachscalar(func)
107+
S = MOI.Utilities.scalar_type(_promote_minus_vcat(T, typeof(func)))
108+
complex_dim = length(complex_scalars)
109+
complex_set = MOI.Utilities.set_with_dimension(
110+
MOI.HermitianPositiveSemidefiniteConeTriangle,
111+
complex_dim,
112+
)
113+
n = complex_set.side_dimension
114+
real_set = MOI.PositiveSemidefiniteConeTriangle(2n)
115+
real_dim = MOI.dimension(real_set)
116+
real_scalars = Vector{S}(undef, real_dim)
117+
complex_index = 0
118+
half_real_set = MOI.PositiveSemidefiniteConeTriangle(n)
119+
half_real_dim = MOI.dimension(half_real_set)
120+
real_index_1 = 0
121+
real_index_2 = half_real_dim + n
122+
for j in 1:n
123+
for i in 1:j
124+
complex_index += 1
125+
real_index_1 += 1
126+
real_scalars[real_index_1] = complex_scalars[complex_index]
127+
real_index_2 += 1
128+
real_scalars[real_index_2] = complex_scalars[complex_index]
129+
if i == j
130+
real_index_2 += n
131+
end
132+
end
133+
end
134+
real_index_1 = half_real_dim
135+
real_index_2 = real_dim - n + 1
136+
for j in 1:n
137+
for i in 1:(j-1)
138+
complex_index += 1
139+
real_index_1 += 1
140+
real_scalars[real_index_1] = complex_scalars[complex_index]
141+
real_index_2 -= 1
142+
real_scalars[real_index_2] =
143+
MOI.Utilities.operate(-, T, complex_scalars[complex_index])
144+
end
145+
real_scalars[real_index_1+1] = zero(S)
146+
real_index_1 += n + 1
147+
real_index_2 -= 2 * (n - j) + 1
148+
end
149+
@assert length(complex_scalars) == complex_index
150+
return MOI.Utilities.vectorize(real_scalars)
151+
end
152+
153+
function MOI.Bridges.inverse_map_function(
154+
BT::Type{<:HermitianToSymmetricPSDBridge},
155+
func,
156+
)
157+
real_scalars = MOI.Utilities.eachscalar(func)
158+
real_set = MOI.Utilities.set_with_dimension(
159+
MOI.PositiveSemidefiniteConeTriangle,
160+
length(real_scalars),
161+
)
162+
@assert iseven(real_set.side_dimension)
163+
n = div(real_set.side_dimension, 2)
164+
complex_set = MOI.HermitianPositiveSemidefiniteConeTriangle(n)
165+
complex_scalars =
166+
Vector{eltype(real_scalars)}(undef, MOI.dimension(complex_set))
167+
real_index = 0
168+
complex_index = 0
169+
for j in 1:n
170+
for i in 1:j
171+
complex_index += 1
172+
real_index += 1
173+
complex_scalars[complex_index] = real_scalars[real_index]
174+
end
175+
end
176+
for j in 1:n
177+
for i in 1:(j-1)
178+
complex_index += 1
179+
real_index += 1
180+
complex_scalars[complex_index] = real_scalars[real_index]
181+
end
182+
real_index += n + 1
183+
end
184+
@assert length(complex_scalars) == complex_index
185+
return MOI.Utilities.vectorize(complex_scalars)
186+
end
187+
188+
function MOI.Bridges.adjoint_map_function(
189+
BT::Type{<:HermitianToSymmetricPSDBridge},
190+
func,
191+
)
192+
real_scalars = MOI.Utilities.eachscalar(func)
193+
real_dim = length(real_scalars)
194+
real_set = MOI.Utilities.set_with_dimension(
195+
MOI.PositiveSemidefiniteConeTriangle,
196+
real_dim,
197+
)
198+
@assert iseven(real_set.side_dimension)
199+
n = div(real_set.side_dimension, 2)
200+
complex_set = MOI.HermitianPositiveSemidefiniteConeTriangle(n)
201+
complex_scalars =
202+
Vector{eltype(real_scalars)}(undef, MOI.dimension(complex_set))
203+
complex_index = 0
204+
half_real_set = MOI.PositiveSemidefiniteConeTriangle(n)
205+
half_real_dim = MOI.dimension(half_real_set)
206+
real_index_1 = 0
207+
real_index_2 = half_real_dim + n
208+
for j in 1:n
209+
for i in 1:j
210+
complex_index += 1
211+
real_index_1 += 1
212+
real_index_2 += 1
213+
complex_scalars[complex_index] =
214+
real_scalars[real_index_1] + real_scalars[real_index_2]
215+
if i == j
216+
real_index_2 += n
217+
end
218+
end
219+
end
220+
real_index_1 = half_real_dim
221+
real_index_2 = real_dim - n + 1
222+
for j in 1:n
223+
for i in 1:(j-1)
224+
complex_index += 1
225+
real_index_1 += 1
226+
real_index_2 -= 1
227+
complex_scalars[complex_index] =
228+
real_scalars[real_index_1] - real_scalars[real_index_2]
229+
end
230+
real_index_1 += n + 1
231+
real_index_2 -= 2 * (n - j) + 1
232+
end
233+
@assert length(complex_scalars) == complex_index
234+
return MOI.Utilities.vectorize(complex_scalars)
235+
end
236+
237+
# FIXME
238+
# It's not so clear how to do this one since the adjoint is not invertible
239+
# and it's not obvious how to generate a PSD matrix in the preimage.
240+
# The following heuristic may not be the best:
241+
function MOI.Bridges.inverse_adjoint_map_function(
242+
BT::Type{<:HermitianToSymmetricPSDBridge{T}},
243+
func,
244+
) where {T}
245+
complex_scalars = MOI.Utilities.eachscalar(func)
246+
S = MOI.Utilities.scalar_type(_promote_minus_vcat(T, typeof(func)))
247+
complex_dim = length(complex_scalars)
248+
complex_set = MOI.Utilities.set_with_dimension(
249+
MOI.HermitianPositiveSemidefiniteConeTriangle,
250+
complex_dim,
251+
)
252+
n = complex_set.side_dimension
253+
real_set = MOI.PositiveSemidefiniteConeTriangle(2n)
254+
real_dim = MOI.dimension(real_set)
255+
real_scalars = Vector{S}(undef, real_dim)
256+
complex_index = 0
257+
half_real_set = MOI.PositiveSemidefiniteConeTriangle(n)
258+
half_real_dim = MOI.dimension(half_real_set)
259+
real_index_1 = 0
260+
real_index_2 = half_real_dim + n
261+
for j in 1:n
262+
for i in 1:j
263+
complex_index += 1
264+
real_index_1 += 1
265+
real_scalars[real_index_1] = complex_scalars[complex_index] / 2
266+
real_index_2 += 1
267+
real_scalars[real_index_2] = complex_scalars[complex_index] / 2
268+
if i == j
269+
real_index_2 += n
270+
end
271+
end
272+
end
273+
real_index_1 = half_real_dim
274+
real_index_2 = real_dim - n + 1
275+
for j in 1:n
276+
for i in 1:(j-1)
277+
complex_index += 1
278+
real_index_1 += 1
279+
real_scalars[real_index_1] = complex_scalars[complex_index]
280+
real_index_2 -= 1
281+
real_scalars[real_index_2] = zero(S)
282+
end
283+
real_scalars[real_index_1+1] = zero(S)
284+
real_index_1 += n + 1
285+
real_index_2 -= 2 * (n - j) + 1
286+
end
287+
@assert length(complex_scalars) == complex_index
288+
return MOI.Utilities.vectorize(real_scalars)
289+
end

src/Utilities/functions.jl

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,10 @@ end
403403
Type of functions obtained by indexing objects obtained by calling `eachscalar`
404404
on functions of type `F`.
405405
"""
406+
function scalar_type end
407+
408+
scalar_type(::Type{<:AbstractVector{T}}) where {T} = T
409+
406410
scalar_type(::Type{MOI.VectorOfVariables}) = MOI.VariableIndex
407411

408412
function scalar_type(::Type{MOI.VectorAffineFunction{T}}) where {T}
@@ -1527,10 +1531,14 @@ const TypedLike{T} = Union{TypedScalarLike{T},TypedVectorLike{T}}
15271531
###################################### +/- #####################################
15281532
## promote_operation
15291533

1534+
function promote_operation(::typeof(-), ::Type{T}, ::Type{T}) where {T}
1535+
return T
1536+
end
1537+
15301538
function promote_operation(
15311539
::typeof(-),
15321540
::Type{T},
1533-
::Type{<:ScalarAffineLike{T}},
1541+
::Type{<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}},
15341542
) where {T}
15351543
return MOI.ScalarAffineFunction{T}
15361544
end
@@ -2895,6 +2903,13 @@ function fill_constant(
28952903
return
28962904
end
28972905

2906+
"""
2907+
vectorize(x::AbstractVector{<:Number})
2908+
2909+
Returns `x`.
2910+
"""
2911+
vectorize(x::AbstractVector{<:Number}) = x
2912+
28982913
"""
28992914
vectorize(x::AbstractVector{MOI.VariableIndex})
29002915

src/Utilities/matrix_of_constraints.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,17 @@ function set_with_dimension(
576576
return S(isqrt(dim))
577577
end
578578

579+
function set_with_dimension(
580+
::Type{MOI.HermitianPositiveSemidefiniteConeTriangle},
581+
dim,
582+
)
583+
# We have `n*(n+1)/2 + n*(n-1)/2 = dim` so
584+
# `n² + n + n² - n = 2dim` hence `n² = dim`
585+
# This can be seen geometrically as the vectorization
586+
# contains the upper triangular followed by the strictly upper triangular.
587+
return MOI.HermitianPositiveSemidefiniteConeTriangle(isqrt(dim))
588+
end
589+
579590
function set_with_dimension(::Type{MOI.LogDetConeTriangle}, dim)
580591
side_dimension = side_dimension_for_vectorized_dimension(dim - 2)
581592
return MOI.LogDetConeTriangle(side_dimension)

src/Utilities/sets.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ Return the dimension `d` such that
130130
`MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(d))` is `n`.
131131
"""
132132
function side_dimension_for_vectorized_dimension(n::Base.Integer)
133+
# We have `d*(d+1)/2 = n` so
134+
# `d² + d - 2n = 0` hence `d = (-1 ± √(1 + 8d)) / 2`
135+
# The integer `√(1 + 8d)` is odd and `√(1 + 8d) - 1` is even.
136+
# We can drop the `- 1` as `div` already discards it.
133137
return div(isqrt(1 + 8n), 2)
134138
end
135139

0 commit comments

Comments
 (0)