diff --git a/docs/src/changelog.md b/docs/src/changelog.md index c028312..fa68d4f 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -43,9 +43,9 @@ and edges used internally. ([#178](https://github.com/ITensor/NamedGraphs.jl/pull/178)). - The "position graph" terminology is replaced with encode and decode terminology. The overloads for implementing a new `AbstractNamedGraph` are - `encoded_graph(g)` (replaces `position_graph`, with a generic - `EncodedGraphView` fallback so a graph type does not need to store an integer - graph), `encoded_vertex(g, v)` (replaces `vertex_positions`), and + `encoded_graph(g)` (replaces `position_graph`, and can return + `EncodedGraphView(g)` for a type that does not store an integer graph), + `encoded_vertex(g, v)` (replaces `vertex_positions`), and `decoded_vertex(g, c)` (replaces `ordered_vertices`). Codes are not stable across mutation. The last two translate a single vertex where the functions they replace returned a whole mapping, so a type that forwarded all three in a diff --git a/docs/src/dev_interface.md b/docs/src/dev_interface.md index 50d531f..6a536a8 100644 --- a/docs/src/dev_interface.md +++ b/docs/src/dev_interface.md @@ -10,9 +10,12 @@ CollapsedDocStrings = true Subtype [`AbstractNamedGraph`](@ref) and overload the minimal interface below, the graph on integer vertex codes and the translation between names and codes. Everything else in the Graphs.jl interface has generic fallbacks in terms of -these. Graph types that do not store an integer graph get a generic -`EncodedGraphView` fallback for `encoded_graph` and only need `encoded_vertex` -and `decoded_vertex`. +these. + +A graph type that does not store an integer graph can return +[`EncodedGraphView(g)`](@ref EncodedGraphView) from `encoded_graph`. The view +answers `nv`, `ne`, `has_vertex`, `has_edge`, `edges`, and the neighbor queries +by asking the named graph itself, so such a type defines those directly. Vertex codes are not stable across mutation: adding or removing vertices may reassign the codes of other vertices. @@ -26,6 +29,7 @@ encoded_vertex decoded_vertex encoded_edge decoded_edge +EncodedGraphView ``` ## Graphs.jl interface extensions diff --git a/src/NamedGraphs.jl b/src/NamedGraphs.jl index 9ad7afd..38fc0f4 100644 --- a/src/NamedGraphs.jl +++ b/src/NamedGraphs.jl @@ -24,7 +24,7 @@ module NamedGraphs export ⊔, AbstractNamedGraph, NamedDiGraph, NamedEdge, NamedGraph, add_edge, add_edges, add_edges!, add_vertex, add_vertices, all_edges, boundary_edges, convert_vertextype, default_root_vertex, directed_graph, disjoint_union, - edge_subgraph, edgeless_graph, empty_graph, forest_cover, + eccentricities, edge_subgraph, edgeless_graph, empty_graph, forest_cover, forest_cover_edge_sequence, in_incident_edges, incident_edges, is_leaf_vertex, leaf_vertices, named_binary_tree, named_comb_tree, named_cycle_graph, named_grid, named_hexagonal_lattice_graph, named_path_digraph, named_path_graph, @@ -40,7 +40,7 @@ if VERSION >= v"1.11.0-DEV.469" # overload, so it is public rather than exported. eval( Meta.parse( - "public decoded_edge, decoded_vertex, encoded_edge, encoded_graph, encoded_vertex, to_graph_index" + "public EncodedGraphView, decoded_edge, decoded_vertex, encoded_edge, encoded_graph, encoded_vertex, to_graph_index" ) ) end diff --git a/src/PartitionedGraphs/PartitionedGraphs.jl b/src/PartitionedGraphs/PartitionedGraphs.jl index 1da09bc..42d85c2 100644 --- a/src/PartitionedGraphs/PartitionedGraphs.jl +++ b/src/PartitionedGraphs/PartitionedGraphs.jl @@ -7,7 +7,7 @@ This module provides data structures and functionalities to work with partitione including quotient vertices and edges, as well as views of partitioned graphs. It defines an abstract supertype `AbstractPartitionedGraph` for graphs that have some notion of a non-trivial partitioning of their vertices. It also provides -a interface of functions that can be overloaded on any subtype of `Graphs.AbstractGraph` to +an interface of functions that can be overloaded on any subtype of `Graphs.AbstractGraph` to make this subtype behave like a partitioned graph, without itself subtyping `AbstractPartitionedGraph`. It defines the following concrete types: @@ -18,7 +18,9 @@ It defines the following concrete types: - `PartitionedGraph`: An implementation of a partitioned graph with extra caching not provided by `PartitionedView`. - `QuotientView`: A view of the quotient graph derived from a partitioned graph. - It provides the following functions: + +It provides the following functions: + - `partitionedgraph`: Partitions an `AbstractGraph`. - `departition`: Removes a single layer of partitioning from a partitioned graph. - `unpartition`: Recursively removes all layers of partitioning from a partitioned graph. @@ -81,7 +83,11 @@ is not supported for partitioned graphs as it is ambiguous which quotient vertex should belong to. To add a vertex to a partitioned graph, one should define the method: ```julia -Graphs.add_subquotientvertex!(g::MyGraphType, quotientvertex::QuotientVertex, vertex) +PartitionedGraphs.add_subquotientvertex!( + g::MyGraphType, + quotientvertex::QuotientVertex, + vertex +) ``` Doing so enables the syntax: diff --git a/src/PartitionedGraphs/quotientedge.jl b/src/PartitionedGraphs/quotientedge.jl index eaf1ada..4e792a7 100644 --- a/src/PartitionedGraphs/quotientedge.jl +++ b/src/PartitionedGraphs/quotientedge.jl @@ -31,10 +31,10 @@ to_quotient_index(edge::AbstractEdge) = QuotientEdge(edge) """ quotientedge(g::AbstractGraph{V}, edge) -> QuotientEdge{V} -Return the the quotient edge corresponding to `edge` of the graph `g`. Note, +Return the quotient edge corresponding to `edge` of the graph `g`. Note, the returned quotient edge may be a self-loop. -See also: `quotientedges`, `quotienttvertex`. +See also: `quotientedges`, `quotientvertex`. """ quotientedge(g::AbstractGraph, edge::Pair) = quotientedge(g, edgetype(g)(edge)) function quotientedge(g::AbstractGraph, edge::AbstractEdge) diff --git a/src/PartitionedGraphs/quotientvertex.jl b/src/PartitionedGraphs/quotientvertex.jl index a163935..06cc171 100644 --- a/src/PartitionedGraphs/quotientvertex.jl +++ b/src/PartitionedGraphs/quotientvertex.jl @@ -61,8 +61,8 @@ Base.getindex(qvs::QuotientVertices, i) = QuotientVertices(qvs.vertices[i]) """ quotientvertices(g::AbstractGraph, vs = vertices(g)) -Return an iterator over unique quotient vertices corresponding to the set vertices `vs` -of the graph `pg`. +Return an iterator over unique quotient vertices corresponding to the vertices `vs` +of the graph `g`. """ quotientvertices(g) = QuotientVertices(g) function quotientvertices(g::AbstractGraph, vs) diff --git a/src/abstractnamedgraph.jl b/src/abstractnamedgraph.jl index f32f345..9d4973b 100644 --- a/src/abstractnamedgraph.jl +++ b/src/abstractnamedgraph.jl @@ -11,8 +11,9 @@ using SimpleTraits: SimpleTraits, @traitfn, Not Abstract type for graphs whose vertices are names of type `V` rather than contiguous integers. Subtypes implement the Graphs.jl interface in terms of a -graph on integer vertex codes through the minimal interface -[`encoded_graph`](@ref), [`encoded_vertex`](@ref), and [`decoded_vertex`](@ref). +graph on integer vertex codes through [`encoded_graph`](@ref), +[`encoded_vertex`](@ref), and [`decoded_vertex`](@ref). The developer interface +page of the documentation covers what a subtype has to define. """ abstract type AbstractNamedGraph{V} <: AbstractGraph{V} end @@ -76,18 +77,6 @@ julia> [decoded_vertex(g, c) for c in 1:nv(g)] decoded_vertex(graph::AbstractNamedGraph, code::Integer) = not_implemented() decoded_vertex(graph::AbstractSimpleGraph, code::Integer) = code -Graphs.rem_vertex!(graph::AbstractNamedGraph, vertex) = not_implemented() -Graphs.add_vertex!(graph::AbstractNamedGraph, vertex) = not_implemented() - -function rename_vertices(f::Function, graph::AbstractNamedGraph) - new_vertices = map(c -> f(decoded_vertex(graph, c)), vertices(encoded_graph(graph))) - return namedgraph(copy(encoded_graph(graph)), new_vertices) -end - -# -# Derived interface (overload for performance) -# - """ encoded_graph(graph::AbstractNamedGraph) -> AbstractGraph{Int} @@ -99,6 +88,11 @@ the edge `encoded_vertex(graph, u) => encoded_vertex(graph, v)` if and only if May be a stored field or a view of `graph`; mutate the graph only through `graph`. +A type that computes its topology directly rather than storing an integer graph +can return [`EncodedGraphView(graph)`](@ref EncodedGraphView), and must then define +`nv`, `ne`, `has_vertex`, `has_edge`, `edges`, and the neighbor hooks itself, +since the view answers those by asking `graph`. + # Examples ```jldoctest @@ -126,9 +120,21 @@ julia> has_edge(cg, 1, 2) true ``` """ -encoded_graph(graph::AbstractNamedGraph) = EncodedGraphView(graph) +encoded_graph(graph::AbstractNamedGraph) = not_implemented() encoded_graph(graph::AbstractSimpleGraph) = graph +Graphs.rem_vertex!(graph::AbstractNamedGraph, vertex) = not_implemented() +Graphs.add_vertex!(graph::AbstractNamedGraph, vertex) = not_implemented() + +function rename_vertices(f::Function, graph::AbstractNamedGraph) + new_vertices = map(c -> f(decoded_vertex(graph, c)), vertices(encoded_graph(graph))) + return namedgraph(copy(encoded_graph(graph)), new_vertices) +end + +# +# Derived interface (overload for performance) +# + """ vertices(graph::AbstractNamedGraph) -> Dictionaries.AbstractIndices @@ -345,7 +351,7 @@ end The neighbors of `vertex` in `graph`, as vertex names. On a directed graph these are the out-neighbors, following the Graphs.jl convention. `inneighbors`, `outneighbors`, and `all_neighbors` select the other directions and behave the -same way in every other respect, including the caveat below. +same way in every other respect. # Examples diff --git a/src/encodedgraphview.jl b/src/encodedgraphview.jl index 5999611..2eb38bc 100644 --- a/src/encodedgraphview.jl +++ b/src/encodedgraphview.jl @@ -1,13 +1,16 @@ -using Graphs: Graphs, Edge, add_edge!, add_vertex!, edges, has_edge, has_vertex, - inneighbors, is_directed, ne, nv, outneighbors, rem_edge!, rem_vertex!, vertices +using Graphs: Graphs, AbstractGraph, Edge, SimpleDiGraph, SimpleGraph, add_edge!, + add_vertex!, blockdiag, edges, has_edge, has_vertex, inneighbors, is_directed, ne, nv, + outneighbors, rem_edge!, rem_vertex!, vertices -# Reinterprets an AbstractNamedGraph as an AbstractGraph{Int} whose vertices -# are the codes `1:nv(graph)` of the named graph's vertices. -# Default output of `encoded_graph(graph::AbstractNamedGraph)` for graph types -# that are implemented directly, as opposed to as a wrapper around a stored -# integer graph, such as NamedGridGraph. -# Assumes `encoded_vertex(g.graph, v)` and `decoded_vertex(g.graph, c)` are -# implemented for the wrapped graph. +""" + EncodedGraphView(graph::AbstractNamedGraph) + +An `AbstractGraph{Int}` presenting `graph` on its vertex codes `1:nv(graph)`, for +graph types that compute their topology directly rather than storing an integer +graph. Such a type can return `EncodedGraphView(graph)` from [`encoded_graph`](@ref) +and must then define `nv`, `ne`, `has_vertex`, `has_edge`, `edges`, and the neighbor +hooks itself, since the view answers every query by asking `graph`. +""" struct EncodedGraphView{G <: AbstractGraph} <: AbstractGraph{Int} graph::G end @@ -22,6 +25,7 @@ end function Graphs.rem_vertex!(g::EncodedGraphView, v::Int) return rem_vertex!(g.graph, decoded_vertex(g.graph, v)) end +Graphs.has_edge(g::EncodedGraphView, s::Int, d::Int) = has_edge(g, Edge(s, d)) function Graphs.has_edge(g::EncodedGraphView, e::Edge) return has_edge(g.graph, decoded_edge(g.graph, e)) end @@ -32,6 +36,16 @@ function Graphs.rem_edge!(g::EncodedGraphView, e::Edge) return rem_edge!(g.graph, decoded_edge(g.graph, e)) end Graphs.edgetype(g::EncodedGraphView) = Edge{Int} + +# A view has no storage of its own, so its copy is a graph that does, as +# `copy(::SubArray)` gives an `Array`. +Base.copy(g::EncodedGraphView) = (is_directed(g) ? SimpleDiGraph : SimpleGraph)(g) + +# Graphs.jl builds the result of `blockdiag` with `T(n)` for the operand type `T`, +# which a view cannot provide, so a view operand is materialized first. +Graphs.blockdiag(g::EncodedGraphView, h::EncodedGraphView) = blockdiag(copy(g), copy(h)) +Graphs.blockdiag(g::EncodedGraphView, h::AbstractGraph) = blockdiag(copy(g), h) +Graphs.blockdiag(g::AbstractGraph, h::EncodedGraphView) = blockdiag(g, copy(h)) function Graphs.edges(g::EncodedGraphView) return Iterators.map(edges(g.graph)) do e return encoded_edge(g.graph, e) diff --git a/src/graphsextensions/abstractgraph.jl b/src/graphsextensions/abstractgraph.jl index 5dd8719..07dbc66 100644 --- a/src/graphsextensions/abstractgraph.jl +++ b/src/graphsextensions/abstractgraph.jl @@ -608,6 +608,36 @@ function mincut_partitions(graph::AbstractGraph, distmx = weights(graph)) return parts[1], parts[2] end +""" + eccentricities(graph::AbstractGraph, vs = vertices(graph), distmx = weights(graph)) + +The eccentricity of each vertex in `vs`, that is, the length of the longest +shortest path from it to any other vertex, as `eccentricity(graph, v, distmx)` +gives for one vertex. The output has one entry per element of `vs`, keyed the +same way, so for the default `vertices(graph)` it is a `Dictionary` from vertex +to eccentricity. + +# Examples + +```jldoctest +julia> using Graphs: path_graph + +julia> using NamedGraphs: NamedGraph, eccentricities + +julia> g = NamedGraph(path_graph(3), ["a", "b", "c"]); + +julia> eccentricities(g) +3-element Dictionaries.Dictionary{String, Int64}: + "a" │ 2 + "b" │ 1 + "c" │ 2 + +julia> eccentricities(g, ["a", "c"]) +2-element Vector{Int64}: + 2 + 2 +``` +""" eccentricities(graph::AbstractGraph) = eccentricities(graph, vertices(graph)) function eccentricities(graph::AbstractGraph, vs, distmx = weights(graph)) diff --git a/src/namedgridgraph.jl b/src/namedgridgraph.jl index 0d70813..378d03b 100644 --- a/src/namedgridgraph.jl +++ b/src/namedgridgraph.jl @@ -114,7 +114,7 @@ function NamedGridGraph(grid_size::NTuple{N, Int}, ishypertorus::Bool = false) w return NamedGridGraph{N, ishypertorus}(grid_size) end # Minimal interface functions -# `encoded_graph` uses the generic `EncodedGraphView` fallback. +encoded_graph(g::NamedGridGraph) = EncodedGraphView(g) function encoded_vertex(g::NamedGridGraph, vertex) # Membership is just a bounds check here, unlike the dictionary lookup in # `NamedGraph`, so checking it up front costs nothing worth avoiding. diff --git a/test/test_abstractnamedgraph.jl b/test/test_abstractnamedgraph.jl index a4f30ed..9f4ba54 100644 --- a/test/test_abstractnamedgraph.jl +++ b/test/test_abstractnamedgraph.jl @@ -400,3 +400,16 @@ end @test Graphs.rem_vertices!(h, vertices(h)) == 4 @test nv(h) == 0 end + +# Defines only the vertex translation, to check that the rest of the required +# interface fails loudly rather than recursing through `EncodedGraphView`. +struct IncompleteNamedGraph <: AbstractNamedGraph{String} end +NamedGraphs.encoded_vertex(::IncompleteNamedGraph, vertex) = 1 +NamedGraphs.decoded_vertex(::IncompleteNamedGraph, code::Integer) = "a" +Graphs.is_directed(::Type{IncompleteNamedGraph}) = false + +@testset "AbstractNamedGraph incomplete subtype" begin + g = IncompleteNamedGraph() + @test_throws ErrorException nv(g) + @test_throws ErrorException collect(edges(g)) +end diff --git a/test/test_encodedgraphview.jl b/test/test_encodedgraphview.jl index ac47135..354b235 100644 --- a/test/test_encodedgraphview.jl +++ b/test/test_encodedgraphview.jl @@ -1,6 +1,8 @@ -using Graphs: Graphs, Edge, edges, edgetype, has_edge, has_vertex, inneighbors, is_directed, - ne, neighbors, nv, outneighbors, vertices -using NamedGraphs: EncodedGraphView, NamedEdge, NamedGridGraph, vertextype +using Graphs: Graphs, Edge, SimpleGraph, adjacency_matrix, blockdiag, edges, edgetype, + has_edge, has_vertex, inneighbors, is_directed, ne, neighbors, nv, outneighbors, + path_graph, vertices +using NamedGraphs: EncodedGraphView, NamedEdge, NamedGraph, NamedGridGraph, encoded_graph, + rename_vertices, vertextype, ⊔ using Test: @test, @test_broken, @testset @testset "EncodedGraphView" begin @@ -16,3 +18,31 @@ using Test: @test, @test_broken, @testset @test length(edges(pg)) == ne(g) @test all(e -> has_edge(pg, e), edges(pg)) end + +@testset "EncodedGraphView as a Graphs.jl graph" begin + g = NamedGridGraph((2, 3)) + @test encoded_graph(g) isa EncodedGraphView + pg = encoded_graph(g) + # Graphs.jl algorithms call the `(g, s, d)` form of `has_edge`. + @test has_edge(pg, 1, 2) == has_edge(pg, Edge(1, 2)) == true + @test has_edge(pg, 1, 6) == has_edge(pg, Edge(1, 6)) == false + A = adjacency_matrix(g) + @test size(A) == (6, 6) + @test count(!iszero, A) == 2 * ne(g) +end + +@testset "EncodedGraphView materializes where a stored graph is needed" begin + g = NamedGridGraph((2, 3)) + pg = encoded_graph(g) + c = copy(pg) + @test c isa SimpleGraph + @test issetequal(collect(edges(c)), collect(edges(pg))) + r = rename_vertices(string, g) + @test r isa NamedGraph{String} + @test (nv(r), ne(r)) == (nv(g), ne(g)) + h = NamedGraph(path_graph(2), ["a", "b"]) + @test nv(blockdiag(g, h)) == nv(blockdiag(h, g)) == nv(g) + 2 + @test ne(blockdiag(g, h)) == ne(g) + 1 + u = g ⊔ g + @test (nv(u), ne(u)) == (2 * nv(g), 2 * ne(g)) +end diff --git a/test/test_exports.jl b/test/test_exports.jl index ffa1bbd..230ec28 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -20,6 +20,7 @@ using Test: @test, @testset :default_root_vertex, :directed_graph, :disjoint_union, + :eccentricities, :edge_subgraph, :edgeless_graph, :empty_graph, @@ -56,6 +57,7 @@ using Test: @test, @testset # `names` includes `public` names as well as exported ones. public_names = if VERSION >= v"1.11.0-DEV.469" [ + :EncodedGraphView, :PartitionedGraphs, :decoded_edge, :decoded_vertex,