diff --git a/docs/make.jl b/docs/make.jl index 4a407f687..cf6e739ac 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -44,6 +44,7 @@ pages_files = [ "core_functions/persistence.md", "core_functions/simplegraphs_generators.md", "core_functions/simplegraphs.md", + "core_functions/wrappedgraphs.md", ], "Algorithms API" => [ "algorithms/biconnectivity.md", diff --git a/docs/src/core_functions/wrappedgraphs.md b/docs/src/core_functions/wrappedgraphs.md new file mode 100644 index 000000000..e2c6a31fe --- /dev/null +++ b/docs/src/core_functions/wrappedgraphs.md @@ -0,0 +1,21 @@ +# Graph views formats + +*Graphs.jl* provides views around directed graphs. +`ReverseGraph` is a graph view that wraps a directed graph and reverse the direction of every edge. +`UndirectedGraph` is a graph view that wraps a directed graph and consider every edge as undirected. + +## Index + +```@index +Pages = ["wrappedgraphs.md"] +``` + +## Full docs + +```@autodocs +Modules = [Graphs] +Pages = [ + "wrappedgraphs/graphviews.jl", +] + +``` \ No newline at end of file diff --git a/src/Graphs.jl b/src/Graphs.jl index f5c548b31..066aec806 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -121,6 +121,11 @@ export squash, weights, + # wrapped graphs + ReverseView, + UndirectedView, + wrapped_graph, + # simplegraphs add_edge!, add_vertex!, @@ -462,6 +467,7 @@ include("utils.jl") include("deprecations.jl") include("core.jl") +include("wrappedGraphs/graphviews.jl") include("SimpleGraphs/SimpleGraphs.jl") using .SimpleGraphs """ diff --git a/src/wrappedGraphs/graphviews.jl b/src/wrappedGraphs/graphviews.jl new file mode 100644 index 000000000..ea679814d --- /dev/null +++ b/src/wrappedGraphs/graphviews.jl @@ -0,0 +1,125 @@ +""" + ReverseView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + +A graph view that wraps a directed graph and reverse the direction of every edge. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = SimpleDiGraph(2); + +julia> add_edge!(g, 1, 2); + +julia> rg = ReverseView(g); + +julia> neighbors(rg, 1) +Int64[] + +julia> neighbors(rg, 2) +1-element Vector{Int64}: + 1 +``` +""" +struct ReverseView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + g::G + + @traitfn ReverseView{T,G}(g::::(IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = + new(g) + @traitfn ReverseView{T,G}(g::::(!IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = + throw(ArgumentError("Your graph needs to be directed")) +end + +ReverseView(g::G) where {T<:Integer,G<:AbstractGraph{T}} = ReverseView{T,G}(g) + +wrapped_graph(g::ReverseView) = g.g + +Graphs.is_directed(::ReverseView{T,G}) where {T,G} = true +Graphs.is_directed(::Type{<:ReverseView{T,G}}) where {T,G} = true + +Graphs.edgetype(g::ReverseView) = Graphs.edgetype(g.g) +Graphs.has_vertex(g::ReverseView, v) = Graphs.has_vertex(g.g, v) +Graphs.ne(g::ReverseView) = Graphs.ne(g.g) +Graphs.nv(g::ReverseView) = Graphs.nv(g.g) +Graphs.vertices(g::ReverseView) = Graphs.vertices(g.g) +Graphs.edges(g::ReverseView) = (reverse(e) for e in Graphs.edges(g.g)) +Graphs.has_edge(g::ReverseView, s, d) = Graphs.has_edge(g.g, d, s) +Graphs.inneighbors(g::ReverseView, v) = Graphs.outneighbors(g.g, v) +Graphs.outneighbors(g::ReverseView, v) = Graphs.inneighbors(g.g, v) + +""" + UndirectedView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + +A graph view that wraps a directed graph and consider every edge as undirected. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = SimpleDiGraph(2); + +julia> add_edge!(g, 1, 2); + +julia> ug = UndirectedView(g); + +julia> neighbors(ug, 1) +1-element Vector{Int64}: + 2 + +julia> neighbors(ug, 2) +1-element Vector{Int64}: + 1 +``` +""" +struct UndirectedView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + g::G + ne::Int + @traitfn function UndirectedView{T,G}( + g::::(IsDirected) + ) where {T<:Integer,G<:AbstractGraph{T}} + ne = count(e -> src(e) <= dst(e) || !has_edge(g, dst(e), src(e)), Graphs.edges(g)) + return new(g, ne) + end + + @traitfn UndirectedView{T,G}( + g::::(!IsDirected) + ) where {T<:Integer,G<:AbstractGraph{T}} = + throw(ArgumentError("Your graph needs to be directed")) +end + +UndirectedView(g::G) where {T<:Integer,G<:AbstractGraph{T}} = UndirectedView{T,G}(g) + +""" + wrapped_graph(g) + +Return the graph wrapped by `g` +""" +function wrapped_graph end + +wrapped_graph(g::UndirectedView) = g.g + +Graphs.is_directed(::UndirectedView) = false +Graphs.is_directed(::Type{<:UndirectedView}) = false + +Graphs.edgetype(g::UndirectedView) = Graphs.edgetype(g.g) +Graphs.has_vertex(g::UndirectedView, v) = Graphs.has_vertex(g.g, v) +Graphs.ne(g::UndirectedView) = g.ne +Graphs.nv(g::UndirectedView) = Graphs.nv(g.g) +Graphs.vertices(g::UndirectedView) = Graphs.vertices(g.g) +function Graphs.has_edge(g::UndirectedView, s, d) + return Graphs.has_edge(g.g, s, d) || Graphs.has_edge(g.g, d, s) +end +Graphs.inneighbors(g::UndirectedView, v) = Graphs.all_neighbors(g.g, v) +Graphs.outneighbors(g::UndirectedView, v) = Graphs.all_neighbors(g.g, v) +function Graphs.edges(g::UndirectedView) + return ( + begin + (u, v) = src(e), dst(e) + if (v < u) + (u, v) = (v, u) + end + Edge(u, v) + end for + e in Graphs.edges(g.g) if (src(e) <= dst(e) || !has_edge(g.g, dst(e), src(e))) + ) +end diff --git a/test/runtests.jl b/test/runtests.jl index 1764fe537..ccd97cb96 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -79,6 +79,7 @@ tests = [ "interface", "core", "operators", + "wrappedGraphs/graphviews", "degeneracy", "distance", "digraph/transitivity", diff --git a/test/wrappedGraphs/graphviews.jl b/test/wrappedGraphs/graphviews.jl new file mode 100644 index 000000000..d03b2ec8a --- /dev/null +++ b/test/wrappedGraphs/graphviews.jl @@ -0,0 +1,105 @@ +@testset "Graph Views" begin + @testset "ReverseView" begin + gx = DiGraph([ + Edge(1, 1), + Edge(1, 2), + Edge(1, 4), + Edge(2, 1), + Edge(2, 2), + Edge(2, 4), + Edge(3, 1), + Edge(4, 3), + ]) + + gr = erdos_renyi(20, 0.1; is_directed=true) + + for g in hcat(test_generic_graphs(gx), test_generic_graphs(gr)) + rg = ReverseView(g) + allocated_rg = DiGraph(nv(g)) + for e in edges(g) + add_edge!(allocated_rg, Edge(dst(e), src(e))) + end + + @test wrapped_graph(rg) == g + @test is_directed(rg) == true + @test eltype(rg) == eltype(g) + @test edgetype(rg) == edgetype(g) + @test has_vertex(rg, 4) == has_vertex(g, 4) + @test nv(rg) == nv(g) == nv(allocated_rg) + @test ne(rg) == ne(g) == ne(allocated_rg) + @test all(adjacency_matrix(rg) .== adjacency_matrix(allocated_rg)) + @test sort(collect(inneighbors(rg, 2))) == + sort(collect(inneighbors(allocated_rg, 2))) + @test sort(collect(outneighbors(rg, 2))) == + sort(collect(outneighbors(allocated_rg, 2))) + @test indegree(rg, 3) == indegree(allocated_rg, 3) + @test degree(rg, 1) == degree(allocated_rg, 1) + @test has_edge(rg, 1, 3) == has_edge(allocated_rg, 1, 3) + @test has_edge(rg, 1, 4) == has_edge(allocated_rg, 1, 4) + + rg_res = @inferred(floyd_warshall_shortest_paths(rg)) + allocated_rg_res = floyd_warshall_shortest_paths(allocated_rg) + @test rg_res.dists == allocated_rg_res.dists # parents may not be the same + + rg_res = @inferred(strongly_connected_components(rg)) + allocated_rg_res = strongly_connected_components(allocated_rg) + @test length(rg_res) == length(allocated_rg_res) + @test sort(length.(rg_res)) == sort(length.(allocated_rg_res)) + end + + @test_throws ArgumentError ReverseView(path_graph(5)) + end + + @testset "UndirectedView" begin + gx = DiGraph([ + Edge(1, 1), + Edge(1, 2), + Edge(1, 4), + Edge(2, 1), + Edge(2, 2), + Edge(2, 4), + Edge(3, 1), + Edge(4, 3), + ]) + + gr = erdos_renyi(20, 0.05; is_directed=true) + + for g in test_generic_graphs(gx) + ug = UndirectedView(g) + @test ne(ug) == 7 # one less edge since there was two edges in reverse directions + end + + for g in hcat(test_generic_graphs(gx), test_generic_graphs(gr)) + ug = UndirectedView(g) + allocated_ug = Graph(g) + + @test wrapped_graph(ug) == g + @test is_directed(ug) == false + @test eltype(ug) == eltype(g) + @test edgetype(ug) == edgetype(g) + @test has_vertex(ug, 4) == has_vertex(g, 4) + @test nv(ug) == nv(g) == nv(allocated_ug) + @test ne(ug) == ne(allocated_ug) + @test all(adjacency_matrix(ug) .== adjacency_matrix(allocated_ug)) + @test sort(collect(inneighbors(ug, 2))) == + sort(collect(inneighbors(allocated_ug, 2))) + @test sort(collect(outneighbors(ug, 2))) == + sort(collect(outneighbors(allocated_ug, 2))) + @test indegree(ug, 3) == indegree(allocated_ug, 3) + @test degree(ug, 1) == degree(allocated_ug, 1) + @test has_edge(ug, 1, 3) == has_edge(allocated_ug, 1, 3) + @test has_edge(ug, 1, 4) == has_edge(allocated_ug, 1, 4) + + ug_res = @inferred(floyd_warshall_shortest_paths(ug)) + allocated_ug_res = floyd_warshall_shortest_paths(allocated_ug) + @test ug_res.dists == allocated_ug_res.dists # parents may not be the same + + ug_res = @inferred(biconnected_components(ug)) + allocated_ug_res = biconnected_components(allocated_ug) + @test length(ug_res) == length(allocated_ug_res) + @test sort(length.(ug_res)) == sort(length.(allocated_ug_res)) + end + + @test_throws ArgumentError UndirectedView(path_graph(5)) + end +end