From a06ca9c8488a2210ff86c1779567fa46d1e7e786 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Tue, 2 Dec 2025 19:12:43 -0500 Subject: [PATCH 01/12] Refactor interface to make more abstract; have `AbstractDataGraph` subtype `AbstractNamedGraph` - Interface now no longer assumes vertex and edge date stored as mutable field. Instead, requires overloading setters and getters. - Subtyping `AbstractNamedGraph` cuts down on the method forwarding almost entirely. --- src/abstractdatagraph.jl | 220 ++++++++++++++++----------------------- src/datagraph.jl | 77 ++++++++++++-- test/test_basics.jl | 12 ++- 3 files changed, 165 insertions(+), 144 deletions(-) diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index 46430b4..7e05d4f 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -1,8 +1,7 @@ -using Dictionaries: set!, unset! +using Dictionaries: set!, unset!, Indices using Graphs: Graphs, AbstractEdge, - AbstractGraph, IsDirected, add_edge!, a_star, @@ -11,29 +10,67 @@ using Graphs: nv, steiner_tree, vertices +using NamedGraphs: NamedGraphs, AbstractNamedGraph, AbstractNamedEdge using NamedGraphs.GraphsExtensions: GraphsExtensions, arrange_edge, incident_edges, is_edge_arranged, vertextype using NamedGraphs.SimilarType: similar_type using SimpleTraits: SimpleTraits, Not, @traitfn -abstract type AbstractDataGraph{V, VD, ED} <: AbstractGraph{V} end +abstract type AbstractDataGraph{V, VD, ED} <: AbstractNamedGraph{V} end # Minimal interface # TODO: Define for `AbstractGraph` as a `DataGraphInterface`. underlying_graph(::AbstractDataGraph) = not_implemented() -underlying_graph_type(::Type{<:AbstractDataGraph}) = not_implemented() -vertex_data(::AbstractDataGraph) = not_implemented() -vertex_data_eltype(::Type{<:AbstractDataGraph}) = not_implemented() -edge_data(::AbstractDataGraph) = not_implemented() -edge_data_eltype(::Type{<:AbstractDataGraph}) = not_implemented() + +has_vertex_data(::AbstractDataGraph, vertex) = not_implemented() +has_edge_data(::AbstractDataGraph, edge) = not_implemented() + +get_vertex_data(::AbstractDataGraph, vertex) = not_implemented() +get_edge_data(::AbstractDataGraph, edge) = not_implemented() + +set_vertex_data!(::AbstractDataGraph, data, vertex) = not_implemented() +set_edge_data!(::AbstractDataGraph, data, edge) = not_implemented() + +unset_vertex_data!(::AbstractDataGraph, data, vertex) = not_implemented() +unset_edge_data!(::AbstractDataGraph, data, edge) = not_implemented() + +GraphsExtensions.rename_vertices(f, ::AbstractDataGraph) = not_implemented() + +# Quasi-derived interface; only required if inference fails + +underlying_graph_type(T::Type{<:AbstractDataGraph}) = Base.promote_op(underlying_graph, T) +vertex_data_eltype(T::Type{<:AbstractDataGraph}) = eltype(Base.promote_op(vertex_data, T)) +edge_data_eltype(T::Type{<:AbstractDataGraph}) = eltype(Base.promote_op(edge_data, T)) # Derived interface + +Graphs.has_vertex(g::AbstractDataGraph, vertex) = has_vertex(underlying_graph(g), vertex) +Graphs.has_edge(g::AbstractDataGraph, edge) = has_edge(underlying_graph(g), edge) + +# For method ambiguities +Graphs.has_vertex(g::AbstractDataGraph, vertex) = has_vertex(underlying_graph(g), vertex) +Graphs.has_edge(g::AbstractDataGraph, edge::AbstractNamedEdge) = has_edge(underlying_graph(g), edge) + +vertex_data(dg::AbstractDataGraph) = map(v -> dg[v], assigned_vertices(dg)) +edge_data(dg::AbstractDataGraph) = map(v -> dg[v], assigned_edges(dg)) + +function assigned_vertices(graph::AbstractDataGraph) + return Indices(filter(v -> isassigned(graph, v), vertices(graph))) +end +function assigned_edges(graph::AbstractDataGraph) + return Indices(filter(e -> isassigned(graph, e), edges(graph))) +end + +function Graphs.edgetype(graph::AbstractDataGraph) + return Graphs.edgetype(underlying_graph(graph)) +end function Graphs.edgetype(graph_type::Type{<:AbstractDataGraph}) - return Graphs.edgetype(underlying_graph_type(graph_type)) + return Base.promote_op(edgetype, graph_type) end function Graphs.is_directed(graph_type::Type{<:AbstractDataGraph}) return Graphs.is_directed(underlying_graph_type(graph_type)) end + underlying_graph_type(graph::AbstractDataGraph) = typeof(underlying_graph(graph)) vertex_data_eltype(graph::AbstractDataGraph) = eltype(vertex_data(graph)) edge_data_eltype(graph::AbstractDataGraph) = eltype(edge_data(graph)) @@ -44,75 +81,25 @@ Base.zero(graph_type::Type{<:AbstractDataGraph}) = graph_type() function Graphs.vertices(graph::AbstractDataGraph) return Graphs.vertices(underlying_graph(graph)) end +function Graphs.add_vertex!(graph::AbstractDataGraph, vertex) + return Graphs.add_vertex!(underlying_graph(graph), vertex) +end -# Graphs overloads +# Simple NamedGraphs overloads for f in [ - :(Graphs.a_star), - :(Graphs.add_edge!), - :(Graphs.add_vertex!), - :(Graphs.adjacency_matrix), - :(Graphs.bellman_ford_shortest_paths), - :(Graphs.bfs_parents), - :(Graphs.bfs_tree), - :(Graphs.boruvka_mst), - :(Graphs.center), - :(Graphs.common_neighbors), - :(Graphs.connected_components), - :(Graphs.degree), - :(Graphs.degree_histogram), - :(Graphs.desopo_pape_shortest_paths), - :(Graphs.dfs_parents), - :(Graphs.dfs_tree), - :(Graphs.diameter), - :(Graphs.dijkstra_shortest_paths), - :(Graphs.eccentricity), - :(Graphs.edges), - :(Graphs.edgetype), - :(Graphs.eltype), - :(Graphs.enumerate_paths), - :(Graphs.floyd_warshall_shortest_paths), - :(Graphs.has_edge), - :(Graphs.has_path), - :(Graphs.has_vertex), - :(Graphs.inneighbors), - :(Graphs.is_connected), - :(Graphs.is_cyclic), - :(Graphs.is_directed), - :(Graphs.is_strongly_connected), - :(Graphs.is_weakly_connected), - :(Graphs.mincut), - :(Graphs.ne), - :(Graphs.neighbors), - :(Graphs.neighborhood), - :(Graphs.neighborhood_dists), - :(Graphs.johnson_shortest_paths), - :(Graphs.spfa_shortest_paths), - :(Graphs.yen_k_shortest_paths), - :(Graphs.kruskal_mst), - :(Graphs.prim_mst), - :(Graphs.nv), - :(Graphs.outneighbors), - :(Graphs.periphery), - :(Graphs.radius), - :(Graphs.steiner_tree), - :(Graphs.topological_sort_by_dfs), - :(Graphs.tree), - :(GraphsExtensions.boundary_edges), - :(GraphsExtensions.boundary_vertices), - :(GraphsExtensions.eccentricities), - :(GraphsExtensions.inner_boundary_vertices), - :(GraphsExtensions.mincut_partitions), - :(GraphsExtensions.outer_boundary_vertices), - :(GraphsExtensions.symrcm_perm), - :(GraphsExtensions.symrcm_permute), + :(NamedGraphs.ordered_vertices), + :(NamedGraphs.vertex_positions), + :(NamedGraphs.position_graph), ] @eval begin - function $f(graph::AbstractDataGraph, args...; kwargs...) - return $f(underlying_graph(graph), args...; kwargs...) - end + $f(graph::AbstractDataGraph) = $f(underlying_graph(graph)) end end +# These cannot be known abstractly. +GraphsExtensions.directed_graph_type(::AbstractDataGraph) = not_implemented() +GraphsExtensions.undirected_graph_type(::AbstractDataGraph) = not_implemented() + # Fix for ambiguity error with `AbstractGraph` version function Graphs.degree(graph::AbstractDataGraph, vertex::Integer) return Graphs.degree(underlying_graph(graph), vertex) @@ -248,37 +235,18 @@ function Base.union( return union(union(graph1, graph2; kwargs...), graph3, graphs_tail...; kwargs...) end -function GraphsExtensions.rename_vertices(f::Function, graph::AbstractDataGraph) - renamed_underlying_graph = GraphsExtensions.rename_vertices(f, underlying_graph(graph)) - # TODO: Base the ouput type on `typeof(graph)`, for example: - # convert_vertextype(eltype(renamed_vertices), typeof(graph))(renamed_underlying_graph) - renamed_graph = DataGraph( - renamed_underlying_graph; - vertex_data_eltype = vertex_data_eltype(graph), - edge_data_eltype = edge_data_eltype(graph), - ) - for v in keys(vertex_data(graph)) - renamed_graph[f(v)] = graph[v] - end - for e in keys(edge_data(graph)) - renamed_graph[GraphsExtensions.rename_vertices(f, e)] = graph[e] - end - return renamed_graph -end - function Graphs.rem_vertex!(graph::AbstractDataGraph, vertex) neighbor_edges = incident_edges(graph, vertex) - # unset!(vertex_data(graph), to_vertex(graph, vertex...)) - unset!(vertex_data(graph), vertex) + unset_vertex_data!(vertex_data(graph), vertex) for neighbor_edge in neighbor_edges - unset!(edge_data(graph), neighbor_edge) + unset_edge_data!(edge_data(graph), neighbor_edge) end Graphs.rem_vertex!(underlying_graph(graph), vertex) return graph end function Graphs.rem_edge!(graph::AbstractDataGraph, edge) - unset!(edge_data(graph), edge) + unset_edge_data!(edge_data(graph), edge) Graphs.rem_edge!(underlying_graph(graph), edge) return graph end @@ -302,23 +270,23 @@ function Graphs.dfs_tree(graph::AbstractDataGraph, s::Integer; kwargs...) end function map_vertex_data(f, graph::AbstractDataGraph; vertices = nothing) - graph′ = copy(graph) + new_graph = copy(graph) vs = isnothing(vertices) ? Graphs.vertices(graph) : vertices for v in vs - graph′[v] = f(graph[v]) + new_graph[v] = f(graph[v]) end - return graph′ + return new_graph end function map_edge_data(f, graph::AbstractDataGraph; edges = nothing) - graph′ = copy(graph) + new_graph = copy(graph) es = isnothing(edges) ? Graphs.edges(graph) : edges for e in es if isassigned(graph, e) - graph′[e] = f(graph[e]) + new_graph[e] = f(graph[e]) end end - return graph′ + return new_graph end function map_data(f, graph::AbstractDataGraph; vertices = nothing, edges = nothing) @@ -326,20 +294,27 @@ function map_data(f, graph::AbstractDataGraph; vertices = nothing, edges = nothi return map_edge_data(f, graph; edges) end -function Base.getindex(graph::AbstractDataGraph, vertex) - return vertex_data(graph)[vertex] -end +Base.getindex(graph::AbstractDataGraph, vertex) = get_vertex_data(graph, vertex) -function Base.get(graph::AbstractDataGraph, vertex, default) - return get(vertex_data(graph), vertex, default) +Base.get!(graph::AbstractGraph, key, default) = get!(() -> default, graph, key) +function Base.get!(default::Base.Callable, graph::AbstractGraph, key) + if isassigned(graph, key) + return graph[key] + else + return graph[key] = default() + end end - -function Base.get!(graph::AbstractDataGraph, vertex, default) - return get!(vertex_data(graph), vertex, default) +Base.get(graph::AbstractGraph, key, default) = get(() -> default, graph, key) +function Base.get(default::Base.Callable, graph::AbstractGraph, key) + if isassigned(graph, key) + return graph[key] + else + return default() + end end function Base.getindex(graph::AbstractDataGraph, edge::AbstractEdge) - data = edge_data(graph)[arrange_edge(graph, edge)] + data = get_edge_data(graph, arrange_edge(graph, edge)) return reverse_data_direction(graph, edge, data) end @@ -348,50 +323,29 @@ function Base.getindex(graph::AbstractDataGraph, edge::Pair) return graph[edgetype(graph)(edge)] end -function Base.get(graph::AbstractDataGraph, edge::AbstractEdge, default) - data = get(edge_data(graph), arrange_edge(graph, edge), default) - return reverse_data_direction(graph, edge, data) -end - -function Base.get(graph::AbstractDataGraph, edge::Pair, default) - return get(graph, edgetype(graph)(edge), default) -end - -function Base.get!(graph::AbstractDataGraph, edge::AbstractEdge, default) - data = get!(edge_data(graph), arrange_edge(graph, edge), default) - return reverse_data_direction(graph, edge, data) -end - -function Base.get!(graph::AbstractDataGraph, edge::Pair, default) - return get!(graph, edgetype(graph)(edge), default) -end - # Support syntax `g[1, 2] = g[(1, 2)]` function Base.getindex(graph::AbstractDataGraph, i1, i2, i...) return graph[(i1, i2, i...)] end -function Base.isassigned(graph::AbstractDataGraph, vertex) - return isassigned(vertex_data(graph), vertex) -end +Base.isassigned(graph::AbstractDataGraph, vertex) = has_vertex_data(graph, vertex) function Base.isassigned(graph::AbstractDataGraph, edge::AbstractEdge) - return isassigned(edge_data(graph), arrange_edge(graph, edge)) + return has_edge_data(graph, arrange_edge(graph, edge)) end - function Base.isassigned(graph::AbstractDataGraph, edge::Pair) return isassigned(graph, edgetype(graph)(edge)) end function Base.setindex!(graph::AbstractDataGraph, data, vertex) - set!(vertex_data(graph), vertex, data) + set_vertex_data!(graph, data, vertex) return graph end function Base.setindex!(graph::AbstractDataGraph, data, edge::AbstractEdge) arranged_edge = arrange_edge(graph, edge) arranged_data = reverse_data_direction(graph, edge, data) - set!(edge_data(graph), arranged_edge, arranged_data) + set_edge_data!(graph, arranged_data, arranged_edge) return graph end diff --git a/src/datagraph.jl b/src/datagraph.jl index 6317224..29ba363 100644 --- a/src/datagraph.jl +++ b/src/datagraph.jl @@ -1,7 +1,14 @@ using Dictionaries: Dictionary -using Graphs: Graphs, edgetype +using Graphs: Graphs, edgetype, has_edge, has_vertex using Graphs.SimpleGraphs: SimpleGraph -using NamedGraphs.GraphsExtensions: convert_vertextype, directed_graph, vertextype +using NamedGraphs: GenericNamedGraph +using NamedGraphs.GraphsExtensions: + convert_vertextype, + directed_graph, + vertextype, + directed_graph_type, + graph_from_vertices, + rename_vertices # TODO: define VertexDataGraph, a graph with only data on the # vertices, and EdgeDataGraph, a graph with only data on the edges. @@ -26,15 +33,69 @@ struct DataGraph{V, VD, ED, G <: AbstractGraph, E <: AbstractEdge} <: AbstractDa ) end end + +# Interface +underlying_graph(graph::DataGraph) = getfield(graph, :underlying_graph) + +has_vertex_data(dg::DataGraph, vertex) = haskey(dg.vertex_data, vertex) +has_edge_data(dg::DataGraph, edge) = haskey(dg.edge_data, edge) + +get_vertex_data(dg::DataGraph, vertex) = dg.vertex_data[vertex] +get_edge_data(dg::DataGraph, edge) = dg.edge_data[edge] + +function set_vertex_data!(dg::DataGraph, data, vertex) + set!(dg.vertex_data, vertex, data) + return dg +end +function set_edge_data!(dg::DataGraph, data, edge) + set!(dg.edge_data, edge, data) + return dg +end + +function unset_vertex_data!(dg::DataGraph, vertex) + unset!(dg.vertex_data, vertex) + return dg +end +function unset_edge_data!(dg::DataGraph, edge) + unset!(dg.edge_data, edge) + return dg +end + underlying_graph_type(G::Type{<:DataGraph}) = fieldtype(G, :underlying_graph) vertex_data_eltype(G::Type{<:DataGraph}) = eltype(fieldtype(G, :vertex_data)) edge_data_eltype(G::Type{<:DataGraph}) = eltype(fieldtype(G, :edge_data)) -underlying_graph(graph::DataGraph) = getfield(graph, :underlying_graph) -vertex_data(graph::DataGraph) = getfield(graph, :vertex_data) -edge_data(graph::DataGraph) = getfield(graph, :edge_data) -# TODO: Is this needed? Maybe define a generic `AbstractDataGraph` version. -Graphs.is_directed(G::Type{<:DataGraph}) = Graphs.is_directed(underlying_graph_type(G)) +# Extras + +Graphs.edgetype(T::Type{<:DataGraph}) = keytype(fieldtype(T, :edge_data)) + +function GraphsExtensions.graph_from_vertices(T::Type{<:DataGraph}, vertices) + return T(graph_from_vertices(underlying_graph_type(T), vertices)) +end + +function GraphsExtensions.rename_vertices(f::Function, graph::DataGraph) + renamed_underlying_graph = GraphsExtensions.rename_vertices(f, underlying_graph(graph)) + + renamed_graph = DataGraph( + renamed_underlying_graph; + vertex_data_eltype = vertex_data_eltype(graph), + edge_data_eltype = edge_data_eltype(graph), + ) + + for v in vertices(graph) + if isassigned(graph, v) + renamed_graph[f(v)] = graph[v] + end + end + + for e in edges(graph) + if isassigned(graph, e) + renamed_graph[rename_vertices(f, e)] = graph[e] + end + end + + return renamed_graph +end # TODO: Implement in terms of `set_underlying_graph`, `set_vertex_data`, etc. # TODO: Use `https://github.com/JuliaObjects/Accessors.jl`? @@ -94,7 +155,7 @@ function GraphsExtensions.directed_graph_type(graph_type::Type{<:DataGraph}) return DataGraph{ vertextype(graph_type), vertex_data_eltype(graph_type), - edgetype(graph_type), + edge_data_eltype(graph_type), directed_graph_type(underlying_graph_type(graph_type)), edgetype(graph_type), } diff --git a/test/test_basics.jl b/test/test_basics.jl index 6270d6c..a1173fc 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -1,5 +1,11 @@ using DataGraphs: - DataGraphs, DataGraph, edge_data, edge_data_eltype, vertex_data, vertex_data_eltype + DataGraphs, + DataGraph, + edge_data, + edge_data_eltype, + vertex_data, + vertex_data_eltype, + underlying_graph using Dictionaries: AbstractIndices, Dictionary, Indices, dictionary using Graphs: add_edge!, @@ -392,7 +398,7 @@ using Test: @test, @test_broken, @testset (3, 1) => (4, 1), (4, 1) => (4, 2), ] - @test t isa NamedDiGraph{Tuple{Int, Int}} + @test underlying_graph(t) isa NamedDiGraph{Tuple{Int, Int}} @test nv(t) == nv(g) @test ne(t) == nv(g) - 1 @test all(e -> has_edge(t, e), es) @@ -407,7 +413,7 @@ using Test: @test, @test_broken, @testset (3, 2) => (2, 2), (2, 2) => (1, 2), ] - @test t isa NamedDiGraph{Tuple{Int, Int}} + @test underlying_graph(t) isa NamedDiGraph{Tuple{Int, Int}} @test nv(t) == nv(g) @test ne(t) == nv(g) - 1 @test all(e -> has_edge(t, e), es) From c9eaeb0eb72a5c35d41da4ed4600beb2b95d3852 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 3 Dec 2025 11:41:05 -0500 Subject: [PATCH 02/12] Fix method overwrites --- ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl | 8 -------- src/abstractdatagraph.jl | 3 --- 2 files changed, 11 deletions(-) diff --git a/ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl b/ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl index 8595bc3..0543ff1 100644 --- a/ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl +++ b/ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl @@ -4,14 +4,6 @@ using NamedGraphs: NamedGraphs, AbstractNamedGraph DataGraphs.is_underlying_graph(::Type{<:AbstractNamedGraph}) = true -for f in [:(NamedGraphs.position_graph), :(NamedGraphs.vertex_positions)] - @eval begin - function $f(graph::AbstractDataGraph) - return $f(underlying_graph(graph)) - end - end -end - using Graphs: edgetype, vertices using NamedGraphs.OrdinalIndexing: OrdinalSuffixedInteger # TODO: Define through some intermediate `to_vertex` function diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index 7e05d4f..4ff3403 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -46,9 +46,6 @@ edge_data_eltype(T::Type{<:AbstractDataGraph}) = eltype(Base.promote_op(edge_dat Graphs.has_vertex(g::AbstractDataGraph, vertex) = has_vertex(underlying_graph(g), vertex) Graphs.has_edge(g::AbstractDataGraph, edge) = has_edge(underlying_graph(g), edge) - -# For method ambiguities -Graphs.has_vertex(g::AbstractDataGraph, vertex) = has_vertex(underlying_graph(g), vertex) Graphs.has_edge(g::AbstractDataGraph, edge::AbstractNamedEdge) = has_edge(underlying_graph(g), edge) vertex_data(dg::AbstractDataGraph) = map(v -> dg[v], assigned_vertices(dg)) From 692b7b657c203562ff3ce6d005bdaf0e7c8dd8da Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 3 Dec 2025 11:47:46 -0500 Subject: [PATCH 03/12] Remove `NamedGraphs` ext and move code to core module --- .../DataGraphsNamedGraphsExt.jl | 32 ------------------- src/DataGraphs.jl | 3 -- src/abstractdatagraph.jl | 30 ++++++++++++++++- 3 files changed, 29 insertions(+), 36 deletions(-) delete mode 100644 ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl diff --git a/ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl b/ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl deleted file mode 100644 index 0543ff1..0000000 --- a/ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl +++ /dev/null @@ -1,32 +0,0 @@ -module DataGraphsNamedGraphsExt -using DataGraphs: DataGraphs, AbstractDataGraph, underlying_graph -using NamedGraphs: NamedGraphs, AbstractNamedGraph - -DataGraphs.is_underlying_graph(::Type{<:AbstractNamedGraph}) = true - -using Graphs: edgetype, vertices -using NamedGraphs.OrdinalIndexing: OrdinalSuffixedInteger -# TODO: Define through some intermediate `to_vertex` function -# (analagous to Julia's `to_indices`) instead of through -# overloading `Base.getindex`. -function Base.getindex(graph::AbstractDataGraph, vertex::OrdinalSuffixedInteger) - return graph[vertices(graph)[vertex]] -end -function Base.getindex( - graph::AbstractDataGraph, edge::Pair{<:OrdinalSuffixedInteger, <:OrdinalSuffixedInteger} - ) - return graph[edgetype(graph)(vertices(graph)[edge[1]], vertices(graph)[edge[2]])] -end -function Base.setindex!(graph::AbstractDataGraph, value, vertex::OrdinalSuffixedInteger) - graph[vertices(graph)[vertex]] = value - return graph -end -function Base.setindex!( - graph::AbstractDataGraph, - value, - edge::Pair{<:OrdinalSuffixedInteger, <:OrdinalSuffixedInteger}, - ) - graph[edgetype(graph)(vertices(graph)[edge[1]], vertices(graph)[edge[2]])] = value - return graph -end -end diff --git a/src/DataGraphs.jl b/src/DataGraphs.jl index 7569491..d009b5d 100644 --- a/src/DataGraphs.jl +++ b/src/DataGraphs.jl @@ -4,9 +4,6 @@ include("utils.jl") include("traits/isunderlyinggraph.jl") include("abstractdatagraph.jl") include("datagraph.jl") -# TODO: Turn into a weak dependency once `GraphsExtensions` -# is split off from `NamedGraphs`. -include("../ext/DataGraphsNamedGraphsExt/DataGraphsNamedGraphsExt.jl") export AbstractDataGraph, DataGraph diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index 4ff3403..ccafb69 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -14,8 +14,11 @@ using NamedGraphs: NamedGraphs, AbstractNamedGraph, AbstractNamedEdge using NamedGraphs.GraphsExtensions: GraphsExtensions, arrange_edge, incident_edges, is_edge_arranged, vertextype using NamedGraphs.SimilarType: similar_type +using NamedGraphs.OrdinalIndexing: OrdinalSuffixedInteger using SimpleTraits: SimpleTraits, Not, @traitfn +is_underlying_graph(::Type{<:AbstractNamedGraph}) = true + abstract type AbstractDataGraph{V, VD, ED} <: AbstractNamedGraph{V} end # Minimal interface @@ -291,7 +294,6 @@ function map_data(f, graph::AbstractDataGraph; vertices = nothing, edges = nothi return map_edge_data(f, graph; edges) end -Base.getindex(graph::AbstractDataGraph, vertex) = get_vertex_data(graph, vertex) Base.get!(graph::AbstractGraph, key, default) = get!(() -> default, graph, key) function Base.get!(default::Base.Callable, graph::AbstractGraph, key) @@ -310,6 +312,18 @@ function Base.get(default::Base.Callable, graph::AbstractGraph, key) end end +Base.getindex(graph::AbstractDataGraph, vertex) = get_vertex_data(graph, vertex) + +function Base.getindex(graph::AbstractDataGraph, vertex::OrdinalSuffixedInteger) + return graph[vertices(graph)[vertex]] +end + +function Base.getindex( + graph::AbstractDataGraph, edge::Pair{<:OrdinalSuffixedInteger, <:OrdinalSuffixedInteger} + ) + return graph[edgetype(graph)(vertices(graph)[edge[1]], vertices(graph)[edge[2]])] +end + function Base.getindex(graph::AbstractDataGraph, edge::AbstractEdge) data = get_edge_data(graph, arrange_edge(graph, edge)) return reverse_data_direction(graph, edge, data) @@ -357,6 +371,20 @@ function Base.setindex!(graph::AbstractDataGraph, x, i1, i2, i...) return graph end +function Base.setindex!(graph::AbstractDataGraph, value, vertex::OrdinalSuffixedInteger) + graph[vertices(graph)[vertex]] = value + return graph +end + +function Base.setindex!( + graph::AbstractDataGraph, + value, + edge::Pair{<:OrdinalSuffixedInteger, <:OrdinalSuffixedInteger}, + ) + graph[edgetype(graph)(vertices(graph)[edge[1]], vertices(graph)[edge[2]])] = value + return graph +end + function induced_subgraph_datagraph(graph::AbstractDataGraph, subvertices) underlying_subgraph, vlist = Graphs.induced_subgraph(underlying_graph(graph), subvertices) subgraph = similar_type(graph)(underlying_subgraph) From accfdcb05904dcda75e0f2f6bacbb165be2e16c2 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 4 Dec 2025 13:18:18 -0500 Subject: [PATCH 04/12] Make `rename_vertices` generic by making use of `similar_graph` from `NamedGraphs` --- src/abstractdatagraph.jl | 31 ++++++++++++++++++++++++++++--- src/datagraph.jl | 34 ++++------------------------------ 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index ccafb69..0978a33 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -12,7 +12,12 @@ using Graphs: vertices using NamedGraphs: NamedGraphs, AbstractNamedGraph, AbstractNamedEdge using NamedGraphs.GraphsExtensions: - GraphsExtensions, arrange_edge, incident_edges, is_edge_arranged, vertextype + GraphsExtensions, + arrange_edge, + incident_edges, + is_edge_arranged, + vertextype, + similar_graph using NamedGraphs.SimilarType: similar_type using NamedGraphs.OrdinalIndexing: OrdinalSuffixedInteger using SimpleTraits: SimpleTraits, Not, @traitfn @@ -37,8 +42,6 @@ set_edge_data!(::AbstractDataGraph, data, edge) = not_implemented() unset_vertex_data!(::AbstractDataGraph, data, vertex) = not_implemented() unset_edge_data!(::AbstractDataGraph, data, edge) = not_implemented() -GraphsExtensions.rename_vertices(f, ::AbstractDataGraph) = not_implemented() - # Quasi-derived interface; only required if inference fails underlying_graph_type(T::Type{<:AbstractDataGraph}) = Base.promote_op(underlying_graph, T) @@ -169,6 +172,28 @@ end return digraph end +function GraphsExtensions.rename_vertices(f::Function, graph::AbstractDataGraph) + + # Use the two-argument `similar_graph` method so the new graph has correct vertex type + renamed_graph = similar_graph(graph, map(f, vertices(graph))) + + for vertex in vertices(graph) + if isassigned(graph, vertex) + renamed_graph[f(vertex)] = graph[vertex] + end + end + + for edge in edges(graph) + renamed_edge = rename_vertices(f, edge) + add_edge!(renamed_graph, renamed_edge) + if isassigned(graph, edge) + renamed_graph[renamed_edge] = graph[edge] + end + end + + return renamed_graph +end + function Base.reverse(graph::AbstractDataGraph) reversed_graph = typeof(graph)(reverse(underlying_graph(graph))) for v in vertices(graph) diff --git a/src/datagraph.jl b/src/datagraph.jl index 29ba363..a4622d5 100644 --- a/src/datagraph.jl +++ b/src/datagraph.jl @@ -7,7 +7,7 @@ using NamedGraphs.GraphsExtensions: directed_graph, vertextype, directed_graph_type, - graph_from_vertices, + similar_graph, rename_vertices # TODO: define VertexDataGraph, a graph with only data on the @@ -69,36 +69,11 @@ edge_data_eltype(G::Type{<:DataGraph}) = eltype(fieldtype(G, :edge_data)) Graphs.edgetype(T::Type{<:DataGraph}) = keytype(fieldtype(T, :edge_data)) -function GraphsExtensions.graph_from_vertices(T::Type{<:DataGraph}, vertices) - return T(graph_from_vertices(underlying_graph_type(T), vertices)) +function GraphsExtensions.similar_graph(T::Type{<:DataGraph}) + similar_underlying_graph = similar_graph(underlying_graph_type(T)) + return T(similar_underlying_graph) end -function GraphsExtensions.rename_vertices(f::Function, graph::DataGraph) - renamed_underlying_graph = GraphsExtensions.rename_vertices(f, underlying_graph(graph)) - - renamed_graph = DataGraph( - renamed_underlying_graph; - vertex_data_eltype = vertex_data_eltype(graph), - edge_data_eltype = edge_data_eltype(graph), - ) - - for v in vertices(graph) - if isassigned(graph, v) - renamed_graph[f(v)] = graph[v] - end - end - - for e in edges(graph) - if isassigned(graph, e) - renamed_graph[rename_vertices(f, e)] = graph[e] - end - end - - return renamed_graph -end - -# TODO: Implement in terms of `set_underlying_graph`, `set_vertex_data`, etc. -# TODO: Use `https://github.com/JuliaObjects/Accessors.jl`? function Base.copy(graph::DataGraph) # Need to manually copy the keys of Dictionaries, see: # https://github.com/andyferris/Dictionaries.jl/issues/98 @@ -150,7 +125,6 @@ function GraphsExtensions.convert_vertextype(vertextype::Type, graph::DataGraph) return DataGraph{vertextype}(graph) end -# TODO: implement generic version in terms of `set_underlying_graph_type`. function GraphsExtensions.directed_graph_type(graph_type::Type{<:DataGraph}) return DataGraph{ vertextype(graph_type), From b282355cba605d01aa704438ec11c68594473eb8 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 4 Dec 2025 15:27:04 -0500 Subject: [PATCH 05/12] Get `vertex_data_eltype` and `edge_data_eltype` directly from type params of abstract type. --- src/abstractdatagraph.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index 0978a33..edcf712 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -26,6 +26,9 @@ is_underlying_graph(::Type{<:AbstractNamedGraph}) = true abstract type AbstractDataGraph{V, VD, ED} <: AbstractNamedGraph{V} end +vertex_data_eltype(::Type{<:AbstractDataGraph{V, VD, ED}}) where {V, VD, ED} = VD +edge_data_eltype(::Type{<:AbstractDataGraph{V, VD, ED}}) where {V, VD, ED} = ED + # Minimal interface # TODO: Define for `AbstractGraph` as a `DataGraphInterface`. underlying_graph(::AbstractDataGraph) = not_implemented() @@ -45,8 +48,6 @@ unset_edge_data!(::AbstractDataGraph, data, edge) = not_implemented() # Quasi-derived interface; only required if inference fails underlying_graph_type(T::Type{<:AbstractDataGraph}) = Base.promote_op(underlying_graph, T) -vertex_data_eltype(T::Type{<:AbstractDataGraph}) = eltype(Base.promote_op(vertex_data, T)) -edge_data_eltype(T::Type{<:AbstractDataGraph}) = eltype(Base.promote_op(edge_data, T)) # Derived interface @@ -75,8 +76,8 @@ function Graphs.is_directed(graph_type::Type{<:AbstractDataGraph}) end underlying_graph_type(graph::AbstractDataGraph) = typeof(underlying_graph(graph)) -vertex_data_eltype(graph::AbstractDataGraph) = eltype(vertex_data(graph)) -edge_data_eltype(graph::AbstractDataGraph) = eltype(edge_data(graph)) +vertex_data_eltype(graph::AbstractDataGraph) = vertex_data_eltype(typeof(graph)) +edge_data_eltype(graph::AbstractDataGraph) = edge_data_eltype(typeof(graph)) Base.zero(graph_type::Type{<:AbstractDataGraph}) = graph_type() From 8e85d6b6b92d22aeac449686f153d50e7ea20df6 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 4 Dec 2025 15:29:01 -0500 Subject: [PATCH 06/12] Add `Vertex/EdgeDataView` wrappers that lazily act like Dictionaries --- src/DataGraphs.jl | 1 + src/abstractdatagraph.jl | 4 ++-- src/dataview.jl | 44 +++++++++++++++++++++++++++++++++++ test/test_basics.jl | 50 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 src/dataview.jl diff --git a/src/DataGraphs.jl b/src/DataGraphs.jl index d009b5d..dc3a60b 100644 --- a/src/DataGraphs.jl +++ b/src/DataGraphs.jl @@ -2,6 +2,7 @@ module DataGraphs include("utils.jl") include("traits/isunderlyinggraph.jl") +include("dataview.jl") include("abstractdatagraph.jl") include("datagraph.jl") diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index edcf712..0aebb1a 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -55,8 +55,8 @@ Graphs.has_vertex(g::AbstractDataGraph, vertex) = has_vertex(underlying_graph(g) Graphs.has_edge(g::AbstractDataGraph, edge) = has_edge(underlying_graph(g), edge) Graphs.has_edge(g::AbstractDataGraph, edge::AbstractNamedEdge) = has_edge(underlying_graph(g), edge) -vertex_data(dg::AbstractDataGraph) = map(v -> dg[v], assigned_vertices(dg)) -edge_data(dg::AbstractDataGraph) = map(v -> dg[v], assigned_edges(dg)) +vertex_data(dg::AbstractDataGraph) = VertexDataView(dg) +edge_data(dg::AbstractDataGraph) = EdgeDataView(dg) function assigned_vertices(graph::AbstractDataGraph) return Indices(filter(v -> isassigned(graph, v), vertices(graph))) diff --git a/src/dataview.jl b/src/dataview.jl new file mode 100644 index 0000000..0850f6c --- /dev/null +++ b/src/dataview.jl @@ -0,0 +1,44 @@ +using Dictionaries: Dictionaries, AbstractDictionary, gettokenvalue + +struct VertexDataView{V, VD, G <: AbstractGraph{V}} <: AbstractDictionary{V, VD} + graph::G + function VertexDataView(graph) + V = vertextype(graph) + VD = vertex_data_eltype(graph) + G = typeof(graph) + return new{V, VD, G}(graph) + end +end + +Base.keys(view::VertexDataView) = assigned_vertices(view.graph) + +struct EdgeDataView{E, ED, G <: AbstractGraph} <: AbstractDictionary{E, ED} + graph::G + function EdgeDataView(graph) + E = edgetype(graph) + ED = edge_data_eltype(graph) + G = typeof(graph) + return new{E, ED, G}(graph) + end +end + +Base.keys(view::EdgeDataView) = assigned_edges(view.graph) + +const VertexOrEdgeDataView{K, V, G} = Union{VertexDataView{K, V, G}, EdgeDataView{K, V, G}} + +Base.isassigned(view::VertexOrEdgeDataView{K}, key::K) where {K} = isassigned(view.graph, key) + +Base.getindex(view::VertexOrEdgeDataView, key) = view.graph[key] +# Method ambiguity +Base.getindex(view::VertexOrEdgeDataView{K}, key::K) where {K} = view.graph[key] + +Dictionaries.istokenizable(::Type{<:VertexOrEdgeDataView}) = true +Dictionaries.istokenassigned(view::VertexOrEdgeDataView, token) = istokenassigned(keys(view), token) +function Dictionaries.gettokenvalue(view::VertexOrEdgeDataView, token) + return view[gettokenvalue(keys(view), token)] +end + +function Base.setindex!(view::VertexOrEdgeDataView{K, V}, data::V, key::K) where {K, V} + setindex!(view.graph, data, key) + return view +end diff --git a/test/test_basics.jl b/test/test_basics.jl index a1173fc..e34c6de 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -1,11 +1,13 @@ using DataGraphs: - DataGraphs, DataGraph, + DataGraphs, + EdgeDataView, + VertexDataView, edge_data, edge_data_eltype, + underlying_graph, vertex_data, - vertex_data_eltype, - underlying_graph + vertex_data_eltype using Dictionaries: AbstractIndices, Dictionary, Indices, dictionary using Graphs: add_edge!, @@ -250,11 +252,9 @@ using Test: @test, @test_broken, @testset @test has_edge(dg, 2.0 => 3.0) @test has_edge(dg, 3.0 => 4.0) @test vertex_data(dg) == Dictionary{Float64, String}() - @test vertex_data(dg) isa Dictionary{Float64, String} @test keytype(vertex_data(dg)) === Float64 @test eltype(vertex_data(dg)) === String @test edge_data(dg) == Dictionary{NamedEdge{Float64}, Symbol}() - @test edge_data(dg) isa Dictionary{NamedEdge{Float64}, Symbol} @test keytype(edge_data(dg)) === NamedEdge{Float64} @test eltype(edge_data(dg)) === Symbol end @@ -474,4 +474,44 @@ using Test: @test, @test_broken, @testset @test g[2nd => 3rd] === :e_bc @test g[3rd => 2nd] === :e_bc end + + @testset "Data views" begin + g = DataGraph( + NamedGraph(path_graph(3), ["a", "b", "c"]); + vertex_data_eltype = Int, + edge_data_eltype = Float64 + ) + g["b"] = 2 + g["c"] = 3 + g["a" => "b"] = -1.0 + g["b" => "c"] = -2.0 + + vdata = vertex_data(g) + @test vdata isa VertexDataView + + @test keytype(VertexDataView(g)) === String + @test eltype(VertexDataView(g)) === Int + + @test collect(keys(vdata)) == ["b", "c"] + @test collect(vdata) == [2, 3] + + @test !haskey(vdata, "a") + @test haskey(vdata, "b") + @test haskey(vdata, "c") + + g["a"] = 1 + vdata = vertex_data(g) + @test collect(keys(vdata)) == collect(vertices(g)) + vdata["a"] = 4 + @test g["a"] == 4 + @test vdata["a"] == 4 + + @test length(keys(EdgeDataView(g))) == 2 + @test keytype(EdgeDataView(g)) === NamedEdge{String} + @test eltype(EdgeDataView(g)) === Float64 + + edata = edge_data(g) + @test edata["a" => "b"] == -1.0 + @test edata[edgetype(g)("b" => "c")] == -2.0 + end end From e5e8860eb475f325f52cbe42d79d006e4da059ee Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 4 Dec 2025 16:45:26 -0500 Subject: [PATCH 07/12] The underlying graph type of `DataGraph` now must be a `AbstractNamedGraph` Tests have been adjusted accordingly. --- examples/datagraph.jl | 16 +++++++++------- src/datagraph.jl | 3 +-- test/test_basics.jl | 29 ++++++++++++++--------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/datagraph.jl b/examples/datagraph.jl index 433ba80..4ba6ddb 100644 --- a/examples/datagraph.jl +++ b/examples/datagraph.jl @@ -1,11 +1,13 @@ using DataGraphs: DataGraph -using Graphs: Edge, grid, has_edge, has_vertex +using Graphs: has_edge, has_vertex +using NamedGraphs: NamedEdge +using NamedGraphs.NamedGraphGenerators: named_grid -g = grid((4,)) +g = named_grid((4)) dg = DataGraph(g; vertex_data_eltype = String, edge_data_eltype = Symbol) -@show !isassigned(dg, Edge(1, 2)) +@show !isassigned(dg, NamedEdge(1, 2)) @show !isassigned(dg, 1 => 2) -@show !isassigned(dg, Edge(1 => 2)) +@show !isassigned(dg, NamedEdge(1 => 2)) @show !isassigned(dg, 1 => 3) @show !isassigned(dg, 1) @show !isassigned(dg, 2) @@ -33,10 +35,10 @@ dg[4] = "V4" dg[1 => 2] = :E12 dg[2 => 3] = :E23 -dg[Edge(3, 4)] = :E34 +dg[NamedEdge(3, 4)] = :E34 #@show isassigned(dg, (1, 2)) -@show isassigned(dg, Edge(2, 3)) +@show isassigned(dg, NamedEdge(2, 3)) @show isassigned(dg, 3 => 4) -@show dg[Edge(1, 2)] == :E12 +@show dg[NamedEdge(1, 2)] == :E12 @show dg[2 => 3] == :E23 @show dg[3 => 4] == :E34 diff --git a/src/datagraph.jl b/src/datagraph.jl index a4622d5..e10fab1 100644 --- a/src/datagraph.jl +++ b/src/datagraph.jl @@ -1,6 +1,5 @@ using Dictionaries: Dictionary using Graphs: Graphs, edgetype, has_edge, has_vertex -using Graphs.SimpleGraphs: SimpleGraph using NamedGraphs: GenericNamedGraph using NamedGraphs.GraphsExtensions: convert_vertextype, @@ -15,7 +14,7 @@ using NamedGraphs.GraphsExtensions: # TODO: Use https://github.com/vtjnash/ComputedFieldTypes.jl to # automatically determine `E` from `G` from `edgetype(G)` # and `V` from `G` as `vertextype(G)`. -struct DataGraph{V, VD, ED, G <: AbstractGraph, E <: AbstractEdge} <: AbstractDataGraph{V, VD, ED} +struct DataGraph{V, VD, ED, G <: AbstractNamedGraph, E <: AbstractEdge} <: AbstractDataGraph{V, VD, ED} underlying_graph::G vertex_data::Dictionary{V, VD} edge_data::Dictionary{E, ED} diff --git a/test/test_basics.jl b/test/test_basics.jl index e34c6de..f826dd0 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -31,7 +31,6 @@ using Graphs: src, steiner_tree, vertices -using Graphs.SimpleGraphs: SimpleDiGraph, SimpleEdge, SimpleGraph using GraphsFlows: GraphsFlows using NamedGraphs: NamedDiGraph, NamedEdge, NamedGraph using NamedGraphs.GraphsExtensions: ⊔, rename_vertices, subgraph, vertextype @@ -55,11 +54,11 @@ using Test: @test, @test_broken, @testset end @testset "Basics" begin - g = grid((4,)) + g = named_grid(4) dg = DataGraph(g; vertex_data_eltype = String, edge_data_eltype = Symbol) - @test !isassigned(dg, SimpleEdge(1, 2)) + @test !isassigned(dg, NamedEdge(1, 2)) @test !isassigned(dg, 1 => 2) - @test !isassigned(dg, SimpleEdge(1 => 2)) + @test !isassigned(dg, NamedEdge(1 => 2)) @test !isassigned(dg, 1 => 3) @test !isassigned(dg, 1) @test !isassigned(dg, 2) @@ -94,11 +93,11 @@ using Test: @test, @test_broken, @testset dg[1 => 2] = :E12 dg[2 => 3] = :E23 - dg[SimpleEdge(3, 4)] = :E34 + dg[NamedEdge(3, 4)] = :E34 #@test isassigned(dg, (1, 2)) - @test isassigned(dg, SimpleEdge(2, 3)) + @test isassigned(dg, NamedEdge(2, 3)) @test isassigned(dg, 3 => 4) - @test dg[SimpleEdge(1, 2)] == :E12 + @test dg[NamedEdge(1, 2)] == :E12 @test dg[2 => 3] == :E23 @test dg[3 => 4] == :E34 @@ -109,7 +108,7 @@ using Test: @test, @test_broken, @testset @test dg[(1, 1) => (1, (1, 1))] == "X" vdata = map(v -> "V$v", Indices(1:4)) - edata = map(e -> "E$(src(e))$(dst(e))", Indices(SimpleEdge.([1 => 2, 2 => 3, 3 => 4]))) + edata = map(e -> "E$(src(e))$(dst(e))", Indices(NamedEdge.([1 => 2, 2 => 3, 3 => 4]))) # TODO: Make a more compact constructor that directly accepts # vertex and edge data? Maybe `DataGraph(g; vertex_data=vdata, edge_data=edata)` # or `DataGraph(g; vertex_data=v -> "V$v", edge_data=e -> "E$(src(e))$(dst(e))")`. @@ -129,13 +128,13 @@ using Test: @test, @test_broken, @testset @test dg[2 => 3] == "E23" @test dg[3 => 4] == "E34" - @test DataGraph(g) isa DataGraph{Int, Any, Any, SimpleGraph{Int}, SimpleEdge{Int}} + @test DataGraph(g) isa DataGraph{Int, Any, Any, NamedGraph{Int}, NamedEdge{Int}} dg_uint16 = DataGraph{UInt16}(dg) @test dg_uint16 isa - DataGraph{UInt16, String, String, SimpleGraph{UInt16}, SimpleEdge{UInt16}} + DataGraph{UInt16, String, String, NamedGraph{UInt16}, NamedEdge{UInt16}} @test vertextype(dg_uint16) === UInt16 - @test edgetype(dg_uint16) === SimpleEdge{UInt16} + @test edgetype(dg_uint16) === NamedEdge{UInt16} @test vertex_data_eltype(dg_uint16) === String @test edge_data_eltype(dg_uint16) === String @test dg_uint16[1] == "V1" @@ -181,7 +180,7 @@ using Test: @test, @test_broken, @testset end @testset "get and get! functions" begin - g = grid((4,)) + g = named_grid(4) dg = DataGraph(g; vertex_data_eltype = String, edge_data_eltype = Symbol) # Test for vertices @@ -303,11 +302,11 @@ using Test: @test, @test_broken, @testset end @testset "union" begin - g1 = DataGraph(grid((4,))) + g1 = DataGraph(named_grid(4)) g1[1] = ["A", "B", "C"] g1[1 => 2] = ["E", "F"] - g2 = DataGraph(SimpleGraph(5)) + g2 = DataGraph(NamedGraph(5)) add_edge!(g2, 1 => 5) g2[1] = ["C", "D", "E"] @@ -355,7 +354,7 @@ using Test: @test, @test_broken, @testset @test issetequal(comps[2], [4, 5, 6]) end @testset "reverse" begin - g = DataGraph(SimpleDiGraph(4)) + g = DataGraph(NamedDiGraph(4)) add_edge!(g, 1 => 2) add_edge!(g, 3 => 4) g[1 => 2] = :A From 7c07b7f9dadf46287e5aade516f23cbe8933ff1a Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 4 Dec 2025 16:46:01 -0500 Subject: [PATCH 08/12] Make `Base.reverse` method `AbstractDataGraph` more generic. --- src/abstractdatagraph.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index 0aebb1a..ab0b47f 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -4,6 +4,7 @@ using Graphs: AbstractEdge, IsDirected, add_edge!, + add_vertex!, a_star, edges, ne, @@ -175,7 +176,7 @@ end function GraphsExtensions.rename_vertices(f::Function, graph::AbstractDataGraph) - # Use the two-argument `similar_graph` method so the new graph has correct vertex type + # Uses the two-argument `similar_graph` method so the new graph has correct vertex type renamed_graph = similar_graph(graph, map(f, vertices(graph))) for vertex in vertices(graph) @@ -196,13 +197,15 @@ function GraphsExtensions.rename_vertices(f::Function, graph::AbstractDataGraph) end function Base.reverse(graph::AbstractDataGraph) - reversed_graph = typeof(graph)(reverse(underlying_graph(graph))) + reversed_graph = similar_graph(graph) for v in vertices(graph) + add_vertex!(reversed_graph, v) if isassigned(graph, v) reversed_graph[v] = graph[v] end end for e in edges(graph) + add_edge!(reversed_graph, reverse(e)) if isassigned(graph, e) reversed_graph[reverse(e)] = graph[e] end @@ -210,6 +213,7 @@ function Base.reverse(graph::AbstractDataGraph) return reversed_graph end + function Graphs.merge_vertices( graph::AbstractDataGraph, merge_vertices; From 6b4826873e6f922c3cc7ad394eca48665122c10f Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 4 Dec 2025 17:01:54 -0500 Subject: [PATCH 09/12] Remove surplus code --- src/abstractdatagraph.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index ab0b47f..010ebe3 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -222,7 +222,6 @@ function Graphs.merge_vertices( merge_edge_data = merge_data, kwargs..., ) - underlying_merged_graph = Graphs.merge_vertices(underlying_graph(graph); kwargs...) return not_implemented() end @@ -234,8 +233,6 @@ function Graphs.merge_vertices!( merge_edge_data = merge_data, kwargs..., ) - underlying_merged_graph = copy(underlying_graph(graph)) - Graphs.merge_vertices!(underlying_merged_graph; kwargs...) return not_implemented() end From 4c0042d98247a36d9f6d8a4c84553e83eaa1df95 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 11 Dec 2025 15:55:37 -0500 Subject: [PATCH 10/12] DataViews now use a lazy filter to construct keys --- src/dataview.jl | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/dataview.jl b/src/dataview.jl index 0850f6c..5ab8a89 100644 --- a/src/dataview.jl +++ b/src/dataview.jl @@ -1,4 +1,4 @@ -using Dictionaries: Dictionaries, AbstractDictionary, gettokenvalue +using Dictionaries: Dictionaries, AbstractDictionary, gettokenvalue, filterview struct VertexDataView{V, VD, G <: AbstractGraph{V}} <: AbstractDictionary{V, VD} graph::G @@ -10,7 +10,7 @@ struct VertexDataView{V, VD, G <: AbstractGraph{V}} <: AbstractDictionary{V, VD} end end -Base.keys(view::VertexDataView) = assigned_vertices(view.graph) +_keys(view::VertexDataView) = Indices(vertices(view.graph)) struct EdgeDataView{E, ED, G <: AbstractGraph} <: AbstractDictionary{E, ED} graph::G @@ -22,23 +22,42 @@ struct EdgeDataView{E, ED, G <: AbstractGraph} <: AbstractDictionary{E, ED} end end -Base.keys(view::EdgeDataView) = assigned_edges(view.graph) +_keys(view::EdgeDataView) = Indices(edges(view.graph)) const VertexOrEdgeDataView{K, V, G} = Union{VertexDataView{K, V, G}, EdgeDataView{K, V, G}} -Base.isassigned(view::VertexOrEdgeDataView{K}, key::K) where {K} = isassigned(view.graph, key) +function Base.keys(view::VertexOrEdgeDataView) + return filterview(k -> isassigned(view.graph, k), _keys(view)) +end -Base.getindex(view::VertexOrEdgeDataView, key) = view.graph[key] -# Method ambiguity +Base.isassigned(view::VertexOrEdgeDataView{K}, key::K) where {K} = isassigned(view.graph, key) Base.getindex(view::VertexOrEdgeDataView{K}, key::K) where {K} = view.graph[key] +Base.getindex(view::EdgeDataView, key::Pair) = view.graph[key] Dictionaries.istokenizable(::Type{<:VertexOrEdgeDataView}) = true -Dictionaries.istokenassigned(view::VertexOrEdgeDataView, token) = istokenassigned(keys(view), token) +function Dictionaries.istokenassigned(view::VertexOrEdgeDataView, token) + ind = gettokenvalue(keys(view), token) + return isassigned(view, ind) +end function Dictionaries.gettokenvalue(view::VertexOrEdgeDataView, token) - return view[gettokenvalue(keys(view), token)] + ind = gettokenvalue(keys(view), token) + return view[ind] end function Base.setindex!(view::VertexOrEdgeDataView{K, V}, data::V, key::K) where {K, V} setindex!(view.graph, data, key) return view end +function Dictionaries.settokenvalue!(view::VertexOrEdgeDataView{<:Any, T}, token, value::T) where {T} + setindex!(view, value, Dictionaries.gettokenvalue(view, token)) + return view +end + +function Dictionaries.gettoken!(view::VertexOrEdgeDataView{K}, ind::K) where {K} + # This will intentially error if `ind` is not assigned in `view`. + return Dictionaries.gettoken(keys(view), ind) +end +function Dictionaries.deletetoken!(view::VertexOrEdgeDataView, token) + Dictionaries.unset!(view.graph, gettokenvalue(keys(view), token)) + return view +end From fe3e9863aa3fa1dbbf78def3f62b0d37fdb42761 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 11 Dec 2025 15:57:13 -0500 Subject: [PATCH 11/12] Remove redundant methods These are defined on the abstract type --- src/datagraph.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/datagraph.jl b/src/datagraph.jl index e10fab1..ea959a2 100644 --- a/src/datagraph.jl +++ b/src/datagraph.jl @@ -66,8 +66,6 @@ edge_data_eltype(G::Type{<:DataGraph}) = eltype(fieldtype(G, :edge_data)) # Extras -Graphs.edgetype(T::Type{<:DataGraph}) = keytype(fieldtype(T, :edge_data)) - function GraphsExtensions.similar_graph(T::Type{<:DataGraph}) similar_underlying_graph = similar_graph(underlying_graph_type(T)) return T(similar_underlying_graph) @@ -119,7 +117,6 @@ function DataGraph{V}(graph::DataGraph) where {V} return _DataGraph(converted_underlying_graph, converted_vertex_data, converted_edge_data) end -GraphsExtensions.convert_vertextype(::Type{V}, graph::DataGraph{V}) where {V} = graph function GraphsExtensions.convert_vertextype(vertextype::Type, graph::DataGraph) return DataGraph{vertextype}(graph) end From 598147807984782d1b22b5d98513e86da7ebe978 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Thu, 11 Dec 2025 15:58:02 -0500 Subject: [PATCH 12/12] Improve and reorganise indexing interface --- src/DataGraphs.jl | 1 + src/abstractdatagraph.jl | 179 ++++++++++++++++++++------------------- src/indexing.jl | 68 +++++++++++++++ 3 files changed, 159 insertions(+), 89 deletions(-) create mode 100644 src/indexing.jl diff --git a/src/DataGraphs.jl b/src/DataGraphs.jl index dc3a60b..cd07b12 100644 --- a/src/DataGraphs.jl +++ b/src/DataGraphs.jl @@ -4,6 +4,7 @@ include("utils.jl") include("traits/isunderlyinggraph.jl") include("dataview.jl") include("abstractdatagraph.jl") +include("indexing.jl") include("datagraph.jl") export AbstractDataGraph, DataGraph diff --git a/src/abstractdatagraph.jl b/src/abstractdatagraph.jl index 010ebe3..2271330 100644 --- a/src/abstractdatagraph.jl +++ b/src/abstractdatagraph.jl @@ -11,14 +11,21 @@ using Graphs: nv, steiner_tree, vertices -using NamedGraphs: NamedGraphs, AbstractNamedGraph, AbstractNamedEdge +using NamedGraphs: + NamedGraphs, + AbstractNamedGraph, + AbstractNamedEdge, + AbstractVertices, + AbstractEdges, + position_graph_type using NamedGraphs.GraphsExtensions: GraphsExtensions, arrange_edge, incident_edges, is_edge_arranged, vertextype, - similar_graph + similar_graph, + add_vertices! using NamedGraphs.SimilarType: similar_type using NamedGraphs.OrdinalIndexing: OrdinalSuffixedInteger using SimpleTraits: SimpleTraits, Not, @traitfn @@ -27,6 +34,9 @@ is_underlying_graph(::Type{<:AbstractNamedGraph}) = true abstract type AbstractDataGraph{V, VD, ED} <: AbstractNamedGraph{V} end +vertex_data_eltype(::Type{<:AbstractGraph}) = Any +edge_data_eltype(::Type{<:AbstractGraph}) = Any + vertex_data_eltype(::Type{<:AbstractDataGraph{V, VD, ED}}) where {V, VD, ED} = VD edge_data_eltype(::Type{<:AbstractDataGraph{V, VD, ED}}) where {V, VD, ED} = ED @@ -48,21 +58,64 @@ unset_edge_data!(::AbstractDataGraph, data, edge) = not_implemented() # Quasi-derived interface; only required if inference fails -underlying_graph_type(T::Type{<:AbstractDataGraph}) = Base.promote_op(underlying_graph, T) +underlying_graph_type(T::Type{<:AbstractGraph}) = Base.promote_op(underlying_graph, T) # Derived interface +has_vertices_data(g::AbstractGraph, vertices) = all(v -> has_vertex_data(g, v), vertices) +has_edges_data(g::AbstractGraph, edges) = all(e -> has_edge_data(g, e), edges) + +function get_vertices_data(g::AbstractGraph, vertices) + return map(v -> get_vertex_data(g, v), Indices(vertices)) +end +function vertices_data_eltype(G::Type{<:AbstractGraph}, V::Type{<:AbstractVertices}) + return Dictionary{eltype(V), vertex_data_eltype(G)} +end + +function get_edges_data(g::AbstractGraph, edges) + return map(e -> get_edge_data(g, e), Indices(edges)) +end +function edges_data_eltype(G::Type{<:AbstractGraph}, E::Type{<:AbstractEdges{V, ET} where {V, ET}}) + return Dictionary{eltype(E), edge_data_eltype(G)} +end + +function set_vertices_data!(g::AbstractGraph, val, vertices) + for v in vertices + g[v] = val[v] + end + return g +end +function set_edges_data!(g::AbstractGraph, val, edges) + for e in edges + g[e] = val[e] + end + return g +end + +function unset_vertices_data!(g::AbstractGraph, vertices) + for v in vertices + unset_vertex_data!(g, v) + end + return g +end +function unset_edges_data!(g::AbstractGraph, edges) + for e in edges + unset_edge_data!(g, e) + end + return g +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) Graphs.has_edge(g::AbstractDataGraph, edge::AbstractNamedEdge) = has_edge(underlying_graph(g), edge) -vertex_data(dg::AbstractDataGraph) = VertexDataView(dg) -edge_data(dg::AbstractDataGraph) = EdgeDataView(dg) +vertex_data(dg::AbstractGraph) = VertexDataView(dg) +edge_data(dg::AbstractGraph) = EdgeDataView(dg) -function assigned_vertices(graph::AbstractDataGraph) +function assigned_vertices(graph::AbstractGraph) return Indices(filter(v -> isassigned(graph, v), vertices(graph))) end -function assigned_edges(graph::AbstractDataGraph) +function assigned_edges(graph::AbstractGraph) return Indices(filter(e -> isassigned(graph, e), edges(graph))) end @@ -70,17 +123,21 @@ function Graphs.edgetype(graph::AbstractDataGraph) return Graphs.edgetype(underlying_graph(graph)) end function Graphs.edgetype(graph_type::Type{<:AbstractDataGraph}) - return Base.promote_op(edgetype, graph_type) + return edgetype(underlying_graph_type(graph_type)) end function Graphs.is_directed(graph_type::Type{<:AbstractDataGraph}) return Graphs.is_directed(underlying_graph_type(graph_type)) end -underlying_graph_type(graph::AbstractDataGraph) = typeof(underlying_graph(graph)) -vertex_data_eltype(graph::AbstractDataGraph) = vertex_data_eltype(typeof(graph)) -edge_data_eltype(graph::AbstractDataGraph) = edge_data_eltype(typeof(graph)) +underlying_graph_type(graph::AbstractGraph) = typeof(underlying_graph(graph)) +vertex_data_eltype(graph::AbstractGraph) = vertex_data_eltype(typeof(graph)) +edge_data_eltype(graph::AbstractGraph) = edge_data_eltype(typeof(graph)) + +function NamedGraphs.position_graph_type(type::Type{<:AbstractDataGraph}) + return position_graph_type(underlying_graph_type(type)) +end -Base.zero(graph_type::Type{<:AbstractDataGraph}) = graph_type() +Base.zero(graph_type::Type{<:AbstractDataGraph}) = similar_graph(graph_type) # Graphs overloads function Graphs.vertices(graph::AbstractDataGraph) @@ -123,12 +180,12 @@ function Graphs.eccentricity(graph::AbstractDataGraph, distmx::AbstractMatrix) end # Fix for ambiguity error with `AbstractGraph` version -function indegree(graph::AbstractDataGraph, vertex::Integer) +function Graphs.indegree(graph::AbstractDataGraph, vertex::Integer) return indegree(underlying_graph(graph), vertex) end # Fix for ambiguity error with `AbstractGraph` version -function outdegree(graph::AbstractDataGraph, vertex::Integer) +function Graphs.outdegree(graph::AbstractDataGraph, vertex::Integer) return outdegree(underlying_graph(graph), vertex) end @@ -177,7 +234,10 @@ end function GraphsExtensions.rename_vertices(f::Function, graph::AbstractDataGraph) # Uses the two-argument `similar_graph` method so the new graph has correct vertex type - renamed_graph = similar_graph(graph, map(f, vertices(graph))) + renamed_vertices = map(f, vertices(graph)) + renamed_graph = similar_graph(graph, eltype(renamed_vertices)) + + add_vertices!(renamed_graph, renamed_vertices) for vertex in vertices(graph) if isassigned(graph, vertex) @@ -296,7 +356,7 @@ function Graphs.dfs_tree(graph::AbstractDataGraph, s::Integer; kwargs...) return Graphs.dfs_tree(underlying_graph(graph), s; kwargs...) end -function map_vertex_data(f, graph::AbstractDataGraph; vertices = nothing) +function map_vertex_data(f, graph::AbstractGraph; vertices = nothing) new_graph = copy(graph) vs = isnothing(vertices) ? Graphs.vertices(graph) : vertices for v in vs @@ -305,7 +365,7 @@ function map_vertex_data(f, graph::AbstractDataGraph; vertices = nothing) return new_graph end -function map_edge_data(f, graph::AbstractDataGraph; edges = nothing) +function map_edge_data(f, graph::AbstractGraph; edges = nothing) new_graph = copy(graph) es = isnothing(edges) ? Graphs.edges(graph) : edges for e in es @@ -316,22 +376,22 @@ function map_edge_data(f, graph::AbstractDataGraph; edges = nothing) return new_graph end -function map_data(f, graph::AbstractDataGraph; vertices = nothing, edges = nothing) +function map_data(f, graph::AbstractGraph; vertices = nothing, edges = nothing) graph = map_vertex_data(f, graph; vertices) return map_edge_data(f, graph; edges) end -Base.get!(graph::AbstractGraph, key, default) = get!(() -> default, graph, key) -function Base.get!(default::Base.Callable, graph::AbstractGraph, key) +Base.get!(graph::AbstractDataGraph, key, default) = get!(() -> default, graph, key) +function Base.get!(default::Base.Callable, graph::AbstractDataGraph, key) if isassigned(graph, key) return graph[key] else return graph[key] = default() end end -Base.get(graph::AbstractGraph, key, default) = get(() -> default, graph, key) -function Base.get(default::Base.Callable, graph::AbstractGraph, key) +Base.get(graph::AbstractDataGraph, key, default) = get(() -> default, graph, key) +function Base.get(default::Base.Callable, graph::AbstractDataGraph, key) if isassigned(graph, key) return graph[key] else @@ -339,77 +399,18 @@ function Base.get(default::Base.Callable, graph::AbstractGraph, key) end end -Base.getindex(graph::AbstractDataGraph, vertex) = get_vertex_data(graph, vertex) - -function Base.getindex(graph::AbstractDataGraph, vertex::OrdinalSuffixedInteger) - return graph[vertices(graph)[vertex]] -end - -function Base.getindex( - graph::AbstractDataGraph, edge::Pair{<:OrdinalSuffixedInteger, <:OrdinalSuffixedInteger} - ) - return graph[edgetype(graph)(vertices(graph)[edge[1]], vertices(graph)[edge[2]])] -end - -function Base.getindex(graph::AbstractDataGraph, edge::AbstractEdge) - data = get_edge_data(graph, arrange_edge(graph, edge)) - return reverse_data_direction(graph, edge, data) -end - -# Support syntax `g[v1 => v2]` -function Base.getindex(graph::AbstractDataGraph, edge::Pair) - return graph[edgetype(graph)(edge)] +function Base.isassigned(graph::AbstractDataGraph, index) + return _isassigned(graph, to_graph_indices(graph, index)) end - -# Support syntax `g[1, 2] = g[(1, 2)]` -function Base.getindex(graph::AbstractDataGraph, i1, i2, i...) - return graph[(i1, i2, i...)] -end - -Base.isassigned(graph::AbstractDataGraph, vertex) = has_vertex_data(graph, vertex) - -function Base.isassigned(graph::AbstractDataGraph, edge::AbstractEdge) +_isassigned(graph::AbstractDataGraph, vertex) = has_vertex_data(graph, vertex) +function _isassigned(graph::AbstractDataGraph, edge::AbstractEdge) return has_edge_data(graph, arrange_edge(graph, edge)) end -function Base.isassigned(graph::AbstractDataGraph, edge::Pair) - return isassigned(graph, edgetype(graph)(edge)) -end - -function Base.setindex!(graph::AbstractDataGraph, data, vertex) - set_vertex_data!(graph, data, vertex) - return graph -end - -function Base.setindex!(graph::AbstractDataGraph, data, edge::AbstractEdge) - arranged_edge = arrange_edge(graph, edge) - arranged_data = reverse_data_direction(graph, edge, data) - set_edge_data!(graph, arranged_data, arranged_edge) - return graph -end - -function Base.setindex!(graph::AbstractDataGraph, data, edge::Pair) - graph[edgetype(graph)(edge)] = data - return graph +function _isassigned(graph::AbstractDataGraph, edges::AbstractEdges) + return has_edges_data(graph, edges) end - -# Support syntax `g[1, 2] = g[(1, 2)]` -function Base.setindex!(graph::AbstractDataGraph, x, i1, i2, i...) - graph[(i1, i2, i...)] = x - return graph -end - -function Base.setindex!(graph::AbstractDataGraph, value, vertex::OrdinalSuffixedInteger) - graph[vertices(graph)[vertex]] = value - return graph -end - -function Base.setindex!( - graph::AbstractDataGraph, - value, - edge::Pair{<:OrdinalSuffixedInteger, <:OrdinalSuffixedInteger}, - ) - graph[edgetype(graph)(vertices(graph)[edge[1]], vertices(graph)[edge[2]])] = value - return graph +function _isassigned(graph::AbstractDataGraph, vertices::AbstractVertices) + return has_vertices_data(graph, vertices) end function induced_subgraph_datagraph(graph::AbstractDataGraph, subvertices) diff --git a/src/indexing.jl b/src/indexing.jl new file mode 100644 index 0000000..f1169da --- /dev/null +++ b/src/indexing.jl @@ -0,0 +1,68 @@ +using NamedGraphs: to_graph_indices, AbstractEdges, AbstractVertices + +function Base.getindex(graph::AbstractDataGraph, indices) + return _getindex(graph, to_graph_indices(graph, indices)) +end + +_getindex(graph::AbstractGraph, vertex) = get_vertex_data(graph, vertex) + +function _getindex(graph::AbstractGraph, edge::AbstractEdge) + data = get_edge_data(graph, arrange_edge(graph, edge)) + return reverse_data_direction(graph, edge, data) +end + +function _getindex(graph::AbstractGraph, vertices::AbstractVertices) + return get_vertices_data(graph, vertices) +end +function _getindex(graph::AbstractGraph, edges::AbstractEdges) + return get_edges_data(graph, edges) +end + +# Support syntax `g[1, 2] = g[(1, 2)]` +function Base.getindex(graph::AbstractDataGraph, i1, i2, i...) + return graph[(i1, i2, i...)] +end + +function Base.setindex!(graph::AbstractDataGraph, data, index) + _setindex!(graph, data, to_graph_indices(graph, index)) + return graph +end + +function _setindex!(graph::AbstractGraph, data, vertex) + set_vertex_data!(graph, data, vertex) + return graph +end + +function _setindex!(graph::AbstractGraph, data, edge::AbstractEdge) + arranged_edge = arrange_edge(graph, edge) + arranged_data = reverse_data_direction(graph, edge, data) + set_edge_data!(graph, arranged_data, arranged_edge) + return graph +end + +function _setindex!(graph::AbstractGraph, val, vertices::AbstractVertices) + return set_vertices_data!(graph, val, vertices) +end +function _setindex!(graph::AbstractGraph, val, edges::AbstractEdges) + return set_edges_data!(graph, val, edges) +end + +# Support syntax `g[1, 2] = g[(1, 2)]` +function Base.setindex!(graph::AbstractDataGraph, x, i1, i2, i...) + graph[(i1, i2, i...)] = x + return graph +end + +# Ordinal Indexing + +function NamedGraphs.to_graph_indices( + graph::AbstractGraph, + pair::Pair{<:OrdinalSuffixedInteger, <:OrdinalSuffixedInteger} + ) + vs = vertices(graph) + v1, v2 = pair + return to_graph_indices(graph, vs[v1] => vs[v2]) +end +function NamedGraphs.to_graph_indices(graph::AbstractGraph, vertex::OrdinalSuffixedInteger) + return to_graph_indices(graph, vertices(graph)[vertex]) +end