Skip to content

Commit 69e68ce

Browse files
samuelsonricblegat
andauthored
Fast implementation of the minimum fill ordering heuristic. (#394)
* Integrate with package CliqueTrees. * Replace chordal completion implementation. * Fix tests for example * Fix format * Remove fill_in in doc * CliqueTrees compat: 0.4 -> 0.5. --------- Co-authored-by: Benoît Legat <benoit.legat@gmail.com>
1 parent b43a510 commit 69e68ce

File tree

5 files changed

+81
-333
lines changed

5 files changed

+81
-333
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ repo = "https://github.com/jump-dev/SumOfSquares.jl.git"
44
version = "0.7.3"
55

66
[deps]
7+
CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8"
78
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
89
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
910
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -20,6 +21,7 @@ StarAlgebras = "0c0c59c1-dc5f-42e9-9a8b-b5dc384a6cd1"
2021
SymbolicWedderburn = "858aa9a9-4c7c-4c62-b466-2421203962a2"
2122

2223
[compat]
24+
CliqueTrees = "0.5"
2325
DataStructures = "0.18"
2426
JuMP = "1.10"
2527
MathOptInterface = "1.13"

docs/src/reference/certificate.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ SumOfSquares.Certificate.Sparsity.Preorder
3232
Chordal extension:
3333
```@docs
3434
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.neighbors
35-
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.fill_in
3635
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.is_clique
3736
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.LabelledGraph
3837
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.add_node!

docs/src/tutorials/Sparsity/term_sparsity.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ bound
6969
# However, this bound was obtained with an SDP with 4 matrices of size 3x3.
7070

7171
@test length.blocks) == 4 #src
72-
@test ν.blocks[1].basis.monomials == [x[2], x[1], x[1]*x[2]] #src
73-
@test ν.blocks[2].basis.monomials == [x[2], x[2]*x[3], x[1]*x[2]] #src
74-
@test ν.blocks[3].basis.monomials == [x[3], x[2], x[2]*x[3]] #src
75-
@test ν.blocks[4].basis.monomials == [1, x[2]*x[3], x[1]*x[2]] #src
72+
@test ν.blocks[1].basis.monomials == [1, x[2]*x[3], x[1]*x[2]] #src
73+
@test ν.blocks[2].basis.monomials == [x[3], x[2], x[2]*x[3]] #src
74+
@test ν.blocks[3].basis.monomials == [x[2], x[2]*x[3], x[1]*x[2]] #src
75+
@test ν.blocks[4].basis.monomials == [x[2], x[1], x[1]*x[2]] #src
7676
[sub.basis for sub in ν.blocks]

src/Certificate/Sparsity/ChordalExtensionGraph.jl

Lines changed: 72 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module ChordalExtensionGraph
22

3-
using DataStructures
3+
import CliqueTrees
4+
import DataStructures
5+
import SparseArrays
46

57
export AbstractCompletion, ChordalCompletion, ClusterCompletion
68
export LabelledGraph, add_node!, add_edge!, add_clique!, chordal_extension
@@ -9,26 +11,31 @@ abstract type AbstractGreedyAlgorithm end
911

1012
abstract type AbstractCompletion end
1113

12-
struct ChordalCompletion{A<:AbstractGreedyAlgorithm} <: AbstractCompletion
14+
struct ClusterCompletion <: AbstractCompletion end
15+
16+
struct ChordalCompletion{A<:CliqueTrees.EliminationAlgorithm} <:
17+
AbstractCompletion
1318
algo::A
1419
end
15-
ChordalCompletion() = ChordalCompletion(GreedyFillIn())
1620

17-
struct ClusterCompletion <: AbstractCompletion end
21+
# the default algorithm is the greedy minimum fill heuristic
22+
const GreedyFillIn = CliqueTrees.MF
23+
24+
function ChordalCompletion()
25+
return ChordalCompletion(GreedyFillIn())
26+
end
1827

1928
# With a `Vector{Vector{Int}}` with unsorted neighbors, computing fill-in
2029
# would be inefficient.
2130
# With a `Vector{Vector{Int}}` with sorted neighbors, adding an edge would be
2231
# inefficient.
2332
# With `Vector{Set{Int}}` both adding edges and computing fill-in is efficient.
24-
2533
struct Graph
2634
neighbors::Vector{Set{Int}}
27-
disabled::BitSet
2835
end
29-
Graph() = Graph(Set{Int}[], BitSet())
36+
Graph() = Graph(Set{Int}[])
3037
Base.broadcastable(g::Graph) = Ref(g)
31-
Base.copy(G::Graph) = Graph(deepcopy(G.neighbors), copy(G.disabled))
38+
Base.copy(G::Graph) = Graph(deepcopy(G.neighbors))
3239
function add_node!(G::Graph)
3340
push!(G.neighbors, Set{Int}())
3441
return length(G.neighbors)
@@ -42,15 +49,14 @@ function add_edge!(G::Graph, i::Int, j::Int)
4249
return (i, j)
4350
end
4451
function add_clique!(G::Graph, nodes::Vector{Int})
45-
for i in 1:(length(nodes)-1)
46-
for j in (i+1):length(nodes)
52+
n = length(nodes)
53+
54+
for i in 1:(n-1)
55+
for j in (i+1):n
4756
add_edge!(G, nodes[i], nodes[j])
4857
end
4958
end
5059
end
51-
disable_node!(G::Graph, node::Int) = push!(G.disabled, node)
52-
is_enabled(G::Graph, node::Int) = !(node in G.disabled)
53-
enable_all_nodes!(G::Graph) = empty!(G.disabled)
5460

5561
"""
5662
neighbors(G::Graph, node::Int}
@@ -61,94 +67,24 @@ function neighbors(G::Graph, node::Int)
6167
return G.neighbors[node]
6268
end
6369

64-
function _num_edges_subgraph(
65-
G::Graph,
66-
nodes::Union{Vector{Int},Set{Int}},
67-
node::Int,
68-
)
69-
neighs = neighbors(G, node)
70-
return count(nodes) do node
71-
return is_enabled(G, node) && node in neighs
72-
end
73-
end
74-
function num_edges_subgraph(G::Graph, nodes::Union{Vector{Int},Set{Int}})
75-
return mapreduce(+, nodes; init = 0) do node
76-
return is_enabled(G, node) ? _num_edges_subgraph(G, nodes, node) : 0
77-
end
78-
end
79-
80-
function num_missing_edges_subgraph(
81-
G::Graph,
82-
nodes::Union{Vector{Int},Set{Int}},
83-
)
84-
n = count(node -> is_enabled(G, node), nodes)
85-
# A clique is a completely connected graph. As such it has n*(n-1)/2 undirected
86-
# or equivalently n*(n-1) directed edges.
87-
return div(n * (n - 1) - num_edges_subgraph(G, nodes), 2)
88-
end
89-
90-
"""
91-
fill_in(G::Graph{T}, i::T}
92-
93-
Return number of edges that need to be added to make the neighbors of `i` a clique.
94-
"""
95-
function fill_in(G::Graph, node::Int)
96-
return num_missing_edges_subgraph(G, neighbors(G, node))
97-
end
98-
9970
"""
10071
is_clique(G::Graph{T}, x::Vector{T})
10172
10273
Return a `Bool` indication whether `x` is a clique in `G`.
10374
"""
10475
function is_clique(G::Graph, nodes::Vector{Int})
105-
return iszero(num_missing_edges_subgraph(G, nodes))
106-
end
76+
n = length(nodes)
10777

108-
struct FillInCache
109-
graph::Graph
110-
fill_in::Vector{Int}
111-
end
112-
function FillInCache(graph::Graph)
113-
return FillInCache(graph, [fill_in(graph, i) for i in 1:num_nodes(graph)])
114-
end
115-
Base.copy(G::FillInCache) = FillInCache(copy(G.graph), copy(G.fill_in))
116-
117-
num_nodes(G::FillInCache) = num_nodes(G.graph)
118-
neighbors(G::FillInCache, node::Int) = neighbors(G.graph, node)
119-
function add_edge!(G::FillInCache, i::Int, j::Int)
120-
ni = neighbors(G, i)
121-
nj = neighbors(G, j)
122-
if i in nj
123-
@assert j in ni
124-
return
125-
end
126-
@assert !(j in ni)
127-
for node in ni
128-
@assert node != i
129-
@assert node != j
130-
if node in nj
131-
G.fill_in[node] -= 1
78+
for i in 1:(n-1)
79+
for j in (i+1):n
80+
if nodes[j] neighbors(G, nodes[i])
81+
return false
82+
end
13283
end
13384
end
134-
G.fill_in[i] +=
135-
count(k -> is_enabled(G.graph, k), ni) -
136-
_num_edges_subgraph(G.graph, ni, j)
137-
G.fill_in[j] +=
138-
count(k -> is_enabled(G.graph, k), nj) -
139-
_num_edges_subgraph(G.graph, nj, i)
140-
return add_edge!(G.graph, i, j)
141-
end
142-
fill_in(G::FillInCache, node::Int) = G.fill_in[node]
143-
function disable_node!(G::FillInCache, node::Int)
144-
for neighbor in neighbors(G, node)
145-
nodes = neighbors(G, neighbor)
146-
G.fill_in[neighbor] -=
147-
(length(nodes) - 1) - _num_edges_subgraph(G.graph, nodes, node)
148-
end
149-
return disable_node!(G.graph, node)
85+
86+
return true
15087
end
151-
is_enabled(G::FillInCache, node::Int) = is_enabled(G.graph, node)
15288

15389
"""
15490
struct LabelledGraph{T}
@@ -244,54 +180,35 @@ function add_clique!(G::LabelledGraph{T}, x::Vector{T}) where {T}
244180
return add_clique!(G.graph, add_node!.(G, x))
245181
end
246182

247-
struct GreedyFillIn <: AbstractGreedyAlgorithm end
248-
cache(G::Graph, ::GreedyFillIn) = FillInCache(G)
249-
heuristic_value(G::FillInCache, node::Int, ::GreedyFillIn) = fill_in(G, node)
250-
251-
function _greedy_triangulation!(G, algo::AbstractGreedyAlgorithm)
252-
candidate_cliques = Vector{Int}[]
253-
for i in 1:num_nodes(G)
254-
node = argmin(map(1:num_nodes(G)) do node
255-
if is_enabled(G, node)
256-
heuristic_value(G, node, algo)
257-
else
258-
typemax(Int)
259-
end
260-
end)
261-
#=
262-
# look at all its neighbors. In G we want the neighbors to form a clique.
263-
neighbor_nodes = collect(neighbors(G, node))
264-
265-
# add neighbors and node as a potentially maximal clique
266-
candidate_clique = [neighbor for neighbor in neighbor_nodes if is_enabled(G, neighbor)]
267-
push!(candidate_clique, node)
268-
push!(candidate_cliques, candidate_clique)
269-
270-
# add edges to G to make the new candidate_clique at least potentially a clique
271-
for i in eachindex(neighbor_nodes)
272-
if is_enabled(G, i)
273-
for j in (i + 1):length(neighbor_nodes)
274-
if is_enabled(G, j)
275-
add_edge!(G, neighbor_nodes[i], neighbor_nodes[j])
276-
end
277-
end
278-
end
279-
end
280-
=#
281-
neighbor_nodes = [
282-
neighbor for
283-
neighbor in neighbors(G, node) if is_enabled(G, neighbor)
284-
]
285-
push!(candidate_cliques, [node, neighbor_nodes...])
286-
for i in eachindex(neighbor_nodes)
287-
for j in (i+1):length(neighbor_nodes)
288-
add_edge!(G, neighbor_nodes[i], neighbor_nodes[j])
289-
end
290-
end
183+
"""
184+
completion(G::Graph, comp::ClusterCompletion)
291185
292-
disable_node!(G, node)
186+
Return a cluster completion of `G` and the corresponding maximal cliques.
187+
"""
188+
function completion(G::Graph, ::ClusterCompletion)
189+
H = copy(G)
190+
union_find = DataStructures.IntDisjointSets(num_nodes(G))
191+
for from in 1:num_nodes(G)
192+
for to in neighbors(G, from)
193+
DataStructures.union!(union_find, from, to)
194+
end
195+
end
196+
cliques = [Int[] for i in 1:DataStructures.num_groups(union_find)]
197+
ids = zeros(Int, num_nodes(G))
198+
k = 0
199+
for node in 1:num_nodes(G)
200+
root = DataStructures.find_root!(union_find, node)
201+
if iszero(ids[root])
202+
k += 1
203+
ids[root] = k
204+
end
205+
push!(cliques[ids[root]], node)
206+
end
207+
@assert k == length(cliques)
208+
for clique in cliques
209+
add_clique!(H, clique)
293210
end
294-
return candidate_cliques
211+
return H, cliques
295212
end
296213

297214
"""
@@ -307,63 +224,34 @@ Information and Computation 208, no. 3 (2010): 259-275.
307224
Utrecht University, Utrecht, The Netherlands www.cs.uu.nl
308225
"""
309226
function completion(G::Graph, comp::ChordalCompletion)
227+
# construct a copy H of the graph G
310228
H = copy(G)
311229

312-
candidate_cliques = _greedy_triangulation!(cache(H, comp.algo), comp.algo)
313-
enable_all_nodes!(H)
314-
315-
sort!.(candidate_cliques)
316-
unique!(candidate_cliques)
317-
318-
# check whether candidate cliques are actually cliques
319-
candidate_cliques = candidate_cliques[[
320-
is_clique(H, clique) for clique in candidate_cliques
321-
]]
322-
sort!(candidate_cliques, by = x -> length(x))
323-
reverse!(candidate_cliques) # TODO use `rev=true` in `sort!`.
230+
# construct adjacency matrix of H
231+
matrix = SparseArrays.spzeros(Bool, num_nodes(H), num_nodes(H))
324232

325-
maximal_cliques = [first(candidate_cliques)]
326-
for clique in Iterators.drop(candidate_cliques, 1)
327-
if all(other_clique -> !(clique other_clique), maximal_cliques)
328-
push!(maximal_cliques, clique)
329-
end
233+
for j in axes(matrix, 2)
234+
list = neighbors(H, j)
235+
SparseArrays.getcolptr(matrix)[j+1] =
236+
SparseArrays.getcolptr(matrix)[j] + length(list)
237+
append!(SparseArrays.rowvals(matrix), list)
238+
append!(SparseArrays.nonzeros(matrix), zeros(Bool, length(list)))
330239
end
331240

332-
if length(Set(Iterators.flatten(maximal_cliques))) != num_nodes(H)
333-
error("Maximal cliques do not cover all nodes.")
334-
end
241+
# sort row indices of adjacency matrix
242+
matrix = copy(transpose(matrix))
335243

336-
return H, maximal_cliques
337-
end
244+
# construct tree decomposition of H
245+
label, tree = CliqueTrees.cliquetree(matrix; alg = comp.algo)
338246

339-
"""
340-
completion(G::Graph, comp::ChordalCompletion)
247+
# construct vector of cliques and append fill edges to H
248+
cliques = Vector{Vector{Int}}(undef, length(tree))
341249

342-
Return a cluster completion of `G` and the corresponding maximal cliques.
343-
"""
344-
function completion(G::Graph, ::ClusterCompletion)
345-
H = copy(G)
346-
union_find = IntDisjointSets(num_nodes(G))
347-
for from in 1:num_nodes(G)
348-
for to in neighbors(G, from)
349-
union!(union_find, from, to)
350-
end
351-
end
352-
cliques = [Int[] for i in 1:num_groups(union_find)]
353-
ids = zeros(Int, num_nodes(G))
354-
k = 0
355-
for node in 1:num_nodes(G)
356-
root = find_root!(union_find, node)
357-
if iszero(ids[root])
358-
k += 1
359-
ids[root] = k
360-
end
361-
push!(cliques[ids[root]], node)
362-
end
363-
@assert k == length(cliques)
364-
for clique in cliques
250+
for (i, v) in enumerate(tree)
251+
clique = cliques[i] = label[v]
365252
add_clique!(H, clique)
366253
end
254+
367255
return H, cliques
368256
end
369257

0 commit comments

Comments
 (0)