anhtld commited on
Commit
047a1d9
·
verified ·
1 Parent(s): d6727ca

Auto-sync: 2026-06-25 22:43:25 (part 8)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/INSTALLER +1 -0
  3. outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/METADATA +177 -0
  4. outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/RECORD +0 -0
  5. outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/WHEEL +5 -0
  6. outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/entry_points.txt +2 -0
  7. outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/licenses/LICENSE.txt +37 -0
  8. outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/top_level.txt +1 -0
  9. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/flow/tests/wlm3.gpickle.bz2 +3 -0
  10. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/flow/utils.py +194 -0
  11. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/graph_hashing.py +435 -0
  12. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/graphical.py +483 -0
  13. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/hierarchy.py +57 -0
  14. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/hybrid.py +196 -0
  15. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isolate.py +107 -0
  16. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/__init__.py +7 -0
  17. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/ismags.py +1306 -0
  18. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/isomorph.py +336 -0
  19. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/isomorphvf2.py +1262 -0
  20. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/matchhelpers.py +352 -0
  21. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/temporalisomorphvf2.py +308 -0
  22. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/__init__.py +0 -0
  23. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99 +0 -0
  24. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99 +0 -0
  25. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99 +0 -0
  26. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99 +0 -0
  27. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_ismags.py +719 -0
  28. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphism.py +103 -0
  29. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py +490 -0
  30. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_match_helpers.py +64 -0
  31. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py +212 -0
  32. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py +202 -0
  33. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp.py +1655 -0
  34. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py +3118 -0
  35. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py +196 -0
  36. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tree_isomorphism.py +264 -0
  37. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/vf2pp.py +1102 -0
  38. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/vf2userfunc.py +192 -0
  39. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/__init__.py +2 -0
  40. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/hits_alg.py +337 -0
  41. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/pagerank_alg.py +498 -0
  42. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/__init__.py +0 -0
  43. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/test_hits.py +77 -0
  44. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/test_pagerank.py +213 -0
  45. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_prediction.py +687 -0
  46. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/lowest_common_ancestors.py +280 -0
  47. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/matching.py +1148 -0
  48. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/__init__.py +27 -0
  49. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/contraction.py +738 -0
  50. outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/tests/test_contraction.py +544 -0
.gitattributes CHANGED
@@ -69,3 +69,7 @@ outputs/audit_venv/lib/python3.11/site-packages/matplotlib/mpl-data/fonts/ttf/ST
69
  outputs/audit_venv/lib/python3.11/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf filter=lfs diff=lfs merge=lfs -text
70
  outputs/audit_venv/lib/python3.11/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf filter=lfs diff=lfs merge=lfs -text
71
  outputs/audit_venv/lib/python3.11/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
69
  outputs/audit_venv/lib/python3.11/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf filter=lfs diff=lfs merge=lfs -text
70
  outputs/audit_venv/lib/python3.11/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf filter=lfs diff=lfs merge=lfs -text
71
  outputs/audit_venv/lib/python3.11/site-packages/matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf filter=lfs diff=lfs merge=lfs -text
72
+ outputs/audit_venv/lib/python3.11/site-packages/networkx/drawing/tests/baseline/test_display_complex.png filter=lfs diff=lfs merge=lfs -text
73
+ outputs/audit_venv/lib/python3.11/site-packages/numpy/_core/_multiarray_tests.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
74
+ outputs/audit_venv/lib/python3.11/site-packages/numpy/_core/_multiarray_umath.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
75
+ outputs/audit_venv/lib/python3.11/site-packages/numpy/_core/_simd.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/INSTALLER ADDED
@@ -0,0 +1 @@
 
 
1
+ pip
outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/METADATA ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.2
2
+ Name: networkx
3
+ Version: 3.6.1+computecanada
4
+ Summary: Python package for creating and manipulating graphs and networks
5
+ Description-Content-Type: text/x-rst
6
+ Keywords: Networks,Graph Theory,Mathematics,network,graph,discrete mathematics,math
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
20
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
21
+ Classifier: Topic :: Scientific/Engineering :: Physics
22
+ Author-email: Aric Hagberg <hagberg@lanl.gov>
23
+ Maintainer-email: NetworkX Developers <networkx-discuss@googlegroups.com>
24
+ Project-URL: Homepage, https://networkx.org/
25
+ Project-URL: Bug Tracker, https://github.com/networkx/networkx/issues
26
+ Project-URL: Documentation, https://networkx.org/documentation/stable/
27
+ Project-URL: Source Code, https://github.com/networkx/networkx
28
+ Platform: Linux
29
+ Platform: Mac OSX
30
+ Platform: Windows
31
+ Platform: Unix
32
+ Requires-Python: !=3.14.1,>=3.11
33
+ Requires-Dist: asv; extra == "benchmarking"
34
+ Requires-Dist: virtualenv; extra == "benchmarking"
35
+ Requires-Dist: numpy>=1.25; extra == "default"
36
+ Requires-Dist: scipy>=1.11.2; extra == "default"
37
+ Requires-Dist: matplotlib>=3.8; extra == "default"
38
+ Requires-Dist: pandas>=2.0; extra == "default"
39
+ Requires-Dist: pre-commit>=4.1; extra == "developer"
40
+ Requires-Dist: mypy>=1.15; extra == "developer"
41
+ Requires-Dist: sphinx>=8.0; extra == "doc"
42
+ Requires-Dist: pydata-sphinx-theme>=0.16; extra == "doc"
43
+ Requires-Dist: sphinx-gallery>=0.18; extra == "doc"
44
+ Requires-Dist: numpydoc>=1.8.0; extra == "doc"
45
+ Requires-Dist: pillow>=10; extra == "doc"
46
+ Requires-Dist: texext>=0.6.7; extra == "doc"
47
+ Requires-Dist: myst-nb>=1.1; extra == "doc"
48
+ Requires-Dist: intersphinx-registry; extra == "doc"
49
+ Requires-Dist: osmnx>=2.0.0; extra == "example"
50
+ Requires-Dist: momepy>=0.7.2; extra == "example"
51
+ Requires-Dist: contextily>=1.6; extra == "example"
52
+ Requires-Dist: seaborn>=0.13; extra == "example"
53
+ Requires-Dist: cairocffi>=1.7; extra == "example"
54
+ Requires-Dist: igraph>=0.11; extra == "example"
55
+ Requires-Dist: scikit-learn>=1.5; extra == "example"
56
+ Requires-Dist: iplotx>=0.9.0; extra == "example"
57
+ Requires-Dist: lxml>=4.6; extra == "extra"
58
+ Requires-Dist: pygraphviz>=1.14; extra == "extra"
59
+ Requires-Dist: pydot>=3.0.1; extra == "extra"
60
+ Requires-Dist: sympy>=1.10; extra == "extra"
61
+ Requires-Dist: build>=0.10; extra == "release"
62
+ Requires-Dist: twine>=4.0; extra == "release"
63
+ Requires-Dist: wheel>=0.40; extra == "release"
64
+ Requires-Dist: changelist==0.5; extra == "release"
65
+ Requires-Dist: pytest>=7.2; extra == "test"
66
+ Requires-Dist: pytest-cov>=4.0; extra == "test"
67
+ Requires-Dist: pytest-xdist>=3.0; extra == "test"
68
+ Requires-Dist: pytest-mpl; extra == "test-extras"
69
+ Requires-Dist: pytest-randomly; extra == "test-extras"
70
+ Provides-Extra: benchmarking
71
+ Provides-Extra: default
72
+ Provides-Extra: developer
73
+ Provides-Extra: doc
74
+ Provides-Extra: example
75
+ Provides-Extra: extra
76
+ Provides-Extra: release
77
+ Provides-Extra: test
78
+ Provides-Extra: test-extras
79
+ License-File: LICENSE.txt
80
+ License-Expression: BSD-3-Clause
81
+ Dynamic: license-file
82
+
83
+ NetworkX
84
+ ========
85
+
86
+
87
+ .. image::
88
+ https://github.com/networkx/networkx/actions/workflows/test.yml/badge.svg?branch=main
89
+ :target: https://github.com/networkx/networkx/actions/workflows/test.yml
90
+
91
+ .. image::
92
+ https://img.shields.io/pypi/v/networkx.svg?
93
+ :target: https://pypi.python.org/pypi/networkx
94
+
95
+ .. image::
96
+ https://img.shields.io/pypi/l/networkx.svg?
97
+ :target: https://github.com/networkx/networkx/blob/main/LICENSE.txt
98
+
99
+ .. image::
100
+ https://img.shields.io/pypi/pyversions/networkx.svg?
101
+ :target: https://pypi.python.org/pypi/networkx
102
+
103
+ .. image::
104
+ https://img.shields.io/github/labels/networkx/networkx/good%20first%20issue?color=green&label=contribute
105
+ :target: https://github.com/networkx/networkx/contribute
106
+
107
+ .. image::
108
+ https://insights.linuxfoundation.org/api/badge/health-score?project=networkx
109
+ :target: https://insights.linuxfoundation.org/project/networkx
110
+
111
+
112
+ NetworkX is a Python package for the creation, manipulation,
113
+ and study of the structure, dynamics, and functions
114
+ of complex networks.
115
+
116
+ - **Website (including documentation):** https://networkx.org
117
+ - **Mailing list:** https://groups.google.com/forum/#!forum/networkx-discuss
118
+ - **Source:** https://github.com/networkx/networkx
119
+ - **Bug reports:** https://github.com/networkx/networkx/issues
120
+ - **Report a security vulnerability:** https://tidelift.com/security
121
+ - **Tutorial:** https://networkx.org/documentation/latest/tutorial.html
122
+ - **GitHub Discussions:** https://github.com/networkx/networkx/discussions
123
+ - **Discord (Scientific Python) invite link:** https://discord.com/invite/vur45CbwMz
124
+ - **NetworkX meetings calendar (open to all):** https://scientific-python.org/calendars/networkx.ics
125
+
126
+ Simple example
127
+ --------------
128
+
129
+ Find the shortest path between two nodes in an undirected graph:
130
+
131
+ .. code:: pycon
132
+
133
+ >>> import networkx as nx
134
+ >>> G = nx.Graph()
135
+ >>> G.add_edge("A", "B", weight=4)
136
+ >>> G.add_edge("B", "D", weight=2)
137
+ >>> G.add_edge("A", "C", weight=3)
138
+ >>> G.add_edge("C", "D", weight=4)
139
+ >>> nx.shortest_path(G, "A", "D", weight="weight")
140
+ ['A', 'B', 'D']
141
+
142
+ Install
143
+ -------
144
+
145
+ Install the latest released version of NetworkX:
146
+
147
+ .. code:: shell
148
+
149
+ $ pip install networkx
150
+
151
+ Install with all optional dependencies:
152
+
153
+ .. code:: shell
154
+
155
+ $ pip install networkx[default]
156
+
157
+ For additional details,
158
+ please see the `installation guide <https://networkx.org/documentation/stable/install.html>`_.
159
+
160
+ Bugs
161
+ ----
162
+
163
+ Please report any bugs that you find `here <https://github.com/networkx/networkx/issues>`_.
164
+ Or, even better, fork the repository on `GitHub <https://github.com/networkx/networkx>`_
165
+ and create a pull request (PR). We welcome all changes, big or small, and we
166
+ will help you make the PR if you are new to `git` (just ask on the issue and/or
167
+ see the `contributor guide <https://networkx.org/documentation/latest/developer/contribute.html>`_).
168
+
169
+ License
170
+ -------
171
+
172
+ Released under the `3-clause BSD license <https://github.com/networkx/networkx/blob/main/LICENSE.txt>`_::
173
+
174
+ Copyright (c) 2004-2025, NetworkX Developers
175
+ Aric Hagberg <hagberg@lanl.gov>
176
+ Dan Schult <dschult@colgate.edu>
177
+ Pieter Swart <swart@lanl.gov>
outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/RECORD ADDED
The diff for this file is too large to render. See raw diff
 
outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/WHEEL ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Wheel-Version: 1.0
2
+ Generator: wheelfile 0.0.8
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [networkx.backends]
2
+ nx_loopback = networkx.classes.tests.dispatch_interface:backend_interface
outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/licenses/LICENSE.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ NetworkX is distributed with the 3-clause BSD license.
2
+
3
+ ::
4
+
5
+ Copyright (c) 2004-2025, NetworkX Developers
6
+ Aric Hagberg <hagberg@lanl.gov>
7
+ Dan Schult <dschult@colgate.edu>
8
+ Pieter Swart <swart@lanl.gov>
9
+ All rights reserved.
10
+
11
+ Redistribution and use in source and binary forms, with or without
12
+ modification, are permitted provided that the following conditions are
13
+ met:
14
+
15
+ * Redistributions of source code must retain the above copyright
16
+ notice, this list of conditions and the following disclaimer.
17
+
18
+ * Redistributions in binary form must reproduce the above
19
+ copyright notice, this list of conditions and the following
20
+ disclaimer in the documentation and/or other materials provided
21
+ with the distribution.
22
+
23
+ * Neither the name of the NetworkX Developers nor the names of its
24
+ contributors may be used to endorse or promote products derived
25
+ from this software without specific prior written permission.
26
+
27
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ networkx
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/flow/tests/wlm3.gpickle.bz2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ccacba1e0fbfb30bec361f0e48ec88c999d3474fcda5ddf93bd444ace17cfa0e
3
+ size 88132
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/flow/utils.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility classes and functions for network flow algorithms.
3
+ """
4
+
5
+ from collections import deque
6
+
7
+ import networkx as nx
8
+
9
+ __all__ = [
10
+ "CurrentEdge",
11
+ "Level",
12
+ "GlobalRelabelThreshold",
13
+ "build_residual_network",
14
+ "detect_unboundedness",
15
+ "build_flow_dict",
16
+ ]
17
+
18
+
19
+ class CurrentEdge:
20
+ """Mechanism for iterating over out-edges incident to a node in a circular
21
+ manner. StopIteration exception is raised when wraparound occurs.
22
+ """
23
+
24
+ __slots__ = ("_edges", "_it", "_curr")
25
+
26
+ def __init__(self, edges):
27
+ self._edges = edges
28
+ if self._edges:
29
+ self._rewind()
30
+
31
+ def get(self):
32
+ return self._curr
33
+
34
+ def move_to_next(self):
35
+ try:
36
+ self._curr = next(self._it)
37
+ except StopIteration:
38
+ self._rewind()
39
+ raise
40
+
41
+ def _rewind(self):
42
+ self._it = iter(self._edges.items())
43
+ self._curr = next(self._it)
44
+
45
+ def __eq__(self, other):
46
+ return (getattr(self, "_curr", None), self._edges) == (
47
+ (getattr(other, "_curr", None), other._edges)
48
+ )
49
+
50
+
51
+ class Level:
52
+ """Active and inactive nodes in a level."""
53
+
54
+ __slots__ = ("active", "inactive")
55
+
56
+ def __init__(self):
57
+ self.active = set()
58
+ self.inactive = set()
59
+
60
+
61
+ class GlobalRelabelThreshold:
62
+ """Measurement of work before the global relabeling heuristic should be
63
+ applied.
64
+ """
65
+
66
+ def __init__(self, n, m, freq):
67
+ self._threshold = (n + m) / freq if freq else float("inf")
68
+ self._work = 0
69
+
70
+ def add_work(self, work):
71
+ self._work += work
72
+
73
+ def is_reached(self):
74
+ return self._work >= self._threshold
75
+
76
+ def clear_work(self):
77
+ self._work = 0
78
+
79
+
80
+ @nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True)
81
+ def build_residual_network(G, capacity):
82
+ """Build a residual network and initialize a zero flow.
83
+
84
+ The residual network :samp:`R` from an input graph :samp:`G` has the
85
+ same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair
86
+ of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a
87
+ self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists
88
+ in :samp:`G`.
89
+
90
+ For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']`
91
+ is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists
92
+ in :samp:`G` or zero otherwise. If the capacity is infinite,
93
+ :samp:`R[u][v]['capacity']` will have a high arbitrary finite value
94
+ that does not affect the solution of the problem. This value is stored in
95
+ :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`,
96
+ :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and
97
+ satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`.
98
+
99
+ The flow value, defined as the total flow into :samp:`t`, the sink, is
100
+ stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not
101
+ specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such
102
+ that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum
103
+ :samp:`s`-:samp:`t` cut.
104
+
105
+ """
106
+ if G.is_multigraph():
107
+ raise nx.NetworkXError("MultiGraph and MultiDiGraph not supported (yet).")
108
+
109
+ R = nx.DiGraph()
110
+ R.__networkx_cache__ = None # Disable caching
111
+ R.add_nodes_from(G)
112
+
113
+ inf = float("inf")
114
+ # Extract edges with positive capacities. Self loops excluded.
115
+ edge_list = [
116
+ (u, v, attr)
117
+ for u, v, attr in G.edges(data=True)
118
+ if u != v and attr.get(capacity, inf) > 0
119
+ ]
120
+ # Simulate infinity with three times the sum of the finite edge capacities
121
+ # or any positive value if the sum is zero. This allows the
122
+ # infinite-capacity edges to be distinguished for unboundedness detection
123
+ # and directly participate in residual capacity calculation. If the maximum
124
+ # flow is finite, these edges cannot appear in the minimum cut and thus
125
+ # guarantee correctness. Since the residual capacity of an
126
+ # infinite-capacity edge is always at least 2/3 of inf, while that of an
127
+ # finite-capacity edge is at most 1/3 of inf, if an operation moves more
128
+ # than 1/3 of inf units of flow to t, there must be an infinite-capacity
129
+ # s-t path in G.
130
+ inf = (
131
+ 3
132
+ * sum(
133
+ attr[capacity]
134
+ for u, v, attr in edge_list
135
+ if capacity in attr and attr[capacity] != inf
136
+ )
137
+ or 1
138
+ )
139
+ if G.is_directed():
140
+ for u, v, attr in edge_list:
141
+ r = min(attr.get(capacity, inf), inf)
142
+ if not R.has_edge(u, v):
143
+ # Both (u, v) and (v, u) must be present in the residual
144
+ # network.
145
+ R.add_edge(u, v, capacity=r)
146
+ R.add_edge(v, u, capacity=0)
147
+ else:
148
+ # The edge (u, v) was added when (v, u) was visited.
149
+ R[u][v]["capacity"] = r
150
+ else:
151
+ for u, v, attr in edge_list:
152
+ # Add a pair of edges with equal residual capacities.
153
+ r = min(attr.get(capacity, inf), inf)
154
+ R.add_edge(u, v, capacity=r)
155
+ R.add_edge(v, u, capacity=r)
156
+
157
+ # Record the value simulating infinity.
158
+ R.graph["inf"] = inf
159
+
160
+ return R
161
+
162
+
163
+ @nx._dispatchable(
164
+ graphs="R",
165
+ preserve_edge_attrs={"R": {"capacity": float("inf")}},
166
+ preserve_graph_attrs=True,
167
+ )
168
+ def detect_unboundedness(R, s, t):
169
+ """Detect an infinite-capacity s-t path in R."""
170
+ q = deque([s])
171
+ seen = {s}
172
+ inf = R.graph["inf"]
173
+ while q:
174
+ u = q.popleft()
175
+ for v, attr in R[u].items():
176
+ if attr["capacity"] == inf and v not in seen:
177
+ if v == t:
178
+ raise nx.NetworkXUnbounded(
179
+ "Infinite capacity path, flow unbounded above."
180
+ )
181
+ seen.add(v)
182
+ q.append(v)
183
+
184
+
185
+ @nx._dispatchable(graphs={"G": 0, "R": 1}, preserve_edge_attrs={"R": {"flow": None}})
186
+ def build_flow_dict(G, R):
187
+ """Build a flow dictionary from a residual network."""
188
+ flow_dict = {}
189
+ for u in G:
190
+ flow_dict[u] = {v: 0 for v in G[u]}
191
+ flow_dict[u].update(
192
+ (v, attr["flow"]) for v, attr in R[u].items() if attr["flow"] > 0
193
+ )
194
+ return flow_dict
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/graph_hashing.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Functions for hashing graphs to strings.
3
+ Isomorphic graphs should be assigned identical hashes.
4
+ For now, only Weisfeiler-Lehman hashing is implemented.
5
+ """
6
+
7
+ import warnings
8
+ from collections import Counter, defaultdict
9
+ from hashlib import blake2b
10
+
11
+ import networkx as nx
12
+
13
+ __all__ = ["weisfeiler_lehman_graph_hash", "weisfeiler_lehman_subgraph_hashes"]
14
+
15
+
16
+ def _hash_label(label, digest_size):
17
+ return blake2b(label.encode("ascii"), digest_size=digest_size).hexdigest()
18
+
19
+
20
+ def _init_node_labels(G, edge_attr, node_attr):
21
+ if node_attr:
22
+ return {u: str(dd[node_attr]) for u, dd in G.nodes(data=True)}
23
+ elif edge_attr:
24
+ return {u: "" for u in G}
25
+ else:
26
+ warnings.warn(
27
+ "The hashes produced for graphs without node or edge attributes "
28
+ "changed in v3.5 due to a bugfix (see documentation).",
29
+ UserWarning,
30
+ stacklevel=2,
31
+ )
32
+ if nx.is_directed(G):
33
+ return {u: str(G.in_degree(u)) + "_" + str(G.out_degree(u)) for u in G}
34
+ else:
35
+ return {u: str(deg) for u, deg in G.degree()}
36
+
37
+
38
+ def _neighborhood_aggregate_undirected(G, node, node_labels, edge_attr=None):
39
+ """
40
+ Compute new labels for given node in an undirected graph by aggregating
41
+ the labels of each node's neighbors.
42
+ """
43
+ label_list = []
44
+ for nbr in G.neighbors(node):
45
+ prefix = "" if edge_attr is None else str(G[node][nbr][edge_attr])
46
+ label_list.append(prefix + node_labels[nbr])
47
+ return node_labels[node] + "".join(sorted(label_list))
48
+
49
+
50
+ def _neighborhood_aggregate_directed(G, node, node_labels, edge_attr=None):
51
+ """
52
+ Compute new labels for given node in a directed graph by aggregating
53
+ the labels of each node's neighbors.
54
+ """
55
+ successor_labels = []
56
+ for nbr in G.successors(node):
57
+ prefix = "s_" + "" if edge_attr is None else str(G[node][nbr][edge_attr])
58
+ successor_labels.append(prefix + node_labels[nbr])
59
+
60
+ predecessor_labels = []
61
+ for nbr in G.predecessors(node):
62
+ prefix = "p_" + "" if edge_attr is None else str(G[nbr][node][edge_attr])
63
+ predecessor_labels.append(prefix + node_labels[nbr])
64
+ return (
65
+ node_labels[node]
66
+ + "".join(sorted(successor_labels))
67
+ + "".join(sorted(predecessor_labels))
68
+ )
69
+
70
+
71
+ @nx.utils.not_implemented_for("multigraph")
72
+ @nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
73
+ def weisfeiler_lehman_graph_hash(
74
+ G, edge_attr=None, node_attr=None, iterations=3, digest_size=16
75
+ ):
76
+ """Return Weisfeiler Lehman (WL) graph hash.
77
+
78
+ .. Warning:: Hash values for directed graphs and graphs without edge or
79
+ node attributes have changed in v3.5. In previous versions,
80
+ directed graphs did not distinguish in- and outgoing edges. Also,
81
+ graphs without attributes set initial states such that effectively
82
+ one extra iteration of WL occurred than indicated by `iterations`.
83
+ For undirected graphs without node or edge labels, the old
84
+ hashes can be obtained by increasing the iteration count by one.
85
+ For more details, see `issue #7806
86
+ <https://github.com/networkx/networkx/issues/7806>`_.
87
+
88
+ The function iteratively aggregates and hashes neighborhoods of each node.
89
+ After each node's neighbors are hashed to obtain updated node labels,
90
+ a hashed histogram of resulting labels is returned as the final hash.
91
+
92
+ Hashes are identical for isomorphic graphs and strong guarantees that
93
+ non-isomorphic graphs will get different hashes. See [1]_ for details.
94
+
95
+ If no node or edge attributes are provided, the degree of each node
96
+ is used as its initial label.
97
+ Otherwise, node and/or edge labels are used to compute the hash.
98
+
99
+ Parameters
100
+ ----------
101
+ G : graph
102
+ The graph to be hashed.
103
+ Can have node and/or edge attributes. Can also have no attributes.
104
+ edge_attr : string, optional (default=None)
105
+ The key in edge attribute dictionary to be used for hashing.
106
+ If None, edge labels are ignored.
107
+ node_attr: string, optional (default=None)
108
+ The key in node attribute dictionary to be used for hashing.
109
+ If None, and no edge_attr given, use the degrees of the nodes as labels.
110
+ iterations: int, optional (default=3)
111
+ Number of neighbor aggregations to perform.
112
+ Should be larger for larger graphs.
113
+ digest_size: int, optional (default=16)
114
+ Size (in bytes) of blake2b hash digest to use for hashing node labels.
115
+
116
+ Returns
117
+ -------
118
+ h : string
119
+ Hexadecimal string corresponding to hash of `G` (length ``2 * digest_size``).
120
+
121
+ Raises
122
+ ------
123
+ ValueError
124
+ If `iterations` is not a positve number.
125
+
126
+ Examples
127
+ --------
128
+ Two graphs with edge attributes that are isomorphic, except for
129
+ differences in the edge labels.
130
+
131
+ >>> G1 = nx.Graph()
132
+ >>> G1.add_edges_from(
133
+ ... [
134
+ ... (1, 2, {"label": "A"}),
135
+ ... (2, 3, {"label": "A"}),
136
+ ... (3, 1, {"label": "A"}),
137
+ ... (1, 4, {"label": "B"}),
138
+ ... ]
139
+ ... )
140
+ >>> G2 = nx.Graph()
141
+ >>> G2.add_edges_from(
142
+ ... [
143
+ ... (5, 6, {"label": "B"}),
144
+ ... (6, 7, {"label": "A"}),
145
+ ... (7, 5, {"label": "A"}),
146
+ ... (7, 8, {"label": "A"}),
147
+ ... ]
148
+ ... )
149
+
150
+ Omitting the `edge_attr` option, results in identical hashes.
151
+
152
+ >>> nx.weisfeiler_lehman_graph_hash(G1)
153
+ 'c045439172215f49e0bef8c3d26c6b61'
154
+ >>> nx.weisfeiler_lehman_graph_hash(G2)
155
+ 'c045439172215f49e0bef8c3d26c6b61'
156
+
157
+ With edge labels, the graphs are no longer assigned
158
+ the same hash digest.
159
+
160
+ >>> nx.weisfeiler_lehman_graph_hash(G1, edge_attr="label")
161
+ 'c653d85538bcf041d88c011f4f905f10'
162
+ >>> nx.weisfeiler_lehman_graph_hash(G2, edge_attr="label")
163
+ '3dcd84af1ca855d0eff3c978d88e7ec7'
164
+
165
+ Notes
166
+ -----
167
+ To return the WL hashes of each subgraph of a graph, use
168
+ `weisfeiler_lehman_subgraph_hashes`
169
+
170
+ Similarity between hashes does not imply similarity between graphs.
171
+
172
+ References
173
+ ----------
174
+ .. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
175
+ Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
176
+ Graph Kernels. Journal of Machine Learning Research. 2011.
177
+ http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
178
+
179
+ See also
180
+ --------
181
+ weisfeiler_lehman_subgraph_hashes
182
+ """
183
+
184
+ if G.is_directed():
185
+ _neighborhood_aggregate = _neighborhood_aggregate_directed
186
+ warnings.warn(
187
+ "The hashes produced for directed graphs changed in version v3.5"
188
+ " due to a bugfix to track in and out edges separately (see documentation).",
189
+ UserWarning,
190
+ stacklevel=2,
191
+ )
192
+ else:
193
+ _neighborhood_aggregate = _neighborhood_aggregate_undirected
194
+
195
+ def weisfeiler_lehman_step(G, labels, edge_attr=None):
196
+ """
197
+ Apply neighborhood aggregation to each node
198
+ in the graph.
199
+ Computes a dictionary with labels for each node.
200
+ """
201
+ new_labels = {}
202
+ for node in G.nodes():
203
+ label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
204
+ new_labels[node] = _hash_label(label, digest_size)
205
+ return new_labels
206
+
207
+ if iterations <= 0:
208
+ raise ValueError("The WL algorithm requires that `iterations` be positive")
209
+
210
+ # set initial node labels
211
+ node_labels = _init_node_labels(G, edge_attr, node_attr)
212
+
213
+ # If the graph has no attributes, initial labels are the nodes' degrees.
214
+ # This is equivalent to doing the first iterations of WL.
215
+ if not edge_attr and not node_attr:
216
+ iterations -= 1
217
+
218
+ subgraph_hash_counts = []
219
+ for _ in range(iterations):
220
+ node_labels = weisfeiler_lehman_step(G, node_labels, edge_attr=edge_attr)
221
+ counter = Counter(node_labels.values())
222
+ # sort the counter, extend total counts
223
+ subgraph_hash_counts.extend(sorted(counter.items(), key=lambda x: x[0]))
224
+
225
+ # hash the final counter
226
+ return _hash_label(str(tuple(subgraph_hash_counts)), digest_size)
227
+
228
+
229
+ @nx.utils.not_implemented_for("multigraph")
230
+ @nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
231
+ def weisfeiler_lehman_subgraph_hashes(
232
+ G,
233
+ edge_attr=None,
234
+ node_attr=None,
235
+ iterations=3,
236
+ digest_size=16,
237
+ include_initial_labels=False,
238
+ ):
239
+ """
240
+ Return a dictionary of subgraph hashes by node.
241
+
242
+ .. Warning:: Hash values for directed graphs have changed in version
243
+ v3.5. In previous versions, directed graphs did not distinguish in-
244
+ and outgoing edges.
245
+ Graphs without attributes previously performed an extra iteration of
246
+ WL at initialisation, which was not visible in the output of this
247
+ function. This hash value is now included in the returned dictionary,
248
+ shifting the other calculated hashes one position to the right. To
249
+ obtain the same last subgraph hash, increase the number of iterations
250
+ by one.
251
+ For more details, see `issue #7806
252
+ <https://github.com/networkx/networkx/issues/7806>`_.
253
+
254
+ Dictionary keys are nodes in `G`, and values are a list of hashes.
255
+ Each hash corresponds to a subgraph rooted at a given node u in `G`.
256
+ Lists of subgraph hashes are sorted in increasing order of depth from
257
+ their root node, with the hash at index i corresponding to a subgraph
258
+ of nodes at most i-hops (i edges) distance from u. Thus, each list will contain
259
+ `iterations` elements - a hash for a subgraph at each depth. If
260
+ `include_initial_labels` is set to `True`, each list will additionally
261
+ have contain a hash of the initial node label (or equivalently a
262
+ subgraph of depth 0) prepended, totalling ``iterations + 1`` elements.
263
+
264
+ The function iteratively aggregates and hashes neighborhoods of each node.
265
+ This is achieved for each step by replacing for each node its label from
266
+ the previous iteration with its hashed 1-hop neighborhood aggregate.
267
+ The new node label is then appended to a list of node labels for each
268
+ node.
269
+
270
+ To aggregate neighborhoods for a node $u$ at each step, all labels of
271
+ nodes adjacent to $u$ are concatenated. If the `edge_attr` parameter is set,
272
+ labels for each neighboring node are prefixed with the value of this attribute
273
+ along the connecting edge from this neighbor to node $u$. The resulting string
274
+ is then hashed to compress this information into a fixed digest size.
275
+
276
+ Thus, at the i-th iteration, nodes within i hops influence any given
277
+ hashed node label. We can therefore say that at depth $i$ for node $u$
278
+ we have a hash for a subgraph induced by the i-hop neighborhood of $u$.
279
+
280
+ The output can be used to create general Weisfeiler-Lehman graph kernels,
281
+ or generate features for graphs or nodes - for example to generate 'words' in
282
+ a graph as seen in the 'graph2vec' algorithm.
283
+ See [1]_ & [2]_ respectively for details.
284
+
285
+ Hashes are identical for isomorphic subgraphs and there exist strong
286
+ guarantees that non-isomorphic graphs will get different hashes.
287
+ See [1]_ for details.
288
+
289
+ If no node or edge attributes are provided, the degree of each node
290
+ is used as its initial label.
291
+ Otherwise, node and/or edge labels are used to compute the hash.
292
+
293
+ Parameters
294
+ ----------
295
+ G : graph
296
+ The graph to be hashed.
297
+ Can have node and/or edge attributes. Can also have no attributes.
298
+ edge_attr : string, optional (default=None)
299
+ The key in edge attribute dictionary to be used for hashing.
300
+ If None, edge labels are ignored.
301
+ node_attr : string, optional (default=None)
302
+ The key in node attribute dictionary to be used for hashing.
303
+ If None, and no edge_attr given, use the degrees of the nodes as labels.
304
+ If None, and edge_attr is given, each node starts with an identical label.
305
+ iterations : int, optional (default=3)
306
+ Number of neighbor aggregations to perform.
307
+ Should be larger for larger graphs.
308
+ digest_size : int, optional (default=16)
309
+ Size (in bytes) of blake2b hash digest to use for hashing node labels.
310
+ The default size is 16 bytes.
311
+ include_initial_labels : bool, optional (default=False)
312
+ If True, include the hashed initial node label as the first subgraph
313
+ hash for each node.
314
+
315
+ Returns
316
+ -------
317
+ node_subgraph_hashes : dict
318
+ A dictionary with each key given by a node in G, and each value given
319
+ by the subgraph hashes in order of depth from the key node.
320
+ Hashes are hexadecimal strings (hence ``2 * digest_size`` long).
321
+
322
+
323
+ Raises
324
+ ------
325
+ ValueError
326
+ If `iterations` is not a positve number.
327
+
328
+ Examples
329
+ --------
330
+ Finding similar nodes in different graphs:
331
+
332
+ >>> G1 = nx.Graph()
333
+ >>> G1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 5), (4, 6), (5, 7), (6, 7)])
334
+ >>> G2 = nx.Graph()
335
+ >>> G2.add_edges_from([(1, 3), (2, 3), (1, 6), (1, 5), (4, 6)])
336
+ >>> g1_hashes = nx.weisfeiler_lehman_subgraph_hashes(
337
+ ... G1, iterations=4, digest_size=8
338
+ ... )
339
+ >>> g2_hashes = nx.weisfeiler_lehman_subgraph_hashes(
340
+ ... G2, iterations=4, digest_size=8
341
+ ... )
342
+
343
+ Even though G1 and G2 are not isomorphic (they have different numbers of edges),
344
+ the hash sequence of depth 3 for node 1 in G1 and node 5 in G2 are similar:
345
+
346
+ >>> g1_hashes[1]
347
+ ['f6fc42039fba3776', 'a93b64973cfc8897', 'db1b43ae35a1878f', '57872a7d2059c1c0']
348
+ >>> g2_hashes[5]
349
+ ['f6fc42039fba3776', 'a93b64973cfc8897', 'db1b43ae35a1878f', '1716d2a4012fa4bc']
350
+
351
+ The first 3 WL subgraph hashes match. From this we can conclude that it's very
352
+ likely the neighborhood of 3 hops around these nodes are isomorphic.
353
+
354
+ However the 4-hop neighborhoods of ``G1`` and ``G2`` are not isomorphic since the
355
+ 4th hashes in the lists above are not equal.
356
+
357
+ These nodes may be candidates to be classified together since their local topology
358
+ is similar.
359
+
360
+ Notes
361
+ -----
362
+ To hash the full graph when subgraph hashes are not needed, use
363
+ `weisfeiler_lehman_graph_hash` for efficiency.
364
+
365
+ Similarity between hashes does not imply similarity between graphs.
366
+
367
+ References
368
+ ----------
369
+ .. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen,
370
+ Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman
371
+ Graph Kernels. Journal of Machine Learning Research. 2011.
372
+ http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf
373
+ .. [2] Annamalai Narayanan, Mahinthan Chandramohan, Rajasekar Venkatesan,
374
+ Lihui Chen, Yang Liu and Shantanu Jaiswa. graph2vec: Learning
375
+ Distributed Representations of Graphs. arXiv. 2017
376
+ https://arxiv.org/pdf/1707.05005.pdf
377
+
378
+ See also
379
+ --------
380
+ weisfeiler_lehman_graph_hash
381
+ """
382
+
383
+ if G.is_directed():
384
+ _neighborhood_aggregate = _neighborhood_aggregate_directed
385
+ warnings.warn(
386
+ "The hashes produced for directed graphs changed in v3.5"
387
+ " due to a bugfix (see documentation).",
388
+ UserWarning,
389
+ stacklevel=2,
390
+ )
391
+ else:
392
+ _neighborhood_aggregate = _neighborhood_aggregate_undirected
393
+
394
+ def weisfeiler_lehman_step(G, labels, node_subgraph_hashes, edge_attr=None):
395
+ """
396
+ Apply neighborhood aggregation to each node
397
+ in the graph.
398
+ Computes a dictionary with labels for each node.
399
+ Appends the new hashed label to the dictionary of subgraph hashes
400
+ originating from and indexed by each node in G
401
+ """
402
+ new_labels = {}
403
+ for node in G.nodes():
404
+ label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr)
405
+ hashed_label = _hash_label(label, digest_size)
406
+ new_labels[node] = hashed_label
407
+ node_subgraph_hashes[node].append(hashed_label)
408
+ return new_labels
409
+
410
+ if iterations <= 0:
411
+ raise ValueError("The WL algorithm requires that `iterations` be positive")
412
+
413
+ node_labels = _init_node_labels(G, edge_attr, node_attr)
414
+
415
+ if include_initial_labels:
416
+ node_subgraph_hashes = {
417
+ k: [_hash_label(v, digest_size)] for k, v in node_labels.items()
418
+ }
419
+ else:
420
+ node_subgraph_hashes = defaultdict(list)
421
+
422
+ # If the graph has no attributes, initial labels are the nodes' degrees.
423
+ # This is equivalent to doing the first iterations of WL.
424
+ if not edge_attr and not node_attr:
425
+ iterations -= 1
426
+ for node in G.nodes():
427
+ hashed_label = _hash_label(node_labels[node], digest_size)
428
+ node_subgraph_hashes[node].append(hashed_label)
429
+
430
+ for _ in range(iterations):
431
+ node_labels = weisfeiler_lehman_step(
432
+ G, node_labels, node_subgraph_hashes, edge_attr
433
+ )
434
+
435
+ return dict(node_subgraph_hashes)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/graphical.py ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test sequences for graphiness."""
2
+
3
+ import heapq
4
+
5
+ import networkx as nx
6
+
7
+ __all__ = [
8
+ "is_graphical",
9
+ "is_multigraphical",
10
+ "is_pseudographical",
11
+ "is_digraphical",
12
+ "is_valid_degree_sequence_erdos_gallai",
13
+ "is_valid_degree_sequence_havel_hakimi",
14
+ ]
15
+
16
+
17
+ @nx._dispatchable(graphs=None)
18
+ def is_graphical(sequence, method="eg"):
19
+ """Returns True if sequence is a valid degree sequence.
20
+
21
+ A degree sequence is valid if some graph can realize it.
22
+
23
+ Parameters
24
+ ----------
25
+ sequence : list or iterable container
26
+ A sequence of integer node degrees
27
+
28
+ method : "eg" | "hh" (default: 'eg')
29
+ The method used to validate the degree sequence.
30
+ "eg" corresponds to the Erdős-Gallai algorithm
31
+ [EG1960]_, [choudum1986]_, and
32
+ "hh" to the Havel-Hakimi algorithm
33
+ [havel1955]_, [hakimi1962]_, [CL1996]_.
34
+
35
+ Returns
36
+ -------
37
+ valid : bool
38
+ True if the sequence is a valid degree sequence and False if not.
39
+
40
+ Examples
41
+ --------
42
+ >>> G = nx.path_graph(4)
43
+ >>> sequence = (d for n, d in G.degree())
44
+ >>> nx.is_graphical(sequence)
45
+ True
46
+
47
+ To test a non-graphical sequence:
48
+ >>> sequence_list = [d for n, d in G.degree()]
49
+ >>> sequence_list[-1] += 1
50
+ >>> nx.is_graphical(sequence_list)
51
+ False
52
+
53
+ References
54
+ ----------
55
+ .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
56
+ .. [choudum1986] S.A. Choudum. "A simple proof of the Erdős-Gallai theorem on
57
+ graph sequences." Bulletin of the Australian Mathematical Society, 33,
58
+ pp 67-70, 1986. https://doi.org/10.1017/S0004972700002872
59
+ .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
60
+ Casopis Pest. Mat. 80, 477-480, 1955.
61
+ .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
62
+ Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
63
+ .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
64
+ Chapman and Hall/CRC, 1996.
65
+ """
66
+ if method == "eg":
67
+ valid = is_valid_degree_sequence_erdos_gallai(list(sequence))
68
+ elif method == "hh":
69
+ valid = is_valid_degree_sequence_havel_hakimi(list(sequence))
70
+ else:
71
+ msg = "`method` must be 'eg' or 'hh'"
72
+ raise nx.NetworkXException(msg)
73
+ return valid
74
+
75
+
76
+ def _basic_graphical_tests(deg_sequence):
77
+ # Sort and perform some simple tests on the sequence
78
+ deg_sequence = nx.utils.make_list_of_ints(deg_sequence)
79
+ p = len(deg_sequence)
80
+ num_degs = [0] * p
81
+ dmax, dmin, dsum, n = 0, p, 0, 0
82
+ for d in deg_sequence:
83
+ # Reject if degree is negative or larger than the sequence length
84
+ if d < 0 or d >= p:
85
+ raise nx.NetworkXUnfeasible
86
+ # Process only the non-zero integers
87
+ elif d > 0:
88
+ dmax, dmin, dsum, n = max(dmax, d), min(dmin, d), dsum + d, n + 1
89
+ num_degs[d] += 1
90
+ # Reject sequence if it has odd sum or is oversaturated
91
+ if dsum % 2 or dsum > n * (n - 1):
92
+ raise nx.NetworkXUnfeasible
93
+ return dmax, dmin, dsum, n, num_degs
94
+
95
+
96
+ @nx._dispatchable(graphs=None)
97
+ def is_valid_degree_sequence_havel_hakimi(deg_sequence):
98
+ r"""Returns True if deg_sequence can be realized by a simple graph.
99
+
100
+ The validation proceeds using the Havel-Hakimi theorem
101
+ [havel1955]_, [hakimi1962]_, [CL1996]_.
102
+ Worst-case run time is $O(s)$ where $s$ is the sum of the sequence.
103
+
104
+ Parameters
105
+ ----------
106
+ deg_sequence : list
107
+ A list of integers where each element specifies the degree of a node
108
+ in a graph.
109
+
110
+ Returns
111
+ -------
112
+ valid : bool
113
+ True if deg_sequence is graphical and False if not.
114
+
115
+ Examples
116
+ --------
117
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
118
+ >>> sequence = (d for _, d in G.degree())
119
+ >>> nx.is_valid_degree_sequence_havel_hakimi(sequence)
120
+ True
121
+
122
+ To test a non-valid sequence:
123
+ >>> sequence_list = [d for _, d in G.degree()]
124
+ >>> sequence_list[-1] += 1
125
+ >>> nx.is_valid_degree_sequence_havel_hakimi(sequence_list)
126
+ False
127
+
128
+ Notes
129
+ -----
130
+ The ZZ condition says that for the sequence d if
131
+
132
+ .. math::
133
+ |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
134
+
135
+ then d is graphical. This was shown in Theorem 6 in [1]_.
136
+
137
+ References
138
+ ----------
139
+ .. [1] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
140
+ of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
141
+ .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs"
142
+ Casopis Pest. Mat. 80, 477-480, 1955.
143
+ .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as
144
+ Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962.
145
+ .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs",
146
+ Chapman and Hall/CRC, 1996.
147
+ """
148
+ try:
149
+ dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
150
+ except nx.NetworkXUnfeasible:
151
+ return False
152
+ # Accept if sequence has no non-zero degrees or passes the ZZ condition
153
+ if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
154
+ return True
155
+
156
+ modstubs = [0] * (dmax + 1)
157
+ # Successively reduce degree sequence by removing the maximum degree
158
+ while n > 0:
159
+ # Retrieve the maximum degree in the sequence
160
+ while num_degs[dmax] == 0:
161
+ dmax -= 1
162
+ # If there are not enough stubs to connect to, then the sequence is
163
+ # not graphical
164
+ if dmax > n - 1:
165
+ return False
166
+
167
+ # Remove largest stub in list
168
+ num_degs[dmax], n = num_degs[dmax] - 1, n - 1
169
+ # Reduce the next dmax largest stubs
170
+ mslen = 0
171
+ k = dmax
172
+ for i in range(dmax):
173
+ while num_degs[k] == 0:
174
+ k -= 1
175
+ num_degs[k], n = num_degs[k] - 1, n - 1
176
+ if k > 1:
177
+ modstubs[mslen] = k - 1
178
+ mslen += 1
179
+ # Add back to the list any non-zero stubs that were removed
180
+ for i in range(mslen):
181
+ stub = modstubs[i]
182
+ num_degs[stub], n = num_degs[stub] + 1, n + 1
183
+ return True
184
+
185
+
186
+ @nx._dispatchable(graphs=None)
187
+ def is_valid_degree_sequence_erdos_gallai(deg_sequence):
188
+ r"""Returns True if deg_sequence can be realized by a simple graph.
189
+
190
+ The validation is done using the Erdős-Gallai theorem [EG1960]_.
191
+
192
+ Parameters
193
+ ----------
194
+ deg_sequence : list
195
+ A list of integers
196
+
197
+ Returns
198
+ -------
199
+ valid : bool
200
+ True if deg_sequence is graphical and False if not.
201
+
202
+ Examples
203
+ --------
204
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
205
+ >>> sequence = (d for _, d in G.degree())
206
+ >>> nx.is_valid_degree_sequence_erdos_gallai(sequence)
207
+ True
208
+
209
+ To test a non-valid sequence:
210
+ >>> sequence_list = [d for _, d in G.degree()]
211
+ >>> sequence_list[-1] += 1
212
+ >>> nx.is_valid_degree_sequence_erdos_gallai(sequence_list)
213
+ False
214
+
215
+ Notes
216
+ -----
217
+
218
+ This implementation uses an equivalent form of the Erdős-Gallai criterion.
219
+ Worst-case run time is $O(n)$ where $n$ is the length of the sequence.
220
+
221
+ Specifically, a sequence d is graphical if and only if the
222
+ sum of the sequence is even and for all strong indices k in the sequence,
223
+
224
+ .. math::
225
+
226
+ \sum_{i=1}^{k} d_i \leq k(k-1) + \sum_{j=k+1}^{n} \min(d_i,k)
227
+ = k(n-1) - ( k \sum_{j=0}^{k-1} n_j - \sum_{j=0}^{k-1} j n_j )
228
+
229
+ A strong index k is any index where d_k >= k and the value n_j is the
230
+ number of occurrences of j in d. The maximal strong index is called the
231
+ Durfee index.
232
+
233
+ This particular rearrangement comes from the proof of Theorem 3 in [2]_.
234
+
235
+ The ZZ condition says that for the sequence d if
236
+
237
+ .. math::
238
+ |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)}
239
+
240
+ then d is graphical. This was shown in Theorem 6 in [2]_.
241
+
242
+ References
243
+ ----------
244
+ .. [1] A. Tripathi and S. Vijay. "A note on a theorem of Erdős & Gallai",
245
+ Discrete Mathematics, 265, pp. 417-420 (2003).
246
+ .. [2] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory
247
+ of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992).
248
+ .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960.
249
+ """
250
+ try:
251
+ dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence)
252
+ except nx.NetworkXUnfeasible:
253
+ return False
254
+ # Accept if sequence has no non-zero degrees or passes the ZZ condition
255
+ if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1):
256
+ return True
257
+
258
+ # Perform the EG checks using the reformulation of Zverovich and Zverovich
259
+ k, sum_deg, sum_nj, sum_jnj = 0, 0, 0, 0
260
+ for dk in range(dmax, dmin - 1, -1):
261
+ if dk < k + 1: # Check if already past Durfee index
262
+ return True
263
+ if num_degs[dk] > 0:
264
+ run_size = num_degs[dk] # Process a run of identical-valued degrees
265
+ if dk < k + run_size: # Check if end of run is past Durfee index
266
+ run_size = dk - k # Adjust back to Durfee index
267
+ sum_deg += run_size * dk
268
+ for v in range(run_size):
269
+ sum_nj += num_degs[k + v]
270
+ sum_jnj += (k + v) * num_degs[k + v]
271
+ k += run_size
272
+ if sum_deg > k * (n - 1) - k * sum_nj + sum_jnj:
273
+ return False
274
+ return True
275
+
276
+
277
+ @nx._dispatchable(graphs=None)
278
+ def is_multigraphical(sequence):
279
+ """Returns True if some multigraph can realize the sequence.
280
+
281
+ Parameters
282
+ ----------
283
+ sequence : list
284
+ A list of integers
285
+
286
+ Returns
287
+ -------
288
+ valid : bool
289
+ True if deg_sequence is a multigraphic degree sequence and False if not.
290
+
291
+ Examples
292
+ --------
293
+ >>> G = nx.MultiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
294
+ >>> sequence = (d for _, d in G.degree())
295
+ >>> nx.is_multigraphical(sequence)
296
+ True
297
+
298
+ To test a non-multigraphical sequence:
299
+ >>> sequence_list = [d for _, d in G.degree()]
300
+ >>> sequence_list[-1] += 1
301
+ >>> nx.is_multigraphical(sequence_list)
302
+ False
303
+
304
+ Notes
305
+ -----
306
+ The worst-case run time is $O(n)$ where $n$ is the length of the sequence.
307
+
308
+ References
309
+ ----------
310
+ .. [1] S. L. Hakimi. "On the realizability of a set of integers as
311
+ degrees of the vertices of a linear graph", J. SIAM, 10, pp. 496-506
312
+ (1962).
313
+ """
314
+ try:
315
+ deg_sequence = nx.utils.make_list_of_ints(sequence)
316
+ except nx.NetworkXError:
317
+ return False
318
+ dsum, dmax = 0, 0
319
+ for d in deg_sequence:
320
+ if d < 0:
321
+ return False
322
+ dsum, dmax = dsum + d, max(dmax, d)
323
+ if dsum % 2 or dsum < 2 * dmax:
324
+ return False
325
+ return True
326
+
327
+
328
+ @nx._dispatchable(graphs=None)
329
+ def is_pseudographical(sequence):
330
+ """Returns True if some pseudograph can realize the sequence.
331
+
332
+ Every nonnegative integer sequence with an even sum is pseudographical
333
+ (see [1]_).
334
+
335
+ Parameters
336
+ ----------
337
+ sequence : list or iterable container
338
+ A sequence of integer node degrees
339
+
340
+ Returns
341
+ -------
342
+ valid : bool
343
+ True if the sequence is a pseudographic degree sequence and False if not.
344
+
345
+ Examples
346
+ --------
347
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
348
+ >>> sequence = (d for _, d in G.degree())
349
+ >>> nx.is_pseudographical(sequence)
350
+ True
351
+
352
+ To test a non-pseudographical sequence:
353
+ >>> sequence_list = [d for _, d in G.degree()]
354
+ >>> sequence_list[-1] += 1
355
+ >>> nx.is_pseudographical(sequence_list)
356
+ False
357
+
358
+ Notes
359
+ -----
360
+ The worst-case run time is $O(n)$ where n is the length of the sequence.
361
+
362
+ References
363
+ ----------
364
+ .. [1] F. Boesch and F. Harary. "Line removal algorithms for graphs
365
+ and their degree lists", IEEE Trans. Circuits and Systems, CAS-23(12),
366
+ pp. 778-782 (1976).
367
+ """
368
+ try:
369
+ deg_sequence = nx.utils.make_list_of_ints(sequence)
370
+ except nx.NetworkXError:
371
+ return False
372
+ return sum(deg_sequence) % 2 == 0 and min(deg_sequence) >= 0
373
+
374
+
375
+ @nx._dispatchable(graphs=None)
376
+ def is_digraphical(in_sequence, out_sequence):
377
+ r"""Returns True if some directed graph can realize the in- and out-degree
378
+ sequences.
379
+
380
+ Parameters
381
+ ----------
382
+ in_sequence : list or iterable container
383
+ A sequence of integer node in-degrees
384
+
385
+ out_sequence : list or iterable container
386
+ A sequence of integer node out-degrees
387
+
388
+ Returns
389
+ -------
390
+ valid : bool
391
+ True if in and out-sequences are digraphic False if not.
392
+
393
+ Examples
394
+ --------
395
+ >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)])
396
+ >>> in_seq = (d for n, d in G.in_degree())
397
+ >>> out_seq = (d for n, d in G.out_degree())
398
+ >>> nx.is_digraphical(in_seq, out_seq)
399
+ True
400
+
401
+ To test a non-digraphical scenario:
402
+ >>> in_seq_list = [d for n, d in G.in_degree()]
403
+ >>> in_seq_list[-1] += 1
404
+ >>> nx.is_digraphical(in_seq_list, out_seq)
405
+ False
406
+
407
+ Notes
408
+ -----
409
+ This algorithm is from Kleitman and Wang [1]_.
410
+ The worst case runtime is $O(s \times \log n)$ where $s$ and $n$ are the
411
+ sum and length of the sequences respectively.
412
+
413
+ References
414
+ ----------
415
+ .. [1] D.J. Kleitman and D.L. Wang
416
+ Algorithms for Constructing Graphs and Digraphs with Given Valences
417
+ and Factors, Discrete Mathematics, 6(1), pp. 79-88 (1973)
418
+ """
419
+ try:
420
+ in_deg_sequence = nx.utils.make_list_of_ints(in_sequence)
421
+ out_deg_sequence = nx.utils.make_list_of_ints(out_sequence)
422
+ except nx.NetworkXError:
423
+ return False
424
+ # Process the sequences and form two heaps to store degree pairs with
425
+ # either zero or non-zero out degrees
426
+ sumin, sumout, nin, nout = 0, 0, len(in_deg_sequence), len(out_deg_sequence)
427
+ maxn = max(nin, nout)
428
+ maxin = 0
429
+ if maxn == 0:
430
+ return True
431
+ stubheap, zeroheap = [], []
432
+ for n in range(maxn):
433
+ in_deg, out_deg = 0, 0
434
+ if n < nout:
435
+ out_deg = out_deg_sequence[n]
436
+ if n < nin:
437
+ in_deg = in_deg_sequence[n]
438
+ if in_deg < 0 or out_deg < 0:
439
+ return False
440
+ sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg)
441
+ if in_deg > 0:
442
+ stubheap.append((-1 * out_deg, -1 * in_deg))
443
+ elif out_deg > 0:
444
+ zeroheap.append(-1 * out_deg)
445
+ if sumin != sumout:
446
+ return False
447
+ heapq.heapify(stubheap)
448
+ heapq.heapify(zeroheap)
449
+
450
+ modstubs = [(0, 0)] * (maxin + 1)
451
+ # Successively reduce degree sequence by removing the maximum out degree
452
+ while stubheap:
453
+ # Take the first value in the sequence with non-zero in degree
454
+ (freeout, freein) = heapq.heappop(stubheap)
455
+ freein *= -1
456
+ if freein > len(stubheap) + len(zeroheap):
457
+ return False
458
+
459
+ # Attach out stubs to the nodes with the most in stubs
460
+ mslen = 0
461
+ for i in range(freein):
462
+ if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0]):
463
+ stubout = heapq.heappop(zeroheap)
464
+ stubin = 0
465
+ else:
466
+ (stubout, stubin) = heapq.heappop(stubheap)
467
+ if stubout == 0:
468
+ return False
469
+ # Check if target is now totally connected
470
+ if stubout + 1 < 0 or stubin < 0:
471
+ modstubs[mslen] = (stubout + 1, stubin)
472
+ mslen += 1
473
+
474
+ # Add back the nodes to the heap that still have available stubs
475
+ for i in range(mslen):
476
+ stub = modstubs[i]
477
+ if stub[1] < 0:
478
+ heapq.heappush(stubheap, stub)
479
+ else:
480
+ heapq.heappush(zeroheap, stub[0])
481
+ if freeout < 0:
482
+ heapq.heappush(zeroheap, freeout)
483
+ return True
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/hierarchy.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Flow Hierarchy.
3
+ """
4
+
5
+ import networkx as nx
6
+
7
+ __all__ = ["flow_hierarchy"]
8
+
9
+
10
+ @nx._dispatchable(edge_attrs="weight")
11
+ def flow_hierarchy(G, weight=None):
12
+ """Returns the flow hierarchy of a directed network.
13
+
14
+ Flow hierarchy is defined as the fraction of edges not participating
15
+ in cycles in a directed graph [1]_.
16
+
17
+ Parameters
18
+ ----------
19
+ G : DiGraph or MultiDiGraph
20
+ A directed graph
21
+
22
+ weight : string, optional (default=None)
23
+ Attribute to use for edge weights. If None the weight defaults to 1.
24
+
25
+ Returns
26
+ -------
27
+ h : float
28
+ Flow hierarchy value
29
+
30
+ Raises
31
+ ------
32
+ NetworkXError
33
+ If `G` is not a directed graph or if `G` has no edges.
34
+
35
+ Notes
36
+ -----
37
+ The algorithm described in [1]_ computes the flow hierarchy through
38
+ exponentiation of the adjacency matrix. This function implements an
39
+ alternative approach that finds strongly connected components.
40
+ An edge is in a cycle if and only if it is in a strongly connected
41
+ component, which can be found in $O(m)$ time using Tarjan's algorithm.
42
+
43
+ References
44
+ ----------
45
+ .. [1] Luo, J.; Magee, C.L. (2011),
46
+ Detecting evolving patterns of self-organizing networks by flow
47
+ hierarchy measurement, Complexity, Volume 16 Issue 6 53-61.
48
+ DOI: 10.1002/cplx.20368
49
+ http://web.mit.edu/~cmagee/www/documents/28-DetectingEvolvingPatterns_FlowHierarchy.pdf
50
+ """
51
+ # corner case: G has no edges
52
+ if nx.is_empty(G):
53
+ raise nx.NetworkXError("flow_hierarchy not applicable to empty graphs")
54
+ if not G.is_directed():
55
+ raise nx.NetworkXError("G must be a digraph in flow_hierarchy")
56
+ scc = nx.strongly_connected_components(G)
57
+ return 1 - sum(G.subgraph(c).size(weight) for c in scc) / G.size(weight)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/hybrid.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Provides functions for finding and testing for locally `(k, l)`-connected
3
+ graphs.
4
+
5
+ """
6
+
7
+ import copy
8
+
9
+ import networkx as nx
10
+
11
+ __all__ = ["kl_connected_subgraph", "is_kl_connected"]
12
+
13
+
14
+ @nx._dispatchable(returns_graph=True)
15
+ def kl_connected_subgraph(G, k, l, low_memory=False, same_as_graph=False):
16
+ """Returns the maximum locally `(k, l)`-connected subgraph of `G`.
17
+
18
+ A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
19
+ graph there are at least `l` edge-disjoint paths of length at most `k`
20
+ joining `u` to `v`.
21
+
22
+ Parameters
23
+ ----------
24
+ G : NetworkX graph
25
+ The graph in which to find a maximum locally `(k, l)`-connected
26
+ subgraph.
27
+
28
+ k : integer
29
+ The maximum length of paths to consider. A higher number means a looser
30
+ connectivity requirement.
31
+
32
+ l : integer
33
+ The number of edge-disjoint paths. A higher number means a stricter
34
+ connectivity requirement.
35
+
36
+ low_memory : bool
37
+ If this is True, this function uses an algorithm that uses slightly
38
+ more time but less memory.
39
+
40
+ same_as_graph : bool
41
+ If True then return a tuple of the form `(H, is_same)`,
42
+ where `H` is the maximum locally `(k, l)`-connected subgraph and
43
+ `is_same` is a Boolean representing whether `G` is locally `(k,
44
+ l)`-connected (and hence, whether `H` is simply a copy of the input
45
+ graph `G`).
46
+
47
+ Returns
48
+ -------
49
+ NetworkX graph or two-tuple
50
+ If `same_as_graph` is True, then this function returns a
51
+ two-tuple as described above. Otherwise, it returns only the maximum
52
+ locally `(k, l)`-connected subgraph.
53
+
54
+ See also
55
+ --------
56
+ is_kl_connected
57
+
58
+ References
59
+ ----------
60
+ .. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
61
+ Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
62
+ 2004. 89--104.
63
+
64
+ """
65
+ H = copy.deepcopy(G) # subgraph we construct by removing from G
66
+
67
+ graphOK = True
68
+ deleted_some = True # hack to start off the while loop
69
+ while deleted_some:
70
+ deleted_some = False
71
+ # We use `for edge in list(H.edges()):` instead of
72
+ # `for edge in H.edges():` because we edit the graph `H` in
73
+ # the loop. Hence using an iterator will result in
74
+ # `RuntimeError: dictionary changed size during iteration`
75
+ for edge in list(H.edges()):
76
+ (u, v) = edge
77
+ # Get copy of graph needed for this search
78
+ if low_memory:
79
+ verts = {u, v}
80
+ for i in range(k):
81
+ for w in verts.copy():
82
+ verts.update(G[w])
83
+ G2 = G.subgraph(verts).copy()
84
+ else:
85
+ G2 = copy.deepcopy(G)
86
+ ###
87
+ path = [u, v]
88
+ cnt = 0
89
+ accept = 0
90
+ while path:
91
+ cnt += 1 # Found a path
92
+ if cnt >= l:
93
+ accept = 1
94
+ break
95
+ # record edges along this graph
96
+ prev = u
97
+ for w in path:
98
+ if prev != w:
99
+ G2.remove_edge(prev, w)
100
+ prev = w
101
+ # path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
102
+ try:
103
+ path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
104
+ except nx.NetworkXNoPath:
105
+ path = False
106
+ # No Other Paths
107
+ if accept == 0:
108
+ H.remove_edge(u, v)
109
+ deleted_some = True
110
+ if graphOK:
111
+ graphOK = False
112
+ # We looked through all edges and removed none of them.
113
+ # So, H is the maximal (k,l)-connected subgraph of G
114
+ if same_as_graph:
115
+ return (H, graphOK)
116
+ return H
117
+
118
+
119
+ @nx._dispatchable
120
+ def is_kl_connected(G, k, l, low_memory=False):
121
+ """Returns True if and only if `G` is locally `(k, l)`-connected.
122
+
123
+ A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the
124
+ graph there are at least `l` edge-disjoint paths of length at most `k`
125
+ joining `u` to `v`.
126
+
127
+ Parameters
128
+ ----------
129
+ G : NetworkX graph
130
+ The graph to test for local `(k, l)`-connectedness.
131
+
132
+ k : integer
133
+ The maximum length of paths to consider. A higher number means a looser
134
+ connectivity requirement.
135
+
136
+ l : integer
137
+ The number of edge-disjoint paths. A higher number means a stricter
138
+ connectivity requirement.
139
+
140
+ low_memory : bool
141
+ If this is True, this function uses an algorithm that uses slightly
142
+ more time but less memory.
143
+
144
+ Returns
145
+ -------
146
+ bool
147
+ Whether the graph is locally `(k, l)`-connected subgraph.
148
+
149
+ See also
150
+ --------
151
+ kl_connected_subgraph
152
+
153
+ References
154
+ ----------
155
+ .. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid
156
+ Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg,
157
+ 2004. 89--104.
158
+
159
+ """
160
+ graphOK = True
161
+ for edge in G.edges():
162
+ (u, v) = edge
163
+ # Get copy of graph needed for this search
164
+ if low_memory:
165
+ verts = {u, v}
166
+ for i in range(k):
167
+ [verts.update(G.neighbors(w)) for w in verts.copy()]
168
+ G2 = G.subgraph(verts)
169
+ else:
170
+ G2 = copy.deepcopy(G)
171
+ ###
172
+ path = [u, v]
173
+ cnt = 0
174
+ accept = 0
175
+ while path:
176
+ cnt += 1 # Found a path
177
+ if cnt >= l:
178
+ accept = 1
179
+ break
180
+ # record edges along this graph
181
+ prev = u
182
+ for w in path:
183
+ if w != prev:
184
+ G2.remove_edge(prev, w)
185
+ prev = w
186
+ # path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1?
187
+ try:
188
+ path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1?
189
+ except nx.NetworkXNoPath:
190
+ path = False
191
+ # No Other Paths
192
+ if accept == 0:
193
+ graphOK = False
194
+ break
195
+ # return status
196
+ return graphOK
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isolate.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Functions for identifying isolate (degree zero) nodes.
3
+ """
4
+
5
+ import networkx as nx
6
+
7
+ __all__ = ["is_isolate", "isolates", "number_of_isolates"]
8
+
9
+
10
+ @nx._dispatchable
11
+ def is_isolate(G, n):
12
+ """Determines whether a node is an isolate.
13
+
14
+ An *isolate* is a node with no neighbors (that is, with degree
15
+ zero). For directed graphs, this means no in-neighbors and no
16
+ out-neighbors.
17
+
18
+ Parameters
19
+ ----------
20
+ G : NetworkX graph
21
+
22
+ n : node
23
+ A node in `G`.
24
+
25
+ Returns
26
+ -------
27
+ is_isolate : bool
28
+ True if and only if `n` has no neighbors.
29
+
30
+ Examples
31
+ --------
32
+ >>> G = nx.Graph()
33
+ >>> G.add_edge(1, 2)
34
+ >>> G.add_node(3)
35
+ >>> nx.is_isolate(G, 2)
36
+ False
37
+ >>> nx.is_isolate(G, 3)
38
+ True
39
+ """
40
+ return G.degree(n) == 0
41
+
42
+
43
+ @nx._dispatchable
44
+ def isolates(G):
45
+ """Iterator over isolates in the graph.
46
+
47
+ An *isolate* is a node with no neighbors (that is, with degree
48
+ zero). For directed graphs, this means no in-neighbors and no
49
+ out-neighbors.
50
+
51
+ Parameters
52
+ ----------
53
+ G : NetworkX graph
54
+
55
+ Returns
56
+ -------
57
+ iterator
58
+ An iterator over the isolates of `G`.
59
+
60
+ Examples
61
+ --------
62
+ To get a list of all isolates of a graph, use the :class:`list`
63
+ constructor:
64
+
65
+ >>> G = nx.Graph()
66
+ >>> G.add_edge(1, 2)
67
+ >>> G.add_node(3)
68
+ >>> list(nx.isolates(G))
69
+ [3]
70
+
71
+ To remove all isolates in the graph, first create a list of the
72
+ isolates, then use :meth:`Graph.remove_nodes_from`:
73
+
74
+ >>> G.remove_nodes_from(list(nx.isolates(G)))
75
+ >>> list(G)
76
+ [1, 2]
77
+
78
+ For digraphs, isolates have zero in-degree and zero out_degree:
79
+
80
+ >>> G = nx.DiGraph([(0, 1), (1, 2)])
81
+ >>> G.add_node(3)
82
+ >>> list(nx.isolates(G))
83
+ [3]
84
+
85
+ """
86
+ return (n for n, d in G.degree() if d == 0)
87
+
88
+
89
+ @nx._dispatchable
90
+ def number_of_isolates(G):
91
+ """Returns the number of isolates in the graph.
92
+
93
+ An *isolate* is a node with no neighbors (that is, with degree
94
+ zero). For directed graphs, this means no in-neighbors and no
95
+ out-neighbors.
96
+
97
+ Parameters
98
+ ----------
99
+ G : NetworkX graph
100
+
101
+ Returns
102
+ -------
103
+ int
104
+ The number of degree zero nodes in the graph `G`.
105
+
106
+ """
107
+ return sum(1 for v in isolates(G))
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from networkx.algorithms.isomorphism.isomorph import *
2
+ from networkx.algorithms.isomorphism.vf2userfunc import *
3
+ from networkx.algorithms.isomorphism.matchhelpers import *
4
+ from networkx.algorithms.isomorphism.temporalisomorphvf2 import *
5
+ from networkx.algorithms.isomorphism.ismags import *
6
+ from networkx.algorithms.isomorphism.tree_isomorphism import *
7
+ from networkx.algorithms.isomorphism.vf2pp import *
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/ismags.py ADDED
@@ -0,0 +1,1306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ISMAGS Algorithm
3
+ ================
4
+
5
+ Provides a Python implementation of the ISMAGS algorithm. [1]_
6
+
7
+ ISMAGS does a symmetry analysis to find the constraints on isomorphisms if
8
+ we preclude yielding isomorphisms that differ by a symmetry of the subgraph.
9
+ For example, if the subgraph contains a 4-cycle, every isomorphism would have a
10
+ symmetric version with the nodes rotated relative to the original isomorphism.
11
+ By encoding these symmetries as constraints we reduce the search space for
12
+ isomorphisms and we also simplify processing the resulting isomorphisms.
13
+
14
+ ISMAGS finds (subgraph) isomorphisms between two graphs, taking the
15
+ symmetry of the subgraph into account. In most cases the VF2 algorithm is
16
+ faster (at least on small graphs) than this implementation, but in some cases
17
+ there are an exponential number of isomorphisms that are symmetrically
18
+ equivalent. In that case, the ISMAGS algorithm will provide only one isomorphism
19
+ per symmetry group, speeding up finding isomorphisms and avoiding the task of
20
+ post-processing many effectively identical isomorphisms.
21
+
22
+ >>> petersen = nx.petersen_graph()
23
+ >>> ismags = nx.isomorphism.ISMAGS(petersen, petersen)
24
+ >>> isomorphisms = list(ismags.isomorphisms_iter(symmetry=False))
25
+ >>> len(isomorphisms)
26
+ 120
27
+ >>> isomorphisms = list(ismags.isomorphisms_iter(symmetry=True))
28
+ >>> answer = [{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}]
29
+ >>> answer == isomorphisms
30
+ True
31
+
32
+ In addition, this implementation also provides an interface to find the
33
+ largest common induced subgraph [2]_ between any two graphs, again taking
34
+ symmetry into account. Given ``graph`` and ``subgraph`` the algorithm will remove
35
+ nodes from the ``subgraph`` until ``subgraph`` is isomorphic to a subgraph of
36
+ ``graph``. Since only the symmetry of ``subgraph`` is taken into account it is
37
+ worth thinking about how you provide your graphs:
38
+
39
+ >>> graph1 = nx.path_graph(4)
40
+ >>> graph2 = nx.star_graph(3)
41
+ >>> ismags = nx.isomorphism.ISMAGS(graph1, graph2)
42
+ >>> ismags.is_isomorphic()
43
+ False
44
+ >>> largest_common_subgraph = list(ismags.largest_common_subgraph())
45
+ >>> answer = [{1: 0, 0: 1, 2: 2}, {2: 0, 1: 1, 3: 2}]
46
+ >>> answer == largest_common_subgraph
47
+ True
48
+ >>> ismags2 = nx.isomorphism.ISMAGS(graph2, graph1)
49
+ >>> largest_common_subgraph = list(ismags2.largest_common_subgraph())
50
+ >>> answer = [
51
+ ... {1: 0, 0: 1, 2: 2},
52
+ ... {1: 0, 0: 1, 3: 2},
53
+ ... {2: 0, 0: 1, 1: 2},
54
+ ... {2: 0, 0: 1, 3: 2},
55
+ ... {3: 0, 0: 1, 1: 2},
56
+ ... {3: 0, 0: 1, 2: 2},
57
+ ... ]
58
+ >>> answer == largest_common_subgraph
59
+ True
60
+
61
+ However, when not taking symmetry into account, it doesn't matter:
62
+
63
+ >>> largest_common_subgraph = list(ismags.largest_common_subgraph(symmetry=False))
64
+ >>> answer = [
65
+ ... {1: 0, 0: 1, 2: 2},
66
+ ... {1: 0, 2: 1, 0: 2},
67
+ ... {2: 0, 1: 1, 3: 2},
68
+ ... {2: 0, 3: 1, 1: 2},
69
+ ... {1: 0, 0: 1, 2: 3},
70
+ ... {1: 0, 2: 1, 0: 3},
71
+ ... {2: 0, 1: 1, 3: 3},
72
+ ... {2: 0, 3: 1, 1: 3},
73
+ ... {1: 0, 0: 2, 2: 3},
74
+ ... {1: 0, 2: 2, 0: 3},
75
+ ... {2: 0, 1: 2, 3: 3},
76
+ ... {2: 0, 3: 2, 1: 3},
77
+ ... ]
78
+ >>> answer == largest_common_subgraph
79
+ True
80
+ >>> largest_common_subgraph = list(ismags2.largest_common_subgraph(symmetry=False))
81
+ >>> answer = [
82
+ ... {1: 0, 0: 1, 2: 2},
83
+ ... {1: 0, 0: 1, 3: 2},
84
+ ... {2: 0, 0: 1, 1: 2},
85
+ ... {2: 0, 0: 1, 3: 2},
86
+ ... {3: 0, 0: 1, 1: 2},
87
+ ... {3: 0, 0: 1, 2: 2},
88
+ ... {1: 1, 0: 2, 2: 3},
89
+ ... {1: 1, 0: 2, 3: 3},
90
+ ... {2: 1, 0: 2, 1: 3},
91
+ ... {2: 1, 0: 2, 3: 3},
92
+ ... {3: 1, 0: 2, 1: 3},
93
+ ... {3: 1, 0: 2, 2: 3},
94
+ ... ]
95
+ >>> answer == largest_common_subgraph
96
+ True
97
+
98
+ Notes
99
+ -----
100
+ - Node and edge equality is assumed to be transitive: if A is equal to B, and
101
+ B is equal to C, then A is equal to C.
102
+
103
+ - With a method that yields subgraph isomorphisms, we can construct functions like
104
+ ``is_subgraph_isomorphic`` by checking for any yielded mapping. And functions like
105
+ ``is_isomorphic`` by checking whether the subgraph has the same number of nodes as
106
+ the graph and is also subgraph isomorphic. This subpackage also allows a
107
+ ``symmetry`` bool keyword argument so you can find isomorphisms with or
108
+ without the symmetry constraints.
109
+
110
+ - For more information, see [2]_ and the documentation for :class:`ISMAGS`
111
+ which includes a description of the algorithm.
112
+
113
+ References
114
+ ----------
115
+ .. [1] M. Houbraken, S. Demeyer, T. Michoel, P. Audenaert, D. Colle,
116
+ M. Pickavet, "The Index-Based Subgraph Matching Algorithm with General
117
+ Symmetries (ISMAGS): Exploiting Symmetry for Faster Subgraph
118
+ Enumeration", PLoS One 9(5): e97896, 2014.
119
+ https://doi.org/10.1371/journal.pone.0097896
120
+ .. [2] https://en.wikipedia.org/wiki/Maximum_common_induced_subgraph
121
+ """
122
+
123
+ __all__ = ["ISMAGS"]
124
+
125
+ import itertools
126
+ from collections import Counter, defaultdict
127
+ from functools import reduce, wraps
128
+
129
+ import networkx as nx
130
+
131
+
132
+ def are_all_equal(iterable):
133
+ """
134
+ Returns ``True`` if and only if all elements in `iterable` are equal; and
135
+ ``False`` otherwise.
136
+
137
+ Parameters
138
+ ----------
139
+ iterable: collections.abc.Iterable
140
+ The container whose elements will be checked.
141
+
142
+ Returns
143
+ -------
144
+ bool
145
+ ``True`` iff all elements in `iterable` compare equal, ``False``
146
+ otherwise.
147
+ """
148
+ try:
149
+ shape = iterable.shape
150
+ except AttributeError:
151
+ pass
152
+ else:
153
+ if len(shape) > 1:
154
+ message = "The function does not works on multidimensional arrays."
155
+ raise NotImplementedError(message) from None
156
+
157
+ iterator = iter(iterable)
158
+ first = next(iterator, None)
159
+ return all(item == first for item in iterator)
160
+
161
+
162
+ def make_partition(items, test, check=True):
163
+ """
164
+ Partitions items into sets based on the outcome of ``test(item1, item2)``.
165
+ Pairs of items for which `test` returns `True` end up in the same set.
166
+
167
+ Parameters
168
+ ----------
169
+ items : collections.abc.Iterable[collections.abc.Hashable]
170
+ Items to partition
171
+ test : collections.abc.Callable[collections.abc.Hashable, collections.abc.Hashable]
172
+ A function that will be called with 2 arguments, taken from items.
173
+ Should return `True` if those 2 items match/tests so need to end up in the same
174
+ part of the partition, and `False` otherwise.
175
+ check : bool optional (default: True)
176
+ If ``True``, check that the resulting partition satisfies the match criteria.
177
+ Every item should match every item in its part and none outside the part.
178
+
179
+ Returns
180
+ -------
181
+ list[set]
182
+ A partition as a list of sets (the parts). Each set contains some of
183
+ the items in `items`, such that all items are in exactly one part and every
184
+ pair of items in each part matches. The following will be true:
185
+ ``all(thing_matcher(*pair) for pair in itertools.combinations(items, 2))``
186
+
187
+ Notes
188
+ -----
189
+ The function `test` is assumed to be transitive: if ``test(a, b)`` and
190
+ ``test(b, c)`` return ``True``, then ``test(a, c)`` must also be ``True``.
191
+ The function `test` is assumed to be commutative: if ``test(a, b)``
192
+ returns ``True`` then ``test(b, a)`` returns ``True``.
193
+ """
194
+ partition = []
195
+ for item in items:
196
+ for part in partition:
197
+ p_item = next(iter(part))
198
+ if test(item, p_item):
199
+ part.add(item)
200
+ break
201
+ else: # No break
202
+ partition.append({item})
203
+
204
+ if check:
205
+ if not all(
206
+ test(t1, t2) and test(t2, t1)
207
+ for part in partition
208
+ for t1, t2 in itertools.combinations(part, 2)
209
+ ):
210
+ raise nx.NetworkXError(
211
+ f"\nInvalid partition created with {test}.\n"
212
+ "Some items in a part do not match. This leads to\n"
213
+ f"{partition=}"
214
+ )
215
+ if not all(
216
+ not test(t1, t2) and not test(t2, t1)
217
+ for p1 in partition
218
+ for p2 in partition
219
+ if p1 != p2
220
+ for t1, t2 in itertools.product(p1, p2)
221
+ ):
222
+ raise nx.NetworkXError(
223
+ f"\nInvalid partition created with {test}.\n"
224
+ "Some items match multiple parts. This leads to\n"
225
+ f"{partition=}"
226
+ )
227
+ return [set(part) for part in partition]
228
+
229
+
230
+ def node_to_part_ID_dict(partition):
231
+ """
232
+ Creates a dictionary that maps each item in each part to the index of
233
+ the part to which it belongs.
234
+
235
+ Parameters
236
+ ----------
237
+ partition: collections.abc.Sequence[collections.abc.Iterable]
238
+ As returned by :func:`make_partition`.
239
+
240
+ Returns
241
+ -------
242
+ dict
243
+ """
244
+ return {node: ID for ID, part in enumerate(partition) for node in part}
245
+
246
+
247
+ def color_degree_by_node(G, n_colors, e_colors):
248
+ """Returns a dict by node to counts of edge and node color for that node.
249
+
250
+ This returns a dict by node to a 2-tuple of node color and degree by
251
+ (edge color and nbr color). E.g. ``{0: (1, {(0, 2): 5})}`` means that
252
+ node ``0`` has node type 1 and has 5 edges of type 0 that go to nodes of type 2.
253
+ Thus, this is a measure of degree (edge count) by color of edge and color
254
+ of the node on the other side of that edge.
255
+
256
+ For directed graphs the degree counts is a 2-tuple of (in, out) degree counts.
257
+
258
+ Ideally, if edge_match is None, this could get simplified to just the node
259
+ color on the other end of the edge. Similarly if node_match is None then only
260
+ edge color is tracked. And if both are None, we simply count the number of edges.
261
+ """
262
+ # n_colors might be incomplete when using `largest_common_subgraph()`
263
+ if len(n_colors) < len(G):
264
+ for n, nbrs in G.adjacency():
265
+ if n not in n_colors:
266
+ n_colors[n] = None
267
+ for v in nbrs:
268
+ e_colors[n, v] = None
269
+ # undirected colored degree
270
+ if not G.is_directed():
271
+ return {
272
+ u: (n_colors[u], Counter((e_colors[u, v], n_colors[v]) for v in nbrs))
273
+ for u, nbrs in G.adjacency()
274
+ }
275
+ # directed colored out and in degree
276
+ return {
277
+ u: (
278
+ n_colors[u],
279
+ Counter((e_colors[u, v], n_colors[v]) for v in nbrs),
280
+ Counter((e_colors[v, u], n_colors[v]) for v in G._pred[u]),
281
+ )
282
+ for u, nbrs in G.adjacency()
283
+ }
284
+
285
+
286
+ class EdgeLookup:
287
+ """Class to handle getitem for undirected edges.
288
+
289
+ Note that ``items()`` iterates over one of the two representations of the edge
290
+ (u, v) and (v, u). So this technically doesn't violate the Mapping
291
+ invariant that (k,v) pairs reported by ``items()`` satisfy ``.__getitem__(k) == v``.
292
+ But we are violating the spirit of the protocol by having keys available
293
+ for lookup by ``__getitem__`` that are not reported by ``items()``.
294
+
295
+ Note that if we used frozensets for undirected edges we would have the same
296
+ behavior we see here. You could ``__getitem__`` either ``{u, v}`` or ``{v, u}``
297
+ and get the same value -- yet ``items()`` would only report one of the two.
298
+ So from that perspective we *are* following the Mapping protocol. Our keys
299
+ are undirected edges. We are using 2-tuples as an imperfect representation
300
+ of these edges. We are not using 2-tuples as keys. Only as imperfect edges
301
+ and we use the edges as keys.
302
+ """
303
+
304
+ def __init__(self, edge_dict):
305
+ self.edge_dict = edge_dict
306
+
307
+ def __getitem__(self, edge):
308
+ if edge in self.edge_dict:
309
+ return self.edge_dict[edge]
310
+ return self.edge_dict[edge[::-1]]
311
+
312
+ def items(self):
313
+ return self.edge_dict.items()
314
+
315
+
316
+ class ISMAGS:
317
+ """
318
+ Implements the ISMAGS subgraph matching algorithm. [1]_ ISMAGS stands for
319
+ "Index-based Subgraph Matching Algorithm with General Symmetries". As the
320
+ name implies, it is symmetry aware and will only generate non-symmetric
321
+ isomorphisms.
322
+
323
+ Attributes
324
+ ----------
325
+ graph: networkx.Graph
326
+ subgraph: networkx.Graph
327
+
328
+ Notes
329
+ -----
330
+ ISMAGS does a symmetry analysis to find the constraints on isomorphisms if
331
+ we preclude yielding isomorphisms that differ by a symmetry of the subgraph.
332
+ For example, if the subgraph is a 4-cycle, every isomorphism would have a
333
+ symmetric version with the nodes rotated relative to the original isomorphism.
334
+ By encoding these symmetries as constraints we reduce the search space for
335
+ isomorphisms and we also simplify processing the resulting isomorphisms.
336
+
337
+ **Symmetry Analysis**
338
+
339
+ The constraints in ISMAGS are based off the handling in ``nauty`` and its many
340
+ variants, in particular ``saucy``, as discussed in the ISMAGS paper [1]_.
341
+ That paper cites [3]_ for details on symmetry handling. Figure 2 of [3]_
342
+ describes the DFS approach to symmetries used here and relying on a data structure
343
+ called an Ordered Pair Partitions(OPP). This consists of a pair of partitions
344
+ where each part represents nodes with the same degree-by-color over all colors.
345
+ We refine these partitions simultaneously in a way to result in permutations
346
+ of the nodes that preserve the graph structure. We thus find automorphisms
347
+ for the subgraph of interest. From those we identify pairs of nodes which
348
+ are structurally equivalent. We then constrain our problem by requiring the
349
+ first of the pair to always be assigned first in the isomorphism -- thus
350
+ constraining the isomorphisms reported to only one example from the set of all
351
+ symmetrically equivalent isomorphisms. These constraints are computed once
352
+ based on the subgraph symmetries and then used throughout the DFS search for
353
+ isomorphisms.
354
+
355
+ Finding the symmetries involves a DFS of the OPP wherein we "couple" a node
356
+ to a node in its degree-by-color part of the partition. This "coupling" is done
357
+ via assigning a new color in the top partition to the node being coupled,
358
+ and the same new color in the bottom partition to the node being coupled to.
359
+ This new color has only one node in each partition. The new color also requires
360
+ that we "refine" both top and bottom partitions by splitting parts until each
361
+ part represents a common degree-by-color value. Those refinements introduce
362
+ new colors as the parts are split during refinement. Parts do not get combined
363
+ during refinement. So the coupling/refining process always results in at least
364
+ one new part with only one node in both the top and bottom partition. In practice
365
+ we usually refine into many new one-node parts in both partitions.
366
+ We continue in this way until each node has its own part/color in the top partition
367
+ -- and the node in the bottom partition with that color is the symmetric node.
368
+ That is, an OPP represents an automorphism, and thus a symmetry
369
+ of the subgraph when each color has a single node in the top partition and a single
370
+ node in the bottom partition. From those automorphisms we build up a set of nodes
371
+ that can be obtained from each other by symmetry (they are mutually symmetric).
372
+ That set of nodes is called an "orbit" of the subgraph under symmetry.
373
+
374
+ After finding the orbits for one symmetry, we backtrack in the DFS by removing the
375
+ latest coupling and replacing it with a coupling from the same top node to a new
376
+ bottom node in its degree-by-color grouping. When all possible couplings for that
377
+ node are considered we backtrack to the previously coupled node and recouple in
378
+ a DFS manner.
379
+
380
+ We can prune the DFS search tree in helpful ways. The paper [2]_ demonstrates 6
381
+ situations of interest in the DFS where pruning is possible:
382
+
383
+ - An **Automorphism OPP** is an OPP where every part in both partitions
384
+ contains a single node. The mapping/automorphism is found by mapping
385
+ each top node to the bottom node in the same color part. For example,
386
+ ``[({1}, {2}, {3}); ({2}, {3}, {1})]`` represents the mapping of each
387
+ node to the next in a triangle. It rotates the nodes around the triangle.
388
+ - The **Identity OPP** is the first automorphism found during the DFS. It
389
+ appears on the left side of the DFS tree and is first due to our ordering of
390
+ coupling nodes to be in an arbitrary but fixed ordering of the nodes. This
391
+ automorphism does not show any symmetries, but it ensures the orbit for each
392
+ node includes itself and it sets us up for handling the symmetries. Note that
393
+ a subgraph with no symmetries will only have the identity automorphism.
394
+ - A **Non-isomorphic OPP** occurs when refinement creates a different number of
395
+ parts in the top partition than in the bottom partition. This means no symmetries
396
+ will be found during further processing of that subtree of the DFS. We prune
397
+ the subtree and continue.
398
+ - A **Matching OPP** is such that each top part that has more than one node is
399
+ in fact equal to the bottom part with the same color. The many-node-parts match
400
+ exactly. The single-node parts then represent symmetries that do not permute
401
+ any matching nodes. Matching OPPs arise while finding the Identity Mapping. But
402
+ the single-node parts are identical in the two partitions, so no useful symmetries
403
+ are found. But after the Identity Mapping is found, every Matching OPP encountered
404
+ will have different nodes in at least two single-node parts of the same color.
405
+ So these positions in the DFS provide us with symmetries without any
406
+ need to find the whole automorphism. We can prune the subtree, update the orbits
407
+ and backtrack. Any larger symmetries that combine these symmetries with symmetries
408
+ of the many-node-parts do not need to be explored because the symmetry "generators"
409
+ found in this way provide a basis for all symmetries. We will find the symmetry
410
+ generators of the many-node-parts at another subtree of the DFS.
411
+ - An **Orbit Pruning OPP** is an OPP where the node coupling to be considered next
412
+ has both nodes already known to be in the same orbit. We have already identified
413
+ those permutations when we discovered the orbit. So we can prune the resulting
414
+ subtree. This is the primary pruning discussed in [1]_.
415
+ - A **Coset Point** in the DFS is a point of the tree when a node is first
416
+ back-tracked. That is, its couplings have all been analyzed once and we backtrack
417
+ to its parent. So, said another way, when a node is backtracked to and is about to
418
+ be mapped to a different node for the first time, its child in the DFS has been
419
+ completely analysed. Thus the orbit for that child at this point in the DFS is
420
+ the full orbit for symmetries involving only that child or larger nodes in the
421
+ node order. All smaller nodes are mapped to themselves.
422
+ This orbit is due to symmetries not involving smaller nodes. Such an orbit is
423
+ called the "coset" of that node. The Coset Point does not lead to pruning or to
424
+ more symmetries. It is the point in the process where we store the **coset** of
425
+ the node being backtracked. We use the cosets to construct the symmetry
426
+ constraints.
427
+
428
+ Once the pruned DFS tree has been traversed, we have collected the cosets of some
429
+ special nodes. Often most nodes are not coupled during the progression down the left
430
+ side of the DFS. They are separated from other nodes during the partition refinement
431
+ process after coupling. So they never get coupled directly. Thus the number of cosets
432
+ we find is typically many fewer than the number of nodes.
433
+
434
+ We turn those cosets into constraints on the nodes when building non-symmetric
435
+ isomorphisms. The node whose coset is used is paired with each other node in the
436
+ coset. These node-pairs form the constraints. During isomorphism construction we
437
+ always select the first of the constraint before the other. This removes subtrees
438
+ from the DFS traversal space used to build isomorphisms.
439
+
440
+ The constraints we obtain via symmetry analysis of the subgraph are used for
441
+ finding non-symmetric isomorphisms. We prune the isomorphism tree based on
442
+ the constraints we obtain from the symmetry analysis.
443
+
444
+ **Isomorphism Construction**
445
+
446
+ Once we have symmetry constraints on the isomorphisms, ISMAGS constructs the allowed
447
+ isomorphisms by mapping each node of the subgraph to all possible nodes (with the
448
+ same degree-by-color) from the graph. We partition all nodes into degree-by-color
449
+ parts and order the subgraph nodes we consider using smallest part size first.
450
+ The idea is to try to map the most difficult subgraph nodes first (most difficult
451
+ here means least number of graph candidates).
452
+
453
+ By considering each potential subgraph node to graph candidate mapping image in turn,
454
+ we perform a DFS traversal of partial mappings. If the mapping is rejected due to
455
+ the graph neighbors not matching the degree-by-color of the subgraph neighbors, or
456
+ rejected due to the constraints imposed from symmetry, we prune that subtree and
457
+ consider a new graph candidate node for that subgraph node. When no more graph
458
+ candidates remain we backtrack to the previous node in the mapping and consider a
459
+ new graph candidate for that node. If we ever get to a depth where all subgraph nodes
460
+ are mapped and no structural requirements or symmetry constraints are violated,
461
+ we have found an isomorphism. We yield that mapping and backtrack to find other
462
+ isomorphisms.
463
+
464
+ As we visit more neighbors, the graph candidate nodes have to satisfy more structural
465
+ restrictions. As described in the ISMAGS paper, [1]_, we store each set of structural
466
+ restrictions separately as a set of possible candidate nodes rather than computing
467
+ the intersection of that set with the known graph candidates for the subgraph node.
468
+ We delay taking the intersection until that node is selected to be in the mapping.
469
+ While choosing the node with fewest candidates, we avoid computing the intersection
470
+ by using the size of the minimal set to be intersected rather than the size of the
471
+ intersection. This may make the node ordering slightly worse via a savings of
472
+ many intersections most of which are not ever needed.
473
+
474
+ References
475
+ ----------
476
+ .. [1] M. Houbraken, S. Demeyer, T. Michoel, P. Audenaert, D. Colle,
477
+ M. Pickavet, "The Index-Based Subgraph Matching Algorithm with General
478
+ Symmetries (ISMAGS): Exploiting Symmetry for Faster Subgraph
479
+ Enumeration", PLoS One 9(5): e97896, 2014.
480
+ https://doi.org/10.1371/journal.pone.0097896
481
+ .. [2] https://en.wikipedia.org/wiki/Maximum_common_induced_subgraph
482
+ .. [3] Hadi Katebi, Karem A. Sakallah and Igor L. Markov
483
+ "Graph Symmetry Detection and Canonical Labeling: Differences and Synergies"
484
+ in "Turing-100. The Alan Turing Centenary" Ed: A. Voronkov p. 181 -- 195, (2012).
485
+ https://doi.org/10.29007/gzc1 https://arxiv.org/abs/1208.6271
486
+ """
487
+
488
+ def __init__(self, graph, subgraph, node_match=None, edge_match=None, cache=None):
489
+ """
490
+ Parameters
491
+ ----------
492
+ graph: networkx.Graph
493
+ subgraph: networkx.Graph
494
+ node_match: collections.abc.Callable or None
495
+ Function used to determine whether two nodes are equivalent. Its
496
+ signature should look like ``f(n1: dict, n2: dict) -> bool``, with
497
+ `n1` and `n2` node property dicts. See also
498
+ :func:`~networkx.algorithms.isomorphism.categorical_node_match` and
499
+ friends.
500
+ If `None`, all nodes are considered equal.
501
+ edge_match: collections.abc.Callable or None
502
+ Function used to determine whether two edges are equivalent. Its
503
+ signature should look like ``f(e1: dict, e2: dict) -> bool``, with
504
+ `e1` and `e2` edge property dicts. See also
505
+ :func:`~networkx.algorithms.isomorphism.categorical_edge_match` and
506
+ friends.
507
+ If `None`, all edges are considered equal.
508
+ cache: collections.abc.Mapping
509
+ A cache used for caching graph symmetries.
510
+ """
511
+ if graph.is_directed() != subgraph.is_directed():
512
+ raise ValueError("Directed and undirected graphs cannot be compared.")
513
+
514
+ # TODO: allow for precomputed partitions and colors
515
+ self.graph = graph
516
+ self.subgraph = subgraph
517
+ self._symmetry_cache = cache
518
+ # Naming conventions are taken from the original paper.
519
+ # For your sanity:
520
+ # sg: subgraph
521
+ # g: graph
522
+ # e: edge(s)
523
+ # n: node(s)
524
+ # So: sgn means "subgraph nodes".
525
+ node_parts = self.create_aligned_partitions(
526
+ node_match, self.subgraph.nodes, self.graph.nodes
527
+ )
528
+ self._sgn_partition, self._gn_partition, self.N_node_colors = node_parts
529
+ self._sgn_colors = node_to_part_ID_dict(self._sgn_partition)
530
+ self._gn_colors = node_to_part_ID_dict(self._gn_partition)
531
+
532
+ edge_partitions = self.create_aligned_partitions(
533
+ edge_match, self.subgraph.edges(), self.graph.edges()
534
+ )
535
+ self._sge_partition, self._ge_partition, self.N_edge_colors = edge_partitions
536
+ if self.graph.is_directed():
537
+ self._sge_colors = node_to_part_ID_dict(self._sge_partition)
538
+ self._ge_colors = node_to_part_ID_dict(self._ge_partition)
539
+ else: # allow lookups (u, v) or (v, u)
540
+ self._sge_colors = EdgeLookup(node_to_part_ID_dict(self._sge_partition))
541
+ self._ge_colors = EdgeLookup(node_to_part_ID_dict(self._ge_partition))
542
+
543
+ def create_aligned_partitions(self, thing_matcher, sg_things, g_things):
544
+ """Partitions of "things" (nodes or edges) from subgraph and graph
545
+ based on function `thing_matcher`.
546
+
547
+ Returns: sg_partition, g_partition, number_of_matched_parts
548
+
549
+ The first `number_of_matched_parts` parts in each partition
550
+ match in order, e.g. 2nd part matches other's 2nd part.
551
+ Warning: nodes in parts after that have no matching nodes in the other graph.
552
+ For morphisms those nodes can't appear in the mapping.
553
+ """
554
+ if thing_matcher is None:
555
+ sg_partition = [set(sg_things)]
556
+ g_partition = [set(g_things)]
557
+ return sg_partition, g_partition, 1
558
+
559
+ # Use thing_matcher to create a partition
560
+ # Note: isinstance(G.edges(), OutEdgeDataView) is only true for multi(di)graph
561
+ sg_multiedge = isinstance(sg_things, nx.classes.reportviews.OutEdgeDataView)
562
+ g_multiedge = isinstance(g_things, nx.classes.reportviews.OutEdgeDataView)
563
+ if not sg_multiedge:
564
+
565
+ def sg_match(thing1, thing2):
566
+ return thing_matcher(sg_things[thing1], sg_things[thing2])
567
+
568
+ else: # multiedges (note nodes of multigraphs use simple case above)
569
+
570
+ def sg_match(thing1, thing2):
571
+ (u1, v1), (u2, v2) = thing1, thing2
572
+ return thing_matcher(self.subgraph[u1][v1], self.subgraph[u2][v2])
573
+
574
+ if not g_multiedge:
575
+
576
+ def g_match(thing1, thing2):
577
+ return thing_matcher(g_things[thing1], g_things[thing2])
578
+
579
+ else: # multiedges (note nodes of multigraphs use simple case above)
580
+
581
+ def g_match(thing1, thing2):
582
+ (u1, v1), (u2, v2) = thing1, thing2
583
+ return thing_matcher(self.graph[u1][v1], self.graph[u2][v2])
584
+
585
+ sg_partition = make_partition(sg_things, sg_match)
586
+ g_partition = make_partition(g_things, g_match)
587
+
588
+ # Align order of g_partition to that of sg_partition
589
+ sgc_to_gc = {}
590
+ gc_to_sgc = {}
591
+ sN, N = len(sg_partition), len(g_partition)
592
+ for sgc, gc in itertools.product(range(sN), range(N)):
593
+ sgt = next(iter(sg_partition[sgc]))
594
+ gt = next(iter(g_partition[gc]))
595
+ sgt_ = sg_things[sgt] if not sg_multiedge else self.subgraph[sgt[0]][sgt[1]]
596
+ gt_ = g_things[gt] if not g_multiedge else self.graph[gt[0]][gt[1]]
597
+ if thing_matcher(sgt_, gt_):
598
+ # TODO: remove these two if-checks when confident they never arise
599
+ # The `check` feature in match_partitions should ensure they do not
600
+ if sgc in sgc_to_gc:
601
+ raise nx.NetworkXError(
602
+ f"\nMatching function {thing_matcher} seems faulty.\n"
603
+ f"Partition found: {sg_partition=}\n"
604
+ f"So {sgt} in subgraph part {sg_partition[sgc]} matches two "
605
+ f"graph parts {g_partition[gc]} and "
606
+ f"{g_partition[sgc_to_gc[sgc]]}\n"
607
+ )
608
+ if gc in gc_to_sgc:
609
+ raise nx.NetworkXError(
610
+ f"\nMatching function seems broken: {thing_matcher}\n"
611
+ f"Partitions found: {g_partition=} {sg_partition=}\n"
612
+ f"So {gt} in graph part {g_partition[gc]} matches two "
613
+ f"subgraph parts {sg_partition[sgc]} and "
614
+ f"{sg_partition[gc_to_sgc[gc]]}\n"
615
+ )
616
+ sgc_to_gc[sgc] = gc
617
+ gc_to_sgc[gc] = sgc
618
+ ## return two lists and the number of partitions that match.
619
+ new_order = [
620
+ (sg_partition[sgc], g_partition[gc]) for sgc, gc in sgc_to_gc.items()
621
+ ]
622
+ Ncolors = len(new_order)
623
+ if Ncolors:
624
+ new_sg_p, new_g_p = [list(x) for x in zip(*new_order)]
625
+ else:
626
+ new_sg_p, new_g_p = [], []
627
+ if Ncolors < sN:
628
+ extra = [sg_partition[c] for c in range(sN) if c not in sgc_to_gc]
629
+ new_sg_p = list(new_sg_p) + extra
630
+ new_g_p = list(new_g_p) + [set()] * len(extra)
631
+ if Ncolors < N:
632
+ extra = [g_partition[c] for c in range(N) if c not in gc_to_sgc]
633
+ new_g_p = list(new_g_p) + extra
634
+ new_sg_p = list(new_sg_p) + [set()] * len(extra)
635
+
636
+ return new_sg_p, new_g_p, Ncolors
637
+
638
+ def find_isomorphisms(self, symmetry=True):
639
+ """Find all subgraph isomorphisms between subgraph and graph
640
+
641
+ Finds isomorphisms where :attr:`subgraph` <= :attr:`graph`.
642
+
643
+ Parameters
644
+ ----------
645
+ symmetry: bool
646
+ Whether symmetry should be taken into account. If False, found
647
+ isomorphisms may be symmetrically equivalent.
648
+
649
+ Yields
650
+ ------
651
+ dict
652
+ The found isomorphism mappings of {graph_node: subgraph_node}.
653
+ """
654
+ # The networkx VF2 algorithm is slightly funny in when it yields an
655
+ # empty dict and when not.
656
+ if not self.subgraph:
657
+ yield {}
658
+ return
659
+ elif not self.graph:
660
+ return
661
+ elif len(self.graph) < len(self.subgraph):
662
+ return
663
+ elif len(self._sgn_partition) > self.N_node_colors:
664
+ # some subgraph nodes have a color that doesn't occur in graph
665
+ return
666
+ elif len(self._sge_partition) > self.N_edge_colors:
667
+ # some subgraph edges have a color that doesn't occur in graph
668
+ return
669
+
670
+ if symmetry:
671
+ cosets = self.analyze_subgraph_symmetry()
672
+ # Turn cosets into constraints.
673
+ constraints = [(n, co) for n, cs in cosets.items() for co in cs if n != co]
674
+ else:
675
+ constraints = []
676
+
677
+ cand_sets = self._get_node_color_candidate_sets()
678
+
679
+ lookahead_candidates = self._get_color_degree_candidates()
680
+ for sgn, lookahead_cands in lookahead_candidates.items():
681
+ cand_sets[sgn].add(frozenset(lookahead_cands))
682
+
683
+ if any(cand_sets.values()):
684
+ # Choose start node based on a heuristic for the min # of candidates
685
+ # Heuristic here is length of smallest frozenset in candidates' set
686
+ # of frozensets for that node. Using the smallest length avoids
687
+ # computing the intersection of the frozensets for each node.
688
+ start_sgn = min(cand_sets, key=lambda n: min(len(x) for x in cand_sets[n]))
689
+ cand_sets[start_sgn] = (frozenset.intersection(*cand_sets[start_sgn]),)
690
+ yield from self._map_nodes(start_sgn, cand_sets, constraints)
691
+ return
692
+
693
+ def _get_color_degree_candidates(self):
694
+ """
695
+ Returns a mapping of {subgraph node: set of graph nodes} for
696
+ which the graph nodes are feasible mapping candidate_sets for the
697
+ subgraph node, as determined by looking ahead one edge.
698
+ """
699
+ g_deg = color_degree_by_node(self.graph, self._gn_colors, self._ge_colors)
700
+ sg_deg = color_degree_by_node(self.subgraph, self._sgn_colors, self._sge_colors)
701
+
702
+ return {
703
+ sgn: {
704
+ gn
705
+ for gn, (_, *g_counts) in g_deg.items()
706
+ if all(
707
+ sg_cnt <= g_counts[idx][color]
708
+ for idx, counts in enumerate(needed_counts)
709
+ for color, sg_cnt in counts.items()
710
+ )
711
+ }
712
+ for sgn, (_, *needed_counts) in sg_deg.items()
713
+ }
714
+
715
+ def largest_common_subgraph(self, symmetry=True):
716
+ """
717
+ Find the largest common induced subgraphs between :attr:`subgraph` and
718
+ :attr:`graph`.
719
+
720
+ Parameters
721
+ ----------
722
+ symmetry: bool
723
+ Whether symmetry should be taken into account. If False, found
724
+ largest common subgraphs may be symmetrically equivalent.
725
+
726
+ Yields
727
+ ------
728
+ dict
729
+ The found isomorphism mappings of {graph_node: subgraph_node}.
730
+ """
731
+ # The networkx VF2 algorithm is slightly funny in when it yields an
732
+ # empty dict and when not.
733
+ if not self.subgraph:
734
+ yield {}
735
+ return
736
+ elif not self.graph:
737
+ return
738
+
739
+ if symmetry:
740
+ cosets = self.analyze_subgraph_symmetry()
741
+ # Turn cosets into constraints.
742
+ constraints = [(n, cn) for n, cs in cosets.items() for cn in cs if n != cn]
743
+ else:
744
+ constraints = []
745
+
746
+ candidate_sets = self._get_node_color_candidate_sets()
747
+
748
+ if any(candidate_sets.values()):
749
+ relevant_parts = self._sgn_partition[: self.N_node_colors]
750
+ to_be_mapped = {frozenset(n for p in relevant_parts for n in p)}
751
+ yield from self._largest_common_subgraph(
752
+ candidate_sets, constraints, to_be_mapped
753
+ )
754
+ else:
755
+ return
756
+
757
+ def analyze_subgraph_symmetry(self):
758
+ """
759
+ Find a minimal set of permutations and corresponding co-sets that
760
+ describe the symmetry of ``self.subgraph``, given the node and edge
761
+ equalities given by `node_partition` and `edge_colors`, respectively.
762
+
763
+ Returns
764
+ -------
765
+ dict[collections.abc.Hashable, set[collections.abc.Hashable]]
766
+ The found co-sets. The co-sets is a dictionary of
767
+ ``{node key: set of node keys}``.
768
+ Every key-value pair describes which ``values`` can be interchanged
769
+ without changing nodes less than ``key``.
770
+ """
771
+ partition, edge_colors = self._sgn_partition, self._sge_colors
772
+
773
+ if self._symmetry_cache is not None:
774
+ key = hash(
775
+ (
776
+ tuple(self.subgraph.nodes),
777
+ tuple(self.subgraph.edges),
778
+ tuple(map(tuple, node_partition)),
779
+ tuple(edge_colors.items()),
780
+ self.subgraph.is_directed(),
781
+ )
782
+ )
783
+ if key in self._symmetry_cache:
784
+ return self._symmetry_cache[key]
785
+ partition = self._refine_node_partition(self.subgraph, partition, edge_colors)
786
+ cosets = self._process_ordered_pair_partitions(
787
+ self.subgraph, partition, partition, edge_colors
788
+ )
789
+ if self._symmetry_cache is not None:
790
+ self._symmetry_cache[key] = cosets
791
+ return cosets
792
+
793
+ def is_isomorphic(self, symmetry=False):
794
+ """
795
+ Returns True if :attr:`graph` is isomorphic to :attr:`subgraph` and
796
+ False otherwise.
797
+
798
+ Returns
799
+ -------
800
+ bool
801
+ """
802
+ return len(self.subgraph) == len(self.graph) and self.subgraph_is_isomorphic(
803
+ symmetry
804
+ )
805
+
806
+ def subgraph_is_isomorphic(self, symmetry=False):
807
+ """
808
+ Returns True if a subgraph of :attr:`graph` is isomorphic to
809
+ :attr:`subgraph` and False otherwise.
810
+
811
+ Returns
812
+ -------
813
+ bool
814
+ """
815
+ # symmetry=False, since we only need to know whether there is any
816
+ # example; figuring out all symmetry elements probably costs more time
817
+ # than it gains.
818
+ isom = next(self.subgraph_isomorphisms_iter(symmetry=symmetry), None)
819
+ return isom is not None
820
+
821
+ def isomorphisms_iter(self, symmetry=True):
822
+ """
823
+ Does the same as :meth:`find_isomorphisms` if :attr:`graph` and
824
+ :attr:`subgraph` have the same number of nodes.
825
+ """
826
+ if len(self.graph) == len(self.subgraph):
827
+ yield from self.subgraph_isomorphisms_iter(symmetry=symmetry)
828
+
829
+ def subgraph_isomorphisms_iter(self, symmetry=True):
830
+ """Alternative name for :meth:`find_isomorphisms`."""
831
+ return self.find_isomorphisms(symmetry)
832
+
833
+ def _get_node_color_candidate_sets(self):
834
+ """
835
+ Per node in subgraph find all nodes in graph that have the same color.
836
+ Stored as a dict-of-set-of-frozenset. The dict is keyed by node to a
837
+ collection of frozensets of graph nodes. Each of these frozensets are
838
+ a restriction. The node can be mapped only to nodes in the frozenset.
839
+ Thus it must be mapped to nodes in the intersection of all these sets.
840
+ We store the sets to delay taking the intersection of them. This helps
841
+ for two reasons: Firstly any duplicate restriction sets can be ignored;
842
+ Secondly, some nodes will not need the intersection to be constructed.
843
+ Note: a dict-of-list-of-set would store duplicate sets in the list and
844
+ we want to avoid that. But I wonder if checking hash/equality when `add`ing
845
+ removes the benefit of avoiding computing intersections.
846
+ """
847
+ candidate_sets = defaultdict(set)
848
+ for sgn in self.subgraph.nodes:
849
+ sgn_color = self._sgn_colors[sgn]
850
+ if sgn_color >= self.N_node_colors: # color has no candidates
851
+ candidate_sets[sgn] # creates empty set entry in defaultdict
852
+ else:
853
+ candidate_sets[sgn].add(frozenset(self._gn_partition[sgn_color]))
854
+ return dict(candidate_sets)
855
+
856
+ @classmethod
857
+ def _refine_node_partition(cls, graph, partition, edge_colors):
858
+ def equal_color(node1, node2):
859
+ return color_degree[node1] == color_degree[node2]
860
+
861
+ node_colors = node_to_part_ID_dict(partition)
862
+ color_degree = color_degree_by_node(graph, node_colors, edge_colors)
863
+ while not all(are_all_equal(color_degree[n] for n in p) for p in partition):
864
+ partition = [
865
+ p
866
+ for part in partition
867
+ for p in (
868
+ [part]
869
+ if are_all_equal(color_degree[n] for n in part)
870
+ else sorted(make_partition(part, equal_color, check=False), key=len)
871
+ )
872
+ ]
873
+ node_colors = node_to_part_ID_dict(partition)
874
+ color_degree = color_degree_by_node(graph, node_colors, edge_colors)
875
+ return partition
876
+
877
+ def _map_nodes(self, sgn, candidate_sets, constraints, to_be_mapped=None):
878
+ """
879
+ Find all subgraph isomorphisms honoring constraints.
880
+ The collection `candidate_sets` is stored as a dict-of-set-of-frozenset.
881
+ The dict is keyed by node to a collection of candidate frozensets. Any
882
+ viable candidate must belong to all the frozensets in the collection.
883
+ So each frozenset added to the collection is a restriction on the candidates.
884
+
885
+ According to the paper, we store the collection of sets rather than their
886
+ intersection to delay computing many intersections with the hope of avoiding
887
+ them completely. Having the middle collection be a set also means that
888
+ duplicate restrictions on candidates are ignored, avoiding another intersection.
889
+ """
890
+ # shortcuts for speed
891
+ subgraph = self.subgraph
892
+ subgraph_adj = subgraph._adj
893
+ graph = self.graph
894
+ graph_adj = graph._adj
895
+ self_ge_partition = self._ge_partition
896
+ self_sge_colors = self._sge_colors
897
+ is_directed = subgraph.is_directed()
898
+
899
+ gn_ID_to_node = list(graph)
900
+ gn_node_to_ID = {n: id for id, n in enumerate(graph)}
901
+
902
+ mapping = {}
903
+ rev_mapping = {}
904
+ if to_be_mapped is None:
905
+ to_be_mapped = subgraph_adj.keys()
906
+
907
+ # Note that we don't copy candidates here. This means we leak
908
+ # information between the branches of the search. This is intentional!
909
+ # Specifically, we modify candidates here. That's OK because we substitute
910
+ # the set of frozensets with a set containing the frozenset intersection.
911
+ # So, it doesn't change the membership rule or the length rule for sorting.
912
+ # Membership: any candidate must be an element of each of the frozensets.
913
+ # Length: length of the intersection set. Use heuristic min(len of frozensets).
914
+ # This intersection improves future length heuristics which can only occur
915
+ # after this element of the queu is popped. But it means future additional
916
+ # restriction frozensets that duplicate previous ones are not ignored.
917
+ sgn_candidates = frozenset.intersection(*candidate_sets[sgn])
918
+ candidate_sets[sgn] = {sgn_candidates}
919
+ queue = [(sgn, candidate_sets, iter(sgn_candidates))]
920
+ while queue: # DFS over all possible mappings
921
+ sgn, candidate_sets, sgn_cand_iter = queue[-1]
922
+
923
+ for gn in sgn_cand_iter:
924
+ # We're going to try to map sgn to gn.
925
+ if gn in rev_mapping:
926
+ continue # pragma: no cover
927
+
928
+ # REDUCTION and COMBINATION
929
+ if sgn in mapping:
930
+ old_gn = mapping[sgn]
931
+ del rev_mapping[old_gn]
932
+ mapping[sgn] = gn
933
+ rev_mapping[gn] = sgn
934
+ # BASECASE
935
+ if len(mapping) == len(to_be_mapped):
936
+ yield rev_mapping.copy()
937
+ del mapping[sgn]
938
+ del rev_mapping[gn]
939
+ continue
940
+ left_to_map = to_be_mapped - mapping.keys()
941
+
942
+ # We copy the candidates dict. But it is not a deepcopy.
943
+ # This avoids inner set copies, yet still allows updates b/c setitem
944
+ # changes sgn in new dict without changing original set.
945
+ # Below be careful to not change the sets of frozensets.
946
+ cand_sets = candidate_sets.copy()
947
+
948
+ # update the candidate_sets for unmapped sgn based on sgn mapped
949
+ if not is_directed:
950
+ sgn_nbrs = subgraph_adj[sgn]
951
+ not_gn_nbrs = graph_adj.keys() - graph_adj[gn].keys()
952
+ for sgn2 in left_to_map:
953
+ # edge color must match when sgn2 connected to sgn
954
+ if sgn2 not in sgn_nbrs:
955
+ gn2_cands = not_gn_nbrs
956
+ else:
957
+ g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]]
958
+ gn2_cands = {n for e in g_edges if gn in e for n in e}
959
+ # Node color compatibility should be taken care of by the
960
+ # initial candidate lists made by find_subgraphs
961
+
962
+ # Add gn2_cands to the right collection.
963
+ # Do not change the original set. So do not use |= operator
964
+ cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)}
965
+ else: # directed
966
+ sgn_nbrs = subgraph_adj[sgn].keys()
967
+ sgn_preds = subgraph._pred[sgn].keys()
968
+ not_gn_nbrs = (
969
+ graph_adj.keys() - graph_adj[gn].keys() - graph._pred[gn].keys()
970
+ )
971
+ for sgn2 in left_to_map:
972
+ # edge color must match when sgn2 connected to sgn
973
+ if sgn2 not in sgn_nbrs:
974
+ if sgn2 not in sgn_preds:
975
+ gn2_cands = not_gn_nbrs
976
+ else: # sgn2 in sgn_preds
977
+ g_edges = self_ge_partition[self_sge_colors[sgn2, sgn]]
978
+ gn2_cands = {e[0] for e in g_edges if gn == e[1]}
979
+ else:
980
+ if sgn2 not in sgn_preds:
981
+ g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]]
982
+ gn2_cands = {e[1] for e in g_edges if gn == e[0]}
983
+ else:
984
+ # gn2 must have correct color in both directions
985
+ g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]]
986
+ gn2_cands = {e[1] for e in g_edges if gn == e[0]}
987
+ g_edges = self_ge_partition[self_sge_colors[sgn2, sgn]]
988
+ gn2_cands &= {e[0] for e in g_edges if gn == e[1]}
989
+ # Do not change the original set. So do not use |= operator
990
+ cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)}
991
+
992
+ for sgn2 in left_to_map:
993
+ # symmetry must match. constraints mean gn2>gn iff sgn2>sgn
994
+ if (sgn, sgn2) in constraints:
995
+ gn2_cands = set(gn_ID_to_node[gn_node_to_ID[gn] + 1 :])
996
+ elif (sgn2, sgn) in constraints:
997
+ gn2_cands = set(gn_ID_to_node[: gn_node_to_ID[gn]])
998
+ else:
999
+ continue # pragma: no cover
1000
+ # Do not change the original set. So do not use |= operator
1001
+ cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)}
1002
+
1003
+ # The next node is the one that is unmapped and has fewest candidates
1004
+ # Use the heuristic of the min size of the frozensets rather than
1005
+ # intersection of all frozensets to delay computing intersections.
1006
+ new_sgn = min(
1007
+ left_to_map, key=lambda n: min(len(x) for x in cand_sets[n])
1008
+ )
1009
+ new_sgn_candidates = frozenset.intersection(*cand_sets[new_sgn])
1010
+ if not new_sgn_candidates:
1011
+ continue
1012
+ cand_sets[new_sgn] = {new_sgn_candidates}
1013
+ queue.append((new_sgn, cand_sets, iter(new_sgn_candidates)))
1014
+ break
1015
+ else: # all gn candidates tried for sgn.
1016
+ queue.pop()
1017
+ if sgn in mapping:
1018
+ del rev_mapping[mapping[sgn]]
1019
+ del mapping[sgn]
1020
+
1021
+ def _largest_common_subgraph(self, candidates, constraints, to_be_mapped=None):
1022
+ """
1023
+ Find all largest common subgraphs honoring constraints.
1024
+ """
1025
+ # to_be_mapped is a set of frozensets of subgraph nodes
1026
+ if to_be_mapped is None:
1027
+ to_be_mapped = {frozenset(self.subgraph.nodes)}
1028
+
1029
+ # The LCS problem is basically a repeated subgraph isomorphism problem
1030
+ # with smaller and smaller subgraphs. We store the nodes that are
1031
+ # "part of" the subgraph in to_be_mapped, and we make it a little
1032
+ # smaller every iteration.
1033
+
1034
+ current_size = len(next(iter(to_be_mapped), []))
1035
+
1036
+ found_iso = False
1037
+ if current_size <= len(self.graph):
1038
+ # There's no point in trying to find isomorphisms of
1039
+ # graph >= subgraph if subgraph has more nodes than graph.
1040
+
1041
+ # Try the isomorphism first with the nodes with lowest ID. So sort
1042
+ # them. Those are more likely to be part of the final correspondence.
1043
+ # In theory, this makes finding the first answer(s) faster.
1044
+ for nodes in sorted(to_be_mapped, key=sorted):
1045
+ # Find the isomorphism between subgraph[to_be_mapped] <= graph
1046
+ next_sgn = min(nodes, key=lambda n: min(len(x) for x in candidates[n]))
1047
+ isomorphs = self._map_nodes(
1048
+ next_sgn, candidates, constraints, to_be_mapped=nodes
1049
+ )
1050
+
1051
+ # This is effectively `yield from isomorphs`, except that we look
1052
+ # whether an item was yielded.
1053
+ try:
1054
+ item = next(isomorphs)
1055
+ except StopIteration:
1056
+ pass
1057
+ else:
1058
+ yield item
1059
+ yield from isomorphs
1060
+ found_iso = True
1061
+
1062
+ # BASECASE
1063
+ if found_iso or current_size == 1:
1064
+ # Shrinking has no point because either 1) we end up with a smaller
1065
+ # common subgraph (and we want the largest), or 2) there'll be no
1066
+ # more subgraph.
1067
+ return
1068
+
1069
+ left_to_be_mapped = set()
1070
+ for nodes in to_be_mapped:
1071
+ for sgn in nodes:
1072
+ # We're going to remove sgn from to_be_mapped, but subject to
1073
+ # symmetry constraints. We know that for every constraint we
1074
+ # have those subgraph nodes are equal. So whenever we would
1075
+ # remove the lower part of a constraint, remove the higher
1076
+ # instead. This is all dealth with by _remove_node. And because
1077
+ # left_to_be_mapped is a set, we don't do double work.
1078
+
1079
+ # And finally, make the subgraph one node smaller.
1080
+ # REDUCTION
1081
+ new_nodes = self._remove_node(sgn, nodes, constraints)
1082
+ left_to_be_mapped.add(new_nodes)
1083
+ # COMBINATION
1084
+ yield from self._largest_common_subgraph(
1085
+ candidates, constraints, to_be_mapped=left_to_be_mapped
1086
+ )
1087
+
1088
+ @staticmethod
1089
+ def _remove_node(node, nodes, constraints):
1090
+ """
1091
+ Returns a new set where node has been removed from nodes, subject to
1092
+ symmetry constraints. We know, that for every constraint we have
1093
+ those subgraph nodes are equal. So whenever we would remove the
1094
+ lower part of a constraint, remove the higher instead.
1095
+ """
1096
+ while True:
1097
+ for low, high in constraints:
1098
+ if low == node and high in nodes:
1099
+ node = high
1100
+ break
1101
+ else: # no break, couldn't find node in constraints
1102
+ return frozenset(nodes - {node})
1103
+
1104
+ @staticmethod
1105
+ def _get_permutations_by_length(items):
1106
+ """
1107
+ Get all permutations of items, but only permute items with the same
1108
+ length.
1109
+
1110
+ >>> found = list(ISMAGS._get_permutations_by_length([{1}, {2}, {3, 4}, {4, 5}]))
1111
+ >>> answer = [
1112
+ ... (({1}, {2}), ({3, 4}, {4, 5})),
1113
+ ... (({1}, {2}), ({4, 5}, {3, 4})),
1114
+ ... (({2}, {1}), ({3, 4}, {4, 5})),
1115
+ ... (({2}, {1}), ({4, 5}, {3, 4})),
1116
+ ... ]
1117
+ >>> found == answer
1118
+ True
1119
+ """
1120
+ by_len = defaultdict(list)
1121
+ for item in items:
1122
+ by_len[len(item)].append(item)
1123
+
1124
+ return list(
1125
+ itertools.product(
1126
+ *(itertools.permutations(by_len[l]) for l in sorted(by_len))
1127
+ )
1128
+ )
1129
+
1130
+ def _refine_opp(cls, graph, top, bottom, edge_colors):
1131
+ def equal_color(node1, node2):
1132
+ return color_degree[node1] == color_degree[node2]
1133
+
1134
+ top = cls._refine_node_partition(graph, top, edge_colors)
1135
+
1136
+ possible_bottoms = [bottom]
1137
+ while possible_bottoms:
1138
+ bottom = possible_bottoms.pop()
1139
+ node_colors = node_to_part_ID_dict(bottom)
1140
+ color_degree = color_degree_by_node(graph, node_colors, edge_colors)
1141
+ if all(are_all_equal(color_degree[n] for n in p) for p in bottom):
1142
+ if len(top) == len(bottom):
1143
+ yield top, bottom
1144
+ # else Non-isomorphic OPP (pruned here)
1145
+ # either way continue to next possible bottom
1146
+ continue
1147
+ # refine bottom partition
1148
+ more_bottoms = [[]]
1149
+ for part in bottom:
1150
+ if len(part) == 1 or are_all_equal(color_degree[node] for node in part):
1151
+ for new_bottom in more_bottoms:
1152
+ new_bottom.append(part)
1153
+ else:
1154
+ # This part needs to be refined
1155
+ refined_part = make_partition(part, equal_color, check=False)
1156
+ R = len(refined_part)
1157
+ if R == 1 or R == len({len(p) for p in refined_part}):
1158
+ # no two parts have same length -- simple case
1159
+ for n_p in more_bottoms:
1160
+ n_p.extend(sorted(refined_part, key=len))
1161
+ else:
1162
+ # Any part might match any other part with the same size.
1163
+ # Before refinement they were the same color. So we need to
1164
+ # include all possible orderings/colors within each size.
1165
+ permutations = cls._get_permutations_by_length(refined_part)
1166
+ # Add all permutations of the refined parts to each possible
1167
+ # bottom. So the number of new possible bottoms is multiplied
1168
+ # by the number of permutations of the refined parts.
1169
+ new_partitions = []
1170
+ for new_partition in more_bottoms:
1171
+ for p in permutations:
1172
+ # p is tuple-of-tuples-of-sets. Flatten to list-of-sets
1173
+ flat_p = [s for tup in p for s in tup]
1174
+ new_partitions.append(new_partition + flat_p)
1175
+ more_bottoms = new_partitions
1176
+
1177
+ # reverse more_bottoms to keep the "finding identity" bottom first
1178
+ possible_bottoms.extend(more_bottoms[::-1])
1179
+
1180
+ @staticmethod
1181
+ def _find_permutations(top_partition, bottom_partition):
1182
+ """
1183
+ Return a set of 2-tuples of nodes. These nodes are not equal
1184
+ but are mapped to each other in the symmetry represented by this OPP.
1185
+ Swapping all the 2-tuples of nodes in this set permutes the nodes
1186
+ but retains the graph structure. Thus it is a symmetry of the subgraph.
1187
+ """
1188
+ # Find permutations
1189
+ permutations = set()
1190
+ for top, bot in zip(top_partition, bottom_partition):
1191
+ if len(top) > 1 or len(bot) > 1:
1192
+ # ignore parts with > 1 element when they are equal
1193
+ # These are called Matching OPPs in Katebi 2012.
1194
+ # Symmetries in matching partitions are built by considering
1195
+ # only parts that have 1 element.
1196
+ if top == bot:
1197
+ continue
1198
+ raise IndexError(
1199
+ "Not all nodes are matched. This is"
1200
+ f" impossible: {top_partition}, {bottom_partition}"
1201
+ )
1202
+ # top and bot have only one element
1203
+ elif top != bot:
1204
+ permutations.add(frozenset((next(iter(top)), next(iter(bot)))))
1205
+ return permutations
1206
+
1207
+ def _process_ordered_pair_partitions(
1208
+ self,
1209
+ graph,
1210
+ top_partition,
1211
+ bottom_partition,
1212
+ edge_colors,
1213
+ ):
1214
+ if all(len(top) <= 1 for top in top_partition):
1215
+ # no symmetries. Each node unique.
1216
+ return {}
1217
+
1218
+ # first mapping found is the identity mapping
1219
+ finding_identity = True
1220
+
1221
+ orbit_id = {node: orbit_i for orbit_i, node in enumerate(graph)}
1222
+ orbits = [{node} for node in graph]
1223
+ cosets = {}
1224
+
1225
+ node_to_ID = {n: i for i, n in enumerate(graph)}
1226
+ sort_by_ID = node_to_ID.__getitem__
1227
+
1228
+ def _load_next_queue_entry(queue, top_partition, bottom_partition):
1229
+ # find smallest node (by ID) in a |part|>1 and its partition index
1230
+ unmapped_nodes = (
1231
+ (node_to_ID[node], node, idx)
1232
+ for idx, t_part in enumerate(top_partition)
1233
+ for node in t_part
1234
+ if len(t_part) > 1
1235
+ )
1236
+ _, node, part_i = min(unmapped_nodes)
1237
+ b_part = bottom_partition[part_i]
1238
+ node2_iter = iter(sorted(b_part, key=sort_by_ID))
1239
+
1240
+ queue.append([top_partition, bottom_partition, node, part_i, node2_iter])
1241
+
1242
+ queue = []
1243
+ _load_next_queue_entry(queue, top_partition, bottom_partition)
1244
+
1245
+ while queue:
1246
+ tops, bottoms, node, part_i, node2_iter = queue[-1]
1247
+
1248
+ for node2 in node2_iter:
1249
+ if node != node2 and orbit_id[node] == orbit_id[node2]:
1250
+ # Orbit prune
1251
+ continue
1252
+
1253
+ # couple node to node2
1254
+ new_top_part = {node}
1255
+ new_bot_part = {node2}
1256
+
1257
+ new_top = [top.copy() for top in tops]
1258
+ new_top[part_i] -= new_top_part
1259
+ new_top.insert(part_i, new_top_part)
1260
+
1261
+ new_bot = [bot.copy() for bot in bottoms]
1262
+ new_bot[part_i] -= new_bot_part
1263
+ new_bot.insert(part_i, new_bot_part)
1264
+
1265
+ # collect OPPs
1266
+ opps = self._refine_opp(graph, new_top, new_bot, edge_colors)
1267
+ new_q = []
1268
+ for opp in opps:
1269
+ # Use OPP to find any of: Identity, Automorphism or Matching OPPs
1270
+ # else load the OPP onto queue for further exploration
1271
+ # Note that we check for Orbit pruning later because orbits may
1272
+ # be updated while OPP is sitting on the queue.
1273
+ # Note that we check for Non-isomorphic OPPs in `_refine_opp`.
1274
+ if finding_identity:
1275
+ # Note: allow zero size parts in identity check
1276
+ # b/c largest_common_subgraph allows empty parts
1277
+ if all(len(top) <= 1 for top in opp[0]):
1278
+ # Identity found. Set flag. Can now prune Matching OPPs
1279
+ finding_identity = False
1280
+ continue
1281
+ elif all(len(t) <= 1 or t == b for t, b in zip(*opp)):
1282
+ # Found a symmetry! (Full mapping or Matching OPP)
1283
+ # update orbits using the permutations from the OPP.
1284
+ permutations = self._find_permutations(*opp)
1285
+ for n1, n2 in permutations:
1286
+ orb1 = orbit_id[n1]
1287
+ orb2 = orbit_id[n2]
1288
+ if orb1 != orb2:
1289
+ orbit_set2 = orbits[orb2]
1290
+ orbits[orb1].update(orbit_set2)
1291
+ orbits[orb2] = set()
1292
+ orbit_id.update((n, orb1) for n in orbit_set2)
1293
+ continue
1294
+
1295
+ _load_next_queue_entry(new_q, *opp)
1296
+ # reverse order to maintain node order DFS (Identity comes first)
1297
+ queue.extend(new_q[::-1])
1298
+ break
1299
+ else: # no more node2 options
1300
+ queue.pop()
1301
+ if node not in cosets:
1302
+ # coset of `node` is its orbit at the time `node` has completed
1303
+ # its first DFS traversal. DFS is about to go to previous node.
1304
+ # Make copy so future orbit changes do not change the coset.
1305
+ cosets[node] = orbits[orbit_id[node]].copy()
1306
+ return cosets
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/isomorph.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Graph isomorphism functions.
3
+ """
4
+
5
+ import itertools
6
+ from collections import Counter
7
+
8
+ import networkx as nx
9
+ from networkx.exception import NetworkXError
10
+
11
+ __all__ = [
12
+ "could_be_isomorphic",
13
+ "fast_could_be_isomorphic",
14
+ "faster_could_be_isomorphic",
15
+ "is_isomorphic",
16
+ ]
17
+
18
+
19
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1})
20
+ def could_be_isomorphic(G1, G2, *, properties="dtc"):
21
+ """Returns False if graphs are definitely not isomorphic.
22
+ True does NOT guarantee isomorphism.
23
+
24
+ Parameters
25
+ ----------
26
+ G1, G2 : graphs
27
+ The two graphs `G1` and `G2` must be the same type.
28
+
29
+ properties : str, default="dct"
30
+ Determines which properties of the graph are checked. Each character
31
+ indicates a particular property as follows:
32
+
33
+ - if ``"d"`` in `properties`: degree of each node
34
+ - if ``"t"`` in `properties`: number of triangles for each node
35
+ - if ``"c"`` in `properties`: number of maximal cliques for each node
36
+
37
+ Unrecognized characters are ignored. The default is ``"dtc"``, which
38
+ compares the sequence of ``(degree, num_triangles, num_cliques)`` properties
39
+ between `G1` and `G2`. Generally, ``properties="dt"`` would be faster, and
40
+ ``properties="d"`` faster still. See Notes for additional details on
41
+ property selection.
42
+
43
+ Returns
44
+ -------
45
+ bool
46
+ A Boolean value representing whether `G1` could be isomorphic with `G2`
47
+ according to the specified `properties`.
48
+
49
+ Notes
50
+ -----
51
+ The triangle sequence contains the number of triangles each node is part of.
52
+ The clique sequence contains for each node the number of maximal cliques
53
+ involving that node.
54
+
55
+ Some properties are faster to compute than others. And there are other
56
+ properties we could include and don't. But of the three properties listed here,
57
+ comparing the degree distributions is the fastest. The "triangles" property
58
+ is slower (and also a stricter version of "could") and the "maximal cliques"
59
+ property is slower still, but usually faster than doing a full isomorphism
60
+ check.
61
+ """
62
+
63
+ # Check global properties
64
+ if G1.order() != G2.order():
65
+ return False
66
+
67
+ properties_to_check = set(properties)
68
+ G1_props, G2_props = [], []
69
+
70
+ def _properties_consistent():
71
+ # Ravel the properties into a table with # nodes rows and # properties columns
72
+ G1_ptable = [tuple(p[n] for p in G1_props) for n in G1]
73
+ G2_ptable = [tuple(p[n] for p in G2_props) for n in G2]
74
+
75
+ return sorted(G1_ptable) == sorted(G2_ptable)
76
+
77
+ # The property table is built and checked as each individual property is
78
+ # added. The reason for this is the building/checking the property table
79
+ # is in general much faster than computing the properties, making it
80
+ # worthwhile to check multiple times to enable early termination when
81
+ # a subset of properties don't match
82
+
83
+ # Degree sequence
84
+ if "d" in properties_to_check:
85
+ G1_props.append(G1.degree())
86
+ G2_props.append(G2.degree())
87
+ if not _properties_consistent():
88
+ return False
89
+ # Sequence of triangles per node
90
+ if "t" in properties_to_check:
91
+ G1_props.append(nx.triangles(G1))
92
+ G2_props.append(nx.triangles(G2))
93
+ if not _properties_consistent():
94
+ return False
95
+ # Sequence of maximal cliques per node
96
+ if "c" in properties_to_check:
97
+ G1_props.append(Counter(itertools.chain.from_iterable(nx.find_cliques(G1))))
98
+ G2_props.append(Counter(itertools.chain.from_iterable(nx.find_cliques(G2))))
99
+ if not _properties_consistent():
100
+ return False
101
+
102
+ # All checked conditions passed
103
+ return True
104
+
105
+
106
+ def graph_could_be_isomorphic(G1, G2):
107
+ """
108
+ .. deprecated:: 3.5
109
+
110
+ `graph_could_be_isomorphic` is a deprecated alias for `could_be_isomorphic`.
111
+ Use `could_be_isomorphic` instead.
112
+ """
113
+ import warnings
114
+
115
+ warnings.warn(
116
+ "graph_could_be_isomorphic is deprecated, use `could_be_isomorphic` instead.",
117
+ category=DeprecationWarning,
118
+ stacklevel=2,
119
+ )
120
+ return could_be_isomorphic(G1, G2)
121
+
122
+
123
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1})
124
+ def fast_could_be_isomorphic(G1, G2):
125
+ """Returns False if graphs are definitely not isomorphic.
126
+
127
+ True does NOT guarantee isomorphism.
128
+
129
+ Parameters
130
+ ----------
131
+ G1, G2 : graphs
132
+ The two graphs G1 and G2 must be the same type.
133
+
134
+ Notes
135
+ -----
136
+ Checks for matching degree and triangle sequences. The triangle
137
+ sequence contains the number of triangles each node is part of.
138
+ """
139
+ # Check global properties
140
+ if G1.order() != G2.order():
141
+ return False
142
+
143
+ # Check local properties
144
+ d1 = G1.degree()
145
+ t1 = nx.triangles(G1)
146
+ props1 = [[d, t1[v]] for v, d in d1]
147
+ props1.sort()
148
+
149
+ d2 = G2.degree()
150
+ t2 = nx.triangles(G2)
151
+ props2 = [[d, t2[v]] for v, d in d2]
152
+ props2.sort()
153
+
154
+ if props1 != props2:
155
+ return False
156
+
157
+ # OK...
158
+ return True
159
+
160
+
161
+ def fast_graph_could_be_isomorphic(G1, G2):
162
+ """
163
+ .. deprecated:: 3.5
164
+
165
+ `fast_graph_could_be_isomorphic` is a deprecated alias for
166
+ `fast_could_be_isomorphic`. Use `fast_could_be_isomorphic` instead.
167
+ """
168
+ import warnings
169
+
170
+ warnings.warn(
171
+ "fast_graph_could_be_isomorphic is deprecated, use fast_could_be_isomorphic instead",
172
+ category=DeprecationWarning,
173
+ stacklevel=2,
174
+ )
175
+ return fast_could_be_isomorphic(G1, G2)
176
+
177
+
178
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1})
179
+ def faster_could_be_isomorphic(G1, G2):
180
+ """Returns False if graphs are definitely not isomorphic.
181
+
182
+ True does NOT guarantee isomorphism.
183
+
184
+ Parameters
185
+ ----------
186
+ G1, G2 : graphs
187
+ The two graphs G1 and G2 must be the same type.
188
+
189
+ Notes
190
+ -----
191
+ Checks for matching degree sequences.
192
+ """
193
+ # Check global properties
194
+ if G1.order() != G2.order():
195
+ return False
196
+
197
+ # Check local properties
198
+ d1 = sorted(d for n, d in G1.degree())
199
+ d2 = sorted(d for n, d in G2.degree())
200
+
201
+ if d1 != d2:
202
+ return False
203
+
204
+ # OK...
205
+ return True
206
+
207
+
208
+ def faster_graph_could_be_isomorphic(G1, G2):
209
+ """
210
+ .. deprecated:: 3.5
211
+
212
+ `faster_graph_could_be_isomorphic` is a deprecated alias for
213
+ `faster_could_be_isomorphic`. Use `faster_could_be_isomorphic` instead.
214
+ """
215
+ import warnings
216
+
217
+ warnings.warn(
218
+ "faster_graph_could_be_isomorphic is deprecated, use faster_could_be_isomorphic instead",
219
+ category=DeprecationWarning,
220
+ stacklevel=2,
221
+ )
222
+ return faster_could_be_isomorphic(G1, G2)
223
+
224
+
225
+ @nx._dispatchable(
226
+ graphs={"G1": 0, "G2": 1},
227
+ preserve_edge_attrs="edge_match",
228
+ preserve_node_attrs="node_match",
229
+ )
230
+ def is_isomorphic(G1, G2, node_match=None, edge_match=None):
231
+ """Returns True if the graphs G1 and G2 are isomorphic and False otherwise.
232
+
233
+ Parameters
234
+ ----------
235
+ G1, G2: graphs
236
+ The two graphs G1 and G2 must be the same type.
237
+
238
+ node_match : callable
239
+ A function that returns True if node n1 in G1 and n2 in G2 should
240
+ be considered equal during the isomorphism test.
241
+ If node_match is not specified then node attributes are not considered.
242
+
243
+ The function will be called like
244
+
245
+ node_match(G1.nodes[n1], G2.nodes[n2]).
246
+
247
+ That is, the function will receive the node attribute dictionaries
248
+ for n1 and n2 as inputs.
249
+
250
+ edge_match : callable
251
+ A function that returns True if the edge attribute dictionary
252
+ for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should
253
+ be considered equal during the isomorphism test. If edge_match is
254
+ not specified then edge attributes are not considered.
255
+
256
+ The function will be called like
257
+
258
+ edge_match(G1[u1][v1], G2[u2][v2]).
259
+
260
+ That is, the function will receive the edge attribute dictionaries
261
+ of the edges under consideration.
262
+
263
+ Notes
264
+ -----
265
+ Uses the vf2 algorithm [1]_.
266
+
267
+ Examples
268
+ --------
269
+ >>> import networkx.algorithms.isomorphism as iso
270
+
271
+ For digraphs G1 and G2, using 'weight' edge attribute (default: 1)
272
+
273
+ >>> G1 = nx.DiGraph()
274
+ >>> G2 = nx.DiGraph()
275
+ >>> nx.add_path(G1, [1, 2, 3, 4], weight=1)
276
+ >>> nx.add_path(G2, [10, 20, 30, 40], weight=2)
277
+ >>> em = iso.numerical_edge_match("weight", 1)
278
+ >>> nx.is_isomorphic(G1, G2) # no weights considered
279
+ True
280
+ >>> nx.is_isomorphic(G1, G2, edge_match=em) # match weights
281
+ False
282
+
283
+ For multidigraphs G1 and G2, using 'fill' node attribute (default: '')
284
+
285
+ >>> G1 = nx.MultiDiGraph()
286
+ >>> G2 = nx.MultiDiGraph()
287
+ >>> G1.add_nodes_from([1, 2, 3], fill="red")
288
+ >>> G2.add_nodes_from([10, 20, 30, 40], fill="red")
289
+ >>> nx.add_path(G1, [1, 2, 3, 4], weight=3, linewidth=2.5)
290
+ >>> nx.add_path(G2, [10, 20, 30, 40], weight=3)
291
+ >>> nm = iso.categorical_node_match("fill", "red")
292
+ >>> nx.is_isomorphic(G1, G2, node_match=nm)
293
+ True
294
+
295
+ For multidigraphs G1 and G2, using 'weight' edge attribute (default: 7)
296
+
297
+ >>> G1.add_edge(1, 2, weight=7)
298
+ 1
299
+ >>> G2.add_edge(10, 20)
300
+ 1
301
+ >>> em = iso.numerical_multiedge_match("weight", 7, rtol=1e-6)
302
+ >>> nx.is_isomorphic(G1, G2, edge_match=em)
303
+ True
304
+
305
+ For multigraphs G1 and G2, using 'weight' and 'linewidth' edge attributes
306
+ with default values 7 and 2.5. Also using 'fill' node attribute with
307
+ default value 'red'.
308
+
309
+ >>> em = iso.numerical_multiedge_match(["weight", "linewidth"], [7, 2.5])
310
+ >>> nm = iso.categorical_node_match("fill", "red")
311
+ >>> nx.is_isomorphic(G1, G2, edge_match=em, node_match=nm)
312
+ True
313
+
314
+ See Also
315
+ --------
316
+ numerical_node_match, numerical_edge_match, numerical_multiedge_match
317
+ categorical_node_match, categorical_edge_match, categorical_multiedge_match
318
+
319
+ References
320
+ ----------
321
+ .. [1] L. P. Cordella, P. Foggia, C. Sansone, M. Vento,
322
+ "An Improved Algorithm for Matching Large Graphs",
323
+ 3rd IAPR-TC15 Workshop on Graph-based Representations in
324
+ Pattern Recognition, Cuen, pp. 149-159, 2001.
325
+ https://www.researchgate.net/publication/200034365_An_Improved_Algorithm_for_Matching_Large_Graphs
326
+ """
327
+ if G1.is_directed() and G2.is_directed():
328
+ GM = nx.algorithms.isomorphism.DiGraphMatcher
329
+ elif (not G1.is_directed()) and (not G2.is_directed()):
330
+ GM = nx.algorithms.isomorphism.GraphMatcher
331
+ else:
332
+ raise NetworkXError("Graphs G1 and G2 are not of the same type.")
333
+
334
+ gm = GM(G1, G2, node_match=node_match, edge_match=edge_match)
335
+
336
+ return gm.is_isomorphic()
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/isomorphvf2.py ADDED
@@ -0,0 +1,1262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ *************
3
+ VF2 Algorithm
4
+ *************
5
+
6
+ An implementation of VF2 algorithm for graph isomorphism testing.
7
+
8
+ The simplest interface to use this module is to call the
9
+ :func:`is_isomorphic <networkx.algorithms.isomorphism.is_isomorphic>`
10
+ function.
11
+
12
+ Introduction
13
+ ------------
14
+
15
+ The GraphMatcher and DiGraphMatcher are responsible for matching
16
+ graphs or directed graphs in a predetermined manner. This
17
+ usually means a check for an isomorphism, though other checks
18
+ are also possible. For example, a subgraph of one graph
19
+ can be checked for isomorphism to a second graph.
20
+
21
+ Matching is done via syntactic feasibility. It is also possible
22
+ to check for semantic feasibility. Feasibility, then, is defined
23
+ as the logical AND of the two functions.
24
+
25
+ To include a semantic check, the (Di)GraphMatcher class should be
26
+ subclassed, and the
27
+ :meth:`semantic_feasibility <networkx.algorithms.isomorphism.GraphMatcher.semantic_feasibility>`
28
+ function should be redefined. By default, the semantic feasibility function always
29
+ returns ``True``. The effect of this is that semantics are not
30
+ considered in the matching of G1 and G2.
31
+
32
+ Examples
33
+ --------
34
+
35
+ Suppose 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()
42
+ True
43
+
44
+ GM.mapping stores the isomorphism mapping from G1 to G2.
45
+
46
+ >>> GM.mapping
47
+ {0: 0, 1: 1, 2: 2, 3: 3}
48
+
49
+
50
+ Suppose G1 and G2 are isomorphic directed graphs.
51
+ Verification 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()
57
+ True
58
+
59
+ DiGM.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
+
66
+ Subgraph Isomorphism
67
+ --------------------
68
+ Graph theory literature can be ambiguous about the meaning of the
69
+ above statement, and we seek to clarify it now.
70
+
71
+ In the VF2 literature, a mapping ``M`` is said to be a graph-subgraph
72
+ isomorphism iff ``M`` is an isomorphism between ``G2`` and a subgraph of ``G1``.
73
+ Thus, to say that ``G1`` and ``G2`` are graph-subgraph isomorphic is to say
74
+ that a subgraph of ``G1`` is isomorphic to ``G2``.
75
+
76
+ Other literature uses the phrase 'subgraph isomorphic' as in '``G1`` does
77
+ not have a subgraph isomorphic to ``G2``'. Another use is as an in adverb
78
+ for isomorphic. Thus, to say that ``G1`` and ``G2`` are subgraph isomorphic
79
+ is to say that a subgraph of ``G1`` is isomorphic to ``G2``.
80
+
81
+ Finally, the term 'subgraph' can have multiple meanings. In this
82
+ context, 'subgraph' always means a 'node-induced subgraph'. Edge-induced
83
+ subgraph isomorphisms are not directly supported, but one should be
84
+ able to perform the check by making use of
85
+ :func:`line_graph <networkx.generators.line.line_graph>`. For
86
+ subgraphs which are not induced, the term 'monomorphism' is preferred
87
+ over 'isomorphism'.
88
+
89
+ Let ``G = (N, E)`` be a graph with a set of nodes ``N`` and set of edges ``E``.
90
+
91
+ If ``G' = (N', E')`` is a subgraph, then:
92
+ ``N'`` is a subset of ``N`` and
93
+ ``E'`` is a subset of ``E``.
94
+
95
+ If ``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
+
99
+ If ``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
+
103
+ If ``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
+
107
+ Note that if ``G'`` is a node-induced subgraph of ``G``, then it is always a
108
+ subgraph monomorphism of ``G``, but the opposite is not always true, as a
109
+ monomorphism can have fewer edges.
110
+
111
+ References
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
+
125
+ See Also
126
+ --------
127
+ :meth:`semantic_feasibility <networkx.algorithms.isomorphism.GraphMatcher.semantic_feasibility>`
128
+ :meth:`syntactic_feasibility <networkx.algorithms.isomorphism.GraphMatcher.syntactic_feasibility>`
129
+
130
+ Notes
131
+ -----
132
+
133
+ The implementation handles both directed and undirected graphs as well
134
+ as multigraphs.
135
+
136
+ In general, the subgraph isomorphism problem is NP-complete whereas the
137
+ graph isomorphism problem is most likely not NP-complete (although no
138
+ polynomial-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
+
147
+ import sys
148
+
149
+ import networkx as nx
150
+
151
+ __all__ = ["GraphMatcher", "DiGraphMatcher"]
152
+
153
+
154
+ class 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
+
622
+ class 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
+
1047
+ class 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
+
1135
+ class 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]
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/matchhelpers.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions which help end users define customize node_match and
2
+ edge_match functions to use during isomorphism checks.
3
+ """
4
+
5
+ import math
6
+ import types
7
+ from itertools import permutations
8
+
9
+ __all__ = [
10
+ "categorical_node_match",
11
+ "categorical_edge_match",
12
+ "categorical_multiedge_match",
13
+ "numerical_node_match",
14
+ "numerical_edge_match",
15
+ "numerical_multiedge_match",
16
+ "generic_node_match",
17
+ "generic_edge_match",
18
+ "generic_multiedge_match",
19
+ ]
20
+
21
+
22
+ def copyfunc(f, name=None):
23
+ """Returns a deepcopy of a function."""
24
+ return types.FunctionType(
25
+ f.__code__, f.__globals__, name or f.__name__, f.__defaults__, f.__closure__
26
+ )
27
+
28
+
29
+ def allclose(x, y, rtol=1.0000000000000001e-05, atol=1e-08):
30
+ """Returns True if x and y are sufficiently close, elementwise.
31
+
32
+ Parameters
33
+ ----------
34
+ rtol : float
35
+ The relative error tolerance.
36
+ atol : float
37
+ The absolute error tolerance.
38
+
39
+ """
40
+ # assume finite weights, see numpy.allclose() for reference
41
+ return all(math.isclose(xi, yi, rel_tol=rtol, abs_tol=atol) for xi, yi in zip(x, y))
42
+
43
+
44
+ categorical_doc = """
45
+ Returns a comparison function for a categorical node attribute.
46
+
47
+ The value(s) of the attr(s) must be hashable and comparable via the ==
48
+ operator since they are placed into a set([]) object. If the sets from
49
+ G1 and G2 are the same, then the constructed function returns True.
50
+
51
+ Parameters
52
+ ----------
53
+ attr : string | list
54
+ The categorical node attribute to compare, or a list of categorical
55
+ node attributes to compare.
56
+ default : value | list
57
+ The default value for the categorical node attribute, or a list of
58
+ default values for the categorical node attributes.
59
+
60
+ Returns
61
+ -------
62
+ match : function
63
+ The customized, categorical `node_match` function.
64
+
65
+ Examples
66
+ --------
67
+ >>> import networkx.algorithms.isomorphism as iso
68
+ >>> nm = iso.categorical_node_match("size", 1)
69
+ >>> nm = iso.categorical_node_match(["color", "size"], ["red", 2])
70
+
71
+ """
72
+
73
+
74
+ def categorical_node_match(attr, default):
75
+ if isinstance(attr, str):
76
+
77
+ def match(data1, data2):
78
+ return data1.get(attr, default) == data2.get(attr, default)
79
+
80
+ else:
81
+ attrs = list(zip(attr, default)) # Python 3
82
+
83
+ def match(data1, data2):
84
+ return all(data1.get(attr, d) == data2.get(attr, d) for attr, d in attrs)
85
+
86
+ return match
87
+
88
+
89
+ categorical_edge_match = copyfunc(categorical_node_match, "categorical_edge_match")
90
+
91
+
92
+ def categorical_multiedge_match(attr, default):
93
+ if isinstance(attr, str):
94
+
95
+ def match(datasets1, datasets2):
96
+ values1 = {data.get(attr, default) for data in datasets1.values()}
97
+ values2 = {data.get(attr, default) for data in datasets2.values()}
98
+ return values1 == values2
99
+
100
+ else:
101
+ attrs = list(zip(attr, default)) # Python 3
102
+
103
+ def match(datasets1, datasets2):
104
+ values1 = set()
105
+ for data1 in datasets1.values():
106
+ x = tuple(data1.get(attr, d) for attr, d in attrs)
107
+ values1.add(x)
108
+ values2 = set()
109
+ for data2 in datasets2.values():
110
+ x = tuple(data2.get(attr, d) for attr, d in attrs)
111
+ values2.add(x)
112
+ return values1 == values2
113
+
114
+ return match
115
+
116
+
117
+ # Docstrings for categorical functions.
118
+ categorical_node_match.__doc__ = categorical_doc
119
+ categorical_edge_match.__doc__ = categorical_doc.replace("node", "edge")
120
+ tmpdoc = categorical_doc.replace("node", "edge")
121
+ tmpdoc = tmpdoc.replace("categorical_edge_match", "categorical_multiedge_match")
122
+ categorical_multiedge_match.__doc__ = tmpdoc
123
+
124
+
125
+ numerical_doc = """
126
+ Returns a comparison function for a numerical node attribute.
127
+
128
+ The value(s) of the attr(s) must be numerical and sortable. If the
129
+ sorted list of values from G1 and G2 are the same within some
130
+ tolerance, then the constructed function returns True.
131
+
132
+ Parameters
133
+ ----------
134
+ attr : string | list
135
+ The numerical node attribute to compare, or a list of numerical
136
+ node attributes to compare.
137
+ default : value | list
138
+ The default value for the numerical node attribute, or a list of
139
+ default values for the numerical node attributes.
140
+ rtol : float
141
+ The relative error tolerance.
142
+ atol : float
143
+ The absolute error tolerance.
144
+
145
+ Returns
146
+ -------
147
+ match : function
148
+ The customized, numerical `node_match` function.
149
+
150
+ Examples
151
+ --------
152
+ >>> import networkx.algorithms.isomorphism as iso
153
+ >>> nm = iso.numerical_node_match("weight", 1.0)
154
+ >>> nm = iso.numerical_node_match(["weight", "linewidth"], [0.25, 0.5])
155
+
156
+ """
157
+
158
+
159
+ def numerical_node_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08):
160
+ if isinstance(attr, str):
161
+
162
+ def match(data1, data2):
163
+ return math.isclose(
164
+ data1.get(attr, default),
165
+ data2.get(attr, default),
166
+ rel_tol=rtol,
167
+ abs_tol=atol,
168
+ )
169
+
170
+ else:
171
+ attrs = list(zip(attr, default)) # Python 3
172
+
173
+ def match(data1, data2):
174
+ values1 = [data1.get(attr, d) for attr, d in attrs]
175
+ values2 = [data2.get(attr, d) for attr, d in attrs]
176
+ return allclose(values1, values2, rtol=rtol, atol=atol)
177
+
178
+ return match
179
+
180
+
181
+ numerical_edge_match = copyfunc(numerical_node_match, "numerical_edge_match")
182
+
183
+
184
+ def numerical_multiedge_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08):
185
+ if isinstance(attr, str):
186
+
187
+ def match(datasets1, datasets2):
188
+ values1 = sorted(data.get(attr, default) for data in datasets1.values())
189
+ values2 = sorted(data.get(attr, default) for data in datasets2.values())
190
+ return allclose(values1, values2, rtol=rtol, atol=atol)
191
+
192
+ else:
193
+ attrs = list(zip(attr, default)) # Python 3
194
+
195
+ def match(datasets1, datasets2):
196
+ values1 = []
197
+ for data1 in datasets1.values():
198
+ x = tuple(data1.get(attr, d) for attr, d in attrs)
199
+ values1.append(x)
200
+ values2 = []
201
+ for data2 in datasets2.values():
202
+ x = tuple(data2.get(attr, d) for attr, d in attrs)
203
+ values2.append(x)
204
+ values1.sort()
205
+ values2.sort()
206
+ for xi, yi in zip(values1, values2):
207
+ if not allclose(xi, yi, rtol=rtol, atol=atol):
208
+ return False
209
+ else:
210
+ return True
211
+
212
+ return match
213
+
214
+
215
+ # Docstrings for numerical functions.
216
+ numerical_node_match.__doc__ = numerical_doc
217
+ numerical_edge_match.__doc__ = numerical_doc.replace("node", "edge")
218
+ tmpdoc = numerical_doc.replace("node", "edge")
219
+ tmpdoc = tmpdoc.replace("numerical_edge_match", "numerical_multiedge_match")
220
+ numerical_multiedge_match.__doc__ = tmpdoc
221
+
222
+
223
+ generic_doc = """
224
+ Returns a comparison function for a generic attribute.
225
+
226
+ The value(s) of the attr(s) are compared using the specified
227
+ operators. If all the attributes are equal, then the constructed
228
+ function returns True.
229
+
230
+ Parameters
231
+ ----------
232
+ attr : string | list
233
+ The node attribute to compare, or a list of node attributes
234
+ to compare.
235
+ default : value | list
236
+ The default value for the node attribute, or a list of
237
+ default values for the node attributes.
238
+ op : callable | list
239
+ The operator to use when comparing attribute values, or a list
240
+ of operators to use when comparing values for each attribute.
241
+
242
+ Returns
243
+ -------
244
+ match : function
245
+ The customized, generic `node_match` function.
246
+
247
+ Examples
248
+ --------
249
+ >>> from operator import eq
250
+ >>> from math import isclose
251
+ >>> from networkx.algorithms.isomorphism import generic_node_match
252
+ >>> nm = generic_node_match("weight", 1.0, isclose)
253
+ >>> nm = generic_node_match("color", "red", eq)
254
+ >>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [isclose, eq])
255
+
256
+ """
257
+
258
+
259
+ def generic_node_match(attr, default, op):
260
+ if isinstance(attr, str):
261
+
262
+ def match(data1, data2):
263
+ return op(data1.get(attr, default), data2.get(attr, default))
264
+
265
+ else:
266
+ attrs = list(zip(attr, default, op)) # Python 3
267
+
268
+ def match(data1, data2):
269
+ for attr, d, operator in attrs:
270
+ if not operator(data1.get(attr, d), data2.get(attr, d)):
271
+ return False
272
+ else:
273
+ return True
274
+
275
+ return match
276
+
277
+
278
+ generic_edge_match = copyfunc(generic_node_match, "generic_edge_match")
279
+
280
+
281
+ def generic_multiedge_match(attr, default, op):
282
+ """Returns a comparison function for a generic attribute.
283
+
284
+ The value(s) of the attr(s) are compared using the specified
285
+ operators. If all the attributes are equal, then the constructed
286
+ function returns True. Potentially, the constructed edge_match
287
+ function can be slow since it must verify that no isomorphism
288
+ exists between the multiedges before it returns False.
289
+
290
+ Parameters
291
+ ----------
292
+ attr : string | list
293
+ The edge attribute to compare, or a list of node attributes
294
+ to compare.
295
+ default : value | list
296
+ The default value for the edge attribute, or a list of
297
+ default values for the edgeattributes.
298
+ op : callable | list
299
+ The operator to use when comparing attribute values, or a list
300
+ of operators to use when comparing values for each attribute.
301
+
302
+ Returns
303
+ -------
304
+ match : function
305
+ The customized, generic `edge_match` function.
306
+
307
+ Examples
308
+ --------
309
+ >>> from operator import eq
310
+ >>> from math import isclose
311
+ >>> from networkx.algorithms.isomorphism import generic_node_match
312
+ >>> nm = generic_node_match("weight", 1.0, isclose)
313
+ >>> nm = generic_node_match("color", "red", eq)
314
+ >>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [isclose, eq])
315
+
316
+ """
317
+
318
+ # This is slow, but generic.
319
+ # We must test every possible isomorphism between the edges.
320
+ if isinstance(attr, str):
321
+ attr = [attr]
322
+ default = [default]
323
+ op = [op]
324
+ attrs = list(zip(attr, default)) # Python 3
325
+
326
+ def match(datasets1, datasets2):
327
+ values1 = []
328
+ for data1 in datasets1.values():
329
+ x = tuple(data1.get(attr, d) for attr, d in attrs)
330
+ values1.append(x)
331
+ values2 = []
332
+ for data2 in datasets2.values():
333
+ x = tuple(data2.get(attr, d) for attr, d in attrs)
334
+ values2.append(x)
335
+ for vals2 in permutations(values2):
336
+ for xi, yi in zip(values1, vals2):
337
+ if not all(map(lambda x, y, z: z(x, y), xi, yi, op)):
338
+ # This is not an isomorphism, go to next permutation.
339
+ break
340
+ else:
341
+ # Then we found an isomorphism.
342
+ return True
343
+ else:
344
+ # Then there are no isomorphisms between the multiedges.
345
+ return False
346
+
347
+ return match
348
+
349
+
350
+ # Docstrings for numerical functions.
351
+ generic_node_match.__doc__ = generic_doc
352
+ generic_edge_match.__doc__ = generic_doc.replace("node", "edge")
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/temporalisomorphvf2.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ *****************************
3
+ Time-respecting VF2 Algorithm
4
+ *****************************
5
+
6
+ An extension of the VF2 algorithm for time-respecting graph isomorphism
7
+ testing in temporal graphs.
8
+
9
+ A temporal graph is one in which edges contain a datetime attribute,
10
+ denoting when interaction occurred between the incident nodes. A
11
+ time-respecting subgraph of a temporal graph is a subgraph such that
12
+ all interactions incident to a node occurred within a time threshold,
13
+ delta, of each other. A directed time-respecting subgraph has the
14
+ added constraint that incoming interactions to a node must precede
15
+ outgoing interactions from the same node - this enforces a sense of
16
+ directed flow.
17
+
18
+ Introduction
19
+ ------------
20
+
21
+ The TimeRespectingGraphMatcher and TimeRespectingDiGraphMatcher
22
+ extend the GraphMatcher and DiGraphMatcher classes, respectively,
23
+ to include temporal constraints on matches. This is achieved through
24
+ a semantic check, via the semantic_feasibility() function.
25
+
26
+ As well as including G1 (the graph in which to seek embeddings) and
27
+ G2 (the subgraph structure of interest), the name of the temporal
28
+ attribute on the edges and the time threshold, delta, must be supplied
29
+ as arguments to the matching constructors.
30
+
31
+ A delta of zero is the strictest temporal constraint on the match -
32
+ only embeddings in which all interactions occur at the same time will
33
+ be returned. A delta of one day will allow embeddings in which
34
+ adjacent interactions occur up to a day apart.
35
+
36
+ Examples
37
+ --------
38
+
39
+ Examples will be provided when the datetime type has been incorporated.
40
+
41
+
42
+ Temporal Subgraph Isomorphism
43
+ -----------------------------
44
+
45
+ A brief discussion of the somewhat diverse current literature will be
46
+ included here.
47
+
48
+ References
49
+ ----------
50
+
51
+ [1] Redmond, U. and Cunningham, P. Temporal subgraph isomorphism. In:
52
+ The 2013 IEEE/ACM International Conference on Advances in Social
53
+ Networks Analysis and Mining (ASONAM). Niagara Falls, Canada; 2013:
54
+ pages 1451 - 1452. [65]
55
+
56
+ For a discussion of the literature on temporal networks:
57
+
58
+ [3] P. Holme and J. Saramaki. Temporal networks. Physics Reports,
59
+ 519(3):97–125, 2012.
60
+
61
+ Notes
62
+ -----
63
+
64
+ Handles directed and undirected graphs and graphs with parallel edges.
65
+
66
+ """
67
+
68
+ import networkx as nx
69
+
70
+ from .isomorphvf2 import DiGraphMatcher, GraphMatcher
71
+
72
+ __all__ = ["TimeRespectingGraphMatcher", "TimeRespectingDiGraphMatcher"]
73
+
74
+
75
+ class TimeRespectingGraphMatcher(GraphMatcher):
76
+ def __init__(self, G1, G2, temporal_attribute_name, delta):
77
+ """Initialize TimeRespectingGraphMatcher.
78
+
79
+ G1 and G2 should be nx.Graph or nx.MultiGraph instances.
80
+
81
+ Examples
82
+ --------
83
+ To create a TimeRespectingGraphMatcher which checks for
84
+ syntactic and semantic feasibility:
85
+
86
+ >>> from networkx.algorithms import isomorphism
87
+ >>> from datetime import timedelta
88
+ >>> G1 = nx.Graph(nx.path_graph(4, create_using=nx.Graph()))
89
+
90
+ >>> G2 = nx.Graph(nx.path_graph(4, create_using=nx.Graph()))
91
+
92
+ >>> GM = isomorphism.TimeRespectingGraphMatcher(
93
+ ... G1, G2, "date", timedelta(days=1)
94
+ ... )
95
+ """
96
+ self.temporal_attribute_name = temporal_attribute_name
97
+ self.delta = delta
98
+ super().__init__(G1, G2)
99
+
100
+ def one_hop(self, Gx, Gx_node, neighbors):
101
+ """
102
+ Edges one hop out from a node in the mapping should be
103
+ time-respecting with respect to each other.
104
+ """
105
+ dates = []
106
+ for n in neighbors:
107
+ if isinstance(Gx, nx.Graph): # Graph G[u][v] returns the data dictionary.
108
+ dates.append(Gx[Gx_node][n][self.temporal_attribute_name])
109
+ else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
110
+ for edge in Gx[Gx_node][
111
+ n
112
+ ].values(): # Iterates all edges between node pair.
113
+ dates.append(edge[self.temporal_attribute_name])
114
+ if any(x is None for x in dates):
115
+ raise ValueError("Datetime not supplied for at least one edge.")
116
+ return not dates or max(dates) - min(dates) <= self.delta
117
+
118
+ def two_hop(self, Gx, core_x, Gx_node, neighbors):
119
+ """
120
+ Paths of length 2 from Gx_node should be time-respecting.
121
+ """
122
+ return all(
123
+ self.one_hop(Gx, v, [n for n in Gx[v] if n in core_x] + [Gx_node])
124
+ for v in neighbors
125
+ )
126
+
127
+ def semantic_feasibility(self, G1_node, G2_node):
128
+ """Returns True if adding (G1_node, G2_node) is semantically
129
+ feasible.
130
+
131
+ Any subclass which redefines semantic_feasibility() must
132
+ maintain the self.tests if needed, to keep the match() method
133
+ functional. Implementations should consider multigraphs.
134
+ """
135
+ neighbors = [n for n in self.G1[G1_node] if n in self.core_1]
136
+ if not self.one_hop(self.G1, G1_node, neighbors): # Fail fast on first node.
137
+ return False
138
+ if not self.two_hop(self.G1, self.core_1, G1_node, neighbors):
139
+ return False
140
+ # Otherwise, this node is semantically feasible!
141
+ return True
142
+
143
+
144
+ class TimeRespectingDiGraphMatcher(DiGraphMatcher):
145
+ def __init__(self, G1, G2, temporal_attribute_name, delta):
146
+ """Initialize TimeRespectingDiGraphMatcher.
147
+
148
+ G1 and G2 should be nx.DiGraph or nx.MultiDiGraph instances.
149
+
150
+ Examples
151
+ --------
152
+ To create a TimeRespectingDiGraphMatcher which checks for
153
+ syntactic and semantic feasibility:
154
+
155
+ >>> from networkx.algorithms import isomorphism
156
+ >>> from datetime import timedelta
157
+ >>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
158
+
159
+ >>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
160
+
161
+ >>> GM = isomorphism.TimeRespectingDiGraphMatcher(
162
+ ... G1, G2, "date", timedelta(days=1)
163
+ ... )
164
+ """
165
+ self.temporal_attribute_name = temporal_attribute_name
166
+ self.delta = delta
167
+ super().__init__(G1, G2)
168
+
169
+ def get_pred_dates(self, Gx, Gx_node, core_x, pred):
170
+ """
171
+ Get the dates of edges from predecessors.
172
+ """
173
+ pred_dates = []
174
+ if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary.
175
+ for n in pred:
176
+ pred_dates.append(Gx[n][Gx_node][self.temporal_attribute_name])
177
+ else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
178
+ for n in pred:
179
+ for edge in Gx[n][
180
+ Gx_node
181
+ ].values(): # Iterates all edge data between node pair.
182
+ pred_dates.append(edge[self.temporal_attribute_name])
183
+ return pred_dates
184
+
185
+ def get_succ_dates(self, Gx, Gx_node, core_x, succ):
186
+ """
187
+ Get the dates of edges to successors.
188
+ """
189
+ succ_dates = []
190
+ if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary.
191
+ for n in succ:
192
+ succ_dates.append(Gx[Gx_node][n][self.temporal_attribute_name])
193
+ else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
194
+ for n in succ:
195
+ for edge in Gx[Gx_node][
196
+ n
197
+ ].values(): # Iterates all edge data between node pair.
198
+ succ_dates.append(edge[self.temporal_attribute_name])
199
+ return succ_dates
200
+
201
+ def one_hop(self, Gx, Gx_node, core_x, pred, succ):
202
+ """
203
+ The ego node.
204
+ """
205
+ pred_dates = self.get_pred_dates(Gx, Gx_node, core_x, pred)
206
+ succ_dates = self.get_succ_dates(Gx, Gx_node, core_x, succ)
207
+ return self.test_one(pred_dates, succ_dates) and self.test_two(
208
+ pred_dates, succ_dates
209
+ )
210
+
211
+ def two_hop_pred(self, Gx, Gx_node, core_x, pred):
212
+ """
213
+ The predecessors of the ego node.
214
+ """
215
+ return all(
216
+ self.one_hop(
217
+ Gx,
218
+ p,
219
+ core_x,
220
+ self.preds(Gx, core_x, p),
221
+ self.succs(Gx, core_x, p, Gx_node),
222
+ )
223
+ for p in pred
224
+ )
225
+
226
+ def two_hop_succ(self, Gx, Gx_node, core_x, succ):
227
+ """
228
+ The successors of the ego node.
229
+ """
230
+ return all(
231
+ self.one_hop(
232
+ Gx,
233
+ s,
234
+ core_x,
235
+ self.preds(Gx, core_x, s, Gx_node),
236
+ self.succs(Gx, core_x, s),
237
+ )
238
+ for s in succ
239
+ )
240
+
241
+ def preds(self, Gx, core_x, v, Gx_node=None):
242
+ pred = [n for n in Gx.predecessors(v) if n in core_x]
243
+ if Gx_node:
244
+ pred.append(Gx_node)
245
+ return pred
246
+
247
+ def succs(self, Gx, core_x, v, Gx_node=None):
248
+ succ = [n for n in Gx.successors(v) if n in core_x]
249
+ if Gx_node:
250
+ succ.append(Gx_node)
251
+ return succ
252
+
253
+ def test_one(self, pred_dates, succ_dates):
254
+ """
255
+ Edges one hop out from Gx_node in the mapping should be
256
+ time-respecting with respect to each other, regardless of
257
+ direction.
258
+ """
259
+ time_respecting = True
260
+ dates = pred_dates + succ_dates
261
+
262
+ if any(x is None for x in dates):
263
+ raise ValueError("Date or datetime not supplied for at least one edge.")
264
+
265
+ dates.sort() # Small to large.
266
+ if 0 < len(dates) and not (dates[-1] - dates[0] <= self.delta):
267
+ time_respecting = False
268
+ return time_respecting
269
+
270
+ def test_two(self, pred_dates, succ_dates):
271
+ """
272
+ Edges from a dual Gx_node in the mapping should be ordered in
273
+ a time-respecting manner.
274
+ """
275
+ time_respecting = True
276
+ pred_dates.sort()
277
+ succ_dates.sort()
278
+ # First out before last in; negative of the necessary condition for time-respect.
279
+ if (
280
+ 0 < len(succ_dates)
281
+ and 0 < len(pred_dates)
282
+ and succ_dates[0] < pred_dates[-1]
283
+ ):
284
+ time_respecting = False
285
+ return time_respecting
286
+
287
+ def semantic_feasibility(self, G1_node, G2_node):
288
+ """Returns True if adding (G1_node, G2_node) is semantically
289
+ feasible.
290
+
291
+ Any subclass which redefines semantic_feasibility() must
292
+ maintain the self.tests if needed, to keep the match() method
293
+ functional. Implementations should consider multigraphs.
294
+ """
295
+ pred, succ = (
296
+ [n for n in self.G1.predecessors(G1_node) if n in self.core_1],
297
+ [n for n in self.G1.successors(G1_node) if n in self.core_1],
298
+ )
299
+ if not self.one_hop(
300
+ self.G1, G1_node, self.core_1, pred, succ
301
+ ): # Fail fast on first node.
302
+ return False
303
+ if not self.two_hop_pred(self.G1, G1_node, self.core_1, pred):
304
+ return False
305
+ if not self.two_hop_succ(self.G1, G1_node, self.core_1, succ):
306
+ return False
307
+ # Otherwise, this node is semantically feasible!
308
+ return True
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/__init__.py ADDED
File without changes
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99 ADDED
Binary file (1.44 kB). View file
 
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99 ADDED
Binary file (1.44 kB). View file
 
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99 ADDED
Binary file (310 Bytes). View file
 
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99 ADDED
Binary file (1.6 kB). View file
 
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_ismags.py ADDED
@@ -0,0 +1,719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for ISMAGS isomorphism algorithm.
3
+ """
4
+
5
+ import random
6
+
7
+ import pytest
8
+
9
+ import networkx as nx
10
+ from networkx.algorithms import isomorphism as iso
11
+
12
+ graph_classes = [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
13
+
14
+
15
+ def _matches_to_sets(matches):
16
+ """
17
+ Helper function to facilitate comparing collections of dictionaries in
18
+ which order does not matter.
19
+ """
20
+ return {frozenset(m.items()) for m in matches}
21
+
22
+
23
+ graph_examples = [
24
+ # node_data, edge_data, [id used in name for the test]
25
+ pytest.param([0, 1, 2, 3], [(0, 0)], id="isolated-nodes-and-selfloops"),
26
+ pytest.param([], nx.star_graph(3).edges, id="3-star"),
27
+ pytest.param(
28
+ # 6-cycle with 2-paths stuck onto nodes 0, 2, 4 (stretched symmetry)
29
+ [],
30
+ [
31
+ (0, 1),
32
+ (1, 2),
33
+ (2, 3),
34
+ (3, 4),
35
+ (4, 5),
36
+ (5, 0),
37
+ (0, 6),
38
+ (6, 7),
39
+ (2, 8),
40
+ (8, 9),
41
+ (4, 10),
42
+ (10, 11),
43
+ ],
44
+ id="sun:6-cycle-with-2-path-rays",
45
+ ),
46
+ # 0-1-2-3-5
47
+ # / \
48
+ # 4 6
49
+ pytest.param([], [(0, 1), (1, 2), (1, 4), (2, 3), (3, 5), (3, 6)], id="tree"),
50
+ pytest.param([], nx.petersen_graph().edges, id="petersen_graph"),
51
+ # Example Fig 3 from Houbraken, et al (ISMAGS paper)
52
+ pytest.param(
53
+ [], nx.cycle_graph([1, 2, 4, 3]).edges, id="houbraken-ismags-paper-fig3"
54
+ ),
55
+ pytest.param(
56
+ # path with node labels
57
+ [
58
+ (0, {"name": "a"}),
59
+ (1, {"name": "a"}),
60
+ (2, {"name": "b"}),
61
+ (3, {"name": "b"}),
62
+ (4, {"name": "a"}),
63
+ (5, {"name": "a"}),
64
+ ],
65
+ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
66
+ id="path-with-node-labels",
67
+ ),
68
+ pytest.param(
69
+ # 5 - 4 \ / 12 - 13
70
+ # 0 - 3
71
+ # 9 - 8 / \ 16 - 17
72
+ # Assume 0 and 3 are coupled and no longer equivalent.
73
+ # Coupling node 4 to 8 means that 5 and 9
74
+ # are no longer equivalent, pushing them in their own partitions.
75
+ # So, [{5}, {9}] is no longer considered equivalent to {13, 17}.
76
+ # Minimal example with this trait. Adding all permutations of
77
+ # same-size parts at each step finds the symmetry.
78
+ [],
79
+ [
80
+ (0, 3),
81
+ (3, 0), # added to provide symmetry for DiGraphs
82
+ (0, 4),
83
+ (4, 5),
84
+ (0, 8),
85
+ (8, 9),
86
+ (3, 12),
87
+ (12, 13),
88
+ (3, 16),
89
+ (16, 17),
90
+ ],
91
+ id="gh8055-tricky-case",
92
+ ),
93
+ pytest.param(
94
+ [], nx.path_graph([1, 2, 3, "a", "b", "c"]).edges, id="unsortable-nodes"
95
+ ),
96
+ # Example from Katebi, 2012, Fig 1-3.
97
+ # Node order specified to (almost) match their DFS order
98
+ pytest.param(
99
+ [3, 5, 6, 4, 2, 1, 0],
100
+ set(nx.cycle_graph(4).edges) | set(nx.cycle_graph(range(4, 7)).edges),
101
+ id="katebi-paper-fig2",
102
+ ),
103
+ pytest.param(
104
+ [], [(0, 1), (1, 2), (2, 3), (3, 6), (2, 4), (4, 5)], id="len-2-rays-tri-star"
105
+ ),
106
+ # Example of refining permutations with two different length parts at the same time.
107
+ # Underlying shape is a 4-cycle and 2-path. Multiedges make all nodes degree-3
108
+ # Full simple graph is then obtained by extending each edge as a path thru 1 node.
109
+ # 0
110
+ # Underlying // \ 4 When 0->0 coupling occurs,
111
+ # MultiGraph 1 3 \\\ refining {1, 2, 3, 4, 5}
112
+ # \ // 5 refined parts [{1}, {3}, {2, 4, 5}]
113
+ # 2 with different parts having different lengths.
114
+ # 0
115
+ # /|\ 4
116
+ # 6 7 8 /|\
117
+ # Full: |/ | / | \
118
+ # 1 3 12 13 14
119
+ # | / \ \ | /
120
+ # 9 10 11 \|/
121
+ # \ | / 5
122
+ # 2
123
+ # Nodes 0-5 are the degree-3 nodes.
124
+ # Nodes 6-14 are degree 2 nodes on paths between the degree-3 nodes.
125
+ pytest.param(
126
+ [],
127
+ [
128
+ (0, 6),
129
+ (0, 7),
130
+ (0, 8),
131
+ (1, 6),
132
+ (1, 7),
133
+ (1, 9),
134
+ (2, 9),
135
+ (2, 10),
136
+ (2, 11),
137
+ (3, 8),
138
+ (3, 10),
139
+ (3, 11),
140
+ (4, 12),
141
+ (4, 13),
142
+ (4, 14),
143
+ (5, 12),
144
+ (5, 13),
145
+ (5, 14),
146
+ ],
147
+ id="refining-parts-finds-different-lengths",
148
+ ),
149
+ # Underlying structure from previous example
150
+ pytest.param(
151
+ [],
152
+ [(0, 1), (0, 1), (1, 2), (2, 3), (2, 3), (3, 0), (4, 5), (4, 5), (4, 5)],
153
+ id="basic-structure-for-refining-parts-test",
154
+ ),
155
+ ]
156
+
157
+
158
+ @pytest.mark.parametrize("graph_constructor", graph_classes)
159
+ class TestSelfIsomorphism:
160
+ @pytest.mark.parametrize(["node_data", "edge_data"], graph_examples)
161
+ def test_self_isomorphism(self, graph_constructor, node_data, edge_data):
162
+ """
163
+ For some small, symmetric graphs, make sure that 1) they are isomorphic
164
+ to themselves, and 2) that only the identity mapping is found.
165
+ """
166
+ graph = graph_constructor()
167
+ graph.add_nodes_from(node_data)
168
+ graph.add_edges_from(edge_data)
169
+
170
+ ismags = iso.ISMAGS(
171
+ graph, graph, node_match=iso.categorical_node_match("name", None)
172
+ )
173
+ assert ismags.is_isomorphic()
174
+ assert ismags.is_isomorphic(symmetry=True)
175
+ assert ismags.subgraph_is_isomorphic()
176
+ ismags_answer = list(ismags.subgraph_isomorphisms_iter(symmetry=True))
177
+ assert ismags_answer == [{n: n for n in graph.nodes}]
178
+
179
+
180
+ class TestSubgraphIsomorphism:
181
+ def test_isomorphism_4_sun(self):
182
+ g1 = nx.cycle_graph(4)
183
+ g2 = nx.cycle_graph(4)
184
+ g2.add_edges_from(list(zip(g2, range(4, 8))))
185
+ ismags = iso.ISMAGS(g2, g1)
186
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == [
187
+ {n: n for n in g1.nodes}
188
+ ]
189
+ assert sum(1 for _ in ismags.subgraph_isomorphisms_iter(symmetry=False)) == 8
190
+
191
+ def test_isomorphism_path_in_tristar(self):
192
+ g1 = nx.path_graph(3)
193
+
194
+ g2 = g1.copy()
195
+ g2.add_edge(1, 3)
196
+
197
+ ismags = iso.ISMAGS(g2, g1)
198
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
199
+ expected_symmetric = [
200
+ {0: 0, 1: 1, 2: 2},
201
+ {0: 0, 1: 1, 3: 2},
202
+ {2: 0, 1: 1, 3: 2},
203
+ ]
204
+ assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
205
+
206
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
207
+ expected_asymmetric = [
208
+ {0: 2, 1: 1, 2: 0},
209
+ {0: 2, 1: 1, 3: 0},
210
+ {2: 2, 1: 1, 3: 0},
211
+ ]
212
+ assert _matches_to_sets(matches) == _matches_to_sets(
213
+ expected_symmetric + expected_asymmetric
214
+ )
215
+
216
+ def test_labeled_nodes(self):
217
+ g1 = nx.cycle_graph(3)
218
+ g1.nodes[1]["attr"] = True
219
+
220
+ g2 = g1.copy()
221
+ g2.add_edge(1, 3)
222
+ ismags = iso.ISMAGS(g2, g1, node_match=lambda x, y: x == y)
223
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
224
+ expected_symmetric = [{0: 0, 1: 1, 2: 2}]
225
+ assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
226
+
227
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
228
+ expected_asymmetric = [{0: 2, 1: 1, 2: 0}]
229
+ assert _matches_to_sets(matches) == _matches_to_sets(
230
+ expected_symmetric + expected_asymmetric
231
+ )
232
+
233
+ def test_labeled_edges(self):
234
+ g1 = nx.Graph()
235
+ nx.add_cycle(g1, range(3))
236
+ g1.edges[1, 2]["attr"] = True
237
+
238
+ g2 = g1.copy()
239
+ g2.add_edge(1, 3)
240
+ ismags = iso.ISMAGS(g2, g1, edge_match=lambda x, y: x == y)
241
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
242
+ expected_symmetric = [{0: 0, 1: 1, 2: 2}]
243
+ assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
244
+
245
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
246
+ expected_asymmetric = [{1: 2, 0: 0, 2: 1}]
247
+ assert _matches_to_sets(matches) == _matches_to_sets(
248
+ expected_symmetric + expected_asymmetric
249
+ )
250
+
251
+ def test_exceptions_for_bad_match_functions(self):
252
+ def non_transitive_match(attrs1, attrs2):
253
+ return abs(attrs1["freq"] - attrs2["freq"]) <= 1
254
+
255
+ def simple_non_commutative_match(attrs1, attrs2):
256
+ return attrs1["freq"] == 1 + attrs2["freq"]
257
+
258
+ def non_commutative_match(attrs1, attrs2):
259
+ # red matches red and green
260
+ # green and blue only match themselves
261
+ if attrs2["color"] == "red":
262
+ return attrs2["color"] in {"red", "green"}
263
+ else:
264
+ return attrs1["color"] == attrs2["color"]
265
+
266
+ G1 = nx.Graph()
267
+ G1.add_node(0, color="red", freq=0)
268
+ G1.add_node(1, color="red", freq=1)
269
+ G1.add_node(2, color="blue", freq=2)
270
+
271
+ G2 = nx.Graph()
272
+ G2.add_node("A", color="red", freq=0)
273
+ G2.add_node("B", color="green", freq=1)
274
+ G2.add_node("C", color="blue", freq=2)
275
+
276
+ with pytest.raises(nx.NetworkXError, match="\nInvalid partition"):
277
+ iso.ISMAGS(G1, G2, node_match=non_transitive_match)
278
+
279
+ with pytest.raises(nx.NetworkXError, match="\nInvalid partition"):
280
+ iso.ISMAGS(G1, G2, node_match=simple_non_commutative_match)
281
+
282
+ with pytest.raises(nx.NetworkXError, match="\nInvalid partition"):
283
+ iso.ISMAGS(G1, G2, node_match=non_commutative_match)
284
+
285
+
286
+ def test_noncomparable_nodes():
287
+ node1 = object()
288
+ node2 = object()
289
+ node3 = object()
290
+
291
+ # Graph
292
+ G = nx.path_graph([node1, node2, node3])
293
+ gm = iso.ISMAGS(G, G)
294
+ assert gm.is_isomorphic()
295
+ assert gm.subgraph_is_isomorphic()
296
+
297
+ # DiGraph
298
+ G = nx.path_graph([node1, node2, node3], create_using=nx.DiGraph)
299
+ H = nx.path_graph([node3, node2, node1], create_using=nx.DiGraph)
300
+ dgm = iso.ISMAGS(G, H)
301
+ assert dgm.is_isomorphic()
302
+ assert dgm.is_isomorphic(symmetry=True)
303
+ assert dgm.subgraph_is_isomorphic()
304
+
305
+
306
+ @pytest.mark.parametrize("graph_constructor", graph_classes)
307
+ def test_selfloop(graph_constructor):
308
+ # Simple test for graphs with selfloops
309
+ g1 = graph_constructor([(0, 1), (0, 2), (1, 2), (1, 3), (2, 2), (2, 4)])
310
+ nodes = range(5)
311
+ rng = random.Random(42)
312
+
313
+ for _ in range(3):
314
+ new_nodes = list(nodes)
315
+ rng.shuffle(new_nodes)
316
+ d = dict(zip(nodes, new_nodes))
317
+ g2 = nx.relabel_nodes(g1, d)
318
+ assert iso.ISMAGS(g1, g2).is_isomorphic()
319
+
320
+
321
+ class TestWikipediaExample:
322
+ # example in wikipedia is g1a and g2b
323
+ # 1 have letter nodes, 2 have number nodes
324
+ # b have some edges reversed vs a (undirected still isomorphic)
325
+ # reversed edges marked with comment `#`
326
+ # isomorphism = {'a': 1, 'g': 2, 'b': 3, 'c': 6, 'h': 4, 'i': 5, 'j': 7, 'd': 8}
327
+
328
+ # Nodes 'a', 'b', 'c' and 'd' form a column.
329
+ # Nodes 'g', 'h', 'i' and 'j' form a column.
330
+ g1a_edges = [
331
+ ["a", "g"],
332
+ ["a", "h"], # edge direction swapped from g1b
333
+ ["a", "i"],
334
+ ["b", "g"], # edge direction swapped from g1b
335
+ ["b", "h"],
336
+ ["b", "j"],
337
+ ["c", "g"], # edge direction swapped from g1b
338
+ ["c", "i"], # edge direction swapped from g1b
339
+ ["c", "j"],
340
+ ["d", "h"], # edge direction swapped from g1b
341
+ ["d", "i"],
342
+ ["d", "j"], # edge direction swapped from g1b
343
+ ]
344
+
345
+ g1b_edges = [
346
+ ["a", "g"],
347
+ ["h", "a"], # edge direction swapped from g1a
348
+ ["a", "i"],
349
+ ["g", "b"], # edge direction swapped from g1a
350
+ ["b", "h"],
351
+ ["b", "j"],
352
+ ["g", "c"], # edge direction swapped from g1a
353
+ ["i", "c"], # edge direction swapped from g1a
354
+ ["c", "j"],
355
+ ["h", "d"], # edge direction swapped from g1a
356
+ ["d", "i"],
357
+ ["j", "d"], # edge direction swapped from g1a
358
+ ]
359
+
360
+ g2b_edges = [
361
+ [1, 2],
362
+ [1, 4], # edge direction swapped from g2a
363
+ [1, 5],
364
+ [3, 2], # edge direction swapped from g2a
365
+ [3, 4],
366
+ [3, 7],
367
+ [6, 2], # edge direction swapped from g2a
368
+ [6, 5], # edge direction swapped from g2a
369
+ [6, 7],
370
+ [8, 4], # edge direction swapped from g2a
371
+ [8, 5],
372
+ [8, 7], # edge direction swapped from g2a
373
+ ]
374
+
375
+ # Nodes 1,2,3,4 form the clockwise corners of a large square.
376
+ # Nodes 5,6,7,8 form the clockwise corners of a small square
377
+ g2a_edges = [
378
+ [1, 2],
379
+ [4, 1], # edge direction swapped from g2b
380
+ [1, 5],
381
+ [2, 3], # edge direction swapped from g2b
382
+ [3, 4],
383
+ [3, 7],
384
+ [2, 6], # edge direction swapped from g2b
385
+ [5, 6], # edge direction swapped from g2b
386
+ [6, 7],
387
+ [4, 8], # edge direction swapped from g2b
388
+ [8, 5],
389
+ [7, 8], # edge direction swapped from g2b
390
+ ]
391
+
392
+ @pytest.mark.parametrize("graph_constructor", [nx.Graph, nx.MultiGraph])
393
+ def test_graph(self, graph_constructor):
394
+ g1a = graph_constructor(self.g1a_edges)
395
+ g1b = graph_constructor(self.g1b_edges)
396
+ g2a = graph_constructor(self.g2a_edges)
397
+ g2b = graph_constructor(self.g2b_edges)
398
+ assert iso.ISMAGS(g1a, g1b).is_isomorphic()
399
+ assert iso.ISMAGS(g1a, g2a).is_isomorphic()
400
+ assert iso.ISMAGS(g1a, g2b).is_isomorphic()
401
+
402
+ assert iso.ISMAGS(g1a, nx.path_graph(range(5))).subgraph_is_isomorphic()
403
+ assert not iso.ISMAGS(g1a, nx.path_graph(range(6))).subgraph_is_isomorphic()
404
+
405
+ @pytest.mark.parametrize("graph_constructor", [nx.DiGraph, nx.MultiDiGraph])
406
+ def test_digraph(self, graph_constructor):
407
+ g1a = graph_constructor(self.g1a_edges)
408
+ g1b = graph_constructor(self.g1b_edges)
409
+ g2a = graph_constructor(self.g2a_edges)
410
+ g2b = graph_constructor(self.g2b_edges)
411
+ assert iso.ISMAGS(g1a, g2b).is_isomorphic()
412
+ assert iso.ISMAGS(g1b, g2a).is_isomorphic()
413
+ assert not iso.ISMAGS(g1a, g1b).is_isomorphic()
414
+ assert not iso.ISMAGS(g2a, g2b).is_isomorphic()
415
+ assert not iso.ISMAGS(g1a, g2a).is_isomorphic()
416
+ assert not iso.ISMAGS(g1b, g2b).is_isomorphic()
417
+
418
+ P2 = nx.path_graph(range(2), create_using=graph_constructor)
419
+ assert iso.ISMAGS(g1a, P2).subgraph_is_isomorphic()
420
+ P3 = nx.path_graph(range(3), create_using=graph_constructor)
421
+ assert not iso.ISMAGS(g1a, P3).subgraph_is_isomorphic()
422
+
423
+
424
+ class TestLargestCommonSubgraph:
425
+ def test_mcis(self):
426
+ # Example graphs from DOI: 10.1002/spe.588
427
+ graph1 = nx.Graph()
428
+ graph1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 4), (4, 5)])
429
+ graph1.nodes[1]["color"] = 0
430
+
431
+ graph2 = nx.Graph()
432
+ graph2.add_edges_from(
433
+ [(1, 2), (2, 3), (2, 4), (3, 4), (3, 5), (5, 6), (5, 7), (6, 7)]
434
+ )
435
+ graph2.nodes[1]["color"] = 1
436
+ graph2.nodes[6]["color"] = 2
437
+ graph2.nodes[7]["color"] = 2
438
+
439
+ ismags = iso.ISMAGS(
440
+ graph1, graph2, node_match=iso.categorical_node_match("color", None)
441
+ )
442
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == []
443
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=False)) == []
444
+ found_mcis = _matches_to_sets(ismags.largest_common_subgraph())
445
+ expected = _matches_to_sets(
446
+ [{2: 2, 3: 4, 4: 3, 5: 5}, {2: 4, 3: 2, 4: 3, 5: 5}]
447
+ )
448
+ assert expected == found_mcis
449
+
450
+ ismags = iso.ISMAGS(
451
+ graph2, graph1, node_match=iso.categorical_node_match("color", None)
452
+ )
453
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == []
454
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=False)) == []
455
+ found_mcis = _matches_to_sets(ismags.largest_common_subgraph())
456
+ # Same answer, but reversed.
457
+ expected = _matches_to_sets(
458
+ [{2: 2, 3: 4, 4: 3, 5: 5}, {4: 2, 2: 3, 3: 4, 5: 5}]
459
+ )
460
+ assert expected == found_mcis
461
+
462
+ def test_symmetry_mcis(self):
463
+ graph1 = nx.Graph()
464
+ nx.add_path(graph1, range(4))
465
+
466
+ graph2 = nx.Graph()
467
+ nx.add_path(graph2, range(3))
468
+ graph2.add_edge(1, 3)
469
+
470
+ # Only the symmetry of graph2 is taken into account here.
471
+ ismags1 = iso.ISMAGS(
472
+ graph1, graph2, node_match=iso.categorical_node_match("color", None)
473
+ )
474
+ assert list(ismags1.subgraph_isomorphisms_iter(symmetry=True)) == []
475
+ found_mcis = _matches_to_sets(ismags1.largest_common_subgraph())
476
+ expected = _matches_to_sets([{0: 0, 1: 1, 2: 2}, {1: 0, 3: 2, 2: 1}])
477
+ assert expected == found_mcis
478
+
479
+ # Only the symmetry of graph1 is taken into account here.
480
+ ismags2 = iso.ISMAGS(
481
+ graph2, graph1, node_match=iso.categorical_node_match("color", None)
482
+ )
483
+ assert list(ismags2.subgraph_isomorphisms_iter(symmetry=True)) == []
484
+ found_mcis = _matches_to_sets(ismags2.largest_common_subgraph())
485
+ expected = _matches_to_sets(
486
+ [
487
+ {3: 2, 0: 0, 1: 1},
488
+ {2: 0, 0: 2, 1: 1},
489
+ {3: 0, 0: 2, 1: 1},
490
+ {3: 0, 1: 1, 2: 2},
491
+ {0: 0, 1: 1, 2: 2},
492
+ {2: 0, 3: 2, 1: 1},
493
+ ]
494
+ )
495
+
496
+ assert expected == found_mcis
497
+
498
+ found_mcis1 = _matches_to_sets(ismags1.largest_common_subgraph(symmetry=False))
499
+ found_mcis2 = ismags2.largest_common_subgraph(symmetry=False)
500
+ found_mcis2 = [{v: k for k, v in d.items()} for d in found_mcis2]
501
+ found_mcis2 = _matches_to_sets(found_mcis2)
502
+
503
+ expected = _matches_to_sets(
504
+ [
505
+ {3: 2, 1: 3, 2: 1},
506
+ {2: 0, 0: 2, 1: 1},
507
+ {1: 2, 3: 3, 2: 1},
508
+ {3: 0, 1: 3, 2: 1},
509
+ {0: 2, 2: 3, 1: 1},
510
+ {3: 0, 1: 2, 2: 1},
511
+ {2: 0, 0: 3, 1: 1},
512
+ {0: 0, 2: 3, 1: 1},
513
+ {1: 0, 3: 3, 2: 1},
514
+ {1: 0, 3: 2, 2: 1},
515
+ {0: 3, 1: 1, 2: 2},
516
+ {0: 0, 1: 1, 2: 2},
517
+ ]
518
+ )
519
+ assert expected == found_mcis1
520
+ assert expected == found_mcis2
521
+
522
+
523
+ def is_isomorphic(G, SG, edge_match=None, node_match=None):
524
+ return iso.ISMAGS(G, SG, node_match, edge_match).is_isomorphic()
525
+
526
+
527
+ class TestDiGraphISO:
528
+ def test_wikipedia_graph(self):
529
+ edges1 = [
530
+ (1, 5),
531
+ (1, 2),
532
+ (1, 4),
533
+ (3, 2),
534
+ (6, 2),
535
+ (3, 4),
536
+ (7, 3),
537
+ (4, 8),
538
+ (5, 8),
539
+ (6, 5),
540
+ (6, 7),
541
+ (7, 8),
542
+ ]
543
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
544
+
545
+ G1 = nx.DiGraph(edges1)
546
+ G2 = nx.relabel_nodes(G1, mapped)
547
+
548
+ result = next(nx.isomorphism.ISMAGS(G1, G2).find_isomorphisms())
549
+ assert result == mapped
550
+
551
+ # Change the direction of an edge
552
+ G1.remove_edge(1, 5)
553
+ G1.add_edge(5, 1)
554
+ result = list(nx.isomorphism.ISMAGS(G1, G2).find_isomorphisms())
555
+ assert result == []
556
+
557
+ def test_non_isomorphic_same_degree_sequence(self):
558
+ r"""
559
+ G1 G2
560
+ 1-------------2 1-------------2
561
+ | \ | | \ |
562
+ | 5-------6 | | 5-------6 |
563
+ | | | | | | | |
564
+ | 8-------7 | | 8-------7 |
565
+ | / | | \ |
566
+ 4-------------3 4-------------3
567
+ """
568
+ edges1 = [
569
+ (1, 5),
570
+ (1, 2),
571
+ (4, 1),
572
+ (3, 2),
573
+ (3, 4),
574
+ (5, 8),
575
+ (6, 5),
576
+ (6, 7),
577
+ (7, 8),
578
+ (4, 8),
579
+ ]
580
+ edges2 = [
581
+ (1, 5),
582
+ (1, 2),
583
+ (4, 1),
584
+ (3, 2),
585
+ (3, 4),
586
+ (5, 8),
587
+ (6, 5),
588
+ (6, 7),
589
+ (8, 7),
590
+ (3, 7),
591
+ ]
592
+
593
+ G1 = nx.DiGraph(edges1)
594
+ G2 = nx.DiGraph(edges2)
595
+ assert not is_isomorphic(G1, G2)
596
+
597
+ def test_is_isomorphic(self):
598
+ G1 = nx.Graph([[1, 2], [1, 3], [1, 5], [2, 3]])
599
+ G2 = nx.Graph([[10, 20], [20, 30], [10, 30], [10, 50]])
600
+ G4 = nx.Graph([[1, 2], [1, 3], [1, 5], [2, 4]])
601
+ assert is_isomorphic(G1, G2)
602
+ assert not is_isomorphic(G1, G4)
603
+ assert is_isomorphic(G1.to_directed(), G2.to_directed())
604
+ assert not is_isomorphic(G1.to_directed(), G4.to_directed())
605
+ with pytest.raises(
606
+ ValueError, match="Directed and undirected graphs cannot be compared."
607
+ ):
608
+ is_isomorphic(G1.to_directed(), G1)
609
+
610
+
611
+ @pytest.mark.parametrize("graph_class", graph_classes)
612
+ def test_simple_node_match(graph_class):
613
+ g1 = graph_class([(0, 0), (0, 1), (1, 0)])
614
+ g2 = g1.copy()
615
+ nm = iso.numerical_node_match("size", 1)
616
+ assert is_isomorphic(g1, g2, node_match=nm)
617
+
618
+ g2.nodes[0]["size"] = 3
619
+ assert not is_isomorphic(g1, g2, node_match=nm)
620
+
621
+
622
+ @pytest.mark.parametrize("graph_class", graph_classes)
623
+ def test_simple_node_and_edge_match(graph_class):
624
+ g1 = graph_class()
625
+ g1.add_weighted_edges_from([(0, 0, 1.2), (0, 1, 1.4), (1, 0, 1.6)])
626
+ g2 = g1.copy()
627
+ nm = iso.numerical_node_match("size", 1)
628
+ if g1.is_multigraph():
629
+ em = iso.numerical_multiedge_match("weight", 1)
630
+ else:
631
+ em = iso.numerical_edge_match("weight", 1)
632
+ assert is_isomorphic(g1, g2, node_match=nm, edge_match=em)
633
+
634
+ g2.nodes[0]["size"] = 3
635
+ assert not is_isomorphic(g1, g2, node_match=nm, edge_match=em)
636
+
637
+ g2 = g1.copy()
638
+ if g1.is_multigraph():
639
+ g2.edges[0, 1, 0]["weight"] = 2.1
640
+ else:
641
+ g2.edges[0, 1]["weight"] = 2.1
642
+ assert not is_isomorphic(g1, g2, node_match=nm, edge_match=em)
643
+
644
+ g2 = g1.copy()
645
+ g2.nodes[0]["size"] = 3
646
+ if g1.is_multigraph():
647
+ g2.edges[0, 1, 0]["weight"] = 2.1
648
+ else:
649
+ g2.edges[0, 1]["weight"] = 2.1
650
+ assert not is_isomorphic(g1, g2, node_match=nm, edge_match=em)
651
+
652
+
653
+ @pytest.mark.parametrize("graph_class", graph_classes)
654
+ def test_simple_edge_match(graph_class):
655
+ # 16 simple tests
656
+ w = "weight"
657
+ edges = [(0, 0, 1), (0, 0, 1.5), (0, 1, 2), (1, 0, 3)]
658
+ g1 = graph_class()
659
+ g1.add_weighted_edges_from(edges)
660
+ g2 = g1.copy()
661
+ if g1.is_multigraph():
662
+ em = iso.numerical_multiedge_match("weight", 1)
663
+ else:
664
+ em = iso.numerical_edge_match("weight", 1)
665
+ assert is_isomorphic(g1, g2, edge_match=em)
666
+
667
+ for mod1, mod2 in [(False, True), (True, False), (True, True)]:
668
+ # mod1 tests a regular edge weight difference
669
+ # mod2 tests a selfloop weight difference
670
+ if g1.is_multigraph():
671
+ if mod1:
672
+ data1 = {0: {"weight": 10}}
673
+ if mod2:
674
+ data2 = {0: {"weight": 1}, 1: {"weight": 2.5}}
675
+ else:
676
+ if mod1:
677
+ data1 = {"weight": 10}
678
+ if mod2:
679
+ data2 = {"weight": 2.5}
680
+
681
+ g2 = g1.copy()
682
+ if mod1:
683
+ if not g1.is_directed():
684
+ g2._adj[1][0] = data1
685
+ g2._adj[0][1] = data1
686
+ else:
687
+ g2._succ[1][0] = data1
688
+ g2._pred[0][1] = data1
689
+ if mod2:
690
+ if not g1.is_directed():
691
+ g2._adj[0][0] = data2
692
+ else:
693
+ g2._succ[0][0] = data2
694
+ g2._pred[0][0] = data2
695
+
696
+ assert not is_isomorphic(g1, g2, edge_match=em)
697
+
698
+
699
+ @pytest.mark.parametrize("graph_class", graph_classes)
700
+ def test_weightkey(graph_class):
701
+ g1 = graph_class()
702
+ g2 = graph_class()
703
+ if g1.is_multigraph():
704
+ edge_match = iso.numerical_multiedge_match
705
+ else:
706
+ edge_match = iso.numerical_edge_match
707
+
708
+ g1.add_edge("A", "B", weight=1)
709
+ g2.add_edge("C", "D", weight=0)
710
+
711
+ assert nx.is_isomorphic(g1, g2)
712
+ em = edge_match("nonexistent attribute", 1)
713
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
714
+ em = edge_match("weight", 1)
715
+ assert not nx.is_isomorphic(g1, g2, edge_match=em)
716
+
717
+ g2 = graph_class()
718
+ g2.add_edge("C", "D")
719
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphism.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import partial
2
+
3
+ import pytest
4
+
5
+ import networkx as nx
6
+ from networkx.algorithms import isomorphism as iso
7
+
8
+ # Convenience functions for testing that the behavior of `could_be_isomorphic`
9
+ # with the "properties" kwarg is equivalent to the corresponding function (i.e.
10
+ # nx.fast_could_be_isomorphic or nx.faster_could_be_isomorphic)
11
+ fast_cbi = partial(nx.could_be_isomorphic, properties="dt")
12
+ faster_cbi = partial(nx.could_be_isomorphic, properties="d")
13
+
14
+
15
+ def test_graph_could_be_isomorphic_variants_deprecated():
16
+ G1 = nx.Graph([(1, 2), (1, 3), (1, 5), (2, 3)])
17
+ G2 = nx.Graph([(10, 20), (20, 30), (10, 30), (10, 50)])
18
+ with pytest.deprecated_call(): # graph_could_be_isomorphic
19
+ result = nx.isomorphism.isomorph.graph_could_be_isomorphic(G1, G2)
20
+ assert nx.could_be_isomorphic(G1, G2) == result
21
+ with pytest.deprecated_call(): # fast_graph_could_be_isomorphic
22
+ result = nx.isomorphism.isomorph.fast_graph_could_be_isomorphic(G1, G2)
23
+ assert nx.fast_could_be_isomorphic(G1, G2) == result
24
+ with pytest.deprecated_call():
25
+ result = nx.isomorphism.isomorph.faster_graph_could_be_isomorphic(G1, G2)
26
+ assert nx.faster_could_be_isomorphic(G1, G2) == result
27
+
28
+
29
+ @pytest.mark.parametrize("atlas_ids", [(699, 706), (864, 870)])
30
+ def test_could_be_isomorphic_combined_properties(atlas_ids):
31
+ """There are two pairs of graphs from the graph atlas that have the same
32
+ combined degree-triangle distribution, but a different maximal clique
33
+ distribution. See gh-7852."""
34
+ G, H = (nx.graph_atlas(idx) for idx in atlas_ids)
35
+
36
+ assert not nx.is_isomorphic(G, H)
37
+
38
+ # Degree only
39
+ assert nx.faster_could_be_isomorphic(G, H)
40
+ assert nx.could_be_isomorphic(G, H, properties="d")
41
+ # Degrees & triangles
42
+ assert nx.fast_could_be_isomorphic(G, H)
43
+ assert nx.could_be_isomorphic(G, H, properties="dt")
44
+ # Full properties table (degrees, triangles, cliques)
45
+ assert not nx.could_be_isomorphic(G, H)
46
+ assert not nx.could_be_isomorphic(G, H, properties="dtc")
47
+ # For these two cases, the clique distribution alone is enough to verify
48
+ # the graphs can't be isomorphic
49
+ assert not nx.could_be_isomorphic(G, H, properties="c")
50
+
51
+
52
+ def test_could_be_isomorphic_individual_vs_combined_dt():
53
+ """A test case where G and H have identical degree and triangle distributions,
54
+ but are different when compared together"""
55
+ G = nx.Graph([(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (3, 4), (4, 5), (4, 6)])
56
+ H = G.copy()
57
+ # Modify graphs to produce different clique distributions
58
+ G.add_edge(0, 7)
59
+ H.add_edge(4, 7)
60
+ assert nx.could_be_isomorphic(G, H, properties="d")
61
+ assert nx.could_be_isomorphic(G, H, properties="t")
62
+ assert not nx.could_be_isomorphic(G, H, properties="dt")
63
+ assert not nx.could_be_isomorphic(G, H, properties="c")
64
+
65
+
66
+ class TestIsomorph:
67
+ @classmethod
68
+ def setup_class(cls):
69
+ cls.G1 = nx.Graph([[1, 2], [1, 3], [1, 5], [2, 3]])
70
+ cls.G2 = nx.Graph([[10, 20], [20, 30], [10, 30], [10, 50]])
71
+ cls.G3 = nx.Graph([[1, 2], [1, 3], [1, 5], [2, 5]])
72
+ cls.G4 = nx.Graph([[1, 2], [1, 3], [1, 5], [2, 4]])
73
+ cls.G5 = nx.Graph([[1, 2], [1, 3]])
74
+ cls.G6 = nx.Graph([[10, 20], [20, 30], [10, 30], [10, 50], [20, 50]])
75
+
76
+ def test_could_be_isomorphic(self):
77
+ assert iso.could_be_isomorphic(self.G1, self.G2)
78
+ assert iso.could_be_isomorphic(self.G1, self.G3)
79
+ assert not iso.could_be_isomorphic(self.G1, self.G4)
80
+ assert iso.could_be_isomorphic(self.G3, self.G2)
81
+ assert not iso.could_be_isomorphic(self.G1, self.G6)
82
+
83
+ @pytest.mark.parametrize("fn", (iso.fast_could_be_isomorphic, fast_cbi))
84
+ def test_fast_could_be_isomorphic(self, fn):
85
+ assert fn(self.G3, self.G2)
86
+ assert not fn(self.G3, self.G5)
87
+ assert not fn(self.G1, self.G6)
88
+
89
+ @pytest.mark.parametrize("fn", (iso.faster_could_be_isomorphic, faster_cbi))
90
+ def test_faster_could_be_isomorphic(self, fn):
91
+ assert fn(self.G3, self.G2)
92
+ assert not fn(self.G3, self.G5)
93
+ assert not fn(self.G1, self.G6)
94
+
95
+ def test_is_isomorphic(self):
96
+ assert iso.is_isomorphic(self.G1, self.G2)
97
+ assert not iso.is_isomorphic(self.G1, self.G4)
98
+ assert iso.is_isomorphic(self.G1.to_directed(), self.G2.to_directed())
99
+ assert not iso.is_isomorphic(self.G1.to_directed(), self.G4.to_directed())
100
+ with pytest.raises(
101
+ nx.NetworkXError, match="Graphs G1 and G2 are not of the same type."
102
+ ):
103
+ iso.is_isomorphic(self.G1.to_directed(), self.G1)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for VF2 isomorphism algorithm.
3
+ """
4
+
5
+ import importlib.resources
6
+ import random
7
+ import struct
8
+
9
+ import pytest
10
+
11
+ import networkx as nx
12
+ from networkx.algorithms import isomorphism as iso
13
+
14
+
15
+ class TestWikipediaExample:
16
+ # Source: https://en.wikipedia.org/wiki/Graph_isomorphism
17
+
18
+ # Nodes 'a', 'b', 'c' and 'd' form a column.
19
+ # Nodes 'g', 'h', 'i' and 'j' form a column.
20
+ # isomorphism: {'a': 1, 'g': 2, 'b': 3, 'c': 6, 'h': 4, 'i': 5, 'j': 7, 'd': 8}
21
+ g1edges = [
22
+ ["a", "g"],
23
+ ["a", "h"],
24
+ ["a", "i"],
25
+ ["b", "g"],
26
+ ["b", "h"],
27
+ ["b", "j"],
28
+ ["c", "g"],
29
+ ["c", "i"],
30
+ ["c", "j"],
31
+ ["d", "h"],
32
+ ["d", "i"],
33
+ ["d", "j"],
34
+ ]
35
+
36
+ # Nodes 1,2,3,4 form the clockwise corners of a large square.
37
+ # Nodes 5,6,7,8 form the clockwise corners of a small square
38
+ g2edges = [
39
+ [1, 2],
40
+ [3, 2],
41
+ [3, 4],
42
+ [1, 4],
43
+ [6, 5],
44
+ [6, 7],
45
+ [8, 7],
46
+ [8, 5],
47
+ [1, 5],
48
+ [6, 2],
49
+ [3, 7],
50
+ [8, 4],
51
+ ]
52
+
53
+ @pytest.mark.parametrize(
54
+ ["graph_class", "numb_maps"], [(nx.Graph, 48), (nx.DiGraph, 24)]
55
+ )
56
+ def test_morphic_and_number_of_mappings(self, graph_class, numb_maps):
57
+ g1 = graph_class(self.g1edges)
58
+ g2 = graph_class(self.g2edges)
59
+ Matcher = iso.DiGraphMatcher if g1.is_directed() else iso.GraphMatcher
60
+ gm = Matcher(g1, g2)
61
+ assert gm.is_isomorphic()
62
+ assert gm.subgraph_is_monomorphic()
63
+ assert gm.subgraph_is_isomorphic()
64
+
65
+ mapping = list(gm.mapping.items())
66
+ # this mapping is only one of the 48 possibilities
67
+ all_mappings = list(gm.isomorphisms_iter())
68
+ assert len(all_mappings) == numb_maps
69
+ assert dict(mapping) in all_mappings
70
+
71
+ @pytest.mark.parametrize("graph_class", [nx.Graph, nx.DiGraph])
72
+ def test_subgraph(self, graph_class):
73
+ g1 = graph_class(self.g1edges)
74
+ g2 = graph_class(self.g2edges)
75
+ g3 = g2.subgraph([1, 2, 3, 4])
76
+ Matcher = iso.DiGraphMatcher if g1.is_directed() else iso.GraphMatcher
77
+ gm = Matcher(g1, g3)
78
+ assert not gm.is_isomorphic()
79
+ assert gm.subgraph_is_isomorphic()
80
+ assert gm.subgraph_is_monomorphic()
81
+
82
+ @pytest.mark.parametrize("graph_class", [nx.Graph, nx.DiGraph])
83
+ def test_subgraph_mono(self, graph_class):
84
+ g1 = graph_class(self.g1edges)
85
+ g2 = graph_class(["ag", "cg", "ci", "di", "bg"])
86
+ Matcher = iso.DiGraphMatcher if g1.is_directed() else iso.GraphMatcher
87
+ gm = Matcher(g1, g2)
88
+ assert not gm.is_isomorphic()
89
+ assert not gm.subgraph_is_isomorphic()
90
+ assert gm.subgraph_is_monomorphic()
91
+
92
+ # note: this shows need to recreate the matcher for each graph change
93
+ # Also checking not monomorphic
94
+ g2.add_edge("c", "a")
95
+ gm = Matcher(g1, g2)
96
+ assert not gm.subgraph_is_monomorphic()
97
+
98
+
99
+ class TestVF2GraphDB:
100
+ # https://web.archive.org/web/20090303210205/http://amalfi.dis.unina.it/graph/db/
101
+
102
+ @staticmethod
103
+ def create_graph(filename):
104
+ """Creates a Graph instance from the filename."""
105
+
106
+ # The file is assumed to be in the format from the VF2 graph database.
107
+ # Each file is composed of 16-bit numbers (unsigned short int).
108
+ # So we will want to read 2 bytes at a time.
109
+
110
+ # We can read the number as follows:
111
+ # number = struct.unpack('<H', file.read(2))
112
+ # This says, expect the data in little-endian encoding
113
+ # as an unsigned short int and unpack 2 bytes from the file.
114
+
115
+ fh = open(filename, mode="rb")
116
+
117
+ # Grab the number of nodes.
118
+ # Node numeration is 0-based, so the first node has index 0.
119
+ nodes = struct.unpack("<H", fh.read(2))[0]
120
+
121
+ graph = nx.Graph()
122
+ for from_node in range(nodes):
123
+ # Get the number of edges.
124
+ edges = struct.unpack("<H", fh.read(2))[0]
125
+ for edge in range(edges):
126
+ # Get the terminal node.
127
+ to_node = struct.unpack("<H", fh.read(2))[0]
128
+ graph.add_edge(from_node, to_node)
129
+
130
+ fh.close()
131
+ return graph
132
+
133
+ def test_graph(self):
134
+ head = importlib.resources.files("networkx.algorithms.isomorphism.tests")
135
+ g1 = self.create_graph(head / "iso_r01_s80.A99")
136
+ g2 = self.create_graph(head / "iso_r01_s80.B99")
137
+ gm = iso.GraphMatcher(g1, g2)
138
+ assert gm.is_isomorphic()
139
+
140
+ def test_subgraph(self):
141
+ # A is the subgraph
142
+ # B is the full graph
143
+ head = importlib.resources.files("networkx.algorithms.isomorphism.tests")
144
+ subgraph = self.create_graph(head / "si2_b06_m200.A99")
145
+ graph = self.create_graph(head / "si2_b06_m200.B99")
146
+ gm = iso.GraphMatcher(graph, subgraph)
147
+ assert not gm.is_isomorphic()
148
+ assert gm.subgraph_is_isomorphic()
149
+ assert gm.subgraph_is_monomorphic()
150
+
151
+ def test_subgraph_monomorphic(self):
152
+ # A is the subgraph
153
+ # B is the full graph
154
+ head = importlib.resources.files("networkx.algorithms.isomorphism.tests")
155
+ subgraph = self.create_graph(head / "si2_b06_m200.A99")
156
+ graph = self.create_graph(head / "si2_b06_m200.B99")
157
+ subgraph.remove_edge(29, 28)
158
+ gm = iso.GraphMatcher(graph, subgraph)
159
+ assert not gm.is_isomorphic()
160
+ assert not gm.subgraph_is_isomorphic()
161
+ assert gm.subgraph_is_monomorphic()
162
+
163
+ # now add new edge to make sg no longer monomorphic
164
+ subgraph.add_edge(29, 2)
165
+ assert not gm.subgraph_is_monomorphic()
166
+
167
+
168
+ class TestAtlas:
169
+ @classmethod
170
+ def setup_class(cls):
171
+ global atlas
172
+ from networkx.generators import atlas
173
+
174
+ cls.GAG = atlas.graph_atlas_g()
175
+
176
+ def test_graph_atlas(self):
177
+ rng = random.Random(42)
178
+ # Atlas = nx.graph_atlas_g()[0:208] # 208, 6 nodes or less
179
+ Atlas = self.GAG[0:100]
180
+ alphabet = list(range(26))
181
+ for graph in Atlas:
182
+ nlist = list(graph)
183
+ labels = alphabet[: len(nlist)]
184
+ for _ in range(10):
185
+ rng.shuffle(labels)
186
+ d = dict(zip(nlist, labels))
187
+ relabel = nx.relabel_nodes(graph, d)
188
+ gm = iso.GraphMatcher(graph, relabel)
189
+ assert gm.is_isomorphic()
190
+
191
+
192
+ def test_multiedge():
193
+ # Simple test for multigraphs
194
+ # Need something much more rigorous
195
+ edges = [
196
+ (0, 1),
197
+ (1, 2),
198
+ (2, 3),
199
+ (3, 4),
200
+ (4, 5),
201
+ (5, 6),
202
+ (6, 7),
203
+ (7, 8),
204
+ (8, 9),
205
+ (9, 10),
206
+ (10, 11),
207
+ (10, 11),
208
+ (11, 12),
209
+ (11, 12),
210
+ (12, 13),
211
+ (12, 13),
212
+ (13, 14),
213
+ (13, 14),
214
+ (14, 15),
215
+ (14, 15),
216
+ (15, 16),
217
+ (15, 16),
218
+ (16, 17),
219
+ (16, 17),
220
+ (17, 18),
221
+ (17, 18),
222
+ (18, 19),
223
+ (18, 19),
224
+ (19, 0),
225
+ (19, 0),
226
+ ]
227
+ nodes = list(range(20))
228
+
229
+ rng = random.Random(42)
230
+
231
+ for g1 in [nx.MultiGraph(), nx.MultiDiGraph()]:
232
+ g1.add_edges_from(edges)
233
+ for _ in range(10):
234
+ new_nodes = list(nodes)
235
+ rng.shuffle(new_nodes)
236
+ d = dict(zip(nodes, new_nodes))
237
+ g2 = nx.relabel_nodes(g1, d)
238
+ if not g1.is_directed():
239
+ gm = iso.GraphMatcher(g1, g2)
240
+ else:
241
+ gm = iso.DiGraphMatcher(g1, g2)
242
+ assert gm.is_isomorphic()
243
+ # Testing if monomorphism works in multigraphs
244
+ assert gm.subgraph_is_monomorphic()
245
+
246
+
247
+ @pytest.mark.parametrize("G1", [nx.Graph(), nx.MultiGraph()])
248
+ @pytest.mark.parametrize("G2", [nx.Graph(), nx.MultiGraph()])
249
+ def test_matcher_raises(G1, G2):
250
+ undirected_matchers = [iso.GraphMatcher, iso.MultiGraphMatcher]
251
+ directed_matchers = [iso.DiGraphMatcher, iso.MultiDiGraphMatcher]
252
+
253
+ for matcher in undirected_matchers:
254
+ matcher(G1, G2)
255
+
256
+ msg = r"\(Multi-\)GraphMatcher\(\) not defined for directed graphs"
257
+ with pytest.raises(nx.NetworkXError, match=msg):
258
+ matcher(G1.to_directed(), G2.to_directed())
259
+
260
+ for matcher in directed_matchers:
261
+ matcher(G1.to_directed(), G2.to_directed())
262
+
263
+ msg = r"\(Multi-\)DiGraphMatcher\(\) not defined for undirected graphs"
264
+ with pytest.raises(nx.NetworkXError, match=msg):
265
+ matcher(G1, G2)
266
+
267
+ for matcher in undirected_matchers + directed_matchers:
268
+ msg = r"G1 and G2 must have the same directedness"
269
+ with pytest.raises(nx.NetworkXError, match=msg):
270
+ matcher(G1, G2.to_directed())
271
+ with pytest.raises(nx.NetworkXError, match=msg):
272
+ matcher(G1.to_directed(), G2)
273
+
274
+
275
+ def test_selfloop():
276
+ # Simple test for graphs with selfloops
277
+ edges = [
278
+ (0, 1),
279
+ (0, 2),
280
+ (1, 2),
281
+ (1, 3),
282
+ (2, 2),
283
+ (2, 4),
284
+ (3, 1),
285
+ (3, 2),
286
+ (4, 2),
287
+ (4, 5),
288
+ (5, 4),
289
+ ]
290
+ nodes = list(range(6))
291
+
292
+ rng = random.Random(42)
293
+
294
+ for g1 in [nx.Graph(), nx.DiGraph()]:
295
+ g1.add_edges_from(edges)
296
+ for _ in range(100):
297
+ new_nodes = list(nodes)
298
+ rng.shuffle(new_nodes)
299
+ d = dict(zip(nodes, new_nodes))
300
+ g2 = nx.relabel_nodes(g1, d)
301
+ if not g1.is_directed():
302
+ gm = iso.GraphMatcher(g1, g2)
303
+ else:
304
+ gm = iso.DiGraphMatcher(g1, g2)
305
+ assert gm.is_isomorphic()
306
+
307
+
308
+ def test_selfloop_mono():
309
+ # Simple test for graphs with selfloops
310
+ edges0 = [
311
+ (0, 1),
312
+ (0, 2),
313
+ (1, 2),
314
+ (1, 3),
315
+ (2, 4),
316
+ (3, 1),
317
+ (3, 2),
318
+ (4, 2),
319
+ (4, 5),
320
+ (5, 4),
321
+ ]
322
+ edges = edges0 + [(2, 2)]
323
+ nodes = list(range(6))
324
+
325
+ rng = random.Random(42)
326
+
327
+ for g1 in [nx.Graph(), nx.DiGraph()]:
328
+ g1.add_edges_from(edges)
329
+ for _ in range(100):
330
+ new_nodes = list(nodes)
331
+ rng.shuffle(new_nodes)
332
+ d = dict(zip(nodes, new_nodes))
333
+ g2 = nx.relabel_nodes(g1, d)
334
+ g2.remove_edges_from(nx.selfloop_edges(g2))
335
+ if not g1.is_directed():
336
+ gm = iso.GraphMatcher(g2, g1)
337
+ else:
338
+ gm = iso.DiGraphMatcher(g2, g1)
339
+ assert not gm.subgraph_is_monomorphic()
340
+
341
+
342
+ def test_isomorphism_iter1():
343
+ # As described in:
344
+ # http://groups.google.com/group/networkx-discuss/browse_thread/thread/2ff65c67f5e3b99f/d674544ebea359bb?fwc=1
345
+ g1 = nx.DiGraph(["AB", "BC"])
346
+ g2 = nx.DiGraph(["YZ"])
347
+ g3 = nx.DiGraph(["ZY"])
348
+ gm12 = iso.DiGraphMatcher(g1, g2)
349
+ gm13 = iso.DiGraphMatcher(g1, g3)
350
+ x = list(gm12.subgraph_isomorphisms_iter())
351
+ y = list(gm13.subgraph_isomorphisms_iter())
352
+ assert {"A": "Y", "B": "Z"} in x
353
+ assert {"B": "Y", "C": "Z"} in x
354
+ assert {"A": "Z", "B": "Y"} in y
355
+ assert {"B": "Z", "C": "Y"} in y
356
+ assert len(x) == len(y)
357
+ assert len(x) == 2
358
+
359
+
360
+ def test_monomorphism_iter1():
361
+ g1 = nx.DiGraph(["AB", "BC", "CA"])
362
+ g2 = nx.DiGraph(["XY", "YZ"])
363
+ gm12 = iso.DiGraphMatcher(g1, g2)
364
+ x = list(gm12.subgraph_monomorphisms_iter())
365
+ assert {"A": "X", "B": "Y", "C": "Z"} in x
366
+ assert {"A": "Y", "B": "Z", "C": "X"} in x
367
+ assert {"A": "Z", "B": "X", "C": "Y"} in x
368
+ assert len(x) == 3
369
+ gm21 = iso.DiGraphMatcher(g2, g1)
370
+ # Check if StopIteration exception returns False
371
+ assert not gm21.subgraph_is_monomorphic()
372
+
373
+
374
+ def test_isomorphism_iter2():
375
+ # Path
376
+ for L in range(2, 10):
377
+ g1 = nx.path_graph(L)
378
+ gm = iso.GraphMatcher(g1, g1)
379
+ s = len(list(gm.isomorphisms_iter()))
380
+ assert s == 2
381
+ # Cycle
382
+ for L in range(3, 10):
383
+ g1 = nx.cycle_graph(L)
384
+ gm = iso.GraphMatcher(g1, g1)
385
+ s = len(list(gm.isomorphisms_iter()))
386
+ assert s == 2 * L
387
+
388
+
389
+ def test_multiple():
390
+ # Verify that we can use the graph matcher multiple times
391
+ edges = [("A", "B"), ("B", "A"), ("B", "C")]
392
+ for g1, g2 in [(nx.Graph(), nx.Graph()), (nx.DiGraph(), nx.DiGraph())]:
393
+ g1.add_edges_from(edges)
394
+ g2.add_edges_from(edges)
395
+ g3 = nx.subgraph(g2, ["A", "B"])
396
+ if not g1.is_directed():
397
+ gmA = iso.GraphMatcher(g1, g2)
398
+ gmB = iso.GraphMatcher(g1, g3)
399
+ else:
400
+ gmA = iso.DiGraphMatcher(g1, g2)
401
+ gmB = iso.DiGraphMatcher(g1, g3)
402
+ assert gmA.is_isomorphic()
403
+
404
+ g2.remove_node("C")
405
+ if not g1.is_directed():
406
+ gmA = iso.GraphMatcher(g1, g2)
407
+ else:
408
+ gmA = iso.DiGraphMatcher(g1, g2)
409
+ assert gmA.subgraph_is_isomorphic()
410
+ assert gmB.subgraph_is_isomorphic()
411
+ assert gmA.subgraph_is_monomorphic()
412
+ assert gmB.subgraph_is_monomorphic()
413
+
414
+
415
+ def test_noncomparable_nodes():
416
+ node1 = object()
417
+ node2 = object()
418
+ node3 = object()
419
+
420
+ # Graph
421
+ G = nx.path_graph([node1, node2, node3])
422
+ gm = iso.GraphMatcher(G, G)
423
+ assert gm.is_isomorphic()
424
+ assert gm.subgraph_is_monomorphic()
425
+
426
+ # DiGraph
427
+ G = nx.path_graph([node1, node2, node3], create_using=nx.DiGraph)
428
+ H = nx.path_graph([node3, node2, node1], create_using=nx.DiGraph)
429
+ dgm = iso.DiGraphMatcher(G, H)
430
+ assert dgm.is_isomorphic()
431
+ assert dgm.subgraph_is_monomorphic()
432
+
433
+
434
+ def test_monomorphism_edge_match():
435
+ G = nx.DiGraph()
436
+ G.add_node(1)
437
+ G.add_node(2)
438
+ G.add_edge(1, 2, label="A")
439
+ G.add_edge(2, 1, label="B")
440
+ G.add_edge(2, 2, label="C")
441
+
442
+ SG = nx.DiGraph()
443
+ SG.add_node(5)
444
+ SG.add_node(6)
445
+ SG.add_edge(5, 6, label="A")
446
+
447
+ gm = iso.DiGraphMatcher(G, SG, edge_match=iso.categorical_edge_match("label", None))
448
+ assert gm.subgraph_is_monomorphic()
449
+ G.edges[1, 2]["label"] = None
450
+ assert not gm.subgraph_is_monomorphic()
451
+
452
+
453
+ @pytest.mark.parametrize(
454
+ ("e1", "e2", "isomorphic", "subgraph_is_isomorphic"),
455
+ [
456
+ ([(0, 1), (0, 2)], [(0, 1), (0, 2), (1, 2)], False, False),
457
+ ([(0, 1), (0, 2)], [(0, 1), (0, 2), (2, 1)], False, False),
458
+ ([(0, 1), (1, 2)], [(0, 1), (1, 2), (2, 0)], False, False),
459
+ ([(0, 1)], [(0, 1), (1, 2), (2, 0)], False, False),
460
+ ([(0, 1), (1, 2), (2, 0)], [(0, 1)], False, True),
461
+ ([(0, 1), (1, 2)], [(0, 1), (2, 0), (2, 1)], False, False),
462
+ ([(0, 1), (0, 2), (1, 2)], [(0, 1), (0, 2), (2, 1)], True, True),
463
+ ([(0, 1), (0, 2), (1, 2)], [(0, 1), (1, 2), (2, 0)], False, False),
464
+ (
465
+ [(0, 0), (0, 1), (1, 2), (2, 1)],
466
+ [(0, 1), (1, 0), (1, 2), (2, 0)],
467
+ False,
468
+ False,
469
+ ),
470
+ (
471
+ [(0, 0), (1, 0), (1, 2), (2, 1)],
472
+ [(0, 1), (0, 2), (1, 0), (1, 2)],
473
+ False,
474
+ False,
475
+ ),
476
+ (
477
+ [(0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1), (2, 2)],
478
+ [(0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)],
479
+ False,
480
+ False,
481
+ ),
482
+ ],
483
+ )
484
+ def test_three_node(e1, e2, isomorphic, subgraph_is_isomorphic):
485
+ """Test some edge cases distilled from arbitrary search of the input space."""
486
+ G1 = nx.DiGraph(e1)
487
+ G2 = nx.DiGraph(e2)
488
+ gm = iso.DiGraphMatcher(G1, G2)
489
+ assert gm.is_isomorphic() == isomorphic
490
+ assert gm.subgraph_is_isomorphic() == subgraph_is_isomorphic
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_match_helpers.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from operator import eq
2
+
3
+ import networkx as nx
4
+ from networkx.algorithms import isomorphism as iso
5
+
6
+
7
+ def test_categorical_node_match():
8
+ nm = iso.categorical_node_match(["x", "y", "z"], [None] * 3)
9
+ assert nm({"x": 1, "y": 2, "z": 3}, {"x": 1, "y": 2, "z": 3})
10
+ assert not nm({"x": 1, "y": 2, "z": 2}, {"x": 1, "y": 2, "z": 1})
11
+
12
+
13
+ class TestGenericMultiEdgeMatch:
14
+ def setup_method(self):
15
+ self.G1 = nx.MultiDiGraph()
16
+ self.G2 = nx.MultiDiGraph()
17
+ self.G3 = nx.MultiDiGraph()
18
+ self.G4 = nx.MultiDiGraph()
19
+ attr_dict1 = {"id": "edge1", "minFlow": 0, "maxFlow": 10}
20
+ attr_dict2 = {"id": "edge2", "minFlow": -3, "maxFlow": 7}
21
+ attr_dict3 = {"id": "edge3", "minFlow": 13, "maxFlow": 117}
22
+ attr_dict4 = {"id": "edge4", "minFlow": 13, "maxFlow": 117}
23
+ attr_dict5 = {"id": "edge5", "minFlow": 8, "maxFlow": 12}
24
+ attr_dict6 = {"id": "edge6", "minFlow": 8, "maxFlow": 12}
25
+ for attr_dict in [
26
+ attr_dict1,
27
+ attr_dict2,
28
+ attr_dict3,
29
+ attr_dict4,
30
+ attr_dict5,
31
+ attr_dict6,
32
+ ]:
33
+ self.G1.add_edge(1, 2, **attr_dict)
34
+ for attr_dict in [
35
+ attr_dict5,
36
+ attr_dict3,
37
+ attr_dict6,
38
+ attr_dict1,
39
+ attr_dict4,
40
+ attr_dict2,
41
+ ]:
42
+ self.G2.add_edge(2, 3, **attr_dict)
43
+ for attr_dict in [attr_dict3, attr_dict5]:
44
+ self.G3.add_edge(3, 4, **attr_dict)
45
+ for attr_dict in [attr_dict6, attr_dict4]:
46
+ self.G4.add_edge(4, 5, **attr_dict)
47
+
48
+ def test_generic_multiedge_match(self):
49
+ full_match = iso.generic_multiedge_match(
50
+ ["id", "flowMin", "flowMax"], [None] * 3, [eq] * 3
51
+ )
52
+ flow_match = iso.generic_multiedge_match(
53
+ ["flowMin", "flowMax"], [None] * 2, [eq] * 2
54
+ )
55
+ min_flow_match = iso.generic_multiedge_match("flowMin", None, eq)
56
+ id_match = iso.generic_multiedge_match("id", None, eq)
57
+ assert flow_match(self.G1[1][2], self.G2[2][3])
58
+ assert min_flow_match(self.G1[1][2], self.G2[2][3])
59
+ assert id_match(self.G1[1][2], self.G2[2][3])
60
+ assert full_match(self.G1[1][2], self.G2[2][3])
61
+ assert flow_match(self.G3[3][4], self.G4[4][5])
62
+ assert min_flow_match(self.G3[3][4], self.G4[4][5])
63
+ assert not id_match(self.G3[3][4], self.G4[4][5])
64
+ assert not full_match(self.G3[3][4], self.G4[4][5])
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for the temporal aspect of the Temporal VF2 isomorphism algorithm.
3
+ """
4
+
5
+ from datetime import date, datetime, timedelta
6
+
7
+ import networkx as nx
8
+ from networkx.algorithms import isomorphism as iso
9
+
10
+
11
+ def provide_g1_edgelist():
12
+ return [(0, 1), (0, 2), (1, 2), (2, 4), (1, 3), (3, 4), (4, 5)]
13
+
14
+
15
+ def put_same_time(G, att_name):
16
+ for e in G.edges(data=True):
17
+ e[2][att_name] = date(2015, 1, 1)
18
+ return G
19
+
20
+
21
+ def put_same_datetime(G, att_name):
22
+ for e in G.edges(data=True):
23
+ e[2][att_name] = datetime(2015, 1, 1)
24
+ return G
25
+
26
+
27
+ def put_sequence_time(G, att_name):
28
+ current_date = date(2015, 1, 1)
29
+ for e in G.edges(data=True):
30
+ current_date += timedelta(days=1)
31
+ e[2][att_name] = current_date
32
+ return G
33
+
34
+
35
+ def put_time_config_0(G, att_name):
36
+ G[0][1][att_name] = date(2015, 1, 2)
37
+ G[0][2][att_name] = date(2015, 1, 2)
38
+ G[1][2][att_name] = date(2015, 1, 3)
39
+ G[1][3][att_name] = date(2015, 1, 1)
40
+ G[2][4][att_name] = date(2015, 1, 1)
41
+ G[3][4][att_name] = date(2015, 1, 3)
42
+ G[4][5][att_name] = date(2015, 1, 3)
43
+ return G
44
+
45
+
46
+ def put_time_config_1(G, att_name):
47
+ G[0][1][att_name] = date(2015, 1, 2)
48
+ G[0][2][att_name] = date(2015, 1, 1)
49
+ G[1][2][att_name] = date(2015, 1, 3)
50
+ G[1][3][att_name] = date(2015, 1, 1)
51
+ G[2][4][att_name] = date(2015, 1, 2)
52
+ G[3][4][att_name] = date(2015, 1, 4)
53
+ G[4][5][att_name] = date(2015, 1, 3)
54
+ return G
55
+
56
+
57
+ def put_time_config_2(G, att_name):
58
+ G[0][1][att_name] = date(2015, 1, 1)
59
+ G[0][2][att_name] = date(2015, 1, 1)
60
+ G[1][2][att_name] = date(2015, 1, 3)
61
+ G[1][3][att_name] = date(2015, 1, 2)
62
+ G[2][4][att_name] = date(2015, 1, 2)
63
+ G[3][4][att_name] = date(2015, 1, 3)
64
+ G[4][5][att_name] = date(2015, 1, 2)
65
+ return G
66
+
67
+
68
+ class TestTimeRespectingGraphMatcher:
69
+ """
70
+ A test class for the undirected temporal graph matcher.
71
+ """
72
+
73
+ def provide_g1_topology(self):
74
+ G1 = nx.Graph()
75
+ G1.add_edges_from(provide_g1_edgelist())
76
+ return G1
77
+
78
+ def provide_g2_path_3edges(self):
79
+ G2 = nx.Graph()
80
+ G2.add_edges_from([(0, 1), (1, 2), (2, 3)])
81
+ return G2
82
+
83
+ def test_timdelta_zero_timeRespecting_returnsTrue(self):
84
+ G1 = self.provide_g1_topology()
85
+ temporal_name = "date"
86
+ G1 = put_same_time(G1, temporal_name)
87
+ G2 = self.provide_g2_path_3edges()
88
+ d = timedelta()
89
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
90
+ assert gm.subgraph_is_isomorphic()
91
+
92
+ def test_timdelta_zero_datetime_timeRespecting_returnsTrue(self):
93
+ G1 = self.provide_g1_topology()
94
+ temporal_name = "date"
95
+ G1 = put_same_datetime(G1, temporal_name)
96
+ G2 = self.provide_g2_path_3edges()
97
+ d = timedelta()
98
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
99
+ assert gm.subgraph_is_isomorphic()
100
+
101
+ def test_attNameStrange_timdelta_zero_timeRespecting_returnsTrue(self):
102
+ G1 = self.provide_g1_topology()
103
+ temporal_name = "strange_name"
104
+ G1 = put_same_time(G1, temporal_name)
105
+ G2 = self.provide_g2_path_3edges()
106
+ d = timedelta()
107
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
108
+ assert gm.subgraph_is_isomorphic()
109
+
110
+ def test_notTimeRespecting_returnsFalse(self):
111
+ G1 = self.provide_g1_topology()
112
+ temporal_name = "date"
113
+ G1 = put_sequence_time(G1, temporal_name)
114
+ G2 = self.provide_g2_path_3edges()
115
+ d = timedelta()
116
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
117
+ assert not gm.subgraph_is_isomorphic()
118
+
119
+ def test_timdelta_one_config0_returns_no_embeddings(self):
120
+ G1 = self.provide_g1_topology()
121
+ temporal_name = "date"
122
+ G1 = put_time_config_0(G1, temporal_name)
123
+ G2 = self.provide_g2_path_3edges()
124
+ d = timedelta(days=1)
125
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
126
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
127
+ assert count_match == 0
128
+
129
+ def test_timdelta_one_config1_returns_four_embedding(self):
130
+ G1 = self.provide_g1_topology()
131
+ temporal_name = "date"
132
+ G1 = put_time_config_1(G1, temporal_name)
133
+ G2 = self.provide_g2_path_3edges()
134
+ d = timedelta(days=1)
135
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
136
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
137
+ assert count_match == 4
138
+
139
+ def test_timdelta_one_config2_returns_ten_embeddings(self):
140
+ G1 = self.provide_g1_topology()
141
+ temporal_name = "date"
142
+ G1 = put_time_config_2(G1, temporal_name)
143
+ G2 = self.provide_g2_path_3edges()
144
+ d = timedelta(days=1)
145
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
146
+ L = list(gm.subgraph_isomorphisms_iter())
147
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
148
+ assert count_match == 10
149
+
150
+
151
+ class TestDiTimeRespectingGraphMatcher:
152
+ """
153
+ A test class for the directed time-respecting graph matcher.
154
+ """
155
+
156
+ def provide_g1_topology(self):
157
+ G1 = nx.DiGraph()
158
+ G1.add_edges_from(provide_g1_edgelist())
159
+ return G1
160
+
161
+ def provide_g2_path_3edges(self):
162
+ G2 = nx.DiGraph()
163
+ G2.add_edges_from([(0, 1), (1, 2), (2, 3)])
164
+ return G2
165
+
166
+ def test_timdelta_zero_same_dates_returns_true(self):
167
+ G1 = self.provide_g1_topology()
168
+ temporal_name = "date"
169
+ G1 = put_same_time(G1, temporal_name)
170
+ G2 = self.provide_g2_path_3edges()
171
+ d = timedelta()
172
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
173
+ assert gm.subgraph_is_isomorphic()
174
+
175
+ def test_attNameStrange_timdelta_zero_same_dates_returns_true(self):
176
+ G1 = self.provide_g1_topology()
177
+ temporal_name = "strange"
178
+ G1 = put_same_time(G1, temporal_name)
179
+ G2 = self.provide_g2_path_3edges()
180
+ d = timedelta()
181
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
182
+ assert gm.subgraph_is_isomorphic()
183
+
184
+ def test_timdelta_one_config0_returns_no_embeddings(self):
185
+ G1 = self.provide_g1_topology()
186
+ temporal_name = "date"
187
+ G1 = put_time_config_0(G1, temporal_name)
188
+ G2 = self.provide_g2_path_3edges()
189
+ d = timedelta(days=1)
190
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
191
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
192
+ assert count_match == 0
193
+
194
+ def test_timdelta_one_config1_returns_one_embedding(self):
195
+ G1 = self.provide_g1_topology()
196
+ temporal_name = "date"
197
+ G1 = put_time_config_1(G1, temporal_name)
198
+ G2 = self.provide_g2_path_3edges()
199
+ d = timedelta(days=1)
200
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
201
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
202
+ assert count_match == 1
203
+
204
+ def test_timdelta_one_config2_returns_two_embeddings(self):
205
+ G1 = self.provide_g1_topology()
206
+ temporal_name = "date"
207
+ G1 = put_time_config_2(G1, temporal_name)
208
+ G2 = self.provide_g2_path_3edges()
209
+ d = timedelta(days=1)
210
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
211
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
212
+ assert count_match == 2
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import time
3
+
4
+ import pytest
5
+
6
+ import networkx as nx
7
+
8
+
9
+ def _check_isomorphism(t1, t2, isomorphism):
10
+ assert nx.is_directed(t1) == nx.is_directed(t2)
11
+ # Apply mapping and check for equality
12
+ H = nx.relabel_nodes(t1, dict(isomorphism))
13
+ return nx.utils.graphs_equal(t2, H)
14
+
15
+
16
+ @pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph))
17
+ def test_tree_isomorphism_raises_on_directed_and_multigraphs(graph_constructor):
18
+ t1 = graph_constructor([(0, 1)])
19
+ t2 = graph_constructor([(1, 2)])
20
+ with pytest.raises(nx.NetworkXNotImplemented):
21
+ nx.isomorphism.tree_isomorphism(t1, t2)
22
+
23
+
24
+ def test_input_not_tree():
25
+ tree = nx.Graph([(0, 1), (0, 2)])
26
+ not_tree = nx.cycle_graph(3)
27
+
28
+ # tree_isomorphism
29
+ with pytest.raises(nx.NetworkXError, match="t1 is not a tree"):
30
+ nx.isomorphism.tree_isomorphism(not_tree, tree)
31
+ with pytest.raises(nx.NetworkXError, match="t2 is not a tree"):
32
+ nx.isomorphism.tree_isomorphism(tree, not_tree)
33
+
34
+ # rooted_tree_isomorphism
35
+ with pytest.raises(nx.NetworkXError, match="t1 is not a tree"):
36
+ nx.isomorphism.rooted_tree_isomorphism(not_tree, 0, tree, 0)
37
+ with pytest.raises(nx.NetworkXError, match="t2 is not a tree"):
38
+ nx.isomorphism.rooted_tree_isomorphism(tree, 0, not_tree, 0)
39
+
40
+
41
+ def test_hardcoded():
42
+ # define a test problem
43
+ edges_1 = [
44
+ ("a", "b"),
45
+ ("a", "c"),
46
+ ("a", "d"),
47
+ ("b", "e"),
48
+ ("b", "f"),
49
+ ("e", "j"),
50
+ ("e", "k"),
51
+ ("c", "g"),
52
+ ("c", "h"),
53
+ ("g", "m"),
54
+ ("d", "i"),
55
+ ("f", "l"),
56
+ ]
57
+
58
+ edges_2 = [
59
+ ("v", "y"),
60
+ ("v", "z"),
61
+ ("u", "x"),
62
+ ("q", "u"),
63
+ ("q", "v"),
64
+ ("p", "t"),
65
+ ("n", "p"),
66
+ ("n", "q"),
67
+ ("n", "o"),
68
+ ("o", "r"),
69
+ ("o", "s"),
70
+ ("s", "w"),
71
+ ]
72
+
73
+ # there are two possible correct isomorphisms
74
+ # it currently returns isomorphism1
75
+ # but the second is also correct
76
+ isomorphism1 = [
77
+ ("a", "n"),
78
+ ("b", "q"),
79
+ ("c", "o"),
80
+ ("d", "p"),
81
+ ("e", "v"),
82
+ ("f", "u"),
83
+ ("g", "s"),
84
+ ("h", "r"),
85
+ ("i", "t"),
86
+ ("j", "y"),
87
+ ("k", "z"),
88
+ ("l", "x"),
89
+ ("m", "w"),
90
+ ]
91
+
92
+ # could swap y and z
93
+ isomorphism2 = [
94
+ ("a", "n"),
95
+ ("b", "q"),
96
+ ("c", "o"),
97
+ ("d", "p"),
98
+ ("e", "v"),
99
+ ("f", "u"),
100
+ ("g", "s"),
101
+ ("h", "r"),
102
+ ("i", "t"),
103
+ ("j", "z"),
104
+ ("k", "y"),
105
+ ("l", "x"),
106
+ ("m", "w"),
107
+ ]
108
+
109
+ t1 = nx.Graph()
110
+ t1.add_edges_from(edges_1)
111
+ root1 = "a"
112
+
113
+ t2 = nx.Graph()
114
+ t2.add_edges_from(edges_2)
115
+ root2 = "n"
116
+
117
+ isomorphism = sorted(nx.isomorphism.rooted_tree_isomorphism(t1, root1, t2, root2))
118
+
119
+ # is correct by hand
120
+ assert isomorphism in (isomorphism1, isomorphism2)
121
+
122
+ # check algorithmically
123
+ assert _check_isomorphism(t1, t2, isomorphism)
124
+
125
+ # try again as digraph
126
+ t1 = nx.DiGraph()
127
+ t1.add_edges_from(edges_1)
128
+ root1 = "a"
129
+
130
+ t2 = nx.DiGraph()
131
+ t2.add_edges_from(edges_2)
132
+ root2 = "n"
133
+
134
+ isomorphism = sorted(nx.isomorphism.rooted_tree_isomorphism(t1, root1, t2, root2))
135
+
136
+ # is correct by hand
137
+ assert isomorphism in (isomorphism1, isomorphism2)
138
+
139
+ # check algorithmically
140
+ assert _check_isomorphism(t1, t2, isomorphism)
141
+
142
+
143
+ # NOTE: number of nonisomorphic_trees grows very rapidly - do not increase n
144
+ # further without marking "slow"
145
+ @pytest.mark.parametrize("n", range(2, 15))
146
+ def test_tree_isomorphic_all_non_isomorphic_trees_relabeled(n):
147
+ """Tests every non-isomorphic tree with `n` nodes is isomorphic with a
148
+ copy of itself with an arbitrary node-remapping."""
149
+ for tree in nx.nonisomorphic_trees(n):
150
+ nodes = list(tree)
151
+ random.shuffle(shuffled := nodes.copy())
152
+ node_mapping = dict(zip(nodes, shuffled))
153
+ # Shuffle the edge list to ensure no dependence on edge order
154
+ random.shuffle(
155
+ new_edges := [
156
+ # Randomly order edges to ensure no dependence on node order within edges
157
+ (node_mapping[u], node_mapping[v])
158
+ if random.randint(0, 1)
159
+ else (node_mapping[v], node_mapping[u])
160
+ for (u, v) in tree.edges
161
+ ]
162
+ )
163
+ relabeled = nx.Graph(new_edges)
164
+ # Does not necessarily have to be the same as node_mapping
165
+ iso_mapping = nx.isomorphism.tree_isomorphism(tree, relabeled)
166
+
167
+ assert iso_mapping != []
168
+ assert _check_isomorphism(tree, relabeled, iso_mapping)
169
+
170
+
171
+ def test_trivial_rooted_tree_isomorphism():
172
+ t1 = nx.Graph()
173
+ t1.add_node("a")
174
+
175
+ t2 = nx.Graph()
176
+ t2.add_node("n")
177
+
178
+ assert nx.isomorphism.rooted_tree_isomorphism(t1, "a", t2, "n") == [("a", "n")]
179
+
180
+
181
+ def test_rooted_tree_isomorphism_different_order():
182
+ t1 = nx.Graph([("a", "b"), ("a", "c")])
183
+ t2 = nx.Graph([("a", "b")])
184
+ assert nx.isomorphism.tree_isomorphism(t1, t2) == []
185
+
186
+
187
+ # NOTE: number of nonisomorphic_trees grows very rapidly - do not increase n
188
+ # further without marking "slow"
189
+ @pytest.mark.parametrize("n", range(4, 12))
190
+ def test_tree_isomorphism_all_non_isomorphic_pairs(n):
191
+ test_trees = list(nx.nonisomorphic_trees(n))
192
+ assert all(
193
+ nx.isomorphism.tree_isomorphism(test_trees[i], test_trees[j]) == []
194
+ for i in range(len(test_trees) - 1)
195
+ for j in range(i + 1, len(test_trees))
196
+ )
197
+
198
+
199
+ def test_long_paths_graphs():
200
+ """Smoke test for potential RecursionError. See gh-7945."""
201
+ G = nx.path_graph(10_000)
202
+ nx.isomorphism.rooted_tree_isomorphism(G, 0, G, 0) == [(n, n) for n in G]
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp.py ADDED
@@ -0,0 +1,1655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import itertools as it
2
+
3
+ import pytest
4
+
5
+ import networkx as nx
6
+ from networkx import vf2pp_all_isomorphisms, vf2pp_is_isomorphic, vf2pp_isomorphism
7
+
8
+ labels_same = ["blue"]
9
+
10
+ labels_many = [
11
+ "white",
12
+ "red",
13
+ "blue",
14
+ "green",
15
+ "orange",
16
+ "black",
17
+ "purple",
18
+ "yellow",
19
+ "brown",
20
+ "cyan",
21
+ "solarized",
22
+ "pink",
23
+ "none",
24
+ ]
25
+
26
+ graph_classes = [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
27
+
28
+
29
+ class TestPreCheck:
30
+ def test_first_graph_empty(self):
31
+ G1 = nx.Graph()
32
+ G2 = nx.Graph([(0, 1), (1, 2)])
33
+ assert not vf2pp_is_isomorphic(G1, G2)
34
+
35
+ def test_second_graph_empty(self):
36
+ G1 = nx.Graph([(0, 1), (1, 2)])
37
+ G2 = nx.Graph()
38
+ assert not vf2pp_is_isomorphic(G1, G2)
39
+
40
+ def test_different_order1(self):
41
+ G1 = nx.path_graph(5)
42
+ G2 = nx.path_graph(6)
43
+ assert not vf2pp_is_isomorphic(G1, G2)
44
+
45
+ def test_different_order2(self):
46
+ G1 = nx.barbell_graph(100, 20)
47
+ G2 = nx.barbell_graph(101, 20)
48
+ assert not vf2pp_is_isomorphic(G1, G2)
49
+
50
+ def test_different_order3(self):
51
+ G1 = nx.complete_graph(7)
52
+ G2 = nx.complete_graph(8)
53
+ assert not vf2pp_is_isomorphic(G1, G2)
54
+
55
+ def test_different_degree_sequences1(self):
56
+ G1 = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (0, 4)])
57
+ G2 = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (0, 4), (2, 5)])
58
+ assert not vf2pp_is_isomorphic(G1, G2)
59
+
60
+ G2.remove_node(3)
61
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(["a"]))), "label")
62
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle("a"))), "label")
63
+
64
+ assert vf2pp_is_isomorphic(G1, G2)
65
+
66
+ def test_different_degree_sequences2(self):
67
+ G1 = nx.Graph(
68
+ [
69
+ (0, 1),
70
+ (1, 2),
71
+ (0, 2),
72
+ (2, 3),
73
+ (3, 4),
74
+ (4, 5),
75
+ (5, 6),
76
+ (6, 3),
77
+ (4, 7),
78
+ (7, 8),
79
+ (8, 3),
80
+ ]
81
+ )
82
+ G2 = G1.copy()
83
+ G2.add_edge(8, 0)
84
+ assert not vf2pp_is_isomorphic(G1, G2)
85
+
86
+ G1.add_edge(6, 1)
87
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(["a"]))), "label")
88
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle("a"))), "label")
89
+
90
+ assert vf2pp_is_isomorphic(G1, G2)
91
+
92
+ def test_different_degree_sequences3(self):
93
+ G1 = nx.Graph([(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)])
94
+ G2 = nx.Graph(
95
+ [(0, 1), (0, 6), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)]
96
+ )
97
+ assert not vf2pp_is_isomorphic(G1, G2)
98
+
99
+ G1.add_edge(3, 5)
100
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(["a"]))), "label")
101
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle("a"))), "label")
102
+
103
+ assert vf2pp_is_isomorphic(G1, G2)
104
+
105
+ def test_label_distribution(self):
106
+ G1 = nx.Graph([(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)])
107
+ G2 = nx.Graph([(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)])
108
+
109
+ colors1 = ["blue", "blue", "blue", "yellow", "black", "purple", "purple"]
110
+ colors2 = ["blue", "blue", "yellow", "yellow", "black", "purple", "purple"]
111
+
112
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(colors1[::-1]))), "label")
113
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(colors2[::-1]))), "label")
114
+
115
+ assert not vf2pp_is_isomorphic(G1, G2, node_label="label")
116
+ G2.nodes[3]["label"] = "blue"
117
+ assert vf2pp_is_isomorphic(G1, G2, node_label="label")
118
+
119
+
120
+ class TestAllGraphTypesEdgeCases:
121
+ @pytest.mark.parametrize("graph_type", graph_classes)
122
+ def test_both_graphs_empty(self, graph_type):
123
+ G = graph_type()
124
+ H = graph_type()
125
+ assert vf2pp_isomorphism(G, H) is None
126
+
127
+ G.add_node(0)
128
+
129
+ assert vf2pp_isomorphism(G, H) is None
130
+ assert vf2pp_isomorphism(H, G) is None
131
+
132
+ H.add_node(0)
133
+ assert vf2pp_isomorphism(G, H) == {0: 0}
134
+
135
+ @pytest.mark.parametrize("graph_type", graph_classes)
136
+ def test_first_graph_empty(self, graph_type):
137
+ G = graph_type()
138
+ H = graph_type([(0, 1)])
139
+ assert vf2pp_isomorphism(G, H) is None
140
+
141
+ @pytest.mark.parametrize("graph_type", graph_classes)
142
+ def test_second_graph_empty(self, graph_type):
143
+ G = graph_type([(0, 1)])
144
+ H = graph_type()
145
+ assert vf2pp_isomorphism(G, H) is None
146
+
147
+
148
+ class TestGraphISOVF2pp:
149
+ def test_custom_graph1_same_labels(self):
150
+ G1 = nx.Graph()
151
+
152
+ mapped = {1: "A", 2: "B", 3: "C", 4: "D", 5: "Z", 6: "E"}
153
+ edges1 = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 6), (3, 4), (5, 1), (5, 2)]
154
+ # 5-1 5-1 D-A
155
+ # |/|\ |/|\ |/|\ 6-X; 2-C; 5-D; 1-A; 3-B; 7-E; 4-Z
156
+ # 2-3-4 2-3-4 C-B-Z
157
+ # | | |/ | |/
158
+ # 6 6 7 X E
159
+
160
+ G1.add_edges_from(edges1)
161
+ G2 = nx.relabel_nodes(G1, mapped)
162
+
163
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
164
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_many))), "label")
165
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
166
+
167
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
168
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
169
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
170
+
171
+ # Add edge making G1 symmetrical
172
+ G1.add_edge(3, 7)
173
+ G1.nodes[7]["label"] = "blue"
174
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
175
+
176
+ all_self_mappings = list(vf2pp_all_isomorphisms(G1, G1))
177
+ assert len(all_self_mappings) == 2
178
+ # check that one of the mappings is not the identity for node 2
179
+ assert sum(1 for m in all_self_mappings if m[2] == 2) == 1
180
+
181
+ # Make G2 isomorphic to G1
182
+ G2.add_edges_from([(mapped[3], "X"), (mapped[6], mapped[5])])
183
+ G1.add_edge(4, 7)
184
+ G2.nodes["X"]["label"] = "blue"
185
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
186
+
187
+ # Re-structure maintaining isomorphism
188
+ G1.remove_edges_from([(1, 4), (1, 3)])
189
+ G2.remove_edges_from([(mapped[1], mapped[5]), (mapped[1], mapped[2])])
190
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
191
+
192
+ def test_custom_graph2_same_labels(self):
193
+ G1 = nx.Graph()
194
+
195
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
196
+ edges1 = [(1, 2), (1, 5), (5, 6), (2, 3), (2, 4), (3, 4), (4, 5), (2, 7)]
197
+ # 6-5
198
+ # /|\ 6-F; 2-C; 3-D; 1-A; 7-B; 4-E; 5-G
199
+ # 1-2-4
200
+ # /|/
201
+ # 7 3
202
+
203
+ G1.add_edges_from(edges1)
204
+ G2 = nx.relabel_nodes(G1, mapped)
205
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
206
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
207
+
208
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
209
+
210
+ # Obtain two isomorphic subgraphs from the graph
211
+ G2.remove_edge(mapped[1], mapped[2])
212
+ G2.add_edge(mapped[1], mapped[4])
213
+ H1 = nx.Graph(G1.subgraph([2, 3, 4, 7]))
214
+ H2 = nx.Graph(G2.subgraph([mapped[1], mapped[4], mapped[5], mapped[6]]))
215
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
216
+
217
+ # Add edges maintaining isomorphism
218
+ H1.add_edges_from([(3, 7), (4, 7)])
219
+ H2.add_edges_from([(mapped[1], mapped[6]), (mapped[4], mapped[6])])
220
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
221
+
222
+ def test_custom_graph2_different_labels(self):
223
+ G1 = nx.Graph()
224
+
225
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
226
+ edges1 = [(1, 2), (1, 5), (5, 6), (2, 3), (2, 4), (3, 4), (4, 5), (2, 7)]
227
+
228
+ G1.add_edges_from(edges1)
229
+ G2 = nx.relabel_nodes(G1, mapped)
230
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
231
+ nx.set_node_attributes(
232
+ G2,
233
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
234
+ "label",
235
+ )
236
+
237
+ # Adding new nodes
238
+ G1.add_node(0)
239
+ G2.add_node("Z")
240
+ G1.nodes[0]["label"] = G1.nodes[1]["label"]
241
+ G2.nodes["Z"]["label"] = G1.nodes[1]["label"]
242
+ mapped.update({0: "Z"})
243
+
244
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
245
+
246
+ # Change the color of one of the nodes
247
+ G2.nodes["Z"]["label"] = G1.nodes[2]["label"]
248
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
249
+
250
+ # Add an extra edge
251
+ G1.nodes[0]["label"] = "blue"
252
+ G2.nodes["Z"]["label"] = "blue"
253
+ G1.add_edge(0, 1)
254
+
255
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
256
+
257
+ # Add extra edge to both
258
+ G2.add_edge("Z", "A")
259
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
260
+
261
+ def test_custom_graph3_same_labels(self):
262
+ G1 = nx.Graph()
263
+
264
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
265
+ edges1 = [
266
+ (1, 2),
267
+ (1, 3),
268
+ (2, 3),
269
+ (3, 4),
270
+ (4, 5),
271
+ (4, 7),
272
+ (4, 9),
273
+ (5, 8),
274
+ (8, 9),
275
+ (5, 6),
276
+ (6, 7),
277
+ (5, 2),
278
+ ]
279
+ G1.add_edges_from(edges1)
280
+ G2 = nx.relabel_nodes(G1, mapped)
281
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
282
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
283
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
284
+
285
+ # Connect nodes maintaining symmetry
286
+ G1.add_edges_from([(6, 9), (7, 8)])
287
+ G2.add_edges_from([(mapped[6], mapped[8]), (mapped[7], mapped[9])])
288
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
289
+
290
+ # Make isomorphic
291
+ G1.add_edges_from([(6, 8), (7, 9)])
292
+ G2.add_edges_from([(mapped[6], mapped[9]), (mapped[7], mapped[8])])
293
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
294
+
295
+ # Connect more nodes
296
+ G1.add_edges_from([(2, 7), (3, 6)])
297
+ G2.add_edges_from([(mapped[2], mapped[7]), (mapped[3], mapped[6])])
298
+ G1.add_node(10)
299
+ G2.add_node("Z")
300
+ G1.nodes[10]["label"] = "blue"
301
+ G2.nodes["Z"]["label"] = "blue"
302
+
303
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
304
+
305
+ # Connect the newly added node, to opposite sides of the graph
306
+ G1.add_edges_from([(10, 1), (10, 5), (10, 8)])
307
+ G2.add_edges_from([("Z", mapped[1]), ("Z", mapped[4]), ("Z", mapped[9])])
308
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
309
+
310
+ # Get two subgraphs that are not isomorphic but are easy to make
311
+ H1 = nx.Graph(G1.subgraph([2, 3, 4, 5, 6, 7, 10]))
312
+ H2 = nx.Graph(
313
+ G2.subgraph(
314
+ [mapped[4], mapped[5], mapped[6], mapped[7], mapped[8], mapped[9], "Z"]
315
+ )
316
+ )
317
+ assert vf2pp_isomorphism(H1, H2, node_label="label") is None
318
+
319
+ # Restructure both to make them isomorphic
320
+ H1.add_edges_from([(10, 2), (10, 6), (3, 6), (2, 7), (2, 6), (3, 7)])
321
+ H2.add_edges_from(
322
+ [("Z", mapped[7]), (mapped[6], mapped[9]), (mapped[7], mapped[8])]
323
+ )
324
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
325
+
326
+ # Add edges with opposite direction in each Graph
327
+ H1.add_edge(3, 5)
328
+ H2.add_edge(mapped[5], mapped[7])
329
+ assert vf2pp_isomorphism(H1, H2, node_label="label") is None
330
+
331
+ def test_custom_graph3_different_labels(self):
332
+ G1 = nx.Graph()
333
+
334
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
335
+ edges1 = [
336
+ (1, 2),
337
+ (1, 3),
338
+ (2, 3),
339
+ (3, 4),
340
+ (4, 5),
341
+ (4, 7),
342
+ (4, 9),
343
+ (5, 8),
344
+ (8, 9),
345
+ (5, 6),
346
+ (6, 7),
347
+ (5, 2),
348
+ ]
349
+ G1.add_edges_from(edges1)
350
+ G2 = nx.relabel_nodes(G1, mapped)
351
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
352
+ nx.set_node_attributes(
353
+ G2,
354
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
355
+ "label",
356
+ )
357
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
358
+
359
+ # Add extra edge to G1
360
+ G1.add_edge(1, 7)
361
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
362
+
363
+ # Compensate in G2
364
+ G2.add_edge(9, 1)
365
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
366
+
367
+ # Add extra node
368
+ G1.add_node("A")
369
+ G2.add_node("K")
370
+ G1.nodes["A"]["label"] = "green"
371
+ G2.nodes["K"]["label"] = "green"
372
+ mapped.update({"A": "K"})
373
+
374
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
375
+
376
+ # Connect A to one side of G1 and K to the opposite
377
+ G1.add_edge("A", 6)
378
+ G2.add_edge("K", 5)
379
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
380
+
381
+ # Make the graphs symmetrical
382
+ G1.add_edge(1, 5)
383
+ G1.add_edge(2, 9)
384
+ G2.add_edge(9, 3)
385
+ G2.add_edge(8, 4)
386
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
387
+
388
+ # Assign same colors so the two opposite sides are identical
389
+ for node in G1.nodes():
390
+ color = "red"
391
+ G1.nodes[node]["label"] = color
392
+ G2.nodes[mapped[node]]["label"] = color
393
+
394
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
395
+
396
+ def test_custom_graph4_different_labels(self):
397
+ G1 = nx.Graph()
398
+ edges1 = [
399
+ (1, 2),
400
+ (2, 3),
401
+ (3, 8),
402
+ (3, 4),
403
+ (4, 5),
404
+ (4, 6),
405
+ (3, 6),
406
+ (8, 7),
407
+ (8, 9),
408
+ (5, 9),
409
+ (10, 11),
410
+ (11, 12),
411
+ (12, 13),
412
+ (11, 13),
413
+ ]
414
+
415
+ mapped = {
416
+ 1: "n",
417
+ 2: "m",
418
+ 3: "l",
419
+ 4: "j",
420
+ 5: "k",
421
+ 6: "i",
422
+ 7: "g",
423
+ 8: "h",
424
+ 9: "f",
425
+ 10: "b",
426
+ 11: "a",
427
+ 12: "d",
428
+ 13: "e",
429
+ }
430
+
431
+ G1.add_edges_from(edges1)
432
+ G2 = nx.relabel_nodes(G1, mapped)
433
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
434
+ nx.set_node_attributes(
435
+ G2,
436
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
437
+ "label",
438
+ )
439
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
440
+
441
+ def test_custom_graph4_same_labels(self):
442
+ G1 = nx.Graph()
443
+ edges1 = [
444
+ (1, 2),
445
+ (2, 3),
446
+ (3, 8),
447
+ (3, 4),
448
+ (4, 5),
449
+ (4, 6),
450
+ (3, 6),
451
+ (8, 7),
452
+ (8, 9),
453
+ (5, 9),
454
+ (10, 11),
455
+ (11, 12),
456
+ (12, 13),
457
+ (11, 13),
458
+ ]
459
+
460
+ mapped = {
461
+ 1: "n",
462
+ 2: "m",
463
+ 3: "l",
464
+ 4: "j",
465
+ 5: "k",
466
+ 6: "i",
467
+ 7: "g",
468
+ 8: "h",
469
+ 9: "f",
470
+ 10: "b",
471
+ 11: "a",
472
+ 12: "d",
473
+ 13: "e",
474
+ }
475
+
476
+ G1.add_edges_from(edges1)
477
+ G2 = nx.relabel_nodes(G1, mapped)
478
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
479
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
480
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
481
+
482
+ all_self_mappings = list(vf2pp_all_isomorphisms(G1, G1))
483
+ assert len(all_self_mappings) == 2
484
+ # check that one of the mappings is not the identity for node 2
485
+ assert sum(1 for m in all_self_mappings if m[12] == 12) == 1
486
+
487
+ # Add nodes of different label
488
+ G1.add_node(0)
489
+ G2.add_node("z")
490
+ G1.nodes[0]["label"] = "green"
491
+ G2.nodes["z"]["label"] = "blue"
492
+
493
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
494
+
495
+ # Make the labels identical
496
+ G2.nodes["z"]["label"] = "green"
497
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
498
+
499
+ # Change the structure of the graphs, keeping them isomorphic
500
+ G1.add_edge(2, 5)
501
+ G2.remove_edge("i", "l")
502
+ G2.add_edge("g", "l")
503
+ G2.add_edge("m", "f")
504
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
505
+
506
+ # Change the structure of the disconnected sub-graph, keeping it isomorphic
507
+ G1.remove_node(13)
508
+ G2.remove_node("d")
509
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
510
+
511
+ # Connect the newly added node to the disconnected graph, which now is just a path of size 3
512
+ G1.add_edge(0, 10)
513
+ G2.add_edge("e", "z")
514
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
515
+
516
+ # Connect the two disconnected sub-graphs, forming a single graph
517
+ G1.add_edge(11, 3)
518
+ G1.add_edge(0, 8)
519
+ G2.add_edge("a", "l")
520
+ G2.add_edge("z", "j")
521
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
522
+
523
+ def test_custom_graph5_same_labels(self):
524
+ # Lots of symmetries
525
+ # 1
526
+ # /|\
527
+ # / 2 \
528
+ # | / \ |
529
+ # 5-6 3-4
530
+ # | \ / |
531
+ # \ 7 /
532
+ # \|/
533
+ # 8
534
+ G1 = nx.Graph()
535
+ edges1 = [
536
+ (1, 5),
537
+ (1, 2),
538
+ (1, 4),
539
+ (2, 3),
540
+ (2, 6),
541
+ (3, 4),
542
+ (3, 7),
543
+ (4, 8),
544
+ (5, 8),
545
+ (5, 6),
546
+ (6, 7),
547
+ (7, 8),
548
+ ]
549
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
550
+
551
+ G1.add_edges_from(edges1)
552
+ G2 = nx.relabel_nodes(G1, mapped)
553
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
554
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
555
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
556
+
557
+ all_self_mappings = list(vf2pp_all_isomorphisms(G1, G1))
558
+ assert len(all_self_mappings) == 48
559
+ # check that one of the mappings is not the identity for node 2
560
+ assert all(sum(1 for m in all_self_mappings if m[u] == u) == 6 for u in G1)
561
+
562
+ # Add different edges in each graph, maintaining symmetry
563
+ G1.add_edges_from([(3, 6), (2, 7), (2, 5), (1, 3), (4, 7), (6, 8)])
564
+ G2.add_edges_from(
565
+ [
566
+ (mapped[6], mapped[3]),
567
+ (mapped[2], mapped[7]),
568
+ (mapped[1], mapped[6]),
569
+ (mapped[5], mapped[7]),
570
+ (mapped[3], mapped[8]),
571
+ (mapped[2], mapped[4]),
572
+ ]
573
+ )
574
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
575
+
576
+ # Obtain two different but isomorphic subgraphs from G1 and G2
577
+ H1 = nx.Graph(G1.subgraph([1, 5, 8, 6, 7, 3]))
578
+ H2 = nx.Graph(
579
+ G2.subgraph(
580
+ [mapped[1], mapped[4], mapped[8], mapped[7], mapped[3], mapped[5]]
581
+ )
582
+ )
583
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
584
+
585
+ # Delete corresponding node from the two graphs
586
+ H1.remove_node(8)
587
+ H2.remove_node(mapped[7])
588
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
589
+
590
+ # Re-orient, maintaining isomorphism
591
+ H1.add_edge(1, 6)
592
+ H1.remove_edge(3, 6)
593
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
594
+
595
+ def test_custom_graph5_different_labels(self):
596
+ G1 = nx.Graph()
597
+ edges1 = [
598
+ (1, 5),
599
+ (1, 2),
600
+ (1, 4),
601
+ (2, 3),
602
+ (2, 6),
603
+ (3, 4),
604
+ (3, 7),
605
+ (4, 8),
606
+ (5, 8),
607
+ (5, 6),
608
+ (6, 7),
609
+ (7, 8),
610
+ ]
611
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
612
+
613
+ G1.add_edges_from(edges1)
614
+ G2 = nx.relabel_nodes(G1, mapped)
615
+
616
+ colors = ["red", "blue", "grey", "none", "brown", "solarized", "yellow", "pink"]
617
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
618
+ nx.set_node_attributes(
619
+ G2,
620
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
621
+ "label",
622
+ )
623
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
624
+
625
+ # Assign different colors to matching nodes
626
+ c = 0
627
+ for node in G1.nodes():
628
+ color1 = colors[c]
629
+ color2 = colors[(c + 3) % len(colors)]
630
+ G1.nodes[node]["label"] = color1
631
+ G2.nodes[mapped[node]]["label"] = color2
632
+ c += 1
633
+
634
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
635
+
636
+ # Get symmetrical sub-graphs of G1,G2 and compare them
637
+ H1 = G1.subgraph([1, 5])
638
+ H2 = G2.subgraph(["i", "c"])
639
+ c = 0
640
+ for node1, node2 in zip(H1.nodes(), H2.nodes()):
641
+ H1.nodes[node1]["label"] = "red"
642
+ H2.nodes[node2]["label"] = "red"
643
+ c += 1
644
+
645
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
646
+
647
+ def test_disconnected_graph_all_same_labels(self):
648
+ G1 = nx.Graph()
649
+ G1.add_nodes_from(list(range(10)))
650
+
651
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
652
+ G2 = nx.relabel_nodes(G1, mapped)
653
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
654
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
655
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
656
+
657
+ def test_disconnected_graph_all_different_labels(self):
658
+ G1 = nx.Graph()
659
+ G1.add_nodes_from(list(range(10)))
660
+
661
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
662
+ G2 = nx.relabel_nodes(G1, mapped)
663
+
664
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
665
+ nx.set_node_attributes(
666
+ G2,
667
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
668
+ "label",
669
+ )
670
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
671
+
672
+ def test_disconnected_graph_some_same_labels(self):
673
+ G1 = nx.Graph()
674
+ G1.add_nodes_from(list(range(10)))
675
+
676
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
677
+ G2 = nx.relabel_nodes(G1, mapped)
678
+
679
+ colors = [
680
+ "white",
681
+ "white",
682
+ "white",
683
+ "purple",
684
+ "purple",
685
+ "red",
686
+ "red",
687
+ "pink",
688
+ "pink",
689
+ "pink",
690
+ ]
691
+
692
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(colors))), "label")
693
+ nx.set_node_attributes(
694
+ G2, dict(zip([mapped[n] for n in G1], it.cycle(colors))), "label"
695
+ )
696
+
697
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
698
+
699
+
700
+ class TestMultiGraphISOVF2pp:
701
+ def test_custom_multigraph1_same_labels(self):
702
+ G1 = nx.MultiGraph()
703
+
704
+ mapped = {1: "A", 2: "B", 3: "C", 4: "D", 5: "Z", 6: "E"}
705
+ edges1 = [
706
+ (1, 2),
707
+ (1, 3),
708
+ (1, 4),
709
+ (1, 4),
710
+ (1, 4),
711
+ (2, 3),
712
+ (2, 6),
713
+ (2, 6),
714
+ (3, 4),
715
+ (3, 4),
716
+ (5, 1),
717
+ (5, 1),
718
+ (5, 2),
719
+ (5, 2),
720
+ ]
721
+
722
+ G1.add_edges_from(edges1)
723
+ G2 = nx.relabel_nodes(G1, mapped)
724
+
725
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
726
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
727
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
728
+ assert m
729
+
730
+ # Transfer the 2-clique to the right side of G1
731
+ G1.remove_edges_from([(2, 6), (2, 6)])
732
+ G1.add_edges_from([(3, 6), (3, 6)])
733
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
734
+ assert not m
735
+
736
+ # Delete an edges, making them symmetrical, so the position of the 2-clique doesn't matter
737
+ G2.remove_edge(mapped[1], mapped[4])
738
+ G1.remove_edge(1, 4)
739
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
740
+ assert m
741
+
742
+ # Add self-loops
743
+ G1.add_edges_from([(5, 5), (5, 5), (1, 1)])
744
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
745
+ assert not m
746
+
747
+ # Compensate in G2
748
+ G2.add_edges_from(
749
+ [(mapped[1], mapped[1]), (mapped[4], mapped[4]), (mapped[4], mapped[4])]
750
+ )
751
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
752
+ assert m
753
+
754
+ def test_custom_multigraph1_different_labels(self):
755
+ G1 = nx.MultiGraph()
756
+
757
+ mapped = {1: "A", 2: "B", 3: "C", 4: "D", 5: "Z", 6: "E"}
758
+ edges1 = [
759
+ (1, 2),
760
+ (1, 3),
761
+ (1, 4),
762
+ (1, 4),
763
+ (1, 4),
764
+ (2, 3),
765
+ (2, 6),
766
+ (2, 6),
767
+ (3, 4),
768
+ (3, 4),
769
+ (5, 1),
770
+ (5, 1),
771
+ (5, 2),
772
+ (5, 2),
773
+ ]
774
+
775
+ G1.add_edges_from(edges1)
776
+ G2 = nx.relabel_nodes(G1, mapped)
777
+
778
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
779
+ nx.set_node_attributes(
780
+ G2,
781
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
782
+ "label",
783
+ )
784
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
785
+ assert m
786
+ assert m == mapped
787
+
788
+ # Re-structure G1, maintaining the degree sequence
789
+ G1.remove_edge(1, 4)
790
+ G1.add_edge(1, 5)
791
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
792
+ assert not m
793
+
794
+ # Restructure G2, making it isomorphic to G1
795
+ G2.remove_edge("A", "D")
796
+ G2.add_edge("A", "Z")
797
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
798
+ assert m
799
+ assert m == mapped
800
+
801
+ # Add edge from node to itself
802
+ G1.add_edges_from([(6, 6), (6, 6), (6, 6)])
803
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
804
+ assert not m
805
+
806
+ # Same for G2
807
+ G2.add_edges_from([("E", "E"), ("E", "E"), ("E", "E")])
808
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
809
+ assert m
810
+ assert m == mapped
811
+
812
+ def test_custom_multigraph2_same_labels(self):
813
+ G1 = nx.MultiGraph()
814
+
815
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
816
+ edges1 = [
817
+ (1, 2),
818
+ (1, 2),
819
+ (1, 5),
820
+ (1, 5),
821
+ (1, 5),
822
+ (5, 6),
823
+ (2, 3),
824
+ (2, 3),
825
+ (2, 4),
826
+ (3, 4),
827
+ (3, 4),
828
+ (4, 5),
829
+ (4, 5),
830
+ (4, 5),
831
+ (2, 7),
832
+ (2, 7),
833
+ (2, 7),
834
+ ]
835
+
836
+ G1.add_edges_from(edges1)
837
+ G2 = nx.relabel_nodes(G1, mapped)
838
+
839
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
840
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
841
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
842
+ assert m
843
+
844
+ # Obtain two non-isomorphic subgraphs from the graph
845
+ G2.add_edge(mapped[1], mapped[4])
846
+ H1 = nx.MultiGraph(G1.subgraph([2, 3, 4, 7]))
847
+ H2 = nx.MultiGraph(G2.subgraph([mapped[1], mapped[4], mapped[5], mapped[6]]))
848
+
849
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
850
+ assert not m
851
+
852
+ # Make them isomorphic
853
+ H1.remove_edge(3, 4)
854
+ H1.add_edges_from([(2, 3), (2, 4), (2, 4)])
855
+ H2.add_edges_from([(mapped[5], mapped[6]), (mapped[5], mapped[6])])
856
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
857
+ assert m
858
+
859
+ # Remove triangle edge
860
+ H1.remove_edges_from([(2, 3), (2, 3), (2, 3)])
861
+ H2.remove_edges_from([(mapped[5], mapped[4])] * 3)
862
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
863
+ assert m
864
+
865
+ # Change the edge orientation such that H1 is rotated H2
866
+ H1.remove_edges_from([(2, 7), (2, 7)])
867
+ H1.add_edges_from([(3, 4), (3, 4)])
868
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
869
+ assert m
870
+
871
+ # Add extra edges maintaining degree sequence, but in a non-symmetrical manner
872
+ H2.add_edge(mapped[5], mapped[1])
873
+ H1.add_edge(3, 4)
874
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
875
+ assert not m
876
+
877
+ def test_custom_multigraph2_different_labels(self):
878
+ G1 = nx.MultiGraph()
879
+
880
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
881
+ edges1 = [
882
+ (1, 2),
883
+ (1, 2),
884
+ (1, 5),
885
+ (1, 5),
886
+ (1, 5),
887
+ (5, 6),
888
+ (2, 3),
889
+ (2, 3),
890
+ (2, 4),
891
+ (3, 4),
892
+ (3, 4),
893
+ (4, 5),
894
+ (4, 5),
895
+ (4, 5),
896
+ (2, 7),
897
+ (2, 7),
898
+ (2, 7),
899
+ ]
900
+
901
+ G1.add_edges_from(edges1)
902
+ G2 = nx.relabel_nodes(G1, mapped)
903
+
904
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
905
+ nx.set_node_attributes(
906
+ G2,
907
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
908
+ "label",
909
+ )
910
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
911
+ assert m
912
+ assert m == mapped
913
+
914
+ # Re-structure G1
915
+ G1.remove_edge(2, 7)
916
+ G1.add_edge(5, 6)
917
+
918
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
919
+ assert not m
920
+
921
+ # Same for G2
922
+ G2.remove_edge("B", "C")
923
+ G2.add_edge("G", "F")
924
+
925
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
926
+ assert m
927
+ assert m == mapped
928
+
929
+ # Delete node from G1 and G2, keeping them isomorphic
930
+ G1.remove_node(3)
931
+ G2.remove_node("D")
932
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
933
+ assert m
934
+
935
+ # Change G1 edges
936
+ G1.remove_edge(1, 2)
937
+ G1.remove_edge(2, 7)
938
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
939
+ assert not m
940
+
941
+ # Make G2 identical to G1, but with different edge orientation and different labels
942
+ G2.add_edges_from([("A", "C"), ("C", "E"), ("C", "E")])
943
+ G2.remove_edges_from(
944
+ [("A", "G"), ("A", "G"), ("F", "G"), ("E", "G"), ("E", "G")]
945
+ )
946
+
947
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
948
+ assert not m
949
+
950
+ # Make all labels the same, so G1 and G2 are also isomorphic
951
+ for n1, n2 in zip(G1.nodes(), G2.nodes()):
952
+ G1.nodes[n1]["label"] = "blue"
953
+ G2.nodes[n2]["label"] = "blue"
954
+
955
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
956
+ assert m
957
+
958
+ def test_custom_multigraph3_same_labels(self):
959
+ G1 = nx.MultiGraph()
960
+
961
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
962
+ edges1 = [
963
+ (1, 2),
964
+ (1, 3),
965
+ (1, 3),
966
+ (2, 3),
967
+ (2, 3),
968
+ (3, 4),
969
+ (4, 5),
970
+ (4, 7),
971
+ (4, 9),
972
+ (4, 9),
973
+ (4, 9),
974
+ (5, 8),
975
+ (5, 8),
976
+ (8, 9),
977
+ (8, 9),
978
+ (5, 6),
979
+ (6, 7),
980
+ (6, 7),
981
+ (6, 7),
982
+ (5, 2),
983
+ ]
984
+ G1.add_edges_from(edges1)
985
+ G2 = nx.relabel_nodes(G1, mapped)
986
+
987
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
988
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
989
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
990
+ assert m
991
+
992
+ # Connect nodes maintaining symmetry
993
+ G1.add_edges_from([(6, 9), (7, 8), (5, 8), (4, 9), (4, 9)])
994
+ G2.add_edges_from(
995
+ [
996
+ (mapped[6], mapped[8]),
997
+ (mapped[7], mapped[9]),
998
+ (mapped[5], mapped[8]),
999
+ (mapped[4], mapped[9]),
1000
+ (mapped[4], mapped[9]),
1001
+ ]
1002
+ )
1003
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1004
+ assert not m
1005
+
1006
+ # Make isomorphic
1007
+ G1.add_edges_from([(6, 8), (6, 8), (7, 9), (7, 9), (7, 9)])
1008
+ G2.add_edges_from(
1009
+ [
1010
+ (mapped[6], mapped[8]),
1011
+ (mapped[6], mapped[9]),
1012
+ (mapped[7], mapped[8]),
1013
+ (mapped[7], mapped[9]),
1014
+ (mapped[7], mapped[9]),
1015
+ ]
1016
+ )
1017
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1018
+ assert m
1019
+
1020
+ # Connect more nodes
1021
+ G1.add_edges_from([(2, 7), (2, 7), (3, 6), (3, 6)])
1022
+ G2.add_edges_from(
1023
+ [
1024
+ (mapped[2], mapped[7]),
1025
+ (mapped[2], mapped[7]),
1026
+ (mapped[3], mapped[6]),
1027
+ (mapped[3], mapped[6]),
1028
+ ]
1029
+ )
1030
+ G1.add_node(10)
1031
+ G2.add_node("Z")
1032
+ G1.nodes[10]["label"] = "blue"
1033
+ G2.nodes["Z"]["label"] = "blue"
1034
+
1035
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1036
+ assert m
1037
+
1038
+ # Connect the newly added node, to opposite sides of the graph
1039
+ G1.add_edges_from([(10, 1), (10, 5), (10, 8), (10, 10), (10, 10)])
1040
+ G2.add_edges_from(
1041
+ [
1042
+ ("Z", mapped[1]),
1043
+ ("Z", mapped[4]),
1044
+ ("Z", mapped[9]),
1045
+ ("Z", "Z"),
1046
+ ("Z", "Z"),
1047
+ ]
1048
+ )
1049
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1050
+ assert not m
1051
+
1052
+ # We connected the new node to opposite sides, so G1 must be symmetrical
1053
+ # to G2. Re-structure them to be so
1054
+ G1.remove_edges_from([(1, 3), (4, 9), (4, 9), (7, 9)])
1055
+ G2.remove_edges_from(
1056
+ [
1057
+ (mapped[1], mapped[3]),
1058
+ (mapped[4], mapped[9]),
1059
+ (mapped[4], mapped[9]),
1060
+ (mapped[7], mapped[9]),
1061
+ ]
1062
+ )
1063
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1064
+ assert m
1065
+
1066
+ # Get two subgraphs that are not isomorphic but are easy to make
1067
+ H1 = nx.Graph(G1.subgraph([2, 3, 4, 5, 6, 7, 10]))
1068
+ H2 = nx.Graph(
1069
+ G2.subgraph(
1070
+ [mapped[4], mapped[5], mapped[6], mapped[7], mapped[8], mapped[9], "Z"]
1071
+ )
1072
+ )
1073
+
1074
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1075
+ assert not m
1076
+
1077
+ # Restructure both to make them isomorphic
1078
+ H1.add_edges_from([(10, 2), (10, 6), (3, 6), (2, 7), (2, 6), (3, 7)])
1079
+ H2.add_edges_from(
1080
+ [("Z", mapped[7]), (mapped[6], mapped[9]), (mapped[7], mapped[8])]
1081
+ )
1082
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1083
+ assert m
1084
+
1085
+ # Remove one self-loop in H2
1086
+ H2.remove_edge("Z", "Z")
1087
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1088
+ assert not m
1089
+
1090
+ # Compensate in H1
1091
+ H1.remove_edge(10, 10)
1092
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1093
+ assert m
1094
+
1095
+ def test_custom_multigraph3_different_labels(self):
1096
+ G1 = nx.MultiGraph()
1097
+
1098
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
1099
+ edges1 = [
1100
+ (1, 2),
1101
+ (1, 3),
1102
+ (1, 3),
1103
+ (2, 3),
1104
+ (2, 3),
1105
+ (3, 4),
1106
+ (4, 5),
1107
+ (4, 7),
1108
+ (4, 9),
1109
+ (4, 9),
1110
+ (4, 9),
1111
+ (5, 8),
1112
+ (5, 8),
1113
+ (8, 9),
1114
+ (8, 9),
1115
+ (5, 6),
1116
+ (6, 7),
1117
+ (6, 7),
1118
+ (6, 7),
1119
+ (5, 2),
1120
+ ]
1121
+
1122
+ G1.add_edges_from(edges1)
1123
+ G2 = nx.relabel_nodes(G1, mapped)
1124
+
1125
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
1126
+ nx.set_node_attributes(
1127
+ G2,
1128
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
1129
+ "label",
1130
+ )
1131
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1132
+ assert m
1133
+ assert m == mapped
1134
+
1135
+ # Delete edge maintaining isomorphism
1136
+ G1.remove_edge(4, 9)
1137
+ G2.remove_edge(4, 6)
1138
+
1139
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1140
+ assert m
1141
+ assert m == mapped
1142
+
1143
+ # Change edge orientation such that G1 mirrors G2
1144
+ G1.add_edges_from([(4, 9), (1, 2), (1, 2)])
1145
+ G1.remove_edges_from([(1, 3), (1, 3)])
1146
+ G2.add_edges_from([(3, 5), (7, 9)])
1147
+ G2.remove_edge(8, 9)
1148
+
1149
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1150
+ assert not m
1151
+
1152
+ # Make all labels the same, so G1 and G2 are also isomorphic
1153
+ for n1, n2 in zip(G1.nodes(), G2.nodes()):
1154
+ G1.nodes[n1]["label"] = "blue"
1155
+ G2.nodes[n2]["label"] = "blue"
1156
+
1157
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1158
+ assert m
1159
+
1160
+ G1.add_node(10)
1161
+ G2.add_node("Z")
1162
+ G1.nodes[10]["label"] = "green"
1163
+ G2.nodes["Z"]["label"] = "green"
1164
+
1165
+ # Add different number of edges between the new nodes and themselves
1166
+ G1.add_edges_from([(10, 10), (10, 10)])
1167
+ G2.add_edges_from([("Z", "Z")])
1168
+
1169
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1170
+ assert not m
1171
+
1172
+ # Make the number of self-edges equal
1173
+ G1.remove_edge(10, 10)
1174
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1175
+ assert m
1176
+
1177
+ # Connect the new node to the graph
1178
+ G1.add_edges_from([(10, 3), (10, 4)])
1179
+ G2.add_edges_from([("Z", 8), ("Z", 3)])
1180
+
1181
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1182
+ assert m
1183
+
1184
+ # Remove central node
1185
+ G1.remove_node(4)
1186
+ G2.remove_node(3)
1187
+ G1.add_edges_from([(5, 6), (5, 6), (5, 7)])
1188
+ G2.add_edges_from([(1, 6), (1, 6), (6, 2)])
1189
+
1190
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1191
+ assert m
1192
+
1193
+ def test_custom_multigraph4_same_labels(self):
1194
+ G1 = nx.MultiGraph()
1195
+ edges1 = [
1196
+ (1, 2),
1197
+ (1, 2),
1198
+ (2, 2),
1199
+ (2, 3),
1200
+ (3, 8),
1201
+ (3, 8),
1202
+ (3, 4),
1203
+ (4, 5),
1204
+ (4, 5),
1205
+ (4, 5),
1206
+ (4, 6),
1207
+ (3, 6),
1208
+ (3, 6),
1209
+ (6, 6),
1210
+ (8, 7),
1211
+ (7, 7),
1212
+ (8, 9),
1213
+ (9, 9),
1214
+ (8, 9),
1215
+ (8, 9),
1216
+ (5, 9),
1217
+ (10, 11),
1218
+ (11, 12),
1219
+ (12, 13),
1220
+ (11, 13),
1221
+ (10, 10),
1222
+ (10, 11),
1223
+ (11, 13),
1224
+ ]
1225
+
1226
+ mapped = {
1227
+ 1: "n",
1228
+ 2: "m",
1229
+ 3: "l",
1230
+ 4: "j",
1231
+ 5: "k",
1232
+ 6: "i",
1233
+ 7: "g",
1234
+ 8: "h",
1235
+ 9: "f",
1236
+ 10: "b",
1237
+ 11: "a",
1238
+ 12: "d",
1239
+ 13: "e",
1240
+ }
1241
+
1242
+ G1.add_edges_from(edges1)
1243
+ G2 = nx.relabel_nodes(G1, mapped)
1244
+
1245
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
1246
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
1247
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1248
+ assert m
1249
+
1250
+ # Add extra but corresponding edges to both graphs
1251
+ G1.add_edges_from([(2, 2), (2, 3), (2, 8), (3, 4)])
1252
+ G2.add_edges_from([("m", "m"), ("m", "l"), ("m", "h"), ("l", "j")])
1253
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1254
+ assert m
1255
+
1256
+ # Obtain subgraphs
1257
+ H1 = nx.MultiGraph(G1.subgraph([2, 3, 4, 6, 10, 11, 12, 13]))
1258
+ H2 = nx.MultiGraph(
1259
+ G2.subgraph(
1260
+ [
1261
+ mapped[2],
1262
+ mapped[3],
1263
+ mapped[8],
1264
+ mapped[9],
1265
+ mapped[10],
1266
+ mapped[11],
1267
+ mapped[12],
1268
+ mapped[13],
1269
+ ]
1270
+ )
1271
+ )
1272
+
1273
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1274
+ assert not m
1275
+
1276
+ # Make them isomorphic
1277
+ H2.remove_edges_from(
1278
+ [(mapped[3], mapped[2]), (mapped[9], mapped[8]), (mapped[2], mapped[2])]
1279
+ )
1280
+ H2.add_edges_from([(mapped[9], mapped[9]), (mapped[2], mapped[8])])
1281
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1282
+ assert m
1283
+
1284
+ # Re-structure the disconnected sub-graph
1285
+ H1.remove_node(12)
1286
+ H2.remove_node(mapped[12])
1287
+ H1.add_edge(13, 13)
1288
+ H2.add_edge(mapped[13], mapped[13])
1289
+
1290
+ # Connect the two disconnected components, forming a single graph
1291
+ H1.add_edges_from([(3, 13), (6, 11)])
1292
+ H2.add_edges_from([(mapped[8], mapped[10]), (mapped[2], mapped[11])])
1293
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1294
+ assert m
1295
+
1296
+ # Change orientation of self-loops in one graph, maintaining the degree sequence
1297
+ H1.remove_edges_from([(2, 2), (3, 6)])
1298
+ H1.add_edges_from([(6, 6), (2, 3)])
1299
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1300
+ assert not m
1301
+
1302
+ def test_custom_multigraph4_different_labels(self):
1303
+ G1 = nx.MultiGraph()
1304
+ edges1 = [
1305
+ (1, 2),
1306
+ (1, 2),
1307
+ (2, 2),
1308
+ (2, 3),
1309
+ (3, 8),
1310
+ (3, 8),
1311
+ (3, 4),
1312
+ (4, 5),
1313
+ (4, 5),
1314
+ (4, 5),
1315
+ (4, 6),
1316
+ (3, 6),
1317
+ (3, 6),
1318
+ (6, 6),
1319
+ (8, 7),
1320
+ (7, 7),
1321
+ (8, 9),
1322
+ (9, 9),
1323
+ (8, 9),
1324
+ (8, 9),
1325
+ (5, 9),
1326
+ (10, 11),
1327
+ (11, 12),
1328
+ (12, 13),
1329
+ (11, 13),
1330
+ ]
1331
+
1332
+ mapped = {
1333
+ 1: "n",
1334
+ 2: "m",
1335
+ 3: "l",
1336
+ 4: "j",
1337
+ 5: "k",
1338
+ 6: "i",
1339
+ 7: "g",
1340
+ 8: "h",
1341
+ 9: "f",
1342
+ 10: "b",
1343
+ 11: "a",
1344
+ 12: "d",
1345
+ 13: "e",
1346
+ }
1347
+
1348
+ G1.add_edges_from(edges1)
1349
+ G2 = nx.relabel_nodes(G1, mapped)
1350
+
1351
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
1352
+ nx.set_node_attributes(
1353
+ G2,
1354
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
1355
+ "label",
1356
+ )
1357
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1358
+ assert m == mapped
1359
+
1360
+ # Add extra but corresponding edges to both graphs
1361
+ G1.add_edges_from([(2, 2), (2, 3), (2, 8), (3, 4)])
1362
+ G2.add_edges_from([("m", "m"), ("m", "l"), ("m", "h"), ("l", "j")])
1363
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1364
+ assert m == mapped
1365
+
1366
+ # Obtain isomorphic subgraphs
1367
+ H1 = nx.MultiGraph(G1.subgraph([2, 3, 4, 6]))
1368
+ H2 = nx.MultiGraph(G2.subgraph(["m", "l", "j", "i"]))
1369
+
1370
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1371
+ assert m
1372
+
1373
+ # Delete the 3-clique, keeping only the path-graph. Also, H1 mirrors H2
1374
+ H1.remove_node(4)
1375
+ H2.remove_node("j")
1376
+ H1.remove_edges_from([(2, 2), (2, 3), (6, 6)])
1377
+ H2.remove_edges_from([("l", "i"), ("m", "m"), ("m", "m")])
1378
+
1379
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1380
+ assert not m
1381
+
1382
+ # Assign the same labels so that mirroring means isomorphic
1383
+ for n1, n2 in zip(H1.nodes(), H2.nodes()):
1384
+ H1.nodes[n1]["label"] = "red"
1385
+ H2.nodes[n2]["label"] = "red"
1386
+
1387
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1388
+ assert m
1389
+
1390
+ # Leave only one node with self-loop
1391
+ H1.remove_nodes_from([3, 6])
1392
+ H2.remove_nodes_from(["m", "l"])
1393
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1394
+ assert m
1395
+
1396
+ # Remove one self-loop from H1
1397
+ H1.remove_edge(2, 2)
1398
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1399
+ assert not m
1400
+
1401
+ # Same for H2
1402
+ H2.remove_edge("i", "i")
1403
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1404
+ assert m
1405
+
1406
+ # Compose H1 with the disconnected sub-graph of G1. Same for H2
1407
+ S1 = nx.compose(H1, nx.MultiGraph(G1.subgraph([10, 11, 12, 13])))
1408
+ S2 = nx.compose(H2, nx.MultiGraph(G2.subgraph(["a", "b", "d", "e"])))
1409
+
1410
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1411
+ assert m
1412
+
1413
+ # Connect the two components
1414
+ S1.add_edges_from([(13, 13), (13, 13), (2, 13)])
1415
+ S2.add_edges_from([("a", "a"), ("a", "a"), ("i", "e")])
1416
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
1417
+ assert m
1418
+
1419
+ def test_custom_multigraph5_same_labels(self):
1420
+ # Lots of symmetries
1421
+ # 1
1422
+ # /|\
1423
+ # / 2 \
1424
+ # | / \ |
1425
+ # 5-6 3-4
1426
+ # | \ / |
1427
+ # \ 7 /
1428
+ # \|/
1429
+ # 8
1430
+ G1 = nx.MultiGraph()
1431
+ edges1 = [
1432
+ (1, 5),
1433
+ (1, 2),
1434
+ (1, 4),
1435
+ (2, 3),
1436
+ (2, 6),
1437
+ (3, 4),
1438
+ (3, 7),
1439
+ (4, 8),
1440
+ (5, 8),
1441
+ (5, 6),
1442
+ (6, 7),
1443
+ (7, 8),
1444
+ ]
1445
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
1446
+
1447
+ G1.add_edges_from(edges1)
1448
+ G2 = nx.relabel_nodes(G1, mapped)
1449
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
1450
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
1451
+
1452
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1453
+ assert m
1454
+
1455
+ # Add multiple edges and self-loops, maintaining isomorphism
1456
+ G1.add_edges_from(
1457
+ [(1, 2), (1, 2), (3, 7), (8, 8), (8, 8), (7, 8), (2, 3), (5, 6)]
1458
+ )
1459
+ G2.add_edges_from(
1460
+ [
1461
+ ("a", "h"),
1462
+ ("a", "h"),
1463
+ ("d", "j"),
1464
+ ("c", "c"),
1465
+ ("c", "c"),
1466
+ ("j", "c"),
1467
+ ("d", "h"),
1468
+ ("g", "b"),
1469
+ ]
1470
+ )
1471
+
1472
+ all_self_mappings = list(vf2pp_all_isomorphisms(G1, G1))
1473
+ assert len(all_self_mappings) == 1
1474
+
1475
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1476
+ assert m
1477
+
1478
+ # Make G2 to be the rotated G1
1479
+ G2.remove_edges_from(
1480
+ [
1481
+ ("a", "h"),
1482
+ ("a", "h"),
1483
+ ("d", "j"),
1484
+ ("c", "c"),
1485
+ ("c", "c"),
1486
+ ("j", "c"),
1487
+ ("d", "h"),
1488
+ ("g", "b"),
1489
+ ]
1490
+ )
1491
+ G2.add_edges_from(
1492
+ [
1493
+ ("d", "i"),
1494
+ ("a", "h"),
1495
+ ("g", "b"),
1496
+ ("g", "b"),
1497
+ ("i", "i"),
1498
+ ("i", "i"),
1499
+ ("b", "j"),
1500
+ ("d", "j"),
1501
+ ]
1502
+ )
1503
+
1504
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1505
+ assert m
1506
+
1507
+ def test_disconnected_multigraph_all_same_labels(self):
1508
+ G1 = nx.MultiGraph()
1509
+ G1.add_nodes_from(list(range(10)))
1510
+ G1.add_edges_from([(i, i) for i in range(10)])
1511
+
1512
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
1513
+ G2 = nx.relabel_nodes(G1, mapped)
1514
+
1515
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
1516
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
1517
+
1518
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1519
+ assert m
1520
+
1521
+ # Add self-loops to non-mapped nodes. Should be the same, as the graph is disconnected.
1522
+ G1.add_edges_from([(i, i) for i in range(5, 8)] * 3)
1523
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1524
+ assert not m
1525
+
1526
+ # Compensate in G2
1527
+ G2.add_edges_from([(i, i) for i in range(3)] * 3)
1528
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1529
+ assert m
1530
+
1531
+ # Add one more self-loop in G2
1532
+ G2.add_edges_from([(0, 0), (1, 1), (1, 1)])
1533
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1534
+ assert not m
1535
+
1536
+ # Compensate in G1
1537
+ G1.add_edges_from([(5, 5), (7, 7), (7, 7)])
1538
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1539
+ assert m
1540
+
1541
+ def test_disconnected_multigraph_all_different_labels(self):
1542
+ G1 = nx.MultiGraph()
1543
+ G1.add_nodes_from(list(range(10)))
1544
+ G1.add_edges_from([(i, i) for i in range(10)])
1545
+
1546
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
1547
+ G2 = nx.relabel_nodes(G1, mapped)
1548
+
1549
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
1550
+ nx.set_node_attributes(
1551
+ G2,
1552
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
1553
+ "label",
1554
+ )
1555
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1556
+ assert m
1557
+ assert m == mapped
1558
+
1559
+ # Add self-loops to non-mapped nodes. Now it is not the same, as there are different labels
1560
+ G1.add_edges_from([(i, i) for i in range(5, 8)] * 3)
1561
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1562
+ assert not m
1563
+
1564
+ # Add self-loops to non mapped nodes in G2 as well
1565
+ G2.add_edges_from([(mapped[i], mapped[i]) for i in range(3)] * 7)
1566
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1567
+ assert not m
1568
+
1569
+ # Add self-loops to mapped nodes in G2
1570
+ G2.add_edges_from([(mapped[i], mapped[i]) for i in range(5, 8)] * 3)
1571
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1572
+ assert not m
1573
+
1574
+ # Add self-loops to G1 so that they are even in both graphs
1575
+ G1.add_edges_from([(i, i) for i in range(3)] * 7)
1576
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
1577
+ assert m
1578
+
1579
+
1580
+ class TestDiGraphISOVF2pp:
1581
+ def test_wikipedia_graph(self):
1582
+ edges1 = [
1583
+ (1, 5),
1584
+ (1, 2),
1585
+ (1, 4),
1586
+ (3, 2),
1587
+ (6, 2),
1588
+ (3, 4),
1589
+ (7, 3),
1590
+ (4, 8),
1591
+ (5, 8),
1592
+ (6, 5),
1593
+ (6, 7),
1594
+ (7, 8),
1595
+ ]
1596
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
1597
+
1598
+ G1 = nx.DiGraph(edges1)
1599
+ G2 = nx.relabel_nodes(G1, mapped)
1600
+
1601
+ assert vf2pp_isomorphism(G1, G2) == mapped
1602
+
1603
+ # Change the direction of an edge
1604
+ G1.remove_edge(1, 5)
1605
+ G1.add_edge(5, 1)
1606
+ assert vf2pp_isomorphism(G1, G2) is None
1607
+
1608
+ def test_non_isomorphic_same_degree_sequence(self):
1609
+ r"""
1610
+ G1 G2
1611
+ 1-------------2 1-------------2
1612
+ | \ | | \ |
1613
+ | 5-------6 | | 5-------6 |
1614
+ | | | | | | | |
1615
+ | 8-------7 | | 8-------7 |
1616
+ | / | | \ |
1617
+ 4-------------3 4-------------3
1618
+ """
1619
+ edges1 = [
1620
+ (1, 5),
1621
+ (1, 2),
1622
+ (4, 1),
1623
+ (3, 2),
1624
+ (3, 4),
1625
+ (5, 8),
1626
+ (6, 5),
1627
+ (6, 7),
1628
+ (7, 8),
1629
+ (4, 8),
1630
+ ]
1631
+ edges2 = [
1632
+ (1, 5),
1633
+ (1, 2),
1634
+ (4, 1),
1635
+ (3, 2),
1636
+ (3, 4),
1637
+ (5, 8),
1638
+ (6, 5),
1639
+ (6, 7),
1640
+ (7, 8),
1641
+ (3, 7),
1642
+ ]
1643
+
1644
+ G1 = nx.DiGraph(edges1)
1645
+ G2 = nx.DiGraph(edges2)
1646
+ assert vf2pp_isomorphism(G1, G2) is None
1647
+
1648
+
1649
+ def test_isomorphvf2pp_multidigraphs():
1650
+ g = nx.MultiDiGraph({0: [1, 1, 2, 2, 3], 1: [2, 3, 3], 2: [3]})
1651
+ h = nx.MultiDiGraph({0: [1, 1, 2, 2, 3], 1: [2, 3, 3], 2: [3]})
1652
+ assert nx.vf2pp_is_isomorphic(g, h)
1653
+
1654
+ h = nx.MultiDiGraph({0: [1, 1, 2, 2, 3], 1: [2, 3, 3], 3: [2]})
1655
+ assert not nx.vf2pp_is_isomorphic(g, h)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py ADDED
@@ -0,0 +1,3118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import itertools as it
2
+
3
+ import pytest
4
+
5
+ import networkx as nx
6
+ from networkx import vf2pp_is_isomorphic, vf2pp_isomorphism
7
+ from networkx.algorithms.isomorphism.vf2pp import (
8
+ _consistent_PT,
9
+ _cut_PT,
10
+ _feasibility,
11
+ _find_candidates,
12
+ _find_candidates_Di,
13
+ _GraphParameters,
14
+ _initialize_parameters,
15
+ _matching_order,
16
+ _restore_Tinout,
17
+ _restore_Tinout_Di,
18
+ _StateParameters,
19
+ _update_Tinout,
20
+ )
21
+
22
+ labels_same = ["blue"]
23
+
24
+ labels_many = [
25
+ "white",
26
+ "red",
27
+ "blue",
28
+ "green",
29
+ "orange",
30
+ "black",
31
+ "purple",
32
+ "yellow",
33
+ "brown",
34
+ "cyan",
35
+ "solarized",
36
+ "pink",
37
+ "none",
38
+ ]
39
+
40
+
41
+ class TestNodeOrdering:
42
+ def test_empty_graph(self):
43
+ G1 = nx.Graph()
44
+ G2 = nx.Graph()
45
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
46
+ assert len(set(_matching_order(gparams))) == 0
47
+
48
+ def test_single_node(self):
49
+ G1 = nx.Graph()
50
+ G2 = nx.Graph()
51
+ G1.add_node(1)
52
+ G2.add_node(1)
53
+
54
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
55
+ nx.set_node_attributes(
56
+ G2,
57
+ dict(zip(G2, it.cycle(labels_many))),
58
+ "label",
59
+ )
60
+ l1, l2 = (
61
+ nx.get_node_attributes(G1, "label"),
62
+ nx.get_node_attributes(G2, "label"),
63
+ )
64
+
65
+ gparams = _GraphParameters(
66
+ G1,
67
+ G2,
68
+ l1,
69
+ l2,
70
+ nx.utils.groups(l1),
71
+ nx.utils.groups(l2),
72
+ nx.utils.groups(dict(G2.degree())),
73
+ )
74
+ m = _matching_order(gparams)
75
+ assert m == [1]
76
+
77
+ def test_matching_order(self):
78
+ labels = [
79
+ "blue",
80
+ "blue",
81
+ "red",
82
+ "red",
83
+ "red",
84
+ "red",
85
+ "green",
86
+ "green",
87
+ "green",
88
+ "yellow",
89
+ "purple",
90
+ "purple",
91
+ "blue",
92
+ "blue",
93
+ ]
94
+ G1 = nx.Graph(
95
+ [
96
+ (0, 1),
97
+ (0, 2),
98
+ (1, 2),
99
+ (2, 5),
100
+ (2, 4),
101
+ (1, 3),
102
+ (1, 4),
103
+ (3, 6),
104
+ (4, 6),
105
+ (6, 7),
106
+ (7, 8),
107
+ (9, 10),
108
+ (9, 11),
109
+ (11, 12),
110
+ (11, 13),
111
+ (12, 13),
112
+ (10, 13),
113
+ ]
114
+ )
115
+ G2 = G1.copy()
116
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels))), "label")
117
+ nx.set_node_attributes(
118
+ G2,
119
+ dict(zip(G2, it.cycle(labels))),
120
+ "label",
121
+ )
122
+ l1, l2 = (
123
+ nx.get_node_attributes(G1, "label"),
124
+ nx.get_node_attributes(G2, "label"),
125
+ )
126
+ gparams = _GraphParameters(
127
+ G1,
128
+ G2,
129
+ l1,
130
+ l2,
131
+ nx.utils.groups(l1),
132
+ nx.utils.groups(l2),
133
+ nx.utils.groups(dict(G2.degree())),
134
+ )
135
+
136
+ expected = [9, 11, 10, 13, 12, 1, 2, 4, 0, 3, 6, 5, 7, 8]
137
+ assert _matching_order(gparams) == expected
138
+
139
+ def test_matching_order_all_branches(self):
140
+ G1 = nx.Graph(
141
+ [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 4), (3, 4)]
142
+ )
143
+ G1.add_node(5)
144
+ G2 = G1.copy()
145
+
146
+ G1.nodes[0]["label"] = "black"
147
+ G1.nodes[1]["label"] = "blue"
148
+ G1.nodes[2]["label"] = "blue"
149
+ G1.nodes[3]["label"] = "red"
150
+ G1.nodes[4]["label"] = "red"
151
+ G1.nodes[5]["label"] = "blue"
152
+
153
+ G2.nodes[0]["label"] = "black"
154
+ G2.nodes[1]["label"] = "blue"
155
+ G2.nodes[2]["label"] = "blue"
156
+ G2.nodes[3]["label"] = "red"
157
+ G2.nodes[4]["label"] = "red"
158
+ G2.nodes[5]["label"] = "blue"
159
+
160
+ l1, l2 = (
161
+ nx.get_node_attributes(G1, "label"),
162
+ nx.get_node_attributes(G2, "label"),
163
+ )
164
+ gparams = _GraphParameters(
165
+ G1,
166
+ G2,
167
+ l1,
168
+ l2,
169
+ nx.utils.groups(l1),
170
+ nx.utils.groups(l2),
171
+ nx.utils.groups(dict(G2.degree())),
172
+ )
173
+
174
+ expected = [0, 4, 1, 3, 2, 5]
175
+ assert _matching_order(gparams) == expected
176
+
177
+
178
+ class TestGraphCandidateSelection:
179
+ G1_edges = [
180
+ (1, 2),
181
+ (1, 4),
182
+ (1, 5),
183
+ (2, 3),
184
+ (2, 4),
185
+ (3, 4),
186
+ (4, 5),
187
+ (1, 6),
188
+ (6, 7),
189
+ (6, 8),
190
+ (8, 9),
191
+ (7, 9),
192
+ ]
193
+ mapped = {
194
+ 0: "x",
195
+ 1: "a",
196
+ 2: "b",
197
+ 3: "c",
198
+ 4: "d",
199
+ 5: "e",
200
+ 6: "f",
201
+ 7: "g",
202
+ 8: "h",
203
+ 9: "i",
204
+ }
205
+
206
+ def test_no_covered_neighbors_no_labels(self):
207
+ G1 = nx.Graph()
208
+ G1.add_edges_from(self.G1_edges)
209
+ G1.add_node(0)
210
+ G2 = nx.relabel_nodes(G1, self.mapped)
211
+
212
+ G1_degree = dict(G1.degree)
213
+ l1 = dict(G1.nodes(data="label", default=-1))
214
+ l2 = dict(G2.nodes(data="label", default=-1))
215
+ gparams = _GraphParameters(
216
+ G1,
217
+ G2,
218
+ l1,
219
+ l2,
220
+ nx.utils.groups(l1),
221
+ nx.utils.groups(l2),
222
+ nx.utils.groups(dict(G2.degree())),
223
+ )
224
+
225
+ m = {9: self.mapped[9], 1: self.mapped[1]}
226
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
227
+
228
+ T1 = {7, 8, 2, 4, 5}
229
+ T1_tilde = {0, 3, 6}
230
+ T2 = {"g", "h", "b", "d", "e"}
231
+ T2_tilde = {"x", "c", "f"}
232
+
233
+ sparams = _StateParameters(
234
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
235
+ )
236
+
237
+ u = 3
238
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
239
+ assert candidates == {self.mapped[u]}
240
+
241
+ u = 0
242
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
243
+ assert candidates == {self.mapped[u]}
244
+
245
+ m.pop(9)
246
+ m_rev.pop(self.mapped[9])
247
+
248
+ T1 = {2, 4, 5, 6}
249
+ T1_tilde = {0, 3, 7, 8, 9}
250
+ T2 = {"g", "h", "b", "d", "e", "f"}
251
+ T2_tilde = {"x", "c", "g", "h", "i"}
252
+
253
+ sparams = _StateParameters(
254
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
255
+ )
256
+
257
+ u = 7
258
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
259
+ assert candidates == {
260
+ self.mapped[u],
261
+ self.mapped[8],
262
+ self.mapped[3],
263
+ self.mapped[9],
264
+ }
265
+
266
+ def test_no_covered_neighbors_with_labels(self):
267
+ G1 = nx.Graph()
268
+ G1.add_edges_from(self.G1_edges)
269
+ G1.add_node(0)
270
+ G2 = nx.relabel_nodes(G1, self.mapped)
271
+
272
+ G1_degree = dict(G1.degree)
273
+ nx.set_node_attributes(
274
+ G1,
275
+ dict(zip(G1, it.cycle(labels_many))),
276
+ "label",
277
+ )
278
+ nx.set_node_attributes(
279
+ G2,
280
+ dict(
281
+ zip(
282
+ [self.mapped[n] for n in G1],
283
+ it.cycle(labels_many),
284
+ )
285
+ ),
286
+ "label",
287
+ )
288
+ l1 = dict(G1.nodes(data="label", default=-1))
289
+ l2 = dict(G2.nodes(data="label", default=-1))
290
+ gparams = _GraphParameters(
291
+ G1,
292
+ G2,
293
+ l1,
294
+ l2,
295
+ nx.utils.groups(l1),
296
+ nx.utils.groups(l2),
297
+ nx.utils.groups(dict(G2.degree())),
298
+ )
299
+
300
+ m = {9: self.mapped[9], 1: self.mapped[1]}
301
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
302
+
303
+ T1 = {7, 8, 2, 4, 5, 6}
304
+ T1_tilde = {0, 3}
305
+ T2 = {"g", "h", "b", "d", "e", "f"}
306
+ T2_tilde = {"x", "c"}
307
+
308
+ sparams = _StateParameters(
309
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
310
+ )
311
+
312
+ u = 3
313
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
314
+ assert candidates == {self.mapped[u]}
315
+
316
+ u = 0
317
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
318
+ assert candidates == {self.mapped[u]}
319
+
320
+ # Change label of disconnected node
321
+ G1.nodes[u]["label"] = "blue"
322
+ l1 = dict(G1.nodes(data="label", default=-1))
323
+ l2 = dict(G2.nodes(data="label", default=-1))
324
+ gparams = _GraphParameters(
325
+ G1,
326
+ G2,
327
+ l1,
328
+ l2,
329
+ nx.utils.groups(l1),
330
+ nx.utils.groups(l2),
331
+ nx.utils.groups(dict(G2.degree())),
332
+ )
333
+
334
+ # No candidate
335
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
336
+ assert candidates == set()
337
+
338
+ m.pop(9)
339
+ m_rev.pop(self.mapped[9])
340
+
341
+ T1 = {2, 4, 5, 6}
342
+ T1_tilde = {0, 3, 7, 8, 9}
343
+ T2 = {"b", "d", "e", "f"}
344
+ T2_tilde = {"x", "c", "g", "h", "i"}
345
+
346
+ sparams = _StateParameters(
347
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
348
+ )
349
+
350
+ u = 7
351
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
352
+ assert candidates == {self.mapped[u]}
353
+
354
+ G1.nodes[8]["label"] = G1.nodes[7]["label"]
355
+ G2.nodes[self.mapped[8]]["label"] = G1.nodes[7]["label"]
356
+ l1 = dict(G1.nodes(data="label", default=-1))
357
+ l2 = dict(G2.nodes(data="label", default=-1))
358
+ gparams = _GraphParameters(
359
+ G1,
360
+ G2,
361
+ l1,
362
+ l2,
363
+ nx.utils.groups(l1),
364
+ nx.utils.groups(l2),
365
+ nx.utils.groups(dict(G2.degree())),
366
+ )
367
+
368
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
369
+ assert candidates == {self.mapped[u], self.mapped[8]}
370
+
371
+ def test_covered_neighbors_no_labels(self):
372
+ G1 = nx.Graph()
373
+ G1.add_edges_from(self.G1_edges)
374
+ G1.add_node(0)
375
+ G2 = nx.relabel_nodes(G1, self.mapped)
376
+
377
+ G1_degree = dict(G1.degree)
378
+ l1 = dict(G1.nodes(data=None, default=-1))
379
+ l2 = dict(G2.nodes(data=None, default=-1))
380
+ gparams = _GraphParameters(
381
+ G1,
382
+ G2,
383
+ l1,
384
+ l2,
385
+ nx.utils.groups(l1),
386
+ nx.utils.groups(l2),
387
+ nx.utils.groups(dict(G2.degree())),
388
+ )
389
+
390
+ m = {9: self.mapped[9], 1: self.mapped[1]}
391
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
392
+
393
+ T1 = {7, 8, 2, 4, 5, 6}
394
+ T1_tilde = {0, 3}
395
+ T2 = {"g", "h", "b", "d", "e", "f"}
396
+ T2_tilde = {"x", "c"}
397
+
398
+ sparams = _StateParameters(
399
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
400
+ )
401
+
402
+ u = 5
403
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
404
+ assert candidates == {self.mapped[u]}
405
+
406
+ u = 6
407
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
408
+ assert candidates == {self.mapped[u], self.mapped[2]}
409
+
410
+ def test_covered_neighbors_with_labels(self):
411
+ G1 = nx.Graph()
412
+ G1.add_edges_from(self.G1_edges)
413
+ G1.add_node(0)
414
+ G2 = nx.relabel_nodes(G1, self.mapped)
415
+
416
+ G1_degree = dict(G1.degree)
417
+ nx.set_node_attributes(
418
+ G1,
419
+ dict(zip(G1, it.cycle(labels_many))),
420
+ "label",
421
+ )
422
+ nx.set_node_attributes(
423
+ G2,
424
+ dict(
425
+ zip(
426
+ [self.mapped[n] for n in G1],
427
+ it.cycle(labels_many),
428
+ )
429
+ ),
430
+ "label",
431
+ )
432
+ l1 = dict(G1.nodes(data="label", default=-1))
433
+ l2 = dict(G2.nodes(data="label", default=-1))
434
+ gparams = _GraphParameters(
435
+ G1,
436
+ G2,
437
+ l1,
438
+ l2,
439
+ nx.utils.groups(l1),
440
+ nx.utils.groups(l2),
441
+ nx.utils.groups(dict(G2.degree())),
442
+ )
443
+
444
+ m = {9: self.mapped[9], 1: self.mapped[1]}
445
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
446
+
447
+ T1 = {7, 8, 2, 4, 5, 6}
448
+ T1_tilde = {0, 3}
449
+ T2 = {"g", "h", "b", "d", "e", "f"}
450
+ T2_tilde = {"x", "c"}
451
+
452
+ sparams = _StateParameters(
453
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
454
+ )
455
+
456
+ u = 5
457
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
458
+ assert candidates == {self.mapped[u]}
459
+
460
+ u = 6
461
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
462
+ assert candidates == {self.mapped[u]}
463
+
464
+ # Assign to 2, the same label as 6
465
+ G1.nodes[2]["label"] = G1.nodes[u]["label"]
466
+ G2.nodes[self.mapped[2]]["label"] = G1.nodes[u]["label"]
467
+ l1 = dict(G1.nodes(data="label", default=-1))
468
+ l2 = dict(G2.nodes(data="label", default=-1))
469
+ gparams = _GraphParameters(
470
+ G1,
471
+ G2,
472
+ l1,
473
+ l2,
474
+ nx.utils.groups(l1),
475
+ nx.utils.groups(l2),
476
+ nx.utils.groups(dict(G2.degree())),
477
+ )
478
+
479
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
480
+ assert candidates == {self.mapped[u], self.mapped[2]}
481
+
482
+
483
+ class TestDiGraphCandidateSelection:
484
+ G1_edges = [
485
+ (1, 2),
486
+ (1, 4),
487
+ (5, 1),
488
+ (2, 3),
489
+ (4, 2),
490
+ (3, 4),
491
+ (4, 5),
492
+ (1, 6),
493
+ (6, 7),
494
+ (6, 8),
495
+ (8, 9),
496
+ (7, 9),
497
+ ]
498
+ mapped = {
499
+ 0: "x",
500
+ 1: "a",
501
+ 2: "b",
502
+ 3: "c",
503
+ 4: "d",
504
+ 5: "e",
505
+ 6: "f",
506
+ 7: "g",
507
+ 8: "h",
508
+ 9: "i",
509
+ }
510
+
511
+ def test_no_covered_neighbors_no_labels(self):
512
+ G1 = nx.DiGraph()
513
+ G1.add_edges_from(self.G1_edges)
514
+ G1.add_node(0)
515
+ G2 = nx.relabel_nodes(G1, self.mapped)
516
+
517
+ G1_degree = {
518
+ n: (in_degree, out_degree)
519
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
520
+ }
521
+
522
+ l1 = dict(G1.nodes(data="label", default=-1))
523
+ l2 = dict(G2.nodes(data="label", default=-1))
524
+ gparams = _GraphParameters(
525
+ G1,
526
+ G2,
527
+ l1,
528
+ l2,
529
+ nx.utils.groups(l1),
530
+ nx.utils.groups(l2),
531
+ nx.utils.groups(
532
+ {
533
+ node: (in_degree, out_degree)
534
+ for (node, in_degree), (_, out_degree) in zip(
535
+ G2.in_degree(), G2.out_degree()
536
+ )
537
+ }
538
+ ),
539
+ )
540
+
541
+ m = {9: self.mapped[9], 1: self.mapped[1]}
542
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
543
+
544
+ T1_out = {2, 4, 6}
545
+ T1_in = {5, 7, 8}
546
+ T1_tilde = {0, 3}
547
+ T2_out = {"b", "d", "f"}
548
+ T2_in = {"e", "g", "h"}
549
+ T2_tilde = {"x", "c"}
550
+
551
+ sparams = _StateParameters(
552
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
553
+ )
554
+
555
+ u = 3
556
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
557
+ assert candidates == {self.mapped[u]}
558
+
559
+ u = 0
560
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
561
+ assert candidates == {self.mapped[u]}
562
+
563
+ m.pop(9)
564
+ m_rev.pop(self.mapped[9])
565
+
566
+ T1_out = {2, 4, 6}
567
+ T1_in = {5}
568
+ T1_tilde = {0, 3, 7, 8, 9}
569
+ T2_out = {"b", "d", "f"}
570
+ T2_in = {"e"}
571
+ T2_tilde = {"x", "c", "g", "h", "i"}
572
+
573
+ sparams = _StateParameters(
574
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
575
+ )
576
+
577
+ u = 7
578
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
579
+ assert candidates == {self.mapped[u], self.mapped[8], self.mapped[3]}
580
+
581
+ def test_no_covered_neighbors_with_labels(self):
582
+ G1 = nx.DiGraph()
583
+ G1.add_edges_from(self.G1_edges)
584
+ G1.add_node(0)
585
+ G2 = nx.relabel_nodes(G1, self.mapped)
586
+
587
+ G1_degree = {
588
+ n: (in_degree, out_degree)
589
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
590
+ }
591
+ nx.set_node_attributes(
592
+ G1,
593
+ dict(zip(G1, it.cycle(labels_many))),
594
+ "label",
595
+ )
596
+ nx.set_node_attributes(
597
+ G2,
598
+ dict(
599
+ zip(
600
+ [self.mapped[n] for n in G1],
601
+ it.cycle(labels_many),
602
+ )
603
+ ),
604
+ "label",
605
+ )
606
+ l1 = dict(G1.nodes(data="label", default=-1))
607
+ l2 = dict(G2.nodes(data="label", default=-1))
608
+ gparams = _GraphParameters(
609
+ G1,
610
+ G2,
611
+ l1,
612
+ l2,
613
+ nx.utils.groups(l1),
614
+ nx.utils.groups(l2),
615
+ nx.utils.groups(
616
+ {
617
+ node: (in_degree, out_degree)
618
+ for (node, in_degree), (_, out_degree) in zip(
619
+ G2.in_degree(), G2.out_degree()
620
+ )
621
+ }
622
+ ),
623
+ )
624
+
625
+ m = {9: self.mapped[9], 1: self.mapped[1]}
626
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
627
+
628
+ T1_out = {2, 4, 6}
629
+ T1_in = {5, 7, 8}
630
+ T1_tilde = {0, 3}
631
+ T2_out = {"b", "d", "f"}
632
+ T2_in = {"e", "g", "h"}
633
+ T2_tilde = {"x", "c"}
634
+
635
+ sparams = _StateParameters(
636
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
637
+ )
638
+
639
+ u = 3
640
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
641
+ assert candidates == {self.mapped[u]}
642
+
643
+ u = 0
644
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
645
+ assert candidates == {self.mapped[u]}
646
+
647
+ # Change label of disconnected node
648
+ G1.nodes[u]["label"] = "blue"
649
+ l1 = dict(G1.nodes(data="label", default=-1))
650
+ l2 = dict(G2.nodes(data="label", default=-1))
651
+ gparams = _GraphParameters(
652
+ G1,
653
+ G2,
654
+ l1,
655
+ l2,
656
+ nx.utils.groups(l1),
657
+ nx.utils.groups(l2),
658
+ nx.utils.groups(
659
+ {
660
+ node: (in_degree, out_degree)
661
+ for (node, in_degree), (_, out_degree) in zip(
662
+ G2.in_degree(), G2.out_degree()
663
+ )
664
+ }
665
+ ),
666
+ )
667
+
668
+ # No candidate
669
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
670
+ assert candidates == set()
671
+
672
+ m.pop(9)
673
+ m_rev.pop(self.mapped[9])
674
+
675
+ T1_out = {2, 4, 6}
676
+ T1_in = {5}
677
+ T1_tilde = {0, 3, 7, 8, 9}
678
+ T2_out = {"b", "d", "f"}
679
+ T2_in = {"e"}
680
+ T2_tilde = {"x", "c", "g", "h", "i"}
681
+
682
+ sparams = _StateParameters(
683
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
684
+ )
685
+
686
+ u = 7
687
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
688
+ assert candidates == {self.mapped[u]}
689
+
690
+ G1.nodes[8]["label"] = G1.nodes[7]["label"]
691
+ G2.nodes[self.mapped[8]]["label"] = G1.nodes[7]["label"]
692
+ l1 = dict(G1.nodes(data="label", default=-1))
693
+ l2 = dict(G2.nodes(data="label", default=-1))
694
+ gparams = _GraphParameters(
695
+ G1,
696
+ G2,
697
+ l1,
698
+ l2,
699
+ nx.utils.groups(l1),
700
+ nx.utils.groups(l2),
701
+ nx.utils.groups(
702
+ {
703
+ node: (in_degree, out_degree)
704
+ for (node, in_degree), (_, out_degree) in zip(
705
+ G2.in_degree(), G2.out_degree()
706
+ )
707
+ }
708
+ ),
709
+ )
710
+
711
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
712
+ assert candidates == {self.mapped[u], self.mapped[8]}
713
+
714
+ def test_covered_neighbors_no_labels(self):
715
+ G1 = nx.DiGraph()
716
+ G1.add_edges_from(self.G1_edges)
717
+ G1.add_node(0)
718
+ G2 = nx.relabel_nodes(G1, self.mapped)
719
+
720
+ G1_degree = {
721
+ n: (in_degree, out_degree)
722
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
723
+ }
724
+
725
+ l1 = dict(G1.nodes(data=None, default=-1))
726
+ l2 = dict(G2.nodes(data=None, default=-1))
727
+ gparams = _GraphParameters(
728
+ G1,
729
+ G2,
730
+ l1,
731
+ l2,
732
+ nx.utils.groups(l1),
733
+ nx.utils.groups(l2),
734
+ nx.utils.groups(
735
+ {
736
+ node: (in_degree, out_degree)
737
+ for (node, in_degree), (_, out_degree) in zip(
738
+ G2.in_degree(), G2.out_degree()
739
+ )
740
+ }
741
+ ),
742
+ )
743
+
744
+ m = {9: self.mapped[9], 1: self.mapped[1]}
745
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
746
+
747
+ T1_out = {2, 4, 6}
748
+ T1_in = {5, 7, 8}
749
+ T1_tilde = {0, 3}
750
+ T2_out = {"b", "d", "f"}
751
+ T2_in = {"e", "g", "h"}
752
+ T2_tilde = {"x", "c"}
753
+
754
+ sparams = _StateParameters(
755
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
756
+ )
757
+
758
+ u = 5
759
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
760
+ assert candidates == {self.mapped[u]}
761
+
762
+ u = 6
763
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
764
+ assert candidates == {self.mapped[u]}
765
+
766
+ # Change the direction of an edge to make the degree orientation same as first candidate of u.
767
+ G1.remove_edge(4, 2)
768
+ G1.add_edge(2, 4)
769
+ G2.remove_edge("d", "b")
770
+ G2.add_edge("b", "d")
771
+
772
+ gparams = _GraphParameters(
773
+ G1,
774
+ G2,
775
+ l1,
776
+ l2,
777
+ nx.utils.groups(l1),
778
+ nx.utils.groups(l2),
779
+ nx.utils.groups(
780
+ {
781
+ node: (in_degree, out_degree)
782
+ for (node, in_degree), (_, out_degree) in zip(
783
+ G2.in_degree(), G2.out_degree()
784
+ )
785
+ }
786
+ ),
787
+ )
788
+
789
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
790
+ assert candidates == {self.mapped[u], self.mapped[2]}
791
+
792
+ def test_covered_neighbors_with_labels(self):
793
+ G1 = nx.DiGraph()
794
+ G1.add_edges_from(self.G1_edges)
795
+ G1.add_node(0)
796
+ G2 = nx.relabel_nodes(G1, self.mapped)
797
+
798
+ G1.remove_edge(4, 2)
799
+ G1.add_edge(2, 4)
800
+ G2.remove_edge("d", "b")
801
+ G2.add_edge("b", "d")
802
+
803
+ G1_degree = {
804
+ n: (in_degree, out_degree)
805
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
806
+ }
807
+
808
+ nx.set_node_attributes(
809
+ G1,
810
+ dict(zip(G1, it.cycle(labels_many))),
811
+ "label",
812
+ )
813
+ nx.set_node_attributes(
814
+ G2,
815
+ dict(
816
+ zip(
817
+ [self.mapped[n] for n in G1],
818
+ it.cycle(labels_many),
819
+ )
820
+ ),
821
+ "label",
822
+ )
823
+ l1 = dict(G1.nodes(data="label", default=-1))
824
+ l2 = dict(G2.nodes(data="label", default=-1))
825
+ gparams = _GraphParameters(
826
+ G1,
827
+ G2,
828
+ l1,
829
+ l2,
830
+ nx.utils.groups(l1),
831
+ nx.utils.groups(l2),
832
+ nx.utils.groups(
833
+ {
834
+ node: (in_degree, out_degree)
835
+ for (node, in_degree), (_, out_degree) in zip(
836
+ G2.in_degree(), G2.out_degree()
837
+ )
838
+ }
839
+ ),
840
+ )
841
+
842
+ m = {9: self.mapped[9], 1: self.mapped[1]}
843
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
844
+
845
+ T1_out = {2, 4, 6}
846
+ T1_in = {5, 7, 8}
847
+ T1_tilde = {0, 3}
848
+ T2_out = {"b", "d", "f"}
849
+ T2_in = {"e", "g", "h"}
850
+ T2_tilde = {"x", "c"}
851
+
852
+ sparams = _StateParameters(
853
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
854
+ )
855
+
856
+ u = 5
857
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
858
+ assert candidates == {self.mapped[u]}
859
+
860
+ u = 6
861
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
862
+ assert candidates == {self.mapped[u]}
863
+
864
+ # Assign to 2, the same label as 6
865
+ G1.nodes[2]["label"] = G1.nodes[u]["label"]
866
+ G2.nodes[self.mapped[2]]["label"] = G1.nodes[u]["label"]
867
+ l1 = dict(G1.nodes(data="label", default=-1))
868
+ l2 = dict(G2.nodes(data="label", default=-1))
869
+ gparams = _GraphParameters(
870
+ G1,
871
+ G2,
872
+ l1,
873
+ l2,
874
+ nx.utils.groups(l1),
875
+ nx.utils.groups(l2),
876
+ nx.utils.groups(
877
+ {
878
+ node: (in_degree, out_degree)
879
+ for (node, in_degree), (_, out_degree) in zip(
880
+ G2.in_degree(), G2.out_degree()
881
+ )
882
+ }
883
+ ),
884
+ )
885
+
886
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
887
+ assert candidates == {self.mapped[u], self.mapped[2]}
888
+
889
+ # Change the direction of an edge to make the degree orientation same as first candidate of u.
890
+ G1.remove_edge(2, 4)
891
+ G1.add_edge(4, 2)
892
+ G2.remove_edge("b", "d")
893
+ G2.add_edge("d", "b")
894
+
895
+ gparams = _GraphParameters(
896
+ G1,
897
+ G2,
898
+ l1,
899
+ l2,
900
+ nx.utils.groups(l1),
901
+ nx.utils.groups(l2),
902
+ nx.utils.groups(
903
+ {
904
+ node: (in_degree, out_degree)
905
+ for (node, in_degree), (_, out_degree) in zip(
906
+ G2.in_degree(), G2.out_degree()
907
+ )
908
+ }
909
+ ),
910
+ )
911
+
912
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
913
+ assert candidates == {self.mapped[u]}
914
+
915
+ def test_same_in_out_degrees_no_candidate(self):
916
+ g1 = nx.DiGraph([(4, 1), (4, 2), (3, 4), (5, 4), (6, 4)])
917
+ g2 = nx.DiGraph([(1, 4), (2, 4), (3, 4), (4, 5), (4, 6)])
918
+
919
+ l1 = dict(g1.nodes(data=None, default=-1))
920
+ l2 = dict(g2.nodes(data=None, default=-1))
921
+ gparams = _GraphParameters(
922
+ g1,
923
+ g2,
924
+ l1,
925
+ l2,
926
+ nx.utils.groups(l1),
927
+ nx.utils.groups(l2),
928
+ nx.utils.groups(
929
+ {
930
+ node: (in_degree, out_degree)
931
+ for (node, in_degree), (_, out_degree) in zip(
932
+ g2.in_degree(), g2.out_degree()
933
+ )
934
+ }
935
+ ),
936
+ )
937
+
938
+ g1_degree = {
939
+ n: (in_degree, out_degree)
940
+ for (n, in_degree), (_, out_degree) in zip(g1.in_degree, g1.out_degree)
941
+ }
942
+
943
+ m = {1: 1, 2: 2, 3: 3}
944
+ m_rev = m.copy()
945
+
946
+ T1_out = {4}
947
+ T1_in = {4}
948
+ T1_tilde = {5, 6}
949
+ T2_out = {4}
950
+ T2_in = {4}
951
+ T2_tilde = {5, 6}
952
+
953
+ sparams = _StateParameters(
954
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
955
+ )
956
+
957
+ u = 4
958
+ # despite the same in and out degree, there's no candidate for u=4
959
+ candidates = _find_candidates_Di(u, gparams, sparams, g1_degree)
960
+ assert candidates == set()
961
+ # Notice how the regular candidate selection method returns wrong result.
962
+ assert _find_candidates(u, gparams, sparams, g1_degree) == {4}
963
+
964
+
965
+ class TestGraphISOFeasibility:
966
+ def test_const_covered_neighbors(self):
967
+ G1 = nx.Graph([(0, 1), (1, 2), (3, 0), (3, 2)])
968
+ G2 = nx.Graph([("a", "b"), ("b", "c"), ("k", "a"), ("k", "c")])
969
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
970
+ sparams = _StateParameters(
971
+ {0: "a", 1: "b", 2: "c"},
972
+ {"a": 0, "b": 1, "c": 2},
973
+ None,
974
+ None,
975
+ None,
976
+ None,
977
+ None,
978
+ None,
979
+ None,
980
+ None,
981
+ )
982
+ u, v = 3, "k"
983
+ assert _consistent_PT(u, v, gparams, sparams)
984
+
985
+ def test_const_no_covered_neighbors(self):
986
+ G1 = nx.Graph([(0, 1), (1, 2), (3, 4), (3, 5)])
987
+ G2 = nx.Graph([("a", "b"), ("b", "c"), ("k", "w"), ("k", "z")])
988
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
989
+ sparams = _StateParameters(
990
+ {0: "a", 1: "b", 2: "c"},
991
+ {"a": 0, "b": 1, "c": 2},
992
+ None,
993
+ None,
994
+ None,
995
+ None,
996
+ None,
997
+ None,
998
+ None,
999
+ None,
1000
+ )
1001
+ u, v = 3, "k"
1002
+ assert _consistent_PT(u, v, gparams, sparams)
1003
+
1004
+ def test_const_mixed_covered_uncovered_neighbors(self):
1005
+ G1 = nx.Graph([(0, 1), (1, 2), (3, 0), (3, 2), (3, 4), (3, 5)])
1006
+ G2 = nx.Graph(
1007
+ [("a", "b"), ("b", "c"), ("k", "a"), ("k", "c"), ("k", "w"), ("k", "z")]
1008
+ )
1009
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
1010
+ sparams = _StateParameters(
1011
+ {0: "a", 1: "b", 2: "c"},
1012
+ {"a": 0, "b": 1, "c": 2},
1013
+ None,
1014
+ None,
1015
+ None,
1016
+ None,
1017
+ None,
1018
+ None,
1019
+ None,
1020
+ None,
1021
+ )
1022
+ u, v = 3, "k"
1023
+ assert _consistent_PT(u, v, gparams, sparams)
1024
+
1025
+ def test_const_fail_cases(self):
1026
+ G1 = nx.Graph(
1027
+ [
1028
+ (0, 1),
1029
+ (1, 2),
1030
+ (10, 0),
1031
+ (10, 3),
1032
+ (10, 4),
1033
+ (10, 5),
1034
+ (10, 6),
1035
+ (4, 1),
1036
+ (5, 3),
1037
+ ]
1038
+ )
1039
+ G2 = nx.Graph(
1040
+ [
1041
+ ("a", "b"),
1042
+ ("b", "c"),
1043
+ ("k", "a"),
1044
+ ("k", "d"),
1045
+ ("k", "e"),
1046
+ ("k", "f"),
1047
+ ("k", "g"),
1048
+ ("e", "b"),
1049
+ ("f", "d"),
1050
+ ]
1051
+ )
1052
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
1053
+ sparams = _StateParameters(
1054
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1055
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1056
+ None,
1057
+ None,
1058
+ None,
1059
+ None,
1060
+ None,
1061
+ None,
1062
+ None,
1063
+ None,
1064
+ )
1065
+ u, v = 10, "k"
1066
+ assert _consistent_PT(u, v, gparams, sparams)
1067
+
1068
+ # Delete one uncovered neighbor of u. Notice how it still passes the test.
1069
+ # Two reasons for this:
1070
+ # 1. If u, v had different degrees from the beginning, they wouldn't
1071
+ # be selected as candidates in the first place.
1072
+ # 2. Even if they are selected, consistency is basically 1-look-ahead,
1073
+ # meaning that we take into consideration the relation of the
1074
+ # candidates with their mapped neighbors. The node we deleted is
1075
+ # not a covered neighbor.
1076
+ # Such nodes will be checked by the cut_PT function, which is
1077
+ # basically the 2-look-ahead, checking the relation of the
1078
+ # candidates with T1, T2 (in which belongs the node we just deleted).
1079
+ G1.remove_node(6)
1080
+ assert _consistent_PT(u, v, gparams, sparams)
1081
+
1082
+ # Add one more covered neighbor of u in G1
1083
+ G1.add_edge(u, 2)
1084
+ assert not _consistent_PT(u, v, gparams, sparams)
1085
+
1086
+ # Compensate in G2
1087
+ G2.add_edge(v, "c")
1088
+ assert _consistent_PT(u, v, gparams, sparams)
1089
+
1090
+ # Add one more covered neighbor of v in G2
1091
+ G2.add_edge(v, "x")
1092
+ G1.add_node(7)
1093
+ sparams.mapping.update({7: "x"})
1094
+ sparams.reverse_mapping.update({"x": 7})
1095
+ assert not _consistent_PT(u, v, gparams, sparams)
1096
+
1097
+ # Compendate in G1
1098
+ G1.add_edge(u, 7)
1099
+ assert _consistent_PT(u, v, gparams, sparams)
1100
+
1101
+ @pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph))
1102
+ def test_cut_inconsistent_labels(self, graph_type):
1103
+ G1 = graph_type(
1104
+ [
1105
+ (0, 1),
1106
+ (1, 2),
1107
+ (10, 0),
1108
+ (10, 3),
1109
+ (10, 4),
1110
+ (10, 5),
1111
+ (10, 6),
1112
+ (4, 1),
1113
+ (5, 3),
1114
+ ]
1115
+ )
1116
+ G2 = graph_type(
1117
+ [
1118
+ ("a", "b"),
1119
+ ("b", "c"),
1120
+ ("k", "a"),
1121
+ ("k", "d"),
1122
+ ("k", "e"),
1123
+ ("k", "f"),
1124
+ ("k", "g"),
1125
+ ("e", "b"),
1126
+ ("f", "d"),
1127
+ ]
1128
+ )
1129
+
1130
+ l1 = {n: "blue" for n in G1.nodes()}
1131
+ l2 = {n: "blue" for n in G2.nodes()}
1132
+ l1.update({6: "green"}) # Change the label of one neighbor of u
1133
+
1134
+ gparams = _GraphParameters(
1135
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1136
+ )
1137
+ sparams = _StateParameters(
1138
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1139
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1140
+ None,
1141
+ None,
1142
+ None,
1143
+ None,
1144
+ None,
1145
+ None,
1146
+ None,
1147
+ None,
1148
+ )
1149
+
1150
+ u, v = 10, "k"
1151
+ assert _cut_PT(u, v, gparams, sparams)
1152
+
1153
+ def test_cut_consistent_labels(self):
1154
+ G1 = nx.Graph(
1155
+ [
1156
+ (0, 1),
1157
+ (1, 2),
1158
+ (10, 0),
1159
+ (10, 3),
1160
+ (10, 4),
1161
+ (10, 5),
1162
+ (10, 6),
1163
+ (4, 1),
1164
+ (5, 3),
1165
+ ]
1166
+ )
1167
+ G2 = nx.Graph(
1168
+ [
1169
+ ("a", "b"),
1170
+ ("b", "c"),
1171
+ ("k", "a"),
1172
+ ("k", "d"),
1173
+ ("k", "e"),
1174
+ ("k", "f"),
1175
+ ("k", "g"),
1176
+ ("e", "b"),
1177
+ ("f", "d"),
1178
+ ]
1179
+ )
1180
+
1181
+ l1 = {n: "blue" for n in G1.nodes()}
1182
+ l2 = {n: "blue" for n in G2.nodes()}
1183
+
1184
+ gparams = _GraphParameters(
1185
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1186
+ )
1187
+ sparams = _StateParameters(
1188
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1189
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1190
+ {4, 5},
1191
+ None,
1192
+ {6},
1193
+ None,
1194
+ {"e", "f"},
1195
+ None,
1196
+ {"g"},
1197
+ None,
1198
+ )
1199
+
1200
+ u, v = 10, "k"
1201
+ assert not _cut_PT(u, v, gparams, sparams)
1202
+
1203
+ def test_cut_same_labels(self):
1204
+ G1 = nx.Graph(
1205
+ [
1206
+ (0, 1),
1207
+ (1, 2),
1208
+ (10, 0),
1209
+ (10, 3),
1210
+ (10, 4),
1211
+ (10, 5),
1212
+ (10, 6),
1213
+ (4, 1),
1214
+ (5, 3),
1215
+ ]
1216
+ )
1217
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
1218
+ G2 = nx.relabel_nodes(G1, mapped)
1219
+ l1 = {n: "blue" for n in G1.nodes()}
1220
+ l2 = {n: "blue" for n in G2.nodes()}
1221
+
1222
+ gparams = _GraphParameters(
1223
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1224
+ )
1225
+ sparams = _StateParameters(
1226
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1227
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1228
+ {4, 5},
1229
+ None,
1230
+ {6},
1231
+ None,
1232
+ {"e", "f"},
1233
+ None,
1234
+ {"g"},
1235
+ None,
1236
+ )
1237
+
1238
+ u, v = 10, "k"
1239
+ assert not _cut_PT(u, v, gparams, sparams)
1240
+
1241
+ # Change intersection between G1[u] and T1, so it's not the same as the
1242
+ # one between G2[v] and T2
1243
+ G1.remove_edge(u, 4)
1244
+ assert _cut_PT(u, v, gparams, sparams)
1245
+
1246
+ # Compensate in G2
1247
+ G2.remove_edge(v, mapped[4])
1248
+ assert not _cut_PT(u, v, gparams, sparams)
1249
+
1250
+ # Change intersection between G2[v] and T2_tilde, so it's not the same
1251
+ # as the one between G1[u] and T1_tilde
1252
+ G2.remove_edge(v, mapped[6])
1253
+ assert _cut_PT(u, v, gparams, sparams)
1254
+
1255
+ # Compensate in G1
1256
+ G1.remove_edge(u, 6)
1257
+ assert not _cut_PT(u, v, gparams, sparams)
1258
+
1259
+ # Add disconnected nodes, which will form the new Ti_out
1260
+ G1.add_nodes_from([6, 7, 8])
1261
+ G2.add_nodes_from(["g", "y", "z"])
1262
+ sparams.T1_tilde.update({6, 7, 8})
1263
+ sparams.T2_tilde.update({"g", "y", "z"})
1264
+
1265
+ l1 = {n: "blue" for n in G1.nodes()}
1266
+ l2 = {n: "blue" for n in G2.nodes()}
1267
+ gparams = _GraphParameters(
1268
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1269
+ )
1270
+
1271
+ assert not _cut_PT(u, v, gparams, sparams)
1272
+
1273
+ # Add some new nodes to the mapping
1274
+ sparams.mapping.update({6: "g", 7: "y"})
1275
+ sparams.reverse_mapping.update({"g": 6, "y": 7})
1276
+
1277
+ # Add more nodes to T1, T2.
1278
+ G1.add_edges_from([(6, 20), (7, 20), (6, 21)])
1279
+ G2.add_edges_from([("g", "i"), ("g", "j"), ("y", "j")])
1280
+
1281
+ sparams.mapping.update({20: "j", 21: "i"})
1282
+ sparams.reverse_mapping.update({"j": 20, "i": 21})
1283
+ sparams.T1.update({20, 21})
1284
+ sparams.T2.update({"i", "j"})
1285
+ sparams.T1_tilde.difference_update({6, 7})
1286
+ sparams.T2_tilde.difference_update({"g", "y"})
1287
+
1288
+ assert not _cut_PT(u, v, gparams, sparams)
1289
+
1290
+ # Add nodes from the new T1 and T2, as neighbors of u and v respectively
1291
+ G1.add_edges_from([(u, 20), (u, 21)])
1292
+ G2.add_edges_from([(v, "i"), (v, "j")])
1293
+ l1 = {n: "blue" for n in G1.nodes()}
1294
+ l2 = {n: "blue" for n in G2.nodes()}
1295
+ gparams = _GraphParameters(
1296
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1297
+ )
1298
+
1299
+ assert not _cut_PT(u, v, gparams, sparams)
1300
+
1301
+ # Change the edges, maintaining the G1[u]-T1 intersection
1302
+ G1.remove_edge(u, 20)
1303
+ G1.add_edge(u, 4)
1304
+ assert not _cut_PT(u, v, gparams, sparams)
1305
+
1306
+ # Connect u to 8 which is still in T1_tilde
1307
+ G1.add_edge(u, 8)
1308
+ assert _cut_PT(u, v, gparams, sparams)
1309
+
1310
+ # Same for v and z, so that inters(G1[u], T1out) == inters(G2[v], T2out)
1311
+ G2.add_edge(v, "z")
1312
+ assert not _cut_PT(u, v, gparams, sparams)
1313
+
1314
+ def test_cut_different_labels(self):
1315
+ G1 = nx.Graph(
1316
+ [
1317
+ (0, 1),
1318
+ (1, 2),
1319
+ (1, 14),
1320
+ (0, 4),
1321
+ (1, 5),
1322
+ (2, 6),
1323
+ (3, 7),
1324
+ (3, 6),
1325
+ (4, 10),
1326
+ (4, 9),
1327
+ (6, 10),
1328
+ (20, 9),
1329
+ (20, 15),
1330
+ (20, 12),
1331
+ (20, 11),
1332
+ (12, 13),
1333
+ (11, 13),
1334
+ (20, 8),
1335
+ (20, 3),
1336
+ (20, 5),
1337
+ (20, 0),
1338
+ ]
1339
+ )
1340
+ mapped = {
1341
+ 0: "a",
1342
+ 1: "b",
1343
+ 2: "c",
1344
+ 3: "d",
1345
+ 4: "e",
1346
+ 5: "f",
1347
+ 6: "g",
1348
+ 7: "h",
1349
+ 8: "i",
1350
+ 9: "j",
1351
+ 10: "k",
1352
+ 11: "l",
1353
+ 12: "m",
1354
+ 13: "n",
1355
+ 14: "o",
1356
+ 15: "p",
1357
+ 20: "x",
1358
+ }
1359
+ G2 = nx.relabel_nodes(G1, mapped)
1360
+
1361
+ l1 = {n: "none" for n in G1.nodes()}
1362
+ l2 = {}
1363
+
1364
+ l1.update(
1365
+ {
1366
+ 9: "blue",
1367
+ 15: "blue",
1368
+ 12: "blue",
1369
+ 11: "green",
1370
+ 3: "green",
1371
+ 8: "red",
1372
+ 0: "red",
1373
+ 5: "yellow",
1374
+ }
1375
+ )
1376
+ l2.update({mapped[n]: l for n, l in l1.items()})
1377
+
1378
+ gparams = _GraphParameters(
1379
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1380
+ )
1381
+ sparams = _StateParameters(
1382
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1383
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1384
+ {4, 5, 6, 7, 14},
1385
+ None,
1386
+ {9, 10, 15, 12, 11, 13, 8},
1387
+ None,
1388
+ {"e", "f", "g", "h", "o"},
1389
+ None,
1390
+ {"j", "k", "l", "m", "n", "i", "p"},
1391
+ None,
1392
+ )
1393
+
1394
+ u, v = 20, "x"
1395
+ assert not _cut_PT(u, v, gparams, sparams)
1396
+
1397
+ # Change the orientation of the labels on neighbors of u compared to
1398
+ # neighbors of v. Leave the structure intact
1399
+ l1.update({9: "red"})
1400
+ assert _cut_PT(u, v, gparams, sparams)
1401
+
1402
+ # compensate in G2
1403
+ l2.update({mapped[9]: "red"})
1404
+ assert not _cut_PT(u, v, gparams, sparams)
1405
+
1406
+ # Change the intersection of G1[u] and T1
1407
+ G1.add_edge(u, 4)
1408
+ assert _cut_PT(u, v, gparams, sparams)
1409
+
1410
+ # Same for G2[v] and T2
1411
+ G2.add_edge(v, mapped[4])
1412
+ assert not _cut_PT(u, v, gparams, sparams)
1413
+
1414
+ # Change the intersection of G2[v] and T2_tilde
1415
+ G2.remove_edge(v, mapped[8])
1416
+ assert _cut_PT(u, v, gparams, sparams)
1417
+
1418
+ # Same for G1[u] and T1_tilde
1419
+ G1.remove_edge(u, 8)
1420
+ assert not _cut_PT(u, v, gparams, sparams)
1421
+
1422
+ # Place 8 and mapped[8] in T1 and T2 respectively, by connecting it to covered nodes
1423
+ G1.add_edge(8, 3)
1424
+ G2.add_edge(mapped[8], mapped[3])
1425
+ sparams.T1.add(8)
1426
+ sparams.T2.add(mapped[8])
1427
+ sparams.T1_tilde.remove(8)
1428
+ sparams.T2_tilde.remove(mapped[8])
1429
+
1430
+ assert not _cut_PT(u, v, gparams, sparams)
1431
+
1432
+ # Remove neighbor of u from T1
1433
+ G1.remove_node(5)
1434
+ l1.pop(5)
1435
+ sparams.T1.remove(5)
1436
+ assert _cut_PT(u, v, gparams, sparams)
1437
+
1438
+ # Same in G2
1439
+ G2.remove_node(mapped[5])
1440
+ l2.pop(mapped[5])
1441
+ sparams.T2.remove(mapped[5])
1442
+ assert not _cut_PT(u, v, gparams, sparams)
1443
+
1444
+ def test_feasibility_same_labels(self):
1445
+ G1 = nx.Graph(
1446
+ [
1447
+ (0, 1),
1448
+ (1, 2),
1449
+ (1, 14),
1450
+ (0, 4),
1451
+ (1, 5),
1452
+ (2, 6),
1453
+ (3, 7),
1454
+ (3, 6),
1455
+ (4, 10),
1456
+ (4, 9),
1457
+ (6, 10),
1458
+ (20, 9),
1459
+ (20, 15),
1460
+ (20, 12),
1461
+ (20, 11),
1462
+ (12, 13),
1463
+ (11, 13),
1464
+ (20, 8),
1465
+ (20, 2),
1466
+ (20, 5),
1467
+ (20, 0),
1468
+ ]
1469
+ )
1470
+ mapped = {
1471
+ 0: "a",
1472
+ 1: "b",
1473
+ 2: "c",
1474
+ 3: "d",
1475
+ 4: "e",
1476
+ 5: "f",
1477
+ 6: "g",
1478
+ 7: "h",
1479
+ 8: "i",
1480
+ 9: "j",
1481
+ 10: "k",
1482
+ 11: "l",
1483
+ 12: "m",
1484
+ 13: "n",
1485
+ 14: "o",
1486
+ 15: "p",
1487
+ 20: "x",
1488
+ }
1489
+ G2 = nx.relabel_nodes(G1, mapped)
1490
+
1491
+ l1 = {n: "blue" for n in G1.nodes()}
1492
+ l2 = {mapped[n]: "blue" for n in G1.nodes()}
1493
+
1494
+ gparams = _GraphParameters(
1495
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1496
+ )
1497
+ sparams = _StateParameters(
1498
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1499
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1500
+ {4, 5, 6, 7, 14},
1501
+ None,
1502
+ {9, 10, 15, 12, 11, 13, 8},
1503
+ None,
1504
+ {"e", "f", "g", "h", "o"},
1505
+ None,
1506
+ {"j", "k", "l", "m", "n", "i", "p"},
1507
+ None,
1508
+ )
1509
+
1510
+ u, v = 20, "x"
1511
+ assert not _cut_PT(u, v, gparams, sparams)
1512
+
1513
+ # Change structure in G2 such that, ONLY consistency is harmed
1514
+ G2.remove_edge(mapped[20], mapped[2])
1515
+ G2.add_edge(mapped[20], mapped[3])
1516
+
1517
+ # Consistency check fails, while the cutting rules are satisfied!
1518
+ assert not _cut_PT(u, v, gparams, sparams)
1519
+ assert not _consistent_PT(u, v, gparams, sparams)
1520
+
1521
+ # Compensate in G1 and make it consistent
1522
+ G1.remove_edge(20, 2)
1523
+ G1.add_edge(20, 3)
1524
+ assert not _cut_PT(u, v, gparams, sparams)
1525
+ assert _consistent_PT(u, v, gparams, sparams)
1526
+
1527
+ # ONLY fail the cutting check
1528
+ G2.add_edge(v, mapped[10])
1529
+ assert _cut_PT(u, v, gparams, sparams)
1530
+ assert _consistent_PT(u, v, gparams, sparams)
1531
+
1532
+ def test_feasibility_different_labels(self):
1533
+ G1 = nx.Graph(
1534
+ [
1535
+ (0, 1),
1536
+ (1, 2),
1537
+ (1, 14),
1538
+ (0, 4),
1539
+ (1, 5),
1540
+ (2, 6),
1541
+ (3, 7),
1542
+ (3, 6),
1543
+ (4, 10),
1544
+ (4, 9),
1545
+ (6, 10),
1546
+ (20, 9),
1547
+ (20, 15),
1548
+ (20, 12),
1549
+ (20, 11),
1550
+ (12, 13),
1551
+ (11, 13),
1552
+ (20, 8),
1553
+ (20, 2),
1554
+ (20, 5),
1555
+ (20, 0),
1556
+ ]
1557
+ )
1558
+ mapped = {
1559
+ 0: "a",
1560
+ 1: "b",
1561
+ 2: "c",
1562
+ 3: "d",
1563
+ 4: "e",
1564
+ 5: "f",
1565
+ 6: "g",
1566
+ 7: "h",
1567
+ 8: "i",
1568
+ 9: "j",
1569
+ 10: "k",
1570
+ 11: "l",
1571
+ 12: "m",
1572
+ 13: "n",
1573
+ 14: "o",
1574
+ 15: "p",
1575
+ 20: "x",
1576
+ }
1577
+ G2 = nx.relabel_nodes(G1, mapped)
1578
+
1579
+ l1 = {n: "none" for n in G1.nodes()}
1580
+ l2 = {}
1581
+
1582
+ l1.update(
1583
+ {
1584
+ 9: "blue",
1585
+ 15: "blue",
1586
+ 12: "blue",
1587
+ 11: "green",
1588
+ 2: "green",
1589
+ 8: "red",
1590
+ 0: "red",
1591
+ 5: "yellow",
1592
+ }
1593
+ )
1594
+ l2.update({mapped[n]: l for n, l in l1.items()})
1595
+
1596
+ gparams = _GraphParameters(
1597
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1598
+ )
1599
+ sparams = _StateParameters(
1600
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1601
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1602
+ {4, 5, 6, 7, 14},
1603
+ None,
1604
+ {9, 10, 15, 12, 11, 13, 8},
1605
+ None,
1606
+ {"e", "f", "g", "h", "o"},
1607
+ None,
1608
+ {"j", "k", "l", "m", "n", "i", "p"},
1609
+ None,
1610
+ )
1611
+
1612
+ u, v = 20, "x"
1613
+ assert not _cut_PT(u, v, gparams, sparams)
1614
+
1615
+ # Change structure in G2 such that, ONLY consistency is harmed
1616
+ G2.remove_edge(mapped[20], mapped[2])
1617
+ G2.add_edge(mapped[20], mapped[3])
1618
+ l2.update({mapped[3]: "green"})
1619
+
1620
+ # Consistency check fails, while the cutting rules are satisfied!
1621
+ assert not _cut_PT(u, v, gparams, sparams)
1622
+ assert not _consistent_PT(u, v, gparams, sparams)
1623
+
1624
+ # Compensate in G1 and make it consistent
1625
+ G1.remove_edge(20, 2)
1626
+ G1.add_edge(20, 3)
1627
+ l1.update({3: "green"})
1628
+ assert not _cut_PT(u, v, gparams, sparams)
1629
+ assert _consistent_PT(u, v, gparams, sparams)
1630
+
1631
+ # ONLY fail the cutting check
1632
+ l1.update({5: "red"})
1633
+ assert _cut_PT(u, v, gparams, sparams)
1634
+ assert _consistent_PT(u, v, gparams, sparams)
1635
+
1636
+
1637
+ class TestMultiGraphISOFeasibility:
1638
+ def test_const_covered_neighbors(self):
1639
+ G1 = nx.MultiGraph(
1640
+ [(0, 1), (0, 1), (1, 2), (3, 0), (3, 0), (3, 0), (3, 2), (3, 2)]
1641
+ )
1642
+ G2 = nx.MultiGraph(
1643
+ [
1644
+ ("a", "b"),
1645
+ ("a", "b"),
1646
+ ("b", "c"),
1647
+ ("k", "a"),
1648
+ ("k", "a"),
1649
+ ("k", "a"),
1650
+ ("k", "c"),
1651
+ ("k", "c"),
1652
+ ]
1653
+ )
1654
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
1655
+ sparams = _StateParameters(
1656
+ {0: "a", 1: "b", 2: "c"},
1657
+ {"a": 0, "b": 1, "c": 2},
1658
+ None,
1659
+ None,
1660
+ None,
1661
+ None,
1662
+ None,
1663
+ None,
1664
+ None,
1665
+ None,
1666
+ )
1667
+ u, v = 3, "k"
1668
+ assert _consistent_PT(u, v, gparams, sparams)
1669
+
1670
+ def test_const_no_covered_neighbors(self):
1671
+ G1 = nx.MultiGraph([(0, 1), (0, 1), (1, 2), (3, 4), (3, 4), (3, 5)])
1672
+ G2 = nx.MultiGraph([("a", "b"), ("b", "c"), ("k", "w"), ("k", "w"), ("k", "z")])
1673
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
1674
+ sparams = _StateParameters(
1675
+ {0: "a", 1: "b", 2: "c"},
1676
+ {"a": 0, "b": 1, "c": 2},
1677
+ None,
1678
+ None,
1679
+ None,
1680
+ None,
1681
+ None,
1682
+ None,
1683
+ None,
1684
+ None,
1685
+ )
1686
+ u, v = 3, "k"
1687
+ assert _consistent_PT(u, v, gparams, sparams)
1688
+
1689
+ def test_const_mixed_covered_uncovered_neighbors(self):
1690
+ G1 = nx.MultiGraph(
1691
+ [(0, 1), (1, 2), (3, 0), (3, 0), (3, 0), (3, 2), (3, 2), (3, 4), (3, 5)]
1692
+ )
1693
+ G2 = nx.MultiGraph(
1694
+ [
1695
+ ("a", "b"),
1696
+ ("b", "c"),
1697
+ ("k", "a"),
1698
+ ("k", "a"),
1699
+ ("k", "a"),
1700
+ ("k", "c"),
1701
+ ("k", "c"),
1702
+ ("k", "w"),
1703
+ ("k", "z"),
1704
+ ]
1705
+ )
1706
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
1707
+ sparams = _StateParameters(
1708
+ {0: "a", 1: "b", 2: "c"},
1709
+ {"a": 0, "b": 1, "c": 2},
1710
+ None,
1711
+ None,
1712
+ None,
1713
+ None,
1714
+ None,
1715
+ None,
1716
+ None,
1717
+ None,
1718
+ )
1719
+ u, v = 3, "k"
1720
+ assert _consistent_PT(u, v, gparams, sparams)
1721
+
1722
+ def test_const_fail_cases(self):
1723
+ G1 = nx.MultiGraph(
1724
+ [
1725
+ (0, 1),
1726
+ (1, 2),
1727
+ (10, 0),
1728
+ (10, 0),
1729
+ (10, 0),
1730
+ (10, 3),
1731
+ (10, 3),
1732
+ (10, 4),
1733
+ (10, 5),
1734
+ (10, 6),
1735
+ (10, 6),
1736
+ (4, 1),
1737
+ (5, 3),
1738
+ ]
1739
+ )
1740
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
1741
+ G2 = nx.relabel_nodes(G1, mapped)
1742
+
1743
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
1744
+ sparams = _StateParameters(
1745
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1746
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1747
+ None,
1748
+ None,
1749
+ None,
1750
+ None,
1751
+ None,
1752
+ None,
1753
+ None,
1754
+ None,
1755
+ )
1756
+ u, v = 10, "k"
1757
+ assert _consistent_PT(u, v, gparams, sparams)
1758
+
1759
+ # Delete one uncovered neighbor of u. Notice how it still passes the test.
1760
+ # Two reasons for this:
1761
+ # 1. If u, v had different degrees from the beginning, they wouldn't be
1762
+ # selected as candidates in the first place.
1763
+ # 2. Even if they are selected, consistency is basically 1-look-ahead,
1764
+ # meaning that we take into consideration the relation of the candidates
1765
+ # with their mapped neighbors. The node we deleted is not a covered
1766
+ # neighbor. Such nodes will be checked by the cut_PT function, which
1767
+ # is basically the 2-look-ahead, checking the relation of the candidates
1768
+ # with T1, T2 (in which belongs the node we just deleted).
1769
+ G1.remove_node(6)
1770
+ assert _consistent_PT(u, v, gparams, sparams)
1771
+
1772
+ # Add one more covered neighbor of u in G1
1773
+ G1.add_edge(u, 2)
1774
+ assert not _consistent_PT(u, v, gparams, sparams)
1775
+
1776
+ # Compensate in G2
1777
+ G2.add_edge(v, "c")
1778
+ assert _consistent_PT(u, v, gparams, sparams)
1779
+
1780
+ # Add one more covered neighbor of v in G2
1781
+ G2.add_edge(v, "x")
1782
+ G1.add_node(7)
1783
+ sparams.mapping.update({7: "x"})
1784
+ sparams.reverse_mapping.update({"x": 7})
1785
+ assert not _consistent_PT(u, v, gparams, sparams)
1786
+
1787
+ # Compendate in G1
1788
+ G1.add_edge(u, 7)
1789
+ assert _consistent_PT(u, v, gparams, sparams)
1790
+
1791
+ # Delete an edge between u and a covered neighbor
1792
+ G1.remove_edges_from([(u, 0), (u, 0)])
1793
+ assert not _consistent_PT(u, v, gparams, sparams)
1794
+
1795
+ # Compensate in G2
1796
+ G2.remove_edges_from([(v, mapped[0]), (v, mapped[0])])
1797
+ assert _consistent_PT(u, v, gparams, sparams)
1798
+
1799
+ # Remove an edge between v and a covered neighbor
1800
+ G2.remove_edge(v, mapped[3])
1801
+ assert not _consistent_PT(u, v, gparams, sparams)
1802
+
1803
+ # Compensate in G1
1804
+ G1.remove_edge(u, 3)
1805
+ assert _consistent_PT(u, v, gparams, sparams)
1806
+
1807
+ def test_cut_same_labels(self):
1808
+ G1 = nx.MultiGraph(
1809
+ [
1810
+ (0, 1),
1811
+ (1, 2),
1812
+ (10, 0),
1813
+ (10, 0),
1814
+ (10, 0),
1815
+ (10, 3),
1816
+ (10, 3),
1817
+ (10, 4),
1818
+ (10, 4),
1819
+ (10, 5),
1820
+ (10, 5),
1821
+ (10, 5),
1822
+ (10, 5),
1823
+ (10, 6),
1824
+ (10, 6),
1825
+ (4, 1),
1826
+ (5, 3),
1827
+ ]
1828
+ )
1829
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
1830
+ G2 = nx.relabel_nodes(G1, mapped)
1831
+ l1 = {n: "blue" for n in G1.nodes()}
1832
+ l2 = {n: "blue" for n in G2.nodes()}
1833
+
1834
+ gparams = _GraphParameters(
1835
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1836
+ )
1837
+ sparams = _StateParameters(
1838
+ {0: "a", 1: "b", 2: "c", 3: "d"},
1839
+ {"a": 0, "b": 1, "c": 2, "d": 3},
1840
+ {4, 5},
1841
+ None,
1842
+ {6},
1843
+ None,
1844
+ {"e", "f"},
1845
+ None,
1846
+ {"g"},
1847
+ None,
1848
+ )
1849
+
1850
+ u, v = 10, "k"
1851
+ assert not _cut_PT(u, v, gparams, sparams)
1852
+
1853
+ # Remove one of the multiple edges between u and a neighbor
1854
+ G1.remove_edge(u, 4)
1855
+ assert _cut_PT(u, v, gparams, sparams)
1856
+
1857
+ # Compensate in G2
1858
+ G1.remove_edge(u, 4)
1859
+ G2.remove_edges_from([(v, mapped[4]), (v, mapped[4])])
1860
+ assert not _cut_PT(u, v, gparams, sparams)
1861
+
1862
+ # Change intersection between G2[v] and T2_tilde, so it's not the same
1863
+ # as the one between G1[u] and T1_tilde
1864
+ G2.remove_edge(v, mapped[6])
1865
+ assert _cut_PT(u, v, gparams, sparams)
1866
+
1867
+ # Compensate in G1
1868
+ G1.remove_edge(u, 6)
1869
+ assert not _cut_PT(u, v, gparams, sparams)
1870
+
1871
+ # Add more edges between u and neighbor which belongs in T1_tilde
1872
+ G1.add_edges_from([(u, 5), (u, 5), (u, 5)])
1873
+ assert _cut_PT(u, v, gparams, sparams)
1874
+
1875
+ # Compensate in G2
1876
+ G2.add_edges_from([(v, mapped[5]), (v, mapped[5]), (v, mapped[5])])
1877
+ assert not _cut_PT(u, v, gparams, sparams)
1878
+
1879
+ # Add disconnected nodes, which will form the new Ti_out
1880
+ G1.add_nodes_from([6, 7, 8])
1881
+ G2.add_nodes_from(["g", "y", "z"])
1882
+ G1.add_edges_from([(u, 6), (u, 6), (u, 6), (u, 8)])
1883
+ G2.add_edges_from([(v, "g"), (v, "g"), (v, "g"), (v, "z")])
1884
+
1885
+ sparams.T1_tilde.update({6, 7, 8})
1886
+ sparams.T2_tilde.update({"g", "y", "z"})
1887
+
1888
+ l1 = {n: "blue" for n in G1.nodes()}
1889
+ l2 = {n: "blue" for n in G2.nodes()}
1890
+ gparams = _GraphParameters(
1891
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1892
+ )
1893
+
1894
+ assert not _cut_PT(u, v, gparams, sparams)
1895
+
1896
+ # Add some new nodes to the mapping
1897
+ sparams.mapping.update({6: "g", 7: "y"})
1898
+ sparams.reverse_mapping.update({"g": 6, "y": 7})
1899
+
1900
+ # Add more nodes to T1, T2.
1901
+ G1.add_edges_from([(6, 20), (7, 20), (6, 21)])
1902
+ G2.add_edges_from([("g", "i"), ("g", "j"), ("y", "j")])
1903
+
1904
+ sparams.T1.update({20, 21})
1905
+ sparams.T2.update({"i", "j"})
1906
+ sparams.T1_tilde.difference_update({6, 7})
1907
+ sparams.T2_tilde.difference_update({"g", "y"})
1908
+
1909
+ assert not _cut_PT(u, v, gparams, sparams)
1910
+
1911
+ # Remove some edges
1912
+ G2.remove_edge(v, "g")
1913
+ assert _cut_PT(u, v, gparams, sparams)
1914
+
1915
+ G1.remove_edge(u, 6)
1916
+ G1.add_edge(u, 8)
1917
+ G2.add_edge(v, "z")
1918
+ assert not _cut_PT(u, v, gparams, sparams)
1919
+
1920
+ # Add nodes from the new T1 and T2, as neighbors of u and v respectively
1921
+ G1.add_edges_from([(u, 20), (u, 20), (u, 20), (u, 21)])
1922
+ G2.add_edges_from([(v, "i"), (v, "i"), (v, "i"), (v, "j")])
1923
+ l1 = {n: "blue" for n in G1.nodes()}
1924
+ l2 = {n: "blue" for n in G2.nodes()}
1925
+ gparams = _GraphParameters(
1926
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
1927
+ )
1928
+
1929
+ assert not _cut_PT(u, v, gparams, sparams)
1930
+
1931
+ # Change the edges
1932
+ G1.remove_edge(u, 20)
1933
+ G1.add_edge(u, 4)
1934
+ assert _cut_PT(u, v, gparams, sparams)
1935
+
1936
+ G2.remove_edge(v, "i")
1937
+ G2.add_edge(v, mapped[4])
1938
+ assert not _cut_PT(u, v, gparams, sparams)
1939
+
1940
+ def test_cut_different_labels(self):
1941
+ G1 = nx.MultiGraph(
1942
+ [
1943
+ (0, 1),
1944
+ (0, 1),
1945
+ (1, 2),
1946
+ (1, 2),
1947
+ (1, 14),
1948
+ (0, 4),
1949
+ (1, 5),
1950
+ (2, 6),
1951
+ (3, 7),
1952
+ (3, 6),
1953
+ (4, 10),
1954
+ (4, 9),
1955
+ (6, 10),
1956
+ (20, 9),
1957
+ (20, 9),
1958
+ (20, 9),
1959
+ (20, 15),
1960
+ (20, 15),
1961
+ (20, 12),
1962
+ (20, 11),
1963
+ (20, 11),
1964
+ (20, 11),
1965
+ (12, 13),
1966
+ (11, 13),
1967
+ (20, 8),
1968
+ (20, 8),
1969
+ (20, 3),
1970
+ (20, 3),
1971
+ (20, 5),
1972
+ (20, 5),
1973
+ (20, 5),
1974
+ (20, 0),
1975
+ (20, 0),
1976
+ (20, 0),
1977
+ ]
1978
+ )
1979
+ mapped = {
1980
+ 0: "a",
1981
+ 1: "b",
1982
+ 2: "c",
1983
+ 3: "d",
1984
+ 4: "e",
1985
+ 5: "f",
1986
+ 6: "g",
1987
+ 7: "h",
1988
+ 8: "i",
1989
+ 9: "j",
1990
+ 10: "k",
1991
+ 11: "l",
1992
+ 12: "m",
1993
+ 13: "n",
1994
+ 14: "o",
1995
+ 15: "p",
1996
+ 20: "x",
1997
+ }
1998
+ G2 = nx.relabel_nodes(G1, mapped)
1999
+
2000
+ l1 = {n: "none" for n in G1.nodes()}
2001
+ l2 = {}
2002
+
2003
+ l1.update(
2004
+ {
2005
+ 9: "blue",
2006
+ 15: "blue",
2007
+ 12: "blue",
2008
+ 11: "green",
2009
+ 3: "green",
2010
+ 8: "red",
2011
+ 0: "red",
2012
+ 5: "yellow",
2013
+ }
2014
+ )
2015
+ l2.update({mapped[n]: l for n, l in l1.items()})
2016
+
2017
+ gparams = _GraphParameters(
2018
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2019
+ )
2020
+ sparams = _StateParameters(
2021
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2022
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2023
+ {4, 5, 6, 7, 14},
2024
+ None,
2025
+ {9, 10, 15, 12, 11, 13, 8},
2026
+ None,
2027
+ {"e", "f", "g", "h", "o"},
2028
+ None,
2029
+ {"j", "k", "l", "m", "n", "i", "p"},
2030
+ None,
2031
+ )
2032
+
2033
+ u, v = 20, "x"
2034
+ assert not _cut_PT(u, v, gparams, sparams)
2035
+
2036
+ # Change the orientation of the labels on neighbors of u compared to
2037
+ # neighbors of v. Leave the structure intact
2038
+ l1.update({9: "red"})
2039
+ assert _cut_PT(u, v, gparams, sparams)
2040
+
2041
+ # compensate in G2
2042
+ l2.update({mapped[9]: "red"})
2043
+ assert not _cut_PT(u, v, gparams, sparams)
2044
+
2045
+ # Change the intersection of G1[u] and T1
2046
+ G1.add_edge(u, 4)
2047
+ assert _cut_PT(u, v, gparams, sparams)
2048
+
2049
+ # Same for G2[v] and T2
2050
+ G2.add_edge(v, mapped[4])
2051
+ assert not _cut_PT(u, v, gparams, sparams)
2052
+
2053
+ # Delete one from the multiple edges
2054
+ G2.remove_edge(v, mapped[8])
2055
+ assert _cut_PT(u, v, gparams, sparams)
2056
+
2057
+ # Same for G1[u] and T1_tilde
2058
+ G1.remove_edge(u, 8)
2059
+ assert not _cut_PT(u, v, gparams, sparams)
2060
+
2061
+ # Place 8 and mapped[8] in T1 and T2 respectively, by connecting it to covered nodes
2062
+ G1.add_edges_from([(8, 3), (8, 3), (8, u)])
2063
+ G2.add_edges_from([(mapped[8], mapped[3]), (mapped[8], mapped[3])])
2064
+ sparams.T1.add(8)
2065
+ sparams.T2.add(mapped[8])
2066
+ sparams.T1_tilde.remove(8)
2067
+ sparams.T2_tilde.remove(mapped[8])
2068
+
2069
+ assert _cut_PT(u, v, gparams, sparams)
2070
+
2071
+ # Fix uneven edges
2072
+ G1.remove_edge(8, u)
2073
+ assert not _cut_PT(u, v, gparams, sparams)
2074
+
2075
+ # Remove neighbor of u from T1
2076
+ G1.remove_node(5)
2077
+ l1.pop(5)
2078
+ sparams.T1.remove(5)
2079
+ assert _cut_PT(u, v, gparams, sparams)
2080
+
2081
+ # Same in G2
2082
+ G2.remove_node(mapped[5])
2083
+ l2.pop(mapped[5])
2084
+ sparams.T2.remove(mapped[5])
2085
+ assert not _cut_PT(u, v, gparams, sparams)
2086
+
2087
+ def test_feasibility_same_labels(self):
2088
+ G1 = nx.MultiGraph(
2089
+ [
2090
+ (0, 1),
2091
+ (0, 1),
2092
+ (1, 2),
2093
+ (1, 2),
2094
+ (1, 14),
2095
+ (0, 4),
2096
+ (1, 5),
2097
+ (2, 6),
2098
+ (3, 7),
2099
+ (3, 6),
2100
+ (4, 10),
2101
+ (4, 9),
2102
+ (6, 10),
2103
+ (20, 9),
2104
+ (20, 9),
2105
+ (20, 9),
2106
+ (20, 15),
2107
+ (20, 15),
2108
+ (20, 12),
2109
+ (20, 11),
2110
+ (20, 11),
2111
+ (20, 11),
2112
+ (12, 13),
2113
+ (11, 13),
2114
+ (20, 8),
2115
+ (20, 8),
2116
+ (20, 3),
2117
+ (20, 3),
2118
+ (20, 5),
2119
+ (20, 5),
2120
+ (20, 5),
2121
+ (20, 0),
2122
+ (20, 0),
2123
+ (20, 0),
2124
+ ]
2125
+ )
2126
+ mapped = {
2127
+ 0: "a",
2128
+ 1: "b",
2129
+ 2: "c",
2130
+ 3: "d",
2131
+ 4: "e",
2132
+ 5: "f",
2133
+ 6: "g",
2134
+ 7: "h",
2135
+ 8: "i",
2136
+ 9: "j",
2137
+ 10: "k",
2138
+ 11: "l",
2139
+ 12: "m",
2140
+ 13: "n",
2141
+ 14: "o",
2142
+ 15: "p",
2143
+ 20: "x",
2144
+ }
2145
+ G2 = nx.relabel_nodes(G1, mapped)
2146
+ l1 = {n: "blue" for n in G1.nodes()}
2147
+ l2 = {mapped[n]: "blue" for n in G1.nodes()}
2148
+
2149
+ gparams = _GraphParameters(
2150
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2151
+ )
2152
+ sparams = _StateParameters(
2153
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2154
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2155
+ {4, 5, 6, 7, 14},
2156
+ None,
2157
+ {9, 10, 15, 12, 11, 13, 8},
2158
+ None,
2159
+ {"e", "f", "g", "h", "o"},
2160
+ None,
2161
+ {"j", "k", "l", "m", "n", "i", "p"},
2162
+ None,
2163
+ )
2164
+
2165
+ u, v = 20, "x"
2166
+ assert not _cut_PT(u, v, gparams, sparams)
2167
+
2168
+ # Change structure in G2 such that, ONLY consistency is harmed
2169
+ G2.remove_edges_from([(mapped[20], mapped[3]), (mapped[20], mapped[3])])
2170
+ G2.add_edges_from([(mapped[20], mapped[2]), (mapped[20], mapped[2])])
2171
+
2172
+ # Consistency check fails, while the cutting rules are satisfied!
2173
+ assert not _cut_PT(u, v, gparams, sparams)
2174
+ assert not _consistent_PT(u, v, gparams, sparams)
2175
+
2176
+ # Compensate in G1 and make it consistent
2177
+ G1.remove_edges_from([(20, 3), (20, 3)])
2178
+ G1.add_edges_from([(20, 2), (20, 2)])
2179
+ assert not _cut_PT(u, v, gparams, sparams)
2180
+ assert _consistent_PT(u, v, gparams, sparams)
2181
+
2182
+ # ONLY fail the cutting check
2183
+ G2.add_edges_from([(v, mapped[10])] * 5)
2184
+ assert _cut_PT(u, v, gparams, sparams)
2185
+ assert _consistent_PT(u, v, gparams, sparams)
2186
+
2187
+ # Pass all tests
2188
+ G1.add_edges_from([(u, 10)] * 5)
2189
+ assert not _cut_PT(u, v, gparams, sparams)
2190
+ assert _consistent_PT(u, v, gparams, sparams)
2191
+
2192
+ def test_feasibility_different_labels(self):
2193
+ G1 = nx.MultiGraph(
2194
+ [
2195
+ (0, 1),
2196
+ (0, 1),
2197
+ (1, 2),
2198
+ (1, 2),
2199
+ (1, 14),
2200
+ (0, 4),
2201
+ (1, 5),
2202
+ (2, 6),
2203
+ (3, 7),
2204
+ (3, 6),
2205
+ (4, 10),
2206
+ (4, 9),
2207
+ (6, 10),
2208
+ (20, 9),
2209
+ (20, 9),
2210
+ (20, 9),
2211
+ (20, 15),
2212
+ (20, 15),
2213
+ (20, 12),
2214
+ (20, 11),
2215
+ (20, 11),
2216
+ (20, 11),
2217
+ (12, 13),
2218
+ (11, 13),
2219
+ (20, 8),
2220
+ (20, 8),
2221
+ (20, 2),
2222
+ (20, 2),
2223
+ (20, 5),
2224
+ (20, 5),
2225
+ (20, 5),
2226
+ (20, 0),
2227
+ (20, 0),
2228
+ (20, 0),
2229
+ ]
2230
+ )
2231
+ mapped = {
2232
+ 0: "a",
2233
+ 1: "b",
2234
+ 2: "c",
2235
+ 3: "d",
2236
+ 4: "e",
2237
+ 5: "f",
2238
+ 6: "g",
2239
+ 7: "h",
2240
+ 8: "i",
2241
+ 9: "j",
2242
+ 10: "k",
2243
+ 11: "l",
2244
+ 12: "m",
2245
+ 13: "n",
2246
+ 14: "o",
2247
+ 15: "p",
2248
+ 20: "x",
2249
+ }
2250
+ G2 = nx.relabel_nodes(G1, mapped)
2251
+ l1 = {n: "none" for n in G1.nodes()}
2252
+ l2 = {}
2253
+
2254
+ l1.update(
2255
+ {
2256
+ 9: "blue",
2257
+ 15: "blue",
2258
+ 12: "blue",
2259
+ 11: "green",
2260
+ 2: "green",
2261
+ 8: "red",
2262
+ 0: "red",
2263
+ 5: "yellow",
2264
+ }
2265
+ )
2266
+ l2.update({mapped[n]: l for n, l in l1.items()})
2267
+
2268
+ gparams = _GraphParameters(
2269
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2270
+ )
2271
+ sparams = _StateParameters(
2272
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2273
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2274
+ {4, 5, 6, 7, 14},
2275
+ None,
2276
+ {9, 10, 15, 12, 11, 13, 8},
2277
+ None,
2278
+ {"e", "f", "g", "h", "o"},
2279
+ None,
2280
+ {"j", "k", "l", "m", "n", "i", "p"},
2281
+ None,
2282
+ )
2283
+
2284
+ u, v = 20, "x"
2285
+ assert not _cut_PT(u, v, gparams, sparams)
2286
+
2287
+ # Change structure in G2 such that, ONLY consistency is harmed
2288
+ G2.remove_edges_from([(mapped[20], mapped[2]), (mapped[20], mapped[2])])
2289
+ G2.add_edges_from([(mapped[20], mapped[3]), (mapped[20], mapped[3])])
2290
+ l2.update({mapped[3]: "green"})
2291
+
2292
+ # Consistency check fails, while the cutting rules are satisfied!
2293
+ assert not _cut_PT(u, v, gparams, sparams)
2294
+ assert not _consistent_PT(u, v, gparams, sparams)
2295
+
2296
+ # Compensate in G1 and make it consistent
2297
+ G1.remove_edges_from([(20, 2), (20, 2)])
2298
+ G1.add_edges_from([(20, 3), (20, 3)])
2299
+ l1.update({3: "green"})
2300
+ assert not _cut_PT(u, v, gparams, sparams)
2301
+ assert _consistent_PT(u, v, gparams, sparams)
2302
+
2303
+ # ONLY fail the cutting check
2304
+ l1.update({5: "red"})
2305
+ assert _cut_PT(u, v, gparams, sparams)
2306
+ assert _consistent_PT(u, v, gparams, sparams)
2307
+
2308
+
2309
+ class TestDiGraphISOFeasibility:
2310
+ def test_const_covered_neighbors(self):
2311
+ G1 = nx.DiGraph([(0, 1), (1, 2), (0, 3), (2, 3)])
2312
+ G2 = nx.DiGraph([("a", "b"), ("b", "c"), ("a", "k"), ("c", "k")])
2313
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
2314
+ sparams = _StateParameters(
2315
+ {0: "a", 1: "b", 2: "c"},
2316
+ {"a": 0, "b": 1, "c": 2},
2317
+ None,
2318
+ None,
2319
+ None,
2320
+ None,
2321
+ None,
2322
+ None,
2323
+ None,
2324
+ None,
2325
+ )
2326
+ u, v = 3, "k"
2327
+ assert _consistent_PT(u, v, gparams, sparams)
2328
+
2329
+ def test_const_no_covered_neighbors(self):
2330
+ G1 = nx.DiGraph([(0, 1), (1, 2), (3, 4), (3, 5)])
2331
+ G2 = nx.DiGraph([("a", "b"), ("b", "c"), ("k", "w"), ("k", "z")])
2332
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
2333
+ sparams = _StateParameters(
2334
+ {0: "a", 1: "b", 2: "c"},
2335
+ {"a": 0, "b": 1, "c": 2},
2336
+ None,
2337
+ None,
2338
+ None,
2339
+ None,
2340
+ None,
2341
+ None,
2342
+ None,
2343
+ None,
2344
+ )
2345
+ u, v = 3, "k"
2346
+ assert _consistent_PT(u, v, gparams, sparams)
2347
+
2348
+ def test_const_mixed_covered_uncovered_neighbors(self):
2349
+ G1 = nx.DiGraph([(0, 1), (1, 2), (3, 0), (3, 2), (3, 4), (3, 5)])
2350
+ G2 = nx.DiGraph(
2351
+ [("a", "b"), ("b", "c"), ("k", "a"), ("k", "c"), ("k", "w"), ("k", "z")]
2352
+ )
2353
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
2354
+ sparams = _StateParameters(
2355
+ {0: "a", 1: "b", 2: "c"},
2356
+ {"a": 0, "b": 1, "c": 2},
2357
+ None,
2358
+ None,
2359
+ None,
2360
+ None,
2361
+ None,
2362
+ None,
2363
+ None,
2364
+ None,
2365
+ )
2366
+ u, v = 3, "k"
2367
+ assert _consistent_PT(u, v, gparams, sparams)
2368
+
2369
+ def test_const_fail_cases(self):
2370
+ G1 = nx.DiGraph(
2371
+ [
2372
+ (0, 1),
2373
+ (2, 1),
2374
+ (10, 0),
2375
+ (10, 3),
2376
+ (10, 4),
2377
+ (5, 10),
2378
+ (10, 6),
2379
+ (1, 4),
2380
+ (5, 3),
2381
+ ]
2382
+ )
2383
+ G2 = nx.DiGraph(
2384
+ [
2385
+ ("a", "b"),
2386
+ ("c", "b"),
2387
+ ("k", "a"),
2388
+ ("k", "d"),
2389
+ ("k", "e"),
2390
+ ("f", "k"),
2391
+ ("k", "g"),
2392
+ ("b", "e"),
2393
+ ("f", "d"),
2394
+ ]
2395
+ )
2396
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
2397
+ sparams = _StateParameters(
2398
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2399
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2400
+ None,
2401
+ None,
2402
+ None,
2403
+ None,
2404
+ None,
2405
+ None,
2406
+ None,
2407
+ None,
2408
+ )
2409
+ u, v = 10, "k"
2410
+ assert _consistent_PT(u, v, gparams, sparams)
2411
+
2412
+ # Delete one uncovered neighbor of u. Notice how it still passes the
2413
+ # test. Two reasons for this:
2414
+ # 1. If u, v had different degrees from the beginning, they wouldn't
2415
+ # be selected as candidates in the first place.
2416
+ # 2. Even if they are selected, consistency is basically
2417
+ # 1-look-ahead, meaning that we take into consideration the
2418
+ # relation of the candidates with their mapped neighbors.
2419
+ # The node we deleted is not a covered neighbor.
2420
+ # Such nodes will be checked by the cut_PT function, which is
2421
+ # basically the 2-look-ahead, checking the relation of the
2422
+ # candidates with T1, T2 (in which belongs the node we just deleted).
2423
+ G1.remove_node(6)
2424
+ assert _consistent_PT(u, v, gparams, sparams)
2425
+
2426
+ # Add one more covered neighbor of u in G1
2427
+ G1.add_edge(u, 2)
2428
+ assert not _consistent_PT(u, v, gparams, sparams)
2429
+
2430
+ # Compensate in G2
2431
+ G2.add_edge(v, "c")
2432
+ assert _consistent_PT(u, v, gparams, sparams)
2433
+
2434
+ # Add one more covered neighbor of v in G2
2435
+ G2.add_edge(v, "x")
2436
+ G1.add_node(7)
2437
+ sparams.mapping.update({7: "x"})
2438
+ sparams.reverse_mapping.update({"x": 7})
2439
+ assert not _consistent_PT(u, v, gparams, sparams)
2440
+
2441
+ # Compensate in G1
2442
+ G1.add_edge(u, 7)
2443
+ assert _consistent_PT(u, v, gparams, sparams)
2444
+
2445
+ def test_cut_inconsistent_labels(self):
2446
+ G1 = nx.DiGraph(
2447
+ [
2448
+ (0, 1),
2449
+ (2, 1),
2450
+ (10, 0),
2451
+ (10, 3),
2452
+ (10, 4),
2453
+ (5, 10),
2454
+ (10, 6),
2455
+ (1, 4),
2456
+ (5, 3),
2457
+ ]
2458
+ )
2459
+ G2 = nx.DiGraph(
2460
+ [
2461
+ ("a", "b"),
2462
+ ("c", "b"),
2463
+ ("k", "a"),
2464
+ ("k", "d"),
2465
+ ("k", "e"),
2466
+ ("f", "k"),
2467
+ ("k", "g"),
2468
+ ("b", "e"),
2469
+ ("f", "d"),
2470
+ ]
2471
+ )
2472
+
2473
+ l1 = {n: "blue" for n in G1.nodes()}
2474
+ l2 = {n: "blue" for n in G2.nodes()}
2475
+ l1.update({5: "green"}) # Change the label of one neighbor of u
2476
+
2477
+ gparams = _GraphParameters(
2478
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2479
+ )
2480
+ sparams = _StateParameters(
2481
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2482
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2483
+ None,
2484
+ None,
2485
+ None,
2486
+ None,
2487
+ None,
2488
+ None,
2489
+ None,
2490
+ None,
2491
+ )
2492
+
2493
+ u, v = 10, "k"
2494
+ assert _cut_PT(u, v, gparams, sparams)
2495
+
2496
+ def test_cut_consistent_labels(self):
2497
+ G1 = nx.DiGraph(
2498
+ [
2499
+ (0, 1),
2500
+ (2, 1),
2501
+ (10, 0),
2502
+ (10, 3),
2503
+ (10, 4),
2504
+ (5, 10),
2505
+ (10, 6),
2506
+ (1, 4),
2507
+ (5, 3),
2508
+ ]
2509
+ )
2510
+ G2 = nx.DiGraph(
2511
+ [
2512
+ ("a", "b"),
2513
+ ("c", "b"),
2514
+ ("k", "a"),
2515
+ ("k", "d"),
2516
+ ("k", "e"),
2517
+ ("f", "k"),
2518
+ ("k", "g"),
2519
+ ("b", "e"),
2520
+ ("f", "d"),
2521
+ ]
2522
+ )
2523
+
2524
+ l1 = {n: "blue" for n in G1.nodes()}
2525
+ l2 = {n: "blue" for n in G2.nodes()}
2526
+
2527
+ gparams = _GraphParameters(
2528
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2529
+ )
2530
+ sparams = _StateParameters(
2531
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2532
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2533
+ {4},
2534
+ {5, 10},
2535
+ {6},
2536
+ None,
2537
+ {"e"},
2538
+ {"f", "k"},
2539
+ {"g"},
2540
+ None,
2541
+ )
2542
+
2543
+ u, v = 10, "k"
2544
+ assert not _cut_PT(u, v, gparams, sparams)
2545
+
2546
+ def test_cut_same_labels(self):
2547
+ G1 = nx.DiGraph(
2548
+ [
2549
+ (0, 1),
2550
+ (2, 1),
2551
+ (10, 0),
2552
+ (10, 3),
2553
+ (10, 4),
2554
+ (5, 10),
2555
+ (10, 6),
2556
+ (1, 4),
2557
+ (5, 3),
2558
+ ]
2559
+ )
2560
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
2561
+ G2 = nx.relabel_nodes(G1, mapped)
2562
+ l1 = {n: "blue" for n in G1.nodes()}
2563
+ l2 = {n: "blue" for n in G2.nodes()}
2564
+
2565
+ gparams = _GraphParameters(
2566
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2567
+ )
2568
+ sparams = _StateParameters(
2569
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2570
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2571
+ {4},
2572
+ {5, 10},
2573
+ {6},
2574
+ None,
2575
+ {"e"},
2576
+ {"f", "k"},
2577
+ {"g"},
2578
+ None,
2579
+ )
2580
+
2581
+ u, v = 10, "k"
2582
+ assert not _cut_PT(u, v, gparams, sparams)
2583
+
2584
+ # Change intersection between G1[u] and T1_out, so it's not the same as
2585
+ # the one between G2[v] and T2_out
2586
+ G1.remove_edge(u, 4)
2587
+ assert _cut_PT(u, v, gparams, sparams)
2588
+
2589
+ # Compensate in G2
2590
+ G2.remove_edge(v, mapped[4])
2591
+ assert not _cut_PT(u, v, gparams, sparams)
2592
+
2593
+ # Change intersection between G1[u] and T1_in, so it's not the same as
2594
+ # the one between G2[v] and T2_in
2595
+ G1.remove_edge(5, u)
2596
+ assert _cut_PT(u, v, gparams, sparams)
2597
+
2598
+ # Compensate in G2
2599
+ G2.remove_edge(mapped[5], v)
2600
+ assert not _cut_PT(u, v, gparams, sparams)
2601
+
2602
+ # Change intersection between G2[v] and T2_tilde, so it's not the same
2603
+ # as the one between G1[u] and T1_tilde
2604
+ G2.remove_edge(v, mapped[6])
2605
+ assert _cut_PT(u, v, gparams, sparams)
2606
+
2607
+ # Compensate in G1
2608
+ G1.remove_edge(u, 6)
2609
+ assert not _cut_PT(u, v, gparams, sparams)
2610
+
2611
+ # Add disconnected nodes, which will form the new Ti_tilde
2612
+ G1.add_nodes_from([6, 7, 8])
2613
+ G2.add_nodes_from(["g", "y", "z"])
2614
+ sparams.T1_tilde.update({6, 7, 8})
2615
+ sparams.T2_tilde.update({"g", "y", "z"})
2616
+
2617
+ l1 = {n: "blue" for n in G1.nodes()}
2618
+ l2 = {n: "blue" for n in G2.nodes()}
2619
+ gparams = _GraphParameters(
2620
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2621
+ )
2622
+
2623
+ assert not _cut_PT(u, v, gparams, sparams)
2624
+
2625
+ def test_cut_different_labels(self):
2626
+ G1 = nx.DiGraph(
2627
+ [
2628
+ (0, 1),
2629
+ (1, 2),
2630
+ (14, 1),
2631
+ (0, 4),
2632
+ (1, 5),
2633
+ (2, 6),
2634
+ (3, 7),
2635
+ (3, 6),
2636
+ (10, 4),
2637
+ (4, 9),
2638
+ (6, 10),
2639
+ (20, 9),
2640
+ (20, 15),
2641
+ (20, 12),
2642
+ (20, 11),
2643
+ (12, 13),
2644
+ (11, 13),
2645
+ (20, 8),
2646
+ (20, 3),
2647
+ (20, 5),
2648
+ (0, 20),
2649
+ ]
2650
+ )
2651
+ mapped = {
2652
+ 0: "a",
2653
+ 1: "b",
2654
+ 2: "c",
2655
+ 3: "d",
2656
+ 4: "e",
2657
+ 5: "f",
2658
+ 6: "g",
2659
+ 7: "h",
2660
+ 8: "i",
2661
+ 9: "j",
2662
+ 10: "k",
2663
+ 11: "l",
2664
+ 12: "m",
2665
+ 13: "n",
2666
+ 14: "o",
2667
+ 15: "p",
2668
+ 20: "x",
2669
+ }
2670
+ G2 = nx.relabel_nodes(G1, mapped)
2671
+
2672
+ l1 = {n: "none" for n in G1.nodes()}
2673
+ l2 = {}
2674
+
2675
+ l1.update(
2676
+ {
2677
+ 9: "blue",
2678
+ 15: "blue",
2679
+ 12: "blue",
2680
+ 11: "green",
2681
+ 3: "green",
2682
+ 8: "red",
2683
+ 0: "red",
2684
+ 5: "yellow",
2685
+ }
2686
+ )
2687
+ l2.update({mapped[n]: l for n, l in l1.items()})
2688
+
2689
+ gparams = _GraphParameters(
2690
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2691
+ )
2692
+ sparams = _StateParameters(
2693
+ {0: "a", 1: "b", 2: "c", 3: "d"},
2694
+ {"a": 0, "b": 1, "c": 2, "d": 3},
2695
+ {4, 5, 6, 7, 20},
2696
+ {14, 20},
2697
+ {9, 10, 15, 12, 11, 13, 8},
2698
+ None,
2699
+ {"e", "f", "g", "x"},
2700
+ {"o", "x"},
2701
+ {"j", "k", "l", "m", "n", "i", "p"},
2702
+ None,
2703
+ )
2704
+
2705
+ u, v = 20, "x"
2706
+ assert not _cut_PT(u, v, gparams, sparams)
2707
+
2708
+ # Change the orientation of the labels on neighbors of u compared to
2709
+ # neighbors of v. Leave the structure intact
2710
+ l1.update({9: "red"})
2711
+ assert _cut_PT(u, v, gparams, sparams)
2712
+
2713
+ # compensate in G2
2714
+ l2.update({mapped[9]: "red"})
2715
+ assert not _cut_PT(u, v, gparams, sparams)
2716
+
2717
+ # Change the intersection of G1[u] and T1_out
2718
+ G1.add_edge(u, 4)
2719
+ assert _cut_PT(u, v, gparams, sparams)
2720
+
2721
+ # Same for G2[v] and T2_out
2722
+ G2.add_edge(v, mapped[4])
2723
+ assert not _cut_PT(u, v, gparams, sparams)
2724
+
2725
+ # Change the intersection of G1[u] and T1_in
2726
+ G1.add_edge(u, 14)
2727
+ assert _cut_PT(u, v, gparams, sparams)
2728
+
2729
+ # Same for G2[v] and T2_in
2730
+ G2.add_edge(v, mapped[14])
2731
+ assert not _cut_PT(u, v, gparams, sparams)
2732
+
2733
+ # Change the intersection of G2[v] and T2_tilde
2734
+ G2.remove_edge(v, mapped[8])
2735
+ assert _cut_PT(u, v, gparams, sparams)
2736
+
2737
+ # Same for G1[u] and T1_tilde
2738
+ G1.remove_edge(u, 8)
2739
+ assert not _cut_PT(u, v, gparams, sparams)
2740
+
2741
+ # Place 8 and mapped[8] in T1 and T2 respectively, by connecting it to covered nodes
2742
+ G1.add_edge(8, 3)
2743
+ G2.add_edge(mapped[8], mapped[3])
2744
+ sparams.T1.add(8)
2745
+ sparams.T2.add(mapped[8])
2746
+ sparams.T1_tilde.remove(8)
2747
+ sparams.T2_tilde.remove(mapped[8])
2748
+
2749
+ assert not _cut_PT(u, v, gparams, sparams)
2750
+
2751
+ # Remove neighbor of u from T1
2752
+ G1.remove_node(5)
2753
+ l1.pop(5)
2754
+ sparams.T1.remove(5)
2755
+ assert _cut_PT(u, v, gparams, sparams)
2756
+
2757
+ # Same in G2
2758
+ G2.remove_node(mapped[5])
2759
+ l2.pop(mapped[5])
2760
+ sparams.T2.remove(mapped[5])
2761
+ assert not _cut_PT(u, v, gparams, sparams)
2762
+
2763
+ def test_predecessor_T1_in_fail(self):
2764
+ G1 = nx.DiGraph(
2765
+ [(0, 1), (0, 3), (4, 0), (1, 5), (5, 2), (3, 6), (4, 6), (6, 5)]
2766
+ )
2767
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g"}
2768
+ G2 = nx.relabel_nodes(G1, mapped)
2769
+ l1 = {n: "blue" for n in G1.nodes()}
2770
+ l2 = {n: "blue" for n in G2.nodes()}
2771
+
2772
+ gparams = _GraphParameters(
2773
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
2774
+ )
2775
+ sparams = _StateParameters(
2776
+ {0: "a", 1: "b", 2: "c"},
2777
+ {"a": 0, "b": 1, "c": 2},
2778
+ {3, 5},
2779
+ {4, 5},
2780
+ {6},
2781
+ None,
2782
+ {"d", "f"},
2783
+ {"f"}, # mapped[4] is missing from T2_in
2784
+ {"g"},
2785
+ None,
2786
+ )
2787
+
2788
+ u, v = 6, "g"
2789
+ assert _cut_PT(u, v, gparams, sparams)
2790
+
2791
+ sparams.T2_in.add("e")
2792
+ assert not _cut_PT(u, v, gparams, sparams)
2793
+
2794
+
2795
+ class TestGraphTinoutUpdating:
2796
+ edges = [
2797
+ (1, 3),
2798
+ (2, 3),
2799
+ (3, 4),
2800
+ (4, 9),
2801
+ (4, 5),
2802
+ (3, 9),
2803
+ (5, 8),
2804
+ (5, 7),
2805
+ (8, 7),
2806
+ (6, 7),
2807
+ ]
2808
+ mapped = {
2809
+ 0: "x",
2810
+ 1: "a",
2811
+ 2: "b",
2812
+ 3: "c",
2813
+ 4: "d",
2814
+ 5: "e",
2815
+ 6: "f",
2816
+ 7: "g",
2817
+ 8: "h",
2818
+ 9: "i",
2819
+ }
2820
+ G1 = nx.Graph()
2821
+ G1.add_edges_from(edges)
2822
+ G1.add_node(0)
2823
+ G2 = nx.relabel_nodes(G1, mapping=mapped)
2824
+
2825
+ def test_updating(self):
2826
+ G2_degree = dict(self.G2.degree)
2827
+ gparams, sparams = _initialize_parameters(self.G1, self.G2, G2_degree)
2828
+ m, m_rev, T1, _, T1_tilde, _, T2, _, T2_tilde, _ = sparams
2829
+
2830
+ # Add node to the mapping
2831
+ m[4] = self.mapped[4]
2832
+ m_rev[self.mapped[4]] = 4
2833
+ _update_Tinout(4, self.mapped[4], gparams, sparams)
2834
+
2835
+ assert T1 == {3, 5, 9}
2836
+ assert T2 == {"c", "i", "e"}
2837
+ assert T1_tilde == {0, 1, 2, 6, 7, 8}
2838
+ assert T2_tilde == {"x", "a", "b", "f", "g", "h"}
2839
+
2840
+ # Add node to the mapping
2841
+ m[5] = self.mapped[5]
2842
+ m_rev.update({self.mapped[5]: 5})
2843
+ _update_Tinout(5, self.mapped[5], gparams, sparams)
2844
+
2845
+ assert T1 == {3, 9, 8, 7}
2846
+ assert T2 == {"c", "i", "h", "g"}
2847
+ assert T1_tilde == {0, 1, 2, 6}
2848
+ assert T2_tilde == {"x", "a", "b", "f"}
2849
+
2850
+ # Add node to the mapping
2851
+ m[6] = self.mapped[6]
2852
+ m_rev.update({self.mapped[6]: 6})
2853
+ _update_Tinout(6, self.mapped[6], gparams, sparams)
2854
+
2855
+ assert T1 == {3, 9, 8, 7}
2856
+ assert T2 == {"c", "i", "h", "g"}
2857
+ assert T1_tilde == {0, 1, 2}
2858
+ assert T2_tilde == {"x", "a", "b"}
2859
+
2860
+ # Add node to the mapping
2861
+ m[3] = self.mapped[3]
2862
+ m_rev.update({self.mapped[3]: 3})
2863
+ _update_Tinout(3, self.mapped[3], gparams, sparams)
2864
+
2865
+ assert T1 == {1, 2, 9, 8, 7}
2866
+ assert T2 == {"a", "b", "i", "h", "g"}
2867
+ assert T1_tilde == {0}
2868
+ assert T2_tilde == {"x"}
2869
+
2870
+ # Add node to the mapping
2871
+ m[0] = self.mapped[0]
2872
+ m_rev.update({self.mapped[0]: 0})
2873
+ _update_Tinout(0, self.mapped[0], gparams, sparams)
2874
+
2875
+ assert T1 == {1, 2, 9, 8, 7}
2876
+ assert T2 == {"a", "b", "i", "h", "g"}
2877
+ assert T1_tilde == set()
2878
+ assert T2_tilde == set()
2879
+
2880
+ def test_restoring(self):
2881
+ m = {0: "x", 3: "c", 4: "d", 5: "e", 6: "f"}
2882
+ m_rev = {"x": 0, "c": 3, "d": 4, "e": 5, "f": 6}
2883
+
2884
+ T1 = {1, 2, 7, 9, 8}
2885
+ T2 = {"a", "b", "g", "i", "h"}
2886
+ T1_tilde = set()
2887
+ T2_tilde = set()
2888
+
2889
+ gparams = _GraphParameters(self.G1, self.G2, {}, {}, {}, {}, {})
2890
+ sparams = _StateParameters(
2891
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
2892
+ )
2893
+
2894
+ # Remove a node from the mapping
2895
+ m.pop(0)
2896
+ m_rev.pop("x")
2897
+ _restore_Tinout(0, self.mapped[0], gparams, sparams)
2898
+
2899
+ assert T1 == {1, 2, 7, 9, 8}
2900
+ assert T2 == {"a", "b", "g", "i", "h"}
2901
+ assert T1_tilde == {0}
2902
+ assert T2_tilde == {"x"}
2903
+
2904
+ # Remove a node from the mapping
2905
+ m.pop(6)
2906
+ m_rev.pop("f")
2907
+ _restore_Tinout(6, self.mapped[6], gparams, sparams)
2908
+
2909
+ assert T1 == {1, 2, 7, 9, 8}
2910
+ assert T2 == {"a", "b", "g", "i", "h"}
2911
+ assert T1_tilde == {0, 6}
2912
+ assert T2_tilde == {"x", "f"}
2913
+
2914
+ # Remove a node from the mapping
2915
+ m.pop(3)
2916
+ m_rev.pop("c")
2917
+ _restore_Tinout(3, self.mapped[3], gparams, sparams)
2918
+
2919
+ assert T1 == {7, 9, 8, 3}
2920
+ assert T2 == {"g", "i", "h", "c"}
2921
+ assert T1_tilde == {0, 6, 1, 2}
2922
+ assert T2_tilde == {"x", "f", "a", "b"}
2923
+
2924
+ # Remove a node from the mapping
2925
+ m.pop(5)
2926
+ m_rev.pop("e")
2927
+ _restore_Tinout(5, self.mapped[5], gparams, sparams)
2928
+
2929
+ assert T1 == {9, 3, 5}
2930
+ assert T2 == {"i", "c", "e"}
2931
+ assert T1_tilde == {0, 6, 1, 2, 7, 8}
2932
+ assert T2_tilde == {"x", "f", "a", "b", "g", "h"}
2933
+
2934
+ # Remove a node from the mapping
2935
+ m.pop(4)
2936
+ m_rev.pop("d")
2937
+ _restore_Tinout(4, self.mapped[4], gparams, sparams)
2938
+
2939
+ assert T1 == set()
2940
+ assert T2 == set()
2941
+ assert T1_tilde == set(self.G1.nodes())
2942
+ assert T2_tilde == set(self.G2.nodes())
2943
+
2944
+
2945
+ class TestDiGraphTinoutUpdating:
2946
+ edges = [
2947
+ (1, 3),
2948
+ (3, 2),
2949
+ (3, 4),
2950
+ (4, 9),
2951
+ (4, 5),
2952
+ (3, 9),
2953
+ (5, 8),
2954
+ (5, 7),
2955
+ (8, 7),
2956
+ (7, 6),
2957
+ ]
2958
+ mapped = {
2959
+ 0: "x",
2960
+ 1: "a",
2961
+ 2: "b",
2962
+ 3: "c",
2963
+ 4: "d",
2964
+ 5: "e",
2965
+ 6: "f",
2966
+ 7: "g",
2967
+ 8: "h",
2968
+ 9: "i",
2969
+ }
2970
+ G1 = nx.DiGraph(edges)
2971
+ G1.add_node(0)
2972
+ G2 = nx.relabel_nodes(G1, mapping=mapped)
2973
+
2974
+ def test_updating(self):
2975
+ G2_degree = {
2976
+ n: (in_degree, out_degree)
2977
+ for (n, in_degree), (_, out_degree) in zip(
2978
+ self.G2.in_degree, self.G2.out_degree
2979
+ )
2980
+ }
2981
+ gparams, sparams = _initialize_parameters(self.G1, self.G2, G2_degree)
2982
+ m, m_rev, T1_out, T1_in, T1_tilde, _, T2_out, T2_in, T2_tilde, _ = sparams
2983
+
2984
+ # Add node to the mapping
2985
+ m[4] = self.mapped[4]
2986
+ m_rev[self.mapped[4]] = 4
2987
+ _update_Tinout(4, self.mapped[4], gparams, sparams)
2988
+
2989
+ assert T1_out == {5, 9}
2990
+ assert T1_in == {3}
2991
+ assert T2_out == {"i", "e"}
2992
+ assert T2_in == {"c"}
2993
+ assert T1_tilde == {0, 1, 2, 6, 7, 8}
2994
+ assert T2_tilde == {"x", "a", "b", "f", "g", "h"}
2995
+
2996
+ # Add node to the mapping
2997
+ m[5] = self.mapped[5]
2998
+ m_rev[self.mapped[5]] = 5
2999
+ _update_Tinout(5, self.mapped[5], gparams, sparams)
3000
+
3001
+ assert T1_out == {9, 8, 7}
3002
+ assert T1_in == {3}
3003
+ assert T2_out == {"i", "g", "h"}
3004
+ assert T2_in == {"c"}
3005
+ assert T1_tilde == {0, 1, 2, 6}
3006
+ assert T2_tilde == {"x", "a", "b", "f"}
3007
+
3008
+ # Add node to the mapping
3009
+ m[6] = self.mapped[6]
3010
+ m_rev[self.mapped[6]] = 6
3011
+ _update_Tinout(6, self.mapped[6], gparams, sparams)
3012
+
3013
+ assert T1_out == {9, 8, 7}
3014
+ assert T1_in == {3, 7}
3015
+ assert T2_out == {"i", "g", "h"}
3016
+ assert T2_in == {"c", "g"}
3017
+ assert T1_tilde == {0, 1, 2}
3018
+ assert T2_tilde == {"x", "a", "b"}
3019
+
3020
+ # Add node to the mapping
3021
+ m[3] = self.mapped[3]
3022
+ m_rev[self.mapped[3]] = 3
3023
+ _update_Tinout(3, self.mapped[3], gparams, sparams)
3024
+
3025
+ assert T1_out == {9, 8, 7, 2}
3026
+ assert T1_in == {7, 1}
3027
+ assert T2_out == {"i", "g", "h", "b"}
3028
+ assert T2_in == {"g", "a"}
3029
+ assert T1_tilde == {0}
3030
+ assert T2_tilde == {"x"}
3031
+
3032
+ # Add node to the mapping
3033
+ m[0] = self.mapped[0]
3034
+ m_rev[self.mapped[0]] = 0
3035
+ _update_Tinout(0, self.mapped[0], gparams, sparams)
3036
+
3037
+ assert T1_out == {9, 8, 7, 2}
3038
+ assert T1_in == {7, 1}
3039
+ assert T2_out == {"i", "g", "h", "b"}
3040
+ assert T2_in == {"g", "a"}
3041
+ assert T1_tilde == set()
3042
+ assert T2_tilde == set()
3043
+
3044
+ def test_restoring(self):
3045
+ m = {0: "x", 3: "c", 4: "d", 5: "e", 6: "f"}
3046
+ m_rev = {"x": 0, "c": 3, "d": 4, "e": 5, "f": 6}
3047
+
3048
+ T1_out = {2, 7, 9, 8}
3049
+ T1_in = {1, 7}
3050
+ T2_out = {"b", "g", "i", "h"}
3051
+ T2_in = {"a", "g"}
3052
+ T1_tilde = set()
3053
+ T2_tilde = set()
3054
+
3055
+ gparams = _GraphParameters(self.G1, self.G2, {}, {}, {}, {}, {})
3056
+ sparams = _StateParameters(
3057
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
3058
+ )
3059
+
3060
+ # Remove a node from the mapping
3061
+ m.pop(0)
3062
+ m_rev.pop("x")
3063
+ _restore_Tinout_Di(0, self.mapped[0], gparams, sparams)
3064
+
3065
+ assert T1_out == {2, 7, 9, 8}
3066
+ assert T1_in == {1, 7}
3067
+ assert T2_out == {"b", "g", "i", "h"}
3068
+ assert T2_in == {"a", "g"}
3069
+ assert T1_tilde == {0}
3070
+ assert T2_tilde == {"x"}
3071
+
3072
+ # Remove a node from the mapping
3073
+ m.pop(6)
3074
+ m_rev.pop("f")
3075
+ _restore_Tinout_Di(6, self.mapped[6], gparams, sparams)
3076
+
3077
+ assert T1_out == {2, 9, 8, 7}
3078
+ assert T1_in == {1}
3079
+ assert T2_out == {"b", "i", "h", "g"}
3080
+ assert T2_in == {"a"}
3081
+ assert T1_tilde == {0, 6}
3082
+ assert T2_tilde == {"x", "f"}
3083
+
3084
+ # Remove a node from the mapping
3085
+ m.pop(3)
3086
+ m_rev.pop("c")
3087
+ _restore_Tinout_Di(3, self.mapped[3], gparams, sparams)
3088
+
3089
+ assert T1_out == {9, 8, 7}
3090
+ assert T1_in == {3}
3091
+ assert T2_out == {"i", "h", "g"}
3092
+ assert T2_in == {"c"}
3093
+ assert T1_tilde == {0, 6, 1, 2}
3094
+ assert T2_tilde == {"x", "f", "a", "b"}
3095
+
3096
+ # Remove a node from the mapping
3097
+ m.pop(5)
3098
+ m_rev.pop("e")
3099
+ _restore_Tinout_Di(5, self.mapped[5], gparams, sparams)
3100
+
3101
+ assert T1_out == {9, 5}
3102
+ assert T1_in == {3}
3103
+ assert T2_out == {"i", "e"}
3104
+ assert T2_in == {"c"}
3105
+ assert T1_tilde == {0, 6, 1, 2, 8, 7}
3106
+ assert T2_tilde == {"x", "f", "a", "b", "h", "g"}
3107
+
3108
+ # Remove a node from the mapping
3109
+ m.pop(4)
3110
+ m_rev.pop("d")
3111
+ _restore_Tinout_Di(4, self.mapped[4], gparams, sparams)
3112
+
3113
+ assert T1_out == set()
3114
+ assert T1_in == set()
3115
+ assert T2_out == set()
3116
+ assert T2_in == set()
3117
+ assert T1_tilde == set(self.G1.nodes())
3118
+ assert T2_tilde == set(self.G2.nodes())
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for VF2 isomorphism algorithm for weighted graphs.
3
+ """
4
+
5
+ import math
6
+ from operator import eq
7
+
8
+ import networkx as nx
9
+ import networkx.algorithms.isomorphism as iso
10
+
11
+
12
+ def test_simple():
13
+ # 16 simple tests
14
+ w = "weight"
15
+ edges = [(0, 0, 1), (0, 0, 1.5), (0, 1, 2), (1, 0, 3)]
16
+ for g1 in [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]:
17
+ g1.add_weighted_edges_from(edges)
18
+ g2 = g1.subgraph(g1.nodes())
19
+ if g1.is_multigraph():
20
+ em = iso.numerical_multiedge_match("weight", 1)
21
+ else:
22
+ em = iso.numerical_edge_match("weight", 1)
23
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
24
+
25
+ for mod1, mod2 in [(False, True), (True, False), (True, True)]:
26
+ # mod1 tests a regular edge
27
+ # mod2 tests a selfloop
28
+ if g2.is_multigraph():
29
+ if mod1:
30
+ data1 = {0: {"weight": 10}}
31
+ if mod2:
32
+ data2 = {0: {"weight": 1}, 1: {"weight": 2.5}}
33
+ else:
34
+ if mod1:
35
+ data1 = {"weight": 10}
36
+ if mod2:
37
+ data2 = {"weight": 2.5}
38
+
39
+ g2 = g1.subgraph(g1.nodes()).copy()
40
+ if mod1:
41
+ if not g1.is_directed():
42
+ g2._adj[1][0] = data1
43
+ g2._adj[0][1] = data1
44
+ else:
45
+ g2._succ[1][0] = data1
46
+ g2._pred[0][1] = data1
47
+ if mod2:
48
+ if not g1.is_directed():
49
+ g2._adj[0][0] = data2
50
+ else:
51
+ g2._succ[0][0] = data2
52
+ g2._pred[0][0] = data2
53
+
54
+ assert not nx.is_isomorphic(g1, g2, edge_match=em)
55
+
56
+
57
+ def test_weightkey():
58
+ g1 = nx.DiGraph()
59
+ g2 = nx.DiGraph()
60
+
61
+ g1.add_edge("A", "B", weight=1)
62
+ g2.add_edge("C", "D", weight=0)
63
+
64
+ assert nx.is_isomorphic(g1, g2)
65
+ em = iso.numerical_edge_match("nonexistent attribute", 1)
66
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
67
+ em = iso.numerical_edge_match("weight", 1)
68
+ assert not nx.is_isomorphic(g1, g2, edge_match=em)
69
+
70
+ g2 = nx.DiGraph()
71
+ g2.add_edge("C", "D")
72
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
73
+
74
+
75
+ class TestMatch_Graph:
76
+ def setup_method(self):
77
+ self.g1 = nx.Graph()
78
+ self.g2 = nx.Graph()
79
+ self.build()
80
+
81
+ def build(self):
82
+ self.nm = iso.categorical_node_match("color", "")
83
+ self.em = iso.numerical_edge_match("weight", 1)
84
+
85
+ self.g1.add_node("A", color="red")
86
+ self.g2.add_node("C", color="blue")
87
+
88
+ self.g1.add_edge("A", "B", weight=1)
89
+ self.g2.add_edge("C", "D", weight=1)
90
+
91
+ def test_noweight_nocolor(self):
92
+ assert nx.is_isomorphic(self.g1, self.g2)
93
+
94
+ def test_color1(self):
95
+ assert not nx.is_isomorphic(self.g1, self.g2, node_match=self.nm)
96
+
97
+ def test_color2(self):
98
+ self.g1.nodes["A"]["color"] = "blue"
99
+ assert nx.is_isomorphic(self.g1, self.g2, node_match=self.nm)
100
+
101
+ def test_weight1(self):
102
+ assert nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
103
+
104
+ def test_weight2(self):
105
+ self.g1.add_edge("A", "B", weight=2)
106
+ assert not nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
107
+
108
+ def test_colorsandweights1(self):
109
+ assert not nx.is_isomorphic(
110
+ self.g1, self.g2, node_match=self.nm, edge_match=self.em
111
+ )
112
+
113
+ def test_colorsandweights2(self):
114
+ self.g1.nodes["A"]["color"] = "blue"
115
+ assert nx.is_isomorphic(
116
+ self.g1, self.g2, node_match=self.nm, edge_match=self.em
117
+ )
118
+
119
+ def test_colorsandweights3(self):
120
+ # make the colors agree and the weights disagree
121
+ self.g1.nodes["A"]["color"] = "blue"
122
+ self.g1.add_edge("A", "B", weight=2)
123
+ assert not nx.is_isomorphic(
124
+ self.g1, self.g2, node_match=self.nm, edge_match=self.em
125
+ )
126
+
127
+
128
+ class TestMatch_DiGraph(TestMatch_Graph):
129
+ def setup_method(self):
130
+ self.g1 = nx.DiGraph()
131
+ self.g2 = nx.DiGraph()
132
+ self.build()
133
+
134
+
135
+ class TestMatch_MultiGraph:
136
+ def setup_method(self):
137
+ self.g1 = nx.MultiGraph()
138
+ self.g2 = nx.MultiGraph()
139
+ self.GM = iso.MultiGraphMatcher
140
+ self.build()
141
+
142
+ def build(self):
143
+ g1 = self.g1
144
+ g2 = self.g2
145
+
146
+ # We will assume integer weights only.
147
+ g1.add_edge("A", "B", color="green", weight=0, size=0.5)
148
+ g1.add_edge("A", "B", color="red", weight=1, size=0.35)
149
+ g1.add_edge("A", "B", color="red", weight=2, size=0.65)
150
+
151
+ g2.add_edge("C", "D", color="green", weight=1, size=0.5)
152
+ g2.add_edge("C", "D", color="red", weight=0, size=0.45)
153
+ g2.add_edge("C", "D", color="red", weight=2, size=0.65)
154
+
155
+ if g1.is_multigraph():
156
+ self.em = iso.numerical_multiedge_match("weight", 1)
157
+ self.emc = iso.categorical_multiedge_match("color", "")
158
+ self.emcm = iso.categorical_multiedge_match(["color", "weight"], ["", 1])
159
+ self.emg1 = iso.generic_multiedge_match("color", "red", eq)
160
+ self.emg2 = iso.generic_multiedge_match(
161
+ ["color", "weight", "size"],
162
+ ["red", 1, 0.5],
163
+ [eq, eq, math.isclose],
164
+ )
165
+
166
+ def test_weights_only(self):
167
+ assert nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
168
+
169
+ def test_colors_only(self):
170
+ gm = self.GM(self.g1, self.g2, edge_match=self.emc)
171
+ assert gm.is_isomorphic()
172
+
173
+ def test_colorsandweights(self):
174
+ gm = self.GM(self.g1, self.g2, edge_match=self.emcm)
175
+ assert not gm.is_isomorphic()
176
+
177
+ def test_generic_color_only(self):
178
+ gm = self.GM(self.g1, self.g2, edge_match=self.emg1)
179
+ assert gm.is_isomorphic()
180
+
181
+ def test_generic_all_3_attr(self):
182
+ gm = self.GM(self.g1, self.g2, edge_match=self.emg2)
183
+ assert not gm.is_isomorphic()
184
+
185
+ self.g2.edges["C", "D", 0]["weight"] = 0
186
+ self.g2.edges["C", "D", 1]["weight"] = 1
187
+ self.g2.edges["C", "D", 1]["size"] = 0.35
188
+ assert gm.is_isomorphic()
189
+
190
+
191
+ class TestMatch_MultiDiGraph(TestMatch_MultiGraph):
192
+ def setup_method(self):
193
+ self.g1 = nx.MultiDiGraph()
194
+ self.g2 = nx.MultiDiGraph()
195
+ self.GM = iso.MultiDiGraphMatcher
196
+ self.build()
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tree_isomorphism.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ An algorithm for finding if two undirected trees are isomorphic,
3
+ and if so returns an isomorphism between the two sets of nodes.
4
+
5
+ This algorithm uses a routine to tell if two rooted trees (trees with a
6
+ specified root node) are isomorphic, which may be independently useful.
7
+
8
+ This implements an algorithm from:
9
+ The Design and Analysis of Computer Algorithms
10
+ by Aho, Hopcroft, and Ullman
11
+ Addison-Wesley Publishing 1974
12
+ Example 3.2 pp. 84-86.
13
+
14
+ A more understandable version of this algorithm is described in:
15
+ Homework Assignment 5
16
+ McGill University SOCS 308-250B, Winter 2002
17
+ by Matthew Suderman
18
+ http://crypto.cs.mcgill.ca/~crepeau/CS250/2004/HW5+.pdf
19
+ """
20
+
21
+ from collections import defaultdict
22
+
23
+ import networkx as nx
24
+ from networkx.utils.decorators import not_implemented_for
25
+
26
+ __all__ = ["rooted_tree_isomorphism", "tree_isomorphism"]
27
+
28
+
29
+ @nx._dispatchable(graphs={"t1": 0, "t2": 2}, returns_graph=True)
30
+ def root_trees(t1, root1, t2, root2):
31
+ """Create a single digraph dT of free trees t1 and t2
32
+ # with roots root1 and root2 respectively
33
+ # rename the nodes with consecutive integers
34
+ # so that all nodes get a unique name between both trees
35
+
36
+ # our new "fake" root node is 0
37
+ # t1 is numbers from 1 ... n
38
+ # t2 is numbered from n+1 to 2n
39
+ """
40
+
41
+ dT = nx.DiGraph()
42
+
43
+ newroot1 = 1 # left root will be 1
44
+ newroot2 = nx.number_of_nodes(t1) + 1 # right will be n+1
45
+
46
+ # may be overlap in node names here so need separate maps
47
+ # given the old name, what is the new
48
+ namemap1 = {root1: newroot1}
49
+ namemap2 = {root2: newroot2}
50
+
51
+ # add an edge from our new root to root1 and root2
52
+ dT.add_edge(0, namemap1[root1])
53
+ dT.add_edge(0, namemap2[root2])
54
+
55
+ for i, (v1, v2) in enumerate(nx.bfs_edges(t1, root1)):
56
+ namemap1[v2] = i + namemap1[root1] + 1
57
+ dT.add_edge(namemap1[v1], namemap1[v2])
58
+
59
+ for i, (v1, v2) in enumerate(nx.bfs_edges(t2, root2)):
60
+ namemap2[v2] = i + namemap2[root2] + 1
61
+ dT.add_edge(namemap2[v1], namemap2[v2])
62
+
63
+ # now we really want the inverse of namemap1 and namemap2
64
+ # giving the old name given the new
65
+ # since the values of namemap1 and namemap2 are unique
66
+ # there won't be collisions
67
+ namemap = {}
68
+ for old, new in namemap1.items():
69
+ namemap[new] = old
70
+ for old, new in namemap2.items():
71
+ namemap[new] = old
72
+
73
+ return (dT, namemap, newroot1, newroot2)
74
+
75
+
76
+ @nx._dispatchable(graphs={"t1": 0, "t2": 2})
77
+ def rooted_tree_isomorphism(t1, root1, t2, root2):
78
+ """
79
+ Return an isomorphic mapping between rooted trees `t1` and `t2` with roots
80
+ `root1` and `root2`, respectively.
81
+
82
+ These trees may be either directed or undirected,
83
+ but if they are directed, all edges should flow from the root.
84
+
85
+ It returns the isomorphism, a mapping of the nodes of `t1` onto the nodes
86
+ of `t2`, such that two trees are then identical.
87
+
88
+ Note that two trees may have more than one isomorphism, and this
89
+ routine just returns one valid mapping.
90
+ This is a subroutine used to implement `tree_isomorphism`, but will
91
+ be somewhat faster if you already have rooted trees.
92
+
93
+ Parameters
94
+ ----------
95
+ t1 : NetworkX graph
96
+ One of the trees being compared
97
+
98
+ root1 : node
99
+ A node of `t1` which is the root of the tree
100
+
101
+ t2 : NetworkX graph
102
+ The other tree being compared
103
+
104
+ root2 : node
105
+ a node of `t2` which is the root of the tree
106
+
107
+ Returns
108
+ -------
109
+ isomorphism : list
110
+ A list of pairs in which the left element is a node in `t1`
111
+ and the right element is a node in `t2`. The pairs are in
112
+ arbitrary order. If the nodes in one tree is mapped to the names in
113
+ the other, then trees will be identical. Note that an isomorphism
114
+ will not necessarily be unique.
115
+
116
+ If `t1` and `t2` are not isomorphic, then it returns the empty list.
117
+
118
+ Raises
119
+ ------
120
+ NetworkXError
121
+ If either `t1` or `t2` is not a tree
122
+ """
123
+
124
+ if not nx.is_tree(t1):
125
+ raise nx.NetworkXError("t1 is not a tree")
126
+ if not nx.is_tree(t2):
127
+ raise nx.NetworkXError("t2 is not a tree")
128
+
129
+ # get the rooted tree formed by combining them
130
+ # with unique names
131
+ (dT, namemap, newroot1, newroot2) = root_trees(t1, root1, t2, root2)
132
+
133
+ # Group nodes by their distance from the root
134
+ L = defaultdict(list)
135
+ for n, dist in nx.shortest_path_length(dT, source=0).items():
136
+ L[dist].append(n)
137
+
138
+ # height
139
+ h = max(L)
140
+
141
+ # each node has a label, initially set to 0
142
+ label = {v: 0 for v in dT}
143
+ # and also ordered_labels and ordered_children
144
+ # which will store ordered tuples
145
+ ordered_labels = {v: () for v in dT}
146
+ ordered_children = {v: () for v in dT}
147
+
148
+ # nothing to do on last level so start on h-1
149
+ # also nothing to do for our fake level 0, so skip that
150
+ for i in range(h - 1, 0, -1):
151
+ # update the ordered_labels and ordered_children
152
+ # for any children
153
+ for v in L[i]:
154
+ # nothing to do if no children
155
+ if dT.out_degree(v) > 0:
156
+ # get all the pairs of labels and nodes of children and sort by labels
157
+ # reverse=True to preserve DFS order, see gh-7945
158
+ s = sorted(((label[u], u) for u in dT.successors(v)), reverse=True)
159
+
160
+ # invert to give a list of two tuples
161
+ # the sorted labels, and the corresponding children
162
+ ordered_labels[v], ordered_children[v] = list(zip(*s))
163
+
164
+ # now collect and sort the sorted ordered_labels
165
+ # for all nodes in L[i], carrying along the node
166
+ forlabel = sorted((ordered_labels[v], v) for v in L[i])
167
+
168
+ # now assign labels to these nodes, according to the sorted order
169
+ # starting from 0, where identical ordered_labels get the same label
170
+ current = 0
171
+ for i, (ol, v) in enumerate(forlabel):
172
+ # advance to next label if not 0, and different from previous
173
+ if (i != 0) and (ol != forlabel[i - 1][0]):
174
+ current += 1
175
+ label[v] = current
176
+
177
+ # they are isomorphic if the labels of newroot1 and newroot2 are 0
178
+ isomorphism = []
179
+ if label[newroot1] == 0 and label[newroot2] == 0:
180
+ # now lets get the isomorphism by walking the ordered_children
181
+ stack = [(newroot1, newroot2)]
182
+ while stack:
183
+ curr_v, curr_w = stack.pop()
184
+ isomorphism.append((curr_v, curr_w))
185
+ stack.extend(zip(ordered_children[curr_v], ordered_children[curr_w]))
186
+
187
+ # get the mapping back in terms of the old names
188
+ # return in sorted order for neatness
189
+ isomorphism = [(namemap[u], namemap[v]) for (u, v) in isomorphism]
190
+
191
+ return isomorphism
192
+
193
+
194
+ @not_implemented_for("directed")
195
+ @not_implemented_for("multigraph")
196
+ @nx._dispatchable(graphs={"t1": 0, "t2": 1})
197
+ def tree_isomorphism(t1, t2):
198
+ """
199
+ Return an isomorphic mapping between two trees `t1` and `t2`.
200
+
201
+ If `t1` and `t2` are not isomorphic, an empty list is returned.
202
+ Note that two trees may have more than one isomorphism, and this routine just
203
+ returns one valid mapping.
204
+
205
+ Parameters
206
+ ----------
207
+ t1 : undirected NetworkX graph
208
+ One of the trees being compared
209
+
210
+ t2 : undirected NetworkX graph
211
+ The other tree being compared
212
+
213
+ Returns
214
+ -------
215
+ isomorphism : list
216
+ A list of pairs in which the left element is a node in `t1`
217
+ and the right element is a node in `t2`. The pairs are in
218
+ arbitrary order. If the nodes in one tree is mapped to the names in
219
+ the other, then trees will be identical. Note that an isomorphism
220
+ will not necessarily be unique.
221
+
222
+ If `t1` and `t2` are not isomorphic, then it returns the empty list.
223
+
224
+ Raises
225
+ ------
226
+ NetworkXError
227
+ If either `t1` or `t2` is not a tree
228
+
229
+ Notes
230
+ -----
231
+ This runs in ``O(n*log(n))`` time for trees with ``n`` nodes.
232
+ """
233
+ if not nx.is_tree(t1):
234
+ raise nx.NetworkXError("t1 is not a tree")
235
+ if not nx.is_tree(t2):
236
+ raise nx.NetworkXError("t2 is not a tree")
237
+
238
+ # To be isomorphic, t1 and t2 must have the same number of nodes and sorted
239
+ # degree sequences
240
+ if not nx.faster_could_be_isomorphic(t1, t2):
241
+ return []
242
+
243
+ # A tree can have either 1 or 2 centers.
244
+ # If the number doesn't match then t1 and t2 are not isomorphic.
245
+ center1 = nx.center(t1)
246
+ center2 = nx.center(t2)
247
+
248
+ if len(center1) != len(center2):
249
+ return []
250
+
251
+ # If there is only 1 center in each, then use it.
252
+ if len(center1) == 1:
253
+ return rooted_tree_isomorphism(t1, center1[0], t2, center2[0])
254
+
255
+ # If there both have 2 centers, then try the first for t1
256
+ # with the first for t2.
257
+ attempts = rooted_tree_isomorphism(t1, center1[0], t2, center2[0])
258
+
259
+ # If that worked we're done.
260
+ if len(attempts) > 0:
261
+ return attempts
262
+
263
+ # Otherwise, try center1[0] with the center2[1], and see if that works
264
+ return rooted_tree_isomorphism(t1, center1[0], t2, center2[1])
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/vf2pp.py ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ***************
3
+ VF2++ Algorithm
4
+ ***************
5
+
6
+ An implementation of the VF2++ algorithm [1]_ for Graph Isomorphism testing.
7
+
8
+ The simplest interface to use this module is to call:
9
+
10
+ `vf2pp_is_isomorphic`: to check whether two graphs are isomorphic.
11
+ `vf2pp_isomorphism`: to obtain the node mapping between two graphs,
12
+ in case they are isomorphic.
13
+ `vf2pp_all_isomorphisms`: to generate all possible mappings between two graphs,
14
+ if isomorphic.
15
+
16
+ Introduction
17
+ ------------
18
+ The VF2++ algorithm, follows a similar logic to that of VF2, while also
19
+ introducing new easy-to-check cutting rules and determining the optimal access
20
+ order of nodes. It is also implemented in a non-recursive manner, which saves
21
+ both time and space, when compared to its previous counterpart.
22
+
23
+ The optimal node ordering is obtained after taking into consideration both the
24
+ degree but also the label rarity of each node.
25
+ This way we place the nodes that are more likely to match, first in the order,
26
+ thus examining the most promising branches in the beginning.
27
+ The rules also consider node labels, making it easier to prune unfruitful
28
+ branches early in the process.
29
+
30
+ Examples
31
+ --------
32
+
33
+ Suppose G1 and G2 are Isomorphic Graphs. Verification is as follows:
34
+
35
+ Without node labels:
36
+
37
+ >>> import networkx as nx
38
+ >>> G1 = nx.path_graph(4)
39
+ >>> G2 = nx.path_graph(4)
40
+ >>> nx.vf2pp_is_isomorphic(G1, G2, node_label=None)
41
+ True
42
+ >>> nx.vf2pp_isomorphism(G1, G2, node_label=None)
43
+ {1: 1, 2: 2, 0: 0, 3: 3}
44
+
45
+ With node labels:
46
+
47
+ >>> G1 = nx.path_graph(4)
48
+ >>> G2 = nx.path_graph(4)
49
+ >>> mapped = {1: 1, 2: 2, 3: 3, 0: 0}
50
+ >>> nx.set_node_attributes(
51
+ ... G1, dict(zip(G1, ["blue", "red", "green", "yellow"])), "label"
52
+ ... )
53
+ >>> nx.set_node_attributes(
54
+ ... G2,
55
+ ... dict(zip([mapped[u] for u in G1], ["blue", "red", "green", "yellow"])),
56
+ ... "label",
57
+ ... )
58
+ >>> nx.vf2pp_is_isomorphic(G1, G2, node_label="label")
59
+ True
60
+ >>> nx.vf2pp_isomorphism(G1, G2, node_label="label")
61
+ {1: 1, 2: 2, 0: 0, 3: 3}
62
+
63
+ References
64
+ ----------
65
+ .. [1] Jüttner, Alpár & Madarasi, Péter. (2018). "VF2++—An improved subgraph
66
+ isomorphism algorithm". Discrete Applied Mathematics. 242.
67
+ https://doi.org/10.1016/j.dam.2018.02.018
68
+
69
+ """
70
+
71
+ import collections
72
+
73
+ import networkx as nx
74
+
75
+ __all__ = ["vf2pp_isomorphism", "vf2pp_is_isomorphic", "vf2pp_all_isomorphisms"]
76
+
77
+ _GraphParameters = collections.namedtuple(
78
+ "_GraphParameters",
79
+ [
80
+ "G1",
81
+ "G2",
82
+ "G1_labels",
83
+ "G2_labels",
84
+ "nodes_of_G1Labels",
85
+ "nodes_of_G2Labels",
86
+ "G2_nodes_of_degree",
87
+ ],
88
+ )
89
+
90
+ _StateParameters = collections.namedtuple(
91
+ "_StateParameters",
92
+ [
93
+ "mapping",
94
+ "reverse_mapping",
95
+ "T1",
96
+ "T1_in",
97
+ "T1_tilde",
98
+ "T1_tilde_in",
99
+ "T2",
100
+ "T2_in",
101
+ "T2_tilde",
102
+ "T2_tilde_in",
103
+ ],
104
+ )
105
+
106
+
107
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"})
108
+ def vf2pp_isomorphism(G1, G2, node_label=None, default_label=None):
109
+ """Return an isomorphic mapping between `G1` and `G2` if it exists.
110
+
111
+ Parameters
112
+ ----------
113
+ G1, G2 : NetworkX Graph or MultiGraph instances.
114
+ The two graphs to check for isomorphism.
115
+
116
+ node_label : str, optional
117
+ The name of the node attribute to be used when comparing nodes.
118
+ The default is `None`, meaning node attributes are not considered
119
+ in the comparison. Any node that doesn't have the `node_label`
120
+ attribute uses `default_label` instead.
121
+
122
+ default_label : scalar
123
+ Default value to use when a node doesn't have an attribute
124
+ named `node_label`. Default is `None`.
125
+
126
+ Returns
127
+ -------
128
+ dict or None
129
+ Node mapping if the two graphs are isomorphic. None otherwise.
130
+ """
131
+ try:
132
+ mapping = next(vf2pp_all_isomorphisms(G1, G2, node_label, default_label))
133
+ return mapping
134
+ except StopIteration:
135
+ return None
136
+
137
+
138
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"})
139
+ def vf2pp_is_isomorphic(G1, G2, node_label=None, default_label=None):
140
+ """Examines whether G1 and G2 are isomorphic.
141
+
142
+ Parameters
143
+ ----------
144
+ G1, G2 : NetworkX Graph or MultiGraph instances.
145
+ The two graphs to check for isomorphism.
146
+
147
+ node_label : str, optional
148
+ The name of the node attribute to be used when comparing nodes.
149
+ The default is `None`, meaning node attributes are not considered
150
+ in the comparison. Any node that doesn't have the `node_label`
151
+ attribute uses `default_label` instead.
152
+
153
+ default_label : scalar
154
+ Default value to use when a node doesn't have an attribute
155
+ named `node_label`. Default is `None`.
156
+
157
+ Returns
158
+ -------
159
+ bool
160
+ True if the two graphs are isomorphic, False otherwise.
161
+ """
162
+ if vf2pp_isomorphism(G1, G2, node_label, default_label) is not None:
163
+ return True
164
+ return False
165
+
166
+
167
+ @nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"})
168
+ def vf2pp_all_isomorphisms(G1, G2, node_label=None, default_label=None):
169
+ """Yields all the possible mappings between G1 and G2.
170
+
171
+ Parameters
172
+ ----------
173
+ G1, G2 : NetworkX Graph or MultiGraph instances.
174
+ The two graphs to check for isomorphism.
175
+
176
+ node_label : str, optional
177
+ The name of the node attribute to be used when comparing nodes.
178
+ The default is `None`, meaning node attributes are not considered
179
+ in the comparison. Any node that doesn't have the `node_label`
180
+ attribute uses `default_label` instead.
181
+
182
+ default_label : scalar
183
+ Default value to use when a node doesn't have an attribute
184
+ named `node_label`. Default is `None`.
185
+
186
+ Yields
187
+ ------
188
+ dict
189
+ Isomorphic mapping between the nodes in `G1` and `G2`.
190
+ """
191
+ if G1.number_of_nodes() == 0 or G2.number_of_nodes() == 0:
192
+ return False
193
+
194
+ # Create the degree dicts based on graph type
195
+ if G1.is_directed():
196
+ G1_degree = {
197
+ n: (in_degree, out_degree)
198
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
199
+ }
200
+ G2_degree = {
201
+ n: (in_degree, out_degree)
202
+ for (n, in_degree), (_, out_degree) in zip(G2.in_degree, G2.out_degree)
203
+ }
204
+ else:
205
+ G1_degree = dict(G1.degree)
206
+ G2_degree = dict(G2.degree)
207
+
208
+ if not G1.is_directed():
209
+ find_candidates = _find_candidates
210
+ restore_Tinout = _restore_Tinout
211
+ else:
212
+ find_candidates = _find_candidates_Di
213
+ restore_Tinout = _restore_Tinout_Di
214
+
215
+ # Check that both graphs have the same number of nodes and degree sequence
216
+ if G1.order() != G2.order():
217
+ return False
218
+ if sorted(G1_degree.values()) != sorted(G2_degree.values()):
219
+ return False
220
+
221
+ # Initialize parameters and cache necessary information about degree and labels
222
+ graph_params, state_params = _initialize_parameters(
223
+ G1, G2, G2_degree, node_label, default_label
224
+ )
225
+
226
+ # Check if G1 and G2 have the same labels, and that number of nodes per label
227
+ # is equal between the two graphs
228
+ if not _precheck_label_properties(graph_params):
229
+ return False
230
+
231
+ # Calculate the optimal node ordering
232
+ node_order = _matching_order(graph_params)
233
+
234
+ # Initialize the stack
235
+ stack = []
236
+ candidates = iter(
237
+ find_candidates(node_order[0], graph_params, state_params, G1_degree)
238
+ )
239
+ stack.append((node_order[0], candidates))
240
+
241
+ mapping = state_params.mapping
242
+ reverse_mapping = state_params.reverse_mapping
243
+
244
+ # Index of the node from the order, currently being examined
245
+ matching_node = 1
246
+
247
+ while stack:
248
+ current_node, candidate_nodes = stack[-1]
249
+
250
+ try:
251
+ candidate = next(candidate_nodes)
252
+ except StopIteration:
253
+ # If no remaining candidates, return to a previous state, and follow another branch
254
+ stack.pop()
255
+ matching_node -= 1
256
+ if stack:
257
+ # Pop the previously added u-v pair, and look for a different candidate _v for u
258
+ popped_node1, _ = stack[-1]
259
+ popped_node2 = mapping[popped_node1]
260
+ mapping.pop(popped_node1)
261
+ reverse_mapping.pop(popped_node2)
262
+ restore_Tinout(popped_node1, popped_node2, graph_params, state_params)
263
+ continue
264
+
265
+ if _feasibility(current_node, candidate, graph_params, state_params):
266
+ # Terminate if mapping is extended to its full
267
+ if len(mapping) == G2.number_of_nodes() - 1:
268
+ cp_mapping = mapping.copy()
269
+ cp_mapping[current_node] = candidate
270
+ yield cp_mapping
271
+ continue
272
+
273
+ # Feasibility rules pass, so extend the mapping and update the parameters
274
+ mapping[current_node] = candidate
275
+ reverse_mapping[candidate] = current_node
276
+ _update_Tinout(current_node, candidate, graph_params, state_params)
277
+ # Append the next node and its candidates to the stack
278
+ candidates = iter(
279
+ find_candidates(
280
+ node_order[matching_node], graph_params, state_params, G1_degree
281
+ )
282
+ )
283
+ stack.append((node_order[matching_node], candidates))
284
+ matching_node += 1
285
+
286
+
287
+ def _precheck_label_properties(graph_params):
288
+ G1, G2, G1_labels, G2_labels, nodes_of_G1Labels, nodes_of_G2Labels, _ = graph_params
289
+ if any(
290
+ label not in nodes_of_G1Labels or len(nodes_of_G1Labels[label]) != len(nodes)
291
+ for label, nodes in nodes_of_G2Labels.items()
292
+ ):
293
+ return False
294
+ return True
295
+
296
+
297
+ def _initialize_parameters(G1, G2, G2_degree, node_label=None, default_label=-1):
298
+ """Initializes all the necessary parameters for VF2++
299
+
300
+ Parameters
301
+ ----------
302
+ G1,G2: NetworkX Graph or MultiGraph instances.
303
+ The two graphs to check for isomorphism or monomorphism
304
+
305
+ G1_labels,G2_labels: dict
306
+ The label of every node in G1 and G2 respectively
307
+
308
+ Returns
309
+ -------
310
+ graph_params: namedtuple
311
+ Contains all the Graph-related parameters:
312
+
313
+ G1,G2
314
+ G1_labels,G2_labels: dict
315
+
316
+ state_params: namedtuple
317
+ Contains all the State-related parameters:
318
+
319
+ mapping: dict
320
+ The mapping as extended so far. Maps nodes of G1 to nodes of G2
321
+
322
+ reverse_mapping: dict
323
+ The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
324
+ It's basically "mapping" reversed
325
+
326
+ T1, T2: set
327
+ Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
328
+ that are not in the mapping, but are neighbors of nodes that are.
329
+
330
+ T1_out, T2_out: set
331
+ Ti_out contains all the nodes from Gi, that are neither in the mapping
332
+ nor in Ti
333
+ """
334
+ G1_labels = dict(G1.nodes(data=node_label, default=default_label))
335
+ G2_labels = dict(G2.nodes(data=node_label, default=default_label))
336
+
337
+ graph_params = _GraphParameters(
338
+ G1,
339
+ G2,
340
+ G1_labels,
341
+ G2_labels,
342
+ nx.utils.groups(G1_labels),
343
+ nx.utils.groups(G2_labels),
344
+ nx.utils.groups(G2_degree),
345
+ )
346
+
347
+ T1, T1_in = set(), set()
348
+ T2, T2_in = set(), set()
349
+ if G1.is_directed():
350
+ T1_tilde, T1_tilde_in = (
351
+ set(G1.nodes()),
352
+ set(),
353
+ ) # todo: do we need Ti_tilde_in? What nodes does it have?
354
+ T2_tilde, T2_tilde_in = set(G2.nodes()), set()
355
+ else:
356
+ T1_tilde, T1_tilde_in = set(G1.nodes()), set()
357
+ T2_tilde, T2_tilde_in = set(G2.nodes()), set()
358
+
359
+ state_params = _StateParameters(
360
+ {},
361
+ {},
362
+ T1,
363
+ T1_in,
364
+ T1_tilde,
365
+ T1_tilde_in,
366
+ T2,
367
+ T2_in,
368
+ T2_tilde,
369
+ T2_tilde_in,
370
+ )
371
+
372
+ return graph_params, state_params
373
+
374
+
375
+ def _matching_order(graph_params):
376
+ """The node ordering as introduced in VF2++.
377
+
378
+ Notes
379
+ -----
380
+ Taking into account the structure of the Graph and the node labeling, the
381
+ nodes are placed in an order such that, most of the unfruitful/infeasible
382
+ branches of the search space can be pruned on high levels, significantly
383
+ decreasing the number of visited states. The premise is that, the algorithm
384
+ will be able to recognize inconsistencies early, proceeding to go deep into
385
+ the search tree only if it's needed.
386
+
387
+ Parameters
388
+ ----------
389
+ graph_params: namedtuple
390
+ Contains:
391
+
392
+ G1,G2: NetworkX Graph or MultiGraph instances.
393
+ The two graphs to check for isomorphism or monomorphism.
394
+
395
+ G1_labels,G2_labels: dict
396
+ The label of every node in G1 and G2 respectively.
397
+
398
+ Returns
399
+ -------
400
+ node_order: list
401
+ The ordering of the nodes.
402
+ """
403
+ G1, G2, G1_labels, _, _, nodes_of_G2Labels, _ = graph_params
404
+ if not G1 and not G2:
405
+ return {}
406
+
407
+ if G1.is_directed():
408
+ G1 = G1.to_undirected(as_view=True)
409
+
410
+ V1_unordered = set(G1.nodes())
411
+ label_rarity = {label: len(nodes) for label, nodes in nodes_of_G2Labels.items()}
412
+ used_degrees = {node: 0 for node in G1}
413
+ node_order = []
414
+
415
+ while V1_unordered:
416
+ max_rarity = min(label_rarity[G1_labels[x]] for x in V1_unordered)
417
+ rarest_nodes = [
418
+ n for n in V1_unordered if label_rarity[G1_labels[n]] == max_rarity
419
+ ]
420
+ max_node = max(rarest_nodes, key=G1.degree)
421
+
422
+ for dlevel_nodes in nx.bfs_layers(G1, max_node):
423
+ nodes_to_add = dlevel_nodes.copy()
424
+ while nodes_to_add:
425
+ max_used_degree = max(used_degrees[n] for n in nodes_to_add)
426
+ max_used_degree_nodes = [
427
+ n for n in nodes_to_add if used_degrees[n] == max_used_degree
428
+ ]
429
+ max_degree = max(G1.degree[n] for n in max_used_degree_nodes)
430
+ max_degree_nodes = [
431
+ n for n in max_used_degree_nodes if G1.degree[n] == max_degree
432
+ ]
433
+ next_node = min(
434
+ max_degree_nodes, key=lambda x: label_rarity[G1_labels[x]]
435
+ )
436
+
437
+ node_order.append(next_node)
438
+ for node in G1.neighbors(next_node):
439
+ used_degrees[node] += 1
440
+
441
+ nodes_to_add.remove(next_node)
442
+ label_rarity[G1_labels[next_node]] -= 1
443
+ V1_unordered.discard(next_node)
444
+
445
+ return node_order
446
+
447
+
448
+ def _find_candidates(
449
+ u, graph_params, state_params, G1_degree
450
+ ): # todo: make the 4th argument the degree of u
451
+ """Given node u of G1, finds the candidates of u from G2.
452
+
453
+ Parameters
454
+ ----------
455
+ u: Graph node
456
+ The node from G1 for which to find the candidates from G2.
457
+
458
+ graph_params: namedtuple
459
+ Contains all the Graph-related parameters:
460
+
461
+ G1,G2: NetworkX Graph or MultiGraph instances.
462
+ The two graphs to check for isomorphism or monomorphism
463
+
464
+ G1_labels,G2_labels: dict
465
+ The label of every node in G1 and G2 respectively
466
+
467
+ state_params: namedtuple
468
+ Contains all the State-related parameters:
469
+
470
+ mapping: dict
471
+ The mapping as extended so far. Maps nodes of G1 to nodes of G2
472
+
473
+ reverse_mapping: dict
474
+ The reverse mapping as extended so far. Maps nodes from G2 to nodes
475
+ of G1. It's basically "mapping" reversed
476
+
477
+ T1, T2: set
478
+ Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
479
+ that are not in the mapping, but are neighbors of nodes that are.
480
+
481
+ T1_tilde, T2_tilde: set
482
+ Ti_tilde contains all the nodes from Gi, that are neither in the
483
+ mapping nor in Ti
484
+
485
+ Returns
486
+ -------
487
+ candidates: set
488
+ The nodes from G2 which are candidates for u.
489
+ """
490
+ G1, G2, G1_labels, _, _, nodes_of_G2Labels, G2_nodes_of_degree = graph_params
491
+ mapping, reverse_mapping, _, _, _, _, _, _, T2_tilde, _ = state_params
492
+
493
+ covered_nbrs = [nbr for nbr in G1[u] if nbr in mapping]
494
+ if not covered_nbrs:
495
+ candidates = set(nodes_of_G2Labels[G1_labels[u]])
496
+ candidates.intersection_update(G2_nodes_of_degree[G1_degree[u]])
497
+ candidates.intersection_update(T2_tilde)
498
+ candidates.difference_update(reverse_mapping)
499
+ if G1.is_multigraph():
500
+ candidates.difference_update(
501
+ {
502
+ node
503
+ for node in candidates
504
+ if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
505
+ }
506
+ )
507
+ return candidates
508
+
509
+ nbr1 = covered_nbrs[0]
510
+ common_nodes = set(G2[mapping[nbr1]])
511
+
512
+ for nbr1 in covered_nbrs[1:]:
513
+ common_nodes.intersection_update(G2[mapping[nbr1]])
514
+
515
+ common_nodes.difference_update(reverse_mapping)
516
+ common_nodes.intersection_update(G2_nodes_of_degree[G1_degree[u]])
517
+ common_nodes.intersection_update(nodes_of_G2Labels[G1_labels[u]])
518
+ if G1.is_multigraph():
519
+ common_nodes.difference_update(
520
+ {
521
+ node
522
+ for node in common_nodes
523
+ if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
524
+ }
525
+ )
526
+ return common_nodes
527
+
528
+
529
+ def _find_candidates_Di(u, graph_params, state_params, G1_degree):
530
+ G1, G2, G1_labels, _, _, nodes_of_G2Labels, G2_nodes_of_degree = graph_params
531
+ mapping, reverse_mapping, _, _, _, _, _, _, T2_tilde, _ = state_params
532
+
533
+ covered_successors = [succ for succ in G1[u] if succ in mapping]
534
+ covered_predecessors = [pred for pred in G1.pred[u] if pred in mapping]
535
+
536
+ if not (covered_successors or covered_predecessors):
537
+ candidates = set(nodes_of_G2Labels[G1_labels[u]])
538
+ candidates.intersection_update(G2_nodes_of_degree[G1_degree[u]])
539
+ candidates.intersection_update(T2_tilde)
540
+ candidates.difference_update(reverse_mapping)
541
+ if G1.is_multigraph():
542
+ candidates.difference_update(
543
+ {
544
+ node
545
+ for node in candidates
546
+ if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
547
+ }
548
+ )
549
+ return candidates
550
+
551
+ if covered_successors:
552
+ succ1 = covered_successors[0]
553
+ common_nodes = set(G2.pred[mapping[succ1]])
554
+
555
+ for succ1 in covered_successors[1:]:
556
+ common_nodes.intersection_update(G2.pred[mapping[succ1]])
557
+ else:
558
+ pred1 = covered_predecessors.pop()
559
+ common_nodes = set(G2[mapping[pred1]])
560
+
561
+ for pred1 in covered_predecessors:
562
+ common_nodes.intersection_update(G2[mapping[pred1]])
563
+
564
+ common_nodes.difference_update(reverse_mapping)
565
+ common_nodes.intersection_update(G2_nodes_of_degree[G1_degree[u]])
566
+ common_nodes.intersection_update(nodes_of_G2Labels[G1_labels[u]])
567
+ if G1.is_multigraph():
568
+ common_nodes.difference_update(
569
+ {
570
+ node
571
+ for node in common_nodes
572
+ if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
573
+ }
574
+ )
575
+ return common_nodes
576
+
577
+
578
+ def _feasibility(node1, node2, graph_params, state_params):
579
+ """Given a candidate pair of nodes u and v from G1 and G2 respectively,
580
+ checks if it's feasible to extend the mapping, i.e. if u and v can be matched.
581
+
582
+ Notes
583
+ -----
584
+ This function performs all the necessary checking by applying both consistency
585
+ and cutting rules.
586
+
587
+ Parameters
588
+ ----------
589
+ node1, node2: Graph node
590
+ The candidate pair of nodes being checked for matching
591
+
592
+ graph_params: namedtuple
593
+ Contains all the Graph-related parameters:
594
+
595
+ G1,G2: NetworkX Graph or MultiGraph instances.
596
+ The two graphs to check for isomorphism or monomorphism
597
+
598
+ G1_labels,G2_labels: dict
599
+ The label of every node in G1 and G2 respectively
600
+
601
+ state_params: namedtuple
602
+ Contains all the State-related parameters:
603
+
604
+ mapping: dict
605
+ The mapping as extended so far. Maps nodes of G1 to nodes of G2
606
+
607
+ reverse_mapping: dict
608
+ The reverse mapping as extended so far. Maps nodes from G2 to nodes
609
+ of G1. It's basically "mapping" reversed
610
+
611
+ T1, T2: set
612
+ Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
613
+ that are not in the mapping, but are neighbors of nodes that are.
614
+
615
+ T1_out, T2_out: set
616
+ Ti_out contains all the nodes from Gi, that are neither in the mapping
617
+ nor in Ti
618
+
619
+ Returns
620
+ -------
621
+ True if all checks are successful, False otherwise.
622
+ """
623
+ G1 = graph_params.G1
624
+
625
+ if _cut_PT(node1, node2, graph_params, state_params):
626
+ return False
627
+
628
+ if G1.is_multigraph():
629
+ if not _consistent_PT(node1, node2, graph_params, state_params):
630
+ return False
631
+
632
+ return True
633
+
634
+
635
+ def _cut_PT(u, v, graph_params, state_params):
636
+ """Implements the cutting rules for the ISO problem.
637
+
638
+ Parameters
639
+ ----------
640
+ u, v: Graph node
641
+ The two candidate nodes being examined.
642
+
643
+ graph_params: namedtuple
644
+ Contains all the Graph-related parameters:
645
+
646
+ G1,G2: NetworkX Graph or MultiGraph instances.
647
+ The two graphs to check for isomorphism or monomorphism
648
+
649
+ G1_labels,G2_labels: dict
650
+ The label of every node in G1 and G2 respectively
651
+
652
+ state_params: namedtuple
653
+ Contains all the State-related parameters:
654
+
655
+ mapping: dict
656
+ The mapping as extended so far. Maps nodes of G1 to nodes of G2
657
+
658
+ reverse_mapping: dict
659
+ The reverse mapping as extended so far. Maps nodes from G2 to nodes
660
+ of G1. It's basically "mapping" reversed
661
+
662
+ T1, T2: set
663
+ Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
664
+ that are not in the mapping, but are neighbors of nodes that are.
665
+
666
+ T1_tilde, T2_tilde: set
667
+ Ti_out contains all the nodes from Gi, that are neither in the
668
+ mapping nor in Ti
669
+
670
+ Returns
671
+ -------
672
+ True if we should prune this branch, i.e. the node pair failed the cutting checks. False otherwise.
673
+ """
674
+ G1, G2, G1_labels, G2_labels, _, _, _ = graph_params
675
+ (
676
+ _,
677
+ _,
678
+ T1,
679
+ T1_in,
680
+ T1_tilde,
681
+ _,
682
+ T2,
683
+ T2_in,
684
+ T2_tilde,
685
+ _,
686
+ ) = state_params
687
+
688
+ u_labels_predecessors, v_labels_predecessors = {}, {}
689
+ if G1.is_directed():
690
+ u_labels_predecessors = nx.utils.groups(
691
+ {n1: G1_labels[n1] for n1 in G1.pred[u]}
692
+ )
693
+ v_labels_predecessors = nx.utils.groups(
694
+ {n2: G2_labels[n2] for n2 in G2.pred[v]}
695
+ )
696
+
697
+ if set(u_labels_predecessors.keys()) != set(v_labels_predecessors.keys()):
698
+ return True
699
+
700
+ u_labels_successors = nx.utils.groups({n1: G1_labels[n1] for n1 in G1[u]})
701
+ v_labels_successors = nx.utils.groups({n2: G2_labels[n2] for n2 in G2[v]})
702
+
703
+ # if the neighbors of u, do not have the same labels as those of v, NOT feasible.
704
+ if set(u_labels_successors.keys()) != set(v_labels_successors.keys()):
705
+ return True
706
+
707
+ for label, G1_nbh in u_labels_successors.items():
708
+ G2_nbh = v_labels_successors[label]
709
+
710
+ if G1.is_multigraph():
711
+ # Check for every neighbor in the neighborhood, if u-nbr1 has same edges as v-nbr2
712
+ u_nbrs_edges = sorted(G1.number_of_edges(u, x) for x in G1_nbh)
713
+ v_nbrs_edges = sorted(G2.number_of_edges(v, x) for x in G2_nbh)
714
+ if any(
715
+ u_nbr_edges != v_nbr_edges
716
+ for u_nbr_edges, v_nbr_edges in zip(u_nbrs_edges, v_nbrs_edges)
717
+ ):
718
+ return True
719
+
720
+ if len(T1.intersection(G1_nbh)) != len(T2.intersection(G2_nbh)):
721
+ return True
722
+ if len(T1_tilde.intersection(G1_nbh)) != len(T2_tilde.intersection(G2_nbh)):
723
+ return True
724
+ if G1.is_directed() and len(T1_in.intersection(G1_nbh)) != len(
725
+ T2_in.intersection(G2_nbh)
726
+ ):
727
+ return True
728
+
729
+ if not G1.is_directed():
730
+ return False
731
+
732
+ for label, G1_pred in u_labels_predecessors.items():
733
+ G2_pred = v_labels_predecessors[label]
734
+
735
+ if G1.is_multigraph():
736
+ # Check for every neighbor in the neighborhood, if u-nbr1 has same edges as v-nbr2
737
+ u_pred_edges = sorted(G1.number_of_edges(u, x) for x in G1_pred)
738
+ v_pred_edges = sorted(G2.number_of_edges(v, x) for x in G2_pred)
739
+ if any(
740
+ u_nbr_edges != v_nbr_edges
741
+ for u_nbr_edges, v_nbr_edges in zip(u_pred_edges, v_pred_edges)
742
+ ):
743
+ return True
744
+
745
+ if len(T1.intersection(G1_pred)) != len(T2.intersection(G2_pred)):
746
+ return True
747
+ if len(T1_tilde.intersection(G1_pred)) != len(T2_tilde.intersection(G2_pred)):
748
+ return True
749
+ if len(T1_in.intersection(G1_pred)) != len(T2_in.intersection(G2_pred)):
750
+ return True
751
+
752
+ return False
753
+
754
+
755
+ def _consistent_PT(u, v, graph_params, state_params):
756
+ """Checks the consistency of extending the mapping using the current node pair.
757
+
758
+ Parameters
759
+ ----------
760
+ u, v: Graph node
761
+ The two candidate nodes being examined.
762
+
763
+ graph_params: namedtuple
764
+ Contains all the Graph-related parameters:
765
+
766
+ G1,G2: NetworkX Graph or MultiGraph instances.
767
+ The two graphs to check for isomorphism or monomorphism
768
+
769
+ G1_labels,G2_labels: dict
770
+ The label of every node in G1 and G2 respectively
771
+
772
+ state_params: namedtuple
773
+ Contains all the State-related parameters:
774
+
775
+ mapping: dict
776
+ The mapping as extended so far. Maps nodes of G1 to nodes of G2
777
+
778
+ reverse_mapping: dict
779
+ The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
780
+ It's basically "mapping" reversed
781
+
782
+ T1, T2: set
783
+ Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
784
+ that are not in the mapping, but are neighbors of nodes that are.
785
+
786
+ T1_out, T2_out: set
787
+ Ti_out contains all the nodes from Gi, that are neither in the mapping
788
+ nor in Ti
789
+
790
+ Returns
791
+ -------
792
+ True if the pair passes all the consistency checks successfully. False otherwise.
793
+ """
794
+ G1, G2 = graph_params.G1, graph_params.G2
795
+ mapping, reverse_mapping = state_params.mapping, state_params.reverse_mapping
796
+
797
+ for neighbor in G1[u]:
798
+ if neighbor in mapping:
799
+ if G1.number_of_edges(u, neighbor) != G2.number_of_edges(
800
+ v, mapping[neighbor]
801
+ ):
802
+ return False
803
+
804
+ for neighbor in G2[v]:
805
+ if neighbor in reverse_mapping:
806
+ if G1.number_of_edges(u, reverse_mapping[neighbor]) != G2.number_of_edges(
807
+ v, neighbor
808
+ ):
809
+ return False
810
+
811
+ if not G1.is_directed():
812
+ return True
813
+
814
+ for predecessor in G1.pred[u]:
815
+ if predecessor in mapping:
816
+ if G1.number_of_edges(predecessor, u) != G2.number_of_edges(
817
+ mapping[predecessor], v
818
+ ):
819
+ return False
820
+
821
+ for predecessor in G2.pred[v]:
822
+ if predecessor in reverse_mapping:
823
+ if G1.number_of_edges(
824
+ reverse_mapping[predecessor], u
825
+ ) != G2.number_of_edges(predecessor, v):
826
+ return False
827
+
828
+ return True
829
+
830
+
831
+ def _update_Tinout(new_node1, new_node2, graph_params, state_params):
832
+ """Updates the Ti/Ti_out (i=1,2) when a new node pair u-v is added to the mapping.
833
+
834
+ Notes
835
+ -----
836
+ This function should be called right after the feasibility checks are passed,
837
+ and node1 is mapped to node2. The purpose of this function is to avoid brute
838
+ force computing of Ti/Ti_out by iterating over all nodes of the graph and
839
+ checking which nodes satisfy the necessary conditions. Instead, in every step
840
+ of the algorithm we focus exclusively on the two nodes that are being added
841
+ to the mapping, incrementally updating Ti/Ti_out.
842
+
843
+ Parameters
844
+ ----------
845
+ new_node1, new_node2: Graph node
846
+ The two new nodes, added to the mapping.
847
+
848
+ graph_params: namedtuple
849
+ Contains all the Graph-related parameters:
850
+
851
+ G1,G2: NetworkX Graph or MultiGraph instances.
852
+ The two graphs to check for isomorphism or monomorphism
853
+
854
+ G1_labels,G2_labels: dict
855
+ The label of every node in G1 and G2 respectively
856
+
857
+ state_params: namedtuple
858
+ Contains all the State-related parameters:
859
+
860
+ mapping: dict
861
+ The mapping as extended so far. Maps nodes of G1 to nodes of G2
862
+
863
+ reverse_mapping: dict
864
+ The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
865
+ It's basically "mapping" reversed
866
+
867
+ T1, T2: set
868
+ Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
869
+ that are not in the mapping, but are neighbors of nodes that are.
870
+
871
+ T1_tilde, T2_tilde: set
872
+ Ti_out contains all the nodes from Gi, that are neither in the mapping nor in Ti
873
+ """
874
+ G1, G2, _, _, _, _, _ = graph_params
875
+ (
876
+ mapping,
877
+ reverse_mapping,
878
+ T1,
879
+ T1_in,
880
+ T1_tilde,
881
+ T1_tilde_in,
882
+ T2,
883
+ T2_in,
884
+ T2_tilde,
885
+ T2_tilde_in,
886
+ ) = state_params
887
+
888
+ uncovered_successors_G1 = {succ for succ in G1[new_node1] if succ not in mapping}
889
+ uncovered_successors_G2 = {
890
+ succ for succ in G2[new_node2] if succ not in reverse_mapping
891
+ }
892
+
893
+ # Add the uncovered neighbors of node1 and node2 in T1 and T2 respectively
894
+ T1.update(uncovered_successors_G1)
895
+ T2.update(uncovered_successors_G2)
896
+ T1.discard(new_node1)
897
+ T2.discard(new_node2)
898
+
899
+ T1_tilde.difference_update(uncovered_successors_G1)
900
+ T2_tilde.difference_update(uncovered_successors_G2)
901
+ T1_tilde.discard(new_node1)
902
+ T2_tilde.discard(new_node2)
903
+
904
+ if not G1.is_directed():
905
+ return
906
+
907
+ uncovered_predecessors_G1 = {
908
+ pred for pred in G1.pred[new_node1] if pred not in mapping
909
+ }
910
+ uncovered_predecessors_G2 = {
911
+ pred for pred in G2.pred[new_node2] if pred not in reverse_mapping
912
+ }
913
+
914
+ T1_in.update(uncovered_predecessors_G1)
915
+ T2_in.update(uncovered_predecessors_G2)
916
+ T1_in.discard(new_node1)
917
+ T2_in.discard(new_node2)
918
+
919
+ T1_tilde.difference_update(uncovered_predecessors_G1)
920
+ T2_tilde.difference_update(uncovered_predecessors_G2)
921
+ T1_tilde.discard(new_node1)
922
+ T2_tilde.discard(new_node2)
923
+
924
+
925
+ def _restore_Tinout(popped_node1, popped_node2, graph_params, state_params):
926
+ """Restores the previous version of Ti/Ti_out when a node pair is deleted
927
+ from the mapping.
928
+
929
+ Parameters
930
+ ----------
931
+ popped_node1, popped_node2: Graph node
932
+ The two nodes deleted from the mapping.
933
+
934
+ graph_params: namedtuple
935
+ Contains all the Graph-related parameters:
936
+
937
+ G1,G2: NetworkX Graph or MultiGraph instances.
938
+ The two graphs to check for isomorphism or monomorphism
939
+
940
+ G1_labels,G2_labels: dict
941
+ The label of every node in G1 and G2 respectively
942
+
943
+ state_params: namedtuple
944
+ Contains all the State-related parameters:
945
+
946
+ mapping: dict
947
+ The mapping as extended so far. Maps nodes of G1 to nodes of G2
948
+
949
+ reverse_mapping: dict
950
+ The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
951
+ It's basically "mapping" reversed
952
+
953
+ T1, T2: set
954
+ Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
955
+ that are not in the mapping, but are neighbors of nodes that are.
956
+
957
+ T1_tilde, T2_tilde: set
958
+ Ti_out contains all the nodes from Gi, that are neither in the mapping
959
+ nor in Ti
960
+ """
961
+ # If the node we want to remove from the mapping, has at least one covered
962
+ # neighbor, add it to T1.
963
+ G1, G2, _, _, _, _, _ = graph_params
964
+ (
965
+ mapping,
966
+ reverse_mapping,
967
+ T1,
968
+ T1_in,
969
+ T1_tilde,
970
+ T1_tilde_in,
971
+ T2,
972
+ T2_in,
973
+ T2_tilde,
974
+ T2_tilde_in,
975
+ ) = state_params
976
+
977
+ is_added = False
978
+ for neighbor in G1[popped_node1]:
979
+ if neighbor in mapping:
980
+ # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
981
+ is_added = True
982
+ T1.add(popped_node1)
983
+ else:
984
+ # check if its neighbor has another connection with a covered node.
985
+ # If not, only then exclude it from T1
986
+ if any(nbr in mapping for nbr in G1[neighbor]):
987
+ continue
988
+ T1.discard(neighbor)
989
+ T1_tilde.add(neighbor)
990
+
991
+ # Case where the node is not present in neither the mapping nor T1.
992
+ # By definition, it should belong to T1_tilde
993
+ if not is_added:
994
+ T1_tilde.add(popped_node1)
995
+
996
+ is_added = False
997
+ for neighbor in G2[popped_node2]:
998
+ if neighbor in reverse_mapping:
999
+ is_added = True
1000
+ T2.add(popped_node2)
1001
+ else:
1002
+ if any(nbr in reverse_mapping for nbr in G2[neighbor]):
1003
+ continue
1004
+ T2.discard(neighbor)
1005
+ T2_tilde.add(neighbor)
1006
+
1007
+ if not is_added:
1008
+ T2_tilde.add(popped_node2)
1009
+
1010
+
1011
+ def _restore_Tinout_Di(popped_node1, popped_node2, graph_params, state_params):
1012
+ # If the node we want to remove from the mapping, has at least one covered neighbor, add it to T1.
1013
+ G1, G2, _, _, _, _, _ = graph_params
1014
+ (
1015
+ mapping,
1016
+ reverse_mapping,
1017
+ T1,
1018
+ T1_in,
1019
+ T1_tilde,
1020
+ T1_tilde_in,
1021
+ T2,
1022
+ T2_in,
1023
+ T2_tilde,
1024
+ T2_tilde_in,
1025
+ ) = state_params
1026
+
1027
+ is_added = False
1028
+ for successor in G1[popped_node1]:
1029
+ if successor in mapping:
1030
+ # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
1031
+ is_added = True
1032
+ T1_in.add(popped_node1)
1033
+ else:
1034
+ # check if its neighbor has another connection with a covered node.
1035
+ # If not, only then exclude it from T1
1036
+ if not any(pred in mapping for pred in G1.pred[successor]):
1037
+ T1.discard(successor)
1038
+
1039
+ if not any(succ in mapping for succ in G1[successor]):
1040
+ T1_in.discard(successor)
1041
+
1042
+ if successor not in T1:
1043
+ if successor not in T1_in:
1044
+ T1_tilde.add(successor)
1045
+
1046
+ for predecessor in G1.pred[popped_node1]:
1047
+ if predecessor in mapping:
1048
+ # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
1049
+ is_added = True
1050
+ T1.add(popped_node1)
1051
+ else:
1052
+ # check if its neighbor has another connection with a covered node.
1053
+ # If not, only then exclude it from T1
1054
+ if not any(pred in mapping for pred in G1.pred[predecessor]):
1055
+ T1.discard(predecessor)
1056
+
1057
+ if not any(succ in mapping for succ in G1[predecessor]):
1058
+ T1_in.discard(predecessor)
1059
+
1060
+ if not (predecessor in T1 or predecessor in T1_in):
1061
+ T1_tilde.add(predecessor)
1062
+
1063
+ # Case where the node is not present in neither the mapping nor T1.
1064
+ # By definition it should belong to T1_tilde
1065
+ if not is_added:
1066
+ T1_tilde.add(popped_node1)
1067
+
1068
+ is_added = False
1069
+ for successor in G2[popped_node2]:
1070
+ if successor in reverse_mapping:
1071
+ is_added = True
1072
+ T2_in.add(popped_node2)
1073
+ else:
1074
+ if not any(pred in reverse_mapping for pred in G2.pred[successor]):
1075
+ T2.discard(successor)
1076
+
1077
+ if not any(succ in reverse_mapping for succ in G2[successor]):
1078
+ T2_in.discard(successor)
1079
+
1080
+ if successor not in T2:
1081
+ if successor not in T2_in:
1082
+ T2_tilde.add(successor)
1083
+
1084
+ for predecessor in G2.pred[popped_node2]:
1085
+ if predecessor in reverse_mapping:
1086
+ # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
1087
+ is_added = True
1088
+ T2.add(popped_node2)
1089
+ else:
1090
+ # check if its neighbor has another connection with a covered node.
1091
+ # If not, only then exclude it from T1
1092
+ if not any(pred in reverse_mapping for pred in G2.pred[predecessor]):
1093
+ T2.discard(predecessor)
1094
+
1095
+ if not any(succ in reverse_mapping for succ in G2[predecessor]):
1096
+ T2_in.discard(predecessor)
1097
+
1098
+ if not (predecessor in T2 or predecessor in T2_in):
1099
+ T2_tilde.add(predecessor)
1100
+
1101
+ if not is_added:
1102
+ T2_tilde.add(popped_node2)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/vf2userfunc.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module to simplify the specification of user-defined equality functions for
3
+ node and edge attributes during isomorphism checks.
4
+
5
+ During the construction of an isomorphism, the algorithm considers two
6
+ candidate nodes n1 in G1 and n2 in G2. The graphs G1 and G2 are then
7
+ compared with respect to properties involving n1 and n2, and if the outcome
8
+ is good, then the candidate nodes are considered isomorphic. NetworkX
9
+ provides a simple mechanism for users to extend the comparisons to include
10
+ node and edge attributes.
11
+
12
+ Node attributes are handled by the node_match keyword. When considering
13
+ n1 and n2, the algorithm passes their node attribute dictionaries to
14
+ node_match, and if it returns False, then n1 and n2 cannot be
15
+ considered to be isomorphic.
16
+
17
+ Edge attributes are handled by the edge_match keyword. When considering
18
+ n1 and n2, the algorithm must verify that outgoing edges from n1 are
19
+ commensurate with the outgoing edges for n2. If the graph is directed,
20
+ then a similar check is also performed for incoming edges.
21
+
22
+ Focusing only on outgoing edges, we consider pairs of nodes (n1, v1) from
23
+ G1 and (n2, v2) from G2. For graphs and digraphs, there is only one edge
24
+ between (n1, v1) and only one edge between (n2, v2). Those edge attribute
25
+ dictionaries are passed to edge_match, and if it returns False, then
26
+ n1 and n2 cannot be considered isomorphic. For multigraphs and
27
+ multidigraphs, there can be multiple edges between (n1, v1) and also
28
+ multiple edges between (n2, v2). Now, there must exist an isomorphism
29
+ from "all the edges between (n1, v1)" to "all the edges between (n2, v2)".
30
+ So, all of the edge attribute dictionaries are passed to edge_match, and
31
+ it must determine if there is an isomorphism between the two sets of edges.
32
+ """
33
+
34
+ from . import isomorphvf2 as vf2
35
+
36
+ __all__ = ["GraphMatcher", "DiGraphMatcher", "MultiGraphMatcher", "MultiDiGraphMatcher"]
37
+
38
+
39
+ def _semantic_feasibility(self, G1_node, G2_node):
40
+ """Returns True if mapping G1_node to G2_node is semantically feasible."""
41
+ # Make sure the nodes match
42
+ if self.node_match is not None:
43
+ nm = self.node_match(self.G1.nodes[G1_node], self.G2.nodes[G2_node])
44
+ if not nm:
45
+ return False
46
+
47
+ # Make sure the edges match
48
+ if self.edge_match is not None:
49
+ # Cached lookups
50
+ G1nbrs = self.G1_adj[G1_node]
51
+ G2nbrs = self.G2_adj[G2_node]
52
+ core_1 = self.core_1
53
+ edge_match = self.edge_match
54
+
55
+ for neighbor in G1nbrs:
56
+ # G1_node is not in core_1, so we must handle R_self separately
57
+ if neighbor == G1_node:
58
+ if G2_node in G2nbrs and not edge_match(
59
+ G1nbrs[G1_node], G2nbrs[G2_node]
60
+ ):
61
+ return False
62
+ elif neighbor in core_1:
63
+ G2_nbr = core_1[neighbor]
64
+ if G2_nbr in G2nbrs and not edge_match(
65
+ G1nbrs[neighbor], G2nbrs[G2_nbr]
66
+ ):
67
+ return False
68
+ # syntactic check has already verified that neighbors are symmetric
69
+
70
+ return True
71
+
72
+
73
+ class GraphMatcher(vf2.GraphMatcher):
74
+ """VF2 isomorphism checker for undirected graphs."""
75
+
76
+ def __init__(self, G1, G2, node_match=None, edge_match=None):
77
+ """Initialize graph matcher.
78
+
79
+ Parameters
80
+ ----------
81
+ G1, G2: graph
82
+ The graphs to be tested.
83
+
84
+ node_match: callable
85
+ A function that returns True iff node n1 in G1 and n2 in G2
86
+ should be considered equal during the isomorphism test. The
87
+ function will be called like::
88
+
89
+ node_match(G1.nodes[n1], G2.nodes[n2])
90
+
91
+ That is, the function will receive the node attribute dictionaries
92
+ of the nodes under consideration. If None, then no attributes are
93
+ considered when testing for an isomorphism.
94
+
95
+ edge_match: callable
96
+ A function that returns True iff the edge attribute dictionary for
97
+ the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be
98
+ considered equal during the isomorphism test. The function will be
99
+ called like::
100
+
101
+ edge_match(G1[u1][v1], G2[u2][v2])
102
+
103
+ That is, the function will receive the edge attribute dictionaries
104
+ of the edges under consideration. If None, then no attributes are
105
+ considered when testing for an isomorphism.
106
+
107
+ """
108
+ vf2.GraphMatcher.__init__(self, G1, G2)
109
+
110
+ self.node_match = node_match
111
+ self.edge_match = edge_match
112
+
113
+ # These will be modified during checks to minimize code repeat.
114
+ self.G1_adj = self.G1.adj
115
+ self.G2_adj = self.G2.adj
116
+
117
+ semantic_feasibility = _semantic_feasibility
118
+
119
+
120
+ class DiGraphMatcher(vf2.DiGraphMatcher):
121
+ """VF2 isomorphism checker for directed graphs."""
122
+
123
+ def __init__(self, G1, G2, node_match=None, edge_match=None):
124
+ """Initialize graph matcher.
125
+
126
+ Parameters
127
+ ----------
128
+ G1, G2 : graph
129
+ The graphs to be tested.
130
+
131
+ node_match : callable
132
+ A function that returns True iff node n1 in G1 and n2 in G2
133
+ should be considered equal during the isomorphism test. The
134
+ function will be called like::
135
+
136
+ node_match(G1.nodes[n1], G2.nodes[n2])
137
+
138
+ That is, the function will receive the node attribute dictionaries
139
+ of the nodes under consideration. If None, then no attributes are
140
+ considered when testing for an isomorphism.
141
+
142
+ edge_match : callable
143
+ A function that returns True iff the edge attribute dictionary for
144
+ the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be
145
+ considered equal during the isomorphism test. The function will be
146
+ called like::
147
+
148
+ edge_match(G1[u1][v1], G2[u2][v2])
149
+
150
+ That is, the function will receive the edge attribute dictionaries
151
+ of the edges under consideration. If None, then no attributes are
152
+ considered when testing for an isomorphism.
153
+
154
+ """
155
+ vf2.DiGraphMatcher.__init__(self, G1, G2)
156
+
157
+ self.node_match = node_match
158
+ self.edge_match = edge_match
159
+
160
+ # These will be modified during checks to minimize code repeat.
161
+ self.G1_adj = self.G1.adj
162
+ self.G2_adj = self.G2.adj
163
+
164
+ def semantic_feasibility(self, G1_node, G2_node):
165
+ """Returns True if mapping G1_node to G2_node is semantically feasible."""
166
+
167
+ # Test node_match and also test edge_match on successors
168
+ feasible = _semantic_feasibility(self, G1_node, G2_node)
169
+ if not feasible:
170
+ return False
171
+
172
+ # Test edge_match on predecessors
173
+ self.G1_adj = self.G1.pred
174
+ self.G2_adj = self.G2.pred
175
+ feasible = _semantic_feasibility(self, G1_node, G2_node)
176
+ self.G1_adj = self.G1.adj
177
+ self.G2_adj = self.G2.adj
178
+
179
+ return feasible
180
+
181
+
182
+ # The "semantics" of edge_match are different for multi(di)graphs, but
183
+ # the implementation is the same. So, technically we do not need to
184
+ # provide "multi" versions, but we do so to match NetworkX's base classes.
185
+
186
+
187
+ class MultiGraphMatcher(GraphMatcher):
188
+ """VF2 isomorphism checker for undirected multigraphs."""
189
+
190
+
191
+ class MultiDiGraphMatcher(DiGraphMatcher):
192
+ """VF2 isomorphism checker for directed multigraphs."""
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from networkx.algorithms.link_analysis.hits_alg import *
2
+ from networkx.algorithms.link_analysis.pagerank_alg import *
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/hits_alg.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hubs and authorities analysis of graph structure."""
2
+
3
+ import networkx as nx
4
+
5
+ __all__ = ["hits"]
6
+
7
+
8
+ @nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}})
9
+ def hits(G, max_iter=100, tol=1.0e-8, nstart=None, normalized=True):
10
+ """Returns HITS hubs and authorities values for nodes.
11
+
12
+ The HITS algorithm computes two numbers for a node.
13
+ Authorities estimates the node value based on the incoming links.
14
+ Hubs estimates the node value based on outgoing links.
15
+
16
+ Parameters
17
+ ----------
18
+ G : graph
19
+ A NetworkX graph
20
+
21
+ max_iter : integer, optional
22
+ Maximum number of iterations in power method.
23
+
24
+ tol : float, optional
25
+ Error tolerance used to check convergence in power method iteration.
26
+
27
+ nstart : dictionary, optional
28
+ Starting value of each node for power method iteration.
29
+
30
+ normalized : bool (default=True)
31
+ Normalize results by the sum of all of the values.
32
+
33
+ Returns
34
+ -------
35
+ (hubs,authorities) : two-tuple of dictionaries
36
+ Two dictionaries keyed by node containing the hub and authority
37
+ values.
38
+
39
+ Raises
40
+ ------
41
+ PowerIterationFailedConvergence
42
+ If the algorithm fails to converge to the specified tolerance
43
+ within the specified number of iterations of the power iteration
44
+ method.
45
+
46
+ Examples
47
+ --------
48
+ >>> G = nx.path_graph(4)
49
+ >>> h, a = nx.hits(G)
50
+
51
+ Notes
52
+ -----
53
+ The eigenvector calculation is done by the power iteration method
54
+ and has no guarantee of convergence. The iteration will stop
55
+ after max_iter iterations or an error tolerance of
56
+ number_of_nodes(G)*tol has been reached.
57
+
58
+ The HITS algorithm was designed for directed graphs but this
59
+ algorithm does not check if the input graph is directed and will
60
+ execute on undirected graphs.
61
+
62
+ References
63
+ ----------
64
+ .. [1] A. Langville and C. Meyer,
65
+ "A survey of eigenvector methods of web information retrieval."
66
+ https://epubs.siam.org/doi/epdf/10.1137/S0036144503424786
67
+ .. [2] Jon Kleinberg,
68
+ Authoritative sources in a hyperlinked environment
69
+ Journal of the ACM 46 (5): 604-32, 1999.
70
+ https://www.cs.cornell.edu/home/kleinber/auth.pdf
71
+ doi:10.1145/324133.324140.
72
+ """
73
+ import numpy as np
74
+ import scipy as sp
75
+
76
+ if len(G) == 0:
77
+ return {}, {}
78
+ A = nx.adjacency_matrix(G, nodelist=list(G), dtype=float)
79
+
80
+ if nstart is not None:
81
+ nstart = np.array(list(nstart.values()))
82
+ if max_iter <= 0:
83
+ raise nx.PowerIterationFailedConvergence(max_iter)
84
+ try:
85
+ _, _, vt = sp.sparse.linalg.svds(A, k=1, v0=nstart, maxiter=max_iter, tol=tol)
86
+ except sp.sparse.linalg.ArpackNoConvergence as exc:
87
+ raise nx.PowerIterationFailedConvergence(max_iter) from exc
88
+
89
+ a = vt.flatten().real
90
+ h = A @ a
91
+ if normalized:
92
+ h /= h.sum()
93
+ a /= a.sum()
94
+ hubs = dict(zip(G, map(float, h)))
95
+ authorities = dict(zip(G, map(float, a)))
96
+ return hubs, authorities
97
+
98
+
99
+ def _hits_python(G, max_iter=100, tol=1.0e-8, nstart=None, normalized=True):
100
+ if isinstance(G, nx.MultiGraph | nx.MultiDiGraph):
101
+ raise Exception("hits() not defined for graphs with multiedges.")
102
+ if len(G) == 0:
103
+ return {}, {}
104
+ # choose fixed starting vector if not given
105
+ if nstart is None:
106
+ h = dict.fromkeys(G, 1.0 / G.number_of_nodes())
107
+ else:
108
+ h = nstart
109
+ # normalize starting vector
110
+ s = 1.0 / sum(h.values())
111
+ for k in h:
112
+ h[k] *= s
113
+ for _ in range(max_iter): # power iteration: make up to max_iter iterations
114
+ hlast = h
115
+ h = dict.fromkeys(hlast.keys(), 0)
116
+ a = dict.fromkeys(hlast.keys(), 0)
117
+ # this "matrix multiply" looks odd because it is
118
+ # doing a left multiply a^T=hlast^T*G
119
+ for n in h:
120
+ for nbr in G[n]:
121
+ a[nbr] += hlast[n] * G[n][nbr].get("weight", 1)
122
+ # now multiply h=Ga
123
+ for n in h:
124
+ for nbr in G[n]:
125
+ h[n] += a[nbr] * G[n][nbr].get("weight", 1)
126
+ # normalize vector
127
+ s = 1.0 / max(h.values())
128
+ for n in h:
129
+ h[n] *= s
130
+ # normalize vector
131
+ s = 1.0 / max(a.values())
132
+ for n in a:
133
+ a[n] *= s
134
+ # check convergence, l1 norm
135
+ err = sum(abs(h[n] - hlast[n]) for n in h)
136
+ if err < tol:
137
+ break
138
+ else:
139
+ raise nx.PowerIterationFailedConvergence(max_iter)
140
+ if normalized:
141
+ s = 1.0 / sum(a.values())
142
+ for n in a:
143
+ a[n] *= s
144
+ s = 1.0 / sum(h.values())
145
+ for n in h:
146
+ h[n] *= s
147
+ return h, a
148
+
149
+
150
+ def _hits_numpy(G, normalized=True):
151
+ """Returns HITS hubs and authorities values for nodes.
152
+
153
+ The HITS algorithm computes two numbers for a node.
154
+ Authorities estimates the node value based on the incoming links.
155
+ Hubs estimates the node value based on outgoing links.
156
+
157
+ Parameters
158
+ ----------
159
+ G : graph
160
+ A NetworkX graph
161
+
162
+ normalized : bool (default=True)
163
+ Normalize results by the sum of all of the values.
164
+
165
+ Returns
166
+ -------
167
+ (hubs,authorities) : two-tuple of dictionaries
168
+ Two dictionaries keyed by node containing the hub and authority
169
+ values.
170
+
171
+ Examples
172
+ --------
173
+ >>> G = nx.path_graph(4)
174
+
175
+ The `hubs` and `authorities` are given by the eigenvectors corresponding to the
176
+ maximum eigenvalues of the hubs_matrix and the authority_matrix, respectively.
177
+
178
+ The ``hubs`` and ``authority`` matrices are computed from the adjacency
179
+ matrix:
180
+
181
+ >>> adj_ary = nx.to_numpy_array(G)
182
+ >>> hubs_matrix = adj_ary @ adj_ary.T
183
+ >>> authority_matrix = adj_ary.T @ adj_ary
184
+
185
+ `_hits_numpy` maps the eigenvector corresponding to the maximum eigenvalue
186
+ of the respective matrices to the nodes in `G`:
187
+
188
+ >>> from networkx.algorithms.link_analysis.hits_alg import _hits_numpy
189
+ >>> hubs, authority = _hits_numpy(G)
190
+
191
+ Notes
192
+ -----
193
+ The eigenvector calculation uses NumPy's interface to LAPACK.
194
+
195
+ The HITS algorithm was designed for directed graphs but this
196
+ algorithm does not check if the input graph is directed and will
197
+ execute on undirected graphs.
198
+
199
+ References
200
+ ----------
201
+ .. [1] A. Langville and C. Meyer,
202
+ "A survey of eigenvector methods of web information retrieval."
203
+ http://citeseer.ist.psu.edu/713792.html
204
+ .. [2] Jon Kleinberg,
205
+ Authoritative sources in a hyperlinked environment
206
+ Journal of the ACM 46 (5): 604-32, 1999.
207
+ doi:10.1145/324133.324140.
208
+ http://www.cs.cornell.edu/home/kleinber/auth.pdf.
209
+ """
210
+ import numpy as np
211
+
212
+ if len(G) == 0:
213
+ return {}, {}
214
+ adj_ary = nx.to_numpy_array(G)
215
+ # Hub matrix
216
+ H = adj_ary @ adj_ary.T
217
+ e, ev = np.linalg.eig(H)
218
+ h = ev[:, np.argmax(e)] # eigenvector corresponding to the maximum eigenvalue
219
+ # Authority matrix
220
+ A = adj_ary.T @ adj_ary
221
+ e, ev = np.linalg.eig(A)
222
+ a = ev[:, np.argmax(e)] # eigenvector corresponding to the maximum eigenvalue
223
+ if normalized:
224
+ h /= h.sum()
225
+ a /= a.sum()
226
+ else:
227
+ h /= h.max()
228
+ a /= a.max()
229
+ hubs = dict(zip(G, map(float, h)))
230
+ authorities = dict(zip(G, map(float, a)))
231
+ return hubs, authorities
232
+
233
+
234
+ def _hits_scipy(G, max_iter=100, tol=1.0e-6, nstart=None, normalized=True):
235
+ """Returns HITS hubs and authorities values for nodes.
236
+
237
+
238
+ The HITS algorithm computes two numbers for a node.
239
+ Authorities estimates the node value based on the incoming links.
240
+ Hubs estimates the node value based on outgoing links.
241
+
242
+ Parameters
243
+ ----------
244
+ G : graph
245
+ A NetworkX graph
246
+
247
+ max_iter : integer, optional
248
+ Maximum number of iterations in power method.
249
+
250
+ tol : float, optional
251
+ Error tolerance used to check convergence in power method iteration.
252
+
253
+ nstart : dictionary, optional
254
+ Starting value of each node for power method iteration.
255
+
256
+ normalized : bool (default=True)
257
+ Normalize results by the sum of all of the values.
258
+
259
+ Returns
260
+ -------
261
+ (hubs,authorities) : two-tuple of dictionaries
262
+ Two dictionaries keyed by node containing the hub and authority
263
+ values.
264
+
265
+ Examples
266
+ --------
267
+ >>> from networkx.algorithms.link_analysis.hits_alg import _hits_scipy
268
+ >>> G = nx.path_graph(4)
269
+ >>> h, a = _hits_scipy(G)
270
+
271
+ Notes
272
+ -----
273
+ This implementation uses SciPy sparse matrices.
274
+
275
+ The eigenvector calculation is done by the power iteration method
276
+ and has no guarantee of convergence. The iteration will stop
277
+ after max_iter iterations or an error tolerance of
278
+ number_of_nodes(G)*tol has been reached.
279
+
280
+ The HITS algorithm was designed for directed graphs but this
281
+ algorithm does not check if the input graph is directed and will
282
+ execute on undirected graphs.
283
+
284
+ Raises
285
+ ------
286
+ PowerIterationFailedConvergence
287
+ If the algorithm fails to converge to the specified tolerance
288
+ within the specified number of iterations of the power iteration
289
+ method.
290
+
291
+ References
292
+ ----------
293
+ .. [1] A. Langville and C. Meyer,
294
+ "A survey of eigenvector methods of web information retrieval."
295
+ http://citeseer.ist.psu.edu/713792.html
296
+ .. [2] Jon Kleinberg,
297
+ Authoritative sources in a hyperlinked environment
298
+ Journal of the ACM 46 (5): 604-632, 1999.
299
+ doi:10.1145/324133.324140.
300
+ http://www.cs.cornell.edu/home/kleinber/auth.pdf.
301
+ """
302
+ import numpy as np
303
+
304
+ if len(G) == 0:
305
+ return {}, {}
306
+ A = nx.to_scipy_sparse_array(G, nodelist=list(G))
307
+ (n, _) = A.shape # should be square
308
+ ATA = A.T @ A # authority matrix
309
+ # choose fixed starting vector if not given
310
+ if nstart is None:
311
+ x = np.ones((n, 1)) / n
312
+ else:
313
+ x = np.array([nstart.get(n, 0) for n in list(G)], dtype=float)
314
+ x /= x.sum()
315
+
316
+ # power iteration on authority matrix
317
+ i = 0
318
+ while True:
319
+ xlast = x
320
+ x = ATA @ x
321
+ x /= x.max()
322
+ # check convergence, l1 norm
323
+ err = np.absolute(x - xlast).sum()
324
+ if err < tol:
325
+ break
326
+ if i > max_iter:
327
+ raise nx.PowerIterationFailedConvergence(max_iter)
328
+ i += 1
329
+
330
+ a = x.flatten()
331
+ h = A @ a
332
+ if normalized:
333
+ h /= h.sum()
334
+ a /= a.sum()
335
+ hubs = dict(zip(G, map(float, h)))
336
+ authorities = dict(zip(G, map(float, a)))
337
+ return hubs, authorities
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/pagerank_alg.py ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PageRank analysis of graph structure."""
2
+
3
+ import networkx as nx
4
+
5
+ __all__ = ["pagerank", "google_matrix"]
6
+
7
+
8
+ @nx._dispatchable(edge_attrs="weight")
9
+ def pagerank(
10
+ G,
11
+ alpha=0.85,
12
+ personalization=None,
13
+ max_iter=100,
14
+ tol=1.0e-6,
15
+ nstart=None,
16
+ weight="weight",
17
+ dangling=None,
18
+ ):
19
+ """Returns the PageRank of the nodes in the graph.
20
+
21
+ PageRank computes a ranking of the nodes in the graph G based on
22
+ the structure of the incoming links. It was originally designed as
23
+ an algorithm to rank web pages.
24
+
25
+ Parameters
26
+ ----------
27
+ G : graph
28
+ A NetworkX graph. Undirected graphs will be converted to a directed
29
+ graph with two directed edges for each undirected edge.
30
+
31
+ alpha : float, optional
32
+ Damping parameter for PageRank, default=0.85.
33
+
34
+ personalization: dict, optional
35
+ The "personalization vector" consisting of a dictionary with a
36
+ key some subset of graph nodes and personalization value each of those.
37
+ At least one personalization value must be non-zero.
38
+ If not specified, a nodes personalization value will be zero.
39
+ By default, a uniform distribution is used.
40
+
41
+ max_iter : integer, optional
42
+ Maximum number of iterations in power method eigenvalue solver.
43
+
44
+ tol : float, optional
45
+ Error tolerance used to check convergence in power method solver.
46
+ The iteration will stop after a tolerance of ``len(G) * tol`` is reached.
47
+
48
+ nstart : dictionary, optional
49
+ Starting value of PageRank iteration for each node.
50
+
51
+ weight : key, optional
52
+ Edge data key to use as weight. If None weights are set to 1.
53
+
54
+ dangling: dict, optional
55
+ The outedges to be assigned to any "dangling" nodes, i.e., nodes without
56
+ any outedges. The dict key is the node the outedge points to and the dict
57
+ value is the weight of that outedge. By default, dangling nodes are given
58
+ outedges according to the personalization vector (uniform if not
59
+ specified). This must be selected to result in an irreducible transition
60
+ matrix (see notes under google_matrix). It may be common to have the
61
+ dangling dict to be the same as the personalization dict.
62
+
63
+
64
+ Returns
65
+ -------
66
+ pagerank : dictionary
67
+ Dictionary of nodes with PageRank as value
68
+
69
+ Examples
70
+ --------
71
+ >>> G = nx.DiGraph(nx.path_graph(4))
72
+ >>> pr = nx.pagerank(G, alpha=0.9)
73
+
74
+ Notes
75
+ -----
76
+ The eigenvector calculation is done by the power iteration method
77
+ and has no guarantee of convergence. The iteration will stop after
78
+ an error tolerance of ``len(G) * tol`` has been reached. If the
79
+ number of iterations exceed `max_iter`, a
80
+ :exc:`networkx.exception.PowerIterationFailedConvergence` exception
81
+ is raised.
82
+
83
+ The PageRank algorithm was designed for directed graphs but this
84
+ algorithm does not check if the input graph is directed and will
85
+ execute on undirected graphs by converting each edge in the
86
+ directed graph to two edges.
87
+
88
+ See Also
89
+ --------
90
+ google_matrix
91
+ :func:`~networkx.algorithms.bipartite.link_analysis.birank`
92
+
93
+ Raises
94
+ ------
95
+ PowerIterationFailedConvergence
96
+ If the algorithm fails to converge to the specified tolerance
97
+ within the specified number of iterations of the power iteration
98
+ method.
99
+
100
+ References
101
+ ----------
102
+ .. [1] A. Langville and C. Meyer,
103
+ "A survey of eigenvector methods of web information retrieval."
104
+ http://citeseer.ist.psu.edu/713792.html
105
+ .. [2] Page, Lawrence; Brin, Sergey; Motwani, Rajeev and Winograd, Terry,
106
+ The PageRank citation ranking: Bringing order to the Web. 1999
107
+ http://dbpubs.stanford.edu:8090/pub/showDoc.Fulltext?lang=en&doc=1999-66&format=pdf
108
+
109
+ """
110
+ return _pagerank_scipy(
111
+ G, alpha, personalization, max_iter, tol, nstart, weight, dangling
112
+ )
113
+
114
+
115
+ def _pagerank_python(
116
+ G,
117
+ alpha=0.85,
118
+ personalization=None,
119
+ max_iter=100,
120
+ tol=1.0e-6,
121
+ nstart=None,
122
+ weight="weight",
123
+ dangling=None,
124
+ ):
125
+ if len(G) == 0:
126
+ return {}
127
+
128
+ D = G.to_directed()
129
+
130
+ # Create a copy in (right) stochastic form
131
+ W = nx.stochastic_graph(D, weight=weight)
132
+ N = W.number_of_nodes()
133
+
134
+ # Choose fixed starting vector if not given
135
+ if nstart is None:
136
+ x = dict.fromkeys(W, 1.0 / N)
137
+ else:
138
+ # Normalized nstart vector
139
+ s = sum(nstart.values())
140
+ x = {k: v / s for k, v in nstart.items()}
141
+
142
+ if personalization is None:
143
+ # Assign uniform personalization vector if not given
144
+ p = dict.fromkeys(W, 1.0 / N)
145
+ else:
146
+ s = sum(personalization.values())
147
+ p = {k: v / s for k, v in personalization.items()}
148
+
149
+ if dangling is None:
150
+ # Use personalization vector if dangling vector not specified
151
+ dangling_weights = p
152
+ else:
153
+ s = sum(dangling.values())
154
+ dangling_weights = {k: v / s for k, v in dangling.items()}
155
+ dangling_nodes = [n for n in W if W.out_degree(n, weight=weight) == 0.0]
156
+
157
+ # power iteration: make up to max_iter iterations
158
+ for _ in range(max_iter):
159
+ xlast = x
160
+ x = dict.fromkeys(xlast.keys(), 0)
161
+ danglesum = alpha * sum(xlast[n] for n in dangling_nodes)
162
+ for n in x:
163
+ # this matrix multiply looks odd because it is
164
+ # doing a left multiply x^T=xlast^T*W
165
+ for _, nbr, wt in W.edges(n, data=weight):
166
+ x[nbr] += alpha * xlast[n] * wt
167
+ x[n] += danglesum * dangling_weights.get(n, 0) + (1.0 - alpha) * p.get(n, 0)
168
+ # check convergence, l1 norm
169
+ err = sum(abs(x[n] - xlast[n]) for n in x)
170
+ if err < N * tol:
171
+ return x
172
+ raise nx.PowerIterationFailedConvergence(max_iter)
173
+
174
+
175
+ @nx._dispatchable(edge_attrs="weight")
176
+ def google_matrix(
177
+ G, alpha=0.85, personalization=None, nodelist=None, weight="weight", dangling=None
178
+ ):
179
+ """Returns the Google matrix of the graph.
180
+
181
+ Parameters
182
+ ----------
183
+ G : graph
184
+ A NetworkX graph. Undirected graphs will be converted to a directed
185
+ graph with two directed edges for each undirected edge.
186
+
187
+ alpha : float
188
+ The damping factor.
189
+
190
+ personalization: dict, optional
191
+ The "personalization vector" consisting of a dictionary with a
192
+ key some subset of graph nodes and personalization value each of those.
193
+ At least one personalization value must be non-zero.
194
+ If not specified, a nodes personalization value will be zero.
195
+ By default, a uniform distribution is used.
196
+
197
+ nodelist : list, optional
198
+ The rows and columns are ordered according to the nodes in nodelist.
199
+ If nodelist is None, then the ordering is produced by G.nodes().
200
+
201
+ weight : key, optional
202
+ Edge data key to use as weight. If None weights are set to 1.
203
+
204
+ dangling: dict, optional
205
+ The outedges to be assigned to any "dangling" nodes, i.e., nodes without
206
+ any outedges. The dict key is the node the outedge points to and the dict
207
+ value is the weight of that outedge. By default, dangling nodes are given
208
+ outedges according to the personalization vector (uniform if not
209
+ specified) This must be selected to result in an irreducible transition
210
+ matrix (see notes below). It may be common to have the dangling dict to
211
+ be the same as the personalization dict.
212
+
213
+ Returns
214
+ -------
215
+ A : 2D NumPy ndarray
216
+ Google matrix of the graph
217
+
218
+ Notes
219
+ -----
220
+ The array returned represents the transition matrix that describes the
221
+ Markov chain used in PageRank. For PageRank to converge to a unique
222
+ solution (i.e., a unique stationary distribution in a Markov chain), the
223
+ transition matrix must be irreducible. In other words, it must be that
224
+ there exists a path between every pair of nodes in the graph, or else there
225
+ is the potential of "rank sinks."
226
+
227
+ This implementation works with Multi(Di)Graphs. For multigraphs the
228
+ weight between two nodes is set to be the sum of all edge weights
229
+ between those nodes.
230
+
231
+ See Also
232
+ --------
233
+ pagerank
234
+ """
235
+ import numpy as np
236
+
237
+ if nodelist is None:
238
+ nodelist = list(G)
239
+
240
+ A = nx.to_numpy_array(G, nodelist=nodelist, weight=weight)
241
+ N = len(G)
242
+ if N == 0:
243
+ return A
244
+
245
+ # Personalization vector
246
+ if personalization is None:
247
+ p = np.repeat(1.0 / N, N)
248
+ else:
249
+ p = np.array([personalization.get(n, 0) for n in nodelist], dtype=float)
250
+ if p.sum() == 0:
251
+ raise ZeroDivisionError
252
+ p /= p.sum()
253
+
254
+ # Dangling nodes
255
+ if dangling is None:
256
+ dangling_weights = p
257
+ else:
258
+ # Convert the dangling dictionary into an array in nodelist order
259
+ dangling_weights = np.array([dangling.get(n, 0) for n in nodelist], dtype=float)
260
+ dangling_weights /= dangling_weights.sum()
261
+ dangling_nodes = np.where(A.sum(axis=1) == 0)[0]
262
+
263
+ # Assign dangling_weights to any dangling nodes (nodes with no out links)
264
+ A[dangling_nodes] = dangling_weights
265
+
266
+ A /= A.sum(axis=1)[:, np.newaxis] # Normalize rows to sum to 1
267
+
268
+ return alpha * A + (1 - alpha) * p
269
+
270
+
271
+ def _pagerank_numpy(
272
+ G, alpha=0.85, personalization=None, weight="weight", dangling=None
273
+ ):
274
+ """Returns the PageRank of the nodes in the graph.
275
+
276
+ PageRank computes a ranking of the nodes in the graph G based on
277
+ the structure of the incoming links. It was originally designed as
278
+ an algorithm to rank web pages.
279
+
280
+ Parameters
281
+ ----------
282
+ G : graph
283
+ A NetworkX graph. Undirected graphs will be converted to a directed
284
+ graph with two directed edges for each undirected edge.
285
+
286
+ alpha : float, optional
287
+ Damping parameter for PageRank, default=0.85.
288
+
289
+ personalization: dict, optional
290
+ The "personalization vector" consisting of a dictionary with a
291
+ key some subset of graph nodes and personalization value each of those.
292
+ At least one personalization value must be non-zero.
293
+ If not specified, a nodes personalization value will be zero.
294
+ By default, a uniform distribution is used.
295
+
296
+ weight : key, optional
297
+ Edge data key to use as weight. If None weights are set to 1.
298
+
299
+ dangling: dict, optional
300
+ The outedges to be assigned to any "dangling" nodes, i.e., nodes without
301
+ any outedges. The dict key is the node the outedge points to and the dict
302
+ value is the weight of that outedge. By default, dangling nodes are given
303
+ outedges according to the personalization vector (uniform if not
304
+ specified) This must be selected to result in an irreducible transition
305
+ matrix (see notes under google_matrix). It may be common to have the
306
+ dangling dict to be the same as the personalization dict.
307
+
308
+ Returns
309
+ -------
310
+ pagerank : dictionary
311
+ Dictionary of nodes with PageRank as value.
312
+
313
+ Examples
314
+ --------
315
+ >>> from networkx.algorithms.link_analysis.pagerank_alg import _pagerank_numpy
316
+ >>> G = nx.DiGraph(nx.path_graph(4))
317
+ >>> pr = _pagerank_numpy(G, alpha=0.9)
318
+
319
+ Notes
320
+ -----
321
+ The eigenvector calculation uses NumPy's interface to the LAPACK
322
+ eigenvalue solvers. This will be the fastest and most accurate
323
+ for small graphs.
324
+
325
+ This implementation works with Multi(Di)Graphs. For multigraphs the
326
+ weight between two nodes is set to be the sum of all edge weights
327
+ between those nodes.
328
+
329
+ See Also
330
+ --------
331
+ pagerank, google_matrix
332
+
333
+ References
334
+ ----------
335
+ .. [1] A. Langville and C. Meyer,
336
+ "A survey of eigenvector methods of web information retrieval."
337
+ http://citeseer.ist.psu.edu/713792.html
338
+ .. [2] Page, Lawrence; Brin, Sergey; Motwani, Rajeev and Winograd, Terry,
339
+ The PageRank citation ranking: Bringing order to the Web. 1999
340
+ http://dbpubs.stanford.edu:8090/pub/showDoc.Fulltext?lang=en&doc=1999-66&format=pdf
341
+ """
342
+ import numpy as np
343
+
344
+ if len(G) == 0:
345
+ return {}
346
+ M = google_matrix(
347
+ G, alpha, personalization=personalization, weight=weight, dangling=dangling
348
+ )
349
+ # use numpy LAPACK solver
350
+ eigenvalues, eigenvectors = np.linalg.eig(M.T)
351
+ ind = np.argmax(eigenvalues)
352
+ # eigenvector of largest eigenvalue is at ind, normalized
353
+ largest = np.array(eigenvectors[:, ind]).flatten().real
354
+ norm = largest.sum()
355
+ return dict(zip(G, map(float, largest / norm)))
356
+
357
+
358
+ def _pagerank_scipy(
359
+ G,
360
+ alpha=0.85,
361
+ personalization=None,
362
+ max_iter=100,
363
+ tol=1.0e-6,
364
+ nstart=None,
365
+ weight="weight",
366
+ dangling=None,
367
+ ):
368
+ """Returns the PageRank of the nodes in the graph.
369
+
370
+ PageRank computes a ranking of the nodes in the graph G based on
371
+ the structure of the incoming links. It was originally designed as
372
+ an algorithm to rank web pages.
373
+
374
+ Parameters
375
+ ----------
376
+ G : graph
377
+ A NetworkX graph. Undirected graphs will be converted to a directed
378
+ graph with two directed edges for each undirected edge.
379
+
380
+ alpha : float, optional
381
+ Damping parameter for PageRank, default=0.85.
382
+
383
+ personalization: dict, optional
384
+ The "personalization vector" consisting of a dictionary with a
385
+ key some subset of graph nodes and personalization value each of those.
386
+ At least one personalization value must be non-zero.
387
+ If not specified, a nodes personalization value will be zero.
388
+ By default, a uniform distribution is used.
389
+
390
+ max_iter : integer, optional
391
+ Maximum number of iterations in power method eigenvalue solver.
392
+
393
+ tol : float, optional
394
+ Error tolerance used to check convergence in power method solver.
395
+ The iteration will stop after a tolerance of ``len(G) * tol`` is reached.
396
+
397
+ nstart : dictionary, optional
398
+ Starting value of PageRank iteration for each node.
399
+
400
+ weight : key, optional
401
+ Edge data key to use as weight. If None weights are set to 1.
402
+
403
+ dangling: dict, optional
404
+ The outedges to be assigned to any "dangling" nodes, i.e., nodes without
405
+ any outedges. The dict key is the node the outedge points to and the dict
406
+ value is the weight of that outedge. By default, dangling nodes are given
407
+ outedges according to the personalization vector (uniform if not
408
+ specified) This must be selected to result in an irreducible transition
409
+ matrix (see notes under google_matrix). It may be common to have the
410
+ dangling dict to be the same as the personalization dict.
411
+
412
+ Returns
413
+ -------
414
+ pagerank : dictionary
415
+ Dictionary of nodes with PageRank as value
416
+
417
+ Examples
418
+ --------
419
+ >>> from networkx.algorithms.link_analysis.pagerank_alg import _pagerank_scipy
420
+ >>> G = nx.DiGraph(nx.path_graph(4))
421
+ >>> pr = _pagerank_scipy(G, alpha=0.9)
422
+
423
+ Notes
424
+ -----
425
+ The eigenvector calculation uses power iteration with a SciPy
426
+ sparse matrix representation.
427
+
428
+ This implementation works with Multi(Di)Graphs. For multigraphs the
429
+ weight between two nodes is set to be the sum of all edge weights
430
+ between those nodes.
431
+
432
+ See Also
433
+ --------
434
+ pagerank
435
+
436
+ Raises
437
+ ------
438
+ PowerIterationFailedConvergence
439
+ If the algorithm fails to converge to the specified tolerance
440
+ within the specified number of iterations of the power iteration
441
+ method.
442
+
443
+ References
444
+ ----------
445
+ .. [1] A. Langville and C. Meyer,
446
+ "A survey of eigenvector methods of web information retrieval."
447
+ http://citeseer.ist.psu.edu/713792.html
448
+ .. [2] Page, Lawrence; Brin, Sergey; Motwani, Rajeev and Winograd, Terry,
449
+ The PageRank citation ranking: Bringing order to the Web. 1999
450
+ http://dbpubs.stanford.edu:8090/pub/showDoc.Fulltext?lang=en&doc=1999-66&format=pdf
451
+ """
452
+ import numpy as np
453
+ import scipy as sp
454
+
455
+ N = len(G)
456
+ if N == 0:
457
+ return {}
458
+
459
+ nodelist = list(G)
460
+ A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, dtype=float)
461
+ S = A.sum(axis=1)
462
+ S[S != 0] = 1.0 / S[S != 0]
463
+ Q = sp.sparse.dia_array((S.T, 0), shape=A.shape).tocsr()
464
+ A = Q @ A
465
+
466
+ # initial vector
467
+ if nstart is None:
468
+ x = np.repeat(1.0 / N, N)
469
+ else:
470
+ x = np.array([nstart.get(n, 0) for n in nodelist], dtype=float)
471
+ x /= x.sum()
472
+
473
+ # Personalization vector
474
+ if personalization is None:
475
+ p = np.repeat(1.0 / N, N)
476
+ else:
477
+ p = np.array([personalization.get(n, 0) for n in nodelist], dtype=float)
478
+ if p.sum() == 0:
479
+ raise ZeroDivisionError
480
+ p /= p.sum()
481
+ # Dangling nodes
482
+ if dangling is None:
483
+ dangling_weights = p
484
+ else:
485
+ # Convert the dangling dictionary into an array in nodelist order
486
+ dangling_weights = np.array([dangling.get(n, 0) for n in nodelist], dtype=float)
487
+ dangling_weights /= dangling_weights.sum()
488
+ is_dangling = np.where(S == 0)[0]
489
+
490
+ # power iteration: make up to max_iter iterations
491
+ for _ in range(max_iter):
492
+ xlast = x
493
+ x = alpha * (x @ A + sum(x[is_dangling]) * dangling_weights) + (1 - alpha) * p
494
+ # check convergence, l1 norm
495
+ err = np.absolute(x - xlast).sum()
496
+ if err < N * tol:
497
+ return dict(zip(nodelist, map(float, x)))
498
+ raise nx.PowerIterationFailedConvergence(max_iter)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/__init__.py ADDED
File without changes
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/test_hits.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+
3
+ import networkx as nx
4
+ from networkx.algorithms.link_analysis.hits_alg import (
5
+ _hits_numpy,
6
+ _hits_python,
7
+ _hits_scipy,
8
+ )
9
+
10
+ np = pytest.importorskip("numpy")
11
+ sp = pytest.importorskip("scipy")
12
+
13
+ # Example from
14
+ # A. Langville and C. Meyer, "A survey of eigenvector methods of web
15
+ # information retrieval." http://citeseer.ist.psu.edu/713792.html
16
+
17
+
18
+ class TestHITS:
19
+ @classmethod
20
+ def setup_class(cls):
21
+ G = nx.DiGraph()
22
+
23
+ edges = [(1, 3), (1, 5), (2, 1), (3, 5), (5, 4), (5, 3), (6, 5)]
24
+
25
+ G.add_edges_from(edges, weight=1)
26
+ cls.G = G
27
+ cls.G.a = dict(
28
+ zip(sorted(G), [0.000000, 0.000000, 0.366025, 0.133975, 0.500000, 0.000000])
29
+ )
30
+ cls.G.h = dict(
31
+ zip(sorted(G), [0.366025, 0.000000, 0.211325, 0.000000, 0.211325, 0.211325])
32
+ )
33
+
34
+ def test_hits_numpy(self):
35
+ G = self.G
36
+ h, a = _hits_numpy(G)
37
+ for n in G:
38
+ assert h[n] == pytest.approx(G.h[n], abs=1e-4)
39
+ for n in G:
40
+ assert a[n] == pytest.approx(G.a[n], abs=1e-4)
41
+
42
+ @pytest.mark.parametrize("hits_alg", (nx.hits, _hits_python, _hits_scipy))
43
+ def test_hits(self, hits_alg):
44
+ G = self.G
45
+ h, a = hits_alg(G, tol=1.0e-08)
46
+ for n in G:
47
+ assert h[n] == pytest.approx(G.h[n], abs=1e-4)
48
+ for n in G:
49
+ assert a[n] == pytest.approx(G.a[n], abs=1e-4)
50
+ nstart = {i: 1.0 / 2 for i in G}
51
+ h, a = hits_alg(G, nstart=nstart)
52
+ for n in G:
53
+ assert h[n] == pytest.approx(G.h[n], abs=1e-4)
54
+ for n in G:
55
+ assert a[n] == pytest.approx(G.a[n], abs=1e-4)
56
+
57
+ def test_empty(self):
58
+ G = nx.Graph()
59
+ assert nx.hits(G) == ({}, {})
60
+ assert _hits_numpy(G) == ({}, {})
61
+ assert _hits_python(G) == ({}, {})
62
+ assert _hits_scipy(G) == ({}, {})
63
+
64
+ def test_hits_not_convergent(self):
65
+ G = nx.path_graph(50)
66
+ with pytest.raises(nx.PowerIterationFailedConvergence):
67
+ _hits_scipy(G, max_iter=1)
68
+ with pytest.raises(nx.PowerIterationFailedConvergence):
69
+ _hits_python(G, max_iter=1)
70
+ with pytest.raises(nx.PowerIterationFailedConvergence):
71
+ _hits_scipy(G, max_iter=0)
72
+ with pytest.raises(nx.PowerIterationFailedConvergence):
73
+ _hits_python(G, max_iter=0)
74
+ with pytest.raises(nx.PowerIterationFailedConvergence):
75
+ nx.hits(G, max_iter=0)
76
+ with pytest.raises(nx.PowerIterationFailedConvergence):
77
+ nx.hits(G, max_iter=1)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/test_pagerank.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ import pytest
4
+
5
+ import networkx as nx
6
+ from networkx.algorithms.link_analysis.pagerank_alg import (
7
+ _pagerank_numpy,
8
+ _pagerank_python,
9
+ _pagerank_scipy,
10
+ )
11
+ from networkx.classes.tests import dispatch_interface
12
+
13
+ np = pytest.importorskip("numpy")
14
+ pytest.importorskip("scipy")
15
+
16
+ # Example from
17
+ # A. Langville and C. Meyer, "A survey of eigenvector methods of web
18
+ # information retrieval." http://citeseer.ist.psu.edu/713792.html
19
+
20
+
21
+ class TestPageRank:
22
+ @classmethod
23
+ def setup_class(cls):
24
+ G = nx.DiGraph()
25
+ edges = [
26
+ (1, 2),
27
+ (1, 3),
28
+ # 2 is a dangling node
29
+ (3, 1),
30
+ (3, 2),
31
+ (3, 5),
32
+ (4, 5),
33
+ (4, 6),
34
+ (5, 4),
35
+ (5, 6),
36
+ (6, 4),
37
+ ]
38
+ G.add_edges_from(edges)
39
+ cls.G = G
40
+ cls.G.pagerank = dict(
41
+ zip(
42
+ sorted(G),
43
+ [
44
+ 0.03721197,
45
+ 0.05395735,
46
+ 0.04150565,
47
+ 0.37508082,
48
+ 0.20599833,
49
+ 0.28624589,
50
+ ],
51
+ )
52
+ )
53
+ cls.dangling_node_index = 1
54
+ cls.dangling_edges = {1: 2, 2: 3, 3: 0, 4: 0, 5: 0, 6: 0}
55
+ cls.G.dangling_pagerank = dict(
56
+ zip(
57
+ sorted(G),
58
+ [0.10844518, 0.18618601, 0.0710892, 0.2683668, 0.15919783, 0.20671497],
59
+ )
60
+ )
61
+
62
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python))
63
+ def test_pagerank(self, alg):
64
+ G = self.G
65
+ p = alg(G, alpha=0.9, tol=1.0e-08)
66
+ for n in G:
67
+ assert p[n] == pytest.approx(G.pagerank[n], abs=1e-4)
68
+
69
+ nstart = {n: random.random() for n in G}
70
+ p = alg(G, alpha=0.9, tol=1.0e-08, nstart=nstart)
71
+ for n in G:
72
+ assert p[n] == pytest.approx(G.pagerank[n], abs=1e-4)
73
+
74
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python))
75
+ def test_pagerank_max_iter(self, alg):
76
+ with pytest.raises(nx.PowerIterationFailedConvergence):
77
+ alg(self.G, max_iter=0)
78
+
79
+ def test_numpy_pagerank(self):
80
+ G = self.G
81
+ p = _pagerank_numpy(G, alpha=0.9)
82
+ for n in G:
83
+ assert p[n] == pytest.approx(G.pagerank[n], abs=1e-4)
84
+
85
+ def test_google_matrix(self):
86
+ G = self.G
87
+ M = nx.google_matrix(G, alpha=0.9, nodelist=sorted(G))
88
+ _, ev = np.linalg.eig(M.T)
89
+ p = ev[:, 0] / ev[:, 0].sum()
90
+ for a, b in zip(p, self.G.pagerank.values()):
91
+ assert a == pytest.approx(b, abs=1e-7)
92
+
93
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python, _pagerank_numpy))
94
+ def test_personalization(self, alg):
95
+ G = nx.complete_graph(4)
96
+ personalize = {0: 1, 1: 1, 2: 4, 3: 4}
97
+ answer = {
98
+ 0: 0.23246732615667579,
99
+ 1: 0.23246732615667579,
100
+ 2: 0.267532673843324,
101
+ 3: 0.2675326738433241,
102
+ }
103
+ p = alg(G, alpha=0.85, personalization=personalize)
104
+ for n in G:
105
+ assert p[n] == pytest.approx(answer[n], abs=1e-4)
106
+
107
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python, nx.google_matrix))
108
+ def test_zero_personalization_vector(self, alg):
109
+ G = nx.complete_graph(4)
110
+ personalize = {0: 0, 1: 0, 2: 0, 3: 0}
111
+ pytest.raises(ZeroDivisionError, alg, G, personalization=personalize)
112
+
113
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python))
114
+ def test_one_nonzero_personalization_value(self, alg):
115
+ G = nx.complete_graph(4)
116
+ personalize = {0: 0, 1: 0, 2: 0, 3: 1}
117
+ answer = {
118
+ 0: 0.22077931820379187,
119
+ 1: 0.22077931820379187,
120
+ 2: 0.22077931820379187,
121
+ 3: 0.3376620453886241,
122
+ }
123
+ p = alg(G, alpha=0.85, personalization=personalize)
124
+ for n in G:
125
+ assert p[n] == pytest.approx(answer[n], abs=1e-4)
126
+
127
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python))
128
+ def test_incomplete_personalization(self, alg):
129
+ G = nx.complete_graph(4)
130
+ personalize = {3: 1}
131
+ answer = {
132
+ 0: 0.22077931820379187,
133
+ 1: 0.22077931820379187,
134
+ 2: 0.22077931820379187,
135
+ 3: 0.3376620453886241,
136
+ }
137
+ p = alg(G, alpha=0.85, personalization=personalize)
138
+ for n in G:
139
+ assert p[n] == pytest.approx(answer[n], abs=1e-4)
140
+
141
+ def test_dangling_matrix(self):
142
+ """
143
+ Tests that the google_matrix doesn't change except for the dangling
144
+ nodes.
145
+ """
146
+ G = self.G
147
+ dangling = self.dangling_edges
148
+ dangling_sum = sum(dangling.values())
149
+ M1 = nx.google_matrix(G, personalization=dangling)
150
+ M2 = nx.google_matrix(G, personalization=dangling, dangling=dangling)
151
+ for i in range(len(G)):
152
+ for j in range(len(G)):
153
+ if i == self.dangling_node_index and (j + 1) in dangling:
154
+ assert M2[i, j] == pytest.approx(
155
+ dangling[j + 1] / dangling_sum, abs=1e-4
156
+ )
157
+ else:
158
+ assert M2[i, j] == pytest.approx(M1[i, j], abs=1e-4)
159
+
160
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python, _pagerank_numpy))
161
+ def test_dangling_pagerank(self, alg):
162
+ pr = alg(self.G, dangling=self.dangling_edges)
163
+ for n in self.G:
164
+ assert pr[n] == pytest.approx(self.G.dangling_pagerank[n], abs=1e-4)
165
+
166
+ def test_empty(self):
167
+ G = nx.Graph()
168
+ assert nx.pagerank(G) == {}
169
+ assert _pagerank_python(G) == {}
170
+ assert _pagerank_numpy(G) == {}
171
+ assert nx.google_matrix(G).shape == (0, 0)
172
+
173
+ @pytest.mark.parametrize("alg", (nx.pagerank, _pagerank_python))
174
+ def test_multigraph(self, alg):
175
+ G = nx.MultiGraph()
176
+ G.add_edges_from([(1, 2), (1, 2), (1, 2), (2, 3), (2, 3), ("3", 3), ("3", 3)])
177
+ answer = {
178
+ 1: 0.21066048614468322,
179
+ 2: 0.3395308825985378,
180
+ 3: 0.28933951385531687,
181
+ "3": 0.16046911740146227,
182
+ }
183
+ p = alg(G)
184
+ for n in G:
185
+ assert p[n] == pytest.approx(answer[n], abs=1e-4)
186
+
187
+
188
+ class TestPageRankScipy(TestPageRank):
189
+ def test_scipy_pagerank(self):
190
+ G = self.G
191
+ p = _pagerank_scipy(G, alpha=0.9, tol=1.0e-08)
192
+ for n in G:
193
+ assert p[n] == pytest.approx(G.pagerank[n], abs=1e-4)
194
+ personalize = {n: random.random() for n in G}
195
+ p = _pagerank_scipy(G, alpha=0.9, tol=1.0e-08, personalization=personalize)
196
+
197
+ nstart = {n: random.random() for n in G}
198
+ p = _pagerank_scipy(G, alpha=0.9, tol=1.0e-08, nstart=nstart)
199
+ for n in G:
200
+ assert p[n] == pytest.approx(G.pagerank[n], abs=1e-4)
201
+
202
+ def test_scipy_pagerank_max_iter(self):
203
+ with pytest.raises(nx.PowerIterationFailedConvergence):
204
+ _pagerank_scipy(self.G, max_iter=0)
205
+
206
+ def test_dangling_scipy_pagerank(self):
207
+ pr = _pagerank_scipy(self.G, dangling=self.dangling_edges)
208
+ for n in self.G:
209
+ assert pr[n] == pytest.approx(self.G.dangling_pagerank[n], abs=1e-4)
210
+
211
+ def test_empty_scipy(self):
212
+ G = nx.Graph()
213
+ assert _pagerank_scipy(G) == {}
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_prediction.py ADDED
@@ -0,0 +1,687 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Link prediction algorithms.
3
+ """
4
+
5
+ from math import log
6
+
7
+ import networkx as nx
8
+ from networkx.utils import not_implemented_for
9
+
10
+ __all__ = [
11
+ "resource_allocation_index",
12
+ "jaccard_coefficient",
13
+ "adamic_adar_index",
14
+ "preferential_attachment",
15
+ "cn_soundarajan_hopcroft",
16
+ "ra_index_soundarajan_hopcroft",
17
+ "within_inter_cluster",
18
+ "common_neighbor_centrality",
19
+ ]
20
+
21
+
22
+ def _apply_prediction(G, func, ebunch=None):
23
+ """Applies the given function to each edge in the specified iterable
24
+ of edges.
25
+
26
+ `G` is an instance of :class:`networkx.Graph`.
27
+
28
+ `func` is a function on two inputs, each of which is a node in the
29
+ graph. The function can return anything, but it should return a
30
+ value representing a prediction of the likelihood of a "link"
31
+ joining the two nodes.
32
+
33
+ `ebunch` is an iterable of pairs of nodes. If not specified, all
34
+ non-edges in the graph `G` will be used.
35
+
36
+ """
37
+ if ebunch is None:
38
+ ebunch = nx.non_edges(G)
39
+ else:
40
+ for u, v in ebunch:
41
+ if u not in G:
42
+ raise nx.NodeNotFound(f"Node {u} not in G.")
43
+ if v not in G:
44
+ raise nx.NodeNotFound(f"Node {v} not in G.")
45
+ return ((u, v, func(u, v)) for u, v in ebunch)
46
+
47
+
48
+ @not_implemented_for("directed")
49
+ @not_implemented_for("multigraph")
50
+ @nx._dispatchable
51
+ def resource_allocation_index(G, ebunch=None):
52
+ r"""Compute the resource allocation index of all node pairs in ebunch.
53
+
54
+ Resource allocation index of `u` and `v` is defined as
55
+
56
+ .. math::
57
+
58
+ \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{|\Gamma(w)|}
59
+
60
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
61
+
62
+ Parameters
63
+ ----------
64
+ G : graph
65
+ A NetworkX undirected graph.
66
+
67
+ ebunch : iterable of node pairs, optional (default = None)
68
+ Resource allocation index will be computed for each pair of
69
+ nodes given in the iterable. The pairs must be given as
70
+ 2-tuples (u, v) where u and v are nodes in the graph. If ebunch
71
+ is None then all nonexistent edges in the graph will be used.
72
+ Default value: None.
73
+
74
+ Returns
75
+ -------
76
+ piter : iterator
77
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
78
+ pair of nodes and p is their resource allocation index.
79
+
80
+ Raises
81
+ ------
82
+ NetworkXNotImplemented
83
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
84
+
85
+ NodeNotFound
86
+ If `ebunch` has a node that is not in `G`.
87
+
88
+ Examples
89
+ --------
90
+ >>> G = nx.complete_graph(5)
91
+ >>> preds = nx.resource_allocation_index(G, [(0, 1), (2, 3)])
92
+ >>> for u, v, p in preds:
93
+ ... print(f"({u}, {v}) -> {p:.8f}")
94
+ (0, 1) -> 0.75000000
95
+ (2, 3) -> 0.75000000
96
+
97
+ References
98
+ ----------
99
+ .. [1] T. Zhou, L. Lu, Y.-C. Zhang.
100
+ Predicting missing links via local information.
101
+ Eur. Phys. J. B 71 (2009) 623.
102
+ https://arxiv.org/pdf/0901.0553.pdf
103
+ """
104
+
105
+ def predict(u, v):
106
+ return sum(1 / G.degree(w) for w in nx.common_neighbors(G, u, v))
107
+
108
+ return _apply_prediction(G, predict, ebunch)
109
+
110
+
111
+ @not_implemented_for("directed")
112
+ @not_implemented_for("multigraph")
113
+ @nx._dispatchable
114
+ def jaccard_coefficient(G, ebunch=None):
115
+ r"""Compute the Jaccard coefficient of all node pairs in ebunch.
116
+
117
+ Jaccard coefficient of nodes `u` and `v` is defined as
118
+
119
+ .. math::
120
+
121
+ \frac{|\Gamma(u) \cap \Gamma(v)|}{|\Gamma(u) \cup \Gamma(v)|}
122
+
123
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
124
+
125
+ Parameters
126
+ ----------
127
+ G : graph
128
+ A NetworkX undirected graph.
129
+
130
+ ebunch : iterable of node pairs, optional (default = None)
131
+ Jaccard coefficient will be computed for each pair of nodes
132
+ given in the iterable. The pairs must be given as 2-tuples
133
+ (u, v) where u and v are nodes in the graph. If ebunch is None
134
+ then all nonexistent edges in the graph will be used.
135
+ Default value: None.
136
+
137
+ Returns
138
+ -------
139
+ piter : iterator
140
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
141
+ pair of nodes and p is their Jaccard coefficient.
142
+
143
+ Raises
144
+ ------
145
+ NetworkXNotImplemented
146
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
147
+
148
+ NodeNotFound
149
+ If `ebunch` has a node that is not in `G`.
150
+
151
+ Examples
152
+ --------
153
+ >>> G = nx.complete_graph(5)
154
+ >>> preds = nx.jaccard_coefficient(G, [(0, 1), (2, 3)])
155
+ >>> for u, v, p in preds:
156
+ ... print(f"({u}, {v}) -> {p:.8f}")
157
+ (0, 1) -> 0.60000000
158
+ (2, 3) -> 0.60000000
159
+
160
+ References
161
+ ----------
162
+ .. [1] D. Liben-Nowell, J. Kleinberg.
163
+ The Link Prediction Problem for Social Networks (2004).
164
+ http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
165
+ """
166
+
167
+ def predict(u, v):
168
+ union_size = len(set(G[u]) | set(G[v]))
169
+ if union_size == 0:
170
+ return 0
171
+ return len(nx.common_neighbors(G, u, v)) / union_size
172
+
173
+ return _apply_prediction(G, predict, ebunch)
174
+
175
+
176
+ @not_implemented_for("directed")
177
+ @not_implemented_for("multigraph")
178
+ @nx._dispatchable
179
+ def adamic_adar_index(G, ebunch=None):
180
+ r"""Compute the Adamic-Adar index of all node pairs in ebunch.
181
+
182
+ Adamic-Adar index of `u` and `v` is defined as
183
+
184
+ .. math::
185
+
186
+ \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{\log |\Gamma(w)|}
187
+
188
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
189
+ This index leads to zero-division for nodes only connected via self-loops.
190
+ It is intended to be used when no self-loops are present.
191
+
192
+ Parameters
193
+ ----------
194
+ G : graph
195
+ NetworkX undirected graph.
196
+
197
+ ebunch : iterable of node pairs, optional (default = None)
198
+ Adamic-Adar index will be computed for each pair of nodes given
199
+ in the iterable. The pairs must be given as 2-tuples (u, v)
200
+ where u and v are nodes in the graph. If ebunch is None then all
201
+ nonexistent edges in the graph will be used.
202
+ Default value: None.
203
+
204
+ Returns
205
+ -------
206
+ piter : iterator
207
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
208
+ pair of nodes and p is their Adamic-Adar index.
209
+
210
+ Raises
211
+ ------
212
+ NetworkXNotImplemented
213
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
214
+
215
+ NodeNotFound
216
+ If `ebunch` has a node that is not in `G`.
217
+
218
+ Examples
219
+ --------
220
+ >>> G = nx.complete_graph(5)
221
+ >>> preds = nx.adamic_adar_index(G, [(0, 1), (2, 3)])
222
+ >>> for u, v, p in preds:
223
+ ... print(f"({u}, {v}) -> {p:.8f}")
224
+ (0, 1) -> 2.16404256
225
+ (2, 3) -> 2.16404256
226
+
227
+ References
228
+ ----------
229
+ .. [1] D. Liben-Nowell, J. Kleinberg.
230
+ The Link Prediction Problem for Social Networks (2004).
231
+ http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
232
+ """
233
+
234
+ def predict(u, v):
235
+ return sum(1 / log(G.degree(w)) for w in nx.common_neighbors(G, u, v))
236
+
237
+ return _apply_prediction(G, predict, ebunch)
238
+
239
+
240
+ @not_implemented_for("directed")
241
+ @not_implemented_for("multigraph")
242
+ @nx._dispatchable
243
+ def common_neighbor_centrality(G, ebunch=None, alpha=0.8):
244
+ r"""Return the CCPA score for each pair of nodes.
245
+
246
+ Compute the Common Neighbor and Centrality based Parameterized Algorithm(CCPA)
247
+ score of all node pairs in ebunch.
248
+
249
+ CCPA score of `u` and `v` is defined as
250
+
251
+ .. math::
252
+
253
+ \alpha \cdot (|\Gamma (u){\cap }^{}\Gamma (v)|)+(1-\alpha )\cdot \frac{N}{{d}_{uv}}
254
+
255
+ where $\Gamma(u)$ denotes the set of neighbors of $u$, $\Gamma(v)$ denotes the
256
+ set of neighbors of $v$, $\alpha$ is parameter varies between [0,1], $N$ denotes
257
+ total number of nodes in the Graph and ${d}_{uv}$ denotes shortest distance
258
+ between $u$ and $v$.
259
+
260
+ This algorithm is based on two vital properties of nodes, namely the number
261
+ of common neighbors and their centrality. Common neighbor refers to the common
262
+ nodes between two nodes. Centrality refers to the prestige that a node enjoys
263
+ in a network.
264
+
265
+ .. seealso::
266
+
267
+ :func:`common_neighbors`
268
+
269
+ Parameters
270
+ ----------
271
+ G : graph
272
+ NetworkX undirected graph.
273
+
274
+ ebunch : iterable of node pairs, optional (default = None)
275
+ Preferential attachment score will be computed for each pair of
276
+ nodes given in the iterable. The pairs must be given as
277
+ 2-tuples (u, v) where u and v are nodes in the graph. If ebunch
278
+ is None then all nonexistent edges in the graph will be used.
279
+ Default value: None.
280
+
281
+ alpha : Parameter defined for participation of Common Neighbor
282
+ and Centrality Algorithm share. Values for alpha should
283
+ normally be between 0 and 1. Default value set to 0.8
284
+ because author found better performance at 0.8 for all the
285
+ dataset.
286
+ Default value: 0.8
287
+
288
+
289
+ Returns
290
+ -------
291
+ piter : iterator
292
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
293
+ pair of nodes and p is their Common Neighbor and Centrality based
294
+ Parameterized Algorithm(CCPA) score.
295
+
296
+ Raises
297
+ ------
298
+ NetworkXNotImplemented
299
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
300
+
301
+ NetworkXAlgorithmError
302
+ If self loops exist in `ebunch` or in `G` (if `ebunch` is `None`).
303
+
304
+ NodeNotFound
305
+ If `ebunch` has a node that is not in `G`.
306
+
307
+ Examples
308
+ --------
309
+ >>> G = nx.complete_graph(5)
310
+ >>> preds = nx.common_neighbor_centrality(G, [(0, 1), (2, 3)])
311
+ >>> for u, v, p in preds:
312
+ ... print(f"({u}, {v}) -> {p}")
313
+ (0, 1) -> 3.4000000000000004
314
+ (2, 3) -> 3.4000000000000004
315
+
316
+ References
317
+ ----------
318
+ .. [1] Ahmad, I., Akhtar, M.U., Noor, S. et al.
319
+ Missing Link Prediction using Common Neighbor and Centrality based Parameterized Algorithm.
320
+ Sci Rep 10, 364 (2020).
321
+ https://doi.org/10.1038/s41598-019-57304-y
322
+ """
323
+
324
+ # When alpha == 1, the CCPA score simplifies to the number of common neighbors.
325
+ if alpha == 1:
326
+
327
+ def predict(u, v):
328
+ if u == v:
329
+ raise nx.NetworkXAlgorithmError("Self loops are not supported")
330
+
331
+ return len(nx.common_neighbors(G, u, v))
332
+
333
+ else:
334
+ spl = dict(nx.shortest_path_length(G))
335
+ inf = float("inf")
336
+
337
+ def predict(u, v):
338
+ if u == v:
339
+ raise nx.NetworkXAlgorithmError("Self loops are not supported")
340
+ path_len = spl[u].get(v, inf)
341
+
342
+ n_nbrs = len(nx.common_neighbors(G, u, v))
343
+ return alpha * n_nbrs + (1 - alpha) * len(G) / path_len
344
+
345
+ return _apply_prediction(G, predict, ebunch)
346
+
347
+
348
+ @not_implemented_for("directed")
349
+ @not_implemented_for("multigraph")
350
+ @nx._dispatchable
351
+ def preferential_attachment(G, ebunch=None):
352
+ r"""Compute the preferential attachment score of all node pairs in ebunch.
353
+
354
+ Preferential attachment score of `u` and `v` is defined as
355
+
356
+ .. math::
357
+
358
+ |\Gamma(u)| |\Gamma(v)|
359
+
360
+ where $\Gamma(u)$ denotes the set of neighbors of $u$.
361
+
362
+ Parameters
363
+ ----------
364
+ G : graph
365
+ NetworkX undirected graph.
366
+
367
+ ebunch : iterable of node pairs, optional (default = None)
368
+ Preferential attachment score will be computed for each pair of
369
+ nodes given in the iterable. The pairs must be given as
370
+ 2-tuples (u, v) where u and v are nodes in the graph. If ebunch
371
+ is None then all nonexistent edges in the graph will be used.
372
+ Default value: None.
373
+
374
+ Returns
375
+ -------
376
+ piter : iterator
377
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
378
+ pair of nodes and p is their preferential attachment score.
379
+
380
+ Raises
381
+ ------
382
+ NetworkXNotImplemented
383
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
384
+
385
+ NodeNotFound
386
+ If `ebunch` has a node that is not in `G`.
387
+
388
+ Examples
389
+ --------
390
+ >>> G = nx.complete_graph(5)
391
+ >>> preds = nx.preferential_attachment(G, [(0, 1), (2, 3)])
392
+ >>> for u, v, p in preds:
393
+ ... print(f"({u}, {v}) -> {p}")
394
+ (0, 1) -> 16
395
+ (2, 3) -> 16
396
+
397
+ References
398
+ ----------
399
+ .. [1] D. Liben-Nowell, J. Kleinberg.
400
+ The Link Prediction Problem for Social Networks (2004).
401
+ http://www.cs.cornell.edu/home/kleinber/link-pred.pdf
402
+ """
403
+
404
+ def predict(u, v):
405
+ return G.degree(u) * G.degree(v)
406
+
407
+ return _apply_prediction(G, predict, ebunch)
408
+
409
+
410
+ @not_implemented_for("directed")
411
+ @not_implemented_for("multigraph")
412
+ @nx._dispatchable(node_attrs="community")
413
+ def cn_soundarajan_hopcroft(G, ebunch=None, community="community"):
414
+ r"""Count the number of common neighbors of all node pairs in ebunch
415
+ using community information.
416
+
417
+ For two nodes $u$ and $v$, this function computes the number of
418
+ common neighbors and bonus one for each common neighbor belonging to
419
+ the same community as $u$ and $v$. Mathematically,
420
+
421
+ .. math::
422
+
423
+ |\Gamma(u) \cap \Gamma(v)| + \sum_{w \in \Gamma(u) \cap \Gamma(v)} f(w)
424
+
425
+ where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
426
+ and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
427
+ neighbors of $u$.
428
+
429
+ Parameters
430
+ ----------
431
+ G : graph
432
+ A NetworkX undirected graph.
433
+
434
+ ebunch : iterable of node pairs, optional (default = None)
435
+ The score will be computed for each pair of nodes given in the
436
+ iterable. The pairs must be given as 2-tuples (u, v) where u
437
+ and v are nodes in the graph. If ebunch is None then all
438
+ nonexistent edges in the graph will be used.
439
+ Default value: None.
440
+
441
+ community : string, optional (default = 'community')
442
+ Nodes attribute name containing the community information.
443
+ G[u][community] identifies which community u belongs to. Each
444
+ node belongs to at most one community. Default value: 'community'.
445
+
446
+ Returns
447
+ -------
448
+ piter : iterator
449
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
450
+ pair of nodes and p is their score.
451
+
452
+ Raises
453
+ ------
454
+ NetworkXNotImplemented
455
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
456
+
457
+ NetworkXAlgorithmError
458
+ If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
459
+
460
+ NodeNotFound
461
+ If `ebunch` has a node that is not in `G`.
462
+
463
+ Examples
464
+ --------
465
+ >>> G = nx.path_graph(3)
466
+ >>> G.nodes[0]["community"] = 0
467
+ >>> G.nodes[1]["community"] = 0
468
+ >>> G.nodes[2]["community"] = 0
469
+ >>> preds = nx.cn_soundarajan_hopcroft(G, [(0, 2)])
470
+ >>> for u, v, p in preds:
471
+ ... print(f"({u}, {v}) -> {p}")
472
+ (0, 2) -> 2
473
+
474
+ References
475
+ ----------
476
+ .. [1] Sucheta Soundarajan and John Hopcroft.
477
+ Using community information to improve the precision of link
478
+ prediction methods.
479
+ In Proceedings of the 21st international conference companion on
480
+ World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
481
+ http://doi.acm.org/10.1145/2187980.2188150
482
+ """
483
+
484
+ def predict(u, v):
485
+ Cu = _community(G, u, community)
486
+ Cv = _community(G, v, community)
487
+ cnbors = nx.common_neighbors(G, u, v)
488
+ neighbors = (
489
+ sum(_community(G, w, community) == Cu for w in cnbors) if Cu == Cv else 0
490
+ )
491
+ return len(cnbors) + neighbors
492
+
493
+ return _apply_prediction(G, predict, ebunch)
494
+
495
+
496
+ @not_implemented_for("directed")
497
+ @not_implemented_for("multigraph")
498
+ @nx._dispatchable(node_attrs="community")
499
+ def ra_index_soundarajan_hopcroft(G, ebunch=None, community="community"):
500
+ r"""Compute the resource allocation index of all node pairs in
501
+ ebunch using community information.
502
+
503
+ For two nodes $u$ and $v$, this function computes the resource
504
+ allocation index considering only common neighbors belonging to the
505
+ same community as $u$ and $v$. Mathematically,
506
+
507
+ .. math::
508
+
509
+ \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{f(w)}{|\Gamma(w)|}
510
+
511
+ where $f(w)$ equals 1 if $w$ belongs to the same community as $u$
512
+ and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of
513
+ neighbors of $u$.
514
+
515
+ Parameters
516
+ ----------
517
+ G : graph
518
+ A NetworkX undirected graph.
519
+
520
+ ebunch : iterable of node pairs, optional (default = None)
521
+ The score will be computed for each pair of nodes given in the
522
+ iterable. The pairs must be given as 2-tuples (u, v) where u
523
+ and v are nodes in the graph. If ebunch is None then all
524
+ nonexistent edges in the graph will be used.
525
+ Default value: None.
526
+
527
+ community : string, optional (default = 'community')
528
+ Nodes attribute name containing the community information.
529
+ G[u][community] identifies which community u belongs to. Each
530
+ node belongs to at most one community. Default value: 'community'.
531
+
532
+ Returns
533
+ -------
534
+ piter : iterator
535
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
536
+ pair of nodes and p is their score.
537
+
538
+ Raises
539
+ ------
540
+ NetworkXNotImplemented
541
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
542
+
543
+ NetworkXAlgorithmError
544
+ If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
545
+
546
+ NodeNotFound
547
+ If `ebunch` has a node that is not in `G`.
548
+
549
+ Examples
550
+ --------
551
+ >>> G = nx.Graph()
552
+ >>> G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
553
+ >>> G.nodes[0]["community"] = 0
554
+ >>> G.nodes[1]["community"] = 0
555
+ >>> G.nodes[2]["community"] = 1
556
+ >>> G.nodes[3]["community"] = 0
557
+ >>> preds = nx.ra_index_soundarajan_hopcroft(G, [(0, 3)])
558
+ >>> for u, v, p in preds:
559
+ ... print(f"({u}, {v}) -> {p:.8f}")
560
+ (0, 3) -> 0.50000000
561
+
562
+ References
563
+ ----------
564
+ .. [1] Sucheta Soundarajan and John Hopcroft.
565
+ Using community information to improve the precision of link
566
+ prediction methods.
567
+ In Proceedings of the 21st international conference companion on
568
+ World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608.
569
+ http://doi.acm.org/10.1145/2187980.2188150
570
+ """
571
+
572
+ def predict(u, v):
573
+ Cu = _community(G, u, community)
574
+ Cv = _community(G, v, community)
575
+ if Cu != Cv:
576
+ return 0
577
+ cnbors = nx.common_neighbors(G, u, v)
578
+ return sum(1 / G.degree(w) for w in cnbors if _community(G, w, community) == Cu)
579
+
580
+ return _apply_prediction(G, predict, ebunch)
581
+
582
+
583
+ @not_implemented_for("directed")
584
+ @not_implemented_for("multigraph")
585
+ @nx._dispatchable(node_attrs="community")
586
+ def within_inter_cluster(G, ebunch=None, delta=0.001, community="community"):
587
+ """Compute the ratio of within- and inter-cluster common neighbors
588
+ of all node pairs in ebunch.
589
+
590
+ For two nodes `u` and `v`, if a common neighbor `w` belongs to the
591
+ same community as them, `w` is considered as within-cluster common
592
+ neighbor of `u` and `v`. Otherwise, it is considered as
593
+ inter-cluster common neighbor of `u` and `v`. The ratio between the
594
+ size of the set of within- and inter-cluster common neighbors is
595
+ defined as the WIC measure. [1]_
596
+
597
+ Parameters
598
+ ----------
599
+ G : graph
600
+ A NetworkX undirected graph.
601
+
602
+ ebunch : iterable of node pairs, optional (default = None)
603
+ The WIC measure will be computed for each pair of nodes given in
604
+ the iterable. The pairs must be given as 2-tuples (u, v) where
605
+ u and v are nodes in the graph. If ebunch is None then all
606
+ nonexistent edges in the graph will be used.
607
+ Default value: None.
608
+
609
+ delta : float, optional (default = 0.001)
610
+ Value to prevent division by zero in case there is no
611
+ inter-cluster common neighbor between two nodes. See [1]_ for
612
+ details. Default value: 0.001.
613
+
614
+ community : string, optional (default = 'community')
615
+ Nodes attribute name containing the community information.
616
+ G[u][community] identifies which community u belongs to. Each
617
+ node belongs to at most one community. Default value: 'community'.
618
+
619
+ Returns
620
+ -------
621
+ piter : iterator
622
+ An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
623
+ pair of nodes and p is their WIC measure.
624
+
625
+ Raises
626
+ ------
627
+ NetworkXNotImplemented
628
+ If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
629
+
630
+ NetworkXAlgorithmError
631
+ - If `delta` is less than or equal to zero.
632
+ - If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
633
+
634
+ NodeNotFound
635
+ If `ebunch` has a node that is not in `G`.
636
+
637
+ Examples
638
+ --------
639
+ >>> G = nx.Graph()
640
+ >>> G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)])
641
+ >>> G.nodes[0]["community"] = 0
642
+ >>> G.nodes[1]["community"] = 1
643
+ >>> G.nodes[2]["community"] = 0
644
+ >>> G.nodes[3]["community"] = 0
645
+ >>> G.nodes[4]["community"] = 0
646
+ >>> preds = nx.within_inter_cluster(G, [(0, 4)])
647
+ >>> for u, v, p in preds:
648
+ ... print(f"({u}, {v}) -> {p:.8f}")
649
+ (0, 4) -> 1.99800200
650
+ >>> preds = nx.within_inter_cluster(G, [(0, 4)], delta=0.5)
651
+ >>> for u, v, p in preds:
652
+ ... print(f"({u}, {v}) -> {p:.8f}")
653
+ (0, 4) -> 1.33333333
654
+
655
+ References
656
+ ----------
657
+ .. [1] Jorge Carlos Valverde-Rebaza and Alneu de Andrade Lopes.
658
+ Link prediction in complex networks based on cluster information.
659
+ In Proceedings of the 21st Brazilian conference on Advances in
660
+ Artificial Intelligence (SBIA'12)
661
+ https://doi.org/10.1007/978-3-642-34459-6_10
662
+ """
663
+ if delta <= 0:
664
+ raise nx.NetworkXAlgorithmError("Delta must be greater than zero")
665
+
666
+ def predict(u, v):
667
+ Cu = _community(G, u, community)
668
+ Cv = _community(G, v, community)
669
+ if Cu != Cv:
670
+ return 0
671
+ cnbors = nx.common_neighbors(G, u, v)
672
+ within = {w for w in cnbors if _community(G, w, community) == Cu}
673
+ inter = cnbors - within
674
+ return len(within) / (len(inter) + delta)
675
+
676
+ return _apply_prediction(G, predict, ebunch)
677
+
678
+
679
+ def _community(G, u, community):
680
+ """Get the community of the given node."""
681
+ node_u = G.nodes[u]
682
+ try:
683
+ return node_u[community]
684
+ except KeyError as err:
685
+ raise nx.NetworkXAlgorithmError(
686
+ f"No community information available for Node {u}"
687
+ ) from err
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/lowest_common_ancestors.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algorithms for finding the lowest common ancestor of trees and DAGs."""
2
+
3
+ from collections import defaultdict
4
+ from collections.abc import Mapping, Set
5
+ from itertools import combinations_with_replacement
6
+
7
+ import networkx as nx
8
+ from networkx.utils import UnionFind, arbitrary_element, not_implemented_for
9
+
10
+ __all__ = [
11
+ "all_pairs_lowest_common_ancestor",
12
+ "tree_all_pairs_lowest_common_ancestor",
13
+ "lowest_common_ancestor",
14
+ ]
15
+
16
+
17
+ @not_implemented_for("undirected")
18
+ @nx._dispatchable
19
+ def all_pairs_lowest_common_ancestor(G, pairs=None):
20
+ """Return the lowest common ancestor of all pairs or the provided pairs
21
+
22
+ Parameters
23
+ ----------
24
+ G : NetworkX directed graph
25
+
26
+ pairs : iterable of pairs of nodes, optional (default: all pairs)
27
+ The pairs of nodes of interest.
28
+ If None, will find the LCA of all pairs of nodes.
29
+
30
+ Yields
31
+ ------
32
+ ((node1, node2), lca) : 2-tuple
33
+ Where lca is least common ancestor of node1 and node2.
34
+ Note that for the default case, the order of the node pair is not considered,
35
+ e.g. you will not get both ``(a, b)`` and ``(b, a)``
36
+
37
+ Raises
38
+ ------
39
+ NetworkXPointlessConcept
40
+ If `G` is null.
41
+ NetworkXError
42
+ If `G` is not a DAG.
43
+
44
+ Examples
45
+ --------
46
+ >>> from pprint import pprint
47
+
48
+ The default behavior is to yield the lowest common ancestor for all
49
+ possible combinations of nodes in `G`, including self-pairings:
50
+
51
+ >>> G = nx.DiGraph([(0, 1), (0, 3), (1, 2)])
52
+ >>> pprint(dict(nx.all_pairs_lowest_common_ancestor(G)))
53
+ {(0, 0): 0,
54
+ (0, 1): 0,
55
+ (0, 2): 0,
56
+ (0, 3): 0,
57
+ (1, 1): 1,
58
+ (1, 2): 1,
59
+ (1, 3): 0,
60
+ (2, 2): 2,
61
+ (3, 2): 0,
62
+ (3, 3): 3}
63
+
64
+ The pairs argument can be used to limit the output to only the
65
+ specified node pairings:
66
+
67
+ >>> dict(nx.all_pairs_lowest_common_ancestor(G, pairs=[(1, 2), (2, 3)]))
68
+ {(1, 2): 1, (2, 3): 0}
69
+
70
+ Notes
71
+ -----
72
+ Only defined on non-null directed acyclic graphs.
73
+
74
+ See Also
75
+ --------
76
+ lowest_common_ancestor
77
+ """
78
+ if not nx.is_directed_acyclic_graph(G):
79
+ raise nx.NetworkXError("LCA only defined on directed acyclic graphs.")
80
+ if len(G) == 0:
81
+ raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
82
+
83
+ if pairs is None:
84
+ pairs = combinations_with_replacement(G, 2)
85
+ else:
86
+ # Convert iterator to iterable, if necessary. Trim duplicates.
87
+ pairs = dict.fromkeys(pairs)
88
+ # Verify that each of the nodes in the provided pairs is in G
89
+ nodeset = set(G)
90
+ for pair in pairs:
91
+ if set(pair) - nodeset:
92
+ raise nx.NodeNotFound(
93
+ f"Node(s) {set(pair) - nodeset} from pair {pair} not in G."
94
+ )
95
+
96
+ # Once input validation is done, construct the generator
97
+ def generate_lca_from_pairs(G, pairs):
98
+ ancestor_cache = {}
99
+
100
+ for v, w in pairs:
101
+ if v not in ancestor_cache:
102
+ ancestor_cache[v] = nx.ancestors(G, v)
103
+ ancestor_cache[v].add(v)
104
+ if w not in ancestor_cache:
105
+ ancestor_cache[w] = nx.ancestors(G, w)
106
+ ancestor_cache[w].add(w)
107
+
108
+ common_ancestors = ancestor_cache[v] & ancestor_cache[w]
109
+
110
+ if common_ancestors:
111
+ common_ancestor = next(iter(common_ancestors))
112
+ while True:
113
+ successor = None
114
+ for lower_ancestor in G.successors(common_ancestor):
115
+ if lower_ancestor in common_ancestors:
116
+ successor = lower_ancestor
117
+ break
118
+ if successor is None:
119
+ break
120
+ common_ancestor = successor
121
+ yield ((v, w), common_ancestor)
122
+
123
+ return generate_lca_from_pairs(G, pairs)
124
+
125
+
126
+ @not_implemented_for("undirected")
127
+ @nx._dispatchable
128
+ def lowest_common_ancestor(G, node1, node2, default=None):
129
+ """Compute the lowest common ancestor of the given pair of nodes.
130
+
131
+ Parameters
132
+ ----------
133
+ G : NetworkX directed graph
134
+
135
+ node1, node2 : nodes in the graph.
136
+
137
+ default : object
138
+ Returned if no common ancestor between `node1` and `node2`
139
+
140
+ Returns
141
+ -------
142
+ The lowest common ancestor of node1 and node2,
143
+ or default if they have no common ancestors.
144
+
145
+ Examples
146
+ --------
147
+ >>> G = nx.DiGraph()
148
+ >>> nx.add_path(G, (0, 1, 2, 3))
149
+ >>> nx.add_path(G, (0, 4, 3))
150
+ >>> nx.lowest_common_ancestor(G, 2, 4)
151
+ 0
152
+
153
+ See Also
154
+ --------
155
+ all_pairs_lowest_common_ancestor"""
156
+
157
+ ans = list(all_pairs_lowest_common_ancestor(G, pairs=[(node1, node2)]))
158
+ if ans:
159
+ assert len(ans) == 1
160
+ return ans[0][1]
161
+ return default
162
+
163
+
164
+ @not_implemented_for("undirected")
165
+ @nx._dispatchable
166
+ def tree_all_pairs_lowest_common_ancestor(G, root=None, pairs=None):
167
+ r"""Yield the lowest common ancestor for sets of pairs in a tree.
168
+
169
+ Parameters
170
+ ----------
171
+ G : NetworkX directed graph (must be a tree)
172
+
173
+ root : node, optional (default: None)
174
+ The root of the subtree to operate on.
175
+ If None, assume the entire graph has exactly one source and use that.
176
+
177
+ pairs : iterable or iterator of pairs of nodes, optional (default: None)
178
+ The pairs of interest. If None, Defaults to all pairs of nodes
179
+ under `root` that have a lowest common ancestor.
180
+
181
+ Returns
182
+ -------
183
+ lcas : generator of tuples `((u, v), lca)` where `u` and `v` are nodes
184
+ in `pairs` and `lca` is their lowest common ancestor.
185
+
186
+ Examples
187
+ --------
188
+ >>> import pprint
189
+ >>> G = nx.DiGraph([(1, 3), (2, 4), (1, 2)])
190
+ >>> pprint.pprint(dict(nx.tree_all_pairs_lowest_common_ancestor(G)))
191
+ {(1, 1): 1,
192
+ (2, 1): 1,
193
+ (2, 2): 2,
194
+ (3, 1): 1,
195
+ (3, 2): 1,
196
+ (3, 3): 3,
197
+ (3, 4): 1,
198
+ (4, 1): 1,
199
+ (4, 2): 2,
200
+ (4, 4): 4}
201
+
202
+ We can also use `pairs` argument to specify the pairs of nodes for which we
203
+ want to compute lowest common ancestors. Here is an example:
204
+
205
+ >>> dict(nx.tree_all_pairs_lowest_common_ancestor(G, pairs=[(1, 4), (2, 3)]))
206
+ {(2, 3): 1, (1, 4): 1}
207
+
208
+ Notes
209
+ -----
210
+ Only defined on non-null trees represented with directed edges from
211
+ parents to children. Uses Tarjan's off-line lowest-common-ancestors
212
+ algorithm. Runs in time $O(4 \times (V + E + P))$ time, where 4 is the largest
213
+ value of the inverse Ackermann function likely to ever come up in actual
214
+ use, and $P$ is the number of pairs requested (or $V^2$ if all are needed).
215
+
216
+ Tarjan, R. E. (1979), "Applications of path compression on balanced trees",
217
+ Journal of the ACM 26 (4): 690-715, doi:10.1145/322154.322161.
218
+
219
+ See Also
220
+ --------
221
+ all_pairs_lowest_common_ancestor: similar routine for general DAGs
222
+ lowest_common_ancestor: just a single pair for general DAGs
223
+ """
224
+ if len(G) == 0:
225
+ raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.")
226
+
227
+ # Index pairs of interest for efficient lookup from either side.
228
+ if pairs is not None:
229
+ pair_dict = defaultdict(set)
230
+ # See note on all_pairs_lowest_common_ancestor.
231
+ if not isinstance(pairs, Mapping | Set):
232
+ pairs = set(pairs)
233
+ for u, v in pairs:
234
+ for n in (u, v):
235
+ if n not in G:
236
+ msg = f"The node {str(n)} is not in the digraph."
237
+ raise nx.NodeNotFound(msg)
238
+ pair_dict[u].add(v)
239
+ pair_dict[v].add(u)
240
+
241
+ # If root is not specified, find the exactly one node with in degree 0 and
242
+ # use it. Raise an error if none are found, or more than one is. Also check
243
+ # for any nodes with in degree larger than 1, which would imply G is not a
244
+ # tree.
245
+ if root is None:
246
+ for n, deg in G.in_degree:
247
+ if deg == 0:
248
+ if root is not None:
249
+ msg = "No root specified and tree has multiple sources."
250
+ raise nx.NetworkXError(msg)
251
+ root = n
252
+ # checking deg>1 is not sufficient for MultiDiGraphs
253
+ elif deg > 1 and len(G.pred[n]) > 1:
254
+ msg = "Tree LCA only defined on trees; use DAG routine."
255
+ raise nx.NetworkXError(msg)
256
+ if root is None:
257
+ raise nx.NetworkXError("Graph contains a cycle.")
258
+
259
+ # Iterative implementation of Tarjan's offline lca algorithm
260
+ # as described in CLRS on page 521 (2nd edition)/page 584 (3rd edition)
261
+ uf = UnionFind()
262
+ ancestors = {}
263
+ for node in G:
264
+ ancestors[node] = uf[node]
265
+
266
+ colors = defaultdict(bool)
267
+ for node in nx.dfs_postorder_nodes(G, root):
268
+ colors[node] = True
269
+ for v in pair_dict[node] if pairs is not None else G:
270
+ if colors[v]:
271
+ # If the user requested both directions of a pair, give it.
272
+ # Otherwise, just give one.
273
+ if pairs is not None and (node, v) in pairs:
274
+ yield (node, v), ancestors[uf[v]]
275
+ if pairs is None or (v, node) in pairs:
276
+ yield (v, node), ancestors[uf[v]]
277
+ if node != root:
278
+ parent = arbitrary_element(G.pred[node])
279
+ uf.union(parent, node)
280
+ ancestors[uf[parent]] = parent
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/matching.py ADDED
@@ -0,0 +1,1148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functions for computing and verifying matchings in a graph."""
2
+
3
+ from itertools import combinations, repeat
4
+
5
+ import networkx as nx
6
+ from networkx.utils import not_implemented_for
7
+
8
+ __all__ = [
9
+ "is_matching",
10
+ "is_maximal_matching",
11
+ "is_perfect_matching",
12
+ "max_weight_matching",
13
+ "min_weight_matching",
14
+ "maximal_matching",
15
+ ]
16
+
17
+
18
+ @not_implemented_for("multigraph")
19
+ @not_implemented_for("directed")
20
+ @nx._dispatchable
21
+ def maximal_matching(G):
22
+ r"""Find a maximal matching in the graph.
23
+
24
+ A matching is a subset of edges in which no node occurs more than once.
25
+ A maximal matching cannot add more edges and still be a matching.
26
+
27
+ Parameters
28
+ ----------
29
+ G : NetworkX graph
30
+ Undirected graph
31
+
32
+ Returns
33
+ -------
34
+ matching : set
35
+ A maximal matching of the graph.
36
+
37
+ Examples
38
+ --------
39
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
40
+ >>> sorted(nx.maximal_matching(G))
41
+ [(1, 2), (3, 5)]
42
+
43
+ Notes
44
+ -----
45
+ The algorithm greedily selects a maximal matching M of the graph G
46
+ (i.e. no superset of M exists). It runs in $O(|E|)$ time.
47
+ """
48
+ matching = set()
49
+ nodes = set()
50
+ for edge in G.edges():
51
+ # If the edge isn't covered, add it to the matching
52
+ # then remove neighborhood of u and v from consideration.
53
+ u, v = edge
54
+ if u not in nodes and v not in nodes and u != v:
55
+ matching.add(edge)
56
+ nodes.update(edge)
57
+ return matching
58
+
59
+
60
+ def matching_dict_to_set(matching):
61
+ """Converts matching dict format to matching set format
62
+
63
+ Converts a dictionary representing a matching (as returned by
64
+ :func:`max_weight_matching`) to a set representing a matching (as
65
+ returned by :func:`maximal_matching`).
66
+
67
+ In the definition of maximal matching adopted by NetworkX,
68
+ self-loops are not allowed, so the provided dictionary is expected
69
+ to never have any mapping from a key to itself. However, the
70
+ dictionary is expected to have mirrored key/value pairs, for
71
+ example, key ``u`` with value ``v`` and key ``v`` with value ``u``.
72
+
73
+ """
74
+ edges = set()
75
+ for edge in matching.items():
76
+ u, v = edge
77
+ if (v, u) in edges or edge in edges:
78
+ continue
79
+ if u == v:
80
+ raise nx.NetworkXError(f"Selfloops cannot appear in matchings {edge}")
81
+ edges.add(edge)
82
+ return edges
83
+
84
+
85
+ @nx._dispatchable
86
+ def is_matching(G, matching):
87
+ """Return True if ``matching`` is a valid matching of ``G``
88
+
89
+ A *matching* in a graph is a set of edges in which no two distinct
90
+ edges share a common endpoint. Each node is incident to at most one
91
+ edge in the matching. The edges are said to be independent.
92
+
93
+ Parameters
94
+ ----------
95
+ G : NetworkX graph
96
+
97
+ matching : dict or set
98
+ A dictionary or set representing a matching. If a dictionary, it
99
+ must have ``matching[u] == v`` and ``matching[v] == u`` for each
100
+ edge ``(u, v)`` in the matching. If a set, it must have elements
101
+ of the form ``(u, v)``, where ``(u, v)`` is an edge in the
102
+ matching.
103
+
104
+ Returns
105
+ -------
106
+ bool
107
+ Whether the given set or dictionary represents a valid matching
108
+ in the graph.
109
+
110
+ Raises
111
+ ------
112
+ NetworkXError
113
+ If the proposed matching has an edge to a node not in G.
114
+ Or if the matching is not a collection of 2-tuple edges.
115
+
116
+ Examples
117
+ --------
118
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)])
119
+ >>> nx.is_maximal_matching(G, {1: 3, 2: 4}) # using dict to represent matching
120
+ True
121
+
122
+ >>> nx.is_matching(G, {(1, 3), (2, 4)}) # using set to represent matching
123
+ True
124
+
125
+ """
126
+ if isinstance(matching, dict):
127
+ matching = matching_dict_to_set(matching)
128
+
129
+ nodes = set()
130
+ for edge in matching:
131
+ if len(edge) != 2:
132
+ raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
133
+ u, v = edge
134
+ if u not in G or v not in G:
135
+ raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
136
+ if u == v:
137
+ return False
138
+ if not G.has_edge(u, v):
139
+ return False
140
+ if u in nodes or v in nodes:
141
+ return False
142
+ nodes.update(edge)
143
+ return True
144
+
145
+
146
+ @nx._dispatchable
147
+ def is_maximal_matching(G, matching):
148
+ """Return True if ``matching`` is a maximal matching of ``G``
149
+
150
+ A *maximal matching* in a graph is a matching in which adding any
151
+ edge would cause the set to no longer be a valid matching.
152
+
153
+ Parameters
154
+ ----------
155
+ G : NetworkX graph
156
+
157
+ matching : dict or set
158
+ A dictionary or set representing a matching. If a dictionary, it
159
+ must have ``matching[u] == v`` and ``matching[v] == u`` for each
160
+ edge ``(u, v)`` in the matching. If a set, it must have elements
161
+ of the form ``(u, v)``, where ``(u, v)`` is an edge in the
162
+ matching.
163
+
164
+ Returns
165
+ -------
166
+ bool
167
+ Whether the given set or dictionary represents a valid maximal
168
+ matching in the graph.
169
+
170
+ Examples
171
+ --------
172
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)])
173
+ >>> nx.is_maximal_matching(G, {(1, 2), (3, 4)})
174
+ True
175
+
176
+ """
177
+ if isinstance(matching, dict):
178
+ matching = matching_dict_to_set(matching)
179
+ # If the given set is not a matching, then it is not a maximal matching.
180
+ edges = set()
181
+ nodes = set()
182
+ for edge in matching:
183
+ if len(edge) != 2:
184
+ raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
185
+ u, v = edge
186
+ if u not in G or v not in G:
187
+ raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
188
+ if u == v:
189
+ return False
190
+ if not G.has_edge(u, v):
191
+ return False
192
+ if u in nodes or v in nodes:
193
+ return False
194
+ nodes.update(edge)
195
+ edges.add(edge)
196
+ edges.add((v, u))
197
+ # A matching is maximal if adding any new edge from G to it
198
+ # causes the resulting set to match some node twice.
199
+ # Be careful to check for adding selfloops
200
+ for u, v in G.edges:
201
+ if (u, v) not in edges:
202
+ # could add edge (u, v) to edges and have a bigger matching
203
+ if u not in nodes and v not in nodes and u != v:
204
+ return False
205
+ return True
206
+
207
+
208
+ @nx._dispatchable
209
+ def is_perfect_matching(G, matching):
210
+ """Return True if ``matching`` is a perfect matching for ``G``
211
+
212
+ A *perfect matching* in a graph is a matching in which exactly one edge
213
+ is incident upon each vertex.
214
+
215
+ Parameters
216
+ ----------
217
+ G : NetworkX graph
218
+
219
+ matching : dict or set
220
+ A dictionary or set representing a matching. If a dictionary, it
221
+ must have ``matching[u] == v`` and ``matching[v] == u`` for each
222
+ edge ``(u, v)`` in the matching. If a set, it must have elements
223
+ of the form ``(u, v)``, where ``(u, v)`` is an edge in the
224
+ matching.
225
+
226
+ Returns
227
+ -------
228
+ bool
229
+ Whether the given set or dictionary represents a valid perfect
230
+ matching in the graph.
231
+
232
+ Examples
233
+ --------
234
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6)])
235
+ >>> my_match = {1: 2, 3: 5, 4: 6}
236
+ >>> nx.is_perfect_matching(G, my_match)
237
+ True
238
+
239
+ """
240
+ if isinstance(matching, dict):
241
+ matching = matching_dict_to_set(matching)
242
+
243
+ nodes = set()
244
+ for edge in matching:
245
+ if len(edge) != 2:
246
+ raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}")
247
+ u, v = edge
248
+ if u not in G or v not in G:
249
+ raise nx.NetworkXError(f"matching contains edge {edge} with node not in G")
250
+ if u == v:
251
+ return False
252
+ if not G.has_edge(u, v):
253
+ return False
254
+ if u in nodes or v in nodes:
255
+ return False
256
+ nodes.update(edge)
257
+ return len(nodes) == len(G)
258
+
259
+
260
+ @not_implemented_for("multigraph")
261
+ @not_implemented_for("directed")
262
+ @nx._dispatchable(edge_attrs="weight")
263
+ def min_weight_matching(G, weight="weight"):
264
+ """Compute a minimum-weight maximum-cardinality matching of `G`.
265
+
266
+ The minimum-weight maximum-cardinality matching is the matching
267
+ that has the minimum weight among all maximum-cardinality matchings.
268
+
269
+ Use the maximum-weight algorithm with edge weights subtracted
270
+ from the maximum weight of all edges.
271
+
272
+ A matching is a subset of edges in which no node occurs more than once.
273
+ The weight of a matching is the sum of the weights of its edges.
274
+ A maximal matching cannot add more edges and still be a matching.
275
+ The cardinality of a matching is the number of matched edges.
276
+
277
+ This method replaces the edge weights with 1 plus the maximum edge weight
278
+ minus the original edge weight.
279
+
280
+ new_weight = (max_weight + 1) - edge_weight
281
+
282
+ then runs :func:`max_weight_matching` with the new weights.
283
+ The max weight matching with these new weights corresponds
284
+ to the min weight matching using the original weights.
285
+ Adding 1 to the max edge weight keeps all edge weights positive
286
+ and as integers if they started as integers.
287
+
288
+ Read the documentation of `max_weight_matching` for more information.
289
+
290
+ Parameters
291
+ ----------
292
+ G : NetworkX graph
293
+ Undirected graph
294
+
295
+ weight: string, optional (default='weight')
296
+ Edge data key corresponding to the edge weight.
297
+ If key not found, uses 1 as weight.
298
+
299
+ Returns
300
+ -------
301
+ matching : set
302
+ A minimal weight matching of the graph.
303
+
304
+ See Also
305
+ --------
306
+ max_weight_matching
307
+ """
308
+ if len(G.edges) == 0:
309
+ return max_weight_matching(G, maxcardinality=True, weight=weight)
310
+ G_edges = G.edges(data=weight, default=1)
311
+ max_weight = 1 + max(w for _, _, w in G_edges)
312
+ InvG = nx.Graph()
313
+ edges = ((u, v, max_weight - w) for u, v, w in G_edges)
314
+ InvG.add_weighted_edges_from(edges, weight=weight)
315
+ return max_weight_matching(InvG, maxcardinality=True, weight=weight)
316
+
317
+
318
+ @not_implemented_for("multigraph")
319
+ @not_implemented_for("directed")
320
+ @nx._dispatchable(edge_attrs="weight")
321
+ def max_weight_matching(G, maxcardinality=False, weight="weight"):
322
+ """Compute a maximum-weighted matching of G.
323
+
324
+ A matching is a subset of edges in which no node occurs more than once.
325
+ The weight of a matching is the sum of the weights of its edges.
326
+ A maximal matching cannot add more edges and still be a matching.
327
+ The cardinality of a matching is the number of matched edges.
328
+
329
+ Parameters
330
+ ----------
331
+ G : NetworkX graph
332
+ Undirected graph
333
+
334
+ maxcardinality: bool, optional (default=False)
335
+ If maxcardinality is True, compute the maximum-cardinality matching
336
+ with maximum weight among all maximum-cardinality matchings.
337
+
338
+ weight: string, optional (default='weight')
339
+ Edge data key corresponding to the edge weight.
340
+ If key not found, uses 1 as weight.
341
+
342
+
343
+ Returns
344
+ -------
345
+ matching : set
346
+ A maximal matching of the graph.
347
+
348
+ Examples
349
+ --------
350
+ >>> G = nx.Graph()
351
+ >>> edges = [(1, 2, 6), (1, 3, 2), (2, 3, 1), (2, 4, 7), (3, 5, 9), (4, 5, 3)]
352
+ >>> G.add_weighted_edges_from(edges)
353
+ >>> sorted(nx.max_weight_matching(G))
354
+ [(2, 4), (5, 3)]
355
+
356
+ Notes
357
+ -----
358
+ If G has edges with weight attributes the edge data are used as
359
+ weight values else the weights are assumed to be 1.
360
+
361
+ This function takes time O(number_of_nodes ** 3).
362
+
363
+ If all edge weights are integers, the algorithm uses only integer
364
+ computations. If floating point weights are used, the algorithm
365
+ could return a slightly suboptimal matching due to numeric
366
+ precision errors.
367
+
368
+ This method is based on the "blossom" method for finding augmenting
369
+ paths and the "primal-dual" method for finding a matching of maximum
370
+ weight, both methods invented by Jack Edmonds [1]_.
371
+
372
+ Bipartite graphs can also be matched using the functions present in
373
+ :mod:`networkx.algorithms.bipartite.matching`.
374
+
375
+ References
376
+ ----------
377
+ .. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs",
378
+ Zvi Galil, ACM Computing Surveys, 1986.
379
+ """
380
+ #
381
+ # The algorithm is taken from "Efficient Algorithms for Finding Maximum
382
+ # Matching in Graphs" by Zvi Galil, ACM Computing Surveys, 1986.
383
+ # It is based on the "blossom" method for finding augmenting paths and
384
+ # the "primal-dual" method for finding a matching of maximum weight, both
385
+ # methods invented by Jack Edmonds.
386
+ #
387
+ # A C program for maximum weight matching by Ed Rothberg was used
388
+ # extensively to validate this new code.
389
+ #
390
+ # Many terms used in the code comments are explained in the paper
391
+ # by Galil. You will probably need the paper to make sense of this code.
392
+ #
393
+
394
+ class NoNode:
395
+ """Dummy value which is different from any node."""
396
+
397
+ class Blossom:
398
+ """Representation of a non-trivial blossom or sub-blossom."""
399
+
400
+ __slots__ = ["childs", "edges", "mybestedges"]
401
+
402
+ # b.childs is an ordered list of b's sub-blossoms, starting with
403
+ # the base and going round the blossom.
404
+
405
+ # b.edges is the list of b's connecting edges, such that
406
+ # b.edges[i] = (v, w) where v is a vertex in b.childs[i]
407
+ # and w is a vertex in b.childs[wrap(i+1)].
408
+
409
+ # If b is a top-level S-blossom,
410
+ # b.mybestedges is a list of least-slack edges to neighboring
411
+ # S-blossoms, or None if no such list has been computed yet.
412
+ # This is used for efficient computation of delta3.
413
+
414
+ # Generate the blossom's leaf vertices.
415
+ def leaves(self):
416
+ stack = [*self.childs]
417
+ while stack:
418
+ t = stack.pop()
419
+ if isinstance(t, Blossom):
420
+ stack.extend(t.childs)
421
+ else:
422
+ yield t
423
+
424
+ # Get a list of vertices.
425
+ gnodes = list(G)
426
+ if not gnodes:
427
+ return set() # don't bother with empty graphs
428
+
429
+ # Find the maximum edge weight.
430
+ maxweight = 0
431
+ allinteger = True
432
+ for i, j, d in G.edges(data=True):
433
+ wt = d.get(weight, 1)
434
+ if i != j and wt > maxweight:
435
+ maxweight = wt
436
+ allinteger = allinteger and (str(type(wt)).split("'")[1] in ("int", "long"))
437
+
438
+ # If v is a matched vertex, mate[v] is its partner vertex.
439
+ # If v is a single vertex, v does not occur as a key in mate.
440
+ # Initially all vertices are single; updated during augmentation.
441
+ mate = {}
442
+
443
+ # If b is a top-level blossom,
444
+ # label.get(b) is None if b is unlabeled (free),
445
+ # 1 if b is an S-blossom,
446
+ # 2 if b is a T-blossom.
447
+ # The label of a vertex is found by looking at the label of its top-level
448
+ # containing blossom.
449
+ # If v is a vertex inside a T-blossom, label[v] is 2 iff v is reachable
450
+ # from an S-vertex outside the blossom.
451
+ # Labels are assigned during a stage and reset after each augmentation.
452
+ label = {}
453
+
454
+ # If b is a labeled top-level blossom,
455
+ # labeledge[b] = (v, w) is the edge through which b obtained its label
456
+ # such that w is a vertex in b, or None if b's base vertex is single.
457
+ # If w is a vertex inside a T-blossom and label[w] == 2,
458
+ # labeledge[w] = (v, w) is an edge through which w is reachable from
459
+ # outside the blossom.
460
+ labeledge = {}
461
+
462
+ # If v is a vertex, inblossom[v] is the top-level blossom to which v
463
+ # belongs.
464
+ # If v is a top-level vertex, inblossom[v] == v since v is itself
465
+ # a (trivial) top-level blossom.
466
+ # Initially all vertices are top-level trivial blossoms.
467
+ inblossom = dict(zip(gnodes, gnodes))
468
+
469
+ # If b is a sub-blossom,
470
+ # blossomparent[b] is its immediate parent (sub-)blossom.
471
+ # If b is a top-level blossom, blossomparent[b] is None.
472
+ blossomparent = dict(zip(gnodes, repeat(None)))
473
+
474
+ # If b is a (sub-)blossom,
475
+ # blossombase[b] is its base VERTEX (i.e. recursive sub-blossom).
476
+ blossombase = dict(zip(gnodes, gnodes))
477
+
478
+ # If w is a free vertex (or an unreached vertex inside a T-blossom),
479
+ # bestedge[w] = (v, w) is the least-slack edge from an S-vertex,
480
+ # or None if there is no such edge.
481
+ # If b is a (possibly trivial) top-level S-blossom,
482
+ # bestedge[b] = (v, w) is the least-slack edge to a different S-blossom
483
+ # (v inside b), or None if there is no such edge.
484
+ # This is used for efficient computation of delta2 and delta3.
485
+ bestedge = {}
486
+
487
+ # If v is a vertex,
488
+ # dualvar[v] = 2 * u(v) where u(v) is the v's variable in the dual
489
+ # optimization problem (if all edge weights are integers, multiplication
490
+ # by two ensures that all values remain integers throughout the algorithm).
491
+ # Initially, u(v) = maxweight / 2.
492
+ dualvar = dict(zip(gnodes, repeat(maxweight)))
493
+
494
+ # If b is a non-trivial blossom,
495
+ # blossomdual[b] = z(b) where z(b) is b's variable in the dual
496
+ # optimization problem.
497
+ blossomdual = {}
498
+
499
+ # If (v, w) in allowedge or (w, v) in allowedg, then the edge
500
+ # (v, w) is known to have zero slack in the optimization problem;
501
+ # otherwise the edge may or may not have zero slack.
502
+ allowedge = {}
503
+
504
+ # Queue of newly discovered S-vertices.
505
+ queue = []
506
+
507
+ # Return 2 * slack of edge (v, w) (does not work inside blossoms).
508
+ def slack(v, w):
509
+ return dualvar[v] + dualvar[w] - 2 * G[v][w].get(weight, 1)
510
+
511
+ # Assign label t to the top-level blossom containing vertex w,
512
+ # coming through an edge from vertex v.
513
+ def assignLabel(w, t, v):
514
+ b = inblossom[w]
515
+ assert label.get(w) is None and label.get(b) is None
516
+ label[w] = label[b] = t
517
+ if v is not None:
518
+ labeledge[w] = labeledge[b] = (v, w)
519
+ else:
520
+ labeledge[w] = labeledge[b] = None
521
+ bestedge[w] = bestedge[b] = None
522
+ if t == 1:
523
+ # b became an S-vertex/blossom; add it(s vertices) to the queue.
524
+ if isinstance(b, Blossom):
525
+ queue.extend(b.leaves())
526
+ else:
527
+ queue.append(b)
528
+ elif t == 2:
529
+ # b became a T-vertex/blossom; assign label S to its mate.
530
+ # (If b is a non-trivial blossom, its base is the only vertex
531
+ # with an external mate.)
532
+ base = blossombase[b]
533
+ assignLabel(mate[base], 1, base)
534
+
535
+ # Trace back from vertices v and w to discover either a new blossom
536
+ # or an augmenting path. Return the base vertex of the new blossom,
537
+ # or NoNode if an augmenting path was found.
538
+ def scanBlossom(v, w):
539
+ # Trace back from v and w, placing breadcrumbs as we go.
540
+ path = []
541
+ base = NoNode
542
+ while v is not NoNode:
543
+ # Look for a breadcrumb in v's blossom or put a new breadcrumb.
544
+ b = inblossom[v]
545
+ if label[b] & 4:
546
+ base = blossombase[b]
547
+ break
548
+ assert label[b] == 1
549
+ path.append(b)
550
+ label[b] = 5
551
+ # Trace one step back.
552
+ if labeledge[b] is None:
553
+ # The base of blossom b is single; stop tracing this path.
554
+ assert blossombase[b] not in mate
555
+ v = NoNode
556
+ else:
557
+ assert labeledge[b][0] == mate[blossombase[b]]
558
+ v = labeledge[b][0]
559
+ b = inblossom[v]
560
+ assert label[b] == 2
561
+ # b is a T-blossom; trace one more step back.
562
+ v = labeledge[b][0]
563
+ # Swap v and w so that we alternate between both paths.
564
+ if w is not NoNode:
565
+ v, w = w, v
566
+ # Remove breadcrumbs.
567
+ for b in path:
568
+ label[b] = 1
569
+ # Return base vertex, if we found one.
570
+ return base
571
+
572
+ # Construct a new blossom with given base, through S-vertices v and w.
573
+ # Label the new blossom as S; set its dual variable to zero;
574
+ # relabel its T-vertices to S and add them to the queue.
575
+ def addBlossom(base, v, w):
576
+ bb = inblossom[base]
577
+ bv = inblossom[v]
578
+ bw = inblossom[w]
579
+ # Create blossom.
580
+ b = Blossom()
581
+ blossombase[b] = base
582
+ blossomparent[b] = None
583
+ blossomparent[bb] = b
584
+ # Make list of sub-blossoms and their interconnecting edge endpoints.
585
+ b.childs = path = []
586
+ b.edges = edgs = [(v, w)]
587
+ # Trace back from v to base.
588
+ while bv != bb:
589
+ # Add bv to the new blossom.
590
+ blossomparent[bv] = b
591
+ path.append(bv)
592
+ edgs.append(labeledge[bv])
593
+ assert label[bv] == 2 or (
594
+ label[bv] == 1 and labeledge[bv][0] == mate[blossombase[bv]]
595
+ )
596
+ # Trace one step back.
597
+ v = labeledge[bv][0]
598
+ bv = inblossom[v]
599
+ # Add base sub-blossom; reverse lists.
600
+ path.append(bb)
601
+ path.reverse()
602
+ edgs.reverse()
603
+ # Trace back from w to base.
604
+ while bw != bb:
605
+ # Add bw to the new blossom.
606
+ blossomparent[bw] = b
607
+ path.append(bw)
608
+ edgs.append((labeledge[bw][1], labeledge[bw][0]))
609
+ assert label[bw] == 2 or (
610
+ label[bw] == 1 and labeledge[bw][0] == mate[blossombase[bw]]
611
+ )
612
+ # Trace one step back.
613
+ w = labeledge[bw][0]
614
+ bw = inblossom[w]
615
+ # Set label to S.
616
+ assert label[bb] == 1
617
+ label[b] = 1
618
+ labeledge[b] = labeledge[bb]
619
+ # Set dual variable to zero.
620
+ blossomdual[b] = 0
621
+ # Relabel vertices.
622
+ for v in b.leaves():
623
+ if label[inblossom[v]] == 2:
624
+ # This T-vertex now turns into an S-vertex because it becomes
625
+ # part of an S-blossom; add it to the queue.
626
+ queue.append(v)
627
+ inblossom[v] = b
628
+ # Compute b.mybestedges.
629
+ bestedgeto = {}
630
+ for bv in path:
631
+ if isinstance(bv, Blossom):
632
+ if bv.mybestedges is not None:
633
+ # Walk this subblossom's least-slack edges.
634
+ nblist = bv.mybestedges
635
+ # The sub-blossom won't need this data again.
636
+ bv.mybestedges = None
637
+ else:
638
+ # This subblossom does not have a list of least-slack
639
+ # edges; get the information from the vertices.
640
+ nblist = [
641
+ (v, w) for v in bv.leaves() for w in G.neighbors(v) if v != w
642
+ ]
643
+ else:
644
+ nblist = [(bv, w) for w in G.neighbors(bv) if bv != w]
645
+ for k in nblist:
646
+ (i, j) = k
647
+ if inblossom[j] == b:
648
+ i, j = j, i
649
+ bj = inblossom[j]
650
+ if (
651
+ bj != b
652
+ and label.get(bj) == 1
653
+ and ((bj not in bestedgeto) or slack(i, j) < slack(*bestedgeto[bj]))
654
+ ):
655
+ bestedgeto[bj] = k
656
+ # Forget about least-slack edge of the subblossom.
657
+ bestedge[bv] = None
658
+ b.mybestedges = list(bestedgeto.values())
659
+ # Select bestedge[b].
660
+ mybestedge = None
661
+ bestedge[b] = None
662
+ for k in b.mybestedges:
663
+ kslack = slack(*k)
664
+ if mybestedge is None or kslack < mybestslack:
665
+ mybestedge = k
666
+ mybestslack = kslack
667
+ bestedge[b] = mybestedge
668
+
669
+ # Expand the given top-level blossom.
670
+ def expandBlossom(b, endstage):
671
+ # This is an obnoxiously complicated recursive function for the sake of
672
+ # a stack-transformation. So, we hack around the complexity by using
673
+ # a trampoline pattern. By yielding the arguments to each recursive
674
+ # call, we keep the actual callstack flat.
675
+
676
+ def _recurse(b, endstage):
677
+ # Convert sub-blossoms into top-level blossoms.
678
+ for s in b.childs:
679
+ blossomparent[s] = None
680
+ if isinstance(s, Blossom):
681
+ if endstage and blossomdual[s] == 0:
682
+ # Recursively expand this sub-blossom.
683
+ yield s
684
+ else:
685
+ for v in s.leaves():
686
+ inblossom[v] = s
687
+ else:
688
+ inblossom[s] = s
689
+ # If we expand a T-blossom during a stage, its sub-blossoms must be
690
+ # relabeled.
691
+ if (not endstage) and label.get(b) == 2:
692
+ # Start at the sub-blossom through which the expanding
693
+ # blossom obtained its label, and relabel sub-blossoms untili
694
+ # we reach the base.
695
+ # Figure out through which sub-blossom the expanding blossom
696
+ # obtained its label initially.
697
+ entrychild = inblossom[labeledge[b][1]]
698
+ # Decide in which direction we will go round the blossom.
699
+ j = b.childs.index(entrychild)
700
+ if j & 1:
701
+ # Start index is odd; go forward and wrap.
702
+ j -= len(b.childs)
703
+ jstep = 1
704
+ else:
705
+ # Start index is even; go backward.
706
+ jstep = -1
707
+ # Move along the blossom until we get to the base.
708
+ v, w = labeledge[b]
709
+ while j != 0:
710
+ # Relabel the T-sub-blossom.
711
+ if jstep == 1:
712
+ p, q = b.edges[j]
713
+ else:
714
+ q, p = b.edges[j - 1]
715
+ label[w] = None
716
+ label[q] = None
717
+ assignLabel(w, 2, v)
718
+ # Step to the next S-sub-blossom and note its forward edge.
719
+ allowedge[(p, q)] = allowedge[(q, p)] = True
720
+ j += jstep
721
+ if jstep == 1:
722
+ v, w = b.edges[j]
723
+ else:
724
+ w, v = b.edges[j - 1]
725
+ # Step to the next T-sub-blossom.
726
+ allowedge[(v, w)] = allowedge[(w, v)] = True
727
+ j += jstep
728
+ # Relabel the base T-sub-blossom WITHOUT stepping through to
729
+ # its mate (so don't call assignLabel).
730
+ bw = b.childs[j]
731
+ label[w] = label[bw] = 2
732
+ labeledge[w] = labeledge[bw] = (v, w)
733
+ bestedge[bw] = None
734
+ # Continue along the blossom until we get back to entrychild.
735
+ j += jstep
736
+ while b.childs[j] != entrychild:
737
+ # Examine the vertices of the sub-blossom to see whether
738
+ # it is reachable from a neighboring S-vertex outside the
739
+ # expanding blossom.
740
+ bv = b.childs[j]
741
+ if label.get(bv) == 1:
742
+ # This sub-blossom just got label S through one of its
743
+ # neighbors; leave it be.
744
+ j += jstep
745
+ continue
746
+ if isinstance(bv, Blossom):
747
+ for v in bv.leaves():
748
+ if label.get(v):
749
+ break
750
+ else:
751
+ v = bv
752
+ # If the sub-blossom contains a reachable vertex, assign
753
+ # label T to the sub-blossom.
754
+ if label.get(v):
755
+ assert label[v] == 2
756
+ assert inblossom[v] == bv
757
+ label[v] = None
758
+ label[mate[blossombase[bv]]] = None
759
+ assignLabel(v, 2, labeledge[v][0])
760
+ j += jstep
761
+ # Remove the expanded blossom entirely.
762
+ label.pop(b, None)
763
+ labeledge.pop(b, None)
764
+ bestedge.pop(b, None)
765
+ del blossomparent[b]
766
+ del blossombase[b]
767
+ del blossomdual[b]
768
+
769
+ # Now, we apply the trampoline pattern. We simulate a recursive
770
+ # callstack by maintaining a stack of generators, each yielding a
771
+ # sequence of function arguments. We grow the stack by appending a call
772
+ # to _recurse on each argument tuple, and shrink the stack whenever a
773
+ # generator is exhausted.
774
+ stack = [_recurse(b, endstage)]
775
+ while stack:
776
+ top = stack[-1]
777
+ for s in top:
778
+ stack.append(_recurse(s, endstage))
779
+ break
780
+ else:
781
+ stack.pop()
782
+
783
+ # Swap matched/unmatched edges over an alternating path through blossom b
784
+ # between vertex v and the base vertex. Keep blossom bookkeeping
785
+ # consistent.
786
+ def augmentBlossom(b, v):
787
+ # This is an obnoxiously complicated recursive function for the sake of
788
+ # a stack-transformation. So, we hack around the complexity by using
789
+ # a trampoline pattern. By yielding the arguments to each recursive
790
+ # call, we keep the actual callstack flat.
791
+
792
+ def _recurse(b, v):
793
+ # Bubble up through the blossom tree from vertex v to an immediate
794
+ # sub-blossom of b.
795
+ t = v
796
+ while blossomparent[t] != b:
797
+ t = blossomparent[t]
798
+ # Recursively deal with the first sub-blossom.
799
+ if isinstance(t, Blossom):
800
+ yield (t, v)
801
+ # Decide in which direction we will go round the blossom.
802
+ i = j = b.childs.index(t)
803
+ if i & 1:
804
+ # Start index is odd; go forward and wrap.
805
+ j -= len(b.childs)
806
+ jstep = 1
807
+ else:
808
+ # Start index is even; go backward.
809
+ jstep = -1
810
+ # Move along the blossom until we get to the base.
811
+ while j != 0:
812
+ # Step to the next sub-blossom and augment it recursively.
813
+ j += jstep
814
+ t = b.childs[j]
815
+ if jstep == 1:
816
+ w, x = b.edges[j]
817
+ else:
818
+ x, w = b.edges[j - 1]
819
+ if isinstance(t, Blossom):
820
+ yield (t, w)
821
+ # Step to the next sub-blossom and augment it recursively.
822
+ j += jstep
823
+ t = b.childs[j]
824
+ if isinstance(t, Blossom):
825
+ yield (t, x)
826
+ # Match the edge connecting those sub-blossoms.
827
+ mate[w] = x
828
+ mate[x] = w
829
+ # Rotate the list of sub-blossoms to put the new base at the front.
830
+ b.childs = b.childs[i:] + b.childs[:i]
831
+ b.edges = b.edges[i:] + b.edges[:i]
832
+ blossombase[b] = blossombase[b.childs[0]]
833
+ assert blossombase[b] == v
834
+
835
+ # Now, we apply the trampoline pattern. We simulate a recursive
836
+ # callstack by maintaining a stack of generators, each yielding a
837
+ # sequence of function arguments. We grow the stack by appending a call
838
+ # to _recurse on each argument tuple, and shrink the stack whenever a
839
+ # generator is exhausted.
840
+ stack = [_recurse(b, v)]
841
+ while stack:
842
+ top = stack[-1]
843
+ for args in top:
844
+ stack.append(_recurse(*args))
845
+ break
846
+ else:
847
+ stack.pop()
848
+
849
+ # Swap matched/unmatched edges over an alternating path between two
850
+ # single vertices. The augmenting path runs through S-vertices v and w.
851
+ def augmentMatching(v, w):
852
+ for s, j in ((v, w), (w, v)):
853
+ # Match vertex s to vertex j. Then trace back from s
854
+ # until we find a single vertex, swapping matched and unmatched
855
+ # edges as we go.
856
+ while 1:
857
+ bs = inblossom[s]
858
+ assert label[bs] == 1
859
+ assert (labeledge[bs] is None and blossombase[bs] not in mate) or (
860
+ labeledge[bs][0] == mate[blossombase[bs]]
861
+ )
862
+ # Augment through the S-blossom from s to base.
863
+ if isinstance(bs, Blossom):
864
+ augmentBlossom(bs, s)
865
+ # Update mate[s]
866
+ mate[s] = j
867
+ # Trace one step back.
868
+ if labeledge[bs] is None:
869
+ # Reached single vertex; stop.
870
+ break
871
+ t = labeledge[bs][0]
872
+ bt = inblossom[t]
873
+ assert label[bt] == 2
874
+ # Trace one more step back.
875
+ s, j = labeledge[bt]
876
+ # Augment through the T-blossom from j to base.
877
+ assert blossombase[bt] == t
878
+ if isinstance(bt, Blossom):
879
+ augmentBlossom(bt, j)
880
+ # Update mate[j]
881
+ mate[j] = s
882
+
883
+ # Verify that the optimum solution has been reached.
884
+ def verifyOptimum():
885
+ if maxcardinality:
886
+ # Vertices may have negative dual;
887
+ # find a constant non-negative number to add to all vertex duals.
888
+ vdualoffset = max(0, -min(dualvar.values()))
889
+ else:
890
+ vdualoffset = 0
891
+ # 0. all dual variables are non-negative
892
+ assert min(dualvar.values()) + vdualoffset >= 0
893
+ assert len(blossomdual) == 0 or min(blossomdual.values()) >= 0
894
+ # 0. all edges have non-negative slack and
895
+ # 1. all matched edges have zero slack;
896
+ for i, j, d in G.edges(data=True):
897
+ wt = d.get(weight, 1)
898
+ if i == j:
899
+ continue # ignore self-loops
900
+ s = dualvar[i] + dualvar[j] - 2 * wt
901
+ iblossoms = [i]
902
+ jblossoms = [j]
903
+ while blossomparent[iblossoms[-1]] is not None:
904
+ iblossoms.append(blossomparent[iblossoms[-1]])
905
+ while blossomparent[jblossoms[-1]] is not None:
906
+ jblossoms.append(blossomparent[jblossoms[-1]])
907
+ iblossoms.reverse()
908
+ jblossoms.reverse()
909
+ for bi, bj in zip(iblossoms, jblossoms):
910
+ if bi != bj:
911
+ break
912
+ s += 2 * blossomdual[bi]
913
+ assert s >= 0
914
+ if mate.get(i) == j or mate.get(j) == i:
915
+ assert mate[i] == j and mate[j] == i
916
+ assert s == 0
917
+ # 2. all single vertices have zero dual value;
918
+ for v in gnodes:
919
+ assert (v in mate) or dualvar[v] + vdualoffset == 0
920
+ # 3. all blossoms with positive dual value are full.
921
+ for b in blossomdual:
922
+ if blossomdual[b] > 0:
923
+ assert len(b.edges) % 2 == 1
924
+ for i, j in b.edges[1::2]:
925
+ assert mate[i] == j and mate[j] == i
926
+ # Ok.
927
+
928
+ # Main loop: continue until no further improvement is possible.
929
+ while 1:
930
+ # Each iteration of this loop is a "stage".
931
+ # A stage finds an augmenting path and uses that to improve
932
+ # the matching.
933
+
934
+ # Remove labels from top-level blossoms/vertices.
935
+ label.clear()
936
+ labeledge.clear()
937
+
938
+ # Forget all about least-slack edges.
939
+ bestedge.clear()
940
+ for b in blossomdual:
941
+ b.mybestedges = None
942
+
943
+ # Loss of labeling means that we can not be sure that currently
944
+ # allowable edges remain allowable throughout this stage.
945
+ allowedge.clear()
946
+
947
+ # Make queue empty.
948
+ queue[:] = []
949
+
950
+ # Label single blossoms/vertices with S and put them in the queue.
951
+ for v in gnodes:
952
+ if (v not in mate) and label.get(inblossom[v]) is None:
953
+ assignLabel(v, 1, None)
954
+
955
+ # Loop until we succeed in augmenting the matching.
956
+ augmented = 0
957
+ while 1:
958
+ # Each iteration of this loop is a "substage".
959
+ # A substage tries to find an augmenting path;
960
+ # if found, the path is used to improve the matching and
961
+ # the stage ends. If there is no augmenting path, the
962
+ # primal-dual method is used to pump some slack out of
963
+ # the dual variables.
964
+
965
+ # Continue labeling until all vertices which are reachable
966
+ # through an alternating path have got a label.
967
+ while queue and not augmented:
968
+ # Take an S vertex from the queue.
969
+ v = queue.pop()
970
+ assert label[inblossom[v]] == 1
971
+
972
+ # Scan its neighbors:
973
+ for w in G.neighbors(v):
974
+ if w == v:
975
+ continue # ignore self-loops
976
+ # w is a neighbor to v
977
+ bv = inblossom[v]
978
+ bw = inblossom[w]
979
+ if bv == bw:
980
+ # this edge is internal to a blossom; ignore it
981
+ continue
982
+ if (v, w) not in allowedge:
983
+ kslack = slack(v, w)
984
+ if kslack <= 0:
985
+ # edge k has zero slack => it is allowable
986
+ allowedge[(v, w)] = allowedge[(w, v)] = True
987
+ if (v, w) in allowedge:
988
+ if label.get(bw) is None:
989
+ # (C1) w is a free vertex;
990
+ # label w with T and label its mate with S (R12).
991
+ assignLabel(w, 2, v)
992
+ elif label.get(bw) == 1:
993
+ # (C2) w is an S-vertex (not in the same blossom);
994
+ # follow back-links to discover either an
995
+ # augmenting path or a new blossom.
996
+ base = scanBlossom(v, w)
997
+ if base is not NoNode:
998
+ # Found a new blossom; add it to the blossom
999
+ # bookkeeping and turn it into an S-blossom.
1000
+ addBlossom(base, v, w)
1001
+ else:
1002
+ # Found an augmenting path; augment the
1003
+ # matching and end this stage.
1004
+ augmentMatching(v, w)
1005
+ augmented = 1
1006
+ break
1007
+ elif label.get(w) is None:
1008
+ # w is inside a T-blossom, but w itself has not
1009
+ # yet been reached from outside the blossom;
1010
+ # mark it as reached (we need this to relabel
1011
+ # during T-blossom expansion).
1012
+ assert label[bw] == 2
1013
+ label[w] = 2
1014
+ labeledge[w] = (v, w)
1015
+ elif label.get(bw) == 1:
1016
+ # keep track of the least-slack non-allowable edge to
1017
+ # a different S-blossom.
1018
+ if bestedge.get(bv) is None or kslack < slack(*bestedge[bv]):
1019
+ bestedge[bv] = (v, w)
1020
+ elif label.get(w) is None:
1021
+ # w is a free vertex (or an unreached vertex inside
1022
+ # a T-blossom) but we can not reach it yet;
1023
+ # keep track of the least-slack edge that reaches w.
1024
+ if bestedge.get(w) is None or kslack < slack(*bestedge[w]):
1025
+ bestedge[w] = (v, w)
1026
+
1027
+ if augmented:
1028
+ break
1029
+
1030
+ # There is no augmenting path under these constraints;
1031
+ # compute delta and reduce slack in the optimization problem.
1032
+ # (Note that our vertex dual variables, edge slacks and delta's
1033
+ # are pre-multiplied by two.)
1034
+ deltatype = -1
1035
+ delta = deltaedge = deltablossom = None
1036
+
1037
+ # Compute delta1: the minimum value of any vertex dual.
1038
+ if not maxcardinality:
1039
+ deltatype = 1
1040
+ delta = min(dualvar.values())
1041
+
1042
+ # Compute delta2: the minimum slack on any edge between
1043
+ # an S-vertex and a free vertex.
1044
+ for v in G.nodes():
1045
+ if label.get(inblossom[v]) is None and bestedge.get(v) is not None:
1046
+ d = slack(*bestedge[v])
1047
+ if deltatype == -1 or d < delta:
1048
+ delta = d
1049
+ deltatype = 2
1050
+ deltaedge = bestedge[v]
1051
+
1052
+ # Compute delta3: half the minimum slack on any edge between
1053
+ # a pair of S-blossoms.
1054
+ for b in blossomparent:
1055
+ if (
1056
+ blossomparent[b] is None
1057
+ and label.get(b) == 1
1058
+ and bestedge.get(b) is not None
1059
+ ):
1060
+ kslack = slack(*bestedge[b])
1061
+ if allinteger:
1062
+ assert (kslack % 2) == 0
1063
+ d = kslack // 2
1064
+ else:
1065
+ d = kslack / 2.0
1066
+ if deltatype == -1 or d < delta:
1067
+ delta = d
1068
+ deltatype = 3
1069
+ deltaedge = bestedge[b]
1070
+
1071
+ # Compute delta4: minimum z variable of any T-blossom.
1072
+ for b in blossomdual:
1073
+ if (
1074
+ blossomparent[b] is None
1075
+ and label.get(b) == 2
1076
+ and (deltatype == -1 or blossomdual[b] < delta)
1077
+ ):
1078
+ delta = blossomdual[b]
1079
+ deltatype = 4
1080
+ deltablossom = b
1081
+
1082
+ if deltatype == -1:
1083
+ # No further improvement possible; max-cardinality optimum
1084
+ # reached. Do a final delta update to make the optimum
1085
+ # verifiable.
1086
+ assert maxcardinality
1087
+ deltatype = 1
1088
+ delta = max(0, min(dualvar.values()))
1089
+
1090
+ # Update dual variables according to delta.
1091
+ for v in gnodes:
1092
+ if label.get(inblossom[v]) == 1:
1093
+ # S-vertex: 2*u = 2*u - 2*delta
1094
+ dualvar[v] -= delta
1095
+ elif label.get(inblossom[v]) == 2:
1096
+ # T-vertex: 2*u = 2*u + 2*delta
1097
+ dualvar[v] += delta
1098
+ for b in blossomdual:
1099
+ if blossomparent[b] is None:
1100
+ if label.get(b) == 1:
1101
+ # top-level S-blossom: z = z + 2*delta
1102
+ blossomdual[b] += delta
1103
+ elif label.get(b) == 2:
1104
+ # top-level T-blossom: z = z - 2*delta
1105
+ blossomdual[b] -= delta
1106
+
1107
+ # Take action at the point where minimum delta occurred.
1108
+ if deltatype == 1:
1109
+ # No further improvement possible; optimum reached.
1110
+ break
1111
+ elif deltatype == 2:
1112
+ # Use the least-slack edge to continue the search.
1113
+ (v, w) = deltaedge
1114
+ assert label[inblossom[v]] == 1
1115
+ allowedge[(v, w)] = allowedge[(w, v)] = True
1116
+ queue.append(v)
1117
+ elif deltatype == 3:
1118
+ # Use the least-slack edge to continue the search.
1119
+ (v, w) = deltaedge
1120
+ allowedge[(v, w)] = allowedge[(w, v)] = True
1121
+ assert label[inblossom[v]] == 1
1122
+ queue.append(v)
1123
+ elif deltatype == 4:
1124
+ # Expand the least-z blossom.
1125
+ expandBlossom(deltablossom, False)
1126
+
1127
+ # End of a this substage.
1128
+
1129
+ # Paranoia check that the matching is symmetric.
1130
+ for v in mate:
1131
+ assert mate[mate[v]] == v
1132
+
1133
+ # Stop when no more augmenting path can be found.
1134
+ if not augmented:
1135
+ break
1136
+
1137
+ # End of a stage; expand all S-blossoms which have zero dual.
1138
+ for b in list(blossomdual.keys()):
1139
+ if b not in blossomdual:
1140
+ continue # already expanded
1141
+ if blossomparent[b] is None and label.get(b) == 1 and blossomdual[b] == 0:
1142
+ expandBlossom(b, True)
1143
+
1144
+ # Verify that we reached the optimum solution (only for integer weights).
1145
+ if allinteger:
1146
+ verifyOptimum()
1147
+
1148
+ return matching_dict_to_set(mate)
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Subpackages related to graph-minor problems.
3
+
4
+ In graph theory, an undirected graph H is called a minor of the graph G if H
5
+ can be formed from G by deleting edges and vertices and by contracting edges
6
+ [1]_.
7
+
8
+ References
9
+ ----------
10
+ .. [1] https://en.wikipedia.org/wiki/Graph_minor
11
+ """
12
+
13
+ from networkx.algorithms.minors.contraction import (
14
+ contracted_edge,
15
+ contracted_nodes,
16
+ equivalence_classes,
17
+ identified_nodes,
18
+ quotient_graph,
19
+ )
20
+
21
+ __all__ = [
22
+ "contracted_edge",
23
+ "contracted_nodes",
24
+ "equivalence_classes",
25
+ "identified_nodes",
26
+ "quotient_graph",
27
+ ]
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/contraction.py ADDED
@@ -0,0 +1,738 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Provides functions for computing minors of a graph."""
2
+
3
+ from itertools import chain, combinations, permutations, product
4
+
5
+ import networkx as nx
6
+ from networkx import density
7
+ from networkx.exception import NetworkXException
8
+ from networkx.utils import arbitrary_element
9
+
10
+ __all__ = [
11
+ "contracted_edge",
12
+ "contracted_nodes",
13
+ "equivalence_classes",
14
+ "identified_nodes",
15
+ "quotient_graph",
16
+ ]
17
+
18
+ chaini = chain.from_iterable
19
+
20
+
21
+ def equivalence_classes(iterable, relation):
22
+ """Returns equivalence classes of `relation` when applied to `iterable`.
23
+
24
+ The equivalence classes, or blocks, consist of objects from `iterable`
25
+ which are all equivalent. They are defined to be equivalent if the
26
+ `relation` function returns `True` when passed any two objects from that
27
+ class, and `False` otherwise. To define an equivalence relation the
28
+ function must be reflexive, symmetric and transitive.
29
+
30
+ Parameters
31
+ ----------
32
+ iterable : list, tuple, or set
33
+ An iterable of elements/nodes.
34
+
35
+ relation : function
36
+ A Boolean-valued function that implements an equivalence relation
37
+ (reflexive, symmetric, transitive binary relation) on the elements
38
+ of `iterable` - it must take two elements and return `True` if
39
+ they are related, or `False` if not.
40
+
41
+ Returns
42
+ -------
43
+ set of frozensets
44
+ A set of frozensets representing the partition induced by the equivalence
45
+ relation function `relation` on the elements of `iterable`. Each
46
+ member set in the return set represents an equivalence class, or
47
+ block, of the partition.
48
+
49
+ Duplicate elements will be ignored so it makes the most sense for
50
+ `iterable` to be a :class:`set`.
51
+
52
+ Notes
53
+ -----
54
+ This function does not check that `relation` represents an equivalence
55
+ relation. You can check that your equivalence classes provide a partition
56
+ using `is_partition`.
57
+
58
+ Examples
59
+ --------
60
+ Let `X` be the set of integers from `0` to `9`, and consider an equivalence
61
+ relation `R` on `X` of congruence modulo `3`: this means that two integers
62
+ `x` and `y` in `X` are equivalent under `R` if they leave the same
63
+ remainder when divided by `3`, i.e. `(x - y) mod 3 = 0`.
64
+
65
+ The equivalence classes of this relation are `{0, 3, 6, 9}`, `{1, 4, 7}`,
66
+ `{2, 5, 8}`: `0`, `3`, `6`, `9` are all divisible by `3` and leave zero
67
+ remainder; `1`, `4`, `7` leave remainder `1`; while `2`, `5` and `8` leave
68
+ remainder `2`. We can see this by calling `equivalence_classes` with
69
+ `X` and a function implementation of `R`.
70
+
71
+ >>> X = set(range(10))
72
+ >>> def mod3(x, y):
73
+ ... return (x - y) % 3 == 0
74
+ >>> equivalence_classes(X, mod3) # doctest: +SKIP
75
+ {frozenset({1, 4, 7}), frozenset({8, 2, 5}), frozenset({0, 9, 3, 6})}
76
+ """
77
+ # For simplicity of implementation, we initialize the return value as a
78
+ # list of lists, then convert it to a set of sets at the end of the
79
+ # function.
80
+ blocks = []
81
+ # Determine the equivalence class for each element of the iterable.
82
+ for y in iterable:
83
+ # Each element y must be in *exactly one* equivalence class.
84
+ #
85
+ # Each block is guaranteed to be non-empty
86
+ for block in blocks:
87
+ x = arbitrary_element(block)
88
+ if relation(x, y):
89
+ block.append(y)
90
+ break
91
+ else:
92
+ # If the element y is not part of any known equivalence class, it
93
+ # must be in its own, so we create a new singleton equivalence
94
+ # class for it.
95
+ blocks.append([y])
96
+ return {frozenset(block) for block in blocks}
97
+
98
+
99
+ @nx._dispatchable(edge_attrs="weight", returns_graph=True)
100
+ def quotient_graph(
101
+ G,
102
+ partition,
103
+ edge_relation=None,
104
+ node_data=None,
105
+ edge_data=None,
106
+ weight="weight",
107
+ relabel=False,
108
+ create_using=None,
109
+ ):
110
+ """Returns the quotient graph of `G` under the specified equivalence
111
+ relation on nodes.
112
+
113
+ Parameters
114
+ ----------
115
+ G : NetworkX graph
116
+ The graph for which to return the quotient graph with the
117
+ specified node relation.
118
+
119
+ partition : function, or dict or list of lists, tuples or sets
120
+ If a function, this function must represent an equivalence
121
+ relation on the nodes of `G`. It must take two arguments *u*
122
+ and *v* and return True exactly when *u* and *v* are in the
123
+ same equivalence class. The equivalence classes form the nodes
124
+ in the returned graph.
125
+
126
+ If a dict of lists/tuples/sets, the keys can be any meaningful
127
+ block labels, but the values must be the block lists/tuples/sets
128
+ (one list/tuple/set per block), and the blocks must form a valid
129
+ partition of the nodes of the graph. That is, each node must be
130
+ in exactly one block of the partition.
131
+
132
+ If a list of sets, the list must form a valid partition of
133
+ the nodes of the graph. That is, each node must be in exactly
134
+ one block of the partition.
135
+
136
+ edge_relation : Boolean function with two arguments
137
+ This function must represent an edge relation on the *blocks* of
138
+ the `partition` of `G`. It must take two arguments, *B* and *C*,
139
+ each one a set of nodes, and return True exactly when there should be
140
+ an edge joining block *B* to block *C* in the returned graph.
141
+
142
+ If `edge_relation` is not specified, it is assumed to be the
143
+ following relation. Block *B* is related to block *C* if and
144
+ only if some node in *B* is adjacent to some node in *C*,
145
+ according to the edge set of `G`.
146
+
147
+ node_data : function
148
+ This function takes one argument, *B*, a set of nodes in `G`,
149
+ and must return a dictionary representing the node data
150
+ attributes to set on the node representing *B* in the quotient graph.
151
+ If None, the following node attributes will be set:
152
+
153
+ * 'graph', the subgraph of the graph `G` that this block
154
+ represents,
155
+ * 'nnodes', the number of nodes in this block,
156
+ * 'nedges', the number of edges within this block,
157
+ * 'density', the density of the subgraph of `G` that this
158
+ block represents.
159
+
160
+ edge_data : function
161
+ This function takes two arguments, *B* and *C*, each one a set
162
+ of nodes, and must return a dictionary representing the edge
163
+ data attributes to set on the edge joining *B* and *C*, should
164
+ there be an edge joining *B* and *C* in the quotient graph (if
165
+ no such edge occurs in the quotient graph as determined by
166
+ `edge_relation`, then the output of this function is ignored).
167
+
168
+ If the quotient graph would be a multigraph, this function is
169
+ not applied, since the edge data from each edge in the graph
170
+ `G` appears in the edges of the quotient graph.
171
+
172
+ weight : string or None, optional (default="weight")
173
+ The name of an edge attribute that holds the numerical value
174
+ used as a weight. If None then each edge has weight 1.
175
+
176
+ relabel : bool
177
+ If True, relabel the nodes of the quotient graph to be
178
+ nonnegative integers. Otherwise, the nodes are identified with
179
+ :class:`frozenset` instances representing the blocks given in
180
+ `partition`.
181
+
182
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
183
+ Graph type to create. If graph instance, then cleared before populated.
184
+
185
+ Returns
186
+ -------
187
+ NetworkX graph
188
+ The quotient graph of `G` under the equivalence relation
189
+ specified by `partition`. If the partition were given as a
190
+ list of :class:`set` instances and `relabel` is False,
191
+ each node will be a :class:`frozenset` corresponding to the same
192
+ :class:`set`.
193
+
194
+ Raises
195
+ ------
196
+ NetworkXException
197
+ If the given partition is not a valid partition of the nodes of
198
+ `G`.
199
+
200
+ Examples
201
+ --------
202
+ The quotient graph of the complete bipartite graph under the "same
203
+ neighbors" equivalence relation is `K_2`. Under this relation, two nodes
204
+ are equivalent if they are not adjacent but have the same neighbor set.
205
+
206
+ >>> G = nx.complete_bipartite_graph(2, 3)
207
+ >>> same_neighbors = lambda u, v: (u not in G[v] and v not in G[u] and G[u] == G[v])
208
+ >>> Q = nx.quotient_graph(G, same_neighbors)
209
+ >>> K2 = nx.complete_graph(2)
210
+ >>> nx.is_isomorphic(Q, K2)
211
+ True
212
+
213
+ The quotient graph of a directed graph under the "same strongly connected
214
+ component" equivalence relation is the condensation of the graph (see
215
+ :func:`condensation`). This example comes from the Wikipedia article
216
+ *`Strongly connected component`_*.
217
+
218
+ >>> G = nx.DiGraph()
219
+ >>> edges = [
220
+ ... "ab",
221
+ ... "be",
222
+ ... "bf",
223
+ ... "bc",
224
+ ... "cg",
225
+ ... "cd",
226
+ ... "dc",
227
+ ... "dh",
228
+ ... "ea",
229
+ ... "ef",
230
+ ... "fg",
231
+ ... "gf",
232
+ ... "hd",
233
+ ... "hf",
234
+ ... ]
235
+ >>> G.add_edges_from(tuple(x) for x in edges)
236
+ >>> components = list(nx.strongly_connected_components(G))
237
+ >>> sorted(sorted(component) for component in components)
238
+ [['a', 'b', 'e'], ['c', 'd', 'h'], ['f', 'g']]
239
+ >>>
240
+ >>> C = nx.condensation(G, components)
241
+ >>> component_of = C.graph["mapping"]
242
+ >>> same_component = lambda u, v: component_of[u] == component_of[v]
243
+ >>> Q = nx.quotient_graph(G, same_component)
244
+ >>> nx.is_isomorphic(C, Q)
245
+ True
246
+
247
+ Node identification can be represented as the quotient of a graph under the
248
+ equivalence relation that places the two nodes in one block and each other
249
+ node in its own singleton block.
250
+
251
+ >>> K24 = nx.complete_bipartite_graph(2, 4)
252
+ >>> K34 = nx.complete_bipartite_graph(3, 4)
253
+ >>> C = nx.contracted_nodes(K34, 1, 2)
254
+ >>> nodes = {1, 2}
255
+ >>> is_contracted = lambda u, v: u in nodes and v in nodes
256
+ >>> Q = nx.quotient_graph(K34, is_contracted)
257
+ >>> nx.is_isomorphic(Q, C)
258
+ True
259
+ >>> nx.is_isomorphic(Q, K24)
260
+ True
261
+
262
+ The blockmodeling technique described in [1]_ can be implemented as a
263
+ quotient graph.
264
+
265
+ >>> G = nx.path_graph(6)
266
+ >>> partition = [{0, 1}, {2, 3}, {4, 5}]
267
+ >>> M = nx.quotient_graph(G, partition, relabel=True)
268
+ >>> list(M.edges())
269
+ [(0, 1), (1, 2)]
270
+
271
+ Here is the sample example but using partition as a dict of block sets.
272
+
273
+ >>> G = nx.path_graph(6)
274
+ >>> partition = {0: {0, 1}, 2: {2, 3}, 4: {4, 5}}
275
+ >>> M = nx.quotient_graph(G, partition, relabel=True)
276
+ >>> list(M.edges())
277
+ [(0, 1), (1, 2)]
278
+
279
+ Partitions can be represented in various ways:
280
+
281
+ 0. a list/tuple/set of block lists/tuples/sets
282
+ 1. a dict with block labels as keys and blocks lists/tuples/sets as values
283
+ 2. a dict with block lists/tuples/sets as keys and block labels as values
284
+ 3. a function from nodes in the original iterable to block labels
285
+ 4. an equivalence relation function on the target iterable
286
+
287
+ As `quotient_graph` is designed to accept partitions represented as (0), (1) or
288
+ (4) only, the `equivalence_classes` function can be used to get the partitions
289
+ in the right form, in order to call `quotient_graph`.
290
+
291
+ .. _Strongly connected component: https://en.wikipedia.org/wiki/Strongly_connected_component
292
+
293
+ References
294
+ ----------
295
+ .. [1] Patrick Doreian, Vladimir Batagelj, and Anuska Ferligoj.
296
+ *Generalized Blockmodeling*.
297
+ Cambridge University Press, 2004.
298
+
299
+ """
300
+ # If the user provided an equivalence relation as a function to compute
301
+ # the blocks of the partition on the nodes of G induced by the
302
+ # equivalence relation.
303
+ if callable(partition):
304
+ # equivalence_classes always return partition of whole G.
305
+ partition = equivalence_classes(G, partition)
306
+ if not nx.community.is_partition(G, partition):
307
+ raise nx.NetworkXException(
308
+ "Input `partition` is not an equivalence relation for nodes of G"
309
+ )
310
+ return _quotient_graph(
311
+ G,
312
+ partition,
313
+ edge_relation,
314
+ node_data,
315
+ edge_data,
316
+ weight,
317
+ relabel,
318
+ create_using,
319
+ )
320
+
321
+ # If the partition is a dict, it is assumed to be one where the keys are
322
+ # user-defined block labels, and values are block lists, tuples or sets.
323
+ if isinstance(partition, dict):
324
+ partition = list(partition.values())
325
+
326
+ # If the user provided partition as a collection of sets. Then we
327
+ # need to check if partition covers all of G nodes. If the answer
328
+ # is 'No' then we need to prepare suitable subgraph view.
329
+ partition_nodes = set().union(*partition)
330
+ if len(partition_nodes) != len(G):
331
+ G = G.subgraph(partition_nodes)
332
+ # Each node in the graph/subgraph must be in exactly one block.
333
+ if not nx.community.is_partition(G, partition):
334
+ raise NetworkXException("each node must be in exactly one part of `partition`")
335
+ return _quotient_graph(
336
+ G,
337
+ partition,
338
+ edge_relation,
339
+ node_data,
340
+ edge_data,
341
+ weight,
342
+ relabel,
343
+ create_using,
344
+ )
345
+
346
+
347
+ def _quotient_graph(
348
+ G, partition, edge_relation, node_data, edge_data, weight, relabel, create_using
349
+ ):
350
+ """Construct the quotient graph assuming input has been checked"""
351
+ if create_using is None:
352
+ H = G.__class__()
353
+ else:
354
+ H = nx.empty_graph(0, create_using)
355
+ # By default set some basic information about the subgraph that each block
356
+ # represents on the nodes in the quotient graph.
357
+ if node_data is None:
358
+
359
+ def node_data(b):
360
+ S = G.subgraph(b)
361
+ return {
362
+ "graph": S,
363
+ "nnodes": len(S),
364
+ "nedges": S.number_of_edges(),
365
+ "density": density(S),
366
+ }
367
+
368
+ # Each block of the partition becomes a node in the quotient graph.
369
+ partition = [frozenset(b) for b in partition]
370
+ H.add_nodes_from((b, node_data(b)) for b in partition)
371
+ # By default, the edge relation is the relation defined as follows. B is
372
+ # adjacent to C if a node in B is adjacent to a node in C, according to the
373
+ # edge set of G.
374
+ #
375
+ # This is not a particularly efficient implementation of this relation:
376
+ # there are O(n^2) pairs to check and each check may require O(log n) time
377
+ # (to check set membership). This can certainly be parallelized.
378
+ if edge_relation is None:
379
+
380
+ def edge_relation(b, c):
381
+ return any(v in G[u] for u, v in product(b, c))
382
+
383
+ # By default, sum the weights of the edges joining pairs of nodes across
384
+ # blocks to get the weight of the edge joining those two blocks.
385
+ if edge_data is None:
386
+
387
+ def edge_data(b, c):
388
+ edgedata = (
389
+ d
390
+ for u, v, d in G.edges(b | c, data=True)
391
+ if (u in b and v in c) or (u in c and v in b)
392
+ )
393
+ return {"weight": sum(d.get(weight, 1) for d in edgedata)}
394
+
395
+ block_pairs = permutations(H, 2) if H.is_directed() else combinations(H, 2)
396
+ # In a multigraph, add one edge in the quotient graph for each edge
397
+ # in the original graph.
398
+ if H.is_multigraph():
399
+ edges = chaini(
400
+ (
401
+ (b, c, G.get_edge_data(u, v, default={}))
402
+ for u, v in product(b, c)
403
+ if v in G[u]
404
+ )
405
+ for b, c in block_pairs
406
+ if edge_relation(b, c)
407
+ )
408
+ # In a simple graph, apply the edge data function to each pair of
409
+ # blocks to determine the edge data attributes to apply to each edge
410
+ # in the quotient graph.
411
+ else:
412
+ edges = (
413
+ (b, c, edge_data(b, c)) for (b, c) in block_pairs if edge_relation(b, c)
414
+ )
415
+ H.add_edges_from(edges)
416
+ # If requested by the user, relabel the nodes to be integers,
417
+ # numbered in increasing order from zero in the same order as the
418
+ # iteration order of `partition`.
419
+ if relabel:
420
+ # Can't use nx.convert_node_labels_to_integers() here since we
421
+ # want the order of iteration to be the same for backward
422
+ # compatibility with the nx.blockmodel() function.
423
+ labels = {b: i for i, b in enumerate(partition)}
424
+ H = nx.relabel_nodes(H, labels)
425
+ return H
426
+
427
+
428
+ @nx._dispatchable(
429
+ preserve_all_attrs=True, mutates_input={"not copy": 4}, returns_graph=True
430
+ )
431
+ def contracted_nodes(
432
+ G, u, v, self_loops=True, copy=True, *, store_contraction_as="contraction"
433
+ ):
434
+ """Returns the graph that results from contracting `u` and `v`.
435
+
436
+ Node contraction identifies the two nodes as a single node incident to any
437
+ edge that was incident to the original two nodes.
438
+ Information about the contracted nodes and any modified edges are stored on
439
+ the output graph in a ``"contraction"`` attribute - see Examples for details.
440
+
441
+ Parameters
442
+ ----------
443
+ G : NetworkX graph
444
+ The graph whose nodes will be contracted.
445
+
446
+ u, v : nodes
447
+ Must be nodes in `G`.
448
+
449
+ self_loops : Boolean
450
+ If this is True, any edges joining `u` and `v` in `G` become
451
+ self-loops on the new node in the returned graph.
452
+
453
+ copy : Boolean
454
+ If this is True (the default), make a copy of
455
+ `G` and return that instead of directly changing `G`.
456
+
457
+ store_contraction_as : str or None, default="contraction"
458
+ Name of the node/edge attribute where information about the contraction
459
+ should be stored. By default information about the contracted node and
460
+ any contracted edges is stored in a ``"contraction"`` attribute on the
461
+ resulting node and edge. If `None`, information about the contracted
462
+ nodes/edges and their data are not stored.
463
+
464
+ Returns
465
+ -------
466
+ Networkx graph
467
+ If `copy` is True,
468
+ A new graph object of the same type as `G` (leaving `G` unmodified)
469
+ with `u` and `v` identified in a single node. The right node `v`
470
+ will be merged into the node `u`, so only `u` will appear in the
471
+ returned graph.
472
+ If `copy` is False,
473
+ Modifies `G` with `u` and `v` identified in a single node.
474
+ The right node `v` will be merged into the node `u`, so
475
+ only `u` will appear in the returned graph.
476
+
477
+ Notes
478
+ -----
479
+ For multigraphs, the edge keys for the realigned edges may
480
+ not be the same as the edge keys for the old edges. This is
481
+ natural because edge keys are unique only within each pair of nodes.
482
+
483
+ This function is also available as `identified_nodes`.
484
+
485
+ Examples
486
+ --------
487
+ Contracting two nonadjacent nodes of the cycle graph on four nodes `C_4`
488
+ yields the path graph (ignoring parallel edges):
489
+
490
+ >>> G = nx.cycle_graph(4)
491
+ >>> M = nx.contracted_nodes(G, 1, 3)
492
+ >>> P3 = nx.path_graph(3)
493
+ >>> nx.is_isomorphic(M, P3)
494
+ True
495
+
496
+ Information about the contracted nodes is stored on the resulting graph in
497
+ a ``"contraction"`` attribute. For instance, the contracted node is stored
498
+ as an attribute on ``u``:
499
+
500
+ >>> H = nx.contracted_nodes(P3, 0, 2)
501
+ >>> H.nodes(data=True)
502
+ NodeDataView({0: {'contraction': {2: {}}}, 1: {}})
503
+
504
+ Any node attributes on the contracted node are also preserved:
505
+
506
+ >>> nx.set_node_attributes(P3, dict(enumerate("rgb")), name="color")
507
+ >>> P3.nodes(data=True)
508
+ NodeDataView({0: {'color': 'r'}, 1: {'color': 'g'}, 2: {'color': 'b'}})
509
+ >>> H = nx.contracted_nodes(P3, 0, 2)
510
+ >>> H.nodes[0]
511
+ {'color': 'r', 'contraction': {2: {'color': 'b'}}}
512
+
513
+ Edges are handled similarly: when ``u`` and ``v`` are adjacent to a third node
514
+ ``w``, the edge ``(v, w)`` will be contracted into the edge ``(u, w)`` with
515
+ its attributes stored into a ``"contraction"`` attribute on edge ``(u, w)``:
516
+
517
+ >>> nx.set_edge_attributes(P3, {(0, 1): 10, (1, 2): 100}, name="weight")
518
+ >>> P3.edges(data=True)
519
+ EdgeDataView([(0, 1, {'weight': 10}), (1, 2, {'weight': 100})])
520
+ >>> H = nx.contracted_nodes(P3, 0, 2)
521
+ >>> H.edges(data=True)
522
+ EdgeDataView([(0, 1, {'weight': 10, 'contraction': {(2, 1): {'weight': 100}}})])
523
+
524
+ Attributes from contracted nodes/edges can be combined with those of the
525
+ nodes/edges onto which they were contracted:
526
+
527
+ >>> # Concatenate colors of contracted nodes
528
+ >>> for u, cdict in H.nodes(data="contraction"):
529
+ ... if cdict is not None:
530
+ ... H.nodes[u]["color"] += "".join(n["color"] for n in cdict.values())
531
+ ... del H.nodes[u]["contraction"] # Remove contraction attr (optional)
532
+ >>> H.nodes(data=True)
533
+ NodeDataView({0: {'color': 'rb'}, 1: {'color': 'g'}})
534
+ >>> # Sum contracted edge weights
535
+ >>> for u, v, cdict in H.edges(data="contraction"):
536
+ ... if cdict is not None:
537
+ ... H[u][v]["weight"] += sum(n["weight"] for n in cdict.values())
538
+ ... del H.edges[(u, v)]["contraction"] # Remove contraction attr (optional)
539
+ >>> H.edges(data=True)
540
+ EdgeDataView([(0, 1, {'weight': 110})])
541
+
542
+ If `G` is a multigraph, then a new edge is added instead. Any edge attributes
543
+ are still preserved:
544
+
545
+ >>> MG = nx.MultiGraph(P3)
546
+ >>> MH = nx.contracted_nodes(MG, 0, 2)
547
+ >>> MH.edges(keys=True, data=True)
548
+ MultiEdgeDataView([(0, 1, 0, {'weight': 10}), (0, 1, 1, {'weight': 100})])
549
+
550
+ If ``selfloops=True`` (the default), any edges adjoining `u` and `v` become
551
+ self-loops on ``u`` in the resulting graph:
552
+
553
+ >>> G = nx.Graph([(1, 2)])
554
+ >>> H = nx.contracted_nodes(G, 1, 2)
555
+ >>> H.nodes, H.edges
556
+ (NodeView((1,)), EdgeView([(1, 1)]))
557
+ >>> H = nx.contracted_nodes(G, 1, 2, self_loops=False)
558
+ >>> H.nodes, H.edges
559
+ (NodeView((1,)), EdgeView([]))
560
+
561
+ Note however that any self loops in the original graph `G` are preserved:
562
+
563
+ >>> G = nx.Graph([(1, 2), (2, 2)])
564
+ >>> H = nx.contracted_nodes(G, 1, 2, self_loops=False)
565
+ >>> H.nodes, H.edges
566
+ (NodeView((1,)), EdgeView([(1, 1)]))
567
+
568
+ The same reasoning applies to MultiGraphs:
569
+
570
+ >>> MG = nx.MultiGraph([(1, 2), (2, 2)])
571
+ >>> # Edge (1, 1, 0) in MH corresponds to edge (2, 2) in MG
572
+ >>> MH = nx.contracted_nodes(MG, 1, 2, self_loops=False)
573
+ >>> MH.edges(keys=True)
574
+ MultiEdgeView([(1, 1, 0)])
575
+ >>> # MH has two (1, 1) edges - one from edge (2, 2) in MG, and one
576
+ >>> # resulting from the contraction of 2->1
577
+ >>> MH = nx.contracted_nodes(MG, 1, 2, self_loops=True)
578
+ >>> MH.edges(keys=True)
579
+ MultiEdgeView([(1, 1, 0), (1, 1, 1)])
580
+
581
+
582
+ In a ``MultiDiGraph`` with a self loop, the in and out edges will
583
+ be treated separately as edges, so while contracting a node which
584
+ has a self loop the contraction will add multiple edges:
585
+
586
+ >>> G = nx.MultiDiGraph([(1, 2), (2, 2)])
587
+ >>> H = nx.contracted_nodes(G, 1, 2)
588
+ >>> list(H.edges()) # edge 1->2, 2->2, 2<-2 from the original Graph G
589
+ [(1, 1), (1, 1), (1, 1)]
590
+ >>> H = nx.contracted_nodes(G, 1, 2, self_loops=False)
591
+ >>> list(H.edges()) # edge 2->2, 2<-2 from the original Graph G
592
+ [(1, 1), (1, 1)]
593
+
594
+ See Also
595
+ --------
596
+ contracted_edge
597
+ quotient_graph
598
+
599
+ """
600
+ # Copying has significant overhead and can be disabled if needed
601
+ H = G.copy() if copy else G
602
+
603
+ # edge code uses G.edges(v) instead of G.adj[v] to handle multiedges
604
+ if H.is_directed():
605
+ edges_to_remap = chain(G.in_edges(v, data=True), G.out_edges(v, data=True))
606
+ else:
607
+ edges_to_remap = G.edges(v, data=True)
608
+
609
+ # If the H=G, the generators change as H changes
610
+ # This makes the edges_to_remap independent of H
611
+ if not copy:
612
+ edges_to_remap = list(edges_to_remap)
613
+
614
+ v_data = H.nodes[v]
615
+ H.remove_node(v)
616
+
617
+ # A bit of input munging to extract whether contraction info should be
618
+ # stored, and if so bind to a shorter name
619
+ if _store_contraction := (store_contraction_as is not None):
620
+ contraction = store_contraction_as
621
+
622
+ for prev_w, prev_x, d in edges_to_remap:
623
+ w = prev_w if prev_w != v else u
624
+ x = prev_x if prev_x != v else u
625
+
626
+ if ({prev_w, prev_x} == {u, v}) and not self_loops:
627
+ continue
628
+
629
+ if not H.has_edge(w, x) or G.is_multigraph():
630
+ H.add_edge(w, x, **d)
631
+ continue
632
+
633
+ # Store information about the contracted edge iff `store_contraction` is not None
634
+ if _store_contraction:
635
+ if contraction in H.edges[(w, x)]:
636
+ H.edges[(w, x)][contraction][(prev_w, prev_x)] = d
637
+ else:
638
+ H.edges[(w, x)][contraction] = {(prev_w, prev_x): d}
639
+
640
+ # Store information about the contracted node iff `store_contraction`
641
+ if _store_contraction:
642
+ if contraction in H.nodes[u]:
643
+ H.nodes[u][contraction][v] = v_data
644
+ else:
645
+ H.nodes[u][contraction] = {v: v_data}
646
+
647
+ return H
648
+
649
+
650
+ identified_nodes = contracted_nodes
651
+
652
+
653
+ @nx._dispatchable(
654
+ preserve_all_attrs=True, mutates_input={"not copy": 3}, returns_graph=True
655
+ )
656
+ def contracted_edge(
657
+ G, edge, self_loops=True, copy=True, *, store_contraction_as="contraction"
658
+ ):
659
+ """Returns the graph that results from contracting the specified edge.
660
+
661
+ Edge contraction identifies the two endpoints of the edge as a single node
662
+ incident to any edge that was incident to the original two nodes. A graph
663
+ that results from edge contraction is called a *minor* of the original
664
+ graph.
665
+
666
+ Parameters
667
+ ----------
668
+ G : NetworkX graph
669
+ The graph whose edge will be contracted.
670
+
671
+ edge : tuple
672
+ Must be a pair of nodes in `G`.
673
+
674
+ self_loops : Boolean
675
+ If this is True, any edges (including `edge`) joining the
676
+ endpoints of `edge` in `G` become self-loops on the new node in the
677
+ returned graph.
678
+
679
+ copy : Boolean (default True)
680
+ If this is True, a the contraction will be performed on a copy of `G`,
681
+ otherwise the contraction will happen in place.
682
+
683
+ store_contraction_as : str or None, default="contraction"
684
+ Name of the node/edge attribute where information about the contraction
685
+ should be stored. By default information about the contracted node and
686
+ any contracted edges is stored in a ``"contraction"`` attribute on the
687
+ resulting node and edge. If `None`, information about the contracted
688
+ nodes/edges and their data are not stored.
689
+
690
+ Returns
691
+ -------
692
+ Networkx graph
693
+ A new graph object of the same type as `G` (leaving `G` unmodified)
694
+ with endpoints of `edge` identified in a single node. The right node
695
+ of `edge` will be merged into the left one, so only the left one will
696
+ appear in the returned graph.
697
+
698
+ Raises
699
+ ------
700
+ ValueError
701
+ If `edge` is not an edge in `G`.
702
+
703
+ Examples
704
+ --------
705
+ Attempting to contract two nonadjacent nodes yields an error:
706
+
707
+ >>> G = nx.cycle_graph(4)
708
+ >>> nx.contracted_edge(G, (1, 3))
709
+ Traceback (most recent call last):
710
+ ...
711
+ ValueError: Edge (1, 3) does not exist in graph G; cannot contract it
712
+
713
+ Contracting two adjacent nodes in the cycle graph on *n* nodes yields the
714
+ cycle graph on *n - 1* nodes:
715
+
716
+ >>> C5 = nx.cycle_graph(5)
717
+ >>> C4 = nx.cycle_graph(4)
718
+ >>> M = nx.contracted_edge(C5, (0, 1), self_loops=False)
719
+ >>> nx.is_isomorphic(M, C4)
720
+ True
721
+
722
+ See also
723
+ --------
724
+ contracted_nodes
725
+ quotient_graph
726
+
727
+ """
728
+ u, v = edge[:2]
729
+ if not G.has_edge(u, v):
730
+ raise ValueError(f"Edge {edge} does not exist in graph G; cannot contract it")
731
+ return contracted_nodes(
732
+ G,
733
+ u,
734
+ v,
735
+ self_loops=self_loops,
736
+ copy=copy,
737
+ store_contraction_as=store_contraction_as,
738
+ )
outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/tests/test_contraction.py ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for the :mod:`networkx.algorithms.minors.contraction` module."""
2
+
3
+ import pytest
4
+
5
+ import networkx as nx
6
+ from networkx.utils import arbitrary_element, edges_equal, nodes_equal
7
+
8
+
9
+ def test_quotient_graph_complete_multipartite():
10
+ """Tests that the quotient graph of the complete *n*-partite graph
11
+ under the "same neighbors" node relation is the complete graph on *n*
12
+ nodes.
13
+
14
+ """
15
+ G = nx.complete_multipartite_graph(2, 3, 4)
16
+ # Two nodes are equivalent if they are not adjacent but have the same
17
+ # neighbor set.
18
+
19
+ def same_neighbors(u, v):
20
+ return u not in G[v] and v not in G[u] and G[u] == G[v]
21
+
22
+ expected = nx.complete_graph(3)
23
+ actual = nx.quotient_graph(G, same_neighbors)
24
+ # It won't take too long to run a graph isomorphism algorithm on such
25
+ # small graphs.
26
+ assert nx.is_isomorphic(expected, actual)
27
+
28
+
29
+ def test_quotient_graph_complete_bipartite():
30
+ """Tests that the quotient graph of the complete bipartite graph under
31
+ the "same neighbors" node relation is `K_2`.
32
+
33
+ """
34
+ G = nx.complete_bipartite_graph(2, 3)
35
+ # Two nodes are equivalent if they are not adjacent but have the same
36
+ # neighbor set.
37
+
38
+ def same_neighbors(u, v):
39
+ return u not in G[v] and v not in G[u] and G[u] == G[v]
40
+
41
+ expected = nx.complete_graph(2)
42
+ actual = nx.quotient_graph(G, same_neighbors)
43
+ # It won't take too long to run a graph isomorphism algorithm on such
44
+ # small graphs.
45
+ assert nx.is_isomorphic(expected, actual)
46
+
47
+
48
+ def test_quotient_graph_edge_relation():
49
+ """Tests for specifying an alternate edge relation for the quotient
50
+ graph.
51
+
52
+ """
53
+ G = nx.path_graph(5)
54
+
55
+ def identity(u, v):
56
+ return u == v
57
+
58
+ def same_parity(b, c):
59
+ return arbitrary_element(b) % 2 == arbitrary_element(c) % 2
60
+
61
+ actual = nx.quotient_graph(G, identity, same_parity)
62
+ expected = nx.Graph()
63
+ expected.add_edges_from([(0, 2), (0, 4), (2, 4)])
64
+ expected.add_edge(1, 3)
65
+ assert nx.is_isomorphic(actual, expected)
66
+
67
+
68
+ def test_condensation_as_quotient():
69
+ """This tests that the condensation of a graph can be viewed as the
70
+ quotient graph under the "in the same connected component" equivalence
71
+ relation.
72
+
73
+ """
74
+ # This example graph comes from the file `test_strongly_connected.py`.
75
+ G = nx.DiGraph()
76
+ G.add_edges_from(
77
+ [
78
+ (1, 2),
79
+ (2, 3),
80
+ (2, 11),
81
+ (2, 12),
82
+ (3, 4),
83
+ (4, 3),
84
+ (4, 5),
85
+ (5, 6),
86
+ (6, 5),
87
+ (6, 7),
88
+ (7, 8),
89
+ (7, 9),
90
+ (7, 10),
91
+ (8, 9),
92
+ (9, 7),
93
+ (10, 6),
94
+ (11, 2),
95
+ (11, 4),
96
+ (11, 6),
97
+ (12, 6),
98
+ (12, 11),
99
+ ]
100
+ )
101
+ scc = list(nx.strongly_connected_components(G))
102
+ C = nx.condensation(G, scc)
103
+ component_of = C.graph["mapping"]
104
+ # Two nodes are equivalent if they are in the same connected component.
105
+
106
+ def same_component(u, v):
107
+ return component_of[u] == component_of[v]
108
+
109
+ Q = nx.quotient_graph(G, same_component)
110
+ assert nx.is_isomorphic(C, Q)
111
+
112
+
113
+ def test_path():
114
+ G = nx.path_graph(6)
115
+ partition = [{0, 1}, {2, 3}, {4, 5}]
116
+ M = nx.quotient_graph(G, partition, relabel=True)
117
+ assert nodes_equal(M, [0, 1, 2])
118
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
119
+ for n in M:
120
+ assert M.nodes[n]["nedges"] == 1
121
+ assert M.nodes[n]["nnodes"] == 2
122
+ assert M.nodes[n]["density"] == 1
123
+
124
+
125
+ def test_path__partition_provided_as_dict_of_lists():
126
+ G = nx.path_graph(6)
127
+ partition = {0: [0, 1], 2: [2, 3], 4: [4, 5]}
128
+ M = nx.quotient_graph(G, partition, relabel=True)
129
+ assert nodes_equal(M, [0, 1, 2])
130
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
131
+ for n in M:
132
+ assert M.nodes[n]["nedges"] == 1
133
+ assert M.nodes[n]["nnodes"] == 2
134
+ assert M.nodes[n]["density"] == 1
135
+
136
+
137
+ def test_path__partition_provided_as_dict_of_tuples():
138
+ G = nx.path_graph(6)
139
+ partition = {0: (0, 1), 2: (2, 3), 4: (4, 5)}
140
+ M = nx.quotient_graph(G, partition, relabel=True)
141
+ assert nodes_equal(M, [0, 1, 2])
142
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
143
+ for n in M:
144
+ assert M.nodes[n]["nedges"] == 1
145
+ assert M.nodes[n]["nnodes"] == 2
146
+ assert M.nodes[n]["density"] == 1
147
+
148
+
149
+ def test_path__partition_provided_as_dict_of_sets():
150
+ G = nx.path_graph(6)
151
+ partition = {0: {0, 1}, 2: {2, 3}, 4: {4, 5}}
152
+ M = nx.quotient_graph(G, partition, relabel=True)
153
+ assert nodes_equal(M, [0, 1, 2])
154
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
155
+ for n in M:
156
+ assert M.nodes[n]["nedges"] == 1
157
+ assert M.nodes[n]["nnodes"] == 2
158
+ assert M.nodes[n]["density"] == 1
159
+
160
+
161
+ def test_multigraph_path():
162
+ G = nx.MultiGraph(nx.path_graph(6))
163
+ partition = [{0, 1}, {2, 3}, {4, 5}]
164
+ M = nx.quotient_graph(G, partition, relabel=True)
165
+ assert nodes_equal(M, [0, 1, 2])
166
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
167
+ for n in M:
168
+ assert M.nodes[n]["nedges"] == 1
169
+ assert M.nodes[n]["nnodes"] == 2
170
+ assert M.nodes[n]["density"] == 1
171
+
172
+
173
+ def test_directed_path():
174
+ G = nx.DiGraph()
175
+ nx.add_path(G, range(6))
176
+ partition = [{0, 1}, {2, 3}, {4, 5}]
177
+ M = nx.quotient_graph(G, partition, relabel=True)
178
+ assert nodes_equal(M, [0, 1, 2])
179
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)], directed=True)
180
+ for n in M:
181
+ assert M.nodes[n]["nedges"] == 1
182
+ assert M.nodes[n]["nnodes"] == 2
183
+ assert M.nodes[n]["density"] == 0.5
184
+
185
+
186
+ def test_directed_multigraph_path():
187
+ G = nx.MultiDiGraph()
188
+ nx.add_path(G, range(6))
189
+ partition = [{0, 1}, {2, 3}, {4, 5}]
190
+ M = nx.quotient_graph(G, partition, relabel=True)
191
+ assert nodes_equal(M, [0, 1, 2])
192
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)], directed=True)
193
+ for n in M:
194
+ assert M.nodes[n]["nedges"] == 1
195
+ assert M.nodes[n]["nnodes"] == 2
196
+ assert M.nodes[n]["density"] == 0.5
197
+
198
+
199
+ def test_overlapping_blocks():
200
+ with pytest.raises(nx.NetworkXException):
201
+ G = nx.path_graph(6)
202
+ partition = [{0, 1, 2}, {2, 3}, {4, 5}]
203
+ nx.quotient_graph(G, partition)
204
+
205
+
206
+ def test_weighted_path():
207
+ G = nx.path_graph(6)
208
+ for i in range(5):
209
+ G[i][i + 1]["w"] = i + 1
210
+ partition = [{0, 1}, {2, 3}, {4, 5}]
211
+ M = nx.quotient_graph(G, partition, weight="w", relabel=True)
212
+ assert nodes_equal(M, [0, 1, 2])
213
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
214
+ assert M[0][1]["weight"] == 2
215
+ assert M[1][2]["weight"] == 4
216
+ for n in M:
217
+ assert M.nodes[n]["nedges"] == 1
218
+ assert M.nodes[n]["nnodes"] == 2
219
+ assert M.nodes[n]["density"] == 1
220
+
221
+
222
+ def test_barbell():
223
+ G = nx.barbell_graph(3, 0)
224
+ partition = [{0, 1, 2}, {3, 4, 5}]
225
+ M = nx.quotient_graph(G, partition, relabel=True)
226
+ assert nodes_equal(M, [0, 1])
227
+ assert edges_equal(M.edges(), [(0, 1)])
228
+ for n in M:
229
+ assert M.nodes[n]["nedges"] == 3
230
+ assert M.nodes[n]["nnodes"] == 3
231
+ assert M.nodes[n]["density"] == 1
232
+
233
+
234
+ def test_barbell_plus():
235
+ G = nx.barbell_graph(3, 0)
236
+ # Add an extra edge joining the bells.
237
+ G.add_edge(0, 5)
238
+ partition = [{0, 1, 2}, {3, 4, 5}]
239
+ M = nx.quotient_graph(G, partition, relabel=True)
240
+ assert nodes_equal(M, [0, 1])
241
+ assert edges_equal(M.edges(), [(0, 1)])
242
+ assert M[0][1]["weight"] == 2
243
+ for n in M:
244
+ assert M.nodes[n]["nedges"] == 3
245
+ assert M.nodes[n]["nnodes"] == 3
246
+ assert M.nodes[n]["density"] == 1
247
+
248
+
249
+ def test_blockmodel():
250
+ G = nx.path_graph(6)
251
+ partition = [[0, 1], [2, 3], [4, 5]]
252
+ M = nx.quotient_graph(G, partition, relabel=True)
253
+ assert nodes_equal(M.nodes(), [0, 1, 2])
254
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
255
+ for n in M.nodes():
256
+ assert M.nodes[n]["nedges"] == 1
257
+ assert M.nodes[n]["nnodes"] == 2
258
+ assert M.nodes[n]["density"] == 1.0
259
+
260
+
261
+ def test_multigraph_blockmodel():
262
+ G = nx.MultiGraph(nx.path_graph(6))
263
+ partition = [[0, 1], [2, 3], [4, 5]]
264
+ M = nx.quotient_graph(G, partition, create_using=nx.MultiGraph(), relabel=True)
265
+ assert nodes_equal(M.nodes(), [0, 1, 2])
266
+ assert edges_equal(M.edges(), [(0, 1), (1, 2)])
267
+ for n in M.nodes():
268
+ assert M.nodes[n]["nedges"] == 1
269
+ assert M.nodes[n]["nnodes"] == 2
270
+ assert M.nodes[n]["density"] == 1.0
271
+
272
+
273
+ def test_quotient_graph_incomplete_partition():
274
+ G = nx.path_graph(6)
275
+ partition = []
276
+ H = nx.quotient_graph(G, partition, relabel=True)
277
+ assert nodes_equal(H.nodes(), [])
278
+ assert edges_equal(H.edges(), [])
279
+
280
+ partition = [[0, 1], [2, 3], [5]]
281
+ H = nx.quotient_graph(G, partition, relabel=True)
282
+ assert nodes_equal(H.nodes(), [0, 1, 2])
283
+ assert edges_equal(H.edges(), [(0, 1)])
284
+
285
+
286
+ @pytest.mark.parametrize("store_contraction_as", ("contraction", "c", None))
287
+ @pytest.mark.parametrize("copy", (True, False))
288
+ @pytest.mark.parametrize("selfloops", (True, False))
289
+ def test_undirected_node_contraction(store_contraction_as, copy, selfloops):
290
+ """Tests for node contraction in an undirected graph."""
291
+ G = nx.cycle_graph(4)
292
+ actual = nx.contracted_nodes(
293
+ G,
294
+ 0,
295
+ 1,
296
+ copy=copy,
297
+ self_loops=selfloops,
298
+ store_contraction_as=store_contraction_as,
299
+ )
300
+
301
+ expected = nx.cycle_graph(3)
302
+ if selfloops:
303
+ expected.add_edge(0, 0)
304
+
305
+ assert nx.is_isomorphic(actual, expected)
306
+
307
+ if not copy:
308
+ assert actual is G
309
+
310
+ # Test contracted node attributes
311
+ if store_contraction_as is not None:
312
+ assert actual.nodes[0][store_contraction_as] == {1: {}}
313
+ else:
314
+ assert actual.nodes[0] == {}
315
+ # There should be no contracted edges for this case
316
+ assert all(d == {} for _, _, d in actual.edges(data=True))
317
+
318
+
319
+ @pytest.mark.parametrize("store_contraction_as", ("contraction", "c", None))
320
+ @pytest.mark.parametrize("copy", (True, False))
321
+ @pytest.mark.parametrize("selfloops", (True, False))
322
+ def test_directed_node_contraction(store_contraction_as, copy, selfloops):
323
+ """Tests for node contraction in a directed graph."""
324
+ G = nx.DiGraph(nx.cycle_graph(4))
325
+ actual = nx.contracted_nodes(
326
+ G,
327
+ 0,
328
+ 1,
329
+ copy=copy,
330
+ self_loops=selfloops,
331
+ store_contraction_as=store_contraction_as,
332
+ )
333
+
334
+ expected = nx.DiGraph(nx.cycle_graph(3))
335
+ if selfloops:
336
+ expected.add_edge(0, 0)
337
+
338
+ assert nx.is_isomorphic(actual, expected)
339
+
340
+ if not copy:
341
+ assert actual is G
342
+ # Test contracted node attributes
343
+ if store_contraction_as is not None:
344
+ assert actual.nodes[0][store_contraction_as] == {1: {}}
345
+ else:
346
+ assert actual.nodes[0] == {}
347
+ # Test contracted edge attributes (only relevant if self loops is enabled)
348
+ if selfloops and store_contraction_as:
349
+ assert actual.edges[(0, 0)][store_contraction_as] == {(1, 0): {}}
350
+ else:
351
+ assert all(d == {} for _, _, d in actual.edges(data=True))
352
+
353
+
354
+ @pytest.mark.parametrize("store_contraction_as", ("contraction", "c", None))
355
+ @pytest.mark.parametrize("copy", (True, False))
356
+ @pytest.mark.parametrize("selfloops", (True, False))
357
+ def test_contracted_nodes_multigraph(store_contraction_as, copy, selfloops):
358
+ """Tests that using a MultiGraph creates multiple edges. `store_contraction_as`
359
+ has no effect for multigraphs."""
360
+ G = nx.path_graph(3, create_using=nx.MultiGraph)
361
+ G.add_edges_from([(0, 1), (0, 0), (0, 2)])
362
+ actual = nx.contracted_nodes(
363
+ G,
364
+ 0,
365
+ 2,
366
+ copy=copy,
367
+ self_loops=selfloops,
368
+ store_contraction_as=store_contraction_as,
369
+ )
370
+ # Two (0, 1) edges from G, another from the contraction of edge (1, 2)
371
+ expected = nx.MultiGraph([(0, 1), (0, 1), (0, 1), (0, 0)])
372
+ # One (0, 0) edge from G, another from the contraction of edge (0, 2), but
373
+ # only if `selfloops` is True
374
+ if selfloops:
375
+ expected.add_edge(0, 0)
376
+
377
+ assert edges_equal(actual.edges, expected.edges)
378
+ if not copy:
379
+ assert actual is G
380
+
381
+
382
+ def test_multigraph_keys():
383
+ """Tests that multiedge keys are reset in new graph."""
384
+ G = nx.path_graph(3, create_using=nx.MultiGraph())
385
+ G.add_edge(0, 1, 5)
386
+ G.add_edge(0, 0, 0)
387
+ G.add_edge(0, 2, 5)
388
+ actual = nx.contracted_nodes(G, 0, 2)
389
+ expected = nx.MultiGraph()
390
+ expected.add_edge(0, 1, 0)
391
+ expected.add_edge(0, 1, 5)
392
+ expected.add_edge(0, 1, 2) # keyed as 2 b/c 2 edges already in G
393
+ expected.add_edge(0, 0, 0)
394
+ expected.add_edge(0, 0, 1) # this comes from (0, 2, 5)
395
+ assert edges_equal(actual.edges, expected.edges)
396
+
397
+
398
+ @pytest.mark.parametrize("store_contraction_as", ("contraction", "c", None))
399
+ @pytest.mark.parametrize("copy", (True, False))
400
+ @pytest.mark.parametrize("selfloops", (True, False))
401
+ def test_node_attributes(store_contraction_as, copy, selfloops):
402
+ """Tests that node contraction preserves node attributes."""
403
+ G = nx.cycle_graph(4)
404
+ # Add some data to the two nodes being contracted.
405
+ G.nodes[0]["foo"] = "bar"
406
+ G.nodes[1]["baz"] = "xyzzy"
407
+ actual = nx.contracted_nodes(
408
+ G,
409
+ 0,
410
+ 1,
411
+ copy=copy,
412
+ self_loops=selfloops,
413
+ store_contraction_as=store_contraction_as,
414
+ )
415
+ # We expect that contracting the nodes 0 and 1 in C_4 yields K_3, but
416
+ # with nodes labeled 0, 2, and 3.
417
+ expected = nx.complete_graph(3)
418
+ expected = nx.relabel_nodes(expected, {1: 2, 2: 3})
419
+ expected.nodes[0]["foo"] = "bar"
420
+ # ... and a self-loop (0, 0), if self_loops=True
421
+ if selfloops:
422
+ expected.add_edge(0, 0)
423
+
424
+ if store_contraction_as:
425
+ cdict = {1: {"baz": "xyzzy"}}
426
+ expected.nodes[0].update({"foo": "bar", store_contraction_as: cdict})
427
+
428
+ assert nx.is_isomorphic(actual, expected)
429
+ assert actual.nodes(data=True) == expected.nodes(data=True)
430
+ if not copy:
431
+ assert actual is G
432
+
433
+
434
+ @pytest.mark.parametrize("store_contraction_as", ("contraction", "c", None))
435
+ def test_edge_attributes(store_contraction_as):
436
+ """Tests that node contraction preserves edge attributes."""
437
+ # Shape: src1 --> dest <-- src2
438
+ G = nx.DiGraph([("src1", "dest"), ("src2", "dest")])
439
+ G["src1"]["dest"]["value"] = "src1-->dest"
440
+ G["src2"]["dest"]["value"] = "src2-->dest"
441
+
442
+ # New Shape: src1 --> dest
443
+ H = nx.contracted_nodes(
444
+ G, "src1", "src2", store_contraction_as=store_contraction_as
445
+ )
446
+ assert H.edges[("src1", "dest")]["value"] == "src1-->dest" # Should be unchanged
447
+ if store_contraction_as:
448
+ assert (
449
+ H.edges[("src1", "dest")][store_contraction_as][("src2", "dest")]["value"]
450
+ == "src2-->dest"
451
+ )
452
+ else:
453
+ assert store_contraction_as not in H.edges[("src1", "dest")]
454
+
455
+ G = nx.MultiDiGraph(G)
456
+ # New Shape: src1 -(x2)-> dest
457
+ H = nx.contracted_nodes(
458
+ G, "src1", "src2", store_contraction_as=store_contraction_as
459
+ )
460
+ # store_contraction should not affect multigraphs
461
+ assert len(H.edges(("src1", "dest"))) == 2
462
+ assert H.edges[("src1", "dest", 0)]["value"] == "src1-->dest"
463
+ assert H.edges[("src1", "dest", 1)]["value"] == "src2-->dest"
464
+
465
+
466
+ def test_contract_loop_graph():
467
+ """Tests for node contraction when nodes have loops."""
468
+ G = nx.cycle_graph(4)
469
+ G.add_edge(0, 0)
470
+ actual = nx.contracted_nodes(G, 0, 1)
471
+ expected = nx.complete_graph([0, 2, 3])
472
+ expected.add_edge(0, 0)
473
+ assert edges_equal(actual.edges, expected.edges)
474
+ actual = nx.contracted_nodes(G, 1, 0)
475
+ expected = nx.complete_graph([1, 2, 3])
476
+ expected.add_edge(1, 1)
477
+ assert edges_equal(actual.edges, expected.edges)
478
+
479
+
480
+ @pytest.mark.parametrize("store_contraction_as", ("contraction", "c", None))
481
+ @pytest.mark.parametrize("copy", (True, False))
482
+ @pytest.mark.parametrize("selfloops", (True, False))
483
+ def test_undirected_edge_contraction(store_contraction_as, copy, selfloops):
484
+ """Tests for node contraction in an undirected graph."""
485
+ G = nx.cycle_graph(4)
486
+ actual = nx.contracted_edge(
487
+ G,
488
+ (0, 1),
489
+ copy=copy,
490
+ self_loops=selfloops,
491
+ store_contraction_as=store_contraction_as,
492
+ )
493
+
494
+ expected = nx.cycle_graph(3)
495
+ if selfloops:
496
+ expected.add_edge(0, 0)
497
+
498
+ assert nx.is_isomorphic(actual, expected)
499
+
500
+ if not copy:
501
+ assert actual is G
502
+
503
+ # Test contracted node attributes
504
+ if store_contraction_as is not None:
505
+ assert actual.nodes[0][store_contraction_as] == {1: {}}
506
+ else:
507
+ assert actual.nodes[0] == {}
508
+ # There should be no contracted edges for this case
509
+ assert all(d == {} for _, _, d in actual.edges(data=True))
510
+
511
+
512
+ @pytest.mark.parametrize("edge", [(0, 1), (0, 1, 0)])
513
+ @pytest.mark.parametrize("store_contraction_as", ("contraction", "c", None))
514
+ @pytest.mark.parametrize("copy", [True, False])
515
+ @pytest.mark.parametrize("selfloops", [True, False])
516
+ def test_multigraph_edge_contraction(edge, store_contraction_as, copy, selfloops):
517
+ """Tests for edge contraction in a multigraph"""
518
+ G = nx.cycle_graph(4, create_using=nx.MultiGraph)
519
+ actual = nx.contracted_edge(
520
+ G,
521
+ edge,
522
+ copy=copy,
523
+ self_loops=selfloops,
524
+ store_contraction_as=store_contraction_as,
525
+ )
526
+ expected = nx.relabel_nodes(
527
+ nx.complete_graph(3, create_using=nx.MultiGraph), {0: 0, 1: 2, 2: 3}
528
+ )
529
+ if selfloops:
530
+ expected.add_edge(0, 0)
531
+
532
+ assert edges_equal(actual.edges, expected.edges)
533
+ if not copy:
534
+ assert actual is G
535
+
536
+
537
+ def test_nonexistent_edge():
538
+ """Tests that attempting to contract a nonexistent edge raises an
539
+ exception.
540
+
541
+ """
542
+ G = nx.cycle_graph(4)
543
+ with pytest.raises(ValueError):
544
+ nx.contracted_edge(G, (0, 2))