Skip to content

Commit 1bbe0a5

Browse files
authored
Avoid allocation when creating adjacency graph (#104)
1 parent 123889f commit 1bbe0a5

4 files changed

Lines changed: 86 additions & 24 deletions

File tree

src/SparseMatrixColorings.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ $EXPORTS
99
"""
1010
module SparseMatrixColorings
1111

12-
using DataStructures: DisjointSets, find_root!, root_union!, num_groups
1312
using ADTypes: ADTypes
13+
using Base.Iterators: Iterators
1414
using Compat: @compat, stack
15+
using DataStructures: DisjointSets, find_root!, root_union!, num_groups
1516
using DocStringExtensions: README, EXPORTS, SIGNATURES, TYPEDEF, TYPEDFIELDS
1617
using LinearAlgebra:
1718
Adjoint,

src/coloring.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ The vertices are colored in a greedy fashion, following the `order` supplied.
7474
7575
> [_New Acyclic and Star Coloring Algorithms with Application to Computing Hessians_](https://epubs.siam.org/doi/abs/10.1137/050639879), Gebremedhin et al. (2007), Algorithm 4.1
7676
"""
77-
function star_coloring(g::Graph, order::AbstractOrder)
77+
function star_coloring(g::Graph{false}, order::AbstractOrder)
7878
# Initialize data structures
7979
nvertices = length(g)
8080
color = zeros(Int, nvertices)
@@ -267,7 +267,7 @@ The vertices are colored in a greedy fashion, following the `order` supplied.
267267
268268
> [_New Acyclic and Star Coloring Algorithms with Application to Computing Hessians_](https://epubs.siam.org/doi/abs/10.1137/050639879), Gebremedhin et al. (2007), Algorithm 3.1
269269
"""
270-
function acyclic_coloring(g::Graph, order::AbstractOrder)
270+
function acyclic_coloring(g::Graph{false}, order::AbstractOrder)
271271
# Initialize data structures
272272
nvertices = length(g)
273273
nedges = nnz(g) ÷ 2 # symmetric sparse matrix with empty diagonal

src/graph.jl

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,70 @@
11
## Standard graph
22

33
"""
4-
Graph{T}
4+
Graph{loops,T}
55
66
Undirected graph structure stored in Compressed Sparse Column (CSC) format.
7+
Note that the underyling CSC may not be square, so the term "graph" is slightly abusive.
8+
9+
The type parameter `loops` must be set to:
10+
- `true` if coefficients `(i, i)` present in the CSC are counted as edges in the graph (e.g. for each half of a bipartite graph)
11+
- `false` otherwise (e.g. for an adjacency graph)
712
813
# Fields
914
1015
- `colptr::Vector{T}`: same as for `SparseMatrixCSC`
1116
- `rowval::Vector{T}`: same as for `SparseMatrixCSC`
1217
"""
13-
struct Graph{T<:Integer}
18+
struct Graph{loops,T<:Integer}
1419
colptr::Vector{T}
1520
rowval::Vector{T}
1621
end
1722

18-
Graph(S::SparseMatrixCSC) = Graph(S.colptr, S.rowval)
23+
function Graph{loops}(S::SparseMatrixCSC{Tv,Ti}) where {loops,Tv,Ti}
24+
return Graph{loops,Ti}(S.colptr, S.rowval)
25+
end
1926

2027
Base.length(g::Graph) = length(g.colptr) - 1
21-
SparseArrays.nnz(g::Graph) = length(g.rowval)
28+
29+
SparseArrays.nnz(g::Graph{true}) = length(g.rowval)
30+
31+
function SparseArrays.nnz(g::Graph{false})
32+
n = 0
33+
for j in 1:(length(g.colptr) - 1)
34+
for k in g.colptr[j]:(g.colptr[j + 1] - 1)
35+
i = g.rowval[k]
36+
if i != j
37+
n += 1
38+
end
39+
end
40+
end
41+
return n
42+
end
2243

2344
vertices(g::Graph) = 1:length(g)
24-
neighbors(g::Graph, v::Integer) = view(g.rowval, g.colptr[v]:(g.colptr[v + 1] - 1))
25-
degree(g::Graph, v::Integer) = length(g.colptr[v]:(g.colptr[v + 1] - 1))
45+
46+
function neighbors(g::Graph{true}, v::Integer)
47+
return view(g.rowval, g.colptr[v]:(g.colptr[v + 1] - 1))
48+
end
49+
50+
function neighbors(g::Graph{false}, v::Integer)
51+
neighbors_with_loops = view(g.rowval, g.colptr[v]:(g.colptr[v + 1] - 1))
52+
return Iterators.filter(!=(v), neighbors_with_loops) # TODO: optimize
53+
end
54+
55+
function degree(g::Graph{true}, v::Integer)
56+
return length(g.colptr[v]:(g.colptr[v + 1] - 1))
57+
end
58+
59+
function degree(g::Graph{false}, v::Integer)
60+
d = length(g.colptr[v]:(g.colptr[v + 1] - 1))
61+
for k in g.colptr[v]:(g.colptr[v + 1] - 1)
62+
if g.rowval[k] == v
63+
d -= 1
64+
end
65+
end
66+
return d
67+
end
2668

2769
maximum_degree(g::Graph) = maximum(Base.Fix1(degree, g), vertices(g))
2870
minimum_degree(g::Graph) = minimum(Base.Fix1(degree, g), vertices(g))
@@ -42,8 +84,8 @@ A bipartite graph has two "sides", which we number `1` and `2`.
4284
- `g2::Graph{T}`: contains the neighbors for vertices on side `2`
4385
"""
4486
struct BipartiteGraph{T<:Integer}
45-
g1::Graph{T}
46-
g2::Graph{T}
87+
g1::Graph{true,T}
88+
g2::Graph{true,T}
4789
end
4890

4991
Base.length(bg::BipartiteGraph, ::Val{1}) = length(bg.g1)
@@ -92,7 +134,7 @@ The adjacency graph of a symmetrix matric `A ∈ ℝ^{n × n}` is `G(A) = (V, E)
92134
93135
> [_What Color Is Your Jacobian? Graph Coloring for Computing Derivatives_](https://epubs.siam.org/doi/10.1137/S0036144504444711), Gebremedhin et al. (2005)
94136
"""
95-
adjacency_graph(A::SparseMatrixCSC) = Graph(A - Diagonal(A))
137+
adjacency_graph(A::SparseMatrixCSC) = Graph{false}(A)
96138

97139
"""
98140
bipartite_graph(A::SparseMatrixCSC; symmetric_pattern::Bool)
@@ -112,12 +154,12 @@ When `symmetric_pattern` is `true`, this construction is more efficient.
112154
> [_What Color Is Your Jacobian? Graph Coloring for Computing Derivatives_](https://epubs.siam.org/doi/10.1137/S0036144504444711), Gebremedhin et al. (2005)
113155
"""
114156
function bipartite_graph(A::SparseMatrixCSC; symmetric_pattern::Bool=false)
115-
g2 = Graph(A) # columns to rows
157+
g2 = Graph{true}(A) # columns to rows
116158
if symmetric_pattern
117159
checksquare(A) # proxy for checking full symmetry
118160
g1 = g2
119161
else
120-
g1 = Graph(sparse(transpose(A))) # rows to columns
162+
g1 = Graph{true}(sparse(transpose(A))) # rows to columns
121163
end
122164
return BipartiteGraph(g1, g2)
123165
end

test/graph.jl

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,40 @@
11
using LinearAlgebra
22
using SparseArrays
3-
using SparseMatrixColorings: Graph, adjacency_graph, bipartite_graph, neighbors
3+
using SparseMatrixColorings: Graph, adjacency_graph, bipartite_graph, degree, neighbors
44
using Test
55

66
## Standard graph
77

88
@testset "Graph" begin
9-
g = Graph(sparse([
9+
g = Graph{true}(sparse([
1010
1 0 1
1111
1 1 0
1212
0 0 0
1313
]))
1414

1515
@test length(g) == 3
16+
@test nnz(g) == 4
1617
@test neighbors(g, 1) == [1, 2]
1718
@test neighbors(g, 2) == [2]
1819
@test neighbors(g, 3) == [1]
20+
@test degree(g, 1) == 2
21+
@test degree(g, 2) == 1
22+
@test degree(g, 3) == 1
23+
24+
g = Graph{false}(sparse([
25+
1 0 1
26+
1 1 0
27+
0 0 0
28+
]))
29+
30+
@test length(g) == 3
31+
@test nnz(g) == 2
32+
@test collect(neighbors(g, 1)) == [2]
33+
@test collect(neighbors(g, 2)) == Int[]
34+
@test collect(neighbors(g, 3)) == [1]
35+
@test degree(g, 1) == 1
36+
@test degree(g, 2) == 0
37+
@test degree(g, 3) == 1
1938
end;
2039

2140
## Bipartite graph (fig 3.1 of "What color is your Jacobian?")
@@ -76,12 +95,12 @@ end;
7695
B = transpose(A) * A
7796
g = adjacency_graph(B - Diagonal(B))
7897
@test length(g) == 8
79-
@test neighbors(g, 1) == [6, 7, 8]
80-
@test neighbors(g, 2) == [5, 7, 8]
81-
@test neighbors(g, 3) == [5, 6, 8]
82-
@test neighbors(g, 4) == [5, 6, 7]
83-
@test neighbors(g, 5) == [2, 3, 4, 6, 7, 8]
84-
@test neighbors(g, 6) == [1, 3, 4, 5, 7, 8]
85-
@test neighbors(g, 7) == [1, 2, 4, 5, 6, 8]
86-
@test neighbors(g, 8) == [1, 2, 3, 5, 6, 7]
98+
@test collect(neighbors(g, 1)) == [6, 7, 8]
99+
@test collect(neighbors(g, 2)) == [5, 7, 8]
100+
@test collect(neighbors(g, 3)) == [5, 6, 8]
101+
@test collect(neighbors(g, 4)) == [5, 6, 7]
102+
@test collect(neighbors(g, 5)) == [2, 3, 4, 6, 7, 8]
103+
@test collect(neighbors(g, 6)) == [1, 3, 4, 5, 7, 8]
104+
@test collect(neighbors(g, 7)) == [1, 2, 4, 5, 6, 8]
105+
@test collect(neighbors(g, 8)) == [1, 2, 3, 5, 6, 7]
87106
end

0 commit comments

Comments
 (0)