Skip to content

Commit 26563e3

Browse files
samuelsonricodow
authored andcommitted
Pivoted sparse Cholesky for QuadtoSOCBridge.
1 parent 667a148 commit 26563e3

3 files changed

Lines changed: 149 additions & 2 deletions

File tree

Project.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "1.50.0"
44

55
[deps]
66
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
7+
CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8"
78
CodecBzip2 = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd"
89
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
910
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
@@ -22,10 +23,12 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2223
LDLFactorizations = "40e66cde-538c-5869-a4ad-c39174c6795b"
2324

2425
[extensions]
26+
MathOptInterfaceCliqueTreesExt = "CliqueTrees"
2527
MathOptInterfaceLDLFactorizationsExt = "LDLFactorizations"
2628

2729
[compat]
2830
BenchmarkTools = "1"
31+
CliqueTrees = "1.17"
2932
CodecBzip2 = "0.6, 0.7, 0.8"
3033
CodecZlib = "0.6, 0.7"
3134
ForwardDiff = "1"
@@ -45,9 +48,10 @@ Test = "1"
4548
julia = "1.10"
4649

4750
[extras]
48-
LDLFactorizations = "40e66cde-538c-5869-a4ad-c39174c6795b"
51+
CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8"
4952
JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
53+
LDLFactorizations = "40e66cde-538c-5869-a4ad-c39174c6795b"
5054
ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc"
5155

5256
[targets]
53-
test = ["LDLFactorizations", "JSONSchema", "ParallelTestRunner"]
57+
test = ["CliqueTrees", "LDLFactorizations", "JSONSchema", "ParallelTestRunner"]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
module MathOptInterfaceCliqueTreesExt
8+
9+
import CliqueTrees.Multifrontal: ChordalCholesky
10+
import LinearAlgebra: RowMaximum, cholesky!
11+
import MathOptInterface as MOI
12+
import SparseArrays: findnz, sparse
13+
14+
function MOI.Bridges.Constraint.compute_sparse_sqrt_fallback(
15+
Q::AbstractMatrix,
16+
::F,
17+
::S,
18+
) where {F<:MOI.ScalarQuadraticFunction,S<:MOI.AbstractSet}
19+
G = cholesky!(ChordalCholesky(Q), RowMaximum())
20+
U = sparse(G.U) * G.P
21+
22+
# Verify the factorization reconstructs Q. This catches indefinite
23+
# matrices where the diagonal is all zeros (e.g., [0 -1; -1 0]).
24+
if !isapprox(Q, U' * U; atol = 1e-10)
25+
msg = """
26+
Unable to transform a quadratic constraint into a SecondOrderCone
27+
constraint because the quadratic constraint is not convex.
28+
"""
29+
throw(MOI.UnsupportedConstraint{F,S}(msg))
30+
end
31+
32+
return findnz(U)
33+
end
34+
35+
end # module
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
module TestConstraintQuadToSOCCliqueTrees
8+
9+
import SparseArrays
10+
using Test
11+
12+
import CliqueTrees
13+
import MathOptInterface as MOI
14+
15+
function runtests()
16+
for name in names(@__MODULE__; all = true)
17+
if startswith("$(name)", "test_")
18+
@testset "$(name)" begin
19+
getfield(@__MODULE__, name)()
20+
end
21+
end
22+
end
23+
return
24+
end
25+
26+
function test_compute_sparse_sqrt_edge_cases()
27+
for A in Any[
28+
# Trivial Cholesky
29+
[1.0 0.0; 0.0 2.0],
30+
# Cholesky works, with pivoting
31+
[1.0 0.0 1.0; 0.0 1.0 1.0; 1.0 1.0 3.0],
32+
# Cholesky fails due to 0 eigenvalue. Pivoted Cholesky works.
33+
[1.0 1.0; 1.0 1.0],
34+
# Cholesky succeeds, even though 0 eigenvalue
35+
[2.0 2.0; 2.0 2.0],
36+
# Cholesky fails because of 0 column/row. Pivoted Cholesky works.
37+
[2.0 0.0; 0.0 0.0],
38+
# Early zero pivot - this case breaks LDLFactorizations but works
39+
# with CliqueTrees' pivoted Cholesky.
40+
[1.0 1.0 0.0; 1.0 1.0 0.0; 0.0 0.0 1.0],
41+
]
42+
B = SparseArrays.sparse(A)
43+
f = zero(MOI.ScalarQuadraticFunction{eltype(A)})
44+
s = MOI.GreaterThan(zero(eltype(A)))
45+
I, J, V = MOI.Bridges.Constraint.compute_sparse_sqrt(B, f, s)
46+
U = zeros(eltype(A), size(A))
47+
for (i, j, v) in zip(I, J, V)
48+
U[i, j] += v
49+
end
50+
@test isapprox(A, U' * U; atol = 1e-10)
51+
end
52+
# Test failures
53+
for A in Any[
54+
[-1.0 0.0; 0.0 1.0],
55+
# Indefinite matrix
56+
[0.0 -1.0; -1.0 0.0],
57+
# BigFloat not supported
58+
BigFloat[-1.0 0.0; 0.0 1.0],
59+
BigFloat[1.0 0.0; 0.0 2.0],
60+
BigFloat[1.0 1.0; 1.0 1.0],
61+
]
62+
B = SparseArrays.sparse(A)
63+
f = zero(MOI.ScalarQuadraticFunction{eltype(A)})
64+
s = MOI.GreaterThan(zero(eltype(A)))
65+
@test_throws(
66+
MOI.UnsupportedConstraint{typeof(f),typeof(s)},
67+
MOI.Bridges.Constraint.compute_sparse_sqrt(B, f, s),
68+
)
69+
end
70+
return
71+
end
72+
73+
function test_semidefinite_cholesky_fail()
74+
inner = MOI.Utilities.Model{Float64}()
75+
model = MOI.Bridges.Constraint.QuadtoSOC{Float64}(inner)
76+
x = MOI.add_variables(model, 2)
77+
f = 0.5 * x[1] * x[1] + 1.0 * x[1] * x[2] + 0.5 * x[2] * x[2]
78+
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
79+
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
80+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
81+
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
82+
y = MOI.get(inner, MOI.ListOfVariableIndices())
83+
sum_y = 1.0 * y[1] + 1.0 * y[2]
84+
@test isapprox(g, MOI.Utilities.vectorize([1.0, 1.0, sum_y, 0.0]))
85+
return
86+
end
87+
88+
function test_early_zero_pivot()
89+
# This matrix has an early zero pivot that causes LDLFactorizations to
90+
# halt early, but CliqueTrees' pivoted Cholesky handles it correctly.
91+
inner = MOI.Utilities.Model{Float64}()
92+
model = MOI.Bridges.Constraint.QuadtoSOC{Float64}(inner)
93+
x = MOI.add_variables(model, 3)
94+
# (x[1] + x[2])^2 + x[3]^2 = x[1]^2 + 2*x[1]*x[2] + x[2]^2 + x[3]^2
95+
# Q = [1 1 0; 1 1 0; 0 0 1]
96+
f = 0.5 * x[1] * x[1] + 1.0 * x[1] * x[2] + 0.5 * x[2] * x[2] + 0.5 * x[3] * x[3]
97+
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
98+
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
99+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
100+
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
101+
# Verify the constraint was created successfully
102+
@test MOI.output_dimension(g) == 5 # [1, rhs, Ux...]
103+
return
104+
end
105+
106+
end # module
107+
108+
TestConstraintQuadToSOCCliqueTrees.runtests()

0 commit comments

Comments
 (0)