Skip to content

Commit 17bc044

Browse files
authored
Optimal coloring algorithm with JuMP formulation (#271)
1 parent b53dbf9 commit 17bc044

File tree

8 files changed

+179
-0
lines changed

8 files changed

+179
-0
lines changed

Project.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,24 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1515
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
1616
CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8"
1717
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
18+
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
19+
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
1820

1921
[extensions]
2022
SparseMatrixColoringsCUDAExt = "CUDA"
2123
SparseMatrixColoringsCliqueTreesExt = "CliqueTrees"
2224
SparseMatrixColoringsColorsExt = "Colors"
25+
SparseMatrixColoringsJuMPExt = ["JuMP", "MathOptInterface"]
2326

2427
[compat]
2528
ADTypes = "1.2.1"
2629
CUDA = "5.8.2"
2730
CliqueTrees = "1"
2831
Colors = "0.12.11, 0.13"
2932
DocStringExtensions = "0.8,0.9"
33+
JuMP = "1.29.1"
3034
LinearAlgebra = "<0.0.1, 1"
35+
MathOptInterface = "1.45.0"
3136
PrecompileTools = "1.2.1"
3237
Random = "<0.0.1, 1"
3338
SparseArrays = "<0.0.1, 1"

docs/src/api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ SparseMatrixColorings
1717
coloring
1818
fast_coloring
1919
ColoringProblem
20+
```
21+
22+
## Coloring algorithms
23+
24+
```@docs
2025
GreedyColoringAlgorithm
2126
ConstantColoringAlgorithm
27+
OptimalColoringAlgorithm
2228
```
2329

2430
## Result analysis
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
module SparseMatrixColoringsJuMPExt
2+
3+
using ADTypes: ADTypes
4+
using JuMP:
5+
Model,
6+
is_solved_and_feasible,
7+
optimize!,
8+
primal_status,
9+
set_silent,
10+
set_start_value,
11+
value,
12+
@variable,
13+
@constraint,
14+
@objective
15+
using JuMP
16+
import MathOptInterface as MOI
17+
using SparseMatrixColorings:
18+
BipartiteGraph, OptimalColoringAlgorithm, nb_vertices, neighbors, pattern, vertices
19+
20+
function optimal_distance2_coloring(
21+
bg::BipartiteGraph,
22+
::Val{side},
23+
optimizer::O;
24+
silent::Bool=true,
25+
assert_solved::Bool=true,
26+
) where {side,O}
27+
other_side = 3 - side
28+
n = nb_vertices(bg, Val(side))
29+
model = Model(optimizer)
30+
silent && set_silent(model)
31+
# one variable per vertex to color, removing some renumbering symmetries
32+
@variable(model, 1 <= color[i=1:n] <= i, Int)
33+
# one variable to count the number of distinct colors
34+
@variable(model, ncolors, Int)
35+
@constraint(model, [ncolors; color] in MOI.CountDistinct(n + 1))
36+
# distance-2 coloring: neighbors of the same vertex must have distinct colors
37+
for i in vertices(bg, Val(other_side))
38+
neigh = neighbors(bg, Val(other_side), i)
39+
@constraint(model, color[neigh] in MOI.AllDifferent(length(neigh)))
40+
end
41+
# minimize the number of distinct colors (can't use maximum because they are not necessarily numbered contiguously)
42+
@objective(model, Min, ncolors)
43+
# actual solving step where time is spent
44+
optimize!(model)
45+
if assert_solved
46+
# assert feasibility and optimality
47+
@assert is_solved_and_feasible(model)
48+
else
49+
# only assert feasibility
50+
@assert primal_status(model) == MOI.FEASIBLE_POINT
51+
end
52+
# native solver solutions are floating point numbers
53+
color_int = round.(Int, value.(color))
54+
# remap to 1:cmax in case they are not contiguous
55+
true_ncolors = 0
56+
remap = fill(0, maximum(color_int))
57+
for c in color_int
58+
if remap[c] == 0
59+
true_ncolors += 1
60+
remap[c] = true_ncolors
61+
end
62+
end
63+
return remap[color_int]
64+
end
65+
66+
function ADTypes.column_coloring(A::AbstractMatrix, algo::OptimalColoringAlgorithm)
67+
bg = BipartiteGraph(A)
68+
return optimal_distance2_coloring(
69+
bg, Val(2), algo.optimizer; algo.silent, algo.assert_solved
70+
)
71+
end
72+
73+
function ADTypes.row_coloring(A::AbstractMatrix, algo::OptimalColoringAlgorithm)
74+
bg = BipartiteGraph(A)
75+
return optimal_distance2_coloring(
76+
bg, Val(1), algo.optimizer; algo.silent, algo.assert_solved
77+
)
78+
end
79+
80+
end

src/SparseMatrixColorings.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ include("decompression.jl")
5656
include("check.jl")
5757
include("examples.jl")
5858
include("show_colors.jl")
59+
include("optimal.jl")
5960

6061
include("precompile.jl")
6162

@@ -64,6 +65,7 @@ export DynamicDegreeBasedOrder, SmallestLast, IncidenceDegree, DynamicLargestFir
6465
export PerfectEliminationOrder
6566
export ColoringProblem, GreedyColoringAlgorithm, AbstractColoringResult
6667
export ConstantColoringAlgorithm
68+
export OptimalColoringAlgorithm
6769
export coloring, fast_coloring
6870
export column_colors, row_colors, ncolors
6971
export column_groups, row_groups

src/optimal.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
OptimalColoringAlgorithm
3+
4+
Coloring algorithm that relies on mathematical programming with [JuMP](https://jump.dev/) to find an optimal coloring.
5+
6+
!!! warning
7+
This algorithm is only available when JuMP is loaded. If you encounter a method error, run `import JuMP` in your REPL and try again.
8+
It only works for nonsymmetric, unidirectional colorings problems.
9+
10+
!!! danger
11+
The coloring problem is NP-hard, so it is unreasonable to expect an optimal solution in reasonable time for large instances.
12+
13+
# Constructor
14+
15+
OptimalColoringAlgorithm(optimizer; silent::Bool=true, assert_solved::Bool=true)
16+
17+
The `optimizer` argument can be any JuMP-compatible optimizer.
18+
However, the problem formulation is best suited to CP-SAT optimizers like [MiniZinc](https://github.com/jump-dev/MiniZinc.jl).
19+
You can use [`optimizer_with_attributes`](https://jump.dev/JuMP.jl/stable/api/JuMP/#optimizer_with_attributes) to set solver-specific parameters.
20+
21+
# Keyword arguments
22+
23+
- `silent`: whether to suppress solver output
24+
- `assert_solved`: whether to check that the solver found an optimal solution (as opposed to running out of time for example)
25+
"""
26+
struct OptimalColoringAlgorithm{O} <: ADTypes.AbstractColoringAlgorithm
27+
optimizer::O
28+
silent::Bool
29+
assert_solved::Bool
30+
end
31+
32+
function OptimalColoringAlgorithm(optimizer; silent::Bool=true, assert_solved::Bool=true)
33+
return OptimalColoringAlgorithm(optimizer, silent, assert_solved)
34+
end

test/Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ CliqueTrees = "60701a23-6482-424a-84db-faee86b9b1f8"
1212
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
1313
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
1414
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
15+
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
1516
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
17+
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
1618
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
1719
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1820
MatrixDepot = "b51810bb-c9f3-55da-ae3c-350fc1fbce05"
21+
MiniZinc = "a7f392d2-6c35-496e-b8cc-0974fbfcbf91"
1922
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2023
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
2124
SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35"

test/optimal.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using SparseArrays
2+
using SparseMatrixColorings
3+
using StableRNGs
4+
using Test
5+
using JuMP
6+
using MiniZinc
7+
using HiGHS
8+
9+
rng = StableRNG(0)
10+
11+
asymmetric_params = vcat(
12+
[(10, 20, p) for p in (0.0:0.1:0.5)], [(20, 10, p) for p in (0.0:0.1:0.5)]
13+
)
14+
15+
algo = GreedyColoringAlgorithm()
16+
optalgo = OptimalColoringAlgorithm(() -> MiniZinc.Optimizer{Float64}("highs"); silent=false)
17+
18+
@testset "Column coloring" begin
19+
problem = ColoringProblem(; structure=:nonsymmetric, partition=:column)
20+
for (m, n, p) in asymmetric_params
21+
A = sprand(rng, m, n, p)
22+
result = coloring(A, problem, algo)
23+
optresult = coloring(A, problem, optalgo)
24+
@test ncolors(result) >= ncolors(optresult)
25+
end
26+
end
27+
28+
@testset "Row coloring" begin
29+
problem = ColoringProblem(; structure=:nonsymmetric, partition=:row)
30+
for (m, n, p) in asymmetric_params
31+
A = sprand(rng, m, n, p)
32+
result = coloring(A, problem, algo)
33+
optresult = coloring(A, problem, optalgo)
34+
@test ncolors(result) >= ncolors(optresult)
35+
end
36+
end
37+
38+
@testset "Too big" begin
39+
A = sprand(rng, Bool, 100, 100, 0.1)
40+
optalgo_timelimit = OptimalColoringAlgorithm(
41+
optimizer_with_attributes(HiGHS.Optimizer, "time_limit" => 10.0); # 1 second
42+
silent=false,
43+
assert_solved=false,
44+
)
45+
@test_throws AssertionError coloring(A, ColoringProblem(), optalgo_timelimit)
46+
end

test/runtests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ include("utils.jl")
5959
@testset "Constant coloring" begin
6060
include("constant.jl")
6161
end
62+
@testset "Optimal coloring" begin
63+
include("optimal.jl")
64+
end
6265
@testset "ADTypes coloring algorithms" begin
6366
include("adtypes.jl")
6467
end

0 commit comments

Comments
 (0)