diff --git a/Project.toml b/Project.toml index 015a81b..9614857 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.4.2" +version = "0.5.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" diff --git a/README.md b/README.md index d8f2870..3a8c865 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ julia> using NamedGraphs: NamedGraph julia> using NamedGraphs.GraphsExtensions: ⊔, disjoint_union, subgraph, rename_vertices julia> g = NamedGraph(grid((4,)), ["A", "B", "C", "D"]) -NamedGraph{String} with 4 vertices: +NamedGraphs.NamedGraph{String} with 4 vertices: 4-element Dictionaries.Indices{String} "A" "B" @@ -81,7 +81,7 @@ julia> neighbors(g, "B") "C" julia> subgraph(g, ["A", "B"]) -NamedGraph{String} with 2 vertices: +NamedGraphs.NamedGraph{String} with 2 vertices: 2-element Dictionaries.Indices{String} "A" "B" @@ -108,7 +108,7 @@ julia> dims = (2, 2) (2, 2) julia> g = NamedGraph(grid(dims), Tuple.(CartesianIndices(dims))) -NamedGraph{Tuple{Int64, Int64}} with 4 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices: 4-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 1) (2, 1) @@ -152,7 +152,7 @@ You can use vertex names to get [induced subgraphs](https://juliagraphs.org/Grap ```julia julia> subgraph(v -> v[1] == 1, g) -NamedGraph{Tuple{Int64, Int64}} with 2 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 2 vertices: 2-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 1) (1, 2) @@ -162,7 +162,7 @@ and 1 edge(s): julia> subgraph(v -> v[2] == 2, g) -NamedGraph{Tuple{Int64, Int64}} with 2 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 2 vertices: 2-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 2) (2, 2) @@ -172,7 +172,7 @@ and 1 edge(s): julia> subgraph(g, [(1, 1), (2, 2)]) -NamedGraph{Tuple{Int64, Int64}} with 2 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 2 vertices: 2-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 1) (2, 2) @@ -186,7 +186,7 @@ You can also take [disjoint unions](https://en.wikipedia.org/wiki/Disjoint_union ```julia julia> g₁ = g -NamedGraph{Tuple{Int64, Int64}} with 4 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices: 4-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 1) (2, 1) @@ -201,7 +201,7 @@ and 4 edge(s): julia> g₂ = g -NamedGraph{Tuple{Int64, Int64}} with 4 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices: 4-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 1) (2, 1) @@ -216,7 +216,7 @@ and 4 edge(s): julia> disjoint_union(g₁, g₂) -NamedGraph{Tuple{Tuple{Int64, Int64}, Int64}} with 8 vertices: +NamedGraphs.NamedGraph{Tuple{Tuple{Int64, Int64}, Int64}} with 8 vertices: 8-element Dictionaries.Indices{Tuple{Tuple{Int64, Int64}, Int64}} ((1, 1), 1) ((2, 1), 1) @@ -239,7 +239,7 @@ and 8 edge(s): julia> g₁ ⊔ g₂ # Same as above -NamedGraph{Tuple{Tuple{Int64, Int64}, Int64}} with 8 vertices: +NamedGraphs.NamedGraph{Tuple{Tuple{Int64, Int64}, Int64}} with 8 vertices: 8-element Dictionaries.Indices{Tuple{Tuple{Int64, Int64}, Int64}} ((1, 1), 1) ((2, 1), 1) @@ -279,7 +279,7 @@ The original graphs can be obtained from subgraphs: ```julia julia> rename_vertices(first, subgraph(v -> v[2] == 1, g₁ ⊔ g₂)) -NamedGraph{Tuple{Int64, Int64}} with 4 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices: 4-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 1) (2, 1) @@ -294,7 +294,7 @@ and 4 edge(s): julia> rename_vertices(first, subgraph(v -> v[2] == 2, g₁ ⊔ g₂)) -NamedGraph{Tuple{Int64, Int64}} with 4 vertices: +NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices: 4-element Dictionaries.Indices{Tuple{Int64, Int64}} (1, 1) (2, 1) diff --git a/src/NamedGraphs.jl b/src/NamedGraphs.jl index c28c480..23f7f15 100644 --- a/src/NamedGraphs.jl +++ b/src/NamedGraphs.jl @@ -1,4 +1,5 @@ module NamedGraphs +include("lib/SimilarType/src/SimilarType.jl") include("lib/Keys/src/Keys.jl") include("lib/GraphGenerators/src/GraphGenerators.jl") include("lib/GraphsExtensions/src/GraphsExtensions.jl") @@ -16,7 +17,7 @@ include("namedgraph.jl") include("lib/NamedGraphGenerators/src/NamedGraphGenerators.jl") include("lib/PartitionedGraphs/src/PartitionedGraphs.jl") -export NamedGraph, NamedDiGraph, NamedEdge +export AbstractNamedGraphs, NamedDiGraph, NamedEdge, NamedGraph using PackageExtensionCompat: @require_extensions function __init__() diff --git a/src/decorate.jl b/src/decorate.jl index d8f9468..3e73ed2 100644 --- a/src/decorate.jl +++ b/src/decorate.jl @@ -3,7 +3,7 @@ using Graphs.SimpleGraphs: SimpleGraph using .GraphsExtensions: GraphsExtensions, add_edges! function GraphsExtensions.decorate_graph_edges( - g::AbstractNamedGraph; edge_map::Function=Returns(SimpleGraph(1)) + g::AbstractNamedGraph; edge_map::Function=Returns(NamedGraph(1)) ) g_dec = copy(g) es = edges(g_dec) @@ -19,7 +19,7 @@ function GraphsExtensions.decorate_graph_edges( end function GraphsExtensions.decorate_graph_vertices( - g::AbstractNamedGraph; vertex_map::Function=Returns(SimpleGraph(1)) + g::AbstractNamedGraph; vertex_map::Function=Returns(NamedGraph(1)) ) g_dec = copy(g) vs = vertices(g_dec) diff --git a/src/lib/GraphGenerators/src/GraphGenerators.jl b/src/lib/GraphGenerators/src/GraphGenerators.jl index e37d12e..11eeb30 100644 --- a/src/lib/GraphGenerators/src/GraphGenerators.jl +++ b/src/lib/GraphGenerators/src/GraphGenerators.jl @@ -1,6 +1,7 @@ module GraphGenerators using Dictionaries: Dictionary -using Graphs: SimpleGraph, add_edge! +using Graphs: add_edge!, dst, edges, nv, src +using Graphs.SimpleGraphs: SimpleDiGraph, SimpleGraph, binary_tree function comb_tree(dims::Tuple) @assert length(dims) == 2 @@ -28,4 +29,16 @@ function comb_tree(tooth_lengths::Vector{<:Integer}) end return graph end + +# TODO: More efficient implementation based +# on the implementation of `binary_tree`. +function binary_arborescence(k::Integer) + graph = binary_tree(k) + digraph = SimpleDiGraph(nv(graph)) + for e in edges(graph) + @assert dst(e) > src(e) + add_edge!(digraph, e) + end + return digraph +end end diff --git a/src/lib/GraphsExtensions/.JuliaFormatter.toml b/src/lib/GraphsExtensions/.JuliaFormatter.toml new file mode 100644 index 0000000..08f664c --- /dev/null +++ b/src/lib/GraphsExtensions/.JuliaFormatter.toml @@ -0,0 +1,2 @@ +style = "blue" +indent = 2 diff --git a/src/lib/GraphsExtensions/Project.toml b/src/lib/GraphsExtensions/Project.toml new file mode 100644 index 0000000..c3f9433 --- /dev/null +++ b/src/lib/GraphsExtensions/Project.toml @@ -0,0 +1,6 @@ +[deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +NamedGraphs = "678767b0-92e7-4007-89e4-4527a8725b19" +SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" +SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" diff --git a/src/lib/GraphsExtensions/src/GraphsExtensions.jl b/src/lib/GraphsExtensions/src/GraphsExtensions.jl index 93460cd..f4a017d 100644 --- a/src/lib/GraphsExtensions/src/GraphsExtensions.jl +++ b/src/lib/GraphsExtensions/src/GraphsExtensions.jl @@ -1,5 +1,6 @@ module GraphsExtensions include("abstractgraph.jl") +include("abstracttrees.jl") include("boundary.jl") include("shortestpaths.jl") include("symrcm.jl") diff --git a/src/lib/GraphsExtensions/src/abstractgraph.jl b/src/lib/GraphsExtensions/src/abstractgraph.jl index 7c0a132..2c1c891 100644 --- a/src/lib/GraphsExtensions/src/abstractgraph.jl +++ b/src/lib/GraphsExtensions/src/abstractgraph.jl @@ -1,4 +1,3 @@ -using AbstractTrees: AbstractTrees, PostOrderDFS, PreOrderDFS using Dictionaries: Dictionary, Indices, dictionary using Graphs: Graphs, @@ -6,15 +5,21 @@ using Graphs: AbstractGraph, IsDirected, Δ, + a_star, add_edge!, add_vertex!, degree, dfs_tree, eccentricity, edgetype, + has_edge, indegree, induced_subgraph, inneighbors, + is_connected, + is_cyclic, + is_directed, + is_tree, outdegree, outneighbors, ne, @@ -23,7 +28,6 @@ using Graphs: rem_edge!, rem_vertex!, weights -using Graphs.SimpleGraphs: AbstractSimpleGraph using SimpleTraits: SimpleTraits, Not, @traitfn using SplitApplyCombine: groupfind @@ -47,24 +51,13 @@ function convert_vertextype(V::Type, graph::AbstractGraph) return not_implemented() end -# TODO: Handle metadata in a generic way -@traitfn function directed_graph(graph::::(!IsDirected)) - digraph = directed_graph_type(typeof(graph))() - for v in vertices(graph) - add_vertex!(digraph, v) - end - for e in edges(graph) - add_edge!(digraph, e) - add_edge!(digraph, reverse(e)) - end - return digraph +function graph_from_vertices(graph_type::Type{<:AbstractGraph}, vertices) + return graph_type(vertices) end -@traitfn function directed_graph(graph::AbstractSimpleGraph::(!IsDirected)) - digraph = directed_graph_type(typeof(graph))() - for v in vertices(graph) - add_vertex!(digraph) - end +# TODO: Handle metadata in a generic way +@traitfn function directed_graph(graph::::(!IsDirected)) + digraph = graph_from_vertices(directed_graph_type(graph), vertices(graph)) for e in edges(graph) add_edge!(digraph, e) add_edge!(digraph, reverse(e)) @@ -80,7 +73,7 @@ end # to avoid method overwrite warnings, see: # https://github.com/mauro3/SimpleTraits.jl#method-overwritten-warnings @traitfn function undirected_graph(graph::::IsDirected) - undigraph = undirected_graph_type(typeof(graph))(vertices(graph)) + undigraph = graph_from_vertices(undirected_graph_type(typeof(graph)), vertices(graph)) for e in edges(graph) # TODO: Check for repeated edges? add_edge!(undigraph, e) @@ -92,18 +85,6 @@ end vertextype(::Type{<:AbstractGraph{V}}) where {V} = V vertextype(graph::AbstractGraph) = vertextype(typeof(graph)) -# Function `f` maps original vertices `vᵢ` of `g` -# to new vertices `f(vᵢ)` of the output graph. -rename_vertices(f::Function, g::AbstractGraph) = not_implemented() - -function rename_vertices(g::AbstractGraph, name_map) - return rename_vertices(v -> name_map[v], g) -end - -function permute_vertices(graph::AbstractGraph, permutation) - return not_implemented() -end - # Uniform interface for `outneighbors`, `inneighbors`, and `all_neighbors` function _neighbors(graph::AbstractGraph, vertex; dir=:out) if dir == :out @@ -131,10 +112,15 @@ end end # Alternative syntax to `getindex` for getting a subgraph +# TODO: Should this preserve vertex names by +# converting to `NamedGraph` if indexed by +# something besides `Base.OneTo`? function subgraph(graph::AbstractGraph, vertices) return induced_subgraph(graph, vertices)[1] end +# TODO: Should this preserve vertex names by +# converting to `NamedGraph`? function subgraph(f::Function, graph::AbstractGraph) return subgraph(graph, filter(f, vertices(graph))) end @@ -151,26 +137,43 @@ function outdegrees(graph::AbstractGraph, vertices=vertices(graph)) return map(vertex -> outdegree(graph, vertex), vertices) end -# Used for tree iteration. -# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree). -struct TreeGraph{G,V} - graph::G - vertex::V +# `Graphs.is_tree` only works on undirected graphs. +# TODO: Raise an issue. +@traitfn function is_ditree(graph::AbstractGraph::IsDirected) + # For directed graphs, `is_connected(graph)` returns `true` + # if `graph` is weakly connected. + return is_connected(graph) && ne(graph) == nv(graph) - 1 end -function AbstractTrees.children(t::TreeGraph) - return [TreeGraph(t.graph, vertex) for vertex in child_vertices(t.graph, t.vertex)] + +# TODO: Define in `Graphs.jl`. +# https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.tree.recognition.is_tree.html +# https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.tree.recognition.is_arborescence.html +# https://networkx.org/documentation/stable/_modules/networkx/algorithms/tree/recognition.html#is_arborescence +# https://networkx.org/documentation/stable/_modules/networkx/algorithms/tree/recognition.html#is_tree +# https://en.wikipedia.org/wiki/Arborescence_(graph_theory) +# directed rooted tree +@traitfn function is_arborescence(graph::AbstractGraph::IsDirected) + return is_ditree(graph) && all(v -> indegree(graph, v) ≤ 1, vertices(graph)) end -AbstractTrees.printnode(io::IO, t::TreeGraph) = print(io, t.vertex) # # Graph unions # +# Function `f` maps original vertices `vᵢ` of `g` +# to new vertices `f(vᵢ)` of the output graph. +rename_vertices(f, g::AbstractGraph) = not_implemented() + +# TODO: Does this relabel the vertices and/or change the adjacency matrix? +function permute_vertices(graph::AbstractGraph, permutation) + return not_implemented() +end + # https://en.wikipedia.org/wiki/Disjoint_union # Input maps the new index being appended to the vertices # to the associated graph. function disjoint_union(graphs::Dictionary{<:Any,<:AbstractGraph}) - return union((rename_vertices(v -> (v, i), graphs[i]) for i in keys(graphs))...) + return reduce(union, (rename_vertices(v -> (v, i), graphs[i]) for i in keys(graphs))) end function disjoint_union(graphs::Vector{<:AbstractGraph}) @@ -195,25 +198,8 @@ function ⊔(graphs...; kwargs...) return disjoint_union(graphs...; kwargs...) end -# vcat, hcat, hvncat -# function vcat(graph1::AbstractGraph, graph2::AbstractGraph; kwargs...) -# return hvncat(1, graph1, graph2; kwargs...) -# end -# -# function hcat(graph1::AbstractGraph, graph2::AbstractGraph; kwargs...) -# return hvncat(2, graph1, graph2; kwargs...) -# end -# -# # TODO: define `disjoint_union(graphs...; dim::Int, new_dim_names)` to do a disjoint union -# # of a number of graphs. -# function disjoint_union(graph1::AbstractGraph, graph2::AbstractGraph; dim::Int=0, kwargs...) -# return hvncat(dim, graph1, graph2; kwargs...) -# end - """ -TODO: Make this more sophisticated, check that -only two vertices have degree 1 and none have -degree 0, meaning it is a path/linear graph: +Check if an undirected graph is a path/linear graph: https://en.wikipedia.org/wiki/Path_graph @@ -221,9 +207,16 @@ but not a path/linear forest: https://en.wikipedia.org/wiki/Linear_forest """ -function is_path_graph(graph::AbstractGraph) - # Maximum degree - return Δ(graph) == 2 +@traitfn function is_path_graph(graph::::(!IsDirected)) + return is_tree(graph) && (Δ(graph) == 2) +end + +""" +https://juliagraphs.org/Graphs.jl/dev/core_functions/simplegraphs_generators/#Graphs.SimpleGraphs.cycle_graph-Tuple%7BT%7D%20where%20T%3C:Integer +https://en.wikipedia.org/wiki/Cycle_graph +""" +@traitfn function is_cycle_graph(graph::::(!IsDirected)) + return all(==(2), degrees(graph)) end function out_incident_edges(graph::AbstractGraph, vertex) @@ -275,11 +268,117 @@ end # # root_index = findfirst(vertex -> length(outneighbors(vertex)) == length(neighbors(vertex)), vertices(graph)) # root = vertices(graph)[root_index] -# [node.vertex for node in Leaves(TreeGraph(graph, root))] +# map(nodevalue, Leaves(tree_graph_node(graph, root))) # +@traitfn function is_leaf_vertex(graph::::(!IsDirected), vertex) + # @assert !is_cyclic(graph) + return isone(length(neighbors(graph, vertex))) +end + +# Check if a vertex is a leaf. +# Assumes the graph is a DAG. +@traitfn function is_leaf_vertex(graph::::IsDirected, vertex) + # @assert !is_cyclic(graph) + return isempty(child_vertices(graph, vertex)) +end + +# Get the children of a vertex. +# Assumes the graph is a DAG. +@traitfn function child_vertices(graph::::IsDirected, vertex) + # @assert !is_cyclic(graph) + return outneighbors(graph, vertex) +end + +# Get the edges from the input vertex towards the child vertices. +# Assumes the graph is a DAG. +@traitfn function child_edges(graph::::IsDirected, vertex) + # @assert !is_cyclic(graph) + return map(child -> edgetype(graph)(vertex, child), child_vertices(graph, vertex)) +end + function leaf_vertices(graph::AbstractGraph) - # @assert is_tree(graph) - return filter(v -> is_leaf(graph, v), vertices(graph)) + # @assert !is_cyclic(graph) + return filter(v -> is_leaf_vertex(graph, v), vertices(graph)) +end + +""" +Determine if an edge involves a leaf (at src or dst) +""" +@traitfn function is_leaf_edge(g::::(!IsDirected), e::AbstractEdge) + return has_edge(g, e) && (is_leaf_vertex(g, src(e)) || is_leaf_vertex(g, dst(e))) +end +@traitfn function is_leaf_edge(g::::IsDirected, e::AbstractEdge) + return has_edge(g, e) && is_leaf_vertex(g, dst(e)) +end +function is_leaf_edge(g::AbstractGraph, e::Pair) + return is_leaf_edge(g, edgetype(g)(e)) +end + +""" +Determine if a node has any neighbors which are leaves +""" +function has_leaf_neighbor(g::AbstractGraph, v) + return any(w -> is_leaf_vertex(g, w), neighbors(g, v)) +end + +""" +Get all edges which do not involve a leaf + +https://en.wikipedia.org/wiki/Tree_(graph_theory)#Definitions +""" +function non_leaf_edges(g::AbstractGraph) + return Iterators.filter(e -> !is_leaf_edge(g, e), edges(g)) +end + +""" +Get distance of a vertex from a leaf +""" +function distance_to_leaves(g::AbstractGraph, v) + return map(Indices(leaf_vertices(g))) do leaf + v == leaf && return 0 + path = a_star(g, v, leaf) + isempty(path) && return typemax(Int) + return length(path) + end +end + +function minimum_distance_to_leaves(g::AbstractGraph, v) + return minimum(distance_to_leaves(g, v)) +end + +@traitfn function is_root_vertex(graph::::IsDirected, vertex) + return isempty(parent_vertices(graph, vertex)) +end + +@traitfn function is_rooted(graph::::IsDirected) + return isone(count(v -> is_root_vertex(graph, v), vertices(graph))) +end + +@traitfn function is_binary_arborescence(graph::AbstractGraph::IsDirected) + (is_rooted(graph) && is_arborescence(graph)) || return false + for v in vertices(graph) + if length(child_vertices(graph, v)) > 2 + return false + end + end + return true +end + +""" +Return the root vertex of a rooted directed graph. + +This will return the first root vertex that is found, +so won't error if there is more than one. +""" +@traitfn function root_vertex(graph::::IsDirected) + if is_cyclic(graph) + return error("Graph must not have any cycles.") + end + v = first(vertices(graph)) + while !is_root_vertex(graph, v) + v = parent_vertex(graph, v) + end + return v end # @@ -291,99 +390,88 @@ end return post_order_dfs_vertices(dfs_tree_graph, root_vertex) end +# Traverse the tree using a [post-order depth-first search](https://en.wikipedia.org/wiki/Tree_traversal#Depth-first_search), returning the vertices. +# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree) +@traitfn function post_order_dfs_vertices(graph::::IsDirected, root_vertex) + # @assert is_tree(graph) + # Outputs a rooted directed tree (https://en.wikipedia.org/wiki/Arborescence_(graph_theory)) + return map(nodevalue, PostOrderDFS(tree_graph_node(graph, root_vertex))) +end + @traitfn function pre_order_dfs_vertices(graph::::(!IsDirected), root_vertex) dfs_tree_graph = dfs_tree(graph, root_vertex) return pre_order_dfs_vertices(dfs_tree_graph, root_vertex) end +@traitfn function pre_order_dfs_vertices(graph::::IsDirected, root_vertex) + # @assert is_tree(graph) + return map(nodevalue, PreOrderDFS(tree_graph_node(graph, root_vertex))) +end + @traitfn function post_order_dfs_edges(graph::::(!IsDirected), root_vertex) dfs_tree_graph = dfs_tree(graph, root_vertex) return post_order_dfs_edges(dfs_tree_graph, root_vertex) end -@traitfn function is_leaf(graph::::(!IsDirected), vertex) +# Traverse the tree using a [post-order depth-first search](https://en.wikipedia.org/wiki/Tree_traversal#Depth-first_search), returning the edges where the source is the current vertex and the destination is the parent vertex. +# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree). +# Returns a list of edges directed **towards the root vertex**! +@traitfn function post_order_dfs_edges(graph::::IsDirected, root_vertex) # @assert is_tree(graph) - return isone(length(neighbors(graph, vertex))) + vertices = post_order_dfs_vertices(graph, root_vertex) + # Remove the root vertex + pop!(vertices) + return map(vertex -> parent_edge(graph, vertex), vertices) end # Paths for undirected tree-like graphs # TODO: Use `a_star`. @traitfn function vertex_path(graph::::(!IsDirected), s, t) + # @assert is_tree(graph) dfs_tree_graph = dfs_tree(graph, t) return vertex_path(dfs_tree_graph, s, t) end # TODO: Use `a_star`. @traitfn function edge_path(graph::::(!IsDirected), s, t) + # @assert is_tree(graph) dfs_tree_graph = dfs_tree(graph, t) return edge_path(dfs_tree_graph, s, t) end # -# Rooted directed tree functions. +# Rooted directed tree/directed acyclic graph functions. # [Rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree) +# [Directed acyclic graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph) # -# Get the parent vertex of a vertex. -# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree) -@traitfn function parent_vertex(graph::::IsDirected, vertex) - # @assert is_tree(graph) - in_neighbors = inneighbors(graph, vertex) - isempty(in_neighbors) && return nothing - return only(in_neighbors) -end - -# Returns the edge directed **towards the parent/root vertex**! -@traitfn function parent_edge(graph::::IsDirected, vertex) - # @assert is_tree(graph) - parent = parent_vertex(graph, vertex) - isnothing(parent) && return nothing - return edgetype(graph)(vertex, parent) -end - -# Get the children of a vertex. -# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree) -@traitfn function child_vertices(graph::::IsDirected, vertex) - # @assert is_tree(graph) - return outneighbors(graph, vertex) -end - -# Get the edges from the input vertex towards the child vertices. -@traitfn function child_edges(graph::::IsDirected, vertex) - # @assert is_tree(graph) - return [ - edgetype(graph)(vertex, child_vertex) for child_vertex in child_vertices(graph, vertex) - ] +# Get the parent vertices of a vertex. +# Assumes the graph is a DAG. +@traitfn function parent_vertices(graph::::IsDirected, vertex) + # @assert !is_cyclic(graph) + return inneighbors(graph, vertex) end -# Check if a vertex is a leaf. -# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree) -@traitfn function is_leaf(graph::::IsDirected, vertex) - # @assert is_tree(graph) - return isempty(outneighbors(graph, vertex)) -end - -# Traverse the tree using a [post-order depth-first search](https://en.wikipedia.org/wiki/Tree_traversal#Depth-first_search), returning the vertices. -# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree) -@traitfn function post_order_dfs_vertices(graph::::IsDirected, root_vertex) - # @assert is_tree(graph) - # Outputs a rooted directed tree (https://en.wikipedia.org/wiki/Arborescence_(graph_theory)) - return [node.vertex for node in PostOrderDFS(TreeGraph(graph, root_vertex))] +# Get the parent vertex of a vertex. +# Assumes the graph is a DAG. +@traitfn function parent_vertex(graph::::IsDirected, vertex) + # @assert !is_cyclic(graph) + parents = parent_vertices(graph, vertex) + return isempty(parents) ? nothing : only(parents) end -@traitfn function pre_order_dfs_vertices(graph::::IsDirected, root_vertex) - return [node.vertex for node in PreOrderDFS(TreeGraph(graph, root_vertex))] +# Returns the edges directed **towards the parent vertices**! +# Assumes the graph is a DAG. +@traitfn function parent_edges(graph::::IsDirected, vertex) + # @assert !is_cyclic(graph) + return map(parent -> edgetype(graph)(vertex, parent), parent_vertices(graph, vertex)) end -# Traverse the tree using a [post-order depth-first search](https://en.wikipedia.org/wiki/Tree_traversal#Depth-first_search), returning the edges where the source is the current vertex and the destination is the parent vertex. -# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree). -# Returns a list of edges directed **towards the root vertex**! -@traitfn function post_order_dfs_edges(graph::::IsDirected, root_vertex) - # @assert is_tree(graph) - vertices = post_order_dfs_vertices(graph, root_vertex) - # Remove the root vertex - pop!(vertices) - return [parent_edge(graph, vertex) for vertex in vertices] +# Returns the edge directed **towards the parent vertex**! +# Assumes the graph is a DAG. +@traitfn function parent_edge(graph::::IsDirected, vertex) + parents = parent_edges(graph, vertex) + return isempty(parents) ? nothing : only(parents) end # Paths for directed tree-like graphs @@ -425,7 +513,7 @@ function add_vertex(g::AbstractGraph, vs) return g end -function add_vertices!(graph::AbstractGraph, vs::Vector) +function add_vertices!(graph::AbstractGraph, vs) for vertex in vs add_vertex!(graph, vertex) end @@ -460,14 +548,14 @@ end function add_edge(g::AbstractGraph, edge) g = copy(g) - add_edge!(g, edges) + add_edge!(g, edgetype(g)(edge)) return g end """Add a list of edges to a graph g""" function add_edges!(g::AbstractGraph, edges) for e in edges - add_edge!(g, e) + add_edge!(g, edgetype(g)(e)) end return g end @@ -480,14 +568,14 @@ end function rem_edge(g::AbstractGraph, edge) g = copy(g) - rem_edge!(g, edges) + rem_edge!(g, edgetype(g)(edge)) return g end """Remove a list of edges from a graph g""" function rem_edges!(g::AbstractGraph, edges) for e in edges - rem_edge!(g, e) + rem_edge!(g, edgetype(g)(e)) end return g end @@ -530,7 +618,7 @@ function random_bfs_tree(g::AbstractGraph, s; maxiter=1000 * (nv(g) + ne(g))) d[vn] = d[v] + 1 if (vn ∉ Q) if (vn ∉ visited) - add_edge!(g_out, v => vn) + add_edge!(g_out, edgetype(g)(v, vn)) push!(visited, vn) end push!(Q, vn) diff --git a/src/lib/GraphsExtensions/src/abstracttrees.jl b/src/lib/GraphsExtensions/src/abstracttrees.jl new file mode 100644 index 0000000..47342e6 --- /dev/null +++ b/src/lib/GraphsExtensions/src/abstracttrees.jl @@ -0,0 +1,64 @@ +# AbstractTreeGraph +# Tree view of a graph. +abstract type AbstractTreeGraph{V} <: AbstractGraph{V} end +parent_graph_type(type::Type{<:AbstractTreeGraph}) = not_implemented() +parent_graph(graph::AbstractTreeGraph) = not_implemented() + +Graphs.is_directed(type::Type{<:AbstractTreeGraph}) = is_directed(parent_graph_type(type)) +Graphs.edgetype(graph::AbstractTreeGraph) = edgetype(parent_graph(graph)) +function Graphs.outneighbors(graph::AbstractTreeGraph, vertex) + return outneighbors(parent_graph(graph), vertex) +end +function Graphs.inneighbors(graph::AbstractTreeGraph, vertex) + return inneighbors(parent_graph(graph), vertex) +end +Graphs.nv(graph::AbstractTreeGraph) = nv(parent_graph(graph)) +Graphs.ne(graph::AbstractTreeGraph) = ne(parent_graph(graph)) +Graphs.vertices(graph::AbstractTreeGraph) = vertices(parent_graph(graph)) + +# AbstractTrees +using AbstractTrees: + AbstractTrees, IndexNode, PostOrderDFS, PreOrderDFS, children, nodevalue + +# Used for tree iteration. +# Assumes the graph is a [rooted directed tree](https://en.wikipedia.org/wiki/Tree_(graph_theory)#Rooted_tree). +tree_graph_node(g::AbstractTreeGraph, vertex) = IndexNode(g, vertex) +function tree_graph_node(g::AbstractGraph, vertex) + return tree_graph_node(TreeGraph(g), vertex) +end +tree_graph_node(g::AbstractGraph) = tree_graph_node(g, root_vertex(g)) + +# Make an `AbstractTreeGraph` act as an `AbstractTree` starting at +# the root vertex. +AbstractTrees.children(g::AbstractTreeGraph) = children(tree_graph_node(g)) +AbstractTrees.nodevalue(g::AbstractTreeGraph) = nodevalue(tree_graph_node(g)) + +AbstractTrees.rootindex(tree::AbstractTreeGraph) = root_vertex(tree) +function AbstractTrees.nodevalue(tree::AbstractTreeGraph, node_index) + return node_index +end +function AbstractTrees.childindices(tree::AbstractTreeGraph, node_index) + return child_vertices(tree, node_index) +end +function AbstractTrees.parentindex(tree::AbstractTreeGraph, node_index) + return parent_vertex(tree, node_index) +end + +# TreeGraph +struct TreeGraph{V,G<:AbstractGraph{V}} <: AbstractTreeGraph{V} + graph::G + global function _TreeGraph(g::AbstractGraph) + # No check for being a tree + return new{vertextype(g),typeof(g)}(g) + end +end +@traitfn function TreeGraph(g::AbstractGraph::IsDirected) + @assert is_arborescence(g) + return _TreeGraph(g) +end +@traitfn function TreeGraph(g::AbstractGraph::(!IsDirected)) + @assert is_tree(g) + return _TreeGraph(g) +end +parent_graph(graph::TreeGraph) = getfield(graph, :graph) +parent_graph_type(type::Type{<:TreeGraph}) = fieldtype(type, :graph) diff --git a/src/lib/GraphsExtensions/src/simplegraph.jl b/src/lib/GraphsExtensions/src/simplegraph.jl index 31be24a..3c0ecd4 100644 --- a/src/lib/GraphsExtensions/src/simplegraph.jl +++ b/src/lib/GraphsExtensions/src/simplegraph.jl @@ -1,3 +1,11 @@ +using Graphs.SimpleGraphs: AbstractSimpleGraph + +# https://github.com/JuliaGraphs/Graphs.jl/issues/365 +function graph_from_vertices(graph_type::Type{<:AbstractSimpleGraph}, vertices) + @assert vertices == Base.OneTo(length(vertices)) + return graph_type(length(vertices)) +end + using Graphs.SimpleGraphs: SimpleDiGraph, SimpleGraph directed_graph_type(G::Type{<:SimpleGraph}) = SimpleDiGraph{vertextype(G)} diff --git a/src/lib/GraphsExtensions/test/Project.toml b/src/lib/GraphsExtensions/test/Project.toml new file mode 100644 index 0000000..787a24a --- /dev/null +++ b/src/lib/GraphsExtensions/test/Project.toml @@ -0,0 +1,5 @@ +[deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +NamedGraphs = "678767b0-92e7-4007-89e4-4527a8725b19" diff --git a/src/lib/GraphsExtensions/test/runtests.jl b/src/lib/GraphsExtensions/test/runtests.jl new file mode 100644 index 0000000..c724dd7 --- /dev/null +++ b/src/lib/GraphsExtensions/test/runtests.jl @@ -0,0 +1,535 @@ +@eval module $(gensym()) +using AbstractTrees: + IndexNode, + Leaves, + PostOrderDFS, + childindices, + children, + nodevalue, + nodevalues, + parent, + parentindex, + rootindex +using Dictionaries: Dictionary, Indices +using Graphs: + add_edge!, + add_vertex!, + edges, + edgetype, + inneighbors, + is_cyclic, + is_directed, + ne, + nv, + outneighbors, + rem_edge!, + vertices +using Graphs.SimpleGraphs: + SimpleDiGraph, + SimpleEdge, + SimpleGraph, + binary_tree, + cycle_digraph, + cycle_graph, + grid, + path_digraph, + path_graph +using NamedGraphs: NamedGraph +using NamedGraphs.GraphGenerators: binary_arborescence +using NamedGraphs.GraphsExtensions: + TreeGraph, + ⊔, + add_edge, + add_edges, + add_edges!, + all_edges, + child_edges, + child_vertices, + degrees, + directed_graph, + directed_graph_type, + disjoint_union, + distance_to_leaves, + has_leaf_neighbor, + incident_edges, + indegrees, + is_arborescence, + is_binary_arborescence, + is_cycle_graph, + is_ditree, + is_leaf_edge, + is_leaf_vertex, + is_path_graph, + is_root_vertex, + is_rooted, + is_self_loop, + leaf_vertices, + minimum_distance_to_leaves, + non_leaf_edges, + outdegrees, + permute_vertices, + rem_edge, + rem_edges, + rem_edges!, + rename_vertices, + root_vertex, + subgraph, + tree_graph_node, + undirected_graph, + undirected_graph_type, + vertextype +using Test: @test, @test_broken, @test_throws, @testset + +# TODO: Still need to test: +# - post_order_dfs_vertices +# - pre_order_dfs_vertices +# - post_order_dfs_edges +# - vertex_path +# - edge_path +# - parent_vertices +# - parent_vertex +# - parent_edges +# - parent_edge +# - mincut_partitions +# - eccentricities +# - decorate_graph_edges +# - decorate_graph_vertices +# - random_bfs_tree + +@testset "NamedGraphs.GraphsExtensions" begin + # is_self_loop + @test is_self_loop(SimpleEdge(1, 1)) + @test !is_self_loop(SimpleEdge(1, 2)) + @test is_self_loop(1 => 1) + @test !is_self_loop(1 => 2) + + # directed_graph_type + @test directed_graph_type(SimpleGraph{Int}) === SimpleDiGraph{Int} + @test directed_graph_type(SimpleGraph(4)) === SimpleDiGraph{Int} + + # undirected_graph_type + @test undirected_graph_type(SimpleGraph{Int}) === SimpleGraph{Int} + @test undirected_graph_type(SimpleGraph(4)) === SimpleGraph{Int} + + # directed_graph + @test directed_graph(path_digraph(4)) == path_digraph(4) + @test typeof(directed_graph(path_digraph(4))) === SimpleDiGraph{Int} + g = path_graph(4) + dig = directed_graph(g) + @test typeof(dig) === SimpleDiGraph{Int} + @test nv(dig) == 4 + @test ne(dig) == 6 + @test issetequal( + edges(dig), edgetype(dig).([1 => 2, 2 => 1, 2 => 3, 3 => 2, 3 => 4, 4 => 3]) + ) + + # undirected_graph + @test undirected_graph(path_graph(4)) == path_graph(4) + @test typeof(undirected_graph(path_graph(4))) === SimpleGraph{Int} + dig = path_digraph(4) + g = undirected_graph(dig) + @test typeof(g) === SimpleGraph{Int} + @test g == path_graph(4) + + # vertextype + for f in (path_graph, path_digraph) + for vtype in (Int, UInt64) + @test vertextype(f(vtype(4))) === vtype + @test vertextype(typeof(f(vtype(4)))) === vtype + end + end + + # rename_vertices + vs = ["a", "b", "c", "d"] + g = rename_vertices(v -> vs[v], NamedGraph(path_graph(4))) + @test nv(g) == 4 + @test ne(g) == 3 + @test issetequal(vertices(g), vs) + @test issetequal(edges(g), edgetype(g).(["a" => "b", "b" => "c", "c" => "d"])) + @test g isa NamedGraph + # Not defined for AbstractSimpleGraph. + @test_throws ErrorException rename_vertices(v -> vs[v], path_graph(4)) + + # permute_vertices + g = path_graph(4) + @test_broken permute_vertices(g, [2, 1, 4, 3]) + + # all_edges + g = path_graph(4) + @test issetequal( + all_edges(g), edgetype(g).([1 => 2, 2 => 1, 2 => 3, 3 => 2, 3 => 4, 4 => 3]) + ) + g = path_digraph(4) + @test issetequal(all_edges(g), edgetype(g).([1 => 2, 2 => 3, 3 => 4])) + + # subgraph + g = subgraph(path_graph(4), 2:4) + @test nv(g) == 3 + @test ne(g) == 2 + # TODO: Should this preserve vertex names by + # converting to `NamedGraph` if indexed by + # something besides `Base.OneTo`? + @test vertices(g) == 1:3 + @test issetequal(edges(g), edgetype(g).([1 => 2, 2 => 3])) + @test subgraph(v -> v ∈ 2:4, path_graph(4)) == g + + # degrees + @test degrees(path_graph(4)) == [1, 2, 2, 1] + @test degrees(path_graph(4), 2:4) == [2, 2, 1] + @test degrees(path_digraph(4)) == [1, 2, 2, 1] + @test degrees(path_digraph(4), 2:4) == [2, 2, 1] + @test degrees(path_graph(4), Indices(2:4)) == Dictionary(2:4, [2, 2, 1]) + + # indegrees + @test indegrees(path_graph(4)) == [1, 2, 2, 1] + @test indegrees(path_graph(4), 2:4) == [2, 2, 1] + @test indegrees(path_digraph(4)) == [0, 1, 1, 1] + @test indegrees(path_digraph(4), 2:4) == [1, 1, 1] + + # outdegrees + @test outdegrees(path_graph(4)) == [1, 2, 2, 1] + @test outdegrees(path_graph(4), 2:4) == [2, 2, 1] + @test outdegrees(path_digraph(4)) == [1, 1, 1, 0] + @test outdegrees(path_digraph(4), 2:4) == [1, 1, 0] + + # TreeGraph + # Binary tree: + # + # 1 + # / \ + # / \ + # 2 3 + # / \ / \ + # 4 5 6 7 + # + # with vertex 1 as root. + g = binary_arborescence(3) + @test is_arborescence(g) + @test is_binary_arborescence(g) + @test is_ditree(g) + g′ = copy(g) + add_edge!(g′, 2 => 3) + @test !is_arborescence(g′) + @test !is_ditree(g′) + t = TreeGraph(g) + @test is_directed(t) + @test ne(t) == 6 + @test nv(t) == 7 + @test vertices(t) == 1:7 + @test issetequal(outneighbors(t, 1), [2, 3]) + @test issetequal(outneighbors(t, 2), [4, 5]) + @test issetequal(outneighbors(t, 3), [6, 7]) + @test isempty(inneighbors(t, 1)) + for v in 2:3 + @test only(inneighbors(t, v)) == 1 + end + for v in 4:5 + @test only(inneighbors(t, v)) == 2 + end + for v in 6:7 + @test only(inneighbors(t, v)) == 3 + end + @test edgetype(t) === SimpleEdge{Int} + @test vertextype(t) == Int + @test nodevalue(t) == 1 + for v in 1:7 + @test tree_graph_node(g, v) == IndexNode(t, v) + end + @test rootindex(t) == 1 + @test issetequal(nodevalue.(children(t)), 2:3) + @test issetequal(childindices(t, 1), 2:3) + @test issetequal(childindices(t, 2), 4:5) + @test issetequal(childindices(t, 3), 6:7) + for v in 4:7 + @test isempty(childindices(t, v)) + end + @test isnothing(parentindex(t, 1)) + for v in 2:3 + @test parentindex(t, v) == 1 + end + for v in 4:5 + @test parentindex(t, v) == 2 + end + for v in 6:7 + @test parentindex(t, v) == 3 + end + @test IndexNode(t) == IndexNode(t, 1) + @test tree_graph_node(g) == tree_graph_node(g, 1) + for dfs_g in ( + collect(nodevalues(PostOrderDFS(tree_graph_node(g, 1)))), + collect(nodevalues(PostOrderDFS(t))), + ) + @test length(dfs_g) == 7 + @test dfs_g == [4, 5, 2, 6, 7, 3, 1] + end + @test issetequal(nodevalue.(children(tree_graph_node(g, 1))), 2:3) + @test issetequal(nodevalue.(children(tree_graph_node(g, 2))), 4:5) + @test issetequal(nodevalue.(children(tree_graph_node(g, 3))), 6:7) + for v in 4:7 + @test isempty(children(tree_graph_node(g, v))) + end + for n in (tree_graph_node(g), t) + @test issetequal(nodevalue.(Leaves(n)), 4:7) + end + @test issetequal(nodevalue.(Leaves(t)), 4:7) + @test isnothing(nodevalue(parent(tree_graph_node(g, 1)))) + for v in 2:3 + @test nodevalue(parent(tree_graph_node(g, v))) == 1 + end + for v in 4:5 + @test nodevalue(parent(tree_graph_node(g, v))) == 2 + end + for v in 6:7 + @test nodevalue(parent(tree_graph_node(g, v))) == 3 + end + + # disjoint_union, ⊔ + g1 = NamedGraph(path_graph(3)) + g2 = NamedGraph(path_graph(3)) + for g in ( + disjoint_union(g1, g2), + disjoint_union([g1, g2]), + disjoint_union([1 => g1, 2 => g2]), + disjoint_union(Dictionary([1, 2], [g1, g2])), + g1 ⊔ g2, + (1 => g1) ⊔ (2 => g2), + ) + @test nv(g) == 6 + @test ne(g) == 4 + @test issetequal(vertices(g), [(1, 1), (2, 1), (3, 1), (1, 2), (2, 2), (3, 2)]) + end + for g in ( + disjoint_union("x" => g1, "y" => g2), + disjoint_union(["x" => g1, "y" => g2]), + disjoint_union(Dictionary(["x", "y"], [g1, g2])), + ("x" => g1) ⊔ ("y" => g2), + ) + @test nv(g) == 6 + @test ne(g) == 4 + @test issetequal( + vertices(g), [(1, "x"), (2, "x"), (3, "x"), (1, "y"), (2, "y"), (3, "y")] + ) + end + + # is_path_graph + @test is_path_graph(path_graph(4)) + @test !is_path_graph(cycle_graph(4)) + # Only defined for undirected graphs at the moment. + @test_throws MethodError is_path_graph(path_digraph(4)) + @test !is_path_graph(grid((3, 2))) + + # is_cycle_graph + @test is_cycle_graph(cycle_graph(4)) + @test !is_cycle_graph(path_graph(4)) + # Only defined for undirected graphs at the moment. + @test_throws MethodError is_cycle_graph(cycle_digraph(4)) + @test !is_cycle_graph(grid((3, 2))) + @test is_cycle_graph(grid((2, 2))) + + # incident_edges + g = path_graph(4) + @test issetequal(incident_edges(g, 2), SimpleEdge.([2 => 1, 2 => 3])) + @test issetequal(incident_edges(g, 2; dir=:out), SimpleEdge.([2 => 1, 2 => 3])) + @test issetequal(incident_edges(g, 2; dir=:in), SimpleEdge.([1 => 2, 3 => 2])) + # TODO: Only output out edges? + @test issetequal( + incident_edges(g, 2; dir=:both), SimpleEdge.([2 => 1, 1 => 2, 2 => 3, 3 => 2]) + ) + + # is_leaf_vertex + g = binary_tree(3) + for v in 1:3 + @test !is_leaf_vertex(g, v) + end + for v in 4:7 + @test is_leaf_vertex(g, v) + end + g = binary_arborescence(3) + for v in 1:3 + @test !is_leaf_vertex(g, v) + end + for v in 4:7 + @test is_leaf_vertex(g, v) + end + + # child_vertices + g = binary_arborescence(3) + @test issetequal(child_vertices(g, 1), 2:3) + @test issetequal(child_vertices(g, 2), 4:5) + @test issetequal(child_vertices(g, 3), 6:7) + for v in 4:7 + @test isempty(child_vertices(g, v)) + end + + # child_edges + g = binary_arborescence(3) + @test issetequal(child_edges(g, 1), SimpleEdge.([1 => 2, 1 => 3])) + @test issetequal(child_edges(g, 2), SimpleEdge.([2 => 4, 2 => 5])) + @test issetequal(child_edges(g, 3), SimpleEdge.([3 => 6, 3 => 7])) + for v in 4:7 + @test isempty(child_edges(g, v)) + end + + # leaf_vertices + g = binary_tree(3) + @test issetequal(leaf_vertices(g), 4:7) + g = binary_arborescence(3) + @test issetequal(leaf_vertices(g), 4:7) + + # is_leaf_edge + g = binary_tree(3) + for e in [1 => 2, 1 => 3] + @test !is_leaf_edge(g, e) + @test !is_leaf_edge(g, reverse(e)) + end + for e in [2 => 4, 2 => 5, 3 => 6, 3 => 7] + @test is_leaf_edge(g, e) + @test is_leaf_edge(g, reverse(e)) + end + g = binary_arborescence(3) + for e in [1 => 2, 1 => 3] + @test !is_leaf_edge(g, e) + @test !is_leaf_edge(g, reverse(e)) + end + for e in [2 => 4, 2 => 5, 3 => 6, 3 => 7] + @test is_leaf_edge(g, e) + @test !is_leaf_edge(g, reverse(e)) + end + + # has_leaf_neighbor + for g in (binary_tree(3), binary_arborescence(3)) + for v in [1; 4:7] + @test !has_leaf_neighbor(g, v) + end + for v in 2:3 + @test has_leaf_neighbor(g, v) + end + end + + # non_leaf_edges + g = binary_tree(3) + es = collect(non_leaf_edges(g)) + es = [es; reverse.(es)] + for e in SimpleEdge.([1 => 2, 1 => 3]) + @test e in es + @test reverse(e) in es + end + for e in SimpleEdge.([2 => 4, 2 => 5, 3 => 6, 3 => 7]) + @test !(e in es) + @test !(reverse(e) in es) + end + g = binary_arborescence(3) + es = collect(non_leaf_edges(g)) + for e in SimpleEdge.([1 => 2, 1 => 3]) + @test e in es + @test !(reverse(e) in es) + end + for e in SimpleEdge.([2 => 4, 2 => 5, 3 => 6, 3 => 7]) + @test !(e in es) + @test !(reverse(e) in es) + end + + # distance_to_leaves + g = binary_tree(3) + d = distance_to_leaves(g, 3) + d_ref = Dict([4 => 3, 5 => 3, 6 => 1, 7 => 1]) + for v in keys(d) + @test is_leaf_vertex(g, v) + @test d[v] == d_ref[v] + end + g = binary_arborescence(3) + d = distance_to_leaves(g, 3) + d_ref = Dict([4 => typemax(Int), 5 => typemax(Int), 6 => 1, 7 => 1]) + for v in keys(d) + @test is_leaf_vertex(g, v) + @test d[v] == d_ref[v] + end + d = distance_to_leaves(g, 1) + d_ref = Dict([4 => 2, 5 => 2, 6 => 2, 7 => 2]) + for v in keys(d) + @test is_leaf_vertex(g, v) + @test d[v] == d_ref[v] + end + + # minimum_distance_to_leaves + for g in (binary_tree(3), binary_arborescence(3)) + @test minimum_distance_to_leaves(g, 1) == 2 + @test minimum_distance_to_leaves(g, 3) == 1 + @test minimum_distance_to_leaves(g, 7) == 0 + end + + # is_root_vertex + g = binary_arborescence(3) + @test is_root_vertex(g, 1) + for v in 2:7 + @test !is_root_vertex(g, v) + end + g = binary_tree(3) + for v in vertices(g) + @test_throws MethodError is_root_vertex(g, v) + end + + # is_rooted + @test is_rooted(binary_arborescence(3)) + g = binary_arborescence(3) + add_edge!(g, 2 => 3) + @test is_rooted(g) + g = binary_arborescence(3) + add_vertex!(g) + add_edge!(g, 8 => 3) + @test !is_rooted(g) + @test is_rooted(path_digraph(4)) + @test_throws MethodError is_rooted(binary_tree(3)) + + # is_binary_arborescence + @test is_binary_arborescence(binary_arborescence(3)) + g = binary_arborescence(3) + add_vertex!(g) + add_edge!(g, 3 => 8) + @test !is_binary_arborescence(g) + @test_throws MethodError is_binary_arborescence(binary_tree(3)) + + # root_vertex + @test root_vertex(binary_arborescence(3)) == 1 + # No root vertex of cyclic graph. + g = binary_arborescence(3) + add_edge!(g, 7 => 1) + @test_throws ErrorException root_vertex(g) + @test_throws MethodError root_vertex(binary_tree(3)) + + # add_edge + g = SimpleGraph(4) + add_edge!(g, 1 => 2) + @test add_edge(SimpleGraph(4), 1 => 2) == g + + # add_edges + @test add_edges(SimpleGraph(4), [1 => 2, 2 => 3, 3 => 4]) == path_graph(4) + + # add_edges! + g = SimpleGraph(4) + add_edges!(g, [1 => 2, 2 => 3, 3 => 4]) + @test g == path_graph(4) + + # rem_edge + g = path_graph(4) + # https://github.com/JuliaGraphs/Graphs.jl/issues/364 + rem_edge!(g, 2, 3) + @test rem_edge(path_graph(4), 2 => 3) == g + + # rem_edges + g = path_graph(4) + # https://github.com/JuliaGraphs/Graphs.jl/issues/364 + rem_edge!(g, 2, 3) + rem_edge!(g, 3, 4) + @test rem_edges(path_graph(4), [2 => 3, 3 => 4]) == g + + # rem_edges! + g = path_graph(4) + # https://github.com/JuliaGraphs/Graphs.jl/issues/364 + rem_edge!(g, 2, 3) + rem_edge!(g, 3, 4) + g′ = path_graph(4) + rem_edges!(g′, [2 => 3, 3 => 4]) + @test g′ == g +end +end diff --git a/src/lib/SimilarType/src/SimilarType.jl b/src/lib/SimilarType/src/SimilarType.jl new file mode 100644 index 0000000..4484dde --- /dev/null +++ b/src/lib/SimilarType/src/SimilarType.jl @@ -0,0 +1,4 @@ +module SimilarType +similar_type(object) = similar_type(typeof(object)) +similar_type(type::Type) = type +end diff --git a/src/namedgraph.jl b/src/namedgraph.jl index 95a9f50..c00cc5b 100644 --- a/src/namedgraph.jl +++ b/src/namedgraph.jl @@ -62,12 +62,14 @@ function Graphs.rem_vertex!(graph::GenericNamedGraph, vertex) end function GraphsExtensions.rename_vertices(f::Function, g::GenericNamedGraph) - # TODO: Could be `set_vertices(g, f.(g.parent_vertex_to_vertex))`. + # TODO: Could be implemented as `set_vertices(g, f.(g.parent_vertex_to_vertex))`. return GenericNamedGraph(g.parent_graph, f.(g.parent_vertex_to_vertex)) end function GraphsExtensions.rename_vertices(f::Function, g::AbstractSimpleGraph) - return rename_vertices(f, GenericNamedGraph(g)) + return error( + "Can't rename the vertices of a graph of type `$(typeof(g)) <: AbstractSimpleGraph`, try converting to a named graph.", + ) end function GraphsExtensions.convert_vertextype(V::Type, graph::GenericNamedGraph) diff --git a/test/runtests.jl b/test/runtests.jl index 28545b7..85e1e93 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,6 @@ -using NamedGraphs -using Test - +@eval module $(gensym()) +using Test: @testset test_path = joinpath(@__DIR__) - test_files = filter( file -> startswith(file, "test_") && endswith(file, ".jl"), readdir(test_path) ) @@ -13,3 +11,4 @@ test_files = filter( include(file_path) end end +end diff --git a/test/test_abstractgraph.jl b/test/test_abstractgraph.jl index 79bc3ff..214297e 100644 --- a/test/test_abstractgraph.jl +++ b/test/test_abstractgraph.jl @@ -2,7 +2,7 @@ using Graphs: binary_tree, dfs_tree, edgetype, grid, path_graph using NamedGraphs.GraphGenerators: comb_tree using NamedGraphs.GraphsExtensions: - is_leaf, + is_leaf_vertex, is_path_graph, edge_path, leaf_vertices, @@ -83,21 +83,21 @@ end @testset "Tree graph leaf vertices" begin # undirected trees g = comb_tree((3, 2)) - @test is_leaf(g, 4) - @test !is_leaf(g, 1) + @test is_leaf_vertex(g, 4) + @test !is_leaf_vertex(g, 1) @test issetequal(leaf_vertices(g), [4, 5, 6]) ng = named_comb_tree((3, 2)) - @test is_leaf(ng, (1, 2)) - @test is_leaf(ng, (2, 2)) - @test !is_leaf(ng, (1, 1)) + @test is_leaf_vertex(ng, (1, 2)) + @test is_leaf_vertex(ng, (2, 2)) + @test !is_leaf_vertex(ng, (1, 1)) @test issetequal(leaf_vertices(ng), [(1, 2), (2, 2), (3, 2)]) # directed trees dng = dfs_tree(ng, (2, 2)) - @test is_leaf(dng, (1, 2)) - @test !is_leaf(dng, (2, 2)) - @test !is_leaf(dng, (1, 1)) + @test is_leaf_vertex(dng, (1, 2)) + @test !is_leaf_vertex(dng, (2, 2)) + @test !is_leaf_vertex(dng, (1, 1)) @test issetequal(leaf_vertices(dng), [(1, 2), (3, 2)]) end end diff --git a/test/test_abstractnamedgraph.jl b/test/test_abstractnamedgraph.jl index b08398a..8473f15 100644 --- a/test/test_abstractnamedgraph.jl +++ b/test/test_abstractnamedgraph.jl @@ -63,14 +63,14 @@ end ng = NamedGraph(g, string_names) # rename to integers vmap_int = Dictionary(vertices(ng), integer_names) - ng_int = rename_vertices(ng, vmap_int) + ng_int = rename_vertices(v -> vmap_int[v], ng) @test isa(ng_int, NamedGraph{Int}) @test has_vertex(ng_int, 3) @test has_edge(ng_int, 1 => 2) @test has_edge(ng_int, 2 => 4) # rename to tuples vmap_tuple = Dictionary(vertices(ng), tuple_names) - ng_tuple = rename_vertices(ng, vmap_tuple) + ng_tuple = rename_vertices(v -> vmap_tuple[v], ng) @test isa(ng_tuple, NamedGraph{Tuple{String,Int}}) @test has_vertex(ng_tuple, ("X", 1)) @test has_edge(ng_tuple, ("X", 1) => ("X", 2)) @@ -86,7 +86,7 @@ end ndg = named_grid((2, 2)) # rename to integers vmap_int = Dictionary(vertices(ndg), integer_names) - ndg_int = rename_vertices(ndg, vmap_int) + ndg_int = rename_vertices(v -> vmap_int[v], ndg) @test isa(ndg_int, NamedGraph{Int}) @test has_vertex(ndg_int, 1) @test has_edge(ndg_int, 1 => 2) @@ -94,7 +94,7 @@ end @test length(a_star(ndg_int, 1, 4)) == 2 # rename to strings vmap_string = Dictionary(vertices(ndg), string_names) - ndg_string = rename_vertices(ndg, vmap_string) + ndg_string = rename_vertices(v -> vmap_string[v], ndg) @test isa(ndg_string, NamedGraph{String}) @test has_vertex(ndg_string, "A") @test has_edge(ndg_string, "A" => "B") @@ -102,7 +102,7 @@ end @test length(a_star(ndg_string, "A", "D")) == 2 # rename to strings vmap_tuple = Dictionary(vertices(ndg), tuple_names) - ndg_tuple = rename_vertices(ndg, vmap_tuple) + ndg_tuple = rename_vertices(v -> vmap_tuple[v], ndg) @test isa(ndg_tuple, NamedGraph{Tuple{String,Int}}) @test has_vertex(ndg_tuple, ("X", 1)) @test has_edge(ndg_tuple, ("X", 1) => ("X", 2)) @@ -120,14 +120,14 @@ end nddg = NamedDiGraph(DiGraph(collect(edges(g))), vertices(ndg)) # rename to integers vmap_int = Dictionary(vertices(nddg), integer_names) - nddg_int = rename_vertices(nddg, vmap_int) + nddg_int = rename_vertices(v -> vmap_int[v], nddg) @test isa(nddg_int, NamedDiGraph{Int}) @test has_vertex(nddg_int, 1) @test has_edge(nddg_int, 1 => 2) @test has_edge(nddg_int, 2 => 4) # rename to strings vmap_string = Dictionary(vertices(nddg), string_names) - nddg_string = rename_vertices(nddg, vmap_string) + nddg_string = rename_vertices(v -> vmap_string[v], nddg) @test isa(nddg_string, NamedDiGraph{String}) @test has_vertex(nddg_string, "A") @test has_edge(nddg_string, "A" => "B") @@ -135,7 +135,7 @@ end @test !has_edge(nddg_string, "D" => "B") # rename to strings vmap_tuple = Dictionary(vertices(nddg), tuple_names) - nddg_tuple = rename_vertices(nddg, vmap_tuple) + nddg_tuple = rename_vertices(v -> vmap_tuple[v], nddg) @test isa(nddg_tuple, NamedDiGraph{Tuple{String,Int}}) @test has_vertex(nddg_tuple, ("X", 1)) @test has_edge(nddg_tuple, ("X", 1) => ("X", 2)) diff --git a/test/test_namedgraphgenerators.jl b/test/test_namedgraphgenerators.jl index 2b06456..b536acd 100644 --- a/test/test_namedgraphgenerators.jl +++ b/test/test_namedgraphgenerators.jl @@ -1,6 +1,6 @@ @eval module $(gensym()) using Graphs: edges, neighbors, vertices -using NamedGraphs.GraphsExtensions: is_path_graph +using NamedGraphs.GraphsExtensions: is_cycle_graph using NamedGraphs.NamedGraphGenerators: named_hexagonal_lattice_graph, named_triangular_lattice_graph using Test: @test, @testset @@ -9,7 +9,7 @@ using Test: @test, @testset g = named_hexagonal_lattice_graph(1, 1) #Should just be 1 hexagon - @test is_path_graph(g) + @test is_cycle_graph(g) #Check consistency with the output of hexagonal_lattice_graph(7,7) in networkx g = named_hexagonal_lattice_graph(7, 7) @@ -24,7 +24,7 @@ using Test: @test, @testset g = named_triangular_lattice_graph(1, 1) #Should just be 1 triangle - @test is_path_graph(g) + @test is_cycle_graph(g) g = named_hexagonal_lattice_graph(2, 1) dims = maximum(vertices(g))