Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
julia-version: ['1.10', '1']
julia-version: ['1.10', '1.11']

steps:
- uses: actions/checkout@v5
Expand All @@ -40,4 +40,4 @@ jobs:
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
fail_ci_if_error: false
2 changes: 1 addition & 1 deletion docs/src/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ problem = ColoringProblem()

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

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

```@example tutorial
Expand Down
89 changes: 59 additions & 30 deletions src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ It is passed as an argument to the main function [`coloring`](@ref).
GreedyColoringAlgorithm{decompression}(order=NaturalOrder(); postprocessing=false)
GreedyColoringAlgorithm(order=NaturalOrder(); postprocessing=false, decompression=:direct)

- `order::AbstractOrder`: the order in which the columns or rows are colored, which can impact the number of colors.
- `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.
- `postprocessing::Bool`: whether or not the coloring will be refined by assigning the neutral color `0` to some vertices.
- `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.

Expand All @@ -94,26 +94,31 @@ See their respective docstrings for details.
- [`AbstractOrder`](@ref)
- [`decompress`](@ref)
"""
struct GreedyColoringAlgorithm{decompression,O<:AbstractOrder} <:
struct GreedyColoringAlgorithm{decompression,N,O<:NTuple{N,AbstractOrder}} <:
ADTypes.AbstractColoringAlgorithm
order::O
orders::O
postprocessing::Bool
end

function GreedyColoringAlgorithm{decompression}(
order::AbstractOrder=NaturalOrder(); postprocessing::Bool=false
) where {decompression}
check_valid_algorithm(decompression)
return GreedyColoringAlgorithm{decompression,typeof(order)}(order, postprocessing)
function GreedyColoringAlgorithm{decompression}(
order_or_orders::Union{AbstractOrder,Tuple}=NaturalOrder();
postprocessing::Bool=false,
) where {decompression}
check_valid_algorithm(decompression)
if order_or_orders isa AbstractOrder
orders = (order_or_orders,)
else
orders = order_or_orders
end
return new{decompression,length(orders),typeof(orders)}(orders, postprocessing)
end
end

function GreedyColoringAlgorithm(
order::AbstractOrder=NaturalOrder();
order_or_orders::Union{AbstractOrder,Tuple}=NaturalOrder();
postprocessing::Bool=false,
decompression::Symbol=:direct,
)
check_valid_algorithm(decompression)
return GreedyColoringAlgorithm{decompression,typeof(order)}(order, postprocessing)
return GreedyColoringAlgorithm{decompression}(order_or_orders; postprocessing)
end

## Coloring
Expand Down Expand Up @@ -229,8 +234,11 @@ function _coloring(
)
symmetric_pattern = symmetric_pattern || A isa Union{Symmetric,Hermitian}
bg = BipartiteGraph(A; symmetric_pattern)
vertices_in_order = vertices(bg, Val(2), algo.order)
color = partial_distance2_coloring(bg, Val(2), vertices_in_order)
color_by_order = map(algo.orders) do order
vertices_in_order = vertices(bg, Val(2), order)
return partial_distance2_coloring(bg, Val(2), vertices_in_order)
end
color = argmin(maximum, color_by_order)
if speed_setting isa WithResult
return ColumnColoringResult(A, bg, color)
else
Expand All @@ -248,8 +256,11 @@ function _coloring(
)
symmetric_pattern = symmetric_pattern || A isa Union{Symmetric,Hermitian}
bg = BipartiteGraph(A; symmetric_pattern)
vertices_in_order = vertices(bg, Val(1), algo.order)
color = partial_distance2_coloring(bg, Val(1), vertices_in_order)
color_by_order = map(algo.orders) do order
vertices_in_order = vertices(bg, Val(1), order)
return partial_distance2_coloring(bg, Val(1), vertices_in_order)
end
color = argmin(maximum, color_by_order)
if speed_setting isa WithResult
return RowColoringResult(A, bg, color)
else
Expand All @@ -266,8 +277,11 @@ function _coloring(
symmetric_pattern::Bool,
)
ag = AdjacencyGraph(A; has_diagonal=true)
vertices_in_order = vertices(ag, algo.order)
color, star_set = star_coloring(ag, vertices_in_order, algo.postprocessing)
color_and_star_set_by_order = map(algo.orders) do order
vertices_in_order = vertices(ag, order)
return star_coloring(ag, vertices_in_order, algo.postprocessing)
end
color, star_set = argmin(maximum ∘ first, color_and_star_set_by_order)
if speed_setting isa WithResult
return StarSetColoringResult(A, ag, color, star_set)
else
Expand All @@ -284,8 +298,11 @@ function _coloring(
symmetric_pattern::Bool,
) where {R}
ag = AdjacencyGraph(A; has_diagonal=true)
vertices_in_order = vertices(ag, algo.order)
color, tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
color_and_tree_set_by_order = map(algo.orders) do order
vertices_in_order = vertices(ag, order)
return acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
end
color, tree_set = argmin(maximum ∘ first, color_and_tree_set_by_order)
if speed_setting isa WithResult
return TreeSetColoringResult(A, ag, color, tree_set, R)
else
Expand All @@ -303,15 +320,21 @@ function _coloring(
) where {R}
A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern)
ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; has_diagonal=false)
vertices_in_order = vertices(ag, algo.order)
color, star_set = star_coloring(ag, vertices_in_order, algo.postprocessing)
outputs_by_order = map(algo.orders) do order
vertices_in_order = vertices(ag, order)
_color, _star_set = star_coloring(ag, vertices_in_order, algo.postprocessing)
_row_color, _column_color, _ = remap_colors(
eltype(ag), _color, maximum(_color), size(A)...
)
return (_color, _star_set, _row_color, _column_color)
end
(color, star_set, row_color, column_color) = argmin(
t -> maximum(t[3]) + maximum(t[4]), outputs_by_order
) # can't use ncolors without computing the full result
if speed_setting isa WithResult
symmetric_result = StarSetColoringResult(A_and_Aᵀ, ag, color, star_set)
return BicoloringResult(A, ag, symmetric_result, R)
else
row_color, column_color, _ = remap_colors(
eltype(ag), color, maximum(color), size(A)...
)
return row_color, column_color
end
end
Expand All @@ -326,15 +349,21 @@ function _coloring(
) where {R}
A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern)
ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; has_diagonal=false)
vertices_in_order = vertices(ag, algo.order)
color, tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
outputs_by_order = map(algo.orders) do order
vertices_in_order = vertices(ag, order)
_color, _tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing)
_row_color, _column_color, _ = remap_colors(
eltype(ag), _color, maximum(_color), size(A)...
)
return (; _color, _tree_set, _row_color, _column_color)
end
(color, tree_set, row_color, column_color) = argmin(
t -> maximum(t[3]) + maximum(t[4]), outputs_by_order
) # can't use ncolors without computing the full result
Comment thread
gdalle marked this conversation as resolved.
if speed_setting isa WithResult
symmetric_result = TreeSetColoringResult(A_and_Aᵀ, ag, color, tree_set, R)
return BicoloringResult(A, ag, symmetric_result, R)
else
row_color, column_color, _ = remap_colors(
eltype(ag), color, maximum(color), size(A)...
)
return row_color, column_color
end
end
Expand Down
96 changes: 96 additions & 0 deletions test/order.jl
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,99 @@ end;
@test isperm(π)
end
end

@testset "Multiple orders" begin
# 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
@testset "Column coloring" begin
A = [
0 0 1 1
0 1 0 1
0 0 1 1
1 1 0 0
]
problem = ColoringProblem{:nonsymmetric,:column}()
algo = GreedyColoringAlgorithm(NaturalOrder())
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
@test ncolors(coloring(A, problem, better_algo)) <
ncolors(coloring(A, problem, algo))
end
@testset "Row coloring" begin
A = [
1 0 0 0
0 0 1 0
0 1 1 1
1 0 0 1
]
problem = ColoringProblem{:nonsymmetric,:row}()
algo = GreedyColoringAlgorithm(NaturalOrder())
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
@test ncolors(coloring(A, problem, better_algo)) <
ncolors(coloring(A, problem, algo))
end
@testset "Star coloring" begin
A = [
0 1 0 1 1
1 1 0 1 0
0 0 1 0 1
1 1 0 1 0
1 0 1 0 0
]
problem = ColoringProblem{:symmetric,:column}()
algo = GreedyColoringAlgorithm(NaturalOrder())
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
@test ncolors(coloring(A, problem, better_algo)) <
ncolors(coloring(A, problem, algo))
end
@testset "Acyclic coloring" begin
A = [
1 0 0 0 0 1 0
0 0 0 1 0 0 0
0 0 0 1 0 0 0
0 1 1 1 0 1 1
0 0 0 0 0 0 1
1 0 0 1 0 0 1
0 0 0 1 1 1 1
]
problem = ColoringProblem{:symmetric,:column}()
algo = GreedyColoringAlgorithm{:substitution}(NaturalOrder())
better_algo = GreedyColoringAlgorithm{:substitution}((
NaturalOrder(), LargestFirst()
))
@test ncolors(coloring(A, problem, better_algo)) <
ncolors(coloring(A, problem, algo))
end
@testset "Star bicoloring" begin
A = [
0 1 0 0 0
1 0 1 0 0
0 1 0 0 1
0 0 0 0 0
0 0 1 0 1
]
problem = ColoringProblem{:nonsymmetric,:bidirectional}()
algo = GreedyColoringAlgorithm(NaturalOrder())
better_algo = GreedyColoringAlgorithm((NaturalOrder(), LargestFirst()))
@test ncolors(coloring(A, problem, better_algo)) <
ncolors(coloring(A, problem, algo))
end
@testset "Acyclic bicoloring" begin
A = [
0 1 0 1 1 0 1 0 1
1 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0
1 0 0 1 1 0 1 0 0
1 0 0 1 0 0 0 0 0
0 0 0 0 0 0 0 0 0
1 0 0 1 0 0 0 0 0
0 0 0 0 0 0 0 0 0
1 1 0 0 0 0 0 0 0
]
problem = ColoringProblem{:nonsymmetric,:bidirectional}()
algo = GreedyColoringAlgorithm{:substitution}(NaturalOrder())
better_algo = GreedyColoringAlgorithm{:substitution}((
NaturalOrder(), LargestFirst()
))
@test ncolors(coloring(A, problem, better_algo)) <
ncolors(coloring(A, problem, algo))
end
end
8 changes: 4 additions & 4 deletions test/random.jl
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ end;
problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional)
@testset for algo in (
GreedyColoringAlgorithm(
RandomOrder(rng); postprocessing=false, decompression=:direct
RandomOrder(StableRNG(0), 0); postprocessing=false, decompression=:direct
),
GreedyColoringAlgorithm(
RandomOrder(rng); postprocessing=true, decompression=:direct
RandomOrder(StableRNG(0), 0); postprocessing=true, decompression=:direct
),
)
@testset "$((; m, n, p))" for (m, n, p) in asymmetric_params
Expand All @@ -102,10 +102,10 @@ end;
problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional)
@testset for algo in (
GreedyColoringAlgorithm(
RandomOrder(rng); postprocessing=false, decompression=:substitution
RandomOrder(StableRNG(0), 0); postprocessing=false, decompression=:substitution
),
GreedyColoringAlgorithm(
RandomOrder(rng); postprocessing=true, decompression=:substitution
RandomOrder(StableRNG(0), 0); postprocessing=true, decompression=:substitution
),
)
@testset "$((; m, n, p))" for (m, n, p) in asymmetric_params
Expand Down
10 changes: 10 additions & 0 deletions test/type_stability.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,21 @@ rng = StableRNG(63)
ColoringProblem(; structure, partition),
GreedyColoringAlgorithm(order; decompression),
)
@test_opt coloring(
A,
ColoringProblem(; structure, partition),
GreedyColoringAlgorithm((NaturalOrder(), order); decompression),
)
@inferred coloring(
A,
ColoringProblem(; structure, partition),
GreedyColoringAlgorithm(order; decompression),
)
@inferred coloring(
A,
ColoringProblem(; structure, partition),
GreedyColoringAlgorithm((NaturalOrder(), order); decompression),
)
end
end
end;
Expand Down
36 changes: 36 additions & 0 deletions test/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ using SparseMatrixColorings:
structurally_biorthogonal
using Test

const _ALL_ORDERS = (
NaturalOrder(), LargestFirst(), SmallestLast(), IncidenceDegree(), DynamicLargestFirst()
)

function test_coloring_decompression(
A0::AbstractMatrix,
problem::ColoringProblem{structure,partition},
Expand Down Expand Up @@ -169,6 +173,22 @@ function test_coloring_decompression(
@show color_vec
end
end

@testset "More orders is better" begin
more_orders = (algo.orders..., _ALL_ORDERS...)
better_algo = GreedyColoringAlgorithm{decompression}(
more_orders; algo.postprocessing
)
all_algos = [
GreedyColoringAlgorithm{decompression}(order; algo.postprocessing) for
order in more_orders
]
result = coloring(A0, problem, algo)
better_result = coloring(A0, problem, better_algo)
all_results = [coloring(A0, problem, _algo) for _algo in all_algos]
@test ncolors(better_result) <= ncolors(result)
@test ncolors(better_result) == minimum(ncolors, all_results)
end
end

function test_bicoloring_decompression(
Expand Down Expand Up @@ -216,6 +236,22 @@ function test_bicoloring_decompression(
end
end
end

@testset "More orders is better" begin
more_orders = (algo.orders..., _ALL_ORDERS...)
better_algo = GreedyColoringAlgorithm{decompression}(
more_orders; algo.postprocessing
)
all_algos = [
GreedyColoringAlgorithm{decompression}(order; algo.postprocessing) for
order in more_orders
]
result = coloring(A0, problem, algo)
better_result = coloring(A0, problem, better_algo)
all_results = [coloring(A0, problem, _algo) for _algo in all_algos]
@test ncolors(better_result) <= ncolors(result)
@test ncolors(better_result) == minimum(ncolors, all_results)
end
end

function test_structured_coloring_decompression(A::AbstractMatrix)
Expand Down
Loading