Skip to content

Commit 9c9a097

Browse files
blegatmatbesancon
andauthored
Implement starting values for Constraint.QuadtoSOCBridge (#2115)
* Implement starting values for Constraint.QuadtoSOCBridge * Fixes * Fix format * Remove show * Add removed line back ?? * Update src/attributes.jl Co-authored-by: Mathieu Besançon <[email protected]> * Update src/attributes.jl Co-authored-by: Mathieu Besançon <[email protected]> * Update src/attributes.jl Co-authored-by: Mathieu Besançon <[email protected]> * Remove docstring notes * Add test with copy_to --------- Co-authored-by: Mathieu Besançon <[email protected]>
1 parent 3f44cba commit 9c9a097

File tree

2 files changed

+205
-26
lines changed

2 files changed

+205
-26
lines changed

src/Bridges/Constraint/bridges/quad_to_soc.jl

Lines changed: 148 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,25 @@
99
1010
`QuadtoSOCBridge` converts quadratic inequalities
1111
```math
12-
\\frac{1}{2}x^T Q x + a^T x + b \\le 0
12+
\\frac{1}{2}x^T Q x + a^T x \\le ub
1313
```
1414
into [`MOI.RotatedSecondOrderCone`](@ref) constraints, but it only applies when
1515
``Q`` is positive definite.
1616
1717
This is because, if `Q` is positive definite, there exists `U` such that
1818
``Q = U^T U``, and so the inequality can then be rewritten as;
1919
```math
20-
\\|U x\\|_2^2 \\le 2 (-a^T x - b)
20+
\\|U x\\|_2^2 \\le 2 (-a^T x + ub)
2121
```
2222
23-
Therefore, `QuadtoSOCBridge` implements the following reformulation:
23+
Therefore, `QuadtoSOCBridge` implements the following reformulations:
2424
25-
* ``\\frac{1}{2}x^T Q x + a^T x + b \\le 0`` into
26-
``(1, -a^T x - b, Ux) \\in RotatedSecondOrderCone``
25+
* ``\\frac{1}{2}x^T Q x + a^T x \\le ub`` into
26+
``(1, -a^T x + ub, Ux) \\in RotatedSecondOrderCone``
27+
where ``Q = U^T U``
28+
* ``\\frac{1}{2}x^T Q x + a^T x \\ge lb`` into
29+
``(1, a^T x - lb, Ux) \\in RotatedSecondOrderCone``
30+
where ``-Q = U^T U``
2731
2832
## Source node
2933
@@ -50,6 +54,7 @@ struct QuadtoSOCBridge{T} <: AbstractBridge
5054
dimension::Int # dimension of the SOC constraint
5155
less_than::Bool # whether the constraint was ≤ or ≥
5256
set_constant::T # the constant that was on the set
57+
index_to_variable_map::Vector{MOI.VariableIndex}
5358
end
5459

5560
const QuadtoSOC{T,OT<:MOI.ModelLike} =
@@ -84,17 +89,17 @@ function bridge_constraint(
8489
)
8590
end
8691
# Construct the VectorAffineFunction. We're aiming for:
87-
# | 1 |
88-
# | -a^T x - b | ∈ RotatedSecondOrderCone()
89-
# | Lx + 0 |
92+
# | 1 |
93+
# | -a^T x + ub | ∈ RotatedSecondOrderCone()
94+
# | Ux + 0 |
9095
# Start with the -a^T x terms...
9196
vector_terms = MOI.VectorAffineTerm{T}[
9297
MOI.VectorAffineTerm(
9398
2,
9499
MOI.ScalarAffineTerm(scale * term.coefficient, term.variable),
95100
) for term in func.affine_terms
96101
]
97-
# Add the Lx terms...
102+
# Note that `L = U'` so we add the terms of L'x
98103
L, p = SparseArrays.sparse(F.L), F.p
99104
I, J, V = SparseArrays.findnz(L)
100105
for i in 1:length(V)
@@ -108,31 +113,32 @@ function bridge_constraint(
108113
MOI.VectorAffineTerm(J[i] + 2, MOI.ScalarAffineTerm(V[i], xi)),
109114
)
110115
end
111-
# This is the [1, b, 0] vector...
116+
# This is the [1, ub, 0] vector...
112117
set_constant = MOI.constant(set)
113-
vector_constant = vcat(
114-
one(T),
115-
scale * (func.constant - set_constant),
116-
zeros(T, size(L, 1)),
117-
)
118+
MOI.throw_if_scalar_and_constant_not_zero(func, typeof(set))
119+
vector_constant = vcat(one(T), -scale * set_constant, zeros(T, size(L, 1)))
118120
f = MOI.VectorAffineFunction(vector_terms, vector_constant)
119121
dimension = MOI.output_dimension(f)
120122
soc = MOI.add_constraint(model, f, MOI.RotatedSecondOrderCone(dimension))
121-
return QuadtoSOCBridge(soc, dimension, less_than, set_constant)
123+
return QuadtoSOCBridge(
124+
soc,
125+
dimension,
126+
less_than,
127+
set_constant,
128+
index_to_variable_map,
129+
)
122130
end
123131

124132
function _matrix_from_quadratic_terms(
125133
terms::Vector{MOI.ScalarQuadraticTerm{T}},
126134
) where {T}
127135
variable_to_index_map = Dict{MOI.VariableIndex,Int}()
128-
index_to_variable_map = Dict{Int,MOI.VariableIndex}()
129-
n = 0
136+
index_to_variable_map = MOI.VariableIndex[]
130137
for term in terms
131138
for variable in (term.variable_1, term.variable_2)
132139
if !(variable in keys(variable_to_index_map))
133-
n += 1
134-
variable_to_index_map[variable] = n
135-
index_to_variable_map[n] = variable
140+
push!(index_to_variable_map, variable)
141+
variable_to_index_map[variable] = length(index_to_variable_map)
136142
end
137143
end
138144
end
@@ -150,6 +156,7 @@ function _matrix_from_quadratic_terms(
150156
end
151157
end
152158
# Duplicate terms are summed together in `sparse`
159+
n = length(index_to_variable_map)
153160
return SparseArrays.sparse(I, J, V, n, n), index_to_variable_map
154161
end
155162

@@ -209,13 +216,32 @@ function MOI.delete(model::MOI.ModelLike, bridge::QuadtoSOCBridge)
209216
return
210217
end
211218

212-
# Attributes, Bridge acting as a constraint
219+
function MOI.supports(
220+
model::MOI.ModelLike,
221+
attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart},
222+
::Type{QuadtoSOCBridge{T}},
223+
) where {T}
224+
F, S = MOI.VectorAffineFunction{T}, MOI.RotatedSecondOrderCone
225+
return MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) &&
226+
MOI.supports(model, attr, MOI.ConstraintIndex{F,S})
227+
end
228+
213229
function MOI.get(
214230
model::MOI.ModelLike,
215-
attr::MOI.ConstraintPrimal,
231+
attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart},
216232
bridge::QuadtoSOCBridge,
217233
)
234+
# The constraint primal is x'Qx/2 + a'x
235+
# If `less_than` then `Q = U'U` and we have the value of
236+
# `Ux` and `-a'x + ub`, so we get it with
237+
# `(Ux)'Ux / 2 - (-a'x + ub) + ub`
238+
# Otherwise, `Q = -U'U` and we have the value of
239+
# `Ux` and `a'x - ub`, so we get it with
240+
# `-(Ux)'Ux / 2 + (a'x - lb) + ub`
218241
soc = MOI.get(model, attr, bridge.soc)
242+
if soc === nothing
243+
return nothing
244+
end
219245
output = sum(soc[i]^2 for i in 3:bridge.dimension)
220246
output /= 2
221247
output -= soc[1] * soc[2]
@@ -226,6 +252,68 @@ function MOI.get(
226252
return output
227253
end
228254

255+
function _primal_start_or_error(model, attr, v)
256+
var_attr = MOI.VariablePrimalStart()
257+
value = MOI.get(model, MOI.VariablePrimalStart(), v)
258+
if isnothing(value)
259+
error(
260+
"In order to set the `$attr`, the",
261+
"`MOI.Bridges.Constraint.QuadtoSOCBridge` needs to get the ",
262+
"`$var_attr` but it is not set. Set the `$var_attr` first before ",
263+
"setting the `$attr` in order to fix this.",
264+
)
265+
end
266+
return value
267+
end
268+
269+
function MOI.set(
270+
model::MOI.ModelLike,
271+
attr::MOI.ConstraintPrimalStart,
272+
bridge::QuadtoSOCBridge{T},
273+
value,
274+
) where {T}
275+
# `value` represent `x'Qx/2 + a'x + ε` where `ε` is
276+
# the difference between the value of the slack variable and the value of the function.
277+
# That is, if `less_than`, we set
278+
# | 1 |
279+
# | -a'x - ε + ub |
280+
# | U * x |
281+
# which is obtained as
282+
# | 1 |
283+
# | x'Qx/2 - value + ub |
284+
# | U * x |
285+
# Otherwise, we set
286+
# | 1 |
287+
# | a'x + ε - lb |
288+
# | U * x |
289+
# which is obtained as
290+
# | 1 |
291+
# | value - x'Qx/2 - lb |
292+
# | U * x |
293+
# where we compute `x'Qx/2` and `U * x` using the starting values of the variable.
294+
soc = MOI.get(model, MOI.ConstraintFunction(), bridge.soc)
295+
Ux = MOI.Utilities.eval_variables(MOI.Utilities.eachscalar(soc)[3:end]) do v
296+
return _primal_start_or_error(model, attr, v)
297+
end
298+
if bridge.less_than
299+
s2 = Ux'Ux / 2 - value + bridge.set_constant
300+
else
301+
s2 = Ux'Ux / 2 + value - bridge.set_constant
302+
end
303+
MOI.set(model, attr, bridge.soc, [1; s2; Ux])
304+
return
305+
end
306+
307+
function MOI.set(
308+
model::MOI.ModelLike,
309+
attr::MOI.ConstraintPrimalStart,
310+
bridge::QuadtoSOCBridge,
311+
::Nothing,
312+
)
313+
MOI.set(model, attr, bridge.soc, nothing)
314+
return
315+
end
316+
229317
# Lemma: If (1, s, x), (v, u, y) in RotatedSecondOrderCone and
230318
# (1, s, x) ⋅ (v, u, y) = 0, then we have
231319
# y = -u*x, v = u*||x||_2^2/2.
@@ -261,13 +349,48 @@ end
261349
# is exactly the same. Q.E.D.
262350
function MOI.get(
263351
model::MOI.ModelLike,
264-
attr::MOI.ConstraintDual,
352+
attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart},
265353
bridge::QuadtoSOCBridge,
266354
)
267-
λ = MOI.get(model, attr, bridge.soc)[2]
355+
dual = MOI.get(model, attr, bridge.soc)
356+
if dual === nothing
357+
return nothing
358+
end
359+
λ = dual[2]
268360
return bridge.less_than ? -λ : λ
269361
end
270362

363+
# Let `(v, u, y)` be the dual of the RSOC and `λ` be the dual of the quadratic.
364+
# From same reasoning as above, we know that `u` is `-λ`.
365+
# From the Lemma above, we have ``
366+
# `y = -u * x`, `v = u*||x||_2^2/2`
367+
function MOI.set(
368+
model::MOI.ModelLike,
369+
attr::MOI.ConstraintDualStart,
370+
bridge::QuadtoSOCBridge{T},
371+
λ,
372+
) where {T}
373+
u = bridge.less_than ? -λ : λ
374+
x = T[
375+
_primal_start_or_error(model, attr, xi) for
376+
xi in bridge.index_to_variable_map
377+
]
378+
v = u * sum(x .^ 2) / 2
379+
y = -u * x
380+
MOI.set(model, attr, bridge.soc, [v; u; y])
381+
return
382+
end
383+
384+
function MOI.set(
385+
model::MOI.ModelLike,
386+
attr::MOI.ConstraintDualStart,
387+
bridge::QuadtoSOCBridge,
388+
::Nothing,
389+
)
390+
MOI.set(model, attr, bridge.soc, nothing)
391+
return
392+
end
393+
271394
function MOI.get(
272395
::MOI.ModelLike,
273396
::MOI.ConstraintSet,

test/Bridges/Constraint/quad_to_soc.jl

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ function test_quadratic_constraints_with_2_variables()
106106
end
107107

108108
function test_qcp()
109-
mock = MOI.Utilities.MockOptimizer(MOI.Utilities.Model{Float64}())
109+
model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
110+
mock = MOI.Utilities.MockOptimizer(model)
110111
config = MOI.Test.Config()
111112
bridged_mock = MOI.Bridges.Constraint.QuadtoSOC{Float64}(mock)
112113
MOI.Utilities.set_mock_optimize!(
@@ -159,6 +160,15 @@ function test_qcp()
159160
MOI.Test.test_quadratic_constraint_LessThan(bridged_mock, config)
160161
MOI.empty!(bridged_mock)
161162
MOI.Test.test_quadratic_constraint_GreaterThan(bridged_mock, config)
163+
F = MOI.ScalarQuadraticFunction{Float64}
164+
S = MOI.GreaterThan{Float64}
165+
ci = first(MOI.get(bridged_mock, MOI.ListOfConstraintIndices{F,S}()))
166+
for attr in [MOI.ConstraintPrimalStart(), MOI.ConstraintDualStart()]
167+
err = ErrorException(
168+
"In order to set the `$attr`, the`MOI.Bridges.Constraint.QuadtoSOCBridge` needs to get the `MathOptInterface.VariablePrimalStart()` but it is not set. Set the `MathOptInterface.VariablePrimalStart()` first before setting the `$attr` in order to fix this.",
169+
)
170+
@test_throws err MOI.set(bridged_mock, attr, ci, 0.0)
171+
end
162172
return
163173
end
164174

@@ -207,6 +217,7 @@ function test_runtests()
207217
variables: x, y
208218
[1.0, 0.0, 2.0 * x, 2.0 * y] in RotatedSecondOrderCone(4)
209219
""",
220+
variable_start = 0.0,
210221
)
211222
MOI.Bridges.runtests(
212223
MOI.Bridges.Constraint.QuadtoSOCBridge,
@@ -219,9 +230,54 @@ function test_runtests()
219230
[1.0, 0.0, 2.0 * x, 2.0 * y] in RotatedSecondOrderCone(4)
220231
""",
221232
)
233+
MOI.Bridges.runtests(
234+
MOI.Bridges.Constraint.QuadtoSOCBridge,
235+
"""
236+
variables: x, y
237+
2.0 * x * x + 0.5 * y * y + 2.0 * x <= 3.0
238+
""",
239+
"""
240+
variables: x, y
241+
[1.0, -2.0 * x + 3.0, 2.0 * x, 1.0 * y] in RotatedSecondOrderCone(4)
242+
""",
243+
)
244+
MOI.Bridges.runtests(
245+
MOI.Bridges.Constraint.QuadtoSOCBridge,
246+
"""
247+
variables: x, y, z
248+
-2.0 * x * x + -1.0 * y * y + -2.0 * x * y + 3.0 * z >= 4.0
249+
""",
250+
"""
251+
variables: x, y, z
252+
[1.0, 3.0 * z + -4.0, 2.0 * x + 1.0 * y, 1.0 * y] in RotatedSecondOrderCone(4)
253+
""",
254+
)
222255
return
223256
end
224257

258+
"""
259+
test_copy_to_start()
260+
261+
Tests that `copy_to` copies the variable starting values first and the
262+
constraint primal and dual starting values second as is needed by the
263+
[`MOI.Bridges.Constraint.QuadtoSOCBridge`](@ref).
264+
"""
265+
function test_copy_to_start()
266+
src = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
267+
x = MOI.add_variable(src)
268+
c = MOI.add_constraint(src, 1.0x * x + 2.0x, MOI.LessThan(-1.0))
269+
MOI.set(src, MOI.ConstraintPrimalStart(), c, 1.0)
270+
MOI.set(src, MOI.ConstraintDualStart(), c, -1.0)
271+
MOI.set(src, MOI.VariablePrimalStart(), x, -1.0)
272+
273+
model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
274+
dest = MOI.Bridges.Constraint.QuadtoSOC{Float64}(model)
275+
index_map = MOI.copy_to(dest, src)
276+
@test MOI.get(dest, MOI.ConstraintPrimalStart(), index_map[c]) == 1.0
277+
@test MOI.get(dest, MOI.ConstraintDualStart(), index_map[c]) == -1.0
278+
@test MOI.get(dest, MOI.VariablePrimalStart(), index_map[x]) == -1.0
279+
end
280+
225281
end # module
226282

227283
TestConstraintQuadToSOC.runtests()

0 commit comments

Comments
 (0)