diff --git a/Project.toml b/Project.toml index 2775d4a..e8b9800 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "DataGraphs" uuid = "b5a273c3-7e6c-41f6-98bd-8d7f1525a36a" -version = "0.4.2" +version = "0.4.3" authors = ["Matthew Fishman and contributors"] [workspace] diff --git a/src/DataGraphs.jl b/src/DataGraphs.jl index 6f93972..e201e5e 100644 --- a/src/DataGraphs.jl +++ b/src/DataGraphs.jl @@ -6,6 +6,9 @@ include("dataview.jl") include("abstractdatagraph.jl") include("indexing.jl") include("datagraph.jl") +include("abstractedgeorvertexdatagraph.jl") +include("vertexdatagraph.jl") +include("edgedatagraph.jl") # TODO: Turn into an extension once `PartitionedGraphs` is excised. include("lib/DataGraphsPartitionedGraphsExt/src/DataGraphsPartitionedGraphsExt.jl") diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index 2fefa68..d1c87c6 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -6,7 +6,8 @@ using NamedGraphs.GraphsExtensions: GraphsExtensions, add_edges!, add_vertices!, using NamedGraphs.OrdinalIndexing: OrdinalSuffixedInteger using NamedGraphs.SimilarType: similar_type using NamedGraphs: NamedGraphs, AbstractEdges, AbstractNamedEdge, AbstractNamedGraph, - AbstractVertices, NamedDiGraph, NamedGraph, position_graph_type, similar_graph + AbstractVertices, NamedDiGraph, NamedGraph, Vertices, position_graph_type, + similar_graph, subgraph_edges using SimpleTraits: SimpleTraits, @traitfn, Not abstract type AbstractDataGraph{V, VD, ED} <: AbstractNamedGraph{V} end @@ -21,12 +22,15 @@ edge_data_type(::Type{<:AbstractDataGraph{V, VD, ED}}) where {V, VD, ED} = ED # TODO: Define for `AbstractGraph` as a `DataGraphInterface`. underlying_graph(::AbstractDataGraph) = not_implemented() +# `isassigned` is_vertex_assigned(::AbstractDataGraph, vertex) = not_implemented() is_edge_assigned(::AbstractDataGraph, edge) = not_implemented() +# `getindex` get_vertex_data(::AbstractDataGraph, vertex) = not_implemented() get_edge_data(::AbstractDataGraph, edge) = not_implemented() +# `setindex!` set_vertex_data!(::AbstractDataGraph, data, vertex) = not_implemented() set_edge_data!(::AbstractDataGraph, data, edge) = not_implemented() @@ -41,12 +45,6 @@ function get_edges_data(g::AbstractGraph, edges) return map(e -> getindex(g, e), Indices(edges)) end -Graphs.has_vertex(g::AbstractDataGraph, vertex) = has_vertex(underlying_graph(g), vertex) -Graphs.has_edge(g::AbstractDataGraph, edge) = has_edge(underlying_graph(g), edge) -function Graphs.has_edge(g::AbstractDataGraph, edge::AbstractNamedEdge) - return has_edge(underlying_graph(g), edge) -end - vertex_data(dg::AbstractGraph) = VertexDataView(dg) edge_data(dg::AbstractGraph) = EdgeDataView(dg) @@ -57,9 +55,7 @@ function assigned_edges(graph::AbstractGraph) return Indices(filter(e -> isassigned(graph, e), edges(graph))) end -function Graphs.edgetype(graph::AbstractDataGraph) - return Graphs.edgetype(underlying_graph(graph)) -end +Graphs.edgetype(graph::AbstractDataGraph) = edgetype(underlying_graph(graph)) function Graphs.edgetype(graph_type::Type{<:AbstractDataGraph}) return edgetype(underlying_graph_type(graph_type)) end @@ -75,9 +71,38 @@ function NamedGraphs.position_graph_type(type::Type{<:AbstractDataGraph}) return position_graph_type(underlying_graph_type(type)) end -function Base.copyto!(dst_graph::AbstractDataGraph, src_graph::AbstractDataGraph) - vertex_data(dst_graph) .= vertex_data(src_graph) - edge_data(dst_graph) .= edge_data(src_graph) +function Base.copy(graph::AbstractDataGraph) + copy_graph = similar_graph(graph) + # Allow copies of graphs with undefined data. + copyto!(copy_graph, graph, assigned_vertices(graph)) + copyto!(copy_graph, graph, assigned_edges(graph)) + return copy_graph +end + +# Base method to specialize on. +function Base.copyto!(graph_dst::AbstractDataGraph, src, keys) + for key in keys + graph_dst[key] = src[key] + end + return graph_dst +end + +function Base.copyto!(graph_dst::AbstractDataGraph, src) + copyto!_datagraph(graph_dst, src) + return graph_dst +end + +# To prevent method ambiguities +function copyto!_datagraph(dst::AbstractDataGraph, src::AbstractDataGraph) + if !issetequal(vertices(dst), vertices(src)) + throw(ArgumentError("destination and source graphs must have the same vertices")) + end + copyto!(dst, src, vertices(src)) + copyto!(dst, src, edges(src)) + return dst +end +function copyto!_datagraph(dst_graph::AbstractDataGraph, src) + copyto!(dst_graph, src, keys(src)) return dst_graph end @@ -110,8 +135,8 @@ GraphsExtensions.convert_vertextype(::Type, ::AbstractDataGraph) = not_implement function Base.:(==)(dg1::AbstractDataGraph, dg2::AbstractDataGraph) underlying_graph(dg1) == underlying_graph(dg2) || return false - vertex_data(dg1) == vertex_data(dg2) || return false - edge_data(dg1) == edge_data(dg2) || return false + assigned_vertex_data(dg1) == assigned_vertex_data(dg2) || return false + assigned_edge_data(dg1) == assigned_edge_data(dg2) || return false return true end @@ -426,3 +451,8 @@ function Base.show(io::IO, mime::MIME"text/plain", graph::AbstractDataGraph) end Base.show(io::IO, graph::AbstractDataGraph) = show(io, MIME"text/plain"(), graph) + +function GraphsExtensions.forest_cover_edge_sequence(graph::AbstractDataGraph; kwargs...) + dummy_graph = add_edges!(NamedGraph(vertices(graph)), edges(graph)) + return GraphsExtensions.forest_cover_edge_sequence(dummy_graph; kwargs...) +end diff --git a/src/abstractedgeorvertexdatagraph.jl b/src/abstractedgeorvertexdatagraph.jl new file mode 100644 index 0000000..c48a8e5 --- /dev/null +++ b/src/abstractedgeorvertexdatagraph.jl @@ -0,0 +1,289 @@ +using Dictionaries: Dictionaries, Indices, isinsertable, set! +using Graphs: edges, edgetype, has_edge, has_vertex, rem_edge!, rem_vertex!, vertices +using NamedGraphs: NamedGraphs, Vertices, similar_graph, subgraph_edges, to_graph_index + +abstract type AbstractVertexOrEdgeDataGraph{T, V} <: AbstractDataGraph{V, T, T} end + +Graphs.edgetype(graph::AbstractVertexOrEdgeDataGraph) = edgetype(typeof(graph)) + +function NamedGraphs.similar_graph( + graph::AbstractVertexOrEdgeDataGraph + ) + return similar_graph(graph, valtype(graph)) +end + +function NamedGraphs.similar_graph( + graph::AbstractVertexOrEdgeDataGraph, + vertices + ) + return similar_graph(graph, valtype(graph), vertices) +end +function NamedGraphs.similar_graph( + graph::AbstractVertexOrEdgeDataGraph, + T::Type, + vertices + ) + return similar_graph(graph, T, Vertices(vertices)) +end + +# For ambiguity resolution only. +function NamedGraphs.similar_graph( + graph::AbstractVertexOrEdgeDataGraph, + VD::Type, + ED::Type + ) + # No notion of both vertex and edge data, will go to `AbstractDataGraph` fallback. + new_graph = similar_graph(graph, VD, ED, vertices(graph)) # -> DataGraph + add_edges!(new_graph, edges(graph)) + return new_graph +end + +function Base.copy(graph::AbstractVertexOrEdgeDataGraph) + graph_dst = similar_graph(graph) + # Allow copies of graphs with undefined data. + copyto!(graph_dst, graph, filter(key -> isassigned(graph, key), keys(graph))) + return graph_dst +end + +function Base.copyto!(graph_dst::AbstractVertexOrEdgeDataGraph, src) + copyto!(graph_dst, src, keys(src)) + return graph_dst +end + +Base.iterate(graph::AbstractVertexOrEdgeDataGraph) = iterate(index_data(graph)) +function Base.iterate(graph::AbstractVertexOrEdgeDataGraph, state) + return iterate(index_data(graph), state) +end + +Base.keytype(graph::AbstractVertexOrEdgeDataGraph) = keytype(typeof(graph)) + +Base.valtype(graph::AbstractVertexOrEdgeDataGraph) = valtype(typeof(graph)) +Base.valtype(::Type{<:AbstractVertexOrEdgeDataGraph{T}}) where {T} = T + +Base.eltype(graph::AbstractVertexOrEdgeDataGraph) = eltype(typeof(graph)) +Base.eltype(::Type{<:AbstractVertexOrEdgeDataGraph{T}}) where {T} = T + +Base.length(graph::AbstractVertexOrEdgeDataGraph) = length(index_data(graph)) +Base.keys(graph::AbstractVertexOrEdgeDataGraph) = keys(index_data(graph)) +Base.values(graph::AbstractVertexOrEdgeDataGraph) = values(index_data(graph)) + +Dictionaries.issettable(::AbstractVertexOrEdgeDataGraph) = true +Dictionaries.isinsertable(::AbstractVertexOrEdgeDataGraph) = false + +function Base.insert!(graph::AbstractVertexOrEdgeDataGraph, ind, data) + isinsertable(graph) || throw(ArgumentError("Graph does not support insertion.")) + insert!_datagraph(graph, to_graph_index(graph, ind), data) + return graph +end + +function Base.delete!(graph::AbstractVertexOrEdgeDataGraph, ind) + delete!_datagraph(graph, to_graph_index(graph, ind)) + return graph +end + +function Dictionaries.set!(graph::AbstractVertexOrEdgeDataGraph, ind, data) + set!_datagraph(graph, to_graph_index(graph, ind), data) + return graph +end + +# ================================== vertex data graph =================================== # + +abstract type AbstractVertexDataGraph{T, V} <: AbstractVertexOrEdgeDataGraph{T, V} end + +Base.keytype(::Type{<:AbstractVertexDataGraph{T, V}}) where {T, V} = V + +is_edge_assigned(::AbstractVertexDataGraph, _edge) = false + +# `setindex!` +function set_index_data!(graph::AbstractVertexDataGraph, data, vertex) + if !has_vertex(graph, vertex) + throw(IndexError("Graph does not contain vertex $vertex")) + end + set_vertex_data!(graph, data, vertex) + return graph +end + +# `insert!` +function insert!_datagraph(graph::AbstractVertexDataGraph, vertex, data) + if has_vertex(graph, vertex) + throw(IndexError("Graph already contains vertex $vertex")) + end + insert_vertex_data!(graph, vertex, data) + return graph +end + +# `delete!` +function delete!_datagraph(graph::AbstractVertexDataGraph, vertex) + if !has_vertex(graph, vertex) + throw(IndexError("Graph does not contain vertex $vertex")) + end + rem_vertex!(graph, vertex) + return graph +end + +# `set!` +function set!_datagraph(graph::AbstractVertexDataGraph, vertex, data) + if has_vertex(graph, vertex) + graph[vertex] = data + else + insert!(graph, vertex, data) + end + return graph +end + +function NamedGraphs.similar_graph( + graph::AbstractVertexDataGraph, + T::Type + ) + new_graph = similar_graph(graph, T, vertices(graph)) + # we can add edges to a `AbstractVertexDataGraph`. + add_edges!(new_graph, edges(graph)) + return new_graph +end + +# Base method to overload. +function NamedGraphs.similar_graph( + ::AbstractVertexDataGraph, + T::Type, + vertices::Vertices + ) + return similar_graph(VertexDataGraph{T}, vertices) +end + +function NamedGraphs.induced_subgraph_from_vertices( + graph::AbstractVertexDataGraph, + subvertices + ) + subnetwork = similar_graph(graph, subvertices) + add_edges!(subnetwork, subgraph_edges(graph, subvertices)) + tensors = view(vertex_data(graph), Indices(subvertices)) + copyto!(subnetwork, tensors) + return subnetwork, subvertices +end + +# Internal +index_data(graph::AbstractVertexDataGraph) = vertex_data(graph) + +function Base.show(io::IO, mime::MIME"text/plain", graph::AbstractVertexDataGraph) + println(io, "$(typeof(graph)) with $(nv(graph)) vertices:") + show(io, mime, vertices(graph)) + println(io, "\n") + println(io, "and $(ne(graph)) edge(s):") + for e in edges(graph) + show(io, mime, e) + println(io) + end + println(io) + println(io, "with vertex data:") + show(io, mime, vertex_data(graph)) + return nothing +end + +# =================================== edge data graph ==================================== # + +abstract type AbstractEdgeDataGraph{T, V} <: AbstractVertexOrEdgeDataGraph{T, V} end + +Base.keytype(::Type{<:AbstractEdgeDataGraph{T, V}}) where {T, V} = NamedEdge{V} + +is_vertex_assigned(::AbstractEdgeDataGraph, _vertex) = false + +# `setindex!` +function set_index_data!(graph::AbstractEdgeDataGraph, data, edge::AbstractEdge) + v1 = src(edge) + v2 = dst(edge) + if !has_vertex(graph, v1) + throw(IndexError("Graph does not contain vertex $v1")) + elseif !has_vertex(graph, v2) + throw(IndexError("Graph does not contain vertex $v2")) + end + set_edge_data!(graph, data, edge) + return graph +end + +# `insert!` +function insert!_datagraph(graph::AbstractEdgeDataGraph, edge::AbstractEdge, data) + v1 = src(edge) + v2 = dst(edge) + if has_vertex(graph, v1) && has_vertex(graph, v2) + throw(IndexError("Graph already contains vertices $v1 and $v2")) + end + insert_edge_data!(graph, edge, data) + return graph +end +function insert_edge_data!(graph::AbstractEdgeDataGraph, edge::AbstractEdge, data) + v1 = src(edge) + v2 = dst(edge) + has_vertex(graph, v1) || add_vertex!(graph, v1) + has_vertex(graph, v2) || add_vertex!(graph, v2) + set_edge_data!(graph, data, edge) + return graph +end + +# `delete!` +function delete!_datagraph(graph::AbstractEdgeDataGraph, edge) + if !has_edge(graph, edge) + throw(IndexError("Graph does not contain edge $edge")) + end + rem_edge!(graph, edge) + return graph +end + +# `set!` +function set!_datagraph(graph::AbstractEdgeDataGraph, edge::AbstractEdge, data) + if !has_vertex(graph, src(edge)) || !has_vertex(graph, dst(edge)) + insert!(graph, edge, data) + else + graph[edge] = data + end + return graph +end + +function NamedGraphs.similar_graph( + graph::AbstractEdgeDataGraph, + T::Type + ) + new_graph = similar_graph(graph, T, vertices(graph)) + # we can't generically add edges to an `AbstractEdgeDataGraph`. + return new_graph +end + +# Base method to overload. +function NamedGraphs.similar_graph( + ::AbstractEdgeDataGraph, + T::Type, + vertices::Vertices + ) + return similar_graph(EdgeDataGraph{T}, collect(vertices)) +end + +function NamedGraphs.induced_subgraph_from_vertices( + graph::AbstractEdgeDataGraph, + subvertices + ) + subnetwork = similar_graph(graph, subvertices) + subedges = subgraph_edges(graph, subvertices) + + tensors = view(edge_data(graph), Indices(subedges)) + + copyto!(subnetwork, tensors) + + return subnetwork, subvertices +end + +# Internal +index_data(graph::AbstractEdgeDataGraph) = edge_data(graph) + +function Base.show(io::IO, mime::MIME"text/plain", graph::AbstractEdgeDataGraph) + println(io, "$(typeof(graph)) with $(nv(graph)) vertices:") + show(io, mime, vertices(graph)) + println(io, "\n") + println(io, "and $(ne(graph)) edge(s):") + for e in edges(graph) + show(io, mime, e) + println(io) + end + println(io) + println(io, "with edge data:") + show(io, mime, edge_data(graph)) + return nothing +end diff --git a/src/dataview.jl b/src/dataview.jl index 8eb055f..cd7eb63 100644 --- a/src/dataview.jl +++ b/src/dataview.jl @@ -90,6 +90,9 @@ function Base.setindex!(view::EdgeDataView, vals, keys::Indices{<:Pair}) end function Base.setindex!(view::VertexOrEdgeDataView{K, V}, data::V, key::K) where {K, V} + if !haskey(view, key) + throw(IndexError("Dictionary does not contain index: $key")) + end setindex!(view.graph, data, key) return view end diff --git a/src/edgedatagraph.jl b/src/edgedatagraph.jl new file mode 100644 index 0000000..0dbaf41 --- /dev/null +++ b/src/edgedatagraph.jl @@ -0,0 +1,155 @@ +using Graphs: dst, has_edge, rem_edge!, rem_vertex!, src +using NamedGraphs: NamedEdge, NamedGraph, ordered_vertices, position_graph, vertex_positions + +struct EdgeDataGraph{T, V} <: AbstractEdgeDataGraph{T, V} + underlying_graph::NamedGraph{V} + edge_data::Dictionary{NamedEdge{V}, T} + function EdgeDataGraph{T, V}( + ::UndefInitializer, + vertices + ) where {T, V} + graph = NamedGraph{V}(vertices) + edge_data = Dictionary{NamedEdge{V}, T}() + return new{T, V}(graph, edge_data) + end +end + +Graphs.is_directed(::Type{<:EdgeDataGraph}) = false + +struct EdgeDataDiGraph{T, V} <: AbstractEdgeDataGraph{T, V} + underlying_graph::NamedDiGraph{V} + edge_data::Dictionary{NamedEdge{V}, T} + function EdgeDataDiGraph{T, V}( + ::UndefInitializer, + vertices + ) where {T, V} + graph = NamedDiGraph{V}(vertices) + edge_data = Dictionary{NamedEdge{V}, T}() + return new{T, V}(graph, edge_data) + end +end + +Graphs.is_directed(::Type{<:EdgeDataDiGraph}) = true + +for GType in (:EdgeDataGraph, :EdgeDataDiGraph) + @eval begin + $GType(::UndefInitializer, vertices) = $GType{Any}(undef, vertices) + function $GType{T}(::UndefInitializer, vertices) where {T} + return $GType{T, eltype(vertices)}(undef, vertices) + end + + $GType(data) = $GType{valtype(data)}(data) + $GType{T}(data) where {T} = $GType{T, vertextype(keytype(data))}(data) + + function $GType{T, V}(data) where {T, V} + edges = NamedEdge{V}.(keys(data)) + vertices = union(src.(edges), dst.(edges)) + graph = $GType{T, V}(undef, vertices) + copyto!(graph, data) + return graph + end + + function Base.:(==)(dg1::$GType, dg2::$GType) + return dg1.underlying_graph == dg2.underlying_graph && + dg1.edge_data == dg2.edge_data + end + end +end + +# ====================================== Graphs.jl ======================================= # + +for GType in (:EdgeDataGraph, :EdgeDataDiGraph) + @eval begin + Graphs.edgetype(::Type{<:$GType{T, V}}) where {T, V} = NamedEdge{V} + + function Graphs.add_vertex!(graph::$GType, vertex) + return add_vertex!(graph.underlying_graph, vertex) + end + + function Graphs.add_edge!(graph::$GType, edge) + G = esc($GType) + throw( + ArgumentError( + "cannot add data-free edges to $G; use `insert!`, `setindex!` or `set!` instead" + ) + ) + return nothing + end + + function Graphs.rem_vertex!(graph::$GType, vertex) + for edge in incident_edges(graph, vertex) + unset!(graph.edge_data, edge) + end + rem_vertex!(graph.underlying_graph, vertex) + return graph + end + + function Graphs.rem_edge!(graph::$GType, edge) + unset!(graph.edge_data, edge) + rem_edge!(graph.underlying_graph, edge) + return graph + end + + Graphs.vertices(graph::$GType) = vertices(graph.underlying_graph) + end +end + +# ==================================== NamedGraphs.jl ==================================== # + +for GType in (:EdgeDataGraph, :EdgeDataDiGraph) + @eval begin + function NamedGraphs.vertex_positions(graph::$GType) + return vertex_positions(graph.underlying_graph) + end + + function NamedGraphs.ordered_vertices(graph::$GType) + return ordered_vertices(graph.underlying_graph) + end + + function NamedGraphs.position_graph(graph::$GType) + return position_graph(graph.underlying_graph) + end + + function NamedGraphs.similar_graph(graph::$GType, T::Type, vertices::Vertices) + new_graph = $GType{T}(undef, collect(vertices)) + return new_graph + end + + # We know how to add edges keys for these particurly concrete types + function NamedGraphs.similar_graph(graph::$GType, T::Type) + new_graph = similar_graph(graph, T, vertices(graph)) + add_edges!(new_graph.underlying_graph, edges(graph)) + return new_graph + end + + NamedGraphs.similar_graph(T::Type{<:$GType}, vertices) = T(undef, vertices) + end +end + +# ==================================== DataGraphs.jl ===================================== # + +for GType in (:EdgeDataGraph, :EdgeDataDiGraph) + @eval begin + edge_data_type(::Type{<:$GType{T}}) where {T} = T + + function set_edge_data!(graph::$GType, data, edge) + # Edges `upsert` if vertices are present. + has_edge(graph, edge) || add_edge!(graph.underlying_graph, edge) + set!(graph.edge_data, edge, data) + return graph + end + + get_edge_data(graph::$GType, edge) = graph.edge_data[edge] + + is_vertex_assigned(::$GType, _vertex) = false + is_edge_assigned(graph::$GType, edge) = isassigned(graph.edge_data, edge) + end +end + +# =================================== Dictionaries.jl ==================================== # + +for GType in (:EdgeDataGraph, :EdgeDataDiGraph) + @eval begin + Dictionaries.isinsertable(::$GType) = true + end +end diff --git a/src/vertexdatagraph.jl b/src/vertexdatagraph.jl new file mode 100644 index 0000000..0f83af5 --- /dev/null +++ b/src/vertexdatagraph.jl @@ -0,0 +1,148 @@ +using Dictionaries: Dictionary, set! +using Graphs: Graphs, has_edge, rem_vertex! +using NamedGraphs: + NamedDiGraph, NamedEdge, NamedGraph, ordered_vertices, position_graph, vertex_positions + +struct VertexDataGraph{T, V} <: AbstractVertexDataGraph{T, V} + underlying_graph::NamedGraph{V} + vertex_data::Dictionary{V, T} + function VertexDataGraph{T, V}( + ::UndefInitializer, + vertices + ) where {T, V} + graph = NamedGraph{V}(vertices) + vertex_data = Dictionary{V, T}() + return new{T, V}(graph, vertex_data) + end +end + +struct VertexDataDiGraph{T, V} <: AbstractVertexDataGraph{T, V} + underlying_graph::NamedDiGraph{V} + vertex_data::Dictionary{V, T} + function VertexDataDiGraph{T, V}( + ::UndefInitializer, + vertices + ) where {T, V} + graph = NamedDiGraph{V}(vertices) + vertex_data = Dictionary{V, T}() + return new{T, V}(graph, vertex_data) + end +end + +Graphs.is_directed(::Type{<:VertexDataGraph}) = false +Graphs.is_directed(::Type{<:VertexDataDiGraph}) = true + +for GType in (:VertexDataGraph, :VertexDataDiGraph) + @eval begin + $GType(::UndefInitializer, vertices) = $GType{Any}(undef, vertices) + function $GType{T}(::UndefInitializer, vertices) where {T} + return $GType{T, eltype(vertices)}(undef, vertices) + end + + $GType(data) = $GType{valtype(data)}(data) + $GType{T}(data) where {T} = $GType{T, keytype(data)}(data) + + function $GType{T, V}(data) where {T, V} + vertices = keys(data) + cache = $GType{T, V}(undef, vertices) + return copyto!(cache, data) + end + + function Base.:(==)(dg1::$GType, dg2::$GType) + return dg1.underlying_graph == dg2.underlying_graph && + dg1.vertex_data == dg2.vertex_data + end + end +end + +# ====================================== Graphs.jl ======================================= # + +for GType in (:VertexDataGraph, :VertexDataDiGraph) + @eval begin + Graphs.edgetype(::Type{<:$GType{T, V}}) where {T, V} = NamedEdge{V} + + function Graphs.add_vertex!(graph::$GType, vertex) + return throw( + ArgumentError( + "cannot add data-free vertices to $GType; use `insert!`, `setindex!` or `set!` instead" + ) + ) + end + + function Graphs.add_edge!(graph::$GType, edge::NamedEdge) + return add_edge!(graph.underlying_graph, edge) + end + + function Graphs.rem_vertex!(graph::$GType, vertex) + unset!(graph.vertex_data, vertex) + rem_vertex!(graph.underlying_graph, vertex) + return graph + end + + function Graphs.rem_edge!(graph::$GType, vertex) + rem_edge!(graph.underlying_graph, vertex) + return graph + end + + Graphs.vertices(graph::$GType) = vertices(graph.underlying_graph) + end +end + +# ==================================== NamedGraphs.jl ==================================== # + +for GType in (:VertexDataGraph, :VertexDataDiGraph) + @eval begin + function NamedGraphs.vertex_positions(graph::$GType) + return vertex_positions(graph.underlying_graph) + end + + function NamedGraphs.ordered_vertices(graph::$GType) + return ordered_vertices(graph.underlying_graph) + end + + function NamedGraphs.position_graph(graph::$GType) + return position_graph(graph.underlying_graph) + end + + function NamedGraphs.similar_graph(graph::$GType, T::Type, vertices::Vertices) + new_graph = $GType{T}(undef, collect(vertices)) + return new_graph + end + + NamedGraphs.similar_graph(T::Type{<:$GType}, vertices) = T(undef, vertices) + end +end + +# ==================================== DataGraphs.jl ===================================== # + +for GType in (:VertexDataGraph, :VertexDataDiGraph) + @eval begin + vertex_data_type(::Type{<:$GType{T}}) where {T} = T + + function set_vertex_data!(graph::$GType, data, vertex) + # We use an upsert here as we have already checked if the vertex (i.e. key) exists, + # but it might not exist in the internal `Dictionary`, so add it if not. + set!(graph.vertex_data, vertex, data) + return graph + end + + get_vertex_data(graph::$GType, vertex) = graph.vertex_data[vertex] + + is_vertex_assigned(graph::$GType, vertex) = isassigned(graph.vertex_data, vertex) + is_edge_assigned(::$GType, _edge) = false + end +end + +# =================================== Dictionaries.jl ==================================== # + +for GType in (:VertexDataGraph, :VertexDataDiGraph) + @eval begin + Dictionaries.isinsertable(::$GType) = true + + function insert_vertex_data!(graph::$GType, vertex, data) + add_vertex!(graph.underlying_graph, vertex) + insert!(graph.vertex_data, vertex, data) + return graph + end + end +end diff --git a/test/test_basics.jl b/test/test_basics.jl index 727da8d..7d3fc77 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -620,8 +620,6 @@ using Test: @test, @test_broken, @testset @test g_copy["a" => "b"] == -1.0 @test g_copy["b" => "c"] == -2.0 - @test_throws IndexError copyto!(empty_graph(g_copy), g_copy) - g2 = similar_graph(g, ["u", "v"]) @test similar_graph(g2) isa typeof(g) @test has_vertex(g2, "u") diff --git a/test/test_vertexoredgedatagraph.jl b/test/test_vertexoredgedatagraph.jl new file mode 100644 index 0000000..778fad6 --- /dev/null +++ b/test/test_vertexoredgedatagraph.jl @@ -0,0 +1,510 @@ +using DataGraphs: DataGraphs, EdgeDataDiGraph, EdgeDataGraph, EdgeDataView, + VertexDataDiGraph, VertexDataGraph, VertexDataView, edge_data, edge_data_type, + underlying_graph, vertex_data, vertex_data_type +using Dictionaries: + AbstractDictionary, Dictionary, IndexError, Indices, isinsertable, issettable, set! +using Graphs: AbstractGraph, AbstractSimpleGraph, add_edge!, add_vertex!, dst, edges, + edgetype, has_edge, has_vertex, is_directed, ne, nv, rem_edge!, rem_vertex!, src, + vertices +using NamedGraphs.GraphsExtensions: add_edge, subgraph, vertextype +using NamedGraphs: NamedDiGraph, NamedEdge, NamedGraph, ordered_vertices, position_graph, + similar_graph, vertex_positions +using Test: @test, @test_throws, @testset + +@testset "VertexDataGraph and EdgeDataGraph" begin + @testset "$GType basics" for GType in ( + VertexDataGraph{String, Int}, + VertexDataDiGraph{String, Int}, + ) + @testset "undef constructor" begin + g = GType(undef, [1, 2, 3]) + @test g isa GType + @test nv(g) == 3 + @test ne(g) == 0 + @test has_vertex(g, 1) + @test has_vertex(g, 2) + @test has_vertex(g, 3) + @test !has_vertex(g, 4) + @test Set(collect(vertices(g))) == Set([1, 2, 3]) + end + + @testset "data constructor" begin + data = Dictionary([1, 2, 3], ["V1", "V2", "V3"]) + g = GType(data) + @test g isa GType + @test nv(g) == 3 + @test isassigned(g, 1) + @test isassigned(g, 2) + @test isassigned(g, 3) + @test g[1] == "V1" + @test g[2] == "V2" + @test g[3] == "V3" + + data = Dictionary([1.0, 2.0, 3.0], SubString.(["V1", "V2", "V3"])) + g = GType(data) + @test g isa GType + end + + @testset "copy" begin + data = Dictionary([1, 2, 3], ["V1", "V2", "V3"]) + g = GType(data) + add_edge!(g, NamedEdge(1, 2)) + g_copy = copy(g) + + @test g_copy == g + @test g_copy !== g + + # Test we can copy a graph with undefined data. + g = GType(undef, [1, 2, 3]) + g[1] = "V1" + add_edge!(g, NamedEdge(2, 3)) + g_copy = copy(g) + + @test has_vertex(g_copy, 1) + @test has_vertex(g_copy, 2) + @test has_vertex(g_copy, 3) + @test has_edge(g_copy, 2 => 3) + @test isassigned(g_copy, 1) + @test g_copy[1] == "V1" + @test !isassigned(g_copy, 2) + @test !isassigned(g_copy, 3) + end + + @testset "Graphs.jl interface" begin + g = GType(undef, [1, 2, 3]) + @test vertextype(g) == Int + @test edgetype(g) == NamedEdge{Int} + + add_edge!(g, NamedEdge(1, 2)) + add_edge!(g, NamedEdge(2, 3)) + @test ne(g) == 2 + @test has_edge(g, NamedEdge(1, 2)) + @test has_edge(g, NamedEdge(2, 3)) + @test !has_edge(g, NamedEdge(1, 3)) + @test length(collect(edges(g))) == 2 + + rem_edge!(g, NamedEdge(1, 2)) + @test ne(g) == 1 + @test !has_edge(g, NamedEdge(1, 2)) + end + + @testset "rem_vertex!" begin + g = GType(undef, [1, 2, 3]) + add_edge!(g, NamedEdge(1, 2)) + rem_vertex!(g, 1) + @test !has_vertex(g, 1) + @test nv(g) == 2 + @test ne(g) == 0 + end + + @testset "DataGraphs interface" begin + g = GType(undef, [1, 2, 3]) + @test vertex_data_type(g) == String + @test vertex_data_type(GType) == String + @test !isassigned(g, 1) + @test !isassigned(g, 2) + @test !isassigned(g, 3) + add_edge!(g, NamedEdge(1, 2)) + @test !isassigned(g, 1 => 2) + end + + @testset "setindex! and getindex" begin + g = GType(undef, [1, 2, 3]) + g[1] = "V1" + g[2] = "V2" + g[3] = "V3" + @test isassigned(g, 1) + @test isassigned(g, 2) + @test isassigned(g, 3) + @test g[1] == "V1" + @test g[2] == "V2" + @test g[3] == "V3" + end + + @testset "NamedGraphs interface" begin + g = GType(undef, [1, 2, 3]) + @test position_graph(g) isa AbstractSimpleGraph{Int} + @test ordered_vertices(g) == [1, 2, 3] + @test keys(vertex_positions(g)) == vertices(g) + + g = add_edge(g, NamedEdge(1, 2)) + g[1] = "1" + + gs = similar_graph(g) + @test gs isa GType + @test has_vertex(gs, 1) + @test has_vertex(gs, 2) + @test has_vertex(gs, 3) + @test has_edge(gs, 1 => 2) + @test !isassigned(gs, 1) + + gs = similar_graph(g, vertices(g)) + @test vertices(gs) == vertices(g) + @test ne(gs) == 0 + @test !isassigned(gs, 1) + + gs = similar_graph(g, [1, 2, 4]) + @test has_vertex(gs, 1) + @test has_vertex(gs, 2) + @test has_vertex(gs, 4) + @test nv(gs) == 3 + @test ne(gs) == 0 + @test !isassigned(gs, 1) + + gs = similar_graph(g, Char) + @test vertex_data_type(gs) === Char + @test nv(gs) == 3 + @test ne(gs) == 1 + gs[1] = 'C' + @test gs[1] == 'C' + + gs = similar_graph(g, Float64, vertices(g)) + @test ne(gs) == 0 + + gs = similar_graph(GType, [1.0, 2.0]) + @test gs isa GType + @test has_vertex(gs, 1) + @test has_vertex(gs, 2) + @test ne(gs) == 0 + + g = GType(Dictionary([1, 2, 3], ["V1", "V2", "V3"])) + add_edge!(g, NamedEdge(1, 2)) + add_edge!(g, NamedEdge(2, 3)) + sg = subgraph(g, [1, 2]) + @test sg isa GType + @test has_vertex(sg, 1) + @test has_vertex(sg, 2) + @test !has_vertex(sg, 3) + @test has_edge(sg, 1 => 2) + @test !has_edge(sg, 2 => 3) + @test sg[1] == "V1" + @test sg[2] == "V2" + @test !isassigned(sg, 3) + end + + @testset "Dictionaries interface" begin + g = GType(undef, [1, 2, 3]) + @test keytype(g) == Int + @test valtype(g) == String + @test keys(g) isa Indices + @test length(g) == 3 + @test vertex_data(g) isa VertexDataView + + @test issettable(g) + @test isinsertable(g) + + insert!(g, 4, "V4") + @test_throws IndexError insert!(g, 4, "V4_again") + @test has_vertex(g, 4) + @test isassigned(g, 4) + @test g[4] == "V4" + @test nv(g) == 4 + + @test_throws IndexError g[5] = "V5" + @test !has_vertex(g, 5) + @test !isassigned(g, 5) + + set!(g, 5, "V5") + @test has_vertex(g, 5) + @test g[5] == "V5" + + g[5] = "V5_again" + @test g[5] == "V5_again" + @test nv(g) == 5 + end + + @testset "show" begin + g = GType(Dictionary([1, 2, 3], ["V1", "V2", "V3"])) + io = IOBuffer() + show(io, g) + str = String(take!(io)) + @test occursin("$GType", str) + @test occursin("V1", str) + @test occursin("V2", str) + @test occursin("V3", str) + @test !occursin("edge data", str) + @test occursin("vertex data", str) + end + end + + @testset "$GType basics" for GType in + (EdgeDataGraph{String, Int}, EdgeDataDiGraph{String, Int}) + @testset "undef constructor" begin + g = GType(undef, [1, 2, 3]) + @test g isa GType + @test nv(g) == 3 + @test ne(g) == 0 + end + + @testset "data constructor" begin + data = Dictionary([NamedEdge(1, 2), NamedEdge(2, 3)], ["E12", "E23"]) + g = GType(data) + @test g isa GType + @test nv(g) == 3 + @test ne(g) == 2 + @test isassigned(g, NamedEdge(1, 2)) + @test isassigned(g, NamedEdge(2, 3)) + @test g[NamedEdge(1, 2)] == "E12" + @test g[NamedEdge(2, 3)] == "E23" + + # With pairs + data = Dictionary([1.0 => 2.0, 2.0 => 3.0], ["E12", "E23"]) + g = GType(data) + @test g isa GType + @test nv(g) == 3 + @test ne(g) == 2 + @test isassigned(g, NamedEdge(1, 2)) + @test isassigned(g, NamedEdge(2, 3)) + @test g[NamedEdge(1, 2)] == "E12" + @test g[NamedEdge(2, 3)] == "E23" + end + + @testset "copy" begin + data = Dictionary([1 => 2, 2 => 3], ["E12", "E23"]) + g = GType(data) + g_copy = copy(g) + + @test g_copy == g + @test g_copy !== g + + # Test we can copy a graph with undefined data. + g = GType(undef, [1, 2, 3]) + g[1 => 2] = "E12" + g_copy = copy(g) + + @test has_vertex(g_copy, 1) + @test has_vertex(g_copy, 2) + @test has_vertex(g_copy, 3) + @test has_edge(g_copy, 1 => 2) + @test isassigned(g_copy, 1 => 2) + @test g_copy[1 => 2] == "E12" + @test !isassigned(g_copy, 2 => 3) + end + + @testset "Graphs.jl interface" begin + g = GType(undef, [1, 2, 3]) + @test has_vertex(g, 1) + @test has_vertex(g, 2) + @test !has_vertex(g, 4) + @test edgetype(g) == NamedEdge{Int} + + @test_throws ArgumentError add_edge!(g, NamedEdge(1, 2)) + @test_throws ArgumentError add_edge!(g, 2 => 3) + + add_vertex!(g, 4) + @test has_vertex(g, 4) + @test nv(g) == 4 + end + + @testset "DataGraphs interface" begin + g = GType(undef, [1, 2, 3]) + @test edge_data_type(GType) == String + @test edge_data_type(g) == String + @test !isassigned(g, 1) + @test !isassigned(g, NamedEdge(1, 2)) + end + + @testset "setindex! and getindex" begin + data = Dictionary([NamedEdge(1, 2), NamedEdge(2, 3)], ["E12", "E23"]) + g = GType(data) + g[NamedEdge(1, 2)] = "E12_updated" + @test g[NamedEdge(1, 2)] == "E12_updated" + @test g[NamedEdge(2, 3)] == "E23" + end + + @testset "rem_edge!" begin + data = Dictionary([NamedEdge(1, 2), NamedEdge(2, 3)], ["E12", "E23"]) + g = GType(data) + rem_edge!(g, NamedEdge(1, 2)) + @test !has_edge(g, NamedEdge(1, 2)) + @test ne(g) == 1 + end + + @testset "rem_vertex!" begin + data = Dictionary([NamedEdge(1, 2), NamedEdge(2, 3)], ["E12", "E23"]) + g = GType(data) + rem_vertex!(g, 1) + @test !has_vertex(g, 1) + @test !has_edge(g, NamedEdge(1, 2)) + @test !isassigned(g, NamedEdge(1, 2)) + @test ne(g) == 1 + @test g[NamedEdge(2, 3)] == "E23" + end + + @testset "NamedGraphs interface" begin + g = GType(Dictionary([1 => 2, 2 => 3], ["E12", "E23"])) + @test issetequal(vertices(g), [1, 2, 3]) + @test position_graph(g) isa AbstractSimpleGraph{Int} + @test ordered_vertices(g) isa AbstractVector + @test vertex_positions(g) isa AbstractDictionary + + gs = similar_graph(g) + @test gs isa GType + @test has_vertex(gs, 1) + @test has_vertex(gs, 2) + @test has_vertex(gs, 3) + @test has_edge(gs, 1 => 2) + @test !isassigned(gs, 1 => 2) + + gs = similar_graph(g, vertices(g)) + @test vertices(gs) == vertices(g) + @test ne(gs) == 0 + @test !isassigned(gs, 1 => 2) + + gs = similar_graph(g, [1, 2, 4]) + @test has_vertex(gs, 1) + @test has_vertex(gs, 2) + @test has_vertex(gs, 4) + @test nv(gs) == 3 + @test ne(gs) == 0 + @test !isassigned(gs, 1 => 2) + + gs = similar_graph(g, Char) + @test vertex_data_type(gs) === Char + @test nv(gs) == 3 + @test ne(gs) == 2 + gs[3 => 1] = 'C' + @test gs[3 => 1] == 'C' + @test ne(gs) == 3 + + gs = similar_graph(g, Float64, vertices(g)) + @test ne(gs) == 0 + + gs = similar_graph(GType, [1.0, 2.0]) + @test gs isa GType + @test has_vertex(gs, 1) + @test has_vertex(gs, 2) + @test ne(gs) == 0 + + g = GType(Dictionary([1 => 2, 2 => 3], ["E12", "E23"])) + sg = subgraph(g, [1, 2]) + @test sg isa GType + @test has_vertex(sg, 1) + @test has_vertex(sg, 2) + @test !has_vertex(sg, 3) + @test has_edge(sg, 1 => 2) + @test !has_edge(sg, 2 => 3) + @test sg[1 => 2] == "E12" + end + + @testset "Dictionaries interface" begin + g = GType(undef, [1, 2, 3]) + @test keytype(g) == NamedEdge{Int} + @test valtype(g) == String + @test edge_data(g) isa EdgeDataView + + @test issettable(g) + @test isinsertable(g) + + g = GType(undef, [1.0, 2.0, 3.0]) + @test keytype(g) == NamedEdge{Int} + + g = GType(undef, [1, 2, 3]) + insert!(g, 3 => 4, "E34") + @test has_vertex(g, 4) + @test has_edge(g, 3 => 4) + @test isassigned(g, 3 => 4) + @test g[3 => 4] == "E34" + + g = GType(undef, [1, 2, 3]) + insert!(g, 5 => 6, "E56") + @test has_vertex(g, 5) + @test has_vertex(g, 6) + @test has_edge(g, 5 => 6) + @test isassigned(g, 5 => 6) + @test g[5 => 6] == "E56" + + g = GType(undef, [1, 2, 3]) + @test_throws IndexError insert!(g, 2 => 3, "E23") + @test !has_edge(g, 2 => 3) + + g = GType(undef, [1, 2, 3]) + set!(g, 1 => 2, "E12") + @test has_edge(g, 1 => 2) + @test g[1 => 2] == "E12" + + g = GType(undef, [1, 2, 3]) + set!(g, 3 => 4, "E34") + @test has_vertex(g, 4) + @test has_edge(g, 3 => 4) + @test g[3 => 4] == "E34" + + g = GType(undef, [1, 2, 3]) + set!(g, 4 => 5, "E45") + @test has_vertex(g, 4) + @test has_vertex(g, 5) + @test has_edge(g, 4 => 5) + @test g[4 => 5] == "E45" + set!(g, 4 => 5, "E45_again") + @test g[4 => 5] == "E45_again" + + g = GType(undef, [1, 2, 3]) + g[2 => 3] = "E23" + @test has_edge(g, 2 => 3) + @test g[2 => 3] == "E23" + @test_throws IndexError g[3 => 4] = "E34" + @test_throws IndexError g[4 => 5] = "E45" + + g = EdgeDataGraph{String, Int}(undef, [1, 2, 3]) + @test_throws IndexError edge_data(g)[2 => 3] = "E23" + end + + @testset "show" begin + g = GType(Dictionary([1 => 2, 2 => 3], ["E12", "E23"])) + io = IOBuffer() + show(io, g) + str = String(take!(io)) + @test occursin("$GType", str) + @test occursin("edge data", str) + @test occursin("E12", str) + @test occursin("E23", str) + @test !occursin("vertex data", str) + end + end + + @testset "(un)directed graph specific" begin + @testset "basics" begin + g = VertexDataGraph(undef, [1, 2, 3]) + @test !is_directed(VertexDataGraph) + @test !is_directed(g) + + g = VertexDataDiGraph(undef, [1, 2, 3]) + @test is_directed(VertexDataDiGraph) + @test is_directed(g) + + add_edge!(g, NamedEdge(1, 2)) + @test has_edge(g, NamedEdge(1, 2)) + @test !has_edge(g, NamedEdge(2, 1)) + @test ne(g) == 1 + + add_edge!(g, NamedEdge(2, 1)) + @test has_edge(g, NamedEdge(2, 1)) + @test ne(g) == 2 + + g = EdgeDataGraph(undef, [1, 2, 3]) + @test !is_directed(VertexDataGraph) + @test !is_directed(g) + + g = EdgeDataDiGraph(undef, [1, 2, 3]) + @test is_directed(VertexDataDiGraph) + @test is_directed(g) + end + + @testset "undirected edge access" begin + data = Dictionary([NamedEdge(1, 2)], ["E12"]) + g = EdgeDataGraph(data) + @test g[NamedEdge(1, 2)] == "E12" + @test g[NamedEdge(2, 1)] == "E12" + @test g[1 => 2] == "E12" + @test g[2 => 1] == "E12" + end + + @testset "directed edge access" begin + data = Dictionary([NamedEdge(1, 2)], ["E12"]) + g = EdgeDataDiGraph(data) + @test has_edge(g, NamedEdge(1, 2)) + @test !has_edge(g, NamedEdge(2, 1)) + @test g[NamedEdge(1, 2)] == "E12" + end + end +end