Skip to content

Commit 66e07d6

Browse files
committed
Replace chordal completion implementation.
1 parent 66abbe4 commit 66e07d6

File tree

3 files changed

+55
-357
lines changed

3 files changed

+55
-357
lines changed

docs/src/constraints.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,6 @@ SumOfSquares.PolyJuMP.bridges
344344
Chordal extension:
345345
```@docs
346346
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.neighbors
347-
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.fill_in
348347
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.is_clique
349348
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.LabelledGraph
350349
SumOfSquares.Certificate.Sparsity.ChordalExtensionGraph.add_node!

src/Certificate/Sparsity/ChordalExtensionGraph.jl

Lines changed: 52 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
module ChordalExtensionGraph
22

3-
using CliqueTrees
4-
using CliqueTrees: EliminationAlgorithm
5-
using DataStructures
6-
using SparseArrays
3+
import CliqueTrees
4+
import DataStructures
5+
import SparseArrays
76

87
export AbstractCompletion, ChordalCompletion, ClusterCompletion
98
export LabelledGraph, add_node!, add_edge!, add_clique!, chordal_extension
@@ -12,35 +11,30 @@ abstract type AbstractGreedyAlgorithm end
1211

1312
abstract type AbstractCompletion end
1413

15-
struct ChordalCompletion{A<:AbstractGreedyAlgorithm} <: AbstractCompletion
16-
algo::A
17-
end
18-
ChordalCompletion() = ChordalCompletion(GreedyFillIn())
19-
2014
struct ClusterCompletion <: AbstractCompletion end
2115

22-
struct CliqueTreesCompletion{A<:EliminationAlgorithm} <: AbstractCompletion
16+
struct ChordalCompletion{A<:CliqueTrees.EliminationAlgorithm} <: AbstractCompletion
2317
algo::A
2418
end
2519

26-
function CliqueTreesCompletion()
27-
# the default algorithm is the minimum fill heuristic
28-
CliqueTreesCompletion(MF())
20+
# the default algorithm is the greedy minimum fill heuristic
21+
const GreedyFillIn = CliqueTrees.MF
22+
23+
function ChordalCompletion()
24+
ChordalCompletion(GreedyFillIn())
2925
end
3026

3127
# With a `Vector{Vector{Int}}` with unsorted neighbors, computing fill-in
3228
# would be inefficient.
3329
# With a `Vector{Vector{Int}}` with sorted neighbors, adding an edge would be
3430
# inefficient.
3531
# With `Vector{Set{Int}}` both adding edges and computing fill-in is efficient.
36-
3732
struct Graph
3833
neighbors::Vector{Set{Int}}
39-
disabled::BitSet
4034
end
41-
Graph() = Graph(Set{Int}[], BitSet())
35+
Graph() = Graph(Set{Int}[])
4236
Base.broadcastable(g::Graph) = Ref(g)
43-
Base.copy(G::Graph) = Graph(deepcopy(G.neighbors), copy(G.disabled))
37+
Base.copy(G::Graph) = Graph(deepcopy(G.neighbors))
4438
function add_node!(G::Graph)
4539
push!(G.neighbors, Set{Int}())
4640
return length(G.neighbors)
@@ -54,15 +48,14 @@ function add_edge!(G::Graph, i::Int, j::Int)
5448
return (i, j)
5549
end
5650
function add_clique!(G::Graph, nodes::Vector{Int})
57-
for i in 1:(length(nodes)-1)
58-
for j in (i+1):length(nodes)
51+
n = length(nodes)
52+
53+
for i in 1:(n-1)
54+
for j in (i+1):n
5955
add_edge!(G, nodes[i], nodes[j])
6056
end
6157
end
6258
end
63-
disable_node!(G::Graph, node::Int) = push!(G.disabled, node)
64-
is_enabled(G::Graph, node::Int) = !(node in G.disabled)
65-
enable_all_nodes!(G::Graph) = empty!(G.disabled)
6659

6760
"""
6861
neighbors(G::Graph, node::Int}
@@ -73,94 +66,24 @@ function neighbors(G::Graph, node::Int)
7366
return G.neighbors[node]
7467
end
7568

76-
function _num_edges_subgraph(
77-
G::Graph,
78-
nodes::Union{Vector{Int},Set{Int}},
79-
node::Int,
80-
)
81-
neighs = neighbors(G, node)
82-
return count(nodes) do node
83-
return is_enabled(G, node) && node in neighs
84-
end
85-
end
86-
function num_edges_subgraph(G::Graph, nodes::Union{Vector{Int},Set{Int}})
87-
return mapreduce(+, nodes; init = 0) do node
88-
return is_enabled(G, node) ? _num_edges_subgraph(G, nodes, node) : 0
89-
end
90-
end
91-
92-
function num_missing_edges_subgraph(
93-
G::Graph,
94-
nodes::Union{Vector{Int},Set{Int}},
95-
)
96-
n = count(node -> is_enabled(G, node), nodes)
97-
# A clique is a completely connected graph. As such it has n*(n-1)/2 undirected
98-
# or equivalently n*(n-1) directed edges.
99-
return div(n * (n - 1) - num_edges_subgraph(G, nodes), 2)
100-
end
101-
102-
"""
103-
fill_in(G::Graph{T}, i::T}
104-
105-
Return number of edges that need to be added to make the neighbors of `i` a clique.
106-
"""
107-
function fill_in(G::Graph, node::Int)
108-
return num_missing_edges_subgraph(G, neighbors(G, node))
109-
end
110-
11169
"""
11270
is_clique(G::Graph{T}, x::Vector{T})
11371
11472
Return a `Bool` indication whether `x` is a clique in `G`.
11573
"""
11674
function is_clique(G::Graph, nodes::Vector{Int})
117-
return iszero(num_missing_edges_subgraph(G, nodes))
118-
end
75+
n = length(nodes)
11976

120-
struct FillInCache
121-
graph::Graph
122-
fill_in::Vector{Int}
123-
end
124-
function FillInCache(graph::Graph)
125-
return FillInCache(graph, [fill_in(graph, i) for i in 1:num_nodes(graph)])
126-
end
127-
Base.copy(G::FillInCache) = FillInCache(copy(G.graph), copy(G.fill_in))
128-
129-
num_nodes(G::FillInCache) = num_nodes(G.graph)
130-
neighbors(G::FillInCache, node::Int) = neighbors(G.graph, node)
131-
function add_edge!(G::FillInCache, i::Int, j::Int)
132-
ni = neighbors(G, i)
133-
nj = neighbors(G, j)
134-
if i in nj
135-
@assert j in ni
136-
return
137-
end
138-
@assert !(j in ni)
139-
for node in ni
140-
@assert node != i
141-
@assert node != j
142-
if node in nj
143-
G.fill_in[node] -= 1
77+
for i in 1:(n-1)
78+
for j in (i+1):n
79+
if nodes[j] neighbors(G, nodes[i])
80+
return false
81+
end
14482
end
14583
end
146-
G.fill_in[i] +=
147-
count(k -> is_enabled(G.graph, k), ni) -
148-
_num_edges_subgraph(G.graph, ni, j)
149-
G.fill_in[j] +=
150-
count(k -> is_enabled(G.graph, k), nj) -
151-
_num_edges_subgraph(G.graph, nj, i)
152-
return add_edge!(G.graph, i, j)
153-
end
154-
fill_in(G::FillInCache, node::Int) = G.fill_in[node]
155-
function disable_node!(G::FillInCache, node::Int)
156-
for neighbor in neighbors(G, node)
157-
nodes = neighbors(G, neighbor)
158-
G.fill_in[neighbor] -=
159-
(length(nodes) - 1) - _num_edges_subgraph(G.graph, nodes, node)
160-
end
161-
return disable_node!(G.graph, node)
84+
85+
return true
16286
end
163-
is_enabled(G::FillInCache, node::Int) = is_enabled(G.graph, node)
16487

16588
"""
16689
struct LabelledGraph{T}
@@ -256,116 +179,24 @@ function add_clique!(G::LabelledGraph{T}, x::Vector{T}) where {T}
256179
return add_clique!(G.graph, add_node!.(G, x))
257180
end
258181

259-
struct GreedyFillIn <: AbstractGreedyAlgorithm end
260-
cache(G::Graph, ::GreedyFillIn) = FillInCache(G)
261-
heuristic_value(G::FillInCache, node::Int, ::GreedyFillIn) = fill_in(G, node)
262-
263-
function _greedy_triangulation!(G, algo::AbstractGreedyAlgorithm)
264-
candidate_cliques = Vector{Int}[]
265-
for i in 1:num_nodes(G)
266-
node = argmin(map(1:num_nodes(G)) do node
267-
if is_enabled(G, node)
268-
heuristic_value(G, node, algo)
269-
else
270-
typemax(Int)
271-
end
272-
end)
273-
#=
274-
# look at all its neighbors. In G we want the neighbors to form a clique.
275-
neighbor_nodes = collect(neighbors(G, node))
276-
277-
# add neighbors and node as a potentially maximal clique
278-
candidate_clique = [neighbor for neighbor in neighbor_nodes if is_enabled(G, neighbor)]
279-
push!(candidate_clique, node)
280-
push!(candidate_cliques, candidate_clique)
281-
282-
# add edges to G to make the new candidate_clique at least potentially a clique
283-
for i in eachindex(neighbor_nodes)
284-
if is_enabled(G, i)
285-
for j in (i + 1):length(neighbor_nodes)
286-
if is_enabled(G, j)
287-
add_edge!(G, neighbor_nodes[i], neighbor_nodes[j])
288-
end
289-
end
290-
end
291-
end
292-
=#
293-
neighbor_nodes = [
294-
neighbor for
295-
neighbor in neighbors(G, node) if is_enabled(G, neighbor)
296-
]
297-
push!(candidate_cliques, [node, neighbor_nodes...])
298-
for i in eachindex(neighbor_nodes)
299-
for j in (i+1):length(neighbor_nodes)
300-
add_edge!(G, neighbor_nodes[i], neighbor_nodes[j])
301-
end
302-
end
303-
304-
disable_node!(G, node)
305-
end
306-
return candidate_cliques
307-
end
308-
309-
"""
310-
completion(G::Graph, comp::ChordalCompletion)
311-
312-
Return a chordal extension of `G` and the corresponding maximal cliques.
313-
314-
The algoritm is Algorithm 3 in [BA10] with the GreedyFillIn heuristic of Table I.
315-
316-
[BA10] Bodlaender, Hans L., and Arie MCA Koster.
317-
Treewidth computations I. Upper bounds.
318-
Information and Computation 208, no. 3 (2010): 259-275.
319-
Utrecht University, Utrecht, The Netherlands www.cs.uu.nl
320-
"""
321-
function completion(G::Graph, comp::ChordalCompletion)
322-
H = copy(G)
323-
324-
candidate_cliques = _greedy_triangulation!(cache(H, comp.algo), comp.algo)
325-
enable_all_nodes!(H)
326-
327-
sort!.(candidate_cliques)
328-
unique!(candidate_cliques)
329-
330-
# check whether candidate cliques are actually cliques
331-
candidate_cliques = candidate_cliques[[
332-
is_clique(H, clique) for clique in candidate_cliques
333-
]]
334-
sort!(candidate_cliques, by = x -> length(x))
335-
reverse!(candidate_cliques) # TODO use `rev=true` in `sort!`.
336-
337-
maximal_cliques = [first(candidate_cliques)]
338-
for clique in Iterators.drop(candidate_cliques, 1)
339-
if all(other_clique -> !(clique other_clique), maximal_cliques)
340-
push!(maximal_cliques, clique)
341-
end
342-
end
343-
344-
if length(Set(Iterators.flatten(maximal_cliques))) != num_nodes(H)
345-
error("Maximal cliques do not cover all nodes.")
346-
end
347-
348-
return H, maximal_cliques
349-
end
350-
351182
"""
352-
completion(G::Graph, comp::ChordalCompletion)
183+
completion(G::Graph, comp::ClusterCompletion)
353184
354185
Return a cluster completion of `G` and the corresponding maximal cliques.
355186
"""
356187
function completion(G::Graph, ::ClusterCompletion)
357188
H = copy(G)
358-
union_find = IntDisjointSets(num_nodes(G))
189+
union_find = DataStructures.IntDisjointSets(num_nodes(G))
359190
for from in 1:num_nodes(G)
360191
for to in neighbors(G, from)
361-
union!(union_find, from, to)
192+
DataStructures.union!(union_find, from, to)
362193
end
363194
end
364-
cliques = [Int[] for i in 1:num_groups(union_find)]
195+
cliques = [Int[] for i in 1:DataStructures.num_groups(union_find)]
365196
ids = zeros(Int, num_nodes(G))
366197
k = 0
367198
for node in 1:num_nodes(G)
368-
root = find_root!(union_find, node)
199+
root = DataStructures.find_root!(union_find, node)
369200
if iszero(ids[root])
370201
k += 1
371202
ids[root] = k
@@ -379,38 +210,46 @@ function completion(G::Graph, ::ClusterCompletion)
379210
return H, cliques
380211
end
381212

382-
function completion(G::Graph, comp::CliqueTreesCompletion)
213+
"""
214+
completion(G::Graph, comp::ChordalCompletion)
215+
216+
Return a chordal extension of `G` and the corresponding maximal cliques.
217+
218+
The algoritm is Algorithm 3 in [BA10] with the GreedyFillIn heuristic of Table I.
219+
220+
[BA10] Bodlaender, Hans L., and Arie MCA Koster.
221+
Treewidth computations I. Upper bounds.
222+
Information and Computation 208, no. 3 (2010): 259-275.
223+
Utrecht University, Utrecht, The Netherlands www.cs.uu.nl
224+
"""
225+
function completion(G::Graph, comp::ChordalCompletion)
383226
# construct a copy H of the graph G
384227
H = copy(G)
385228

386229
# construct adjacency matrix of H
387-
matrix = spzeros(Bool, num_nodes(H), num_nodes(H))
230+
matrix = SparseArrays.spzeros(Bool, num_nodes(H), num_nodes(H))
388231

389232
for j in axes(matrix, 2)
390-
matrix.colptr[j + 1] = matrix.colptr[j] + length(neighbors(H, j))
391-
append!(rowvals(matrix), neighbors(H, j))
233+
list = neighbors(H, j)
234+
SparseArrays.getcolptr(matrix)[j + 1] = SparseArrays.getcolptr(matrix)[j] + length(list)
235+
append!(SparseArrays.rowvals(matrix), list)
236+
append!(SparseArrays.nonzeros(matrix), zeros(Bool, length(list)))
392237
end
393238

394239
# sort row indices of adjacency matrix
395240
matrix = copy(transpose(matrix))
396241

397242
# construct tree decomposition of H
398-
label, tree = cliquetree(matrix; alg=comp.algo)
243+
label, tree = CliqueTrees.cliquetree(matrix; alg=comp.algo)
399244

400-
# construct chordal extension of H
401-
F = FilledGraph(tree)
245+
# construct vector of cliques and append fill edges to H
246+
cliques = Vector{Vector{Int}}(undef, length(tree))
402247

403-
# append fill edges to H
404-
for v in CliqueTrees.vertices(F)
405-
for w in CliqueTrees.neighbors(F, v)
406-
push!(neighbors(H, label[v]), label[w])
407-
push!(neighbors(H, label[w]), label[v])
408-
end
248+
for (i, v) in enumerate(tree)
249+
clique = cliques[i] = label[v]
250+
add_clique!(H, clique)
409251
end
410252

411-
# compute maximal cliques of H
412-
cliques = [label[clique] for clique in tree]
413-
414253
return H, cliques
415254
end
416255

0 commit comments

Comments
 (0)