Skip to content

Commit

Permalink
Add connected_components, merge_vertices. Fix incident_edges.
Browse files Browse the repository at this point in the history
  • Loading branch information
mtfishman authored Dec 12, 2022
2 parents a95cbe3 + 26e41f8 commit 0f3b94d
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "NamedGraphs"
uuid = "678767b0-92e7-4007-89e4-4527a8725b19"
authors = ["Matthew Fishman <[email protected]> and contributors"]
version = "0.1.4"
version = "0.1.5"

[deps]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
Expand Down
36 changes: 34 additions & 2 deletions src/Graphs/abstractgraph.jl
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,44 @@ function is_tree(graph::AbstractGraph)
return (ne(graph) == nv(graph) - 1) && is_connected(graph)
end

function incident_edges(graph::AbstractGraph, vertex)
function out_incident_edges(graph::AbstractGraph, vertex)
return [
edgetype(graph)(vertex, neighbor_vertex) for neighbor_vertex in neighbors(graph, vertex)
edgetype(graph)(vertex, neighbor_vertex) for neighbor_vertex in outneighbors(graph, vertex)
]
end

function in_incident_edges(graph::AbstractGraph, vertex)
return [
edgetype(graph)(neighbor_vertex, vertex) for neighbor_vertex in inneighbors(graph, vertex)
]
end

function all_incident_edges(graph::AbstractGraph, vertex)
return out_incident_edges(graph, vertex) in_incident_edges(graph, vertex)
end

"""
incident_edges(graph::AbstractGraph, vertex; dir=:out)
Edges incident to the vertex `vertex`.
`dir ∈ (:in, :out, :both)`, defaults to `:out`.
For undirected graphs, returns all incident edges.
Like: https://juliagraphs.org/Graphs.jl/v1.7/algorithms/linalg/#Graphs.LinAlg.adjacency_matrix
"""
function incident_edges(graph::AbstractGraph, vertex; dir=:out)
if dir == :out
return out_incident_edges(graph, vertex)
elseif dir == :in
return in_incident_edges(graph, vertex)
elseif dir == :both
return all_incident_edges(graph, vertex)
end
return error("dir = $dir not supported.")
end

# Get the leaf vertices of a tree-like graph
#
# For the directed case, could also use `AbstractTrees`:
Expand Down
4 changes: 4 additions & 0 deletions src/NamedGraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import Graphs:
bfs_tree,
blockdiag,
common_neighbors,
connected_components,
connected_components!,
degree,
degree_histogram,
dst,
Expand All @@ -38,6 +40,8 @@ import Graphs:
is_directed,
is_strongly_connected,
is_weakly_connected,
merge_vertices,
merge_vertices!,
ne,
neighbors,
neighborhood,
Expand Down
40 changes: 40 additions & 0 deletions src/abstractnamedgraph.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ parent_graph(graph::AbstractNamedGraph) = not_implemented()
# ?
parent_graph_type(graph::AbstractNamedGraph) = not_implemented()

parent_vertextype(graph::AbstractNamedGraph) = vertextype(parent_graph(graph))

# Convert vertex to parent vertex
# Inverse map of `parent_vertex_to_vertex`.
vertex_to_parent_vertex(graph::AbstractNamedGraph, vertex) = not_implemented()
Expand Down Expand Up @@ -83,12 +85,20 @@ function vertices_to_parent_vertices(
return map(vertex_to_parent_vertex(graph), vertices)
end

function vertices_to_parent_vertices(graph::AbstractNamedGraph)
return Base.Fix1(vertices_to_parent_vertices, graph)
end

function parent_vertices_to_vertices(
graph::AbstractNamedGraph, parent_vertices
)
return map(parent_vertex_to_vertex(graph), parent_vertices)
end

function parent_vertices_to_vertices(graph::AbstractNamedGraph)
return Base.Fix1(parent_vertices_to_vertices, graph)
end

parent_vertices(graph::AbstractNamedGraph) = vertices(parent_graph(graph))
parent_edges(graph::AbstractNamedGraph) = edges(parent_graph(graph))
parent_edgetype(graph::AbstractNamedGraph) = edgetype(parent_graph(graph))
Expand Down Expand Up @@ -378,6 +388,36 @@ function adjacency_matrix(graph::AbstractNamedGraph, args...)
return adjacency_matrix(parent_graph(graph), args...)
end

function connected_components(graph::AbstractNamedGraph)
parent_connected_components = connected_components(parent_graph(graph))
return map(parent_vertices_to_vertices(graph), parent_connected_components)
end

function merge_vertices!(graph::AbstractNamedGraph, merge_vertices; merged_vertex=first(merge_vertices))
not_implemented()
end

function merge_vertices(graph::AbstractNamedGraph, merge_vertices; merged_vertex=first(merge_vertices))
merged_graph = copy(graph)
if !has_vertex(graph, merged_vertex)
add_vertex!(merged_graph, merged_vertex)
end
for vertex in merge_vertices
for e in incident_edges(graph, vertex; dir=:both)
merged_edge = rename_vertices(v -> v == vertex ? merged_vertex : v, e)
if src(merged_edge) dst(merged_edge)
add_edge!(merged_graph, merged_edge)
end
end
end
for vertex in merge_vertices
if vertex merged_vertex
rem_vertex!(merged_graph, vertex)
end
end
return merged_graph
end

#
# Graph traversals
#
Expand Down
188 changes: 187 additions & 1 deletion test/test_namedgraph.jl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,82 @@ end
@test has_path(g, "D", "E")
@test !has_path(g, "A", "E")
end
@testset "neighborhood" begin
g = named_grid((4, 4))
@test issetequal(neighborhood(g, (1, 1), nv(g)), vertices(g))
@test issetequal(neighborhood(g, (1, 1), 0), [(1, 1)])
@test issetequal(neighborhood(g, (1, 1), 1), [(1, 1), (2, 1), (1, 2)])
ns = [
(1, 1),
(2, 1),
(1, 2),
(3, 1),
(2, 2),
(1, 3),
]
@test issetequal(neighborhood(g, (1, 1), 2), ns)
ns = [
(1, 1),
(2, 1),
(1, 2),
(3, 1),
(2, 2),
(1, 3),
(4, 1),
(3, 2),
(2, 3),
(1, 4),
]
@test issetequal(neighborhood(g, (1, 1), 3), ns)
ns = [
(1, 1),
(2, 1),
(1, 2),
(3, 1),
(2, 2),
(1, 3),
(4, 1),
(3, 2),
(2, 3),
(1, 4),
(4, 2),
(3, 3),
(2, 4),
]
@test issetequal(neighborhood(g, (1, 1), 4), ns)
ns = [
(1, 1),
(2, 1),
(1, 2),
(3, 1),
(2, 2),
(1, 3),
(4, 1),
(3, 2),
(2, 3),
(1, 4),
(4, 2),
(3, 3),
(2, 4),
(4, 3),
(3, 4),
]
@test issetequal(neighborhood(g, (1, 1), 5), ns)
@test issetequal(neighborhood(g, (1, 1), 6), vertices(g))
ns_ds = [
((1, 1), 0),
((2, 1), 1),
((1, 2), 1),
((3, 1), 2),
((2, 2), 2),
((1, 3), 2),
((4, 1), 3),
((3, 2), 3),
((2, 3), 3),
((1, 4), 3),
]
@test issetequal(neighborhood_dists(g, (1, 1), 3), ns_ds)
end
@testset "Basics (directed)" begin
g = NamedDiGraph(["A", "B", "C", "D"])
add_edge!(g, "A" => "B")
Expand Down Expand Up @@ -244,13 +320,123 @@ end
)
@test_broken f(g, "A")
end
@testset "has_self_loops" begin
end
@testset "Graph connectivity" begin
g = NamedGraph(2)
@test g isa NamedGraph{Int}
add_edge!(g, 1, 2)
@test !has_self_loops(g)
add_edge!(g, 1, 1)
@test has_self_loops(g)

g1 = named_grid((2, 2))
g2 = named_grid((2, 2))
g = g1 g2
t = named_binary_tree(3)

@test is_cyclic(g1)
@test is_cyclic(g2)
@test is_cyclic(g)
@test !is_cyclic(t)

@test is_connected(g1)
@test is_connected(g2)
@test !is_connected(g)
@test is_connected(t)

cc = connected_components(g1)
@test length(cc) == 1
@test length(only(cc)) == nv(g1)
@test issetequal(only(cc), vertices(g1))

cc = connected_components(g)
@test length(cc) == 2
@test length(cc[1]) == nv(g1)
@test length(cc[2]) == nv(g2)
@test issetequal(cc[1], map(v -> (v, 1), vertices(g1)))
@test issetequal(cc[2], map(v -> (v, 2), vertices(g2)))
end
@testset "incident_edges" begin
g = grid((3, 3))
inc_edges = Edge.([2 => 1, 2 => 3, 2 => 5])
@test issetequal(incident_edges(g, 2), inc_edges)
@test issetequal(incident_edges(g, 2; dir=:in), reverse.(inc_edges))
@test issetequal(incident_edges(g, 2; dir=:out), inc_edges)
@test issetequal(incident_edges(g, 2; dir=:both), inc_edges reverse.(inc_edges))

g = named_grid((3, 3))
inc_edges = NamedEdge.([
(2, 1) => (1, 1),
(2, 1) => (3, 1),
(2, 1) => (2, 2),
])
@test issetequal(incident_edges(g, (2, 1)), inc_edges)
@test issetequal(incident_edges(g, (2, 1); dir=:in), reverse.(inc_edges))
@test issetequal(incident_edges(g, (2, 1); dir=:out), inc_edges)
@test issetequal(incident_edges(g, (2, 1); dir=:both), inc_edges reverse.(inc_edges))

g = path_digraph(4)
@test issetequal(incident_edges(g, 3), Edge.([3 => 4]))
@test issetequal(incident_edges(g, 3; dir=:in), Edge.([2 => 3]))
@test issetequal(incident_edges(g, 3; dir=:out), Edge.([3 => 4]))
@test issetequal(incident_edges(g, 3; dir=:both), Edge.([2 => 3, 3 => 4]))

g = NamedDiGraph(path_digraph(4), ["A", "B", "C", "D"])
@test issetequal(incident_edges(g, "C"), NamedEdge.(["C" => "D"]))
@test issetequal(incident_edges(g, "C"; dir=:in), NamedEdge.(["B" => "C"]))
@test issetequal(incident_edges(g, "C"; dir=:out), NamedEdge.(["C" => "D"]))
@test issetequal(incident_edges(g, "C"; dir=:both), NamedEdge.(["B" => "C", "C" => "D"]))
end
@testset "merge_vertices" begin
g = named_grid((3, 3))
mg = merge_vertices(g, [(2, 2), (2, 3), (3, 3)])
@test nv(mg) == 7
@test ne(mg) == 9
merged_vertices = [
(1, 1),
(2, 1),
(3, 1),
(1, 2),
(2, 2),
(3, 2),
(1, 3),
]
for v in merged_vertices
@test has_vertex(mg, v)
end
merged_edges = [
(1, 1) => (2, 1),
(1, 1) => (1, 2),
(2, 1) => (3, 1),
(2, 1) => (2, 2),
(3, 1) => (3, 2),
(1, 2) => (2, 2),
(1, 2) => (1, 3),
(2, 2) => (3, 2),
(2, 2) => (1, 3),
]
for e in merged_edges
@test has_edge(mg, e)
end

sg = SimpleDiGraph(4)
g = NamedDiGraph(sg, ["A", "B", "C", "D"])
add_edge!(g, "A" => "B")
add_edge!(g, "B" => "C")
add_edge!(g, "C" => "D")
mg = merge_vertices(g, ["B", "C"])
@test ne(mg) == 2
@test has_edge(mg, "A" => "B")
@test has_edge(mg, "B" => "D")

sg = SimpleDiGraph(4)
g = NamedDiGraph(sg, ["A", "B", "C", "D"])
add_edge!(g, "B" => "A")
add_edge!(g, "C" => "B")
add_edge!(g, "D" => "C")
mg = merge_vertices(g, ["B", "C"])
@test ne(mg) == 2
@test has_edge(mg, "B" => "A")
@test has_edge(mg, "D" => "B")
end
end

2 comments on commit 0f3b94d

@mtfishman
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/74009

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.5 -m "<description of version>" 0f3b94d5f5ed5e84b524b50de42245a4c251c356
git push origin v0.1.5

Please sign in to comment.