Skip to content

Commit 38aa242

Browse files
committed
Star coloring -- enhanced edition
1 parent 1e8127c commit 38aa242

4 files changed

Lines changed: 156 additions & 111 deletions

File tree

src/coloring.jl

Lines changed: 105 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,46 @@ If `postprocessing=true`, some colors might be replaced with `0` (the "neutral"
7878
"""
7979
function star_coloring(g::AdjacencyGraph, order::AbstractOrder; postprocessing::Bool)
8080
# Initialize data structures
81+
S = pattern(g)
8182
nv = nb_vertices(g)
8283
ne = nb_edges(g)
8384
color = zeros(Int, nv)
8485
forbidden_colors = zeros(Int, nv)
85-
first_neighbor = fill((0, 0), nv) # at first no neighbors have been encountered
86+
first_neighbor = fill((0, 0, 0), nv) # at first no neighbors have been encountered
8687
treated = zeros(Int, nv)
87-
star = Dict{Tuple{Int,Int},Int}()
88-
sizehint!(star, ne)
88+
edge_to_index = Vector{Int}(undef, nnz(S))
89+
star = Vector{Int}(undef, ne)
8990
hub = Int[] # one hub for each star, including the trivial ones
9091
nb_spokes = Int[] # number of spokes for each star
9192
vertices_in_order = vertices(g, order)
9293

94+
# edge_to_index gives an index for each edge
95+
# use forbidden_colors (or color) for the offsets of each column
96+
offsets = forbidden_colors
97+
counter = 0
98+
rvS = rowvals(S)
99+
for j in axes(S, 2)
100+
for k in nzrange(S, j)
101+
i = rvS[k]
102+
if i > j
103+
counter += 1
104+
edge_to_index[k] = counter
105+
k2 = S.colptr[i] + offsets[i]
106+
edge_to_index[k2] = counter
107+
offsets[i] += 1
108+
end
109+
end
110+
end
111+
fill!(offsets, 0)
112+
# Note that we don't need to do that for bicoloring,
113+
# we can build that in the same time than the transposed sparsity pattern of A
114+
93115
for v in vertices_in_order
94-
for w in neighbors(g, v)
95-
iszero(color[w]) && continue
116+
for (iw, w) in enumerate(neighbors2(g, v))
117+
(v == w || iszero(color[w])) && continue
118+
index_vw = edge_to_index[S.colptr[v] + iw - 1]
96119
forbidden_colors[color[w]] = v
97-
(p, q) = first_neighbor[color[w]]
120+
(p, q, _) = first_neighbor[color[w]]
98121
if p == v # Case 1
99122
if treated[q] != v
100123
# forbid colors of neighbors of q
@@ -103,11 +126,11 @@ function star_coloring(g::AdjacencyGraph, order::AbstractOrder; postprocessing::
103126
# forbid colors of neighbors of w
104127
_treat!(treated, forbidden_colors, g, v, w, color)
105128
else
106-
first_neighbor[color[w]] = (v, w)
107-
for x in neighbors(g, w)
108-
(x == v || iszero(color[x])) && continue
109-
wx = _sort(w, x)
110-
if x == hub[star[wx]] # potential Case 2 (which is always false for trivial stars with two vertices, since the associated hub is negative)
129+
first_neighbor[color[w]] = (v, w, index_vw)
130+
for (ix, x) in enumerate(neighbors2(g, w))
131+
(x == w || x == v || iszero(color[x])) && continue
132+
index_wx = edge_to_index[S.colptr[w] + ix - 1]
133+
if x == hub[star[index_wx]] # potential Case 2 (which is always false for trivial stars with two vertices, since the associated hub is negative)
111134
forbidden_colors[color[x]] = v
112135
end
113136
end
@@ -119,58 +142,16 @@ function star_coloring(g::AdjacencyGraph, order::AbstractOrder; postprocessing::
119142
break
120143
end
121144
end
122-
_update_stars!(star, hub, nb_spokes, g, v, color, first_neighbor)
145+
_update_stars!(star, hub, nb_spokes, g, v, color, first_neighbor, edge_to_index)
123146
end
124-
star_set = StarSet(star, hub, nb_spokes)
147+
star_set = StarSet(g, star, hub, nb_spokes, color, edge_to_index)
125148
if postprocessing
126149
# Reuse the vector forbidden_colors to compute offsets during post-processing
127150
postprocess!(color, star_set, g, forbidden_colors)
128151
end
129-
return color, star_set
130-
end
131-
132-
"""
133-
StarSet
134-
135-
Encode a set of 2-colored stars resulting from the [`star_coloring`](@ref) algorithm.
136-
137-
# Fields
138-
139-
$TYPEDFIELDS
140-
"""
141-
struct StarSet
142-
"a mapping from edges (pair of vertices) to their star index"
143-
star::Dict{Tuple{Int,Int},Int}
144-
"a mapping from star indices to their hub (undefined hubs for single-edge stars are the negative value of one of the vertices, picked arbitrarily)"
145-
hub::Vector{Int}
146-
"a mapping from star indices to the vector of their spokes"
147-
spokes::Vector{Vector{Int}}
148-
end
149-
150-
function StarSet(star::Dict{Tuple{Int,Int},Int}, hub::Vector{Int}, nb_spokes::Vector{Int})
151-
# Create a list of spokes for each star, preallocating their sizes based on nb_spokes
152-
spokes = [Vector{Int}(undef, ns) for ns in nb_spokes]
153-
154-
# Reuse nb_spokes as counters to track the current index while filling the spokes
155-
fill!(nb_spokes, 0)
156-
157-
for ((i, j), s) in pairs(star)
158-
h = abs(hub[s])
159-
nb_spokes[s] += 1
160-
index = nb_spokes[s]
161-
162-
# Assign the non-hub vertex (spoke) to the correct position in spokes
163-
if i == h
164-
spokes[s][index] = j
165-
elseif j == h
166-
spokes[s][index] = i
167-
end
168-
end
169-
return StarSet(star, hub, spokes)
152+
return color, star_set, edge_to_index
170153
end
171154

172-
_sort(u, v) = (min(u, v), max(u, v))
173-
174155
function _treat!(
175156
# modified
176157
treated::AbstractVector{<:Integer},
@@ -191,86 +172,112 @@ end
191172

192173
function _update_stars!(
193174
# modified
194-
star::Dict{<:Tuple,<:Integer},
175+
star::AbstractVector{<:Integer},
195176
hub::AbstractVector{<:Integer},
196177
nb_spokes::AbstractVector{<:Integer},
197178
# not modified
198179
g::AdjacencyGraph,
199180
v::Integer,
200181
color::AbstractVector{<:Integer},
201182
first_neighbor::AbstractVector{<:Tuple},
183+
edge_to_index::AbstractVector{<:Integer},
202184
)
203-
for w in neighbors(g, v)
204-
iszero(color[w]) && continue
205-
vw = _sort(v, w)
185+
S = pattern(g)
186+
for (iw, w) in enumerate(neighbors2(g, v))
187+
(v == w || iszero(color[w])) && continue
188+
index_vw = edge_to_index[S.colptr[v] + iw - 1]
206189
x_exists = false
207-
for x in neighbors(g, w)
190+
for (ix, x) in enumerate(neighbors2(g, w))
191+
(x == w) && continue
208192
if x != v && color[x] == color[v] # vw, wx ∈ E
209-
wx = _sort(w, x)
210-
star_wx = star[wx]
193+
index_wx = edge_to_index[S.colptr[w] + ix - 1]
194+
star_wx = star[index_wx]
211195
hub[star_wx] = w # this may already be true
212196
nb_spokes[star_wx] += 1
213-
star[vw] = star_wx
197+
star[index_vw] = star_wx
214198
x_exists = true
215199
break
216200
end
217201
end
218202
if !x_exists
219-
(p, q) = first_neighbor[color[w]]
203+
(p, q, index_pq) = first_neighbor[color[w]]
220204
if p == v && q != w # vw, vq ∈ E and color[w] = color[q]
221-
vq = _sort(v, q)
222-
star_vq = star[vq]
205+
star_vq = star[index_pq]
223206
hub[star_vq] = v # this may already be true
224207
nb_spokes[star_vq] += 1
225-
star[vw] = star_vq
208+
star[index_vw] = star_vq
226209
else # vw forms a new star
227210
push!(hub, -max(v, w)) # star is trivial (composed only of two vertices) so we set the hub to a negative value, but it allows us to choose one of the two vertices
228211
push!(nb_spokes, 1)
229-
star[vw] = length(hub)
212+
star[index_vw] = length(hub)
230213
end
231214
end
232215
end
233216
return nothing
234217
end
235218

236219
"""
237-
symmetric_coefficient(
238-
i::Integer, j::Integer,
239-
color::AbstractVector{<:Integer},
240-
star_set::StarSet
241-
)
242-
243-
Return the indices `(k, c)` such that `A[i, j] = B[k, c]`, where `A` is the uncompressed symmetric matrix and `B` is the column-compressed matrix.
220+
StarSet
244221
245-
This function corresponds to algorithm `DirectRecover2` in the paper.
222+
Encode a set of 2-colored stars resulting from the [`star_coloring`](@ref) algorithm.
246223
247-
# References
224+
# Fields
248225
249-
> [_Efficient Computation of Sparse Hessians Using Coloring and Automatic Differentiation_](https://pubsonline.informs.org/doi/abs/10.1287/ijoc.1080.0286), Gebremedhin et al. (2009), Figure 3
226+
$TYPEDFIELDS
250227
"""
251-
function symmetric_coefficient(
252-
i::Integer, j::Integer, color::AbstractVector{<:Integer}, star_set::StarSet
228+
struct StarSet
229+
"a mapping from edges (pair of vertices) to their star index"
230+
star::Vector{Int}
231+
"a mapping from star indices to their hub (undefined hubs for single-edge stars are the negative value of one of the vertices, picked arbitrarily)"
232+
hub::Vector{Int}
233+
"a mapping from star indices to the vector of their spokes"
234+
spokes::Vector{Vector{Int}}
235+
end
236+
237+
function StarSet(
238+
g::AdjacencyGraph,
239+
star::Vector{Int},
240+
hub::Vector{Int},
241+
nb_spokes::Vector{Int},
242+
color::Vector{Int},
243+
edge_to_index::Vector{Int},
253244
)
254-
(; star, hub) = star_set
255-
if i == j
256-
# diagonal
257-
return i, color[j]
258-
end
259-
if i > j # keys of star are sorted tuples
260-
# star only contains one triangle
261-
i, j = j, i
262-
end
263-
star_id = star[i, j]
264-
h = abs(hub[star_id])
265-
if h == j
266-
# i is the spoke
267-
return i, color[h]
268-
else
269-
# j is the spoke
270-
return j, color[h]
245+
S = pattern(g)
246+
n = S.n
247+
248+
# Create a list of spokes for each star, preallocating their sizes based on nb_spokes
249+
spokes = [Vector{Int}(undef, ns) for ns in nb_spokes]
250+
251+
# Reuse nb_spokes as counters to track the current index while filling the spokes
252+
fill!(nb_spokes, 0)
253+
254+
rvS = rowvals(S)
255+
for j in axes(S, 2)
256+
for k in nzrange(S, j)
257+
i = rvS[k]
258+
if i > j
259+
index_ij = edge_to_index[k]
260+
s = star[index_ij]
261+
h = abs(hub[s])
262+
nb_spokes[s] += 1
263+
index = nb_spokes[s]
264+
265+
# Assign the non-hub vertex (spoke) to the correct position in spokes
266+
if i == h
267+
# i is the hub and j is the spoke
268+
spokes[s][index] = j
269+
else # j == h
270+
# j is the hub and i is the spoke
271+
spokes[s][index] = i
272+
end
273+
end
274+
end
271275
end
276+
return StarSet(star, hub, spokes)
272277
end
273278

279+
_sort(u, v) = (min(u, v), max(u, v))
280+
274281
"""
275282
acyclic_coloring(g::AdjacencyGraph, order::AbstractOrder; postprocessing::Bool)
276283

src/graph.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ function neighbors(g::AdjacencyGraph{T,false}, v::Integer) where {T}
221221
return neighbors_v
222222
end
223223

224+
function neighbors2(g::AdjacencyGraph, v::Integer)
225+
S = pattern(g)
226+
neighbors_v = view(rowvals(S), nzrange(S, v))
227+
return neighbors_v
228+
end
229+
224230
function degree(g::AdjacencyGraph, v::Integer)
225231
d = 0
226232
for u in neighbors(g, v)

src/interface.jl

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,11 @@ function _coloring(
266266
symmetric_pattern::Bool,
267267
)
268268
ag = AdjacencyGraph(A)
269-
color, star_set = star_coloring(ag, algo.order; postprocessing=algo.postprocessing)
269+
color, star_set, edge_to_index = star_coloring(
270+
ag, algo.order; postprocessing=algo.postprocessing
271+
)
270272
if speed_setting isa WithResult
271-
return StarSetColoringResult(A, ag, color, star_set)
273+
return StarSetColoringResult(A, ag, color, edge_to_index, star_set)
272274
else
273275
return color
274276
end
@@ -301,9 +303,13 @@ function _coloring(
301303
) where {R}
302304
A_and_Aᵀ = bidirectional_pattern(A; symmetric_pattern)
303305
ag = AdjacencyGraph(A_and_Aᵀ; has_diagonal=false)
304-
color, star_set = star_coloring(ag, algo.order; postprocessing=algo.postprocessing)
306+
color, star_set, edge_to_index = star_coloring(
307+
ag, algo.order; postprocessing=algo.postprocessing
308+
)
305309
if speed_setting isa WithResult
306-
symmetric_result = StarSetColoringResult(A_and_Aᵀ, ag, color, star_set)
310+
symmetric_result = StarSetColoringResult(
311+
A_and_Aᵀ, ag, color, edge_to_index, star_set
312+
)
307313
return BicoloringResult(A, ag, symmetric_result, R)
308314
else
309315
row_color, column_color, _ = remap_colors(color, maximum(color), size(A)...)

src/result.jl

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,21 +239,47 @@ struct StarSetColoringResult{M<:AbstractMatrix,G<:AdjacencyGraph,V} <:
239239
end
240240

241241
function StarSetColoringResult(
242-
A::AbstractMatrix, ag::AdjacencyGraph, color::Vector{Int}, star_set::StarSet
242+
A::AbstractMatrix,
243+
ag::AdjacencyGraph,
244+
color::Vector{Int},
245+
edge_to_index::Vector{Int},
246+
star_set::StarSet,
243247
)
244-
S = ag.S
248+
# Reuse edge_to_index to store the compressed indices for decompression
249+
compressed_indices = edge_to_index
250+
251+
(; star, hub) = star_set
252+
S = pattern(ag)
253+
n = S.n
245254
group = group_by_color(color)
246-
n = size(S, 1)
247-
rv = rowvals(S)
248-
compressed_indices = zeros(Int, nnz(S))
255+
rvS = rowvals(S)
249256
for j in axes(S, 2)
250257
for k in nzrange(S, j)
251-
i = rv[k]
252-
l, c = symmetric_coefficient(i, j, color, star_set)
253-
# A[i, j] = B[l, c]
254-
compressed_indices[k] = (c - 1) * n + l
258+
i = rvS[k]
259+
if i == j
260+
# diagonal coefficients
261+
c = color[i]
262+
compressed_indices[k] = (c - 1) * n + i
263+
else
264+
# off-diagonal coefficients
265+
index_ij = edge_to_index[k]
266+
s = star[index_ij]
267+
h = abs(hub[s])
268+
269+
# Assign the non-hub vertex (spoke) to the correct position in spokes
270+
if i == h
271+
# i is the hub and j is the spoke
272+
c = color[i]
273+
compressed_indices[k] = (c - 1) * n + j
274+
else # j == h
275+
# j is the hub and i is the spoke
276+
c = color[j]
277+
compressed_indices[k] = (c - 1) * n + i
278+
end
279+
end
255280
end
256281
end
282+
257283
return StarSetColoringResult(A, ag, color, group, star_set, compressed_indices)
258284
end
259285

0 commit comments

Comments
 (0)