diff --git a/Project.toml b/Project.toml index c2e2a3e..d2b6db5 100644 --- a/Project.toml +++ b/Project.toml @@ -30,6 +30,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 +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"] +test = ["Aqua", "Documenter", "Graphs", "HiGHS", "IterTools", "JET", "JuMP", "JuliaFormatter", "LinearAlgebra", "Test", "SparseArrays"] diff --git a/docs/src/algorithms.md b/docs/src/algorithms.md index f99b801..657b753 100644 --- a/docs/src/algorithms.md +++ b/docs/src/algorithms.md @@ -71,6 +71,13 @@ graph_matching GraphsOptim.graph_matching_step_size ``` +## Coloring + +```@docs +fractional_chromatic_number +fractional_clique_number +``` + ## Utils ```@docs @@ -80,4 +87,5 @@ GraphsOptim.is_stochastic GraphsOptim.is_doubly_stochastic GraphsOptim.is_permutation_matrix GraphsOptim.flat_doubly_stochastic +GraphsOptim.indvec ``` diff --git a/src/GraphsOptim.jl b/src/GraphsOptim.jl index 40c82d3..e514036 100644 --- a/src/GraphsOptim.jl +++ b/src/GraphsOptim.jl @@ -7,11 +7,13 @@ module GraphsOptim using Graphs: AbstractGraph, is_directed using Graphs: vertices, edges, nv, ne, src, dst, inneighbors, outneighbors +using Graphs: complement, maximal_cliques using FillArrays: Zeros, Ones, Fill using HiGHS: HiGHS using JuMP: Model, AffExpr using JuMP: objective_function, add_to_expression! using JuMP: set_silent, optimize!, termination_status, value +using JuMP: set_optimizer, objective_value using JuMP: @variable, @constraint, @objective using LinearAlgebra: norm, tr, dot using MathOptInterface: OPTIMAL @@ -21,12 +23,14 @@ using OptimalTransport: sinkhorn export min_cost_flow export min_cost_assignment export FAQ, GOAT, graph_matching +export fractional_chromatic_number, fractional_clique_number export shortest_path include("utils.jl") include("flow.jl") include("assignment.jl") include("graph_matching.jl") +include("fractional_coloring.jl") include("shortest_path.jl") end diff --git a/src/fractional_coloring.jl b/src/fractional_coloring.jl new file mode 100644 index 0000000..1cfa04a --- /dev/null +++ b/src/fractional_coloring.jl @@ -0,0 +1,66 @@ +""" + fractional_chromatic_number(g; optimizer) + +Compute the fractional chromatic number of a graph. Gives the same result as +`fractional_clique_number`, though one function may run faster than the other. +Beware: this can run very slowly for graphs of any substantial size. + +# Keyword arguments + +- `optimizer`: JuMP-compatible solver (default is `HiGHS.Optimizer`) + +# References + +- https://mathworld.wolfram.com/FractionalChromaticNumber.html +""" +function fractional_chromatic_number( + g::AbstractGraph{T}, optimizer=HiGHS.Optimizer +) where {T<:Integer} + if is_directed(g) + throw(ArgumentError("The graph must not be directed")) + end + + ss = maximal_cliques(complement(g)) + M = hcat(indvec.(ss, nv(g))...) + + model = Model(optimizer) + set_silent(model) + @variable(model, x[1:length(ss)] >= 0) + @constraint(model, M * x .>= 1) + @objective(model, Min, sum(x)) + optimize!(model) + return objective_value(model) +end + +""" + fractional_clique_number(g; optimizer) + +Compute the fractional clique number of a graph. Gives the same result as +`fractional_chromatic_number`, though one function may run faster than the other. +Beware: this can run very slowly for graphs of any substantial size. + +# Keyword arguments + +- `optimizer`: JuMP-compatible solver (default is `HiGHS.Optimizer`) + +# References + +- https://mathworld.wolfram.com/FractionalCliqueNumber.html +""" +function fractional_clique_number( + g::AbstractGraph{T}, optimizer=HiGHS.Optimizer +) where {T<:Integer} + if is_directed(g) + throw(ArgumentError("The graph must not be directed")) + end + + model = Model(optimizer) + set_silent(model) + @variable(model, x[1:nv(g)] >= 0) + for clique in maximal_cliques(complement(g)) + @constraint(model, sum(x[clique]) <= 1) + end + @objective(model, Max, sum(x)) + optimize!(model) + return objective_value(model) +end diff --git a/src/utils.jl b/src/utils.jl index d3c7cb8..9a7f608 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -48,3 +48,14 @@ end Return the barycenter of doubly stochastic matrices `J = ๐Ÿ * ๐Ÿแต€ / n`. """ flat_doubly_stochastic(n::Integer) = ones(n) * ones(n)' / n + +""" + indvec(s, n) + +Return a vector of length `n` with ones at indices specified by `s`. +""" +function indvec(s::AbstractVector, n::Integer) + x = zeros(n) + x[s] .= 1 + return x +end diff --git a/test/fractional_coloring.jl b/test/fractional_coloring.jl new file mode 100644 index 0000000..3495bbc --- /dev/null +++ b/test/fractional_coloring.jl @@ -0,0 +1,12 @@ +using Graphs +using GraphsOptim +using IterTools +using Test + +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 + +@test fractional_chromatic_number(kneser_graph(8, 3)) โ‰ˆ 8 / 3 +@test fractional_clique_number(kneser_graph(8, 3)) โ‰ˆ 8 / 3 diff --git a/test/runtests.jl b/test/runtests.jl index 04c8a2c..adfb030 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -40,6 +40,10 @@ using Test include("graph_matching.jl") end + @testset verbose = true "Fractional coloring" begin + include("fractional_coloring.jl") + end + @testset verbose = true "Shortest path" begin include("shortest_path.jl") end