Skip to content

Commit

Permalink
Add compressed visualization (#157)
Browse files Browse the repository at this point in the history
* Add compressed visualization

* Add tests

* Typo

* Bump version
  • Loading branch information
gdalle authored Nov 11, 2024
1 parent 0246517 commit 1e83716
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 76 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "SparseMatrixColorings"
uuid = "0a514795-09f3-496d-8182-132a7b665d35"
authors = ["Guillaume Dalle", "Alexis Montoison"]
version = "0.4.9"
version = "0.4.10"

[deps]
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
Expand Down
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35"
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
79 changes: 65 additions & 14 deletions docs/src/vis.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ SparseMatrixColorings provides some internal utilities for visualization of matr
Using it requires loading at least [Colors.jl](https://github.com/JuliaGraphics/Colors.jl).
We recommend loading the full [Images.jl](https://github.com/JuliaImages/Images.jl) package for convenience, which includes Colors.jl.

## Basic usage

To obtain a visualization, simply call `show_colors` on a coloring result:

```@example img
using ColorSchemes
using Images
using SparseMatrixColorings, SparseArrays
using SparseArrays
using SparseMatrixColorings
using SparseMatrixColorings: show_colors
using StableRNGs
```

## Basic usage

To obtain a visualization, simply call `show_colors` on a coloring result. It returns a tuple of outputs, corresponding to the matrix and its compression(s):

```@example img
S = sparse([
0 0 1 1 0 1
1 0 0 0 1 0
Expand All @@ -26,7 +31,20 @@ S = sparse([
problem = ColoringProblem(; structure=:nonsymmetric, partition=:column)
algo = GreedyColoringAlgorithm(; decompression=:direct)
result = coloring(S, problem, algo)
show_colors(result)
A_img, B_img = show_colors(result; scale=3)
```

The colors on the original matrix look like this:

```@example img
A_img
```

And its column compression looks like that:

```@example img
B_img
```

!!! tip "Terminal support"
Expand All @@ -40,12 +58,45 @@ The size of the matrix entries is defined by `scale`, while gaps between them ar
We recommend using the [ColorSchemes.jl](https://github.com/JuliaGraphics/ColorSchemes.jl) catalogue to customize the `colorscheme`.
Finally, a background color can be passed via the `background` keyword argument. To obtain transparent backgrounds, use the `RGBA` type.

We demonstrate this on a bidirectional coloring.

```@example img
using ColorSchemes
julia_colors = ColorSchemes.julia
white = RGB(1, 1, 1)
show_colors(result; colorscheme=julia_colors, background=white, scale=5, pad=1)
S = sparse([
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
])
problem_bi = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional)
algo_bi = GreedyColoringAlgorithm(RandomOrder(StableRNG(0)); decompression=:direct)
result_bi = coloring(S, problem_bi, algo_bi)
A_img, Br_img, Bc_img = show_colors(
result_bi;
colorscheme=ColorSchemes.progress,
background=RGB(1, 1, 1), # white
scale=10,
pad=2
)
```

In the bidirectional case, columns and rows can both get colors:

```@example img
A_img
```

And there are two compression results, one by row and one by column:

```@example img
Br_img
```

```@example img
Bc_img
```

## Working with large matrices
Expand All @@ -58,15 +109,15 @@ S = sprand(50, 50, 0.1) # sample sparse matrix
problem = ColoringProblem(; structure=:nonsymmetric, partition=:column)
algo = GreedyColoringAlgorithm(; decompression=:direct)
result = coloring(S, problem, algo)
show_colors(result; scale=5, pad=1)
show_colors(result; scale=5, pad=1)[1]
```

Instead of the default `distinguishable_colors` from Colors.jl, one can subsample a continuous colorscheme from ColorSchemes.jl:

```@example img
ncolors = maximum(column_colors(result)) # for partition=:column
colorscheme = get(ColorSchemes.rainbow, range(0.0, 1.0, length=ncolors))
show_colors(result; colorscheme=colorscheme, scale=5, pad=1)
show_colors(result; colorscheme=colorscheme, scale=5, pad=1)[1]
```

## Saving images
Expand All @@ -75,8 +126,8 @@ The resulting image can be saved to a variety of formats, like PNG.
The `scale` and `pad` parameters determine the number of pixels, and thus the size of the file.

```julia
img = show_colors(result, scale=5)
save("coloring.png", img)
A_img, _ = show_colors(result, scale=5)
save("coloring.png", A_img)
```

Refer to the JuliaImages [documentation on saving](https://juliaimages.org/stable/function_reference/#ref_io) for more information.
160 changes: 123 additions & 37 deletions ext/SparseMatrixColoringsColorsExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ using SparseMatrixColorings:
sparsity_pattern,
column_colors,
row_colors,
ncolors
ncolors,
compress
using Colors: Colorant, RGB, RGBA, distinguishable_colors

const DEFAULT_BACKGROUND = RGBA(0, 0, 0, 0)
Expand Down Expand Up @@ -53,8 +54,8 @@ function SparseMatrixColorings.show_colors(
else
colorscheme = default_colorscheme(ncolors(res), convert(RGB, background))
end
out = allocate_output(res, background, scale, pad)
return show_colors!(out, res, colorscheme, scale, pad)
outs = allocate_outputs(res, background, scale, pad)
return show_colors!(outs..., res, colorscheme, scale, pad)
end

function promote_colors(colorscheme, background)
Expand All @@ -65,15 +66,41 @@ function promote_colors(colorscheme, background)
return colorscheme, background
end

function allocate_output(
res::AbstractColoringResult, background::Colorant, scale::Int, pad::Int
)
function allocate_outputs(
res::Union{AbstractColoringResult{s,:column},AbstractColoringResult{s,:row}},
background::Colorant,
scale::Int,
pad::Int,
) where {s}
A = sparsity_pattern(res)
B = compress(A, res)
Base.require_one_based_indexing(A)
Base.require_one_based_indexing(B)
hA, wA = size(A) .* (scale + pad) .+ pad
hB, wB = size(B) .* (scale + pad) .+ pad
A_img = fill(background, hA, wA)
B_img = fill(background, hB, wB)
return A_img, B_img
end

function allocate_outputs(
res::AbstractColoringResult{s,:bidirectional},
background::Colorant,
scale::Int,
pad::Int,
) where {s}
A = sparsity_pattern(res)
Br, Bc = compress(A, res)
Base.require_one_based_indexing(A)
hi, wi = size(A)
h = hi * (scale + pad) + pad
w = wi * (scale + pad) + pad
return fill(background, h, w)
Base.require_one_based_indexing(Br)
Base.require_one_based_indexing(Bc)
hA, wA = size(A) .* (scale + pad) .+ pad
hBr, wBr = size(Br) .* (scale + pad) .+ pad
hBc, wBc = size(Bc) .* (scale + pad) .+ pad
A_img = fill(background, hA, wA)
Br_img = fill(background, hBr, wBr)
Bc_img = fill(background, hBc, wBc)
return A_img, Br_img, Bc_img
end

# Given a CartesianIndex I of an entry in the original matrix,
Expand All @@ -86,61 +113,120 @@ end
## Implementations for different AbstractColoringResult types start here

function show_colors!(
out, res::AbstractColoringResult{s,:column}, colorscheme, scale, pad
A_img::AbstractMatrix{<:Colorant},
B_img::AbstractMatrix{<:Colorant},
res::AbstractColoringResult{s,:column},
colorscheme,
scale,
pad,
) where {s}
color_indices = mod1.(column_colors(res), length(colorscheme)) # cycle color indices if necessary
colors = colorscheme[color_indices]
pattern = sparsity_pattern(res)
for I in CartesianIndices(pattern)
if !iszero(pattern[I])
# cycle color indices if necessary
A_color_indices = mod1.(column_colors(res), length(colorscheme))
B_color_indices = mod1.(1:ncolors(res), length(colorscheme))
A_colors = colorscheme[A_color_indices]
B_colors = colorscheme[B_color_indices]
A = sparsity_pattern(res)
B = compress(A, res)
for I in CartesianIndices(A)
if !iszero(A[I])
r, c = Tuple(I)
area = matrix_entry_area(I, scale, pad)
A_img[area] .= A_colors[c]
end
end
for I in CartesianIndices(B)
if !iszero(B[I])
r, c = Tuple(I)
area = matrix_entry_area(I, scale, pad)
out[area] .= colors[c]
B_img[area] .= B_colors[c]
end
end
return out
return A_img, B_img
end

function show_colors!(
out, res::AbstractColoringResult{s,:row}, colorscheme, scale, pad
A_img::AbstractMatrix{<:Colorant},
B_img::AbstractMatrix{<:Colorant},
res::AbstractColoringResult{s,:row},
colorscheme,
scale,
pad,
) where {s}
color_indices = mod1.(row_colors(res), length(colorscheme)) # cycle color indices if necessary
colors = colorscheme[color_indices]
pattern = sparsity_pattern(res)
for I in CartesianIndices(pattern)
if !iszero(pattern[I])
# cycle color indices if necessary
A_color_indices = mod1.(row_colors(res), length(colorscheme))
B_color_indices = mod1.(1:ncolors(res), length(colorscheme))
A_colors = colorscheme[A_color_indices]
B_colors = colorscheme[B_color_indices]
A = sparsity_pattern(res)
B = compress(A, res)
for I in CartesianIndices(A)
if !iszero(A[I])
r, c = Tuple(I)
area = matrix_entry_area(I, scale, pad)
out[area] .= colors[r]
A_img[area] .= A_colors[r]
end
end
return out
for I in CartesianIndices(B)
if !iszero(B[I])
r, c = Tuple(I)
area = matrix_entry_area(I, scale, pad)
B_img[area] .= B_colors[r]
end
end
return A_img, B_img
end

function show_colors!(
out, res::AbstractColoringResult{s,:bidirectional}, colorscheme, scale, pad
A_img::AbstractMatrix{<:Colorant},
Br_img::AbstractMatrix{<:Colorant},
Bc_img::AbstractMatrix{<:Colorant},
res::AbstractColoringResult{s,:bidirectional},
colorscheme,
scale,
pad,
) where {s}
scale < 3 && throw(ArgumentError("`scale` has to be ≥ 3 to visualize bicoloring"))
ccolor_indices = mod1.(column_colors(res), length(colorscheme)) # cycle color indices if necessary
# cycle color indices if necessary
row_shift = maximum(column_colors(res))
rcolor_indices = mod1.(row_shift .+ row_colors(res), length(colorscheme)) # cycle color indices if necessary
ccolors = colorscheme[ccolor_indices]
rcolors = colorscheme[rcolor_indices]
pattern = sparsity_pattern(res)
for I in CartesianIndices(pattern)
if !iszero(pattern[I])
A_ccolor_indices = mod1.(column_colors(res), length(colorscheme))
A_rcolor_indices = mod1.(row_shift .+ row_colors(res), length(colorscheme))
B_ccolor_indices = mod1.(1:maximum(column_colors(res)), length(colorscheme))
B_rcolor_indices =
mod1.((row_shift + 1):(row_shift + maximum(row_colors(res))), length(colorscheme))
A_ccolors = colorscheme[A_ccolor_indices]
A_rcolors = colorscheme[A_rcolor_indices]
B_ccolors = colorscheme[B_ccolor_indices]
B_rcolors = colorscheme[B_rcolor_indices]
A = sparsity_pattern(res)
Br, Bc = compress(A, res)
for I in CartesianIndices(A)
if !iszero(A[I])
r, c = Tuple(I)
area = matrix_entry_area(I, scale, pad)
for i in axes(area, 1), j in axes(area, 2)
if j > i
out[area[i, j]] = ccolors[c]
A_img[area[i, j]] = A_ccolors[c]
elseif i > j
out[area[i, j]] = rcolors[r]
A_img[area[i, j]] = A_rcolors[r]
end
end
end
end
return out
for I in CartesianIndices(Br)
if !iszero(Br[I])
r, c = Tuple(I)
area = matrix_entry_area(I, scale, pad)
Br_img[area] .= B_rcolors[r]
end
end
for I in CartesianIndices(Bc)
if !iszero(Bc[I])
r, c = Tuple(I)
area = matrix_entry_area(I, scale, pad)
Bc_img[area] .= B_ccolors[c]
end
end
return A_img, Br_img, Bc_img
end

end # module
5 changes: 4 additions & 1 deletion src/show_colors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"""
show_colors(result; kwargs...)
Return an image visualizing an [`AbstractColoringResult`](@ref), with the help of the the [JuliaImages](https://juliaimages.org) ecosystem.
Create a visualization for an [`AbstractColoringResult`](@ref), with the help of the the [JuliaImages](https://juliaimages.org) ecosystem.
- For `:column` or `:row` colorings, it returns a tuple `(A_img, B_img)`.
- For `:bidirectional` colorings, it returns a tuple `(A_img, Br_img, Bc_img)`.
!!! warning
This function is implemented in a package extension, using it requires loading [Colors.jl](https://github.com/JuliaGraphics/Colors.jl).
Expand Down
Loading

2 comments on commit 1e83716

@gdalle
Copy link
Owner Author

@gdalle gdalle commented on 1e83716 Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/119186

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.4.10 -m "<description of version>" 1e8371623d3b18dfc6b9320cfd5b34624dd2005d
git push origin v0.4.10

Please sign in to comment.