matchhelpers.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. """Functions which help end users define customize node_match and
  2. edge_match functions to use during isomorphism checks.
  3. """
  4. import math
  5. import types
  6. from itertools import permutations
  7. __all__ = [
  8. "categorical_node_match",
  9. "categorical_edge_match",
  10. "categorical_multiedge_match",
  11. "numerical_node_match",
  12. "numerical_edge_match",
  13. "numerical_multiedge_match",
  14. "generic_node_match",
  15. "generic_edge_match",
  16. "generic_multiedge_match",
  17. ]
  18. def copyfunc(f, name=None):
  19. """Returns a deepcopy of a function."""
  20. return types.FunctionType(
  21. f.__code__, f.__globals__, name or f.__name__, f.__defaults__, f.__closure__
  22. )
  23. def allclose(x, y, rtol=1.0000000000000001e-05, atol=1e-08):
  24. """Returns True if x and y are sufficiently close, elementwise.
  25. Parameters
  26. ----------
  27. rtol : float
  28. The relative error tolerance.
  29. atol : float
  30. The absolute error tolerance.
  31. """
  32. # assume finite weights, see numpy.allclose() for reference
  33. return all(math.isclose(xi, yi, rel_tol=rtol, abs_tol=atol) for xi, yi in zip(x, y))
  34. categorical_doc = """
  35. Returns a comparison function for a categorical node attribute.
  36. The value(s) of the attr(s) must be hashable and comparable via the ==
  37. operator since they are placed into a set([]) object. If the sets from
  38. G1 and G2 are the same, then the constructed function returns True.
  39. Parameters
  40. ----------
  41. attr : string | list
  42. The categorical node attribute to compare, or a list of categorical
  43. node attributes to compare.
  44. default : value | list
  45. The default value for the categorical node attribute, or a list of
  46. default values for the categorical node attributes.
  47. Returns
  48. -------
  49. match : function
  50. The customized, categorical `node_match` function.
  51. Examples
  52. --------
  53. >>> import networkx.algorithms.isomorphism as iso
  54. >>> nm = iso.categorical_node_match("size", 1)
  55. >>> nm = iso.categorical_node_match(["color", "size"], ["red", 2])
  56. """
  57. def categorical_node_match(attr, default):
  58. if isinstance(attr, str):
  59. def match(data1, data2):
  60. return data1.get(attr, default) == data2.get(attr, default)
  61. else:
  62. attrs = list(zip(attr, default)) # Python 3
  63. def match(data1, data2):
  64. return all(data1.get(attr, d) == data2.get(attr, d) for attr, d in attrs)
  65. return match
  66. categorical_edge_match = copyfunc(categorical_node_match, "categorical_edge_match")
  67. def categorical_multiedge_match(attr, default):
  68. if isinstance(attr, str):
  69. def match(datasets1, datasets2):
  70. values1 = {data.get(attr, default) for data in datasets1.values()}
  71. values2 = {data.get(attr, default) for data in datasets2.values()}
  72. return values1 == values2
  73. else:
  74. attrs = list(zip(attr, default)) # Python 3
  75. def match(datasets1, datasets2):
  76. values1 = set()
  77. for data1 in datasets1.values():
  78. x = tuple(data1.get(attr, d) for attr, d in attrs)
  79. values1.add(x)
  80. values2 = set()
  81. for data2 in datasets2.values():
  82. x = tuple(data2.get(attr, d) for attr, d in attrs)
  83. values2.add(x)
  84. return values1 == values2
  85. return match
  86. # Docstrings for categorical functions.
  87. categorical_node_match.__doc__ = categorical_doc
  88. categorical_edge_match.__doc__ = categorical_doc.replace("node", "edge")
  89. tmpdoc = categorical_doc.replace("node", "edge")
  90. tmpdoc = tmpdoc.replace("categorical_edge_match", "categorical_multiedge_match")
  91. categorical_multiedge_match.__doc__ = tmpdoc
  92. numerical_doc = """
  93. Returns a comparison function for a numerical node attribute.
  94. The value(s) of the attr(s) must be numerical and sortable. If the
  95. sorted list of values from G1 and G2 are the same within some
  96. tolerance, then the constructed function returns True.
  97. Parameters
  98. ----------
  99. attr : string | list
  100. The numerical node attribute to compare, or a list of numerical
  101. node attributes to compare.
  102. default : value | list
  103. The default value for the numerical node attribute, or a list of
  104. default values for the numerical node attributes.
  105. rtol : float
  106. The relative error tolerance.
  107. atol : float
  108. The absolute error tolerance.
  109. Returns
  110. -------
  111. match : function
  112. The customized, numerical `node_match` function.
  113. Examples
  114. --------
  115. >>> import networkx.algorithms.isomorphism as iso
  116. >>> nm = iso.numerical_node_match("weight", 1.0)
  117. >>> nm = iso.numerical_node_match(["weight", "linewidth"], [0.25, 0.5])
  118. """
  119. def numerical_node_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08):
  120. if isinstance(attr, str):
  121. def match(data1, data2):
  122. return math.isclose(
  123. data1.get(attr, default),
  124. data2.get(attr, default),
  125. rel_tol=rtol,
  126. abs_tol=atol,
  127. )
  128. else:
  129. attrs = list(zip(attr, default)) # Python 3
  130. def match(data1, data2):
  131. values1 = [data1.get(attr, d) for attr, d in attrs]
  132. values2 = [data2.get(attr, d) for attr, d in attrs]
  133. return allclose(values1, values2, rtol=rtol, atol=atol)
  134. return match
  135. numerical_edge_match = copyfunc(numerical_node_match, "numerical_edge_match")
  136. def numerical_multiedge_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08):
  137. if isinstance(attr, str):
  138. def match(datasets1, datasets2):
  139. values1 = sorted(data.get(attr, default) for data in datasets1.values())
  140. values2 = sorted(data.get(attr, default) for data in datasets2.values())
  141. return allclose(values1, values2, rtol=rtol, atol=atol)
  142. else:
  143. attrs = list(zip(attr, default)) # Python 3
  144. def match(datasets1, datasets2):
  145. values1 = []
  146. for data1 in datasets1.values():
  147. x = tuple(data1.get(attr, d) for attr, d in attrs)
  148. values1.append(x)
  149. values2 = []
  150. for data2 in datasets2.values():
  151. x = tuple(data2.get(attr, d) for attr, d in attrs)
  152. values2.append(x)
  153. values1.sort()
  154. values2.sort()
  155. for xi, yi in zip(values1, values2):
  156. if not allclose(xi, yi, rtol=rtol, atol=atol):
  157. return False
  158. else:
  159. return True
  160. return match
  161. # Docstrings for numerical functions.
  162. numerical_node_match.__doc__ = numerical_doc
  163. numerical_edge_match.__doc__ = numerical_doc.replace("node", "edge")
  164. tmpdoc = numerical_doc.replace("node", "edge")
  165. tmpdoc = tmpdoc.replace("numerical_edge_match", "numerical_multiedge_match")
  166. numerical_multiedge_match.__doc__ = tmpdoc
  167. generic_doc = """
  168. Returns a comparison function for a generic attribute.
  169. The value(s) of the attr(s) are compared using the specified
  170. operators. If all the attributes are equal, then the constructed
  171. function returns True.
  172. Parameters
  173. ----------
  174. attr : string | list
  175. The node attribute to compare, or a list of node attributes
  176. to compare.
  177. default : value | list
  178. The default value for the node attribute, or a list of
  179. default values for the node attributes.
  180. op : callable | list
  181. The operator to use when comparing attribute values, or a list
  182. of operators to use when comparing values for each attribute.
  183. Returns
  184. -------
  185. match : function
  186. The customized, generic `node_match` function.
  187. Examples
  188. --------
  189. >>> from operator import eq
  190. >>> from math import isclose
  191. >>> from networkx.algorithms.isomorphism import generic_node_match
  192. >>> nm = generic_node_match("weight", 1.0, isclose)
  193. >>> nm = generic_node_match("color", "red", eq)
  194. >>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [isclose, eq])
  195. """
  196. def generic_node_match(attr, default, op):
  197. if isinstance(attr, str):
  198. def match(data1, data2):
  199. return op(data1.get(attr, default), data2.get(attr, default))
  200. else:
  201. attrs = list(zip(attr, default, op)) # Python 3
  202. def match(data1, data2):
  203. for attr, d, operator in attrs:
  204. if not operator(data1.get(attr, d), data2.get(attr, d)):
  205. return False
  206. else:
  207. return True
  208. return match
  209. generic_edge_match = copyfunc(generic_node_match, "generic_edge_match")
  210. def generic_multiedge_match(attr, default, op):
  211. """Returns a comparison function for a generic attribute.
  212. The value(s) of the attr(s) are compared using the specified
  213. operators. If all the attributes are equal, then the constructed
  214. function returns True. Potentially, the constructed edge_match
  215. function can be slow since it must verify that no isomorphism
  216. exists between the multiedges before it returns False.
  217. Parameters
  218. ----------
  219. attr : string | list
  220. The edge attribute to compare, or a list of node attributes
  221. to compare.
  222. default : value | list
  223. The default value for the edge attribute, or a list of
  224. default values for the dgeattributes.
  225. op : callable | list
  226. The operator to use when comparing attribute values, or a list
  227. of operators to use when comparing values for each attribute.
  228. Returns
  229. -------
  230. match : function
  231. The customized, generic `edge_match` function.
  232. Examples
  233. --------
  234. >>> from operator import eq
  235. >>> from math import isclose
  236. >>> from networkx.algorithms.isomorphism import generic_node_match
  237. >>> nm = generic_node_match("weight", 1.0, isclose)
  238. >>> nm = generic_node_match("color", "red", eq)
  239. >>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [isclose, eq])
  240. ...
  241. """
  242. # This is slow, but generic.
  243. # We must test every possible isomorphism between the edges.
  244. if isinstance(attr, str):
  245. attr = [attr]
  246. default = [default]
  247. op = [op]
  248. attrs = list(zip(attr, default)) # Python 3
  249. def match(datasets1, datasets2):
  250. values1 = []
  251. for data1 in datasets1.values():
  252. x = tuple(data1.get(attr, d) for attr, d in attrs)
  253. values1.append(x)
  254. values2 = []
  255. for data2 in datasets2.values():
  256. x = tuple(data2.get(attr, d) for attr, d in attrs)
  257. values2.append(x)
  258. for vals2 in permutations(values2):
  259. for xi, yi in zip(values1, vals2):
  260. if not all(map(lambda x, y, z: z(x, y), xi, yi, op)):
  261. # This is not an isomorphism, go to next permutation.
  262. break
  263. else:
  264. # Then we found an isomorphism.
  265. return True
  266. else:
  267. # Then there are no isomorphisms between the multiedges.
  268. return False
  269. return match
  270. # Docstrings for numerical functions.
  271. generic_node_match.__doc__ = generic_doc
  272. generic_edge_match.__doc__ = generic_doc.replace("node", "edge")