1"""
2*************
3VF2 Algorithm
4*************
5
6An implementation of VF2 algorithm for graph isomorphism testing.
7
8The simplest interface to use this module is to call the
9:func:`is_isomorphic <networkx.algorithms.isomorphism.is_isomorphic>`
10function.
11
12Introduction
13------------
14
15The GraphMatcher and DiGraphMatcher are responsible for matching
16graphs or directed graphs in a predetermined manner. This
17usually means a check for an isomorphism, though other checks
18are also possible. For example, a subgraph of one graph
19can be checked for isomorphism to a second graph.
20
21Matching is done via syntactic feasibility. It is also possible
22to check for semantic feasibility. Feasibility, then, is defined
23as the logical AND of the two functions.
24
25To include a semantic check, the (Di)GraphMatcher class should be
26subclassed, and the
27:meth:`semantic_feasibility <networkx.algorithms.isomorphism.GraphMatcher.semantic_feasibility>`
28function should be redefined. By default, the semantic feasibility function always
29returns ``True``. The effect of this is that semantics are not
30considered in the matching of G1 and G2.
31
32Examples
33--------
34
35Suppose G1 and G2 are isomorphic graphs. Verification is as follows:
36
37>>> from networkx.algorithms import isomorphism
38>>> G1 = nx.path_graph(4)
39>>> G2 = nx.path_graph(4)
40>>> GM = isomorphism.GraphMatcher(G1, G2)
41>>> GM.is_isomorphic()
42True
43
44GM.mapping stores the isomorphism mapping from G1 to G2.
45
46>>> GM.mapping
47{0: 0, 1: 1, 2: 2, 3: 3}
48
49
50Suppose G1 and G2 are isomorphic directed graphs.
51Verification is as follows:
52
53>>> G1 = nx.path_graph(4, create_using=nx.DiGraph)
54>>> G2 = nx.path_graph(4, create_using=nx.DiGraph)
55>>> DiGM = isomorphism.DiGraphMatcher(G1, G2)
56>>> DiGM.is_isomorphic()
57True
58
59DiGM.mapping stores the isomorphism mapping from G1 to G2.
60
61>>> DiGM.mapping
62{0: 0, 1: 1, 2: 2, 3: 3}
63
64
65
66Subgraph Isomorphism
67--------------------
68Graph theory literature can be ambiguous about the meaning of the
69above statement, and we seek to clarify it now.
70
71In the VF2 literature, a mapping ``M`` is said to be a graph-subgraph
72isomorphism iff ``M`` is an isomorphism between ``G2`` and a subgraph of ``G1``.
73Thus, to say that ``G1`` and ``G2`` are graph-subgraph isomorphic is to say
74that a subgraph of ``G1`` is isomorphic to ``G2``.
75
76Other literature uses the phrase 'subgraph isomorphic' as in '``G1`` does
77not have a subgraph isomorphic to ``G2``'. Another use is as an in adverb
78for isomorphic. Thus, to say that ``G1`` and ``G2`` are subgraph isomorphic
79is to say that a subgraph of ``G1`` is isomorphic to ``G2``.
80
81Finally, the term 'subgraph' can have multiple meanings. In this
82context, 'subgraph' always means a 'node-induced subgraph'. Edge-induced
83subgraph isomorphisms are not directly supported, but one should be
84able to perform the check by making use of
85:func:`line_graph <networkx.generators.line.line_graph>`. For
86subgraphs which are not induced, the term 'monomorphism' is preferred
87over 'isomorphism'.
88
89Let ``G = (N, E)`` be a graph with a set of nodes ``N`` and set of edges ``E``.
90
91If ``G' = (N', E')`` is a subgraph, then:
92 ``N'`` is a subset of ``N`` and
93 ``E'`` is a subset of ``E``.
94
95If ``G' = (N', E')`` is a node-induced subgraph, then:
96 ``N'`` is a subset of ``N`` and
97 ``E'`` is the subset of edges in ``E`` relating nodes in ``N'``.
98
99If ``G' = (N', E')`` is an edge-induced subgraph, then:
100 ``N'`` is the subset of nodes in ``N`` related by edges in ``E'`` and
101 ``E'`` is a subset of ``E``.
102
103If ``G' = (N', E')`` is a monomorphism, then:
104 ``N'`` is a subset of ``N`` and
105 ``E'`` is a subset of the set of edges in ``E`` relating nodes in ``N'``.
106
107Note that if ``G'`` is a node-induced subgraph of ``G``, then it is always a
108subgraph monomorphism of ``G``, but the opposite is not always true, as a
109monomorphism can have fewer edges.
110
111References
112----------
113[1] Luigi P. Cordella, Pasquale Foggia, Carlo Sansone, Mario Vento,
114 "A (Sub)Graph Isomorphism Algorithm for Matching Large Graphs",
115 IEEE Transactions on Pattern Analysis and Machine Intelligence,
116 vol. 26, no. 10, pp. 1367-1372, Oct., 2004.
117 http://ieeexplore.ieee.org/iel5/34/29305/01323804.pdf
118
119[2] L. P. Cordella, P. Foggia, C. Sansone, M. Vento, "An Improved
120 Algorithm for Matching Large Graphs", 3rd IAPR-TC15 Workshop
121 on Graph-based Representations in Pattern Recognition, Cuen,
122 pp. 149-159, 2001.
123 https://www.researchgate.net/publication/200034365_An_Improved_Algorithm_for_Matching_Large_Graphs
124
125See Also
126--------
127:meth:`semantic_feasibility <networkx.algorithms.isomorphism.GraphMatcher.semantic_feasibility>`
128:meth:`syntactic_feasibility <networkx.algorithms.isomorphism.GraphMatcher.syntactic_feasibility>`
129
130Notes
131-----
132
133The implementation handles both directed and undirected graphs as well
134as multigraphs.
135
136In general, the subgraph isomorphism problem is NP-complete whereas the
137graph isomorphism problem is most likely not NP-complete (although no
138polynomial-time algorithm is known to exist).
139
140"""
141
142# This work was originally coded by Christopher Ellison
143# as part of the Computational Mechanics Python (CMPy) project.
144# James P. Crutchfield, principal investigator.
145# Complexity Sciences Center and Physics Department, UC Davis.
146
147import sys
148
149import networkx as nx
150
151__all__ = ["GraphMatcher", "DiGraphMatcher"]
152
153
154class GraphMatcher:
155 """Implementation of VF2 algorithm for matching undirected graphs.
156
157 Suitable for Graph and MultiGraph instances.
158 """
159
160 def __init__(self, G1, G2):
161 """Initialize GraphMatcher.
162
163 Parameters
164 ----------
165 G1,G2: NetworkX Graph or MultiGraph instances.
166 The two graphs to check for isomorphism or monomorphism.
167
168 Examples
169 --------
170 To create a GraphMatcher which checks for syntactic feasibility:
171
172 >>> from networkx.algorithms import isomorphism
173 >>> G1 = nx.path_graph(4)
174 >>> G2 = nx.path_graph(4)
175 >>> GM = isomorphism.GraphMatcher(G1, G2)
176 """
177 if G1.is_directed() != G2.is_directed():
178 raise nx.NetworkXError("G1 and G2 must have the same directedness")
179
180 is_directed_matcher = self._is_directed_matcher()
181 if not is_directed_matcher and (G1.is_directed() or G2.is_directed()):
182 raise nx.NetworkXError(
183 "(Multi-)GraphMatcher() not defined for directed graphs. "
184 "Use (Multi-)DiGraphMatcher() instead."
185 )
186
187 if is_directed_matcher and not (G1.is_directed() and G2.is_directed()):
188 raise nx.NetworkXError(
189 "(Multi-)DiGraphMatcher() not defined for undirected graphs. "
190 "Use (Multi-)GraphMatcher() instead."
191 )
192
193 self.G1 = G1
194 self.G2 = G2
195 self.G1_nodes = set(G1.nodes())
196 self.G2_nodes = set(G2.nodes())
197 self.G2_node_order = {n: i for i, n in enumerate(G2)}
198
199 # Set recursion limit.
200 self.old_recursion_limit = sys.getrecursionlimit()
201 expected_max_recursion_level = len(self.G2)
202 if self.old_recursion_limit < 1.5 * expected_max_recursion_level:
203 # Give some breathing room.
204 sys.setrecursionlimit(int(1.5 * expected_max_recursion_level))
205
206 # Declare that we will be searching for a graph-graph isomorphism.
207 self.test = "graph"
208
209 # Initialize state
210 self.initialize()
211
212 def _is_directed_matcher(self):
213 return False
214
215 def reset_recursion_limit(self):
216 """Restores the recursion limit."""
217 # TODO:
218 # Currently, we use recursion and set the recursion level higher.
219 # It would be nice to restore the level, but because the
220 # (Di)GraphMatcher classes make use of cyclic references, garbage
221 # collection will never happen when we define __del__() to
222 # restore the recursion level. The result is a memory leak.
223 # So for now, we do not automatically restore the recursion level,
224 # and instead provide a method to do this manually. Eventually,
225 # we should turn this into a non-recursive implementation.
226 sys.setrecursionlimit(self.old_recursion_limit)
227
228 def candidate_pairs_iter(self):
229 """Iterator over candidate pairs of nodes in G1 and G2."""
230
231 # All computations are done using the current state!
232
233 G1_nodes = self.G1_nodes
234 G2_nodes = self.G2_nodes
235 min_key = self.G2_node_order.__getitem__
236
237 # First we compute the inout-terminal sets.
238 T1_inout = [node for node in self.inout_1 if node not in self.core_1]
239 T2_inout = [node for node in self.inout_2 if node not in self.core_2]
240
241 # If T1_inout and T2_inout are both nonempty.
242 # P(s) = T1_inout x {min T2_inout}
243 if T1_inout and T2_inout:
244 node_2 = min(T2_inout, key=min_key)
245 for node_1 in T1_inout:
246 yield node_1, node_2
247
248 else:
249 # If T1_inout and T2_inout were both empty....
250 # P(s) = (N_1 - M_1) x {min (N_2 - M_2)}
251 # if not (T1_inout or T2_inout): # as suggested by [2], incorrect
252 if 1: # as inferred from [1], correct
253 # First we determine the candidate node for G2
254 other_node = min(G2_nodes - set(self.core_2), key=min_key)
255 for node in self.G1:
256 if node not in self.core_1:
257 yield node, other_node
258
259 # For all other cases, we don't have any candidate pairs.
260
261 def initialize(self):
262 """Reinitializes the state of the algorithm.
263
264 This method should be redefined if using something other than GMState.
265 If only subclassing GraphMatcher, a redefinition is not necessary.
266
267 """
268
269 # core_1[n] contains the index of the node paired with n, which is m,
270 # provided n is in the mapping.
271 # core_2[m] contains the index of the node paired with m, which is n,
272 # provided m is in the mapping.
273 self.core_1 = {}
274 self.core_2 = {}
275
276 # See the paper for definitions of M_x and T_x^{y}
277
278 # inout_1[n] is non-zero if n is in M_1 or in T_1^{inout}
279 # inout_2[m] is non-zero if m is in M_2 or in T_2^{inout}
280 #
281 # The value stored is the depth of the SSR tree when the node became
282 # part of the corresponding set.
283 self.inout_1 = {}
284 self.inout_2 = {}
285 # Practically, these sets simply store the nodes in the subgraph.
286
287 self.state = GMState(self)
288
289 # Provide a convenient way to access the isomorphism mapping.
290 self.mapping = self.core_1.copy()
291
292 def is_isomorphic(self):
293 """Returns True if G1 and G2 are isomorphic graphs."""
294
295 # Let's do two very quick checks!
296 # QUESTION: Should we call faster_graph_could_be_isomorphic(G1,G2)?
297 # For now, I just copy the code.
298
299 # Check global properties
300 if self.G1.order() != self.G2.order():
301 return False
302
303 # Check local properties
304 d1 = sorted(d for n, d in self.G1.degree())
305 d2 = sorted(d for n, d in self.G2.degree())
306 if d1 != d2:
307 return False
308
309 try:
310 x = next(self.isomorphisms_iter())
311 return True
312 except StopIteration:
313 return False
314
315 def isomorphisms_iter(self):
316 """Generator over isomorphisms between G1 and G2."""
317 # Declare that we are looking for a graph-graph isomorphism.
318 self.test = "graph"
319 self.initialize()
320 yield from self.match()
321
322 def match(self):
323 """Extends the isomorphism mapping.
324
325 This function is called recursively to determine if a complete
326 isomorphism can be found between G1 and G2. It cleans up the class
327 variables after each recursive call. If an isomorphism is found,
328 we yield the mapping.
329
330 """
331 if len(self.core_1) == len(self.G2):
332 # Save the final mapping, otherwise garbage collection deletes it.
333 self.mapping = self.core_1.copy()
334 # The mapping is complete.
335 yield self.mapping
336 else:
337 for G1_node, G2_node in self.candidate_pairs_iter():
338 if self.syntactic_feasibility(G1_node, G2_node):
339 if self.semantic_feasibility(G1_node, G2_node):
340 # Recursive call, adding the feasible state.
341 newstate = self.state.__class__(self, G1_node, G2_node)
342 yield from self.match()
343
344 # restore data structures
345 newstate.restore()
346
347 def semantic_feasibility(self, G1_node, G2_node):
348 """Returns True if adding (G1_node, G2_node) is semantically feasible.
349
350 The semantic feasibility function should return True if it is
351 acceptable to add the candidate pair (G1_node, G2_node) to the current
352 partial isomorphism mapping. The logic should focus on semantic
353 information contained in the edge data or a formalized node class.
354
355 By acceptable, we mean that the subsequent mapping can still become a
356 complete isomorphism mapping. Thus, if adding the candidate pair
357 definitely makes it so that the subsequent mapping cannot become a
358 complete isomorphism mapping, then this function must return False.
359
360 The default semantic feasibility function always returns True. The
361 effect is that semantics are not considered in the matching of G1
362 and G2.
363
364 The semantic checks might differ based on the what type of test is
365 being performed. A keyword description of the test is stored in
366 self.test. Here is a quick description of the currently implemented
367 tests::
368
369 test='graph'
370 Indicates that the graph matcher is looking for a graph-graph
371 isomorphism.
372
373 test='subgraph'
374 Indicates that the graph matcher is looking for a subgraph-graph
375 isomorphism such that a subgraph of G1 is isomorphic to G2.
376
377 test='mono'
378 Indicates that the graph matcher is looking for a subgraph-graph
379 monomorphism such that a subgraph of G1 is monomorphic to G2.
380
381 Any subclass which redefines semantic_feasibility() must maintain
382 the above form to keep the match() method functional. Implementations
383 should consider multigraphs.
384 """
385 return True
386
387 def subgraph_is_isomorphic(self):
388 """Returns `True` if a subgraph of ``G1`` is isomorphic to ``G2``.
389
390 Examples
391 --------
392 When creating the `GraphMatcher`, the order of the arguments is important
393
394 >>> G = nx.Graph([("A", "B"), ("B", "C"), ("A", "C")])
395 >>> H = nx.Graph([(0, 1), (1, 2), (0, 2), (1, 3), (0, 4)])
396
397 Check whether a subgraph of G is isomorphic to H:
398
399 >>> isomatcher = nx.isomorphism.GraphMatcher(G, H)
400 >>> isomatcher.subgraph_is_isomorphic()
401 False
402
403 Check whether a subgraph of H is isomorphic to G:
404
405 >>> isomatcher = nx.isomorphism.GraphMatcher(H, G)
406 >>> isomatcher.subgraph_is_isomorphic()
407 True
408 """
409 try:
410 x = next(self.subgraph_isomorphisms_iter())
411 return True
412 except StopIteration:
413 return False
414
415 def subgraph_is_monomorphic(self):
416 """Returns `True` if a subgraph of ``G1`` is monomorphic to ``G2``.
417
418 Examples
419 --------
420 When creating the `GraphMatcher`, the order of the arguments is important.
421
422 >>> G = nx.Graph([("A", "B"), ("B", "C")])
423 >>> H = nx.Graph([(0, 1), (1, 2), (0, 2)])
424
425 Check whether a subgraph of G is monomorphic to H:
426
427 >>> isomatcher = nx.isomorphism.GraphMatcher(G, H)
428 >>> isomatcher.subgraph_is_monomorphic()
429 False
430
431 Check whether a subgraph of H is monomorphic to G:
432
433 >>> isomatcher = nx.isomorphism.GraphMatcher(H, G)
434 >>> isomatcher.subgraph_is_monomorphic()
435 True
436 """
437 try:
438 x = next(self.subgraph_monomorphisms_iter())
439 return True
440 except StopIteration:
441 return False
442
443 def subgraph_isomorphisms_iter(self):
444 """Generator over isomorphisms between a subgraph of ``G1`` and ``G2``.
445
446 Examples
447 --------
448 When creating the `GraphMatcher`, the order of the arguments is important
449
450 >>> G = nx.Graph([("A", "B"), ("B", "C"), ("A", "C")])
451 >>> H = nx.Graph([(0, 1), (1, 2), (0, 2), (1, 3), (0, 4)])
452
453 Yield isomorphic mappings between ``H`` and subgraphs of ``G``:
454
455 >>> isomatcher = nx.isomorphism.GraphMatcher(G, H)
456 >>> list(isomatcher.subgraph_isomorphisms_iter())
457 []
458
459 Yield isomorphic mappings between ``G`` and subgraphs of ``H``:
460
461 >>> isomatcher = nx.isomorphism.GraphMatcher(H, G)
462 >>> next(isomatcher.subgraph_isomorphisms_iter())
463 {0: 'A', 1: 'B', 2: 'C'}
464
465 """
466 # Declare that we are looking for graph-subgraph isomorphism.
467 self.test = "subgraph"
468 self.initialize()
469 yield from self.match()
470
471 def subgraph_monomorphisms_iter(self):
472 """Generator over monomorphisms between a subgraph of ``G1`` and ``G2``.
473
474 Examples
475 --------
476 When creating the `GraphMatcher`, the order of the arguments is important.
477
478 >>> G = nx.Graph([("A", "B"), ("B", "C")])
479 >>> H = nx.Graph([(0, 1), (1, 2), (0, 2)])
480
481 Yield monomorphic mappings between ``H`` and subgraphs of ``G``:
482
483 >>> isomatcher = nx.isomorphism.GraphMatcher(G, H)
484 >>> list(isomatcher.subgraph_monomorphisms_iter())
485 []
486
487 Yield monomorphic mappings between ``G`` and subgraphs of ``H``:
488
489 >>> isomatcher = nx.isomorphism.GraphMatcher(H, G)
490 >>> next(isomatcher.subgraph_monomorphisms_iter())
491 {0: 'A', 1: 'B', 2: 'C'}
492 """
493 # Declare that we are looking for graph-subgraph monomorphism.
494 self.test = "mono"
495 self.initialize()
496 yield from self.match()
497
498 def syntactic_feasibility(self, G1_node, G2_node):
499 """Returns True if adding (G1_node, G2_node) is syntactically feasible.
500
501 This function returns True if it is adding the candidate pair
502 to the current partial isomorphism/monomorphism mapping is allowable.
503 The addition is allowable if the inclusion of the candidate pair does
504 not make it impossible for an isomorphism/monomorphism to be found.
505 """
506
507 # The VF2 algorithm was designed to work with graphs having, at most,
508 # one edge connecting any two nodes. This is not the case when
509 # dealing with an MultiGraphs.
510 #
511 # Basically, when we test the look-ahead rules R_neighbor, we will
512 # make sure that the number of edges are checked. We also add
513 # a R_self check to verify that the number of selfloops is acceptable.
514 #
515 # Users might be comparing Graph instances with MultiGraph instances.
516 # So the generic GraphMatcher class must work with MultiGraphs.
517 # Care must be taken since the value in the innermost dictionary is a
518 # singlet for Graph instances. For MultiGraphs, the value in the
519 # innermost dictionary is a list.
520
521 ###
522 # Test at each step to get a return value as soon as possible.
523 ###
524
525 # Look ahead 0
526
527 # R_self
528
529 # The number of selfloops for G1_node must equal the number of
530 # self-loops for G2_node. Without this check, we would fail on
531 # R_neighbor at the next recursion level. But it is good to prune the
532 # search tree now.
533
534 if self.test == "mono":
535 if self.G1.number_of_edges(G1_node, G1_node) < self.G2.number_of_edges(
536 G2_node, G2_node
537 ):
538 return False
539 else:
540 if self.G1.number_of_edges(G1_node, G1_node) != self.G2.number_of_edges(
541 G2_node, G2_node
542 ):
543 return False
544
545 # R_neighbor
546
547 # For each neighbor n' of n in the partial mapping, the corresponding
548 # node m' is a neighbor of m, and vice versa. Also, the number of
549 # edges must be equal.
550 if self.test != "mono":
551 for neighbor in self.G1[G1_node]:
552 if neighbor in self.core_1:
553 if self.core_1[neighbor] not in self.G2[G2_node]:
554 return False
555 elif self.G1.number_of_edges(
556 neighbor, G1_node
557 ) != self.G2.number_of_edges(self.core_1[neighbor], G2_node):
558 return False
559
560 for neighbor in self.G2[G2_node]:
561 if neighbor in self.core_2:
562 if self.core_2[neighbor] not in self.G1[G1_node]:
563 return False
564 elif self.test == "mono":
565 if self.G1.number_of_edges(
566 self.core_2[neighbor], G1_node
567 ) < self.G2.number_of_edges(neighbor, G2_node):
568 return False
569 else:
570 if self.G1.number_of_edges(
571 self.core_2[neighbor], G1_node
572 ) != self.G2.number_of_edges(neighbor, G2_node):
573 return False
574
575 if self.test != "mono":
576 # Look ahead 1
577
578 # R_terminout
579 # The number of neighbors of n in T_1^{inout} is equal to the
580 # number of neighbors of m that are in T_2^{inout}, and vice versa.
581 num1 = 0
582 for neighbor in self.G1[G1_node]:
583 if (neighbor in self.inout_1) and (neighbor not in self.core_1):
584 num1 += 1
585 num2 = 0
586 for neighbor in self.G2[G2_node]:
587 if (neighbor in self.inout_2) and (neighbor not in self.core_2):
588 num2 += 1
589 if self.test == "graph":
590 if num1 != num2:
591 return False
592 else: # self.test == 'subgraph'
593 if not (num1 >= num2):
594 return False
595
596 # Look ahead 2
597
598 # R_new
599
600 # The number of neighbors of n that are neither in the core_1 nor
601 # T_1^{inout} is equal to the number of neighbors of m
602 # that are neither in core_2 nor T_2^{inout}.
603 num1 = 0
604 for neighbor in self.G1[G1_node]:
605 if neighbor not in self.inout_1:
606 num1 += 1
607 num2 = 0
608 for neighbor in self.G2[G2_node]:
609 if neighbor not in self.inout_2:
610 num2 += 1
611 if self.test == "graph":
612 if num1 != num2:
613 return False
614 else: # self.test == 'subgraph'
615 if not (num1 >= num2):
616 return False
617
618 # Otherwise, this node pair is syntactically feasible!
619 return True
620
621
622class DiGraphMatcher(GraphMatcher):
623 """Implementation of VF2 algorithm for matching directed graphs.
624
625 Suitable for DiGraph and MultiDiGraph instances.
626 """
627
628 def __init__(self, G1, G2):
629 """Initialize DiGraphMatcher.
630
631 G1 and G2 should be nx.Graph or nx.MultiGraph instances.
632
633 Examples
634 --------
635 To create a GraphMatcher which checks for syntactic feasibility:
636
637 >>> from networkx.algorithms import isomorphism
638 >>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
639 >>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
640 >>> DiGM = isomorphism.DiGraphMatcher(G1, G2)
641 """
642 super().__init__(G1, G2)
643
644 def _is_directed_matcher(self):
645 return True
646
647 def candidate_pairs_iter(self):
648 """Iterator over candidate pairs of nodes in G1 and G2."""
649
650 # All computations are done using the current state!
651
652 G1_nodes = self.G1_nodes
653 G2_nodes = self.G2_nodes
654 min_key = self.G2_node_order.__getitem__
655
656 # First we compute the out-terminal sets.
657 T1_out = [node for node in self.out_1 if node not in self.core_1]
658 T2_out = [node for node in self.out_2 if node not in self.core_2]
659
660 # If T1_out and T2_out are both nonempty.
661 # P(s) = T1_out x {min T2_out}
662 if T1_out and T2_out:
663 node_2 = min(T2_out, key=min_key)
664 for node_1 in T1_out:
665 yield node_1, node_2
666
667 # If T1_out and T2_out were both empty....
668 # We compute the in-terminal sets.
669
670 # elif not (T1_out or T2_out): # as suggested by [2], incorrect
671 else: # as suggested by [1], correct
672 T1_in = [node for node in self.in_1 if node not in self.core_1]
673 T2_in = [node for node in self.in_2 if node not in self.core_2]
674
675 # If T1_in and T2_in are both nonempty.
676 # P(s) = T1_out x {min T2_out}
677 if T1_in and T2_in:
678 node_2 = min(T2_in, key=min_key)
679 for node_1 in T1_in:
680 yield node_1, node_2
681
682 # If all terminal sets are empty...
683 # P(s) = (N_1 - M_1) x {min (N_2 - M_2)}
684
685 # elif not (T1_in or T2_in): # as suggested by [2], incorrect
686 else: # as inferred from [1], correct
687 node_2 = min(G2_nodes - set(self.core_2), key=min_key)
688 for node_1 in G1_nodes:
689 if node_1 not in self.core_1:
690 yield node_1, node_2
691
692 # For all other cases, we don't have any candidate pairs.
693
694 def initialize(self):
695 """Reinitializes the state of the algorithm.
696
697 This method should be redefined if using something other than DiGMState.
698 If only subclassing GraphMatcher, a redefinition is not necessary.
699 """
700
701 # core_1[n] contains the index of the node paired with n, which is m,
702 # provided n is in the mapping.
703 # core_2[m] contains the index of the node paired with m, which is n,
704 # provided m is in the mapping.
705 self.core_1 = {}
706 self.core_2 = {}
707
708 # See the paper for definitions of M_x and T_x^{y}
709
710 # in_1[n] is non-zero if n is in M_1 or in T_1^{in}
711 # out_1[n] is non-zero if n is in M_1 or in T_1^{out}
712 #
713 # in_2[m] is non-zero if m is in M_2 or in T_2^{in}
714 # out_2[m] is non-zero if m is in M_2 or in T_2^{out}
715 #
716 # The value stored is the depth of the search tree when the node became
717 # part of the corresponding set.
718 self.in_1 = {}
719 self.in_2 = {}
720 self.out_1 = {}
721 self.out_2 = {}
722
723 self.state = DiGMState(self)
724
725 # Provide a convenient way to access the isomorphism mapping.
726 self.mapping = self.core_1.copy()
727
728 def syntactic_feasibility(self, G1_node, G2_node):
729 """Returns True if adding (G1_node, G2_node) is syntactically feasible.
730
731 This function returns True if it is adding the candidate pair
732 to the current partial isomorphism/monomorphism mapping is allowable.
733 The addition is allowable if the inclusion of the candidate pair does
734 not make it impossible for an isomorphism/monomorphism to be found.
735 """
736
737 # The VF2 algorithm was designed to work with graphs having, at most,
738 # one edge connecting any two nodes. This is not the case when
739 # dealing with an MultiGraphs.
740 #
741 # Basically, when we test the look-ahead rules R_pred and R_succ, we
742 # will make sure that the number of edges are checked. We also add
743 # a R_self check to verify that the number of selfloops is acceptable.
744
745 # Users might be comparing DiGraph instances with MultiDiGraph
746 # instances. So the generic DiGraphMatcher class must work with
747 # MultiDiGraphs. Care must be taken since the value in the innermost
748 # dictionary is a singlet for DiGraph instances. For MultiDiGraphs,
749 # the value in the innermost dictionary is a list.
750
751 ###
752 # Test at each step to get a return value as soon as possible.
753 ###
754
755 # Look ahead 0
756
757 # R_self
758
759 # The number of selfloops for G1_node must equal the number of
760 # self-loops for G2_node. Without this check, we would fail on R_pred
761 # at the next recursion level. This should prune the tree even further.
762 if self.test == "mono":
763 if self.G1.number_of_edges(G1_node, G1_node) < self.G2.number_of_edges(
764 G2_node, G2_node
765 ):
766 return False
767 else:
768 if self.G1.number_of_edges(G1_node, G1_node) != self.G2.number_of_edges(
769 G2_node, G2_node
770 ):
771 return False
772
773 # R_pred
774
775 # For each predecessor n' of n in the partial mapping, the
776 # corresponding node m' is a predecessor of m, and vice versa. Also,
777 # the number of edges must be equal
778 if self.test != "mono":
779 for predecessor in self.G1.pred[G1_node]:
780 if predecessor in self.core_1:
781 if self.core_1[predecessor] not in self.G2.pred[G2_node]:
782 return False
783 elif self.G1.number_of_edges(
784 predecessor, G1_node
785 ) != self.G2.number_of_edges(self.core_1[predecessor], G2_node):
786 return False
787
788 for predecessor in self.G2.pred[G2_node]:
789 if predecessor in self.core_2:
790 if self.core_2[predecessor] not in self.G1.pred[G1_node]:
791 return False
792 elif self.test == "mono":
793 if self.G1.number_of_edges(
794 self.core_2[predecessor], G1_node
795 ) < self.G2.number_of_edges(predecessor, G2_node):
796 return False
797 else:
798 if self.G1.number_of_edges(
799 self.core_2[predecessor], G1_node
800 ) != self.G2.number_of_edges(predecessor, G2_node):
801 return False
802
803 # R_succ
804
805 # For each successor n' of n in the partial mapping, the corresponding
806 # node m' is a successor of m, and vice versa. Also, the number of
807 # edges must be equal.
808 if self.test != "mono":
809 for successor in self.G1[G1_node]:
810 if successor in self.core_1:
811 if self.core_1[successor] not in self.G2[G2_node]:
812 return False
813 elif self.G1.number_of_edges(
814 G1_node, successor
815 ) != self.G2.number_of_edges(G2_node, self.core_1[successor]):
816 return False
817
818 for successor in self.G2[G2_node]:
819 if successor in self.core_2:
820 if self.core_2[successor] not in self.G1[G1_node]:
821 return False
822 elif self.test == "mono":
823 if self.G1.number_of_edges(
824 G1_node, self.core_2[successor]
825 ) < self.G2.number_of_edges(G2_node, successor):
826 return False
827 else:
828 if self.G1.number_of_edges(
829 G1_node, self.core_2[successor]
830 ) != self.G2.number_of_edges(G2_node, successor):
831 return False
832
833 if self.test != "mono":
834 # Look ahead 1
835
836 # R_termin
837 # The number of predecessors of n that are in T_1^{in} is equal to the
838 # number of predecessors of m that are in T_2^{in}.
839 num1 = 0
840 for predecessor in self.G1.pred[G1_node]:
841 if (predecessor in self.in_1) and (predecessor not in self.core_1):
842 num1 += 1
843 num2 = 0
844 for predecessor in self.G2.pred[G2_node]:
845 if (predecessor in self.in_2) and (predecessor not in self.core_2):
846 num2 += 1
847 if self.test == "graph":
848 if num1 != num2:
849 return False
850 else: # self.test == 'subgraph'
851 if not (num1 >= num2):
852 return False
853
854 # The number of successors of n that are in T_1^{in} is equal to the
855 # number of successors of m that are in T_2^{in}.
856 num1 = 0
857 for successor in self.G1[G1_node]:
858 if (successor in self.in_1) and (successor not in self.core_1):
859 num1 += 1
860 num2 = 0
861 for successor in self.G2[G2_node]:
862 if (successor in self.in_2) and (successor not in self.core_2):
863 num2 += 1
864 if self.test == "graph":
865 if num1 != num2:
866 return False
867 else: # self.test == 'subgraph'
868 if not (num1 >= num2):
869 return False
870
871 # R_termout
872
873 # The number of predecessors of n that are in T_1^{out} is equal to the
874 # number of predecessors of m that are in T_2^{out}.
875 num1 = 0
876 for predecessor in self.G1.pred[G1_node]:
877 if (predecessor in self.out_1) and (predecessor not in self.core_1):
878 num1 += 1
879 num2 = 0
880 for predecessor in self.G2.pred[G2_node]:
881 if (predecessor in self.out_2) and (predecessor not in self.core_2):
882 num2 += 1
883 if self.test == "graph":
884 if num1 != num2:
885 return False
886 else: # self.test == 'subgraph'
887 if not (num1 >= num2):
888 return False
889
890 # The number of successors of n that are in T_1^{out} is equal to the
891 # number of successors of m that are in T_2^{out}.
892 num1 = 0
893 for successor in self.G1[G1_node]:
894 if (successor in self.out_1) and (successor not in self.core_1):
895 num1 += 1
896 num2 = 0
897 for successor in self.G2[G2_node]:
898 if (successor in self.out_2) and (successor not in self.core_2):
899 num2 += 1
900 if self.test == "graph":
901 if num1 != num2:
902 return False
903 else: # self.test == 'subgraph'
904 if not (num1 >= num2):
905 return False
906
907 # Look ahead 2
908
909 # R_new
910
911 # The number of predecessors of n that are neither in the core_1 nor
912 # T_1^{in} nor T_1^{out} is equal to the number of predecessors of m
913 # that are neither in core_2 nor T_2^{in} nor T_2^{out}.
914 num1 = 0
915 for predecessor in self.G1.pred[G1_node]:
916 if (predecessor not in self.in_1) and (predecessor not in self.out_1):
917 num1 += 1
918 num2 = 0
919 for predecessor in self.G2.pred[G2_node]:
920 if (predecessor not in self.in_2) and (predecessor not in self.out_2):
921 num2 += 1
922 if self.test == "graph":
923 if num1 != num2:
924 return False
925 else: # self.test == 'subgraph'
926 if not (num1 >= num2):
927 return False
928
929 # The number of successors of n that are neither in the core_1 nor
930 # T_1^{in} nor T_1^{out} is equal to the number of successors of m
931 # that are neither in core_2 nor T_2^{in} nor T_2^{out}.
932 num1 = 0
933 for successor in self.G1[G1_node]:
934 if (successor not in self.in_1) and (successor not in self.out_1):
935 num1 += 1
936 num2 = 0
937 for successor in self.G2[G2_node]:
938 if (successor not in self.in_2) and (successor not in self.out_2):
939 num2 += 1
940 if self.test == "graph":
941 if num1 != num2:
942 return False
943 else: # self.test == 'subgraph'
944 if not (num1 >= num2):
945 return False
946
947 # Otherwise, this node pair is syntactically feasible!
948 return True
949
950 def subgraph_is_isomorphic(self):
951 """Returns `True` if a subgraph of ``G1`` is isomorphic to ``G2``.
952
953 Examples
954 --------
955 When creating the `DiGraphMatcher`, the order of the arguments is important
956
957 >>> G = nx.DiGraph([("A", "B"), ("B", "A"), ("B", "C"), ("C", "B")])
958 >>> H = nx.DiGraph(nx.path_graph(5))
959
960 Check whether a subgraph of G is isomorphic to H:
961
962 >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H)
963 >>> isomatcher.subgraph_is_isomorphic()
964 False
965
966 Check whether a subgraph of H is isomorphic to G:
967
968 >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G)
969 >>> isomatcher.subgraph_is_isomorphic()
970 True
971 """
972 return super().subgraph_is_isomorphic()
973
974 def subgraph_is_monomorphic(self):
975 """Returns `True` if a subgraph of ``G1`` is monomorphic to ``G2``.
976
977 Examples
978 --------
979 When creating the `DiGraphMatcher`, the order of the arguments is important.
980
981 >>> G = nx.DiGraph([("A", "B"), ("C", "B"), ("D", "C")])
982 >>> H = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 2)])
983
984 Check whether a subgraph of G is monomorphic to H:
985
986 >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H)
987 >>> isomatcher.subgraph_is_monomorphic()
988 False
989
990 Check whether a subgraph of H is isomorphic to G:
991
992 >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G)
993 >>> isomatcher.subgraph_is_monomorphic()
994 True
995 """
996 return super().subgraph_is_monomorphic()
997
998 def subgraph_isomorphisms_iter(self):
999 """Generator over isomorphisms between a subgraph of ``G1`` and ``G2``.
1000
1001 Examples
1002 --------
1003 When creating the `DiGraphMatcher`, the order of the arguments is important
1004
1005 >>> G = nx.DiGraph([("B", "C"), ("C", "B"), ("C", "D"), ("D", "C")])
1006 >>> H = nx.DiGraph(nx.path_graph(5))
1007
1008 Yield isomorphic mappings between ``H`` and subgraphs of ``G``:
1009
1010 >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H)
1011 >>> list(isomatcher.subgraph_isomorphisms_iter())
1012 []
1013
1014 Yield isomorphic mappings between ``G`` and subgraphs of ``H``:
1015
1016 >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G)
1017 >>> next(isomatcher.subgraph_isomorphisms_iter())
1018 {0: 'B', 1: 'C', 2: 'D'}
1019 """
1020 return super().subgraph_isomorphisms_iter()
1021
1022 def subgraph_monomorphisms_iter(self):
1023 """Generator over monomorphisms between a subgraph of ``G1`` and ``G2``.
1024
1025 Examples
1026 --------
1027 When creating the `DiGraphMatcher`, the order of the arguments is important.
1028
1029 >>> G = nx.DiGraph([("A", "B"), ("C", "B"), ("D", "C")])
1030 >>> H = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 2)])
1031
1032 Yield monomorphic mappings between ``H`` and subgraphs of ``G``:
1033
1034 >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H)
1035 >>> list(isomatcher.subgraph_monomorphisms_iter())
1036 []
1037
1038 Yield monomorphic mappings between ``G`` and subgraphs of ``H``:
1039
1040 >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G)
1041 >>> next(isomatcher.subgraph_monomorphisms_iter())
1042 {3: 'A', 2: 'B', 1: 'C', 0: 'D'}
1043 """
1044 return super().subgraph_monomorphisms_iter()
1045
1046
1047class GMState:
1048 """Internal representation of state for the GraphMatcher class.
1049
1050 This class is used internally by the GraphMatcher class. It is used
1051 only to store state specific data. There will be at most G2.order() of
1052 these objects in memory at a time, due to the depth-first search
1053 strategy employed by the VF2 algorithm.
1054 """
1055
1056 def __init__(self, GM, G1_node=None, G2_node=None):
1057 """Initializes GMState object.
1058
1059 Pass in the GraphMatcher to which this GMState belongs and the
1060 new node pair that will be added to the GraphMatcher's current
1061 isomorphism mapping.
1062 """
1063 self.GM = GM
1064
1065 # Initialize the last stored node pair.
1066 self.G1_node = None
1067 self.G2_node = None
1068 self.depth = len(GM.core_1)
1069
1070 if G1_node is None or G2_node is None:
1071 # Then we reset the class variables
1072 GM.core_1 = {}
1073 GM.core_2 = {}
1074 GM.inout_1 = {}
1075 GM.inout_2 = {}
1076
1077 # Watch out! G1_node == 0 should evaluate to True.
1078 if G1_node is not None and G2_node is not None:
1079 # Add the node pair to the isomorphism mapping.
1080 GM.core_1[G1_node] = G2_node
1081 GM.core_2[G2_node] = G1_node
1082
1083 # Store the node that was added last.
1084 self.G1_node = G1_node
1085 self.G2_node = G2_node
1086
1087 # Now we must update the other two vectors.
1088 # We will add only if it is not in there already!
1089 self.depth = len(GM.core_1)
1090
1091 # First we add the new nodes...
1092 if G1_node not in GM.inout_1:
1093 GM.inout_1[G1_node] = self.depth
1094 if G2_node not in GM.inout_2:
1095 GM.inout_2[G2_node] = self.depth
1096
1097 # Now we add every other node...
1098
1099 # Updates for T_1^{inout}
1100 new_nodes = set()
1101 for node in GM.core_1:
1102 new_nodes.update(
1103 [neighbor for neighbor in GM.G1[node] if neighbor not in GM.core_1]
1104 )
1105 for node in new_nodes:
1106 if node not in GM.inout_1:
1107 GM.inout_1[node] = self.depth
1108
1109 # Updates for T_2^{inout}
1110 new_nodes = set()
1111 for node in GM.core_2:
1112 new_nodes.update(
1113 [neighbor for neighbor in GM.G2[node] if neighbor not in GM.core_2]
1114 )
1115 for node in new_nodes:
1116 if node not in GM.inout_2:
1117 GM.inout_2[node] = self.depth
1118
1119 def restore(self):
1120 """Deletes the GMState object and restores the class variables."""
1121 # First we remove the node that was added from the core vectors.
1122 # Watch out! G1_node == 0 should evaluate to True.
1123 if self.G1_node is not None and self.G2_node is not None:
1124 del self.GM.core_1[self.G1_node]
1125 del self.GM.core_2[self.G2_node]
1126
1127 # Now we revert the other two vectors.
1128 # Thus, we delete all entries which have this depth level.
1129 for vector in (self.GM.inout_1, self.GM.inout_2):
1130 for node in list(vector.keys()):
1131 if vector[node] == self.depth:
1132 del vector[node]
1133
1134
1135class DiGMState:
1136 """Internal representation of state for the DiGraphMatcher class.
1137
1138 This class is used internally by the DiGraphMatcher class. It is used
1139 only to store state specific data. There will be at most G2.order() of
1140 these objects in memory at a time, due to the depth-first search
1141 strategy employed by the VF2 algorithm.
1142
1143 """
1144
1145 def __init__(self, GM, G1_node=None, G2_node=None):
1146 """Initializes DiGMState object.
1147
1148 Pass in the DiGraphMatcher to which this DiGMState belongs and the
1149 new node pair that will be added to the GraphMatcher's current
1150 isomorphism mapping.
1151 """
1152 self.GM = GM
1153
1154 # Initialize the last stored node pair.
1155 self.G1_node = None
1156 self.G2_node = None
1157 self.depth = len(GM.core_1)
1158
1159 if G1_node is None or G2_node is None:
1160 # Then we reset the class variables
1161 GM.core_1 = {}
1162 GM.core_2 = {}
1163 GM.in_1 = {}
1164 GM.in_2 = {}
1165 GM.out_1 = {}
1166 GM.out_2 = {}
1167
1168 # Watch out! G1_node == 0 should evaluate to True.
1169 if G1_node is not None and G2_node is not None:
1170 # Add the node pair to the isomorphism mapping.
1171 GM.core_1[G1_node] = G2_node
1172 GM.core_2[G2_node] = G1_node
1173
1174 # Store the node that was added last.
1175 self.G1_node = G1_node
1176 self.G2_node = G2_node
1177
1178 # Now we must update the other four vectors.
1179 # We will add only if it is not in there already!
1180 self.depth = len(GM.core_1)
1181
1182 # First we add the new nodes...
1183 for vector in (GM.in_1, GM.out_1):
1184 if G1_node not in vector:
1185 vector[G1_node] = self.depth
1186 for vector in (GM.in_2, GM.out_2):
1187 if G2_node not in vector:
1188 vector[G2_node] = self.depth
1189
1190 # Now we add every other node...
1191
1192 # Updates for T_1^{in}
1193 new_nodes = set()
1194 for node in GM.core_1:
1195 new_nodes.update(
1196 [
1197 predecessor
1198 for predecessor in GM.G1.predecessors(node)
1199 if predecessor not in GM.core_1
1200 ]
1201 )
1202 for node in new_nodes:
1203 if node not in GM.in_1:
1204 GM.in_1[node] = self.depth
1205
1206 # Updates for T_2^{in}
1207 new_nodes = set()
1208 for node in GM.core_2:
1209 new_nodes.update(
1210 [
1211 predecessor
1212 for predecessor in GM.G2.predecessors(node)
1213 if predecessor not in GM.core_2
1214 ]
1215 )
1216 for node in new_nodes:
1217 if node not in GM.in_2:
1218 GM.in_2[node] = self.depth
1219
1220 # Updates for T_1^{out}
1221 new_nodes = set()
1222 for node in GM.core_1:
1223 new_nodes.update(
1224 [
1225 successor
1226 for successor in GM.G1.successors(node)
1227 if successor not in GM.core_1
1228 ]
1229 )
1230 for node in new_nodes:
1231 if node not in GM.out_1:
1232 GM.out_1[node] = self.depth
1233
1234 # Updates for T_2^{out}
1235 new_nodes = set()
1236 for node in GM.core_2:
1237 new_nodes.update(
1238 [
1239 successor
1240 for successor in GM.G2.successors(node)
1241 if successor not in GM.core_2
1242 ]
1243 )
1244 for node in new_nodes:
1245 if node not in GM.out_2:
1246 GM.out_2[node] = self.depth
1247
1248 def restore(self):
1249 """Deletes the DiGMState object and restores the class variables."""
1250
1251 # First we remove the node that was added from the core vectors.
1252 # Watch out! G1_node == 0 should evaluate to True.
1253 if self.G1_node is not None and self.G2_node is not None:
1254 del self.GM.core_1[self.G1_node]
1255 del self.GM.core_2[self.G2_node]
1256
1257 # Now we revert the other four vectors.
1258 # Thus, we delete all entries which have this depth level.
1259 for vector in (self.GM.in_1, self.GM.in_2, self.GM.out_1, self.GM.out_2):
1260 for node in list(vector.keys()):
1261 if vector[node] == self.depth:
1262 del vector[node]