Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

View file

@ -0,0 +1,6 @@
from networkx.algorithms.isomorphism.isomorph import *
from networkx.algorithms.isomorphism.vf2userfunc import *
from networkx.algorithms.isomorphism.matchhelpers import *
from networkx.algorithms.isomorphism.temporalisomorphvf2 import *
from networkx.algorithms.isomorphism.ismags import *
from networkx.algorithms.isomorphism.tree_isomorphism import *

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,233 @@
"""
Graph isomorphism functions.
"""
import networkx as nx
from networkx.exception import NetworkXError
__all__ = [
"could_be_isomorphic",
"fast_could_be_isomorphic",
"faster_could_be_isomorphic",
"is_isomorphic",
]
def could_be_isomorphic(G1, G2):
"""Returns False if graphs are definitely not isomorphic.
True does NOT guarantee isomorphism.
Parameters
----------
G1, G2 : graphs
The two graphs G1 and G2 must be the same type.
Notes
-----
Checks for matching degree, triangle, and number of cliques sequences.
"""
# Check global properties
if G1.order() != G2.order():
return False
# Check local properties
d1 = G1.degree()
t1 = nx.triangles(G1)
c1 = nx.number_of_cliques(G1)
props1 = [[d, t1[v], c1[v]] for v, d in d1]
props1.sort()
d2 = G2.degree()
t2 = nx.triangles(G2)
c2 = nx.number_of_cliques(G2)
props2 = [[d, t2[v], c2[v]] for v, d in d2]
props2.sort()
if props1 != props2:
return False
# OK...
return True
graph_could_be_isomorphic = could_be_isomorphic
def fast_could_be_isomorphic(G1, G2):
"""Returns False if graphs are definitely not isomorphic.
True does NOT guarantee isomorphism.
Parameters
----------
G1, G2 : graphs
The two graphs G1 and G2 must be the same type.
Notes
-----
Checks for matching degree and triangle sequences.
"""
# Check global properties
if G1.order() != G2.order():
return False
# Check local properties
d1 = G1.degree()
t1 = nx.triangles(G1)
props1 = [[d, t1[v]] for v, d in d1]
props1.sort()
d2 = G2.degree()
t2 = nx.triangles(G2)
props2 = [[d, t2[v]] for v, d in d2]
props2.sort()
if props1 != props2:
return False
# OK...
return True
fast_graph_could_be_isomorphic = fast_could_be_isomorphic
def faster_could_be_isomorphic(G1, G2):
"""Returns False if graphs are definitely not isomorphic.
True does NOT guarantee isomorphism.
Parameters
----------
G1, G2 : graphs
The two graphs G1 and G2 must be the same type.
Notes
-----
Checks for matching degree sequences.
"""
# Check global properties
if G1.order() != G2.order():
return False
# Check local properties
d1 = sorted(d for n, d in G1.degree())
d2 = sorted(d for n, d in G2.degree())
if d1 != d2:
return False
# OK...
return True
faster_graph_could_be_isomorphic = faster_could_be_isomorphic
def is_isomorphic(G1, G2, node_match=None, edge_match=None):
"""Returns True if the graphs G1 and G2 are isomorphic and False otherwise.
Parameters
----------
G1, G2: graphs
The two graphs G1 and G2 must be the same type.
node_match : callable
A function that returns True if node n1 in G1 and n2 in G2 should
be considered equal during the isomorphism test.
If node_match is not specified then node attributes are not considered.
The function will be called like
node_match(G1.nodes[n1], G2.nodes[n2]).
That is, the function will receive the node attribute dictionaries
for n1 and n2 as inputs.
edge_match : callable
A function that returns True if the edge attribute dictionary
for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
be considered equal during the isomorphism test. If edge_match is
not specified then edge attributes are not considered.
The function will be called like
edge_match(G1[u1][v1], G2[u2][v2]).
That is, the function will receive the edge attribute dictionaries
of the edges under consideration.
Notes
-----
Uses the vf2 algorithm [1]_.
Examples
--------
>>> import networkx.algorithms.isomorphism as iso
For digraphs G1 and G2, using 'weight' edge attribute (default: 1)
>>> G1 = nx.DiGraph()
>>> G2 = nx.DiGraph()
>>> nx.add_path(G1, [1, 2, 3, 4], weight=1)
>>> nx.add_path(G2, [10, 20, 30, 40], weight=2)
>>> em = iso.numerical_edge_match("weight", 1)
>>> nx.is_isomorphic(G1, G2) # no weights considered
True
>>> nx.is_isomorphic(G1, G2, edge_match=em) # match weights
False
For multidigraphs G1 and G2, using 'fill' node attribute (default: '')
>>> G1 = nx.MultiDiGraph()
>>> G2 = nx.MultiDiGraph()
>>> G1.add_nodes_from([1, 2, 3], fill="red")
>>> G2.add_nodes_from([10, 20, 30, 40], fill="red")
>>> nx.add_path(G1, [1, 2, 3, 4], weight=3, linewidth=2.5)
>>> nx.add_path(G2, [10, 20, 30, 40], weight=3)
>>> nm = iso.categorical_node_match("fill", "red")
>>> nx.is_isomorphic(G1, G2, node_match=nm)
True
For multidigraphs G1 and G2, using 'weight' edge attribute (default: 7)
>>> G1.add_edge(1, 2, weight=7)
1
>>> G2.add_edge(10, 20)
1
>>> em = iso.numerical_multiedge_match("weight", 7, rtol=1e-6)
>>> nx.is_isomorphic(G1, G2, edge_match=em)
True
For multigraphs G1 and G2, using 'weight' and 'linewidth' edge attributes
with default values 7 and 2.5. Also using 'fill' node attribute with
default value 'red'.
>>> em = iso.numerical_multiedge_match(["weight", "linewidth"], [7, 2.5])
>>> nm = iso.categorical_node_match("fill", "red")
>>> nx.is_isomorphic(G1, G2, edge_match=em, node_match=nm)
True
See Also
--------
numerical_node_match, numerical_edge_match, numerical_multiedge_match
categorical_node_match, categorical_edge_match, categorical_multiedge_match
References
----------
.. [1] L. P. Cordella, P. Foggia, C. Sansone, M. Vento,
"An Improved Algorithm for Matching Large Graphs",
3rd IAPR-TC15 Workshop on Graph-based Representations in
Pattern Recognition, Cuen, pp. 149-159, 2001.
http://amalfi.dis.unina.it/graph/db/papers/vf-algorithm.pdf
"""
if G1.is_directed() and G2.is_directed():
GM = nx.algorithms.isomorphism.DiGraphMatcher
elif (not G1.is_directed()) and (not G2.is_directed()):
GM = nx.algorithms.isomorphism.GraphMatcher
else:
raise NetworkXError("Graphs G1 and G2 are not of the same type.")
gm = GM(G1, G2, node_match=node_match, edge_match=edge_match)
return gm.is_isomorphic()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,387 @@
"""Functions which help end users define customize node_match and
edge_match functions to use during isomorphism checks.
"""
from itertools import permutations
import types
__all__ = [
"categorical_node_match",
"categorical_edge_match",
"categorical_multiedge_match",
"numerical_node_match",
"numerical_edge_match",
"numerical_multiedge_match",
"generic_node_match",
"generic_edge_match",
"generic_multiedge_match",
]
def copyfunc(f, name=None):
"""Returns a deepcopy of a function."""
return types.FunctionType(
f.__code__, f.__globals__, name or f.__name__, f.__defaults__, f.__closure__
)
def allclose(x, y, rtol=1.0000000000000001e-05, atol=1e-08):
"""Returns True if x and y are sufficiently close, elementwise.
Parameters
----------
rtol : float
The relative error tolerance.
atol : float
The absolute error tolerance.
"""
# assume finite weights, see numpy.allclose() for reference
for xi, yi in zip(x, y):
if not (abs(xi - yi) <= atol + rtol * abs(yi)):
return False
return True
def close(x, y, rtol=1.0000000000000001e-05, atol=1e-08):
"""Returns True if x and y are sufficiently close.
Parameters
----------
rtol : float
The relative error tolerance.
atol : float
The absolute error tolerance.
"""
# assume finite weights, see numpy.allclose() for reference
return abs(x - y) <= atol + rtol * abs(y)
categorical_doc = """
Returns a comparison function for a categorical node attribute.
The value(s) of the attr(s) must be hashable and comparable via the ==
operator since they are placed into a set([]) object. If the sets from
G1 and G2 are the same, then the constructed function returns True.
Parameters
----------
attr : string | list
The categorical node attribute to compare, or a list of categorical
node attributes to compare.
default : value | list
The default value for the categorical node attribute, or a list of
default values for the categorical node attributes.
Returns
-------
match : function
The customized, categorical `node_match` function.
Examples
--------
>>> import networkx.algorithms.isomorphism as iso
>>> nm = iso.categorical_node_match("size", 1)
>>> nm = iso.categorical_node_match(["color", "size"], ["red", 2])
"""
def categorical_node_match(attr, default):
if isinstance(attr, str):
def match(data1, data2):
return data1.get(attr, default) == data2.get(attr, default)
else:
attrs = list(zip(attr, default)) # Python 3
def match(data1, data2):
return all(data1.get(attr, d) == data2.get(attr, d) for attr, d in attrs)
return match
try:
categorical_edge_match = copyfunc(categorical_node_match, "categorical_edge_match")
except NotImplementedError:
# IronPython lacks support for types.FunctionType.
# https://github.com/networkx/networkx/issues/949
# https://github.com/networkx/networkx/issues/1127
def categorical_edge_match(*args, **kwargs):
return categorical_node_match(*args, **kwargs)
def categorical_multiedge_match(attr, default):
if isinstance(attr, str):
def match(datasets1, datasets2):
values1 = {data.get(attr, default) for data in datasets1.values()}
values2 = {data.get(attr, default) for data in datasets2.values()}
return values1 == values2
else:
attrs = list(zip(attr, default)) # Python 3
def match(datasets1, datasets2):
values1 = set()
for data1 in datasets1.values():
x = tuple(data1.get(attr, d) for attr, d in attrs)
values1.add(x)
values2 = set()
for data2 in datasets2.values():
x = tuple(data2.get(attr, d) for attr, d in attrs)
values2.add(x)
return values1 == values2
return match
# Docstrings for categorical functions.
categorical_node_match.__doc__ = categorical_doc
categorical_edge_match.__doc__ = categorical_doc.replace("node", "edge")
tmpdoc = categorical_doc.replace("node", "edge")
tmpdoc = tmpdoc.replace("categorical_edge_match", "categorical_multiedge_match")
categorical_multiedge_match.__doc__ = tmpdoc
numerical_doc = """
Returns a comparison function for a numerical node attribute.
The value(s) of the attr(s) must be numerical and sortable. If the
sorted list of values from G1 and G2 are the same within some
tolerance, then the constructed function returns True.
Parameters
----------
attr : string | list
The numerical node attribute to compare, or a list of numerical
node attributes to compare.
default : value | list
The default value for the numerical node attribute, or a list of
default values for the numerical node attributes.
rtol : float
The relative error tolerance.
atol : float
The absolute error tolerance.
Returns
-------
match : function
The customized, numerical `node_match` function.
Examples
--------
>>> import networkx.algorithms.isomorphism as iso
>>> nm = iso.numerical_node_match("weight", 1.0)
>>> nm = iso.numerical_node_match(["weight", "linewidth"], [0.25, 0.5])
"""
def numerical_node_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08):
if isinstance(attr, str):
def match(data1, data2):
return close(
data1.get(attr, default), data2.get(attr, default), rtol=rtol, atol=atol
)
else:
attrs = list(zip(attr, default)) # Python 3
def match(data1, data2):
values1 = [data1.get(attr, d) for attr, d in attrs]
values2 = [data2.get(attr, d) for attr, d in attrs]
return allclose(values1, values2, rtol=rtol, atol=atol)
return match
try:
numerical_edge_match = copyfunc(numerical_node_match, "numerical_edge_match")
except NotImplementedError:
# IronPython lacks support for types.FunctionType.
# https://github.com/networkx/networkx/issues/949
# https://github.com/networkx/networkx/issues/1127
def numerical_edge_match(*args, **kwargs):
return numerical_node_match(*args, **kwargs)
def numerical_multiedge_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08):
if isinstance(attr, str):
def match(datasets1, datasets2):
values1 = sorted([data.get(attr, default) for data in datasets1.values()])
values2 = sorted([data.get(attr, default) for data in datasets2.values()])
return allclose(values1, values2, rtol=rtol, atol=atol)
else:
attrs = list(zip(attr, default)) # Python 3
def match(datasets1, datasets2):
values1 = []
for data1 in datasets1.values():
x = tuple(data1.get(attr, d) for attr, d in attrs)
values1.append(x)
values2 = []
for data2 in datasets2.values():
x = tuple(data2.get(attr, d) for attr, d in attrs)
values2.append(x)
values1.sort()
values2.sort()
for xi, yi in zip(values1, values2):
if not allclose(xi, yi, rtol=rtol, atol=atol):
return False
else:
return True
return match
# Docstrings for numerical functions.
numerical_node_match.__doc__ = numerical_doc
numerical_edge_match.__doc__ = numerical_doc.replace("node", "edge")
tmpdoc = numerical_doc.replace("node", "edge")
tmpdoc = tmpdoc.replace("numerical_edge_match", "numerical_multiedge_match")
numerical_multiedge_match.__doc__ = tmpdoc
generic_doc = """
Returns a comparison function for a generic attribute.
The value(s) of the attr(s) are compared using the specified
operators. If all the attributes are equal, then the constructed
function returns True.
Parameters
----------
attr : string | list
The node attribute to compare, or a list of node attributes
to compare.
default : value | list
The default value for the node attribute, or a list of
default values for the node attributes.
op : callable | list
The operator to use when comparing attribute values, or a list
of operators to use when comparing values for each attribute.
Returns
-------
match : function
The customized, generic `node_match` function.
Examples
--------
>>> from operator import eq
>>> from networkx.algorithms.isomorphism.matchhelpers import close
>>> from networkx.algorithms.isomorphism import generic_node_match
>>> nm = generic_node_match("weight", 1.0, close)
>>> nm = generic_node_match("color", "red", eq)
>>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [close, eq])
"""
def generic_node_match(attr, default, op):
if isinstance(attr, str):
def match(data1, data2):
return op(data1.get(attr, default), data2.get(attr, default))
else:
attrs = list(zip(attr, default, op)) # Python 3
def match(data1, data2):
for attr, d, operator in attrs:
if not operator(data1.get(attr, d), data2.get(attr, d)):
return False
else:
return True
return match
try:
generic_edge_match = copyfunc(generic_node_match, "generic_edge_match")
except NotImplementedError:
# IronPython lacks support for types.FunctionType.
# https://github.com/networkx/networkx/issues/949
# https://github.com/networkx/networkx/issues/1127
def generic_edge_match(*args, **kwargs):
return generic_node_match(*args, **kwargs)
def generic_multiedge_match(attr, default, op):
"""Returns a comparison function for a generic attribute.
The value(s) of the attr(s) are compared using the specified
operators. If all the attributes are equal, then the constructed
function returns True. Potentially, the constructed edge_match
function can be slow since it must verify that no isomorphism
exists between the multiedges before it returns False.
Parameters
----------
attr : string | list
The edge attribute to compare, or a list of node attributes
to compare.
default : value | list
The default value for the edge attribute, or a list of
default values for the dgeattributes.
op : callable | list
The operator to use when comparing attribute values, or a list
of operators to use when comparing values for each attribute.
Returns
-------
match : function
The customized, generic `edge_match` function.
Examples
--------
>>> from operator import eq
>>> from networkx.algorithms.isomorphism.matchhelpers import close
>>> from networkx.algorithms.isomorphism import generic_node_match
>>> nm = generic_node_match("weight", 1.0, close)
>>> nm = generic_node_match("color", "red", eq)
>>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [close, eq])
...
"""
# This is slow, but generic.
# We must test every possible isomorphism between the edges.
if isinstance(attr, str):
attr = [attr]
default = [default]
op = [op]
attrs = list(zip(attr, default)) # Python 3
def match(datasets1, datasets2):
values1 = []
for data1 in datasets1.values():
x = tuple(data1.get(attr, d) for attr, d in attrs)
values1.append(x)
values2 = []
for data2 in datasets2.values():
x = tuple(data2.get(attr, d) for attr, d in attrs)
values2.append(x)
for vals2 in permutations(values2):
for xi, yi in zip(values1, vals2):
if not all(map(lambda x, y, z: z(x, y), xi, yi, op)):
# This is not an isomorphism, go to next permutation.
break
else:
# Then we found an isomorphism.
return True
else:
# Then there are no isomorphisms between the multiedges.
return False
return match
# Docstrings for numerical functions.
generic_node_match.__doc__ = generic_doc
generic_edge_match.__doc__ = generic_doc.replace("node", "edge")

View file

@ -0,0 +1,307 @@
"""
*****************************
Time-respecting VF2 Algorithm
*****************************
An extension of the VF2 algorithm for time-respecting graph ismorphism
testing in temporal graphs.
A temporal graph is one in which edges contain a datetime attribute,
denoting when interaction occurred between the incident nodes. A
time-respecting subgraph of a temporal graph is a subgraph such that
all interactions incident to a node occurred within a time threshold,
delta, of each other. A directed time-respecting subgraph has the
added constraint that incoming interactions to a node must precede
outgoing interactions from the same node - this enforces a sense of
directed flow.
Introduction
------------
The TimeRespectingGraphMatcher and TimeRespectingDiGraphMatcher
extend the GraphMatcher and DiGraphMatcher classes, respectively,
to include temporal constraints on matches. This is achieved through
a semantic check, via the semantic_feasibility() function.
As well as including G1 (the graph in which to seek embeddings) and
G2 (the subgraph structure of interest), the name of the temporal
attribute on the edges and the time threshold, delta, must be supplied
as arguments to the matching constructors.
A delta of zero is the strictest temporal constraint on the match -
only embeddings in which all interactions occur at the same time will
be returned. A delta of one day will allow embeddings in which
adjacent interactions occur up to a day apart.
Examples
--------
Examples will be provided when the datetime type has been incorporated.
Temporal Subgraph Isomorphism
-----------------------------
A brief discussion of the somewhat diverse current literature will be
included here.
References
----------
[1] Redmond, U. and Cunningham, P. Temporal subgraph isomorphism. In:
The 2013 IEEE/ACM International Conference on Advances in Social
Networks Analysis and Mining (ASONAM). Niagara Falls, Canada; 2013:
pages 1451 - 1452. [65]
For a discussion of the literature on temporal networks:
[3] P. Holme and J. Saramaki. Temporal networks. Physics Reports,
519(3):97125, 2012.
Notes
-----
Handles directed and undirected graphs and graphs with parallel edges.
"""
import networkx as nx
from .isomorphvf2 import GraphMatcher, DiGraphMatcher
__all__ = ["TimeRespectingGraphMatcher", "TimeRespectingDiGraphMatcher"]
class TimeRespectingGraphMatcher(GraphMatcher):
def __init__(self, G1, G2, temporal_attribute_name, delta):
"""Initialize TimeRespectingGraphMatcher.
G1 and G2 should be nx.Graph or nx.MultiGraph instances.
Examples
--------
To create a TimeRespectingGraphMatcher which checks for
syntactic and semantic feasibility:
>>> from networkx.algorithms import isomorphism
>>> from datetime import timedelta
>>> G1 = nx.Graph(nx.path_graph(4, create_using=nx.Graph()))
>>> G2 = nx.Graph(nx.path_graph(4, create_using=nx.Graph()))
>>> GM = isomorphism.TimeRespectingGraphMatcher(
... G1, G2, "date", timedelta(days=1)
... )
"""
self.temporal_attribute_name = temporal_attribute_name
self.delta = delta
super().__init__(G1, G2)
def one_hop(self, Gx, Gx_node, neighbors):
"""
Edges one hop out from a node in the mapping should be
time-respecting with respect to each other.
"""
dates = []
for n in neighbors:
if isinstance(Gx, nx.Graph): # Graph G[u][v] returns the data dictionary.
dates.append(Gx[Gx_node][n][self.temporal_attribute_name])
else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
for edge in Gx[Gx_node][
n
].values(): # Iterates all edges between node pair.
dates.append(edge[self.temporal_attribute_name])
if any(x is None for x in dates):
raise ValueError("Datetime not supplied for at least one edge.")
return not dates or max(dates) - min(dates) <= self.delta
def two_hop(self, Gx, core_x, Gx_node, neighbors):
"""
Paths of length 2 from Gx_node should be time-respecting.
"""
return all(
self.one_hop(Gx, v, [n for n in Gx[v] if n in core_x] + [Gx_node])
for v in neighbors
)
def semantic_feasibility(self, G1_node, G2_node):
"""Returns True if adding (G1_node, G2_node) is semantically
feasible.
Any subclass which redefines semantic_feasibility() must
maintain the self.tests if needed, to keep the match() method
functional. Implementations should consider multigraphs.
"""
neighbors = [n for n in self.G1[G1_node] if n in self.core_1]
if not self.one_hop(self.G1, G1_node, neighbors): # Fail fast on first node.
return False
if not self.two_hop(self.G1, self.core_1, G1_node, neighbors):
return False
# Otherwise, this node is semantically feasible!
return True
class TimeRespectingDiGraphMatcher(DiGraphMatcher):
def __init__(self, G1, G2, temporal_attribute_name, delta):
"""Initialize TimeRespectingDiGraphMatcher.
G1 and G2 should be nx.DiGraph or nx.MultiDiGraph instances.
Examples
--------
To create a TimeRespectingDiGraphMatcher which checks for
syntactic and semantic feasibility:
>>> from networkx.algorithms import isomorphism
>>> from datetime import timedelta
>>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
>>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
>>> GM = isomorphism.TimeRespectingDiGraphMatcher(
... G1, G2, "date", timedelta(days=1)
... )
"""
self.temporal_attribute_name = temporal_attribute_name
self.delta = delta
super().__init__(G1, G2)
def get_pred_dates(self, Gx, Gx_node, core_x, pred):
"""
Get the dates of edges from predecessors.
"""
pred_dates = []
if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary.
for n in pred:
pred_dates.append(Gx[n][Gx_node][self.temporal_attribute_name])
else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
for n in pred:
for edge in Gx[n][
Gx_node
].values(): # Iterates all edge data between node pair.
pred_dates.append(edge[self.temporal_attribute_name])
return pred_dates
def get_succ_dates(self, Gx, Gx_node, core_x, succ):
"""
Get the dates of edges to successors.
"""
succ_dates = []
if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary.
for n in succ:
succ_dates.append(Gx[Gx_node][n][self.temporal_attribute_name])
else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
for n in succ:
for edge in Gx[Gx_node][
n
].values(): # Iterates all edge data between node pair.
succ_dates.append(edge[self.temporal_attribute_name])
return succ_dates
def one_hop(self, Gx, Gx_node, core_x, pred, succ):
"""
The ego node.
"""
pred_dates = self.get_pred_dates(Gx, Gx_node, core_x, pred)
succ_dates = self.get_succ_dates(Gx, Gx_node, core_x, succ)
return self.test_one(pred_dates, succ_dates) and self.test_two(
pred_dates, succ_dates
)
def two_hop_pred(self, Gx, Gx_node, core_x, pred):
"""
The predeccessors of the ego node.
"""
return all(
self.one_hop(
Gx,
p,
core_x,
self.preds(Gx, core_x, p),
self.succs(Gx, core_x, p, Gx_node),
)
for p in pred
)
def two_hop_succ(self, Gx, Gx_node, core_x, succ):
"""
The successors of the ego node.
"""
return all(
self.one_hop(
Gx,
s,
core_x,
self.preds(Gx, core_x, s, Gx_node),
self.succs(Gx, core_x, s),
)
for s in succ
)
def preds(self, Gx, core_x, v, Gx_node=None):
pred = [n for n in Gx.predecessors(v) if n in core_x]
if Gx_node:
pred.append(Gx_node)
return pred
def succs(self, Gx, core_x, v, Gx_node=None):
succ = [n for n in Gx.successors(v) if n in core_x]
if Gx_node:
succ.append(Gx_node)
return succ
def test_one(self, pred_dates, succ_dates):
"""
Edges one hop out from Gx_node in the mapping should be
time-respecting with respect to each other, regardless of
direction.
"""
time_respecting = True
dates = pred_dates + succ_dates
if any(x is None for x in dates):
raise ValueError("Date or datetime not supplied for at least one edge.")
dates.sort() # Small to large.
if 0 < len(dates) and not (dates[-1] - dates[0] <= self.delta):
time_respecting = False
return time_respecting
def test_two(self, pred_dates, succ_dates):
"""
Edges from a dual Gx_node in the mapping should be ordered in
a time-respecting manner.
"""
time_respecting = True
pred_dates.sort()
succ_dates.sort()
# First out before last in; negative of the necessary condition for time-respect.
if (
0 < len(succ_dates)
and 0 < len(pred_dates)
and succ_dates[0] < pred_dates[-1]
):
time_respecting = False
return time_respecting
def semantic_feasibility(self, G1_node, G2_node):
"""Returns True if adding (G1_node, G2_node) is semantically
feasible.
Any subclass which redefines semantic_feasibility() must
maintain the self.tests if needed, to keep the match() method
functional. Implementations should consider multigraphs.
"""
pred, succ = (
[n for n in self.G1.predecessors(G1_node) if n in self.core_1],
[n for n in self.G1.successors(G1_node) if n in self.core_1],
)
if not self.one_hop(
self.G1, G1_node, self.core_1, pred, succ
): # Fail fast on first node.
return False
if not self.two_hop_pred(self.G1, G1_node, self.core_1, pred):
return False
if not self.two_hop_succ(self.G1, G1_node, self.core_1, succ):
return False
# Otherwise, this node is semantically feasible!
return True

View file

@ -0,0 +1,327 @@
"""
Tests for ISMAGS isomorphism algorithm.
"""
import pytest
import networkx as nx
from networkx.algorithms import isomorphism as iso
def _matches_to_sets(matches):
"""
Helper function to facilitate comparing collections of dictionaries in
which order does not matter.
"""
return set(map(lambda m: frozenset(m.items()), matches))
class TestSelfIsomorphism:
data = [
(
[
(0, dict(name="a")),
(1, dict(name="a")),
(2, dict(name="b")),
(3, dict(name="b")),
(4, dict(name="a")),
(5, dict(name="a")),
],
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
),
(range(1, 5), [(1, 2), (2, 4), (4, 3), (3, 1)]),
(
[],
[
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 0),
(0, 6),
(6, 7),
(2, 8),
(8, 9),
(4, 10),
(10, 11),
],
),
([], [(0, 1), (1, 2), (1, 4), (2, 3), (3, 5), (3, 6)]),
]
def test_self_isomorphism(self):
"""
For some small, symmetric graphs, make sure that 1) they are isomorphic
to themselves, and 2) that only the identity mapping is found.
"""
for node_data, edge_data in self.data:
graph = nx.Graph()
graph.add_nodes_from(node_data)
graph.add_edges_from(edge_data)
ismags = iso.ISMAGS(
graph, graph, node_match=iso.categorical_node_match("name", None)
)
assert ismags.is_isomorphic()
assert ismags.subgraph_is_isomorphic()
assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == [
{n: n for n in graph.nodes}
]
def test_edgecase_self_isomorphism(self):
"""
This edgecase is one of the cases in which it is hard to find all
symmetry elements.
"""
graph = nx.Graph()
nx.add_path(graph, range(5))
graph.add_edges_from([(2, 5), (5, 6)])
ismags = iso.ISMAGS(graph, graph)
ismags_answer = list(ismags.find_isomorphisms(True))
assert ismags_answer == [{n: n for n in graph.nodes}]
graph = nx.relabel_nodes(graph, {0: 0, 1: 1, 2: 2, 3: 3, 4: 6, 5: 4, 6: 5})
ismags = iso.ISMAGS(graph, graph)
ismags_answer = list(ismags.find_isomorphisms(True))
assert ismags_answer == [{n: n for n in graph.nodes}]
@pytest.mark.skip()
def test_directed_self_isomorphism(self):
"""
For some small, directed, symmetric graphs, make sure that 1) they are
isomorphic to themselves, and 2) that only the identity mapping is
found.
"""
for node_data, edge_data in self.data:
graph = nx.Graph()
graph.add_nodes_from(node_data)
graph.add_edges_from(edge_data)
ismags = iso.ISMAGS(
graph, graph, node_match=iso.categorical_node_match("name", None)
)
assert ismags.is_isomorphic()
assert ismags.subgraph_is_isomorphic()
assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == [
{n: n for n in graph.nodes}
]
class TestSubgraphIsomorphism:
def test_isomorphism(self):
g1 = nx.Graph()
nx.add_cycle(g1, range(4))
g2 = nx.Graph()
nx.add_cycle(g2, range(4))
g2.add_edges_from([(n, m) for n, m in zip(g2, range(4, 8))])
ismags = iso.ISMAGS(g2, g1)
assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == [
{n: n for n in g1.nodes}
]
def test_isomorphism2(self):
g1 = nx.Graph()
nx.add_path(g1, range(3))
g2 = g1.copy()
g2.add_edge(1, 3)
ismags = iso.ISMAGS(g2, g1)
matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
expected_symmetric = [
{0: 0, 1: 1, 2: 2},
{0: 0, 1: 1, 3: 2},
{2: 0, 1: 1, 3: 2},
]
assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
expected_asymmetric = [
{0: 2, 1: 1, 2: 0},
{0: 2, 1: 1, 3: 0},
{2: 2, 1: 1, 3: 0},
]
assert _matches_to_sets(matches) == _matches_to_sets(
expected_symmetric + expected_asymmetric
)
def test_labeled_nodes(self):
g1 = nx.Graph()
nx.add_cycle(g1, range(3))
g1.nodes[1]["attr"] = True
g2 = g1.copy()
g2.add_edge(1, 3)
ismags = iso.ISMAGS(g2, g1, node_match=lambda x, y: x == y)
matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
expected_symmetric = [{0: 0, 1: 1, 2: 2}]
assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
expected_asymmetric = [{0: 2, 1: 1, 2: 0}]
assert _matches_to_sets(matches) == _matches_to_sets(
expected_symmetric + expected_asymmetric
)
def test_labeled_edges(self):
g1 = nx.Graph()
nx.add_cycle(g1, range(3))
g1.edges[1, 2]["attr"] = True
g2 = g1.copy()
g2.add_edge(1, 3)
ismags = iso.ISMAGS(g2, g1, edge_match=lambda x, y: x == y)
matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
expected_symmetric = [{0: 0, 1: 1, 2: 2}]
assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
expected_asymmetric = [{1: 2, 0: 0, 2: 1}]
assert _matches_to_sets(matches) == _matches_to_sets(
expected_symmetric + expected_asymmetric
)
class TestWikipediaExample:
# Nodes 'a', 'b', 'c' and 'd' form a column.
# Nodes 'g', 'h', 'i' and 'j' form a column.
g1edges = [
["a", "g"],
["a", "h"],
["a", "i"],
["b", "g"],
["b", "h"],
["b", "j"],
["c", "g"],
["c", "i"],
["c", "j"],
["d", "h"],
["d", "i"],
["d", "j"],
]
# Nodes 1,2,3,4 form the clockwise corners of a large square.
# Nodes 5,6,7,8 form the clockwise corners of a small square
g2edges = [
[1, 2],
[2, 3],
[3, 4],
[4, 1],
[5, 6],
[6, 7],
[7, 8],
[8, 5],
[1, 5],
[2, 6],
[3, 7],
[4, 8],
]
def test_graph(self):
g1 = nx.Graph()
g2 = nx.Graph()
g1.add_edges_from(self.g1edges)
g2.add_edges_from(self.g2edges)
gm = iso.ISMAGS(g1, g2)
assert gm.is_isomorphic()
class TestLargestCommonSubgraph:
def test_mcis(self):
# Example graphs from DOI: 10.1002/spe.588
graph1 = nx.Graph()
graph1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 4), (4, 5)])
graph1.nodes[1]["color"] = 0
graph2 = nx.Graph()
graph2.add_edges_from(
[(1, 2), (2, 3), (2, 4), (3, 4), (3, 5), (5, 6), (5, 7), (6, 7)]
)
graph2.nodes[1]["color"] = 1
graph2.nodes[6]["color"] = 2
graph2.nodes[7]["color"] = 2
ismags = iso.ISMAGS(
graph1, graph2, node_match=iso.categorical_node_match("color", None)
)
assert list(ismags.subgraph_isomorphisms_iter(True)) == []
assert list(ismags.subgraph_isomorphisms_iter(False)) == []
found_mcis = _matches_to_sets(ismags.largest_common_subgraph())
expected = _matches_to_sets(
[{2: 2, 3: 4, 4: 3, 5: 5}, {2: 4, 3: 2, 4: 3, 5: 5}]
)
assert expected == found_mcis
ismags = iso.ISMAGS(
graph2, graph1, node_match=iso.categorical_node_match("color", None)
)
assert list(ismags.subgraph_isomorphisms_iter(True)) == []
assert list(ismags.subgraph_isomorphisms_iter(False)) == []
found_mcis = _matches_to_sets(ismags.largest_common_subgraph())
# Same answer, but reversed.
expected = _matches_to_sets(
[{2: 2, 3: 4, 4: 3, 5: 5}, {4: 2, 2: 3, 3: 4, 5: 5}]
)
assert expected == found_mcis
def test_symmetry_mcis(self):
graph1 = nx.Graph()
nx.add_path(graph1, range(4))
graph2 = nx.Graph()
nx.add_path(graph2, range(3))
graph2.add_edge(1, 3)
# Only the symmetry of graph2 is taken into account here.
ismags1 = iso.ISMAGS(
graph1, graph2, node_match=iso.categorical_node_match("color", None)
)
assert list(ismags1.subgraph_isomorphisms_iter(True)) == []
found_mcis = _matches_to_sets(ismags1.largest_common_subgraph())
expected = _matches_to_sets([{0: 0, 1: 1, 2: 2}, {1: 0, 3: 2, 2: 1}])
assert expected == found_mcis
# Only the symmetry of graph1 is taken into account here.
ismags2 = iso.ISMAGS(
graph2, graph1, node_match=iso.categorical_node_match("color", None)
)
assert list(ismags2.subgraph_isomorphisms_iter(True)) == []
found_mcis = _matches_to_sets(ismags2.largest_common_subgraph())
expected = _matches_to_sets(
[
{3: 2, 0: 0, 1: 1},
{2: 0, 0: 2, 1: 1},
{3: 0, 0: 2, 1: 1},
{3: 0, 1: 1, 2: 2},
{0: 0, 1: 1, 2: 2},
{2: 0, 3: 2, 1: 1},
]
)
assert expected == found_mcis
found_mcis1 = _matches_to_sets(ismags1.largest_common_subgraph(False))
found_mcis2 = ismags2.largest_common_subgraph(False)
found_mcis2 = [{v: k for k, v in d.items()} for d in found_mcis2]
found_mcis2 = _matches_to_sets(found_mcis2)
expected = _matches_to_sets(
[
{3: 2, 1: 3, 2: 1},
{2: 0, 0: 2, 1: 1},
{1: 2, 3: 3, 2: 1},
{3: 0, 1: 3, 2: 1},
{0: 2, 2: 3, 1: 1},
{3: 0, 1: 2, 2: 1},
{2: 0, 0: 3, 1: 1},
{0: 0, 2: 3, 1: 1},
{1: 0, 3: 3, 2: 1},
{1: 0, 3: 2, 2: 1},
{0: 3, 1: 1, 2: 2},
{0: 0, 1: 1, 2: 2},
]
)
assert expected == found_mcis1
assert expected == found_mcis2

View file

@ -0,0 +1,40 @@
import networkx as nx
from networkx.algorithms import isomorphism as iso
class TestIsomorph:
@classmethod
def setup_class(cls):
cls.G1 = nx.Graph()
cls.G2 = nx.Graph()
cls.G3 = nx.Graph()
cls.G4 = nx.Graph()
cls.G5 = nx.Graph()
cls.G6 = nx.Graph()
cls.G1.add_edges_from([[1, 2], [1, 3], [1, 5], [2, 3]])
cls.G2.add_edges_from([[10, 20], [20, 30], [10, 30], [10, 50]])
cls.G3.add_edges_from([[1, 2], [1, 3], [1, 5], [2, 5]])
cls.G4.add_edges_from([[1, 2], [1, 3], [1, 5], [2, 4]])
cls.G5.add_edges_from([[1, 2], [1, 3]])
cls.G6.add_edges_from([[10, 20], [20, 30], [10, 30], [10, 50], [20, 50]])
def test_could_be_isomorphic(self):
assert iso.could_be_isomorphic(self.G1, self.G2)
assert iso.could_be_isomorphic(self.G1, self.G3)
assert not iso.could_be_isomorphic(self.G1, self.G4)
assert iso.could_be_isomorphic(self.G3, self.G2)
assert not iso.could_be_isomorphic(self.G1, self.G6)
def test_fast_could_be_isomorphic(self):
assert iso.fast_could_be_isomorphic(self.G3, self.G2)
assert not iso.fast_could_be_isomorphic(self.G3, self.G5)
assert not iso.fast_could_be_isomorphic(self.G1, self.G6)
def test_faster_could_be_isomorphic(self):
assert iso.faster_could_be_isomorphic(self.G3, self.G2)
assert not iso.faster_could_be_isomorphic(self.G3, self.G5)
assert not iso.faster_could_be_isomorphic(self.G1, self.G6)
def test_is_isomorphic(self):
assert iso.is_isomorphic(self.G1, self.G2)
assert not iso.is_isomorphic(self.G1, self.G4)

View file

@ -0,0 +1,407 @@
"""
Tests for VF2 isomorphism algorithm.
"""
import os
import struct
import random
import networkx as nx
from networkx.algorithms import isomorphism as iso
class TestWikipediaExample:
# Source: https://en.wikipedia.org/wiki/Graph_isomorphism
# Nodes 'a', 'b', 'c' and 'd' form a column.
# Nodes 'g', 'h', 'i' and 'j' form a column.
g1edges = [
["a", "g"],
["a", "h"],
["a", "i"],
["b", "g"],
["b", "h"],
["b", "j"],
["c", "g"],
["c", "i"],
["c", "j"],
["d", "h"],
["d", "i"],
["d", "j"],
]
# Nodes 1,2,3,4 form the clockwise corners of a large square.
# Nodes 5,6,7,8 form the clockwise corners of a small square
g2edges = [
[1, 2],
[2, 3],
[3, 4],
[4, 1],
[5, 6],
[6, 7],
[7, 8],
[8, 5],
[1, 5],
[2, 6],
[3, 7],
[4, 8],
]
def test_graph(self):
g1 = nx.Graph()
g2 = nx.Graph()
g1.add_edges_from(self.g1edges)
g2.add_edges_from(self.g2edges)
gm = iso.GraphMatcher(g1, g2)
assert gm.is_isomorphic()
# Just testing some cases
assert gm.subgraph_is_monomorphic()
mapping = sorted(gm.mapping.items())
# this mapping is only one of the possibilies
# so this test needs to be reconsidered
# isomap = [('a', 1), ('b', 6), ('c', 3), ('d', 8),
# ('g', 2), ('h', 5), ('i', 4), ('j', 7)]
# assert_equal(mapping, isomap)
def test_subgraph(self):
g1 = nx.Graph()
g2 = nx.Graph()
g1.add_edges_from(self.g1edges)
g2.add_edges_from(self.g2edges)
g3 = g2.subgraph([1, 2, 3, 4])
gm = iso.GraphMatcher(g1, g3)
assert gm.subgraph_is_isomorphic()
def test_subgraph_mono(self):
g1 = nx.Graph()
g2 = nx.Graph()
g1.add_edges_from(self.g1edges)
g2.add_edges_from([[1, 2], [2, 3], [3, 4]])
gm = iso.GraphMatcher(g1, g2)
assert gm.subgraph_is_monomorphic()
class TestVF2GraphDB:
# http://amalfi.dis.unina.it/graph/db/
@staticmethod
def create_graph(filename):
"""Creates a Graph instance from the filename."""
# The file is assumed to be in the format from the VF2 graph database.
# Each file is composed of 16-bit numbers (unsigned short int).
# So we will want to read 2 bytes at a time.
# We can read the number as follows:
# number = struct.unpack('<H', file.read(2))
# This says, expect the data in little-endian encoding
# as an unsigned short int and unpack 2 bytes from the file.
fh = open(filename, mode="rb")
# Grab the number of nodes.
# Node numeration is 0-based, so the first node has index 0.
nodes = struct.unpack("<H", fh.read(2))[0]
graph = nx.Graph()
for from_node in range(nodes):
# Get the number of edges.
edges = struct.unpack("<H", fh.read(2))[0]
for edge in range(edges):
# Get the terminal node.
to_node = struct.unpack("<H", fh.read(2))[0]
graph.add_edge(from_node, to_node)
fh.close()
return graph
def test_graph(self):
head, tail = os.path.split(__file__)
g1 = self.create_graph(os.path.join(head, "iso_r01_s80.A99"))
g2 = self.create_graph(os.path.join(head, "iso_r01_s80.B99"))
gm = iso.GraphMatcher(g1, g2)
assert gm.is_isomorphic()
def test_subgraph(self):
# A is the subgraph
# B is the full graph
head, tail = os.path.split(__file__)
subgraph = self.create_graph(os.path.join(head, "si2_b06_m200.A99"))
graph = self.create_graph(os.path.join(head, "si2_b06_m200.B99"))
gm = iso.GraphMatcher(graph, subgraph)
assert gm.subgraph_is_isomorphic()
# Just testing some cases
assert gm.subgraph_is_monomorphic()
# There isn't a similar test implemented for subgraph monomorphism,
# feel free to create one.
class TestAtlas:
@classmethod
def setup_class(cls):
global atlas
# import platform
# import pytest
# if platform.python_implementation() == 'Jython':
# pytest.mark.skip('graph atlas not available under Jython.')
import networkx.generators.atlas as atlas
cls.GAG = atlas.graph_atlas_g()
def test_graph_atlas(self):
# Atlas = nx.graph_atlas_g()[0:208] # 208, 6 nodes or less
Atlas = self.GAG[0:100]
alphabet = list(range(26))
for graph in Atlas:
nlist = list(graph)
labels = alphabet[: len(nlist)]
for s in range(10):
random.shuffle(labels)
d = dict(zip(nlist, labels))
relabel = nx.relabel_nodes(graph, d)
gm = iso.GraphMatcher(graph, relabel)
assert gm.is_isomorphic()
def test_multiedge():
# Simple test for multigraphs
# Need something much more rigorous
edges = [
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
(6, 7),
(7, 8),
(8, 9),
(9, 10),
(10, 11),
(10, 11),
(11, 12),
(11, 12),
(12, 13),
(12, 13),
(13, 14),
(13, 14),
(14, 15),
(14, 15),
(15, 16),
(15, 16),
(16, 17),
(16, 17),
(17, 18),
(17, 18),
(18, 19),
(18, 19),
(19, 0),
(19, 0),
]
nodes = list(range(20))
for g1 in [nx.MultiGraph(), nx.MultiDiGraph()]:
g1.add_edges_from(edges)
for _ in range(10):
new_nodes = list(nodes)
random.shuffle(new_nodes)
d = dict(zip(nodes, new_nodes))
g2 = nx.relabel_nodes(g1, d)
if not g1.is_directed():
gm = iso.GraphMatcher(g1, g2)
else:
gm = iso.DiGraphMatcher(g1, g2)
assert gm.is_isomorphic()
# Testing if monomorphism works in multigraphs
assert gm.subgraph_is_monomorphic()
def test_selfloop():
# Simple test for graphs with selfloops
edges = [
(0, 1),
(0, 2),
(1, 2),
(1, 3),
(2, 2),
(2, 4),
(3, 1),
(3, 2),
(4, 2),
(4, 5),
(5, 4),
]
nodes = list(range(6))
for g1 in [nx.Graph(), nx.DiGraph()]:
g1.add_edges_from(edges)
for _ in range(100):
new_nodes = list(nodes)
random.shuffle(new_nodes)
d = dict(zip(nodes, new_nodes))
g2 = nx.relabel_nodes(g1, d)
if not g1.is_directed():
gm = iso.GraphMatcher(g1, g2)
else:
gm = iso.DiGraphMatcher(g1, g2)
assert gm.is_isomorphic()
def test_selfloop_mono():
# Simple test for graphs with selfloops
edges0 = [
(0, 1),
(0, 2),
(1, 2),
(1, 3),
(2, 4),
(3, 1),
(3, 2),
(4, 2),
(4, 5),
(5, 4),
]
edges = edges0 + [(2, 2)]
nodes = list(range(6))
for g1 in [nx.Graph(), nx.DiGraph()]:
g1.add_edges_from(edges)
for _ in range(100):
new_nodes = list(nodes)
random.shuffle(new_nodes)
d = dict(zip(nodes, new_nodes))
g2 = nx.relabel_nodes(g1, d)
g2.remove_edges_from(nx.selfloop_edges(g2))
if not g1.is_directed():
gm = iso.GraphMatcher(g2, g1)
else:
gm = iso.DiGraphMatcher(g2, g1)
assert not gm.subgraph_is_monomorphic()
def test_isomorphism_iter1():
# As described in:
# http://groups.google.com/group/networkx-discuss/browse_thread/thread/2ff65c67f5e3b99f/d674544ebea359bb?fwc=1
g1 = nx.DiGraph()
g2 = nx.DiGraph()
g3 = nx.DiGraph()
g1.add_edge("A", "B")
g1.add_edge("B", "C")
g2.add_edge("Y", "Z")
g3.add_edge("Z", "Y")
gm12 = iso.DiGraphMatcher(g1, g2)
gm13 = iso.DiGraphMatcher(g1, g3)
x = list(gm12.subgraph_isomorphisms_iter())
y = list(gm13.subgraph_isomorphisms_iter())
assert {"A": "Y", "B": "Z"} in x
assert {"B": "Y", "C": "Z"} in x
assert {"A": "Z", "B": "Y"} in y
assert {"B": "Z", "C": "Y"} in y
assert len(x) == len(y)
assert len(x) == 2
def test_monomorphism_iter1():
g1 = nx.DiGraph()
g2 = nx.DiGraph()
g1.add_edge("A", "B")
g1.add_edge("B", "C")
g1.add_edge("C", "A")
g2.add_edge("X", "Y")
g2.add_edge("Y", "Z")
gm12 = iso.DiGraphMatcher(g1, g2)
x = list(gm12.subgraph_monomorphisms_iter())
assert {"A": "X", "B": "Y", "C": "Z"} in x
assert {"A": "Y", "B": "Z", "C": "X"} in x
assert {"A": "Z", "B": "X", "C": "Y"} in x
assert len(x) == 3
gm21 = iso.DiGraphMatcher(g2, g1)
# Check if StopIteration exception returns False
assert not gm21.subgraph_is_monomorphic()
def test_isomorphism_iter2():
# Path
for L in range(2, 10):
g1 = nx.path_graph(L)
gm = iso.GraphMatcher(g1, g1)
s = len(list(gm.isomorphisms_iter()))
assert s == 2
# Cycle
for L in range(3, 10):
g1 = nx.cycle_graph(L)
gm = iso.GraphMatcher(g1, g1)
s = len(list(gm.isomorphisms_iter()))
assert s == 2 * L
def test_multiple():
# Verify that we can use the graph matcher multiple times
edges = [("A", "B"), ("B", "A"), ("B", "C")]
for g1, g2 in [(nx.Graph(), nx.Graph()), (nx.DiGraph(), nx.DiGraph())]:
g1.add_edges_from(edges)
g2.add_edges_from(edges)
g3 = nx.subgraph(g2, ["A", "B"])
if not g1.is_directed():
gmA = iso.GraphMatcher(g1, g2)
gmB = iso.GraphMatcher(g1, g3)
else:
gmA = iso.DiGraphMatcher(g1, g2)
gmB = iso.DiGraphMatcher(g1, g3)
assert gmA.is_isomorphic()
g2.remove_node("C")
if not g1.is_directed():
gmA = iso.GraphMatcher(g1, g2)
else:
gmA = iso.DiGraphMatcher(g1, g2)
assert gmA.subgraph_is_isomorphic()
assert gmB.subgraph_is_isomorphic()
assert gmA.subgraph_is_monomorphic()
assert gmB.subgraph_is_monomorphic()
# for m in [gmB.mapping, gmB.mapping]:
# assert_true(m['A'] == 'A')
# assert_true(m['B'] == 'B')
# assert_true('C' not in m)
def test_noncomparable_nodes():
node1 = object()
node2 = object()
node3 = object()
# Graph
G = nx.path_graph([node1, node2, node3])
gm = iso.GraphMatcher(G, G)
assert gm.is_isomorphic()
# Just testing some cases
assert gm.subgraph_is_monomorphic()
# DiGraph
G = nx.path_graph([node1, node2, node3], create_using=nx.DiGraph)
H = nx.path_graph([node3, node2, node1], create_using=nx.DiGraph)
dgm = iso.DiGraphMatcher(G, H)
assert dgm.is_isomorphic()
# Just testing some cases
assert gm.subgraph_is_monomorphic()
def test_monomorphism_edge_match():
G = nx.DiGraph()
G.add_node(1)
G.add_node(2)
G.add_edge(1, 2, label="A")
G.add_edge(2, 1, label="B")
G.add_edge(2, 2, label="C")
SG = nx.DiGraph()
SG.add_node(5)
SG.add_node(6)
SG.add_edge(5, 6, label="A")
gm = iso.DiGraphMatcher(G, SG, edge_match=iso.categorical_edge_match("label", None))
assert gm.subgraph_is_monomorphic()

View file

@ -0,0 +1,63 @@
from operator import eq
import networkx as nx
from networkx.algorithms import isomorphism as iso
def test_categorical_node_match():
nm = iso.categorical_node_match(["x", "y", "z"], [None] * 3)
assert nm(dict(x=1, y=2, z=3), dict(x=1, y=2, z=3))
assert not nm(dict(x=1, y=2, z=2), dict(x=1, y=2, z=1))
class TestGenericMultiEdgeMatch:
def setup(self):
self.G1 = nx.MultiDiGraph()
self.G2 = nx.MultiDiGraph()
self.G3 = nx.MultiDiGraph()
self.G4 = nx.MultiDiGraph()
attr_dict1 = {"id": "edge1", "minFlow": 0, "maxFlow": 10}
attr_dict2 = {"id": "edge2", "minFlow": -3, "maxFlow": 7}
attr_dict3 = {"id": "edge3", "minFlow": 13, "maxFlow": 117}
attr_dict4 = {"id": "edge4", "minFlow": 13, "maxFlow": 117}
attr_dict5 = {"id": "edge5", "minFlow": 8, "maxFlow": 12}
attr_dict6 = {"id": "edge6", "minFlow": 8, "maxFlow": 12}
for attr_dict in [
attr_dict1,
attr_dict2,
attr_dict3,
attr_dict4,
attr_dict5,
attr_dict6,
]:
self.G1.add_edge(1, 2, **attr_dict)
for attr_dict in [
attr_dict5,
attr_dict3,
attr_dict6,
attr_dict1,
attr_dict4,
attr_dict2,
]:
self.G2.add_edge(2, 3, **attr_dict)
for attr_dict in [attr_dict3, attr_dict5]:
self.G3.add_edge(3, 4, **attr_dict)
for attr_dict in [attr_dict6, attr_dict4]:
self.G4.add_edge(4, 5, **attr_dict)
def test_generic_multiedge_match(self):
full_match = iso.generic_multiedge_match(
["id", "flowMin", "flowMax"], [None] * 3, [eq] * 3
)
flow_match = iso.generic_multiedge_match(
["flowMin", "flowMax"], [None] * 2, [eq] * 2
)
min_flow_match = iso.generic_multiedge_match("flowMin", None, eq)
id_match = iso.generic_multiedge_match("id", None, eq)
assert flow_match(self.G1[1][2], self.G2[2][3])
assert min_flow_match(self.G1[1][2], self.G2[2][3])
assert id_match(self.G1[1][2], self.G2[2][3])
assert full_match(self.G1[1][2], self.G2[2][3])
assert flow_match(self.G3[3][4], self.G4[4][5])
assert min_flow_match(self.G3[3][4], self.G4[4][5])
assert not id_match(self.G3[3][4], self.G4[4][5])
assert not full_match(self.G3[3][4], self.G4[4][5])

View file

@ -0,0 +1,210 @@
"""
Tests for the temporal aspect of the Temporal VF2 isomorphism algorithm.
"""
import networkx as nx
from networkx.algorithms import isomorphism as iso
from datetime import date, datetime, timedelta
def provide_g1_edgelist():
return [(0, 1), (0, 2), (1, 2), (2, 4), (1, 3), (3, 4), (4, 5)]
def put_same_time(G, att_name):
for e in G.edges(data=True):
e[2][att_name] = date(2015, 1, 1)
return G
def put_same_datetime(G, att_name):
for e in G.edges(data=True):
e[2][att_name] = datetime(2015, 1, 1)
return G
def put_sequence_time(G, att_name):
current_date = date(2015, 1, 1)
for e in G.edges(data=True):
current_date += timedelta(days=1)
e[2][att_name] = current_date
return G
def put_time_config_0(G, att_name):
G[0][1][att_name] = date(2015, 1, 2)
G[0][2][att_name] = date(2015, 1, 2)
G[1][2][att_name] = date(2015, 1, 3)
G[1][3][att_name] = date(2015, 1, 1)
G[2][4][att_name] = date(2015, 1, 1)
G[3][4][att_name] = date(2015, 1, 3)
G[4][5][att_name] = date(2015, 1, 3)
return G
def put_time_config_1(G, att_name):
G[0][1][att_name] = date(2015, 1, 2)
G[0][2][att_name] = date(2015, 1, 1)
G[1][2][att_name] = date(2015, 1, 3)
G[1][3][att_name] = date(2015, 1, 1)
G[2][4][att_name] = date(2015, 1, 2)
G[3][4][att_name] = date(2015, 1, 4)
G[4][5][att_name] = date(2015, 1, 3)
return G
def put_time_config_2(G, att_name):
G[0][1][att_name] = date(2015, 1, 1)
G[0][2][att_name] = date(2015, 1, 1)
G[1][2][att_name] = date(2015, 1, 3)
G[1][3][att_name] = date(2015, 1, 2)
G[2][4][att_name] = date(2015, 1, 2)
G[3][4][att_name] = date(2015, 1, 3)
G[4][5][att_name] = date(2015, 1, 2)
return G
class TestTimeRespectingGraphMatcher:
"""
A test class for the undirected temporal graph matcher.
"""
def provide_g1_topology(self):
G1 = nx.Graph()
G1.add_edges_from(provide_g1_edgelist())
return G1
def provide_g2_path_3edges(self):
G2 = nx.Graph()
G2.add_edges_from([(0, 1), (1, 2), (2, 3)])
return G2
def test_timdelta_zero_timeRespecting_returnsTrue(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_same_time(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta()
gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
assert gm.subgraph_is_isomorphic()
def test_timdelta_zero_datetime_timeRespecting_returnsTrue(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_same_datetime(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta()
gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
assert gm.subgraph_is_isomorphic()
def test_attNameStrange_timdelta_zero_timeRespecting_returnsTrue(self):
G1 = self.provide_g1_topology()
temporal_name = "strange_name"
G1 = put_same_time(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta()
gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
assert gm.subgraph_is_isomorphic()
def test_notTimeRespecting_returnsFalse(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_sequence_time(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta()
gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
assert not gm.subgraph_is_isomorphic()
def test_timdelta_one_config0_returns_no_embeddings(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_time_config_0(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta(days=1)
gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
count_match = len(list(gm.subgraph_isomorphisms_iter()))
assert count_match == 0
def test_timdelta_one_config1_returns_four_embedding(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_time_config_1(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta(days=1)
gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
count_match = len(list(gm.subgraph_isomorphisms_iter()))
assert count_match == 4
def test_timdelta_one_config2_returns_ten_embeddings(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_time_config_2(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta(days=1)
gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
L = list(gm.subgraph_isomorphisms_iter())
count_match = len(list(gm.subgraph_isomorphisms_iter()))
assert count_match == 10
class TestDiTimeRespectingGraphMatcher:
"""
A test class for the directed time-respecting graph matcher.
"""
def provide_g1_topology(self):
G1 = nx.DiGraph()
G1.add_edges_from(provide_g1_edgelist())
return G1
def provide_g2_path_3edges(self):
G2 = nx.DiGraph()
G2.add_edges_from([(0, 1), (1, 2), (2, 3)])
return G2
def test_timdelta_zero_same_dates_returns_true(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_same_time(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta()
gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
assert gm.subgraph_is_isomorphic()
def test_attNameStrange_timdelta_zero_same_dates_returns_true(self):
G1 = self.provide_g1_topology()
temporal_name = "strange"
G1 = put_same_time(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta()
gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
assert gm.subgraph_is_isomorphic()
def test_timdelta_one_config0_returns_no_embeddings(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_time_config_0(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta(days=1)
gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
count_match = len(list(gm.subgraph_isomorphisms_iter()))
assert count_match == 0
def test_timdelta_one_config1_returns_one_embedding(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_time_config_1(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta(days=1)
gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
count_match = len(list(gm.subgraph_isomorphisms_iter()))
assert count_match == 1
def test_timdelta_one_config2_returns_two_embeddings(self):
G1 = self.provide_g1_topology()
temporal_name = "date"
G1 = put_time_config_2(G1, temporal_name)
G2 = self.provide_g2_path_3edges()
d = timedelta(days=1)
gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
count_match = len(list(gm.subgraph_isomorphisms_iter()))
assert count_match == 2

View file

@ -0,0 +1,289 @@
import networkx as nx
import random
import time
from networkx.classes.function import is_directed
from networkx.algorithms.isomorphism.tree_isomorphism import (
rooted_tree_isomorphism,
tree_isomorphism,
)
# have this work for graph
# given two trees (either the directed or undirected)
# transform t2 according to the isomorphism
# and confirm it is identical to t1
# randomize the order of the edges when constructing
def check_isomorphism(t1, t2, isomorphism):
# get the name of t1, given the name in t2
mapping = {v2: v1 for (v1, v2) in isomorphism}
# these should be the same
d1 = is_directed(t1)
d2 = is_directed(t2)
assert d1 == d2
edges_1 = []
for (u, v) in t1.edges():
if d1:
edges_1.append((u, v))
else:
# if not directed, then need to
# put the edge in a consistent direction
if u < v:
edges_1.append((u, v))
else:
edges_1.append((v, u))
edges_2 = []
for (u, v) in t2.edges():
# translate to names for t1
u = mapping[u]
v = mapping[v]
if d2:
edges_2.append((u, v))
else:
if u < v:
edges_2.append((u, v))
else:
edges_2.append((v, u))
return sorted(edges_1) == sorted(edges_2)
def test_hardcoded():
print("hardcoded test")
# define a test problem
edges_1 = [
("a", "b"),
("a", "c"),
("a", "d"),
("b", "e"),
("b", "f"),
("e", "j"),
("e", "k"),
("c", "g"),
("c", "h"),
("g", "m"),
("d", "i"),
("f", "l"),
]
edges_2 = [
("v", "y"),
("v", "z"),
("u", "x"),
("q", "u"),
("q", "v"),
("p", "t"),
("n", "p"),
("n", "q"),
("n", "o"),
("o", "r"),
("o", "s"),
("s", "w"),
]
# there are two possible correct isomorphisms
# it currently returns isomorphism1
# but the second is also correct
isomorphism1 = [
("a", "n"),
("b", "q"),
("c", "o"),
("d", "p"),
("e", "v"),
("f", "u"),
("g", "s"),
("h", "r"),
("i", "t"),
("j", "y"),
("k", "z"),
("l", "x"),
("m", "w"),
]
# could swap y and z
isomorphism2 = [
("a", "n"),
("b", "q"),
("c", "o"),
("d", "p"),
("e", "v"),
("f", "u"),
("g", "s"),
("h", "r"),
("i", "t"),
("j", "z"),
("k", "y"),
("l", "x"),
("m", "w"),
]
t1 = nx.Graph()
t1.add_edges_from(edges_1)
root1 = "a"
t2 = nx.Graph()
t2.add_edges_from(edges_2)
root2 = "n"
isomorphism = sorted(rooted_tree_isomorphism(t1, root1, t2, root2))
# is correct by hand
assert (isomorphism == isomorphism1) or (isomorphism == isomorphism2)
# check algorithmically
assert check_isomorphism(t1, t2, isomorphism)
# try again as digraph
t1 = nx.DiGraph()
t1.add_edges_from(edges_1)
root1 = "a"
t2 = nx.DiGraph()
t2.add_edges_from(edges_2)
root2 = "n"
isomorphism = sorted(rooted_tree_isomorphism(t1, root1, t2, root2))
# is correct by hand
assert (isomorphism == isomorphism1) or (isomorphism == isomorphism2)
# check algorithmically
assert check_isomorphism(t1, t2, isomorphism)
# randomly swap a tuple (a,b)
def random_swap(t):
(a, b) = t
if random.randint(0, 1) == 1:
return (a, b)
else:
return (b, a)
# given a tree t1, create a new tree t2
# that is isomorphic to t1, with a known isomorphism
# and test that our algorithm found the right one
def positive_single_tree(t1):
assert nx.is_tree(t1)
nodes1 = [n for n in t1.nodes()]
# get a random permutation of this
nodes2 = nodes1.copy()
random.shuffle(nodes2)
# this is one isomorphism, however they may be multiple
# so we don't necessarily get this one back
someisomorphism = [(u, v) for (u, v) in zip(nodes1, nodes2)]
# map from old to new
map1to2 = {u: v for (u, v) in someisomorphism}
# get the edges with the transformed names
edges2 = [random_swap((map1to2[u], map1to2[v])) for (u, v) in t1.edges()]
# randomly permute, to ensure we're not relying on edge order somehow
random.shuffle(edges2)
# so t2 is isomorphic to t1
t2 = nx.Graph()
t2.add_edges_from(edges2)
# lets call our code to see if t1 and t2 are isomorphic
isomorphism = tree_isomorphism(t1, t2)
# make sure we got a correct solution
# although not necessarily someisomorphism
assert len(isomorphism) > 0
assert check_isomorphism(t1, t2, isomorphism)
# run positive_single_tree over all the
# non-isomorphic trees for k from 4 to maxk
# k = 4 is the first level that has more than 1 non-isomorphic tree
# k = 13 takes about 2.86 seconds to run on my laptop
# larger values run slow down significantly
# as the number of trees grows rapidly
def test_positive(maxk=14):
print("positive test")
for k in range(2, maxk + 1):
start_time = time.time()
trial = 0
for t in nx.nonisomorphic_trees(k):
positive_single_tree(t)
trial += 1
print(k, trial, time.time() - start_time)
# test the trivial case of a single node in each tree
# note that nonisomorphic_trees doesn't work for k = 1
def test_trivial():
print("trivial test")
# back to an undirected graph
t1 = nx.Graph()
t1.add_node("a")
root1 = "a"
t2 = nx.Graph()
t2.add_node("n")
root2 = "n"
isomorphism = rooted_tree_isomorphism(t1, root1, t2, root2)
assert isomorphism == [("a", "n")]
assert check_isomorphism(t1, t2, isomorphism)
# test another trivial case where the two graphs have
# different numbers of nodes
def test_trivial_2():
print("trivial test 2")
edges_1 = [("a", "b"), ("a", "c")]
edges_2 = [("v", "y")]
t1 = nx.Graph()
t1.add_edges_from(edges_1)
t2 = nx.Graph()
t2.add_edges_from(edges_2)
isomorphism = tree_isomorphism(t1, t2)
# they cannot be isomorphic,
# since they have different numbers of nodes
assert isomorphism == []
# the function nonisomorphic_trees generates all the non-isomorphic
# trees of a given size. Take each pair of these and verify that
# they are not isomorphic
# k = 4 is the first level that has more than 1 non-isomorphic tree
# k = 11 takes about 4.76 seconds to run on my laptop
# larger values run slow down significantly
# as the number of trees grows rapidly
def test_negative(maxk=11):
print("negative test")
for k in range(4, maxk + 1):
test_trees = list(nx.nonisomorphic_trees(k))
start_time = time.time()
trial = 0
for i in range(len(test_trees) - 1):
for j in range(i + 1, len(test_trees)):
trial += 1
assert tree_isomorphism(test_trees[i], test_trees[j]) == []
print(k, trial, time.time() - start_time)

View file

@ -0,0 +1,200 @@
"""
Tests for VF2 isomorphism algorithm for weighted graphs.
"""
from operator import eq
import networkx as nx
import networkx.algorithms.isomorphism as iso
def test_simple():
# 16 simple tests
w = "weight"
edges = [(0, 0, 1), (0, 0, 1.5), (0, 1, 2), (1, 0, 3)]
for g1 in [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]:
g1.add_weighted_edges_from(edges)
g2 = g1.subgraph(g1.nodes())
if g1.is_multigraph():
em = iso.numerical_multiedge_match("weight", 1)
else:
em = iso.numerical_edge_match("weight", 1)
assert nx.is_isomorphic(g1, g2, edge_match=em)
for mod1, mod2 in [(False, True), (True, False), (True, True)]:
# mod1 tests a regular edge
# mod2 tests a selfloop
if g2.is_multigraph():
if mod1:
data1 = {0: {"weight": 10}}
if mod2:
data2 = {0: {"weight": 1}, 1: {"weight": 2.5}}
else:
if mod1:
data1 = {"weight": 10}
if mod2:
data2 = {"weight": 2.5}
g2 = g1.subgraph(g1.nodes()).copy()
if mod1:
if not g1.is_directed():
g2._adj[1][0] = data1
g2._adj[0][1] = data1
else:
g2._succ[1][0] = data1
g2._pred[0][1] = data1
if mod2:
if not g1.is_directed():
g2._adj[0][0] = data2
else:
g2._succ[0][0] = data2
g2._pred[0][0] = data2
assert not nx.is_isomorphic(g1, g2, edge_match=em)
def test_weightkey():
g1 = nx.DiGraph()
g2 = nx.DiGraph()
g1.add_edge("A", "B", weight=1)
g2.add_edge("C", "D", weight=0)
assert nx.is_isomorphic(g1, g2)
em = iso.numerical_edge_match("nonexistent attribute", 1)
assert nx.is_isomorphic(g1, g2, edge_match=em)
em = iso.numerical_edge_match("weight", 1)
assert not nx.is_isomorphic(g1, g2, edge_match=em)
g2 = nx.DiGraph()
g2.add_edge("C", "D")
assert nx.is_isomorphic(g1, g2, edge_match=em)
class TestNodeMatch_Graph:
def setup_method(self):
self.g1 = nx.Graph()
self.g2 = nx.Graph()
self.build()
def build(self):
self.nm = iso.categorical_node_match("color", "")
self.em = iso.numerical_edge_match("weight", 1)
self.g1.add_node("A", color="red")
self.g2.add_node("C", color="blue")
self.g1.add_edge("A", "B", weight=1)
self.g2.add_edge("C", "D", weight=1)
def test_noweight_nocolor(self):
assert nx.is_isomorphic(self.g1, self.g2)
def test_color1(self):
assert not nx.is_isomorphic(self.g1, self.g2, node_match=self.nm)
def test_color2(self):
self.g1.nodes["A"]["color"] = "blue"
assert nx.is_isomorphic(self.g1, self.g2, node_match=self.nm)
def test_weight1(self):
assert nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
def test_weight2(self):
self.g1.add_edge("A", "B", weight=2)
assert not nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
def test_colorsandweights1(self):
iso = nx.is_isomorphic(self.g1, self.g2, node_match=self.nm, edge_match=self.em)
assert not iso
def test_colorsandweights2(self):
self.g1.nodes["A"]["color"] = "blue"
iso = nx.is_isomorphic(self.g1, self.g2, node_match=self.nm, edge_match=self.em)
assert iso
def test_colorsandweights3(self):
# make the weights disagree
self.g1.add_edge("A", "B", weight=2)
assert not nx.is_isomorphic(
self.g1, self.g2, node_match=self.nm, edge_match=self.em
)
class TestEdgeMatch_MultiGraph:
def setup_method(self):
self.g1 = nx.MultiGraph()
self.g2 = nx.MultiGraph()
self.GM = iso.MultiGraphMatcher
self.build()
def build(self):
g1 = self.g1
g2 = self.g2
# We will assume integer weights only.
g1.add_edge("A", "B", color="green", weight=0, size=0.5)
g1.add_edge("A", "B", color="red", weight=1, size=0.35)
g1.add_edge("A", "B", color="red", weight=2, size=0.65)
g2.add_edge("C", "D", color="green", weight=1, size=0.5)
g2.add_edge("C", "D", color="red", weight=0, size=0.45)
g2.add_edge("C", "D", color="red", weight=2, size=0.65)
if g1.is_multigraph():
self.em = iso.numerical_multiedge_match("weight", 1)
self.emc = iso.categorical_multiedge_match("color", "")
self.emcm = iso.categorical_multiedge_match(["color", "weight"], ["", 1])
self.emg1 = iso.generic_multiedge_match("color", "red", eq)
self.emg2 = iso.generic_multiedge_match(
["color", "weight", "size"],
["red", 1, 0.5],
[eq, eq, iso.matchhelpers.close],
)
else:
self.em = iso.numerical_edge_match("weight", 1)
self.emc = iso.categorical_edge_match("color", "")
self.emcm = iso.categorical_edge_match(["color", "weight"], ["", 1])
self.emg1 = iso.generic_multiedge_match("color", "red", eq)
self.emg2 = iso.generic_edge_match(
["color", "weight", "size"],
["red", 1, 0.5],
[eq, eq, iso.matchhelpers.close],
)
def test_weights_only(self):
assert nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
def test_colors_only(self):
gm = self.GM(self.g1, self.g2, edge_match=self.emc)
assert gm.is_isomorphic()
def test_colorsandweights(self):
gm = self.GM(self.g1, self.g2, edge_match=self.emcm)
assert not gm.is_isomorphic()
def test_generic1(self):
gm = self.GM(self.g1, self.g2, edge_match=self.emg1)
assert gm.is_isomorphic()
def test_generic2(self):
gm = self.GM(self.g1, self.g2, edge_match=self.emg2)
assert not gm.is_isomorphic()
class TestEdgeMatch_DiGraph(TestNodeMatch_Graph):
def setup_method(self):
TestNodeMatch_Graph.setup_method(self)
self.g1 = nx.DiGraph()
self.g2 = nx.DiGraph()
self.build()
class TestEdgeMatch_MultiDiGraph(TestEdgeMatch_MultiGraph):
def setup_method(self):
TestEdgeMatch_MultiGraph.setup_method(self)
self.g1 = nx.MultiDiGraph()
self.g2 = nx.MultiDiGraph()
self.GM = iso.MultiDiGraphMatcher
self.build()

View file

@ -0,0 +1,279 @@
"""
An algorithm for finding if two undirected trees are isomorphic,
and if so returns an isomorphism between the two sets of nodes.
This algorithm uses a routine to tell if two rooted trees (trees with a
specified root node) are isomorphic, which may be independently useful.
This implements an algorithm from:
The Design and Analysis of Computer Algorithms
by Aho, Hopcroft, and Ullman
Addison-Wesley Publishing 1974
Example 3.2 pp. 84-86.
A more understandable version of this algorithm is described in:
Homework Assignment 5
McGill University SOCS 308-250B, Winter 2002
by Matthew Suderman
http://crypto.cs.mcgill.ca/~crepeau/CS250/2004/HW5+.pdf
"""
import networkx as nx
from networkx.utils.decorators import not_implemented_for
__all__ = ["rooted_tree_isomorphism", "tree_isomorphism"]
def root_trees(t1, root1, t2, root2):
""" Create a single digraph dT of free trees t1 and t2
# with roots root1 and root2 respectively
# rename the nodes with consecutive integers
# so that all nodes get a unique name between both trees
# our new "fake" root node is 0
# t1 is numbers from 1 ... n
# t2 is numbered from n+1 to 2n
"""
dT = nx.DiGraph()
newroot1 = 1 # left root will be 1
newroot2 = nx.number_of_nodes(t1) + 1 # right will be n+1
# may be overlap in node names here so need separate maps
# given the old name, what is the new
namemap1 = {root1: newroot1}
namemap2 = {root2: newroot2}
# add an edge from our new root to root1 and root2
dT.add_edge(0, namemap1[root1])
dT.add_edge(0, namemap2[root2])
for i, (v1, v2) in enumerate(nx.bfs_edges(t1, root1)):
namemap1[v2] = i + namemap1[root1] + 1
dT.add_edge(namemap1[v1], namemap1[v2])
for i, (v1, v2) in enumerate(nx.bfs_edges(t2, root2)):
namemap2[v2] = i + namemap2[root2] + 1
dT.add_edge(namemap2[v1], namemap2[v2])
# now we really want the inverse of namemap1 and namemap2
# giving the old name given the new
# since the values of namemap1 and namemap2 are unique
# there won't be collisions
namemap = {}
for old, new in namemap1.items():
namemap[new] = old
for old, new in namemap2.items():
namemap[new] = old
return (dT, namemap, newroot1, newroot2)
# figure out the level of each node, with 0 at root
def assign_levels(G, root):
level = {}
level[root] = 0
for (v1, v2) in nx.bfs_edges(G, root):
level[v2] = level[v1] + 1
return level
# now group the nodes at each level
def group_by_levels(levels):
L = {}
for (n, lev) in levels.items():
if lev not in L:
L[lev] = []
L[lev].append(n)
return L
# now lets get the isomorphism by walking the ordered_children
def generate_isomorphism(v, w, M, ordered_children):
# make sure tree1 comes first
assert v < w
M.append((v, w))
for i, (x, y) in enumerate(zip(ordered_children[v], ordered_children[w])):
generate_isomorphism(x, y, M, ordered_children)
def rooted_tree_isomorphism(t1, root1, t2, root2):
"""
Given two rooted trees `t1` and `t2`,
with roots `root1` and `root2` respectivly
this routine will determine if they are isomorphic.
These trees may be either directed or undirected,
but if they are directed, all edges should flow from the root.
It returns the isomorphism, a mapping of the nodes of `t1` onto the nodes
of `t2`, such that two trees are then identical.
Note that two trees may have more than one isomorphism, and this
routine just returns one valid mapping.
Parameters
----------
`t1` : NetworkX graph
One of the trees being compared
`root1` : a node of `t1` which is the root of the tree
`t2` : undirected NetworkX graph
The other tree being compared
`root2` : a node of `t2` which is the root of the tree
This is a subroutine used to implement `tree_isomorphism`, but will
be somewhat faster if you already have rooted trees.
Returns
-------
isomorphism : list
A list of pairs in which the left element is a node in `t1`
and the right element is a node in `t2`. The pairs are in
arbitrary order. If the nodes in one tree is mapped to the names in
the other, then trees will be identical. Note that an isomorphism
will not necessarily be unique.
If `t1` and `t2` are not isomorphic, then it returns the empty list.
"""
assert nx.is_tree(t1)
assert nx.is_tree(t2)
# get the rooted tree formed by combining them
# with unique names
(dT, namemap, newroot1, newroot2) = root_trees(t1, root1, t2, root2)
# compute the distance from the root, with 0 for our
levels = assign_levels(dT, 0)
# height
h = max(levels.values())
# collect nodes into a dict by level
L = group_by_levels(levels)
# each node has a label, initially set to 0
label = {v: 0 for v in dT}
# and also ordered_labels and ordered_children
# which will store ordered tuples
ordered_labels = {v: () for v in dT}
ordered_children = {v: () for v in dT}
# nothing to do on last level so start on h-1
# also nothing to do for our fake level 0, so skip that
for i in range(h - 1, 0, -1):
# update the ordered_labels and ordered_childen
# for any children
for v in L[i]:
# nothing to do if no children
if dT.out_degree(v) > 0:
# get all the pairs of labels and nodes of children
# and sort by labels
s = sorted([(label[u], u) for u in dT.successors(v)])
# invert to give a list of two tuples
# the sorted labels, and the corresponding children
ordered_labels[v], ordered_children[v] = list(zip(*s))
# now collect and sort the sorted ordered_labels
# for all nodes in L[i], carrying along the node
forlabel = sorted([(ordered_labels[v], v) for v in L[i]])
# now assign labels to these nodes, according to the sorted order
# starting from 0, where idential ordered_labels get the same label
current = 0
for i, (ol, v) in enumerate(forlabel):
# advance to next label if not 0, and different from previous
if (i != 0) and (ol != forlabel[i - 1][0]):
current += 1
label[v] = current
# they are isomorphic if the labels of newroot1 and newroot2 are 0
isomorphism = []
if label[newroot1] == 0 and label[newroot2] == 0:
generate_isomorphism(newroot1, newroot2, isomorphism, ordered_children)
# get the mapping back in terms of the old names
# return in sorted order for neatness
isomorphism = [(namemap[u], namemap[v]) for (u, v) in isomorphism]
return isomorphism
@not_implemented_for("directed", "multigraph")
def tree_isomorphism(t1, t2):
"""
Given two undirected (or free) trees `t1` and `t2`,
this routine will determine if they are isomorphic.
It returns the isomorphism, a mapping of the nodes of `t1` onto the nodes
of `t2`, such that two trees are then identical.
Note that two trees may have more than one isomorphism, and this
routine just returns one valid mapping.
Parameters
----------
t1 : undirected NetworkX graph
One of the trees being compared
t2 : undirected NetworkX graph
The other tree being compared
Returns
-------
isomorphism : list
A list of pairs in which the left element is a node in `t1`
and the right element is a node in `t2`. The pairs are in
arbitrary order. If the nodes in one tree is mapped to the names in
the other, then trees will be identical. Note that an isomorphism
will not necessarily be unique.
If `t1` and `t2` are not isomorphic, then it returns the empty list.
Notes
-----
This runs in O(n*log(n)) time for trees with n nodes.
"""
assert nx.is_tree(t1)
assert nx.is_tree(t2)
# To be isomrophic, t1 and t2 must have the same number of nodes.
if nx.number_of_nodes(t1) != nx.number_of_nodes(t2):
return []
# Another shortcut is that the sorted degree sequences need to be the same.
degree_sequence1 = sorted([d for (n, d) in t1.degree()])
degree_sequence2 = sorted([d for (n, d) in t2.degree()])
if degree_sequence1 != degree_sequence2:
return []
# A tree can have either 1 or 2 centers.
# If the number doesn't match then t1 and t2 are not isomorphic.
center1 = nx.center(t1)
center2 = nx.center(t2)
if len(center1) != len(center2):
return []
# If there is only 1 center in each, then use it.
if len(center1) == 1:
return rooted_tree_isomorphism(t1, center1[0], t2, center2[0])
# If there both have 2 centers, then try the first for t1
# with the first for t2.
attemps = rooted_tree_isomorphism(t1, center1[0], t2, center2[0])
# If that worked we're done.
if len(attemps) > 0:
return attemps
# Otherwise, try center1[0] with the center2[1], and see if that works
return rooted_tree_isomorphism(t1, center1[0], t2, center2[1])

View file

@ -0,0 +1,200 @@
"""
Module to simplify the specification of user-defined equality functions for
node and edge attributes during isomorphism checks.
During the construction of an isomorphism, the algorithm considers two
candidate nodes n1 in G1 and n2 in G2. The graphs G1 and G2 are then
compared with respect to properties involving n1 and n2, and if the outcome
is good, then the candidate nodes are considered isomorphic. NetworkX
provides a simple mechanism for users to extend the comparisons to include
node and edge attributes.
Node attributes are handled by the node_match keyword. When considering
n1 and n2, the algorithm passes their node attribute dictionaries to
node_match, and if it returns False, then n1 and n2 cannot be
considered to be isomorphic.
Edge attributes are handled by the edge_match keyword. When considering
n1 and n2, the algorithm must verify that outgoing edges from n1 are
commensurate with the outgoing edges for n2. If the graph is directed,
then a similar check is also performed for incoming edges.
Focusing only on outgoing edges, we consider pairs of nodes (n1, v1) from
G1 and (n2, v2) from G2. For graphs and digraphs, there is only one edge
between (n1, v1) and only one edge between (n2, v2). Those edge attribute
dictionaries are passed to edge_match, and if it returns False, then
n1 and n2 cannot be considered isomorphic. For multigraphs and
multidigraphs, there can be multiple edges between (n1, v1) and also
multiple edges between (n2, v2). Now, there must exist an isomorphism
from "all the edges between (n1, v1)" to "all the edges between (n2, v2)".
So, all of the edge attribute dictionaries are passed to edge_match, and
it must determine if there is an isomorphism between the two sets of edges.
"""
from . import isomorphvf2 as vf2
__all__ = ["GraphMatcher", "DiGraphMatcher", "MultiGraphMatcher", "MultiDiGraphMatcher"]
def _semantic_feasibility(self, G1_node, G2_node):
"""Returns True if mapping G1_node to G2_node is semantically feasible.
"""
# Make sure the nodes match
if self.node_match is not None:
nm = self.node_match(self.G1.nodes[G1_node], self.G2.nodes[G2_node])
if not nm:
return False
# Make sure the edges match
if self.edge_match is not None:
# Cached lookups
G1nbrs = self.G1_adj[G1_node]
G2nbrs = self.G2_adj[G2_node]
core_1 = self.core_1
edge_match = self.edge_match
for neighbor in G1nbrs:
# G1_node is not in core_1, so we must handle R_self separately
if neighbor == G1_node:
if G2_node in G2nbrs and not edge_match(
G1nbrs[G1_node], G2nbrs[G2_node]
):
return False
elif neighbor in core_1:
G2_nbr = core_1[neighbor]
if G2_nbr in G2nbrs and not edge_match(
G1nbrs[neighbor], G2nbrs[G2_nbr]
):
return False
# syntactic check has already verified that neighbors are symmetric
return True
class GraphMatcher(vf2.GraphMatcher):
"""VF2 isomorphism checker for undirected graphs.
"""
def __init__(self, G1, G2, node_match=None, edge_match=None):
"""Initialize graph matcher.
Parameters
----------
G1, G2: graph
The graphs to be tested.
node_match: callable
A function that returns True iff node n1 in G1 and n2 in G2
should be considered equal during the isomorphism test. The
function will be called like::
node_match(G1.nodes[n1], G2.nodes[n2])
That is, the function will receive the node attribute dictionaries
of the nodes under consideration. If None, then no attributes are
considered when testing for an isomorphism.
edge_match: callable
A function that returns True iff the edge attribute dictionary for
the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be
considered equal during the isomorphism test. The function will be
called like::
edge_match(G1[u1][v1], G2[u2][v2])
That is, the function will receive the edge attribute dictionaries
of the edges under consideration. If None, then no attributes are
considered when testing for an isomorphism.
"""
vf2.GraphMatcher.__init__(self, G1, G2)
self.node_match = node_match
self.edge_match = edge_match
# These will be modified during checks to minimize code repeat.
self.G1_adj = self.G1.adj
self.G2_adj = self.G2.adj
semantic_feasibility = _semantic_feasibility
class DiGraphMatcher(vf2.DiGraphMatcher):
"""VF2 isomorphism checker for directed graphs.
"""
def __init__(self, G1, G2, node_match=None, edge_match=None):
"""Initialize graph matcher.
Parameters
----------
G1, G2 : graph
The graphs to be tested.
node_match : callable
A function that returns True iff node n1 in G1 and n2 in G2
should be considered equal during the isomorphism test. The
function will be called like::
node_match(G1.nodes[n1], G2.nodes[n2])
That is, the function will receive the node attribute dictionaries
of the nodes under consideration. If None, then no attributes are
considered when testing for an isomorphism.
edge_match : callable
A function that returns True iff the edge attribute dictionary for
the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be
considered equal during the isomorphism test. The function will be
called like::
edge_match(G1[u1][v1], G2[u2][v2])
That is, the function will receive the edge attribute dictionaries
of the edges under consideration. If None, then no attributes are
considered when testing for an isomorphism.
"""
vf2.DiGraphMatcher.__init__(self, G1, G2)
self.node_match = node_match
self.edge_match = edge_match
# These will be modified during checks to minimize code repeat.
self.G1_adj = self.G1.adj
self.G2_adj = self.G2.adj
def semantic_feasibility(self, G1_node, G2_node):
"""Returns True if mapping G1_node to G2_node is semantically feasible."""
# Test node_match and also test edge_match on successors
feasible = _semantic_feasibility(self, G1_node, G2_node)
if not feasible:
return False
# Test edge_match on predecessors
self.G1_adj = self.G1.pred
self.G2_adj = self.G2.pred
feasible = _semantic_feasibility(self, G1_node, G2_node)
self.G1_adj = self.G1.adj
self.G2_adj = self.G2.adj
return feasible
# The "semantics" of edge_match are different for multi(di)graphs, but
# the implementation is the same. So, technically we do not need to
# provide "multi" versions, but we do so to match NetworkX's base classes.
class MultiGraphMatcher(GraphMatcher):
"""VF2 isomorphism checker for undirected multigraphs. """
pass
class MultiDiGraphMatcher(DiGraphMatcher):
"""VF2 isomorphism checker for directed multigraphs. """
pass