Skip to content

Commit b3cf23d

Browse files
authored
Allow picking the best of several orders for GreedyColoringAlgorithm (#265)
* Allow picking the best of several orders for GreedyColoringAlgorithm * Fix type params * Tuple * Better tests * Better test * Fix foc * Fix type inference inside closure * Fix seed * Test on 1.11 * Avoid duplicate remap_colors
1 parent 7efc6bc commit b3cf23d

File tree

8 files changed

+246
-42
lines changed

8 files changed

+246
-42
lines changed

.github/workflows/Test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
runs-on: ubuntu-latest
2121
strategy:
2222
matrix:
23-
julia-version: ['1.10', '1']
23+
julia-version: ['1.10', '1.11']
2424

2525
steps:
2626
- uses: actions/checkout@v5
@@ -40,4 +40,4 @@ jobs:
4040
with:
4141
files: lcov.info
4242
token: ${{ secrets.CODECOV_TOKEN }}
43-
fail_ci_if_error: false
43+
fail_ci_if_error: false

docs/src/tutorial.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ problem = ColoringProblem()
3131

3232
The algorithm defines how you want to solve it. It can be either a [`GreedyColoringAlgorithm`](@ref) or a [`ConstantColoringAlgorithm`](@ref). For `GreedyColoringAlgorithm`, you can select options such as
3333

34-
- the order in which vertices are processed (a subtype of [`AbstractOrder`](@ref SparseMatrixColorings.AbstractOrder))
34+
- the order in which vertices are processed (a subtype of [`AbstractOrder`](@ref SparseMatrixColorings.AbstractOrder) , or a tuple of such objects)
3535
- the type of decompression you want (`:direct` or `:substitution`)
3636

3737
```@example tutorial

src/interface.jl

Lines changed: 93 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ It is passed as an argument to the main function [`coloring`](@ref).
7272
GreedyColoringAlgorithm{decompression}(order=NaturalOrder(); postprocessing=false)
7373
GreedyColoringAlgorithm(order=NaturalOrder(); postprocessing=false, decompression=:direct)
7474
75-
- `order::AbstractOrder`: the order in which the columns or rows are colored, which can impact the number of colors.
75+
- `order::Union{AbstractOrder,Tuple}`: the order in which the columns or rows are colored, which can impact the number of colors. Can also be a tuple of different orders to try out, from which the best order (the one with the lowest total number of colors) will be used.
7676
- `postprocessing::Bool`: whether or not the coloring will be refined by assigning the neutral color `0` to some vertices.
7777
- `decompression::Symbol`: either `:direct` or `:substitution`. Usually `:substitution` leads to fewer colors, at the cost of a more expensive coloring (and decompression). When `:substitution` is not applicable, it falls back on `:direct` decompression.
7878
@@ -94,26 +94,31 @@ See their respective docstrings for details.
9494
- [`AbstractOrder`](@ref)
9595
- [`decompress`](@ref)
9696
"""
97-
struct GreedyColoringAlgorithm{decompression,O<:AbstractOrder} <:
97+
struct GreedyColoringAlgorithm{decompression,N,O<:NTuple{N,AbstractOrder}} <:
9898
ADTypes.AbstractColoringAlgorithm
99-
order::O
99+
orders::O
100100
postprocessing::Bool
101-
end
102101

103-
function GreedyColoringAlgorithm{decompression}(
104-
order::AbstractOrder=NaturalOrder(); postprocessing::Bool=false
105-
) where {decompression}
106-
check_valid_algorithm(decompression)
107-
return GreedyColoringAlgorithm{decompression,typeof(order)}(order, postprocessing)
102+
function GreedyColoringAlgorithm{decompression}(
103+
order_or_orders::Union{AbstractOrder,Tuple}=NaturalOrder();
104+
postprocessing::Bool=false,
105+
) where {decompression}
106+
check_valid_algorithm(decompression)
107+
if order_or_orders isa AbstractOrder
108+
orders = (order_or_orders,)
109+
else
110+
orders = order_or_orders
111+
end
112+
return new{decompression,length(orders),typeof(orders)}(orders, postprocessing)
113+
end
108114
end
109115

110116
function GreedyColoringAlgorithm(
111-
order::AbstractOrder=NaturalOrder();
117+
order_or_orders::Union{AbstractOrder,Tuple}=NaturalOrder();
112118
postprocessing::Bool=false,
113119
decompression::Symbol=:direct,
114120
)
115-
check_valid_algorithm(decompression)
116-
return GreedyColoringAlgorithm{decompression,typeof(order)}(order, postprocessing)
121+
return GreedyColoringAlgorithm{decompression}(order_or_orders; postprocessing)
117122
end
118123

119124
## Coloring
@@ -229,8 +234,11 @@ function _coloring(
229234
)
230235
symmetric_pattern = symmetric_pattern || A isa Union{Symmetric,Hermitian}
231236
bg = BipartiteGraph(A; symmetric_pattern)
232-
vertices_in_order = vertices(bg, Val(2), algo.order)
233-
color = partial_distance2_coloring(bg, Val(2), vertices_in_order)
237+
color_by_order = map(algo.orders) do order
238+
vertices_in_order = vertices(bg, Val(2), order)
239+
return partial_distance2_coloring(bg, Val(2), vertices_in_order)
240+
end
241+
color = argmin(maximum, color_by_order)
234242
if speed_setting isa WithResult
235243
return ColumnColoringResult(A, bg, color)
236244
else
@@ -248,8 +256,11 @@ function _coloring(
248256
)
249257
symmetric_pattern = symmetric_pattern || A isa Union{Symmetric,Hermitian}
250258
bg = BipartiteGraph(A; symmetric_pattern)
251-
vertices_in_order = vertices(bg, Val(1), algo.order)
252-
color = partial_distance2_coloring(bg, Val(1), vertices_in_order)
259+
color_by_order = map(algo.orders) do order
260+
vertices_in_order = vertices(bg, Val(1), order)
261+
return partial_distance2_coloring(bg, Val(1), vertices_in_order)
262+
end
263+
color = argmin(maximum, color_by_order)
253264
if speed_setting isa WithResult
254265
return RowColoringResult(A, bg, color)
255266
else
@@ -266,8 +277,11 @@ function _coloring(
266277
symmetric_pattern::Bool,
267278
)
268279
ag = AdjacencyGraph(A; has_diagonal=true)
269-
vertices_in_order = vertices(ag, algo.order)
270-
color, star_set = star_coloring(ag, vertices_in_order, algo.postprocessing)
280+
color_and_star_set_by_order = map(algo.orders) do order
281+
vertices_in_order = vertices(ag, order)
282+
return star_coloring(ag, vertices_in_order, algo.postprocessing)
283+
end
284+
color, star_set = argmin(maximum first, color_and_star_set_by_order)
271285
if speed_setting isa WithResult
272286
return StarSetColoringResult(A, ag, color, star_set)
273287
else
@@ -284,8 +298,11 @@ function _coloring(
284298
symmetric_pattern::Bool,
285299
) where {R}
286300
ag = AdjacencyGraph(A; has_diagonal=true)
287-
vertices_in_order = vertices(ag, algo.order)
288-
color, tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
301+
color_and_tree_set_by_order = map(algo.orders) do order
302+
vertices_in_order = vertices(ag, order)
303+
return acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
304+
end
305+
color, tree_set = argmin(maximum first, color_and_tree_set_by_order)
289306
if speed_setting isa WithResult
290307
return TreeSetColoringResult(A, ag, color, tree_set, R)
291308
else
@@ -303,15 +320,37 @@ function _coloring(
303320
) where {R}
304321
A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern)
305322
ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; has_diagonal=false)
306-
vertices_in_order = vertices(ag, algo.order)
307-
color, star_set = star_coloring(ag, vertices_in_order, algo.postprocessing)
323+
outputs_by_order = map(algo.orders) do order
324+
vertices_in_order = vertices(ag, order)
325+
_color, _star_set = star_coloring(ag, vertices_in_order, algo.postprocessing)
326+
(_row_color, _column_color, _symmetric_to_row, _symmetric_to_column) = remap_colors(
327+
eltype(ag), _color, maximum(_color), size(A)...
328+
)
329+
return (
330+
_color,
331+
_star_set,
332+
_row_color,
333+
_column_color,
334+
_symmetric_to_row,
335+
_symmetric_to_column,
336+
)
337+
end
338+
(color, star_set, row_color, column_color, symmetric_to_row, symmetric_to_column) = argmin(
339+
t -> maximum(t[3]) + maximum(t[4]), outputs_by_order
340+
) # can't use ncolors without computing the full result
308341
if speed_setting isa WithResult
309342
symmetric_result = StarSetColoringResult(A_and_Aᵀ, ag, color, star_set)
310-
return BicoloringResult(A, ag, symmetric_result, R)
311-
else
312-
row_color, column_color, _ = remap_colors(
313-
eltype(ag), color, maximum(color), size(A)...
343+
return BicoloringResult(
344+
A,
345+
ag,
346+
symmetric_result,
347+
row_color,
348+
column_color,
349+
symmetric_to_row,
350+
symmetric_to_column,
351+
R,
314352
)
353+
else
315354
return row_color, column_color
316355
end
317356
end
@@ -326,15 +365,37 @@ function _coloring(
326365
) where {R}
327366
A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern)
328367
ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; has_diagonal=false)
329-
vertices_in_order = vertices(ag, algo.order)
330-
color, tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
368+
outputs_by_order = map(algo.orders) do order
369+
vertices_in_order = vertices(ag, order)
370+
_color, _tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
371+
(_row_color, _column_color, _symmetric_to_row, _symmetric_to_column) = remap_colors(
372+
eltype(ag), _color, maximum(_color), size(A)...
373+
)
374+
return (
375+
_color,
376+
_tree_set,
377+
_row_color,
378+
_column_color,
379+
_symmetric_to_row,
380+
_symmetric_to_column,
381+
)
382+
end
383+
(color, tree_set, row_color, column_color, symmetric_to_row, symmetric_to_column) = argmin(
384+
t -> maximum(t[3]) + maximum(t[4]), outputs_by_order
385+
) # can't use ncolors without computing the full result
331386
if speed_setting isa WithResult
332387
symmetric_result = TreeSetColoringResult(A_and_Aᵀ, ag, color, tree_set, R)
333-
return BicoloringResult(A, ag, symmetric_result, R)
334-
else
335-
row_color, column_color, _ = remap_colors(
336-
eltype(ag), color, maximum(color), size(A)...
388+
return BicoloringResult(
389+
A,
390+
ag,
391+
symmetric_result,
392+
row_color,
393+
column_color,
394+
symmetric_to_row,
395+
symmetric_to_column,
396+
R,
337397
)
398+
else
338399
return row_color, column_color
339400
end
340401
end

src/result.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,14 +686,15 @@ function BicoloringResult(
686686
A::AbstractMatrix,
687687
ag::AdjacencyGraph{T},
688688
symmetric_result::AbstractColoringResult{:symmetric,:column},
689+
row_color::Vector{T},
690+
column_color::Vector{T},
691+
symmetric_to_row::Vector{T},
692+
symmetric_to_column::Vector{T},
689693
decompression_eltype::Type{R},
690694
) where {T,R}
691695
m, n = size(A)
692696
symmetric_color = column_colors(symmetric_result)
693697
num_sym_colors = maximum(symmetric_color)
694-
row_color, column_color, symmetric_to_row, symmetric_to_column = remap_colors(
695-
T, symmetric_color, num_sym_colors, m, n
696-
)
697698
column_group = group_by_color(T, column_color)
698699
row_group = group_by_color(T, row_color)
699700
Br_and_Bc = Matrix{R}(undef, n + m, num_sym_colors)

test/order.jl

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,99 @@ end;
146146
@test isperm(π)
147147
end
148148
end
149+
150+
@testset "Multiple orders" begin
151+
# I used brute force to find examples where LargestFirst is *strictly* better than NaturalOrder, just to check that the best order is indeed selected when multiple orders are provided
152+
@testset "Column coloring" begin
153+
A = [
154+
0 0 1 1
155+
0 1 0 1
156+
0 0 1 1
157+
1 1 0 0
158+
]
159+
problem = ColoringProblem{:nonsymmetric,:column}()
160+
algo = GreedyColoringAlgorithm(NaturalOrder())
161+
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
162+
@test ncolors(coloring(A, problem, better_algo)) <
163+
ncolors(coloring(A, problem, algo))
164+
end
165+
@testset "Row coloring" begin
166+
A = [
167+
1 0 0 0
168+
0 0 1 0
169+
0 1 1 1
170+
1 0 0 1
171+
]
172+
problem = ColoringProblem{:nonsymmetric,:row}()
173+
algo = GreedyColoringAlgorithm(NaturalOrder())
174+
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
175+
@test ncolors(coloring(A, problem, better_algo)) <
176+
ncolors(coloring(A, problem, algo))
177+
end
178+
@testset "Star coloring" begin
179+
A = [
180+
0 1 0 1 1
181+
1 1 0 1 0
182+
0 0 1 0 1
183+
1 1 0 1 0
184+
1 0 1 0 0
185+
]
186+
problem = ColoringProblem{:symmetric,:column}()
187+
algo = GreedyColoringAlgorithm(NaturalOrder())
188+
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
189+
@test ncolors(coloring(A, problem, better_algo)) <
190+
ncolors(coloring(A, problem, algo))
191+
end
192+
@testset "Acyclic coloring" begin
193+
A = [
194+
1 0 0 0 0 1 0
195+
0 0 0 1 0 0 0
196+
0 0 0 1 0 0 0
197+
0 1 1 1 0 1 1
198+
0 0 0 0 0 0 1
199+
1 0 0 1 0 0 1
200+
0 0 0 1 1 1 1
201+
]
202+
problem = ColoringProblem{:symmetric,:column}()
203+
algo = GreedyColoringAlgorithm{:substitution}(NaturalOrder())
204+
better_algo = GreedyColoringAlgorithm{:substitution}((
205+
NaturalOrder(), LargestFirst()
206+
))
207+
@test ncolors(coloring(A, problem, better_algo)) <
208+
ncolors(coloring(A, problem, algo))
209+
end
210+
@testset "Star bicoloring" begin
211+
A = [
212+
0 1 0 0 0
213+
1 0 1 0 0
214+
0 1 0 0 1
215+
0 0 0 0 0
216+
0 0 1 0 1
217+
]
218+
problem = ColoringProblem{:nonsymmetric,:bidirectional}()
219+
algo = GreedyColoringAlgorithm(NaturalOrder())
220+
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
221+
@test ncolors(coloring(A, problem, better_algo)) <
222+
ncolors(coloring(A, problem, algo))
223+
end
224+
@testset "Acyclic bicoloring" begin
225+
A = [
226+
0 1 0 1 1 0 1 0 1
227+
1 0 0 0 0 0 0 0 1
228+
0 0 0 0 0 0 0 0 0
229+
1 0 0 1 1 0 1 0 0
230+
1 0 0 1 0 0 0 0 0
231+
0 0 0 0 0 0 0 0 0
232+
1 0 0 1 0 0 0 0 0
233+
0 0 0 0 0 0 0 0 0
234+
1 1 0 0 0 0 0 0 0
235+
]
236+
problem = ColoringProblem{:nonsymmetric,:bidirectional}()
237+
algo = GreedyColoringAlgorithm{:substitution}(NaturalOrder())
238+
better_algo = GreedyColoringAlgorithm{:substitution}((
239+
NaturalOrder(), LargestFirst()
240+
))
241+
@test ncolors(coloring(A, problem, better_algo)) <
242+
ncolors(coloring(A, problem, algo))
243+
end
244+
end

test/random.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ end;
8181
problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional)
8282
@testset for algo in (
8383
GreedyColoringAlgorithm(
84-
RandomOrder(rng); postprocessing=false, decompression=:direct
84+
RandomOrder(StableRNG(0), 0); postprocessing=false, decompression=:direct
8585
),
8686
GreedyColoringAlgorithm(
87-
RandomOrder(rng); postprocessing=true, decompression=:direct
87+
RandomOrder(StableRNG(0), 0); postprocessing=true, decompression=:direct
8888
),
8989
)
9090
@testset "$((; m, n, p))" for (m, n, p) in asymmetric_params
@@ -102,10 +102,10 @@ end;
102102
problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional)
103103
@testset for algo in (
104104
GreedyColoringAlgorithm(
105-
RandomOrder(rng); postprocessing=false, decompression=:substitution
105+
RandomOrder(StableRNG(0), 0); postprocessing=false, decompression=:substitution
106106
),
107107
GreedyColoringAlgorithm(
108-
RandomOrder(rng); postprocessing=true, decompression=:substitution
108+
RandomOrder(StableRNG(0), 0); postprocessing=true, decompression=:substitution
109109
),
110110
)
111111
@testset "$((; m, n, p))" for (m, n, p) in asymmetric_params

test/type_stability.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@ rng = StableRNG(63)
4040
ColoringProblem(; structure, partition),
4141
GreedyColoringAlgorithm(order; decompression),
4242
)
43+
@test_opt coloring(
44+
A,
45+
ColoringProblem(; structure, partition),
46+
GreedyColoringAlgorithm((NaturalOrder(), order); decompression),
47+
)
4348
@inferred coloring(
4449
A,
4550
ColoringProblem(; structure, partition),
4651
GreedyColoringAlgorithm(order; decompression),
4752
)
53+
@inferred coloring(
54+
A,
55+
ColoringProblem(; structure, partition),
56+
GreedyColoringAlgorithm((NaturalOrder(), order); decompression),
57+
)
4858
end
4959
end
5060
end;

0 commit comments

Comments
 (0)