Skip to content

Commit 9bf405b

Browse files
committed
Revert "Revert LDLFactorizations package extension (#2972)"
This reverts commit def69e1.
1 parent f9e0ab9 commit 9bf405b

File tree

4 files changed

+109
-20
lines changed

4 files changed

+109
-20
lines changed

Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1919

2020
[weakdeps]
2121
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
22+
LDLFactorizations = "40e66cde-538c-5869-a4ad-c39174c6795b"
2223

2324
[extensions]
2425
MathOptInterfaceBenchmarkToolsExt = "BenchmarkTools"
26+
MathOptInterfaceLDLFactorizationsExt = "LDLFactorizations"
2527

2628
[compat]
2729
BenchmarkTools = "1"
@@ -30,6 +32,7 @@ CodecZlib = "0.6, 0.7"
3032
ForwardDiff = "1"
3133
JSON = "0.21, 1"
3234
JSONSchema = "1"
35+
LDLFactorizations = "0.10"
3336
LinearAlgebra = "1"
3437
MutableArithmetics = "1"
3538
NaNMath = "0.3, 1"
@@ -44,8 +47,9 @@ julia = "1.10"
4447

4548
[extras]
4649
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
50+
LDLFactorizations = "40e66cde-538c-5869-a4ad-c39174c6795b"
4751
JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
4852
ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc"
4953

5054
[targets]
51-
test = ["BenchmarkTools", "JSONSchema", "ParallelTestRunner"]
55+
test = ["BenchmarkTools", "LDLFactorizations", "JSONSchema", "ParallelTestRunner"]
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 MathOptInterfaceLDLFactorizationsExt
8+
9+
import LDLFactorizations
10+
import LinearAlgebra
11+
import MathOptInterface as MOI
12+
import SparseArrays
13+
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(
18+
Q::AbstractMatrix,
19+
::F,
20+
::S,
21+
) where {F<:MOI.ScalarQuadraticFunction,S<:MOI.AbstractSet}
22+
n = LinearAlgebra.checksquare(Q)
23+
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])
45+
end
46+
# Finally, permute the columns of L'. The rows stay in the same order.
47+
return I, factor.P[J], V
48+
end
49+
50+
end # module

src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ 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 = """
65+
Unable to transform a quadratic constraint into a SecondOrderCone
66+
constraint because the quadratic constraint is not strongly convex and
67+
our Cholesky decomposition failed.
68+
69+
If the constraint is convex but not strongly convex, you can work-around
70+
this issue by manually installing and loading `LDLFactorizations.jl`:
71+
```julia
72+
import Pkg; Pkg.add("LDLFactorizations")
73+
using LDLFactorizations
74+
```
75+
76+
LDLFactorizations.jl is not included by default because it is licensed
77+
under the LGPL.
78+
"""
79+
return throw(MOI.AddConstraintNotAllowed{F,S}(msg))
80+
end
81+
6382
function compute_sparse_sqrt(Q, func, set)
6483
# There's a big try-catch here because Cholesky can fail even if
6584
# `check = false`. As one example, it currently (v1.12) fails with
@@ -69,20 +88,22 @@ function compute_sparse_sqrt(Q, func, set)
6988
# The try-catch isn't a performance concern because the alternative is not
7089
# being able to reformulate the problem.
7190
try
72-
factor = LinearAlgebra.cholesky(Q)
91+
factor = LinearAlgebra.cholesky(Q; check = false)
92+
if !LinearAlgebra.issuccess(factor)
93+
return compute_sparse_sqrt_fallback(Q, func, set)
94+
end
7395
L, p = SparseArrays.sparse(factor.L), factor.p
7496
# We have Q = P' * L * L' * P. We want to find Q = U' * U, so U = L' * P
7597
# First, compute L'. Note I and J are reversed
7698
J, I, V = SparseArrays.findnz(L)
7799
# Then, we want to permute the columns of L'. The rows stay in the same
78100
# order.
79101
return I, p[J], V
80-
catch
81-
msg = """
82-
Unable to transform a quadratic constraint into a SecondOrderCone
83-
constraint because the quadratic constraint is not strongly convex and
84-
our Cholesky decomposition failed.
85-
"""
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"
86107
throw(MOI.UnsupportedConstraint{typeof(func),typeof(set)}(msg))
87108
end
88109
end

test/Bridges/Constraint/test_QuadtoSOCBridge.jl

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import LinearAlgebra
1010
import SparseArrays
1111
using Test
1212

13+
import LDLFactorizations
1314
import MathOptInterface as MOI
1415

1516
function runtests()
@@ -344,16 +345,13 @@ function test_semidefinite_cholesky_fail()
344345
model = MOI.Bridges.Constraint.QuadtoSOC{Float64}(inner)
345346
x = MOI.add_variables(model, 2)
346347
f = 0.5 * x[1] * x[1] + 1.0 * x[1] * x[2] + 0.5 * x[2] * x[2]
347-
@test_throws(
348-
MOI.UnsupportedConstraint,
349-
MOI.add_constraint(model, f, MOI.LessThan(1.0)),
350-
)
351-
# F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
352-
# ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
353-
# g = MOI.get(inner, MOI.ConstraintFunction(), ci)
354-
# y = MOI.get(inner, MOI.ListOfVariableIndices())
355-
# sum_y = 1.0 * y[1] + 1.0 * y[2]
356-
# @test isapprox(g, MOI.Utilities.vectorize([1.0, 1.0, sum_y, 0.0]))
348+
c = MOI.add_constraint(model, f, MOI.LessThan(1.0))
349+
F, S = MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone
350+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
351+
g = MOI.get(inner, MOI.ConstraintFunction(), ci)
352+
y = MOI.get(inner, MOI.ListOfVariableIndices())
353+
sum_y = 1.0 * y[1] + 1.0 * y[2]
354+
@test isapprox(g, MOI.Utilities.vectorize([1.0, 1.0, sum_y, 0.0]))
357355
return
358356
end
359357

@@ -363,8 +361,12 @@ function test_compute_sparse_sqrt_edge_cases()
363361
[1.0 0.0; 0.0 2.0],
364362
# Cholesky works, with pivoting
365363
[1.0 0.0 1.0; 0.0 1.0 1.0; 1.0 1.0 3.0],
364+
# Cholesky fails due to 0 eigen value. LDL works
365+
[1.0 1.0; 1.0 1.0],
366366
# Cholesky succeeds, even though 0 eigen value
367367
[2.0 2.0; 2.0 2.0],
368+
# Cholesky fails because of 0 column/row. LDL works
369+
[2.0 0.0; 0.0 0.0],
368370
]
369371
B = SparseArrays.sparse(A)
370372
f = zero(MOI.ScalarQuadraticFunction{eltype(A)})
@@ -379,8 +381,6 @@ function test_compute_sparse_sqrt_edge_cases()
379381
# Test failures
380382
for A in Any[
381383
[-1.0 0.0; 0.0 1.0],
382-
[1.0 1.0; 1.0 1.0],
383-
[2.0 0.0; 0.0 0.0],
384384
# Found from test_quadratic_nonconvex_constraint_basic
385385
[0.0 -1.0; -1.0 0.0],
386386
# Different element type. We could potentially make this work in future,
@@ -399,6 +399,20 @@ function test_compute_sparse_sqrt_edge_cases()
399399
return
400400
end
401401

402+
function test_compute_sparse_sqrt_fallback()
403+
# Test the default fallback that is hit when LDLFactorizations isn't loaded.
404+
# We could put the test somewhere else so it runs before this file is
405+
# loaded, but that's pretty flakey for a long-term solution. Instead, we're
406+
# going to abuse the lack of a strong type signature to hit it:
407+
f = zero(MOI.ScalarAffineFunction{Float64})
408+
A = SparseArrays.sparse([-1.0 0.0; 0.0 1.0])
409+
@test_throws(
410+
MOI.AddConstraintNotAllowed{typeof(f),MOI.GreaterThan{Float64}},
411+
MOI.Bridges.Constraint.compute_sparse_sqrt(A, f, MOI.GreaterThan(0.0)),
412+
)
413+
return
414+
end
415+
402416
end # module
403417

404418
TestConstraintQuadToSOC.runtests()

0 commit comments

Comments
 (0)