Skip to content

Proposal: Make raph operators work with different graph types than SimpleGraph and SimpleDiGraph #441

Open
@simonschoelly

Description

@simonschoelly

Here is a proposal for something I would like to work on and I would like your inputs.

Graph operators are functions that take a number of graphs and return another graphs. Most of them such as complement and blockiag are defined in operators.jl, but there are also others such as transitiveclosure and squash.

Currently most of them are defined only for SimpleGraph and SimpleDiGraph. Others, such as induced_subgraph are defined for AbstractGraph but would very likely fail if used on arbitrary graph type. The reason for this is are:

  • Not all functions necessary to execute these operations are defined for all subtypes of AbstractGraph; such functions are: copying graphs, creating empty graphs, adding or removing vertices and edges.
  • Even if these functions is defined, there are issues when vertices and edges store additional features - for example in some cases the resulting graph has edges that are not in any of the input graphs - in that cases it is not clear how we should populate these features and what the calling signature is to add these features.

Furthermore some operations such as blockdiag (merges two graphs without adding edges between the) are only defined for two input graphs. But especially for this function but probably also for others it would be useful if someone could pass an arbitrary amount of graphs. If someone wants to mere 8 graphs they would currently need 7 calls to that operator.

My proposal is now as follows:

  • We define the operators such that the leftmost argument to the operator defines the output.
  • The leftmost argument can also be a graph type (not an instance of that type) - that type is the resulting type of that operator.
  • We create operators with the leftmost argument being SimpleGraph and SimpleDiGraph. The other input graphs can be arbitrary subtypes of AbstractGraph but we just threat them as Simple{Di}Graph.
  • For other graph types in other packages we leave it to these packages to create there own methods with their graph type being the leftmost argument.

To give some examples, here is the methods I would create for the unary operator complement:

complement(g::SimpleGraph{T}) # -> SimpleGraph{T}
complement(::Type{SimpleGraph}, g::AbstractGraph{T}) # -> SimpleGraph{T}
complement(::Type{SimpleGraph{S}}, g::AbstractGraph) # -> SimpleGraph{S}

and here is an example for the n-ary (currently binary) operator blockdiag

blockdiag(g::SimpleGraph{T}, hs::Vararg{<:AbstractGraph}) # -> SimpleGraph{T}

# This one is not completely type stable (will show up yellow with @code_warntype)
# as we calculate the smallest eltype for the resulting graph. An alternative would be
# to always choose Int or Int64 - but I think this is not as bad as one can always use
# the signature defined below if they need complete type stability.
blockdiag(::Type{SimpleGraph}, hs::Vararg{<:AbstractGraph}) # -> SimpleGraph{S} where S is sufficient large.

blockdiag(::Type{SimpleGraph{S}}, hs::Vararg{<:AbstractGraph}) # -> SimpleGraph{S}

Some other points:

  • If one need to pass other arguments to the operators they can do this with keyword arguments - these arguments might depend on the specific implementation, e.g.
    • How to handle self-loops
    • How to handle metadata
  • How should we handle operators on a mixture of directed and undirected graphs? For now I would propose:
    • If the leftmost argument is directed and some argument further to the right is undirected we just handle this argument as it was a directed graph where for every edge also the reverse edge is in that graph.
    • If the leftmost argument is undirected and some rgument further to the right is directed it is not so clear if an edge should count if it only goes in one direction - for now I would just throw an error to avoid an surprises.
  • It might be tempting to define default operator implementations such as
    op(g::AbstractGraph{T}) = op(SimpleGraph{T}, g)
    but this might lead to unexpected surprises for other graph types - so we should leave the implementation of op to them.
  • For a lot of operators such as blockdiag we could also define inplace versions i.e. blockdiag! that write to their first argument but I would leave that for later.
  • Even with this there might still be surprises in the future, if for example SimpleWeightedGraphs.jl implements
    op(g::SimpleWeightedGraph{T}, h::AbstractGraph)
    such that any edge from h not in g is simply populated with the weight one(T) but then later adds an extension
    op(g::SimpleWeightedGraph{T}, h::MetaGraph; edgekey::Symbol)
    that uses edgekey to get the value of an edge in h then that could lead to breaking changes. I don't know how to get around that - but I also don't think that is a case that appears often.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions