Skip to content

Commit 0c18b7b

Browse files
adrhillgdalle
andauthored
Add package extension for visualization of colorings (#149)
* Add Images.jl package extension for matrix visualization * Add visualization dev doc page * Don't export `show_colors` * Tweak defaults so they look good with ImageInTerminal * More errors * Add tests * Better docstring * Include tests in runtest * Turn this into a ColorTypes.jl extension * Drop pre 1.10 compat * Drop pre 1.10 compat v2 * Make all defaults constant * Rename `padding` to `pad` * Fix URLs and references to Images.jl * Update visualization docs with guide on large matrices * Rendering doc outputs requires Images.jl * Document how the code works * Generalize code using review feedback * Better docstring * Fix typo * Move terminal tip to basic usage section * Run JuliaFormatter * Switch from ColorTypes to Colors, minor edits * Rm ColorTypes * Rm Colors * Typo * Remove TODO * Fix import * Fix ref * Modularize code --------- Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com>
1 parent 3968cfe commit 0c18b7b

12 files changed

Lines changed: 316 additions & 3 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
playground.jl
22

3+
*.png
34
*.json
45
*.json.tmp
56

@@ -28,3 +29,4 @@ docs/src/index.md
2829
# committed for packages, but should be committed for applications that require a static
2930
# environment.
3031
Manifest.toml
32+
Manifest-v*.**.toml

Project.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1111
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1212
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1313

14+
[weakdeps]
15+
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
16+
17+
[extensions]
18+
SparseMatrixColoringsColorsExt = "Colors"
19+
1420
[compat]
1521
ADTypes = "1.2.1"
22+
Colors = "0.12.11"
1623
DataStructures = "0.18"
1724
DocStringExtensions = "0.8,0.9"
1825
LinearAlgebra = "<0.0.1, 1"

docs/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[deps]
22
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
3+
ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
34
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
45
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
6+
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
57
SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35"

docs/make.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ makedocs(;
1111
authors="Guillaume Dalle and Alexis Montoison",
1212
sitename="SparseMatrixColorings.jl",
1313
format=Documenter.HTML(),
14-
pages=["Home" => "index.md", "api.md", "dev.md"],
14+
pages=[
15+
"Home" => "index.md", "api.md", "Developer Documentation" => ["dev.md", "vis.md"]
16+
],
1517
plugins=[links],
1618
)
1719

docs/src/dev.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Dev docs
1+
# Internals
22

33
```@meta
44
CollapsedDocStrings = true
@@ -56,6 +56,12 @@ SparseMatrixColorings.matrix_versions
5656
SparseMatrixColorings.same_pattern
5757
```
5858

59+
## Visualization
60+
61+
```@docs
62+
SparseMatrixColorings.show_colors
63+
```
64+
5965
## Examples
6066

6167
```@docs

docs/src/vis.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Visualization
2+
3+
SparseMatrixColorings provides some internal utilities for visualization of matrix colorings via the un-exported function [`SparseMatrixColorings.show_colors`](@ref).
4+
5+
!!! warning
6+
This function makes use of the [JuliaImages](https://juliaimages.org) ecosystem.
7+
Using it requires loading at least [Colors.jl](https://github.com/JuliaGraphics/Colors.jl).
8+
We recommend loading the full [Images.jl](https://github.com/JuliaImages/Images.jl) package for convenience, which includes Colors.jl.
9+
10+
## Basic usage
11+
12+
To obtain a visualization, simply call `show_colors` on a coloring result:
13+
14+
```@example img
15+
using Images
16+
using SparseMatrixColorings, SparseArrays
17+
using SparseMatrixColorings: show_colors
18+
19+
S = sparse([
20+
0 0 1 1 0 1
21+
1 0 0 0 1 0
22+
0 1 0 0 1 0
23+
0 1 1 0 0 0
24+
]);
25+
26+
problem = ColoringProblem(; structure=:nonsymmetric, partition=:column)
27+
algo = GreedyColoringAlgorithm(; decompression=:direct)
28+
result = coloring(S, problem, algo)
29+
show_colors(result)
30+
```
31+
32+
!!! tip "Terminal support"
33+
Loading [ImageInTerminal.jl](https://github.com/JuliaImages/ImageInTerminal.jl) will allow you to show the output of `show_colors` within your terminal.
34+
If you use VSCode's Julia REPL, the matrix will be displayed in the plot tab.
35+
36+
## Customization
37+
38+
The visualization can be customized via keyword arguments.
39+
The size of the matrix entries is defined by `scale`, while gaps between them are dictated by `pad`.
40+
We recommend using the [ColorSchemes.jl](https://github.com/JuliaGraphics/ColorSchemes.jl) catalogue to customize the `colorscheme`.
41+
Finally, a background color can be passed via the `background` keyword argument. To obtain transparent backgrounds, use the `RGBA` type.
42+
43+
```@example img
44+
using ColorSchemes
45+
46+
julia_colors = ColorSchemes.julia
47+
white = RGB(1, 1, 1)
48+
show_colors(result; colorscheme=julia_colors, background=white, scale=5, pad=1)
49+
```
50+
51+
## Working with large matrices
52+
53+
Let's demonstrate visualization of a larger random matrix:
54+
55+
```@example img
56+
S = sprand(50, 50, 0.1) # sample sparse matrix
57+
58+
problem = ColoringProblem(; structure=:nonsymmetric, partition=:column)
59+
algo = GreedyColoringAlgorithm(; decompression=:direct)
60+
result = coloring(S, problem, algo)
61+
show_colors(result; scale=5, pad=1)
62+
```
63+
64+
Instead of the default `distinguishable_colors` from Colors.jl, one can subsample a continuous colorscheme from ColorSchemes.jl:
65+
66+
```@example img
67+
ncolors = maximum(column_colors(result)) # for partition=:column
68+
colorscheme = get(ColorSchemes.rainbow, range(0.0, 1.0, length=ncolors))
69+
show_colors(result; colorscheme=colorscheme, scale=5, pad=1)
70+
```
71+
72+
## Saving images
73+
74+
The resulting image can be saved to a variety of formats, like PNG.
75+
The `scale` and `pad` parameters determine the number of pixels, and thus the size of the file.
76+
77+
```julia
78+
img = show_colors(result, scale=5)
79+
save("coloring.png", img)
80+
```
81+
82+
Refer to the JuliaImages [documentation on saving](https://juliaimages.org/stable/function_reference/#ref_io) for more information.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#=
2+
Visualize colored matrices using the Julia Images ecosystem.
3+
Colors.jl is nearly the most light-weight dependency to achieve this.
4+
5+
This code is written prioritizing maintainability over performance
6+
7+
How it works
8+
≡≡≡≡≡≡≡≡≡≡≡≡
9+
- First, the outer `show_colors` function gets called, which
10+
- handles argument errors
11+
- eagerly promotes color types in `promote_colors` to support transparency
12+
- allocates an output buffer in `allocate_output`
13+
- The allocated output is a matrix filled with the background color
14+
- An internal `show_color!` function is called on the allocated output
15+
- for each non-zero entry in the coloring, the output is filled in
16+
=#
17+
module SparseMatrixColoringsColorsExt
18+
19+
using SparseMatrixColorings:
20+
SparseMatrixColorings,
21+
AbstractColoringResult,
22+
sparsity_pattern,
23+
column_colors,
24+
row_colors
25+
using Colors: Colorant, RGB, RGBA, distinguishable_colors
26+
27+
const DEFAULT_BACKGROUND = RGBA(0, 0, 0, 0)
28+
const DEFAULT_SCALE = 1 # update docstring in src/images.jl when changing this default
29+
const DEFAULT_PAD = 0 # update docstring in src/images.jl when changing this default
30+
31+
# Sample n distinguishable colors, excluding the background color
32+
default_colorscheme(n, background) = distinguishable_colors(n, background; dropseed=true)
33+
34+
ncolors(res::AbstractColoringResult{s,:column}) where {s} = maximum(column_colors(res))
35+
ncolors(res::AbstractColoringResult{s,:row}) where {s} = maximum(row_colors(res))
36+
37+
## Top-level function that handles argument errors, eagerly promotes types and allocates output buffer
38+
39+
function SparseMatrixColorings.show_colors(
40+
res::AbstractColoringResult;
41+
colorscheme=nothing,
42+
background::Colorant=DEFAULT_BACKGROUND, # color used for zero matrix entries and pad
43+
scale::Int=DEFAULT_SCALE, # scale size of matrix entries to `scale × scale` pixels
44+
pad::Int=DEFAULT_PAD, # pad between matrix entries
45+
warn::Bool=true,
46+
)
47+
scale < 1 && throw(ArgumentError("`scale` has to be ≥ 1."))
48+
pad < 0 && throw(ArgumentError("`pad` has to be ≥ 0."))
49+
50+
if !isnothing(colorscheme)
51+
if warn && ncolors(res) > length(colorscheme)
52+
@warn "`show_colors` will reuse colors since the provided `colorscheme` has $(length(colorscheme)) colors and the matrix needs $(ncolors(res)). You can turn off this warning via the keyword argument `warn = false`, or choose a larger `colorscheme` from ColorSchemes.jl."
53+
end
54+
colorscheme, background = promote_colors(colorscheme, background)
55+
else
56+
colorscheme = default_colorscheme(ncolors(res), convert(RGB, background))
57+
end
58+
out = allocate_output(res, background, scale, pad)
59+
return show_colors!(out, res, colorscheme, scale, pad)
60+
end
61+
62+
function promote_colors(colorscheme, background)
63+
# eagerly promote colors to same type
64+
T = promote_type(eltype(colorscheme), typeof(background))
65+
colorscheme = convert.(T, colorscheme)
66+
background = convert(T, background)
67+
return colorscheme, background
68+
end
69+
70+
function allocate_output(
71+
res::AbstractColoringResult, background::Colorant, scale::Int, pad::Int
72+
)
73+
A = sparsity_pattern(res)
74+
Base.require_one_based_indexing(A)
75+
hi, wi = size(A)
76+
h = hi * (scale + pad) + pad
77+
w = wi * (scale + pad) + pad
78+
return fill(background, h, w)
79+
end
80+
81+
# Given a CartesianIndex I of an entry in the original matrix,
82+
# this function returns the corresponding area in the output image as CartesianIndices.
83+
function matrix_entry_area(I::CartesianIndex, scale, pad)
84+
stencil = CartesianIndices((1:scale, 1:scale))
85+
return CartesianIndex(pad, pad) + (I - CartesianIndex(1, 1)) * (scale + pad) .+ stencil
86+
end
87+
88+
## Implementations for different AbstractColoringResult types start here
89+
90+
function show_colors!(
91+
out, res::AbstractColoringResult{s,:column}, colorscheme, scale, pad
92+
) where {s}
93+
color_indices = mod1.(column_colors(res), length(colorscheme)) # cycle color indices if necessary
94+
colors = colorscheme[color_indices]
95+
pattern = sparsity_pattern(res)
96+
for I in CartesianIndices(pattern)
97+
if !iszero(pattern[I])
98+
r, c = Tuple(I)
99+
area = matrix_entry_area(I, scale, pad)
100+
out[area] .= colors[c]
101+
end
102+
end
103+
return out
104+
end
105+
106+
function show_colors!(
107+
out, res::AbstractColoringResult{s,:row}, colorscheme, scale, pad
108+
) where {s}
109+
color_indices = mod1.(row_colors(res), length(colorscheme)) # cycle color indices if necessary
110+
colors = colorscheme[color_indices]
111+
pattern = sparsity_pattern(res)
112+
for I in CartesianIndices(pattern)
113+
if !iszero(pattern[I])
114+
r, c = Tuple(I)
115+
area = matrix_entry_area(I, scale, pad)
116+
out[area] .= colors[r]
117+
end
118+
end
119+
return out
120+
end
121+
122+
end # module

src/SparseMatrixColorings.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ include("adtypes.jl")
5353
include("decompression.jl")
5454
include("check.jl")
5555
include("examples.jl")
56+
include("show_colors.jl")
5657

5758
export NaturalOrder, RandomOrder, LargestFirst
5859
export ColoringProblem, GreedyColoringAlgorithm, AbstractColoringResult

src/show_colors.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Stub for Colors.jl extension in ext/SparseMatrixColoringsColorsExt.jl
2+
3+
"""
4+
show_colors(result; kwargs...)
5+
6+
Return an image visualizing an [`AbstractColoringResult`](@ref), with the help of the the [JuliaImages](https://juliaimages.org) ecosystem.
7+
8+
!!! warning
9+
This function is implemented in a package extension, using it requires loading [Colors.jl](https://github.com/JuliaGraphics/Colors.jl).
10+
11+
# Keyword arguments
12+
13+
- `colorscheme`: colors used for non-zero matrix entries. This can be a vector of `Colorant`s or a subsampled scheme from [ColorSchemes.jl](https://github.com/JuliaGraphics/ColorSchemes.jl).
14+
- `background::Colorant`: color used for zero matrix entries and pad. Defaults to `RGBA(0,0,0,0)`, a transparent background.
15+
- `scale::Int`: scale the size of matrix entries to `scale × scale` pixels. Defaults to `1`.
16+
- `pad::Int`: set padding between matrix entries, in pixels. Defaults to `0`.
17+
18+
For a matrix of size `(n, m)`, the resulting output will be of size `(n * (scale + pad) + pad, m * (scale + pad) + pad)`.
19+
"""
20+
function show_colors end

test/Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e"
77
BlockBandedMatrices = "ffab5731-97b5-5995-9138-79e8c1846df0"
88
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
99
Chairmarks = "0ca39b1e-fe0b-4e98-acfc-b1656634c4de"
10-
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
10+
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
1111
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
1212
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
1313
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
1414
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
1515
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1616
MatrixDepot = "b51810bb-c9f3-55da-ae3c-350fc1fbce05"
1717
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
18+
SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35"
1819
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
1920
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

0 commit comments

Comments
 (0)