From 72d46524ae47d033ba5066b8524ca9076972eddd Mon Sep 17 00:00:00 2001 From: Dan Stahlke Date: Sat, 5 Aug 2023 11:33:36 -0700 Subject: [PATCH 1/2] added optimal_coloring --- Project.toml | 5 +++- src/GraphsOptim.jl | 4 ++++ src/coloring.jl | 59 ++++++++++++++++++++++++++++++++++++++++++++++ test/coloring.jl | 38 +++++++++++++++++++++++++++++ test/runtests.jl | 4 ++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/coloring.jl create mode 100644 test/coloring.jl diff --git a/Project.toml b/Project.toml index c2e2a3e..0d85e15 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" OptimalTransport = "7e02d93a-ae51-4f58-b602-d97af76e3b33" +PicoSAT = "ff2beb65-d7cd-5ff1-a187-74671133a339" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] @@ -23,6 +24,7 @@ JuMP = "1" JuliaFormatter = "1" MathOptInterface = "1.18" OptimalTransport = "0.3" +PicoSAT = "0.4" julia = "1.6" [extras] @@ -30,6 +32,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" @@ -38,4 +41,4 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Documenter", "Graphs", "HiGHS", "JET", "JuMP", "JuliaFormatter", "LinearAlgebra", "Test", "SparseArrays"] +test = ["Aqua", "Documenter", "Graphs", "HiGHS", "JET", "JuMP", "JuliaFormatter", "LinearAlgebra", "Test", "SparseArrays", "IterTools"] diff --git a/src/GraphsOptim.jl b/src/GraphsOptim.jl index 62e58d4..18f8d3f 100644 --- a/src/GraphsOptim.jl +++ b/src/GraphsOptim.jl @@ -7,6 +7,7 @@ module GraphsOptim using Graphs: AbstractGraph, is_directed using Graphs: vertices, edges, nv, ne, src, dst, inneighbors, outneighbors +using Graphs: Coloring, maximal_cliques using FillArrays: Zeros, Fill using HiGHS: HiGHS using JuMP: Model @@ -17,14 +18,17 @@ using LinearAlgebra: norm, tr, dot using MathOptInterface: OPTIMAL using SparseArrays: sparse using OptimalTransport: sinkhorn +using PicoSAT: solve export min_cost_flow export min_cost_assignment export FAQ, GOAT, graph_matching +export optimal_coloring include("utils.jl") include("flow.jl") include("assignment.jl") include("graph_matching.jl") +include("coloring.jl") end diff --git a/src/coloring.jl b/src/coloring.jl new file mode 100644 index 0000000..50cee4e --- /dev/null +++ b/src/coloring.jl @@ -0,0 +1,59 @@ +""" + optimal_coloring(g) + +Finds a proper coloring using the minimum possible number of colors. Returns a +Graphs.Coloring object. Beware: this is an NP-complete problem and so runtime +can in the worst case increase exponentially in the size of the graph. +""" +function optimal_coloring(g::AbstractGraph{T})::Coloring{T} where {T<:Integer} + # It is not clear whether maximum clique or maximal clique is faster. + # Maximum clique should generally make coloring faster but of course can + # itself be time consuming to compute. + max_clique = argmax(length, maximal_cliques(g)) + #max_clique = independent_set(complement(g), DegreeIndependentSet()) + #@show max_clique + + for χ in length(max_clique):nv(g) + #println("Trying χ=$χ") + indexer = reshape(1:(nv(g) * χ), (nv(g), χ)) + cnf = Vector{Int64}[] + + # Seeding the solution on a maximal clique makes it solve faster. + for (i, v) in enumerate(max_clique) + push!(cnf, [indexer[v, i]]) + end + + for i in 1:nv(g) + push!(cnf, indexer[i, :]) + end + for e in edges(g) + if src(e) != dst(e) + for j in 1:χ + push!(cnf, [-indexer[src(e), j], -indexer[dst(e), j]]) + end + end + end + sol = solve(cnf) + if typeof(sol) == Symbol + if sol == :unsatisfiable + # continue + else + throw(ErrorException("PicoSAT.solve returned unrecognized result status")) + end + else + colormatrix = zeros(Bool, (nv(g), χ)) + # typeassert is needed to get JET.test_package to pass + colormatrix[filter(i -> i > 0, sol::AbstractArray)] .= true + colors = map(argmax, eachrow(colormatrix)) + @assert all(colors .>= 1) + @assert all(colors .<= χ) + for e in edges(g) + if src(e) != dst(e) + @assert colors[src(e)] != colors[dst(e)] + end + end + return Coloring{T}(χ, colors) + end + end + @assert false +end diff --git a/test/coloring.jl b/test/coloring.jl new file mode 100644 index 0000000..f395fd3 --- /dev/null +++ b/test/coloring.jl @@ -0,0 +1,38 @@ +using Graphs +using GraphsOptim +using IterTools +using Test + +function queens_graph(n::Integer, m::Integer) + g = SimpleGraph(n * m) + for (ix, iy, jx, jy) in product(1:n, 1:m, 1:n, 1:m) + dx = ix - jx + dy = iy - jy + if dx != 0 || dy != 0 + if dx == 0 || dy == 0 || dx == dy || dx == -dy + add_edge!(g, (ix - 1) * m + iy, (jx - 1) * m + jy) + end + end + end + return g +end + +queens_graph(n::Integer) = queens_graph(n, n) + +function kneser_graph(n::Integer, k::Integer) + ss = collect(subsets(1:n, k)) + return SimpleGraph([isdisjoint(a, b) for a in ss, b in ss]) +end + +# https://oeis.org/A088202 +@test optimal_coloring(queens_graph(1)).num_colors == 1 +@test optimal_coloring(queens_graph(2)).num_colors == 4 +@test optimal_coloring(queens_graph(3)).num_colors == 5 +@test optimal_coloring(queens_graph(4)).num_colors == 5 +@test optimal_coloring(queens_graph(5)).num_colors == 5 +@test optimal_coloring(queens_graph(6)).num_colors == 7 +@test optimal_coloring(queens_graph(7)).num_colors == 7 +# Runs in 52 seconds on i7-12800HX. +#@test optimal_coloring(queens_graph(8)).num_colors == 9 + +@test optimal_coloring(kneser_graph(11, 4)).num_colors == 11 - 2 * 4 + 2 diff --git a/test/runtests.jl b/test/runtests.jl index 5e19bc2..32a9a71 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -39,4 +39,8 @@ using Test @testset verbose = true "Graph matching" begin include("graph_matching.jl") end + + @testset verbose = true "Coloring" begin + include("coloring.jl") + end end; From dc2622eee7e0314094c7fcda531a0d8501d5e504 Mon Sep 17 00:00:00 2001 From: Guillaume Dalle <22795598+gdalle@users.noreply.github.com> Date: Fri, 27 Oct 2023 11:38:10 +0200 Subject: [PATCH 2/2] Base coloring on ILP --- Project.toml | 4 +-- docs/src/algorithms.md | 9 +++++ src/GraphsOptim.jl | 3 +- src/coloring.jl | 80 ++++++++++++++++-------------------------- test/coloring.jl | 20 +++++------ 5 files changed, 52 insertions(+), 64 deletions(-) diff --git a/Project.toml b/Project.toml index 0d85e15..f4a422a 100644 --- a/Project.toml +++ b/Project.toml @@ -11,7 +11,6 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" OptimalTransport = "7e02d93a-ae51-4f58-b602-d97af76e3b33" -PicoSAT = "ff2beb65-d7cd-5ff1-a187-74671133a339" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] @@ -24,7 +23,6 @@ JuMP = "1" JuliaFormatter = "1" MathOptInterface = "1.18" OptimalTransport = "0.3" -PicoSAT = "0.4" julia = "1.6" [extras] @@ -41,4 +39,4 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Documenter", "Graphs", "HiGHS", "JET", "JuMP", "JuliaFormatter", "LinearAlgebra", "Test", "SparseArrays", "IterTools"] +test = ["Aqua", "Documenter", "Graphs", "HiGHS", "JET", "IterTools", "JuMP", "JuliaFormatter", "LinearAlgebra", "SparseArrays", "Test"] diff --git a/docs/src/algorithms.md b/docs/src/algorithms.md index 5fd73ca..71f4815 100644 --- a/docs/src/algorithms.md +++ b/docs/src/algorithms.md @@ -57,6 +57,15 @@ GraphsOptim.min_cost_assignment! graph_matching ``` +## Coloring + +!!! danger "Work in progress" + Come back later! + +```@docs +minimum_coloring +``` + ## Utils ```@docs diff --git a/src/GraphsOptim.jl b/src/GraphsOptim.jl index 18f8d3f..4806db1 100644 --- a/src/GraphsOptim.jl +++ b/src/GraphsOptim.jl @@ -18,12 +18,11 @@ using LinearAlgebra: norm, tr, dot using MathOptInterface: OPTIMAL using SparseArrays: sparse using OptimalTransport: sinkhorn -using PicoSAT: solve export min_cost_flow export min_cost_assignment export FAQ, GOAT, graph_matching -export optimal_coloring +export minimum_coloring include("utils.jl") include("flow.jl") diff --git a/src/coloring.jl b/src/coloring.jl index 50cee4e..103329b 100644 --- a/src/coloring.jl +++ b/src/coloring.jl @@ -1,59 +1,41 @@ """ - optimal_coloring(g) + minimum_coloring( + g, max_nb_colors; + optimizer + ) -Finds a proper coloring using the minimum possible number of colors. Returns a -Graphs.Coloring object. Beware: this is an NP-complete problem and so runtime -can in the worst case increase exponentially in the size of the graph. +Finds a graph coloring using the smallest possible number of colors, assuming that it will not exceed `max_nb_colors`. + +Returns a vector of color indices. + +Beware: this is an NP-complete problem and so runtime can in the worst case increase exponentially in the size of the graph. """ -function optimal_coloring(g::AbstractGraph{T})::Coloring{T} where {T<:Integer} - # It is not clear whether maximum clique or maximal clique is faster. - # Maximum clique should generally make coloring faster but of course can - # itself be time consuming to compute. - max_clique = argmax(length, maximal_cliques(g)) - #max_clique = independent_set(complement(g), DegreeIndependentSet()) - #@show max_clique +function minimum_coloring( + g::AbstractGraph, max_nb_colors::Integer; optimizer=HiGHS.Optimizer +) + model = Model(optimizer) - for χ in length(max_clique):nv(g) - #println("Trying χ=$χ") - indexer = reshape(1:(nv(g) * χ), (nv(g), χ)) - cnf = Vector{Int64}[] + @variable(model, x[1:nv(g), 1:max_nb_colors], Bin) + @variable(model, y[1:max_nb_colors], Bin) - # Seeding the solution on a maximal clique makes it solve faster. - for (i, v) in enumerate(max_clique) - push!(cnf, [indexer[v, i]]) - end + @objective(model, Min, sum(y)) - for i in 1:nv(g) - push!(cnf, indexer[i, :]) - end + for v in 1:nv(g) + @constraint(model, sum(x[v, :]) == 1) + end + for v in 1:nv(g), c in 1:max_nb_colors + @constraint(model, x[v, c] <= y[c]) + end + for c in 1:max_nb_colors for e in edges(g) - if src(e) != dst(e) - for j in 1:χ - push!(cnf, [-indexer[src(e), j], -indexer[dst(e), j]]) - end - end - end - sol = solve(cnf) - if typeof(sol) == Symbol - if sol == :unsatisfiable - # continue - else - throw(ErrorException("PicoSAT.solve returned unrecognized result status")) - end - else - colormatrix = zeros(Bool, (nv(g), χ)) - # typeassert is needed to get JET.test_package to pass - colormatrix[filter(i -> i > 0, sol::AbstractArray)] .= true - colors = map(argmax, eachrow(colormatrix)) - @assert all(colors .>= 1) - @assert all(colors .<= χ) - for e in edges(g) - if src(e) != dst(e) - @assert colors[src(e)] != colors[dst(e)] - end - end - return Coloring{T}(χ, colors) + u, v = src(e), dst(e) + @constraint(model, x[u, c] + x[v, c] <= 1) end end - @assert false + + set_silent(model) + optimize!(model) + # return a vector of color indices + c = [argmax(value.(x[v, :])) for v in 1:nv(g)] + return c end diff --git a/test/coloring.jl b/test/coloring.jl index f395fd3..735989d 100644 --- a/test/coloring.jl +++ b/test/coloring.jl @@ -25,14 +25,14 @@ function kneser_graph(n::Integer, k::Integer) end # https://oeis.org/A088202 -@test optimal_coloring(queens_graph(1)).num_colors == 1 -@test optimal_coloring(queens_graph(2)).num_colors == 4 -@test optimal_coloring(queens_graph(3)).num_colors == 5 -@test optimal_coloring(queens_graph(4)).num_colors == 5 -@test optimal_coloring(queens_graph(5)).num_colors == 5 -@test optimal_coloring(queens_graph(6)).num_colors == 7 -@test optimal_coloring(queens_graph(7)).num_colors == 7 -# Runs in 52 seconds on i7-12800HX. -#@test optimal_coloring(queens_graph(8)).num_colors == 9 -@test optimal_coloring(kneser_graph(11, 4)).num_colors == 11 - 2 * 4 + 2 +nb_colors(c) = length(unique(c)) + +@test minimum_coloring(queens_graph(1), 10) |> nb_colors == 1 +@test minimum_coloring(queens_graph(2), 10) |> nb_colors == 4 +@test minimum_coloring(queens_graph(3), 10) |> nb_colors == 5 +@test minimum_coloring(queens_graph(4), 10) |> nb_colors == 5 +@test minimum_coloring(queens_graph(5), 10) |> nb_colors == 5 +@test minimum_coloring(queens_graph(6), 10) |> nb_colors == 7 + +@test minimum_coloring(kneser_graph(11, 4), 10) |> nb_colors == 11 - 2 * 4 + 2