Skip to content

Commit 8bcb841

Browse files
committed
Refactor how extensions for sparse square root are handled
1 parent bdb3c0f commit 8bcb841

8 files changed

Lines changed: 228 additions & 204 deletions

File tree

Project.toml

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

55
[deps]
66
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
7-
CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8"
87
CodecBzip2 = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd"
98
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
109
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
@@ -20,6 +19,7 @@ SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
2019
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2120

2221
[weakdeps]
22+
CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8"
2323
LDLFactorizations = "40e66cde-538c-5869-a4ad-c39174c6795b"
2424

2525
[extensions]
@@ -38,7 +38,7 @@ LDLFactorizations = "0.10"
3838
LinearAlgebra = "1"
3939
MutableArithmetics = "1"
4040
NaNMath = "0.3, 1"
41-
OrderedCollections = "1"
41+
OrderedCollections = "1.1"
4242
ParallelTestRunner = "2.4.1"
4343
PrecompileTools = "1"
4444
Printf = "1"

ext/MathOptInterfaceCliqueTreesExt.jl

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,28 @@
66

77
module MathOptInterfaceCliqueTreesExt
88

9-
import CliqueTrees.Multifrontal: ChordalCholesky
10-
import LinearAlgebra: RowMaximum, cholesky!
9+
import CliqueTrees
10+
import LinearAlgebra
1111
import MathOptInterface as MOI
12-
import SparseArrays: findnz, sparse
12+
import SparseArrays
1313

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
14+
MOI.Utilities.is_defined(::MOI.Utilities.CliqueTreesExt) = true
2115

22-
# Verify the factorization reconstructs Q. This catches indefinite
23-
# matrices where the diagonal is all zeros (e.g., [0 -1; -1 0]).
16+
function MOI.Utilities.compute_sparse_sqrt(
17+
::MOI.Utilities.CliqueTreesExt,
18+
Q::AbstractMatrix,
19+
)
20+
G = LinearAlgebra.cholesky!(
21+
CliqueTrees.Multifrontal.ChordalCholesky(Q),
22+
LinearAlgebra.RowMaximum(),
23+
)
24+
U = SparseArrays.sparse(G.U) * G.P
25+
# Verify the factorization reconstructs Q. We don't have something like
26+
# LinearAlgebra.issuccess(G)
2427
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))
28+
return nothing
3029
end
31-
32-
return findnz(U)
30+
return SparseArrays.findnz(U)
3331
end
3432

3533
end # module

ext/MathOptInterfaceLDLFactorizationsExt.jl

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,19 @@ import LinearAlgebra
1111
import MathOptInterface as MOI
1212
import SparseArrays
1313

14-
# The type signature of this function is not important, so long as it is more
15-
# specific than the (untyped) generic fallback with the error pointing to
16-
# LDLFactorizations.jl
17-
function MOI.Bridges.Constraint.compute_sparse_sqrt_fallback(
14+
MOI.Utilities.is_defined(::MOI.Utilities.LDLFactorizationsExt) = true
15+
16+
function MOI.Utilities.compute_sparse_sqrt(
17+
::MOI.Utilities.LDLFactorizationsExt,
1818
Q::AbstractMatrix,
19-
::F,
20-
::S,
21-
) where {F<:MOI.ScalarQuadraticFunction,S<:MOI.AbstractSet}
19+
)
2220
n = LinearAlgebra.checksquare(Q)
2321
factor = LDLFactorizations.ldl(Q)
24-
# Ideally we should use `LDLFactorizations.factorized(factor)` here, but it
25-
# has some false negatives. Instead we check that the factorization appeared
26-
# to work. This is a heuristic. There might be other cases where check is
27-
# insufficient.
28-
if minimum(factor.D) < 0 || any(issubnormal, factor.D)
29-
msg = """
30-
Unable to transform a quadratic constraint into a SecondOrderCone
31-
constraint because the quadratic constraint is not convex.
32-
"""
33-
throw(MOI.UnsupportedConstraint{F,S}(msg))
34-
end
35-
# We have Q = P' * L * D * L' * P. We want to find Q = U' * U, so
36-
# U = sqrt(D) * L' * P. First, compute L'. Note I and J are reversed:
37-
J, I, V = SparseArrays.findnz(factor.L)
38-
# Except L doesn't include the identity along the diagonal. Add it back.
39-
append!(J, 1:n)
40-
append!(I, 1:n)
41-
append!(V, ones(n))
42-
# Now scale by sqrt(D)
43-
for (k, i) in enumerate(I)
44-
V[k] *= sqrt(factor.D[i, i])
22+
if !LDLFactorizations.factorized(factor) || minimum(factor.D) < 0
23+
return nothing
4524
end
46-
# Finally, permute the columns of L'. The rows stay in the same order.
25+
L = sqrt.(factor.D) * LinearAlgebra.UnitLowerTriangular(factor.L)
26+
J, I, V = SparseArrays.findnz(SparseArrays.sparse(L))
4727
return I, factor.P[J], V
4828
end
4929

src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ end
6060
const QuadtoSOC{T,OT<:MOI.ModelLike} =
6161
SingleBridgeOptimizer{QuadtoSOCBridge{T},OT}
6262

63-
function compute_sparse_sqrt_fallback(Q, ::F, ::S) where {F,S}
64-
msg = """
63+
function _error_msg(::MOI.Utilities.LDLFactorizationsExt)
64+
return """
6565
Unable to transform a quadratic constraint into a SecondOrderCone
6666
constraint because the quadratic constraint is not strongly convex and
6767
our Cholesky decomposition failed.
@@ -76,36 +76,41 @@ function compute_sparse_sqrt_fallback(Q, ::F, ::S) where {F,S}
7676
LDLFactorizations.jl is not included by default because it is licensed
7777
under the LGPL.
7878
"""
79-
return throw(MOI.AddConstraintNotAllowed{F,S}(msg))
8079
end
8180

82-
function compute_sparse_sqrt(Q, func, set)
83-
# There's a big try-catch here because Cholesky can fail even if
84-
# `check = false`. As one example, it currently (v1.12) fails with
85-
# `BigFloat`. Similarly, we want to guard against errors in
86-
# `compute_sparse_sqrt_fallback`.
87-
#
88-
# The try-catch isn't a performance concern because the alternative is not
89-
# being able to reformulate the problem.
90-
try
91-
factor = LinearAlgebra.cholesky(Q; check = false)
92-
if !LinearAlgebra.issuccess(factor)
93-
return compute_sparse_sqrt_fallback(Q, func, set)
94-
end
95-
L, p = SparseArrays.sparse(factor.L), factor.p
96-
# We have Q = P' * L * L' * P. We want to find Q = U' * U, so U = L' * P
97-
# First, compute L'. Note I and J are reversed
98-
J, I, V = SparseArrays.findnz(L)
99-
# Then, we want to permute the columns of L'. The rows stay in the same
100-
# order.
101-
return I, p[J], V
102-
catch err
103-
if err isa MOI.AddConstraintNotAllowed
104-
rethrow(err)
105-
end
106-
msg = "There was an error computing a matrix square root"
107-
throw(MOI.UnsupportedConstraint{typeof(func),typeof(set)}(msg))
81+
function _error_msg(::MOI.Utilities.CliqueTreesExt)
82+
return """
83+
Unable to transform a quadratic constraint into a SecondOrderCone
84+
constraint because the quadratic constraint is not strongly convex and
85+
our Cholesky decomposition failed.
86+
87+
If the constraint is convex but not strongly convex, you can work-around
88+
this issue by manually installing and loading `CliqueTrees.jl`:
89+
```julia
90+
import Pkg; Pkg.add("CliqueTrees")
91+
using CliqueTrees
92+
```
93+
94+
CliqueTrees.jl is not included by default because it contains a number of
95+
heavy dependencies.
96+
"""
97+
end
98+
99+
function compute_sparse_sqrt(Q, ::F, ::S) where {F,S}
100+
if (ret = MOI.Utilities.compute_sparse_sqrt(Q)) !== nothing
101+
return ret
102+
elseif !MOI.Utilities.is_defined(MOI.Utilities.LDLFactorizationsExt())
103+
msg = _error_msg(MOI.Utilities.LDLFactorizationsExt())
104+
return throw(MOI.AddConstraintNotAllowed{F,S}(msg))
105+
elseif !MOI.Utilities.is_defined(MOI.Utilities.CliqueTreesExt())
106+
msg = _error_msg(MOI.Utilities.CliqueTreesExt())
107+
return throw(MOI.AddConstraintNotAllowed{F,S}(msg))
108108
end
109+
msg = """
110+
Unable to transform a quadratic constraint into a SecondOrderCone
111+
constraint because the quadratic constraint is not convex.
112+
"""
113+
return throw(MOI.UnsupportedConstraint{F,S}(msg))
109114
end
110115

111116
function bridge_constraint(

src/Utilities/Utilities.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,6 @@ include("lazy_iterators.jl")
7070
include("set_dot.jl")
7171

7272
include("distance_to_set.jl")
73+
include("sparse_square_root.jl")
7374

7475
end # module
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
abstract type AbstractExt end
8+
9+
is_defined(::AbstractExt) = false
10+
11+
struct LDLFactorizationsExt <: AbstractExt end
12+
13+
struct CliqueTreesExt <: AbstractExt end
14+
15+
struct LinearAlgebraExt <: AbstractExt end
16+
17+
is_defined(::LinearAlgebraExt) = true
18+
19+
function compute_sparse_sqrt(::LinearAlgebraExt, Q::AbstractMatrix)
20+
factor = LinearAlgebra.cholesky(Q; check = false)
21+
if !LinearAlgebra.issuccess(factor)
22+
return nothing
23+
end
24+
L, p = SparseArrays.sparse(factor.L), factor.p
25+
# We have Q = P' * L * L' * P. We want to find Q = U' * U, so U = L' * P
26+
# First, compute L'. Note I and J are reversed
27+
J, I, V = SparseArrays.findnz(L)
28+
# Then, we want to permute the columns of L'. The rows stay in the same
29+
# order.
30+
return I, p[J], V
31+
end
32+
33+
"""
34+
compute_sparse_sqrt(Q::AbstractMatrix)
35+
36+
Attempts to compute a sparse square root such that `Q = A' * A`.
37+
38+
## Return value
39+
40+
If successful, this function returns an `(I, J, V)` triplet of the sparse `A`
41+
matrix.
42+
43+
If unsuccessful, this function returns `nothing`.
44+
45+
## Extensions
46+
47+
By default, this function attempts to use a Cholesky decomposition. If that
48+
fails, it may optionally use various extension packages.
49+
50+
These extension packages must be loaded before calling `compute_sparse_sqrt`.
51+
52+
The extensions currently supported are:
53+
54+
* The LDL routine in `LDLFactorizations.jl`
55+
* The pivoted Cholesky in `CliqueTrees.jl`
56+
"""
57+
function compute_sparse_sqrt(Q::AbstractMatrix)
58+
# There's a big try-catch here because Cholesky can fail even if
59+
# `check = false`. The try-catch isn't a performance concern because the
60+
# alternative is not being able to reformulate the problem.
61+
for ext in (LinearAlgebraExt(), LDLFactorizationsExt(), CliqueTreesExt())
62+
if is_defined(ext)
63+
try
64+
if (ret = compute_sparse_sqrt(ext, Q)) !== nothing
65+
return ret
66+
end
67+
catch
68+
end
69+
end
70+
end
71+
return nothing
72+
end

0 commit comments

Comments
 (0)