# Determine if the edges of a graph are distinguishable

Mathematica 12.1 introduces edge tagged graph (EdgeTaggedGraph), which is supposed to solve the problem of distinguishability of parallel edges. Unfortunately, the feature is not very well done, and there is no way to guarantee distinguishability. It is allowed that some edges do not have tags at all, or that parallel edges have the very same tag.

Thus, if we were to implement a function which requires that edges be distinguishable, we must check that that is really the case. Since this check will be run every time our function is called, it should be very fast. In Mathematica 12.0 and earlier, which had no edge tagged graphs, one could simply use MultigraphQ. This is an O(1) operation, I assume achieved through caching.

How can I check that all edges of a graph are distinguishable in Mathematica 12.1, with the best possible performance?

My current solution:

nonDistinguishableEdgesQ = MultigraphQ[#] && (Not@EdgeTaggedGraphQ[#] || canonicalEdgeBlock@Not@DuplicateFreeQ@EdgeList[#])&


where canonicalEdgeBlock is from here (a still open question also asking for performance improvements).

I am afraid that unless someone finds an internal function that does this, we are stuck with an O(n) solution. This question is for finding the fastest such solution.

Requirements:

• As fast as possible.
• Must work on all kinds of graphs that Mathematica supports, including mixed graphs.
• It is acceptable (in fact it is practically necessary) for the function to have multiple branches, selecting the fastest possible method for the type of given input graph.
• It is acceptable to use IGraph/M specific functions, such as IGIndexEdgeList, for the implementation.

This is the current version (I am still experimenting with speeding it up): https://github.com/szhorvat/IGraphM/blob/master/IGraphM/PropertyTransformations.m#L15

### Benchmark

Since this is a question, it is appropriate to add a benchmark, so that people can test their attempts. The most important case if tagged graphs where edges are distinguishable. These should be handled as fast as possible.

SeedRandom[99]
g1 = Graph[Range[1000], RandomInteger[{1, 1000}, {50000, 2}]];
tg1 = EdgeTaggedGraph[g1];

g2 = Graph[Range[200], RandomInteger[{1, 200}, {300, 2}]];
tg2 = EdgeTaggedGraph[g2];


The following implementation relies on internal IGraph/M functions, therefore you need to install IGraph/M before it can be used.

nonDistinguishableEdgesQ = MultigraphQ[#] && Not[EdgeTaggedGraphQ[#] && distinguishableTaggedEdgesQ[#]]&;
distinguishableTaggedEdgesQ[graph_] :=
If[MixedGraphQ[graph],
IGraphMPackageScopecanonicalEdgeBlock@DuplicateFreeQ@EdgeList[graph]
,
If[UndirectedGraphQ[graph],
DuplicateFreeQ@Transpose@Append[
Transpose[IGraphMPackageScopeigraphGlobal@"edgeListSortPairs"[IGIndexEdgeList[graph]]],
EdgeTags[graph]
],
DuplicateFreeQ@Transpose@Append[
Transpose[IGIndexEdgeList[graph]],
EdgeTags[graph]
]
]
]


Benchmark results:

RepeatedTiming[nonDistinguishableEdgesQ[tg1], 5]
(* {0.0095, False} *)

RepeatedTiming[nonDistinguishableEdgesQ[tg2], 5]
(* {0.000069, False} *)

• is nonDistinctEQ=MatchQ[{___,Repeated[UndirectedEdge[OrderlessPatternSequence[a_,b_],c___], {2}],___}]@Sort[EdgeList[#]]&; any better?
– kglr
Commented Mar 30, 2020 at 0:35
• @kglr The reason why it will not work is that a<->b and b<->a may not be consecutive in the edge list, even after sorting. Therefore Repeated will not match. Commented Mar 30, 2020 at 6:30
• how about nonDistinctEdgesQ=Not@*DuplicateFreeQ@*Map[SubsetMap[Sort,#, ;;2]&]@*(EdgeList[#,_UndirectedEdge]&)?
– kglr
Commented Mar 30, 2020 at 7:22
• .. or changing Sort[EdgeList[#]]& to SortBy[Sort[#[[;;2]]]&][EdgeList[#]]& in nonDistinctEQ?
– kglr
Commented Mar 31, 2020 at 7:13
• @SHuisman Absolute measurements are misleading. It's about how much it adds in the end. This is a check that some functions need to do before they proceed. Those functions call another, then yet another, each of them doing the check. It will add up. Compare it to something like IGConnectedQ, which takes less time, despite the inefficient conversion from Mathematica to igraph format (!!). (Don't compare it to ConnectedGraphQ, as that one caches the result.) Commented Mar 31, 2020 at 11:11

We can cache the results to speed up subsequent evaluations. There are several ways to do that (including the "LeastRecentlyUsedCache" DataStructure in M12.1), but one issue is that normal caches will hold a reference to the graph whose result was cached. Even if the original graph is Cleared, the cache will hold on to it, preventing memory from being freed. This is a problem if the graph was large.

To get around this, we can use an ExpressionStore, which conveniently allows associating the result directly with the graph. If the graph is removed, so is the cache entry.

After defining:

$exprStore = LanguageNewExpressionStore["MyGraphPropCache"] cachedFun[fun_][arg_] := If[$$exprStore@"containsQ"[arg, fun],$$exprStore@"get"[arg, fun], With[{res = fun[arg]},$exprStore@"put"[arg, fun, res]; res
]
]


we can use cachedFun[distinguishableTaggedEdgesQ][graph] instead of distinguishableTaggedEdgesQ[graph].

• The problem with expression store (which is a weak hash map) is that it stores entries by reference. This means that an expression has to be exact same to be found, meaning it has to point to the same memory location, not just be SameQ. Try for instance: Print[$exprStore@"containsQ"[<|"a" -> 1, "b" -> 2|>, f]];$exprStore@"put"[<|"a" -> 1, "b" -> 2|>, f, 1];Print[$exprStore@"containsQ"[<|"a" -> 1, "b" -> 2|>, f]] - I get False both before and after "put". However, the last line will give True if you store an assoc in a variable and use that variable in all places. Commented Apr 2, 2020 at 9:59 • Because of this, expression stores are not always suitable: they will not work on expressions identical in the sense of SameQ, but different in the sense that they represent two identical copies on the same expression, pointing to different memory locations. Expression stores certainly have their uses (for example, to have some registry of custom objects one creates), but for general caching purposes more often than not we want the cache to work structurally (i.e. with SameQ semantics). Commented Apr 2, 2020 at 10:03 • One small change would be to remove the call to "containsQ" since the "get" method will return Null if the key is missing, and use it like Replace[$exprStore["get"[arg, fun]], Null :> <compute and cache the result>] Commented Apr 2, 2020 at 14:38
• As I think I mentioned in the chat, you can get around the issue of needing the Graph by direct ID by using a secondary ExpressionStore or Association to attach keys, e.g. you could build a secondary store that maps the unique object to its Hash and then use the list of Hash values to resolve collisions, ensuring that you only ever have one version of the graphs that are identical under SameQ`. Commented Apr 2, 2020 at 17:52