Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

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

[extensions]
MathOptInterfaceCliqueTreesExt = "CliqueTrees"
MathOptInterfaceLDLFactorizationsExt = "LDLFactorizations"

[compat]
BenchmarkTools = "1"
CliqueTrees = "1.17"
CodecBzip2 = "0.6, 0.7, 0.8"
CodecZlib = "0.6, 0.7"
ForwardDiff = "1"
Expand All @@ -35,7 +38,7 @@ LDLFactorizations = "0.10"
LinearAlgebra = "1"
MutableArithmetics = "1"
NaNMath = "0.3, 1"
OrderedCollections = "1"
OrderedCollections = "1.1"
ParallelTestRunner = "2.4.1"
PrecompileTools = "1"
Printf = "1"
Expand All @@ -45,9 +48,10 @@ Test = "1"
julia = "1.10"

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

[targets]
test = ["LDLFactorizations", "JSONSchema", "ParallelTestRunner"]
test = ["CliqueTrees", "LDLFactorizations", "JSONSchema", "ParallelTestRunner"]
33 changes: 33 additions & 0 deletions ext/MathOptInterfaceCliqueTreesExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright (c) 2017: Miles Lubin and contributors
# Copyright (c) 2017: Google Inc.
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

module MathOptInterfaceCliqueTreesExt

import CliqueTrees
import LinearAlgebra
import MathOptInterface as MOI
import SparseArrays

MOI.Utilities.is_defined(::MOI.Utilities.CliqueTreesExt) = true

function MOI.Utilities.compute_sparse_sqrt(
::MOI.Utilities.CliqueTreesExt,
Q::AbstractMatrix,
)
G = LinearAlgebra.cholesky!(
CliqueTrees.Multifrontal.ChordalCholesky(Q),
LinearAlgebra.RowMaximum(),
)
U = SparseArrays.sparse(G.U) * G.P
# Verify the factorization reconstructs Q. We don't have something like
# LinearAlgebra.issuccess(G)
if !isapprox(Q, U' * U; atol = 1e-10)
return nothing
end
return SparseArrays.findnz(U)
end

end # module
38 changes: 9 additions & 29 deletions ext/MathOptInterfaceLDLFactorizationsExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,19 @@ import LinearAlgebra
import MathOptInterface as MOI
import SparseArrays

# The type signature of this function is not important, so long as it is more
# specific than the (untyped) generic fallback with the error pointing to
# LDLFactorizations.jl
function MOI.Bridges.Constraint.compute_sparse_sqrt_fallback(
MOI.Utilities.is_defined(::MOI.Utilities.LDLFactorizationsExt) = true

function MOI.Utilities.compute_sparse_sqrt(
::MOI.Utilities.LDLFactorizationsExt,
Q::AbstractMatrix,
::F,
::S,
) where {F<:MOI.ScalarQuadraticFunction,S<:MOI.AbstractSet}
)
n = LinearAlgebra.checksquare(Q)
factor = LDLFactorizations.ldl(Q)
# Ideally we should use `LDLFactorizations.factorized(factor)` here, but it
# has some false negatives. Instead we check that the factorization appeared
# to work. This is a heuristic. There might be other cases where check is
# insufficient.
if minimum(factor.D) < 0 || any(issubnormal, factor.D)
msg = """
Unable to transform a quadratic constraint into a SecondOrderCone
constraint because the quadratic constraint is not convex.
"""
throw(MOI.UnsupportedConstraint{F,S}(msg))
end
# We have Q = P' * L * D * L' * P. We want to find Q = U' * U, so
# U = sqrt(D) * L' * P. First, compute L'. Note I and J are reversed:
J, I, V = SparseArrays.findnz(factor.L)
# Except L doesn't include the identity along the diagonal. Add it back.
append!(J, 1:n)
append!(I, 1:n)
append!(V, ones(n))
# Now scale by sqrt(D)
for (k, i) in enumerate(I)
V[k] *= sqrt(factor.D[i, i])
if !LDLFactorizations.factorized(factor) || minimum(factor.D) < 0
return nothing
end
# Finally, permute the columns of L'. The rows stay in the same order.
L = sqrt.(factor.D) * LinearAlgebra.UnitLowerTriangular(factor.L)
J, I, V = SparseArrays.findnz(SparseArrays.sparse(L))
return I, factor.P[J], V
end

Expand Down
63 changes: 34 additions & 29 deletions src/Bridges/Constraint/bridges/QuadtoSOCBridge.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ end
const QuadtoSOC{T,OT<:MOI.ModelLike} =
SingleBridgeOptimizer{QuadtoSOCBridge{T},OT}

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

function compute_sparse_sqrt(Q, func, set)
# There's a big try-catch here because Cholesky can fail even if
# `check = false`. As one example, it currently (v1.12) fails with
# `BigFloat`. Similarly, we want to guard against errors in
# `compute_sparse_sqrt_fallback`.
#
# The try-catch isn't a performance concern because the alternative is not
# being able to reformulate the problem.
try
factor = LinearAlgebra.cholesky(Q; check = false)
if !LinearAlgebra.issuccess(factor)
return compute_sparse_sqrt_fallback(Q, func, set)
end
L, p = SparseArrays.sparse(factor.L), factor.p
# We have Q = P' * L * L' * P. We want to find Q = U' * U, so U = L' * P
# First, compute L'. Note I and J are reversed
J, I, V = SparseArrays.findnz(L)
# Then, we want to permute the columns of L'. The rows stay in the same
# order.
return I, p[J], V
catch err
if err isa MOI.AddConstraintNotAllowed
rethrow(err)
end
msg = "There was an error computing a matrix square root"
throw(MOI.UnsupportedConstraint{typeof(func),typeof(set)}(msg))
function _error_msg(::MOI.Utilities.CliqueTreesExt)
return """
Unable to transform a quadratic constraint into a SecondOrderCone
constraint because the quadratic constraint is not strongly convex and
our Cholesky decomposition failed.

If the constraint is convex but not strongly convex, you can work-around
this issue by manually installing and loading `CliqueTrees.jl`:
```julia
import Pkg; Pkg.add("CliqueTrees")
using CliqueTrees
```

CliqueTrees.jl is not included by default because it contains a number of
heavy dependencies.
"""
end

function compute_sparse_sqrt(Q, ::F, ::S) where {F,S}
if (ret = MOI.Utilities.compute_sparse_sqrt(Q)) !== nothing
return ret
elseif !MOI.Utilities.is_defined(MOI.Utilities.LDLFactorizationsExt())
msg = _error_msg(MOI.Utilities.LDLFactorizationsExt())
return throw(MOI.AddConstraintNotAllowed{F,S}(msg))
elseif !MOI.Utilities.is_defined(MOI.Utilities.CliqueTreesExt())
msg = _error_msg(MOI.Utilities.CliqueTreesExt())
return throw(MOI.AddConstraintNotAllowed{F,S}(msg))
end
msg = """
Unable to transform a quadratic constraint into a SecondOrderCone
constraint because the quadratic constraint is not convex.
"""
return throw(MOI.UnsupportedConstraint{F,S}(msg))
end

function bridge_constraint(
Expand Down
1 change: 1 addition & 0 deletions src/Utilities/Utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@ include("lazy_iterators.jl")
include("set_dot.jl")

include("distance_to_set.jl")
include("sparse_square_root.jl")

end # module
72 changes: 72 additions & 0 deletions src/Utilities/sparse_square_root.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright (c) 2017: Miles Lubin and contributors
# Copyright (c) 2017: Google Inc.
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

abstract type AbstractExt end

is_defined(::AbstractExt) = false

struct LDLFactorizationsExt <: AbstractExt end

struct CliqueTreesExt <: AbstractExt end

struct LinearAlgebraExt <: AbstractExt end

is_defined(::LinearAlgebraExt) = true

function compute_sparse_sqrt(::LinearAlgebraExt, Q::AbstractMatrix)
factor = LinearAlgebra.cholesky(Q; check = false)
if !LinearAlgebra.issuccess(factor)
return nothing
end
L, p = SparseArrays.sparse(factor.L), factor.p
# We have Q = P' * L * L' * P. We want to find Q = U' * U, so U = L' * P
# First, compute L'. Note I and J are reversed
J, I, V = SparseArrays.findnz(L)
# Then, we want to permute the columns of L'. The rows stay in the same
# order.
return I, p[J], V
end

"""
compute_sparse_sqrt(Q::AbstractMatrix)

Attempts to compute a sparse square root such that `Q = A' * A`.

## Return value

If successful, this function returns an `(I, J, V)` triplet of the sparse `A`
matrix.

If unsuccessful, this function returns `nothing`.

## Extensions

By default, this function attempts to use a Cholesky decomposition. If that
fails, it may optionally use various extension packages.

These extension packages must be loaded before calling `compute_sparse_sqrt`.

The extensions currently supported are:

* The LDL routine in `LDLFactorizations.jl`
* The pivoted Cholesky in `CliqueTrees.jl`
"""
function compute_sparse_sqrt(Q::AbstractMatrix)
# There's a big try-catch here because Cholesky can fail even if
# `check = false`. The try-catch isn't a performance concern because the
# alternative is not being able to reformulate the problem.
for ext in (LinearAlgebraExt(), LDLFactorizationsExt(), CliqueTreesExt())
if is_defined(ext)
try
if (ret = compute_sparse_sqrt(ext, Q)) !== nothing
return ret
end
catch
end
end
end
return nothing
end
Loading
Loading