Skip to content

Commit 496c4b6

Browse files
etiennedegclaudeKrastanov
authored
ReverseView and UndirectedView (#376)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Stefan Krastanov <github.acc@krastanov.org> Co-authored-by: Stefan Krastanov <stefan@krastanov.org>
1 parent 95fcf25 commit 496c4b6

File tree

6 files changed

+270
-0
lines changed

6 files changed

+270
-0
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pages_files = [
4444
"core_functions/persistence.md",
4545
"core_functions/simplegraphs_generators.md",
4646
"core_functions/simplegraphs.md",
47+
"core_functions/wrappedgraphs.md",
4748
],
4849
"Algorithms API" => [
4950
"algorithms/biconnectivity.md",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Graph views formats
2+
3+
*Graphs.jl* provides views around directed graphs.
4+
`ReverseGraph` is a graph view that wraps a directed graph and reverse the direction of every edge.
5+
`UndirectedGraph` is a graph view that wraps a directed graph and consider every edge as undirected.
6+
7+
## Index
8+
9+
```@index
10+
Pages = ["wrappedgraphs.md"]
11+
```
12+
13+
## Full docs
14+
15+
```@autodocs
16+
Modules = [Graphs]
17+
Pages = [
18+
"wrappedGraphs/graphviews.jl",
19+
]
20+
21+
```

src/Graphs.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ export
114114
squash,
115115
weights,
116116

117+
# wrapped graphs
118+
ReverseView,
119+
UndirectedView,
120+
wrapped_graph,
121+
117122
# simplegraphs
118123
add_edge!,
119124
add_vertex!,
@@ -470,6 +475,7 @@ include("utils.jl")
470475
include("deprecations.jl")
471476
include("core.jl")
472477

478+
include("wrappedGraphs/graphviews.jl")
473479
include("SimpleGraphs/SimpleGraphs.jl")
474480
using .SimpleGraphs
475481
"""

src/wrappedGraphs/graphviews.jl

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""
2+
ReverseView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T}
3+
4+
A graph view that wraps a directed graph and reverse the direction of every edge.
5+
6+
!!! warning
7+
Some properties of the view (e.g. the number of edges) are forwarded from the
8+
underlying graph and are not recomputed. Modifying the underlying graph after
9+
constructing the view may lead to incorrect results.
10+
11+
# Examples
12+
```jldoctest
13+
julia> using Graphs
14+
15+
julia> g = SimpleDiGraph(2);
16+
17+
julia> add_edge!(g, 1, 2);
18+
19+
julia> rg = ReverseView(g);
20+
21+
julia> neighbors(rg, 1)
22+
Int64[]
23+
24+
julia> neighbors(rg, 2)
25+
1-element Vector{Int64}:
26+
1
27+
```
28+
"""
29+
struct ReverseView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T}
30+
g::G
31+
32+
@traitfn ReverseView{T,G}(g::::(IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = new(
33+
g
34+
)
35+
@traitfn ReverseView{T,G}(g::::(!IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = throw(
36+
ArgumentError("Your graph needs to be directed")
37+
)
38+
end
39+
40+
ReverseView(g::G) where {T<:Integer,G<:AbstractGraph{T}} = ReverseView{T,G}(g)
41+
42+
wrapped_graph(g::ReverseView) = g.g
43+
44+
Graphs.is_directed(::ReverseView{T,G}) where {T,G} = true
45+
Graphs.is_directed(::Type{<:ReverseView{T,G}}) where {T,G} = true
46+
47+
Graphs.edgetype(g::ReverseView) = Graphs.edgetype(g.g)
48+
Graphs.has_vertex(g::ReverseView, v) = Graphs.has_vertex(g.g, v)
49+
Graphs.ne(g::ReverseView) = Graphs.ne(g.g)
50+
Graphs.nv(g::ReverseView) = Graphs.nv(g.g)
51+
Graphs.vertices(g::ReverseView) = Graphs.vertices(g.g)
52+
Graphs.edges(g::ReverseView) = (reverse(e) for e in Graphs.edges(g.g))
53+
Graphs.has_edge(g::ReverseView, s, d) = Graphs.has_edge(g.g, d, s)
54+
Graphs.inneighbors(g::ReverseView, v) = Graphs.outneighbors(g.g, v)
55+
Graphs.outneighbors(g::ReverseView, v) = Graphs.inneighbors(g.g, v)
56+
57+
"""
58+
UndirectedView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T}
59+
60+
A graph view that wraps a directed graph and consider every edge as undirected.
61+
62+
!!! warning
63+
Some properties of the view, such as the number of edges, are cached at
64+
construction time. Modifying the underlying graph after constructing the view
65+
will lead to incorrect results.
66+
67+
# Examples
68+
```jldoctest
69+
julia> using Graphs
70+
71+
julia> g = SimpleDiGraph(2);
72+
73+
julia> add_edge!(g, 1, 2);
74+
75+
julia> ug = UndirectedView(g);
76+
77+
julia> neighbors(ug, 1)
78+
1-element Vector{Int64}:
79+
2
80+
81+
julia> neighbors(ug, 2)
82+
1-element Vector{Int64}:
83+
1
84+
```
85+
"""
86+
struct UndirectedView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T}
87+
g::G
88+
ne::Int
89+
@traitfn function UndirectedView{T,G}(
90+
g::::(IsDirected)
91+
) where {T<:Integer,G<:AbstractGraph{T}}
92+
ne = count(e -> src(e) <= dst(e) || !has_edge(g, dst(e), src(e)), Graphs.edges(g))
93+
return new(g, ne)
94+
end
95+
96+
@traitfn UndirectedView{T,G}(g::::(!IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = throw(
97+
ArgumentError("Your graph needs to be directed")
98+
)
99+
end
100+
101+
UndirectedView(g::G) where {T<:Integer,G<:AbstractGraph{T}} = UndirectedView{T,G}(g)
102+
103+
"""
104+
wrapped_graph(g)
105+
106+
Return the graph wrapped by `g`
107+
"""
108+
function wrapped_graph end
109+
110+
wrapped_graph(g::UndirectedView) = g.g
111+
112+
Graphs.is_directed(::UndirectedView) = false
113+
Graphs.is_directed(::Type{<:UndirectedView}) = false
114+
115+
Graphs.edgetype(g::UndirectedView) = Graphs.edgetype(g.g)
116+
Graphs.has_vertex(g::UndirectedView, v) = Graphs.has_vertex(g.g, v)
117+
Graphs.ne(g::UndirectedView) = g.ne
118+
Graphs.nv(g::UndirectedView) = Graphs.nv(g.g)
119+
Graphs.vertices(g::UndirectedView) = Graphs.vertices(g.g)
120+
function Graphs.has_edge(g::UndirectedView, s, d)
121+
return Graphs.has_edge(g.g, s, d) || Graphs.has_edge(g.g, d, s)
122+
end
123+
Graphs.inneighbors(g::UndirectedView, v) = Graphs.all_neighbors(g.g, v)
124+
Graphs.outneighbors(g::UndirectedView, v) = Graphs.all_neighbors(g.g, v)
125+
function Graphs.edges(g::UndirectedView)
126+
return (
127+
begin
128+
(u, v) = src(e), dst(e)
129+
if (v < u)
130+
(u, v) = (v, u)
131+
end
132+
Edge(u, v)
133+
end for
134+
e in Graphs.edges(g.g) if (src(e) <= dst(e) || !has_edge(g.g, dst(e), src(e)))
135+
)
136+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ tests = [
8080
"interface",
8181
"core",
8282
"operators",
83+
"wrappedGraphs/graphviews",
8384
"degeneracy",
8485
"distance",
8586
"digraph/transitivity",

test/wrappedGraphs/graphviews.jl

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
@testset "Graph Views" begin
2+
@testset "ReverseView" begin
3+
gx = DiGraph([
4+
Edge(1, 1),
5+
Edge(1, 2),
6+
Edge(1, 4),
7+
Edge(2, 1),
8+
Edge(2, 2),
9+
Edge(2, 4),
10+
Edge(3, 1),
11+
Edge(4, 3),
12+
])
13+
14+
gr = erdos_renyi(20, 0.1; is_directed=true)
15+
16+
for g in hcat(test_generic_graphs(gx), test_generic_graphs(gr))
17+
rg = ReverseView(g)
18+
allocated_rg = DiGraph(nv(g))
19+
for e in edges(g)
20+
add_edge!(allocated_rg, Edge(dst(e), src(e)))
21+
end
22+
23+
@test wrapped_graph(rg) == g
24+
@test is_directed(rg) == true
25+
@test eltype(rg) == eltype(g)
26+
@test edgetype(rg) == edgetype(g)
27+
@test has_vertex(rg, 4) == has_vertex(g, 4)
28+
@test nv(rg) == nv(g) == nv(allocated_rg)
29+
@test ne(rg) == ne(g) == ne(allocated_rg)
30+
@test all(adjacency_matrix(rg) .== adjacency_matrix(allocated_rg))
31+
@test sort(collect(inneighbors(rg, 2))) ==
32+
sort(collect(inneighbors(allocated_rg, 2)))
33+
@test sort(collect(outneighbors(rg, 2))) ==
34+
sort(collect(outneighbors(allocated_rg, 2)))
35+
@test indegree(rg, 3) == indegree(allocated_rg, 3)
36+
@test degree(rg, 1) == degree(allocated_rg, 1)
37+
@test has_edge(rg, 1, 3) == has_edge(allocated_rg, 1, 3)
38+
@test has_edge(rg, 1, 4) == has_edge(allocated_rg, 1, 4)
39+
40+
rg_res = @inferred(floyd_warshall_shortest_paths(rg))
41+
allocated_rg_res = floyd_warshall_shortest_paths(allocated_rg)
42+
@test rg_res.dists == allocated_rg_res.dists # parents may not be the same
43+
44+
rg_res = @inferred(strongly_connected_components(rg))
45+
allocated_rg_res = strongly_connected_components(allocated_rg)
46+
@test length(rg_res) == length(allocated_rg_res)
47+
@test sort(length.(rg_res)) == sort(length.(allocated_rg_res))
48+
end
49+
50+
@test_throws ArgumentError ReverseView(path_graph(5))
51+
end
52+
53+
@testset "UndirectedView" begin
54+
gx = DiGraph([
55+
Edge(1, 1),
56+
Edge(1, 2),
57+
Edge(1, 4),
58+
Edge(2, 1),
59+
Edge(2, 2),
60+
Edge(2, 4),
61+
Edge(3, 1),
62+
Edge(4, 3),
63+
])
64+
65+
gr = erdos_renyi(20, 0.05; is_directed=true)
66+
67+
for g in test_generic_graphs(gx)
68+
ug = UndirectedView(g)
69+
@test ne(ug) == 7 # one less edge since there was two edges in reverse directions
70+
end
71+
72+
for g in hcat(test_generic_graphs(gx), test_generic_graphs(gr))
73+
ug = UndirectedView(g)
74+
allocated_ug = Graph(g)
75+
76+
@test wrapped_graph(ug) == g
77+
@test is_directed(ug) == false
78+
@test eltype(ug) == eltype(g)
79+
@test edgetype(ug) == edgetype(g)
80+
@test has_vertex(ug, 4) == has_vertex(g, 4)
81+
@test nv(ug) == nv(g) == nv(allocated_ug)
82+
@test ne(ug) == ne(allocated_ug)
83+
@test all(adjacency_matrix(ug) .== adjacency_matrix(allocated_ug))
84+
@test sort(collect(inneighbors(ug, 2))) ==
85+
sort(collect(inneighbors(allocated_ug, 2)))
86+
@test sort(collect(outneighbors(ug, 2))) ==
87+
sort(collect(outneighbors(allocated_ug, 2)))
88+
@test indegree(ug, 3) == indegree(allocated_ug, 3)
89+
@test degree(ug, 1) == degree(allocated_ug, 1)
90+
@test has_edge(ug, 1, 3) == has_edge(allocated_ug, 1, 3)
91+
@test has_edge(ug, 1, 4) == has_edge(allocated_ug, 1, 4)
92+
93+
ug_res = @inferred(floyd_warshall_shortest_paths(ug))
94+
allocated_ug_res = floyd_warshall_shortest_paths(allocated_ug)
95+
@test ug_res.dists == allocated_ug_res.dists # parents may not be the same
96+
97+
ug_res = @inferred(biconnected_components(ug))
98+
allocated_ug_res = biconnected_components(allocated_ug)
99+
@test length(ug_res) == length(allocated_ug_res)
100+
@test sort(length.(ug_res)) == sort(length.(allocated_ug_res))
101+
end
102+
103+
@test_throws ArgumentError UndirectedView(path_graph(5))
104+
end
105+
end

0 commit comments

Comments
 (0)