Skip to content

Commit 42622b5

Browse files
authored
Degree-based orderings, take 2 (#125)
* Degree-based orderings * Fix docs * Clearly separate `AdjacencyGraph` and `BipartiteGraph` from the underlying sparsity pattern * Typos * Fix * Fixes * Implement for BipartiteGraph * No abstract * Fix * Codecov * Fix compat * Tested dynamic orders * Fixes * Add experimental warning
1 parent 74e4497 commit 42622b5

7 files changed

Lines changed: 346 additions & 8 deletions

File tree

docs/src/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@ AbstractOrder
4747
NaturalOrder
4848
RandomOrder
4949
LargestFirst
50+
SmallestLast
51+
IncidenceDegree
52+
DynamicLargestFirst
53+
DynamicDegreeBasedOrder
5054
```

docs/src/dev.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ SparseMatrixColorings.LinearSystemColoringResult
4646
SparseMatrixColorings.directly_recoverable_columns
4747
SparseMatrixColorings.symmetrically_orthogonal_columns
4848
SparseMatrixColorings.structurally_orthogonal_columns
49+
SparseMatrixColorings.valid_dynamic_order
4950
```
5051

5152
## Matrix handling

src/SparseMatrixColorings.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ include("examples.jl")
5656
include("show_colors.jl")
5757

5858
export NaturalOrder, RandomOrder, LargestFirst
59+
export DynamicDegreeBasedOrder, SmallestLast, IncidenceDegree, DynamicLargestFirst
5960
export ColoringProblem, GreedyColoringAlgorithm, AbstractColoringResult
6061
export ConstantColoringAlgorithm
6162
export coloring

src/check.jl

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,67 @@ function directly_recoverable_columns(
138138
end
139139
return true
140140
end
141+
142+
"""
143+
valid_dynamic_order(g::AdjacencyGraph, π::AbstractVector{Int}, order::DynamicDegreeBasedOrder)
144+
valid_dynamic_order(bg::AdjacencyGraph, ::Val{side}, π::AbstractVector{Int}, order::DynamicDegreeBasedOrder)
145+
146+
Check that a permutation `π` corresponds to a valid application of a [`DynamicDegreeBasedOrder`](@ref).
147+
148+
This is done by checking, for each ordered vertex, that its back- or forward-degree was the smallest or largest among the remaining vertices (the specifics depend on the order parameters).
149+
150+
!!! warning
151+
This function is not coded with efficiency in mind, it is designed for small-scale tests.
152+
"""
153+
function valid_dynamic_order(
154+
g::AdjacencyGraph, π::AbstractVector{Int}, ::DynamicDegreeBasedOrder{degtype,direction}
155+
) where {degtype,direction}
156+
length(π) != nb_vertices(g) && return false
157+
length(unique(π)) != nb_vertices(g) && return false
158+
for i in eachindex(π)
159+
vi = π[i]
160+
yet_to_be_ordered = direction == :low2high ? π[i:end] : π[begin:i]
161+
considered_for_degree = degtype == :back ? π[begin:(i - 1)] : π[(i + 1):end]
162+
di = degree_in_subset(g, vi, considered_for_degree)
163+
considered_for_degree_switched = copy(considered_for_degree)
164+
for vj in yet_to_be_ordered
165+
replace!(considered_for_degree_switched, vj => vi)
166+
dj = degree_in_subset(g, vj, considered_for_degree_switched)
167+
replace!(considered_for_degree_switched, vi => vj)
168+
if direction == :low2high
169+
dj > di && return false
170+
else
171+
dj < di && return false
172+
end
173+
end
174+
end
175+
return true
176+
end
177+
178+
function valid_dynamic_order(
179+
g::BipartiteGraph,
180+
::Val{side},
181+
π::AbstractVector{Int},
182+
::DynamicDegreeBasedOrder{degtype,direction},
183+
) where {side,degtype,direction}
184+
length(π) != nb_vertices(g, Val(side)) && return false
185+
length(unique(π)) != nb_vertices(g, Val(side)) && return false
186+
for i in eachindex(π)
187+
vi = π[i]
188+
yet_to_be_ordered = direction == :low2high ? π[i:end] : π[begin:i]
189+
considered_for_degree = degtype == :back ? π[begin:(i - 1)] : π[(i + 1):end]
190+
di = degree_dist2_in_subset(g, Val(side), vi, considered_for_degree)
191+
considered_for_degree_switched = copy(considered_for_degree)
192+
for vj in yet_to_be_ordered
193+
replace!(considered_for_degree_switched, vj => vi)
194+
dj = degree_dist2_in_subset(g, Val(side), vj, considered_for_degree_switched)
195+
replace!(considered_for_degree_switched, vi => vj)
196+
if direction == :low2high
197+
dj > di && return false
198+
else
199+
dj < di && return false
200+
end
201+
end
202+
end
203+
return true
204+
end

src/graph.jl

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,25 @@ end
161161
maximum_degree(g::AdjacencyGraph) = maximum(Base.Fix1(degree, g), vertices(g))
162162
minimum_degree(g::AdjacencyGraph) = minimum(Base.Fix1(degree, g), vertices(g))
163163

164+
function has_neighbor(g::AdjacencyGraph, v::Integer, u::Integer)
165+
for w in neighbors(g, v)
166+
if w == u
167+
return true
168+
end
169+
end
170+
return false
171+
end
172+
173+
function degree_in_subset(g::AdjacencyGraph, v::Integer, subset::AbstractVector{Int})
174+
d = 0
175+
for u in subset
176+
if has_neighbor(g, v, u)
177+
d += 1
178+
end
179+
end
180+
return d
181+
end
182+
164183
## Bipartite graph
165184

166185
"""
@@ -235,6 +254,18 @@ function neighbors(bg::BipartiteGraph, ::Val{side}, v::Integer) where {side}
235254
return view(rowvals(S), nzrange(S, v))
236255
end
237256

257+
function neighbors_dist2(bg::BipartiteGraph{T}, ::Val{side}, v::Integer) where {T,side}
258+
# TODO: make more efficient
259+
other_side = 3 - side
260+
neigh = Set{T}()
261+
for u in neighbors(bg, Val(side), v)
262+
for w in neighbors(bg, Val(other_side), u)
263+
w != v && push!(neigh, w)
264+
end
265+
end
266+
return neigh
267+
end
268+
238269
function degree(bg::BipartiteGraph, ::Val{side}, v::Integer) where {side}
239270
return length(neighbors(bg, Val(side), v))
240271
end
@@ -248,13 +279,31 @@ function minimum_degree(bg::BipartiteGraph, ::Val{side}) where {side}
248279
end
249280

250281
function degree_dist2(bg::BipartiteGraph{T}, ::Val{side}, v::Integer) where {T,side}
251-
# not efficient, for testing purposes only
282+
return length(neighbors_dist2(bg, Val(side), v))
283+
end
284+
285+
function has_neighbor_dist2(
286+
bg::BipartiteGraph, ::Val{side}, v::Integer, u::Integer
287+
) where {side}
252288
other_side = 3 - side
253-
neighbors_dist2 = Set{T}()
254-
for u in neighbors(bg, Val(side), v)
255-
for w in neighbors(bg, Val(other_side), u)
256-
w != v && push!(neighbors_dist2, w)
289+
for w1 in neighbors(bg, Val(side), v)
290+
for w2 in neighbors(bg, Val(other_side), w1)
291+
if w2 == u
292+
return true
293+
end
257294
end
258295
end
259-
return length(neighbors_dist2)
296+
return false
297+
end
298+
299+
function degree_dist2_in_subset(
300+
bg::BipartiteGraph, ::Val{side}, v::Integer, subset::AbstractVector{Int}
301+
) where {side}
302+
d = 0
303+
for u in subset
304+
if has_neighbor_dist2(bg, Val(side), v, u)
305+
d += 1
306+
end
307+
end
308+
return d
260309
end

src/order.jl

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ Abstract supertype for the vertex order used inside [`GreedyColoringAlgorithm`](
66
In this algorithm, the rows and columns of a matrix form a graph, and the vertices are colored one after the other in a greedy fashion.
77
Depending on how the vertices are ordered, the number of colors necessary may vary.
88
9-
# Subtypes
9+
# Options
1010
1111
- [`NaturalOrder`](@ref)
1212
- [`RandomOrder`](@ref)
1313
- [`LargestFirst`](@ref)
14+
- [`IncidenceDegree`](@ref) (experimental)
15+
- [`SmallestLast`](@ref) (experimental)
16+
- [`DynamicLargestFirst`](@ref) (experimental)
1417
"""
1518
abstract type AbstractOrder end
1619

@@ -79,3 +82,189 @@ function vertices(bg::BipartiteGraph, ::Val{side}, ::LargestFirst) where {side}
7982
criterion(v) = degrees_dist2[v]
8083
return sort(vertices(bg, Val(side)); by=criterion, rev=true)
8184
end
85+
86+
"""
87+
DynamicDegreeBasedOrder{degtype,direction}
88+
89+
Instance of [`AbstractOrder`](@ref) which sorts vertices using a dynamically computed degree.
90+
91+
!!! danger
92+
This order is still experimental and needs more tests, correctness is not yet guaranteed.
93+
94+
# Type parameters
95+
96+
- `degtype::Symbol`: can be `:forward` (for the forward degree) or `:back` (for the back degree)
97+
- `direction::Symbol`: can be `:low2high` (if the order is defined from lowest to highest, i.e. `1` to `n`) or `:high2low` (if the order is defined from highest to lowest, i.e. `n` to `1`)
98+
99+
# Concrete variants
100+
101+
- [`IncidenceDegree`](@ref)
102+
- [`SmallestLast`](@ref)
103+
- [`DynamicLargestFirst`](@ref)
104+
105+
# References
106+
107+
- [_ColPack: Software for graph coloring and related problems in scientific computing_](https://dl.acm.org/doi/10.1145/2513109.2513110), Gebremedhin et al. (2013), Section 5
108+
"""
109+
struct DynamicDegreeBasedOrder{degtype,direction} <: AbstractOrder end
110+
111+
struct DegreeBuckets{B}
112+
degrees::Vector{Int}
113+
buckets::B
114+
positions::Vector{Int}
115+
end
116+
117+
function DegreeBuckets(degrees::Vector{Int}, dmax)
118+
buckets = Dict(d => Int[] for d in 0:dmax)
119+
positions = similar(degrees, Int)
120+
for v in eachindex(degrees, positions)
121+
d = degrees[v]
122+
push!(buckets[d], v) # TODO: optimize
123+
positions[v] = length(buckets[d])
124+
end
125+
return DegreeBuckets(degrees, buckets, positions)
126+
end
127+
128+
function degree_increasing(; degtype, direction)
129+
increasing =
130+
(degtype == :back && direction == :low2high) ||
131+
(degtype == :forward && direction == :high2low)
132+
return increasing
133+
end
134+
135+
function mark_ordered!(db::DegreeBuckets, v::Integer)
136+
db.degrees[v] = -1
137+
db.positions[v] = -1
138+
return nothing
139+
end
140+
141+
already_ordered(db::DegreeBuckets, v::Integer) = db.degrees[v] == -1
142+
143+
function pop_next_candidate!(db::DegreeBuckets; direction::Symbol)
144+
(; buckets) = db
145+
if direction == :low2high
146+
candidate_degree = maximum(d for (d, bucket) in pairs(buckets) if !isempty(bucket))
147+
else
148+
candidate_degree = minimum(d for (d, bucket) in pairs(buckets) if !isempty(bucket))
149+
end
150+
candidate_bucket = buckets[candidate_degree]
151+
candidate = pop!(candidate_bucket)
152+
mark_ordered!(db, candidate)
153+
return candidate
154+
end
155+
156+
function update_bucket!(db::DegreeBuckets, v::Integer; degtype, direction)
157+
(; degrees, buckets, positions) = db
158+
d, p = degrees[v], positions[v]
159+
bucket = buckets[d]
160+
# select previous or next bucket for the move
161+
d_new = degree_increasing(; degtype, direction) ? d + 1 : d - 1
162+
bucket_new = buckets[d_new]
163+
# put v at the end of its bucket by swapping
164+
w = bucket[end]
165+
bucket[p] = w
166+
positions[w] = p
167+
bucket[end] = v
168+
positions[v] = length(bucket)
169+
# move v from the old bucket to the new one
170+
@assert pop!(bucket) == v
171+
push!(bucket_new, v)
172+
degrees[v] = d_new
173+
positions[v] = length(bucket_new)
174+
return nothing
175+
end
176+
177+
function vertices(
178+
g::AdjacencyGraph, ::DynamicDegreeBasedOrder{degtype,direction}
179+
) where {degtype,direction}
180+
if degree_increasing(; degtype, direction)
181+
degrees = zeros(Int, nb_vertices(g))
182+
else
183+
degrees = [degree(g, v) for v in vertices(g)]
184+
end
185+
db = DegreeBuckets(degrees, maximum_degree(g))
186+
π = Int[]
187+
for _ in 1:nb_vertices(g)
188+
u = pop_next_candidate!(db; direction)
189+
direction == :low2high ? push!(π, u) : pushfirst!(π, u)
190+
for v in neighbors(g, u)
191+
already_ordered(db, v) && continue
192+
update_bucket!(db, v; degtype, direction)
193+
end
194+
end
195+
return π
196+
end
197+
198+
function vertices(
199+
g::BipartiteGraph, ::Val{side}, ::DynamicDegreeBasedOrder{degtype,direction}
200+
) where {side,degtype,direction}
201+
other_side = 3 - side
202+
if degree_increasing(; degtype, direction)
203+
degrees = zeros(Int, nb_vertices(g, Val(side)))
204+
else
205+
degrees = [degree_dist2(g, Val(side), v) for v in vertices(g, Val(side))] # TODO: optimize
206+
end
207+
maxd2 = maximum(v -> degree_dist2(g, Val(side), v), vertices(g, Val(side))) # TODO: optimize
208+
db = DegreeBuckets(degrees, maxd2)
209+
π = Int[]
210+
visited = falses(nb_vertices(g, Val(side)))
211+
for _ in 1:nb_vertices(g, Val(side))
212+
u = pop_next_candidate!(db; direction)
213+
direction == :low2high ? push!(π, u) : pushfirst!(π, u)
214+
for w in neighbors(g, Val(side), u)
215+
for v in neighbors(g, Val(other_side), w)
216+
if v == u || visited[v]
217+
continue
218+
else
219+
visited[v] = true
220+
end
221+
already_ordered(db, v) && continue
222+
update_bucket!(db, v; degtype, direction)
223+
end
224+
end
225+
fill!(visited, false)
226+
end
227+
return π
228+
end
229+
230+
"""
231+
IncidenceDegree()
232+
233+
Instance of [`AbstractOrder`](@ref) which sorts vertices from lowest to highest using the dynamic back degree.
234+
235+
!!! danger
236+
This order is still experimental and needs more tests, correctness is not yet guaranteed.
237+
238+
# See also
239+
240+
- [`DynamicDegreeBasedOrder`](@ref)
241+
"""
242+
const IncidenceDegree = DynamicDegreeBasedOrder{:back,:low2high}
243+
244+
"""
245+
SmallestLast()
246+
247+
Instance of [`AbstractOrder`](@ref) which sorts vertices from highest to lowest using the dynamic back degree.
248+
249+
!!! danger
250+
This order is still experimental and needs more tests, correctness is not yet guaranteed.
251+
252+
# See also
253+
254+
- [`DynamicDegreeBasedOrder`](@ref)
255+
"""
256+
const SmallestLast = DynamicDegreeBasedOrder{:back,:high2low}
257+
258+
"""
259+
DynamicLargestFirst()
260+
261+
Instance of [`AbstractOrder`](@ref) which sorts vertices from lowest to highest using the dynamic forward degree.
262+
263+
!!! danger
264+
This order is still experimental and needs more tests, correctness is not yet guaranteed.
265+
266+
# See also
267+
268+
- [`DynamicDegreeBasedOrder`](@ref)
269+
"""
270+
const DynamicLargestFirst = DynamicDegreeBasedOrder{:forward,:low2high}

0 commit comments

Comments
 (0)