diff --git a/Project.toml b/Project.toml index 1f44e54c..a5832641 100644 --- a/Project.toml +++ b/Project.toml @@ -15,6 +15,12 @@ SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +[weakdeps] +NautyGraphs = "7509a0a4-015a-4167-b44b-0799a1a2605e" + +[extensions] +NautyGraphsExt = "NautyGraphs" + [compat] Aqua = "0.6" ArnoldiMethod = "0.4" @@ -23,6 +29,7 @@ DataStructures = "0.17, 0.18" Documenter = "0.27" Inflate = "0.1.3" JuliaFormatter = "1" +NautyGraphs = "0.5.0" SimpleTraits = "0.9" StableRNGs = "1" Statistics = "1" @@ -36,6 +43,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +NautyGraphs = "7509a0a4-015a-4167-b44b-0799a1a2605e" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" @@ -45,4 +53,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [targets] -test = ["Aqua", "Base64", "DelimitedFiles", "Documenter", "JET", "JuliaFormatter", "LinearAlgebra", "Pkg", "Random", "SparseArrays", "StableRNGs", "Statistics", "Test", "Unitful"] +test = ["Aqua", "Base64", "DelimitedFiles", "Documenter", "JET", "JuliaFormatter", "LinearAlgebra", "NautyGraphs", "Pkg", "Random", "SparseArrays", "StableRNGs", "Statistics", "Test", "Unitful"] diff --git a/ext/NautyGraphsExt.jl b/ext/NautyGraphsExt.jl new file mode 100644 index 00000000..2a2893d8 --- /dev/null +++ b/ext/NautyGraphsExt.jl @@ -0,0 +1,137 @@ +module NautyGraphsExt + +using Graphs, NautyGraphs +using Graphs.Experimental: AlgNautyGraphs + +function Graphs.Experimental.has_induced_subgraphisomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Bool + error( + "Induced subgraph isomorphims are currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +function Graphs.Experimental.has_subgraphisomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Bool + error( + "Subgraph isomorphims are currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +function Graphs.Experimental.has_isomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Bool + if !isnothing(edge_relation) + error( + "Edge relations are currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + end + if !isnothing(vertex_relation) + error( + "Vertex relations are currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + end + return NautyGraph(g1) ≃ NautyGraph(g2) +end + +function Graphs.Experimental.canonize!(g::AbstractGraph, ::AlgNautyGraphs) + ng = is_directed(g) ? NautyDiGraph(g) : NautyGraph(g) + perm = convert(Vector{eltype(g)}, NautyGraphs.canonical_permutation(ng)) + permute!(g, perm) + return perm +end + +function Graphs.Experimental.count_induced_subgraphisomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Int + error( + "Counting induced subgraph isomorphims is currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +function Graphs.Experimental.count_subgraphisomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Int + error( + "Counting subgraph isomorphims is currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +function Graphs.Experimental.count_isomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Int + error( + "Counting isomorphims is currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +function Graphs.Experimental.all_induced_subgraphisomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Channel{Vector{Tuple{eltype(g1),eltype(g2)}}} + error( + "Generating all induced subgraph isomorphims is currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +function Graphs.Experimental.all_subgraphisomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Channel{Vector{Tuple{eltype(g1),eltype(g2)}}} + error( + "Generating all subgraph isomorphims is currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +function Graphs.Experimental.all_isomorph( + g1::AbstractGraph, + g2::AbstractGraph, + ::AlgNautyGraphs; + vertex_relation::Union{Nothing,Function}=nothing, + edge_relation::Union{Nothing,Function}=nothing, +)::Channel{Vector{Tuple{eltype(g1),eltype(g2)}}} + error( + "Generating all isomorphims is currently not supported by `NautyGraphs`. Please use a different isomorphism algorithm.", + ) + return nothing +end + +end diff --git a/src/Experimental/Experimental.jl b/src/Experimental/Experimental.jl index 9506e47e..5b20fea2 100644 --- a/src/Experimental/Experimental.jl +++ b/src/Experimental/Experimental.jl @@ -7,6 +7,7 @@ export description, # isomorphism VF2, vf2, + AlgNautyGraphs, IsomorphismProblem, SubGraphIsomorphismProblem, InducedSubGraphIsomorphismProblem, @@ -14,6 +15,7 @@ export description, has_isomorph, all_isomorph, count_isomorph, + canonize!, has_induced_subgraphisomorph, count_induced_subgraphisomorph, all_induced_subgraphisomorph, @@ -25,6 +27,7 @@ description() = "This module contains experimental graph functions." include("isomorphism.jl") include("vf2.jl") # Julian implementation of VF2 algorithm +include("nautygraphs.jl") include("Parallel/Parallel.jl") include("Traversals/Traversals.jl") include("ShortestPaths/ShortestPaths.jl") diff --git a/src/Experimental/isomorphism.jl b/src/Experimental/isomorphism.jl index 970e5943..7e5b9845 100644 --- a/src/Experimental/isomorphism.jl +++ b/src/Experimental/isomorphism.jl @@ -87,9 +87,7 @@ function has_induced_subgraphisomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Bool - return has_induced_subgraphisomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(has_induced_subgraphisomorph, (g1, g2, alg))) end """ @@ -130,9 +128,7 @@ function has_subgraphisomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Bool - return has_subgraphisomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(has_subgraphisomorph, (g1, g2, alg))) end """ @@ -141,12 +137,14 @@ end Return `true` if the graph `g1` is isomorphic to `g2`. ### Optional Arguments -- `alg`: The algorithm that is used to find the induced subgraph isomorphism. Can be only - `VF2()` at the moment. +- `alg`: The algorithm that is used to find the induced subgraph isomorphism. Can be + `VF2()` or `AlgNautyGraphs()`, if `NautyGraphs` is installed and imported. - `vertex_relation`: A binary function that takes a vertex from `g1` and one from `g2`. An - isomorphism only exists if this function returns `true` for all matched vertices. + isomorphism only exists if this function returns `true` for all matched vertices. Only + works with `VF2()` at the moment. - `edge_relation`: A binary function that takes an edge from `g1` and one from `g2`. An - isomorphism only exists if this function returns `true` for all matched edges. + isomorphism only exists if this function returns `true` for all matched edges. Only + works with `VF2()` at the moment. ### Examples ```doctest.jl @@ -173,9 +171,40 @@ function has_isomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Bool - return has_isomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(has_isomorph, (g1, g2, alg))) +end + +""" + canonize!(g, alg::IsomorphismAlgorithm=AlgNautyGraphs()) + +Permute the vertices of graph `g` into the canonical order defined by the algorithm `alg` and return the permutation. +If graphs `g1` and `g2` are isomorphic, the orders of their vertices will be equal after canonizing them with the same algorithm. + +### Optional Arguments +- `alg`: The algorithm that is used to canonize the graph. Can be only be `AlgNautyGraphs()` + at this moment, which requires `NautyGraphs` to be installed and imported. + +### Examples +```doctest.jl +julia> canonize!(path_graph(3)) +[1, 3, 2] + +julia> g1 = path_digraph(4) +julia> g2 = path_digraph(4)[[2, 3, 1, 4]] +julia> g1 == g2 +false +julia> canonize!(g1) +[4, 2, 3, 1] +julia> canonize!(g2) +[4, 1, 2, 3] +julia> g1 == g2 +true +``` +### See also +[`has_isomorph`](@ref) +""" +function canonize!(g::AbstractGraph, alg::IsomorphismAlgorithm=AlgNautyGraphs()) + throw(MethodError(canonize!, (g, alg))) end """ @@ -214,9 +243,7 @@ function count_induced_subgraphisomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Int - return count_induced_subgraphisomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(count_induced_subgraphisomorph, (g1, g2, alg))) end """ @@ -257,13 +284,7 @@ function count_subgraphisomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Int - return count_subgraphisomorph( - g1::AbstractGraph, - g2::AbstractGraph, - VF2(); - vertex_relation=vertex_relation, - edge_relation=edge_relation, - ) + throw(MethodError(count_subgraphisomorph, (g1, g2, alg))) end """ @@ -304,9 +325,7 @@ function count_isomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Int - return count_isomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(count_isomorph, (g1, g2, alg))) end """ @@ -353,9 +372,7 @@ function all_induced_subgraphisomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Channel{Vector{Tuple{eltype(g1),eltype(g2)}}} - return all_induced_subgraphisomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(all_induced_subgraphisomorph, (g1, g2, alg))) end """ @@ -404,9 +421,7 @@ function all_subgraphisomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Channel{Vector{Tuple{eltype(g1),eltype(g2)}}} - return all_subgraphisomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(all_subgraphisomorph, (g1, g2, alg))) end """ @@ -458,7 +473,5 @@ function all_isomorph( vertex_relation::Union{Nothing,Function}=nothing, edge_relation::Union{Nothing,Function}=nothing, )::Channel{Vector{Tuple{eltype(g1),eltype(g2)}}} - return all_isomorph( - g1, g2, alg; vertex_relation=vertex_relation, edge_relation=edge_relation - ) + throw(MethodError(all_isomorph, (g1, g2, alg))) end diff --git a/src/Experimental/nautygraphs.jl b/src/Experimental/nautygraphs.jl new file mode 100644 index 00000000..c08d8e43 --- /dev/null +++ b/src/Experimental/nautygraphs.jl @@ -0,0 +1,8 @@ +""" + AlgNautyGraphs + +An empty concrete type used to dispatch to [`NautyGraphs`](@ref) isomorphism functions. +""" +struct AlgNautyGraphs <: IsomorphismAlgorithm end + +# The implementation of NautyGraph methods for graph isomorphism is done as a package extension in /ext/NautyGraphsExt.jl diff --git a/src/Graphs.jl b/src/Graphs.jl index 86bc0946..fae137b3 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -52,6 +52,7 @@ import Base: intersect, reverse, reverse!, + permute!, isassigned, getindex, setindex!, @@ -155,6 +156,7 @@ export complement, reverse, reverse!, + permute!, blockdiag, union, intersect, @@ -488,6 +490,36 @@ a `Graph` or `DiGraph`. """ const Edge = Graphs.SimpleGraphs.SimpleEdge +function __init__() + if isdefined(Base.Experimental, :register_error_hint) + Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs + if exc.f == Graphs.Experimental.canonize! + printstyled(io, "\nHint: "; color=:yellow, bold=true) + print( + io, + "Canonization algorithms are implemented in dependencies like NautyGraphs.jl. Please make sure you first install and import `NautyGraphs`.", + ) + elseif exc.f ∈ ( + Graphs.Experimental.has_induced_subgraphisomorph, + Graphs.Experimental.has_subgraphisomorph, + Graphs.Experimental.has_isomorph, + Graphs.Experimental.count_induced_subgraphisomorph, + Graphs.Experimental.count_subgraphisomorph, + Graphs.Experimental.count_isomorph, + Graphs.Experimental.all_induced_subgraphisomorph, + Graphs.Experimental.all_subgraphisomorph, + Graphs.Experimental.all_isomorph, + ) && argtypes[3] == Graphs.Experimental.AlgNautyGraphs + printstyled(io, "\nHint: "; color=:yellow, bold=true) + print( + io, + "Using `AlgNautyGraphs` for isomorphism checking requires NautyGraphs.jl. Please make sure you first install and import `NautyGraphs`.", + ) + end + end + end +end + include("degeneracy.jl") include("digraph/transitivity.jl") include("cycles/johnson.jl") diff --git a/src/operators.jl b/src/operators.jl index d8aeb217..61876628 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -104,6 +104,18 @@ function reverse! end return g end +""" + permute!(g, p) + +Permute the vertices of graph `g` in-place, according to permutation `p`. No checking is done to verify that p is a permutation. +""" +function permute! end +function permute!(g::AbstractSimpleGraph, p::AbstractVector) + permute_adjlist!(g.fadjlist, p) + is_directed(g) && permute_adjlist!(g.badjlist, p) + return g +end + """ blockdiag(g, h) diff --git a/src/utils.jl b/src/utils.jl index f87638ca..3a9f9fe0 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -300,3 +300,20 @@ end collect_if_not_vector(xs::AbstractVector) = xs collect_if_not_vector(xs) = collect(xs) + +""" + permute_adjlist!(adjlist::Vector{Vector{<:Integer}}, p::AbstractVector) + +Internal utility function to permute adjacency lists. No checking is done to verify that p is a permutation. +""" +function permute_adjlist!(adjlist::Vector{<:Vector{<:Integer}}, p::AbstractVector) + pinv = invperm(p) + relabel(i) = pinv[i] + + for adj in adjlist + adj .= relabel.(adj) + sort!(adj) + end + permute!(adjlist, p) + return nothing +end diff --git a/test/ext/nautygraphs/nautygraphs.jl b/test/ext/nautygraphs/nautygraphs.jl new file mode 100644 index 00000000..bb2d3367 --- /dev/null +++ b/test/ext/nautygraphs/nautygraphs.jl @@ -0,0 +1,49 @@ +using Graphs.Experimental + +@testset "nautgraphs" begin + g = Graph(1) + + if VERSION >= v"1.8" + @test_throws "Canonization algorithms" canonize!(g) + @test_throws "Using `AlgNautyGraphs`" has_induced_subgraphisomorph( + g, g, AlgNautyGraphs() + ) + @test_throws "Using `AlgNautyGraphs`" has_subgraphisomorph(g, g, AlgNautyGraphs()) + @test_throws "Using `AlgNautyGraphs`" has_isomorph(g, g, AlgNautyGraphs()) + @test_throws "Using `AlgNautyGraphs`" count_induced_subgraphisomorph( + g, g, AlgNautyGraphs() + ) + @test_throws "Using `AlgNautyGraphs`" count_subgraphisomorph(g, g, AlgNautyGraphs()) + @test_throws "Using `AlgNautyGraphs`" count_isomorph(g, g, AlgNautyGraphs()) + @test_throws "Using `AlgNautyGraphs`" all_induced_subgraphisomorph( + g, g, AlgNautyGraphs() + ) + @test_throws "Using `AlgNautyGraphs`" all_subgraphisomorph(g, g, AlgNautyGraphs()) + @test_throws "Using `AlgNautyGraphs`" all_isomorph(g, g, AlgNautyGraphs()) + end + + using NautyGraphs + + g1 = path_graph(5) + g2 = path_graph(5) + + permute!(g2, [5, 3, 1, 2, 4]) + + @test has_isomorph(g1, g2, AlgNautyGraphs()) + + canonize!(g1, AlgNautyGraphs()) + canonize!(g2, AlgNautyGraphs()) + @test g1 == g2 + + g3 = star_graph(5) + g4 = path_graph(4) + + @test !has_isomorph(g1, g3, AlgNautyGraphs()) + @test !has_isomorph(g1, g4, AlgNautyGraphs()) + + canonize!(g3, AlgNautyGraphs()) + canonize!(g4, AlgNautyGraphs()) + + @test g1 != g3 + @test g1 != g3 +end diff --git a/test/operators.jl b/test/operators.jl index bf4931eb..8d50d7ea 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -160,6 +160,17 @@ @test g == DiGraph{T}(g4) end + gp = SimpleDiGraph(20, 50; rng=rng) + @testset "Permute $g" for g in testlargegraphs(gp) + for _ in 1:10 + perm = randperm(rng, nv(g)) + h = copy(g) + permute!(g, perm) + + @test g == h[perm] + end + end + gx = complete_graph(2) @testset "Blockdiag $g" for g in testlargegraphs(gx) diff --git a/test/runtests.jl b/test/runtests.jl index bf54bc62..bb0188a4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -91,6 +91,7 @@ tests = [ "cycles/limited_length", "cycles/incremental", "edit_distance", + "ext/nautygraphs/nautygraphs", "connectivity", "persistence/persistence", "shortestpaths/utils",