Fixed database typo and removed unnecessary class identifier.
This commit is contained in:
parent
00ad49a143
commit
45fb349a7d
5098 changed files with 952558 additions and 85 deletions
|
@ -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 *
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1153
venv/Lib/site-packages/networkx/algorithms/isomorphism/ismags.py
Normal file
1153
venv/Lib/site-packages/networkx/algorithms/isomorphism/ismags.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
@ -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")
|
|
@ -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):97–125, 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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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])
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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])
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue