Tutorial

Here we give a brief introduction to the contents of the package, see the API reference for more details.

using SparseMatrixColorings
using LinearAlgebra
using SparseArrays
using StableRNGs

Coloring problems and algorithms

SparseMatrixColorings.jl is based on the combination of a coloring problem and a coloring algorithm, which will be passed to the coloring function.

The problem defines what you want to solve. It is always a ColoringProblem, and you can select options such as

  • the structure of the matrix (:nonsymmetric or :symmetric)
  • the type of partition you want (:column, :row or :bidirectional).
problem = ColoringProblem()
ColoringProblem{:nonsymmetric, :column}()

The algorithm defines how you want to solve it. It can be either a GreedyColoringAlgorithm or a ConstantColoringAlgorithm. For GreedyColoringAlgorithm, you can select options such as

  • the order in which vertices are processed (a subtype of AbstractOrder)
  • the type of decompression you want (:direct or :substitution)
algo = GreedyColoringAlgorithm()
GreedyColoringAlgorithm{:direct, NaturalOrder}(NaturalOrder(), false)

Coloring results

The coloring function takes a matrix, a problem and an algorithm, to return a result subtyping AbstractColoringResult.

S = sparse([
    0 0 1 1 0 1
    1 0 0 0 1 0
    0 1 0 0 1 0
    0 1 1 0 0 0
])

result = coloring(S, problem, algo)
SparseMatrixColorings.ColumnColoringResult{SparseArrays.SparseMatrixCSC{Int64, Int64}, Int64, SparseMatrixColorings.BipartiteGraph{Int64}, Vector{SubArray{Int64, 1, Vector{Int64}, Tuple{UnitRange{Int64}}, true}}}(sparse([2, 3, 4, 1, 4, 1, 2, 3, 1], [1, 2, 2, 3, 3, 4, 5, 5, 6], [1, 1, 1, 1, 1, 1, 1, 1, 1], 4, 6), SparseMatrixColorings.BipartiteGraph{Int64}([false true false false; false false true true; … ; false true true false; true false false false], [false false … false true; true false … true false; false true … true false; false true … false false]), [1, 1, 2, 1, 2, 3], SubArray{Int64, 1, Vector{Int64}, Tuple{UnitRange{Int64}}, true}[[1, 2, 4], [3, 5], [6]], [2, 3, 4, 5, 8, 1, 6, 7, 9])

The detailed type and fields of that result are not part of the public API. To access its contents, you can use the following getters:

Here, we have a column coloring, so we can try the following:

column_colors(result)
6-element Vector{Int64}:
 1
 1
 2
 1
 2
 3
column_groups(result)
3-element Vector{SubArray{Int64, 1, Vector{Int64}, Tuple{UnitRange{Int64}}, true}}:
 [1, 2, 4]
 [3, 5]
 [6]
ncolors(result)
3

Compression and decompression

The functions compress and decompress efficiently store and retrieve compressed representations of sparse matrices, using the coloring result as a starting point.

Compression sums all columns or rows with the same color:

M = sparse([
    0 0 4 6 0 9
    1 0 0 0 7 0
    0 2 0 0 8 0
    0 3 5 0 0 0
])

B = compress(M, result)
4×3 Matrix{Int64}:
 6  4  9
 1  7  0
 2  8  0
 3  5  0

Decompression recovers the original matrix from its compressed version:

C = decompress(B, result)
4×6 SparseArrays.SparseMatrixCSC{Int64, Int64} with 9 stored entries:
 ⋅  ⋅  4  6  ⋅  9
 1  ⋅  ⋅  ⋅  7  ⋅
 ⋅  2  ⋅  ⋅  8  ⋅
 ⋅  3  5  ⋅  ⋅  ⋅

The functions decompress! and decompress_single_color! are in-place variants of decompress.

D = [
    10 14 18
    11 15 0
    12 16 0
    13 17 0
]

decompress!(C, D, result)
4×6 SparseArrays.SparseMatrixCSC{Int64, Int64} with 9 stored entries:
  ⋅   ⋅  14  10   ⋅  18
 11   ⋅   ⋅   ⋅  15   ⋅
  ⋅  12   ⋅   ⋅  16   ⋅
  ⋅  13  17   ⋅   ⋅   ⋅
nonzeros(C) .= -1
decompress_single_color!(C, D[:, 2], 2, result)
4×6 SparseArrays.SparseMatrixCSC{Int64, Int64} with 9 stored entries:
  ⋅   ⋅  14  -1   ⋅  -1
 -1   ⋅   ⋅   ⋅  15   ⋅
  ⋅  -1   ⋅   ⋅  16   ⋅
  ⋅  -1  17   ⋅   ⋅   ⋅

Unidirectional variants

We now illustrate the variants of colorings available, on the following matrix:

S = sparse(Symmetric(sprand(StableRNG(0), Bool, 10, 10, 0.4)))
10×10 SparseArrays.SparseMatrixCSC{Bool, Int64} with 31 stored entries:
 1  1  ⋅  1  1  ⋅  ⋅  ⋅  ⋅  ⋅
 1  1  1  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅
 ⋅  1  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅
 1  ⋅  ⋅  1  ⋅  1  ⋅  ⋅  ⋅  ⋅
 1  ⋅  ⋅  ⋅  1  1  ⋅  ⋅  1  1
 ⋅  1  1  1  1  ⋅  ⋅  ⋅  ⋅  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅
 ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅
 ⋅  ⋅  ⋅  ⋅  1  ⋅  1  1  ⋅  1
 ⋅  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  1  1

We start with unidirectional colorings, where only rows or columns are colored and the matrix is not assumed to be symmetric.

Column coloring

problem = ColoringProblem(; structure=:nonsymmetric, partition=:column)
algo = GreedyColoringAlgorithm(; decompression=:direct)
result = coloring(S, problem, algo)
ncolors(result), column_colors(result)
(6, [1, 2, 3, 4, 5, 6, 1, 2, 2, 3])

Here is the colored matrix

and its columnwise compression

Row coloring

problem = ColoringProblem(; structure=:nonsymmetric, partition=:row)
algo = GreedyColoringAlgorithm(; decompression=:direct)
result = coloring(S, problem, algo)
ncolors(result), row_colors(result)
(6, [1, 2, 3, 4, 5, 6, 1, 2, 2, 3])

Here is the colored matrix

and its rowwise compression

Symmetric variants

We continue with unidirectional symmetric colorings, where coloring rows is equivalent to coloring columns. Symmetry is leveraged to possibly reduce the number of necessary colors.

Star coloring

Star coloring is the algorithm used for symmetric matrices with direct decompression.

problem = ColoringProblem(; structure=:symmetric, partition=:column)
algo = GreedyColoringAlgorithm(; decompression=:direct)
result = coloring(S, problem, algo)
ncolors(result), column_colors(result)
(5, [1, 2, 1, 3, 3, 4, 1, 1, 2, 5])

Here is the colored matrix

and its columnwise compression

Acyclic coloring

Acyclic coloring is the algorithm used for symmetric matrices with decompression by substitution.

problem = ColoringProblem(; structure=:symmetric, partition=:column)
algo = GreedyColoringAlgorithm(; decompression=:substitution)
result = coloring(S, problem, algo)
ncolors(result), column_colors(result)
(3, [1, 2, 1, 2, 2, 3, 1, 1, 3, 1])

Here is the colored matrix

and its columnwise compression

Bidirectional variants

We finish with bidirectional colorings, where both rows and columns are colored and the matrix is not assumed to be symmetric.

Bicoloring is most relevant for matrices with dense rows and columns, which is why we consider the following test case:

S_bi = copy(S)
S_bi[:, 1] .= true
S_bi[1, :] .= true
S_bi
10×10 SparseArrays.SparseMatrixCSC{Bool, Int64} with 43 stored entries:
 1  1  1  1  1  1  1  1  1  1
 1  1  1  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅
 1  1  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  ⋅
 1  ⋅  ⋅  1  ⋅  1  ⋅  ⋅  ⋅  ⋅
 1  ⋅  ⋅  ⋅  1  1  ⋅  ⋅  1  1
 1  1  1  1  1  ⋅  ⋅  ⋅  ⋅  ⋅
 1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅
 1  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  1  ⋅
 1  ⋅  ⋅  ⋅  1  ⋅  1  1  ⋅  1
 1  ⋅  ⋅  ⋅  1  ⋅  ⋅  ⋅  1  1

With our implementations, bidirectional coloring works better using a RandomOrder.

Star bicoloring

problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional)
algo = GreedyColoringAlgorithm(RandomOrder(StableRNG(0), 0); decompression=:direct)
result = coloring(S_bi, problem, algo)
ncolors(result), column_colors(result)
(9, [1, 2, 2, 2, 3, 2, 2, 2, 4, 2])

Here is the colored matrix

its columnwise compression

and rowwise compression

Both are necessary to reconstruct the original.

Acyclic bicoloring

problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional)
algo = GreedyColoringAlgorithm(RandomOrder(StableRNG(0), 0); decompression=:substitution)
result = coloring(S_bi, problem, algo)
ncolors(result), column_colors(result)
(8, [1, 2, 2, 3, 4, 2, 2, 2, 3, 5])

Here is the colored matrix

Example block output

its columnwise compression

and rowwise compression

Both are necessary to reconstruct the original.