diff --git a/Project.toml b/Project.toml index 0f76c3a..bb0dfef 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "NamedGraphs" uuid = "678767b0-92e7-4007-89e4-4527a8725b19" authors = ["Matthew Fishman and contributors"] -version = "0.1.4" +version = "0.1.5" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" diff --git a/src/Graphs/abstractgraph.jl b/src/Graphs/abstractgraph.jl index 40dac2e..26d92af 100644 --- a/src/Graphs/abstractgraph.jl +++ b/src/Graphs/abstractgraph.jl @@ -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`: diff --git a/src/NamedGraphs.jl b/src/NamedGraphs.jl index 409856b..2512c26 100644 --- a/src/NamedGraphs.jl +++ b/src/NamedGraphs.jl @@ -20,6 +20,8 @@ import Graphs: bfs_tree, blockdiag, common_neighbors, + connected_components, + connected_components!, degree, degree_histogram, dst, @@ -38,6 +40,8 @@ import Graphs: is_directed, is_strongly_connected, is_weakly_connected, + merge_vertices, + merge_vertices!, ne, neighbors, neighborhood, diff --git a/src/abstractnamedgraph.jl b/src/abstractnamedgraph.jl index c28f12b..3e62e73 100644 --- a/src/abstractnamedgraph.jl +++ b/src/abstractnamedgraph.jl @@ -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() @@ -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)) @@ -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 # diff --git a/test/test_namedgraph.jl b/test/test_namedgraph.jl index e3aa05f..356c85f 100644 --- a/test/test_namedgraph.jl +++ b/test/test_namedgraph.jl @@ -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") @@ -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