Skip to content

Commit 939c9df

Browse files
gdalleamontoison
andauthored
Improve graph structure a little bit (#114)
* Add a function transpose_graph * Use the formatter on src/graph.jl * Remove old code * Add unit tests for transpose_graph * Clean up transpose * No docs * Docs * Codecov --------- Co-authored-by: Alexis Montoison <alexis.montoison@polymtl.ca>
1 parent 620436c commit 939c9df

7 files changed

Lines changed: 118 additions & 63 deletions

File tree

docs/src/dev.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ SparseMatrixColorings.vertices
1616
SparseMatrixColorings.neighbors
1717
SparseMatrixColorings.adjacency_graph
1818
SparseMatrixColorings.bipartite_graph
19+
transpose
1920
```
2021

2122
## Low-level coloring

src/SparseMatrixColorings.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ using SparseArrays:
3535
SparseMatrixCSC,
3636
dropzeros,
3737
dropzeros!,
38+
ftranspose!,
3839
nnz,
3940
nonzeros,
4041
nzrange,

src/coloring.jl

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ The vertices are colored in a greedy fashion, following the `order` supplied.
1919
function partial_distance2_coloring(
2020
bg::BipartiteGraph, ::Val{side}, order::AbstractOrder
2121
) where {side}
22-
color = Vector{Int}(undef, length(bg, Val(side)))
23-
forbidden_colors = Vector{Int}(undef, length(bg, Val(side)))
22+
color = Vector{Int}(undef, nb_vertices(bg, Val(side)))
23+
forbidden_colors = Vector{Int}(undef, nb_vertices(bg, Val(side)))
2424
vertices_in_order = vertices(bg, Val(side), order)
2525
partial_distance2_coloring!(color, forbidden_colors, bg, Val(side), vertices_in_order)
2626
return color
@@ -76,11 +76,11 @@ The vertices are colored in a greedy fashion, following the `order` supplied.
7676
"""
7777
function star_coloring(g::Graph{false}, order::AbstractOrder)
7878
# Initialize data structures
79-
nvertices = length(g)
80-
color = zeros(Int, nvertices)
81-
forbidden_colors = zeros(Int, nvertices)
82-
first_neighbor = fill((0, 0), nvertices) # at first no neighbors have been encountered
83-
treated = zeros(Int, nvertices)
79+
n = nb_vertices(g)
80+
color = zeros(Int, n)
81+
forbidden_colors = zeros(Int, n)
82+
first_neighbor = fill((0, 0), n) # at first no neighbors have been encountered
83+
treated = zeros(Int, n)
8484
star = Dict{Tuple{Int,Int},Int}()
8585
hub = Int[]
8686
vertices_in_order = vertices(g, order)
@@ -269,12 +269,12 @@ The vertices are colored in a greedy fashion, following the `order` supplied.
269269
"""
270270
function acyclic_coloring(g::Graph{false}, order::AbstractOrder)
271271
# Initialize data structures
272-
nvertices = length(g)
273-
nedges = nnz(g) ÷ 2 # symmetric sparse matrix with empty diagonal
274-
color = zeros(Int, nvertices)
275-
forbidden_colors = zeros(Int, nvertices)
276-
first_neighbor = fill((0, 0), nvertices) # at first no neighbors have been encountered
277-
first_visit_to_tree = fill((0, 0), nedges)
272+
n = nb_vertices(g)
273+
e = nb_edges(g) ÷ 2 # symmetric sparse matrix with empty diagonal
274+
color = zeros(Int, n)
275+
forbidden_colors = zeros(Int, n)
276+
first_neighbor = fill((0, 0), n) # at first no neighbors have been encountered
277+
first_visit_to_tree = fill((0, 0), e)
278278
forest = DisjointSets{Tuple{Int,Int}}()
279279
vertices_in_order = vertices(g, order)
280280

src/graph.jl

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,72 @@
33
"""
44
Graph{loops,T}
55
6-
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.
6+
Store a sparse matrix (in CSC) without its values, keeping only the pattern of nonzeros.
7+
It can be seen as a graph mapping columns to rows, hence the name `Graph`.
88
99
The type parameter `loops` must be set to:
1010
- `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)
1111
- `false` otherwise (e.g. for an adjacency graph)
1212
1313
# Fields
1414
15-
- `colptr::Vector{T}`: same as for `SparseMatrixCSC`
16-
- `rowval::Vector{T}`: same as for `SparseMatrixCSC`
15+
Copied from `SparseMatrixCSC`:
16+
17+
- `m::Int`: number of rows
18+
- `n::Int`: number of columns
19+
- `colptr::Vector{T}`: column `j` is in `colptr[j]:(colptr[j+1]-1)`
20+
- `rowval::Vector{T}`: row indices of stored values
1721
"""
1822
struct Graph{loops,T<:Integer}
23+
m::Int
24+
n::Int
1925
colptr::Vector{T}
2026
rowval::Vector{T}
2127
end
2228

2329
function Graph{loops}(S::SparseMatrixCSC{Tv,Ti}) where {loops,Tv,Ti}
24-
return Graph{loops,Ti}(S.colptr, S.rowval)
30+
return Graph{loops,Ti}(S.m, S.n, S.colptr, S.rowval)
2531
end
2632

27-
Base.length(g::Graph) = length(g.colptr) - 1
33+
SparseArrays.nnz(g::Graph) = length(g.rowval)
34+
SparseArrays.rowvals(g::Graph) = g.rowval
35+
SparseArrays.nzrange(g::Graph, j::Integer) = g.colptr[j]:(g.colptr[j + 1] - 1)
36+
37+
nb_vertices(g::Graph) = g.n
38+
vertices(g::Graph) = 1:nb_vertices(g)
2839

29-
SparseArrays.nnz(g::Graph{true}) = length(g.rowval)
40+
nb_edges(g::Graph{true}) = length(g.rowval)
3041

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]
42+
function nb_edges(g::Graph{false})
43+
e = 0
44+
for j in vertices(g)
45+
for k in nzrange(g, j)
46+
i = rowvals(g)[k]
3647
if i != j
37-
n += 1
48+
e += 1
3849
end
3950
end
4051
end
41-
return n
52+
return e
4253
end
4354

44-
vertices(g::Graph) = 1:length(g)
45-
4655
function neighbors(g::Graph{true}, v::Integer)
47-
return view(g.rowval, g.colptr[v]:(g.colptr[v + 1] - 1))
56+
return view(rowvals(g), nzrange(g, v))
4857
end
4958

5059
function neighbors(g::Graph{false}, v::Integer)
51-
neighbors_with_loops = view(g.rowval, g.colptr[v]:(g.colptr[v + 1] - 1))
60+
neighbors_with_loops = view(rowvals(g), nzrange(g, v))
5261
return Iterators.filter(!=(v), neighbors_with_loops) # TODO: optimize
5362
end
5463

5564
function degree(g::Graph{true}, v::Integer)
56-
return length(g.colptr[v]:(g.colptr[v + 1] - 1))
65+
return length(nzrange(g, v))
5766
end
5867

5968
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
69+
d = length(nzrange(g, v))
70+
for k in nzrange(g, v)
71+
if rowvals(g)[k] == v
6372
d -= 1
6473
end
6574
end
@@ -69,6 +78,17 @@ end
6978
maximum_degree(g::Graph) = maximum(Base.Fix1(degree, g), vertices(g))
7079
minimum_degree(g::Graph) = minimum(Base.Fix1(degree, g), vertices(g))
7180

81+
"""
82+
transpose(g::Graph)
83+
84+
Return a [`Graph`](@ref) corresponding to the transpose of (the underlying matrix of) `g`.
85+
"""
86+
function Base.transpose(g::Graph{loops,T}) where {loops,T}
87+
S = SparseMatrixCSC{T,T}(g.m, g.n, g.colptr, g.rowval, g.rowval)
88+
Sᵀ = convert(SparseMatrixCSC, transpose(S)) # TODO: use ftranspose! without segfault?
89+
return Graph{loops}(Sᵀ)
90+
end
91+
7292
## Bipartite graph
7393

7494
"""
@@ -88,16 +108,17 @@ struct BipartiteGraph{T<:Integer}
88108
g2::Graph{true,T}
89109
end
90110

91-
Base.length(bg::BipartiteGraph, ::Val{1}) = length(bg.g1)
92-
Base.length(bg::BipartiteGraph, ::Val{2}) = length(bg.g2)
93-
SparseArrays.nnz(bg::BipartiteGraph) = nnz(bg.g1)
111+
nb_vertices(bg::BipartiteGraph, ::Val{1}) = nb_vertices(bg.g1)
112+
nb_vertices(bg::BipartiteGraph, ::Val{2}) = nb_vertices(bg.g2)
113+
114+
nb_edges(bg::BipartiteGraph) = nb_edges(bg.g1)
94115

95116
"""
96117
vertices(bg::BipartiteGraph, Val(side))
97118
98119
Return the list of vertices of `bg` from the specified `side` as a range `1:n`.
99120
"""
100-
vertices(bg::BipartiteGraph, ::Val{side}) where {side} = 1:length(bg, Val(side))
121+
vertices(bg::BipartiteGraph, ::Val{side}) where {side} = 1:nb_vertices(bg, Val(side))
101122

102123
"""
103124
neighbors(bg::BipartiteGraph, Val(side), v::Integer)
@@ -159,7 +180,7 @@ function bipartite_graph(A::SparseMatrixCSC; symmetric_pattern::Bool=false)
159180
checksquare(A) # proxy for checking full symmetry
160181
g1 = g2
161182
else
162-
g1 = Graph{true}(convert(SparseMatrixCSC, transpose(A))) # rows to columns
183+
g1 = transpose(g2) # rows to columns
163184
end
164185
return BipartiteGraph(g1, g2)
165186
end

src/order.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ end
4141
RandomOrder() = RandomOrder(default_rng())
4242

4343
function vertices(g::Graph, order::RandomOrder)
44-
return randperm(order.rng, length(g))
44+
return randperm(order.rng, nb_vertices(g))
4545
end
4646

4747
function vertices(bg::BipartiteGraph, ::Val{side}, order::RandomOrder) where {side}
48-
return randperm(order.rng, length(bg, Val(side)))
48+
return randperm(order.rng, nb_vertices(bg, Val(side)))
4949
end
5050

5151
"""

test/graph.jl

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,50 @@
11
using LinearAlgebra
22
using SparseArrays
3-
using SparseMatrixColorings: Graph, adjacency_graph, bipartite_graph, degree, neighbors
3+
using SparseMatrixColorings:
4+
Graph, adjacency_graph, bipartite_graph, degree, nb_vertices, nb_edges, neighbors
45
using Test
56

67
## Standard graph
78

89
@testset "Graph" begin
910
g = Graph{true}(sparse([
10-
1 0 1
11-
1 1 0
12-
0 0 0
11+
1 0 1 1
12+
1 1 0 0
13+
0 0 0 1
1314
]))
15+
gᵀ = transpose(g)
1416

15-
@test length(g) == 3
16-
@test nnz(g) == 4
17+
@test nb_vertices(g) == 4
18+
@test nb_edges(g) == 6
19+
@test nnz(g) == 6
1720
@test neighbors(g, 1) == [1, 2]
1821
@test neighbors(g, 2) == [2]
1922
@test neighbors(g, 3) == [1]
23+
@test neighbors(g, 4) == [1, 3]
2024
@test degree(g, 1) == 2
2125
@test degree(g, 2) == 1
2226
@test degree(g, 3) == 1
27+
@test degree(g, 4) == 2
28+
29+
@test nb_vertices(gᵀ) == 3
30+
@test nb_edges(gᵀ) == 6
31+
@test nnz(gᵀ) == 6
32+
@test neighbors(gᵀ, 1) == [1, 3, 4]
33+
@test neighbors(gᵀ, 2) == [1, 2]
34+
@test neighbors(gᵀ, 3) == [4]
35+
@test degree(gᵀ, 1) == 3
36+
@test degree(gᵀ, 2) == 2
37+
@test degree(gᵀ, 3) == 1
2338

2439
g = Graph{false}(sparse([
2540
1 0 1
2641
1 1 0
2742
0 0 0
2843
]))
2944

30-
@test length(g) == 3
31-
@test nnz(g) == 2
45+
@test nb_vertices(g) == 3
46+
@test nb_edges(g) == 2
47+
@test nnz(g) == 4
3248
@test collect(neighbors(g, 1)) == [2]
3349
@test collect(neighbors(g, 2)) == Int[]
3450
@test collect(neighbors(g, 3)) == [1]
@@ -49,8 +65,8 @@ end;
4965

5066
bg = bipartite_graph(A; symmetric_pattern=false)
5167
@test_throws DimensionMismatch bipartite_graph(A; symmetric_pattern=true)
52-
@test length(bg, Val(1)) == 4
53-
@test length(bg, Val(2)) == 8
68+
@test nb_vertices(bg, Val(1)) == 4
69+
@test nb_vertices(bg, Val(2)) == 8
5470
# neighbors of rows
5571
@test neighbors(bg, Val(1), 1) == [1, 6, 7, 8]
5672
@test neighbors(bg, Val(1), 2) == [2, 5, 7, 8]
@@ -73,8 +89,8 @@ end;
7389
1 1 0 1
7490
])
7591
bg = bipartite_graph(A; symmetric_pattern=true)
76-
@test length(bg, Val(1)) == 4
77-
@test length(bg, Val(2)) == 4
92+
@test nb_vertices(bg, Val(1)) == 4
93+
@test nb_vertices(bg, Val(2)) == 4
7894
# neighbors of rows and columns
7995
@test neighbors(bg, Val(1), 1) == neighbors(bg, Val(2), 1) == [1, 3, 4]
8096
@test neighbors(bg, Val(1), 2) == neighbors(bg, Val(2), 2) == [2, 4]
@@ -94,7 +110,7 @@ end;
94110

95111
B = transpose(A) * A
96112
g = adjacency_graph(B - Diagonal(B))
97-
@test length(g) == 8
113+
@test nb_vertices(g) == 8
98114
@test collect(neighbors(g, 1)) == [6, 7, 8]
99115
@test collect(neighbors(g, 2)) == [5, 7, 8]
100116
@test collect(neighbors(g, 3)) == [5, 6, 8]
@@ -104,3 +120,14 @@ end;
104120
@test collect(neighbors(g, 7)) == [1, 2, 4, 5, 6, 8]
105121
@test collect(neighbors(g, 8)) == [1, 2, 3, 5, 6, 7]
106122
end
123+
124+
@testset "Transpose" begin
125+
for _ in 1:1000
126+
A = sprand(rand(100:1000), rand(100:1000), 0.1)
127+
g = Graph{true}(A)
128+
gᵀ = transpose(g)
129+
gᵀ_true = Graph{true}(sparse(transpose(A)))
130+
@test gᵀ.colptr == gᵀ_true.colptr
131+
@test gᵀ.rowval == gᵀ_true.rowval
132+
end
133+
end

test/suitesparse.jl

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ using SparseMatrixColorings:
1212
degree,
1313
minimum_degree,
1414
maximum_degree,
15+
nb_vertices,
16+
nb_edges,
1517
neighbors,
1618
partial_distance2_coloring,
1719
star_coloring,
@@ -34,15 +36,16 @@ colpack_table_6_7 = CSV.read(
3436
original_mat = matrixdepot("$(row[:group])/$(row[:name])")
3537
mat = dropzeros(original_mat)
3638
bg = bipartite_graph(mat)
37-
@test length(bg, Val(1)) == row[:V1]
38-
@test length(bg, Val(2)) == row[:V2]
39-
@test nnz(bg) == row[:E]
39+
@test nb_vertices(bg, Val(1)) == row[:V1]
40+
@test nb_vertices(bg, Val(2)) == row[:V2]
41+
@test nb_edges(bg) == row[:E]
4042
@test maximum_degree(bg, Val(1)) == row[:Δ1]
4143
@test maximum_degree(bg, Val(2)) == row[:Δ2]
4244
color_N1 = partial_distance2_coloring(bg, Val(1), NaturalOrder())
4345
color_N2 = partial_distance2_coloring(bg, Val(2), NaturalOrder())
4446
@test length(unique(color_N1)) == row[:N1]
4547
@test length(unique(color_N2)) == row[:N2]
48+
yield()
4649
end
4750
end;
4851

@@ -61,9 +64,9 @@ what_table_31_32 = CSV.read(
6164
original_mat = matrixdepot("$(row[:group])/$(row[:name])")
6265
mat = original_mat # no dropzeros
6366
bg = bipartite_graph(mat)
64-
@test length(bg, Val(1)) == row[:m]
65-
@test length(bg, Val(2)) == row[:n]
66-
@test nnz(bg) == row[:nnz]
67+
@test nb_vertices(bg, Val(1)) == row[:m]
68+
@test nb_vertices(bg, Val(2)) == row[:n]
69+
@test nb_edges(bg) == row[:nnz]
6770
@test minimum_degree(bg, Val(1)) == row[:ρmin]
6871
@test maximum_degree(bg, Val(1)) == row[:ρmax]
6972
@test minimum_degree(bg, Val(2)) == row[:κmin]
@@ -74,6 +77,7 @@ what_table_31_32 = CSV.read(
7477
else
7578
@test_broken length(unique(color_Nb)) == row[:K]
7679
end
80+
yield()
7781
end
7882
end;
7983

@@ -91,11 +95,12 @@ what_table_41_42 = CSV.read(
9195
mat = dropzeros(sparse(original_mat))
9296
ag = adjacency_graph(mat)
9397
bg = bipartite_graph(mat)
94-
@test length(ag) == row[:V]
95-
@test nnz(ag) ÷ 2 == row[:E]
98+
@test nb_vertices(ag) == row[:V]
99+
@test nb_edges(ag) ÷ 2 == row[:E]
96100
@test maximum_degree(ag) == row[]
97101
@test minimum_degree(ag) == row[]
98102
color_N, _ = star_coloring(ag, NaturalOrder())
99103
@test_skip row[:KS1] <= length(unique(color_N)) <= row[:KS2] # TODO: find better
104+
yield()
100105
end
101106
end;

0 commit comments

Comments
 (0)