Skip to content

Add/line graph #440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ benchmark/Manifest.toml
/docs/src/index.md
/docs/src/contributing.md
/docs/src/license.md
.aider*
1 change: 1 addition & 0 deletions src/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export
egonet,
merge_vertices!,
merge_vertices,
line_graph,

# bfs
gdistances,
Expand Down
85 changes: 85 additions & 0 deletions src/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -879,3 +879,88 @@ function merge_vertices!(g::Graph{T}, vs::Vector{U} where {U<:Integer}) where {T

return new_vertex_ids
end

"""
line_graph(g::SimpleGraph) ::SimpleGraph
Given a graph `g`, return the graph `lg`, whose vertices are integers that enumerate the
edges in `g`, and two vertices in `lg` form an edge iff the corresponding edges in `g`
share a common endpoint. In other words, edges in `lg` are length-2 paths in `g`.
Note that `i ∈ vertices(lg)` corresponds to `collect(edges(g))[i]`.
# Examples
```jldoctest
julia> using Graphs
julia> g = path_graph(5);
julia> lg = line_graph(g)
{4, 3} undirected simple Int64 graph
```
"""
function line_graph(g::SimpleGraph)
vertex_to_edges = [Int[] for _ in 1:nv(g)]
for (i, e) in enumerate(edges(g))
s, d = src(e), dst(e)
push!(vertex_to_edges[s], i)
s == d && continue # do not push self-loops twice
push!(vertex_to_edges[d], i)
end

fadjlist = [Int[] for _ in 1:ne(g)] # edge to neighbors adjacency in lg
m = 0 # number of edges in the line-graph
for es in vertex_to_edges
n = length(es)
for i in 1:(n - 1), j in (i + 1):n # iterate through pairs of edges with same endpoint
ei, ej = es[i], es[j]
m += 1
push!(fadjlist[ei], ej)
push!(fadjlist[ej], ei)
end
end

foreach(sort!, fadjlist)
return SimpleGraph(m, fadjlist)
end

"""
line_graph(g::SimpleDiGraph) ::SimpleDiGraph
Given a digraph `g`, return the digraph `lg`, whose vertices are integers that enumerate
the edges in `g`, and there is an edge in `lg` from `Edge(a,b)` to `Edge(c,d)` iff b==c.
In other words, edges in `lg` are length-2 directed paths in `g`.
Note that `i ∈ vertices(lg)` corresponds to `collect(edges(g))[i]`.
# Examples
```jldoctest
julia> using Graphs
julia> g = cycle_digraph(5);
julia> lg = line_graph(g)
{5, 5} directed simple Int64 graph
```
"""
function line_graph(g::SimpleDiGraph)
vertex_to_edgesout = [Int[] for _ in 1:nv(g)]
vertex_to_edgesin = [Int[] for _ in 1:nv(g)]
for (i, e) in enumerate(edges(g))
s, d = src(e), dst(e)
push!(vertex_to_edgesout[s], i)
push!(vertex_to_edgesin[d], i)
end

fadjilist = [Int[] for _ in 1:ne(g)] # edge to neighbors forward adjacency in lg
badjilist = [Int[] for _ in 1:ne(g)] # edge to neighbors backward adjacency in lg
m = 0 # number of edges in the line-graph
for (e_i, e_o) in zip(vertex_to_edgesin, vertex_to_edgesout)
for ei in e_i, eo in e_o # iterate through length-2 directed paths
ei == eo && continue # a self-loop in g does not induce a self-loop in lg
m += 1
push!(fadjilist[ei], eo)
push!(badjilist[eo], ei)
end
end

foreach(sort!, fadjilist)
foreach(sort!, badjilist)
return SimpleDiGraph(m, fadjilist, badjilist)
end
98 changes: 98 additions & 0 deletions test/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,102 @@
@testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100))
@test length(g) == 10000
end

@testset "Undirected Line Graph" begin
@testset "Undirected Cycle Graphs" begin
for n in 3:9
g = cycle_graph(n)
lg = line_graph(g) # checking if lg is an n-cycle
@test nv(lg) == n
@test ne(lg) == n
@test is_connected(lg)
@test all(degree(lg, v) == 2 for v in vertices(lg))
end
end

@testset "Undirected Path Graphs" begin
for n in 2:9
g = path_graph(n)
lg = line_graph(g) # checking if lg is an n-1-path
@test nv(lg) == n - 1
@test ne(lg) == n - 2
@test is_connected(lg)
@test all(degree(lg, v) <= 2 for v in vertices(lg))
@test any(degree(lg, v) == 1 for v in vertices(lg)) || n == 2 && ne(lg) == 0
end
end

@testset "Undirected Star Graphs" begin
for n in 3:9
g = star_graph(n)
lg = line_graph(g) # checking if lg is a complete graph on n-1 vertices
@test nv(lg) == n - 1
@test ne(lg) == binomial(n - 1, 2) # lg must be a complete graph
end
end

@testset "Undirected Self-loops" begin
for T in
(Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128)
g = SimpleGraph{T}(2, [T[2], T[1, 2], T[]])
lg = line_graph(g)
@test nv(lg) == 2 # only 2 edges (self-loop counts once)
@test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2
end
end
end

@testset "Directed Line Graph" begin
@testset "Directed Cycle Graphs" begin
for n in 3:9
g = cycle_digraph(n)
lg = line_graph(g)
@test nv(lg) == n
@test ne(lg) == n
@test is_directed(lg)
@test is_connected(lg)
@test all(outdegree(lg, v) == 1 for v in vertices(lg))
@test all(indegree(lg, v) == 1 for v in vertices(lg))
end
end

@testset "Directed Path Graphs" begin
for n in 2:9
g = path_digraph(n)
lg = line_graph(g)
@test nv(lg) == n - 1
@test ne(lg) == n - 2
@test is_directed(lg)
@test is_connected(lg)
@test all(outdegree(lg, v) == (v < n - 1 ? 1 : 0) for v in vertices(lg))
@test all(indegree(lg, v) == (v > 1 ? 1 : 0) for v in vertices(lg))
end
end

@testset "Directed Star Graphs" begin
for m in 0:4, n in 0:4
g = SimpleDiGraph(m + n + 1)
foreach(i -> add_edge!(g, i + 1, 1), 1:m)
foreach(j -> add_edge!(g, 1, j + 1 + m), 1:n)
lg = line_graph(g) # checking if lg is the complete bipartite digraph
@test nv(lg) == m + n
@test ne(lg) == m * n
@test all(outdegree(lg, v) == 0 && indegree(lg, v) == m for v in 1:n)
@test all(
outdegree(lg, v) == n && indegree(lg, v) == 0 for v in (n + 1):(n + m)
)
end
end

@testset "Directed Self-loops" begin
for T in
(Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128)
g = SimpleDiGraph{T}(2, [T[1, 2], T[], T[]], [T[1], T[1], T[]])
lg = line_graph(g)
@test nv(lg) == 2
@test ne(lg) == 1
@test has_edge(lg, 1, 2)
end
end
end
end
45 changes: 28 additions & 17 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Aqua
using Documenter
using Graphs
using Graphs.SimpleGraphs
using Graphs.Experimental
using Graphs.SimpleGraphs
using Graphs.Test
using JET
using JuliaFormatter
using Graphs.Test
using Test
using SparseArrays
using LinearAlgebra
Expand Down Expand Up @@ -150,31 +150,42 @@ tests = [
"experimental/experimental",
]

args = lowercase.(ARGS)

@testset verbose = true "Graphs" begin
@testset "Code quality (JET.jl)" begin
@assert get_pkg_version("JET") >= v"0.8.4"
JET.test_package(
Graphs;
target_defined_modules=true,
ignore_missing_comparison=true,
mode=:typo, # TODO: switch back to `:basic` once the union split caused by traits is fixed
)
if "jet" in args || isempty(args)
@testset "Code quality (JET.jl)" begin
@assert get_pkg_version("JET") >= v"0.8.4"
JET.test_package(
Graphs;
target_defined_modules=true,
ignore_missing_comparison=true,
mode=:typo, # TODO: switch back to `:basic` once the union split caused by traits is fixed
)
end
end

@testset "Code quality (Aqua.jl)" begin
Aqua.test_all(Graphs; ambiguities=false)
if "aqua" in args || isempty(args)
@testset "Code quality (Aqua.jl)" begin
Aqua.test_all(Graphs; ambiguities=false)
end
end

@testset "Code formatting (JuliaFormatter.jl)" begin
@test format(Graphs; verbose=false, overwrite=false)
if "juliaformatter" in args || isempty(args)
@testset "Code formatting (JuliaFormatter.jl)" begin
@test format(Graphs; verbose=false, overwrite=false)
end
end

doctest(Graphs)
if "doctest" in args || isempty(args)
doctest(Graphs)
end

@testset verbose = true "Actual tests" begin
for t in tests
tp = joinpath(testdir, "$(t).jl")
include(tp)
if t in args || isempty(args)
include(joinpath(testdir, "$(t).jl"))
end
end
end
end;
Loading