Skip to content

Commit c42818f

Browse files
authored
Add Cluster Completion for Term Sparsity (#178)
1 parent c827e9d commit c42818f

11 files changed

+249
-63
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ version = "0.4.4"
55

66
[deps]
77
ComplexOptInterface = "25c3070e-1275-41b5-afef-2f982c87090a"
8+
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
89
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
910
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1011
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
@@ -19,6 +20,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1920

2021
[compat]
2122
ComplexOptInterface = "0.0.2"
23+
DataStructures = "0.18"
2224
DynamicPolynomials = "0.3.6"
2325
JuMP = "0.21"
2426
MathOptInterface = "0.9.18"

docs/src/constraints.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ Approximations of the cone of copositive matrices:
285285
SumOfSquares.CopositiveInner
286286
```
287287

288+
Sparsity certificates:
289+
```@docs
290+
SumOfSquares.Certificate.MonomialSparsity
291+
```
292+
288293
Attributes
289294
```@docs
290295
SumOfSquares.PolyJuMP.MomentsAttribute
@@ -320,5 +325,5 @@ SumOfSquares.Certificate.ChordalExtensionGraph.LabelledGraph
320325
SumOfSquares.Certificate.ChordalExtensionGraph.add_node!
321326
SumOfSquares.Certificate.ChordalExtensionGraph.add_edge!
322327
SumOfSquares.Certificate.ChordalExtensionGraph.add_clique!
323-
SumOfSquares.Certificate.ChordalExtensionGraph.chordal_extension
328+
SumOfSquares.Certificate.ChordalExtensionGraph.completion
324329
```

examples/Symmetry reduction.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# # Lyapunov Function Search
1+
# # Symmetry reduction
22

3-
#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/generated/Lyapunov Function Search.ipynb)
4-
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/generated/Lyapunov Function Search.ipynb)
5-
# **Adapted from**: SOSTOOLS' SOSDEMO2 (See Section 4.2 of [SOSTOOLS User's Manual](http://sysos.eng.ox.ac.uk/sostools/sostools.pdf))
3+
#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/generated/Symmetry reduction.ipynb)
4+
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/generated/Symmetry reduction.ipynb)
5+
# **Adapted from**: https://github.com/kalmarek/SymbolicWedderburn.jl/blob/tw/ex_sos/examples/ex_C4.jl
66

77
using Pkg
88
pkg"add https://github.com/kalmarek/SymbolicWedderburn.jl#tw/ex_sos"

examples/Term sparsity.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# # Term sparsity
2+
3+
#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/generated/Term sparsity.ipynb)
4+
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/generated/Term sparsity.ipynb)
5+
# **Adapted from**: Example 3.5 of [WML20b]
6+
#
7+
# [WML20a] Wang, Jie, Victor Magron, and Jean-Bernard Lasserre.
8+
# *TSSOS: A Moment-SOS hierarchy that exploits term sparsity*.
9+
# arXiv preprint arXiv:1912.08899 (2020).
10+
#
11+
# [WML20b] Wang, Jie, Victor Magron, and Jean-Bernard Lasserre.
12+
# *Chordal-TSSOS: a moment-SOS hierarchy that exploits term sparsity with chordal extension*.
13+
# arXiv preprint arXiv:2003.03210 (2020).
14+
15+
using Test #src
16+
using DynamicPolynomials
17+
@polyvar x[1:3]
18+
19+
# We would like to find the minimum value of the polynomial
20+
21+
poly = x[1]^2 - 2x[1]*x[2] + 3x[2]^2 - 2x[1]^2*x[2] + 2x[1]^2*x[2]^2 - 2x[2]*x[3] + 6x[3]^2 + 18x[2]^2*x[3] - 54x[2]*x[3]^2 + 142x[2]^2*x[3]^2
22+
23+
# The minimum value of the polynomial can be found to be zero.
24+
25+
import CSDP
26+
solver = CSDP.Optimizer
27+
using SumOfSquares
28+
function sos_min(sparsity)
29+
model = Model(solver)
30+
@variable(model, t)
31+
@objective(model, Max, t)
32+
con_ref = @constraint(model, poly - t in SOSCone(), sparsity = sparsity)
33+
optimize!(model)
34+
return value(t), moment_matrix(con_ref)
35+
end
36+
37+
bound, ν = sos_min(NoSparsity())
38+
@test bound 0 atol=1e-6 #src
39+
bound
40+
41+
# We find the corresponding minimizer `(0, 0, 0)` by matching the moments
42+
# of the moment matrix with a dirac measure centered at this minimizer.
43+
44+
extractatoms(ν, 1e-6)
45+
46+
# We can see below that the basis contained 6 monomials hence we needed to use 6x6 PSD matrix variables.
47+
48+
@test ν.basis.monomials == [x[1]*x[2], x[2]*x[3], x[1], x[2], x[3], 1] #src
49+
ν.basis
50+
51+
# Using the monomial/term sparsity method of [WML20a] based on cluster completion, we find the same bound.
52+
53+
bound, ν = sos_min(MonomialSparsity())
54+
@test bound 0 atol=1e-6 #src
55+
bound
56+
57+
# Which is not suprising as no sparsity reduction could be performed.
58+
59+
@test length.sub_moment_matrices) == 1 #src
60+
@test ν.sub_moment_matrices[1].basis.monomials == [x[1]*x[2], x[2]*x[3], x[1], x[2], x[3], 1] #src
61+
[sub.basis for sub in ν.sub_moment_matrices]
62+
63+
# Using the monomial/term sparsity method of [WML20b] based on chordal completion, the lower bound is smaller than 0.
64+
65+
bound, ν = sos_min(MonomialSparsity(ChordalCompletion()))
66+
@test bound -0.00355 rtol=1e-3 #src
67+
bound
68+
69+
# However, this bound was obtained with an SDP with 4 matrices of size 3x3.
70+
71+
@test length.sub_moment_matrices) == 4 #src
72+
@test ν.sub_moment_matrices[1].basis.monomials == [x[2]*x[3], x[2], x[3]] #src
73+
@test ν.sub_moment_matrices[2].basis.monomials == [x[2]*x[3], x[2], 1] #src
74+
@test ν.sub_moment_matrices[3].basis.monomials == [x[1], x[2], 1] #src
75+
@test ν.sub_moment_matrices[4].basis.monomials == [x[1]*x[2], x[1], 1] #src
76+
[sub.basis for sub in ν.sub_moment_matrices]

src/Certificate/Certificate.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ zero_basis(certificate::Remainder) = zero_basis(certificate.gram_certificate)
175175
zero_basis_type(::Type{Remainder{GCT}}) where {GCT} = zero_basis_type(GCT)
176176

177177
include("sparse/ChordalExtensionGraph.jl")
178+
using .ChordalExtensionGraph: ChordalCompletion, ClusterCompletion
179+
export ChordalCompletion, ClusterCompletion
180+
178181
include("sparse/sparse_putinar.jl")
179182

180183
end

src/Certificate/sparse/ChordalExtensionGraph.jl

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
module ChordalExtensionGraph
22

3+
using DataStructures
4+
5+
export AbstractCompletion, ChordalCompletion, ClusterCompletion
36
export LabelledGraph, add_node!, add_edge!, add_clique!, chordal_extension
47

8+
abstract type AbstractGreedyAlgorithm end
9+
10+
abstract type AbstractCompletion end
11+
12+
struct ChordalCompletion{A<:AbstractGreedyAlgorithm} <: AbstractCompletion
13+
algo::A
14+
end
15+
ChordalCompletion() = ChordalCompletion(GreedyFillIn())
16+
17+
struct ClusterCompletion <: AbstractCompletion end
18+
519
# With a `Vector{Vector{Int}}` with unsorted neighbors, computing fill-in
620
# would be inefficient.
721
# With a `Vector{Vector{Int}}` with sorted neighbors, adding an edge would be
@@ -213,8 +227,6 @@ function add_clique!(G::LabelledGraph{T}, x::Vector{T}) where T
213227
add_clique!(G.graph, add_node!.(G, x))
214228
end
215229

216-
abstract type AbstractGreedyAlgorithm end
217-
218230
struct GreedyFillIn <: AbstractGreedyAlgorithm end
219231
cache(G::Graph, ::GreedyFillIn) = FillInCache(G)
220232
heuristic_value(G::FillInCache, node::Int, ::GreedyFillIn) = fill_in(G, node)
@@ -262,10 +274,22 @@ function _greedy_triangulation!(G, algo::AbstractGreedyAlgorithm)
262274
return candidate_cliques
263275
end
264276

265-
function chordal_extension(G::Graph, algo::AbstractGreedyAlgorithm)
277+
"""
278+
completion(G::Graph, comp::ChordalCompletion)
279+
280+
Return a chordal extension of `G` and the corresponding maximal cliques.
281+
282+
The algoritm is Algorithm 3 in [BA10] with the GreedyFillIn heuristic of Table I.
283+
284+
[BA10] Bodlaender, Hans L., and Arie MCA Koster.
285+
Treewidth computations I. Upper bounds.
286+
Information and Computation 208, no. 3 (2010): 259-275.
287+
Utrecht University, Utrecht, The Netherlands www.cs.uu.nl
288+
"""
289+
function completion(G::Graph, comp::ChordalCompletion)
266290
H = copy(G)
267291

268-
candidate_cliques = _greedy_triangulation!(cache(H, algo), algo)
292+
candidate_cliques = _greedy_triangulation!(cache(H, comp.algo), comp.algo)
269293
enable_all_nodes!(H)
270294

271295
sort!.(candidate_cliques)
@@ -291,19 +315,41 @@ function chordal_extension(G::Graph, algo::AbstractGreedyAlgorithm)
291315
end
292316

293317
"""
294-
chordal_extension(G::LabelledGraph{T})
295-
296-
Return a chordal extension of G and the corresponding maximal cliques.
318+
completion(G::Graph, comp::ChordalCompletion)
297319
298-
The algoritm is Algorithm 3 in [BA10] with the GreedyFillIn heuristic of Table I.
299-
300-
[BA10] Bodlaender, Hans L., and Arie MCA Koster.
301-
Treewidth computations I. Upper bounds.
302-
Information and Computation 208, no. 3 (2010): 259-275.
303-
Utrecht University, Utrecht, The Netherlands www.cs.uu.nl
320+
Return a cluster completion of `G` and the corresponding maximal cliques.
304321
"""
305-
function chordal_extension(G::LabelledGraph{T}, algo::AbstractGreedyAlgorithm) where T
306-
H, cliques = chordal_extension(G.graph, algo)
322+
function completion(G::Graph, ::ClusterCompletion)
323+
H = copy(G)
324+
union_find = IntDisjointSets(num_nodes(G))
325+
for from in 1:num_nodes(G)
326+
for to in neighbors(G, from)
327+
union!(union_find, from, to)
328+
end
329+
end
330+
cliques = [Int[] for i in 1:num_groups(union_find)]
331+
ids = zeros(Int, num_nodes(G))
332+
k = 0
333+
for node in 1:num_nodes(G)
334+
root = find_root!(union_find, node)
335+
if iszero(ids[root])
336+
k += 1
337+
ids[root] = k
338+
end
339+
push!(cliques[ids[root]], node)
340+
end
341+
@assert k == length(cliques)
342+
for clique in cliques
343+
add_clique!(H, clique)
344+
end
345+
return H, cliques
346+
end
347+
348+
function completion(G::LabelledGraph{T}, comp::AbstractCompletion) where T
349+
H, cliques = completion(G.graph, comp)
307350
return LabelledGraph{T}(G.n2int, G.int2n, H), [[G.int2n[i] for i in clique] for clique in cliques]
308351
end
352+
353+
chordal_extension(G, algo) = completion(G, ChordalCompletion(algo))
354+
309355
end #module

src/Certificate/sparse/monomial_sparsity.jl

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
1-
struct MonomialSparsity <: Sparsity
1+
const CEG = SumOfSquares.Certificate.ChordalExtensionGraph
2+
3+
"""
4+
struct MonomialSparsity{C<:CEG.AbstractCompletion} <: Sparsity
5+
k::Int
6+
completion::C
7+
use_all_monomials::Bool
8+
end
9+
10+
Monomial or term sparsity as developed in [].
11+
12+
# [WML20a] Wang, Jie, Victor Magron, and Jean-Bernard Lasserre.
13+
# *TSSOS: A Moment-SOS hierarchy that exploits term sparsity*.
14+
# arXiv preprint arXiv:1912.08899 (2020).
15+
#
16+
# [WML20b] Wang, Jie, Victor Magron, and Jean-Bernard Lasserre.
17+
# *Chordal-TSSOS: a moment-SOS hierarchy that exploits term sparsity with chordal extension*.
18+
# arXiv preprint arXiv:2003.03210 (2020).
19+
"""
20+
struct MonomialSparsity{C<:CEG.AbstractCompletion} <: Sparsity
21+
completion::C
222
k::Int
323
use_all_monomials::Bool
4-
function MonomialSparsity(k::Int=0, use_all_monomials::Bool=false)
5-
return new(k, use_all_monomials)
24+
function MonomialSparsity(
25+
completion::CEG.AbstractCompletion=CEG.ClusterCompletion(),
26+
k::Int=0,
27+
use_all_monomials::Bool=false,
28+
)
29+
return new{typeof(completion)}(completion, k, use_all_monomials)
630
end
731
end
832

33+
# Note: For some implementation of MultivariatePolynomials such as
34+
# https://github.com/blegat/CondensedMatterSOS.jl,
35+
# the product of monomials may be a term so wrap any multiplication of monomials by
36+
# `MP.monomial`.
37+
938
const MP = SumOfSquares.MP
10-
const CEG = SumOfSquares.Certificate.ChordalExtensionGraph
1139
function monomial_sparsity_graph(monos, P, use_all_monomials::Bool)
1240
g = CEG.LabelledGraph{eltype(monos)}()
1341
if use_all_monomials
@@ -20,7 +48,7 @@ function monomial_sparsity_graph(monos, P, use_all_monomials::Bool)
2048
end
2149
for a in monos
2250
for b in monos
23-
if (a * b) in P
51+
if MP.monomial(a * b) in P
2452
if a != b
2553
CEG.add_edge!(g, a, b)
2654
elseif squares !== nothing
@@ -44,21 +72,21 @@ function _add_monos(add, monos, H, squares)
4472
end
4573
for bi in neighbors
4674
b = H.int2n[bi]
47-
add(a * b)
75+
add(MP.monomial(a * b))
4876
end
4977
end
5078
end
51-
function chordal_with_squares(g, squares)
52-
H, cliques = CEG.chordal_extension(g, CEG.GreedyFillIn())
79+
function completion_with_squares(g, squares, completion)
80+
H, cliques = CEG.completion(g, completion)
5381
if squares !== nothing
5482
cliques = filter(monos -> length(monos) > 1 || (monos[1] in squares), collect(cliques))
5583
end
5684
return H, cliques
5785
end
58-
function monomial_sparsity_iteration(P, use_all_monomials::Bool, monos)
86+
function monomial_sparsity_iteration(P, completion, use_all_monomials::Bool, monos)
5987
P_next = Set{eltype(P)}()
6088
g, squares = monomial_sparsity_graph(monos, P, use_all_monomials)
61-
H, cliques = chordal_with_squares(g, squares)
89+
H, cliques = completion_with_squares(g, squares, completion)
6290
_add_monos(mono -> push!(P_next, mono), monos, H, squares)
6391
return P_next, cliques
6492
end
@@ -68,18 +96,18 @@ struct GeneratorP{PT, GT}
6896
end
6997
function Base.in(mono, g::GeneratorP)
7098
return any(g.generator_monos) do g_mono
71-
(mono * g_mono) in g.P
99+
MP.monomial(mono * g_mono) in g.P
72100
end
73101
end
74-
function monomial_sparsity_iteration(P, use_all_monomials::Bool, monos, multiplier_generator_monos)
75-
P_next, cliques = monomial_sparsity_iteration(P, use_all_monomials, monos)
102+
function monomial_sparsity_iteration(P, completion, use_all_monomials::Bool, monos, multiplier_generator_monos)
103+
P_next, cliques = monomial_sparsity_iteration(P, completion, use_all_monomials, monos)
76104
multiplier_cliques = map(multiplier_generator_monos) do m
77105
multiplier_monos, generator_monos = m
78106
g, squares = monomial_sparsity_graph(multiplier_monos, GeneratorP(P, generator_monos), use_all_monomials)
79-
H, _cliques = chordal_with_squares(g, squares)
107+
H, _cliques = completion_with_squares(g, squares, completion)
80108
_add_monos(multiplier_monos, H, squares) do mono
81109
for b in generator_monos
82-
push!(P_next, mono * b)
110+
push!(P_next, MP.monomial(mono * b))
83111
end
84112
end
85113
return _cliques
@@ -101,7 +129,7 @@ function sparsity(monos::AbstractVector{<:MP.AbstractMonomial}, sp::MonomialSpar
101129
iter = 0
102130
while iter < sp.k || iszero(sp.k)
103131
P_prev = P
104-
P, cliques = monomial_sparsity_iteration(P_prev, sp.use_all_monomials, gram_monos, args...)
132+
P, cliques = monomial_sparsity_iteration(P_prev, sp.completion, sp.use_all_monomials, gram_monos, args...)
105133
if iszero(iter)
106134
# If gram_monos + gram_monos !⊆ monos, then it's possible that P_prev !⊆ P
107135
P == P_prev && break

src/SumOfSquares.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ function matrix_cone_type end
2727

2828
export Certificate
2929
include("Certificate/Certificate.jl")
30-
using .Certificate: Sparsity, NoSparsity, VariableSparsity, MonomialSparsity, SignSymmetry
31-
export Sparsity, NoSparsity, VariableSparsity, MonomialSparsity, SignSymmetry
30+
using .Certificate: Sparsity, NoSparsity, VariableSparsity, MonomialSparsity, SignSymmetry, ChordalCompletion, ClusterCompletion
31+
export Sparsity, NoSparsity, VariableSparsity, MonomialSparsity, SignSymmetry, ChordalCompletion, ClusterCompletion
3232
include("rand.jl")
3333

3434
# MOI extension

test/certificate.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ end
135135
certificate_api(preorder)
136136
sparsities = Sparsity[VariableSparsity()]
137137
if certificate isa Certificate.MaxDegree
138-
push!(sparsities, MonomialSparsity(1))
138+
push!(sparsities, MonomialSparsity(ChordalCompletion(), 1))
139139
end
140140
@testset "$(typeof(sparsity))" for sparsity in sparsities
141141
certificate_api(Certificate.SparsePreorder(sparsity, preorder))
@@ -152,7 +152,7 @@ end
152152
if certificate isa Certificate.MaxDegree
153153
_test(Certificate.SparseIdeal(VariableSparsity(), certificate))
154154
end
155-
@testset "$(typeof(sparsity))" for sparsity in [SignSymmetry(), MonomialSparsity(1)]
155+
@testset "$(typeof(sparsity))" for sparsity in [SignSymmetry(), MonomialSparsity(ChordalCompletion(), 1)]
156156
_test(Certificate.SparseIdeal(sparsity, certificate))
157157
_test(Certificate.SparseIdeal(sparsity, Certificate.Remainder(certificate)))
158158
end

test/runtests.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ include("constraint.jl")
3333

3434
include("Mock/mock_tests.jl")
3535

36-
# Tests needed a solver
37-
# FIXME these tests should be moved to `Tests` and tested in `Mock`
36+
# Tests needing a solver
37+
# FIXME these tests should be converted to Literate and moved to `examples` or
38+
# converted to be used with `MockOptimizer` and moved to `test/Tests`
3839
include("solvers.jl")
3940
include("sospoly.jl")
4041
include("domain.jl")

0 commit comments

Comments
 (0)