Skip to content

Conversation

@jack-dunham
Copy link
Contributor

This PR includes the following changes to [PartitionedGraphs]

  • Added some additional NamedGraphs interface functions to various data types.
  • Constructing a graph from a QuotientView now directly calls quotient_graph.
  • Calling edges and vertices on QuotientView now returns according to the interface priority.
  • Constructing graphs from QuotientViews of AbstractSimpleGraphs will now return a graph of similar type.

Jack Dunham added 24 commits November 24, 2025 16:44
- `vertices(::QuotientView)` now directly returns the keys
`paritioned_vertices` return value.
- `edges(::QuotientView)` now directly returns the edges of the
`quotient_graph` return value (to coincide with interface overloading
priority)
- Converting a `QuotientView` to graph now calls `quotient_graph`
directly
- Adding methods for return directed/undirected graph types.

Fix imports

Fix rebase
This behaves similarly to `partitionedgraph` function.
…ype by default

This function can now be used `SimpleGraph` etc without promoting to
`NamedGraph`.
This function constructs a graph with no edges, but with vertices of the
quotient graph.
…ype as a parameter

Allows for more generic quotient graph types.
- argument `vertices` must be of type `Base.OneTo{Int}`.
…e, and triple argument methods.

This interface is to overload `similar_graph(graph)`, i.e. the single
argument method.
This function now acts similarly to `similar_graph`.
Jack Dunham added 4 commits December 15, 2025 11:29
The function `to_graph_indexing` now dispatches to `to_graph_indices`
for type of `AbstractGraphIndices` and `to_graph_index` otherwise.
…stic about the iterables they take

Previous behaviour was to accept `Vertices`/`Edges` only.
…sed on `eltype` of `vertices` argument, if provided.
@jack-dunham
Copy link
Contributor Author

  • Consider if similar_graph should allow converting to the eltype of the input vertices.

On this point, somewhere this could be problematic is if vertices is a vector literal resulting in a vector of eltype differing to the vertextype of the graph, or the vertices argument has some abstract eltype due to some type instability somewhere. For example:

In [19]: g = NamedGraph([1.0, 1.5, 2.0])
NamedGraph{Float64} with 3 vertices:
3-element NamedGraphs.OrderedDictionaries.OrderedIndices{Float64}:
 1.0
 1.5
 2.0

and 0 edge(s):
NamedEdge{Float64}[]

In [20]: similar_graph(g, [1, 2]) # use integer literals out of laziness
NamedGraph{Int64} with 2 vertices:
2-element NamedGraphs.OrderedDictionaries.OrderedIndices{Int64}:
 1
 2

and 0 edge(s):
NamedEdge{Int64}[]

In [21]: similar_graph(g, Any[1, 1.5]) # accidentally `eltype==any` due to type instability somewhere
NamedGraph{Any} with 2 vertices:
2-element NamedGraphs.OrderedDictionaries.OrderedIndices{Any}:
 1
 1.5

and 0 edge(s):
NamedEdge{Any}[]

One ends up with graphs with vertextype that might not be intended. However perhaps it is sensible for similar_graph to not be so opinionated here, and have it up to the user to either be careful about the eltype of vertices or be explicit by manually calling:

h = similar_graph(g)
add_vertices!(h, ...)

to make sure vertextype(g) == vertextype(h).

I have made changes to similar_graph to align with this new, less opinionated behavior, however just wondering @mtfishman if you have a perspective in light of this example.

@mtfishman
Copy link
Member

It's a good point, I was wondering about that as well. I would lean towards taking the input literally for now and leave it up to users to input vertices with the intended element/vertex type, and we could circle back to that if it seems to cause problems.

@mtfishman
Copy link
Member

@jack-dunham I had forgetten, but Base.to_index handles both scalar indexes and array of indices: https://github.com/JuliaLang/julia/blob/v1.12.3/base/indices.jl#L279-L315 (Base.to_indices refers to multiple dimensions). So maybe we could just go with to_graph_index for both single vertices/edges and sets of vertices/edges.

end
end

quotients(qvs::QuotientVertexVertices) = getfield(qvs, :quotients)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a case where it would be better to not define this function and instead directly use qvs.quotients, to make it clearer this is just accessing data in the struct and not really a generic function.

Comment on lines +51 to +54
function QuotientEdges{V, E}(edges::Es) where {V, E, Es}
@assert E <: eltype(Es)
return new{V, E, Es}(edges)
end
Copy link
Member

@mtfishman mtfishman Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to offline discussions, I think we should remove this constructor and just get the edge type from the input (we can revisit if use cases arise that don't have reasonable alternatives).

Comment on lines +70 to +82
function Base.iterate(qes::QuotientEdges, state = nothing)
if isnothing(state)
out = iterate(qes.edges)
else
out = iterate(qes.edges, state)
end
if isnothing(out)
return nothing
else
(v, s) = out
return (QuotientEdge(v), s)
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iterate definitions seem a bit complicated to me. It looks like they are all basically a lazy map over iterating over one of the fields. It seems like that can be implemented more simply like this:

struct Wrapper{T}
    data::T
end
Base.length(w::Wrapper) = length(w.data)
Base.eltype(w::Wrapper) = String # Maybe use `Base.promote_op` if the map is more complicated?
function Base.iterate(w::Wrapper, state...)
    return iterate(Iterators.map(string, w.data), state...)
end

which enables:

julia> x = Wrapper([1, 2, 3])
Wrapper{Vector{Int64}}([1, 2, 3])

julia> collect(x)
3-element Vector{String}:
 "1"
 "2"
 "3"

I think then we wouldn't need iterate_graph_indices.

end
end

quotients(qes::QuotientEdgeEdges) = getfield(qes, :quotients)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function needed? Could we just use direct field access?

Vertices(vertices::Vs) where {Vs} = new{eltype(Vs), Vs}(vertices)
end

Vertices(v1, v2, vertices...) = Vertices(vcat([v1, v2], collect(vertices)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of this constructor, I think we should keep constructors simpler and stricter, for example just have one kind of expected input (in this case, only accept a single collection of vertices). Wouldn't this kind of use case already be handled by the basic constructor, and you can pass a Tuple of vertices, i.e. Vertices(("a", "b", "c"))?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a concrete issue with this constructor, it makes Vertices(["a"]) ambiguous, since it isn't clear if that is supposed to be the set of vertices, or just the length-1 case of passing multiple arguments (which then if I understand this constructor, would interpret ["a"] as a vertex itself). So it is better to just avoid that kind of ambiguity and keep constructors stricter.

end
end

Edges(e1, e2, edges...) = Edges(vcat([e1, e2], collect(edges)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

Comment on lines +35 to +48
Base.iterate(gi::AbstractGraphIndices, state = nothing) = iterate_graph_indices(identity, gi, state)
function iterate_graph_indices(f, gi::AbstractGraphIndices, state)
if isnothing(state)
out = iterate(parent_graph_indices(gi))
else
out = iterate(parent_graph_indices(gi), state)
end
if isnothing(out)
return nothing
else
(v, s) = out
return (f(v), s)
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above regarding a simpler iterate definition.

Comment on lines +48 to +50
function Base.iterate(qvs::QuotientVertices, state = nothing)
return NamedGraphs.iterate_graph_indices(QuotientVertex, qvs, state)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above regarding a simpler iterate definition.

Comment on lines +133 to +135
function Base.iterate(qvs::QuotientVertexVertices, state = nothing)
return NamedGraphs.iterate_graph_indices(v -> quotient_index(qvs)[v], qvs, state)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above regarding a simpler iterate definition.

end
end

QuotientVertices(vertices...) = QuotientVertices(collect(vertices))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the ones for the analogous Vertices and Edges constructors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants