Skip to content

Commit f267f66

Browse files
committed
Add bridge attribute for choosing the Cholesky method
1 parent f604fc1 commit f267f66

2 files changed

Lines changed: 186 additions & 22 deletions

File tree

src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@
44
# Use of this source code is governed by an MIT-style license that can be found
55
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
66

7+
"""
8+
QuadtoSOCSquareRoot <: MOI.AbstractConstraintAttribute
9+
10+
Constraint attribute to override the square root method used by
11+
[`QuadtoSOCBridge`](@ref) for a specific quadratic constraint.
12+
13+
When set, this takes precedence over the default behavior of trying all
14+
available methods. When not set, [`MOI.get`](@ref) returns `nothing`.
15+
16+
## Example
17+
18+
```julia
19+
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
20+
MOI.set(
21+
model,
22+
MOI.Bridges.Constraint.QuadtoSOCSquareRoot(),
23+
c,
24+
MOI.Bridges.Constraint._LinearAlgebra(),
25+
)
26+
```
27+
"""
28+
struct QuadtoSOCSquareRoot <: MOI.AbstractConstraintAttribute end
29+
730
"""
831
QuadtoSOCBridge{T} <: Bridges.Constraint.AbstractBridge
932
@@ -46,15 +69,22 @@ Therefore, `QuadtoSOCBridge` implements the following reformulations:
4669
4770
This bridge errors if `Q` is not positive definite.
4871
"""
49-
struct QuadtoSOCBridge{T} <: AbstractBridge
50-
soc::MOI.ConstraintIndex{
51-
MOI.VectorAffineFunction{T},
52-
MOI.RotatedSecondOrderCone,
72+
mutable struct QuadtoSOCBridge{T} <: AbstractBridge
73+
soc::Union{
74+
Nothing,
75+
MOI.ConstraintIndex{
76+
MOI.VectorAffineFunction{T},
77+
MOI.RotatedSecondOrderCone,
78+
},
5379
}
5480
dimension::Int # dimension of the SOC constraint
5581
less_than::Bool # whether the constraint was ≤ or ≥
5682
set_constant::T # the constant that was on the set
5783
index_to_variable_map::Vector{MOI.VariableIndex}
84+
# Stored for final_touch
85+
func::MOI.ScalarQuadraticFunction{T}
86+
set::Union{MOI.LessThan{T},MOI.GreaterThan{T}}
87+
method::Union{Nothing,_AbstractExt}
5888
end
5989

6090
const QuadtoSOC{T,OT<:MOI.ModelLike} =
@@ -157,13 +187,63 @@ function bridge_constraint(
157187
func::MOI.ScalarQuadraticFunction{T},
158188
set::Union{MOI.LessThan{T},MOI.GreaterThan{T}},
159189
) where {T}
190+
# Delay reformulation until `final_touch` so that the
191+
# `QuadtoSOCSquareRoot` attribute can override the method first.
160192
less_than = set isa MOI.LessThan{T}
193+
set_constant = MOI.constant(set)
194+
MOI.throw_if_scalar_and_constant_not_zero(func, typeof(set))
195+
return QuadtoSOCBridge{T}(
196+
nothing,
197+
0,
198+
less_than,
199+
set_constant,
200+
MOI.VariableIndex[],
201+
func,
202+
set,
203+
nothing,
204+
)
205+
end
206+
207+
MOI.supports(::MOI.ModelLike, ::QuadtoSOCSquareRoot, ::Type{<:QuadtoSOCBridge}) =
208+
true
209+
210+
function MOI.set(
211+
::MOI.ModelLike,
212+
::QuadtoSOCSquareRoot,
213+
bridge::QuadtoSOCBridge,
214+
value::Union{Nothing,_AbstractExt},
215+
)
216+
bridge.method = value
217+
return
218+
end
219+
220+
function MOI.get(
221+
::MOI.ModelLike,
222+
::QuadtoSOCSquareRoot,
223+
bridge::QuadtoSOCBridge,
224+
)
225+
return bridge.method
226+
end
227+
228+
MOI.Bridges.needs_final_touch(::QuadtoSOCBridge) = true
229+
230+
function MOI.Bridges.final_touch(
231+
bridge::QuadtoSOCBridge{T},
232+
model::MOI.ModelLike,
233+
) where {T}
234+
if bridge.soc !== nothing
235+
return
236+
end
237+
func = bridge.func
238+
set = bridge.set
239+
less_than = bridge.less_than
161240
scale = less_than ? -1 : 1
162241
Q, index_to_variable_map =
163242
_matrix_from_quadratic_terms(func.quadratic_terms)
164243
if !less_than
165244
LinearAlgebra.rmul!(Q, -1)
166245
end
246+
bridge.index_to_variable_map = index_to_variable_map
167247
# Construct the VectorAffineFunction. We're aiming for:
168248
# | 1 |
169249
# | -a^T x + ub | ∈ RotatedSecondOrderCone()
@@ -175,10 +255,15 @@ function bridge_constraint(
175255
MOI.ScalarAffineTerm(scale * term.coefficient, term.variable),
176256
) for term in func.affine_terms
177257
]
178-
sqrt_ret = _compute_sparse_sqrt(LinearAlgebra.Symmetric(Q))
258+
Q_sym = LinearAlgebra.Symmetric(Q)
259+
sqrt_ret = if bridge.method !== nothing
260+
_compute_sparse_sqrt(bridge.method, Q_sym)
261+
else
262+
_compute_sparse_sqrt(Q_sym)
263+
end
179264
if sqrt_ret === nothing
180265
msg = _get_sqrt_error_message(is_defined(_CliqueTrees()))
181-
return throw(MOI.UnsupportedConstraint{typeof(func),typeof(set)}(msg))
266+
throw(MOI.UnsupportedConstraint{typeof(func),typeof(set)}(msg))
182267
end
183268
for (i, j, v) in zip(sqrt_ret[1], sqrt_ret[2], sqrt_ret[3])
184269
push!(
@@ -190,19 +275,13 @@ function bridge_constraint(
190275
)
191276
end
192277
# This is the [1, ub, 0] vector...
193-
set_constant = MOI.constant(set)
194-
MOI.throw_if_scalar_and_constant_not_zero(func, typeof(set))
278+
set_constant = bridge.set_constant
195279
vector_constant = vcat(one(T), -scale * set_constant, zeros(T, size(Q, 1)))
196280
f = MOI.VectorAffineFunction(vector_terms, vector_constant)
197-
dimension = MOI.output_dimension(f)
198-
soc = MOI.add_constraint(model, f, MOI.RotatedSecondOrderCone(dimension))
199-
return QuadtoSOCBridge(
200-
soc,
201-
dimension,
202-
less_than,
203-
set_constant,
204-
index_to_variable_map,
205-
)
281+
bridge.dimension = MOI.output_dimension(f)
282+
bridge.soc =
283+
MOI.add_constraint(model, f, MOI.RotatedSecondOrderCone(bridge.dimension))
284+
return
206285
end
207286

208287
function _matrix_from_quadratic_terms(
@@ -267,13 +346,13 @@ end
267346

268347
# Attributes, Bridge acting as a model
269348
function MOI.get(
270-
::QuadtoSOCBridge{T},
349+
bridge::QuadtoSOCBridge{T},
271350
::MOI.NumberOfConstraints{
272351
MOI.VectorAffineFunction{T},
273352
MOI.RotatedSecondOrderCone,
274353
},
275354
)::Int64 where {T}
276-
return 1
355+
return bridge.soc === nothing ? 0 : 1
277356
end
278357

279358
function MOI.get(
@@ -283,12 +362,21 @@ function MOI.get(
283362
MOI.RotatedSecondOrderCone,
284363
},
285364
) where {T}
365+
if bridge.soc === nothing
366+
return MOI.ConstraintIndex{
367+
MOI.VectorAffineFunction{T},
368+
MOI.RotatedSecondOrderCone,
369+
}[]
370+
end
286371
return [bridge.soc]
287372
end
288373

289374
# References
290375
function MOI.delete(model::MOI.ModelLike, bridge::QuadtoSOCBridge)
291-
MOI.delete(model, bridge.soc)
376+
if bridge.soc !== nothing
377+
MOI.delete(model, bridge.soc)
378+
bridge.soc = nothing
379+
end
292380
return
293381
end
294382

@@ -485,6 +573,9 @@ function MOI.get(
485573
attr::MOI.ConstraintFunction,
486574
b::QuadtoSOCBridge{T},
487575
) where {T}
576+
if b.soc === nothing
577+
return copy(b.func)
578+
end
488579
f = MOI.get(model, attr, b.soc)
489580
fs = MOI.Utilities.eachscalar(f)
490581
q = zero(MOI.ScalarQuadraticFunction{T})

test/Bridges/Constraint/test_QuadtoSOCBridge.jl

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ function test_error_for_nonconvex_quadratic_constraints()
3131
model = MOI.Bridges.Constraint.QuadtoSOC{Float64}(inner)
3232
x = MOI.add_variable(model)
3333
F = MOI.ScalarQuadraticFunction{Float64}
34+
# Error is now thrown at final_touch, not add_constraint
35+
MOI.add_constraint(model, 1.0 * x * x, MOI.GreaterThan(0.0))
3436
@test_throws(
3537
MOI.UnsupportedConstraint{F,MOI.GreaterThan{Float64}},
36-
MOI.add_constraint(model, 1.0 * x * x, MOI.GreaterThan(0.0))
38+
MOI.Bridges.final_touch(model),
3739
)
40+
MOI.empty!(model)
41+
MOI.add_variable(model)
42+
x = MOI.get(model, MOI.ListOfVariableIndices())[1]
43+
MOI.add_constraint(model, -1.0 * x * x, MOI.LessThan(0.0))
3844
@test_throws(
3945
MOI.UnsupportedConstraint{F,MOI.LessThan{Float64}},
40-
MOI.add_constraint(model, -1.0 * x * x, MOI.LessThan(0.0))
46+
MOI.Bridges.final_touch(model),
4147
)
4248
return
4349
end
@@ -65,6 +71,7 @@ function test_quadratic_constraints_with_2_variables()
6571
),
6672
)
6773
MOI.Test.test_constraint_qcp_duplicate_off_diagonal(bridged_mock, config)
74+
MOI.Bridges.final_touch(bridged_mock)
6875
ci = first(
6976
MOI.get(
7077
mock,
@@ -164,6 +171,7 @@ function test_fill_reducing_permutation()
164171
Q = Float64[2 1 1; 1 2 0; 1 0 2]
165172
f = 0.5 * x' * Q * x
166173
MOI.add_constraint(bridge, f, MOI.LessThan(2.0))
174+
MOI.Bridges.final_touch(bridge)
167175
indices = MOI.get(
168176
model,
169177
MOI.ListOfConstraintIndices{
@@ -331,6 +339,7 @@ function test_semidefinite_cholesky_fail()
331339
x = MOI.add_variables(model, 2)
332340
f = 0.5 * x[1] * x[1] + 1.0 * x[1] * x[2] + 0.5 * x[2] * x[2]
333341
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
342+
MOI.Bridges.final_touch(model)
334343
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
335344
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
336345
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
@@ -435,6 +444,7 @@ function test_clique_trees_semidefinite_cholesky_fail()
435444
x = MOI.add_variables(model, 2)
436445
f = 0.5 * x[1] * x[1] + 1.0 * x[1] * x[2] + 0.5 * x[2] * x[2]
437446
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
447+
MOI.Bridges.final_touch(model)
438448
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
439449
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
440450
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
@@ -454,6 +464,7 @@ function test_clique_trees_early_zero_pivot()
454464
# Q = [1 1 0; 1 1 0; 0 0 1]
455465
f = sum(0.5 * x[i] * x[i] for i in 1:3) + 1.0 * x[1] * x[2]
456466
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
467+
MOI.Bridges.final_touch(model)
457468
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
458469
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
459470
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
@@ -482,6 +493,68 @@ function test_is_defined_default_fallback()
482493
return
483494
end
484495

496+
function test_quad_to_soc_square_root_attribute()
497+
inner = MOI.Utilities.Model{Float64}()
498+
model = MOI.Bridges.Constraint.QuadtoSOC{Float64}(inner)
499+
x = MOI.add_variables(model, 2)
500+
f = 1.0 * x[1] * x[1] + 1.0 * x[2] * x[2]
501+
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
502+
attr = MOI.Bridges.Constraint.QuadtoSOCSquareRoot()
503+
F = MOI.ScalarQuadraticFunction{Float64}
504+
S = MOI.LessThan{Float64}
505+
@test MOI.supports(model, attr, MOI.ConstraintIndex{F,S})
506+
# Default is nothing
507+
@test MOI.get(model, attr, c) === nothing
508+
# Set to _LinearAlgebra
509+
la = MOI.Bridges.Constraint._LinearAlgebra()
510+
MOI.set(model, attr, c, la)
511+
@test MOI.get(model, attr, c) === la
512+
# final_touch uses the specified method
513+
MOI.Bridges.final_touch(model)
514+
F2, S2 = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
515+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F2,S2}()))
516+
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
517+
@test MOI.output_dimension(g) == 4 # [1, rhs, Ux...]
518+
return
519+
end
520+
521+
function test_quad_to_soc_square_root_attribute_clique_trees()
522+
inner = MOI.Utilities.Model{Float64}()
523+
model = MOI.Bridges.Constraint.QuadtoSOC{Float64}(inner)
524+
x = MOI.add_variables(model, 2)
525+
f = 1.0 * x[1] * x[1] + 1.0 * x[2] * x[2]
526+
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
527+
attr = MOI.Bridges.Constraint.QuadtoSOCSquareRoot()
528+
ct = MOI.Bridges.Constraint._CliqueTrees()
529+
MOI.set(model, attr, c, ct)
530+
@test MOI.get(model, attr, c) === ct
531+
MOI.Bridges.final_touch(model)
532+
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
533+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
534+
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
535+
@test MOI.output_dimension(g) == 4
536+
return
537+
end
538+
539+
function test_quad_to_soc_square_root_attribute_reset()
540+
inner = MOI.Utilities.Model{Float64}()
541+
model = MOI.Bridges.Constraint.QuadtoSOC{Float64}(inner)
542+
x = MOI.add_variables(model, 2)
543+
f = 1.0 * x[1] * x[1] + 1.0 * x[2] * x[2]
544+
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
545+
attr = MOI.Bridges.Constraint.QuadtoSOCSquareRoot()
546+
la = MOI.Bridges.Constraint._LinearAlgebra()
547+
MOI.set(model, attr, c, la)
548+
# Reset to nothing (default behavior)
549+
MOI.set(model, attr, c, nothing)
550+
@test MOI.get(model, attr, c) === nothing
551+
MOI.Bridges.final_touch(model)
552+
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
553+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
554+
@test ci isa MOI.ConstraintIndex
555+
return
556+
end
557+
485558
end # module
486559

487560
TestConstraintQuadToSOC.runtests()

0 commit comments

Comments
 (0)